dna-engine
Version:
An uncomplicated user interface library for cloning semantic templates
1,169 lines (1,167 loc) • 75.8 kB
JavaScript
//! dna-engine v3.2.7 ~~ https://dna-engine.org ~~ MIT License
const dnaName = {
animating: 'dna-animating',
animatingDone: 'dna-animating-done',
array: 'dna-array',
clone: 'dna-clone',
container: 'dna-container',
displayed: 'dna-displayed',
executed: 'dna-executed',
field: 'dna-field',
hidden: 'dna-hidden',
hide: 'dna-hide',
initialized: 'dna-initialized',
lastSeparator: 'dna-last-separator',
menu: 'dna-menu',
menuItem: 'dna-menu-item',
nucleotide: 'dna-nucleotide',
onLoad: 'dna-on-load',
panel: 'dna-panel',
panels: 'dna-panels',
panelsInitialized: 'dna-panels-initialized',
selected: 'dna-selected',
separator: 'dna-separator',
subClone: 'dna-sub-clone',
template: 'dna-template',
unhide: 'dna-unhide',
unselected: 'dna-unselected',
updateModel: 'dna-update-model',
};
const dnaArray = {
find: (array, value, key = 'code') => {
const find = () => array.findIndex(object => object[key] === value);
const index = Array.isArray(array) ? find() : -1;
const item = index === -1 ? null : array[index];
return { index, item };
},
fromMap(map, options) {
const defaults = { key: 'code', kebabCodes: false };
const settings = { ...defaults, ...options };
const codeValue = (key) => settings.kebabCodes ? dna.util.toKebab(key) : key;
const toObj = (item) => dna.util.isObj(item) ? item : { value: item };
return Object.keys(map).map(key => ({ ...{ [settings.key]: codeValue(key) }, ...toObj(map[key]) }));
},
toMap(array, options) {
const defaults = { key: 'code', camelKeys: false };
const settings = { ...defaults, ...options };
const map = {};
const keyName = settings.key;
const getKeyRaw = (obj) => obj[keyName];
const getKeyCamel = (obj) => dna.util.toCamel(String(obj[keyName]));
const getKey = settings.camelKeys ? getKeyCamel : getKeyRaw;
array.forEach(obj => map[getKey(obj)] = obj);
return map;
},
};
const dnaBrowser = {
userAgentData() {
const polyfill = () => {
const brandEntry = globalThis.navigator.userAgent.split(' ').pop()?.split('/') ?? [];
const hasTouch = !!navigator.maxTouchPoints;
const platform = globalThis.navigator.platform;
const mac = hasTouch ? 'iOS' : 'macOS';
const platforms = { 'MacIntel': mac, 'Win32': 'Windows', 'iPhone': 'iOS', 'iPad': 'iOS' };
return {
brands: [{ brand: brandEntry[0] ?? '', version: brandEntry[1] ?? '' }],
mobile: hasTouch || /Android|iPhone|iPad|Mobi/i.test(globalThis.navigator.userAgent),
platform: platforms[platform] ?? platform,
};
};
const uaData = globalThis.navigator['userAgentData'];
return (uaData ?? polyfill());
},
};
const dnaPageToken = {
put: (key, value) => {
globalThis.sessionStorage[key + globalThis.location.pathname] = JSON.stringify(value);
return value;
},
get: (key, defaultValue) => {
const pageKey = key + globalThis.location.pathname;
const value = globalThis.sessionStorage[pageKey];
return value === undefined ? defaultValue : JSON.parse(value);
},
};
const dnaDom = {
stateDepot: [],
state(elem) {
dna.core.assert(dna.dom.isElem(elem), 'Invalid element for getting state', elem);
const data = elem.dataset;
elem.classList.add('dna-state');
if (!data.dnaState)
data.dnaState = String(dna.dom.stateDepot.push({}) - 1);
return dna.dom.stateDepot[Number(data.dnaState)];
},
componentState(elem) {
const component = dna.ui.getComponent(elem);
dna.core.assert(component, 'Component not found for element', elem);
return dna.dom.state(component);
},
cloneState(clone) {
dna.core.assert(dna.dom.isElem(clone), 'Invalid element for copying state', clone);
const copy = (elem) => {
const data = elem.dataset;
const newState = { ...dna.dom.stateDepot[Number(data.dnaState)] };
data.dnaState = String(dna.dom.stateDepot.push(newState) - 1);
};
if (clone.classList.contains('dna-state'))
copy(clone);
dna.dom.forEach(clone.getElementsByClassName('dna-state'), copy);
return clone;
},
removeState(elem) {
dna.core.assert(dna.dom.isElem(elem), 'Invalid element for removing state', elem);
const data = elem.dataset;
if (data.dnaState)
dna.dom.stateDepot[Number(data.dnaState)] = {};
return elem;
},
createCustom(tag, options) {
const elem = globalThis.document.createElement(tag);
if (options?.id)
elem.id = options.id;
if (options?.class)
elem.classList.add(options.class);
if (options?.href)
elem.href = options.href;
if (options?.html)
elem.innerHTML = options.html;
if (options?.name)
elem.name = options.name;
if (options?.rel)
elem.rel = options.rel;
if (options?.src)
elem.src = options.src;
if (options?.text)
elem.textContent = options.text;
if (options?.type)
elem.type = options.type;
if (options?.subTags)
options.subTags.forEach(subTag => elem.appendChild(globalThis.document.createElement(subTag)));
return elem;
},
create(tag, options) {
const elem = globalThis.document.createElement(tag);
if (options?.id)
elem.id = options.id;
if (options?.class)
elem.classList.add(options.class);
if (options?.href)
elem.href = options.href;
if (options?.html)
elem.innerHTML = options.html;
if (options?.name)
elem.name = options.name;
if (options?.rel)
elem.rel = options.rel;
if (options?.src)
elem.src = options.src;
if (options?.text)
elem.textContent = options.text;
if (options?.type)
elem.type = options.type;
if (options?.subTags)
options.subTags.forEach(subTag => elem.appendChild(globalThis.document.createElement(subTag)));
return elem;
},
hasClass(elems, className) {
const elemHasClass = (elem) => elem.classList.contains(className);
return Array.prototype.some.call(elems, elemHasClass);
},
toggleClass(elem, className, state) {
if (state === undefined ? !elem.classList.contains(className) : state)
elem.classList.add(className);
else
elem.classList.remove(className);
return elem;
},
replaceClass(elem, oldName, newName) {
elem.classList.remove(oldName);
elem.classList.add(newName);
return elem;
},
addClass(elems, className) {
const addClass = (elem) => elem.classList.add(className);
Array.prototype.forEach.call(elems, addClass);
return elems;
},
forEach(elems, fn) {
Array.prototype.forEach.call(elems, fn);
return elems;
},
map(elems, fn) {
return Array.prototype.map.call(elems, fn);
},
filter(elems, fn) {
return Array.prototype.filter.call(elems, fn);
},
filterBySelector(elems, selector) {
const elemMatches = (elem) => elem.matches(selector);
return Array.prototype.filter.call(elems, elemMatches);
},
filterByClass(elems, ...classNames) {
const hasClass = (elem) => elem.classList.contains(classNames[0]);
const filtered = Array.prototype.filter.call(elems, hasClass);
const filterMore = () => dna.dom.filterByClass(filtered, ...classNames.splice(1));
return classNames.length === 1 ? filtered : filterMore();
},
find(elems, fn) {
return Array.prototype.find.call(elems, fn) ?? null;
},
index(elem) {
return Array.prototype.indexOf.call(elem.parentElement.children, elem);
},
indexOf(elems, elem) {
return Array.prototype.indexOf.call(elems, elem);
},
findIndex(elems, selector) {
const elemMatches = (elem) => elem.matches(selector);
return Array.prototype.findIndex.call(elems, elemMatches);
},
insertAt(container, elem, index) {
const inbounds = index >= 0 && index <= container.children.length;
dna.core.assert(inbounds, 'Invalid index to insert element', index);
if (index === 0)
container.prepend(elem);
else
container.children[index - 1].insertAdjacentElement('afterend', elem);
return elem;
},
isElem(elem) {
return !!elem && typeof elem === 'object' && !!elem.nodeName;
},
getAttrs(elem) {
return elem ? Object.values(elem.attributes) : [];
},
toElem(elemOrEvent) {
return (dna.dom.isElem(elemOrEvent) ? elemOrEvent : elemOrEvent.target);
},
on(type, listener, options) {
const defaults = { keyFilter: null, selector: null, container: null };
const settings = { ...defaults, ...options };
const noFilter = !settings.keyFilter;
const noSelector = !settings.selector;
const noContainer = !settings.container;
const delegator = (event) => {
const target = event.target;
const elem = !target || noSelector ? target : target.closest(settings.selector);
const expectedKey = () => noFilter || settings.keyFilter === event.key;
const expectedElem = () => noContainer || settings.container.contains(target);
if (elem && expectedKey() && expectedElem())
listener(elem, event, settings.selector);
};
globalThis.document.addEventListener(type, delegator);
},
onClick(listener, selector) {
dna.dom.on('click', listener, { selector: selector ?? null });
},
onChange(listener, selector) {
dna.dom.on('change', listener, { selector: selector ?? null });
},
onInput(listener, selector) {
dna.dom.on('input', listener, { selector: selector ?? null });
},
onKeyDown(listener, selector) {
dna.dom.on('keydown', listener, { selector: selector ?? null });
},
onKeyUp(listener, selector) {
dna.dom.on('keyup', listener, { selector: selector ?? null });
},
onEnterKey(listener, selector) {
dna.dom.on('keyup', listener, { selector: selector ?? null, keyFilter: 'Enter' });
},
onFocusIn(listener, selector) {
dna.dom.on('focusin', listener, { selector: selector ?? null });
},
onFocusOut(listener, selector) {
dna.dom.on('focusout', listener, { selector: selector ?? null });
},
onCut(listener, selector) {
dna.dom.on('cut', listener, { selector: selector ?? null });
},
onPaste(listener, selector) {
dna.dom.on('paste', listener, { selector: selector ?? null });
},
onTouchStart(listener, selector) {
dna.dom.on('touchstart', listener, { selector: selector ?? null });
},
onTouchEnd(listener, selector) {
dna.dom.on('touchend', listener, { selector: selector ?? null });
},
onSubmit(listener, selector) {
dna.dom.on('submit', listener, { selector: selector ?? null });
},
onHoverIn(listener, selector) {
let ready = true;
const delegator = (event) => {
const target = event.target;
const elem = target?.closest(selector);
if (elem && ready)
listener(elem, event, selector);
ready = elem === null;
};
globalThis.document.addEventListener('pointerover', delegator);
},
onHoverOut(listener, selector) {
let ready = false;
let prevTarget = null;
const delegator = (event) => {
const target = event.target;
const elem = target?.closest(selector);
prevTarget = elem ?? prevTarget;
if (elem === null && ready)
listener(prevTarget, event, selector);
ready = elem !== null;
};
globalThis.document.addEventListener('pointerover', delegator);
},
onReady(callback, options) {
const browserless = !globalThis.document;
const state = browserless ? 'browserless' : globalThis.document.readyState;
const message = 'loaded into browserless context -- DOM status interactive';
if (browserless && !options?.quiet)
console.log(dna.util.timestampMsec(), `[dna-engine] ${message}`);
if (['complete', 'browserless'].includes(state))
callback();
else
globalThis.window.addEventListener('DOMContentLoaded', callback);
return state;
},
triggerChange(elem, delay) {
const event = new Event('change', { bubbles: true });
if (delay)
globalThis.setTimeout(() => elem.dispatchEvent(event), delay);
else
elem.dispatchEvent(event);
return event;
},
};
const dnaUi = {
isHidden(elem) {
const computed = globalThis.getComputedStyle(elem);
return computed.display === 'none' || computed.visibility === 'hidden' ||
computed.visibility === 'collapse' || computed.opacity === '0' || elem.clientHeight === 0;
},
isVisible(elem) {
return !dna.ui.isHidden(elem);
},
show(elem) {
const style = elem.style;
style.removeProperty('display');
style.removeProperty('opacity');
style.removeProperty('visibility');
const computed = globalThis.getComputedStyle(elem);
const override = (prop, values, standIn) => values.includes(computed.getPropertyValue(prop)) && style.setProperty(prop, standIn);
override('display', ['none'], 'block');
override('opacity', ['0'], '1');
override('visibility', ['collapse', 'hidden'], 'visible');
return elem;
},
hide(elem) {
elem.style.display = 'none';
return elem;
},
toggle(elem, display) {
return display ?? dna.ui.isHidden(elem) ? dna.ui.show(elem) : dna.ui.hide(elem);
},
fadeIn(elem, options) {
const duration = options?.duration ?? 600;
const computed = globalThis.getComputedStyle(elem);
const startOpacity = options?.reset || dna.ui.isHidden(elem) ? '0' : computed.opacity;
dna.ui.show(elem);
const style = elem.style;
style.transition = 'all 0ms';
style.opacity = startOpacity;
const animate = () => {
style.transition = `all ${duration}ms`;
style.opacity = '1';
};
globalThis.requestAnimationFrame(animate);
const cleanup = () => {
style.removeProperty('transition');
style.removeProperty('opacity');
dna.ui.show(elem);
return elem;
};
return new Promise(resolve => globalThis.setTimeout(() => resolve(cleanup()), duration + 100));
},
fadeOut(elem, options) {
const duration = options?.duration ?? 600;
const style = elem.style;
style.transition = 'all 0ms';
style.opacity = globalThis.getComputedStyle(elem).opacity;
const animate = () => {
style.transition = `all ${duration}ms`;
style.opacity = '0';
};
if (dna.ui.isVisible(elem))
globalThis.requestAnimationFrame(animate);
const cleanup = () => {
style.removeProperty('transition');
style.opacity = '0';
return elem;
};
return new Promise(resolve => globalThis.setTimeout(() => resolve(cleanup()), duration + 100));
},
slideFadeIn(elem, options) {
const duration = options?.duration ?? 600;
const reset = options?.reset ?? false;
const style = elem.style;
const verticals = [
'height',
'border-top-width',
'border-bottom-width',
'padding-top',
'padding-bottom',
'margin-top',
'margin-bottom',
];
const start = () => {
dna.ui.show(elem);
style.transition = 'all 0ms';
style.opacity = '0';
style.overflow = 'hidden';
const computed = globalThis.getComputedStyle(elem);
const heights = verticals.map(prop => computed.getPropertyValue(prop));
verticals.forEach(prop => style.setProperty(prop, '0px'));
const animate = () => {
style.transition = `all ${duration}ms`;
style.opacity = '1';
verticals.forEach((prop, i) => style.setProperty(prop, heights[i]));
};
globalThis.requestAnimationFrame(animate);
};
if (reset || dna.ui.isHidden(elem))
start();
const cleanup = () => {
style.removeProperty('transition');
style.removeProperty('opacity');
style.removeProperty('overflow');
verticals.forEach((prop) => style.removeProperty(prop));
dna.ui.show(elem);
return elem;
};
return new Promise(resolve => globalThis.setTimeout(() => resolve(cleanup()), duration + 100));
},
slideFadeOut(elem, options) {
const duration = options?.duration ?? 600;
const computed = globalThis.getComputedStyle(elem);
const style = elem.style;
style.transition = `all ${duration}ms`;
style.opacity = String(Math.min(1, Number(computed.getPropertyValue('opacity'))));
style.overflow = 'hidden';
const verticals = [
'height',
'border-top-width',
'border-bottom-width',
'padding-top',
'padding-bottom',
'margin-top',
'margin-bottom',
];
const heights = verticals.map(prop => computed.getPropertyValue(prop));
verticals.forEach((prop, i) => style.setProperty(prop, heights[i]));
const animate = () => {
style.opacity = '0';
verticals.forEach(prop => style.setProperty(prop, '0px'));
};
globalThis.requestAnimationFrame(animate);
const cleanup = () => {
style.display = 'none';
style.removeProperty('transition');
style.removeProperty('opacity');
style.removeProperty('overflow');
verticals.forEach((prop) => style.removeProperty(prop));
return elem;
};
return new Promise(resolve => globalThis.setTimeout(() => resolve(cleanup()), duration + 100));
},
slideFade(elem, show) {
return show ? dna.ui.slideFadeIn(elem) : dna.ui.slideFadeOut(elem);
},
slideFadeDelete(elem) {
return dna.ui.slideFadeOut(elem).then(dna.core.remove);
},
smoothHeight(updateUI, options) {
const defaults = {
container: globalThis.document.body,
overflow: true,
duration: 1000,
};
const settings = { ...defaults, ...options };
const container = settings.container;
const style = container.style;
const setBaseline = () => {
const height = String(container.clientHeight) + 'px';
style.minHeight = height;
style.maxHeight = height;
if (settings.overflow)
style.overflow = 'hidden';
container.classList.add(dna.name.animating);
};
const animate = () => {
const turnOffTransition = () => {
style.transition = 'none';
style.maxHeight = 'none';
container.classList.remove(dna.name.animating);
};
const animate = () => {
style.minHeight = '0px';
style.maxHeight = '100vh';
globalThis.setTimeout(turnOffTransition, 1000);
};
const setAnimationLength = () => {
style.transition = `all ${settings.duration}ms`;
globalThis.requestAnimationFrame(animate);
};
globalThis.requestAnimationFrame(setAnimationLength);
};
const cleanup = () => {
container.classList.replace(dna.name.animating, dna.name.animatingDone);
return container;
};
setBaseline();
updateUI();
animate();
const delay = settings.duration + 100;
return new Promise(resolve => globalThis.setTimeout(() => resolve(cleanup()), delay));
},
smoothMove(elem, up) {
const blockingElem = up ? elem.previousElementSibling : elem.nextElementSibling;
const move = () => {
const submissiveElem = blockingElem;
const ghostElem = submissiveElem.cloneNode(true);
submissiveElem.style.display = 'none';
elem.parentElement.insertBefore(ghostElem, submissiveElem);
elem.parentElement.insertBefore(up ? elem : submissiveElem, up ? submissiveElem : elem);
const animate = () => {
dna.ui.slideFadeIn(submissiveElem);
return dna.ui.slideFadeDelete(ghostElem).then(() => elem);
};
return new Promise(resolve => globalThis.requestAnimationFrame(() => resolve(animate())));
};
return blockingElem ? move() : new Promise(resolve => resolve(elem));
},
smoothMoveUp(elem) {
return dna.ui.smoothMove(elem, true);
},
smoothMoveDown(elem) {
return dna.ui.smoothMove(elem, false);
},
pulse(elem, options) {
const defaults = {
duration: 7000,
durationIn: 600,
durationOut: 3000,
noFadeOut: false,
text: null,
};
const settings = { ...defaults, ...options };
dna.core.assert(dna.dom.isElem(elem), 'Invalid element for dna.ui.pulse()', elem);
const data = elem.dataset;
const pulseStart = String(Date.now());
data.dnaPulseStart = pulseStart;
const isLastPulse = () => data.dnaPulseStart === pulseStart;
const style = elem.style;
style.transition = 'all 0ms';
style.opacity = '0';
if (settings.text !== null)
elem.textContent = settings.text;
const animate = () => {
style.transition = `all ${settings.durationIn}ms`;
style.opacity = '1';
};
const fadeAway = () => {
style.transition = `all ${settings.durationOut}ms`;
if (isLastPulse())
style.opacity = '0';
};
if (elem.clientHeight === 0)
dna.ui.slideFadeIn(elem, { duration: settings.durationIn });
else
globalThis.requestAnimationFrame(animate);
if (!settings.noFadeOut)
globalThis.setTimeout(fadeAway, settings.durationIn + settings.duration);
const cleanup = () => {
if (isLastPulse())
style.removeProperty('transition');
return elem;
};
const total = settings.durationIn +
(settings.noFadeOut ? 0 : settings.duration + settings.durationOut);
return new Promise(resolve => globalThis.setTimeout(() => resolve(cleanup()), total + 100));
},
focus(elem, options) {
const input = options?.firstInput ? elem.querySelector('input') : elem;
globalThis.requestAnimationFrame(() => input?.focus());
return elem;
},
setText(elem, text) {
if (elem)
elem.textContent = text;
return elem;
},
toClone(elemOrEvent) {
const elem = elemOrEvent instanceof Event ? elemOrEvent.target : elemOrEvent;
return dna.getClone(elem);
},
getComponent(elem) {
return elem?.closest('[data-component]') ?? null;
},
};
const dnaUtil = {
apply(fn, params) {
const callback = !fn ? null :
typeof fn === 'function' ? fn :
typeof fn === 'string' ? dna.util.getFn(fn) :
null;
dna.core.assert(callback, 'Invalid callback function', fn);
return callback(params[0], ...params.slice(1));
},
getFn(name) {
dna.core.assert(!/[^\p{Letter}\d.]/u.test(name), 'Invalid function name', name);
const fields = name.split('.');
const tag = fields[0];
const tagValue = globalThis[tag];
const toValue = (eval);
const callable = () => ['object', 'function'].includes(toValue('typeof ' + tag));
const getContext = () => dna.registerContext(tag, toValue(tag));
const getTop = () => callable() ? getContext()[tag] : undefined;
const top = tagValue ?? dna.events.db.context[tag] ?? getTop();
const deep = (object, subfields) => !subfields.length ? object :
!object ? undefined :
deep(object[subfields[0]], subfields.slice(1));
return fields.length === 1 ? top : deep(top, fields.slice(1));
},
assign(data, field, value) {
const dataObj = data && typeof data === 'object' ? data : {};
const fields = field.split('.');
const name = fields[0];
if (fields.length > 1 && !dna.util.isObj(dataObj[name]))
dataObj[name] = {};
if (fields.length === 1)
dataObj[name] = value;
else
dna.util.assign(dataObj[name], fields.slice(1).join('.'), value);
return dataObj;
},
printf: (format, ...values) => {
return values.reduce((output, value) => output.replace(/%s/, String(value)), format);
},
round(value, precision) {
return Number(value.toExponential(precision - 1));
},
realTruth: (value) => {
const falseyStr = () => /^(f|false|n|no|0)$/i.test(String(value));
const emptyArray = () => value instanceof Array && value.length === 0;
return !!value && !emptyArray() && !falseyStr();
},
toCamel: (kebabStr) => {
const hump = (match, letter) => letter.toUpperCase();
return String(kebabStr).replace(/-(.)/g, hump);
},
toKebab: (camelStr) => {
const dash = (word) => '-' + word.toLowerCase();
return ('' + camelStr).replace(/([A-Z]+)/g, dash).replace(/\s|^-/g, '');
},
value(data, field) {
const notFound = data === null || data === undefined || field === undefined;
const parts = typeof field === 'string' ? field.split('.') : field;
const fieldValue = notFound ? null : data[parts[0]];
return notFound || parts.length < 2 ? fieldValue : dna.util.value(fieldValue, parts.slice(1));
},
isObj(value) {
return !!value && typeof value === 'object' && !Array.isArray(value);
},
timestamp(date) {
return dna.format.getDateFormatter('timestamp')(date ?? Date.now());
},
timestampMsec(date) {
return dna.format.getDateFormatter('timestamp-msec')(date ?? Date.now());
},
};
const dnaFormat = {
getCurrencyFormatter(iso4217, units = 1) {
const options = {
style: 'currency',
currency: iso4217.toUpperCase(),
};
const formatter = new Intl.NumberFormat([], options).format;
return (value) => formatter(Number(value) / units);
},
getDateFormatter(format) {
const simpleDate = (date) => date.toLocaleString([], { day: 'numeric', month: 'long', year: 'numeric' });
const weekday = (date) => date.toLocaleString([], { weekday: 'long' });
const month = (date) => date.toLocaleString([], { month: 'long' });
const twoDigit = (value) => String(value).padStart(2, '0');
const timestamp = (date) => date.toISOString().replace('T', '+').slice(0, -5);
const timestampMsec = (date) => date.toISOString().replace('T', '+').slice(0, -1);
const timeZone = (date) => date.toLocaleString([], { timeZoneName: 'short' }).split(' ').pop();
const timeZoneLong = (date) => date.toLocaleString([], { timeZoneName: 'long', year: 'numeric' }).split(' ').slice(1).join(' ');
const space = (format) => format.replace(/\s/g, ' ');
const general = {
date: (date) => `${date.getFullYear()}-${twoDigit(date.getMonth() + 1)}-${twoDigit(date.getDate())}`,
time: (date) => date.toLocaleString([], { hour: 'numeric', minute: '2-digit' }).replace(/\s/, '').toLowerCase(),
weekday: (date) => date.toLocaleString([], { weekday: 'short' }),
stamp: (date) => `${general.date(date)} ${general.time(date)} ${general.weekday(date)}`,
long: (date) => `${general.stamp(date)} (${timeZone(date)})`,
};
const transformers = {
date: (date) => date.toDateString(),
general: (date) => general.stamp(date),
generalDate: (date) => general.date(date),
generalWeekday: (date) => general.weekday(date),
generalLong: (date) => general.long(date),
generalTime: (date) => general.time(date),
iso: (date) => date.toISOString(),
locale: (date) => space(date.toLocaleString()),
localeDate: (date) => date.toLocaleDateString(),
localeTime: (date) => space(date.toLocaleTimeString()),
month: (date) => month(date),
simpleDate: (date) => simpleDate(date),
string: (date) => date.toString(),
time: (date) => date.toTimeString(),
timestamp: (date) => timestamp(date),
timestampMsec: (date) => timestampMsec(date),
timeZone: (date) => timeZone(date),
timeZoneLong: (date) => timeZoneLong(date),
utc: (date) => date.toUTCString(),
weekday: (date) => weekday(date),
year: (date) => String(date.getFullYear()),
};
const transformer = transformers[dna.util.toCamel(format)];
dna.core.assert(transformer, 'Unknown date format code', format);
const formatter = (msec) => transformer(new Date(msec));
return formatter;
},
getNumberFormatter(format) {
dna.core.assert(/^#([.]#+)?$/.test(format), 'Unknown numeric format code', format);
const digits = format === '#' ? 0 : format.length - 2;
const numeric = { minimumFractionDigits: digits, maximumFractionDigits: digits };
return new Intl.NumberFormat([], numeric).format;
},
getPercentFormatter(format) {
dna.core.assert(/^#([.]#+)?$/.test(format), 'Unknown percent format code', format);
const digits = format === '#' ? 0 : format.length - 2;
const options = {
style: 'percent',
minimumFractionDigits: digits,
maximumFractionDigits: digits,
};
return new Intl.NumberFormat([], options).format;
},
getFormatter(fn) {
return (value, data) => String(dna.util.apply(fn, [value, data]));
},
};
const dnaPlaceholder = {
setup() {
const hideSelect = (elem) => elem.closest('select')?.classList.add(dna.name.hide);
globalThis.document.querySelectorAll('option.dna-template').forEach(hideSelect);
const isEmpty = (elem) => !!dna.getClones(elem.dataset.placeholder).length;
const fade = (elem) => isEmpty(elem) ? dna.ui.slideFadeOut(elem) : dna.ui.slideFadeIn(elem);
const placeholders = globalThis.document.querySelectorAll('[data-placeholder]');
placeholders.forEach(fade);
return placeholders;
},
};
const dnaPanels = {
display(menu, location, updateUrl) {
const menuData = menu.dataset;
const menuNavName = menuData.menuNav;
const callback = menuData.callback;
const panels = dna.dom.state(menu).dnaPanels;
const menuItems = menu.querySelectorAll('.dna-menu-item');
const savedIndex = Number(dna.pageToken.get(menuNavName, 0));
const bound = (loc) => Math.max(0, Math.min(loc, menuItems.length - 1));
const index = bound(location === undefined ? savedIndex : location);
const update = () => {
if (menu.nodeName === 'SELECT')
menu.selectedIndex = index;
menuItems.forEach(elem => dna.dom.replaceClass(elem, dna.name.selected, dna.name.unselected));
dna.dom.replaceClass(menuItems[index], dna.name.unselected, dna.name.selected);
const hidePanel = (panel) => {
panel.style.display = 'none';
panel.classList.remove(dna.name.displayed);
panel.classList.add(dna.name.hidden);
};
dna.dom.forEach(panels.children, hidePanel);
const panel = panels.children[index];
panel.classList.replace(dna.name.hidden, dna.name.displayed);
dna.ui.fadeIn(panel);
const hash = panel.dataset.hash;
dna.pageToken.put(menuNavName, index);
if (updateUrl && hash)
globalThis.history.pushState(null, '', '#' + hash);
if (callback)
dna.util.apply(callback, [panel, hash]);
return panel;
};
const heightTransition = 100;
const startHieght = panels.clientHeight;
const panel = update();
const endHieght = panels.clientHeight;
panels.style.transition = 'all 0ms';
panels.style.height = String(startHieght) + 'px';
const animate = () => {
panels.style.transition = `all ${heightTransition}ms`;
panels.style.height = String(endHieght) + 'px';
};
globalThis.requestAnimationFrame(animate);
globalThis.setTimeout(() => panels.style.removeProperty('height'), heightTransition + 100);
return panel;
},
clickRotate(menuItem) {
const menu = menuItem.closest(dna.selector.menu);
const index = dna.dom.indexOf(menu.querySelectorAll(dna.selector.menuItem), menuItem);
return dna.panels.display(menu, index, true);
},
selectRotate(menu) {
return dna.panels.display(menu, menu.selectedIndex, true);
},
nextMenuNav: 1,
initialize(panels) {
const generateNavName = () => {
const menuNavName = 'dna-panels-' + String(dna.panels.nextMenuNav++);
const setNavName = (elem) => elem.dataset.menuNav = menuNavName;
const menu = panels.previousElementSibling;
dna.core.assert(menu?.classList.contains('dna-menu'), 'Menu not found for panels', panels);
setNavName(menu);
setNavName(panels);
return menuNavName;
};
const init = () => {
const menuNavName = panels.dataset.menuNav || generateNavName();
const menuSelector = '.dna-menu[data-menu-nav=' + menuNavName + ']';
const menu = globalThis.document.querySelector(menuSelector);
const hash = globalThis.location.hash.replace(/[^\w-]/g, '');
const hashIndex = () => dna.dom.findIndex(panels.children, '[data-hash=' + hash + ']');
const savedIndex = () => dna.pageToken.get(menuNavName, 0);
const first = panels.children[0];
const loc = hash && first.dataset.hash ? hashIndex() : savedIndex();
dna.dom.addClass(panels.children, dna.name.panel);
panels.classList.add(dna.name.panelsInitialized);
dna.core.assert(menu, 'Menu not found for panels', menuNavName);
menu.classList.add(dna.name.panelsInitialized);
dna.dom.state(menu).dnaPanels = panels;
if (!menu.getElementsByClassName(dna.name.menuItem).length)
dna.dom.addClass(menu.children, dna.name.menuItem);
dna.panels.display(menu, loc);
};
const isInitialized = !panels || panels.classList.contains(dna.name.panelsInitialized);
if (!isInitialized && !dna.dom.hasClass(panels.children, dna.name.template))
init();
return panels;
},
setup() {
const panels = globalThis.document.querySelectorAll(dna.selector.panels);
panels.forEach(dna.panels.initialize);
dna.dom.onClick(dna.panels.clickRotate, '.dna-menu .dna-menu-item');
dna.dom.onChange(dna.panels.selectRotate, 'select.dna-menu');
return panels;
},
};
const dnaCompile = {
getRules(elem) {
const state = dna.dom.state(elem);
if (!state.dnaRules)
state.dnaRules = {};
return state.dnaRules;
},
setRule(rules, key, value) {
const settableRules = rules;
settableRules[key] = value;
return rules;
},
setElemRule(elem, key, value) {
const rules = dna.compile.getRules(elem);
rules[key] = value;
return elem;
},
regex: {
dnaField: /^[\s]*(~~|\{\{).*(~~|\}\})[\s]*$/,
dnaBasePair: /~~|{{|}}/,
dnaBasePairs: /~~|\{\{|\}\}/g,
},
setupNucleotide(elem) {
dna.compile.getRules(elem);
elem.classList.add(dna.name.nucleotide);
return elem;
},
isDnaField(node) {
const value = node.firstChild?.nodeValue;
return !!value && dna.compile.regex.dnaField.test(value);
},
addFieldClass(elem) {
const field = dna.dom.state(elem).dnaField;
const htmlCase = () => dna.util.toKebab(field).replace(/[[\]]/g, '').replace(/[.]/g, '-');
if (field)
elem.classList.add('dna-field-' + htmlCase());
return elem;
},
field(elem) {
dna.compile.setupNucleotide(elem);
const field = elem.textContent.replace(dna.compile.regex.dnaBasePairs, '').trim();
dna.dom.state(elem).dnaField = field;
dna.compile.addFieldClass(elem);
const rules = dna.compile.getRules(elem);
const setupTextarea = () => {
elem.classList.add(dna.name.updateModel);
rules.val = true;
};
if (elem.matches('textarea'))
setupTextarea();
else
rules.text = true;
while (elem.firstChild)
elem.removeChild(elem.firstChild);
elem.classList.add(dna.name.field);
return elem;
},
propsAndAttrs(elem) {
const data = elem.dataset;
const rules = dna.compile.getRules(elem);
const props = [];
const attrs = [];
const names = [];
const compileProp = (key, value) => {
names.push(key);
key = key.replace(/^data-prop-/, '').toLowerCase();
value = value.replace(dna.compile.regex.dnaBasePairs, '');
props.push(key, value);
const setupInput = () => {
elem.classList.add(dna.name.updateModel);
dna.dom.state(elem).dnaField = value;
};
if (key === 'checked' && elem.matches('input'))
setupInput();
};
const compileAttr = (key, value) => {
const parts = value.split(dna.compile.regex.dnaBasePair);
const raw = ['[index]', '[count]', '[value]'].indexOf(parts[1]);
if (raw !== -1)
parts[1] = raw;
attrs.push(key.replace(/^data-attr-/, ''), parts);
names.push(key);
const makeUpdatable = () => {
dna.compile.setupNucleotide(elem).classList.add(dna.name.updateModel);
dna.dom.state(elem).dnaField = parts[1];
rules.val = true;
};
const hasTextVal = elem.matches('input:not([type=checkbox]):not([type=radio])') &&
key === 'value' && parts[0] === '' && parts[2] === '';
if (hasTextVal || elem.matches('select') && key === 'data-option')
makeUpdatable();
};
const compile = (attr) => {
if (/^data-prop-/.test(attr.name))
compileProp(attr.name, attr.value);
else if (attr.value.split(dna.compile.regex.dnaBasePair).length === 3)
compileAttr(attr.name, attr.value);
};
dna.dom.getAttrs(elem).forEach(compile);
const getRules = () => dna.compile.getRules(dna.compile.setupNucleotide(elem));
if (props.length > 0)
getRules().props = props;
if (attrs.length > 0)
getRules().attrs = attrs;
if (data.precision)
getRules().precision = Number(data.precision);
if (data.formatCurrency)
getRules().formatter = dnaFormat.getCurrencyFormatter(data.formatCurrency);
if (data.formatCurrency10)
getRules().formatter = dnaFormat.getCurrencyFormatter(data.formatCurrency10, 100);
if (data.formatCurrency100)
getRules().formatter = dnaFormat.getCurrencyFormatter(data.formatCurrency100, 100);
if (data.formatCurrency1000)
getRules().formatter = dnaFormat.getCurrencyFormatter(data.formatCurrency1000, 1000);
if (data.formatCurrency10000)
getRules().formatter = dnaFormat.getCurrencyFormatter(data.formatCurrency10000, 10000);
if (data.formatDate)
getRules().formatter = dnaFormat.getDateFormatter(data.formatDate);
if (data.formatNumber)
getRules().formatter = dnaFormat.getNumberFormatter(data.formatNumber);
if (data.formatPercent)
getRules().formatter = dnaFormat.getPercentFormatter(data.formatPercent);
if (data.format)
getRules().formatter = dnaFormat.getFormatter(data.format);
if (data.transform)
getRules().transform = data.transform;
if (data.callback)
getRules().callback = data.callback;
dna.compile.addFieldClass(elem);
names.forEach(name => elem.removeAttribute(name));
return elem;
},
getDataField(elem, type) {
return elem.dataset[type].replace(dna.compile.regex.dnaBasePairs, '').trim();
},
subTemplateName(holder, arrayField, index) {
const getRules = () => dna.compile.getRules(dna.getClone(holder, { main: true }));
const templateName = typeof holder === 'string' ? holder : getRules().template;
return `${templateName}-${arrayField}--${index}`;
},
rules(elem, type, isLists = false, className, init) {
const addRule = (subElem) => {
dna.compile.setupNucleotide(subElem);
const field = dna.compile.getDataField(subElem, type);
const makeLists = () => field.split(';').map(list => list.replace(']', '').split(/[[,]/));
dna.compile.setElemRule(subElem, type, isLists ? makeLists() : field);
if (className)
subElem.classList.add(className);
if (init)
init(subElem);
subElem.removeAttribute('data-' + type);
};
const selector = '[data-' + type + ']';
if (elem.matches(selector))
addRule(elem);
elem.querySelectorAll(selector).forEach(addRule);
return elem;
},
separators(elem) {
const isWhitespaceNode = (node) => node.nodeType === Node.TEXT_NODE && !/\S/.test(node.nodeValue);
const append = (elem, text, className) => {
const lastChildNode = elem.childNodes[elem.childNodes.length - 1];
if (lastChildNode && isWhitespaceNode(lastChildNode))
lastChildNode.remove();
elem.appendChild(dna.dom.create('span', { class: className, html: text }));
};
const processTemplate = (elem) => {
const data = elem.dataset;
if (data.separator)
append(elem, data.separator, dna.name.separator);
if (data.lastSeparator)
append(elem, data.lastSeparator, dna.name.lastSeparator);
};
const selector = '.dna-template, .dna-sub-clone';
processTemplate(elem);
elem.querySelectorAll(selector).forEach(processTemplate);
return elem;
},
template(name) {
const elem = globalThis.document.getElementById(name);
dna.core.assert(elem, 'Template not found', name);
const initSubs = (elem) => dna.compile.setElemRule(elem, 'subs', []);
const saveName = (elem) => {
dna.dom.state(elem).dnaRules = { template: elem.id, subs: [] };
elem.removeAttribute('id');
initSubs(elem);
return elem;
};
saveName(elem);
dna.dom.forEach(elem.getElementsByClassName(dna.name.template), saveName);
const subElems = elem.querySelectorAll('*');
const fieldElems = dna.dom.filter(subElems, dna.compile.isDnaField);
if (dna.compile.isDnaField(elem))
dna.compile.field(elem);
fieldElems.forEach(dna.compile.field);
dna.compile.rules(elem, 'array', false, dna.name.subClone, initSubs);
dna.compile.rules(elem, 'class', true);
dna.compile.rules(elem, 'require');
dna.compile.rules(elem, 'missing');
dna.compile.rules(elem, 'true');
dna.compile.rules(elem, 'false');
dna.compile.propsAndAttrs(elem);
subElems.forEach(dna.compile.propsAndAttrs);
dna.compile.separators(elem);
const setTypeAttr = (inputElem) => inputElem.type = inputElem.dataset.attrType;
globalThis.document.querySelectorAll('input[data-attr-type]').forEach(setTypeAttr);
return dna.template.stash(elem);
},
};
const dnaTemplate = {
db: {},
stash(elem) {
const name = dna.compile.getRules(elem).template;
const move = (subElem) => {
const name = dna.compile.getRules(subElem).template;
const container = subElem.parentElement;
const containerState = dna.dom.state(container);
const wrapped = container.children.length === 1 && !container.classList.contains(dna.name.container);
const compileSiblings = () => {
containerState.dnaContents = true;
const templateName = (sibling) => {
const compileToName = (id) => id ? dna.compile.template(id).name : name;
const classes = sibling.classList;
return classes.contains(dna.name.template) ? compileToName(sibling.id) :
classes.contains(dna.name.subClone) ? dna.compile.getRules(sibling).template :
false;
};
containerState.dnaContents = dna.dom.map(container.children, templateName);
};
if (!wrapped && !containerState.dnaContents)
compileSiblings();
container.classList.add(dna.name.container, 'dna-contains-' + name);
const template = {
name: name,
elem: subElem,
container: container,
nested: container.closest(dna.selector.clone) !== null,
separators: subElem.querySelectorAll('.dna-separator, .dna-last-separator').length,
wrapped: wrapped,
};
dna.template.db[name] = template;
subElem.classList.remove(dna.name.template);
subElem.classList.add(dna.name.clone, name);
subElem.remove();
};
const prepLoop = (subElem) => {
const rules = dna.compile.getRules(subElem);
const parent = dna.compile.setupNucleotide(subElem.parentElement);
const parentRules = dna.compile.getRules(parent);
const containerRules = dna.compile.getRules(parent.closest('.dna-clone, .dna-sub-clone'));
const index = containerRules.subs.length;
parent.classList.add(dna.name.array);
rules.template = dna.compile.subTemplateName(name, rules.array, index);
parentRules.loop = { name: rules.template, field: rules.array };
containerRules.subs.push(rules.array);
};
move(elem);
elem.querySelectorAll(dna.selector.template).forEach(move);
const subClones = elem.querySelectorAll(dna.selector.subClone);
subClones.forEach(prepLoop);
subClones.forEach(move);
return dna.template.db[name];
},
get(name) {
return dna.template.db[name] || dna.compile.template(name);
},
};
const dnaEvents = {
db: {
context: {},
initializers: [],
},
runOnLoads(options) {
const defaults = { pollInterval: 300 };
const settings = { ...defaults, ...options };
const elems = globalThis.document.querySelectorAll(`[data-on-load]:not(.${dna.name.onLoad})`);
elems.forEach(elem => elem.classList.add(dna.name.onLoad));
const addStart = (elem) => dna.dom.state(elem).dnaOnLoad = { start: Date.now(), checks: 0 };
elems.forEach(addStart);
const runOnLoad = (elem) => {
const data = elem.dataset;
const fnName = data.onLoad;
const fn = dna.util.getFn(fnName);
const onLoad = dna.dom.state(elem).dnaOnLoad;
const waitFor = data.waitFor?.split(',') ?? [];
onLoad.waiting = Date.now() - onLoad.start;
onLoad.checks++;
dna.core.assert(typeof fn === 'function' || !fn, 'Invalid data-on-load function', fnName);
const run = () => {
elem.classList.add(dna.name.executed);
dna.util.apply(fnName, [elem, dna.ui.getComponent(elem)]);
};
if (fn && !waitFor.map(dna.util.getFn).includes(undefined))
run();
else
globalThis.setTimeout(() => runOnLoad(elem), settings.pollInterval);
};
elems.forEach(runOnLoad);
return elems;
},
runInitializers(root) {
const init = (initializer) => {
const initElem = (elem) => {
elem.classList.add(dna.name.initialized);