first draft of url navigation for grid

This commit is contained in:
Bruno Windels 2020-10-12 17:49:06 +02:00
parent 6c2c29a7da
commit b2d6b7014b
11 changed files with 539 additions and 201 deletions

View file

@ -98,7 +98,7 @@ export class RootViewModel extends ViewModel {
createSessionContainer: this._createSessionContainer, createSessionContainer: this._createSessionContainer,
ready: sessionContainer => { ready: sessionContainer => {
const url = this.urlRouter.urlForSegment("session", sessionContainer.sessionId); const url = this.urlRouter.urlForSegment("session", sessionContainer.sessionId);
this.urlRouter.replaceUrl(url); this.urlRouter.history.replaceUrl(url);
this._showSession(sessionContainer); this._showSession(sessionContainer);
}, },
})); }));

View file

@ -28,10 +28,26 @@ export class Navigation {
} }
applyPath(path) { applyPath(path) {
const oldPath = this._path;
this._path = path; this._path = path;
for (const [type, observable] of this._observables) { // clear values not in the new path in reverse order of path
// if the value did not change, this won't emit for (let i = oldPath.segments.length - 1; i >= 0; i -= 1) {
observable.set(this._path.get(type)?.value); const segment = oldPath[i];
if (!this._path.get(segment.type)) {
const observable = this._observables.get(segment.type);
if (observable) {
observable.set(segment.type, undefined);
}
}
}
// change values in order of path
for (const segment of this._path.segments) {
const observable = this._observables.get(segment.type);
if (observable) {
if (!segmentValueEqual(segment?.value, observable.get())) {
observable.set(segment.type, segment.value);
}
}
} }
} }
@ -55,6 +71,27 @@ export class Navigation {
} }
return new Path(segments, this._allowsChild); return new Path(segments, this._allowsChild);
} }
segment(type, value) {
return new Segment(type, value);
}
}
function segmentValueEqual(a, b) {
if (a === b) {
return true;
}
// allow (sparse) arrays
if (Array.isArray(a) && Array.isArray(b)) {
const len = Math.max(a.length, b.length);
for (let i = 0; i < len; i += 1) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
return false;
} }
export class Segment { export class Segment {
@ -89,6 +126,14 @@ class Path {
return null; return null;
} }
until(type) {
const index = this._segments.findIndex(s => s.type === type);
if (index !== -1) {
return new Path(this._segments.slice(0, index + 1), this._allowsChild)
}
return new Path([], this._allowsChild);
}
get(type) { get(type) {
return this._segments.find(s => s.type === type); return this._segments.find(s => s.type === type);
} }

View file

@ -17,69 +17,78 @@ limitations under the License.
import {Segment} from "./Navigation.js"; import {Segment} from "./Navigation.js";
export class URLRouter { export class URLRouter {
constructor({history, navigation, redirect}) { constructor({history, navigation, parseUrlPath, stringifyPath}) {
this._subscription = null; this._subscription = null;
this._history = history; this._history = history;
this._navigation = navigation; this._navigation = navigation;
this._redirect = redirect; this._parseUrlPath = parseUrlPath;
this._stringifyPath = stringifyPath;
} }
attach() { attach() {
this._subscription = this._history.subscribe(url => { this._subscription = this._history.subscribe(url => {
this.applyUrl(url); const redirectedUrl = this.applyUrl(url);
if (redirectedUrl !== url) {
this._history.replaceUrl(redirectedUrl);
}
}); });
this.applyUrl(this._history.get()); this.applyUrl(this._history.get());
} }
applyUrl(url) { dispose() {
const segments = this._segmentsFromUrl(url);
const path = this._redirect(segments, this._navigation);
this._navigation.applyPath(path);
}
stop() {
this._subscription = this._subscription(); this._subscription = this._subscription();
} }
_segmentsFromUrl(url) { applyUrl(url) {
const path = this._history.urlAsPath(url); const urlPath = this._history.urlAsPath(url)
const parts = path.split("/").filter(p => !!p); const navPath = this._navigation.pathFrom(this._parseUrlPath(urlPath));
let index = 0; this._navigation.applyPath(navPath);
const segments = []; return this._history.pathAsUrl(this._stringifyPath(navPath));
while (index < parts.length) {
const type = parts[index];
if ((index + 1) < parts.length) {
index += 1;
const value = parts[index];
segments.push(new Segment(type, value));
} else {
segments.push(new Segment(type));
}
index += 1;
}
return segments;
} }
get history() { get history() {
return this._history; return this._history;
} }
urlForSegment(type, value) { urlForSegments(segments) {
const path = this._navigation.path.with(new Segment(type, value)); let path = this._navigation.path;
if (path) { for (const segment of segments) {
return this.urlForPath(path); path = path.with(segment);
if (!path) {
return;
}
} }
return this.urlForPath(path);
}
urlForSegment(type, value) {
return this.urlForSegments([this._navigation.segment(type, value)]);
} }
urlForPath(path) { urlForPath(path) {
let urlPath = ""; return this.history.pathAsUrl(this._stringifyPath(path));
for (const {type, value} of path.segments) { }
if (typeof value === "boolean") {
urlPath += `/${type}`; openRoomActionUrl(roomId) {
} else { // not a segment to navigation knowns about, so append it manually
urlPath += `/${type}/${value}`; const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${roomId}`;
}
}
return this._history.pathAsUrl(urlPath); return this._history.pathAsUrl(urlPath);
} }
disableGridUrl() {
}
enableGridUrl() {
let path = this._navigation.path.until("session");
const room = this._navigation.get("room");
if (room) {
path = path.with(this._navigation.segment("rooms", [room.value]));
path = path.with(room);
} else {
path = path.with(this._navigation.segment("rooms", []));
path = path.with(this._navigation.segment("empty-grid-tile", 0));
}
return this.urlForPath(path);
}
} }

View file

@ -18,49 +18,30 @@ import {Navigation, Segment} from "./Navigation.js";
import {URLRouter} from "./URLRouter.js"; import {URLRouter} from "./URLRouter.js";
export function createNavigation() { export function createNavigation() {
return new Navigation(function allowsChild(parent, child) { return new Navigation(allowsChild);
const {type} = child;
switch (parent?.type) {
case undefined:
// allowed root segments
return type === "login" || type === "session";
case "session":
return type === "room" || type === "rooms" || type === "settings";
case "rooms":
// downside of the approach: both of these will control which tile is selected
return type === "room" || type === "empty-grid-tile";
default:
return false;
}
});
} }
export function createRouter({history, navigation}) { export function createRouter({history, navigation}) {
return new URLRouter({history, navigation, redirect}); return new URLRouter({history, navigation, stringifyPath, parseUrlPath});
} }
function redirect(urlParts, navigation) { function allowsChild(parent, child) {
const {path} = navigation; const {type} = child;
const segments = urlParts.reduce((output, s) => { switch (parent?.type) {
// redirect open-room action to grid/non-grid url case undefined:
if (s.type === "open-room") { // allowed root segments
const rooms = path.get("rooms"); return type === "login" || type === "session";
if (rooms) { case "session":
output = output.concat(roomsSegmentWithRoom(rooms, s.value, path)); return type === "room" || type === "rooms" || type === "settings";
} case "rooms":
return rooms.concat(new Segment("room", s.value)); // downside of the approach: both of these will control which tile is selected
} return type === "room" || type === "empty-grid-tile";
return output.concat(s); default:
}, []); return false;
return navigation.pathFrom(segments); }
} }
function roomsSegmentWithRoom(rooms, roomId, path) { function roomsSegmentWithRoom(rooms, roomId, path) {
// find the index of either the current room,
// or the current selected empty tile,
// to put the new room in
// TODO: is rooms.value a string or an array?
const room = path.get("room"); const room = path.get("room");
let index = 0; let index = 0;
if (room) { if (room) {
@ -71,20 +52,157 @@ function roomsSegmentWithRoom(rooms, roomId, path) {
index = emptyGridTile.value; index = emptyGridTile.value;
} }
} }
const newRooms = rooms.slice(); const newRooms = rooms.value.slice();
newRooms[index] = roomId; newRooms[index] = roomId;
return new Segment("rooms", newRooms); return new Segment("rooms", newRooms);
} }
function parseUrlValue(type, iterator) { export function parseUrlPath(urlPath, currentNavPath) {
if (type === "rooms") { // substr(1) to take of initial /
const roomIds = iterator.next().value.split(","); const parts = urlPath.substr(1).split("/");
const selectedIndex = parseInt(iterator.next().value, 10); const iterator = parts[Symbol.iterator]();
const roomId = roomIds[selectedIndex]; const segments = [];
if (roomId) { let next;
return [new Segment(type, roomIds), new Segment("room", roomId)]; while (!(next = iterator.next()).done) {
const type = next.value;
if (type === "rooms") {
const roomsValue = iterator.next().value;
if (!roomsValue) { break; }
const roomIds = roomsValue.split(",");
segments.push(new Segment(type, roomIds));
const selectedIndex = parseInt(iterator.next().value || "0", 10);
const roomId = roomIds[selectedIndex];
if (roomId) {
segments.push(new Segment("room", roomId));
} else {
segments.push(new Segment("empty-grid-tile", selectedIndex));
}
} else if (type === "open-room") {
const roomId = iterator.next().value;
if (!roomId) { break; }
const rooms = currentNavPath.get("rooms");
if (rooms) {
segments.push(roomsSegmentWithRoom(rooms, roomId, currentNavPath));
}
segments.push(new Segment("room", roomId));
} else { } else {
return [new Segment(type, roomIds), new Segment("empty-grid-tile", selectedIndex)]; // might be undefined, which will be turned into true by Segment
const value = iterator.next().value;
segments.push(new Segment(type, value));
}
}
return segments;
}
export function stringifyPath(path) {
let urlPath = "";
let prevSegment;
for (const segment of path.segments) {
switch (segment.type) {
case "rooms":
urlPath += `/rooms/${segment.value.join(",")}`;
break;
case "empty-grid-tile":
urlPath += `/${segment.value}`;
break;
case "room":
if (prevSegment?.type === "rooms") {
const index = prevSegment.value.indexOf(segment.value);
urlPath += `/${index}`;
} else {
urlPath += `/${segment.type}/${segment.value}`;
}
break;
default:
urlPath += `/${segment.type}`;
if (segment.value && segment.value !== true) {
urlPath += `/${segment.value}`;
}
}
prevSegment = segment;
}
return urlPath;
}
export function tests() {
return {
"stringify grid url with focused empty tile": assert => {
const nav = new Navigation(allowsChild);
const path = nav.pathFrom([
new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]),
new Segment("empty-grid-tile", 3)
]);
const urlPath = stringifyPath(path);
assert.equal(urlPath, "/session/1/rooms/a,b,c/3");
},
"stringify grid url with focused room": assert => {
const nav = new Navigation(allowsChild);
const path = nav.pathFrom([
new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]),
new Segment("room", "b")
]);
const urlPath = stringifyPath(path);
assert.equal(urlPath, "/session/1/rooms/a,b,c/1");
},
"parse grid url path with focused empty tile": assert => {
const segments = parseUrlPath("/session/1/rooms/a,b,c/3");
assert.equal(segments.length, 3);
assert.equal(segments[0].type, "session");
assert.equal(segments[0].value, "1");
assert.equal(segments[1].type, "rooms");
assert.deepEqual(segments[1].value, ["a", "b", "c"]);
assert.equal(segments[2].type, "empty-grid-tile");
assert.equal(segments[2].value, 3);
},
"parse grid url path with focused room": assert => {
const segments = parseUrlPath("/session/1/rooms/a,b,c/1");
assert.equal(segments.length, 3);
assert.equal(segments[0].type, "session");
assert.equal(segments[0].value, "1");
assert.equal(segments[1].type, "rooms");
assert.deepEqual(segments[1].value, ["a", "b", "c"]);
assert.equal(segments[2].type, "room");
assert.equal(segments[2].value, "b");
},
"parse open-room action replacing the current focused room": assert => {
const nav = new Navigation(allowsChild);
const path = nav.pathFrom([
new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]),
new Segment("room", "b")
]);
const segments = parseUrlPath("/session/1/open-room/d", path);
assert.equal(segments.length, 3);
assert.equal(segments[0].type, "session");
assert.equal(segments[0].value, "1");
assert.equal(segments[1].type, "rooms");
assert.deepEqual(segments[1].value, ["a", "d", "c"]);
assert.equal(segments[2].type, "room");
assert.equal(segments[2].value, "d");
},
"parse open-room action setting a room in an empty tile": assert => {
const nav = new Navigation(allowsChild);
const path = nav.pathFrom([
new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]),
new Segment("empty-grid-tile", 4)
]);
const segments = parseUrlPath("/session/1/open-room/d", path);
assert.equal(segments.length, 3);
assert.equal(segments[0].type, "session");
assert.equal(segments[0].value, "1");
assert.equal(segments[1].type, "rooms");
assert.deepEqual(segments[1].value, ["a", "b", "c", , "d"]); //eslint-disable-line no-sparse-arrays
assert.equal(segments[2].type, "room");
assert.equal(segments[2].value, "d");
},
"parse session url path without id": assert => {
const segments = parseUrlPath("/session");
assert.equal(segments.length, 1);
assert.equal(segments[0].type, "session");
assert.strictEqual(segments[0].value, true);
} }
} }
} }

View file

@ -19,10 +19,46 @@ import {ViewModel} from "../ViewModel.js";
export class RoomGridViewModel extends ViewModel { export class RoomGridViewModel extends ViewModel {
constructor(options) { constructor(options) {
super(options); super(options);
this._width = options.width; this._width = options.width;
this._height = options.height; this._height = options.height;
this._createRoomViewModel = options.createRoomViewModel;
this._selectedIndex = 0; this._selectedIndex = 0;
this._viewModels = []; this._viewModels = (options.roomIds || []).map(roomId => {
if (roomId) {
const vm = this._createRoomViewModel(roomId);
if (vm) {
return this.track(vm);
}
}
});
this._setupNavigation();
}
_setupNavigation() {
const focusTileIndex = this.navigation.observe("empty-grid-tile");
this.track(focusTileIndex.subscribe(index => {
if (typeof index === "number") {
this._setFocusIndex(index);
}
}));
if (typeof focusTileIndex.get() === "number") {
this._selectedIndex = focusTileIndex.get();
}
const focusedRoom = this.navigation.get("room");
this.track(focusedRoom.subscribe(roomId => {
if (roomId) {
this._openRoom(roomId);
}
}));
if (focusedRoom.get()) {
const index = this._viewModels.findIndex(vm => vm && vm.id === focusedRoom.get());
if (index >= 0) {
this._selectedIndex = index;
}
}
} }
roomViewModelAt(i) { roomViewModelAt(i) {
@ -33,15 +69,6 @@ export class RoomGridViewModel extends ViewModel {
return this._selectedIndex; return this._selectedIndex;
} }
setFocusIndex(idx) {
if (idx === this._selectedIndex) {
return;
}
this._selectedIndex = idx;
const vm = this._viewModels[this._selectedIndex];
vm?.focus();
this.emitChange("focusedIndex");
}
get width() { get width() {
return this._width; return this._width;
} }
@ -50,41 +77,91 @@ export class RoomGridViewModel extends ViewModel {
return this._height; return this._height;
} }
/** focusTile(index) {
* Sets a pair of room and room tile view models at the current index if (index === this._selectedIndex) {
* @param {RoomViewModel} vm return;
* @package }
*/ let path = this.navigation.path;
setRoomViewModel(vm) { const vm = this._viewModels[index];
const old = this._viewModels[this._selectedIndex]; if (vm) {
this.disposeTracked(old); path = path.with(this.navigation.segment("room", vm.id));
this._viewModels[this._selectedIndex] = this.track(vm); } else {
this.emitChange(`${this._selectedIndex}`); path = path.with(this.navigation.segment("empty-grid-tile", index));
}
let url = this.urlRouter.urlForPath(path);
url = this.urlRouter.applyUrl(url);
this.urlRouter.history.pushUrl(url);
} }
/** /** called from SessionViewModel */
* @package setRoomIds(roomIds) {
*/ let changed = false;
tryFocusRoom(roomId) { const len = this._height * this._width;
for (let i = 0; i < len; i += 1) {
const newId = roomIds[i];
const vm = this._viewModels[i];
if (newId && !vm) {
this._viewModels[i] = this.track(this._createRoomViewModel(newId));
changed = true;
} else if (newId !== vm?.id) {
this._viewModels[i] = this.disposeTracked(this._viewModels[i]);
if (newId) {
this._viewModels[i] = this.track(this._createRoomViewModel(newId));
}
changed = true;
}
}
if (changed) {
this.emitChange();
}
}
/** called from SessionViewModel */
transferRoomViewModel(index, roomVM) {
const oldVM = this._viewModels[index];
this.disposeTracked(oldVM);
this._viewModels[index] = this.track(roomVM);
}
/** called from SessionViewModel */
releaseRoomViewModel(roomId) {
const index = this._viewModels.findIndex(vm => vm.id === roomId);
if (index !== -1) {
const vm = this._viewModels[index];
this.untrack(vm);
this._viewModels[index] = null;
return vm;
}
}
_setFocusIndex(idx) {
if (idx === this._selectedIndex || idx >= (this._width * this._height)) {
return;
}
this._selectedIndex = idx;
const vm = this._viewModels[this._selectedIndex];
vm?.focus();
this.emitChange("focusedIndex");
}
_setFocusRoom(roomId) {
const index = this._viewModels.findIndex(vm => vm.id === roomId); const index = this._viewModels.findIndex(vm => vm.id === roomId);
if (index >= 0) { if (index >= 0) {
this.setFocusIndex(index); this._setFocusIndex(index);
return true; return true;
} }
return false; return false;
} }
/** _openRoom(roomId) {
* Returns the first set of room vm, if (!this._setFocusRoom(roomId)) {
* and untracking it so it is not owned by this view model anymore. // replace vm at focused index
* @package const vm = this._viewModels[this._selectedIndex];
*/
getAndUntrackFirst() {
for (const vm of this._viewModels) {
if (vm) { if (vm) {
this.untrack(vm); this.disposeTracked(vm);
return vm;
} }
this._viewModels[this._selectedIndex] = this.track(this._createRoomViewModel(roomId));
this.emitChange();
} }
} }
} }

View file

@ -32,27 +32,33 @@ export class SessionViewModel extends ViewModel {
session: sessionContainer.session, session: sessionContainer.session,
}))); })));
this._leftPanelViewModel = new LeftPanelViewModel(this.childOptions({ this._leftPanelViewModel = new LeftPanelViewModel(this.childOptions({
rooms: this._sessionContainer.session.rooms, rooms: this._sessionContainer.session.rooms
// this will go over navigation as well
gridEnabled: {
get: () => !!this._gridViewModel,
set: value => this._enabledGrid(value)
}
})); }));
this._currentRoomViewModel = null; this._currentRoomViewModel = null;
this._gridViewModel = null; this._gridViewModel = null;
// this gives us the active room, also in the grid? this._setupNavigation();
this.track(this.navigator.observe("room").subscribe(roomId => { }
})); _setupNavigation() {
const gridRooms = this.navigation.observe("rooms");
// this gives us a set of room ids in the grid // this gives us a set of room ids in the grid
this.track(this.navigator.observe("rooms").subscribe(value => { this.track(gridRooms.subscribe(roomIds => {
if (value) { this._updateGrid(roomIds);
const roomIds = typeof value === "string" ? value.split(",") : []; }));
// also update grid if (gridRooms.get()) {
this._enabledGrid(roomIds); this._updateGrid(gridRooms.get());
}
const currentRoomId = this.navigation.observe("room");
// this gives us the active room
this.track(currentRoomId.subscribe(roomId => {
if (!this._gridViewModel) {
this._openRoom(roomId);
} }
})); }));
if (currentRoomId.get() && !this._gridViewModel) {
this._openRoom(currentRoomId.get());
}
} }
start() { start() {
@ -88,53 +94,109 @@ export class SessionViewModel extends ViewModel {
return this._currentRoomViewModel; return this._currentRoomViewModel;
} }
// TODO: this should also happen based on URLs // _transitionToGrid() {
_enabledGrid(enabled) { // if (this._gridViewModel) {
if (enabled) { // return;
this._gridViewModel = this.track(new RoomGridViewModel(this.childOptions({width: 3, height: 2}))); // }
// transfer current room // this._gridViewModel = this.track(new RoomGridViewModel(this.childOptions({width: 3, height: 2})));
if (this._currentRoomViewModel) { // let path;
this.untrack(this._currentRoomViewModel); // if (this._currentRoomViewModel) {
this._gridViewModel.setRoomViewModel(this._currentRoomViewModel); // this.untrack(this._currentRoomViewModel);
this._currentRoomViewModel = null; // this._gridViewModel.transferRoomViewModel(0, this._currentRoomViewModel);
// const roomId = this._currentRoomViewModel.id;
// this._currentRoomViewModel = null;
// path = this.navigation.path
// .with(this.navigation.segment("rooms", [roomId]))
// .with(this.navigation.segment("room", roomId));
// } else {
// path = this.navigation.path
// .with(this.navigation.segment("rooms", []))
// .with(this.navigation.segment("empty-grid-tile", 0));
// }
// const url = this.urlRouter.urlForPath(path);
// this.urlRouter.history.pushUrl(url);
// this.emitChange("middlePanelViewType");
// this.navigation.applyPath(path);
// }
// _transitionFromGrid() {
// if (!this._gridViewModel) {
// return;
// }
// const vm = this._gridViewModel.releaseFirstRoomViewModel();
// let path = this.navigation.path.until("session");
// if (vm) {
// path = path.with(this.navigation.segment("room", vm.id));
// this._currentRoomViewModel = this.track(vm);
// }
// this._gridViewModel = this.disposeTracked(this._gridViewModel);
// const url = this.urlRouter.urlForPath(path);
// this.urlRouter.history.pushUrl(url);
// this.emitChange("middlePanelViewType");
// this.navigation.applyPath(path);
// }
_updateGrid(roomIds) {
const changed = !(this._gridViewModel && roomIds);
const currentRoomId = this.navigation.path.get("room");
if (roomIds) {
if (!this._gridViewModel) {
this._gridViewModel = this.track(new RoomGridViewModel(this.childOptions({
width: 3,
height: 2,
createRoomViewModel: roomId => this._createRoomViewModel(roomId),
roomIds: roomIds
})));
const vm = this._currentRoomViewModel;
const index = roomIds.indexOf(vm.id);
if (vm && index !== -1) {
this.untrack(vm);
this._gridViewModel.transferRoomViewModel(index, vm);
this._currentRoomViewModel = null;
}
} else {
this._gridViewModel.setRoomIds(roomIds);
} }
} else { } else if (this._gridViewModel && !roomIds) {
const vm = this._gridViewModel.getAndUntrackFirst(); if (currentRoomId) {
if (vm) { const vm = this._gridViewModel.releaseRoomViewModel(currentRoomId.value);
this._currentRoomViewModel = this.track(vm); this._currentRoomViewModel = this.track(vm);
} }
this._gridViewModel = this.disposeTracked(this._gridViewModel); this._gridViewModel = this.disposeTracked(this._gridViewModel);
} }
this.emitChange("middlePanelViewType"); if (changed) {
this.emitChange("middlePanelViewType");
}
} }
_openRoom(roomId) { _createRoomViewModel(roomId) {
// already open?
if (this._gridViewModel?.tryFocusRoom(roomId)) {
return;
} else if (this._currentRoomViewModel?.id === roomId) {
return;
}
const room = this._session.rooms.get(roomId); const room = this._session.rooms.get(roomId);
// not found? close current room and show placeholder
if (!room) { if (!room) {
if (this._gridViewModel) { return null;
this._gridViewModel.setRoomViewModel(null);
} else {
this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel);
}
return;
} }
const roomVM = new RoomViewModel(this.childOptions({ const roomVM = new RoomViewModel(this.childOptions({
room, room,
ownUserId: this._sessionContainer.session.user.id, ownUserId: this._sessionContainer.session.user.id,
})); }));
roomVM.load(); roomVM.load();
return roomVM;
}
_openRoom(roomId) {
// already open?
if (this._currentRoomViewModel?.id === roomId) {
return;
}
this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel);
const roomVM = this._createRoomViewModel(roomId);
if (roomVM) {
this._currentRoomViewModel = this.track(roomVM);
}
if (this._gridViewModel) { if (this._gridViewModel) {
this._gridViewModel.setRoomViewModel(roomVM); this._gridViewModel.setRoomViewModel(roomVM);
} else { } else {
this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel); this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel);
this._currentRoomViewModel = this.track(roomVM);
this.emitChange("currentRoom"); this.emitChange("currentRoom");
} }
} }

View file

@ -34,15 +34,44 @@ export class LeftPanelViewModel extends ViewModel {
}); });
this._roomListFilterMap = new ApplyMap(roomTileVMs); this._roomListFilterMap = new ApplyMap(roomTileVMs);
this._roomList = this._roomListFilterMap.sortValues((a, b) => a.compare(b)); this._roomList = this._roomListFilterMap.sortValues((a, b) => a.compare(b));
this._currentTileVM = null;
this._setupNavigation();
} }
get gridEnabled() { _setupNavigation() {
return this._gridEnabled.get(); const roomObservable = this.navigation.observe("room");
this.track(roomObservable.subscribe(roomId => this._open(roomId)));
this._open(roomObservable.get());
const gridObservable = this.navigation.observe("rooms");
this.gridEnabled = !!gridObservable.get();
this.track(gridObservable.subscribe(roomIds => {
const changed = this.gridEnabled ^ !!roomIds;
this.gridEnabled = !!roomIds;
if (changed) {
this.emitChange("gridEnabled");
}
}));
}
_open(roomId) {
this._currentTileVM?.close();
this._currentTileVM = null;
if (roomId) {
this._currentTileVM = this._roomListFilterMap.get(roomId);
this._currentTileVM?.open();
}
} }
toggleGrid() { toggleGrid() {
this._gridEnabled.set(!this._gridEnabled.get()); let url;
this.emitChange("gridEnabled"); if (this._gridEnabled) {
url = this.urlRouter.disableGridUrl();
} else {
url = this.urlRouter.enableGridUrl();
}
url = this.urlRouter.applyUrl(url);
this.urlRouter.history.pushUrl(url);
} }
get roomList() { get roomList() {

View file

@ -31,7 +31,7 @@ export class RoomTileViewModel extends ViewModel {
this._isOpen = false; this._isOpen = false;
this._wasUnreadWhenOpening = false; this._wasUnreadWhenOpening = false;
this._hidden = false; this._hidden = false;
this._url = this.urlRouter.urlForSegment("room", this._room.id); this._url = this.urlRouter.openRoomActionUrl(this._room.id);
} }
get hidden() { get hidden() {

View file

@ -35,26 +35,27 @@ export class History extends BaseObservableValue {
return document.location.hash; return document.location.hash;
} }
/** does not emit */
replaceUrl(url) { replaceUrl(url) {
window.history.replaceState(null, null, url); window.history.replaceState(null, null, url);
// replaceState does not cause hashchange
this.emit(url);
} }
/** does not emit */
pushUrl(url) { pushUrl(url) {
const hash = this.urlAsPath(url); window.history.pushState(null, null, url);
// important to check before we expect an echo // const hash = this.urlAsPath(url);
// as setting the hash to it's current value doesn't // // important to check before we expect an echo
// trigger onhashchange // // as setting the hash to it's current value doesn't
if (hash === document.location.hash) { // // trigger onhashchange
return; // if (hash === document.location.hash) {
} // return;
// this operation is silent, // }
// so avoid emitting on echo hashchange event // // this operation is silent,
if (this._boundOnHashChange) { // // so avoid emitting on echo hashchange event
this._expectSetEcho = true; // if (this._boundOnHashChange) {
} // this._expectSetEcho = true;
document.location.hash = hash; // }
// document.location.hash = hash;
} }
urlAsPath(url) { urlAsPath(url) {

View file

@ -23,8 +23,8 @@ export class RoomGridView extends TemplateView {
const children = []; const children = [];
for (let i = 0; i < (vm.height * vm.width); i+=1) { for (let i = 0; i < (vm.height * vm.width); i+=1) {
children.push(t.div({ children.push(t.div({
onClick: () => vm.setFocusIndex(i), onClick: () => vm.focusTile(i),
onFocusin: () => vm.setFocusIndex(i), onFocusin: () => vm.focusTile(i),
className: { className: {
"container": true, "container": true,
[`tile${i}`]: true, [`tile${i}`]: true,

View file

@ -25,22 +25,19 @@ export class RoomTileView extends TemplateView {
"hidden": vm => vm.hidden "hidden": vm => vm.hidden
}; };
return t.li({"className": classes}, [ return t.li({"className": classes}, [
renderAvatar(t, vm, 32), t.a({href: vm.url}, [
t.div({className: "description"}, [ renderAvatar(t, vm, 32),
t.div({className: {"name": true, unread: vm => vm.isUnread}}, vm => vm.name), t.div({className: "description"}, [
t.div({ t.div({className: {"name": true, unread: vm => vm.isUnread}}, vm => vm.name),
className: { t.div({
"badge": true, className: {
highlighted: vm => vm.isHighlighted, "badge": true,
hidden: vm => !vm.badgeCount highlighted: vm => vm.isHighlighted,
} hidden: vm => !vm.badgeCount
}, vm => vm.badgeCount), }
}, vm => vm.badgeCount),
])
]) ])
]); ]);
} }
// called from ListView
clicked() {
this.value.open();
}
} }