|
|
|
|
@@ -1,19 +1,25 @@
|
|
|
|
|
export type Registerer = (htmlElement: HTMLElement) => { destroy: () => void };
|
|
|
|
|
|
|
|
|
|
export type Direction = 'up' | 'down' | 'left' | 'right';
|
|
|
|
|
export type FlowDirection = 'vertical' | 'horizontal';
|
|
|
|
|
|
|
|
|
|
export class Container {
|
|
|
|
|
id: symbol;
|
|
|
|
|
name: string;
|
|
|
|
|
parent?: Container;
|
|
|
|
|
children: Container[] = [];
|
|
|
|
|
htmlElement?: HTMLElement;
|
|
|
|
|
private upNeighbor?: Container;
|
|
|
|
|
private downNeighbor?: Container;
|
|
|
|
|
private leftNeighbor?: Container;
|
|
|
|
|
private rightNeighbor?: Container;
|
|
|
|
|
private parent?: Container;
|
|
|
|
|
private children: Container[] = [];
|
|
|
|
|
private htmlElement?: HTMLElement;
|
|
|
|
|
private neighbors: Record<Direction, Container | undefined> = {
|
|
|
|
|
up: undefined,
|
|
|
|
|
down: undefined,
|
|
|
|
|
left: undefined,
|
|
|
|
|
right: undefined
|
|
|
|
|
};
|
|
|
|
|
private focusByDefault: boolean = false;
|
|
|
|
|
|
|
|
|
|
direction: 'horizontal' | 'vertical' = 'horizontal';
|
|
|
|
|
private direction: FlowDirection = 'vertical';
|
|
|
|
|
|
|
|
|
|
focusIndex: number = 0;
|
|
|
|
|
private focusIndex: number = 0;
|
|
|
|
|
|
|
|
|
|
static focusedObject: Container;
|
|
|
|
|
static objects = new Map<symbol, Container>();
|
|
|
|
|
@@ -22,191 +28,150 @@ export class Container {
|
|
|
|
|
this.id = Symbol();
|
|
|
|
|
this.name = name;
|
|
|
|
|
Container.objects.set(this.id, this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!Container.focusedObject) {
|
|
|
|
|
setDirection(direction: FlowDirection) {
|
|
|
|
|
this.direction = direction;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setFocusByDefault(focusByDefault: boolean) {
|
|
|
|
|
this.focusByDefault = focusByDefault;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createChild(name: string = '') {
|
|
|
|
|
const child = new Container(name);
|
|
|
|
|
this.addChild(child);
|
|
|
|
|
return child;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
focus() {
|
|
|
|
|
if (this.children.length > 0) {
|
|
|
|
|
this.children[this.focusIndex]?.focus();
|
|
|
|
|
} else if (this.htmlElement) {
|
|
|
|
|
this.htmlElement.focus({ preventScroll: true });
|
|
|
|
|
this.htmlElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
|
|
|
|
|
Container.focusedObject = this;
|
|
|
|
|
this.updateFocusIndex();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateFocusIndex(container?: Container) {
|
|
|
|
|
if (container) {
|
|
|
|
|
const index = this.children.indexOf(container);
|
|
|
|
|
this.focusIndex = index === -1 ? this.focusIndex : index;
|
|
|
|
|
}
|
|
|
|
|
if (this.parent) {
|
|
|
|
|
this.parent.updateFocusIndex(this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isFocusable() {
|
|
|
|
|
if (this.htmlElement) {
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
for (const child of this.children) {
|
|
|
|
|
if (child.isFocusable()) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getFocusableNeighbor(direction: Direction): Container | undefined {
|
|
|
|
|
const canLoop =
|
|
|
|
|
(this.direction === 'vertical' &&
|
|
|
|
|
((direction === 'up' && this.focusIndex !== 0) ||
|
|
|
|
|
(direction === 'down' && this.focusIndex !== this.children.length - 1))) ||
|
|
|
|
|
(this.direction === 'horizontal' &&
|
|
|
|
|
((direction === 'left' && this.focusIndex !== 0) ||
|
|
|
|
|
(direction === 'right' && this.focusIndex !== this.children.length - 1)));
|
|
|
|
|
if (this.children.length > 0 && canLoop) {
|
|
|
|
|
if (direction === 'up' || direction === 'left') {
|
|
|
|
|
let index = this.focusIndex - 1;
|
|
|
|
|
while (index >= 0) {
|
|
|
|
|
if (this.children[index].isFocusable()) {
|
|
|
|
|
return this.children[index];
|
|
|
|
|
}
|
|
|
|
|
index--;
|
|
|
|
|
}
|
|
|
|
|
} else if (direction === 'down' || direction === 'right') {
|
|
|
|
|
let index = this.focusIndex + 1;
|
|
|
|
|
while (index < this.children.length) {
|
|
|
|
|
if (this.children[index].isFocusable()) {
|
|
|
|
|
return this.children[index];
|
|
|
|
|
}
|
|
|
|
|
index++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (this.neighbors[direction]?.isFocusable()) {
|
|
|
|
|
return this.neighbors[direction];
|
|
|
|
|
} else {
|
|
|
|
|
return this.parent?.getFocusableNeighbor(direction);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
giveFocus(direction: Direction) {
|
|
|
|
|
const neighbor = this.getFocusableNeighbor(direction);
|
|
|
|
|
console.log('Giving focus to', direction, 'neighbor: ', neighbor?.name, neighbor);
|
|
|
|
|
if (neighbor) {
|
|
|
|
|
neighbor.focus();
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getRegisterer(): Registerer {
|
|
|
|
|
return (htmlElement: HTMLElement) => {
|
|
|
|
|
this.addChildElement(htmlElement);
|
|
|
|
|
if (this.htmlElement) console.warn('Registering to a container that has an element.');
|
|
|
|
|
|
|
|
|
|
this.createChild().addHtmlElement(htmlElement);
|
|
|
|
|
|
|
|
|
|
if (!Container.focusedObject && this.shouldFocusByDefault()) {
|
|
|
|
|
this.focus();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
destroy: () => {
|
|
|
|
|
this.removeChildElement(htmlElement);
|
|
|
|
|
this.removeHtmlElement();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
giveFocus(direction: 'up' | 'down' | 'left' | 'right') {
|
|
|
|
|
console.log('This: ');
|
|
|
|
|
this.log();
|
|
|
|
|
|
|
|
|
|
console.log('Giving focus to', direction, 'neighbor');
|
|
|
|
|
const upNeighbor = this.getUpNeighbor();
|
|
|
|
|
const downNeighbor = this.getDownNeighbor();
|
|
|
|
|
const leftNeighbor = this.getLeftNeighbor();
|
|
|
|
|
const rightNeighbor = this.getRightNeighbor();
|
|
|
|
|
if (direction === 'up' && upNeighbor) {
|
|
|
|
|
if (upNeighbor.direction === 'vertical') {
|
|
|
|
|
upNeighbor.focusIndex = upNeighbor.children.length - 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
upNeighbor.focusElement();
|
|
|
|
|
} else if (direction === 'down' && downNeighbor) {
|
|
|
|
|
if (downNeighbor.direction === 'vertical') {
|
|
|
|
|
downNeighbor.focusIndex = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
downNeighbor.focusElement();
|
|
|
|
|
} else if (direction === 'left' && leftNeighbor) {
|
|
|
|
|
if (leftNeighbor.direction === 'horizontal') {
|
|
|
|
|
leftNeighbor.focusIndex = leftNeighbor.children.length - 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
leftNeighbor.focusElement();
|
|
|
|
|
} else if (direction === 'right' && rightNeighbor) {
|
|
|
|
|
if (rightNeighbor.direction === 'horizontal') {
|
|
|
|
|
rightNeighbor.focusIndex = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rightNeighbor.focusElement();
|
|
|
|
|
} else {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getUpNeighbor(): Container | undefined {
|
|
|
|
|
return this.upNeighbor || this.parent?.getUpNeighbor();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getDownNeighbor(): Container | undefined {
|
|
|
|
|
return this.downNeighbor || this.parent?.getDownNeighbor();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getLeftNeighbor(): Container | undefined {
|
|
|
|
|
return this.leftNeighbor || this.parent?.getLeftNeighbor();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getRightNeighbor(): Container | undefined {
|
|
|
|
|
return this.rightNeighbor || this.parent?.getRightNeighbor();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
focusElement() {
|
|
|
|
|
Container.focusedObject = this;
|
|
|
|
|
if (this.htmlElement) {
|
|
|
|
|
this.htmlElement.focus({ preventScroll: true });
|
|
|
|
|
this.htmlElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
|
|
|
|
|
|
|
|
|
|
if (this.parent) {
|
|
|
|
|
this.parent.focusIndex = this.parent.children.findIndex((child) => child === this);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.children[this.focusIndex].focusElement();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setParent(parent?: Container) {
|
|
|
|
|
this.parent = parent;
|
|
|
|
|
private addChild(child: Container) {
|
|
|
|
|
this.children.push(child);
|
|
|
|
|
child.parent = this;
|
|
|
|
|
this.htmlElement = undefined;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setHtmlElement(htmlElement: HTMLElement) {
|
|
|
|
|
private removeChild(child: Container) {
|
|
|
|
|
this.children = this.children.filter((c) => c !== child);
|
|
|
|
|
child.parent = undefined;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private addHtmlElement(htmlElement: HTMLElement) {
|
|
|
|
|
if (this.children.length > 0) {
|
|
|
|
|
console.warn('Adding an html element to a container that has children.');
|
|
|
|
|
for (const child of this.children) {
|
|
|
|
|
this.removeChild(child);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.htmlElement = htmlElement;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addChild(child: Container) {
|
|
|
|
|
console.log('Adding child', child.name, 'to', this.name);
|
|
|
|
|
if (this.direction === 'vertical' && this.children.length >= 1) {
|
|
|
|
|
child.setUpNeighbor(this.children[this.children.length - 1]);
|
|
|
|
|
} else if (this.direction === 'horizontal' && this.children.length >= 1) {
|
|
|
|
|
child.setLeftNeighbor(this.children[this.children.length - 1]);
|
|
|
|
|
}
|
|
|
|
|
this.children.push(child.setParent(this));
|
|
|
|
|
console.log('After add', this);
|
|
|
|
|
private removeHtmlElement() {
|
|
|
|
|
this.htmlElement = undefined;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private addChildElement(htmlElement: HTMLElement) {
|
|
|
|
|
const childContainer = new Container().setHtmlElement(htmlElement).setParent(this);
|
|
|
|
|
|
|
|
|
|
if (this.direction === 'vertical' && this.children.length >= 1) {
|
|
|
|
|
childContainer.setUpNeighbor(this.children[this.children.length - 1]);
|
|
|
|
|
} else if (this.direction === 'horizontal' && this.children.length >= 1) {
|
|
|
|
|
childContainer.setLeftNeighbor(this.children[this.children.length - 1]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.children.push(childContainer);
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private removeChildElement(htmlElement: HTMLElement) {
|
|
|
|
|
const child = this.children.find((child) => child.htmlElement === htmlElement);
|
|
|
|
|
child?.setParent(undefined)?.removeNeighbors();
|
|
|
|
|
this.children = this.children.filter((child) => child.htmlElement !== htmlElement);
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private removeNeighbors() {
|
|
|
|
|
if (this.upNeighbor?.downNeighbor === this) {
|
|
|
|
|
this.upNeighbor.downNeighbor = undefined;
|
|
|
|
|
}
|
|
|
|
|
if (this.downNeighbor?.upNeighbor === this) {
|
|
|
|
|
this.downNeighbor.upNeighbor = undefined;
|
|
|
|
|
}
|
|
|
|
|
if (this.leftNeighbor?.rightNeighbor === this) {
|
|
|
|
|
this.leftNeighbor.rightNeighbor = undefined;
|
|
|
|
|
}
|
|
|
|
|
if (this.rightNeighbor?.leftNeighbor === this) {
|
|
|
|
|
this.rightNeighbor.leftNeighbor = undefined;
|
|
|
|
|
}
|
|
|
|
|
this.upNeighbor = undefined;
|
|
|
|
|
this.downNeighbor = undefined;
|
|
|
|
|
this.leftNeighbor = undefined;
|
|
|
|
|
this.rightNeighbor = undefined;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setUpNeighbor(upNeighbor: Container) {
|
|
|
|
|
this.upNeighbor = upNeighbor;
|
|
|
|
|
upNeighbor.downNeighbor = this;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setDownNeighbor(downNeighbor: Container) {
|
|
|
|
|
this.downNeighbor = downNeighbor;
|
|
|
|
|
downNeighbor.upNeighbor = this;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setLeftNeighbor(leftNeighbor: Container) {
|
|
|
|
|
this.leftNeighbor = leftNeighbor;
|
|
|
|
|
leftNeighbor.rightNeighbor = this;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setRightNeighbor(rightNeighbor: Container) {
|
|
|
|
|
this.rightNeighbor = rightNeighbor;
|
|
|
|
|
rightNeighbor.leftNeighbor = this;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setDirection(direction: 'horizontal' | 'vertical') {
|
|
|
|
|
this.direction = direction;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log() {
|
|
|
|
|
console.log(this.name, this);
|
|
|
|
|
if (this.parent) {
|
|
|
|
|
console.log('With parent: ');
|
|
|
|
|
this.parent.log();
|
|
|
|
|
}
|
|
|
|
|
private shouldFocusByDefault(): boolean {
|
|
|
|
|
return this.focusByDefault || this.parent?.shouldFocusByDefault() || false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -218,6 +183,8 @@ export function handleKeyboardNavigation(event: KeyboardEvent) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('Currently focused object: ', currentlyFocusedObject.name, currentlyFocusedObject);
|
|
|
|
|
|
|
|
|
|
if (event.key === 'ArrowUp') {
|
|
|
|
|
if (currentlyFocusedObject.giveFocus('up')) event.preventDefault();
|
|
|
|
|
} else if (event.key === 'ArrowDown') {
|
|
|
|
|
@@ -228,16 +195,3 @@ export function handleKeyboardNavigation(event: KeyboardEvent) {
|
|
|
|
|
if (currentlyFocusedObject.giveFocus('right')) event.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const navBar = new Container('navBar').setDirection('vertical');
|
|
|
|
|
const main = new Container('main').setDirection('vertical');
|
|
|
|
|
const home = new Container('home').setDirection('vertical');
|
|
|
|
|
|
|
|
|
|
main.setLeftNeighbor(navBar);
|
|
|
|
|
main.addChild(home);
|
|
|
|
|
|
|
|
|
|
export const navigationContainers = {
|
|
|
|
|
home,
|
|
|
|
|
main,
|
|
|
|
|
navBar
|
|
|
|
|
};
|
|
|
|
|
|