Skip to content

Instantly share code, notes, and snippets.

@doeixd
Last active May 13, 2024 18:37
Show Gist options
  • Save doeixd/567d1865f0419719e0d0a94a81de6216 to your computer and use it in GitHub Desktop.
Save doeixd/567d1865f0419719e0d0a94a81de6216 to your computer and use it in GitHub Desktop.
Small Framework
var states = new WeakMap()
var remove = new WeakMap()
var setups = new WeakSet()
window.states = states
window.remove = remove
window.setups = setups
function createObserver(fn, options) {
const observer = new MutationObserver(fn);
return function (element) {
if (typeof element == 'string') element = document.querySelector(element);
try {
observer.observe(element, options);
} catch (e) {}
};
}
function ensureRunAfterDOM(fn) {
let handleDOMLoaded = Fn(fn);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', handleDOMLoaded);
} else {
handleDOMLoaded();
}
}
function watch(selector, setup_fn, parent = document, wrapper = ((fn) => (args) => fn(args))) {
const setup = (args) => {
const cleanup = () => {
remove.get(args.el).forEach((fn) => fn(args));
setups.delete(args.el);
remove.delete(args.el);
states.delete(args.el);
};
args.style = applyInlineStylesToElement(args.el)
args.cleanup = cleanup;
if (setups.has(args.el)) return;
wrapper(setup_fn)(args);
setups.add(args.el);
};
const eventNames = [
...new Set(
[
...Object.getOwnPropertyNames(document),
...Object.getOwnPropertyNames(
Object.getPrototypeOf(Object.getPrototypeOf(document))
),
...Object.getOwnPropertyNames(Object.getPrototypeOf(window)),
].filter(
(k) =>
k.startsWith('on') &&
(document[k] == null || typeof document[k] == 'function')
)
),
];
const eventDoesExists = (name = '') => {
return eventNames.includes(
'on' + name.toLowerCase().trim().replace(/^on/i, '')
);
};
let on =
(el) => (eventName, handler, attrHandlerOrOptions, attrHandlerOptions) => {
eventName = eventName.trim().toLowerCase();
if (eventDoesExists(eventName)) {
el.addEventListener(
eventName.trim().toLowerCase(),
handler,
attrHandlerOrOptions
);
return;
}
if (['attr', 'attribute'].includes(eventName)) {
let attr = handler;
createObserver(
(mutations) => {
for (let mutation of mutations) {
let shouldRun = false;
if (typeof attr == 'string') {
if (mutation.attributeName == attr) shouldRun = true;
}
if (attr instanceof RegExp) {
if (attr.test(mutation.attributeName)) shouldRun = true;
}
if (shouldRun)
attrHandlerOrOptions({ el, selector, record: mutation });
}
},
{
attributes: true,
attributeOldValue: true,
}
)(el);
return;
}
if (['text', 'textChange', 'textChanged'].includes(eventName)) {
Array.from(el.childNodes)
.filter((e) => e.nodeType === Node.TEXT_NODE && e.textContent.trim())
.forEach((_el) => {
createObserver(
(mutations) => {
for (let mutation of mutations) {
handler({ el: _el, selector, record: mutation });
}
},
{
subtree: true,
characterData: true,
characterDataOldValue: true,
}
)(_el);
});
return;
}
if (['unmount', 'remove', 'dispose', 'cleanup'].includes(eventName)) {
if (!remove.get(el)) remove.set(el, []);
remove.get(el).push(handler);
return
}
el?.addEventListener(eventName, handler, attrHandlerOrOptions)
};
document.querySelectorAll(selector).forEach((el, idx, arr) => {
let state = states.has(el) ? states.get(el) : {};
states.set(el, state);
setup({ on: on(el), state, el, idx, arr, record: {} });
});
createObserver(
(mutations) => {
for (let mutation of mutations) {
if (!mutation.target instanceof Element) continue;
if (mutation.target?.matches(selector)) {
let el = mutation.target;
if (states.has(el)) continue;
let state = states.has(el) ? states.get(el) : {};
states.set(el, state);
setup({ on: on(el), state, record: mutation, arr: [el], idx: 0, el });
}
if (mutation?.addedNodes?.length) {
let idx = -1;
for (let el of mutation.addedNodes) {
idx += 1;
if (!(mutation.target instanceof Element)) continue;
if (el?.matches?.(selector)) {
let state = states.has(el) ? states.get(el) : {};
states.set(el, state);
setup({
on: on(el),
state,
record: mutation,
arr: mutation?.addedNodes ?? [],
idx,
el,
});
}
el?.querySelectorAll?.(selector).forEach((el, idx, arr) => {
let state = states.has(el) ? states.get(el) : {};
states.set(el, state);
setup({ on: on(el), state, record: mutation, arr, idx, el });
});
}
}
if (mutation?.removedNodes?.length) {
for (let el of mutation.removedNodes) {
if (!mutation.target instanceof Element) continue;
el?.querySelectorAll?.(selector).forEach((el, idx, arr) => {
let state = states.has(el) ? states.get(el) : {};
states.set(el, state);
setup({ on: on(el), state, record: mutation, arr, idx, el });
if (remove.has(el)) {
remove
.get(el)
.forEach((fn) =>
fn({ on: on(el), state, record: mutation, arr, idx, el })
);
}
});
}
}
}
},
{
subtree: true,
childList: true,
attributes: true,
}
)(document);
}
function JS(obj) {
return JSON.stringify(obj, null, 2);
}
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
function applyInlineStylesToElement(element) {
return function (styles) {
const oldStyles = JSON.parse(JSON.stringify(element.style));
const applyStyles = (obj) => {
Object.entries(obj).forEach(([key, value]) => {
element.style[key] = value;
});
};
return {
apply: (additionalStyles) => {
applyStyles({ ...styles, ...additionalStyles });
},
revert: () => {
applyStyles(oldStyles);
},
};
};
}
function html(html) {
var template = document.createElement('template');
html = html.trim();
template.innerHTML = html;
return template.content.firstChild;
}
const find = (...args) => document.querySelector(...args);
const findAll = (...args) => Array.from(document.querySelectorAll(...args));
function convertColor(color, toSpace) {
let div = document.createElement('div');
div.style.color = `color-mix(in ${toSpace}, ${color} 100%, transparent)`;
div.style.display = 'none';
document.body.appendChild(div);
let result = window.getComputedStyle(div).color;
div.remove();
return result;
}
function setQueryParam(key, value, type = 'soft') {
const url = new URL(window.location);
url.searchParams.set(key, value);
if (type == 'hard') window.location.search = url.href;
if (type == 'soft') history.pushState(urlParams, '', url.href);
}
function getQueryParam(key) {
const urlParams = new URLSearchParams(window.location.search);
urlParams.get(key);
window.location.search = urlParams;
}
function getAllQueryParam(key) {
const urlParams = new URLSearchParams(window.location.search);
urlParams.getAll(key);
window.location.search = urlParams;
}
const Params = {
get: getQueryParam,
set: setQueryParam,
getAll: getAllQueryParam,
}
window.Params = Params
function match(regex, value) {
return String(value).match(regex)?.[0];
}
function wait(ms, fn) {
return new Promise((resolve) => {
setTimeout(() => resolve(fn().then((l) => l)), ms);
});
}
@doeixd
Copy link
Author

