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