mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2026-07-23 14:23:39 -04:00
Refactored JavaScript architecture
This commit is contained in:
@@ -20,234 +20,180 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Imports
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
import FastClick from "fastclick"
|
||||
|
||||
// import Expander from "./components/expander"
|
||||
|
||||
import GithubSourceFacts from "./components/GithubSourceFacts"
|
||||
import Material from "./components/Material"
|
||||
|
||||
// import Search from './components/search';
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Application
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
class Application {
|
||||
export default class Application {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* Create the application
|
||||
*
|
||||
* @constructor
|
||||
* @param {object} config Configuration object
|
||||
* @return {void}
|
||||
*/
|
||||
constructor(config) {
|
||||
this.config_ = config
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all components
|
||||
* Initialize all components and listeners
|
||||
*/
|
||||
initialize() {
|
||||
|
||||
/* Initialize sticky sidebars */
|
||||
this.initializeSidebar("[data-md-sidebar=primary]", "(min-width: 1200px)")
|
||||
this.initializeSidebar("[data-md-sidebar=secondary]")
|
||||
/* Initialize Modernizr and Fastclick */
|
||||
new Material.Event.Listener(document, "DOMContentLoaded", () => {
|
||||
|
||||
/* Initialize navigation style modifiers */
|
||||
this.initializeNavBlur("[data-md-sidebar=secondary] .md-nav__link")
|
||||
this.initializeNavCollapse("[data-md-collapse]", "(min-width: 1200px)")
|
||||
/* Test for iOS */
|
||||
Modernizr.addTest("ios", () => {
|
||||
return !!navigator.userAgent.match(/(iPad|iPhone|iPod)/g)
|
||||
})
|
||||
|
||||
// TODO
|
||||
if (this.hasGithubRepo()) {
|
||||
const githubSource = new GithubSourceFacts(
|
||||
this.config_.storage,
|
||||
this.config_.repo.url
|
||||
)
|
||||
githubSource.initialize()
|
||||
/* Test for web application context */
|
||||
Modernizr.addTest("standalone", () => {
|
||||
return !!navigator.standalone
|
||||
})
|
||||
|
||||
/* Attack FastClick to mitigate 300ms delay on touch devices */
|
||||
FastClick.attach(document.body)
|
||||
}).listen()
|
||||
|
||||
/* Cross-browser helper to dispatch/fire an event */
|
||||
const dispatch = (el, event) => {
|
||||
return document.createEvent
|
||||
? el.dispatchEvent(new Event(event))
|
||||
: el.fireEvent(`on${event}`, document.createEventObject())
|
||||
}
|
||||
|
||||
}
|
||||
/* Truncate a string after the given number of characters - 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 */
|
||||
const truncate = function(string, n) {
|
||||
let i = n
|
||||
if (string.length > i) {
|
||||
while (string[i] !== " " && --i > 0);
|
||||
return `${string.substring(0, i)}...`
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize sidebar within optional media query range
|
||||
*
|
||||
* @param {(string|HTMLElement)} el - Selector or HTML element
|
||||
* @param {string} [query] - Media query
|
||||
*/
|
||||
initializeSidebar(el, query = null) {
|
||||
const sidebar = new Material.Sidebar(el)
|
||||
const listeners = [
|
||||
new Material.Listener.Viewport.Offset(() => sidebar.update()),
|
||||
new Material.Listener.Viewport.Resize(() => sidebar.update())
|
||||
]
|
||||
/* Component: sidebar with navigation */
|
||||
new Material.Event.MatchMedia("(min-width: 1200px)",
|
||||
new Material.Event.Listener(window, [
|
||||
"scroll", "resize", "orientationchange"
|
||||
], new Material.Sidebar("[data-md-sidebar=primary]")))
|
||||
|
||||
/* Initialize depending on media query */
|
||||
if (typeof query === "string" && query.length) {
|
||||
new Material.Listener.Viewport.Media(query, media => {
|
||||
if (media.matches) {
|
||||
sidebar.update()
|
||||
for (const listener of listeners)
|
||||
listener.listen()
|
||||
} else {
|
||||
sidebar.reset()
|
||||
for (const listener of listeners)
|
||||
listener.unlisten()
|
||||
/* Component: sidebar with table of contents */
|
||||
new Material.Event.MatchMedia("(min-width: 960px)",
|
||||
new Material.Event.Listener(window, [
|
||||
"scroll", "resize", "orientationchange"
|
||||
], new Material.Sidebar("[data-md-sidebar=secondary]")))
|
||||
|
||||
/* Component: link blurring for table of contents */
|
||||
new Material.Event.MatchMedia("(min-width: 960px)",
|
||||
new Material.Event.Listener(window, "scroll",
|
||||
new Material.Nav.Blur("[data-md-sidebar=secondary] .md-nav__link")))
|
||||
|
||||
/* Component: collapsible elements for navigation */
|
||||
const collapsibles = document.querySelectorAll("[data-md-collapse]")
|
||||
for (const collapse of collapsibles)
|
||||
new Material.Event.MatchMedia("(min-width: 1200px)",
|
||||
new Material.Event.Listener(collapse.previousElementSibling, "click",
|
||||
new Material.Nav.Collapse(collapse)))
|
||||
|
||||
/* Component: search body lock for mobile */
|
||||
new Material.Event.MatchMedia("(max-width: 959px)",
|
||||
new Material.Event.Listener("[data-md-toggle=search]", "change",
|
||||
new Material.Search.Lock("[data-md-toggle=search]")))
|
||||
|
||||
/* Component: search results */
|
||||
new Material.Event.Listener(document.forms.search.query, [
|
||||
"focus", "keyup"
|
||||
], new Material.Search.Result("[data-md-search-result]", () => {
|
||||
return fetch(`${this.config_.url.base}/mkdocs/search_index.json`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
return data.docs.map(doc => {
|
||||
doc.location = this.config_.url.base + doc.location
|
||||
doc.text = truncate(doc.text, 140)
|
||||
return doc
|
||||
})
|
||||
})
|
||||
})).listen()
|
||||
|
||||
/* Listener: prevent touches on overlay if navigation is active */
|
||||
new Material.Event.MatchMedia("(max-width: 1199px)",
|
||||
new Material.Event.Listener("[data-md-overlay]", "touchstart",
|
||||
ev => ev.preventDefault()))
|
||||
|
||||
/* Listener: close drawer when anchor links are clicked */
|
||||
new Material.Event.MatchMedia("(max-width: 959px)",
|
||||
new Material.Event.Listener("[data-md-sidebar=primary] [href^='#']",
|
||||
"click", () => {
|
||||
const toggle = document.querySelector("[data-md-toggle=drawer]")
|
||||
if (toggle.checked) {
|
||||
toggle.checked = false
|
||||
dispatch(toggle, "change")
|
||||
}
|
||||
}))
|
||||
|
||||
/* Listener: focus input after activating search */
|
||||
new Material.Event.Listener("[data-md-toggle=search]", "change", ev => {
|
||||
setTimeout(toggle => {
|
||||
const query = document.forms.search.query
|
||||
if (toggle.checked)
|
||||
query.focus()
|
||||
}, 400, ev.target)
|
||||
}).listen()
|
||||
|
||||
/* Listener: activate search on focus */
|
||||
new Material.Event.MatchMedia("(min-width: 960px)",
|
||||
new Material.Event.Listener(document.forms.search.query, "focus", () => {
|
||||
const toggle = document.querySelector("[data-md-toggle=search]")
|
||||
if (!toggle.checked) {
|
||||
toggle.checked = true
|
||||
dispatch(toggle, "change")
|
||||
}
|
||||
}).listen()
|
||||
}))
|
||||
|
||||
/* Initialize without media query */
|
||||
} else {
|
||||
sidebar.update()
|
||||
for (const listener of listeners)
|
||||
listener.listen()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize blurring of anchors above page y-offset
|
||||
*
|
||||
* @param {(string|NodeList<HTMLElement>)} els - Selector or HTML elements
|
||||
*/
|
||||
initializeNavBlur(els) {
|
||||
const blur = new Material.Nav.Blur(els)
|
||||
const listeners = [
|
||||
new Material.Listener.Viewport.Offset(() => blur.update())
|
||||
]
|
||||
|
||||
/* Initialize blur and listeners */
|
||||
blur.update()
|
||||
for (const listener of listeners)
|
||||
listener.listen()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize collapsible nested navigation elements
|
||||
*
|
||||
* @param {(string|NodeList<HTMLElement>)} els - Selector or HTML elements
|
||||
* @param {string} [query] - Media query
|
||||
*/
|
||||
initializeNavCollapse(els, query = null) {
|
||||
const collapsibles = document.querySelectorAll(els)
|
||||
for (const collapsible of collapsibles) {
|
||||
const collapse = new Material.Nav.Collapse(collapsible)
|
||||
const listener = new Material.Listener.Toggle(
|
||||
collapsible.previousElementSibling, () => collapse.update())
|
||||
|
||||
/* Initialize depending on media query */
|
||||
new Material.Listener.Viewport.Media(query, media => {
|
||||
if (media.matches) {
|
||||
listener.listen()
|
||||
} else {
|
||||
collapse.reset()
|
||||
listener.unlisten()
|
||||
/* Listener: disable search when clicking outside */
|
||||
new Material.Event.MatchMedia("(min-width: 960px)",
|
||||
new Material.Event.Listener(document.body, "click", () => {
|
||||
const toggle = document.querySelector("[data-md-toggle=search]")
|
||||
if (toggle.checked) {
|
||||
toggle.checked = false
|
||||
dispatch(toggle, "change")
|
||||
}
|
||||
}).listen()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
/**
|
||||
* Is this application about a Github repository?
|
||||
*
|
||||
* @return {bool} - true if `repo.icon` or `repo.url` contains 'github'
|
||||
*/
|
||||
hasGithubRepo() {
|
||||
return this.config_.repo.icon === "github"
|
||||
|| this.config_.repo.url.includes("github")
|
||||
/* Listener: fix unclickable toggle due to blur handler */
|
||||
new Material.Event.MatchMedia("(min-width: 960px)",
|
||||
new Material.Event.Listener("[data-md-toggle=search]", "click",
|
||||
ev => ev.stopPropagation()))
|
||||
|
||||
/* Listener: prevent search from closing when clicking */
|
||||
new Material.Event.MatchMedia("(min-width: 960px)",
|
||||
new Material.Event.Listener("[data-md-search]", "click",
|
||||
ev => ev.stopPropagation()))
|
||||
|
||||
/* Retrieve the facts for the given repository type */
|
||||
;(() => {
|
||||
const el = document.querySelector("[data-md-source]")
|
||||
switch (el.dataset.mdSource) {
|
||||
case "github": return new Material.Source.Adapter.GitHub(el).fetch()
|
||||
default: return Promise.resolve([])
|
||||
}
|
||||
|
||||
/* Render repository source information */
|
||||
})().then(facts => {
|
||||
const sources = document.querySelectorAll("[data-md-source]")
|
||||
for (const source of sources)
|
||||
new Material.Source.Repository(source)
|
||||
.initialize(facts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default Application
|
||||
|
||||
// const consume = reader => {
|
||||
// let total = 0, body = ""
|
||||
// return new Promise((resolve, reject) => {
|
||||
// function pump() {
|
||||
// reader.read().then(({ done, value }) => {
|
||||
// if (done) {
|
||||
// console.log(body)
|
||||
// resolve()
|
||||
// return
|
||||
// }
|
||||
// total += value.byteLength
|
||||
// // value +=
|
||||
// body += value
|
||||
// console.log(`received ${value.byteLength}, total: ${total}`)
|
||||
// pump()
|
||||
// })
|
||||
// .catch(reject)
|
||||
// }
|
||||
// pump()
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// fetch("/mkdocs/search_index.json")
|
||||
// .then(res => consume(res.body.getReader()))
|
||||
// .then(() => console.log("consumed entire body"))
|
||||
// .catch(e => console.log(e))
|
||||
|
||||
// TODO: wrap in function call
|
||||
// application module export
|
||||
|
||||
/* Initialize application upon DOM ready */
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
/* Test for iOS */
|
||||
Modernizr.addTest("ios", () => {
|
||||
return !!navigator.userAgent.match(/(iPad|iPhone|iPod)/g)
|
||||
})
|
||||
|
||||
/* Test for web application context */
|
||||
Modernizr.addTest("standalone", () => {
|
||||
return !!navigator.standalone
|
||||
})
|
||||
|
||||
/* Attack FastClick to mitigate 300ms delay on touch devices */
|
||||
FastClick.attach(document.body)
|
||||
|
||||
// query.addEventListener("focus", () => {
|
||||
// document.querySelector(".md-search").dataset.mdLocked = ""
|
||||
// })
|
||||
|
||||
/* Intercept click on search mode toggle */
|
||||
|
||||
// TODO: this needs to be abstracted...
|
||||
document.getElementById("query").addEventListener("focus", () => {
|
||||
document.getElementById("search").checked = true
|
||||
})
|
||||
|
||||
// should be registered on body, but leads to problems
|
||||
document.querySelector(".md-container").addEventListener("click", () => {
|
||||
if (document.getElementById("search").checked)
|
||||
document.getElementById("search").checked = false
|
||||
})
|
||||
|
||||
// stop propagation, if search is active...
|
||||
document.querySelector(".md-search").addEventListener("click", ev => {
|
||||
ev.stopPropagation()
|
||||
})
|
||||
// toggleSearchClose.addEventListener("click", ev => {
|
||||
// ev.preventDefault()
|
||||
// // ev.target
|
||||
//
|
||||
// const search = document.getElementById("search")
|
||||
// search.checked = false
|
||||
// })
|
||||
|
||||
// }, 1000);
|
||||
|
||||
fetch(
|
||||
"https://api.github.com/repos/squidfunk/mkdocs-material/releases/latest")
|
||||
.then(response => {
|
||||
return response.json()
|
||||
})
|
||||
// .then(data => {
|
||||
// // console.log(data)
|
||||
// })
|
||||
|
||||
})
|
||||
|
||||
@@ -1,110 +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.
|
||||
*/
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Github Source
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export default
|
||||
class GithubSourceFacts {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @constructor
|
||||
* @param {object} storage - Accessor to storage, eg. `window.sessionStorage`
|
||||
* @param {string} repoUrl - URL to Github repository
|
||||
*/
|
||||
constructor(storage, repoUrl) {
|
||||
this.storage = storage
|
||||
this.storageKey = "github-source-facts"
|
||||
this.apiRepoUrl = repoUrl.replace("github.com/", "api.github.com/repos/")
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve stars and fork counts for the repository before invoking
|
||||
* `GithubSourceFacts.paint`.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
initialize() {
|
||||
const facts = this.storage.getItem(this.storageKey)
|
||||
|
||||
// Retrieve the facts, then invoke paint
|
||||
if (!facts) {
|
||||
fetch(this.apiRepoUrl)
|
||||
.then(response => {
|
||||
return response.json()
|
||||
})
|
||||
.then(data => {
|
||||
const repoFacts = {
|
||||
stars: data.stargazers_count,
|
||||
forks: data.forks_count
|
||||
}
|
||||
|
||||
this.storage.setItem(this.storageKey, JSON.stringify(repoFacts))
|
||||
|
||||
GithubSourceFacts.paint(repoFacts)
|
||||
})
|
||||
.catch(() => {
|
||||
// console.log("parsing failed", ex)
|
||||
})
|
||||
// Use the cached facts
|
||||
} else {
|
||||
GithubSourceFacts.paint(JSON.parse(facts))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates `.md-source__facts` with star and fork counts.
|
||||
*
|
||||
* @param {integer} options.stars - Stars count for the repo
|
||||
* @param {integer} options.forks - Fork count for the repo
|
||||
* @return {void}
|
||||
*/
|
||||
static paint({ stars, forks }) {
|
||||
const lists = document.querySelectorAll(".md-source__facts"); // TODO 2x list in drawer and header
|
||||
|
||||
// TODO: use ... of ...
|
||||
[].forEach.call(lists, list => {
|
||||
let li = (
|
||||
<li class="md-source__fact md-source__fact--hidden">
|
||||
{stars} Stars
|
||||
</li>
|
||||
)
|
||||
setTimeout(fact => {
|
||||
fact.classList.remove("md-source__fact--hidden")
|
||||
}, 100, li)
|
||||
list.appendChild(li)
|
||||
|
||||
li = (
|
||||
<li class="md-source__fact md-source__fact--hidden">
|
||||
{forks} Forks
|
||||
</li>
|
||||
)
|
||||
setTimeout(fact => {
|
||||
fact.classList.remove("md-source__fact--hidden")
|
||||
}, 500, li)
|
||||
list.appendChild(li)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -20,56 +20,20 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import Event from "./Material/Event"
|
||||
import Nav from "./Material/Nav"
|
||||
import Search from "./Material/Search"
|
||||
import Listener from "./Material/Listener"
|
||||
import Sidebar from "./Material/Sidebar"
|
||||
import Source from "./Material/Source"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Module
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export default {
|
||||
Event,
|
||||
Nav,
|
||||
Search,
|
||||
Listener,
|
||||
Sidebar
|
||||
Sidebar,
|
||||
Source
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Definition
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
// export default class Material {
|
||||
//
|
||||
|
||||
//
|
||||
|
||||
//
|
||||
// static initializeSearch() {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Initialize all components
|
||||
// */
|
||||
// static initialize() {
|
||||
//
|
||||
// const search = new Search.Lock("#search", () => {
|
||||
// document.getElementById("query").focus()
|
||||
// })
|
||||
// search.listen() // TODO when this is commented out, focusing the search somehow breaks things...
|
||||
//
|
||||
// const searchx = document.getElementById("search")
|
||||
// const initialize = () => {
|
||||
// const foo = new Search.Index()
|
||||
// console.log(foo)
|
||||
//
|
||||
// searchx.removeEventListener("change", initialize)
|
||||
// }
|
||||
// searchx.addEventListener("change", initialize)
|
||||
// console.log(searchx)
|
||||
//
|
||||
// // TODO nav bar is blurry until 959px, when expanded...
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -20,16 +20,14 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import Media from "./Viewport/Media"
|
||||
import Offset from "./Viewport/Offset"
|
||||
import Resize from "./Viewport/Resize"
|
||||
import Listener from "./Event/Listener"
|
||||
import MatchMedia from "./Event/MatchMedia"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Module
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export default {
|
||||
Media,
|
||||
Offset,
|
||||
Resize
|
||||
Listener,
|
||||
MatchMedia
|
||||
}
|
||||
77
src/assets/javascripts/components/Material/Event/Listener.js
Normal file
77
src/assets/javascripts/components/Material/Event/Listener.js
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Class
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export default class Listener {
|
||||
|
||||
/**
|
||||
* Generic event listener
|
||||
*
|
||||
* @constructor
|
||||
* @param {(string|NodeList<HTMLElement>)} els - Selector or HTML elements
|
||||
* @param {Array.<string>} events - Event names
|
||||
* @param {(object|function)} handler - Handler to be invoked
|
||||
*/
|
||||
constructor(els, events, handler) {
|
||||
this.els_ = (typeof els === "string")
|
||||
? document.querySelectorAll(els)
|
||||
: [].concat(els)
|
||||
|
||||
/* Set handler as function or directly as object */
|
||||
this.handler_ = typeof handler === "function"
|
||||
? { update: handler }
|
||||
: handler
|
||||
|
||||
/* Initialize event names and update handler */
|
||||
this.events_ = [].concat(events)
|
||||
this.update_ = ev => this.handler_.update(ev)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register listener for all relevant events
|
||||
*/
|
||||
listen() {
|
||||
for (const el of this.els_)
|
||||
for (const event of this.events_)
|
||||
el.addEventListener(event, this.update_, false)
|
||||
|
||||
/* Execute setup handler, if implemented */
|
||||
if (typeof this.handler_.setup === "function")
|
||||
this.handler_.setup()
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister listener for all relevant events
|
||||
*/
|
||||
unlisten() {
|
||||
for (const el of this.els_)
|
||||
for (const event of this.events_)
|
||||
el.removeEventListener(event, this.update_)
|
||||
|
||||
/* Execute reset handler, if implemented */
|
||||
if (typeof this.handler_.reset === "function")
|
||||
this.handler_.reset()
|
||||
}
|
||||
}
|
||||
@@ -21,37 +21,34 @@
|
||||
*/
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Definition
|
||||
* Class
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export default class Media {
|
||||
export default class MatchMedia {
|
||||
|
||||
/**
|
||||
* Listener which checks for media queries on dimension changes
|
||||
* Media query listener
|
||||
*
|
||||
* This class listens for state changes of media queries and automatically
|
||||
* switches the given listeners on or off.
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} query - Media query
|
||||
* @param {Function} handler - Event handler to execute
|
||||
* @param {string} query - Media query to test for
|
||||
* @param {Listener} listener - Event listener
|
||||
*/
|
||||
constructor(query, handler) {
|
||||
this.media_ = window.matchMedia(query)
|
||||
this.handler_ = media => {
|
||||
handler(media)
|
||||
constructor(query, listener) {
|
||||
this.handler_ = mq => {
|
||||
if (mq.matches)
|
||||
listener.listen()
|
||||
else
|
||||
listener.unlisten()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register listener for media query check
|
||||
*/
|
||||
listen() {
|
||||
this.media_.addListener(this.handler_)
|
||||
this.handler_(this.media_)
|
||||
}
|
||||
/* Initialize media query listener */
|
||||
const media = window.matchMedia(query)
|
||||
media.addListener(this.handler_)
|
||||
|
||||
/**
|
||||
* Unregister listener for media query check
|
||||
*/
|
||||
unlisten() {
|
||||
this.media_.removeListener(this.handler_)
|
||||
/* Always check at initialization */
|
||||
this.handler_(media)
|
||||
}
|
||||
}
|
||||
@@ -1,40 +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 Abstract from "../Abstract"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Definition
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export default class Resize extends Abstract {
|
||||
|
||||
/**
|
||||
* Listener which monitors changes to the dimensions of the viewport
|
||||
*
|
||||
* @constructor
|
||||
* @param {Function} handler - Event handler to execute
|
||||
*/
|
||||
constructor(handler) {
|
||||
super(window, ["resize", "orientationchange"], handler)
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
*/
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Definition
|
||||
* Class
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export default class Blur {
|
||||
@@ -47,6 +47,13 @@ export default class Blur {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize anchor states
|
||||
*/
|
||||
setup() {
|
||||
this.update()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update anchor states
|
||||
*/
|
||||
@@ -58,7 +65,7 @@ export default class Blur {
|
||||
for (let i = this.index_ + 1; i < this.els_.length; i++) {
|
||||
if (this.anchors_[i].offsetTop <= offset) {
|
||||
if (i > 0)
|
||||
this.els_[i - 1].dataset.mdBlurred = ""
|
||||
this.els_[i - 1].dataset.mdState = "blur"
|
||||
this.index_ = i
|
||||
} else {
|
||||
break
|
||||
@@ -70,7 +77,7 @@ export default class Blur {
|
||||
for (let i = this.index_; i >= 0; i--) {
|
||||
if (this.anchors_[i].offsetTop > offset) {
|
||||
if (i > 0)
|
||||
delete this.els_[i - 1].dataset.mdBlurred
|
||||
delete this.els_[i - 1].dataset.mdState
|
||||
} else {
|
||||
this.index_ = i
|
||||
break
|
||||
@@ -86,8 +93,11 @@ export default class Blur {
|
||||
* Reset anchor states
|
||||
*/
|
||||
reset() {
|
||||
[].forEach.call(this.els_, el => {
|
||||
delete el.dataset.mdBlurred
|
||||
})
|
||||
for (const el of this.els_)
|
||||
delete el.dataset.mdState
|
||||
|
||||
/* Reset index and page y-offset */
|
||||
this.index_ = 0
|
||||
this.offset_ = window.pageYOffset
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*/
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Definition
|
||||
* Class
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export default class Collapse {
|
||||
@@ -39,7 +39,7 @@ export default class Collapse {
|
||||
}
|
||||
|
||||
/**
|
||||
* Make expand and collapse transition smoothly
|
||||
* Animate expand and collapse smoothly
|
||||
*/
|
||||
update() {
|
||||
const current = this.el_.getBoundingClientRect().height
|
||||
@@ -48,43 +48,43 @@ export default class Collapse {
|
||||
if (current) {
|
||||
this.el_.style.maxHeight = `${current}px`
|
||||
requestAnimationFrame(() => {
|
||||
this.el_.dataset.mdAnimated = ""
|
||||
this.el_.dataset.mdState = "animate"
|
||||
this.el_.style.maxHeight = "0px"
|
||||
})
|
||||
|
||||
/* Collapsed, so expand */
|
||||
} else {
|
||||
this.el_.dataset.mdState = "expand"
|
||||
this.el_.style.maxHeight = ""
|
||||
this.el_.dataset.mdExpanded = ""
|
||||
|
||||
/* Read height and unset pseudo-toggled state */
|
||||
const height = this.el_.getBoundingClientRect().height
|
||||
delete this.el_.dataset.mdExpanded
|
||||
delete this.el_.dataset.mdState
|
||||
|
||||
/* Set initial state and animate */
|
||||
this.el_.style.maxHeight = "0px"
|
||||
requestAnimationFrame(() => {
|
||||
this.el_.dataset.mdAnimated = ""
|
||||
this.el_.dataset.mdState = "animate"
|
||||
this.el_.style.maxHeight = `${height}px`
|
||||
})
|
||||
}
|
||||
|
||||
/* Remove state on end of transition */
|
||||
const end = function(ev) {
|
||||
delete ev.target.dataset.mdAnimated
|
||||
delete ev.target.dataset.mdState
|
||||
ev.target.style.maxHeight = ""
|
||||
|
||||
/* Only fire once, so remove event listener again */
|
||||
/* Only fire once, so directly remove event listener */
|
||||
ev.target.removeEventListener("transitionend", end, false)
|
||||
}
|
||||
this.el_.addEventListener("transitionend", end, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Nothing to reset
|
||||
* Reset height and pseudo-toggled state
|
||||
*/
|
||||
reset() {
|
||||
delete this.el_.dataset.mdState
|
||||
this.el_.style.maxHeight = ""
|
||||
delete this.el_.dataset.mdToggled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,14 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import Index from "./Search/Index"
|
||||
import Lock from "./Search/Lock"
|
||||
import Result from "./Search/Result"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Module
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export default {
|
||||
Index,
|
||||
Lock
|
||||
Lock,
|
||||
Result
|
||||
}
|
||||
|
||||
@@ -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" : ""}`
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
*/
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Definition
|
||||
* Class
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export default class Sidebar {
|
||||
@@ -37,7 +37,7 @@ export default class Sidebar {
|
||||
? document.querySelector(el)
|
||||
: el
|
||||
|
||||
/* Index inner and outer container */
|
||||
/* Retrieve inner and outer container */
|
||||
const inner = this.el_.parentNode
|
||||
const outer = this.el_.parentNode.parentNode
|
||||
|
||||
@@ -52,6 +52,13 @@ export default class Sidebar {
|
||||
this.height_ = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize sidebar state
|
||||
*/
|
||||
setup() {
|
||||
this.update()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update locked state and height
|
||||
*/
|
||||
@@ -71,12 +78,12 @@ export default class Sidebar {
|
||||
|
||||
/* Sidebar should be locked, as we're below parent offset */
|
||||
if (offset < this.offset_) {
|
||||
if (!this.el_.dataset.mdLocked)
|
||||
this.el_.dataset.mdLocked = ""
|
||||
if (this.el_.dataset.mdState !== "lock")
|
||||
this.el_.dataset.mdState = "lock"
|
||||
|
||||
/* Sidebar should be unlocked, if locked */
|
||||
} else if (typeof this.el_.dataset.mdLocked === "string") {
|
||||
delete this.el_.dataset.mdLocked
|
||||
} else if (this.el_.dataset.mdState === "lock") {
|
||||
delete this.el_.dataset.mdState
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +91,7 @@ export default class Sidebar {
|
||||
* Reset locked state and height
|
||||
*/
|
||||
reset() {
|
||||
delete this.el_.dataset.mdLocked
|
||||
delete this.el_.dataset.mdState
|
||||
this.el_.style.height = ""
|
||||
this.height_ = 0
|
||||
}
|
||||
|
||||
@@ -20,19 +20,14 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import Adapter from "./Source/Adapter"
|
||||
import Repository from "./Source/Repository"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Definition
|
||||
* Module
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export default
|
||||
class Abstract {
|
||||
|
||||
/**
|
||||
* Dispatch update on next repaint
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
// constructor() {
|
||||
//
|
||||
// }
|
||||
export default {
|
||||
Adapter,
|
||||
Repository
|
||||
}
|
||||
@@ -20,14 +20,12 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import Toggle from "./Listener/Toggle"
|
||||
import Viewport from "./Listener/Viewport"
|
||||
import GitHub from "./Adapter/GitHub"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Module
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export default {
|
||||
Toggle,
|
||||
Viewport
|
||||
GitHub
|
||||
}
|
||||
@@ -20,49 +20,71 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import Cookies from "js-cookie"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Definition
|
||||
* Class
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export default class Abstract {
|
||||
|
||||
/**
|
||||
* Abstract listener
|
||||
* Retrieve source information
|
||||
*
|
||||
* @constructor
|
||||
* @param {(string|HTMLElement)} el - Selector or HTML element
|
||||
* @param {Array.<string>} events - Event names to listen on
|
||||
* @param {Function} handler - Event handler to execute
|
||||
*/
|
||||
constructor(el, events, handler) {
|
||||
if (this === Abstract)
|
||||
throw new Error("Cannot construct abstract instance")
|
||||
|
||||
/* Resolve element */
|
||||
constructor(el) {
|
||||
this.el_ = (typeof el === "string")
|
||||
? document.querySelector(el)
|
||||
: el
|
||||
|
||||
/* Set event names and handler */
|
||||
this.events_ = events
|
||||
this.handler_ = handler
|
||||
/* Retrieve base URL */
|
||||
this.base_ = this.el_.href
|
||||
}
|
||||
|
||||
/**
|
||||
* Register listener for all relevant events
|
||||
* Retrieve data from Cookie or fetch from respective API
|
||||
*
|
||||
* @return {Promise} Promise that returns an array of facts
|
||||
*/
|
||||
listen() {
|
||||
this.events_.forEach(name => {
|
||||
this.el_.addEventListener(name, this.handler_, false)
|
||||
fetch() {
|
||||
return new Promise(resolve => {
|
||||
const cached = Cookies.getJSON(".cache-source")
|
||||
if (typeof cached !== "undefined") {
|
||||
resolve(cached)
|
||||
|
||||
/* If the data is not cached in a cookie, invoke fetch */
|
||||
} else {
|
||||
this.fetch_().then(data => {
|
||||
Cookies.set(".cache-source", data)
|
||||
resolve(data)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister listener for all relevant events
|
||||
* Abstract private function that fetches relevant repository information
|
||||
*
|
||||
* @abstract
|
||||
* @return {Promise} Promise that provides the facts in an array
|
||||
*/
|
||||
unlisten() {
|
||||
this.events_.forEach(name => {
|
||||
this.el_.removeEventListener(name, this.handler_, false)
|
||||
})
|
||||
fetch_() {
|
||||
throw new Error("fetch_(): Not implemented")
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a number with suffix
|
||||
*
|
||||
* @param {Number} number - Number to format
|
||||
* @return {Number} Formatted number
|
||||
*/
|
||||
format_(number) {
|
||||
if (number > 10000)
|
||||
return `${(number / 1000).toFixed(0)}k`
|
||||
else if (number > 1000)
|
||||
return `${(number / 1000).toFixed(1)}k`
|
||||
return number
|
||||
}
|
||||
}
|
||||
@@ -20,21 +20,40 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import Abstract from "../Abstract"
|
||||
import Abstract from "./Abstract"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Definition
|
||||
* Class
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export default class Offset extends Abstract {
|
||||
export default class GitHub extends Abstract {
|
||||
|
||||
/**
|
||||
* Listener which monitors changes to the offset of the viewport
|
||||
* Retrieve source information from GitHub
|
||||
*
|
||||
* @constructor
|
||||
* @param {Function} handler - Event handler to execute
|
||||
* @param {(string|HTMLElement)} el - Selector or HTML element
|
||||
*/
|
||||
constructor(handler) {
|
||||
super(window, ["scroll"], handler)
|
||||
constructor(el) {
|
||||
super(el)
|
||||
|
||||
/* Adjust base URL to reach API endpoints */
|
||||
this.base_ = this.base_.replace("github.com/", "api.github.com/repos/")
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch relevant source information from GitHub
|
||||
*
|
||||
* @return {function} Promise returning an array of facts
|
||||
*/
|
||||
fetch_() {
|
||||
return fetch(this.base_)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
return [
|
||||
`${this.format_(data.stargazers_count)} Stars`,
|
||||
`${this.format_(data.forks_count)} Forks`
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -20,22 +20,39 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import Abstract from "./Abstract"
|
||||
import Cookies from "js-cookie"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Definition
|
||||
* Class
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export default class Toggle extends Abstract {
|
||||
export default class Repository {
|
||||
|
||||
/**
|
||||
* Listener which monitors state changes of a toggle
|
||||
* Render repository information
|
||||
*
|
||||
* @constructor
|
||||
* @param {(string|HTMLElement)} el - Selector or HTML element
|
||||
* @param {Function} handler - Event handler to execute
|
||||
*/
|
||||
constructor(el, handler) {
|
||||
super(el, ["click"], handler)
|
||||
constructor(el) {
|
||||
this.el_ = (typeof el === "string")
|
||||
? document.querySelector(el)
|
||||
: el
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the source repository
|
||||
*
|
||||
* @param {Array.<string>} facts - Facts to be rendered
|
||||
*/
|
||||
initialize(facts) {
|
||||
this.el_.children[0].appendChild(
|
||||
<ul class="md-source__facts">
|
||||
{facts.map(fact => <li class="md-source__fact">{fact}</li>)}
|
||||
</ul>
|
||||
)
|
||||
|
||||
/* Finish rendering with animation */
|
||||
this.el_.dataset.mdState = "done"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user