From b01f6444d0a8206b5aba5c13ee57d577200dc09a Mon Sep 17 00:00:00 2001 From: Xananax Date: Wed, 7 Jun 2023 22:07:47 +0200 Subject: [PATCH] smol updates even mor --- modules/utils/createElementStatusModes.mjs | 51 +++++++++++++++++++++ modules/utils/documentMode.mjs | 14 ------ modules/utils/documentState.mjs | 25 ++--------- modules/utils/elementMode.mjs | 15 +++++++ modules/utils/escapeRegExp.mjs | 11 +++++ modules/utils/getAspectRatio.mjs | 22 +++++++++ modules/utils/getElement.mjs | 19 ++++++++ modules/utils/getLocale.mjs | 16 +++++++ modules/utils/getRandomId.mjs | 14 ++++++ modules/utils/index.mjs | 20 ++++++++- modules/utils/isElement.mjs | 12 +++++ modules/utils/throttle.mjs | 52 ++++++++++++++++++++++ 12 files changed, 233 insertions(+), 38 deletions(-) create mode 100644 modules/utils/createElementStatusModes.mjs delete mode 100644 modules/utils/documentMode.mjs create mode 100644 modules/utils/elementMode.mjs create mode 100644 modules/utils/escapeRegExp.mjs create mode 100644 modules/utils/getAspectRatio.mjs create mode 100644 modules/utils/getElement.mjs create mode 100644 modules/utils/getLocale.mjs create mode 100644 modules/utils/getRandomId.mjs create mode 100644 modules/utils/isElement.mjs create mode 100644 modules/utils/throttle.mjs diff --git a/modules/utils/createElementStatusModes.mjs b/modules/utils/createElementStatusModes.mjs new file mode 100644 index 0000000..29f93e4 --- /dev/null +++ b/modules/utils/createElementStatusModes.mjs @@ -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; diff --git a/modules/utils/documentMode.mjs b/modules/utils/documentMode.mjs deleted file mode 100644 index 0cd9342..0000000 --- a/modules/utils/documentMode.mjs +++ /dev/null @@ -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; diff --git a/modules/utils/documentState.mjs b/modules/utils/documentState.mjs index 12e0ed1..61d5438 100644 --- a/modules/utils/documentState.mjs +++ b/modules/utils/documentState.mjs @@ -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; diff --git a/modules/utils/elementMode.mjs b/modules/utils/elementMode.mjs new file mode 100644 index 0000000..c5700da --- /dev/null +++ b/modules/utils/elementMode.mjs @@ -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; diff --git a/modules/utils/escapeRegExp.mjs b/modules/utils/escapeRegExp.mjs new file mode 100644 index 0000000..a2079bd --- /dev/null +++ b/modules/utils/escapeRegExp.mjs @@ -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 \ No newline at end of file diff --git a/modules/utils/getAspectRatio.mjs b/modules/utils/getAspectRatio.mjs new file mode 100644 index 0000000..affe32b --- /dev/null +++ b/modules/utils/getAspectRatio.mjs @@ -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 \ No newline at end of file diff --git a/modules/utils/getElement.mjs b/modules/utils/getElement.mjs new file mode 100644 index 0000000..f21867b --- /dev/null +++ b/modules/utils/getElement.mjs @@ -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 \ No newline at end of file diff --git a/modules/utils/getLocale.mjs b/modules/utils/getLocale.mjs new file mode 100644 index 0000000..c6d226f --- /dev/null +++ b/modules/utils/getLocale.mjs @@ -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; diff --git a/modules/utils/getRandomId.mjs b/modules/utils/getRandomId.mjs new file mode 100644 index 0000000..a51ea22 --- /dev/null +++ b/modules/utils/getRandomId.mjs @@ -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 \ No newline at end of file diff --git a/modules/utils/index.mjs b/modules/utils/index.mjs index 73169a2..fc0048a 100644 --- a/modules/utils/index.mjs +++ b/modules/utils/index.mjs @@ -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, diff --git a/modules/utils/isElement.mjs b/modules/utils/isElement.mjs new file mode 100644 index 0000000..0838fd8 --- /dev/null +++ b/modules/utils/isElement.mjs @@ -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 \ No newline at end of file diff --git a/modules/utils/throttle.mjs b/modules/utils/throttle.mjs new file mode 100644 index 0000000..dffd692 --- /dev/null +++ b/modules/utils/throttle.mjs @@ -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} R + * @template {Parameters} 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 \ No newline at end of file