diff --git a/src/Container.svelte b/src/Container.svelte index 5eed54b..2346e96 100644 --- a/src/Container.svelte +++ b/src/Container.svelte @@ -69,12 +69,7 @@ } onMount(() => { - rest.container._initializeSelectable(); - - if (focusOnMount) { - console.log('focusing', rest.container.getHtmlElement()); - rest.container.focus(); - } + rest.container._mountSelectable(focusOnMount); return () => { rest.container._unmountContainer(); diff --git a/src/FocusableButton.svelte b/src/FocusableButton.svelte index 69141b7..c8dd7d7 100644 --- a/src/FocusableButton.svelte +++ b/src/FocusableButton.svelte @@ -11,7 +11,7 @@ export const hasFocusWithin = rest.hasFocusWithin; onMount(() => { - rest.container._initializeSelectable(); + rest.container._mountSelectable(); }); diff --git a/src/lib/apis/combined-types.d.ts b/src/lib/apis/combined-types.d.ts index 2131498..a0214ea 100644 --- a/src/lib/apis/combined-types.d.ts +++ b/src/lib/apis/combined-types.d.ts @@ -1,6 +1,6 @@ import type { MovieDownload, MovieFileResource, RadarrRelease } from './radarr/radarr-api'; import type { EpisodeFileResource, EpisodeDownload, SonarrRelease } from './sonarr/sonarr-api'; -export type Release = RadarrRelease | SonarrRelease; -export type FileResource = MovieFileResource | EpisodeFileResource; -export type Download = MovieDownload | EpisodeDownload; +export type Release = RadarrRelease & SonarrRelease; +export type FileResource = MovieFileResource & EpisodeFileResource; +export type Download = MovieDownload & EpisodeDownload; diff --git a/src/lib/components/ManageMedia/DownloadsList.svelte b/src/lib/components/ManageMedia/DownloadsList.svelte index 2fc0f64..2026b2c 100644 --- a/src/lib/components/ManageMedia/DownloadsList.svelte +++ b/src/lib/components/ManageMedia/DownloadsList.svelte @@ -19,7 +19,7 @@ {/each} {:then downloads} {#each downloads as download, index} - diff --git a/src/lib/components/ManageMedia/ManageMediaMenuLayout.svelte b/src/lib/components/ManageMedia/ManageMediaMenuLayout.svelte index f53fc7f..d622d4b 100644 --- a/src/lib/components/ManageMedia/ManageMediaMenuLayout.svelte +++ b/src/lib/components/ManageMedia/ManageMediaMenuLayout.svelte @@ -1,8 +1,10 @@ - +

Header is missing

diff --git a/src/lib/components/ManageMedia/RadarrMediaMangerModal.svelte b/src/lib/components/ManageMedia/RadarrMediaMangerModal.svelte index 24086f7..041b38f 100644 --- a/src/lib/components/ManageMedia/RadarrMediaMangerModal.svelte +++ b/src/lib/components/ManageMedia/RadarrMediaMangerModal.svelte @@ -15,6 +15,8 @@ import { derived, type Readable } from 'svelte/store'; import ReleaseActionsModal from './Releases/ReleaseActionsModal.svelte'; import type { SonarrRelease } from '../../apis/sonarr/sonarr-api'; + import Button from '../Button.svelte'; + import type { FileResource } from '../../apis/combined-types'; export let modalId: symbol; export let hidden: boolean; @@ -63,7 +65,7 @@ ); } - function handleSelectFile(file: MovieFileResource) { + function handleSelectFile(file: FileResource) { modalStack.create( FileActionsModal, { @@ -76,7 +78,7 @@ - +

Download

radarrApi.getReleases(id)} diff --git a/src/lib/components/ManageMedia/Releases/ReleaseActionsModal.svelte b/src/lib/components/ManageMedia/Releases/ReleaseActionsModal.svelte index 404536c..3815139 100644 --- a/src/lib/components/ManageMedia/Releases/ReleaseActionsModal.svelte +++ b/src/lib/components/ManageMedia/Releases/ReleaseActionsModal.svelte @@ -34,7 +34,7 @@
+
{:else if showAll}
- +
{/if} {/if} diff --git a/src/lib/components/ManageMedia/SonarrMediaMangerModal.svelte b/src/lib/components/ManageMedia/SonarrMediaMangerModal.svelte index cca6f51..b3836cb 100644 --- a/src/lib/components/ManageMedia/SonarrMediaMangerModal.svelte +++ b/src/lib/components/ManageMedia/SonarrMediaMangerModal.svelte @@ -89,7 +89,7 @@ - +

Download

diff --git a/src/lib/components/SeriesPage/SeriesPage.svelte b/src/lib/components/SeriesPage/SeriesPage.svelte index caae3c4..0f22b1c 100644 --- a/src/lib/components/SeriesPage/SeriesPage.svelte +++ b/src/lib/components/SeriesPage/SeriesPage.svelte @@ -59,7 +59,7 @@ class="h-screen flex flex-col py-12 px-20 relative" on:enter={scrollIntoView({ top: 0 })} handleNavigateOut={{ - down: () => episodesSelectable?.focusChildren(1) + down: () => episodesSelectable?.focusChild(1) }} > = writable(undefined); focusIndex: Writable = writable(0); @@ -144,13 +145,13 @@ export class Selectable { if (_options.setFocusedElement) { this.htmlElement.focus({ preventScroll: true }); - console.log('Setting focused element to', this.htmlElement); Selectable.focusedObject.set(this); } } } - focusChildren(index: number, options?: Partial): boolean { + focusChild(index: number, options?: Partial): boolean { + // TODO: CLEAN UP const child = this.children[index]; if (child && child.isFocusable()) { child.focus(options); @@ -163,10 +164,10 @@ export class Selectable { /** * @returns {boolean} whether the selectable is focusable */ - isFocusable(): boolean { + isFocusable(canFocusEmpty = this.canFocusEmpty): boolean { + // TODO: CLEAN UP if (!this.isActive) return false; - - if (this.htmlElement && this.canFocusEmpty) { + if (this.htmlElement && canFocusEmpty) { return this.htmlElement.tabIndex >= 0; } else { for (const child of this.children) { @@ -232,22 +233,44 @@ export class Selectable { return currentlyFocusedObject?.giveFocus(direction, bypassActions); } + private static initializeTreeStructure() { + for (let i = 0; i < Selectable._initalizationStack.length; i++) { + const selectable = Selectable._initalizationStack[i]; + const htmlElement = selectable?.getHtmlElement(); + + const previousSelectable = Selectable._initalizationStack[i - 1]; + const previousHtmlElement = previousSelectable?.getHtmlElement(); + + const isParent = + htmlElement && previousHtmlElement && htmlElement.contains(previousHtmlElement); + if (isParent && selectable && previousSelectable && htmlElement && previousHtmlElement) { + // Add all previous elements as children + for (let j = i - 1; j >= 0; j--) { + const potentialChild = Selectable._initalizationStack[j]; + if (potentialChild && htmlElement.contains(potentialChild.htmlElement || null)) { + selectable.addChild(potentialChild, 0); + Selectable._initalizationStack.splice(j, 1); + i = j; + } else break; + } + } + } + } + /** - * This runs after the regsterer has been called and the thmlElement - * has been set. Becasue all the children get initialized before their parents, - * we can't create the parent-child tree structure in the registerer but instead - * have to wait until every element has htmlElement and then later (here) deduce - * the parent-child relationships. + * TODO: Add docs */ - _initializeSelectable() { - console.debug('Initializing', this); + private static finalizeTreeStructure() { 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; }; - const getSiblingSelectable = (parent: Selectable): Selectable | undefined => { + const getSiblingSelectable = ( + parent: Selectable, + child: Selectable + ): Selectable | undefined => { const getElementTree = (start: HTMLElement, end: HTMLElement): HTMLElement[] => { let element = start; const elements: HTMLElement[] = [start]; @@ -261,13 +284,14 @@ export class Selectable { return elements; }; - if (!this.htmlElement) return undefined; + const htmlElement = child.htmlElement; + if (!htmlElement) return undefined; const parentHtmlElement = parent.htmlElement; if (!parentHtmlElement) return undefined; - const thisElementTree = getElementTree(this.htmlElement, parentHtmlElement); + const thisElementTree = getElementTree(htmlElement, parentHtmlElement); let aboveSibling: Selectable | undefined = undefined; @@ -310,37 +334,45 @@ export class Selectable { return aboveSibling; }; + for (const child of this._initalizationStack) { + const htmlElement = child.htmlElement; + const parentSelectable = htmlElement?.parentElement + ? getParentSelectable(htmlElement.parentElement) + : undefined; + + if (parentSelectable) { + const aboveSibling = getSiblingSelectable(parentSelectable, child); + const index = aboveSibling ? parentSelectable.children.indexOf(aboveSibling) : undefined; + parentSelectable.addChild(child, index === undefined ? 0 : index + 1); + console.debug('Attached child tree to parent', child, parentSelectable); + } else { + console.warn('Could not attach child (probably root)', child); + child.focus(); + } + } + + Selectable._initalizationStack = []; + } + + /** TODO update docs + * This runs after the regsterer has been called and the htmlElement + * has been set. Becasue all the children get initialized before their parents, + * we can't create the parent-child tree structure in the registerer but instead + * have to wait until every element has htmlElement and then later (here) deduce + * the parent-child relationships. + */ + _mountSelectable(focusOnMount: boolean = false) { + console.debug('Mounting', this, Selectable._initalizationStack.slice()); + + Selectable.finalizeTreeStructure(); + + if (!get(this.hasFocusWithin) && this.isFocusable(true) && focusOnMount) { + this.focus(); // TODO: CLEAN UP + } + if (!this.htmlElement) { console.error('No html element found for', this); return; - } else if (this.isInitialized) { - console.warn('Selectable already initialized', this); - } - - // console.log('Initializing', this.htmlElement); - - const parentSelectable = this.htmlElement.parentElement - ? getParentSelectable(this.htmlElement.parentElement) - : undefined; - if (parentSelectable) { - const aboveSibling = getSiblingSelectable(parentSelectable); - const index = aboveSibling ? parentSelectable.children.indexOf(aboveSibling) : undefined; - parentSelectable.addChild(this, index === undefined ? 0 : index + 1); - } else { - console.error('No parent selectable found for', this.htmlElement); - } - - if (get(Selectable.focusedObject) === parentSelectable && this.isFocusable()) { - console.log('Focusing on add'); - this.focus(); - } else { - console.log( - 'Not focusing on add', - this, - this.isFocusable(), - get(Selectable.focusedObject), - parentSelectable - ); } } @@ -361,7 +393,7 @@ export class Selectable { } /** - * This only sets the htmlElement. See {@link _initializeSelectable} for the rest of the initialization. + * This only sets the htmlElement. See {@link _mountSelectable} for the rest of the initialization. */ private static createRegisterer( _selectable?: Selectable, @@ -372,6 +404,8 @@ export class Selectable { return (htmlElement: HTMLElement) => { selectable.setHtmlElement(htmlElement); console.debug('Registering', selectable); + Selectable._initalizationStack.push(selectable); + Selectable.initializeTreeStructure(); return { destroy: () => { @@ -411,10 +445,19 @@ export class Selectable { }; } + /** + * TODO: Adding children to focusIndex does not modify focusIndex. + */ private addChild(child: Selectable, index?: number) { + if (child === this) { + console.error('TRYING TO ADD SELF AS A CHILD', this); + return; + } + + const firstChild = this.children.length === 0; + if (index !== undefined) { - const parentFocusWithin = child.parent?.hasFocusWithin && get(child.parent?.hasFocusWithin); - if (parentFocusWithin && this.children.length && index <= get(this.focusIndex)) { + if (this.children.length && index < get(this.focusIndex)) { this.focusIndex.update((prev) => prev + 1); } this.children.splice(index, 0, child); @@ -423,6 +466,31 @@ export class Selectable { } child.parent = this; + + // TODO: CLEAN UP + if (index === get(this.focusIndex) && get(this.hasFocusWithin)) { + child.focus(); + } + + // 1. If parent has focus but also has child(ren), focus the child instead + // 2. If adding to container that doesn't have focus because being empty + // prevented receiving it, check if 1. applies to the parent + + // eslint-disable-next-line @typescript-eslint/no-this-alias + let el: Selectable = this; + while (firstChild) { + if (get(el.hasFocus) && el.children.length) { + el.focus(); + break; + } + + if (!el.canFocusEmpty && el.parent) { + el = el.parent; + } else { + break; + } + } + return this; }