Dit script moet niet direct worden geïnstalleerd - het is een bibliotheek voor andere scripts om op te nemen met de meta-richtlijn // @require https://update.greasyfork.org/scripts/398877/1333419/utilsjs.js
/* GitHub userscript utilities v0.3.0
* Copyright © 2023 Rob Garrison
* License: MIT
*/
/* exported
* $ $$
* addClass removeClass toggleClass
* removeEls removeSelection
* on off make
* debounce
* iterateGenerator
*/
"use strict";
const REGEX = {
WHITESPACE: /\s+/,
NAMESPACE: /[.:]/,
COMMA: /\s*,\s*/
};
/* DOM utilities */
/**
* Find & return a single DOM node
* @param {String} selector - CSS selector string
* @param {HTMLElement} el - DOM node to start the query (defaults to document)
* @returns {HTMLElement|null}
*/
const $ = (selector, el) => (el || document).querySelector(selector);
/**
* Find & return multiple DOM nodes
* @param {String} selector - CSS selector string
* @param {HTMLElement} el - DOM node to start the query (defaults to document)
* @returns {HTMLElement[]}
*/
const $$ = (selector, el) => [...(el || document).querySelectorAll(selector)];
/**
* Common functions
*/
const _ = {};
/**
* Return an array of elements
* @param {HTMLElement|HTMLElement[]|NodeList} elements
* @returns {HTMLElement[]}
*/
_.createElementArray = elements => {
if (Array.isArray(elements)) {
return elements;
}
return elements instanceof NodeList ? [...elements] : [elements];
};
/**
* Common event listener code
* @param {String} type - "add" or "remove" event listener
* @param {HTMLElement[]} els - DOM node array that need listeners
* @param {String} name - Event name, e.g. "click", "mouseover", etc
* @param {Function} handler - Event callback
* @param {Object} options - Event listener options or useCapture - see
* https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#parameters
*/
_.eventListener = (type, els, name, handler, options) => {
const events = name.split(REGEX.WHITESPACE);
_.createElementArray(els).forEach(el => {
events.forEach(ev => {
el?.[`${type}EventListener`](ev, handler, options);
});
});
};
/**
* Create an array of classes/event types from a space or comma separated string
* @param {String} classes - space or comma separated list of classes or events
* @returns {String[]}
*/
_.getClasses = classes => {
if (Array.isArray(classes)) {
return classes;
}
const names = classes.toString();
return names.includes(",") ? names.split(REGEX.COMMA) : [names];
};
/**
* Add class name(s) to one or more elements
* @param {HTMLElements[]|Nodelist|HTMLElement|Node} elements
* @param {string|array} classes - class name(s) to add; string can contain a
* comma separated list
*/
const addClass = (elements, classes) => {
const classNames = _.getClasses(classes);
const els = _.createElementArray(elements);
let index = els.length;
while (index--) {
els[index]?.classList.add(...classNames);
}
};
/**
* Remove class name(s) from one or more elements
* @param {HTMLElements[]|NodeList|HTMLElement|Node} elements
* @param {string|array} classes - class name(s) to add; string can contain a
* comma separated list
*/
const removeClass = (elements, classes) => {
const classNames = _.getClasses(classes);
const els = _.createElementArray(elements);
let index = els.length;
while (index--) {
els[index]?.classList.remove(...classNames);
}
};
/**
* Toggle class name of DOM element(s)
* @param {HTMLElement|HTMLElement[]|NodeList} els
* @param {string} name - class name to toggle (toggle only accepts one name)
* @param {boolean} flag - force toggle; true = add class, false = remove class;
* if undefined, the class will be toggled based on the element's class name
*/
// flag = true, then add class
const toggleClass = (elements, className, flag) => {
const els = _.createElementArray(elements);
let index = elms.length;
while (index--) {
els[index]?.classList.toggle(className, flag);
}
};
/**
* Remove DOM nodes
* @param {String} selector - CSS selector string
* @param {HTMLElement|undefined} el - parent DOM node (defaults to document)
*/
const removeEls = (selector, el) => {
let els = $$(selector, el);
let index = els.length;
while (index--) {
els[index].parentNode.removeChild(els[index]);
}
};
/**
* Remove text selection
*/
const removeSelection = () => {
// remove text selection - https://stackoverflow.com/a/3171348/145346
const sel = window.getSelection
? window.getSelection()
: document.selection;
if (sel) {
if (sel.removeAllRanges) {
sel.removeAllRanges();
} else if (sel.empty) {
sel.empty();
}
}
};
/**
* Add/remove event listener
* @param {HTMLElement|HTMLElement[]|NodeList} els
* @param {string} name - event name(s) to bind, e.g. "mouseup mousedown"; also
* accpets a comma separated string, e.g. "mouseup, mousedown"
* @param {function} handler - event handler
* @param {options} eventListener options
*/
const on = (els, name = "", handler, options) => {
_.eventListener("add", els, name, handler, options);
};
const off = (els, name = "", handler, options) => {
_.eventListener("remove", els, name, handler, options);
}
/**
* **** Helpers ****
*/
/**
* Debounce
* @param {Function} fxn - callback executed after debounce
* @param {Number} time - time (in ms) to delay
* @returns {Function} debounced function
*/
const debounce = (fxn, time = 500) => {
let timer;
return function() {
clearTimeout(timer);
timer = setTimeout(() => {
fxn.apply(this, arguments);
}, time);
}
}
/**
* Iterate through function asynchronously until done
* @param {function} generator
* @param {number} maxPerCycle
*/
const iterateGenerator = (generator, maxPerCycle = 40) => {
let status;
// loop with delay to allow user interaction
const loop = () => {
for (let i = 0; i < maxPerCycle; i++) {
status = generator.next();
}
if (!status.done) {
requestAnimationFrame(loop);
}
};
loop();
}
/**
* @typedef Utils~makeEvents
* @type {object[]}
* @property {string} el - event listener target
* @property {string} type - event type
* @property {func} callback - event callback
*/
/**
* @typedef Utils~makeOptions
* @type {object}
* @property {string} el - HTML element tag, e.g. "div" (default)
* @property {string} appendTo - selector of target element to append menu
* @property {string} className - CSS classes to add to the element
* @property {object} attrs - HTML attributes (as key/value paries) to set
* @property {object} text - string added to el using textContent
* @property {string} html - html to be added using `innerHTML` (overrides `text`)
* @property {array} children - array of elements to append to the created element
* @property {Utils~makeEvent} events - events to attach listeners
*/
/**
* Create a DOM element
* @param {Utils~makeOptions}
* @returns {HTMLElement} (may be already inserted in the DOM)
* @example
make({ el: 'ul', className: 'wrapper', appendTo: 'body' }, [
make({ el: 'li', text: 'item #1' }),
make({ el: 'li', text: 'item #2' })
]);
*/
const make = (obj = {}, children) => {
const el = document.createElement(obj.el || "div");
const { appendTo, attrs, events } = obj;
const xref = {
className: "className",
id: "id",
text: "textContent",
html: "innerHTML", // overrides text setting
type: "type" // button type
};
Object.keys(xref).forEach(key => {
if (obj[key]) {
el[xref[key]] = obj[key];
}
});
if (attrs) {
Object.keys(attrs).forEach(key => {
el.setAttribute(key, attrs[key]);
});
}
if (Array.isArray(children) && children.length) {
children.forEach(child => el.appendChild(child));
}
if (events?.length) {
for (let event of events) {
on(event?.el || el, event.type, event.callback);
}
}
if (appendTo) {
const wrap = typeof appendTo === "string"
? $(appendTo)
: appendTo;
if (wrap) {
wrap.appendChild(el);
}
}
return el;
}