aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src/templates/assets/javascripts/components/search
diff options
context:
space:
mode:
Diffstat (limited to 'src/templates/assets/javascripts/components/search')
-rw-r--r--src/templates/assets/javascripts/components/search/_/index.ts239
-rw-r--r--src/templates/assets/javascripts/components/search/highlight/.eslintrc5
-rw-r--r--src/templates/assets/javascripts/components/search/highlight/index.ts115
-rw-r--r--src/templates/assets/javascripts/components/search/index.ts28
-rw-r--r--src/templates/assets/javascripts/components/search/query/index.ts206
-rw-r--r--src/templates/assets/javascripts/components/search/result/index.ts197
-rw-r--r--src/templates/assets/javascripts/components/search/share/index.ts135
-rw-r--r--src/templates/assets/javascripts/components/search/suggest/index.ts154
8 files changed, 0 insertions, 1079 deletions
diff --git a/src/templates/assets/javascripts/components/search/_/index.ts b/src/templates/assets/javascripts/components/search/_/index.ts
deleted file mode 100644
index aa963b47..00000000
--- a/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/src/templates/assets/javascripts/components/search/highlight/.eslintrc b/src/templates/assets/javascripts/components/search/highlight/.eslintrc
deleted file mode 100644
index 38a5714d..00000000
--- a/src/templates/assets/javascripts/components/search/highlight/.eslintrc
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "rules": {
- "no-null/no-null": "off"
- }
-}
diff --git a/src/templates/assets/javascripts/components/search/highlight/index.ts b/src/templates/assets/javascripts/components/search/highlight/index.ts
deleted file mode 100644
index bc3f94c9..00000000
--- a/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/src/templates/assets/javascripts/components/search/index.ts b/src/templates/assets/javascripts/components/search/index.ts
deleted file mode 100644
index 846d8685..00000000
--- a/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/src/templates/assets/javascripts/components/search/query/index.ts b/src/templates/assets/javascripts/components/search/query/index.ts
deleted file mode 100644
index 4ce21279..00000000
--- a/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/src/templates/assets/javascripts/components/search/result/index.ts b/src/templates/assets/javascripts/components/search/result/index.ts
deleted file mode 100644
index c3c9ef20..00000000
--- a/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/src/templates/assets/javascripts/components/search/share/index.ts b/src/templates/assets/javascripts/components/search/share/index.ts
deleted file mode 100644
index 3db382c8..00000000
--- a/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/src/templates/assets/javascripts/components/search/suggest/index.ts b/src/templates/assets/javascripts/components/search/suggest/index.ts
deleted file mode 100644
index e7881475..00000000
--- a/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, "&nbsp;")
- )
-
- /* 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 }))
- )
-}