doeixd commented May 13, 2024

watch.d.ts

/**
 * @typedef {Object} WatchEventArgs
 * @property {HTMLElement} el - The target element
 * @property {string} selector - The selector that matched the element
 * @property {MutationRecord} [record] - MutationRecord if triggered by MutationObserver
 * @property {number} idx - Index of the element in the matching set
 * @property {NodeList} arr - NodeList of all matching elements
 * @property {Object} state - Object to store arbitrary state
 * @property {function} style - Function to apply inline styles to the element
 * @property {function} cleanup - Function to clean up event listeners and state
 * @property {(eventName: string, handler: Function, attrHandlerOrOptions?: any, attrHandlerOptions?: any) => void} on - Function to attach event listeners to the element
 */

/**
 * @typedef {Object} WatchOptions
 * @property {HTMLElement} [parent=document] - Parent element to search for matching selectors
 * @property {function} [wrapper] - Function to wrap the setup function
 */

/**
 * Function to run after the DOM is fully loaded.
 * @callback Fn
 * @param {any} [args] - Arguments to pass to the function
 * @returns {void}
 */

declare global {
  interface Window {
    states: WeakMap<HTMLElement, any>;
    remove: WeakMap<HTMLElement, Function[]>;
    setups: WeakSet<HTMLElement>;
  }
}

/**
 * Watches for elements matching a selector and runs a setup function when found.
 * @param {string} selector - The CSS selector to match
 * @param {(args: WatchEventArgs) => void} setup_fn - Function to run when a matching element is found
 * @param {WatchOptions} [options] - Additional options
 */
declare function watch(
  selector: string,
  setup_fn: (args: WatchEventArgs) => void,
  options?: WatchOptions
): void;

/**
 * Ensures a function runs after the DOM is fully loaded.
 * @param {Fn} fn - The function to run
 */
declare function ensureRunAfterDOM(fn: Fn): void;

/**
 * Creates a MutationObserver for a specific function and options.
 * @param {MutationCallback} fn - The callback function for the MutationObserver
 * @param {MutationObserverInit} options - The options for the MutationObserver
 * @returns {(element: HTMLElement | string) => void} - A function to observe an element
 */
declare function createObserver(
  fn: MutationCallback,
  options: MutationObserverInit
): (element: HTMLElement | string) => void;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment