Files
jsl/dom.js

182 lines
5.2 KiB
JavaScript

/*
* This file is part of NoccyLabs JavaScript Standard Library (JSL)
* Copyright (c) 2025, NoccyLabs. Licensed under GNU GPL v2 or later
*
*
*/
import { jsonQuery } from './json.js';
/**
* A helper class to create (and manipulate) elements in the DOM.
*
*/
class DomHelper {
/**
* Create a new element without all the fuzz.
*
* @param {string} tag The name of the HTML tag
* @param {object} attr Map of attributes to apply to the element
* @param {array|Element|string} children Children or single child/text to apply
* @returns Element
*/
create(tag, attr = {}, children = []) {
//console.debug(`create: ${tag}`, attr, children);
let el = document.createElement(tag);
this.apply(el, attr);
this.append(el, children);
return el;
};
/**
* Apply attributes from an object to an element
*
* @param {Element} tag The element to modify
* @param {object} attr Key-value pairs of attributes to apply, or if null remove
*/
apply(tag, attr) {
Object.keys(attr).forEach(key => {
if (key.match(/^on:/)) {
const ev = key.substring(3);
tag.addEventListener(ev, attr[key]);
return;
}
if (attr[key] === null) {
tag.removeAttribute(key);
} else {
tag.setAttribute(key, attr[key]);
}
});
}
/**
* Append one or more children or text nodes to an element.
*
* @param {Element} tag The element to modify
* @param {array|Element|string} children List of elements or single element to append to the element
* @returns
*/
append(tag, children) {
// Handle null, to make it easy to skip items with conditionals
if (children === null) return;
//console.debug(`append: ${tag.nodeName}`, children);
if (typeof children == 'object' && children instanceof Element) {
//console.debug(`-> appended element`);
tag.appendChild(children);
return;
}
if (typeof children == 'string' || typeof children == 'number') {
//console.debug(`-> appended string`);
let n = document.createTextNode(children);
tag.appendChild(n);
return;
}
if (Array.isArray(children)) {
children.forEach(child => {
this.append(tag, child);
});
//console.debug(`-> appended nested`);
}
}
/**
* Apply templated values to a string
*
* @param {string} str The string to perform interpolation on
* @param {object} tpl A string of key-value pair to replace between percent signs
* @returns string
*/
interpolate(str, tpl) {
Object.keys(tpl).forEach(key => {
str = str.replace(`%${key}%`, tpl[key]);
});
return str;
}
/**
* Discover parent node matching query
*
*
*/
parent(el, match) {
let m = el; // pointer
while (m.querySelector(match) === null) {
if (!m.parentNode) return false;
m = m.parentNode;
}
return m.querySelector(match);
}
/**
* Apply data to a cloned template element and return it
*
* @param {string|Element} el
* @param {*} data
*/
template(el, data) {
if (!("content" in document.createElement('template'))) {
console.error("Templates not supported on this browser.");
return;
}
const tpl = (el instanceof Element) ? el : document.querySelector(el);
const clone = tpl.content.cloneNode(true);
clone.querySelectorAll('[if]').forEach(el => {
let path = el.getAttribute('if');
let negate = false;
if (path[0] === '!') {
negate = true;
path = path.substring(1);
}
let value = false;
if (path[0] === '.') {
value = jsonQuery(data, path, null);
} else {
value = !!eval('(' + path + ')');
}
if (negate) value = !value;
if (!value) {
el.classList.add('d-none');
} else {
el.classList.remove('d-none');
}
});
clone.querySelectorAll('[template]').forEach(el => {
const path = el.getAttribute('template');
let value = jsonQuery(data, path, null);
if (value !== null) {
if ('value' in el) {
el.value = value;
} else {
el.innerText = value;
}
}
});
clone.querySelectorAll('[templated]').forEach(el => {
let value = eval(el.innerText);
if (value !== null) {
el.innerText = value;
}
});
return clone;
}
};
// The DOM helper
const dom = new DomHelper();
// Proxy for creating any supported HTML element via el.TAG
const el = new Proxy(dom, {
get(target,name) {
return name in target
? target[name]
: (attr = {}, children = []) => target.create(name, attr, children);
}
});
export { dom, el };