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

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

View File

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

View File

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

View File

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

View 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()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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`
]
})
}
}

View File

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