testing moving the functionality into small modules #5
							
								
								
									
										51
									
								
								modules/utils/createElementStatusModes.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								modules/utils/createElementStatusModes.mjs
									
									
									
									
									
										Normal 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;
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								modules/utils/elementMode.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								modules/utils/elementMode.mjs
									
									
									
									
									
										Normal 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;
 | 
			
		||||
							
								
								
									
										11
									
								
								modules/utils/escapeRegExp.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								modules/utils/escapeRegExp.mjs
									
									
									
									
									
										Normal 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
 | 
			
		||||
							
								
								
									
										22
									
								
								modules/utils/getAspectRatio.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								modules/utils/getAspectRatio.mjs
									
									
									
									
									
										Normal 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
 | 
			
		||||
							
								
								
									
										19
									
								
								modules/utils/getElement.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								modules/utils/getElement.mjs
									
									
									
									
									
										Normal 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
 | 
			
		||||
							
								
								
									
										16
									
								
								modules/utils/getLocale.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								modules/utils/getLocale.mjs
									
									
									
									
									
										Normal 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;
 | 
			
		||||
							
								
								
									
										14
									
								
								modules/utils/getRandomId.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								modules/utils/getRandomId.mjs
									
									
									
									
									
										Normal 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
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								modules/utils/isElement.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								modules/utils/isElement.mjs
									
									
									
									
									
										Normal 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
 | 
			
		||||
							
								
								
									
										52
									
								
								modules/utils/throttle.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								modules/utils/throttle.mjs
									
									
									
									
									
										Normal 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
 | 
			
		||||
		Reference in New Issue
	
	Block a user