Rewrote navigation code

This commit is contained in:
Aleksi Lassila
2024-01-03 02:44:38 +02:00
parent 8298b7111f
commit def6df4074
5 changed files with 156 additions and 213 deletions

View File

@@ -1,27 +1,15 @@
<script lang="ts">
import I18n from './lib/components/Lang/I18n.svelte';
import { Link, navigate, Route, Router } from 'svelte-navigator';
import { Link, Route, Router } from 'svelte-navigator';
import { fade } from 'svelte/transition';
import { handleKeyboardNavigation, navigationContainers } from './lib/actions/focusAction';
import { Container, handleKeyboardNavigation } from './lib/actions/focusAction';
import HomePage from './lib/pages/HomePage.svelte';
let focusableElements: HTMLElement[] = [];
const mainContainer = new Container('main').setDirection('horizontal').setFocusByDefault(true);
const navBarContainer = mainContainer.createChild('nav').setDirection('vertical');
const contentContainer = mainContainer.createChild('content').setDirection('vertical');
function registerArrayFocus(node: HTMLElement) {
focusableElements.push(node);
console.log('Added node', node);
return {
destroy() {
focusableElements = focusableElements.filter((el) => el !== node);
console.log('Removed node', node);
}
};
}
const navBarContainer = navigationContainers.navBar.getRegisterer();
const mainContainer = navigationContainers.main.getRegisterer();
const homeContainer = navigationContainers.home.getRegisterer();
const navBarRegisterer = navBarContainer.getRegisterer();
</script>
<I18n />
@@ -29,16 +17,17 @@
<Router>
<nav class="border">
<Link to="/">
<div use:navBarContainer tabindex="0">Home</div>
<div use:navBarRegisterer tabindex="0">Home</div>
</Link>
<Link to="library">
<div use:navBarContainer tabindex="0">Library</div>
<div use:navBarRegisterer tabindex="0">Library</div>
</Link>
</nav>
<div class="flex-1 flex flex-col min-w-0" use:mainContainer>
<div class="flex-1 flex flex-col min-w-0">
<Route path="/">
<HomePage />
<HomePage container={contentContainer} />
</Route>
<Route path="library">
<div transition:fade|global>about path</div>

View File

@@ -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
};

View File

@@ -5,7 +5,7 @@
export let orientation: 'landscape' | 'portrait' = 'landscape';
export let container: Container;
let registerer = container.getRegisterer();
let registerer = container.createChild('carousel').setDirection('horizontal').getRegisterer();
</script>
{#each Array(10) as _, i (i)}

View File

@@ -1,3 +1,7 @@
<script lang="ts">
export let;
</script>
<div>
<slot />
</div>

View File

@@ -1,20 +1,16 @@
<script lang="ts">
import { Container, navigationContainers } from '../actions/focusAction';
import { Container } from '../actions/focusAction';
import Carousel from '../components/Carousel/Carousel.svelte';
import CarouselPlaceholderItems from '../components/Carousel/CarouselPlaceholderItems.svelte';
const carouselContainer1 = new Container('carousel').setDirection('horizontal');
navigationContainers.home.addChild(carouselContainer1);
const carouselContainer2 = new Container('carousel').setDirection('horizontal');
navigationContainers.home.addChild(carouselContainer2);
export let container: Container;
</script>
<div class="flex flex-col">
<Carousel>
<CarouselPlaceholderItems container={carouselContainer1} />
<CarouselPlaceholderItems {container} />
</Carousel>
<Carousel>
<CarouselPlaceholderItems container={carouselContainer2} />
<CarouselPlaceholderItems {container} />
</Carousel>
</div>