Refactored JavaScript architecture

This commit is contained in:
squidfunk
2016-12-15 15:55:40 +01:00
parent 65f23355c8
commit 2fcfb551be
73 changed files with 9541 additions and 921 deletions

View File

@@ -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)}&hellip;`
}
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)
})
}
}

View File

@@ -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
}
}

View 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" : ""}`
}
}
}