Last active
June 26, 2026 12:44
-
-
Save isocroft/e39c96f4eae67c431916636ad273e7fe to your computer and use it in GitHub Desktop.
A collection of very useful helper functions for frontend working with both React and Vanilla JS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { lazy } from "react"; | |
| import type { JSX } from "react"; | |
| import type { UseQueryResult } from "@tanstack/react-query"; | |
| /** | |
| * lazyWithRetry: | |
| * | |
| * | |
| * @param {AsyncFunction<[], { default: () => JSX.Element }>} componentImport | |
| * @param {String=} retryStorageKey | |
| * | |
| * @returns {Object} | |
| */ | |
| export const lazyWithRetry = < | |
| D extends unknown, | |
| E extends Error, | |
| Props = { | |
| queries: Record<string, UseQueryResult<D, E> | null>; | |
| } | |
| >( | |
| componentImport: () => Promise<{ | |
| default: (props?: Props) => JSX.Element | null; | |
| }>, | |
| retryStorageKey = "page-has-been-force-refreshed" | |
| ) => { | |
| return lazy<React.ComponentType<Props | undefined>>(async () => { | |
| const pageHasAlreadyBeenForceRefreshed = JSON.parse( | |
| window.sessionStorage.getItem(retryStorageKey) || "false" | |
| ) as boolean; | |
| function onBeforeUnload(e: BeforeUnloadEvent) { | |
| window.removeEventListener("beforeunload", onBeforeUnload); | |
| window.sessionStorage.removeItem(retryStorageKey); | |
| } | |
| try { | |
| /* @CHECK: https://gist.github.com/raphael-leger/4d703dea6c845788ff9eb36142374bdb#file-lazywithretry-js */ | |
| const component = await componentImport(); | |
| window.sessionStorage.setItem(retryStorageKey, "false"); | |
| return component; | |
| } catch (error) { | |
| if (!pageHasAlreadyBeenForceRefreshed) { | |
| const $retryStorageKey = window.sessionStorage.getItem(retryStorageKey); | |
| if ($retryStorageKey !== "false") { | |
| /* @HINT: Assuming that the user is not on the latest version of the application. */ | |
| /* @HINT: Let's refresh the page immediately. */ | |
| window.sessionStorage.setItem(retryStorageKey, "true"); | |
| window.addEventListener("beforeunload", onBeforeUnload); | |
| window.location.reload(); | |
| } | |
| } else { | |
| /* @HINT: If we get here, it means the page has already been reloaded */ | |
| /* @HINT: Assuming that user is already using the latest version of the application. */ | |
| /* @HINT: Let's let the application crash and raise the error. */ | |
| throw error; | |
| } | |
| /* @INFO: Instead of returning an empty JSX component, return a component with indeterminate spinner */ | |
| return { default: () => null }; | |
| } | |
| }); | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const Settings = lazyWithRetry(() => | |
| * componentLoader(() => import("./pages/Settings/index")) | |
| * ); | |
| * | |
| * console.log(Settings); // { default: () => (<section>...</section>) } | |
| * | |
| */ | |
| /** | |
| * componentLoader: | |
| * | |
| * | |
| * @param {AsyncFunction<[], { default: () => JSX.Element }>} lazyComponent | |
| * @param {Number=} attemptsLeft | |
| * | |
| * @returns {Promise<*>} | |
| */ | |
| export function componentLoader< | |
| D extends unknown, | |
| E extends Error, | |
| M = { | |
| default: (injected?: { | |
| queries: Record<string, UseQueryResult<D, E> | null>; | |
| }) => JSX.Element | null; | |
| } | |
| >(lazyComponent: () => Promise<M>, attemptsLeft = 3) { | |
| return new Promise<M>((resolve, reject) => { | |
| /* @CHECK: https://medium.com/@botfather/react-loading-chunk-failed-error-88d0bb75b406 */ | |
| lazyComponent() | |
| .then(resolve) | |
| .catch((error) => { | |
| window.setTimeout(() => { | |
| if (attemptsLeft === 1) { | |
| reject(error); | |
| return; | |
| } | |
| componentLoader<D, E, M>(lazyComponent, attemptsLeft - 1).then( | |
| resolve, | |
| reject | |
| ); | |
| }, 500); | |
| }); | |
| }); | |
| } | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const loaderPromise = componentLoader(() => import("./pages/Settings/index")) | |
| * | |
| * console.log(loaderPromise); // Promise{<pending>} | |
| * | |
| */ | |
| /** | |
| * verifyDOMElementIsWithinViewPort: | |
| * | |
| * @param {HTMLElement} element | |
| * | |
| * @returns {Boolean} | |
| */ | |
| export const verifyDOMElementIsWhollyWithinViewport = (element: HTMLElement) => { | |
| if (!Boolean(element) || !(element instanceof HTMLElement)) { | |
| throw new TypeError( | |
| `verifyDOMElementIsWhollyWithinViewport(${element}): argument 1 is not a DOM element` | |
| ); | |
| } | |
| const rect = element.getBoundingClientRect(); | |
| const minimumYFrame = 0; | |
| const minimumXFrame = 0; | |
| const maximumYFrame = (window.innerHeight || document.documentElement.clientHeight); | |
| const maximumXFrame = (window.innerWidth || document.documentElement.clientWidth); | |
| return ( | |
| rect.top >= minimumYFrame && | |
| rect.left >= minimumXFrame && | |
| rect.bottom <= maximumYFrame && | |
| rect.right <= maximumXFrame | |
| ); | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const isElementVisibleInViewport = verifyDOMElementIsWhollyWithinViewport( | |
| * document.querySelector('[id="compactor"]') | |
| * ) | |
| * | |
| * console.log(sElementVisibleInViewport) // true | |
| */ | |
| /** | |
| * verifyDOMElementIsNotWithinViewPort: | |
| * | |
| * @param {HTMLElement} element | |
| * | |
| * @returns {Boolean} | |
| */ | |
| export const verifyDOMElementIsNotWithinViewport = (element: HTMLElement) => { | |
| if (!Boolean(element) || !(element instanceof HTMLElement)) { | |
| throw new TypeError( | |
| `verifyDOMElementIsNotWithinViewport(${element}): argument 1 is not a DOM element` | |
| ); | |
| } | |
| const rect = element.getBoundingClientRect(); | |
| const minimumYFrame = 0; | |
| const minimumXFrame = 0; | |
| const maximumYFrame = (window.innerHeight || document.documentElement.clientHeight); | |
| const maximumXFrame = (window.innerWidth || document.documentElement.clientWidth); | |
| return ( | |
| (rect.top < minimumYFrame && | |
| rect.bottom < minimumYFrame) || | |
| (rect.left < minimumXFrame && | |
| rect.right < minimumXFrame) || | |
| rect.y > maximumYFrame || | |
| rect.x > maximumXFrame | |
| ); | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const isElementNotVisibleInViewport = verifyDOMElementIsNotWithinViewport( | |
| * document.querySelector('[id="compactor"]') | |
| * ) | |
| * | |
| * console.log(isElementNotVisibleInViewport) // false | |
| */ | |
| /** | |
| * verifyDOMElementIsPartiallyWithinViewPort: | |
| * | |
| * @param {HTMLElement} element | |
| * | |
| * @returns {Boolean} | |
| */ | |
| export const verifyDOMElementIsPartiallyWithinViewPort = (element: HTMLElement) => { | |
| return !( | |
| verifyDOMElementIsWhollyWithinViewport(element) | |
| ) && !( | |
| verifyDOMElementIsNotWithinViewport(element) | |
| ); | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const isElementPartialyVisibleInViewport = verifyDOMElementIsPartiallyWithinViewPort( | |
| * document.querySelector('[id="compactor"]') | |
| * ) | |
| * | |
| * console.log(isElementPartialyVisibleInViewport) // true | |
| */ | |
| /** | |
| * verifyDOMElementIsWhollyOrPartiallyWithinViewPort: | |
| * | |
| * @param {HTMLElement} element | |
| * | |
| * @returns {Boolean} | |
| */ | |
| export const verifyDOMElementIsWhollyOrPartiallyWithinViewPort = (element: HTMLElement) => { | |
| return ( | |
| verifyDOMElementIsWhollyWithinViewport(element) | |
| ) && !( | |
| verifyDOMElementIsNotWithinViewport(element) | |
| ); | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const isElementPartialyOrWhollyVisibleInViewport = verifyDOMElementIsWhollyOrPartiallyWithinViewPort( | |
| * document.querySelector('[id="compactor"]') | |
| * ) | |
| * | |
| * console.log(isElementPartialyOrWhollyVisibleInViewport) // true | |
| */ | |
| /** | |
| * fileExtension: | |
| * | |
| * @param {String} urlOrFileType | |
| * | |
| * @returns {String} | |
| */ | |
| export const fileExtension = (urlOrFileType?: string | null): string => { | |
| let extension = "blob"; | |
| if ( | |
| urlOrFileType === "image/png" || | |
| urlOrFileType === "image/jpeg" || | |
| urlOrFileType === "image/jpg" || | |
| urlOrFileType === "image/svg+xml" || | |
| urlOrFileType === "application/pdf" || | |
| urlOrFileType === "text/plain" || | |
| urlOrFileType === "application/json" || | |
| urlOrFileType === "text/javascript" || | |
| urlOrFileType === "text/css" || | |
| urlOrFileType === "text/csv" || | |
| urlOrFileType === "text/x-csv" || | |
| urlOrFileType === "application/vnd.ms-excel" || | |
| urlOrFileType === "application/csv" || | |
| urlOrFileType === "application/x-csv" || | |
| urlOrFileType === "text/comma-separated-values" || | |
| urlOrFileType === "text/x-comma-separated-values" || | |
| urlOrFileType === "text/tab-separated-values" || | |
| urlOrFileType === "application/octet-stream" | |
| ) { | |
| [ extension ] = (urlOrFileType || "/").split("/").reverse(); | |
| if (extension === "octet-stream") { | |
| extension = "blob"; | |
| } | |
| if ([ | |
| "x-csv", | |
| "vnd.ms-excel", | |
| "tab-separated-values", | |
| "comma-separated-values", | |
| "x-comma-separated-values"].includes(extension)) { | |
| extension = "csv"; | |
| } | |
| } else if (typeof urlOrFileType === "string") { | |
| const [ urlBaseName ] = urlOrFileType.split(/[#?]/); | |
| [ extension ] = urlBaseName.split(".").reverse(); | |
| } | |
| return extension === "javascript" ? "js" : extension; | |
| }; | |
| /* @EXAMPLE: fileExtension("image/png") */ | |
| /** | |
| * composeClasses: | |
| * | |
| * @param {Array.<*>} styles | |
| * | |
| * @returns {String} | |
| */ | |
| export const composeClasses = (...styles: unknown[]): string => { | |
| return Array.from(new Set(styles.filter((item) => item).join(' '))); | |
| } | |
| /* @EXAMPLE: <AvatarWrapper className={composeClasses('text-align-center', 'position-relative')} /> */ | |
| /** | |
| * isLocalHost: | |
| * | |
| * @returns {Boolean} | |
| * | |
| */ | |
| export const isLocalHost = (): boolean => { | |
| return window.location.port === "" | |
| ? ["http://localhost", "http://127.0.0.1"].includes( | |
| window.location.origin.replace(/\:[\d$]{4,5}/, "") | |
| ) | |
| : Boolean( | |
| window.location.hostname === 'localhost' || | |
| // [::1] is the IPv6 localhost address. | |
| window.location.hostname === '[::1]' || | |
| // 127.0.0.1/8 is considered localhost for IPv4. | |
| window.location.hostname.match( | |
| /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ | |
| ) | |
| ) | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const currentlyLocalHost = isLocalHost(); | |
| * | |
| * console.log(currentlyLocalHost) // false | |
| * | |
| */ | |
| /** | |
| * isRemotsHost: | |
| * | |
| * @returns {Boolean} | |
| * | |
| */ | |
| export const isRemoteHost = (): boolean => { | |
| return window.location.origin.includes(process.env.REACT_APP_REMOTE_HOST); | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const currentlyRemoteHost = isRemoteHost(); | |
| * | |
| * console.log(currentlyRemoteHost) // true | |
| * | |
| */ | |
| /** | |
| * bloToDataURL: | |
| * | |
| * @param {Blob} blob | |
| * | |
| * @returns {Promise<String>} | |
| * | |
| */ | |
| export const blobToDataURL = (blob: Blob): Promise<string> => { | |
| return new Promise((fulfill: Function, reject: Function) => { | |
| let reader: FileReader = new FileReader() | |
| reader.onerror = (ev: ProgressEvent<FileReader>) => | |
| reject(ev.target?.error) | |
| reader.onload = () => fulfill(reader.result) | |
| reader.readAsDataURL(blob) | |
| }) | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * blobToDataURL(new Blob(['Hello World!'], { type: "text/plain" })).then( | |
| * (dataURL) => { | |
| * console.log(dataURL) // "data:text/plain,Hello%20World!" | |
| * }); | |
| * | |
| * | |
| * blobToDataURL(new Blob(new Uint8Array([225, 120, 90]), { type: "image/gif" })).then( | |
| * (dataURL) => { | |
| * console.log(dataURL) // "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" | |
| * }); | |
| * | |
| */ | |
| /** | |
| * dataURLtoObjectURL: converts a data URI to an object URL | |
| * | |
| * | |
| * @param {String} dataURL | |
| * | |
| * @returns {String} | |
| * | |
| * @see https://en.wikipedia.org/wiki/Data_URI_scheme/ | |
| */ | |
| export const dataURLtoObjectURL = (dataURL?: string): string => { | |
| const [ mimeType, base64String ] = (dataURL || ",").split(","); | |
| const [, contentTypeDataPrefix ] = mimeType.split(":") || [, ";"]; | |
| const [ contentType ] = contentTypeDataPrefix | |
| ? contentTypeDataPrefix.split(";") | |
| : ["application/octet-stream"]; | |
| return URL.createObjectURL( | |
| base64StringToBlob(base64String, contentType) | |
| ); | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * | |
| * const objectURL = dataURLtoObjectURL( | |
| * "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII=" | |
| * ) | |
| * | |
| * console.log(objectURL) // "blob:https://coredium.com/aef24cd56bc355fea22b1" | |
| * | |
| */ | |
| /** | |
| * dataURLtoObjectBlob: converts a data URI to a blob | |
| * | |
| * | |
| * @param {String} dataURL | |
| * | |
| * @returns {Blob} | |
| * | |
| * @see https://en.wikipedia.org/wiki/Data_URI_scheme/ | |
| * @see https://en.wikipedia.org/wiki/Binary_large_object/ | |
| */ | |
| export const dataURLtoObjectBlob = (dataURL?: string): Blob => { | |
| const [ mimeType, base64String ] = (dataURL || ",").split(","); | |
| const [, contentTypeDataPrefix ] = mimeType.split(":") || [, ";"]; | |
| const [ contentType ] = contentTypeDataPrefix | |
| ? contentTypeDataPrefix.split(";") | |
| : ["application/octet-stream"]; | |
| return base64StringToBlob(base64String, contentType); | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const fileBlob = dataURItoObjectBlob( | |
| * "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII=" | |
| * ); | |
| * | |
| * console.log(fileBlob); // Blob {size: 68, type: 'image/png'} | |
| */ | |
| /** | |
| * blobToFile: | |
| * | |
| * @param {Blob | Undefined} theBlob | |
| * @param {String | Null | Undefined} fileName | |
| * @param {Boolean} useCast | |
| * | |
| * @returns {File} | |
| * | |
| */ | |
| export const blobToFile = (theBlob?: Blob, fileName?: string | null, useCast = false): File => { | |
| const todaysDate = new Date(); | |
| const defaultFileName = `${todaysDate.getTime()}_${Math.random() * 1}`; | |
| const defaultFileExtension = `.${fileExtension(theBlob?.type)}`; | |
| const fullFileName = defaultFileName + defaultFileExtension; | |
| if (!(theBlob instanceof window.Blob)) { | |
| return new File([""], fullFileName); | |
| } | |
| const blob = <Blob & { lastModifiedDate: Date, name: string }>theBlob; | |
| blob.lastModifiedDate = new Date(); | |
| blob.name = fileName || fullFileName; | |
| return useCast ? <File>theBlob : new File([theBlob], fileName || fullFileName); | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * | |
| * const file = blobToFile(new Blob(['hello!'], { type: 'text/plain' }), "text.txt"); | |
| * | |
| * console.log(file) // File: {} | |
| * | |
| */ | |
| /** | |
| * base64StringToBlob: | |
| * | |
| * @param {String} base64Data | |
| * @param {String} contentType | |
| * @param {Number} sliceSize | |
| * | |
| * @returns {Blob} | |
| * | |
| */ | |
| export const base64StringToBlob = (base64Data: string, contentType?: string | null, sliceSize = 512) => { | |
| const $contentType = contentType || ""; | |
| const byteCharacters = atob(base64Data); | |
| const byteArrays = []; | |
| for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { | |
| const slice = byteCharacters.slice(offset, offset + sliceSize); | |
| const byteNumbers = new Array(slice.length); | |
| for (let i = 0; i < slice.length; i++) { | |
| byteNumbers[i] = slice.charCodeAt(i); | |
| } | |
| const byteArray = new Uint8Array(byteNumbers); | |
| byteArrays.push(byteArray); | |
| } | |
| return new Blob(byteArrays, { type: $contentType }); | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const urlString = blobToDataURL(new Blob(['hello world'], { type: 'text/plain' })); | |
| * | |
| * console.log(urlString) // | |
| * | |
| */ | |
| /** | |
| * getJpegBlob: | |
| * | |
| * @param {HTMLCanvasElement | null} canvas | |
| * | |
| * @returns {Promise<Blob | null>} | |
| * | |
| */ | |
| export function getJpegBlob(canvas: HTMLCanvasElement | null): Promise<Blob | null> { | |
| /* @CHECK: https://stackoverflow.com/a/46182044/5221762 */ | |
| /* @NOTE: May require the `toBlob()` polyfill */ | |
| if (!HTMLCanvasElement.prototype.toBlob) { | |
| window.Object!.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { | |
| value: function (callback, type, quality) { | |
| const canvas = this; | |
| window.setTimeout(function() { | |
| var binStr = atob( canvas.toDataURL(type, quality).split(',')[1] ), | |
| len = binStr.length, | |
| arr = new Uint8Array(len); | |
| for (let index = 0; index < len; index++ ) { | |
| arr[index] = binStr.charCodeAt(index); | |
| } | |
| callback( new Blob( [arr], {type: type || 'image/png'} ) ); | |
| }, 0); | |
| } | |
| }); | |
| } | |
| return new Promise((resolve, reject) => { | |
| try { | |
| if (canvas) { | |
| canvas.toBlob(blob => resolve(blob), 'image/jpeg', 0.95); | |
| } | |
| } catch (e) { | |
| reject(e); | |
| } | |
| }) | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const blob = await getJpegBlob(window.document.getElementsByTagName('canvas')[0]); | |
| * | |
| * console.log(blob) // Blob: {} | |
| * | |
| */ | |
| /** | |
| * getJpegBytes: | |
| * | |
| * @param {HTMLCanvasElement | null} canvas | |
| * | |
| * @returns {Promise<string | ArrayBuffer | null>} | |
| * | |
| */ | |
| export function getJpegBytes(canvas: HTMLCanvasElement | null): Promise<string | ArrayBuffer | null> { | |
| return getJpegBlob(canvas).then((blob) => { | |
| return new Promise((resolve, reject) => { | |
| const fileReader = new FileReader() | |
| fileReader.addEventListener('loadend', () => { | |
| if (this.error) { | |
| reject(this.error) | |
| return; | |
| } | |
| resolve(this.result) | |
| }) | |
| if (blob) { | |
| fileReader.readAsArrayBuffer(blob); | |
| } | |
| }) | |
| }) | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const bytes = await getJpegBytes(window.document.getElementsByTagName('canvas')[0]); | |
| * | |
| * console.log(bytes); // | |
| * | |
| */ | |
| /** | |
| * isBase64String: | |
| * | |
| * @param {String} base64String | |
| * | |
| * @returns {Boolean} | |
| * | |
| */ | |
| export const isBase64String = (base64String: string): boolean => { | |
| let base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; | |
| return typeof base64String !== 'string' | |
| ? false | |
| : (base64String.length % 4 === 0) && base64Regex.test(base64String) | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const isStringified = isBase64String(""); | |
| * | |
| * console.log(isStringified) // | |
| * | |
| */ | |
| /** | |
| * getEmbedUrl: | |
| * | |
| * @param {String} url | |
| * @param {Boolean} autoPlay | |
| * | |
| * | |
| * @returns {String} | |
| * | |
| */ | |
| export const getEmbedUrl = (url: string, autoPlay = false): string => { | |
| if (!(Boolean(url)) || typeof url !== "string") { | |
| throw new TypeError( | |
| `getEmbedUrl("${url}", ${autoPlay}): argument 1 is not a string` | |
| ); | |
| } | |
| const $autoPlay = typeof autoPlay === "boolean" ? autoPlay : Boolean(autoPlay); | |
| try { | |
| const parsedUrl = new URL(url); | |
| const domain = parsedUrl.hostname.replace('www.', '').replace('m.', ''); | |
| let videoId = ''; | |
| if (domain === 'youtube.com' || domain === 'youtu.be') { | |
| if (parsedUrl.pathname.includes('/embed/')) { | |
| return `${url}${$autoPlay ? '?&autoplay=1&mute=1' : ''}`; | |
| } | |
| if (parsedUrl.pathname.includes('/watch')) { | |
| videoId = parsedUrl.searchParams.get('v') || ''; | |
| if (videoId === '') throw new SyntaxError("`videoId` is an empty string"); | |
| return `https://www.youtube.com/embed/${videoId}${$autoPlay ? '?&autoplay=1&mute=1' : ''}`; | |
| } | |
| if (domain === 'youtu.be') { | |
| videoId = parsedUrl.pathname.replace('/', ''); | |
| if (videoId === '') throw new SyntaxError("`videoId` is an empty string"); | |
| return `https://www.youtube-nocookie.com/embed/${videoId}${$autoPlay ? '?&autoplay=1&mute=1' : ''}`; | |
| } | |
| } else if (domain === 'vimeo.com' || domain === 'player.vimeo.com') { | |
| if (parsedUrl.pathname.includes('/video/')) { | |
| return `${url}${$autoPlay ? '?&autoplay=1&muted=1&dnt=1' : ''}`; | |
| } | |
| videoId = parsedUrl.pathname.replace('/', ''); | |
| if (videoId === '') throw new SyntaxError("`videoId` is an empty string"); | |
| return `https://player.vimeo.com/video/${videoId}${$autoPlay ? '?&autoplay=1&muted=1&dnt=1' : ''}`; | |
| } | |
| } catch (error) { | |
| const message = error instanceof SyntaxError | |
| ? "getEmbedUrl(...): Cannot create embed URL" | |
| : "getEmbedUrl(...): possibly invalid URL as argument"; | |
| throw new TypeError( | |
| message, | |
| { cause: error } | |
| ); | |
| } | |
| return ''; | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const embedURL = getEmbedUrl("https://youtube.com/watch?v=9JLpWR_yHZQ", true); | |
| * | |
| * console.log(embedURL) // "https://www.youtube-nocookie.com/embed/9JLpWR_yHZQ" | |
| * | |
| */ | |
| /** | |
| * sleepFor: | |
| * | |
| * @param {Number} durationInMilliSeconds | |
| * | |
| * @returns {void} | |
| * | |
| */ | |
| export const sleepFor = (durationInMilliSeconds = 10) => { | |
| return new Promise( | |
| (resolve) => window.setTimeout(resolve, durationInMilliSeconds) | |
| ); | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const promise = await sleepFor(2500); | |
| * | |
| * console.log(promise) // Promise {<pending>} | |
| * | |
| */ | |
| /** | |
| * waitFor: | |
| * | |
| * | |
| * @param {Function} conditionCallback | |
| * @param {Number} pollIntervalMilliSeconds | |
| * @param {Number} timeoutAfterMilliSeconds | |
| * | |
| * @returns {Void} | |
| * | |
| * @see https://davidwalsh.name/waitfor/ | |
| */ | |
| export const waitFor = async ( | |
| conditionCallback: () => boolean, | |
| pollIntervalMilliSeconds = 50, | |
| timeoutAfterMilliSeconds = 3000 | |
| ) => { | |
| const startTimeMilliSeconds = Date.now(); | |
| while (true) { | |
| if ( | |
| typeof (timeoutAfterMilliSeconds) === "number" | |
| && Date.now() > startTimeMilliSeconds + timeoutAfterMilliSeconds) { | |
| throw new Error("Condition not met bbefore timeout"); | |
| } | |
| const result = conditionCallback(); | |
| if (result) { | |
| return result; | |
| } | |
| await sleepFor(pollIntervalMilliSeconds); | |
| } | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const aFunction = () => { | |
| * await waitFor(() => window.document.body.classList.has('loaded'), 100, 5000) | |
| * }; | |
| * | |
| */ | |
| /** | |
| * htmlEncode: | |
| * | |
| * | |
| * @param {String} rawText | |
| * | |
| * @returns {String} | |
| * | |
| */ | |
| export const htmlEncode = (rawText: string): string => { | |
| if (typeof rawText !== "string") { | |
| throw new TypeError( | |
| `htmlEncode("${rawText}"): argument 1 is not a string` | |
| ); | |
| } | |
| return (rawText || "").replace(/[\u00A0-\u9999<>&]/gim, function (mark: string) { | |
| return '&#' + mark.charCodeAt(0) + ';' | |
| }) | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const encodedHTML = htmlEncode('<h1><img onerror="javascript:return null" /></h1>'); | |
| * | |
| * console.log(encodedHTML); // "" | |
| * | |
| */ | |
| /** | |
| * htmlDecode: | |
| * | |
| * | |
| * @param {String} encodedText | |
| * | |
| * @returns {String | Null} | |
| * | |
| */ | |
| export const htmlDecode = (encodedText: string): string | null => { | |
| if (typeof encodedText !== "string") { | |
| throw new TypeError( | |
| `htmlDecode("${encodedText}"): argument 1 is not a string` | |
| ); | |
| } | |
| const doc = new window.DOMParser().parseFromString(encodedText || " ", 'text/html') | |
| const docElem = doc.documentElement as Node | |
| return docElem.textContent | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const decodedHTML = htmlDecode("<h1>Hi there!</h1>"); | |
| * | |
| * console.log(decodedHTML); // "<h1>Hi there!</h1>" | |
| * | |
| */ | |
| /** | |
| * detectFullScreenTrigger: | |
| * | |
| * | |
| * @param {Event} event | |
| * | |
| * @returns {"user-manual" | "programmatic" | "unknown"} | |
| * | |
| */ | |
| export const detectFullScreenTrigger = (event: Event): string => { | |
| if ( | |
| window.matchMedia && | |
| window.matchMedia('(display-mode: fullscreen)').matches | |
| ) { | |
| // page entered fullscreen mode through the Web Application Manifest | |
| return 'user-manual' | |
| } else if (document.fullscreenEnabled && document.fullscreenElement) { | |
| // page entered fullscreen mode through the Fullscreen API | |
| return 'programmatic' | |
| } | |
| return 'unknown' | |
| }; | |
| /* @EXAMPLE: document.onfullscreenchange = detectFullScreenTrigger; */ | |
| /** | |
| * detectAppleIOS: | |
| * | |
| * | |
| * @returns {Boolean} | |
| * | |
| */ | |
| export const detectAppleIOS = (): boolean => { | |
| const global: Window = window | |
| const navigator: Navigator = global.navigator | |
| const userAgent = navigator.userAgent.toLowerCase() | |
| const vendor = navigator.vendor.toLowerCase() | |
| return /iphone|ipad|ipod/.test(userAgent) && vendor.indexOf('apple') > -1 | |
| } | |
| /* @EXAMPLE: const isIOS = detectAppleIOS() */ | |
| /** | |
| * isInStandaloneMode: | |
| * | |
| * | |
| * @returns {Boolean} | |
| * | |
| */ | |
| export const isInStandaloneMode = (): boolean => { | |
| const global: Window = window | |
| const navigator: Navigator = global.navigator | |
| const location: Location = global.location | |
| /** | |
| * @CHECK: https://stackoverflow.com/questions/21125337/how-to-detect-if-web-app-running-standalone-on-chrome-mobile | |
| */ | |
| if (detectAppleIOS() && navigator instanceof Navigator) { | |
| return navigator.standalone === true | |
| } | |
| return ( | |
| location.search.indexOf('standalone=true') !== -1 && | |
| Boolean(global.matchMedia('(display-mode: standalone)').matches) && | |
| (global.screen.height - document.documentElement.clientHeight < 40 || | |
| global.screen.width - document.documentElement.clientHeight < 40) | |
| ) | |
| }; | |
| /* @EXAMPLE: const standalone = isInStandaloneMode(); */ | |
| /** | |
| * formatHTMLEntity: | |
| * | |
| * | |
| * @param {String} textValue | |
| * @param {String} entityHexValue | |
| * @param {String} prefix | |
| * | |
| * @returns {String} | |
| * | |
| */ | |
| export const formatHTMLEntity = ( | |
| textValue: string, | |
| entityHexVal: string, | |
| prefix: string = '' | |
| ): string => { | |
| if (typeof textValue !== "string") { | |
| throw new TypeError( | |
| `formatHTMLEntity("${textValue}", "${entityHexValue}", "${prefix}"): argument 1 is not a string` | |
| ); | |
| } | |
| if (typeof entityHexValue !== "string") { | |
| throw new TypeError( | |
| `formatHTMLEntity("${textValue}", "${entityHexValue}", "${prefix}"): argument 2 is not a string` | |
| ); | |
| } | |
| if (typeof prefix !== "string") { | |
| throw new TypeError( | |
| `formatHTMLEntity("${textValue}", "${entityHexValue}", "${prefix}"): argument 3 is not a string` | |
| ); | |
| } | |
| const isNumeric = /^\d{2,5}$/.test(entityHexValue) | |
| const number = parseInt(isNumeric ? "8" : entityHexValue, 16) | |
| return ( | |
| (textValue ? textValue + ' ' : '') + | |
| prefix + String.fromCharCode(number) | |
| ) | |
| }; | |
| /* @EXAMPLE: <p className="wrapper">{formatHTMLEntity('View Full Project', '279D')}</p> */ | |
| /** | |
| * isEmpty: | |
| * | |
| * @param {Object} objectValue | |
| * | |
| * @returns {Boolean} | |
| */ | |
| export function isEmpty<T>(objectValue: T): boolean { | |
| if(!objectValue || typeof objectValue !== "object") { | |
| return true; | |
| } | |
| for(const prop in objectValue) { | |
| if(Object.prototype.hasOwnProperty.call(objectValue, prop)) { | |
| return false; | |
| } | |
| } | |
| return JSON.stringify(objectValue) === JSON.stringify({}); | |
| } | |
| /* @EXAMPLE: isEmpty({}) */ | |
| /** | |
| * slugify: | |
| * | |
| * | |
| * @param {String} plainText | |
| * @param {String} delimeter | |
| * | |
| * @returns {String} | |
| * | |
| */ | |
| export const slugify = (plainText: string, delimeter = "_") => { | |
| return (plainText || "") | |
| .toString() | |
| .normalize("NFD") | |
| .replace(/[\u0300-\u036f]/g, "") | |
| .toLowerCase() | |
| .trim() | |
| .replace(/[^a-z0-9 ]/g, "") | |
| .replace(/\s+/g, delimeter); | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const slugifiedText = slugify('Last Name'); | |
| * | |
| * console.log(slugifiedText); // "last_name" | |
| * | |
| */ | |
| /** | |
| * unSlugifyText: | |
| * | |
| * @param {String} slugifiedText | |
| * @param {String} delimeter | |
| * @param {Boolean} shouldTrim | |
| * | |
| * @returns {String} | |
| * | |
| */ | |
| export const unSlugifyText = ( | |
| slugifiedText: string, | |
| delimeter = '_', | |
| shouldTrim = false | |
| ): string => { | |
| if (typeof text !== "string") { | |
| throw new TypeError( | |
| `unSlugifyText("${slugifiedText}", "${delimeter}", ${shouldTrim}): argument 1 is not a string`); | |
| } | |
| let $separator = separator; | |
| if (typeof $separator !== "string") { | |
| throw new TypeError( | |
| `unSlugifyText("${slugifiedText}", "${delimeter}", ${shouldTrim}): argument 2 is not a string` | |
| ); | |
| } | |
| try { | |
| return (slugifiedText || '') | |
| .split(delimeter) | |
| .map( | |
| (slugPart) => | |
| `${slugPart.charAt(0).toUpperCase()}${slugPart.substring(1)}` | |
| ).join(Boolean(shouldTrim) ? '' : ' ') | |
| } catch (error) { | |
| throw new TypeError( | |
| `unSlugifyText("${slugifiedText}", "${delimeter}", ${shouldTrim}): cannot return unslugify input string`, | |
| { cause: error } | |
| ); | |
| } | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const text = unSlugifyText('first_name'); | |
| * | |
| * console.log(text); // "First_Name" | |
| * | |
| */ | |
| /** | |
| * getOrdinalSuffixForNumber: | |
| * | |
| * | |
| * @param {Number} ordinal | |
| * @param {Boolean} asWord | |
| * | |
| * @returns {String} | |
| * | |
| */ | |
| export const getOrdinalSuffixForNumber = (ordinal: number, asWord = false): string => { | |
| let ord = "th"; | |
| if (ordinal % 10 == 1 && ordinal % 100 != 11) { | |
| ord = "st"; | |
| } | |
| else if (ordinal % 10 == 2 && ordinal % 100 != 12) { | |
| ord = asWord ? "ond" : "nd"; | |
| } | |
| else if (ordinal % 10 == 3 && ordinal % 100 != 13) { | |
| ord = "rd"; | |
| } | |
| return ord; | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const ordinalSuffix = getOrdinalSuffixForNumber(23); | |
| * | |
| * console.log(ordinalWithSuffix) // "rd" | |
| * | |
| * console.log(23 + ordinalWithSuffix) // "23rd" | |
| * | |
| */ | |
| /** | |
| * getShortSuffixForAmount: | |
| * | |
| * @param {Number} amount | |
| * | |
| * @returns {String} | |
| * | |
| */ | |
| export const getShortSuffixForAmount = (amount: number): string => { | |
| const strFigure = String(Number.isNaN(amount) ? false : Math.round(amount)) | |
| const [firstPart, ...remainingParts] = strFigure.match( | |
| /\d{1,3}(?=(\d{3})*$)/g | |
| ) || [''] | |
| const shortenedMap: { [key: number]: string } = { | |
| 1: 'K', | |
| 2: 'M', | |
| 3: 'B', | |
| 4: 'T', | |
| 5: 'Z', | |
| } | |
| return firstPart !== '' && remainingParts.length | |
| ? firstPart + shortenedMap[remainingParts.length] | |
| : firstPart | |
| }; | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const amountWithSuffix = getShortSuffixForAmount(305000); | |
| * | |
| * console.log(amountWithSuffix) // "305K" | |
| * | |
| */ | |
| /** | |
| * validateUIDString: | |
| * | |
| * @param {String} uidLineText | |
| * | |
| * @returns {Boolean} | |
| * | |
| */ | |
| export const validateUIDString = (uidLineText: string): boolean => { | |
| const uuidRegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i | |
| const guidRegExp = /^(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}$/gi | |
| let uid = uidLineText; | |
| if (uidLineText.startsWith('{')) { | |
| uid = uidLineText.substring(1, uidLineText.length - 1); | |
| } | |
| return uuidRegExp.test(uid) || guidRegExp.test(uid) | |
| } | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const isValidUUID = validateUIDString('a4caeacc-72cb-4824-80f8-b55961f148c6'); | |
| * | |
| * console.log(isValidUUID); // true | |
| * | |
| */ | |
| /** | |
| * validateWebPageURL: | |
| * | |
| * @param {String} | |
| urlString * | |
| * @returns {Boolean} | |
| * | |
| */ | |
| export const validateWebPageURL = (urlString: string): boolean => { | |
| let result = null; | |
| try { | |
| result = urlString.match( | |
| /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z0-9]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g | |
| ) | |
| } catch (e) { | |
| result = null | |
| } | |
| return result !== null; | |
| } | |
| /*! | |
| * @EXAMPLE: | |
| * | |
| * const isWebPageURL = validateWebPageURL('https://www.example.com?id=98747904'); | |
| * | |
| * console.log(isWebPageURL)); // true | |
| * | |
| */ |
Author
Example code on how to use on a web page to lazy load YouTube/Vimeo videos
<youtube-vimeo-embed data-poster="https://adrianroselli.com/wp-content/uploads/2024/06/web-components-logo.png" data-ratio="1/1">
<p>
<a href="https://youtu.be/Sq5oiHjwFxI?start=14">Kevin Powell: Creating Web Components with Dave Rupert</a>, lasting over an hour and a half.
</p>
</youtube-vimeo-embed>Next
<youtube-vimeo-embed>
<p>
YouTube: <a href="https://youtu.be/PLXAuxZKKjs?start=31">Overlays Underwhelm at WordPress Accessibility Day</a>, 46:48
</p>
</youtube-vimeo-embed>Next
<youtube-vimeo-embed data-ratio="21/9">
<p>
YouTube: <a href="https://youtu.be/DsoZI4TLqMc">The Boy and the Heron trailer</a>, 1:50
</p>
</youtube-vimeo-embed>See more here: https://adrianroselli.com/2024/06/youtube-and-vimeo-web-component.html
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.