mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2026-08-01 09:58:47 -04:00
Refactored JavaScript architecture
This commit is contained in:
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2016 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 lunr from "lunr"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Definition
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export default class Index {
|
||||
|
||||
/**
|
||||
* // TODO: just copy+pasted
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
constructor() {
|
||||
const query = document.getElementById("query")
|
||||
// TODO: put this in search index class...
|
||||
// setTimeout(function() {
|
||||
|
||||
// indexed percentage!
|
||||
|
||||
fetch("/mkdocs/search_index.json") // TODO: prepend BASE URL!!!
|
||||
.then(response => {
|
||||
return response.json()
|
||||
})
|
||||
.then(data => {
|
||||
|
||||
/* Create index */
|
||||
const index = lunr(function() {
|
||||
/* eslint-disable no-invalid-this, lines-around-comment */
|
||||
this.field("title", { boost: 10 })
|
||||
this.field("text")
|
||||
this.ref("location")
|
||||
/* eslint-enable no-invalid-this, lines-around-comment */
|
||||
})
|
||||
|
||||
/* Index articles */
|
||||
const articles = {}
|
||||
data.docs.forEach((article, i) => {
|
||||
// console.log(`indexing...${i}`)
|
||||
const meta = document.querySelector(".md-search-result__meta")
|
||||
meta.innerHTML = `Indexing: ${(i + 1) / data.docs.length}%`
|
||||
|
||||
// TODO: match for two whitespaces, then replace unnecessary whitespace after string
|
||||
article.text = article.text.replace(/\s(\.,\:)\s/gi, (string, g1) => {
|
||||
return `${g1} `
|
||||
})
|
||||
// TODO: window.baseUrl sucks...
|
||||
article.location = window.baseUrl + article.location
|
||||
articles[article.location] = article
|
||||
index.add(article)
|
||||
})
|
||||
|
||||
/* Truncate a string after the given number of characters */
|
||||
const truncate = function(string, n) {
|
||||
let i = n
|
||||
if (string.length > i) {
|
||||
while (string[i] !== " " && --i > 0);
|
||||
return `${string.substring(0, i)}…`
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
/* Register keyhandler to execute search on key up */
|
||||
const queryx = document.getElementById("query")
|
||||
queryx.addEventListener("keyup", () => {
|
||||
const container = document.querySelector(".md-search-result__list")
|
||||
while (container.firstChild)
|
||||
container.removeChild(container.firstChild)
|
||||
|
||||
// /* Abort, if the query is empty */
|
||||
// var bar = document.querySelector('.bar.search');
|
||||
// if (!query.value.length) {
|
||||
// while (meta.firstChild)
|
||||
// meta.removeChild(meta.firstChild);
|
||||
//
|
||||
// /* Restore state */
|
||||
// bar.classList.remove('non-empty');
|
||||
// return;
|
||||
// }
|
||||
|
||||
/* Show reset button */
|
||||
// bar.classList.add('non-empty');
|
||||
|
||||
/* Execute search */
|
||||
const results = index.search(query.value)
|
||||
results.forEach(result => {
|
||||
const article = articles[result.ref]
|
||||
|
||||
container.appendChild(
|
||||
<li class="md-search-result__item">
|
||||
<a href={article.location} title={article.title}
|
||||
class="md-search-result__link">
|
||||
<article class="md-search-result__article">
|
||||
<h1 class="md-search-result__title">
|
||||
{article.title}
|
||||
</h1>
|
||||
<p class="md-search-result__teaser">
|
||||
{truncate(article.text, 140)}
|
||||
</p>
|
||||
</article>
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
})
|
||||
|
||||
/* Show number of search results */
|
||||
// var number = document.createElement('strong');
|
||||
|
||||
const meta = document.querySelector(".md-search-result__meta")
|
||||
meta.innerHTML = `${results.length} search result${
|
||||
results.length !== 1
|
||||
? "s"
|
||||
: ""}`
|
||||
|
||||
/* Update number */
|
||||
// while (meta.firstChild)
|
||||
// meta.removeChild(meta.firstChild);
|
||||
// meta.appendChild(number);
|
||||
})
|
||||
|
||||
// setTimeout(function() {
|
||||
// li.classList.remove('md-source__fact--hidden');
|
||||
// }, 100);
|
||||
|
||||
})
|
||||
.catch(() => {
|
||||
// console.log("parsing failed", ex)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -21,102 +21,68 @@
|
||||
*/
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Definition
|
||||
* Class
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export default class Lock {
|
||||
|
||||
/**
|
||||
* Lock body for full-screen search bar
|
||||
* Lock body for full-screen search modal
|
||||
*
|
||||
* @constructor
|
||||
* @param {(string|HTMLElement)} el - Selector or HTML element
|
||||
* @param {Function} handler - Callback to execute in active search mode
|
||||
*/
|
||||
constructor(el, handler) {
|
||||
constructor(el) {
|
||||
this.el_ = (typeof el === "string")
|
||||
? document.querySelector(el)
|
||||
: el
|
||||
|
||||
/* Initialize page y-offset and callback */
|
||||
this.offset_ = 0
|
||||
this.handler_ = handler
|
||||
|
||||
/* Dispatch update on next repaint */
|
||||
this.handler_ = ev => {
|
||||
this.update(ev)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state
|
||||
*
|
||||
* @param {Event} ev - Event
|
||||
* Setup locked state
|
||||
*/
|
||||
update(ev) {
|
||||
setup() {
|
||||
this.update()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update locked state
|
||||
*/
|
||||
update() {
|
||||
|
||||
/* Entering search mode */
|
||||
if (ev.target.checked) {
|
||||
this.offset_ = window.scrollY
|
||||
if (this.el_.checked) {
|
||||
this.offset_ = window.pageYOffset
|
||||
|
||||
/* First timeout: scroll to top after transition, to omit flickering */
|
||||
/* Scroll to top after transition, to omit flickering */
|
||||
setTimeout(() => {
|
||||
window.scrollTo(0, 0)
|
||||
}, 400)
|
||||
|
||||
/* Second timeout: Lock body after finishing transition and scrolling
|
||||
to top and focus input field. Sadly, the focus event is not dispatched
|
||||
on iOS Safari and there's nothing we can do about it. */
|
||||
setTimeout(() => {
|
||||
|
||||
/* This additional check is necessary to handle fast subsequent clicks
|
||||
on the toggle and the timeout to lock the body must be cancelled */
|
||||
if (ev.target.checked) {
|
||||
document.body.dataset.mdLocked = ""
|
||||
setTimeout(this.handler_, 200)
|
||||
/* Lock body after finishing transition */
|
||||
if (this.el_.checked) {
|
||||
document.body.dataset.mdState = "lock"
|
||||
}
|
||||
}, 400)
|
||||
|
||||
/* Exiting search mode */
|
||||
} else {
|
||||
delete document.body.dataset.mdLocked
|
||||
delete document.body.dataset.mdState
|
||||
|
||||
/* Scroll to former position, but wait for 100ms to prevent flashes on
|
||||
iOS. A short timeout seems to do the trick */
|
||||
setTimeout(() => {
|
||||
window.scrollTo(0, this.offset_)
|
||||
if (typeof this.offset_ !== "undefined")
|
||||
window.scrollTo(0, this.offset_)
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset state
|
||||
*
|
||||
* @param {Event} ev - Event
|
||||
* Reset locked state and page y-offset
|
||||
*/
|
||||
reset() {
|
||||
delete document.body.dataset.mdLocked
|
||||
window.scrollTo(0, this.offset_)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register listener for all relevant events
|
||||
*/
|
||||
listen() {
|
||||
["change"].forEach(name => {
|
||||
this.el_.addEventListener(name, this.handler_, false)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister listener for all relevant events
|
||||
*/
|
||||
unlisten() {
|
||||
["change"].forEach(name => {
|
||||
this.el_.removeEventListener(name, this.handler_, false)
|
||||
})
|
||||
|
||||
/* Final reset */
|
||||
this.reset()
|
||||
if (document.body.dataset.mdState)
|
||||
window.scrollTo(0, this.offset_)
|
||||
delete document.body.dataset.mdState
|
||||
}
|
||||
}
|
||||
|
||||
125
src/assets/javascripts/components/Material/Search/Result.jsx
Normal file
125
src/assets/javascripts/components/Material/Search/Result.jsx
Normal file
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright (c) 2016 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 lunr from "lunr"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Class
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export default class Result {
|
||||
|
||||
/**
|
||||
* Perform search and update results on keyboard events
|
||||
*
|
||||
* @constructor
|
||||
* @param {(string|HTMLElement)} el - Selector or HTML element
|
||||
* @param {(Array.<object>|Function)} data - Promise or array providing data
|
||||
*/
|
||||
constructor(el, data) {
|
||||
this.el_ = (typeof el === "string")
|
||||
? document.querySelector(el)
|
||||
: el
|
||||
|
||||
/* Set data and create metadata and list elements */
|
||||
this.data_ = data
|
||||
this.meta_ = (
|
||||
<div class="md-search-result__meta">
|
||||
Type to start searching
|
||||
</div>
|
||||
)
|
||||
this.list_ = (
|
||||
<ol class="md-search-result__list"></ol>
|
||||
)
|
||||
|
||||
/* Inject created elements */
|
||||
this.el_.appendChild(this.meta_)
|
||||
this.el_.appendChild(this.list_)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update search results
|
||||
*
|
||||
* @param {Event} ev - Input or focus event
|
||||
*/
|
||||
update(ev) {
|
||||
|
||||
/* Initialize index, if this has not be done yet */
|
||||
if (ev.type === "focus" && !this.index_) {
|
||||
|
||||
/* Initialize index */
|
||||
const init = data => {
|
||||
this.index_ = lunr(function() {
|
||||
/* eslint-disable no-invalid-this, lines-around-comment */
|
||||
this.field("title", { boost: 10 })
|
||||
this.field("text")
|
||||
this.ref("location")
|
||||
/* eslint-enable no-invalid-this, lines-around-comment */
|
||||
})
|
||||
|
||||
/* Index documents */
|
||||
this.data_ = data.reduce((docs, doc) => {
|
||||
this.index_.add(doc)
|
||||
docs[doc.location] = doc
|
||||
return docs
|
||||
}, {})
|
||||
}
|
||||
|
||||
/* Initialize index after short timeout to account for transition */
|
||||
setTimeout(() => {
|
||||
return typeof this.data_ === "function"
|
||||
? this.data_().then(init)
|
||||
: init(this.data_)
|
||||
}, 250)
|
||||
|
||||
/* Execute search on new input event after clearing current list */
|
||||
} else if (ev.type === "keyup") {
|
||||
while (this.list_.firstChild)
|
||||
this.list_.removeChild(this.list_.firstChild)
|
||||
|
||||
/* Perform search on index and render documents */
|
||||
const result = this.index_.search(ev.target.value)
|
||||
for (const item of result) {
|
||||
const doc = this.data_[item.ref]
|
||||
this.list_.appendChild(
|
||||
<li class="md-search-result__item">
|
||||
<a href={doc.location} title={doc.title}
|
||||
class="md-search-result__link">
|
||||
<article class="md-search-result__article">
|
||||
<h1 class="md-search-result__title">
|
||||
{doc.title}
|
||||
</h1>
|
||||
<p class="md-search-result__teaser">
|
||||
{doc.text}
|
||||
</p>
|
||||
</article>
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
/* Update search metadata */
|
||||
this.meta_.textContent =
|
||||
`${result.length} search result${result.length !== 1 ? "s" : ""}`
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user