testing moving the functionality into small modules #5
245
index.html
245
index.html
@ -125,15 +125,15 @@
|
|||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: var(--background);
|
color: var(--background);
|
||||||
}
|
}
|
||||||
.menu-is-open .menu {
|
.is-menu-open .menu {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
transition-duration: 0.1s;
|
transition-duration: 0.1s;
|
||||||
}
|
}
|
||||||
.menu-is-open .burger {
|
.is-menu-open .burger {
|
||||||
color: transparent;
|
color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-is-open .burger,
|
.is-menu-open .burger,
|
||||||
#Loading {
|
#Loading {
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@ -197,243 +197,8 @@
|
|||||||
<p>❤</p>
|
<p>❤</p>
|
||||||
</div>
|
</div>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
//@ts-check
|
import blog from "./modules/templateBlog.mjs";
|
||||||
|
blog();
|
||||||
/*****************************************************************
|
|
||||||
*
|
|
||||||
* "THE FRAMEWORK"
|
|
||||||
* a small set of utilities that help
|
|
||||||
*
|
|
||||||
****************************************************************/
|
|
||||||
/**
|
|
||||||
* markdown parser. Remove if you don't use markdown
|
|
||||||
*/
|
|
||||||
// @ts-ignore
|
|
||||||
import { micromark } from "https://esm.sh/micromark@3?bundle";
|
|
||||||
|
|
||||||
const is_debug_mode = /^localhost|127.0.0.1/.test(
|
|
||||||
window.location.hostname
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A small utility to query elements and get back an array
|
|
||||||
*/
|
|
||||||
const $ = (parent, selector) => [
|
|
||||||
...(!selector
|
|
||||||
? document.querySelectorAll(parent)
|
|
||||||
: parent.querySelectorAll(selector)),
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assumes a provided url is external if it begins by a known protocol
|
|
||||||
* @param {string} url
|
|
||||||
*/
|
|
||||||
const isExternal = (url) =>
|
|
||||||
url && /^(https?|mailto|tel|ftp|ipfs|dat):/.test(url);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes sure urls to local pages get passed through the routing system
|
|
||||||
* @param {HTMLElement} container the element containing links to find
|
|
||||||
*/
|
|
||||||
const rewriteLocalUrls = (container) => {
|
|
||||||
$(container, "a").forEach((a) => {
|
|
||||||
const href = a.getAttribute("href");
|
|
||||||
if (href && !isExternal(href)) {
|
|
||||||
a.setAttribute("href", "#/" + href.replace(/^\.?\//, ""));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return container;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the hash part of the url, but only if it starts with a `/`
|
|
||||||
* This allows regular hashes to continue to work
|
|
||||||
* It reads also query parameters
|
|
||||||
*/
|
|
||||||
const getCurrentHashUrl = () => {
|
|
||||||
const [path, searchStr] = (
|
|
||||||
window.location.hash[1] === "/" ? window.location.hash.slice(2) : ""
|
|
||||||
).split("?");
|
|
||||||
const params = new URLSearchParams(searchStr);
|
|
||||||
return { path, params };
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* useful to check for transitions while developing styles, if the loading screen disappears too fast
|
|
||||||
* uses micromark. You can plug a different parser if you prefer
|
|
||||||
*/
|
|
||||||
const wait = is_debug_mode
|
|
||||||
? (val) => new Promise((ok) => setTimeout(ok, 1000, val))
|
|
||||||
: (val) => val;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} htmlString
|
|
||||||
*/
|
|
||||||
const generateDOM = (htmlString) =>
|
|
||||||
/** @type {HTMLElement} */ (
|
|
||||||
new DOMParser().parseFromString(
|
|
||||||
`<div>${htmlString}</div>`,
|
|
||||||
"text/html"
|
|
||||||
).firstChild
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads and parses a markdown document. Makes use of micromark
|
|
||||||
* @param {string} path the path to load
|
|
||||||
*/
|
|
||||||
const loadMarkdown = (path) =>
|
|
||||||
fetch(is_debug_mode ? `./${path}?rand=${Math.random()}` : `./${path}`)
|
|
||||||
.then((response) => response.text())
|
|
||||||
.then(wait)
|
|
||||||
.then((raw) => {
|
|
||||||
const content = rewriteLocalUrls(generateDOM(micromark(raw)));
|
|
||||||
const firstTitleElement = content.querySelector("h1");
|
|
||||||
const title = firstTitleElement
|
|
||||||
? firstTitleElement.textContent
|
|
||||||
: path.replace(/\.\w{2, 4}$/, "");
|
|
||||||
return { title, raw, content };
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* parses a filelist string. That's a string that looks like
|
|
||||||
* ```
|
|
||||||
* name dd-mm-yyyy link name
|
|
||||||
* name dd-mm-yyyy
|
|
||||||
* name linkname
|
|
||||||
* name
|
|
||||||
* ```
|
|
||||||
* @param {string} lines
|
|
||||||
*/
|
|
||||||
const parseFileList = (lines) =>
|
|
||||||
lines
|
|
||||||
.trim()
|
|
||||||
.split(`\n`)
|
|
||||||
.map((line, index) => {
|
|
||||||
const today = new Date().toISOString().split("T")[0];
|
|
||||||
const result = line.match(
|
|
||||||
/(?<name>.+)\.(?<ext>\w{2,3})(?:\s+(?<date>[\d-]+)?(?<title>.+))?/
|
|
||||||
);
|
|
||||||
if (!result) {
|
|
||||||
console.error(`could not parse line ${index}: [${line}]`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const {
|
|
||||||
groups: { name, ext, date = today, title = name },
|
|
||||||
} = result;
|
|
||||||
const href = `/${name}.${ext}`;
|
|
||||||
const date_unix = new Date(date).getTime();
|
|
||||||
return { name, href, date, title, date_unix, index };
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
.sort(({ date_unix: a }, { date_unix: b }) => a - b);
|
|
||||||
|
|
||||||
/*****************************************************************
|
|
||||||
*
|
|
||||||
* Creating references to the important stuff
|
|
||||||
*
|
|
||||||
****************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The elements we will need
|
|
||||||
*/
|
|
||||||
const [Menu, Body, Loading, Source, Burger] = [
|
|
||||||
"Menu",
|
|
||||||
"Body",
|
|
||||||
"Loading",
|
|
||||||
"Source",
|
|
||||||
"Burger",
|
|
||||||
].map((id) => document.getElementById(id));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* cache the original title to append it to the page title
|
|
||||||
*/
|
|
||||||
const mainTitle = document.title;
|
|
||||||
|
|
||||||
const showLoadingOverlay = () =>
|
|
||||||
document.body.classList.add("is-loading");
|
|
||||||
const hideLoadingOverlay = () =>
|
|
||||||
document.body.classList.remove("is-loading");
|
|
||||||
|
|
||||||
/*****************************************************************
|
|
||||||
*
|
|
||||||
* Router
|
|
||||||
*
|
|
||||||
* Things related to main navigation and to loading pages
|
|
||||||
*
|
|
||||||
****************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listens to hashchange event, and attempts to read the url.
|
|
||||||
* If the url is set,
|
|
||||||
*/
|
|
||||||
const onHashChange = async (evt) => {
|
|
||||||
const { path, params } = getCurrentHashUrl();
|
|
||||||
if (!path) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
showLoadingOverlay();
|
|
||||||
const { title, raw, content } = await loadMarkdown(path);
|
|
||||||
document.title = `${title} | ${mainTitle}`;
|
|
||||||
Body.innerHTML = "";
|
|
||||||
Source.innerHTML = raw;
|
|
||||||
Body.appendChild(content);
|
|
||||||
hideLoadingOverlay();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the file list is obtained (presumably through loading)
|
|
||||||
* parses the file list, then fills the side navigation
|
|
||||||
* If there's no page loaded, it also loads the first page in the list
|
|
||||||
* (the list gets sorted by date, but the first line is the one that gets used)
|
|
||||||
* @param {string} lines
|
|
||||||
*/
|
|
||||||
const fillMenuFromFileList = (lines) => {
|
|
||||||
const links = parseFileList(lines);
|
|
||||||
Menu.innerHTML = links
|
|
||||||
.map(({ href, title }) => `<a data-link href="#${href}">${title}</a>`)
|
|
||||||
.join(`\n`);
|
|
||||||
if (!getCurrentHashUrl().path) {
|
|
||||||
const href = links.find(({ index }) => index === 0).href;
|
|
||||||
window.location.hash = `#${href}`;
|
|
||||||
} else {
|
|
||||||
onHashChange();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*****************************************************************
|
|
||||||
*
|
|
||||||
* Bootstrapping
|
|
||||||
*
|
|
||||||
* this is where things actually happen
|
|
||||||
*
|
|
||||||
****************************************************************/
|
|
||||||
|
|
||||||
const loadFileList = () => {
|
|
||||||
showLoadingOverlay();
|
|
||||||
|
|
||||||
fetch("./files.txt")
|
|
||||||
.then((response) => response.text())
|
|
||||||
.then(fillMenuFromFileList);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the article list, parses it, creates the menu items
|
|
||||||
*/
|
|
||||||
(function bootstrap() {
|
|
||||||
Burger.addEventListener("click", () =>
|
|
||||||
document.body.classList.toggle("menu-is-open")
|
|
||||||
);
|
|
||||||
|
|
||||||
window.addEventListener("hashchange", onHashChange);
|
|
||||||
|
|
||||||
loadFileList();
|
|
||||||
|
|
||||||
document.addEventListener(
|
|
||||||
"keyup",
|
|
||||||
({ key }) =>
|
|
||||||
key === "?" && document.body.classList.toggle("is-source-enabled")
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
12
modules/changeTitle.mjs
Normal file
12
modules/changeTitle.mjs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Changes the document's title
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} mainTitle
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const changeTitle = (title, mainTitle = changeTitle.title) =>
|
||||||
|
document && (document.title = `${title} | ${mainTitle}`);
|
||||||
|
|
||||||
|
changeTitle.title = (document && document.title) || "";
|
||||||
|
|
||||||
|
export default changeTitle;
|
13
modules/documentMode.mjs
Normal file
13
modules/documentMode.mjs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
//@ts-check
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a helper to add or remove global classes that begin with `is-`
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
export const documentMode = (name) => ({
|
||||||
|
on: () => document.body.classList.add(`is-${name}`),
|
||||||
|
off: () => document.body.classList.remove(`is-${name}`),
|
||||||
|
toggle: () => document.body.classList.toggle(`is-${name}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default documentMode;
|
24
modules/fetchMarkdown.mjs
Normal file
24
modules/fetchMarkdown.mjs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
//@ts-check
|
||||||
|
|
||||||
|
import { fetchText } from "./fetchText.mjs";
|
||||||
|
import { waitIfLocalHost } from "./waitIfLocalHost.mjs";
|
||||||
|
import { rewriteLocalUrls } from "./rewriteLocalUrls.mjs";
|
||||||
|
import { generateDomFromString } from "./generateDomFromString.mjs";
|
||||||
|
// @ts-ignore
|
||||||
|
import { micromark } from "https://esm.sh/micromark@3?bundle";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and parses a markdown document. Makes use of micromark
|
||||||
|
* @param {string} path the path to load
|
||||||
|
*/
|
||||||
|
export const fetchMarkdown = (path) =>
|
||||||
|
fetchText(path)
|
||||||
|
.then(waitIfLocalHost())
|
||||||
|
.then((raw) => {
|
||||||
|
const content = rewriteLocalUrls(generateDomFromString(micromark(raw)));
|
||||||
|
const firstTitleElement = content.querySelector("h1");
|
||||||
|
const title = firstTitleElement
|
||||||
|
? firstTitleElement.textContent
|
||||||
|
: path.replace(/\.\w{2, 4}$/, "");
|
||||||
|
return { title, raw, content };
|
||||||
|
});
|
16
modules/fetchText.mjs
Normal file
16
modules/fetchText.mjs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
//@ts-check
|
||||||
|
|
||||||
|
import isLocalHost from "./isLocalHost.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a text file. The path provided will be appened with a random number
|
||||||
|
* when running on localhost to avoid caching problems
|
||||||
|
* @param {string} path
|
||||||
|
* @returns {Promise<string>} the loaded file
|
||||||
|
*/
|
||||||
|
export const fetchText = (path) =>
|
||||||
|
fetch(isLocalHost ? `./${path}?rand=${Math.random()}` : `./${path}`).then(
|
||||||
|
(response) => response.text()
|
||||||
|
);
|
||||||
|
|
||||||
|
export default fetchText;
|
13
modules/generateDomFromString.mjs
Normal file
13
modules/generateDomFromString.mjs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
//@ts-check
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates valid dom elements from a string
|
||||||
|
* @param {string} htmlString
|
||||||
|
*/
|
||||||
|
export const generateDomFromString = (htmlString) =>
|
||||||
|
/** @type {HTMLElement} */ (
|
||||||
|
new DOMParser().parseFromString(`<div>${htmlString}</div>`, "text/html")
|
||||||
|
.firstChild
|
||||||
|
);
|
||||||
|
|
||||||
|
export default generateDomFromString;
|
16
modules/getCurrentHashUrl.mjs
Normal file
16
modules/getCurrentHashUrl.mjs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
//@ts-check
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the hash part of the url, but only if it starts with a `/`
|
||||||
|
* This allows regular hashes to continue to work
|
||||||
|
* It reads also query parameters
|
||||||
|
*/
|
||||||
|
export const getCurrentHashUrl = () => {
|
||||||
|
const [path, searchStr] = (
|
||||||
|
window.location.hash[1] === "/" ? window.location.hash.slice(2) : ""
|
||||||
|
).split("?");
|
||||||
|
const params = new URLSearchParams(searchStr);
|
||||||
|
return { path, params };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getCurrentHashUrl;
|
19
modules/getElementById.mjs
Normal file
19
modules/getElementById.mjs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
//@ts-check
|
||||||
|
import { isLocalHost } from "./isLocalHost.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an element by id if the element exists, otherwise throws, but only if running in localhost environments.
|
||||||
|
* Use this in the initial setup to verify all elements exist
|
||||||
|
* @param {string} id
|
||||||
|
* @return {HTMLElement}
|
||||||
|
*/
|
||||||
|
export const getElementById = (id) => {
|
||||||
|
const element = document && document.getElementById(id);
|
||||||
|
if (isLocalHost && !element) {
|
||||||
|
throw new Error(`Element "#${id}" was not found`);
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getElementById;
|
15
modules/index.mjs
Normal file
15
modules/index.mjs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { fetchText } from "./fetchText.mjs";
|
||||||
|
import { generateDomFromString } from "./generateDomFromString.mjs";
|
||||||
|
import { isExternalUrl } from "./isExternalUrl.mjs";
|
||||||
|
import { isLocalHost } from "./isLocalHost.mjs";
|
||||||
|
import { parseFileList } from "./parseFileList.mjs";
|
||||||
|
import {
|
||||||
|
querySelectorDoc,
|
||||||
|
querySelectorParent,
|
||||||
|
querySelectorAll,
|
||||||
|
} from "./querySelectorAll.mjs";
|
||||||
|
import { rewriteLocalUrls } from "./rewriteLocalUrls.mjs";
|
||||||
|
import { wait } from "./wait.mjs";
|
||||||
|
import { waitIfLocalHost } from "./waitIfLocalHost.mjs";
|
||||||
|
|
||||||
|
export { isExternalUrl, isLocalHost, rewriteLocalUrls, wait, waitIfLocalHost };
|
10
modules/isExternalUrl.mjs
Normal file
10
modules/isExternalUrl.mjs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
//@ts-check
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assumes a provided url is external if it begins by a known protocol
|
||||||
|
* @param {string} url
|
||||||
|
*/
|
||||||
|
export const isExternalUrl = (url) =>
|
||||||
|
url && /^(https?|mailto|tel|ftp|ipfs|dat):/.test(url);
|
||||||
|
|
||||||
|
export default isExternalUrl;
|
8
modules/isLocalHost.mjs
Normal file
8
modules/isLocalHost.mjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
//@ts-check
|
||||||
|
|
||||||
|
/** @type {boolean} returns true if the window global object exists and the domain is localhost of 127.0.0.1 */
|
||||||
|
export const isLocalHost =
|
||||||
|
typeof window !== "undefined" &&
|
||||||
|
/^localhost|127.0.0.1/.test(window.location.hostname);
|
||||||
|
|
||||||
|
export default isLocalHost;
|
10
modules/isNotNull.mjs
Normal file
10
modules/isNotNull.mjs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
//@ts-check
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {T} value
|
||||||
|
* @returns {value is NonNullable<T>}
|
||||||
|
*/
|
||||||
|
export const isNotNull = (value) => value !== null;
|
||||||
|
|
||||||
|
export default isNotNull;
|
11
modules/onDocumentKey.mjs
Normal file
11
modules/onDocumentKey.mjs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} keyToListen
|
||||||
|
* @param {()=>void} callback
|
||||||
|
*/
|
||||||
|
export const onDocumentKeyUp = (keyToListen, callback) => {
|
||||||
|
document.addEventListener(
|
||||||
|
"keyup",
|
||||||
|
({ key }) => key === keyToListen && callback()
|
||||||
|
);
|
||||||
|
};
|
40
modules/parseFileList.mjs
Normal file
40
modules/parseFileList.mjs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
//@ts-check
|
||||||
|
|
||||||
|
import { isNotNull } from "./isNotNull.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* parses a filelist string. That's a string that looks like
|
||||||
|
* ```md
|
||||||
|
* name dd-mm-yyyy link name
|
||||||
|
* name dd-mm-yyyy
|
||||||
|
* name linkname
|
||||||
|
* name
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param {string} lines
|
||||||
|
*/
|
||||||
|
export const parseFileList = (lines) =>
|
||||||
|
// @ts-ignore
|
||||||
|
lines
|
||||||
|
.trim()
|
||||||
|
.split(`\n`)
|
||||||
|
.map((line, index) => {
|
||||||
|
const today = new Date().toISOString().split("T")[0];
|
||||||
|
const result = line.match(
|
||||||
|
/(?<name>.+)\.(?<ext>\w{2,3})(?:\s+(?<date>[\d-]+)?(?<title>.+))?/
|
||||||
|
);
|
||||||
|
if (!result) {
|
||||||
|
console.error(`could not parse line number ${index}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
// @ts-ignore
|
||||||
|
groups: { name, ext, date = today, title = name },
|
||||||
|
} = result;
|
||||||
|
const href = `/${name}.${ext}`;
|
||||||
|
const date_unix = new Date(date).getTime();
|
||||||
|
return { name, href, date, title, date_unix, index };
|
||||||
|
})
|
||||||
|
.filter(isNotNull);
|
||||||
|
|
||||||
|
export default parseFileList;
|
40
modules/querySelectorAll.mjs
Normal file
40
modules/querySelectorAll.mjs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
//@ts-check
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A small utility to query elements and get back an array
|
||||||
|
* @template {keyof HTMLElementTagNameMap} K
|
||||||
|
* @type {{
|
||||||
|
* (selector: K): HTMLElementTagNameMap[K][]
|
||||||
|
* (parent: ParentNode, selector: K): HTMLElementTagNameMap[K][]
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
// @ts-ignore
|
||||||
|
export const querySelectorAll = (parent, selector) =>
|
||||||
|
// @ts-ignore
|
||||||
|
typeof selector === "undefined"
|
||||||
|
? // @ts-ignore
|
||||||
|
querySelectorDoc(/** @type {keyof HTMLElementTagNameMap} */ (parent))
|
||||||
|
: querySelectorParent(parent, selector);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A small utility to query elements in the document and get back an array
|
||||||
|
* @template {keyof HTMLElementTagNameMap} K
|
||||||
|
* @param {K} selector
|
||||||
|
* @returns {HTMLElementTagNameMap[K][]}
|
||||||
|
*/
|
||||||
|
export const querySelectorDoc = (selector) => [
|
||||||
|
...document.querySelectorAll(selector),
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A small utility to query elements in a parent and get back an array
|
||||||
|
* @template {keyof HTMLElementTagNameMap} K
|
||||||
|
* @param {ParentNode} parent
|
||||||
|
* @param {K} selector
|
||||||
|
* @returns {HTMLElementTagNameMap[K][]}
|
||||||
|
*/
|
||||||
|
export const querySelectorParent = (parent, selector) => [
|
||||||
|
...parent.querySelectorAll(selector),
|
||||||
|
];
|
||||||
|
|
||||||
|
export default querySelectorAll;
|
20
modules/rewriteLocalUrls.mjs
Normal file
20
modules/rewriteLocalUrls.mjs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
//@ts-check
|
||||||
|
|
||||||
|
import isExternalUrl from "./isExternalUrl.mjs";
|
||||||
|
import { querySelectorParent } from "./querySelectorAll.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes sure urls to local pages get passed through the routing system
|
||||||
|
* @param {HTMLElement} container the element containing links to find
|
||||||
|
*/
|
||||||
|
export const rewriteLocalUrls = (container) => {
|
||||||
|
querySelectorParent(container, "a").forEach((a) => {
|
||||||
|
const href = a.getAttribute("href");
|
||||||
|
if (href && !isExternalUrl(href)) {
|
||||||
|
a.setAttribute("href", "#/" + href.replace(/^\.?\//, ""));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return container;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default rewriteLocalUrls;
|
107
modules/templateBlog.mjs
Normal file
107
modules/templateBlog.mjs
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
//@ts-check
|
||||||
|
import { changeTitle } from "./changeTitle.mjs";
|
||||||
|
import { getCurrentHashUrl } from "./getCurrentHashUrl.mjs";
|
||||||
|
import { fetchMarkdown } from "./fetchMarkdown.mjs";
|
||||||
|
import { fetchText } from "./fetchText.mjs";
|
||||||
|
import { parseFileList } from "./parseFileList.mjs";
|
||||||
|
import { documentMode } from "./documentMode.mjs";
|
||||||
|
import { getElementById } from "./getElementById.mjs";
|
||||||
|
import { onDocumentKeyUp } from "./onDocumentKey.mjs";
|
||||||
|
|
||||||
|
/*****************************************************************
|
||||||
|
*
|
||||||
|
* Creating references to the important stuff
|
||||||
|
*
|
||||||
|
****************************************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The elements we will need
|
||||||
|
*/
|
||||||
|
const [Menu, Body, Source, Burger] = ["Menu", "Body", "Source", "Burger"].map(
|
||||||
|
getElementById
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadingMode = documentMode("loading");
|
||||||
|
const sourceEnableMode = documentMode("source-enabled");
|
||||||
|
const menuOpenMode = documentMode("menu-open");
|
||||||
|
|
||||||
|
/*****************************************************************
|
||||||
|
*
|
||||||
|
* Router
|
||||||
|
*
|
||||||
|
* Things related to main navigation and to loading pages
|
||||||
|
*
|
||||||
|
****************************************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens to hashchange event, and attempts to read the url.
|
||||||
|
* @param {HashChangeEvent} [_evt]
|
||||||
|
*/
|
||||||
|
export const onHashChange = async (_evt) => {
|
||||||
|
const { path, params } = getCurrentHashUrl();
|
||||||
|
if (!path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
loadingMode.on();
|
||||||
|
const { title, raw, content } = await fetchMarkdown(path);
|
||||||
|
changeTitle(title);
|
||||||
|
Body.innerHTML = "";
|
||||||
|
Source.innerHTML = raw;
|
||||||
|
Body.appendChild(content);
|
||||||
|
loadingMode.off();
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadFileList = () => {
|
||||||
|
loadingMode.on();
|
||||||
|
|
||||||
|
fetchText("./files.txt").then(fillMenuFromFileList);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the file list is obtained (presumably through loading)
|
||||||
|
* parses the file list, then fills the side navigation
|
||||||
|
* If there's no page loaded, it also loads the first page in the list
|
||||||
|
* (the list gets sorted by date, but the first line is the one that gets used)
|
||||||
|
* @param {string} lines
|
||||||
|
*/
|
||||||
|
export const fillMenuFromFileList = (lines) => {
|
||||||
|
const links = parseFileList(lines).sort(
|
||||||
|
({ date_unix: a }, { date_unix: b }) => a - b
|
||||||
|
);
|
||||||
|
if (links.length < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Menu.innerHTML = links
|
||||||
|
.map(({ href, title }) => `<a data-link href="#${href}">${title}</a>`)
|
||||||
|
.join(`\n`);
|
||||||
|
if (!getCurrentHashUrl().path) {
|
||||||
|
// @ts-ignore
|
||||||
|
const href = links.find(({ index }) => index === 0).href;
|
||||||
|
window.location.hash = `#${href}`;
|
||||||
|
} else {
|
||||||
|
onHashChange();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*****************************************************************
|
||||||
|
*
|
||||||
|
* Bootstrapping
|
||||||
|
*
|
||||||
|
* this is where things actually happen
|
||||||
|
*
|
||||||
|
****************************************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the article list, parses it, creates the menu items
|
||||||
|
*/
|
||||||
|
export const bootstrap = () => {
|
||||||
|
Burger.addEventListener("click", menuOpenMode.toggle);
|
||||||
|
|
||||||
|
window.addEventListener("hashchange", onHashChange);
|
||||||
|
|
||||||
|
loadFileList();
|
||||||
|
|
||||||
|
onDocumentKeyUp("?", sourceEnableMode.toggle);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default bootstrap;
|
18
modules/wait.mjs
Normal file
18
modules/wait.mjs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
//@ts-check
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits the specified amount of time before returning the value
|
||||||
|
* @param {number} durationMs Duration, in milliseconds. Defaults to 1 second
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const wait =
|
||||||
|
(durationMs = 1000) =>
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {T} [value]
|
||||||
|
* @returns {Promise<T>}
|
||||||
|
*/
|
||||||
|
(value) =>
|
||||||
|
new Promise((ok) => setTimeout(ok, durationMs, value));
|
||||||
|
|
||||||
|
export default wait;
|
31
modules/waitIfLocalHost.mjs
Normal file
31
modules/waitIfLocalHost.mjs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
//@ts-check
|
||||||
|
|
||||||
|
import wait from "./wait.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {T} [value]
|
||||||
|
* @returns {Promise<T>}
|
||||||
|
*/
|
||||||
|
// @ts-ignore
|
||||||
|
const identity = (value) => Promise.resolve(value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits the specified amount of time before returning the value
|
||||||
|
* @param {number} _durationMs Duration, in milliseconds. Defaults to 1 second
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const fakeWait = (_durationMs = 1000) => identity;
|
||||||
|
|
||||||
|
import isLocalHost from "./isLocalHost.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useful to check for transitions while developing styles, if the loading screen
|
||||||
|
* disappears too fast for example.
|
||||||
|
* @template T
|
||||||
|
* @param {number} durationMs Duration, in milliseconds. Defaults to 1 second
|
||||||
|
* @returns {(value?: T | undefined) => Promise<T>}
|
||||||
|
*/
|
||||||
|
export const waitIfLocalHost = isLocalHost ? wait : fakeWait;
|
||||||
|
|
||||||
|
export default waitIfLocalHost;
|
Loading…
Reference in New Issue
Block a user