/* * Copyright (c) 2016-2019 Martin Donath * * 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 "../images/icons/bitbucket.svg" import "../images/icons/github.svg" import "../images/icons/gitlab.svg" import "../stylesheets/application.scss" import "../stylesheets/application-palette.scss" /* ---------------------------------------------------------------------------- * Polyfills * ------------------------------------------------------------------------- */ import "custom-event-polyfill" import "unfetch/polyfill" import Promise from "promise-polyfill" window.Promise = window.Promise || Promise /* ---------------------------------------------------------------------------- * Dependencies * ------------------------------------------------------------------------- */ import Clipboard from "clipboard" import Material from "./components/Material" /* ---------------------------------------------------------------------------- * Functions * ------------------------------------------------------------------------- */ /** * Return the meta tag value for the given key * * @param {string} key - Meta name * * @return {string} Meta content value */ const translate = key => { const meta = document.getElementsByName(`lang:${key}`)[0] if (!(meta instanceof HTMLMetaElement)) throw new ReferenceError return meta.content } /* ---------------------------------------------------------------------------- * Application * ------------------------------------------------------------------------- */ /** * Initialize Material for MkDocs * * @param {Object} config - Configuration */ function initialize(config) { // eslint-disable-line func-style /* Initialize Modernizr */ new Material.Event.Listener(document, "DOMContentLoaded", () => { if (!(document.body instanceof HTMLElement)) throw new ReferenceError /* Test for iOS */ Modernizr.addTest("ios", () => { return !!navigator.userAgent.match(/(iPad|iPhone|iPod)/g) }) /* Wrap all data tables for better overflow scrolling */ const tables = document.querySelectorAll("table:not([class])") // TODO: this is JSX, we should rename the file Array.prototype.forEach.call(tables, table => { const wrap = (
) if (table.nextSibling) { table.parentNode.insertBefore(wrap, table.nextSibling) } else { table.parentNode.appendChild(wrap) } wrap.children[0].appendChild(table) }) /* Clipboard integration */ if (Clipboard.isSupported()) { const blocks = document.querySelectorAll(".codehilite > pre, pre > code") Array.prototype.forEach.call(blocks, (block, index) => { const id = `__code_${index}` /* Create button with message container */ const button = ( ) /* Link to block and insert button */ const parent = block.parentNode parent.id = id parent.insertBefore(button, block) }) /* Initialize Clipboard listener */ const copy = new Clipboard(".md-clipboard") /* Success handler */ copy.on("success", action => { const message = action.trigger.querySelector(".md-clipboard__message") if (!(message instanceof HTMLElement)) throw new ReferenceError /* Clear selection and reset debounce logic */ action.clearSelection() if (message.dataset.mdTimer) clearTimeout(parseInt(message.dataset.mdTimer, 10)) /* Set message indicating success and show it */ message.classList.add("md-clipboard__message--active") message.innerHTML = translate("clipboard.copied") /* Hide message after two seconds */ message.dataset.mdTimer = setTimeout(() => { message.classList.remove("md-clipboard__message--active") message.dataset.mdTimer = "" }, 2000).toString() }) } /* Polyfill details/summary functionality */ if (!Modernizr.details) { const blocks = document.querySelectorAll("details > summary") Array.prototype.forEach.call(blocks, summary => { summary.addEventListener("click", ev => { const details = ev.target.parentNode if (details.hasAttribute("open")) { details.removeAttribute("open") } else { details.setAttribute("open", "") } }) }) } /* Open details after anchor jump */ const details = () => { if (document.location.hash) { const el = document.getElementById(document.location.hash.substring(1)) if (!el) return /* Walk up as long as we're not in a details tag */ let parent = el.parentNode while (parent && !(parent instanceof HTMLDetailsElement)) parent = parent.parentNode /* If there's a details tag, open it */ if (parent && !parent.open) { parent.open = true /* Force reload, so the viewport repositions */ const loc = location.hash location.hash = " " location.hash = loc } } } window.addEventListener("hashchange", details) details() /* Force 1px scroll offset to trigger overflow scrolling */ if (Modernizr.ios) { const scrollable = document.querySelectorAll("[data-md-scrollfix]") Array.prototype.forEach.call(scrollable, item => { item.addEventListener("touchstart", () => { const top = item.scrollTop /* We're at the top of the container */ if (top === 0) { item.scrollTop = 1 /* We're at the bottom of the container */ } else if (top + item.offsetHeight === item.scrollHeight) { item.scrollTop = top - 1 } }) }) } }).listen() /* Component: header shadow toggle */ new Material.Event.Listener(window, [ "scroll", "resize", "orientationchange" ], new Material.Header.Shadow( "[data-md-component=container]", "[data-md-component=header]") ).listen() /* Component: header title toggle */ new Material.Event.Listener(window, [ "scroll", "resize", "orientationchange" ], new Material.Header.Title( "[data-md-component=title]", ".md-typeset h1") ).listen() /* Component: hero visibility toggle */ if (document.querySelector("[data-md-component=hero]")) new Material.Event.Listener(window, [ "scroll", "resize", "orientationchange" ], new Material.Tabs.Toggle("[data-md-component=hero]")).listen() /* Component: tabs visibility toggle */ if (document.querySelector("[data-md-component=tabs]")) new Material.Event.Listener(window, [ "scroll", "resize", "orientationchange" ], new Material.Tabs.Toggle("[data-md-component=tabs]")).listen() /* Component: sidebar with navigation */ new Material.Event.MatchMedia("(min-width: 1220px)", new Material.Event.Listener(window, [ "scroll", "resize", "orientationchange" ], new Material.Sidebar.Position( "[data-md-component=navigation]", "[data-md-component=header]"))) /* Component: sidebar with table of contents (missing on 404 page) */ if (document.querySelector("[data-md-component=toc]")) new Material.Event.MatchMedia("(min-width: 960px)", new Material.Event.Listener(window, [ "scroll", "resize", "orientationchange" ], new Material.Sidebar.Position( "[data-md-component=toc]", "[data-md-component=header]"))) /* 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-component=toc] .md-nav__link"))) /* Component: collapsible elements for navigation */ const collapsibles = document.querySelectorAll("[data-md-component=collapsible]") Array.prototype.forEach.call(collapsibles, collapse => { new Material.Event.MatchMedia("(min-width: 1220px)", new Material.Event.Listener(collapse.previousElementSibling, "click", new Material.Nav.Collapse(collapse))) }) /* Component: active pane monitor for iOS scrolling fixes */ new Material.Event.MatchMedia("(max-width: 1219px)", new Material.Event.Listener( "[data-md-component=navigation] [data-md-toggle]", "change", new Material.Nav.Scrolling("[data-md-component=navigation] nav"))) /* Initialize search, if available */ if (document.querySelector("[data-md-component=search]")) { /* 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("[data-md-component=query]", [ "focus", "keyup", "change" ], new Material.Search.Result("[data-md-component=result]", () => { return fetch(`${config.url.base}/search/search_index.json`, { credentials: "same-origin" }).then(response => response.json()) .then(data => { return data.docs.map(doc => { doc.location = `${config.url.base}/${doc.location}` return doc }) }) })).listen() /* Listener: focus input after form reset */ new Material.Event.Listener("[data-md-component=reset]", "click", () => { setTimeout(() => { const query = document.querySelector("[data-md-component=query]") if (!(query instanceof HTMLInputElement)) throw new ReferenceError query.focus() }, 10) }).listen() /* Listener: focus input after opening search */ new Material.Event.Listener("[data-md-toggle=search]", "change", ev => { setTimeout(toggle => { if (!(toggle instanceof HTMLInputElement)) throw new ReferenceError if (toggle.checked) { const query = document.querySelector("[data-md-component=query]") if (!(query instanceof HTMLInputElement)) throw new ReferenceError query.focus() } }, 400, ev.target) }).listen() /* Listener: open search on focus */ new Material.Event.MatchMedia("(min-width: 960px)", new Material.Event.Listener("[data-md-component=query]", "focus", () => { const toggle = document.querySelector("[data-md-toggle=search]") if (!(toggle instanceof HTMLInputElement)) throw new ReferenceError if (!toggle.checked) { toggle.checked = true toggle.dispatchEvent(new CustomEvent("change")) } })) /* Listener: keyboard handlers */ // eslint-disable-next-line complexity new Material.Event.Listener(window, "keydown", ev => { // TODO: split up into component to reduce complexity const toggle = document.querySelector("[data-md-toggle=search]") if (!(toggle instanceof HTMLInputElement)) throw new ReferenceError const query = document.querySelector("[data-md-component=query]") if (!(query instanceof HTMLInputElement)) throw new ReferenceError /* Skip editable elements */ if (document.activeElement instanceof HTMLElement && document.activeElement.contentEditable === "true") return /* Abort if meta key (macOS) or ctrl key (Windows) is pressed */ if (ev.metaKey || ev.ctrlKey) return /* Search is open */ if (toggle.checked) { /* Enter: prevent form submission */ if (ev.keyCode === 13) { if (query === document.activeElement) { ev.preventDefault() /* Go to current active/focused link */ const focus = document.querySelector( "[data-md-component=search] [href][data-md-state=active]") if (focus instanceof HTMLLinkElement) { window.location = focus.getAttribute("href") /* Close search */ toggle.checked = false toggle.dispatchEvent(new CustomEvent("change")) query.blur() } } /* Escape or Tab: close search */ } else if (ev.keyCode === 9 || ev.keyCode === 27) { toggle.checked = false toggle.dispatchEvent(new CustomEvent("change")) query.blur() /* Horizontal arrows and backspace: focus input */ } else if ([8, 37, 39].indexOf(ev.keyCode) !== -1) { if (query !== document.activeElement) query.focus() /* Vertical arrows: select previous or next search result */ } else if ([38, 40].indexOf(ev.keyCode) !== -1) { const key = ev.keyCode /* Retrieve all results */ const links = Array.prototype.slice.call( document.querySelectorAll( "[data-md-component=query], [data-md-component=search] [href]")) /* Retrieve current active/focused result */ const focus = links.find(link => { if (!(link instanceof HTMLElement)) throw new ReferenceError return link.dataset.mdState === "active" }) if (focus) focus.dataset.mdState = "" /* Calculate index depending on direction, add length to form ring */ const index = Math.max(0, ( links.indexOf(focus) + links.length + (key === 38 ? -1 : +1) ) % links.length) /* Set active state and focus */ if (links[index]) { links[index].dataset.mdState = "active" links[index].focus() } /* Prevent scrolling of page */ ev.preventDefault() ev.stopPropagation() /* Return false prevents the cursor position from changing */ return false } /* Search is closed and we're not inside a form */ } else if (document.activeElement && !document.activeElement.form) { /* F/S: Open search if not in input field */ if (ev.keyCode === 70 || ev.keyCode === 83) { query.focus() ev.preventDefault() } } }).listen() /* Listener: focus query if in search is open and character is typed */ new Material.Event.Listener(window, "keypress", () => { const toggle = document.querySelector("[data-md-toggle=search]") if (!(toggle instanceof HTMLInputElement)) throw new ReferenceError if (toggle.checked) { const query = document.querySelector("[data-md-component=query]") if (!(query instanceof HTMLInputElement)) throw new ReferenceError if (query !== document.activeElement) query.focus() } }).listen() } /* Listener: handle tabbing context for better accessibility */ new Material.Event.Listener(document.body, "keydown", ev => { if (ev.keyCode === 9) { const labels = document.querySelectorAll( "[data-md-component=navigation] .md-nav__link[for]:not([tabindex])") Array.prototype.forEach.call(labels, label => { if (label.offsetHeight) label.tabIndex = 0 }) } }).listen() /* Listener: reset tabbing behavior */ new Material.Event.Listener(document.body, "mousedown", () => { const labels = document.querySelectorAll( "[data-md-component=navigation] .md-nav__link[tabindex]") Array.prototype.forEach.call(labels, label => { label.removeAttribute("tabIndex") }) }).listen() document.body.addEventListener("click", () => { if (document.body.dataset.mdState === "tabbing") document.body.dataset.mdState = "" }) /* Listener: close drawer when anchor links are clicked */ new Material.Event.MatchMedia("(max-width: 959px)", new Material.Event.Listener("[data-md-component=navigation] [href^='#']", "click", () => { const toggle = document.querySelector("[data-md-toggle=drawer]") if (!(toggle instanceof HTMLInputElement)) throw new ReferenceError if (toggle.checked) { toggle.checked = false toggle.dispatchEvent(new CustomEvent("change")) } })) /* Retrieve facts for the given repository type */ ;(() => { const el = document.querySelector("[data-md-source]") if (!el) return Promise.resolve([]) else if (!(el instanceof HTMLAnchorElement)) throw new ReferenceError switch (el.dataset.mdSource) { case "github": return new Material.Source.Adapter.GitHub(el).fetch() default: return Promise.resolve([]) } /* Render repository information */ })().then(facts => { const sources = document.querySelectorAll("[data-md-source]") Array.prototype.forEach.call(sources, source => { new Material.Source.Repository(source) .initialize(facts) }) }) /* Before-print hook */ const print = () => { const details = document.querySelectorAll("details") Array.prototype.forEach.call(details, detail => { detail.setAttribute("open", "") }) } /* Open details before printing */ new Material.Event.MatchMedia("print", { listen: print, unlisten: () => {} }) // Webkit window.onbeforeprint = print // IE, FF } /* ---------------------------------------------------------------------------- * Exports * ------------------------------------------------------------------------- */ /* Provide this for downward compatibility for now */ const app = { initialize } export { app }