dom-unify
Version:
A JavaScript library for manipulating the DOM with a chainable, intuitive API. It simplifies creating, modifying, and navigating DOM elements.
413 lines (355 loc) • 12.2 kB
JavaScript
class DomUnify {
constructor(root) {
this.currentElements = this._normalizeElements(root);
this.lastAdded = [];
this.markedElements = [];
this.lastParents = [];
this.buffer = null;
this.elementHistory = [];
this._eventHandlers = new WeakMap();
this._findCache = new WeakMap();
this.markedElements.push({ elements: [...this.currentElements], name: 'root' });
}
_normalizeElements(input) {
if (!input) return [document.body];
if (typeof input === 'string') {
return Array.from(document.querySelectorAll(input)).filter(el => el instanceof HTMLElement);
}
if (input instanceof HTMLElement) return [input];
if (input instanceof Document) return [input.body];
if (NodeList.prototype.isPrototypeOf(input) || Array.isArray(input)) {
return Array.from(input).filter(el => el instanceof HTMLElement);
}
return [];
}
static safeHTMLToElements(htmlString) {
const cleaned = htmlString
.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '')
.replace(/\son\w+="[^"]*"/gi, '')
.replace(/\son\w+='[^']*'/gi, '');
const container = document.createElement('div');
container.innerHTML = cleaned;
return Array.from(container.childNodes);
}
static createElementFromConfig(config, parent) {
if (typeof config !== 'object' || config === null) return [];
const {
tag = 'div',
class: className,
id,
text,
html,
attrs = {},
styles = {},
dataset = {},
events = {},
children = []
} = config;
const el = document.createElement(tag);
if (className) el.className = className;
if (id) el.id = id;
if (text) el.textContent = text;
if (html) el.innerHTML = html;
for (const [k, v] of Object.entries(attrs)) {
if (v === false || v == null) continue;
el.setAttribute(k, v === true ? '' : v);
}
for (const [k, v] of Object.entries(styles)) {
el.style[k] = v;
}
for (const [k, v] of Object.entries(dataset)) {
el.dataset[k] = v;
}
for (const [event, handler] of Object.entries(events)) {
el.addEventListener(event, handler);
}
for (const child of children) {
if (child instanceof HTMLElement) {
el.appendChild(child);
} else if (typeof child === 'string') {
el.appendChild(document.createTextNode(child));
} else if (typeof child === 'object' && child !== null) {
const childEls = DomUnify.createElementFromConfig(child);
childEls.forEach(el.appendChild.bind(el));
}
}
if (parent) parent.appendChild(el);
return [el];
}
static addToElements(targets, config) {
const elements = [];
for (const target of targets) {
const fragment = document.createDocumentFragment();
if (typeof config === 'string') {
let parsed;
try {
parsed = JSON.parse(config);
const els = Array.isArray(parsed)
? parsed.flatMap(cfg => DomUnify.createElementFromConfig(cfg, fragment))
: DomUnify.createElementFromConfig(parsed, fragment);
elements.push(...els);
} catch {
const els = DomUnify.safeHTMLToElements(config);
els.forEach(el => fragment.appendChild(el));
elements.push(...els);
}
} else if (Array.isArray(config)) {
config.forEach(cfg => {
const els = DomUnify.createElementFromConfig(cfg, fragment);
elements.push(...els);
});
} else if (typeof config === 'object' && config !== null) {
const els = DomUnify.createElementFromConfig(config, fragment);
elements.push(...els);
}
target.appendChild(fragment);
}
return elements;
}
add(config) {
const els = DomUnify.addToElements(this.currentElements, config);
this.lastAdded = els;
return this;
}
set(props = {}) {
for (const el of this.currentElements) {
if (props.text !== undefined) el.textContent = props.text;
if (props.html !== undefined) el.innerHTML = props.html;
if (props.class !== undefined) el.className = props.class;
if (props.id !== undefined) el.id = props.id;
if (props.style) Object.assign(el.style, props.style);
if (props.attr) {
for (const key in props.attr) {
const value = props.attr[key];
if (value === false || value == null) continue;
el.setAttribute(key, value === true ? '' : value);
}
}
if (props.dataset) {
for (const key in props.dataset) {
el.dataset[key] = props.dataset[key];
}
}
}
return this;
}
copy() {
this.buffer = this.currentElements.map(el => el.cloneNode(true));
return this;
}
paste(position = 'append') {
if (!this.buffer || !this.buffer.length) return this;
this.currentElements.forEach(ctx => {
const frag = document.createDocumentFragment();
this.buffer.forEach(el => frag.appendChild(el.cloneNode(true)));
if (position === 'append' || position === undefined) {
ctx.appendChild(frag);
} else if (position === 'prepend') {
ctx.insertBefore(frag, ctx.firstChild);
} else if (typeof position === 'number') {
const children = Array.from(ctx.childNodes);
const index = position < 0 ? children.length + position : position;
const ref = children[index] || null;
ctx.insertBefore(frag, ref);
}
});
return this;
}
duplicate(position = 'append') {
this.currentElements.forEach(orig => {
const clone = orig.cloneNode(true);
const parent = orig.parentNode;
if (!parent) return;
if (position === 'prepend') {
parent.insertBefore(clone, orig);
} else {
parent.insertBefore(clone, orig.nextSibling);
}
});
return this;
}
enter(index) {
this.elementHistory.push([...this.currentElements]);
const entered = [];
for (const el of this.currentElements) {
const children = Array.from(el.children);
if (typeof index === 'number') {
const i = index >= 0 ? index : children.length + index;
if (children[i]) entered.push(children[i]);
} else if (this.lastAdded.length) {
entered.push(...this.lastAdded);
} else if (children.length) {
entered.push(...children);
}
}
this.currentElements = entered;
return this;
}
up(selector) {
this.elementHistory.push([...this.currentElements]);
const result = [];
if (selector === undefined) {
for (const el of this.currentElements) {
const parent = el.parentElement;
if (parent && !result.includes(parent)) result.push(parent);
}
} else if (typeof selector === 'number') {
const levels = Math.abs(selector);
for (const el of this.currentElements) {
let parent = el;
if (selector < 0) {
while (parent.parentElement && parent !== document.body) {
parent = parent.parentElement;
}
} else {
for (let i = 0; i < levels && parent.parentElement; i++) {
parent = parent.parentElement;
}
}
if (parent && !result.includes(parent)) result.push(parent);
}
} else {
for (const el of this.currentElements) {
const parent = el.closest(selector);
if (parent && !result.includes(parent)) result.push(parent);
}
}
this.currentElements = result;
return this;
}
back(steps = 1) {
if (this.currentElements.length === 0 && this.lastParents.length > 0) {
this.currentElements = this.lastParents;
this.lastParents = [];
return this;
}
if (this.elementHistory.length === 0) return this;
let index;
if (steps >= 0) {
index = Math.max(0, this.elementHistory.length - steps);
} else {
index = Math.max(0, Math.abs(steps) - 1);
}
if (index >= this.elementHistory.length) return this;
this.currentElements = [...this.elementHistory[index]];
this.elementHistory = this.elementHistory.slice(0, index + 1);
return this;
}
mark(name) {
if (typeof name !== 'string' || name.trim() === '') return this;
this.markedElements = this.markedElements.filter(ctx => ctx.name !== name);
this.markedElements.push({ elements: [...this.currentElements], name });
return this;
}
getMark(name) {
if (typeof name !== 'string' || name.trim() === '') return this;
const context = [...this.markedElements].reverse().find(ctx => ctx.name === name);
if (context) {
this.currentElements = [...context.elements];
}
return this;
}
delete() {
this.elementHistory.push([...this.currentElements]);
this.lastParents = this.currentElements
.map(el => el.parentElement)
.filter((el, i, arr) => el && arr.indexOf(el) === i);
for (const el of this.currentElements) el.remove();
this.currentElements = [];
return this;
}
cut() {
this.elementHistory.push([...this.currentElements]);
this.buffer = this.currentElements.map(el => el);
this.lastParents = this.currentElements
.map(el => el.parentElement)
.filter((el, i, arr) => el && arr.indexOf(el) === i);
for (const el of this.currentElements) el.remove();
this.currentElements = [];
return this;
}
find(selector) {
if (!selector) {
this.elementHistory.push([...this.currentElements]);
this.currentElements = [];
return this;
}
this.elementHistory.push([...this.currentElements]);
const results = [];
if (selector === '*') {
for (const el of this.currentElements) {
results.push(...Array.from(el.children));
}
} else {
for (const el of this.currentElements) {
let cacheForEl = this._findCache.get(el);
if (!cacheForEl) {
cacheForEl = new Map();
this._findCache.set(el, cacheForEl);
}
if (!cacheForEl.has(selector)) {
const found = Array.from(el.querySelectorAll(selector));
cacheForEl.set(selector, found);
}
results.push(...cacheForEl.get(selector));
}
}
this.currentElements = results;
return this;
}
on(event, handler, ...args) {
if (typeof event !== 'string' || !event.trim()) return this;
let resolvedHandler = handler;
if (typeof handler === 'string') {
resolvedHandler = window[handler];
if (typeof resolvedHandler !== 'function') return this;
}
if (typeof resolvedHandler !== 'function') return this;
for (const element of this.currentElements) {
let handlers = this._eventHandlers.get(element);
if (!handlers) {
handlers = {};
this._eventHandlers.set(element, handlers);
}
if (!handlers[event]) {
handlers[event] = [];
}
const wrappedHandler = (e) => resolvedHandler(...args, e);
element.addEventListener(event, wrappedHandler);
handlers[event].push({ handler: resolvedHandler, wrappedHandler });
}
return this;
}
off(event, handler) {
if (typeof event !== 'string' || !event.trim()) return this;
for (const element of this.currentElements) {
const handlers = this._eventHandlers.get(element);
if (!handlers || !handlers[event]) continue;
if (!handler) {
handlers[event].forEach(({ wrappedHandler }) => {
element.removeEventListener(event, wrappedHandler);
});
handlers[event] = [];
} else {
let resolvedHandler = handler;
if (typeof handler === 'string') {
resolvedHandler = window[handler];
if (typeof resolvedHandler !== 'function') return this;
}
if (typeof resolvedHandler !== 'function') return this;
handlers[event] = handlers[event].filter(({ handler: h, wrappedHandler }) => {
if (h === resolvedHandler) {
element.removeEventListener(event, wrappedHandler);
return false;
}
return true;
});
}
}
return this;
}
}
function dom(root) {
return new DomUnify(root);
}
export { dom };