@anywhichway/lazui
Version:
Single page apps and lazy loading sites with minimal JavaScript or client build processes.
880 lines (852 loc) • 48.8 kB
JavaScript
(async () => {
const responseOrRequestAsObject = async (value) => {
if(typeof value === "string") value = new Request(value);
else value = value.clone();
const object = {};
for(const key in value) {
if(typeof value[key] === "function" || key==="signal") continue;
if(key==="headers") {
object.headers = {};
value.headers.forEach((value,key)=> {
object.headers[key] = value;
});
} else if(!key.startsWith("body")) {
object[key] = value[key];
}
}
if(!["GET","HEAD","DELETE"].includes(value.method)) object.body = await value.text();
return object;
}
if(document?.currentScript && !window.lazuiLoaded) {
const url = new URL(document.currentScript.src),
script = document.currentScript;
document.addEventListener("DOMContentLoaded",async (event) => {
if(window.lazuiLoaded ||!document.body) return;
window.lazuiLoaded = true;
lazui.url = new URL(url.href);
lazui.url.hash = "";
lazui.url.search = "";
directiveExports.url = lazui.url;
if(script.hasAttribute(`${directiveExports.prefix}:usedefaults`)) await handleDirective(script.attributes[`${directiveExports.prefix}:usedefaults`]);
if(script.hasAttribute(`${directiveExports.prefix}:usejson`)) await handleDirective(script.attributes[`${directiveExports.prefix}:usejson`]);
if(script.hasAttribute(`${directiveExports.prefix}:userouter`)) await handleDirective(script.attributes[`${directiveExports.prefix}:userouter`]);
for (const state of [...document.querySelectorAll(`[${directiveExports.prefix}\\:state],[${directiveExports.prefix}\\:state\\:global],[${directiveExports.prefix}\\:state\\:document]`)]) {
for (const attr of [...state.attributes]) {
if (attr.name.startsWith(`${directiveExports.prefix}:state`)) await handleDirective(attr)
}
}
for(const customElement of [...document.querySelectorAll(`[${directiveExports.prefix}\\:tagname]`)]) {
await handleDirective(customElement.attributes[`${directiveExports.prefix}:tagname`]);
}
if(script.hasAttribute(`${directiveExports.prefix}:usehighlighter`)) await handleDirective(script.attributes[`${directiveExports.prefix}:usehighlighter`]);
if(script.hasAttribute("autofocus")) await resolve(document.body,{root:document,state:{}});
document.documentElement.removeAttribute("hidden");
if(typeof resizeFrame !== "undefined") setTimeout(() => resizeFrame(document));
const namespace = directiveExports.prefix.split("-").pop();
document.dispatchEvent(new CustomEvent(`${namespace}:loaded`));
})
}
const useDirectives = (namespace, ...directives) => {
const space = _DIRECTIVES_[namespace] ||= {};
directives.forEach((directive) => {
if (typeof directive === "function") {
const name = directive.name;
if (name) space[name] = directive;
} else if (typeof directive === "object") {
Object.assign(space, directive);
}
})
}
const usePrefix = (prefix) => directiveExports.prefix = prefix;
const isBoolAttribute = (name) => name.startsWith("?") || ["checked", "selected", "disabled", "readonly", "multiple", "ismap", "defer", "noresize", "nowrap", "noshade", "compact", "async", "autofocus", "autoplay", "controls", "default", "formnovalidate", "hidden", "ismap", "loop", "muted", "novalidate", "open", "reversed", "scoped", "seamless", "truespeed", "typemustmatch"].includes(name),
isObject = (value) => value && typeof value === "object",
isHook = (value) => isObject(value) && typeof value.hook ==="function",
isInterpolation = (value) => {
if (isObject(value) && Array.isArray(value.strings) && Array.isArray(value.values) && typeof value.raw === "function") return true;
},
isHTMLScope = (value) => isObject(value) && isObject(value.__state__) && isObject(value.root);
const getTop = (node) => {
while(node.parentElement) node = node.parentElement;
return node===document.documentElement ? document : node;
}
const _STS_ = new WeakMap();
let _OBSRVD_EL_;
const observeNodes = (observation,cb=()=>{}) => {
if(!Array.isArray(observation.observe)) observation.observe = observation.observe ? observation.observe.split(",") : [];
let previous = _OBSRVD_EL_;
_OBSRVD_EL_ = observation; //{el,observe,root};
cb()
_OBSRVD_EL_ = previous;
}
const activate = (object,el,path=[],ancestors=[]) => {
if(!isObject(object)) throw new TypeError(`${typeof(object)}: ${object} can't be activated. It is not an object`);
if(object._isActivated_) return object;
const subscribers = new Map(),
proxy = new Proxy(object,{
deleteProperty(target,property) {
if(property in target) {
const oldValue = target[property];
if(el) {
let event;
el.dispatchEvent(event = new CustomEvent("state:delete", {bubbles: true, detail: {property, oldValue,path,ancestors}}));
if(event.defaultPrevented) return true;
}
delete target[property];
proxy[property] = undefined; // forces re-render
}
return true;
},
set(target,property,value) {
const oldValue = target[property],
wasIn = property in target;
if(oldValue!==value) {
if(value===undefined) delete target[property];
else target[property] = value;
// if(oldValue!==undefined && value!==undefined) target[property] = value;
if(el && value!==undefined) {
let event;
if(wasIn) el.dispatchEvent(event = new CustomEvent("state:change", {bubbles: true, detail: {state:proxy,property, value,oldValue,path,ancestors}}));
else el.dispatchEvent(event = new CustomEvent("state:set", {bubbles: true, detail: {state:proxy,property, value,path,ancestors}}));
if(event.defaultPrevented) {
if(oldValue===undefined) delete target[property];
else target[property] = oldValue;
return true;
}
}
const subscriberSet = new Set([...(subscribers.get(property)||[]),...(subscribers.get("_all_")||[])]);
for(const observation of subscriberSet) {
const {nodes,observe,root,string,recurse,state} = observation;
// should we filter for connected nodes and drop others?
if(nodes.length===0 || !root.isConnected) {
subscriberSet.delete(observation);
} else if(nodes.length===1 && nodes[0].nodeType===Node.ATTRIBUTE_NODE) {
observeNodes(observation,() => nodes[0].value = interpolate(string,state||proxy,root).toString());
}
else {
observeNodes(observation,() => {
const content = interpolate(string, state||proxy, root).toDocumentFragment();
observation.nodes = [...content.childNodes];
nodes[0].replaceWith(...content.childNodes);
for (const node of nodes) node.remove();
if(recurse>0) {
for(const child of content.childNodes) {
if(child.nodeType!==Node.TEXT_NODE) {
render(child,null,{root,state:state||proxy,recurse:recurse-1})
}
}
}
})
}
}
if(el.resizeFrame) el.resizeFrame();
}
return true;
},
get(target,property) {
let value = target[property];
if(property==="observeNodes") return observeNodes;
if(property==="addEventListener") {
if(!el || !el.isConnected) throw new Error("addEventListener not available. State not bound to a connected element");
return el.addEventListener.bind(el);
}
if(property==="dispatchEvent") {
if(!el || !el.isConnected) return ()=> {};
return el.dispatchEvent.bind(el);
}
if(property==="removeEventListener") {
if(!el || !el.isConnected) throw new Error("removeEventListener not available. State not bound to a connected element");
return el.removeEventListener.bind(el);
}
if(property==="delete") {
if(!el || !el.isConnected) return ()=> {};
return () => {
el.remove();
el.dispatchEvent(new CustomEvent("state:deleted", {bubbles: true, detail: {state:proxy}}));
}
}
if(property==="_isActivated_") return true;
if(typeof property=== "symbol") return value;
if(isObject(value)) value = target[property] = activate(value,el,[...path,property],[...ancestors,proxy]);
if(_OBSRVD_EL_) {
const observation = _OBSRVD_EL_,
observe = observation.observe || [];
if(observe.includes(property) || observe.includes("*")) {
let subscriberSet = subscribers.get(property);
if(!subscriberSet) subscribers.set(property,subscriberSet=[]);
subscriberSet.push(observation)
}
}
return value
}
});
return proxy;
}
const getState = (idOrEl, {root = document,options={},throws}={}) => {
if (!isObject(idOrEl) || !(idOrEl instanceof HTMLElement)) idOrEl = getTop(root).getElementById(idOrEl);
const _state = _STS_.get(idOrEl);
if(!_state && throws) throw new Error(`Can't find state: ${idOrEl.id}`);
return _state?.state;
}
const setState = (idOrEl,state, {root = document,options={}}={}) => {
let el = idOrEl;
if(typeof idOrEl === "string") {
el = getTop(root).getElementById(el);
if(!el) {
el = document.createElement("template");
el.setAttribute("id",idOrEl);
if(root===document) root.head.append(el);
else root.append(el);
}
}
//if (!isObject(el) || el.constructor.name!=="HTMLTemplateElement") throw new TypeError(`${idOrEl} is not a valid HTMLTemplateElement or element id`);
state = activate(state,el);
_STS_.set(el, {state});
Object.defineProperty(el,"__state__",{enumerable:false,configurable:true,value:state});
Object.defineProperty(el,"getState",{enumerable:false,configurable:true,value:() => getState(el,{root})});
return state;
}
const getStates = (node,states=[],attrName = `${directiveExports.prefix}:usestate`) => {
if(node===null) {
if(document.documentState) states.push(document.documentState);
if(window.globalState) states.push(window.globalState);
return states;
}
if(node.nodeType===Node.ELEMENT_NODE && node.hasAttribute(attrName)) {
const state = getState(node.getAttribute(attrName))
if(!state) {
console.log(`Can't find state: ${node.getAttribute(attrName)} in ${node.outerHTML}`);
} else if(!states.includes(state)) {
states.push(state);
}
} else if(node.__state__) {
states.push(node.__state__);
}
//if(states.includes(undefined)) debugger;
getStates(node.parentNode,states,attrName);
return states;
}
const statesProxy = (states) => {
states = [...states];
const proxy = new Proxy({},{
get(target,property) {
if(property==="set") {
return (property,value,local) => {
if(!local) {
const parts = property.split(".");
let state = states.find((state) => parts[0] in state);
if(state) {
while(parts.length>0) {
const key = parts.shift();
if(parts.length===0) state[key] = value;
else if(state[key]!=null && typeof state[key]!=="object") throw new TypeError(`Attempt to use ${property} to set a value but ${key} is not an object`);
else state = state[key] ||= {};
}
}
}
return target[property] = value;
}
}
if(property==="get") {
return (property) => {
const parts = property.split(".");
let state = states.find((state) => parts[0] in state);
if(state) {
while(parts.length>0) {
const key = parts.shift();
if(parts.length===0) return state[key];
state = state[key];
if(state && typeof state!=="object") throw new TypeError(`Attempt to use ${property} to get a value but ${key} is not an object`);
}
}
return target[property];
}
}
const value = target[property];
if(value!==undefined) return value;
for(const state of states) {
const value = state[property];
if(value!==undefined) return value;
}
//return exported[this] || globalThis[property];
},
has(target,property) {
if(target[property]!==undefined) return true;
for(const state of states) {
if(property in state) return true;
}
if(globalThis[property]!==undefined || exported[property]) return false;
return true;
},
ownKeys(target) {
const keys = new Set();
for(const state of states) {
for(const key of Object.keys(state)) keys.add(key);
}
return [...keys];
},
set(target,property,value) {
throw new Error(`Can't set property value for ${property} inside reactive string literal. It will cause an infinite loop`);
}
});
return proxy;
}
const handleOnAttribute = (attribute,value) => {
let event;
if(attribute.name.includes(":")) event = attribute.name.split(":").find((part,i,values) => part=="on" ? values[i] : undefined)
else event = attribute.name.slice(2);
if(attribute.name.includes(":")) {
if(typeof value !=="function") value = (new Function("return "+value)).bind(attribute.ownerElement);
attribute.ownerElement.addEventListener(event,(ev) => {
value(ev);
});
} else {
if(typeof value !=="function") value = (new Function("return "+value)).bind(attribute.ownerElement);
else attribute.ownerElement[value.name] = value;
attribute.ownerElement.addEventListener(event,value);
attribute.ownerElement.removeAttribute(attribute.name);
//attribute.value = `event.target.${value.name}(event)`;
}
}
const handleHook = async (node,value) => {
let {hook,placeholder,delay,interval,where="inner"} = value;
placeholder = typeof placeholder === "function" ? placeholder() : placeholder||node.value||node.data||"...";
let toreplace = node.nodeType===Node.ATTRIBUTE_NODE ? [] : await render(node,await placeholder,{where});
(interval ? setInterval : setTimeout)(async () => {
const node = toreplace[0];
let content = await hook(node);
if(content===undefined) return;
if(where==="outer" && toreplace.length>0) animate(() => toreplace.forEach((node) => node.remove()));
if(isObject(content)) {
try {
content = JSON.stringify(content);
} catch {
}
} else {
content = content.toString();
}
if(node.nodeType===Node.ATTRIBUTE_NODE) {
if(node.name.startsWith("on") || node.name.includes("on:")) handleOnAttribute(node,content)
else node.value = content;
} else {
toreplace = await render(node,content,{where})
}
},interval || delay || 1000)
}
const walkSync = (node, callback, root = node, level = 0) => {
if (node.nodeType == Node.TEXT_NODE) {
if (callback) callback(node, level, root);
} else {
if (node.nodeType == Node.ELEMENT_NODE) for (const attr of [...node.attributes]) !callback || callback(attr, level, root)
if(node.tagName!=="SCRIPT" && node.tagName!=="CODE") {
if (callback) callback(node, level, root);
const nodes = [...(node.childNodes ? node.childNodes : node)];
for (const child of nodes) walkSync(child, callback, root, level + 1);
}
}
return node;
};
const walk = async (node, callback, root = node, level = 0) => {
if (node.nodeType == Node.TEXT_NODE) {
if (callback) await callback(node, level, root);
} else {
if (node.nodeType == Node.ELEMENT_NODE) for (const attr of [...node.attributes]) !callback || await callback(attr, level, root)
if(node.tagName!=="SCRIPT" && node.tagName!=="CODE") {
if (callback) callback(node, level, root);
const nodes = [...(node.childNodes ? node.childNodes : node)];
for (const child of nodes) await walk(child, callback, root, level + 1);
}
}
return node;
};
const _DIRECTIVES_ = {},
_HTM_CACHE_ = new Map();
const html = function (strings, ...values) {
const {state, root} = isHTMLScope(this) ? this : {state: {}, root: document},
locator = "_LCTR_",
result = {
strings,
values,
toString() {
return this.raw();
},
raw() {
return strings.reduce((acc, cur, i) => {
const value = values[i - 1];
if(Array.isArray(value)) {
return acc += value.map((value) => (isInterpolation(value) ? value.raw() : (value.toString ? value.toString() : value))).join("")
}
return acc + (isInterpolation(value) ? value.raw() : (value.toString ? value.toString() : value)) + cur
})
},
nodes(){
return this.toDocumentFragment().childNodes;
},
toDocumentFragment() {
const removeAttribute = (attribute) => !attribute.isConnected || attribute.ownerElement.removeAttribute(attribute.name);
let html = (_HTM_CACHE_.get(strings) || strings.reduce((html, string, i) => {
if (i >= values.length) return html + string;
html += string + locator + i;
return html;
}, "")),
templateOutsideHead = html.startsWith("<template>") && !html.startsWith("<head>");
_HTM_CACHE_.set(strings, html);
if (templateOutsideHead) html = "<div>" + html.slice(10).slice(0, -11) + "</div>";
const parsed = new DOMParser().parseFromString(html, "text/html");
walkSync(parsed, (node) => {
if (node.nodeType == Node.ATTRIBUTE_NODE) {
const name = node.name,
owner = node.ownerElement;
let value = node.value;
if (value.startsWith(locator)) {
const index = parseInt(value.substring(locator.length));
value = values[index];
node.rawValue = value;
if (isObject(value)) {
node.value = value instanceof Date ? value + "" : JSON.stringify(value);
} else if (typeof value === "function") {
node.value = "${" + value.name + "}";
if((node.name.startsWith("on") || node.name.includes("on:"))) handleOnAttribute(node,value);
} else if (isBoolAttribute(name)) {
if (!!value) node.value = "";
else removeAttribute(node);
} else {
node.value = value
}
if (node.value === "undefined") removeAttribute(node);
}
} else if (node.nodeType == Node.TEXT_NODE) {
if (node.data.trim().length === 0 || !node.data.includes("_LCTR_")) return;
const parts = node.data.split(/(?=_LCTR_\d+)/g).reduce((acc, cur) => { //\s|\S_LCTR_\d+
if (/\S_LCTR_\d+/.test(cur)) {
acc.push(cur[0]);
acc.push(cur.slice(1));
} else {
acc.push(cur);
}
return acc;
}, []);
node.data = "";
let currentNode = node;
const parent = node.parentElement;
parts.forEach((part,i) => {
if (part.startsWith(locator)) {
const index = parseInt(part.substring(locator.length)),
value = values[index],
type = typeof value,
array = Array.isArray(value) ? value : [value];
let restPart = part.substring(locator.length + index.toString().length) || "";
for (const item of array) {
if (isInterpolation(item)) { //type === "string" && item ||
const nodes = [...item.toDocumentFragment().childNodes],
last = nodes[nodes.length - 1] || currentNode;
if (last) {
currentNode.after(...nodes);
const nextnode = document.createTextNode(restPart + " ");
last.after(currentNode = nextnode);
}
} else if(isHook(value)) {
handleHook(parent,value)
} else if (isObject(item)) {
currentNode.data += (Array.isArray(item) ? item.join(",") : JSON.stringify(item)) + " "
}
else {
currentNode.data += item + restPart;
}
restPart = "";
}
} else {
currentNode.data += (part === "" && i<parts.length-1 ? " " : part)
}
})
}
});
//sanitize
const base = html.startsWith("<head>") ? parsed.head : parsed.body,
fragment = document.createDocumentFragment();
if(html.includes("<style>") && !html.startsWith("<head>")) fragment.append(...parsed.head.querySelectorAll("style")||[]);
while(base.firstChild) fragment.appendChild(base.firstChild);
if (templateOutsideHead) {
const fragment = document.createDocumentFragment(),
template = document.createElement("template");
template.innerHTML = base.innerHTML;
fragment.append(template);
return fragment;
}
return fragment;
}
};
return result;
}
html.nodes = (strings, ...values) => html(strings, ...values).nodes();
html.raw = (strings, ...values) => html(strings, ...values).raw();
html.toString = (strings, ...values) => html(strings, ...values).toString();
html.documentFragment = (strings, ...values) => html(strings, ...values).toDocumentFragment();
const compile = (escaped, root,all) => {
let unescaped = all ? escaped.replaceAll(/>/g, ">").replaceAll(/</g, "<").replaceAll(/&/g, "&").replaceAll(/"/g, '""').replaceAll(/'/g, "'")
: replaceBetween(escaped, "${", "}", (text) => text.replaceAll(/>/g, ">").replaceAll(/</g, "<").replaceAll(/&/g, "&").replaceAll(/"/g, '""').replaceAll(/'/g, "'"));
if(unescaped.startsWith("${") && !unescaped.includes("}")) unescaped += "}"
return Function("html", "root", "return (state) => { with(state) { return html`" + unescaped + "`}}")(html, root); // html.bind({state,root});
}
const interpolate = (text, state = {}, root,all) => compile(text, root,all)(state);
let animations = [];
const animate = (callback) => {
animations.push(callback);
if(animations.length===1) {
requestAnimationFrame(async () => {
for(const callback of animations) await callback();
animations = [];
if(typeof resizeFrame !== "undefined") resizeFrame(document);
});
}
}
const evaluateScripts = (node,target) => {
for(const el of node.querySelectorAll("script")) {
const script = document.createElement("script");
[...el.attributes].forEach((attr) => script.setAttribute(attr.name,attr.value));
if(script.hasAttribute("async") || script.hasAttribute("defer")) {
const src = new URL(script.src);
src.search += (src.search ? "&" : "") + "t=" + Date.now();
script.src = src.href;
}
script.innerHTML = el.innerHTML;
window.self = node.host ? node.host : node.parentElement;
window.currentScript = script;
(target||el).after(script);
script.remove();
delete window.currentScript;
}
}
const getOptions = ({el,handler,root}) => {
let options = handler!=="usejson" && handler!=="options" && el.hasAttribute(`${directiveExports.prefix}:options`) ? (directiveExports.JSON.parse(el.getAttribute(`${directiveExports.prefix}:options`))[handler])||{} : {};
if(typeof options==="string" && options[0]==="#") {
const el = root.getElementById(options);
options = directiveExports.JSON.parse(el.innerHTML);
}
return options;
}
const getContext = (el,state=el.__state__) => {
return statesProxy(getStates(el,state ? [state] : undefined))
}
const handleDirective = async (attr,{state,window=globalThis,document=globalThis.document,root=document,recurse}={}) => {
const {name,value} = attr,
parts = name.substring(5).split(":"); // data-foo:bar
if(parts.length>1) {
const [namespace,handler,...args] = parts,
arg = {
namespace,
handler,
el: attr.ownerElement,
attribute: attr,
rawValue: value,
args,
options:getOptions({el:attr.ownerElement,handler,root}),
state:statesProxy(getStates(attr.ownerElement,state ? [state] : undefined)),
root,
window,
document,
recurse,
lazui: directiveExports
};
if(_DIRECTIVES_[namespace] && _DIRECTIVES_[namespace][handler]) {
await _DIRECTIVES_[namespace][handler](arg);
} else {
try {
const directive = await import(/* webpackIgnore: true */ `${lazui.url.href}/directives/${namespace}/${handler}.js`);
if(directive) {
const f = directive[handler] || directive.default;
(_DIRECTIVES_[namespace] ||={})[handler] = f;
try {
await f(arg)
} catch(e) {
console.warn(`Directive: ${namespace}/${handler} threw an error ${e}`);
}
}
} catch(e) {
console.warn(`Can't find directive: ${namespace}/${handler} ${e}`);
}
}
}
}
function _resizeFrame(document) {
const body = document.body;
this.style.height = (body.offsetHeight < body.scrollHeight ? body.offsetHeight : body.scrollHeight + 5) + "px";
this.style.width = (body.offsetWidth < body.scrollWidth ? body.offsetWidth : body.scrollWidth + 18) + "px";
}
const handleFrame = (node,content,state) => {
const iframe = document.createElement("iframe");
while (node.firstChild) node.removeChild(node.firstChild);
if(node.hasAttribute("title")) iframe.setAttribute("title",node.getAttribute("title"));
iframe.setAttribute("frameborder","0");
iframe.setAttribute("allowfullscreen","false");
iframe.setAttribute("allow","accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture");
iframe.setAttribute("sandbox","allow-forms allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-top-navigation");
iframe.srcdoc = `<head>
<base href="${document.baseURI}">
${node.hasAttribute("title") ? "<title>" + node.getAttribute("title") + "</title>" : ""}
<script src="${document.querySelector('script[src*=\"/lazui\"]').getAttribute('src')}" autofocus></script>
<script>
window.top = window.parent = window.self = window.globalState = window;
window.frameElement = null;
</script>
<style>
html,body {
margin:0;
padding:0;
width:100%;
display:table;
}
</style>
</head>
<body>
${content}
</body>`
node.append(iframe);
iframe.contentWindow.resizeFrame = _resizeFrame.bind(iframe)
if(state) iframe.contentDocument.documentState = state;
}
const resolve = async (content,{state,root,recurse}) => {
await walk(content,async (node) => {
if(node.nodeType===Node.ATTRIBUTE_NODE) {
const stateProxy = statesProxy(getStates(node.ownerElement,state ? [state] : undefined));
if(node.value.includes("${")) {
observeNodes({nodes:[node],observe:["*"],root,string:node.value,state:stateProxy},() => {
const value = interpolate(node.value,stateProxy,root);
if(node.name.startsWith("on") || node.name.includes("on:")) handleOnAttribute(node,value.values[0]);
else if(isHook(value)) {
handleHook(node,value);
} else if (isBoolAttribute(name)) {
if (!!value) node.value = "";
else node.removeAttribute(node.name);
} else if(value.values[0]!==undefined) {
node.rawValue = value.values[0];
node.value = value.values[0].toString();
}
//else if(node.name.startsWith("on")) node.ownerElement.removeAttribute(node.name);
});
} else if(node.name.startsWith("on")) {
handleOnAttribute(node,node.ownerElement[node.name] || node.value)
}
if (node.name.startsWith(directiveExports.prefix)) await handleDirective(node,{state:stateProxy,root,recurse});
return;
}
if(node.nodeType===Node.TEXT_NODE && node.data.includes("${") && node.parentElement?.constructor.name!=="HTMLTemplateElement") {
const stateProxy = statesProxy(getStates(node,state ? [state] : undefined)),
observation = {nodes:[node],observe:["*"],root,string:node.data,recurse,state:stateProxy};
observeNodes(observation,() => {
const interpolated = interpolate(node.data,stateProxy,root,node.parentElement?.tagName==="SCRIPT").toDocumentFragment();
observation.nodes = [...interpolated.childNodes];
node.replaceWith(...interpolated.childNodes);
})
}
})
}
const render = async (el,content,{node=el||document.createDocumentFragment(),where="inner",root=document,state=document.documentState||window.globalState||{},forElement,sanitizer,sanitizerOptions={},animator=(f) => animate(f),recurse=0}={}) => {
if(node===document) {
document.addEventListener("DOMContentLoaded",async () => {
if(string) console.warn("String argument ignored when rendering to document on initial load. Existing document content will be used");
await render(node.head,null,{where:"outer",state,sanitizer,sanitizerOptions,animator});
await render(node.body,null,{node:state,sanitizer,sanitizerOptions,animator});
});
return;
}
if(content?.head) {
evaluateScripts(content.head,el);
content = content.body;
}
sanitizer = typeof sanitizer == "function" ? new sanitizer(sanitizerOptions) : sanitizer;
sanitizer ||= sanitizer!==null && typeof Sanitizer !== "undefined" ? new Sanitizer(sanitizerOptions) : sanitizer;
const string = typeof content === "string" ? content : !content && node.nodeType === Node.TEXT_NODE ? node.data : null;
if(string) {
observeNodes({nodes:[node],observe:["*"],root,string,state},() => content = interpolate(string,state,root).toDocumentFragment())
} else {
if(isObject(content) && content.toDocumentFragment) content = content.toDocumentFragment();
if(!content) (content = node.cloneNode(true));
(content.forEach ? content : [content]).forEach((node) => {
for(const pre of node.querySelectorAll("pre")) {
pre.innerHTML = pre.innerHTML.replaceAll(/\$\{/g, "${");
}
})
if(state!==null) {
if(!(content instanceof DocumentFragment) && !(content instanceof NodeList)) {
// ? node.html rathet thna content.html
content.innerHTML = replaceBetween(content.innerHTML, "`", "`", (text) => text.replaceAll(/</g, "<"))
}
await resolve(content,{state,root,recurse});
}
}
if(where===null) return content;
const callback = recurse>0 ? async ({childNodes}) => {
for(const child of childNodes) {
if(child.nodeType!==Node.TEXT_NODE) await render(child,null,{root,state,sanitizer,sanitizerOptions,animator,recurse:recurse-1})
}
} : undefined;
return update({node,content,state,root,where,animator,callback});
//return node;
}
function replaceBetween(inputString, delimiterStart, delimiterEnd, replacementCallback) {
const placeholders = [];
let startIndex = inputString.indexOf(delimiterStart),
endIndex;
while (startIndex !== -1) {
endIndex = inputString.indexOf(delimiterEnd, startIndex + delimiterStart.length);
if (endIndex !== -1) {
const textBetweenDelimiters = inputString.substring(startIndex + delimiterStart.length, endIndex);
const replacementText = replacementCallback(textBetweenDelimiters);
const placeholder = `_PLCHLDR_${placeholders.length}_`;
placeholders.push({ placeholder, replacementText });
inputString = inputString.substring(0, startIndex) + placeholder + inputString.substring(endIndex + delimiterEnd.length);
} else {
// If the end delimiter is not found, break the loop
break;
}
startIndex = inputString.indexOf(delimiterStart, endIndex+1);
}
// Perform the replacements
while(inputString.includes("_PLCHLDR_")) {
placeholders.forEach(({ placeholder, replacementText }) => inputString = inputString.replace(placeholder, delimiterStart + replacementText + delimiterEnd));
}
return inputString;
}
const update = ({node, content, state=document.documentState||window.globalState||{}, where="inner", root=document.documentElement, animator = (f) => animate(f),callback}) => {
if(where) where = where.toLowerCase(); // note: where can be specifically assigned null
const mode = node.nodeType===Node.ELEMENT_NODE ? node.getAttribute(`${directiveExports.prefix}:mode`) : null;
if(mode) {
if(where!=="inner") throw new Error(`${mode} must use where="inner" not where="${where}"`);
if(mode!=="open" && mode!=="frame") throw new Error(`${directiveExports.prefix}:${mode}, is not currently supported`);
}
const updated = [],
// if no content.childNodes, content is a NodeList or an error
updater = () => {
const getNodes = (content,clone) => {
if(clone) {
if(content.childNodes) return [...content.childNodes].map((node) => node.cloneNode(true) );
return content.cloneNode(true);
}
return content.childNodes ? content.childNodes : content;
}
let childNodes = [...getNodes(content)];
if (where === "inner") {
if (node.nodeType === Node.TEXT_NODE) {
if(mode) throw new Error(`Shadow dom mode: ${mode} for text nodes, is not supported`);
updated.push(...getNodes(content));
node.after(...getNodes(content));
node.remove();
} else {
if (mode === "open") {
const shadowRoot = node.shadowRoot || node.attachShadow({mode});
Object.defineProperty(shadowRoot,"__state__",{enumerable:false,value:state});
updated.push(...getNodes(content));
shadowRoot.replaceChildren(...getNodes(content));
const src = node.getAttribute(`${directiveExports.prefix}:src`);
if(!src || !src.startsWith("http")) evaluateScripts(shadowRoot)
} else if (mode === "frame") {
updated.push(...getNodes(content));
handleFrame(node,getNodes(content),state);
} else {
updated.push(node);
node.replaceChildren(...getNodes(content));
}
}
} else if (where === "outer") {
const target = root instanceof DocumentFragment && root.host ? root.host : node;
updated.push(...getNodes(content))
target.replaceWith(...getNodes(content));
} else if (where === "previoussibling") {
updated.push(...getNodes(content))
const target = root instanceof DocumentFragment && root.host ? root.host : node;
if(target.previousSibling) target.previousSibling.replaceWith(...getNodes(content));
else target.before(...getNodes(content));
} else if (where === "beforebegin") {
const target = root instanceof DocumentFragment && root.host ? root.host : node;
updated.push(...getNodes(content))
target.before(...getNodes(content));
} else if (where === "afterbegin") {
updated.push(...content.childNodes);
if (node.nodeType === Node.TEXT_NODE) node.after(...getNodes(content));
else node.prepend(...getNodes(content));
} else if (where === "firstchild") {
updated.push(...getNodes(content));
if (node.firstChild) node.firstChild.replaceWith(...getNodes(content));
else node.append(...getNodes(content))
} else if (where === "lastchild") {
updated.push(...getNodes(content));
if (node.lastChild) node.lastChild.replaceWith(...getNodes(content));
else node.append(...getNodes(content))
} else if (where === "beforeend") {
updated.push(...getNodes(content))
if (node.nodeType === Node.TEXT_NODE) node.before(...getNodes(content));
else node.append(...content.childNodes);
} else if (where === "afterend") {
updated.push(...getNodes(content))
const target = root instanceof DocumentFragment && root.host ? root.host : node;
target.after(...getNodes(content));
} else if (where === "nextsibling") {
updated.push(...getNodes(content))
const target = root instanceof DocumentFragment && root.host ? root.host : node;
if(target.nextSibling) target.nextSibling.replaceWith(...getNodes(content));
else target.after(...getNodes(content));
} else if (where === "_top") {
let newContent;
if (content.firstElementChild.tagName !== "HTML") {
newContent = document.createElement("html");
if (content.firstElementChild.tagName === "HEAD") {
const head = document.createElement("head");
newContent.append(head);
head.append(...content.firstElementChild.childNodes);
} else if (content.firstElementChild.tagName === "BODY") {
const body = document.createElement("body");
newContent.append(body);
body.append(...content.firstElementChild.childNodes);
} else {
throw new Error("Invalid top level element");
}
}
childNodes = [...newContent.childNodes];
updated.push(...childNodes);
root.replaceChildren(...childNodes)
} else {
let [_where,selector]= where.split("!");
if(_where.startsWith("#")) {
selector = _where;
_where = "root"
}
let targets;
if(selector) {
if(_where==="root") {
targets = root.querySelectorAll(selector);
} else if(_where==="inner" || _where==="outer") {
targets = node.querySelectorAll(selector);
} else if(_where==="parent") {
targets = node.parentElement?.querySelectorAll(selector);
} else if(_where==="body") {
targets = document.body.querySelectorAll(selector);
} else {
throw new Error(`Unable to locate target: ${where}. Invalid where: ${_where}`)
}
} else {
throw new Error(`Invalid target: ${where}`)
}
if(!targets) throw new Error(`Unable to locate target: ${where}`);
for(const target of targets) {
updated.push(target);
if(_where==="inner") {
target.replaceChildren(...getNodes(content,true));
} else {
target.replaceWith(...getNodes(content,true));
}
}
}
if (callback) setTimeout(() => callback({childNodes,root,animate}))
};
if (typeof animator === "function") animator(updater)
else updater();
return updated;
}
const directiveExports = {render,update,html,compile,interpolate,getState,setState,getContext,observeNodes,handleDirective,replaceBetween,router:window,JSON,prefix:"data-lz"},
exported = {...directiveExports,useDirectives,usePrefix};
if(typeof module !== "undefined") {
module.exports = exported;
} else if(typeof window !== "undefined") {
globalThis.lazui = exported;
}
})();