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
import {createElementStatusModes} from "./createElementStatusModes.mjs"
/**
* Creates a document state object that can toggle between exclusive states.
* All passed states' css classnames will be prepended with `mode-`.
* @see {createElementStatusModes}
* @param {string[]} states
*/
export const documentState = (states) => {
const all = states.map((state) => `mode-${state}`);
/**
* @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 };
return createElementStatusModes(all, document.body)
};
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 { createCustomElement } from "./createCustomElement.mjs";
import { createElementStatusModes } from "./createElementStatusModes.mjs";
import { createTrackedResponse } from "./createTrackedResponse.mjs";
import { decodeContentLength } from "./decodeContentLength.mjs";
import { deferredPromise } from "./deferredPromise.mjs";
import { documentMode } from "./documentMode.mjs";
import { documentState } from "./documentState.mjs";
import { elementMode } from "./elementMode.mjs";
import { escapeRegExp } from "./escapeRegExp.mjs";
import { fetchContentLength } from "./fetchContentLength.mjs";
import { fetchHeaders } from "./fetchHeaders.mjs";
import { fetchMarkdown } from "./fetchMarkdown.mjs";
import { fetchText } from "./fetchText.mjs";
import { generateDomFromString } from "./generateDomFromString.mjs";
import { getAspectRatio } from "./getAspectRatio.mjs";
import {
getCurrentHashUrl,
hasCurrentHashUrl,
hasNoHashUrl,
} from "./getCurrentHashUrl.mjs";
import { getElement } from "./getElement.mjs";
import { getElementByCSSSelector } from "./getElementByCSSSelector.mjs";
import { getElementById } from "./getElementById.mjs";
import { getFirstTitleContent } from "./getFirstTitleContent.mjs";
import { getLocale } from "./getLocale.mjs";
import { getPageUniqueId } from "./getPageUniqueId.mjs";
import { getRandomId } from "./getRandomId.mjs";
import { getReasonableUuid } from "./getReasonableUuid.mjs";
import { identity, awaitedIdentity } from "./identity.mjs";
import { isElement } from "./isElement.mjs";
import { html } from "./html.mjs";
import { isExternalUrl } from "./isExternalUrl.mjs";
import { isLocalHost } from "./isLocalHost.mjs";
@ -59,6 +66,7 @@ import {
} from "./querySelectorAll.mjs";
import { retryPromise } from "./retryPromise.mjs";
import { rewriteLocalUrls } from "./rewriteLocalUrls.mjs";
import { throttle } from "./throttle.mjs";
import { today } from "./today.mjs";
import { UnreachableCaseError } from "./UnreachableCaseError.mjs";
import { wait } from "./wait.mjs";
@ -66,27 +74,34 @@ import { waitIfLocalHost } from "./waitIfLocalHost.mjs";
export {
changeTitle,
createElementStatusModes,
createTrackedResponse,
decodeContentLength,
deferredPromise,
documentMode,
elementMode as documentMode,
documentState,
escapeRegExp,
fetchContentLength,
fetchHeaders,
fetchMarkdown,
fetchText,
generateDomFromString,
getAspectRatio,
getCurrentHashUrl,
hasCurrentHashUrl,
hasNoHashUrl,
getElement,
getElementByCSSSelector,
getElementById,
getFirstTitleContent,
getLocale,
getPageUniqueId,
getRandomId,
getReasonableUuid,
html,
identity,
awaitedIdentity,
isElement,
isExternalUrl,
isLocalHost,
isNotNull,
@ -118,6 +133,7 @@ export {
querySelectorAll,
retryPromise,
rewriteLocalUrls,
throttle,
today,
UnreachableCaseError,
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