uhtml
Version:
A micro HTML/SVG render
284 lines (263 loc) • 6.15 kB
JavaScript
;
const udomdiff = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('udomdiff'));
const { empty, gPD, isArray, set } = require('./utils.js');
const { diffFragment } = require('./persistent-fragment.js');
const drop = (require('./range.js'));
const setAttribute = (element, name, value) =>
element.setAttribute(name, value);
/**
* @param {Element} element
* @param {string} name
* @returns {void}
*/
const removeAttribute = (element, name) =>
element.removeAttribute(name);
exports.removeAttribute = removeAttribute;
/**
* @template T
* @param {Element} element
* @param {T} value
* @returns {T}
*/
const aria = (element, value) => {
for (const key in value) {
const $ = value[key];
const name = key === 'role' ? key : `aria-${key}`;
if ($ == null) removeAttribute(element, name);
else setAttribute(element, name, $);
}
return value;
};
exports.aria = aria;
let listeners;
/**
* @template T
* @param {Element} element
* @param {T} value
* @param {string} name
* @returns {T}
*/
const at = (element, value, name) => {
name = name.slice(1);
if (!listeners) listeners = new WeakMap;
const known = listeners.get(element) || set(listeners, element, {});
let current = known[name];
if (current && current[0]) element.removeEventListener(name, ...current);
current = isArray(value) ? value : [value, false];
known[name] = current;
if (current[0]) element.addEventListener(name, ...current);
return value;
};
exports.at = at;
/** @type {WeakMap<Node, Element | import("./persistent-fragment.js").PersistentFragment>} */
const holes = new WeakMap;
/**
* @template T
* @param {import("./literals.js").Detail} detail
* @param {T} value
* @returns {T}
*/
const hole = (detail, value) => {
const { t: node, n: hole } = detail;
let nullish = false;
switch (typeof value) {
case 'object':
if (value !== null) {
(hole || node).replaceWith((detail.n = value.valueOf()));
break;
}
case 'undefined':
nullish = true;
default:
node.data = nullish ? '' : value;
if (hole) {
detail.n = null;
hole.replaceWith(node);
}
break;
}
return value;
};
exports.hole = hole;
/**
* @template T
* @param {Element} element
* @param {T} value
* @returns {T}
*/
const className = (element, value) => maybeDirect(
element, value, value == null ? 'class' : 'className'
);
exports.className = className;
/**
* @template T
* @param {Element} element
* @param {T} value
* @returns {T}
*/
const data = (element, value) => {
const { dataset } = element;
for (const key in value) {
if (value[key] == null) delete dataset[key];
else dataset[key] = value[key];
}
return value;
};
exports.data = data;
/**
* @template T
* @param {Element | CSSStyleDeclaration} ref
* @param {T} value
* @param {string} name
* @returns {T}
*/
const direct = (ref, value, name) => (ref[name] = value);
exports.direct = direct;
/**
* @template T
* @param {Element} element
* @param {T} value
* @param {string} name
* @returns {T}
*/
const dot = (element, value, name) => direct(element, value, name.slice(1));
exports.dot = dot;
/**
* @template T
* @param {Element} element
* @param {T} value
* @param {string} name
* @returns {T}
*/
const maybeDirect = (element, value, name) => (
value == null ?
(removeAttribute(element, name), value) :
direct(element, value, name)
);
exports.maybeDirect = maybeDirect;
/**
* @template T
* @param {Element} element
* @param {T} value
* @returns {T}
*/
const ref = (element, value) => (
(typeof value === 'function' ?
value(element) : (value.current = element)),
value
);
exports.ref = ref;
/**
* @template T
* @param {Element} element
* @param {T} value
* @param {string} name
* @returns {T}
*/
const regular = (element, value, name) => (
(value == null ?
removeAttribute(element, name) :
setAttribute(element, name, value)),
value
);
/**
* @template T
* @param {Element} element
* @param {T} value
* @returns {T}
*/
const style = (element, value) => (
value == null ?
maybeDirect(element, value, 'style') :
direct(element.style, value, 'cssText')
);
exports.style = style;
/**
* @template T
* @param {Element} element
* @param {T} value
* @param {string} name
* @returns {T}
*/
const toggle = (element, value, name) => (
element.toggleAttribute(name.slice(1), value),
value
);
exports.toggle = toggle;
/**
* @param {Node} node
* @param {Node[]} value
* @param {string} _
* @param {Node[]} prev
* @returns {Node[]}
*/
const array = (node, value, prev) => {
// normal diff
const { length } = value;
node.data = `[${length}]`;
if (length)
return udomdiff(node.parentNode, prev, value, diffFragment, node);
/* c8 ignore start */
switch (prev.length) {
case 1:
prev[0].remove();
case 0:
break;
default:
drop(
diffFragment(prev[0], 0),
diffFragment(prev.at(-1), -0),
false
);
break;
}
/* c8 ignore stop */
return empty;
};
exports.array = array;
const attr = new Map([
['aria', aria],
['class', className],
['data', data],
['ref', ref],
['style', style],
]);
exports.attr = attr;
/**
* @param {HTMLElement | SVGElement} element
* @param {string} name
* @param {boolean} svg
* @returns
*/
const attribute = (element, name, svg) => {
switch (name[0]) {
case '.': return dot;
case '?': return toggle;
case '@': return at;
default: return (
svg || ('ownerSVGElement' in element) ?
(name === 'ref' ? ref : regular) :
(attr.get(name) || (
name in element ?
(name.startsWith('on') ?
direct :
(gPD(element, name)?.set ? maybeDirect : regular)
) :
regular
)
)
);
}
};
exports.attribute = attribute;
/**
* @template T
* @param {Element} element
* @param {T} value
* @returns {T}
*/
const text = (element, value) => (
(element.textContent = value == null ? '' : value),
value
);
exports.text = text;