Added support for lazy rendering of search results

This commit is contained in:
squidfunk
2017-08-02 14:09:13 +02:00
parent ef1c5a4043
commit bbb3455c52
6 changed files with 88 additions and 48 deletions

View File

@@ -23,6 +23,30 @@
import escape from "escape-string-regexp"
import lunr from "expose-loader?lunr!lunr"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Truncate a string after the given number of character
*
* This is not a reasonable approach, since the summaries kind of suck. It
* would be better to create something more intelligent, highlighting the
* search occurrences and making a better summary out of it.
*
* @param {string} string - String to be truncated
* @param {number} n - Number of characters
* @return {string} Truncated string
*/
const truncate = (string, n) => {
let i = n
if (string.length > i) {
while (string[i] !== " " && --i > 0);
return `${string.substring(0, i)}...`
}
return string
}
/* ----------------------------------------------------------------------------
* Class
* ------------------------------------------------------------------------- */
@@ -42,6 +66,7 @@ export default class Result {
* @property {Array<string>} lang_ - Search languages
* @property {Object} message_ - Search result messages
* @property {Object} index_ - Search index
* @property {Array<Function>} stack_ - Search result stack
* @property {string} value_ - Last input value
*
* @param {(string|HTMLElement)} el - Selector or HTML element
@@ -81,26 +106,6 @@ export default class Result {
.map(lang => lang.trim())
}
/**
* Truncate a string after the given number of character
*
* This is not a reasonable approach, since the summaries kind of suck. It
* would be better to create something more intelligent, highlighting the
* search occurrences and making a better summary out of it.
*
* @param {string} string - String to be truncated
* @param {number} n - Number of characters
* @return {string} Truncated string
*/
truncate_(string, n) {
let i = n
if (string.length > i) {
while (string[i] !== " " && --i > 0);
return `${string.substring(0, i)}...`
}
return string
}
/**
* Update search results
*
@@ -147,7 +152,8 @@ export default class Result {
const docs = this.docs_,
lang = this.lang_
/* Create index */
/* Create stack and index */
this.stack_ = []
this.index_ = lunr(function() {
/* Remove stemmer, as it cripples search experience */
@@ -161,7 +167,7 @@ export default class Result {
if (lang.length === 1) {
this.use(lunr[lang[0]])
} else if (lang.length > 1) {
this.use(lunr.multiLanguage(...lang))
this.use(lunr.multiLanguage(...lang)) // TODO: remove
}
/* Index fields */
@@ -172,6 +178,16 @@ export default class Result {
/* Index documents */
docs.forEach(doc => this.add(doc))
})
/* Register event handler for lazy rendering */
const container = this.el_.parentNode
if (!(container instanceof HTMLElement))
throw new ReferenceError
container.addEventListener("scroll", () => {
while (this.stack_.length && container.scrollTop +
container.offsetHeight >= container.scrollHeight - 16)
this.stack_.splice(0, 10).forEach(render => render())
})
}
/* eslint-enable no-invalid-this */
@@ -208,7 +224,7 @@ export default class Result {
/* Append trailing wildcard to all terms for prefix querying */
.query(query => {
this.value_.split(" ")
this.value_.toLowerCase().split(" ")
.filter(Boolean)
.forEach(term => {
query.term(term, { wildcard: lunr.Query.wildcard.TRAILING })
@@ -236,12 +252,13 @@ export default class Result {
const highlight = (_, separator, token) =>
`${separator}<em>${token}</em>`
/* Render results */
/* Reset stack and render results */
this.stack_ = []
result.forEach((items, ref) => {
const doc = this.docs_.get(ref)
/* Append search result */
this.list_.appendChild(
/* Render article */
const article = (
<li class="md-search-result__item">
<a href={doc.location} title={doc.title}
class="md-search-result__link">
@@ -256,29 +273,44 @@ export default class Result {
</p> : {}}
</article>
</a>
{items.map(item => {
const section = this.docs_.get(item.ref)
return (
<a href={section.location} title={section.title}
class="md-search-result__link" data-md-rel="anchor">
<article class="md-search-result__article">
<h1 class="md-search-result__title">
{{ __html: section.title.replace(match, highlight) }}
</h1>
{section.text.length ?
<p class="md-search-result__teaser">
{{ __html: this.truncate_(
section.text.replace(match, highlight), 400)
}}
</p> : {}}
</article>
</a>
)
})}
</li>
)
/* Render sections for article */
const sections = items.map(item => {
return () => {
const section = this.docs_.get(item.ref)
article.appendChild(
<a href={section.location} title={section.title}
class="md-search-result__link" data-md-rel="anchor">
<article class="md-search-result__article">
<h1 class="md-search-result__title">
{{ __html: section.title.replace(match, highlight) }}
</h1>
{section.text.length ?
<p class="md-search-result__teaser">
{{ __html: truncate(
section.text.replace(match, highlight), 400)
}}
</p> : {}}
</article>
</a>
)
}
})
/* Push articles and section renderers onto stack */
this.stack_.push(() => this.list_.appendChild(article), ...sections)
})
/* Gradually add results as long as the height of the container grows */
const container = this.el_.parentNode
if (!(container instanceof HTMLElement))
throw new ReferenceError
while (this.stack_.length &&
container.offsetHeight >= container.scrollHeight - 16)
(this.stack_.shift())()
/* Bind click handlers for anchors */
const anchors = this.list_.querySelectorAll("[data-md-rel=anchor]")
Array.prototype.forEach.call(anchors, anchor => {