import "../css/normalize.css"; import "milligram/dist/milligram.min.css"; import "../css/common.css"; import "../css/nav.css"; import "../css/overrides.css"; /** * Converts the given string to an HTML element. * * @param string the string to convert to an HTML element * @param query the type of element to return * @returns {HTMLElement} the HTML element described by the given string */ const stringToHtml = function (string, query) { return new DOMParser().parseFromString(string, "text/html").body.querySelector(query); } /** * Alias for `document.querySelector`. * * @param q {string} the query string * @returns {HTMLElement} the element identified by the query string */ export const $ = q => document.querySelector(q); /** * Runs the given function once the page is loaded. * * This function can be used multiple times. It does not overwrite existing callbacks for the page load event. * * @param fun {function(...*): *} the function to run */ export const doAfterLoad = function (fun) { const oldOnLoad = window.onload || (() => { }); window.onload = (() => { oldOnLoad(); fun(); }); }; /** * Creates a navigation element for navigating through the website. * * Fetches entries asynchronously from the website's API. * * @param [highlightPath] {String} the path to highlight together with its parents * @returns {HTMLElement} a base navigation element that will eventually be filled with contents */ export const nav = function (highlightPath = "") { const base = stringToHtml(` `, "ul"); fetch("https://fwdekker.com/api/nav/") .then(it => it.json()) .then(json => { json.entries.forEach(entry => base.appendChild(stringToHtml(unpackEntry(entry, "/", highlightPath), "li"))) }) .catch(e => { console.error("Failed to fetch navigation elements", e); return []; }); const nav = stringToHtml(``, "nav"); nav.appendChild(base); return nav; }; /** * Unpacks a navigation entry returned from the navigation API into an HTML element. * * @param entry {Object} the entry to unpack * @param [path] {number} the current path traversed, found by joining the names of the entries with `/`s; always starts * and ends with a `/` * @param [highlightPath] {String} the path to highlight together with its parents * @returns {string} the navigation list entry as HTML, described by its children */ const unpackEntry = function (entry, path = "/", highlightPath = "") { const shouldHighlight = highlightPath.startsWith(`${path + entry.name}/`); if (entry.entries.length === 0) return `
  • ${entry.name}
  • `; const depth = path.split("/").length - 2; // -1 because count parts, then another -1 because of leading `/` const arrow = depth === 0 ? "▾" : "▸"; return `
  • ${entry.name} ${arrow}
  • `; }; /** * Creates a header element with the given title and description. * * @param [title] {string} the title to display, possibly including HTML * @param [description] {string} the description to display, possibly including HTML * @returns {HTMLElement} a header element */ export const header = function ({title, description}) { if (title === undefined && description === undefined) return stringToHtml(`
    `, "header"); return stringToHtml(`
    ${(title !== undefined ? `

    ${title}

    ` : "")} ${(description !== undefined ? `

    ${description}

    ` : "")}
    `, "header"); }; /** * Creates a footer element with the given data. * * @param [author] {string|undefined} the author * @param [authorURL] {string|undefined} the URL to link the author's name to * @param [license] {string|undefined} the type of license * @param [licenseURL] {string|undefined} the URL to the license file * @param [vcs] {string|undefined} the type of version control * @param [vcsURL] {string|undefined} the URL to the repository * @param [version] {string|undefined} the page version * @param [privacyPolicyURL] {string|null|undefined} the URL to the privacy policy, or `null` if there should be no * privacy policy, or `undefined` if the default privacy policy should be used * @returns {HTMLElement} a footer element */ export const footer = function ( { author, authorURL, license, licenseURL, vcs, vcsURL, version, privacyPolicyURL = undefined }) { return stringToHtml(` `, "footer"); }; /** * Constructs a link that is used in footers. * * @param prefix {string} the text to display before the text if the text is not undefined * @param text {string|undefined} the text to display, or `undefined` if the returned element should be empty * @param url {string|undefined} the URL to link the text to, or `undefined` if the text should not be a link * @param suffix {string} the text to display after the text if the text is not undefined * @returns {string} a footer link element */ const footerLink = function (prefix, text, url, suffix) { if (text === undefined) return ""; return ` ${prefix} ${url !== undefined ? `${text}` : text} ${suffix} `; }; /** * Unhides the main element on the page and applies default display styling. */ export const showPage = function () { // Flex-based footer positioning, taken from https://stackoverflow.com/a/12253099 const main = $("main"); main.style.display = "flex"; main.style.flexDirection = "column"; main.style.minHeight = "100%"; $("#contents").style.flex = "1"; }