mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-06-14 11:52:32 +03:00
Merge of Insiders features tied to 'Biquinho Vermelho' funding goal
This commit is contained in:
parent
ffd62bca3c
commit
d68fe9102a
29
material/assets/javascripts/bundle.716f8af4.min.js
vendored
Normal file
29
material/assets/javascripts/bundle.716f8af4.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
material/assets/javascripts/bundle.716f8af4.min.js.map
Normal file
7
material/assets/javascripts/bundle.716f8af4.min.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
material/assets/stylesheets/main.fe914879.min.css
vendored
Normal file
2
material/assets/stylesheets/main.fe914879.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
material/assets/stylesheets/main.fe914879.min.css.map
Normal file
1
material/assets/stylesheets/main.fe914879.min.css.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
material/assets/stylesheets/palette.ba0d045b.min.css.map
Normal file
1
material/assets/stylesheets/palette.ba0d045b.min.css.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -39,10 +39,10 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.3754935a.min.css' | url }}">
|
||||
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.fe914879.min.css' | url }}">
|
||||
{% if config.theme.palette %}
|
||||
{% set palette = config.theme.palette %}
|
||||
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.f1a3b89f.min.css' | url }}">
|
||||
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.ba0d045b.min.css' | url }}">
|
||||
{% if palette.primary %}
|
||||
{% import "partials/palette.html" as map %}
|
||||
{% set primary = map.primary(
|
||||
@ -196,7 +196,7 @@
|
||||
"base": base_url,
|
||||
"features": features,
|
||||
"translations": {},
|
||||
"search": "assets/javascripts/workers/search.477d984a.min.js" | url,
|
||||
"search": "assets/javascripts/workers/search.53c85856.min.js" | url,
|
||||
"version": config.extra.version or None
|
||||
} -%}
|
||||
{%- set translations = app.translations -%}
|
||||
@ -223,7 +223,7 @@
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="{{ 'assets/javascripts/bundle.ddd52ceb.min.js' | url }}"></script>
|
||||
<script src="{{ 'assets/javascripts/bundle.716f8af4.min.js' | url }}"></script>
|
||||
{% for path in config["extra_javascript"] %}
|
||||
<script src="{{ path | url }}"></script>
|
||||
{% endfor %}
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -35,5 +35,5 @@
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{ 'overrides/assets/javascripts/bundle.89b9c269.min.js' | url }}"></script>
|
||||
<script src="{{ 'overrides/assets/javascripts/bundle.1d33a92e.min.js' | url }}"></script>
|
||||
{% endblock %}
|
||||
|
@ -12,6 +12,8 @@
|
||||
"meta.source": "Quellcode",
|
||||
"search.config.lang": "de",
|
||||
"search.placeholder": "Suche",
|
||||
"search.share": "Teilen",
|
||||
"search.reset": "Zurücksetzen",
|
||||
"search.result.initializer": "Suche wird initialisiert",
|
||||
"search.result.placeholder": "Suchbegriff eingeben",
|
||||
"search.result.none": "Keine Suchergebnisse",
|
||||
@ -20,6 +22,7 @@
|
||||
"search.result.more.one": "1 weiteres Suchergebnis auf dieser Seite",
|
||||
"search.result.more.other": "# weitere Suchergebnisse auf dieser Seite",
|
||||
"search.result.term.missing": "Es fehlt",
|
||||
"search.title": "Suche",
|
||||
"select.language.title": "Sprache wechseln",
|
||||
"select.version.title": "Version auswählen",
|
||||
"skip.link.title": "Zum Inhalt",
|
||||
|
@ -18,6 +18,7 @@
|
||||
"search.config.pipeline": "trimmer, stopWordFilter",
|
||||
"search.config.separator": "[\s\-]+",
|
||||
"search.placeholder": "Search",
|
||||
"search.share": "Share",
|
||||
"search.reset": "Clear",
|
||||
"search.result.initializer": "Initializing search",
|
||||
"search.result.placeholder": "Type to start searching",
|
||||
@ -27,6 +28,7 @@
|
||||
"search.result.more.one": "1 more on this page",
|
||||
"search.result.more.other": "# more on this page",
|
||||
"search.result.term.missing": "Missing",
|
||||
"search.title": "Search",
|
||||
"select.language.title": "Select language",
|
||||
"select.version.title": "Select version",
|
||||
"skip.link.title": "Skip to content",
|
||||
|
@ -6,14 +6,24 @@
|
||||
<label class="md-search__overlay" for="__search"></label>
|
||||
<div class="md-search__inner" role="search">
|
||||
<form class="md-search__form" name="search">
|
||||
<input type="text" class="md-search__input" name="query" aria-label="{{ lang.t('search.placeholder') }}" placeholder="{{ lang.t('search.placeholder') }}" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" data-md-state="active" required>
|
||||
<input type="text" class="md-search__input" name="query" aria-label="{{ lang.t('search.placeholder') }}" placeholder="{{ lang.t('search.placeholder') }}" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
|
||||
<label class="md-search__icon md-icon" for="__search">
|
||||
{% include ".icons/material/magnify.svg" %}
|
||||
{% include ".icons/material/arrow-left.svg" %}
|
||||
</label>
|
||||
<button type="reset" class="md-search__icon md-icon" aria-label="{{ lang.t('search.reset') }}" tabindex="-1">
|
||||
{% include ".icons/material/close.svg" %}
|
||||
</button>
|
||||
<nav class="md-search__options" aria-label="{{ lang.t('search.title') }}">
|
||||
{% if "search.share" in features %}
|
||||
<a href="javascript:void(0)" class="md-search__icon md-icon" aria-label="{{ lang.t('search.share') }}" data-clipboard data-clipboard-text="" data-md-component="search-share" tabindex="-1">
|
||||
{% include ".icons/material/share-variant.svg" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<button type="reset" class="md-search__icon md-icon" aria-label="{{ lang.t('search.reset') }}" tabindex="-1">
|
||||
{% include ".icons/material/close.svg" %}
|
||||
</button>
|
||||
</nav>
|
||||
{% if "search.suggest" in features %}
|
||||
<div class="md-search__suggest" data-md-component="search-suggest"></div>
|
||||
{% endif %}
|
||||
</form>
|
||||
<div class="md-search__output">
|
||||
<div class="md-search__scrollwrap" data-md-scrollfix>
|
||||
|
@ -36,6 +36,9 @@ export type Flag =
|
||||
| "navigation.sections" /* Sections navigation */
|
||||
| "navigation.tabs" /* Tabs navigation */
|
||||
| "navigation.top" /* Back-to-top button */
|
||||
| "search.highlight" /* Search highlighting */
|
||||
| "search.share" /* Search sharing */
|
||||
| "search.suggest" /* Search suggestions */
|
||||
| "toc.integrate" /* Integrated table of contents */
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
@ -55,6 +55,7 @@ import {
|
||||
mountHeaderTitle,
|
||||
mountPalette,
|
||||
mountSearch,
|
||||
mountSearchHiglight,
|
||||
mountSidebar,
|
||||
mountSource,
|
||||
mountTableOfContents,
|
||||
@ -195,6 +196,13 @@ const content$ = defer(() => merge(
|
||||
...getComponentElements("content")
|
||||
.map(el => mountContent(el, { target$, viewport$, print$ })),
|
||||
|
||||
/* Search highlighting */
|
||||
...getComponentElements("content")
|
||||
.map(el => feature("search.highlight")
|
||||
? mountSearchHiglight(el, { index$, location$ })
|
||||
: NEVER
|
||||
),
|
||||
|
||||
/* Header title */
|
||||
...getComponentElements("header-title")
|
||||
.map(el => mountHeaderTitle(el, { viewport$, header$ })),
|
||||
|
@ -42,6 +42,8 @@ export type ComponentType =
|
||||
| "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 */
|
||||
@ -83,6 +85,8 @@ interface ComponentTypeMap {
|
||||
"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 */
|
||||
|
@ -21,7 +21,7 @@
|
||||
*/
|
||||
|
||||
import { NEVER, Observable, ObservableInput, merge } from "rxjs"
|
||||
import { filter, sample, take } from "rxjs/operators"
|
||||
import { filter, mergeWith, sample, take } from "rxjs/operators"
|
||||
|
||||
import { configuration } from "~/_"
|
||||
import {
|
||||
@ -34,14 +34,21 @@ import {
|
||||
} from "~/browser"
|
||||
import {
|
||||
SearchIndex,
|
||||
SearchResult,
|
||||
isSearchQueryMessage,
|
||||
isSearchReadyMessage,
|
||||
setupSearchWorker
|
||||
} from "~/integrations"
|
||||
|
||||
import { Component, getComponentElement } from "../../_"
|
||||
import {
|
||||
Component,
|
||||
getComponentElement,
|
||||
getComponentElements
|
||||
} from "../../_"
|
||||
import { SearchQuery, mountSearchQuery } from "../query"
|
||||
import { SearchResult, mountSearchResult } from "../result"
|
||||
import { mountSearchResult } from "../result"
|
||||
import { SearchShare, mountSearchShare } from "../share"
|
||||
import { SearchSuggest, mountSearchSuggest } from "../suggest"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
@ -53,6 +60,8 @@ import { SearchResult, mountSearchResult } from "../result"
|
||||
export type Search =
|
||||
| SearchQuery
|
||||
| SearchResult
|
||||
| SearchShare
|
||||
| SearchSuggest
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
@ -88,7 +97,7 @@ export function mountSearch(
|
||||
try {
|
||||
const worker = setupSearchWorker(config.search, index$)
|
||||
|
||||
/* Retrieve nested components */
|
||||
/* Retrieve query and result components */
|
||||
const query = getComponentElement("search-query", el)
|
||||
const result = getComponentElement("search-result", el)
|
||||
|
||||
@ -97,8 +106,12 @@ export function mountSearch(
|
||||
tx$
|
||||
.pipe(
|
||||
filter(isSearchQueryMessage),
|
||||
sample(rx$.pipe(filter(isSearchReadyMessage))),
|
||||
take(1)
|
||||
sample(rx$
|
||||
.pipe(
|
||||
filter(isSearchReadyMessage),
|
||||
take(1)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe(tx$.next.bind(tx$))
|
||||
|
||||
@ -111,10 +124,28 @@ export function mountSearch(
|
||||
const active = getActiveElement()
|
||||
switch (key.type) {
|
||||
|
||||
/* Enter: prevent form submission */
|
||||
/* Enter: go to first (best) result */
|
||||
case "Enter":
|
||||
if (active === query)
|
||||
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 */
|
||||
@ -173,11 +204,21 @@ export function mountSearch(
|
||||
})
|
||||
|
||||
/* Create and return component */
|
||||
const query$ = mountSearchQuery(query, worker)
|
||||
return merge(
|
||||
query$,
|
||||
mountSearchResult(result, worker, { query$ })
|
||||
)
|
||||
const query$ = mountSearchQuery(query, worker)
|
||||
const result$ = mountSearchResult(result, worker, { query$ })
|
||||
return merge(query$, result$)
|
||||
.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) {
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-null/no-null": "off"
|
||||
}
|
||||
}
|
113
src/assets/javascripts/components/search/highlight/index.ts
Normal file
113
src/assets/javascripts/components/search/highlight/index.ts
Normal file
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2021 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
|
||||
} from "rxjs"
|
||||
import { filter, map, startWith } from "rxjs/operators"
|
||||
|
||||
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.has("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 }
|
||||
})
|
||||
)
|
||||
}
|
@ -21,5 +21,8 @@
|
||||
*/
|
||||
|
||||
export * from "./_"
|
||||
export * from "./highlight"
|
||||
export * from "./query"
|
||||
export * from "./result"
|
||||
export * from "./share"
|
||||
export * from "./suggest"
|
||||
|
@ -31,8 +31,10 @@ import {
|
||||
delay,
|
||||
distinctUntilChanged,
|
||||
distinctUntilKeyChanged,
|
||||
filter,
|
||||
finalize,
|
||||
map,
|
||||
take,
|
||||
takeLast,
|
||||
takeUntil,
|
||||
tap
|
||||
@ -43,6 +45,7 @@ import {
|
||||
setSearchQueryPlaceholder
|
||||
} from "~/actions"
|
||||
import {
|
||||
getLocation,
|
||||
setElementFocus,
|
||||
setToggle,
|
||||
watchElementFocus
|
||||
@ -51,7 +54,8 @@ import {
|
||||
SearchMessageType,
|
||||
SearchQueryMessage,
|
||||
SearchWorker,
|
||||
defaultTransform
|
||||
defaultTransform,
|
||||
isSearchReadyMessage
|
||||
} from "~/integrations"
|
||||
|
||||
import { Component } from "../../_"
|
||||
@ -79,11 +83,12 @@ export interface SearchQuery {
|
||||
* is delayed by `1ms` so the input's empty state is allowed to propagate.
|
||||
*
|
||||
* @param el - Search query element
|
||||
* @param worker - Search worker
|
||||
*
|
||||
* @returns Search query observable
|
||||
*/
|
||||
export function watchSearchQuery(
|
||||
el: HTMLInputElement
|
||||
el: HTMLInputElement, { rx$ }: SearchWorker
|
||||
): Observable<SearchQuery> {
|
||||
const fn = __search?.transform || defaultTransform
|
||||
|
||||
@ -98,6 +103,21 @@ export function watchSearchQuery(
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
/* Intercept deep links */
|
||||
const location = getLocation()
|
||||
if (location.searchParams.has("q")) {
|
||||
setToggle("search", true)
|
||||
rx$
|
||||
.pipe(
|
||||
filter(isSearchReadyMessage),
|
||||
take(1)
|
||||
)
|
||||
.subscribe(() => {
|
||||
el.value = location.searchParams.get("q")!
|
||||
setElementFocus(el)
|
||||
})
|
||||
}
|
||||
|
||||
/* Combine into single observable */
|
||||
return combineLatest([value$, focus$])
|
||||
.pipe(
|
||||
@ -114,7 +134,7 @@ export function watchSearchQuery(
|
||||
* @returns Search query component observable
|
||||
*/
|
||||
export function mountSearchQuery(
|
||||
el: HTMLInputElement, { tx$ }: SearchWorker
|
||||
el: HTMLInputElement, { tx$, rx$ }: SearchWorker
|
||||
): Observable<Component<SearchQuery, HTMLInputElement>> {
|
||||
const internal$ = new Subject<SearchQuery>()
|
||||
|
||||
@ -151,7 +171,7 @@ export function mountSearchQuery(
|
||||
.subscribe(() => setElementFocus(el))
|
||||
|
||||
/* Create and return component */
|
||||
return watchSearchQuery(el)
|
||||
return watchSearchQuery(el, { tx$, rx$ })
|
||||
.pipe(
|
||||
tap(internal$),
|
||||
finalize(() => internal$.complete()),
|
||||
|
@ -33,7 +33,6 @@ import {
|
||||
finalize,
|
||||
map,
|
||||
observeOn,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
@ -52,27 +51,16 @@ import {
|
||||
watchElementThreshold
|
||||
} from "~/browser"
|
||||
import {
|
||||
SearchResult as SearchResultData,
|
||||
SearchResult,
|
||||
SearchWorker,
|
||||
isSearchReadyMessage,
|
||||
isSearchResultMessage
|
||||
} from "~/integrations"
|
||||
import { renderSearchResult } from "~/templates"
|
||||
import { renderSearchResultItem } from "~/templates"
|
||||
|
||||
import { Component } from "../../_"
|
||||
import { SearchQuery } from "../query"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Search result
|
||||
*/
|
||||
export interface SearchResult {
|
||||
data: SearchResultData[] /* Search result data */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
* ------------------------------------------------------------------------- */
|
||||
@ -129,9 +117,9 @@ export function mountSearchResult(
|
||||
observeOn(animationFrameScheduler),
|
||||
withLatestFrom(query$)
|
||||
)
|
||||
.subscribe(([{ data }, { value }]) => {
|
||||
.subscribe(([{ items }, { value }]) => {
|
||||
if (value)
|
||||
setSearchResultMeta(meta, data.length)
|
||||
setSearchResultMeta(meta, items.length)
|
||||
else
|
||||
resetSearchResultMeta(meta)
|
||||
})
|
||||
@ -141,9 +129,9 @@ export function mountSearchResult(
|
||||
.pipe(
|
||||
observeOn(animationFrameScheduler),
|
||||
tap(() => resetSearchResultList(list)),
|
||||
switchMap(({ data }) => merge(
|
||||
of(...data.slice(0, 10)),
|
||||
of(...data.slice(10))
|
||||
switchMap(({ items }) => merge(
|
||||
of(...items.slice(0, 10)),
|
||||
of(...items.slice(10))
|
||||
.pipe(
|
||||
bufferCount(4),
|
||||
zipWith(boundary$),
|
||||
@ -152,15 +140,14 @@ export function mountSearchResult(
|
||||
))
|
||||
)
|
||||
.subscribe(result => {
|
||||
addToSearchResultList(list, renderSearchResult(result))
|
||||
addToSearchResultList(list, renderSearchResultItem(result))
|
||||
})
|
||||
|
||||
/* Filter search result list */
|
||||
/* Filter search result message */
|
||||
const result$ = rx$
|
||||
.pipe(
|
||||
filter(isSearchResultMessage),
|
||||
map(({ data }) => ({ data })),
|
||||
startWith({ data: [] })
|
||||
map(({ data }) => data)
|
||||
)
|
||||
|
||||
/* Create and return component */
|
||||
|
123
src/assets/javascripts/components/search/share/index.ts
Normal file
123
src/assets/javascripts/components/search/share/index.ts
Normal file
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2021 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,
|
||||
fromEvent
|
||||
} from "rxjs"
|
||||
import {
|
||||
finalize,
|
||||
map,
|
||||
tap
|
||||
} from "rxjs/operators"
|
||||
|
||||
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 = ""
|
||||
url.searchParams.delete("h")
|
||||
url.searchParams.set("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 internal$ = new Subject<SearchShare>()
|
||||
internal$.subscribe(({ url }) => {
|
||||
el.setAttribute("data-clipboard-text", el.href)
|
||||
el.href = `${url}`
|
||||
})
|
||||
|
||||
/* Prevent following of link */
|
||||
fromEvent(el, "click")
|
||||
.subscribe(ev => ev.preventDefault())
|
||||
|
||||
/* Create and return component */
|
||||
return watchSearchShare(el, options)
|
||||
.pipe(
|
||||
tap(internal$),
|
||||
finalize(() => internal$.complete()),
|
||||
map(state => ({ ref: el, ...state }))
|
||||
)
|
||||
}
|
152
src/assets/javascripts/components/search/suggest/index.ts
Normal file
152
src/assets/javascripts/components/search/suggest/index.ts
Normal file
@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2021 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,
|
||||
fromEvent
|
||||
} from "rxjs"
|
||||
import {
|
||||
combineLatestWith,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
finalize,
|
||||
map,
|
||||
observeOn,
|
||||
tap
|
||||
} from "rxjs/operators"
|
||||
|
||||
import { Keyboard } from "~/browser"
|
||||
import {
|
||||
SearchResult,
|
||||
SearchWorker,
|
||||
isSearchResultMessage
|
||||
} from "~/integrations"
|
||||
|
||||
import { Component, getComponentElement } from "../../_"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Search suggestions
|
||||
*/
|
||||
export interface SearchSuggest {}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mount options
|
||||
*/
|
||||
interface MountOptions {
|
||||
keyboard$: Observable<Keyboard> /* Keyboard observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* 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 worker - Search worker
|
||||
* @param options - Options
|
||||
*
|
||||
* @returns Search result list component observable
|
||||
*/
|
||||
export function mountSearchSuggest(
|
||||
el: HTMLElement, { rx$ }: SearchWorker, { keyboard$ }: MountOptions
|
||||
): Observable<Component<SearchSuggest>> {
|
||||
const internal$ = new Subject<SearchResult>()
|
||||
|
||||
/* Retrieve query component and track all changes */
|
||||
const query = getComponentElement("search-query")
|
||||
const query$ = fromEvent(query, "keydown")
|
||||
.pipe(
|
||||
observeOn(asyncScheduler),
|
||||
map(() => query.value),
|
||||
distinctUntilChanged(),
|
||||
)
|
||||
|
||||
/* Update search suggestions */
|
||||
internal$
|
||||
.pipe(
|
||||
combineLatestWith(query$),
|
||||
map(([{ suggestions }, value]) => {
|
||||
const words = value.split(/([\s-]+)/)
|
||||
if (suggestions?.length && words[words.length - 1]) {
|
||||
const last = suggestions[suggestions.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$ = rx$
|
||||
.pipe(
|
||||
filter(isSearchResultMessage),
|
||||
map(({ data }) => data)
|
||||
)
|
||||
|
||||
/* Create and return component */
|
||||
return result$
|
||||
.pipe(
|
||||
tap(internal$),
|
||||
finalize(() => internal$.complete()),
|
||||
map(() => ({ ref: el }))
|
||||
)
|
||||
}
|
@ -29,6 +29,7 @@ import {
|
||||
SearchHighlightFactoryFn,
|
||||
setupSearchHighlighter
|
||||
} from "../highlighter"
|
||||
import { SearchOptions } from "../options"
|
||||
import {
|
||||
SearchQueryTerms,
|
||||
getSearchQueryTerms,
|
||||
@ -58,21 +59,6 @@ export interface SearchIndexDocument {
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Search index pipeline function
|
||||
*/
|
||||
export type SearchIndexPipelineFn =
|
||||
| "trimmer" /* Trimmer */
|
||||
| "stopWordFilter" /* Stop word filter */
|
||||
| "stemmer" /* Stemmer */
|
||||
|
||||
/**
|
||||
* Search index pipeline
|
||||
*/
|
||||
export type SearchIndexPipeline = SearchIndexPipelineFn[]
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Search index
|
||||
*
|
||||
@ -83,7 +69,7 @@ export interface SearchIndex {
|
||||
config: SearchIndexConfig /* Search index configuration */
|
||||
docs: SearchIndexDocument[] /* Search index documents */
|
||||
index?: object /* Prebuilt index */
|
||||
pipeline?: SearchIndexPipeline /* Search index pipeline */
|
||||
options: SearchOptions /* Search options */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
@ -98,10 +84,25 @@ export interface SearchMetadata {
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Search result document
|
||||
*/
|
||||
export type SearchResultDocument = SearchDocument & SearchMetadata
|
||||
|
||||
/**
|
||||
* Search result item
|
||||
*/
|
||||
export type SearchResultItem = SearchResultDocument[]
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Search result
|
||||
*/
|
||||
export type SearchResult = Array<SearchDocument & SearchMetadata>
|
||||
export interface SearchResult {
|
||||
items: SearchResultItem[] /* Search result items */
|
||||
suggestions?: string[] /* Search suggestions */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
@ -151,12 +152,20 @@ export class Search {
|
||||
*/
|
||||
protected index: lunr.Index
|
||||
|
||||
/**
|
||||
* Search options
|
||||
*/
|
||||
protected options: SearchOptions
|
||||
|
||||
/**
|
||||
* Create the search integration
|
||||
*
|
||||
* @param data - Search index
|
||||
*/
|
||||
public constructor({ config, docs, pipeline, index }: SearchIndex) {
|
||||
public constructor({ config, docs, index, options }: SearchIndex) {
|
||||
this.options = options
|
||||
|
||||
/* Set up document map and highlighter factory */
|
||||
this.documents = setupSearchDocumentMap(docs)
|
||||
this.highlight = setupSearchHighlighter(config)
|
||||
|
||||
@ -177,7 +186,7 @@ export class Search {
|
||||
/* Compute functions to be removed from the pipeline */
|
||||
const fns = difference([
|
||||
"trimmer", "stopWordFilter", "stemmer"
|
||||
], pipeline!)
|
||||
], options.pipeline)
|
||||
|
||||
/* Remove functions from the pipeline for registered languages */
|
||||
for (const lang of config.lang.map(language => (
|
||||
@ -189,11 +198,13 @@ export class Search {
|
||||
}
|
||||
}
|
||||
|
||||
/* Set up fields and reference */
|
||||
this.field("title", { boost: 1000 })
|
||||
this.field("text")
|
||||
/* Set up reference */
|
||||
this.ref("location")
|
||||
|
||||
/* Set up fields */
|
||||
this.field("title", { boost: 1e3 })
|
||||
this.field("text")
|
||||
|
||||
/* Index documents */
|
||||
for (const doc of docs)
|
||||
this.add(doc)
|
||||
@ -221,7 +232,7 @@ export class Search {
|
||||
*
|
||||
* @returns Search results
|
||||
*/
|
||||
public search(query: string): SearchResult[] {
|
||||
public search(query: string): SearchResult {
|
||||
if (query) {
|
||||
try {
|
||||
const highlight = this.highlight(query)
|
||||
@ -236,7 +247,7 @@ export class Search {
|
||||
const groups = this.index.search(`${query}*`)
|
||||
|
||||
/* Apply post-query boosts based on title and search query terms */
|
||||
.reduce<SearchResult>((results, { ref, score, matchData }) => {
|
||||
.reduce<SearchResultItem>((item, { ref, score, matchData }) => {
|
||||
const document = this.documents.get(ref)
|
||||
if (typeof document !== "undefined") {
|
||||
const { location, title, text, parent } = document
|
||||
@ -249,34 +260,55 @@ export class Search {
|
||||
|
||||
/* Highlight title and text and apply post-query boosts */
|
||||
const boost = +!parent + +Object.values(terms).every(t => t)
|
||||
results.push({
|
||||
item.push({
|
||||
location,
|
||||
title: highlight(title),
|
||||
text: highlight(text),
|
||||
text: highlight(text),
|
||||
score: score * (1 + boost),
|
||||
terms
|
||||
})
|
||||
}
|
||||
return results
|
||||
return item
|
||||
}, [])
|
||||
|
||||
/* Sort search results again after applying boosts */
|
||||
.sort((a, b) => b.score - a.score)
|
||||
|
||||
/* Group search results by page */
|
||||
.reduce((results, result) => {
|
||||
.reduce((items, result) => {
|
||||
const document = this.documents.get(result.location)
|
||||
if (typeof document !== "undefined") {
|
||||
const ref = "parent" in document
|
||||
? document.parent!.location
|
||||
: document.location
|
||||
results.set(ref, [...results.get(ref) || [], result])
|
||||
items.set(ref, [...items.get(ref) || [], result])
|
||||
}
|
||||
return results
|
||||
}, new Map<string, SearchResult>())
|
||||
return items
|
||||
}, new Map<string, SearchResultItem>())
|
||||
|
||||
/* Expand grouped search results */
|
||||
return [...groups.values()]
|
||||
/* Generate search suggestions, if desired */
|
||||
let suggestions: string[] | undefined
|
||||
if (this.options.suggestions) {
|
||||
const titles = this.index.query(builder => {
|
||||
for (const clause of clauses)
|
||||
builder.term(clause.term, {
|
||||
fields: ["title"],
|
||||
presence: lunr.Query.presence.REQUIRED,
|
||||
wildcard: lunr.Query.wildcard.TRAILING
|
||||
})
|
||||
})
|
||||
|
||||
/* Retrieve suggestions for best match */
|
||||
suggestions = titles.length
|
||||
? Object.keys(titles[0].matchData.metadata)
|
||||
: []
|
||||
}
|
||||
|
||||
/* Return items and suggestions */
|
||||
return {
|
||||
items: [...groups.values()],
|
||||
...typeof suggestions !== "undefined" && { suggestions }
|
||||
}
|
||||
|
||||
/* Log errors to console (for now) */
|
||||
} catch {
|
||||
@ -285,6 +317,6 @@ export class Search {
|
||||
}
|
||||
|
||||
/* Return nothing in case of error or empty query */
|
||||
return []
|
||||
return { items: [] }
|
||||
}
|
||||
}
|
||||
|
@ -23,5 +23,6 @@
|
||||
export * from "./_"
|
||||
export * from "./document"
|
||||
export * from "./highlighter"
|
||||
export * from "./options"
|
||||
export * from "./query"
|
||||
export * from "./worker"
|
||||
|
48
src/assets/javascripts/integrations/search/options/index.ts
Normal file
48
src/assets/javascripts/integrations/search/options/index.ts
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2021 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.
|
||||
*/
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Search pipeline function
|
||||
*/
|
||||
export type SearchPipelineFn =
|
||||
| "trimmer" /* Trimmer */
|
||||
| "stopWordFilter" /* Stop word filter */
|
||||
| "stemmer" /* Stemmer */
|
||||
|
||||
/**
|
||||
* Search pipeline
|
||||
*/
|
||||
export type SearchPipeline = SearchPipelineFn[]
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Search options
|
||||
*/
|
||||
export interface SearchOptions {
|
||||
pipeline: SearchPipeline /* Search pipeline */
|
||||
suggestions: boolean /* Search suggestions */
|
||||
}
|
@ -23,10 +23,14 @@
|
||||
import { ObservableInput, Subject, from } from "rxjs"
|
||||
import { map, share } from "rxjs/operators"
|
||||
|
||||
import { configuration, translation } from "~/_"
|
||||
import { configuration, feature, translation } from "~/_"
|
||||
import { WorkerHandler, watchWorker } from "~/browser"
|
||||
|
||||
import { SearchIndex, SearchIndexPipeline } from "../../_"
|
||||
import { SearchIndex } from "../../_"
|
||||
import {
|
||||
SearchOptions,
|
||||
SearchPipeline
|
||||
} from "../../options"
|
||||
import {
|
||||
SearchMessage,
|
||||
SearchMessageType,
|
||||
@ -71,10 +75,16 @@ function setupSearchIndex(
|
||||
/* Set pipeline from translation */
|
||||
const pipeline = translation("search.config.pipeline")
|
||||
.split(/\s*,\s*/)
|
||||
.filter(Boolean) as SearchIndexPipeline
|
||||
.filter(Boolean) as SearchPipeline
|
||||
|
||||
/* Determine search options */
|
||||
const options: SearchOptions = {
|
||||
pipeline,
|
||||
suggestions: feature("search.suggest")
|
||||
}
|
||||
|
||||
/* Return search index after defaulting */
|
||||
return { config, docs, index, pipeline }
|
||||
return { config, docs, index, options }
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@ -105,7 +115,7 @@ export function setupSearchWorker(
|
||||
.pipe(
|
||||
map(message => {
|
||||
if (isSearchResultMessage(message)) {
|
||||
for (const result of message.data)
|
||||
for (const result of message.data.items)
|
||||
for (const document of result)
|
||||
document.location = `${config.base}/${document.location}`
|
||||
}
|
||||
|
@ -150,7 +150,7 @@ export async function handler(
|
||||
case SearchMessageType.QUERY:
|
||||
return {
|
||||
type: SearchMessageType.RESULT,
|
||||
data: index ? index.search(message.data) : []
|
||||
data: index ? index.search(message.data) : { items: [] }
|
||||
}
|
||||
|
||||
/* All other messages */
|
||||
|
@ -66,7 +66,7 @@ export interface SearchQueryMessage {
|
||||
*/
|
||||
export interface SearchResultMessage {
|
||||
type: SearchMessageType.RESULT /* Message type */
|
||||
data: SearchResult[] /* Message data */
|
||||
data: SearchResult /* Message data */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
@ -20,11 +20,11 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { translation } from "~/_"
|
||||
import { feature, translation } from "~/_"
|
||||
import {
|
||||
SearchDocument,
|
||||
SearchMetadata,
|
||||
SearchResult
|
||||
SearchResultItem
|
||||
} from "~/integrations/search"
|
||||
import { h, truncate } from "~/utilities"
|
||||
|
||||
@ -65,10 +65,17 @@ function renderSearchDocument(
|
||||
.flat()
|
||||
.slice(0, -1)
|
||||
|
||||
/* Assemble query string for highlighting */
|
||||
const url = new URL(document.location)
|
||||
if (feature("search.highlight"))
|
||||
url.searchParams.set("h", Object.entries(document.terms)
|
||||
.filter(([, match]) => match)
|
||||
.reduce((highlight, [value]) => `${highlight} ${value}`.trim(), "")
|
||||
)
|
||||
|
||||
/* Render article or section, depending on flags */
|
||||
const url = document.location
|
||||
return (
|
||||
<a href={url} class="md-search-result__link" tabIndex={-1}>
|
||||
<a href={`${url}`} class="md-search-result__link" tabIndex={-1}>
|
||||
<article
|
||||
class={["md-search-result__article", ...parent
|
||||
? ["md-search-result__article--document"]
|
||||
@ -104,8 +111,8 @@ function renderSearchDocument(
|
||||
*
|
||||
* @returns Element
|
||||
*/
|
||||
export function renderSearchResult(
|
||||
result: SearchResult
|
||||
export function renderSearchResultItem(
|
||||
result: SearchResultItem
|
||||
): HTMLElement {
|
||||
const threshold = result[0].score
|
||||
const docs = [...result]
|
||||
|
@ -216,10 +216,32 @@
|
||||
// Search form
|
||||
&__form {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: px2rem(48px);
|
||||
background-color: var(--md-default-bg-color);
|
||||
box-shadow: 0 0 px2rem(12px) transparent;
|
||||
transition:
|
||||
color 250ms,
|
||||
background-color 250ms;
|
||||
|
||||
// [tablet landscape +]: Header-embedded search
|
||||
@include break-from-device(tablet landscape) {
|
||||
height: px2rem(36px);
|
||||
background-color: hsla(0, 0%, 0%, 0.26);
|
||||
border-radius: px2rem(2px);
|
||||
|
||||
// Search form on hover
|
||||
&:hover {
|
||||
background-color: hsla(0, 0%, 100%, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust appearance when search is active
|
||||
[data-md-toggle="search"]:checked ~ .md-header & {
|
||||
color: var(--md-default-fg-color);
|
||||
background-color: var(--md-default-bg-color);
|
||||
border-radius: px2rem(2px) px2rem(2px) 0 0;
|
||||
box-shadow: 0 0 px2rem(12px) hsla(0, 0%, 0%, 0.07);
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,14 +249,12 @@
|
||||
&__input {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 px2rem(44px) 0 px2rem(72px);
|
||||
font-size: px2rem(18px);
|
||||
text-overflow: ellipsis;
|
||||
background-color: var(--md-default-bg-color);
|
||||
box-shadow: 0 0 px2rem(12px) transparent;
|
||||
transition:
|
||||
color 250ms,
|
||||
background-color 250ms,
|
||||
box-shadow 250ms;
|
||||
background: transparent;
|
||||
|
||||
// Adjust for right-to-left languages
|
||||
[dir="rtl"] & {
|
||||
@ -257,11 +277,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Adjust appearance when search is active
|
||||
[data-md-toggle="search"]:checked ~ .md-header & {
|
||||
box-shadow: 0 0 px2rem(12px) hsla(0, 0%, 0%, 0.07);
|
||||
}
|
||||
|
||||
// [tablet portrait -]: Search modal
|
||||
@include break-to-device(tablet portrait) {
|
||||
width: 100%;
|
||||
@ -271,40 +286,28 @@
|
||||
|
||||
// [tablet landscape +]: Header-embedded search
|
||||
@include break-from-device(tablet landscape) {
|
||||
width: 100%;
|
||||
height: px2rem(36px);
|
||||
padding-left: px2rem(44px);
|
||||
color: inherit;
|
||||
font-size: px2rem(16px);
|
||||
background-color: hsla(0, 0%, 0%, 0.26);
|
||||
border-radius: px2rem(2px);
|
||||
|
||||
// Adjust for right-to-left languages
|
||||
[dir="rtl"] & {
|
||||
padding-right: px2rem(44px);
|
||||
}
|
||||
|
||||
// Search icon
|
||||
+ .md-search__icon {
|
||||
color: var(--md-primary-bg-color);
|
||||
}
|
||||
|
||||
// Search placeholder
|
||||
&::placeholder {
|
||||
color: var(--md-primary-bg-color--light);
|
||||
}
|
||||
|
||||
// Search input on hover
|
||||
&:hover {
|
||||
background-color: hsla(0, 0%, 100%, 0.12);
|
||||
// Search icon
|
||||
+ .md-search__icon {
|
||||
color: var(--md-primary-bg-color);
|
||||
}
|
||||
|
||||
// Adjust appearance when search is active
|
||||
[data-md-toggle="search"]:checked ~ .md-header & {
|
||||
color: var(--md-default-fg-color);
|
||||
text-overflow: clip;
|
||||
background-color: var(--md-default-bg-color);
|
||||
border-radius: px2rem(2px) px2rem(2px) 0 0;
|
||||
|
||||
// Search icon and placeholder
|
||||
+ .md-search__icon,
|
||||
@ -317,8 +320,7 @@
|
||||
|
||||
// Search icon
|
||||
&__icon {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
display: inline-block;
|
||||
width: px2rem(24px);
|
||||
height: px2rem(24px);
|
||||
cursor: pointer;
|
||||
@ -333,8 +335,10 @@
|
||||
|
||||
// Search focus button
|
||||
&[for="__search"] {
|
||||
position: absolute;
|
||||
top: px2rem(6px);
|
||||
left: px2rem(10px);
|
||||
z-index: 2;
|
||||
|
||||
// Adjust for right-to-left languages
|
||||
[dir="rtl"] & {
|
||||
@ -374,34 +378,48 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search reset button
|
||||
&[type="reset"] {
|
||||
top: px2rem(6px);
|
||||
right: px2rem(10px);
|
||||
// Search options
|
||||
&__options {
|
||||
position: absolute;
|
||||
top: px2rem(6px);
|
||||
right: px2rem(10px);
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
|
||||
// Adjust for right-to-left languages
|
||||
[dir="rtl"] & {
|
||||
right: initial;
|
||||
left: px2rem(10px);
|
||||
}
|
||||
|
||||
// [tablet portrait -]: Search modal
|
||||
@include break-to-device(tablet portrait) {
|
||||
top: px2rem(12px);
|
||||
right: px2rem(16px);
|
||||
|
||||
// Adjust for right-to-left languages
|
||||
[dir="rtl"] & {
|
||||
right: initial;
|
||||
left: px2rem(16px);
|
||||
}
|
||||
}
|
||||
|
||||
// Search option buttons
|
||||
> * {
|
||||
margin-left: px2rem(4px);
|
||||
color: var(--md-default-fg-color--light);
|
||||
transform: scale(0.75);
|
||||
opacity: 0;
|
||||
transition:
|
||||
transform 150ms cubic-bezier(0.1, 0.7, 0.1, 1),
|
||||
opacity 150ms;
|
||||
pointer-events: none;
|
||||
|
||||
// Adjust for right-to-left languages
|
||||
[dir="rtl"] & {
|
||||
right: initial;
|
||||
left: px2rem(10px);
|
||||
}
|
||||
|
||||
// [tablet portrait -]: Search modal
|
||||
@include break-to-device(tablet portrait) {
|
||||
top: px2rem(12px);
|
||||
right: px2rem(16px);
|
||||
|
||||
// Adjust for right-to-left languages
|
||||
[dir="rtl"] & {
|
||||
right: initial;
|
||||
left: px2rem(16px);
|
||||
}
|
||||
// Hide outline for pointer devices
|
||||
&:not(.focus-visible) {
|
||||
outline: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
// Show reset button when search is active and input non-empty
|
||||
@ -419,6 +437,44 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Search suggestions
|
||||
&__suggest {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 px2rem(44px) 0 px2rem(72px);
|
||||
color: var(--md-default-fg-color--lighter);
|
||||
font-size: px2rem(18px);
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
transition: opacity 50ms;
|
||||
|
||||
// Adjust for right-to-left languages
|
||||
[dir="rtl"] & {
|
||||
padding: 0 px2rem(72px) 0 px2rem(44px);
|
||||
}
|
||||
|
||||
// [tablet landscape +]: Header-embedded search
|
||||
@include break-from-device(tablet landscape) {
|
||||
padding-left: px2rem(44px);
|
||||
font-size: px2rem(16px);
|
||||
|
||||
// Adjust for right-to-left languages
|
||||
[dir="rtl"] & {
|
||||
padding-right: px2rem(44px);
|
||||
}
|
||||
}
|
||||
|
||||
// Show suggestions when search is active
|
||||
[data-md-toggle="search"]:checked ~ .md-header & {
|
||||
opacity: 1;
|
||||
transition: opacity 300ms 100ms;
|
||||
}
|
||||
}
|
||||
|
||||
// Search output
|
||||
&__output {
|
||||
position: absolute;
|
||||
|
@ -81,25 +81,20 @@
|
||||
// [tablet portrait +]: Header-embedded search
|
||||
@include break-from-device(tablet landscape) {
|
||||
|
||||
// Search input
|
||||
.md-search__input {
|
||||
// Search form
|
||||
.md-search__form {
|
||||
background-color: hsla(0, 0%, 0%, 0.07);
|
||||
|
||||
// Search icon color
|
||||
+ .md-search__icon {
|
||||
color: hsla(0, 0%, 0%, 0.87);
|
||||
}
|
||||
|
||||
// Placeholder color
|
||||
&::placeholder {
|
||||
color: hsla(0, 0%, 0%, 0.54);
|
||||
}
|
||||
|
||||
// Search input on hover
|
||||
// Search form on hover
|
||||
&:hover {
|
||||
background-color: hsla(0, 0%, 0%, 0.32);
|
||||
}
|
||||
}
|
||||
|
||||
// Search icon
|
||||
.md-search__input + .md-search__icon {
|
||||
color: hsla(0, 0%, 0%, 0.87);
|
||||
}
|
||||
}
|
||||
|
||||
// [screen +]: Add bottom border for tabs
|
||||
@ -144,8 +139,8 @@
|
||||
// [tablet landscape +]: Header-embedded search
|
||||
@include break-from-device(tablet landscape) {
|
||||
|
||||
// Search input
|
||||
.md-search__input {
|
||||
// Search form
|
||||
.md-search__form {
|
||||
background-color: hsla(0, 0%, 100%, 0.12);
|
||||
|
||||
// Search form on hover
|
||||
|
@ -32,6 +32,8 @@
|
||||
"meta.source": "Quellcode",
|
||||
"search.config.lang": "de",
|
||||
"search.placeholder": "Suche",
|
||||
"search.share": "Teilen",
|
||||
"search.reset": "Zurücksetzen",
|
||||
"search.result.initializer": "Suche wird initialisiert",
|
||||
"search.result.placeholder": "Suchbegriff eingeben",
|
||||
"search.result.none": "Keine Suchergebnisse",
|
||||
@ -40,6 +42,7 @@
|
||||
"search.result.more.one": "1 weiteres Suchergebnis auf dieser Seite",
|
||||
"search.result.more.other": "# weitere Suchergebnisse auf dieser Seite",
|
||||
"search.result.term.missing": "Es fehlt",
|
||||
"search.title": "Suche",
|
||||
"select.language.title": "Sprache wechseln",
|
||||
"select.version.title": "Version auswählen",
|
||||
"skip.link.title": "Zum Inhalt",
|
||||
|
@ -38,6 +38,7 @@
|
||||
"search.config.pipeline": "trimmer, stopWordFilter",
|
||||
"search.config.separator": "[\s\-]+",
|
||||
"search.placeholder": "Search",
|
||||
"search.share": "Share",
|
||||
"search.reset": "Clear",
|
||||
"search.result.initializer": "Initializing search",
|
||||
"search.result.placeholder": "Type to start searching",
|
||||
@ -47,6 +48,7 @@
|
||||
"search.result.more.one": "1 more on this page",
|
||||
"search.result.more.other": "# more on this page",
|
||||
"search.result.term.missing": "Missing",
|
||||
"search.title": "Search",
|
||||
"select.language.title": "Select language",
|
||||
"select.version.title": "Select version",
|
||||
"skip.link.title": "Skip to content",
|
||||
|
@ -27,6 +27,8 @@
|
||||
<label class="md-search__overlay" for="__search"></label>
|
||||
<div class="md-search__inner" role="search">
|
||||
<form class="md-search__form" name="search">
|
||||
|
||||
<!-- Search input -->
|
||||
<input
|
||||
type="text"
|
||||
class="md-search__input"
|
||||
@ -38,24 +40,59 @@
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
data-md-component="search-query"
|
||||
data-md-state="active"
|
||||
required
|
||||
/>
|
||||
|
||||
<!-- Button to open search -->
|
||||
<label class="md-search__icon md-icon" for="__search">
|
||||
{% include ".icons/material/magnify.svg" %}
|
||||
{% include ".icons/material/arrow-left.svg" %}
|
||||
</label>
|
||||
<button
|
||||
type="reset"
|
||||
class="md-search__icon md-icon"
|
||||
aria-label="{{ lang.t('search.reset') }}"
|
||||
tabindex="-1"
|
||||
|
||||
<!-- Search options -->
|
||||
<nav
|
||||
class="md-search__options"
|
||||
aria-label="{{ lang.t('search.title') }}"
|
||||
>
|
||||
{% include ".icons/material/close.svg" %}
|
||||
</button>
|
||||
|
||||
<!-- Button to share search -->
|
||||
{% if "search.share" in features %}
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
class="md-search__icon md-icon"
|
||||
aria-label="{{ lang.t('search.share') }}"
|
||||
data-clipboard
|
||||
data-clipboard-text=""
|
||||
data-md-component="search-share"
|
||||
tabindex="-1"
|
||||
>
|
||||
{% include ".icons/material/share-variant.svg" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<!-- Button to reset search -->
|
||||
<button
|
||||
type="reset"
|
||||
class="md-search__icon md-icon"
|
||||
aria-label="{{ lang.t('search.reset') }}"
|
||||
tabindex="-1"
|
||||
>
|
||||
{% include ".icons/material/close.svg" %}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Search suggestions -->
|
||||
{% if "search.suggest" in features %}
|
||||
<div
|
||||
class="md-search__suggest"
|
||||
data-md-component="search-suggest"
|
||||
></div>
|
||||
{% endif %}
|
||||
</form>
|
||||
<div class="md-search__output">
|
||||
<div class="md-search__scrollwrap" data-md-scrollfix>
|
||||
|
||||
<!-- Search results -->
|
||||
<div class="md-search-result" data-md-component="search-result">
|
||||
<div class="md-search-result__meta">
|
||||
{{ lang.t("search.result.initializer") }}
|
||||
|
Loading…
Reference in New Issue
Block a user