diff options
| author | 2023-12-15 01:24:12 +0000 | |
|---|---|---|
| committer | 2023-12-15 01:24:12 +0000 | |
| commit | d1052ddfbe2431eb5d6c1d41301cdf2ad049b6de (patch) | |
| tree | fe0d8d0136127dcafa1e3dc3dd4cf4a31725a22e /dev/src/templates/assets/javascripts/components | |
| parent | c15103048d22c8e3171c8965b8cf15ca99494086 (diff) | |
| download | infini-d1052ddfbe2431eb5d6c1d41301cdf2ad049b6de.tar.gz infini-d1052ddfbe2431eb5d6c1d41301cdf2ad049b6de.zip | |
Deployed daa378d6 to dev with MkDocs 1.5.3 and mike 2.0.0
Diffstat (limited to 'dev/src/templates/assets/javascripts/components')
41 files changed, 0 insertions, 5784 deletions
diff --git a/dev/src/templates/assets/javascripts/components/_/index.ts b/dev/src/templates/assets/javascripts/components/_/index.ts deleted file mode 100644 index 61c471d9..00000000 --- a/dev/src/templates/assets/javascripts/components/_/index.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { getElement, getElements } from "~/browser" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Component type - */ -export type ComponentType = - | "announce" /* Announcement bar */ - | "container" /* Container */ - | "consent" /* Consent */ - | "content" /* Content */ - | "dialog" /* Dialog */ - | "header" /* Header */ - | "header-title" /* Header title */ - | "header-topic" /* Header topic */ - | "main" /* Main area */ - | "outdated" /* Version warning */ - | "palette" /* Color palette */ - | "progress" /* Progress indicator */ - | "search" /* Search */ - | "search-query" /* Search input */ - | "search-result" /* Search results */ - | "search-share" /* Search sharing */ - | "search-suggest" /* Search suggestions */ - | "sidebar" /* Sidebar */ - | "skip" /* Skip link */ - | "source" /* Repository information */ - | "tabs" /* Navigation tabs */ - | "toc" /* Table of contents */ - | "top" /* Back-to-top button */ - -/** - * Component - * - * @template T - Component type - * @template U - Reference type - */ -export type Component< - T extends {} = {}, - U extends HTMLElement = HTMLElement -> = - T & { - ref: U /* Component reference */ - } - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Component type map - */ -interface ComponentTypeMap { - "announce": HTMLElement /* Announcement bar */ - "container": HTMLElement /* Container */ - "consent": HTMLElement /* Consent */ - "content": HTMLElement /* Content */ - "dialog": HTMLElement /* Dialog */ - "header": HTMLElement /* Header */ - "header-title": HTMLElement /* Header title */ - "header-topic": HTMLElement /* Header topic */ - "main": HTMLElement /* Main area */ - "outdated": HTMLElement /* Version warning */ - "palette": HTMLElement /* Color palette */ - "progress": HTMLElement /* Progress indicator */ - "search": HTMLElement /* Search */ - "search-query": HTMLInputElement /* Search input */ - "search-result": HTMLElement /* Search results */ - "search-share": HTMLAnchorElement /* Search sharing */ - "search-suggest": HTMLElement /* Search suggestions */ - "sidebar": HTMLElement /* Sidebar */ - "skip": HTMLAnchorElement /* Skip link */ - "source": HTMLAnchorElement /* Repository information */ - "tabs": HTMLElement /* Navigation tabs */ - "toc": HTMLElement /* Table of contents */ - "top": HTMLAnchorElement /* Back-to-top button */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve the element for a given component or throw a reference error - * - * @template T - Component type - * - * @param type - Component type - * @param node - Node of reference - * - * @returns Element - */ -export function getComponentElement<T extends ComponentType>( - type: T, node: ParentNode = document -): ComponentTypeMap[T] { - return getElement(`[data-md-component=${type}]`, node) -} - -/** - * Retrieve all elements for a given component - * - * @template T - Component type - * - * @param type - Component type - * @param node - Node of reference - * - * @returns Elements - */ -export function getComponentElements<T extends ComponentType>( - type: T, node: ParentNode = document -): ComponentTypeMap[T][] { - return getElements(`[data-md-component=${type}]`, node) -} diff --git a/dev/src/templates/assets/javascripts/components/announce/index.ts b/dev/src/templates/assets/javascripts/components/announce/index.ts deleted file mode 100644 index dd04b4ff..00000000 --- a/dev/src/templates/assets/javascripts/components/announce/index.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - EMPTY, - Observable, - Subject, - defer, - finalize, - fromEvent, - map, - tap -} from "rxjs" - -import { feature } from "~/_" -import { getElement } from "~/browser" - -import { Component } from "../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Announcement bar - */ -export interface Announce { - hash: number /* Content hash */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch announcement bar - * - * @param el - Announcement bar element - * - * @returns Announcement bar observable - */ -export function watchAnnounce( - el: HTMLElement -): Observable<Announce> { - const button = getElement(".md-typeset > :first-child", el) - return fromEvent(button, "click", { once: true }) - .pipe( - map(() => getElement(".md-typeset", el)), - map(content => ({ hash: __md_hash(content.innerHTML) })) - ) -} - -/** - * Mount announcement bar - * - * @param el - Announcement bar element - * - * @returns Announcement bar component observable - */ -export function mountAnnounce( - el: HTMLElement -): Observable<Component<Announce>> { - if (!feature("announce.dismiss") || !el.childElementCount) - return EMPTY - - /* Support instant navigation - see https://t.ly/3FTme */ - if (!el.hidden) { - const content = getElement(".md-typeset", el) - if (__md_hash(content.innerHTML) === __md_get("__announce")) - el.hidden = true - } - - /* Mount component on subscription */ - return defer(() => { - const push$ = new Subject<Announce>() - push$.subscribe(({ hash }) => { - el.hidden = true - - /* Persist preference in local storage */ - __md_set<number>("__announce", hash) - }) - - /* Create and return component */ - return watchAnnounce(el) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/dev/src/templates/assets/javascripts/components/consent/index.ts b/dev/src/templates/assets/javascripts/components/consent/index.ts deleted file mode 100644 index bc99db58..00000000 --- a/dev/src/templates/assets/javascripts/components/consent/index.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - finalize, - map, - tap -} from "rxjs" - -import { Component } from "../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Consent - */ -export interface Consent { - hidden: boolean /* Consent is hidden */ -} - -/** - * Consent defaults - */ -export interface ConsentDefaults { - analytics?: boolean /* Consent for Analytics */ - github?: boolean /* Consent for GitHub */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - target$: Observable<HTMLElement> /* Target observable */ -} - -/** - * Mount options - */ -interface MountOptions { - target$: Observable<HTMLElement> /* Target observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch consent - * - * @param el - Consent element - * @param options - Options - * - * @returns Consent observable - */ -export function watchConsent( - el: HTMLElement, { target$ }: WatchOptions -): Observable<Consent> { - return target$ - .pipe( - map(target => ({ hidden: target !== el })) - ) -} - -/* ------------------------------------------------------------------------- */ - -/** - * Mount consent - * - * @param el - Consent element - * @param options - Options - * - * @returns Consent component observable - */ -export function mountConsent( - el: HTMLElement, options: MountOptions -): Observable<Component<Consent>> { - const internal$ = new Subject<Consent>() - internal$.subscribe(({ hidden }) => { - el.hidden = hidden - }) - - /* Create and return component */ - return watchConsent(el, options) - .pipe( - tap(state => internal$.next(state)), - finalize(() => internal$.complete()), - map(state => ({ ref: el, ...state })) - ) -} diff --git a/dev/src/templates/assets/javascripts/components/content/_/index.ts b/dev/src/templates/assets/javascripts/components/content/_/index.ts deleted file mode 100644 index 899a695c..00000000 --- a/dev/src/templates/assets/javascripts/components/content/_/index.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { Observable, merge } from "rxjs" - -import { Viewport, getElements } from "~/browser" - -import { Component } from "../../_" -import { - Annotation, - mountAnnotationBlock -} from "../annotation" -import { - CodeBlock, - mountCodeBlock -} from "../code" -import { - Details, - mountDetails -} from "../details" -import { - Mermaid, - mountMermaid -} from "../mermaid" -import { - DataTable, - mountDataTable -} from "../table" -import { - ContentTabs, - mountContentTabs -} from "../tabs" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Content - */ -export type Content = - | Annotation - | CodeBlock - | ContentTabs - | DataTable - | Details - | Mermaid - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - target$: Observable<HTMLElement> /* Location target observable */ - print$: Observable<boolean> /* Media print observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount content - * - * This function mounts all components that are found in the content of the - * actual article, including code blocks, data tables and details. - * - * @param el - Content element - * @param options - Options - * - * @returns Content component observable - */ -export function mountContent( - el: HTMLElement, { viewport$, target$, print$ }: MountOptions -): Observable<Component<Content>> { - return merge( - - /* Annotations */ - ...getElements(".annotate:not(.highlight)", el) - .map(child => mountAnnotationBlock(child, { target$, print$ })), - - /* Code blocks */ - ...getElements("pre:not(.mermaid) > code", el) - .map(child => mountCodeBlock(child, { target$, print$ })), - - /* Mermaid diagrams */ - ...getElements("pre.mermaid", el) - .map(child => mountMermaid(child)), - - /* Data tables */ - ...getElements("table:not([class])", el) - .map(child => mountDataTable(child)), - - /* Details */ - ...getElements("details", el) - .map(child => mountDetails(child, { target$, print$ })), - - /* Content tabs */ - ...getElements("[data-tabs]", el) - .map(child => mountContentTabs(child, { viewport$ })) - ) -} diff --git a/dev/src/templates/assets/javascripts/components/content/annotation/_/index.ts b/dev/src/templates/assets/javascripts/components/content/annotation/_/index.ts deleted file mode 100644 index c5138fa4..00000000 --- a/dev/src/templates/assets/javascripts/components/content/annotation/_/index.ts +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - animationFrameScheduler, - auditTime, - combineLatest, - debounceTime, - defer, - delay, - endWith, - filter, - finalize, - fromEvent, - ignoreElements, - map, - merge, - switchMap, - take, - takeUntil, - tap, - throttleTime, - withLatestFrom -} from "rxjs" - -import { - ElementOffset, - getActiveElement, - getElementSize, - watchElementContentOffset, - watchElementFocus, - watchElementOffset, - watchElementVisibility -} from "~/browser" - -import { Component } from "../../../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Annotation - */ -export interface Annotation { - active: boolean /* Annotation is active */ - offset: ElementOffset /* Annotation offset */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - target$: Observable<HTMLElement> /* Location target observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch annotation - * - * @param el - Annotation element - * @param container - Containing element - * - * @returns Annotation observable - */ -export function watchAnnotation( - el: HTMLElement, container: HTMLElement -): Observable<Annotation> { - const offset$ = defer(() => combineLatest([ - watchElementOffset(el), - watchElementContentOffset(container) - ])) - .pipe( - map(([{ x, y }, scroll]): ElementOffset => { - const { width, height } = getElementSize(el) - return ({ - x: x - scroll.x + width / 2, - y: y - scroll.y + height / 2 - }) - }) - ) - - /* Actively watch annotation on focus */ - return watchElementFocus(el) - .pipe( - switchMap(active => offset$ - .pipe( - map(offset => ({ active, offset })), - take(+!active || Infinity) - ) - ) - ) -} - -/** - * Mount annotation - * - * @param el - Annotation element - * @param container - Containing element - * @param options - Options - * - * @returns Annotation component observable - */ -export function mountAnnotation( - el: HTMLElement, container: HTMLElement, { target$ }: MountOptions -): Observable<Component<Annotation>> { - const [tooltip, index] = Array.from(el.children) - - /* Mount component on subscription */ - return defer(() => { - const push$ = new Subject<Annotation>() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - push$.subscribe({ - - /* Handle emission */ - next({ offset }) { - el.style.setProperty("--md-tooltip-x", `${offset.x}px`) - el.style.setProperty("--md-tooltip-y", `${offset.y}px`) - }, - - /* Handle complete */ - complete() { - el.style.removeProperty("--md-tooltip-x") - el.style.removeProperty("--md-tooltip-y") - } - }) - - /* Start animation only when annotation is visible */ - watchElementVisibility(el) - .pipe( - takeUntil(done$) - ) - .subscribe(visible => { - el.toggleAttribute("data-md-visible", visible) - }) - - /* Toggle tooltip presence to mitigate empty lines when copying */ - merge( - push$.pipe(filter(({ active }) => active)), - push$.pipe(debounceTime(250), filter(({ active }) => !active)) - ) - .subscribe({ - - /* Handle emission */ - next({ active }) { - if (active) - el.prepend(tooltip) - else - tooltip.remove() - }, - - /* Handle complete */ - complete() { - el.prepend(tooltip) - } - }) - - /* Toggle tooltip visibility */ - push$ - .pipe( - auditTime(16, animationFrameScheduler) - ) - .subscribe(({ active }) => { - tooltip.classList.toggle("md-tooltip--active", active) - }) - - /* Track relative origin of tooltip */ - push$ - .pipe( - throttleTime(125, animationFrameScheduler), - filter(() => !!el.offsetParent), - map(() => el.offsetParent!.getBoundingClientRect()), - map(({ x }) => x) - ) - .subscribe({ - - /* Handle emission */ - next(origin) { - if (origin) - el.style.setProperty("--md-tooltip-0", `${-origin}px`) - else - el.style.removeProperty("--md-tooltip-0") - }, - - /* Handle complete */ - complete() { - el.style.removeProperty("--md-tooltip-0") - } - }) - - /* Allow to copy link without scrolling to anchor */ - fromEvent<MouseEvent>(index, "click") - .pipe( - takeUntil(done$), - filter(ev => !(ev.metaKey || ev.ctrlKey)) - ) - .subscribe(ev => { - ev.stopPropagation() - ev.preventDefault() - }) - - /* Allow to open link in new tab or blur on close */ - fromEvent<MouseEvent>(index, "mousedown") - .pipe( - takeUntil(done$), - withLatestFrom(push$) - ) - .subscribe(([ev, { active }]) => { - - /* Open in new tab */ - if (ev.button !== 0 || ev.metaKey || ev.ctrlKey) { - ev.preventDefault() - - /* Close annotation */ - } else if (active) { - ev.preventDefault() - - /* Focus parent annotation, if any */ - const parent = el.parentElement!.closest(".md-annotation") - if (parent instanceof HTMLElement) - parent.focus() - else - getActiveElement()?.blur() - } - }) - - /* Open and focus annotation on location target */ - target$ - .pipe( - takeUntil(done$), - filter(target => target === tooltip), - delay(125) - ) - .subscribe(() => el.focus()) - - /* Create and return component */ - return watchAnnotation(el, container) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/dev/src/templates/assets/javascripts/components/content/annotation/block/index.ts b/dev/src/templates/assets/javascripts/components/content/annotation/block/index.ts deleted file mode 100644 index c73b01fa..00000000 --- a/dev/src/templates/assets/javascripts/components/content/annotation/block/index.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { EMPTY, Observable, defer } from "rxjs" - -import { Component } from "../../../_" -import { Annotation } from "../_" -import { mountAnnotationList } from "../list" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - target$: Observable<HTMLElement> /* Location target observable */ - print$: Observable<boolean> /* Media print observable */ -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Find list element directly following a block - * - * @param el - Annotation block element - * - * @returns List element or nothing - */ -function findList(el: HTMLElement): HTMLElement | undefined { - if (el.nextElementSibling) { - const sibling = el.nextElementSibling as HTMLElement - if (sibling.tagName === "OL") - return sibling - - /* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */ - else if (sibling.tagName === "P" && !sibling.children.length) - return findList(sibling) - } - - /* Everything else */ - return undefined -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount annotation block - * - * @param el - Annotation block element - * @param options - Options - * - * @returns Annotation component observable - */ -export function mountAnnotationBlock( - el: HTMLElement, options: MountOptions -): Observable<Component<Annotation>> { - return defer(() => { - const list = findList(el) - return typeof list !== "undefined" - ? mountAnnotationList(list, el, options) - : EMPTY - }) -} diff --git a/dev/src/templates/assets/javascripts/components/content/annotation/index.ts b/dev/src/templates/assets/javascripts/components/content/annotation/index.ts deleted file mode 100644 index c593b723..00000000 --- a/dev/src/templates/assets/javascripts/components/content/annotation/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./block" -export * from "./list" diff --git a/dev/src/templates/assets/javascripts/components/content/annotation/list/index.ts b/dev/src/templates/assets/javascripts/components/content/annotation/list/index.ts deleted file mode 100644 index 725dd583..00000000 --- a/dev/src/templates/assets/javascripts/components/content/annotation/list/index.ts +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - EMPTY, - Observable, - Subject, - defer, - endWith, - finalize, - ignoreElements, - merge, - share, - takeUntil -} from "rxjs" - -import { - getElement, - getElements, - getOptionalElement -} from "~/browser" -import { renderAnnotation } from "~/templates" - -import { Component } from "../../../_" -import { - Annotation, - mountAnnotation -} from "../_" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - target$: Observable<HTMLElement> /* Location target observable */ - print$: Observable<boolean> /* Media print observable */ -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Find all annotation hosts in the containing element - * - * @param container - Containing element - * - * @returns Annotation hosts - */ -function findHosts(container: HTMLElement): HTMLElement[] { - return container.tagName === "CODE" - ? getElements(".c, .c1, .cm", container) - : [container] -} - -/** - * Find all annotation markers in the containing element - * - * @param container - Containing element - * - * @returns Annotation markers - */ -function findMarkers(container: HTMLElement): Text[] { - const markers: Text[] = [] - for (const el of findHosts(container)) { - const nodes: Text[] = [] - - /* Find all text nodes in current element */ - const it = document.createNodeIterator(el, NodeFilter.SHOW_TEXT) - for (let node = it.nextNode(); node; node = it.nextNode()) - nodes.push(node as Text) - - /* Find all markers in each text node */ - for (let text of nodes) { - let match: RegExpExecArray | null - - /* Split text at marker and add to list */ - while ((match = /(\(\d+\))(!)?/.exec(text.textContent!))) { - const [, id, force] = match - if (typeof force === "undefined") { - const marker = text.splitText(match.index) - text = marker.splitText(id.length) - markers.push(marker) - - /* Replace entire text with marker */ - } else { - text.textContent = id - markers.push(text) - break - } - } - } - } - return markers -} - -/** - * Swap the child nodes of two elements - * - * @param source - Source element - * @param target - Target element - */ -function swap(source: HTMLElement, target: HTMLElement): void { - target.append(...Array.from(source.childNodes)) -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount annotation list - * - * This function analyzes the containing code block and checks for markers - * referring to elements in the given annotation list. If no markers are found, - * the list is left untouched. Otherwise, list elements are rendered as - * annotations inside the code block. - * - * @param el - Annotation list element - * @param container - Containing element - * @param options - Options - * - * @returns Annotation component observable - */ -export function mountAnnotationList( - el: HTMLElement, container: HTMLElement, { target$, print$ }: MountOptions -): Observable<Component<Annotation>> { - - /* Compute prefix for tooltip anchors */ - const parent = container.closest("[id]") - const prefix = parent?.id - - /* Find and replace all markers with empty annotations */ - const annotations = new Map<string, HTMLElement>() - for (const marker of findMarkers(container)) { - const [, id] = marker.textContent!.match(/\((\d+)\)/)! - if (getOptionalElement(`:scope > li:nth-child(${id})`, el)) { - annotations.set(id, renderAnnotation(id, prefix)) - marker.replaceWith(annotations.get(id)!) - } - } - - /* Keep list if there are no annotations to render */ - if (annotations.size === 0) - return EMPTY - - /* Mount component on subscription */ - return defer(() => { - const push$ = new Subject() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - - /* Retrieve container pairs for swapping */ - const pairs: [HTMLElement, HTMLElement][] = [] - for (const [id, annotation] of annotations) - pairs.push([ - getElement(".md-typeset", annotation), - getElement(`:scope > li:nth-child(${id})`, el) - ]) - - /* Handle print mode - see https://bit.ly/3rgPdpt */ - print$.pipe(takeUntil(done$)) - .subscribe(active => { - el.hidden = !active - - /* Add class to discern list element */ - el.classList.toggle("md-annotation-list", active) - - /* Show annotations in code block or list (print) */ - for (const [inner, child] of pairs) - if (!active) - swap(child, inner) - else - swap(inner, child) - }) - - /* Create and return component */ - return merge(...[...annotations] - .map(([, annotation]) => ( - mountAnnotation(annotation, container, { target$ }) - )) - ) - .pipe( - finalize(() => push$.complete()), - share() - ) - }) -} diff --git a/dev/src/templates/assets/javascripts/components/content/code/_/index.ts b/dev/src/templates/assets/javascripts/components/content/code/_/index.ts deleted file mode 100644 index ccc09339..00000000 --- a/dev/src/templates/assets/javascripts/components/content/code/_/index.ts +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import ClipboardJS from "clipboard" -import { - EMPTY, - Observable, - Subject, - defer, - distinctUntilChanged, - distinctUntilKeyChanged, - filter, - finalize, - map, - mergeWith, - switchMap, - take, - tap -} from "rxjs" - -import { feature } from "~/_" -import { - getElementContentSize, - watchElementSize, - watchElementVisibility -} from "~/browser" -import { renderClipboardButton } from "~/templates" - -import { Component } from "../../../_" -import { - Annotation, - mountAnnotationList -} from "../../annotation" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Code block - */ -export interface CodeBlock { - scrollable: boolean /* Code block overflows */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - target$: Observable<HTMLElement> /* Location target observable */ - print$: Observable<boolean> /* Media print observable */ -} - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Global sequence number for code blocks - */ -let sequence = 0 - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Find candidate list element directly following a code block - * - * @param el - Code block element - * - * @returns List element or nothing - */ -function findCandidateList(el: HTMLElement): HTMLElement | undefined { - if (el.nextElementSibling) { - const sibling = el.nextElementSibling as HTMLElement - if (sibling.tagName === "OL") - return sibling - - /* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */ - else if (sibling.tagName === "P" && !sibling.children.length) - return findCandidateList(sibling) - } - - /* Everything else */ - return undefined -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch code block - * - * This function monitors size changes of the viewport, as well as switches of - * content tabs with embedded code blocks, as both may trigger overflow. - * - * @param el - Code block element - * - * @returns Code block observable - */ -export function watchCodeBlock( - el: HTMLElement -): Observable<CodeBlock> { - return watchElementSize(el) - .pipe( - map(({ width }) => { - const content = getElementContentSize(el) - return { - scrollable: content.width > width - } - }), - distinctUntilKeyChanged("scrollable") - ) -} - -/** - * Mount code block - * - * This function ensures that an overflowing code block is focusable through - * keyboard, so it can be scrolled without a mouse to improve on accessibility. - * Furthermore, if code annotations are enabled, they are mounted if and only - * if the code block is currently visible, e.g., not in a hidden content tab. - * - * Note that code blocks may be mounted eagerly or lazily. If they're mounted - * lazily (on first visibility), code annotation anchor links will not work, - * as they are evaluated on initial page load, and code annotations in general - * might feel a little bumpier. - * - * @param el - Code block element - * @param options - Options - * - * @returns Code block and annotation component observable - */ -export function mountCodeBlock( - el: HTMLElement, options: MountOptions -): Observable<Component<CodeBlock | Annotation>> { - const { matches: hover } = matchMedia("(hover)") - - /* Defer mounting of code block - see https://bit.ly/3vHVoVD */ - const factory$ = defer(() => { - const push$ = new Subject<CodeBlock>() - push$.subscribe(({ scrollable }) => { - if (scrollable && hover) - el.setAttribute("tabindex", "0") - else - el.removeAttribute("tabindex") - }) - - /* Render button for Clipboard.js integration */ - if (ClipboardJS.isSupported()) { - if (el.closest(".copy") || ( - feature("content.code.copy") && !el.closest(".no-copy") - )) { - const parent = el.closest("pre")! - parent.id = `__code_${sequence++}` - parent.insertBefore( - renderClipboardButton(parent.id), - el - ) - } - } - - /* Handle code annotations */ - const container = el.closest(".highlight") - if (container instanceof HTMLElement) { - const list = findCandidateList(container) - - /* Mount code annotations, if enabled */ - if (typeof list !== "undefined" && ( - container.classList.contains("annotate") || - feature("content.code.annotate") - )) { - const annotations$ = mountAnnotationList(list, el, options) - - /* Create and return component */ - return watchCodeBlock(el) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })), - mergeWith( - watchElementSize(container) - .pipe( - map(({ width, height }) => width && height), - distinctUntilChanged(), - switchMap(active => active ? annotations$ : EMPTY) - ) - ) - ) - } - } - - /* Create and return component */ - return watchCodeBlock(el) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) - - /* Mount code block lazily */ - if (feature("content.lazy")) - return watchElementVisibility(el) - .pipe( - filter(visible => visible), - take(1), - switchMap(() => factory$) - ) - - /* Mount code block */ - return factory$ -} diff --git a/dev/src/templates/assets/javascripts/components/content/code/index.ts b/dev/src/templates/assets/javascripts/components/content/code/index.ts deleted file mode 100644 index 3f86e2b4..00000000 --- a/dev/src/templates/assets/javascripts/components/content/code/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" diff --git a/dev/src/templates/assets/javascripts/components/content/details/index.ts b/dev/src/templates/assets/javascripts/components/content/details/index.ts deleted file mode 100644 index 17bfae45..00000000 --- a/dev/src/templates/assets/javascripts/components/content/details/index.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - defer, - filter, - finalize, - map, - merge, - tap -} from "rxjs" - -import { Component } from "../../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Details - */ -export interface Details { - action: "open" | "close" /* Details state */ - reveal?: boolean /* Details is revealed */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - target$: Observable<HTMLElement> /* Location target observable */ - print$: Observable<boolean> /* Media print observable */ -} - -/** - * Mount options - */ -interface MountOptions { - target$: Observable<HTMLElement> /* Location target observable */ - print$: Observable<boolean> /* Media print observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch details - * - * @param el - Details element - * @param options - Options - * - * @returns Details observable - */ -export function watchDetails( - el: HTMLDetailsElement, { target$, print$ }: WatchOptions -): Observable<Details> { - let open = true - return merge( - - /* Open and focus details on location target */ - target$ - .pipe( - map(target => target.closest("details:not([open])")!), - filter(details => el === details), - map(() => ({ - action: "open", reveal: true - }) as Details) - ), - - /* Open details on print and close afterwards */ - print$ - .pipe( - filter(active => active || !open), - tap(() => open = el.open), - map(active => ({ - action: active ? "open" : "close" - }) as Details) - ) - ) -} - -/** - * Mount details - * - * This function ensures that `details` tags are opened on anchor jumps and - * prior to printing, so the whole content of the page is visible. - * - * @param el - Details element - * @param options - Options - * - * @returns Details component observable - */ -export function mountDetails( - el: HTMLDetailsElement, options: MountOptions -): Observable<Component<Details>> { - return defer(() => { - const push$ = new Subject<Details>() - push$.subscribe(({ action, reveal }) => { - el.toggleAttribute("open", action === "open") - if (reveal) - el.scrollIntoView() - }) - - /* Create and return component */ - return watchDetails(el, options) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/dev/src/templates/assets/javascripts/components/content/index.ts b/dev/src/templates/assets/javascripts/components/content/index.ts deleted file mode 100644 index a29d8b41..00000000 --- a/dev/src/templates/assets/javascripts/components/content/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./annotation" -export * from "./code" -export * from "./details" -export * from "./table" -export * from "./tabs" diff --git a/dev/src/templates/assets/javascripts/components/content/mermaid/index.css b/dev/src/templates/assets/javascripts/components/content/mermaid/index.css deleted file mode 100644 index 3092b8ec..00000000 --- a/dev/src/templates/assets/javascripts/components/content/mermaid/index.css +++ /dev/null @@ -1,430 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -/* ---------------------------------------------------------------------------- - * Rules: general - * ------------------------------------------------------------------------- */ - -/* General node */ -.node circle, -.node ellipse, -.node path, -.node polygon, -.node rect { - fill: var(--md-mermaid-node-bg-color); - stroke: var(--md-mermaid-node-fg-color); -} - -/* General marker */ -marker { - fill: var(--md-mermaid-edge-color) !important; -} - -/* General edge label */ -.edgeLabel .label rect { - fill: transparent; -} - -/* ---------------------------------------------------------------------------- - * Rules: flowcharts - * ------------------------------------------------------------------------- */ - -/* Flowchart node label */ -.label { - color: var(--md-mermaid-label-fg-color); - font-family: var(--md-mermaid-font-family); -} - -/* Flowchart node label container */ -.label foreignObject { - overflow: visible; - line-height: initial; -} - -/* Flowchart edge label in node label */ -.label div .edgeLabel { - color: var(--md-mermaid-label-fg-color); - background-color: var(--md-mermaid-label-bg-color); -} - -/* Flowchart edge label */ -.edgeLabel, -.edgeLabel rect { - color: var(--md-mermaid-edge-color); - background-color: var(--md-mermaid-label-bg-color); - fill: var(--md-mermaid-label-bg-color); -} - -/* Flowchart edge path */ -.edgePath .path, -.flowchart-link { - stroke: var(--md-mermaid-edge-color); - stroke-width: .05rem; -} - -/* Flowchart arrow head */ -.edgePath .arrowheadPath { - fill: var(--md-mermaid-edge-color); - stroke: none; -} - -/* Flowchart subgraph */ -.cluster rect { - fill: var(--md-default-fg-color--lightest); - stroke: var(--md-default-fg-color--lighter); -} - -/* Flowchart subgraph labels */ -.cluster span { - color: var(--md-mermaid-label-fg-color); - font-family: var(--md-mermaid-font-family); -} - -/* Flowchart markers */ -g #flowchart-circleStart, -g #flowchart-circleEnd, -g #flowchart-crossStart, -g #flowchart-crossEnd, -g #flowchart-pointStart, -g #flowchart-pointEnd { - stroke: none; -} - -/* ---------------------------------------------------------------------------- - * Rules: class diagrams - * ------------------------------------------------------------------------- */ - -/* Class group node */ -g.classGroup line, -g.classGroup rect { - fill: var(--md-mermaid-node-bg-color); - stroke: var(--md-mermaid-node-fg-color); -} - -/* Class group node text */ -g.classGroup text { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-label-fg-color); -} - -/* Class label box */ -.classLabel .box { - background-color: var(--md-mermaid-label-bg-color); - opacity: 1; - fill: var(--md-mermaid-label-bg-color); -} - -/* Class label text */ -.classLabel .label { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-label-fg-color); -} - -/* Class group divider */ -.node .divider { - stroke: var(--md-mermaid-node-fg-color); -} - -/* Class relation */ -.relation { - stroke: var(--md-mermaid-edge-color); -} - -/* Class relation cardinality */ -.cardinality { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-label-fg-color); -} - -/* Class relation cardinality text */ -.cardinality text { - fill: inherit !important; -} - -/* Class extension, composition and dependency marker */ -defs #classDiagram-extensionStart, -defs #classDiagram-extensionEnd, -defs #classDiagram-compositionStart, -defs #classDiagram-compositionEnd, -defs #classDiagram-dependencyStart, -defs #classDiagram-dependencyEnd { - fill: var(--md-mermaid-edge-color) !important; - stroke: var(--md-mermaid-edge-color) !important; -} - -/* Class aggregation marker */ -defs #classDiagram-aggregationStart, -defs #classDiagram-aggregationEnd { - fill: var(--md-mermaid-label-bg-color) !important; - stroke: var(--md-mermaid-edge-color) !important; -} - -/* ---------------------------------------------------------------------------- - * Rules: state diagrams - * ------------------------------------------------------------------------- */ - -/* State group node */ -g.stateGroup rect { - fill: var(--md-mermaid-node-bg-color); - stroke: var(--md-mermaid-node-fg-color); -} - -/* State group title */ -g.stateGroup .state-title { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-label-fg-color) !important; -} - -/* State group background */ -g.stateGroup .composit { - fill: var(--md-mermaid-label-bg-color); -} - -/* State node label */ -.nodeLabel { - color: var(--md-mermaid-label-fg-color); - font-family: var(--md-mermaid-font-family); -} - -/* State start and end marker */ -.start-state, -.node circle.state-start, -.node circle.state-end { - fill: var(--md-mermaid-edge-color); - stroke: none; -} - -/* State end marker */ -.end-state-outer, -.end-state-inner { - fill: var(--md-mermaid-edge-color); -} - -/* State end marker */ -.end-state-inner, -.node circle.state-end { - stroke: var(--md-mermaid-label-bg-color); -} - -/* State transition */ -.transition { - stroke: var(--md-mermaid-edge-color); -} - -/* State fork and join */ -[id^=state-fork] rect, -[id^=state-join] rect { - fill: var(--md-mermaid-edge-color) !important; - stroke: none !important; -} - -/* State cluster (yes, 2x... Mermaid WTF) */ -.statediagram-cluster.statediagram-cluster .inner { - fill: var(--md-default-bg-color); -} - -/* State cluster node */ -.statediagram-cluster rect { - fill: var(--md-mermaid-node-bg-color); - stroke: var(--md-mermaid-node-fg-color); -} - -/* State cluster divider */ -.statediagram-state rect.divider { - fill: var(--md-default-fg-color--lightest); - stroke: var(--md-default-fg-color--lighter); -} - -/* State diagram markers */ -defs #statediagram-barbEnd { - stroke: var(--md-mermaid-edge-color); -} - -/* ---------------------------------------------------------------------------- - * Rules: entity-relationship diagrams - * ------------------------------------------------------------------------- */ - -/* Attribute box */ -.attributeBoxEven, -.attributeBoxOdd { - fill: var(--md-mermaid-node-bg-color); - stroke: var(--md-mermaid-node-fg-color); -} - -/* Entity node */ -.entityBox { - fill: var(--md-mermaid-label-bg-color); - stroke: var(--md-mermaid-node-fg-color); -} - -/* Entity node label */ -.entityLabel { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-label-fg-color); -} - -/* Entity relationship label container */ -.relationshipLabelBox { - background-color: var(--md-mermaid-label-bg-color); - opacity: 1; - fill: var(--md-mermaid-label-bg-color); - fill-opacity: 1; -} - -/* Entity relationship label */ -.relationshipLabel { - fill: var(--md-mermaid-label-fg-color); -} - -/* Entity relationship line { */ -.relationshipLine { - stroke: var(--md-mermaid-edge-color); -} - -/* Entity relationship line markers */ -defs #ZERO_OR_ONE_START *, -defs #ZERO_OR_ONE_END *, -defs #ZERO_OR_MORE_START *, -defs #ZERO_OR_MORE_END *, -defs #ONLY_ONE_START *, -defs #ONLY_ONE_END *, -defs #ONE_OR_MORE_START *, -defs #ONE_OR_MORE_END * { - stroke: var(--md-mermaid-edge-color) !important; -} - -/* Entity relationship line markers */ -defs #ZERO_OR_MORE_START circle, -defs #ZERO_OR_MORE_END circle { - fill: var(--md-mermaid-label-bg-color); -} - -/* ---------------------------------------------------------------------------- - * Rules: sequence diagrams - * ------------------------------------------------------------------------- */ - -/* Sequence actor */ -.actor { - fill: var(--md-mermaid-sequence-actor-bg-color); - stroke: var(--md-mermaid-sequence-actor-border-color); -} - -/* Sequence actor text */ -text.actor > tspan { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-sequence-actor-fg-color); -} - -/* Sequence actor line */ -line { - stroke: var(--md-mermaid-sequence-actor-line-color); -} - -/* Sequence actor */ -.actor-man circle, -.actor-man line { - fill: var(--md-mermaid-sequence-actorman-bg-color); - stroke: var(--md-mermaid-sequence-actorman-line-color); -} - -/* Sequence message line */ -.messageLine0, -.messageLine1 { - stroke: var(--md-mermaid-sequence-message-line-color); -} - -/* Sequence note */ -.note { - fill: var(--md-mermaid-sequence-note-bg-color); - stroke: var(--md-mermaid-sequence-note-border-color); -} - -/* Sequence message, loop and note text */ -.messageText, -.loopText, -.loopText > tspan, -.noteText > tspan { - font-family: var(--md-mermaid-font-family) !important; - stroke: none; -} - -/* Sequence message text */ -.messageText { - fill: var(--md-mermaid-sequence-message-fg-color); -} - -/* Sequence loop text */ -.loopText, -.loopText > tspan { - fill: var(--md-mermaid-sequence-loop-fg-color); -} - -/* Sequence note text */ -.noteText > tspan { - fill: var(--md-mermaid-sequence-note-fg-color); -} - -/* Sequence arrow head */ -#arrowhead path { - fill: var(--md-mermaid-sequence-message-line-color); - stroke: none; -} - -/* Sequence loop line */ -.loopLine { - fill: var(--md-mermaid-sequence-loop-bg-color); - stroke: var(--md-mermaid-sequence-loop-border-color); -} - -/* Sequence label box */ -.labelBox { - fill: var(--md-mermaid-sequence-label-bg-color); - stroke: none; -} - -/* Sequence label text */ -.labelText, -.labelText > span { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-sequence-label-fg-color); -} - -/* Sequence number */ -.sequenceNumber { - fill: var(--md-mermaid-sequence-number-fg-color); -} - -/* Sequence rectangle */ -rect.rect { - fill: var(--md-mermaid-sequence-box-bg-color); - stroke: none; -} - -/* Sequence rectangle text */ -rect.rect + text.text { - fill: var(--md-mermaid-sequence-box-fg-color); -} - -/* Sequence diagram markers */ -defs #sequencenumber { - fill: var(--md-mermaid-sequence-number-bg-color) !important; -} diff --git a/dev/src/templates/assets/javascripts/components/content/mermaid/index.ts b/dev/src/templates/assets/javascripts/components/content/mermaid/index.ts deleted file mode 100644 index 3f6480fd..00000000 --- a/dev/src/templates/assets/javascripts/components/content/mermaid/index.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - map, - of, - shareReplay, - tap -} from "rxjs" - -import { watchScript } from "~/browser" -import { h } from "~/utilities" - -import { Component } from "../../_" - -import themeCSS from "./index.css" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Mermaid diagram - */ -export interface Mermaid {} - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Mermaid instance observable - */ -let mermaid$: Observable<void> - -/** - * Global sequence number for diagrams - */ -let sequence = 0 - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Fetch Mermaid script - * - * @returns Mermaid scripts observable - */ -function fetchScripts(): Observable<void> { - return typeof mermaid === "undefined" || mermaid instanceof Element - ? watchScript("https://unpkg.com/mermaid@9.4.3/dist/mermaid.min.js") - : of(undefined) -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount Mermaid diagram - * - * @param el - Code block element - * - * @returns Mermaid diagram component observable - */ -export function mountMermaid( - el: HTMLElement -): Observable<Component<Mermaid>> { - el.classList.remove("mermaid") // Hack: mitigate https://bit.ly/3CiN6Du - mermaid$ ||= fetchScripts() - .pipe( - tap(() => mermaid.initialize({ - startOnLoad: false, - themeCSS, - sequence: { - actorFontSize: "16px", // Hack: mitigate https://bit.ly/3y0NEi3 - messageFontSize: "16px", - noteFontSize: "16px" - } - })), - map(() => undefined), - shareReplay(1) - ) - - /* Render diagram */ - mermaid$.subscribe(() => { - el.classList.add("mermaid") // Hack: mitigate https://bit.ly/3CiN6Du - const id = `__mermaid_${sequence++}` - - /* Create host element to replace code block */ - const host = h("div", { class: "mermaid" }) - const text = el.textContent - - /* Render and inject diagram */ - mermaid.mermaidAPI.render(id, text, (svg: string, fn: Function) => { - - /* Create a shadow root and inject diagram */ - const shadow = host.attachShadow({ mode: "closed" }) - shadow.innerHTML = svg - - /* Replace code block with diagram and bind functions */ - el.replaceWith(host) - fn?.(shadow) - }) - }) - - /* Create and return component */ - return mermaid$ - .pipe( - map(() => ({ ref: el })) - ) -} diff --git a/dev/src/templates/assets/javascripts/components/content/table/index.ts b/dev/src/templates/assets/javascripts/components/content/table/index.ts deleted file mode 100644 index c318e7a6..00000000 --- a/dev/src/templates/assets/javascripts/components/content/table/index.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { Observable, of } from "rxjs" - -import { renderTable } from "~/templates" -import { h } from "~/utilities" - -import { Component } from "../../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Data table - */ -export interface DataTable {} - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Sentinel for replacement - */ -const sentinel = h("table") - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount data table - * - * This function wraps a data table in another scrollable container, so it can - * be smoothly scrolled on smaller screen sizes and won't break the layout. - * - * @param el - Data table element - * - * @returns Data table component observable - */ -export function mountDataTable( - el: HTMLElement -): Observable<Component<DataTable>> { - el.replaceWith(sentinel) - sentinel.replaceWith(renderTable(el)) - - /* Create and return component */ - return of({ ref: el }) -} diff --git a/dev/src/templates/assets/javascripts/components/content/tabs/index.ts b/dev/src/templates/assets/javascripts/components/content/tabs/index.ts deleted file mode 100644 index f57447e2..00000000 --- a/dev/src/templates/assets/javascripts/components/content/tabs/index.ts +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - animationFrameScheduler, - asyncScheduler, - auditTime, - combineLatest, - defer, - endWith, - finalize, - fromEvent, - ignoreElements, - map, - merge, - skip, - startWith, - subscribeOn, - takeUntil, - tap, - withLatestFrom -} from "rxjs" - -import { feature } from "~/_" -import { - Viewport, - getElement, - getElementContentOffset, - getElementContentSize, - getElementOffset, - getElementSize, - getElements, - watchElementContentOffset, - watchElementSize -} from "~/browser" -import { renderTabbedControl } from "~/templates" - -import { Component } from "../../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Content tabs - */ -export interface ContentTabs { - active: HTMLLabelElement /* Active tab label */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable<Viewport> /* Viewport observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch content tabs - * - * @param el - Content tabs element - * - * @returns Content tabs observable - */ -export function watchContentTabs( - el: HTMLElement -): Observable<ContentTabs> { - const inputs = getElements<HTMLInputElement>(":scope > input", el) - const initial = inputs.find(input => input.checked) || inputs[0] - return merge(...inputs.map(input => fromEvent(input, "change") - .pipe( - map(() => getElement<HTMLLabelElement>(`label[for="${input.id}"]`)) - ) - )) - .pipe( - startWith(getElement<HTMLLabelElement>(`label[for="${initial.id}"]`)), - map(active => ({ active })) - ) -} - -/** - * Mount content tabs - * - * This function scrolls the active tab into view. While this functionality is - * provided by browsers as part of `scrollInfoView`, browsers will always also - * scroll the vertical axis, which we do not want. Thus, we decided to provide - * this functionality ourselves. - * - * @param el - Content tabs element - * @param options - Options - * - * @returns Content tabs component observable - */ -export function mountContentTabs( - el: HTMLElement, { viewport$ }: MountOptions -): Observable<Component<ContentTabs>> { - - /* Render content tab previous button for pagination */ - const prev = renderTabbedControl("prev") - el.append(prev) - - /* Render content tab next button for pagination */ - const next = renderTabbedControl("next") - el.append(next) - - /* Mount component on subscription */ - const container = getElement(".tabbed-labels", el) - return defer(() => { - const push$ = new Subject<ContentTabs>() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - combineLatest([push$, watchElementSize(el)]) - .pipe( - auditTime(1, animationFrameScheduler), - takeUntil(done$) - ) - .subscribe({ - - /* Handle emission */ - next([{ active }, size]) { - const offset = getElementOffset(active) - const { width } = getElementSize(active) - - /* Set tab indicator offset and width */ - el.style.setProperty("--md-indicator-x", `${offset.x}px`) - el.style.setProperty("--md-indicator-width", `${width}px`) - - /* Scroll container to active content tab */ - const content = getElementContentOffset(container) - if ( - offset.x < content.x || - offset.x + width > content.x + size.width - ) - container.scrollTo({ - left: Math.max(0, offset.x - 16), - behavior: "smooth" - }) - }, - - /* Handle complete */ - complete() { - el.style.removeProperty("--md-indicator-x") - el.style.removeProperty("--md-indicator-width") - } - }) - - /* Hide content tab buttons on borders */ - combineLatest([ - watchElementContentOffset(container), - watchElementSize(container) - ]) - .pipe( - takeUntil(done$) - ) - .subscribe(([offset, size]) => { - const content = getElementContentSize(container) - prev.hidden = offset.x < 16 - next.hidden = offset.x > content.width - size.width - 16 - }) - - /* Paginate content tab container on click */ - merge( - fromEvent(prev, "click").pipe(map(() => -1)), - fromEvent(next, "click").pipe(map(() => +1)) - ) - .pipe( - takeUntil(done$) - ) - .subscribe(direction => { - const { width } = getElementSize(container) - container.scrollBy({ - left: width * direction, - behavior: "smooth" - }) - }) - - /* Set up linking of content tabs, if enabled */ - if (feature("content.tabs.link")) - push$.pipe( - skip(1), - withLatestFrom(viewport$) - ) - .subscribe(([{ active }, { offset }]) => { - const tab = active.innerText.trim() - if (active.hasAttribute("data-md-switching")) { - active.removeAttribute("data-md-switching") - - /* Determine viewport offset of active tab */ - } else { - const y = el.offsetTop - offset.y - - /* Passively activate other tabs */ - for (const set of getElements("[data-tabs]")) - for (const input of getElements<HTMLInputElement>( - ":scope > input", set - )) { - const label = getElement(`label[for="${input.id}"]`) - if ( - label !== active && - label.innerText.trim() === tab - ) { - label.setAttribute("data-md-switching", "") - input.click() - break - } - } - - /* Bring active tab into view */ - window.scrollTo({ - top: el.offsetTop - y - }) - - /* Persist active tabs in local storage */ - const tabs = __md_get<string[]>("__tabs") || [] - __md_set("__tabs", [...new Set([tab, ...tabs])]) - } - }) - - /* Pause media (audio, video) on switch - see https://bit.ly/3Bk6cel */ - push$.pipe(takeUntil(done$)) - .subscribe(() => { - for (const media of getElements<HTMLAudioElement>("audio, video", el)) - media.pause() - }) - - /* Create and return component */ - return watchContentTabs(el) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) - .pipe( - subscribeOn(asyncScheduler) - ) -} diff --git a/dev/src/templates/assets/javascripts/components/dialog/index.ts b/dev/src/templates/assets/javascripts/components/dialog/index.ts deleted file mode 100644 index 6ff1bd44..00000000 --- a/dev/src/templates/assets/javascripts/components/dialog/index.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - defer, - delay, - finalize, - map, - merge, - of, - switchMap, - tap -} from "rxjs" - -import { getElement } from "~/browser" - -import { Component } from "../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Dialog - */ -export interface Dialog { - message: string /* Dialog message */ - active: boolean /* Dialog is active */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - alert$: Subject<string> /* Alert subject */ -} - -/** - * Mount options - */ -interface MountOptions { - alert$: Subject<string> /* Alert subject */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch dialog - * - * @param _el - Dialog element - * @param options - Options - * - * @returns Dialog observable - */ -export function watchDialog( - _el: HTMLElement, { alert$ }: WatchOptions -): Observable<Dialog> { - return alert$ - .pipe( - switchMap(message => merge( - of(true), - of(false).pipe(delay(2000)) - ) - .pipe( - map(active => ({ message, active })) - ) - ) - ) -} - -/** - * Mount dialog - * - * This function reveals the dialog in the right corner when a new alert is - * emitted through the subject that is passed as part of the options. - * - * @param el - Dialog element - * @param options - Options - * - * @returns Dialog component observable - */ -export function mountDialog( - el: HTMLElement, options: MountOptions -): Observable<Component<Dialog>> { - const inner = getElement(".md-typeset", el) - return defer(() => { - const push$ = new Subject<Dialog>() - push$.subscribe(({ message, active }) => { - el.classList.toggle("md-dialog--active", active) - inner.textContent = message - }) - - /* Create and return component */ - return watchDialog(el, options) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/dev/src/templates/assets/javascripts/components/header/_/index.ts b/dev/src/templates/assets/javascripts/components/header/_/index.ts deleted file mode 100644 index 0f33eb48..00000000 --- a/dev/src/templates/assets/javascripts/components/header/_/index.ts +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - bufferCount, - combineLatest, - combineLatestWith, - defer, - distinctUntilChanged, - distinctUntilKeyChanged, - endWith, - filter, - ignoreElements, - map, - of, - shareReplay, - startWith, - switchMap, - takeUntil -} from "rxjs" - -import { feature } from "~/_" -import { - Viewport, - watchElementSize, - watchToggle -} from "~/browser" - -import { Component } from "../../_" -import { Main } from "../../main" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Header - */ -export interface Header { - height: number /* Header visible height */ - hidden: boolean /* Header is hidden */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable<Viewport> /* Viewport observable */ -} - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ - main$: Observable<Main> /* Main area observable */ -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Compute whether the header is hidden - * - * If the user scrolls past a certain threshold, the header can be hidden when - * scrolling down, and shown when scrolling up. - * - * @param options - Options - * - * @returns Toggle observable - */ -function isHidden({ viewport$ }: WatchOptions): Observable<boolean> { - if (!feature("header.autohide")) - return of(false) - - /* Compute direction and turning point */ - const direction$ = viewport$ - .pipe( - map(({ offset: { y } }) => y), - bufferCount(2, 1), - map(([a, b]) => [a < b, b] as const), - distinctUntilKeyChanged(0) - ) - - /* Compute whether header should be hidden */ - const hidden$ = combineLatest([viewport$, direction$]) - .pipe( - filter(([{ offset }, [, y]]) => Math.abs(y - offset.y) > 100), - map(([, [direction]]) => direction), - distinctUntilChanged() - ) - - /* Compute threshold for hiding */ - const search$ = watchToggle("search") - return combineLatest([viewport$, search$]) - .pipe( - map(([{ offset }, search]) => offset.y > 400 && !search), - distinctUntilChanged(), - switchMap(active => active ? hidden$ : of(false)), - startWith(false) - ) -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch header - * - * @param el - Header element - * @param options - Options - * - * @returns Header observable - */ -export function watchHeader( - el: HTMLElement, options: WatchOptions -): Observable<Header> { - return defer(() => combineLatest([ - watchElementSize(el), - isHidden(options) - ])) - .pipe( - map(([{ height }, hidden]) => ({ - height, - hidden - })), - distinctUntilChanged((a, b) => ( - a.height === b.height && - a.hidden === b.hidden - )), - shareReplay(1) - ) -} - -/** - * Mount header - * - * This function manages the different states of the header, i.e. whether it's - * hidden or rendered with a shadow. This depends heavily on the main area. - * - * @param el - Header element - * @param options - Options - * - * @returns Header component observable - */ -export function mountHeader( - el: HTMLElement, { header$, main$ }: MountOptions -): Observable<Component<Header>> { - return defer(() => { - const push$ = new Subject<Main>() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - push$ - .pipe( - distinctUntilKeyChanged("active"), - combineLatestWith(header$) - ) - .subscribe(([{ active }, { hidden }]) => { - el.classList.toggle("md-header--shadow", active && !hidden) - el.hidden = hidden - }) - - /* Link to main area */ - main$.subscribe(push$) - - /* Create and return component */ - return header$ - .pipe( - takeUntil(done$), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/dev/src/templates/assets/javascripts/components/header/index.ts b/dev/src/templates/assets/javascripts/components/header/index.ts deleted file mode 100644 index cf23ec1a..00000000 --- a/dev/src/templates/assets/javascripts/components/header/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./title" diff --git a/dev/src/templates/assets/javascripts/components/header/title/index.ts b/dev/src/templates/assets/javascripts/components/header/title/index.ts deleted file mode 100644 index f3bc0d08..00000000 --- a/dev/src/templates/assets/javascripts/components/header/title/index.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - EMPTY, - Observable, - Subject, - defer, - distinctUntilKeyChanged, - finalize, - map, - tap -} from "rxjs" - -import { - Viewport, - getElementSize, - getOptionalElement, - watchViewportAt -} from "~/browser" - -import { Component } from "../../_" -import { Header } from "../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Header - */ -export interface HeaderTitle { - active: boolean /* Header title is active */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ -} - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch header title - * - * @param el - Heading element - * @param options - Options - * - * @returns Header title observable - */ -export function watchHeaderTitle( - el: HTMLElement, { viewport$, header$ }: WatchOptions -): Observable<HeaderTitle> { - return watchViewportAt(el, { viewport$, header$ }) - .pipe( - map(({ offset: { y } }) => { - const { height } = getElementSize(el) - return { - active: y >= height - } - }), - distinctUntilKeyChanged("active") - ) -} - -/** - * Mount header title - * - * This function swaps the header title from the site title to the title of the - * current page when the user scrolls past the first headline. - * - * @param el - Header title element - * @param options - Options - * - * @returns Header title component observable - */ -export function mountHeaderTitle( - el: HTMLElement, options: MountOptions -): Observable<Component<HeaderTitle>> { - return defer(() => { - const push$ = new Subject<HeaderTitle>() - push$.subscribe({ - - /* Handle emission */ - next({ active }) { - el.classList.toggle("md-header__title--active", active) - }, - - /* Handle complete */ - complete() { - el.classList.remove("md-header__title--active") - } - }) - - /* Obtain headline, if any */ - const heading = getOptionalElement(".md-content h1") - if (typeof heading === "undefined") - return EMPTY - - /* Create and return component */ - return watchHeaderTitle(heading, options) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/dev/src/templates/assets/javascripts/components/index.ts b/dev/src/templates/assets/javascripts/components/index.ts deleted file mode 100644 index 3d4391d1..00000000 --- a/dev/src/templates/assets/javascripts/components/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./announce" -export * from "./consent" -export * from "./content" -export * from "./dialog" -export * from "./header" -export * from "./main" -export * from "./palette" -export * from "./progress" -export * from "./search" -export * from "./sidebar" -export * from "./source" -export * from "./tabs" -export * from "./toc" -export * from "./top" diff --git a/dev/src/templates/assets/javascripts/components/main/index.ts b/dev/src/templates/assets/javascripts/components/main/index.ts deleted file mode 100644 index 2509f9b9..00000000 --- a/dev/src/templates/assets/javascripts/components/main/index.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - combineLatest, - distinctUntilChanged, - distinctUntilKeyChanged, - map, - switchMap -} from "rxjs" - -import { - Viewport, - watchElementSize -} from "~/browser" - -import { Header } from "../header" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Main area - */ -export interface Main { - offset: number /* Main area top offset */ - height: number /* Main area visible height */ - active: boolean /* Main area is active */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch main area - * - * This function returns an observable that computes the visual parameters of - * the main area which depends on the viewport vertical offset and height, as - * well as the height of the header element, if the header is fixed. - * - * @param el - Main area element - * @param options - Options - * - * @returns Main area observable - */ -export function watchMain( - el: HTMLElement, { viewport$, header$ }: WatchOptions -): Observable<Main> { - - /* Compute necessary adjustment for header */ - const adjust$ = header$ - .pipe( - map(({ height }) => height), - distinctUntilChanged() - ) - - /* Compute the main area's top and bottom borders */ - const border$ = adjust$ - .pipe( - switchMap(() => watchElementSize(el) - .pipe( - map(({ height }) => ({ - top: el.offsetTop, - bottom: el.offsetTop + height - })), - distinctUntilKeyChanged("bottom") - ) - ) - ) - - /* Compute the main area's offset, visible height and if we scrolled past */ - return combineLatest([adjust$, border$, viewport$]) - .pipe( - map(([header, { top, bottom }, { offset: { y }, size: { height } }]) => { - height = Math.max(0, height - - Math.max(0, top - y, header) - - Math.max(0, height + y - bottom) - ) - return { - offset: top - header, - height, - active: top - header <= y - } - }), - distinctUntilChanged((a, b) => ( - a.offset === b.offset && - a.height === b.height && - a.active === b.active - )) - ) -} diff --git a/dev/src/templates/assets/javascripts/components/palette/index.ts b/dev/src/templates/assets/javascripts/components/palette/index.ts deleted file mode 100644 index cf578f60..00000000 --- a/dev/src/templates/assets/javascripts/components/palette/index.ts +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - asyncScheduler, - defer, - finalize, - fromEvent, - map, - mergeMap, - observeOn, - of, - shareReplay, - startWith, - tap -} from "rxjs" - -import { getElements } from "~/browser" -import { h } from "~/utilities" - -import { - Component, - getComponentElement -} from "../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Palette colors - */ -export interface PaletteColor { - scheme?: string /* Color scheme */ - primary?: string /* Primary color */ - accent?: string /* Accent color */ -} - -/** - * Palette - */ -export interface Palette { - index: number /* Palette index */ - color: PaletteColor /* Palette colors */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch color palette - * - * @param inputs - Color palette element - * - * @returns Color palette observable - */ -export function watchPalette( - inputs: HTMLInputElement[] -): Observable<Palette> { - const current = __md_get<Palette>("__palette") || { - index: inputs.findIndex(input => matchMedia( - input.getAttribute("data-md-color-media")! - ).matches) - } - - /* Emit changes in color palette */ - return of(...inputs) - .pipe( - mergeMap(input => fromEvent(input, "change") - .pipe( - map(() => input) - ) - ), - startWith(inputs[Math.max(0, current.index)]), - map(input => ({ - index: inputs.indexOf(input), - color: { - scheme: input.getAttribute("data-md-color-scheme"), - primary: input.getAttribute("data-md-color-primary"), - accent: input.getAttribute("data-md-color-accent") - } - } as Palette)), - shareReplay(1) - ) -} - -/** - * Mount color palette - * - * @param el - Color palette element - * - * @returns Color palette component observable - */ -export function mountPalette( - el: HTMLElement -): Observable<Component<Palette>> { - const meta = h("meta", { name: "theme-color" }) - document.head.appendChild(meta) - - // Add color scheme meta tag - const scheme = h("meta", { name: "color-scheme" }) - document.head.appendChild(scheme) - - /* Mount component on subscription */ - return defer(() => { - const push$ = new Subject<Palette>() - push$.subscribe(palette => { - document.body.setAttribute("data-md-color-switching", "") - - /* Set color palette */ - for (const [key, value] of Object.entries(palette.color)) - document.body.setAttribute(`data-md-color-${key}`, value) - - /* Toggle visibility */ - for (let index = 0; index < inputs.length; index++) { - const label = inputs[index].nextElementSibling - if (label instanceof HTMLElement) - label.hidden = palette.index !== index - } - - /* Persist preference in local storage */ - __md_set("__palette", palette) - }) - - /* Update theme-color meta tag */ - push$ - .pipe( - map(() => { - const header = getComponentElement("header") - const style = window.getComputedStyle(header) - - // Set color scheme - scheme.content = style.colorScheme - - /* Return color in hexadecimal format */ - return style.backgroundColor.match(/\d+/g)! - .map(value => (+value).toString(16).padStart(2, "0")) - .join("") - }) - ) - .subscribe(color => meta.content = `#${color}`) - - /* Revert transition durations after color switch */ - push$.pipe(observeOn(asyncScheduler)) - .subscribe(() => { - document.body.removeAttribute("data-md-color-switching") - }) - - /* Create and return component */ - const inputs = getElements<HTMLInputElement>("input", el) - return watchPalette(inputs) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/dev/src/templates/assets/javascripts/components/progress/index.ts b/dev/src/templates/assets/javascripts/components/progress/index.ts deleted file mode 100644 index 30c722b8..00000000 --- a/dev/src/templates/assets/javascripts/components/progress/index.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - defer, - finalize, - map, - tap -} from "rxjs" - -import { Component } from "../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Progress indicator - */ -export interface Progress { - value: number // Progress value -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - progress$: Subject<number> // Progress subject -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount progress indicator - * - * @param el - Progress indicator element - * @param options - Options - * - * @returns Progress indicator component observable - */ -export function mountProgress( - el: HTMLElement, { progress$ }: MountOptions -): Observable<Component<Progress>> { - - // Mount component on subscription - return defer(() => { - const push$ = new Subject<Progress>() - push$.subscribe(({ value }) => { - el.style.setProperty("--md-progress-value", `${value}`) - }) - - // Create and return component - return progress$ - .pipe( - tap(value => push$.next({ value })), - finalize(() => push$.complete()), - map(value => ({ ref: el, value })) - ) - }) -} diff --git a/dev/src/templates/assets/javascripts/components/search/_/index.ts b/dev/src/templates/assets/javascripts/components/search/_/index.ts deleted file mode 100644 index aa963b47..00000000 --- a/dev/src/templates/assets/javascripts/components/search/_/index.ts +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - NEVER, - Observable, - ObservableInput, - filter, - fromEvent, - merge, - mergeWith -} from "rxjs" - -import { configuration } from "~/_" -import { - Keyboard, - getActiveElement, - getElements, - setToggle -} from "~/browser" -import { - SearchIndex, - SearchResult, - setupSearchWorker -} from "~/integrations" - -import { - Component, - getComponentElement, - getComponentElements -} from "../../_" -import { - SearchQuery, - mountSearchQuery -} from "../query" -import { mountSearchResult } from "../result" -import { - SearchShare, - mountSearchShare -} from "../share" -import { - SearchSuggest, - mountSearchSuggest -} from "../suggest" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search - */ -export type Search = - | SearchQuery - | SearchResult - | SearchShare - | SearchSuggest - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - index$: ObservableInput<SearchIndex> /* Search index observable */ - keyboard$: Observable<Keyboard> /* Keyboard observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount search - * - * This function sets up the search functionality, including the underlying - * web worker and all keyboard bindings. - * - * @param el - Search element - * @param options - Options - * - * @returns Search component observable - */ -export function mountSearch( - el: HTMLElement, { index$, keyboard$ }: MountOptions -): Observable<Component<Search>> { - const config = configuration() - try { - const worker$ = setupSearchWorker(config.search, index$) - - /* Retrieve query and result components */ - const query = getComponentElement("search-query", el) - const result = getComponentElement("search-result", el) - - /* Always close search on result selection */ - fromEvent<PointerEvent>(el, "click") - .pipe( - filter(({ target }) => ( - target instanceof Element && !!target.closest("a") - )) - ) - .subscribe(() => setToggle("search", false)) - - /* Set up search keyboard handlers */ - keyboard$ - .pipe( - filter(({ mode }) => mode === "search") - ) - .subscribe(key => { - const active = getActiveElement() - switch (key.type) { - - /* Enter: go to first (best) result */ - case "Enter": - if (active === query) { - const anchors = new Map<HTMLAnchorElement, number>() - for (const anchor of getElements<HTMLAnchorElement>( - ":first-child [href]", result - )) { - const article = anchor.firstElementChild! - anchors.set(anchor, parseFloat( - article.getAttribute("data-md-score")! - )) - } - - /* Go to result with highest score, if any */ - if (anchors.size) { - const [[best]] = [...anchors].sort(([, a], [, b]) => b - a) - best.click() - } - - /* Otherwise omit form submission */ - key.claim() - } - break - - /* Escape or Tab: close search */ - case "Escape": - case "Tab": - setToggle("search", false) - query.blur() - break - - /* Vertical arrows: select previous or next search result */ - case "ArrowUp": - case "ArrowDown": - if (typeof active === "undefined") { - query.focus() - } else { - const els = [query, ...getElements( - ":not(details) > [href], summary, details[open] [href]", - result - )] - const i = Math.max(0, ( - Math.max(0, els.indexOf(active)) + els.length + ( - key.type === "ArrowUp" ? -1 : +1 - ) - ) % els.length) - els[i].focus() - } - - /* Prevent scrolling of page */ - key.claim() - break - - /* All other keys: hand to search query */ - default: - if (query !== getActiveElement()) - query.focus() - } - }) - - /* Set up global keyboard handlers */ - keyboard$ - .pipe( - filter(({ mode }) => mode === "global") - ) - .subscribe(key => { - switch (key.type) { - - /* Open search and select query */ - case "f": - case "s": - case "/": - query.focus() - query.select() - - /* Prevent scrolling of page */ - key.claim() - break - } - }) - - /* Create and return component */ - const query$ = mountSearchQuery(query, { worker$ }) - return merge( - query$, - mountSearchResult(result, { worker$, query$ }) - ) - .pipe( - mergeWith( - - /* Search sharing */ - ...getComponentElements("search-share", el) - .map(child => mountSearchShare(child, { query$ })), - - /* Search suggestions */ - ...getComponentElements("search-suggest", el) - .map(child => mountSearchSuggest(child, { worker$, keyboard$ })) - ) - ) - - /* Gracefully handle broken search */ - } catch (err) { - el.hidden = true - return NEVER - } -} diff --git a/dev/src/templates/assets/javascripts/components/search/highlight/index.ts b/dev/src/templates/assets/javascripts/components/search/highlight/index.ts deleted file mode 100644 index bc3f94c9..00000000 --- a/dev/src/templates/assets/javascripts/components/search/highlight/index.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - ObservableInput, - combineLatest, - filter, - map, - startWith -} from "rxjs" - -import { getLocation } from "~/browser" -import { - SearchIndex, - setupSearchHighlighter -} from "~/integrations" -import { h } from "~/utilities" - -import { Component } from "../../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search highlighting - */ -export interface SearchHighlight { - nodes: Map<ChildNode, string> /* Map of replacements */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - index$: ObservableInput<SearchIndex> /* Search index observable */ - location$: Observable<URL> /* Location observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount search highlighting - * - * @param el - Content element - * @param options - Options - * - * @returns Search highlighting component observable - */ -export function mountSearchHiglight( - el: HTMLElement, { index$, location$ }: MountOptions -): Observable<Component<SearchHighlight>> { - return combineLatest([ - index$, - location$ - .pipe( - startWith(getLocation()), - filter(url => !!url.searchParams.get("h")) - ) - ]) - .pipe( - map(([index, url]) => setupSearchHighlighter(index.config)( - url.searchParams.get("h")! - )), - map(fn => { - const nodes = new Map<ChildNode, string>() - - /* Traverse text nodes and collect matches */ - const it = document.createNodeIterator(el, NodeFilter.SHOW_TEXT) - for (let node = it.nextNode(); node; node = it.nextNode()) { - if (node.parentElement?.offsetHeight) { - const original = node.textContent! - const replaced = fn(original) - if (replaced.length > original.length) - nodes.set(node as ChildNode, replaced) - } - } - - /* Replace original nodes with matches */ - for (const [node, text] of nodes) { - const { childNodes } = h("span", null, text) - node.replaceWith(...Array.from(childNodes)) - } - - /* Return component */ - return { ref: el, nodes } - }) - ) -} diff --git a/dev/src/templates/assets/javascripts/components/search/index.ts b/dev/src/templates/assets/javascripts/components/search/index.ts deleted file mode 100644 index 846d8685..00000000 --- a/dev/src/templates/assets/javascripts/components/search/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./highlight" -export * from "./query" -export * from "./result" -export * from "./share" -export * from "./suggest" diff --git a/dev/src/templates/assets/javascripts/components/search/query/index.ts b/dev/src/templates/assets/javascripts/components/search/query/index.ts deleted file mode 100644 index 4ce21279..00000000 --- a/dev/src/templates/assets/javascripts/components/search/query/index.ts +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - combineLatest, - distinctUntilChanged, - distinctUntilKeyChanged, - endWith, - finalize, - first, - fromEvent, - ignoreElements, - map, - merge, - shareReplay, - takeUntil, - tap -} from "rxjs" - -import { - getElement, - getLocation, - setToggle, - watchElementFocus, - watchToggle -} from "~/browser" -import { - SearchMessage, - SearchMessageType, - isSearchReadyMessage -} from "~/integrations" - -import { Component } from "../../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search query - */ -export interface SearchQuery { - value: string /* Query value */ - focus: boolean /* Query focus */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - worker$: Subject<SearchMessage> /* Search worker */ -} - -/** - * Mount options - */ -interface MountOptions { - worker$: Subject<SearchMessage> /* Search worker */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch search query - * - * Note that the focus event which triggers re-reading the current query value - * is delayed by `1ms` so the input's empty state is allowed to propagate. - * - * @param el - Search query element - * @param options - Options - * - * @returns Search query observable - */ -export function watchSearchQuery( - el: HTMLInputElement, { worker$ }: WatchOptions -): Observable<SearchQuery> { - - /* Support search deep linking */ - const { searchParams } = getLocation() - if (searchParams.has("q")) { - setToggle("search", true) - - /* Set query from parameter */ - el.value = searchParams.get("q")! - el.focus() - - /* Remove query parameter on close */ - watchToggle("search") - .pipe( - first(active => !active) - ) - .subscribe(() => { - const url = getLocation() - url.searchParams.delete("q") - history.replaceState({}, "", `${url}`) - }) - } - - /* Intercept focus and input events */ - const focus$ = watchElementFocus(el) - const value$ = merge( - worker$.pipe(first(isSearchReadyMessage)), - fromEvent(el, "keyup"), - focus$ - ) - .pipe( - map(() => el.value), - distinctUntilChanged() - ) - - /* Combine into single observable */ - return combineLatest([value$, focus$]) - .pipe( - map(([value, focus]) => ({ value, focus })), - shareReplay(1) - ) -} - -/** - * Mount search query - * - * @param el - Search query element - * @param options - Options - * - * @returns Search query component observable - */ -export function mountSearchQuery( - el: HTMLInputElement, { worker$ }: MountOptions -): Observable<Component<SearchQuery, HTMLInputElement>> { - const push$ = new Subject<SearchQuery>() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - - /* Handle value change */ - combineLatest([ - worker$.pipe(first(isSearchReadyMessage)), - push$ - ], (_, query) => query) - .pipe( - distinctUntilKeyChanged("value") - ) - .subscribe(({ value }) => worker$.next({ - type: SearchMessageType.QUERY, - data: value - })) - - /* Handle focus change */ - push$ - .pipe( - distinctUntilKeyChanged("focus") - ) - .subscribe(({ focus }) => { - if (focus) - setToggle("search", focus) - }) - - /* Handle reset */ - fromEvent(el.form!, "reset") - .pipe( - takeUntil(done$) - ) - .subscribe(() => el.focus()) - - // Focus search query on label click - note that this is necessary to bring - // up the keyboard on iOS and other mobile platforms, as the search dialog is - // not visible at first, and programatically focusing an input element must - // be triggered by a user interaction - see https://t.ly/Cb30n - const label = getElement("header [for=__search]") - fromEvent(label, "click") - .subscribe(() => el.focus()) - - /* Create and return component */ - return watchSearchQuery(el, { worker$ }) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })), - shareReplay(1) - ) -} diff --git a/dev/src/templates/assets/javascripts/components/search/result/index.ts b/dev/src/templates/assets/javascripts/components/search/result/index.ts deleted file mode 100644 index c3c9ef20..00000000 --- a/dev/src/templates/assets/javascripts/components/search/result/index.ts +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - EMPTY, - Observable, - Subject, - bufferCount, - filter, - finalize, - first, - fromEvent, - map, - merge, - mergeMap, - of, - share, - skipUntil, - switchMap, - takeUntil, - tap, - withLatestFrom, - zipWith -} from "rxjs" - -import { translation } from "~/_" -import { - getElement, - getOptionalElement, - watchElementBoundary, - watchToggle -} from "~/browser" -import { - SearchMessage, - SearchResult, - isSearchReadyMessage, - isSearchResultMessage -} from "~/integrations" -import { renderSearchResultItem } from "~/templates" -import { round } from "~/utilities" - -import { Component } from "../../_" -import { SearchQuery } from "../query" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - query$: Observable<SearchQuery> /* Search query observable */ - worker$: Subject<SearchMessage> /* Search worker */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount search result list - * - * This function performs a lazy rendering of the search results, depending on - * the vertical offset of the search result container. - * - * @param el - Search result list element - * @param options - Options - * - * @returns Search result list component observable - */ -export function mountSearchResult( - el: HTMLElement, { worker$, query$ }: MountOptions -): Observable<Component<SearchResult>> { - const push$ = new Subject<SearchResult>() - const boundary$ = watchElementBoundary(el.parentElement!) - .pipe( - filter(Boolean) - ) - - /* Retrieve container */ - const container = el.parentElement! - - /* Retrieve nested components */ - const meta = getElement(":scope > :first-child", el) - const list = getElement(":scope > :last-child", el) - - /* Reveal to accessibility tree – see https://bit.ly/3iAA7t8 */ - watchToggle("search") - .subscribe(active => list.setAttribute( - "role", active ? "list" : "presentation" - )) - - /* Update search result metadata */ - push$ - .pipe( - withLatestFrom(query$), - skipUntil(worker$.pipe(first(isSearchReadyMessage))) - ) - .subscribe(([{ items }, { value }]) => { - switch (items.length) { - - /* No results */ - case 0: - meta.textContent = value.length - ? translation("search.result.none") - : translation("search.result.placeholder") - break - - /* One result */ - case 1: - meta.textContent = translation("search.result.one") - break - - /* Multiple result */ - default: - const count = round(items.length) - meta.textContent = translation("search.result.other", count) - } - }) - - /* Render search result item */ - const render$ = push$ - .pipe( - tap(() => list.innerHTML = ""), - switchMap(({ items }) => merge( - of(...items.slice(0, 10)), - of(...items.slice(10)) - .pipe( - bufferCount(4), - zipWith(boundary$), - switchMap(([chunk]) => chunk) - ) - )), - map(renderSearchResultItem), - share() - ) - - /* Update search result list */ - render$.subscribe(item => list.appendChild(item)) - render$ - .pipe( - mergeMap(item => { - const details = getOptionalElement("details", item) - if (typeof details === "undefined") - return EMPTY - - /* Keep position of details element stable */ - return fromEvent(details, "toggle") - .pipe( - takeUntil(push$), - map(() => details) - ) - }) - ) - .subscribe(details => { - if ( - details.open === false && - details.offsetTop <= container.scrollTop - ) - container.scrollTo({ top: details.offsetTop }) - }) - - /* Filter search result message */ - const result$ = worker$ - .pipe( - filter(isSearchResultMessage), - map(({ data }) => data) - ) - - /* Create and return component */ - return result$ - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) -} diff --git a/dev/src/templates/assets/javascripts/components/search/share/index.ts b/dev/src/templates/assets/javascripts/components/search/share/index.ts deleted file mode 100644 index 3db382c8..00000000 --- a/dev/src/templates/assets/javascripts/components/search/share/index.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - endWith, - finalize, - fromEvent, - ignoreElements, - map, - takeUntil, - tap -} from "rxjs" - -import { getLocation } from "~/browser" - -import { Component } from "../../_" -import { SearchQuery } from "../query" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search sharing - */ -export interface SearchShare { - url: URL /* Deep link for sharing */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - query$: Observable<SearchQuery> /* Search query observable */ -} - -/** - * Mount options - */ -interface MountOptions { - query$: Observable<SearchQuery> /* Search query observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount search sharing - * - * @param _el - Search sharing element - * @param options - Options - * - * @returns Search sharing observable - */ -export function watchSearchShare( - _el: HTMLElement, { query$ }: WatchOptions -): Observable<SearchShare> { - return query$ - .pipe( - map(({ value }) => { - const url = getLocation() - url.hash = "" - - /* Compute readable query strings */ - value = value - .replace(/\s+/g, "+") /* Collapse whitespace */ - .replace(/&/g, "%26") /* Escape '&' character */ - .replace(/=/g, "%3D") /* Escape '=' character */ - - /* Replace query string */ - url.search = `q=${value}` - return { url } - }) - ) -} - -/** - * Mount search sharing - * - * @param el - Search sharing element - * @param options - Options - * - * @returns Search sharing component observable - */ -export function mountSearchShare( - el: HTMLAnchorElement, options: MountOptions -): Observable<Component<SearchShare>> { - const push$ = new Subject<SearchShare>() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - push$.subscribe(({ url }) => { - el.setAttribute("data-clipboard-text", el.href) - el.href = `${url}` - }) - - /* Prevent following of link */ - fromEvent(el, "click") - .pipe( - takeUntil(done$) - ) - .subscribe(ev => ev.preventDefault()) - - /* Create and return component */ - return watchSearchShare(el, options) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) -} diff --git a/dev/src/templates/assets/javascripts/components/search/suggest/index.ts b/dev/src/templates/assets/javascripts/components/search/suggest/index.ts deleted file mode 100644 index e7881475..00000000 --- a/dev/src/templates/assets/javascripts/components/search/suggest/index.ts +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - asyncScheduler, - combineLatestWith, - distinctUntilChanged, - filter, - finalize, - fromEvent, - map, - merge, - observeOn, - tap -} from "rxjs" - -import { Keyboard } from "~/browser" -import { - SearchMessage, - SearchResult, - isSearchResultMessage -} from "~/integrations" - -import { Component, getComponentElement } from "../../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search suggestions - */ -export interface SearchSuggest {} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - keyboard$: Observable<Keyboard> /* Keyboard observable */ - worker$: Subject<SearchMessage> /* Search worker */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount search suggestions - * - * This function will perform a lazy rendering of the search results, depending - * on the vertical offset of the search result container. - * - * @param el - Search result list element - * @param options - Options - * - * @returns Search result list component observable - */ -export function mountSearchSuggest( - el: HTMLElement, { worker$, keyboard$ }: MountOptions -): Observable<Component<SearchSuggest>> { - const push$ = new Subject<SearchResult>() - - /* Retrieve query component and track all changes */ - const query = getComponentElement("search-query") - const query$ = merge( - fromEvent(query, "keydown"), - fromEvent(query, "focus") - ) - .pipe( - observeOn(asyncScheduler), - map(() => query.value), - distinctUntilChanged(), - ) - - /* Update search suggestions */ - push$ - .pipe( - combineLatestWith(query$), - map(([{ suggest }, value]) => { - const words = value.split(/([\s-]+)/) - if (suggest?.length && words[words.length - 1]) { - const last = suggest[suggest.length - 1] - if (last.startsWith(words[words.length - 1])) - words[words.length - 1] = last - } else { - words.length = 0 - } - return words - }) - ) - .subscribe(words => el.innerHTML = words - .join("") - .replace(/\s/g, " ") - ) - - /* Set up search keyboard handlers */ - keyboard$ - .pipe( - filter(({ mode }) => mode === "search") - ) - .subscribe(key => { - switch (key.type) { - - /* Right arrow: accept current suggestion */ - case "ArrowRight": - if ( - el.innerText.length && - query.selectionStart === query.value.length - ) - query.value = el.innerText - break - } - }) - - /* Filter search result message */ - const result$ = worker$ - .pipe( - filter(isSearchResultMessage), - map(({ data }) => data) - ) - - /* Create and return component */ - return result$ - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(() => ({ ref: el })) - ) -} diff --git a/dev/src/templates/assets/javascripts/components/sidebar/index.ts b/dev/src/templates/assets/javascripts/components/sidebar/index.ts deleted file mode 100644 index 82f3d03e..00000000 --- a/dev/src/templates/assets/javascripts/components/sidebar/index.ts +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - animationFrameScheduler, - asyncScheduler, - auditTime, - combineLatest, - defer, - distinctUntilChanged, - endWith, - finalize, - first, - from, - fromEvent, - ignoreElements, - map, - mergeMap, - observeOn, - takeUntil, - tap, - withLatestFrom -} from "rxjs" - -import { - Viewport, - getElement, - getElementContainer, - getElementOffset, - getElementSize, - getElements -} from "~/browser" - -import { Component } from "../_" -import { Header } from "../header" -import { Main } from "../main" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Sidebar - */ -export interface Sidebar { - height: number /* Sidebar height */ - locked: boolean /* Sidebar is locked */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - main$: Observable<Main> /* Main area observable */ -} - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ - main$: Observable<Main> /* Main area observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch sidebar - * - * This function returns an observable that computes the visual parameters of - * the sidebar which depends on the vertical viewport offset, as well as the - * height of the main area. When the page is scrolled beyond the header, the - * sidebar is locked and fills the remaining space. - * - * @param el - Sidebar element - * @param options - Options - * - * @returns Sidebar observable - */ -export function watchSidebar( - el: HTMLElement, { viewport$, main$ }: WatchOptions -): Observable<Sidebar> { - const parent = el.closest<HTMLElement>(".md-grid")! - const adjust = - parent.offsetTop - - parent.parentElement!.offsetTop - - /* Compute the sidebar's available height and if it should be locked */ - return combineLatest([main$, viewport$]) - .pipe( - map(([{ offset, height }, { offset: { y } }]) => { - height = height - + Math.min(adjust, Math.max(0, y - offset)) - - adjust - return { - height, - locked: y >= offset + adjust - } - }), - distinctUntilChanged((a, b) => ( - a.height === b.height && - a.locked === b.locked - )) - ) -} - -/** - * Mount sidebar - * - * This function doesn't set the height of the actual sidebar, but of its first - * child – the `.md-sidebar__scrollwrap` element in order to mitigiate jittery - * sidebars when the footer is scrolled into view. At some point we switched - * from `absolute` / `fixed` positioning to `sticky` positioning, significantly - * reducing jitter in some browsers (respectively Firefox and Safari) when - * scrolling from the top. However, top-aligned sticky positioning means that - * the sidebar snaps to the bottom when the end of the container is reached. - * This is what leads to the mentioned jitter, as the sidebar's height may be - * updated too slowly. - * - * This behaviour can be mitigiated by setting the height of the sidebar to `0` - * while preserving the padding, and the height on its first element. - * - * @param el - Sidebar element - * @param options - Options - * - * @returns Sidebar component observable - */ -export function mountSidebar( - el: HTMLElement, { header$, ...options }: MountOptions -): Observable<Component<Sidebar>> { - const inner = getElement(".md-sidebar__scrollwrap", el) - const { y } = getElementOffset(inner) - return defer(() => { - const push$ = new Subject<Sidebar>() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - const next$ = push$ - .pipe( - auditTime(0, animationFrameScheduler) - ) - - /* Update sidebar height and offset */ - next$.pipe(withLatestFrom(header$)) - .subscribe({ - - /* Handle emission */ - next([{ height }, { height: offset }]) { - inner.style.height = `${height - 2 * y}px` - el.style.top = `${offset}px` - }, - - /* Handle complete */ - complete() { - inner.style.height = "" - el.style.top = "" - } - }) - - /* Bring active item into view on initial load */ - next$.pipe(first()) - .subscribe(() => { - for (const item of getElements(".md-nav__link--active[href]", el)) { - const container = getElementContainer(item) - if (typeof container !== "undefined") { - const offset = item.offsetTop - container.offsetTop - const { height } = getElementSize(container) - container.scrollTo({ - top: offset - height / 2 - }) - } - } - }) - - /* Handle accessibility for expandable items, see https://bit.ly/3jaod9p */ - from(getElements<HTMLLabelElement>("label[tabindex]", el)) - .pipe( - mergeMap(label => fromEvent(label, "click") - .pipe( - observeOn(asyncScheduler), - map(() => label), - takeUntil(done$) - ) - ) - ) - .subscribe(label => { - const input = getElement<HTMLInputElement>(`[id="${label.htmlFor}"]`) - const nav = getElement(`[aria-labelledby="${label.id}"]`) - nav.setAttribute("aria-expanded", `${input.checked}`) - }) - - /* Create and return component */ - return watchSidebar(el, options) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/dev/src/templates/assets/javascripts/components/source/_/index.ts b/dev/src/templates/assets/javascripts/components/source/_/index.ts deleted file mode 100644 index 5f6c4d11..00000000 --- a/dev/src/templates/assets/javascripts/components/source/_/index.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - EMPTY, - Observable, - Subject, - catchError, - defer, - filter, - finalize, - map, - of, - shareReplay, - tap -} from "rxjs" - -import { getElement } from "~/browser" -import { ConsentDefaults } from "~/components/consent" -import { renderSourceFacts } from "~/templates" - -import { - Component, - getComponentElements -} from "../../_" -import { - SourceFacts, - fetchSourceFacts -} from "../facts" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Repository information - */ -export interface Source { - facts: SourceFacts /* Repository facts */ -} - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Repository information observable - */ -let fetch$: Observable<Source> - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch repository information - * - * This function tries to read the repository facts from session storage, and - * if unsuccessful, fetches them from the underlying provider. - * - * @param el - Repository information element - * - * @returns Repository information observable - */ -export function watchSource( - el: HTMLAnchorElement -): Observable<Source> { - return fetch$ ||= defer(() => { - const cached = __md_get<SourceFacts>("__source", sessionStorage) - if (cached) { - return of(cached) - } else { - - /* Check if consent is configured and was given */ - const els = getComponentElements("consent") - if (els.length) { - const consent = __md_get<ConsentDefaults>("__consent") - if (!(consent && consent.github)) - return EMPTY - } - - /* Fetch repository facts */ - return fetchSourceFacts(el.href) - .pipe( - tap(facts => __md_set("__source", facts, sessionStorage)) - ) - } - }) - .pipe( - catchError(() => EMPTY), - filter(facts => Object.keys(facts).length > 0), - map(facts => ({ facts })), - shareReplay(1) - ) -} - -/** - * Mount repository information - * - * @param el - Repository information element - * - * @returns Repository information component observable - */ -export function mountSource( - el: HTMLAnchorElement -): Observable<Component<Source>> { - const inner = getElement(":scope > :last-child", el) - return defer(() => { - const push$ = new Subject<Source>() - push$.subscribe(({ facts }) => { - inner.appendChild(renderSourceFacts(facts)) - inner.classList.add("md-source__repository--active") - }) - - /* Create and return component */ - return watchSource(el) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/dev/src/templates/assets/javascripts/components/source/facts/_/index.ts b/dev/src/templates/assets/javascripts/components/source/facts/_/index.ts deleted file mode 100644 index 154f229f..00000000 --- a/dev/src/templates/assets/javascripts/components/source/facts/_/index.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { EMPTY, Observable } from "rxjs" - -import { fetchSourceFactsFromGitHub } from "../github" -import { fetchSourceFactsFromGitLab } from "../gitlab" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Repository facts for repositories - */ -export interface RepositoryFacts { - stars?: number /* Number of stars */ - forks?: number /* Number of forks */ - version?: string /* Latest version */ -} - -/** - * Repository facts for organizations - */ -export interface OrganizationFacts { - repositories?: number /* Number of repositories */ -} - -/* ------------------------------------------------------------------------- */ - -/** - * Repository facts - */ -export type SourceFacts = - | RepositoryFacts - | OrganizationFacts - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Fetch repository facts - * - * @param url - Repository URL - * - * @returns Repository facts observable - */ -export function fetchSourceFacts( - url: string -): Observable<SourceFacts> { - - /* Try to match GitHub repository */ - let match = url.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i) - if (match) { - const [, user, repo] = match - return fetchSourceFactsFromGitHub(user, repo) - } - - /* Try to match GitLab repository */ - match = url.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i) - if (match) { - const [, base, slug] = match - return fetchSourceFactsFromGitLab(base, slug) - } - - /* Fallback */ - return EMPTY -} diff --git a/dev/src/templates/assets/javascripts/components/source/facts/github/index.ts b/dev/src/templates/assets/javascripts/components/source/facts/github/index.ts deleted file mode 100644 index 12cc55e0..00000000 --- a/dev/src/templates/assets/javascripts/components/source/facts/github/index.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { Repo, User } from "github-types" -import { - EMPTY, - Observable, - catchError, - defaultIfEmpty, - map, - zip -} from "rxjs" - -import { requestJSON } from "~/browser" - -import { SourceFacts } from "../_" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * GitHub release (partial) - */ -interface Release { - tag_name: string /* Tag name */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Fetch GitHub repository facts - * - * @param user - GitHub user or organization - * @param repo - GitHub repository - * - * @returns Repository facts observable - */ -export function fetchSourceFactsFromGitHub( - user: string, repo?: string -): Observable<SourceFacts> { - if (typeof repo !== "undefined") { - const url = `https://api.github.com/repos/${user}/${repo}` - return zip( - - /* Fetch version */ - requestJSON<Release>(`${url}/releases/latest`) - .pipe( - catchError(() => EMPTY), // @todo refactor instant loading - map(release => ({ - version: release.tag_name - })), - defaultIfEmpty({}) - ), - - /* Fetch stars and forks */ - requestJSON<Repo>(url) - .pipe( - catchError(() => EMPTY), // @todo refactor instant loading - map(info => ({ - stars: info.stargazers_count, - forks: info.forks_count - })), - defaultIfEmpty({}) - ) - ) - .pipe( - map(([release, info]) => ({ ...release, ...info })) - ) - - /* User or organization */ - } else { - const url = `https://api.github.com/users/${user}` - return requestJSON<User>(url) - .pipe( - map(info => ({ - repositories: info.public_repos - })), - defaultIfEmpty({}) - ) - } -} diff --git a/dev/src/templates/assets/javascripts/components/source/facts/gitlab/index.ts b/dev/src/templates/assets/javascripts/components/source/facts/gitlab/index.ts deleted file mode 100644 index d85d4afd..00000000 --- a/dev/src/templates/assets/javascripts/components/source/facts/gitlab/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { ProjectSchema } from "gitlab" -import { - EMPTY, - Observable, - catchError, - defaultIfEmpty, - map -} from "rxjs" - -import { requestJSON } from "~/browser" - -import { SourceFacts } from "../_" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Fetch GitLab repository facts - * - * @param base - GitLab base - * @param project - GitLab project - * - * @returns Repository facts observable - */ -export function fetchSourceFactsFromGitLab( - base: string, project: string -): Observable<SourceFacts> { - const url = `https://${base}/api/v4/projects/${encodeURIComponent(project)}` - return requestJSON<ProjectSchema>(url) - .pipe( - catchError(() => EMPTY), // @todo refactor instant loading - map(({ star_count, forks_count }) => ({ - stars: star_count, - forks: forks_count - })), - defaultIfEmpty({}) - ) -} diff --git a/dev/src/templates/assets/javascripts/components/source/facts/index.ts b/dev/src/templates/assets/javascripts/components/source/facts/index.ts deleted file mode 100644 index f9bda64d..00000000 --- a/dev/src/templates/assets/javascripts/components/source/facts/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./github" -export * from "./gitlab" diff --git a/dev/src/templates/assets/javascripts/components/source/index.ts b/dev/src/templates/assets/javascripts/components/source/index.ts deleted file mode 100644 index 7fac4813..00000000 --- a/dev/src/templates/assets/javascripts/components/source/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./facts" diff --git a/dev/src/templates/assets/javascripts/components/tabs/index.ts b/dev/src/templates/assets/javascripts/components/tabs/index.ts deleted file mode 100644 index 1e69df28..00000000 --- a/dev/src/templates/assets/javascripts/components/tabs/index.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - defer, - distinctUntilKeyChanged, - finalize, - map, - of, - switchMap, - tap -} from "rxjs" - -import { feature } from "~/_" -import { - Viewport, - watchElementSize, - watchViewportAt -} from "~/browser" - -import { Component } from "../_" -import { Header } from "../header" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Navigation tabs - */ -export interface Tabs { - hidden: boolean /* Navigation tabs are hidden */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ -} - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch navigation tabs - * - * @param el - Navigation tabs element - * @param options - Options - * - * @returns Navigation tabs observable - */ -export function watchTabs( - el: HTMLElement, { viewport$, header$ }: WatchOptions -): Observable<Tabs> { - return watchElementSize(document.body) - .pipe( - switchMap(() => watchViewportAt(el, { header$, viewport$ })), - map(({ offset: { y } }) => { - return { - hidden: y >= 10 - } - }), - distinctUntilKeyChanged("hidden") - ) -} - -/** - * Mount navigation tabs - * - * This function hides the navigation tabs when scrolling past the threshold - * and makes them reappear in a nice CSS animation when scrolling back up. - * - * @param el - Navigation tabs element - * @param options - Options - * - * @returns Navigation tabs component observable - */ -export function mountTabs( - el: HTMLElement, options: MountOptions -): Observable<Component<Tabs>> { - return defer(() => { - const push$ = new Subject<Tabs>() - push$.subscribe({ - - /* Handle emission */ - next({ hidden }) { - el.hidden = hidden - }, - - /* Handle complete */ - complete() { - el.hidden = false - } - }) - - /* Create and return component */ - return ( - feature("navigation.tabs.sticky") - ? of({ hidden: false }) - : watchTabs(el, options) - ) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/dev/src/templates/assets/javascripts/components/toc/index.ts b/dev/src/templates/assets/javascripts/components/toc/index.ts deleted file mode 100644 index 04b8d85f..00000000 --- a/dev/src/templates/assets/javascripts/components/toc/index.ts +++ /dev/null @@ -1,379 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - asyncScheduler, - bufferCount, - combineLatestWith, - debounceTime, - defer, - distinctUntilChanged, - distinctUntilKeyChanged, - endWith, - filter, - finalize, - ignoreElements, - map, - merge, - observeOn, - of, - repeat, - scan, - share, - skip, - startWith, - switchMap, - takeUntil, - tap, - withLatestFrom -} from "rxjs" - -import { feature } from "~/_" -import { - Viewport, - getElement, - getElementContainer, - getElementSize, - getElements, - getLocation, - getOptionalElement, - watchElementSize -} from "~/browser" - -import { - Component, - getComponentElement -} from "../_" -import { Header } from "../header" -import { Main } from "../main" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Table of contents - */ -export interface TableOfContents { - prev: HTMLAnchorElement[][] /* Anchors (previous) */ - next: HTMLAnchorElement[][] /* Anchors (next) */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ -} - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ - main$: Observable<Main> /* Main area observable */ - target$: Observable<HTMLElement> /* Location target observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch table of contents - * - * This is effectively a scroll spy implementation which will account for the - * fixed header and automatically re-calculate anchor offsets when the viewport - * is resized. The returned observable will only emit if the table of contents - * needs to be repainted. - * - * This implementation tracks an anchor element's entire path starting from its - * level up to the top-most anchor element, e.g. `[h3, h2, h1]`. Although the - * Material theme currently doesn't make use of this information, it enables - * the styling of the entire hierarchy through customization. - * - * Note that the current anchor is the last item of the `prev` anchor list. - * - * @param el - Table of contents element - * @param options - Options - * - * @returns Table of contents observable - */ -export function watchTableOfContents( - el: HTMLElement, { viewport$, header$ }: WatchOptions -): Observable<TableOfContents> { - const table = new Map<HTMLAnchorElement, HTMLElement>() - - /* Compute anchor-to-target mapping */ - const anchors = getElements<HTMLAnchorElement>("[href^=\\#]", el) - for (const anchor of anchors) { - const id = decodeURIComponent(anchor.hash.substring(1)) - const target = getOptionalElement(`[id="${id}"]`) - if (typeof target !== "undefined") - table.set(anchor, target) - } - - /* Compute necessary adjustment for header */ - const adjust$ = header$ - .pipe( - distinctUntilKeyChanged("height"), - map(({ height }) => { - const main = getComponentElement("main") - const grid = getElement(":scope > :first-child", main) - return height + 0.8 * ( - grid.offsetTop - - main.offsetTop - ) - }), - share() - ) - - /* Compute partition of previous and next anchors */ - const partition$ = watchElementSize(document.body) - .pipe( - distinctUntilKeyChanged("height"), - - /* Build index to map anchor paths to vertical offsets */ - switchMap(body => defer(() => { - let path: HTMLAnchorElement[] = [] - return of([...table].reduce((index, [anchor, target]) => { - while (path.length) { - const last = table.get(path[path.length - 1])! - if (last.tagName >= target.tagName) { - path.pop() - } else { - break - } - } - - /* If the current anchor is hidden, continue with its parent */ - let offset = target.offsetTop - while (!offset && target.parentElement) { - target = target.parentElement - offset = target.offsetTop - } - - /* Fix anchor offsets in tables - see https://bit.ly/3CUFOcn */ - let parent = target.offsetParent as HTMLElement - for (; parent; parent = parent.offsetParent as HTMLElement) - offset += parent.offsetTop - - /* Map reversed anchor path to vertical offset */ - return index.set( - [...path = [...path, anchor]].reverse(), - offset - ) - }, new Map<HTMLAnchorElement[], number>())) - }) - .pipe( - - /* Sort index by vertical offset (see https://bit.ly/30z6QSO) */ - map(index => new Map([...index].sort(([, a], [, b]) => a - b))), - combineLatestWith(adjust$), - - /* Re-compute partition when viewport offset changes */ - switchMap(([index, adjust]) => viewport$ - .pipe( - scan(([prev, next], { offset: { y }, size }) => { - const last = y + size.height >= Math.floor(body.height) - - /* Look forward */ - while (next.length) { - const [, offset] = next[0] - if (offset - adjust < y || last) { - prev = [...prev, next.shift()!] - } else { - break - } - } - - /* Look backward */ - while (prev.length) { - const [, offset] = prev[prev.length - 1] - if (offset - adjust >= y && !last) { - next = [prev.pop()!, ...next] - } else { - break - } - } - - /* Return partition */ - return [prev, next] - }, [[], [...index]]), - distinctUntilChanged((a, b) => ( - a[0] === b[0] && - a[1] === b[1] - )) - ) - ) - ) - ) - ) - - /* Compute and return anchor list migrations */ - return partition$ - .pipe( - map(([prev, next]) => ({ - prev: prev.map(([path]) => path), - next: next.map(([path]) => path) - })), - - /* Extract anchor list migrations */ - startWith({ prev: [], next: [] }), - bufferCount(2, 1), - map(([a, b]) => { - - /* Moving down */ - if (a.prev.length < b.prev.length) { - return { - prev: b.prev.slice(Math.max(0, a.prev.length - 1), b.prev.length), - next: [] - } - - /* Moving up */ - } else { - return { - prev: b.prev.slice(-1), - next: b.next.slice(0, b.next.length - a.next.length) - } - } - }) - ) -} - -/* ------------------------------------------------------------------------- */ - -/** - * Mount table of contents - * - * @param el - Table of contents element - * @param options - Options - * - * @returns Table of contents component observable - */ -export function mountTableOfContents( - el: HTMLElement, { viewport$, header$, main$, target$ }: MountOptions -): Observable<Component<TableOfContents>> { - return defer(() => { - const push$ = new Subject<TableOfContents>() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - push$.subscribe(({ prev, next }) => { - - /* Look forward */ - for (const [anchor] of next) { - anchor.classList.remove("md-nav__link--passed") - anchor.classList.remove("md-nav__link--active") - } - - /* Look backward */ - for (const [index, [anchor]] of prev.entries()) { - anchor.classList.add("md-nav__link--passed") - anchor.classList.toggle( - "md-nav__link--active", - index === prev.length - 1 - ) - } - }) - - /* Set up following, if enabled */ - if (feature("toc.follow")) { - - /* Toggle smooth scrolling only for anchor clicks */ - const smooth$ = merge( - viewport$.pipe(debounceTime(1), map(() => undefined)), - viewport$.pipe(debounceTime(250), map(() => "smooth" as const)) - ) - - /* Bring active anchor into view */ // @todo: refactor - push$ - .pipe( - filter(({ prev }) => prev.length > 0), - combineLatestWith(main$.pipe(observeOn(asyncScheduler))), - withLatestFrom(smooth$) - ) - .subscribe(([[{ prev }], behavior]) => { - const [anchor] = prev[prev.length - 1] - if (anchor.offsetHeight) { - - /* Retrieve overflowing container and scroll */ - const container = getElementContainer(anchor) - if (typeof container !== "undefined") { - const offset = anchor.offsetTop - container.offsetTop - const { height } = getElementSize(container) - container.scrollTo({ - top: offset - height / 2, - behavior - }) - } - } - }) - } - - /* Set up anchor tracking, if enabled */ - if (feature("navigation.tracking")) - viewport$ - .pipe( - takeUntil(done$), - distinctUntilKeyChanged("offset"), - debounceTime(250), - skip(1), - takeUntil(target$.pipe(skip(1))), - repeat({ delay: 250 }), - withLatestFrom(push$) - ) - .subscribe(([, { prev }]) => { - const url = getLocation() - - /* Set hash fragment to active anchor */ - const anchor = prev[prev.length - 1] - if (anchor && anchor.length) { - const [active] = anchor - const { hash } = new URL(active.href) - if (url.hash !== hash) { - url.hash = hash - history.replaceState({}, "", `${url}`) - } - - /* Reset anchor when at the top */ - } else { - url.hash = "" - history.replaceState({}, "", `${url}`) - } - }) - - /* Create and return component */ - return watchTableOfContents(el, { viewport$, header$ }) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/dev/src/templates/assets/javascripts/components/top/index.ts b/dev/src/templates/assets/javascripts/components/top/index.ts deleted file mode 100644 index 82e88b61..00000000 --- a/dev/src/templates/assets/javascripts/components/top/index.ts +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - bufferCount, - combineLatest, - distinctUntilChanged, - distinctUntilKeyChanged, - endWith, - finalize, - fromEvent, - ignoreElements, - map, - repeat, - skip, - takeUntil, - tap -} from "rxjs" - -import { Viewport } from "~/browser" - -import { Component } from "../_" -import { Header } from "../header" -import { Main } from "../main" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Back-to-top button - */ -export interface BackToTop { - hidden: boolean /* Back-to-top button is hidden */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - main$: Observable<Main> /* Main area observable */ - target$: Observable<HTMLElement> /* Location target observable */ -} - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ - main$: Observable<Main> /* Main area observable */ - target$: Observable<HTMLElement> /* Location target observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch back-to-top - * - * @param _el - Back-to-top element - * @param options - Options - * - * @returns Back-to-top observable - */ -export function watchBackToTop( - _el: HTMLElement, { viewport$, main$, target$ }: WatchOptions -): Observable<BackToTop> { - - /* Compute direction */ - const direction$ = viewport$ - .pipe( - map(({ offset: { y } }) => y), - bufferCount(2, 1), - map(([a, b]) => a > b && b > 0), - distinctUntilChanged() - ) - - /* Compute whether main area is active */ - const active$ = main$ - .pipe( - map(({ active }) => active) - ) - - /* Compute threshold for hiding */ - return combineLatest([active$, direction$]) - .pipe( - map(([active, direction]) => !(active && direction)), - distinctUntilChanged(), - takeUntil(target$.pipe(skip(1))), - endWith(true), - repeat({ delay: 250 }), - map(hidden => ({ hidden })) - ) -} - -/* ------------------------------------------------------------------------- */ - -/** - * Mount back-to-top - * - * @param el - Back-to-top element - * @param options - Options - * - * @returns Back-to-top component observable - */ -export function mountBackToTop( - el: HTMLElement, { viewport$, header$, main$, target$ }: MountOptions -): Observable<Component<BackToTop>> { - const push$ = new Subject<BackToTop>() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - push$.subscribe({ - - /* Handle emission */ - next({ hidden }) { - el.hidden = hidden - if (hidden) { - el.setAttribute("tabindex", "-1") - el.blur() - } else { - el.removeAttribute("tabindex") - } - }, - - /* Handle complete */ - complete() { - el.style.top = "" - el.hidden = true - el.removeAttribute("tabindex") - } - }) - - /* Watch header height */ - header$ - .pipe( - takeUntil(done$), - distinctUntilKeyChanged("height") - ) - .subscribe(({ height }) => { - el.style.top = `${height + 16}px` - }) - - /* Go back to top */ - fromEvent(el, "click") - .subscribe(ev => { - ev.preventDefault() - window.scrollTo({ top: 0 }) - }) - - /* Create and return component */ - return watchBackToTop(el, { viewport$, main$, target$ }) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) -} |
