Bug fixes and refactoring regarding
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import I18n from './lib/components/Lang/I18n.svelte';
|
||||
import { Link, Route, Router } from 'svelte-navigator';
|
||||
import { handleKeyboardNavigation } from './lib/actions/focusAction';
|
||||
import { handleKeyboardNavigation, Selectable } from './lib/selectable';
|
||||
import { onMount } from 'svelte';
|
||||
import Container from './Container.svelte';
|
||||
import NavbarItem from './lib/components-new/NavbarItem.svelte';
|
||||
@@ -14,35 +14,18 @@
|
||||
import ManagePage from './lib/pages/ManagePage.svelte';
|
||||
import SearchPage from './lib/pages/SearchPage.svelte';
|
||||
|
||||
// const navBarContainer = mainContainer.createChild('nav').setDirection('vertical');
|
||||
// const isNavBarOpen = navBarContainer.hasFocusWithin;
|
||||
//
|
||||
// const contentContainer = mainContainer.createChild('content').setDirection('vertical');
|
||||
Selectable.focusedObject.subscribe((s) => console.log('FocusedObject', s));
|
||||
let mainContent: Selectable;
|
||||
|
||||
// onMount(() => {
|
||||
// contentContainer.focus();
|
||||
// });
|
||||
onMount(() => {
|
||||
mainContent.focus();
|
||||
});
|
||||
|
||||
onMount(() => console.log('didmount'));
|
||||
let mount = false;
|
||||
$: console.log('mount', mount);
|
||||
let isNavBarOpen: Readable<boolean>;
|
||||
</script>
|
||||
|
||||
<I18n />
|
||||
<Container horizontal class="bg-stone-950 text-white flex flex-1 w-screen">
|
||||
<!-- <Container class="flex flex-col">-->
|
||||
<!-- <Container tag="button" on:click={() => (mount = !mount)}>Button 1</Container>-->
|
||||
<!-- <Container tag="button" on:click={() => console.log('Button 2 clicked')}>Button 2</Container>-->
|
||||
<!-- <Container tag="button" on:click={() => console.log('Button 3 clicked')}>Button 3</Container>-->
|
||||
<!-- </Container>-->
|
||||
|
||||
<!-- <div>-->
|
||||
<!-- <Container tag="button" on:click={() => console.log('test')}>Test</Container>-->
|
||||
<!-- </div>-->
|
||||
<!-- {#if mount}-->
|
||||
<!-- <TestElement />-->
|
||||
<!-- {/if}-->
|
||||
<Router>
|
||||
<Container
|
||||
class={classNames('flex flex-col', 'p-4', {
|
||||
@@ -58,7 +41,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-1 justify-center">
|
||||
<NavbarItem to="series">
|
||||
<NavbarItem to="/">
|
||||
<Laptop class="w-8 h-8 mr-3" slot="icon" />
|
||||
<span class="text-xl" slot="text"> Series</span>
|
||||
</NavbarItem>
|
||||
@@ -84,7 +67,7 @@
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
<Container class="flex-1 flex flex-col min-w-0">
|
||||
<Container bind:container={mainContent} class="flex-1 flex flex-col min-w-0">
|
||||
<Route>
|
||||
<SeriesPage />
|
||||
</Route>
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Container } from './lib/actions/focusAction';
|
||||
import { Selectable } from './lib/selectable';
|
||||
import type { Readable } from 'svelte/store';
|
||||
|
||||
export let name: string = '';
|
||||
export let horizontal = false;
|
||||
export let focusOnMount = false;
|
||||
|
||||
const { registerer, ...rest } = new Container(name)
|
||||
const { registerer, ...rest } = new Selectable(name)
|
||||
.setDirection(horizontal ? 'horizontal' : 'vertical')
|
||||
.getStores();
|
||||
export const container = rest.container;
|
||||
export const hasFocus = rest.hasFocus;
|
||||
export const hasFocusWithin = rest.hasFocusWithin;
|
||||
export const focusIndex = rest.focusIndex;
|
||||
|
||||
export let tag = 'div';
|
||||
|
||||
onMount(() => {
|
||||
rest.container._initializeContainer();
|
||||
rest.container._initializeSelectable();
|
||||
|
||||
if (focusOnMount) {
|
||||
rest.container.focus();
|
||||
}
|
||||
|
||||
return () => {
|
||||
rest.container._unmountContainer();
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Container, type FlowDirection } from './lib/actions/focusAction';
|
||||
import { Selectable, type FlowDirection } from './lib/selectable';
|
||||
|
||||
export let name: string = '';
|
||||
export let direction: FlowDirection = 'vertical';
|
||||
|
||||
const { registerer, ...rest } = new Container(name).setDirection(direction).getStores();
|
||||
const { registerer, ...rest } = new Selectable(name).setDirection(direction).getStores();
|
||||
export const container = rest.container;
|
||||
export const hasFocus = rest.hasFocus;
|
||||
export const hasFocusWithin = rest.hasFocusWithin;
|
||||
|
||||
onMount(() => {
|
||||
rest.container._initializeContainer();
|
||||
rest.container._initializeSelectable();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import classNames from 'classnames';
|
||||
import { useNavigate } from 'svelte-navigator';
|
||||
import { get, type Readable } from 'svelte/store';
|
||||
import { type Readable } from 'svelte/store';
|
||||
import Container from '../../Container.svelte';
|
||||
import { Container as Cont } from '../../lib/actions/focusAction';
|
||||
|
||||
export let to: string;
|
||||
let hasFocus: Readable<boolean>;
|
||||
@@ -12,7 +11,6 @@
|
||||
|
||||
function handleClick() {
|
||||
navigate(to);
|
||||
get(Cont.focusedObject)?.giveFocus('right');
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,73 +1,73 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import { ChevronLeft, ChevronRight } from 'radix-icons-svelte';
|
||||
import classNames from 'classnames';
|
||||
import type { Registerer } from '../../actions/focusAction';
|
||||
|
||||
export let gradientFromColor = 'from-stone-950';
|
||||
export let heading = '';
|
||||
|
||||
let carousel: HTMLDivElement | undefined;
|
||||
let scrollX = 0;
|
||||
export let scrollClass = '';
|
||||
</script>
|
||||
|
||||
<div class={classNames('flex flex-col gap-4 group/carousel', $$restProps.class)}>
|
||||
<div class={'flex justify-between items-center gap-4 ' + scrollClass}>
|
||||
<slot name="title">
|
||||
<div class="font-semibold text-xl">{heading}</div>
|
||||
</slot>
|
||||
<div
|
||||
class={classNames(
|
||||
'flex gap-2 sm:opacity-0 transition-opacity sm:group-hover/carousel:opacity-100',
|
||||
{
|
||||
hidden: (carousel?.scrollWidth || 0) === (carousel?.clientWidth || 0)
|
||||
}
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
on:click={() => {
|
||||
carousel?.scrollTo({ left: scrollX - carousel?.clientWidth * 0.8, behavior: 'smooth' });
|
||||
}}
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
on:click={() => {
|
||||
carousel?.scrollTo({ left: scrollX + carousel?.clientWidth * 0.8, behavior: 'smooth' });
|
||||
}}
|
||||
>
|
||||
<ChevronRight size={20} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<div
|
||||
class={classNames(
|
||||
'flex overflow-x-scroll items-center overflow-y-visible gap-4 relative scrollbar-hide p-1',
|
||||
scrollClass
|
||||
)}
|
||||
bind:this={carousel}
|
||||
tabindex="-1"
|
||||
on:scroll={() => (scrollX = carousel?.scrollLeft || scrollX)}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
{#if scrollX > 50}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
class={'absolute inset-y-0 left-0 w-0 sm:w-16 md:w-24 bg-gradient-to-r ' +
|
||||
gradientFromColor}
|
||||
/>
|
||||
{/if}
|
||||
{#if carousel && scrollX < carousel?.scrollWidth - carousel?.clientWidth - 50}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
class={'absolute inset-y-0 right-0 w-0 sm:w-16 md:w-24 bg-gradient-to-l ' +
|
||||
gradientFromColor}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import { ChevronLeft, ChevronRight } from 'radix-icons-svelte';
|
||||
import classNames from 'classnames';
|
||||
import type { Registerer } from '../../selectable';
|
||||
|
||||
export let gradientFromColor = 'from-stone-950';
|
||||
export let heading = '';
|
||||
|
||||
let carousel: HTMLDivElement | undefined;
|
||||
let scrollX = 0;
|
||||
export let scrollClass = '';
|
||||
</script>
|
||||
|
||||
<div class={classNames('flex flex-col gap-4 group/carousel', $$restProps.class)}>
|
||||
<div class={'flex justify-between items-center gap-4 ' + scrollClass}>
|
||||
<slot name="title">
|
||||
<div class="font-semibold text-xl">{heading}</div>
|
||||
</slot>
|
||||
<div
|
||||
class={classNames(
|
||||
'flex gap-2 sm:opacity-0 transition-opacity sm:group-hover/carousel:opacity-100',
|
||||
{
|
||||
hidden: (carousel?.scrollWidth || 0) === (carousel?.clientWidth || 0)
|
||||
}
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
on:click={() => {
|
||||
carousel?.scrollTo({ left: scrollX - carousel?.clientWidth * 0.8, behavior: 'smooth' });
|
||||
}}
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
on:click={() => {
|
||||
carousel?.scrollTo({ left: scrollX + carousel?.clientWidth * 0.8, behavior: 'smooth' });
|
||||
}}
|
||||
>
|
||||
<ChevronRight size={20} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<div
|
||||
class={classNames(
|
||||
'flex overflow-x-scroll items-center overflow-y-visible gap-4 relative scrollbar-hide p-1',
|
||||
scrollClass
|
||||
)}
|
||||
bind:this={carousel}
|
||||
tabindex="-1"
|
||||
on:scroll={() => (scrollX = carousel?.scrollLeft || scrollX)}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
{#if scrollX > 50}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
class={'absolute inset-y-0 left-0 w-0 sm:w-16 md:w-24 bg-gradient-to-r ' +
|
||||
gradientFromColor}
|
||||
/>
|
||||
{/if}
|
||||
{#if carousel && scrollX < carousel?.scrollWidth - carousel?.clientWidth - 50}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
class={'absolute inset-y-0 right-0 w-0 sm:w-16 md:w-24 bg-gradient-to-l ' +
|
||||
gradientFromColor}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
<script lang="ts">
|
||||
import CardPlaceholder from '../Card/CardPlaceholder.svelte';
|
||||
import classNames from 'classnames';
|
||||
import Container from '../../../Container.svelte';
|
||||
import type { Readable } from 'svelte/store';
|
||||
export let size: 'dynamic' | 'md' | 'lg' = 'md';
|
||||
export let orientation: 'landscape' | 'portrait' = 'landscape';
|
||||
|
||||
let focusIndex: Readable<number>;
|
||||
let focusWithin: Readable<boolean>;
|
||||
</script>
|
||||
|
||||
<p
|
||||
class={classNames({
|
||||
'bg-blue-500': $focusWithin
|
||||
})}
|
||||
>
|
||||
Index: {$focusIndex}
|
||||
</p>
|
||||
|
||||
{#each Array(10) as _, i (i)}
|
||||
<Container
|
||||
bind:focusIndex
|
||||
bind:focusWithin
|
||||
class={classNames({
|
||||
'bg-red-500': $focusIndex === i && $focusWithin
|
||||
})}
|
||||
>
|
||||
<CardPlaceholder {size} index={i} {orientation} />
|
||||
</Container>
|
||||
{/each}
|
||||
<script lang="ts">
|
||||
import CardPlaceholder from '../Card/CardPlaceholder.svelte';
|
||||
import classNames from 'classnames';
|
||||
import Container from '../../../Container.svelte';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
export let size: 'dynamic' | 'md' | 'lg' = 'md';
|
||||
export let orientation: 'landscape' | 'portrait' = 'landscape';
|
||||
|
||||
export let focusIndex: Readable<number>;
|
||||
let focusWithin: Writable<boolean>;
|
||||
</script>
|
||||
|
||||
<p
|
||||
class={classNames({
|
||||
'bg-blue-500': $focusWithin
|
||||
})}
|
||||
>
|
||||
Index: {$focusIndex}
|
||||
</p>
|
||||
|
||||
{#each Array(10) as _, i (i)}
|
||||
<Container
|
||||
bind:focusWithin
|
||||
class={classNames({
|
||||
'bg-red-500': $focusIndex === i && $focusWithin
|
||||
})}
|
||||
>
|
||||
<CardPlaceholder {size} index={i} {orientation} />
|
||||
</Container>
|
||||
{/each}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '../actions/focusAction';
|
||||
|
||||
export let container: Container;
|
||||
const registerer = container.getRegisterer();
|
||||
|
||||
export let handleClick = () => {
|
||||
container.focus();
|
||||
};
|
||||
</script>
|
||||
|
||||
<button use:registerer on:click={handleClick} class="outline-none ring-0">
|
||||
<slot />
|
||||
</button>
|
||||
<script lang="ts">
|
||||
import { Selectable } from '../selectable';
|
||||
|
||||
export let container: Selectable;
|
||||
const registerer = container.getRegisterer();
|
||||
|
||||
export let handleClick = () => {
|
||||
container.focus();
|
||||
};
|
||||
</script>
|
||||
|
||||
<button use:registerer on:click={handleClick} class="outline-none ring-0">
|
||||
<slot />
|
||||
</button>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '../actions/focusAction';
|
||||
import Carousel from '../components/Carousel/Carousel.svelte';
|
||||
import CarouselPlaceholderItems from '../components/Carousel/CarouselPlaceholderItems.svelte';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export let container: Container;
|
||||
const focusWithin = container.hasFocusWithin;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={classNames('flex flex-col', {
|
||||
'bg-green-100': $focusWithin
|
||||
})}
|
||||
>
|
||||
<Carousel>
|
||||
<CarouselPlaceholderItems {container} />
|
||||
</Carousel>
|
||||
<Carousel>
|
||||
<CarouselPlaceholderItems {container} />
|
||||
</Carousel>
|
||||
</div>
|
||||
<script lang="ts">
|
||||
import { Selectable } from '../selectable';
|
||||
import Carousel from '../components/Carousel/Carousel.svelte';
|
||||
import CarouselPlaceholderItems from '../components/Carousel/CarouselPlaceholderItems.svelte';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export let container: Selectable;
|
||||
const focusWithin = container.hasFocusWithin;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={classNames('flex flex-col', {
|
||||
'bg-green-100': $focusWithin
|
||||
})}
|
||||
>
|
||||
<Carousel>
|
||||
<CarouselPlaceholderItems {container} />
|
||||
</Carousel>
|
||||
<Carousel>
|
||||
<CarouselPlaceholderItems {container} />
|
||||
</Carousel>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Carousel from '../components/Carousel/Carousel.svelte';
|
||||
import CarouselPlaceholderItems from '../components/Carousel/CarouselPlaceholderItems.svelte';
|
||||
import Container from '../../Container.svelte';
|
||||
import type { Readable } from 'svelte/store';
|
||||
|
||||
settings.update((prev) => ({
|
||||
...prev,
|
||||
@@ -19,13 +20,15 @@
|
||||
jellyfinItemsStore.subscribe((items) => {
|
||||
console.warn('GOT ITEMS', items.data);
|
||||
});
|
||||
|
||||
let focusIndex: Readable<number>;
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<Container focusOnMount>
|
||||
<div>LibraryPage</div>
|
||||
<Container horizontal>
|
||||
<Container horizontal bind:focusIndex>
|
||||
<Carousel>
|
||||
<CarouselPlaceholderItems />
|
||||
<CarouselPlaceholderItems {focusIndex} />
|
||||
</Carousel>
|
||||
</Container>
|
||||
</Container>
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
import Container from '../../Container.svelte';
|
||||
</script>
|
||||
|
||||
<Container>MoviesPage</Container>
|
||||
<Container focusOnMount>MoviesPage</Container>
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
import Container from '../../Container.svelte';
|
||||
</script>
|
||||
|
||||
<Container>SearchPage</Container>
|
||||
<Container focusOnMount>SearchPage</Container>
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
import Container from '../../Container.svelte';
|
||||
</script>
|
||||
|
||||
<Container>SeriesPage</Container>
|
||||
<Container focusOnMount>SeriesPage</Container>
|
||||
|
||||
@@ -5,13 +5,13 @@ export type Registerer = (htmlElement: HTMLElement) => { destroy: () => void };
|
||||
export type Direction = 'up' | 'down' | 'left' | 'right';
|
||||
export type FlowDirection = 'vertical' | 'horizontal';
|
||||
|
||||
export class Container {
|
||||
export class Selectable {
|
||||
id: symbol;
|
||||
name: string;
|
||||
private parent?: Container;
|
||||
private children: Container[] = [];
|
||||
private parent?: Selectable;
|
||||
private children: Selectable[] = [];
|
||||
private htmlElement?: HTMLElement;
|
||||
private neighbors: Record<Direction, Container | undefined> = {
|
||||
private neighbors: Record<Direction, Selectable | undefined> = {
|
||||
up: undefined,
|
||||
down: undefined,
|
||||
left: undefined,
|
||||
@@ -22,27 +22,26 @@ export class Container {
|
||||
|
||||
private direction: FlowDirection = 'vertical';
|
||||
|
||||
static focusedObject: Writable<Container | undefined> = writable(undefined);
|
||||
static focusedObject: Writable<Selectable | undefined> = writable(undefined);
|
||||
|
||||
focusIndex: Writable<number> = writable(0);
|
||||
hasFocus: Readable<boolean> = derived(Container.focusedObject, ($focusedObject) => {
|
||||
console.log('Updating hasFocus', $focusedObject, this);
|
||||
hasFocus: Readable<boolean> = derived(Selectable.focusedObject, ($focusedObject) => {
|
||||
return $focusedObject === this;
|
||||
});
|
||||
hasFocusWithin: Readable<boolean> = derived(Container.focusedObject, ($focusedObject) => {
|
||||
let currentContainer: Container | undefined = $focusedObject;
|
||||
hasFocusWithin: Readable<boolean> = derived(Selectable.focusedObject, ($focusedObject) => {
|
||||
let currentSelectable: Selectable | undefined = $focusedObject;
|
||||
|
||||
while (currentContainer) {
|
||||
if (currentContainer === this) {
|
||||
while (currentSelectable) {
|
||||
if (currentSelectable === this) {
|
||||
return true;
|
||||
}
|
||||
currentContainer = currentContainer.parent;
|
||||
currentSelectable = currentSelectable.parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
static objects = new Map<HTMLElement, Container>();
|
||||
static objects = new Map<HTMLElement, Selectable>();
|
||||
|
||||
constructor(name: string = '') {
|
||||
this.id = Symbol();
|
||||
@@ -56,37 +55,26 @@ export class Container {
|
||||
return this;
|
||||
}
|
||||
|
||||
setFocusByDefault(focusByDefault: boolean) {
|
||||
this.focusByDefault = focusByDefault;
|
||||
return this;
|
||||
}
|
||||
|
||||
setHtmlElement(htmlElement: HTMLElement) {
|
||||
this.htmlElement = htmlElement;
|
||||
Container.objects.set(htmlElement, this);
|
||||
Selectable.objects.set(htmlElement, this);
|
||||
return this;
|
||||
}
|
||||
|
||||
// createChild(htmlElement: HTMLElement, name: string = '') {
|
||||
// const child = new Container(htmlElement, name);
|
||||
// this.addChild(child);
|
||||
// return child;
|
||||
// }
|
||||
|
||||
focus() {
|
||||
if (this.children.length > 0) {
|
||||
this.children[get(this.focusIndex)]?.focus();
|
||||
} else if (this.htmlElement) {
|
||||
this.htmlElement.focus({ preventScroll: true });
|
||||
this.htmlElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
|
||||
Container.focusedObject.set(this);
|
||||
Selectable.focusedObject.set(this);
|
||||
this.updateFocusIndex();
|
||||
}
|
||||
}
|
||||
|
||||
updateFocusIndex(container?: Container) {
|
||||
if (container) {
|
||||
const index = this.children.indexOf(container);
|
||||
updateFocusIndex(selectable?: Selectable) {
|
||||
if (selectable) {
|
||||
const index = this.children.indexOf(selectable);
|
||||
this.focusIndex.update((prev) => (index === -1 ? prev : index));
|
||||
}
|
||||
if (this.parent) {
|
||||
@@ -106,7 +94,7 @@ export class Container {
|
||||
}
|
||||
}
|
||||
|
||||
getFocusableNeighbor(direction: Direction): Container | undefined {
|
||||
getFocusableNeighbor(direction: Direction): Selectable | undefined {
|
||||
const canLoop =
|
||||
(this.direction === 'vertical' &&
|
||||
((direction === 'up' && get(this.focusIndex) !== 0) ||
|
||||
@@ -114,6 +102,7 @@ export class Container {
|
||||
(this.direction === 'horizontal' &&
|
||||
((direction === 'left' && get(this.focusIndex) !== 0) ||
|
||||
(direction === 'right' && get(this.focusIndex) !== this.children.length - 1)));
|
||||
|
||||
if (this.children.length > 0 && canLoop) {
|
||||
if (direction === 'up' || direction === 'left') {
|
||||
let index = get(this.focusIndex) - 1;
|
||||
@@ -137,11 +126,12 @@ export class Container {
|
||||
} else {
|
||||
return this.parent?.getFocusableNeighbor(direction);
|
||||
}
|
||||
|
||||
console.warn('How did we end up here');
|
||||
}
|
||||
|
||||
giveFocus(direction: Direction) {
|
||||
private giveFocus(direction: Direction) {
|
||||
const neighbor = this.getFocusableNeighbor(direction);
|
||||
// console.log('Giving focus to', direction, 'neighbor: ', neighbor?.name, neighbor);
|
||||
if (neighbor) {
|
||||
neighbor.focus();
|
||||
return true;
|
||||
@@ -150,45 +140,30 @@ export class Container {
|
||||
}
|
||||
}
|
||||
|
||||
// getChildRegisterer(): Registerer {
|
||||
// return (htmlElement: HTMLElement) => {
|
||||
// if (this.htmlElement) console.warn('Registering to a container that has an element.');
|
||||
//
|
||||
// this.createChild().addHtmlElement(htmlElement);
|
||||
//
|
||||
// if (!get(Container.focusedObject) && this.shouldFocusByDefault()) {
|
||||
// this.focus();
|
||||
// }
|
||||
//
|
||||
// return {
|
||||
// destroy: () => {
|
||||
// this.removeHtmlElement();
|
||||
// }
|
||||
// };
|
||||
// };
|
||||
// }
|
||||
//
|
||||
// getHtmlElementRegisterer(): Registerer {
|
||||
// return (htmlElement: HTMLElement) => {
|
||||
// if (this.children.length > 0) {
|
||||
// console.warn('Registering an html element to a container that has children.');
|
||||
// for (const child of this.children) {
|
||||
// this.removeChild(child);
|
||||
// }
|
||||
// }
|
||||
// this.addHtmlElement(htmlElement);
|
||||
// return {
|
||||
// destroy: () => {
|
||||
// this.removeHtmlElement();
|
||||
// }
|
||||
// };
|
||||
// };
|
||||
// }
|
||||
static focusUp() {
|
||||
const currentlyFocusedObject = get(Selectable.focusedObject);
|
||||
return currentlyFocusedObject?.giveFocus('up');
|
||||
}
|
||||
|
||||
_initializeContainer() {
|
||||
const getParentContainer = (htmlElement: HTMLElement): Container | undefined => {
|
||||
if (Container.objects.get(htmlElement)) return Container.objects.get(htmlElement);
|
||||
else if (htmlElement.parentElement) return getParentContainer(htmlElement.parentElement);
|
||||
static focusDown() {
|
||||
const currentlyFocusedObject = get(Selectable.focusedObject);
|
||||
return currentlyFocusedObject?.giveFocus('down');
|
||||
}
|
||||
|
||||
static focusLeft() {
|
||||
const currentlyFocusedObject = get(Selectable.focusedObject);
|
||||
return currentlyFocusedObject?.giveFocus('left');
|
||||
}
|
||||
|
||||
static focusRight() {
|
||||
const currentlyFocusedObject = get(Selectable.focusedObject);
|
||||
return currentlyFocusedObject?.giveFocus('right');
|
||||
}
|
||||
|
||||
_initializeSelectable() {
|
||||
const getParentSelectable = (htmlElement: HTMLElement): Selectable | undefined => {
|
||||
if (Selectable.objects.get(htmlElement)) return Selectable.objects.get(htmlElement);
|
||||
else if (htmlElement.parentElement) return getParentSelectable(htmlElement.parentElement);
|
||||
else return undefined;
|
||||
};
|
||||
|
||||
@@ -196,29 +171,31 @@ export class Container {
|
||||
console.error('No html element found for', this);
|
||||
return;
|
||||
} else if (this.isInitialized) {
|
||||
console.warn('Container already initialized', this);
|
||||
console.warn('Selectable already initialized', this);
|
||||
}
|
||||
|
||||
const parentContainer = this.htmlElement.parentElement
|
||||
? getParentContainer(this.htmlElement.parentElement)
|
||||
console.log('Initializing', this.htmlElement);
|
||||
|
||||
const parentSelectable = this.htmlElement.parentElement
|
||||
? getParentSelectable(this.htmlElement.parentElement)
|
||||
: undefined;
|
||||
if (parentContainer) {
|
||||
parentContainer.addChild(this);
|
||||
if (parentSelectable) {
|
||||
parentSelectable.addChild(this);
|
||||
} else {
|
||||
console.error('No parent container found for', this.htmlElement);
|
||||
console.error('No parent selectable found for', this.htmlElement);
|
||||
}
|
||||
|
||||
if (!get(Container.focusedObject) && this.shouldFocusByDefault()) {
|
||||
if (!get(Selectable.focusedObject) && this.shouldFocusByDefault()) {
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
_unmountContainer() {
|
||||
console.log('Unmounting container', this);
|
||||
console.log('Unmounting selectable', this);
|
||||
const isFocusedWithin = get(this.hasFocusWithin);
|
||||
|
||||
if (this.htmlElement) {
|
||||
Container.objects.delete(this.htmlElement);
|
||||
Selectable.objects.delete(this.htmlElement);
|
||||
}
|
||||
|
||||
const parent = this.parent;
|
||||
@@ -231,19 +208,19 @@ export class Container {
|
||||
}
|
||||
|
||||
private static createRegisterer(
|
||||
_container?: Container,
|
||||
_selectable?: Selectable,
|
||||
flowDirection: FlowDirection = 'vertical'
|
||||
): Registerer {
|
||||
const container = _container || new Container().setDirection(flowDirection);
|
||||
const selectable = _selectable || new Selectable().setDirection(flowDirection);
|
||||
|
||||
return (htmlElement: HTMLElement) => {
|
||||
console.log('Registering', htmlElement, container);
|
||||
container.setHtmlElement(htmlElement);
|
||||
console.log('Registering', htmlElement, selectable);
|
||||
selectable.setHtmlElement(htmlElement);
|
||||
|
||||
return {
|
||||
destroy: () => {
|
||||
container.parent?.removeChild(container);
|
||||
Container.objects.delete(htmlElement);
|
||||
selectable.parent?.removeChild(selectable);
|
||||
Selectable.objects.delete(htmlElement);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -255,62 +232,57 @@ export class Container {
|
||||
}
|
||||
|
||||
getRegisterer(): Registerer {
|
||||
return (htmlElement: HTMLElement) => Container.createRegisterer(this)(htmlElement);
|
||||
return (htmlElement: HTMLElement) => Selectable.createRegisterer(this)(htmlElement);
|
||||
}
|
||||
|
||||
static getStores(element: HTMLElement) {
|
||||
return Container.objects.get(element)?.getStores();
|
||||
return Selectable.objects.get(element)?.getStores();
|
||||
}
|
||||
|
||||
getStores(): {
|
||||
container: Container;
|
||||
container: Selectable;
|
||||
hasFocus: Readable<boolean>;
|
||||
hasFocusWithin: Readable<boolean>;
|
||||
registerer: Registerer;
|
||||
focusIndex: Writable<number>;
|
||||
} {
|
||||
return {
|
||||
container: this,
|
||||
hasFocus: this.hasFocus,
|
||||
hasFocusWithin: this.hasFocusWithin,
|
||||
registerer: this.getRegisterer()
|
||||
registerer: this.getRegisterer(),
|
||||
focusIndex: this.focusIndex
|
||||
};
|
||||
}
|
||||
|
||||
private addChild(child: Container) {
|
||||
private addChild(child: Selectable) {
|
||||
this.children.push(child);
|
||||
child.parent = this;
|
||||
return this;
|
||||
}
|
||||
|
||||
private removeChild(child: Container) {
|
||||
private removeChild(child: Selectable) {
|
||||
if (this.children.indexOf(child) < get(this.focusIndex)) {
|
||||
this.focusIndex.update((prev) => prev - 1);
|
||||
}
|
||||
|
||||
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;
|
||||
// }
|
||||
|
||||
private shouldFocusByDefault(): boolean {
|
||||
return this.focusByDefault || this.parent?.shouldFocusByDefault() || false;
|
||||
}
|
||||
}
|
||||
|
||||
export function handleKeyboardNavigation(event: KeyboardEvent) {
|
||||
const currentlyFocusedObject = get(Container.focusedObject);
|
||||
const currentlyFocusedObject = get(Selectable.focusedObject);
|
||||
|
||||
if (!currentlyFocusedObject) {
|
||||
console.error('No focused object!!!');
|
||||
// Find object that can be focused
|
||||
Container.objects.forEach((container) => {
|
||||
Selectable.objects.forEach((container) => {
|
||||
if (container.isFocusable()) {
|
||||
container.focus();
|
||||
}
|
||||
@@ -321,19 +293,12 @@ export function handleKeyboardNavigation(event: KeyboardEvent) {
|
||||
// console.log('Currently focused object: ', currentlyFocusedObject.name, currentlyFocusedObject);
|
||||
|
||||
if (event.key === 'ArrowUp') {
|
||||
if (currentlyFocusedObject.giveFocus('up')) event.preventDefault();
|
||||
if (Selectable.focusUp()) event.preventDefault();
|
||||
} else if (event.key === 'ArrowDown') {
|
||||
if (currentlyFocusedObject.giveFocus('down')) event.preventDefault();
|
||||
if (Selectable.focusDown()) event.preventDefault();
|
||||
} else if (event.key === 'ArrowLeft') {
|
||||
if (currentlyFocusedObject.giveFocus('left')) event.preventDefault();
|
||||
if (Selectable.focusLeft()) event.preventDefault();
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
if (currentlyFocusedObject.giveFocus('right')) event.preventDefault();
|
||||
if (Selectable.focusRight()) event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
export const focusedObject = Container.focusedObject;
|
||||
// export const mainContainer = new Container('main')
|
||||
// .setDirection('horizontal')
|
||||
// .setFocusByDefault(true);
|
||||
|
||||
export const registerer = Container.getRegisterer();
|
||||
Reference in New Issue
Block a user