Improved search result rendering and icon search

This commit is contained in:
squidfunk
2021-02-14 14:51:08 +01:00
parent a8b5cafa73
commit 4ef15bd440
50 changed files with 836 additions and 145 deletions

View File

@@ -21,6 +21,7 @@
*/
import { translation } from "~/_"
import { round } from "~/utilities"
/* ----------------------------------------------------------------------------
* Functions
@@ -49,7 +50,7 @@ export function setSearchResultMeta(
/* Multiple result */
default:
el.textContent = translation("search.result.other", value)
el.textContent = translation("search.result.other", round(value))
}
}

View File

@@ -21,7 +21,9 @@
*/
import { Observable, fromEvent, merge } from "rxjs"
import { map, startWith } from "rxjs/operators"
import { distinctUntilChanged, map, startWith } from "rxjs/operators"
import { getElementContentSize, getElementSize } from "../size"
/* ----------------------------------------------------------------------------
* Types
@@ -74,3 +76,30 @@ export function watchElementOffset(
startWith(getElementOffset(el))
)
}
/**
* Watch element threshold
*
* This function returns an observable which emits whether the bottom scroll
* offset of an elements is within a certain threshold.
*
* @param el - Element
* @param threshold - Threshold
*
* @returns Element threshold observable
*/
export function watchElementThreshold(
el: HTMLElement, threshold = 16
): Observable<boolean> {
return watchElementOffset(el)
.pipe(
map(({ y }) => {
const visible = getElementSize(el)
const content = getElementContentSize(el)
return y >= (
content.height - visible.height - threshold
)
}),
distinctUntilChanged()
)
}

View File

@@ -120,7 +120,7 @@ export function getElementContentSize(el: HTMLElement): ElementSize {
/**
* Watch element size
*
* This function returns an observable that will subscribe to a single internal
* This function returns an observable that subscribes to a single internal
* instance of `ResizeObserver` upon subscription, and emit resize events until
* termination. Note that this function should not be called with the same
* element twice, as the first unsubscription will terminate observation.

View File

@@ -73,7 +73,7 @@ interface WatchOptions<T extends WorkerMessage> {
/**
* Watch a web worker
*
* This function returns an observable that will send all values emitted by the
* This function returns an observable that sends all values emitted by the
* message observable to the web worker. Web worker communication is expected
* to be bidirectional (request-response) and synchronous. Messages that are
* emitted during a pending request are throttled, the last one is emitted.

View File

@@ -20,7 +20,7 @@
* IN THE SOFTWARE.
*/
import { Observable, merge } from "rxjs"
import { Observable, ObservableInput, merge } from "rxjs"
import { filter, sample, take } from "rxjs/operators"
import { configuration } from "~/_"
@@ -75,9 +75,9 @@ interface MountOptions {
*
* @param url - Search index URL
*
* @returns Promise resolving with search index
* @returns Promise or observable
*/
function fetchSearchIndex(url: string) {
function fetchSearchIndex(url: string): ObservableInput<SearchIndex> {
return __search?.index || requestJSON<SearchIndex>(url)
}
@@ -104,7 +104,7 @@ export function mountSearch(
`${config.base}/search/search_index.json`
))
/* Retrieve elements */
/* Retrieve nested components */
const query = getComponentElement("search-query", el)
const result = getComponentElement("search-result", el)

View File

@@ -20,14 +20,25 @@
* IN THE SOFTWARE.
*/
import { Observable, Subject } from "rxjs"
import {
Observable,
Subject,
animationFrameScheduler,
merge,
of
} from "rxjs"
import {
bufferCount,
distinctUntilKeyChanged,
filter,
finalize,
map,
observeOn,
startWith,
switchMap,
tap,
withLatestFrom
withLatestFrom,
zipWith
} from "rxjs/operators"
import {
@@ -36,7 +47,10 @@ import {
resetSearchResultMeta,
setSearchResultMeta
} from "~/actions"
import { getElementOrThrow } from "~/browser"
import {
getElementOrThrow,
watchElementThreshold
} from "~/browser"
import {
SearchResult as SearchResultData,
SearchWorker,
@@ -76,6 +90,9 @@ interface MountOptions {
/**
* Mount search result list
*
* 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
@@ -86,11 +103,16 @@ export function mountSearchResult(
el: HTMLElement, { rx$ }: SearchWorker, { query$ }: MountOptions
): Observable<Component<SearchResult>> {
const internal$ = new Subject<SearchResult>()
const boundary$ = watchElementThreshold(el.parentElement!)
.pipe(
filter(Boolean)
)
/* Update search result metadata */
const meta = getElementOrThrow(":scope > :first-child", el)
internal$
.pipe(
observeOn(animationFrameScheduler),
withLatestFrom(query$)
)
.subscribe(([{ data }, { value }]) => {
@@ -103,16 +125,22 @@ export function mountSearchResult(
/* Update search result list */
const list = getElementOrThrow(":scope > :last-child", el)
internal$
.subscribe(({ data }) => {
resetSearchResultList(list)
/* Compute thresholds and search results */
const thresholds = [...data.map(([best]) => best.score), 0]
for (let index = 0; index < data.length; index++)
addToSearchResultList(list, renderSearchResult(
data[index++], thresholds[index]
))
})
.pipe(
observeOn(animationFrameScheduler),
tap(() => resetSearchResultList(list)),
switchMap(({ data }) => merge(
of(...data.slice(0, 10)),
of(...data.slice(10))
.pipe(
bufferCount(4),
zipWith(boundary$),
switchMap(([chunk]) => of(...chunk))
)
))
)
.subscribe(result => {
addToSearchResultList(list, renderSearchResult(result))
})
/* Filter search result list */
const result$ = rx$

View File

@@ -54,7 +54,7 @@ const enum Flag {
*/
function renderSearchDocument(
document: SearchDocument & SearchMetadata, flag: Flag
) {
): HTMLElement {
const parent = flag & Flag.PARENT
const teaser = flag & Flag.TEASER
@@ -101,13 +101,13 @@ function renderSearchDocument(
* Render a search result
*
* @param result - Search result
* @param threshold - Score threshold
*
* @returns Element
*/
export function renderSearchResult(
result: SearchResult, threshold = Infinity
result: SearchResult
): HTMLElement {
const threshold = result[0].score
const docs = [...result]
/* Find and extract parent article */