182 lines
5.2 KiB
JavaScript
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 };
|