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,
|
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);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue