testing moving the functionality into small modules #5

Open
xananax wants to merge 10 commits from split-into-modules into main
12 changed files with 233 additions and 38 deletions
Showing only changes of commit b01f6444d0 - Show all commits

View File

@ -0,0 +1,51 @@
//@ts-check
/**
*
* Creates exclusive states for an HTML element. These states are added as classes
* and can be used to drive CSS changes.
* @param {string[]} allModes A string list of all possible modes. It's advised
* to prepend (`state-` or `mode-` to each for clarity)
* @param {Element} [element] the element to add the classes to. Defaults to the
* document's body
*/
export const createElementStatusModes = (allModes, element = document.body) => {
/**
* @param {any} mode
* @returns {mode is number}
*/
const isValidIndex = (mode) =>
typeof mode === "number" && mode >= 0 && mode < allModes.length;
/**
* Sets a status mode (class name) on the element.
* Pass a falsy value to clear all modes
* @param {number|null|undefined|false} mode
*/
const set = (mode = false) => {
mode = isValidIndex(mode) ? mode : -1;
const modeClass = allModes[mode];
if (modeClass && element.classList.contains(modeClass)) {
return;
}
element.classList.remove(...allModes);
element.classList.add(modeClass);
};
/**
* Verifies which of the given classes is set.
* @returns {string|undefined} the class if there is one
*/
const get = () =>
allModes.find((className) => element.classList.contains(className));
/**
* @param {number} state
*/
const is = (state) =>
isValidIndex(state) && document.body.classList.contains(allModes[state]);
return { set, get, is };
};
export default createElementStatusModes;

View File

@ -1,14 +0,0 @@
//@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}`),
has: () => document.body.classList.contains(`is-${name}`),
});
export default documentMode;

View File

@ -1,35 +1,16 @@
//@ts-check //@ts-check
import {createElementStatusModes} from "./createElementStatusModes.mjs"
/** /**
* Creates a document state object that can toggle between exclusive states. * Creates a document state object that can toggle between exclusive states.
* All passed states' css classnames will be prepended with `mode-`. * All passed states' css classnames will be prepended with `mode-`.
* @see {createElementStatusModes}
* @param {string[]} states * @param {string[]} states
*/ */
export const documentState = (states) => { export const documentState = (states) => {
const all = states.map((state) => `mode-${state}`); const all = states.map((state) => `mode-${state}`);
/** return createElementStatusModes(all, document.body)
* @param {any} state
* @returns {state is number}
*/
const isValidIndex = (state) =>
typeof state === "number" && state >= 0 && state < all.length;
/**
* @param {number} state
*/
const is = (state) =>
isValidIndex(state) && document.body.classList.contains(all[state]);
/**
* @param {number|undefined|null|false} state
*/
const set = (state = false) => {
document.body.classList.remove(...all);
isValidIndex(state) && document.body.classList.add(all[state]);
};
return { set, is };
}; };
export default documentState; export default documentState;

View File

@ -0,0 +1,15 @@
//@ts-check
/**
* Creates a helper to add or remove global classes that begin with `is-`
* @param {string} name name of the state
* @param {Element} [element] defaults to the document body
*/
export const elementMode = (name, element = document.body) => ({
on: () => element.classList.add(`is-${name}`),
off: () => element.classList.remove(`is-${name}`),
toggle: () => element.classList.toggle(`is-${name}`),
has: () => element.classList.contains(`is-${name}`),
});
export default elementMode;

View File

@ -0,0 +1,11 @@
//@ts-check
/**
* Escapes a string so it can be used in a regular expression
* @param {string} text
*/
export function escapeRegExp(text) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
export default escapeRegExp

View File

@ -0,0 +1,22 @@
//@ts-check
/**
* @typedef {{width: number;height: number}} Size
*/
/**
* Calculates an ideal ratio
* @param {Size} initial the initial size
* @param {Size} current the current size
* @returns
*/
export const getAspectRatio = (initial, current) => {
const ratioW = current.width / initial.width;
const ratioH = current.height / initial.height;
const ratio = Math.min(ratioW, ratioH);
const width = initial.width * ratio;
const height = initial.height * ratio;
return { width, height, ratio };
};
export default getAspectRatio

View File

@ -0,0 +1,19 @@
//@ts-check
import {isElement} from "./isElement.mjs";
/**
* Little utility so people can pass css selectors or elements in initialization
* options.
* A minimal replacement to a more full fledged selector engine like jQuery
* @param {string|HTMLElement} elementOrString
* @returns {HTMLElement | null}
*/
export const getElement = (elementOrString) => {
const element =
elementOrString && typeof elementOrString === "string"
? /** @type {HTMLElement}*/ (document.querySelector(elementOrString))
: elementOrString;
return isElement(element) ? element : null;
};
export default getElement

View File

@ -0,0 +1,16 @@
//@ts-check
/**
* Retrieves the current locale. Only works if `navigator` is available.
* Otherwise, returns the `defaultLang` passed property
* @param {string} [defaultLang] defaults to an empty string
* @returns The browser locale, formatted as `xx_XX`
*/
export const getLocale = (defaultLang = "") =>
(typeof navigator !== "undefined" &&
(navigator.languages ? navigator.languages[0] : navigator.language)
.split(".")[0]
.replace("-", "_")) ||
defaultLang;
export default getLocale;

View File

@ -0,0 +1,14 @@
//@ts-check
/**
* short random string for ids - not guaranteed to be unique
* @see https://www.codemzy.com/blog/random-unique-id-javascript
* @param {number} length the length of the id
*/
export const getRandomId = function (length = 6) {
return Math.random()
.toString(36)
.substring(2, length + 2);
};
export default getRandomId

View File

@ -2,27 +2,34 @@
import { changeTitle } from "./changeTitle.mjs"; import { changeTitle } from "./changeTitle.mjs";
//import { createCustomElement } from "./createCustomElement.mjs"; //import { createCustomElement } from "./createCustomElement.mjs";
import { createElementStatusModes } from "./createElementStatusModes.mjs";
import { createTrackedResponse } from "./createTrackedResponse.mjs"; import { createTrackedResponse } from "./createTrackedResponse.mjs";
import { decodeContentLength } from "./decodeContentLength.mjs"; import { decodeContentLength } from "./decodeContentLength.mjs";
import { deferredPromise } from "./deferredPromise.mjs"; import { deferredPromise } from "./deferredPromise.mjs";
import { documentMode } from "./documentMode.mjs";
import { documentState } from "./documentState.mjs"; import { documentState } from "./documentState.mjs";
import { elementMode } from "./elementMode.mjs";
import { escapeRegExp } from "./escapeRegExp.mjs";
import { fetchContentLength } from "./fetchContentLength.mjs"; import { fetchContentLength } from "./fetchContentLength.mjs";
import { fetchHeaders } from "./fetchHeaders.mjs"; import { fetchHeaders } from "./fetchHeaders.mjs";
import { fetchMarkdown } from "./fetchMarkdown.mjs"; import { fetchMarkdown } from "./fetchMarkdown.mjs";
import { fetchText } from "./fetchText.mjs"; import { fetchText } from "./fetchText.mjs";
import { generateDomFromString } from "./generateDomFromString.mjs"; import { generateDomFromString } from "./generateDomFromString.mjs";
import { getAspectRatio } from "./getAspectRatio.mjs";
import { import {
getCurrentHashUrl, getCurrentHashUrl,
hasCurrentHashUrl, hasCurrentHashUrl,
hasNoHashUrl, hasNoHashUrl,
} from "./getCurrentHashUrl.mjs"; } from "./getCurrentHashUrl.mjs";
import { getElement } from "./getElement.mjs";
import { getElementByCSSSelector } from "./getElementByCSSSelector.mjs"; import { getElementByCSSSelector } from "./getElementByCSSSelector.mjs";
import { getElementById } from "./getElementById.mjs"; import { getElementById } from "./getElementById.mjs";
import { getFirstTitleContent } from "./getFirstTitleContent.mjs"; import { getFirstTitleContent } from "./getFirstTitleContent.mjs";
import { getLocale } from "./getLocale.mjs";
import { getPageUniqueId } from "./getPageUniqueId.mjs"; import { getPageUniqueId } from "./getPageUniqueId.mjs";
import { getRandomId } from "./getRandomId.mjs";
import { getReasonableUuid } from "./getReasonableUuid.mjs"; import { getReasonableUuid } from "./getReasonableUuid.mjs";
import { identity, awaitedIdentity } from "./identity.mjs"; import { identity, awaitedIdentity } from "./identity.mjs";
import { isElement } from "./isElement.mjs";
import { html } from "./html.mjs"; import { html } from "./html.mjs";
import { isExternalUrl } from "./isExternalUrl.mjs"; import { isExternalUrl } from "./isExternalUrl.mjs";
import { isLocalHost } from "./isLocalHost.mjs"; import { isLocalHost } from "./isLocalHost.mjs";
@ -59,6 +66,7 @@ import {
} from "./querySelectorAll.mjs"; } from "./querySelectorAll.mjs";
import { retryPromise } from "./retryPromise.mjs"; import { retryPromise } from "./retryPromise.mjs";
import { rewriteLocalUrls } from "./rewriteLocalUrls.mjs"; import { rewriteLocalUrls } from "./rewriteLocalUrls.mjs";
import { throttle } from "./throttle.mjs";
import { today } from "./today.mjs"; import { today } from "./today.mjs";
import { UnreachableCaseError } from "./UnreachableCaseError.mjs"; import { UnreachableCaseError } from "./UnreachableCaseError.mjs";
import { wait } from "./wait.mjs"; import { wait } from "./wait.mjs";
@ -66,27 +74,34 @@ import { waitIfLocalHost } from "./waitIfLocalHost.mjs";
export { export {
changeTitle, changeTitle,
createElementStatusModes,
createTrackedResponse, createTrackedResponse,
decodeContentLength, decodeContentLength,
deferredPromise, deferredPromise,
documentMode, elementMode as documentMode,
documentState, documentState,
escapeRegExp,
fetchContentLength, fetchContentLength,
fetchHeaders, fetchHeaders,
fetchMarkdown, fetchMarkdown,
fetchText, fetchText,
generateDomFromString, generateDomFromString,
getAspectRatio,
getCurrentHashUrl, getCurrentHashUrl,
hasCurrentHashUrl, hasCurrentHashUrl,
hasNoHashUrl, hasNoHashUrl,
getElement,
getElementByCSSSelector, getElementByCSSSelector,
getElementById, getElementById,
getFirstTitleContent, getFirstTitleContent,
getLocale,
getPageUniqueId, getPageUniqueId,
getRandomId,
getReasonableUuid, getReasonableUuid,
html, html,
identity, identity,
awaitedIdentity, awaitedIdentity,
isElement,
isExternalUrl, isExternalUrl,
isLocalHost, isLocalHost,
isNotNull, isNotNull,
@ -118,6 +133,7 @@ export {
querySelectorAll, querySelectorAll,
retryPromise, retryPromise,
rewriteLocalUrls, rewriteLocalUrls,
throttle,
today, today,
UnreachableCaseError, UnreachableCaseError,
wait, wait,

View File

@ -0,0 +1,12 @@
//@ts-check
/**
* Verifies an element is actually an element.
* @param {any} element
* @returns {element is HTMLElement}
*/
export const isElement = (element) => {
return element instanceof Element || element instanceof Document;
};
export default isElement

View File

@ -0,0 +1,52 @@
//@ts-check
/**
*
* Creates a throttled function that only invokes the provided function at most
* once per within a given number of milliseconds.
*
* @template {(...args: any) => any } F
* @template {ReturnType<F>} R
* @template {Parameters<F>} P
* @param {F} func
* @param {number} [firingRateMs] Firing rate (50ms by default)
*/
export const throttle = (func, firingRateMs = 50) => {
/** @type {R} */
let lastResult;
/** @type {number} */
let last = 0;
/** @type {null|P} */
let funcArguments;
/** @type {number} */
let timeoutID = 0;
const call = () => {
timeoutID = 0;
last = +new Date();
lastResult = func.apply(null, funcArguments);
funcArguments = null;
};
/***
* @param {P} args
*/
const throttled = (...args) => {
funcArguments = args;
const delta = new Date().getTime() - last;
if (!timeoutID)
if (delta >= firingRateMs) {
call();
} else {
timeoutID = setTimeout(call, firingRateMs - delta);
}
return lastResult;
};
return throttled;
};
export default throttle