first draft of url navigation for grid
This commit is contained in:
parent
6c2c29a7da
commit
b2d6b7014b
11 changed files with 539 additions and 201 deletions
|
@ -98,7 +98,7 @@ export class RootViewModel extends ViewModel {
|
|||
createSessionContainer: this._createSessionContainer,
|
||||
ready: sessionContainer => {
|
||||
const url = this.urlRouter.urlForSegment("session", sessionContainer.sessionId);
|
||||
this.urlRouter.replaceUrl(url);
|
||||
this.urlRouter.history.replaceUrl(url);
|
||||
this._showSession(sessionContainer);
|
||||
},
|
||||
}));
|
||||
|
|
|
@ -28,10 +28,26 @@ export class Navigation {
|
|||
}
|
||||
|
||||
applyPath(path) {
|
||||
const oldPath = this._path;
|
||||
this._path = path;
|
||||
for (const [type, observable] of this._observables) {
|
||||
// if the value did not change, this won't emit
|
||||
observable.set(this._path.get(type)?.value);
|
||||
// clear values not in the new path in reverse order of path
|
||||
for (let i = oldPath.segments.length - 1; i >= 0; i -= 1) {
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -89,6 +126,14 @@ class Path {
|
|||
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) {
|
||||
return this._segments.find(s => s.type === type);
|
||||
}
|
||||
|
|
|
@ -17,69 +17,78 @@ limitations under the License.
|
|||
import {Segment} from "./Navigation.js";
|
||||
|
||||
export class URLRouter {
|
||||
constructor({history, navigation, redirect}) {
|
||||
constructor({history, navigation, parseUrlPath, stringifyPath}) {
|
||||
this._subscription = null;
|
||||
this._history = history;
|
||||
this._navigation = navigation;
|
||||
this._redirect = redirect;
|
||||
this._parseUrlPath = parseUrlPath;
|
||||
this._stringifyPath = stringifyPath;
|
||||
}
|
||||
|
||||
attach() {
|
||||
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());
|
||||
}
|
||||
|
||||
applyUrl(url) {
|
||||
const segments = this._segmentsFromUrl(url);
|
||||
const path = this._redirect(segments, this._navigation);
|
||||
this._navigation.applyPath(path);
|
||||
}
|
||||
|
||||
stop() {
|
||||
dispose() {
|
||||
this._subscription = this._subscription();
|
||||
}
|
||||
|
||||
_segmentsFromUrl(url) {
|
||||
const path = this._history.urlAsPath(url);
|
||||
const parts = path.split("/").filter(p => !!p);
|
||||
let index = 0;
|
||||
const segments = [];
|
||||
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;
|
||||
applyUrl(url) {
|
||||
const urlPath = this._history.urlAsPath(url)
|
||||
const navPath = this._navigation.pathFrom(this._parseUrlPath(urlPath));
|
||||
this._navigation.applyPath(navPath);
|
||||
return this._history.pathAsUrl(this._stringifyPath(navPath));
|
||||
}
|
||||
|
||||
get history() {
|
||||
return this._history;
|
||||
}
|
||||
|
||||
urlForSegment(type, value) {
|
||||
const path = this._navigation.path.with(new Segment(type, value));
|
||||
if (path) {
|
||||
return this.urlForPath(path);
|
||||
urlForSegments(segments) {
|
||||
let path = this._navigation.path;
|
||||
for (const segment of segments) {
|
||||
path = path.with(segment);
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return this.urlForPath(path);
|
||||
}
|
||||
|
||||
urlForSegment(type, value) {
|
||||
return this.urlForSegments([this._navigation.segment(type, value)]);
|
||||
}
|
||||
|
||||
urlForPath(path) {
|
||||
let urlPath = "";
|
||||
for (const {type, value} of path.segments) {
|
||||
if (typeof value === "boolean") {
|
||||
urlPath += `/${type}`;
|
||||
} else {
|
||||
urlPath += `/${type}/${value}`;
|
||||
}
|
||||
}
|
||||
return this.history.pathAsUrl(this._stringifyPath(path));
|
||||
}
|
||||
|
||||
openRoomActionUrl(roomId) {
|
||||
// not a segment to navigation knowns about, so append it manually
|
||||
const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${roomId}`;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,49 +18,30 @@ import {Navigation, Segment} from "./Navigation.js";
|
|||
import {URLRouter} from "./URLRouter.js";
|
||||
|
||||
export function createNavigation() {
|
||||
return new Navigation(function allowsChild(parent, child) {
|
||||
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;
|
||||
}
|
||||
});
|
||||
return new Navigation(allowsChild);
|
||||
}
|
||||
|
||||
export function createRouter({history, navigation}) {
|
||||
return new URLRouter({history, navigation, redirect});
|
||||
return new URLRouter({history, navigation, stringifyPath, parseUrlPath});
|
||||
}
|
||||
|
||||
function redirect(urlParts, navigation) {
|
||||
const {path} = navigation;
|
||||
const segments = urlParts.reduce((output, s) => {
|
||||
// redirect open-room action to grid/non-grid url
|
||||
if (s.type === "open-room") {
|
||||
const rooms = path.get("rooms");
|
||||
if (rooms) {
|
||||
output = output.concat(roomsSegmentWithRoom(rooms, s.value, path));
|
||||
}
|
||||
return rooms.concat(new Segment("room", s.value));
|
||||
}
|
||||
return output.concat(s);
|
||||
}, []);
|
||||
return navigation.pathFrom(segments);
|
||||
function allowsChild(parent, child) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
let index = 0;
|
||||
if (room) {
|
||||
|
@ -71,20 +52,157 @@ function roomsSegmentWithRoom(rooms, roomId, path) {
|
|||
index = emptyGridTile.value;
|
||||
}
|
||||
}
|
||||
const newRooms = rooms.slice();
|
||||
const newRooms = rooms.value.slice();
|
||||
newRooms[index] = roomId;
|
||||
return new Segment("rooms", newRooms);
|
||||
}
|
||||
|
||||
function parseUrlValue(type, iterator) {
|
||||
if (type === "rooms") {
|
||||
const roomIds = iterator.next().value.split(",");
|
||||
const selectedIndex = parseInt(iterator.next().value, 10);
|
||||
const roomId = roomIds[selectedIndex];
|
||||
if (roomId) {
|
||||
return [new Segment(type, roomIds), new Segment("room", roomId)];
|
||||
export function parseUrlPath(urlPath, currentNavPath) {
|
||||
// substr(1) to take of initial /
|
||||
const parts = urlPath.substr(1).split("/");
|
||||
const iterator = parts[Symbol.iterator]();
|
||||
const segments = [];
|
||||
let next;
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,10 +19,46 @@ import {ViewModel} from "../ViewModel.js";
|
|||
export class RoomGridViewModel extends ViewModel {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this._width = options.width;
|
||||
this._height = options.height;
|
||||
this._createRoomViewModel = options.createRoomViewModel;
|
||||
|
||||
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) {
|
||||
|
@ -33,15 +69,6 @@ export class RoomGridViewModel extends ViewModel {
|
|||
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() {
|
||||
return this._width;
|
||||
}
|
||||
|
@ -50,41 +77,91 @@ export class RoomGridViewModel extends ViewModel {
|
|||
return this._height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a pair of room and room tile view models at the current index
|
||||
* @param {RoomViewModel} vm
|
||||
* @package
|
||||
*/
|
||||
setRoomViewModel(vm) {
|
||||
const old = this._viewModels[this._selectedIndex];
|
||||
this.disposeTracked(old);
|
||||
this._viewModels[this._selectedIndex] = this.track(vm);
|
||||
this.emitChange(`${this._selectedIndex}`);
|
||||
focusTile(index) {
|
||||
if (index === this._selectedIndex) {
|
||||
return;
|
||||
}
|
||||
let path = this.navigation.path;
|
||||
const vm = this._viewModels[index];
|
||||
if (vm) {
|
||||
path = path.with(this.navigation.segment("room", vm.id));
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @package
|
||||
*/
|
||||
tryFocusRoom(roomId) {
|
||||
/** called from SessionViewModel */
|
||||
setRoomIds(roomIds) {
|
||||
let changed = false;
|
||||
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);
|
||||
if (index >= 0) {
|
||||
this.setFocusIndex(index);
|
||||
this._setFocusIndex(index);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first set of room vm,
|
||||
* and untracking it so it is not owned by this view model anymore.
|
||||
* @package
|
||||
*/
|
||||
getAndUntrackFirst() {
|
||||
for (const vm of this._viewModels) {
|
||||
|
||||
_openRoom(roomId) {
|
||||
if (!this._setFocusRoom(roomId)) {
|
||||
// replace vm at focused index
|
||||
const vm = this._viewModels[this._selectedIndex];
|
||||
if (vm) {
|
||||
this.untrack(vm);
|
||||
return vm;
|
||||
this.disposeTracked(vm);
|
||||
}
|
||||
this._viewModels[this._selectedIndex] = this.track(this._createRoomViewModel(roomId));
|
||||
this.emitChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,27 +32,33 @@ export class SessionViewModel extends ViewModel {
|
|||
session: sessionContainer.session,
|
||||
})));
|
||||
this._leftPanelViewModel = new LeftPanelViewModel(this.childOptions({
|
||||
rooms: this._sessionContainer.session.rooms,
|
||||
// this will go over navigation as well
|
||||
gridEnabled: {
|
||||
get: () => !!this._gridViewModel,
|
||||
set: value => this._enabledGrid(value)
|
||||
}
|
||||
rooms: this._sessionContainer.session.rooms
|
||||
}));
|
||||
this._currentRoomViewModel = null;
|
||||
this._gridViewModel = null;
|
||||
// this gives us the active room, also in the grid?
|
||||
this.track(this.navigator.observe("room").subscribe(roomId => {
|
||||
this._setupNavigation();
|
||||
}
|
||||
|
||||
}));
|
||||
_setupNavigation() {
|
||||
const gridRooms = this.navigation.observe("rooms");
|
||||
// this gives us a set of room ids in the grid
|
||||
this.track(this.navigator.observe("rooms").subscribe(value => {
|
||||
if (value) {
|
||||
const roomIds = typeof value === "string" ? value.split(",") : [];
|
||||
// also update grid
|
||||
this._enabledGrid(roomIds);
|
||||
this.track(gridRooms.subscribe(roomIds => {
|
||||
this._updateGrid(roomIds);
|
||||
}));
|
||||
if (gridRooms.get()) {
|
||||
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() {
|
||||
|
@ -88,53 +94,109 @@ export class SessionViewModel extends ViewModel {
|
|||
return this._currentRoomViewModel;
|
||||
}
|
||||
|
||||
// TODO: this should also happen based on URLs
|
||||
_enabledGrid(enabled) {
|
||||
if (enabled) {
|
||||
this._gridViewModel = this.track(new RoomGridViewModel(this.childOptions({width: 3, height: 2})));
|
||||
// transfer current room
|
||||
if (this._currentRoomViewModel) {
|
||||
this.untrack(this._currentRoomViewModel);
|
||||
this._gridViewModel.setRoomViewModel(this._currentRoomViewModel);
|
||||
this._currentRoomViewModel = null;
|
||||
// _transitionToGrid() {
|
||||
// if (this._gridViewModel) {
|
||||
// return;
|
||||
// }
|
||||
// this._gridViewModel = this.track(new RoomGridViewModel(this.childOptions({width: 3, height: 2})));
|
||||
// let path;
|
||||
// if (this._currentRoomViewModel) {
|
||||
// this.untrack(this._currentRoomViewModel);
|
||||
// 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 {
|
||||
const vm = this._gridViewModel.getAndUntrackFirst();
|
||||
if (vm) {
|
||||
} else if (this._gridViewModel && !roomIds) {
|
||||
if (currentRoomId) {
|
||||
const vm = this._gridViewModel.releaseRoomViewModel(currentRoomId.value);
|
||||
this._currentRoomViewModel = this.track(vm);
|
||||
}
|
||||
this._gridViewModel = this.disposeTracked(this._gridViewModel);
|
||||
}
|
||||
this.emitChange("middlePanelViewType");
|
||||
if (changed) {
|
||||
this.emitChange("middlePanelViewType");
|
||||
}
|
||||
}
|
||||
|
||||
_openRoom(roomId) {
|
||||
// already open?
|
||||
if (this._gridViewModel?.tryFocusRoom(roomId)) {
|
||||
return;
|
||||
} else if (this._currentRoomViewModel?.id === roomId) {
|
||||
return;
|
||||
}
|
||||
_createRoomViewModel(roomId) {
|
||||
const room = this._session.rooms.get(roomId);
|
||||
// not found? close current room and show placeholder
|
||||
if (!room) {
|
||||
if (this._gridViewModel) {
|
||||
this._gridViewModel.setRoomViewModel(null);
|
||||
} else {
|
||||
this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel);
|
||||
}
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
const roomVM = new RoomViewModel(this.childOptions({
|
||||
room,
|
||||
ownUserId: this._sessionContainer.session.user.id,
|
||||
}));
|
||||
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) {
|
||||
this._gridViewModel.setRoomViewModel(roomVM);
|
||||
} else {
|
||||
this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel);
|
||||
this._currentRoomViewModel = this.track(roomVM);
|
||||
this.emitChange("currentRoom");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,15 +34,44 @@ export class LeftPanelViewModel extends ViewModel {
|
|||
});
|
||||
this._roomListFilterMap = new ApplyMap(roomTileVMs);
|
||||
this._roomList = this._roomListFilterMap.sortValues((a, b) => a.compare(b));
|
||||
this._currentTileVM = null;
|
||||
this._setupNavigation();
|
||||
}
|
||||
|
||||
get gridEnabled() {
|
||||
return this._gridEnabled.get();
|
||||
_setupNavigation() {
|
||||
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() {
|
||||
this._gridEnabled.set(!this._gridEnabled.get());
|
||||
this.emitChange("gridEnabled");
|
||||
let url;
|
||||
if (this._gridEnabled) {
|
||||
url = this.urlRouter.disableGridUrl();
|
||||
} else {
|
||||
url = this.urlRouter.enableGridUrl();
|
||||
}
|
||||
url = this.urlRouter.applyUrl(url);
|
||||
this.urlRouter.history.pushUrl(url);
|
||||
}
|
||||
|
||||
get roomList() {
|
||||
|
|
|
@ -31,7 +31,7 @@ export class RoomTileViewModel extends ViewModel {
|
|||
this._isOpen = false;
|
||||
this._wasUnreadWhenOpening = false;
|
||||
this._hidden = false;
|
||||
this._url = this.urlRouter.urlForSegment("room", this._room.id);
|
||||
this._url = this.urlRouter.openRoomActionUrl(this._room.id);
|
||||
}
|
||||
|
||||
get hidden() {
|
||||
|
|
|
@ -35,26 +35,27 @@ export class History extends BaseObservableValue {
|
|||
return document.location.hash;
|
||||
}
|
||||
|
||||
/** does not emit */
|
||||
replaceUrl(url) {
|
||||
window.history.replaceState(null, null, url);
|
||||
// replaceState does not cause hashchange
|
||||
this.emit(url);
|
||||
}
|
||||
|
||||
/** does not emit */
|
||||
pushUrl(url) {
|
||||
const hash = this.urlAsPath(url);
|
||||
// important to check before we expect an echo
|
||||
// as setting the hash to it's current value doesn't
|
||||
// trigger onhashchange
|
||||
if (hash === document.location.hash) {
|
||||
return;
|
||||
}
|
||||
// this operation is silent,
|
||||
// so avoid emitting on echo hashchange event
|
||||
if (this._boundOnHashChange) {
|
||||
this._expectSetEcho = true;
|
||||
}
|
||||
document.location.hash = hash;
|
||||
window.history.pushState(null, null, url);
|
||||
// const hash = this.urlAsPath(url);
|
||||
// // important to check before we expect an echo
|
||||
// // as setting the hash to it's current value doesn't
|
||||
// // trigger onhashchange
|
||||
// if (hash === document.location.hash) {
|
||||
// return;
|
||||
// }
|
||||
// // this operation is silent,
|
||||
// // so avoid emitting on echo hashchange event
|
||||
// if (this._boundOnHashChange) {
|
||||
// this._expectSetEcho = true;
|
||||
// }
|
||||
// document.location.hash = hash;
|
||||
}
|
||||
|
||||
urlAsPath(url) {
|
||||
|
|
|
@ -23,8 +23,8 @@ export class RoomGridView extends TemplateView {
|
|||
const children = [];
|
||||
for (let i = 0; i < (vm.height * vm.width); i+=1) {
|
||||
children.push(t.div({
|
||||
onClick: () => vm.setFocusIndex(i),
|
||||
onFocusin: () => vm.setFocusIndex(i),
|
||||
onClick: () => vm.focusTile(i),
|
||||
onFocusin: () => vm.focusTile(i),
|
||||
className: {
|
||||
"container": true,
|
||||
[`tile${i}`]: true,
|
||||
|
|
|
@ -25,22 +25,19 @@ export class RoomTileView extends TemplateView {
|
|||
"hidden": vm => vm.hidden
|
||||
};
|
||||
return t.li({"className": classes}, [
|
||||
renderAvatar(t, vm, 32),
|
||||
t.div({className: "description"}, [
|
||||
t.div({className: {"name": true, unread: vm => vm.isUnread}}, vm => vm.name),
|
||||
t.div({
|
||||
className: {
|
||||
"badge": true,
|
||||
highlighted: vm => vm.isHighlighted,
|
||||
hidden: vm => !vm.badgeCount
|
||||
}
|
||||
}, vm => vm.badgeCount),
|
||||
t.a({href: vm.url}, [
|
||||
renderAvatar(t, vm, 32),
|
||||
t.div({className: "description"}, [
|
||||
t.div({className: {"name": true, unread: vm => vm.isUnread}}, vm => vm.name),
|
||||
t.div({
|
||||
className: {
|
||||
"badge": true,
|
||||
highlighted: vm => vm.isHighlighted,
|
||||
hidden: vm => !vm.badgeCount
|
||||
}
|
||||
}, vm => vm.badgeCount),
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
// called from ListView
|
||||
clicked() {
|
||||
this.value.open();
|
||||
}
|
||||
}
|
||||
|
|
Reference in a new issue