// // Traversing // let smoothDistance, smoothDuration, smoothEnd, smoothStart; this.$ = function (selector, el) { if (el == null) { el = document; } try { return el.querySelector(selector); } catch (error) {} }; this.$$ = function (selector, el) { if (el == null) { el = document; } try { return el.querySelectorAll(selector); } catch (error) {} }; $.id = (id) => document.getElementById(id); $.hasChild = function (parent, el) { if (!parent) { return; } while (el) { if (el === parent) { return true; } if (el === document.body) { return; } el = el.parentNode; } }; $.closestLink = function (el, parent) { if (parent == null) { parent = document.body; } while (el) { if (el.tagName === "A") { return el; } if (el === parent) { return; } el = el.parentNode; } }; // // Events // $.on = function (el, event, callback, useCapture) { if (useCapture == null) { useCapture = false; } if (event.includes(" ")) { for (var name of event.split(" ")) { $.on(el, name, callback); } } else { el.addEventListener(event, callback, useCapture); } }; $.off = function (el, event, callback, useCapture) { if (useCapture == null) { useCapture = false; } if (event.includes(" ")) { for (var name of event.split(" ")) { $.off(el, name, callback); } } else { el.removeEventListener(event, callback, useCapture); } }; $.trigger = function (el, type, canBubble, cancelable) { const event = new Event(type, { bubbles: canBubble ?? true, cancelable: cancelable ?? true, }); el.dispatchEvent(event); }; $.click = function (el) { const event = new MouseEvent("click", { bubbles: true, cancelable: true, }); el.dispatchEvent(event); }; $.stopEvent = function (event) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); }; $.eventTarget = (event) => event.target.correspondingUseElement || event.target; // // Manipulation // const buildFragment = function (value) { const fragment = document.createDocumentFragment(); if ($.isCollection(value)) { for (var child of $.makeArray(value)) { fragment.appendChild(child); } } else { fragment.innerHTML = value; } return fragment; }; $.append = function (el, value) { if (typeof value === "string") { el.insertAdjacentHTML("beforeend", value); } else { if ($.isCollection(value)) { value = buildFragment(value); } el.appendChild(value); } }; $.prepend = function (el, value) { if (!el.firstChild) { $.append(value); } else if (typeof value === "string") { el.insertAdjacentHTML("afterbegin", value); } else { if ($.isCollection(value)) { value = buildFragment(value); } el.insertBefore(value, el.firstChild); } }; $.before = function (el, value) { if (typeof value === "string" || $.isCollection(value)) { value = buildFragment(value); } el.parentNode.insertBefore(value, el); }; $.after = function (el, value) { if (typeof value === "string" || $.isCollection(value)) { value = buildFragment(value); } if (el.nextSibling) { el.parentNode.insertBefore(value, el.nextSibling); } else { el.parentNode.appendChild(value); } }; $.remove = function (value) { if ($.isCollection(value)) { for (var el of $.makeArray(value)) { if (el.parentNode != null) { el.parentNode.removeChild(el); } } } else { if (value.parentNode != null) { value.parentNode.removeChild(value); } } }; $.empty = function (el) { while (el.firstChild) { el.removeChild(el.firstChild); } }; // Calls the function while the element is off the DOM to avoid triggering // unnecessary reflows and repaints. $.batchUpdate = function (el, fn) { const parent = el.parentNode; const sibling = el.nextSibling; parent.removeChild(el); fn(el); if (sibling) { parent.insertBefore(el, sibling); } else { parent.appendChild(el); } }; // // Offset // $.rect = (el) => el.getBoundingClientRect(); $.offset = function (el, container) { if (container == null) { container = document.body; } let top = 0; let left = 0; while (el && el !== container) { top += el.offsetTop; left += el.offsetLeft; el = el.offsetParent; } return { top, left, }; }; $.scrollParent = function (el) { while ((el = el.parentNode) && el.nodeType === 1) { if (el.scrollTop > 0) { break; } if (["auto", "scroll"].includes(getComputedStyle(el)?.overflowY ?? "")) { break; } } return el; }; $.scrollTo = function (el, parent, position, options) { if (position == null) { position = "center"; } if (options == null) { options = {}; } if (!el) { return; } if (parent == null) { parent = $.scrollParent(el); } if (!parent) { return; } const parentHeight = parent.clientHeight; const parentScrollHeight = parent.scrollHeight; if (!(parentScrollHeight > parentHeight)) { return; } const { top } = $.offset(el, parent); const { offsetTop } = parent.firstElementChild; switch (position) { case "top": parent.scrollTop = top - offsetTop - (options.margin || 0); break; case "center": parent.scrollTop = top - Math.round(parentHeight / 2 - el.offsetHeight / 2); break; case "continuous": var { scrollTop } = parent; var height = el.offsetHeight; var lastElementOffset = parent.lastElementChild.offsetTop + parent.lastElementChild.offsetHeight; var offsetBottom = lastElementOffset > 0 ? parentScrollHeight - lastElementOffset : 0; // If the target element is above the visible portion of its scrollable // ancestor, move it near the top with a gap = options.topGap * target's height. if (top - offsetTop <= scrollTop + height * (options.topGap || 1)) { parent.scrollTop = top - offsetTop - height * (options.topGap || 1); // If the target element is below the visible portion of its scrollable // ancestor, move it near the bottom with a gap = options.bottomGap * target's height. } else if ( top + offsetBottom >= scrollTop + parentHeight - height * ((options.bottomGap || 1) + 1) ) { parent.scrollTop = top + offsetBottom - parentHeight + height * ((options.bottomGap || 1) + 1); } break; } }; $.scrollToWithImageLock = function (el, parent, ...args) { if (parent == null) { parent = $.scrollParent(el); } if (!parent) { return; } $.scrollTo(el, parent, ...args); // Lock the scroll position on the target element for up to 3 seconds while // nearby images are loaded and rendered. for (var image of parent.getElementsByTagName("img")) { if (!image.complete) { (function () { let timeout; const onLoad = function (event) { clearTimeout(timeout); unbind(event.target); return $.scrollTo(el, parent, ...args); }; var unbind = (target) => $.off(target, "load", onLoad); $.on(image, "load", onLoad); return (timeout = setTimeout(unbind.bind(null, image), 3000)); })(); } } }; // Calls the function while locking the element's position relative to the window. $.lockScroll = function (el, fn) { let parent; if ((parent = $.scrollParent(el))) { let { top } = $.rect(el); if (![document.body, document.documentElement].includes(parent)) { top -= $.rect(parent).top; } fn(); parent.scrollTop = $.offset(el, parent).top - top; } else { fn(); } }; let smoothScroll = (smoothStart = smoothEnd = smoothDistance = smoothDuration = null); $.smoothScroll = function (el, end) { smoothEnd = end; if (smoothScroll) { const newDistance = smoothEnd - smoothStart; smoothDuration += Math.min(300, Math.abs(smoothDistance - newDistance)); smoothDistance = newDistance; return; } smoothStart = el.scrollTop; smoothDistance = smoothEnd - smoothStart; smoothDuration = Math.min(300, Math.abs(smoothDistance)); const startTime = Date.now(); smoothScroll = function () { const p = Math.min(1, (Date.now() - startTime) / smoothDuration); const y = Math.max( 0, Math.floor( smoothStart + smoothDistance * (p < 0.5 ? 2 * p * p : p * (4 - p * 2) - 1), ), ); el.scrollTop = y; if (p === 1) { return (smoothScroll = null); } else { return requestAnimationFrame(smoothScroll); } }; return requestAnimationFrame(smoothScroll); }; // // Utilities // $.makeArray = function (object) { if (Array.isArray(object)) { return object; } else { return Array.prototype.slice.apply(object); } }; $.arrayDelete = function (array, object) { const index = array.indexOf(object); if (index >= 0) { array.splice(index, 1); return true; } else { return false; } }; // Returns true if the object is an array or a collection of DOM elements. $.isCollection = (object) => Array.isArray(object) || typeof object?.item === "function"; const ESCAPE_HTML_MAP = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "/": "/", }; const ESCAPE_HTML_REGEXP = /[&<>"'\/]/g; $.escape = (string) => string.replace(ESCAPE_HTML_REGEXP, (match) => ESCAPE_HTML_MAP[match]); const ESCAPE_REGEXP = /([.*+?^=!:${}()|\[\]\/\\])/g; $.escapeRegexp = (string) => string.replace(ESCAPE_REGEXP, "\\$1"); $.urlDecode = (string) => decodeURIComponent(string.replace(/\+/g, "%20")); $.classify = function (string) { string = string.split("_"); for (let i = 0; i < string.length; i++) { var substr = string[i]; string[i] = substr[0].toUpperCase() + substr.slice(1); } return string.join(""); }; // // Miscellaneous // $.noop = function () {}; $.popup = function (value) { try { const win = window.open(); if (win.opener) { win.opener = null; } win.location = value.href || value; } catch (error) { window.open(value.href || value, "_blank"); } }; let isMac = null; $.isMac = () => isMac != null ? isMac : (isMac = navigator.userAgent.includes("Mac")); let isIE = null; $.isIE = () => isIE != null ? isIE : (isIE = navigator.userAgent.includes("MSIE") || navigator.userAgent.includes("rv:11.0")); let isChromeForAndroid = null; $.isChromeForAndroid = () => isChromeForAndroid != null ? isChromeForAndroid : (isChromeForAndroid = navigator.userAgent.includes("Android") && /Chrome\/([.0-9])+ Mobile/.test(navigator.userAgent)); let isAndroid = null; $.isAndroid = () => isAndroid != null ? isAndroid : (isAndroid = navigator.userAgent.includes("Android")); let isIOS = null; $.isIOS = () => isIOS != null ? isIOS : (isIOS = navigator.userAgent.includes("iPhone") || navigator.userAgent.includes("iPad")); $.overlayScrollbarsEnabled = function () { if (!$.isMac()) { return false; } const div = document.createElement("div"); div.setAttribute( "style", "width: 100px; height: 100px; overflow: scroll; position: absolute", ); document.body.appendChild(div); const result = div.offsetWidth === div.clientWidth; document.body.removeChild(div); return result; }; const HIGHLIGHT_DEFAULTS = { className: "highlight", delay: 1000, }; $.highlight = function (el, options) { options = { ...HIGHLIGHT_DEFAULTS, ...(options || {}) }; el.classList.add(options.className); setTimeout(() => el.classList.remove(options.className), options.delay); }; $.copyToClipboard = function (string) { let result; const textarea = document.createElement("textarea"); textarea.style.position = "fixed"; textarea.style.opacity = 0; textarea.value = string; document.body.appendChild(textarea); try { textarea.select(); result = !!document.execCommand("copy"); } catch (error) { result = false; } finally { document.body.removeChild(textarea); } return result; };