- WIP on lean-live, working version

This commit is contained in:
Dave M. 2026-10-19 19:12:41 +00:00
parent 0deeeac3ac
commit ab335bf919
4 changed files with 226 additions and 54 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,58 +1,131 @@
export class Events { export class Events {
#object; #object;
#mutationObserver;
#events = [];
#uids = [];
constructor(object, swapping) { constructor(object, swapping) {
this.#object = object; this.#object = object;
if ( object.url || null === this.object.parentElement.closest('ui-live') ) { console.log(this.object.parentElement.closest('ui-live'))
this.attachLinks();
this.attachForms(); if ( (this.object.url && this.object.action === 'replace') || null === this.object.parentElement.closest('ui-live') ) {
this.attachDataLiveUi(); this.registerEvents(this.object);
this.#mutationObserver = new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node instanceof Element) {
this.registerEvents(node);
}
});
// Check for removed nodes
mutation.removedNodes.forEach(node => {
if (node.hasAttribute ? node.hasAttribute('data-ui-live-event') : false) {
}
});
}
}
});
this.#mutationObserver.observe(this.#object, {
childList:true,
subtree: true
});
} }
if (! swapping) { if (! swapping) {
this.autoloadUrl(); switch(object.loading) {
} case "eager":
} this.autoloadUrl();
autoloadUrl() { break;
this.object.url && this.object.emit("live:autoload", {
element: this.object,
url: this.object.url,
});
}
attachLinks() {
const links = this.object.querySelectorAll("a[href]");
for (const link of links) { case "lazy":
if (link.dataset.uiLive === undefined) { const observer = new IntersectionObserver(
const href = link.getAttribute('href'); (entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.autoloadUrl();
observer.disconnect();
}
});
},
{
threshold: 0.1,
rootMargin: '100px'
}
);
if (href.length && href.substring(0, 1) !== '#') { observer.observe(object);
link.addEventListener("click", (ev) => { break;
ev.preventDefault();
ev.stopPropagation();
this.object.emit("live:navigate", {
element: this.object,
url: href,
link: link,
sourceEvent: ev
});
});
}
} }
} }
} }
attachForms() { registerEvents(parentNode) {
const forms = this.object.querySelectorAll("form"); parentNode = parentNode ?? document;
this.attachLinks(parentNode);
this.attachForms(parentNode);
this.attachDataLiveUi(parentNode);
}
unregisterEvents() {
if (this.#mutationObserver) {
this.#mutationObserver.disconnect();
}
this.#uids.forEach((uid) => {
console.debug("Unregistering "+ uid);
this.#events[uid]();
});
}
autoloadUrl() {
if (this.object.url) {
this.object.emit("live:autoload", {
element: this.object,
url: this.object.url,
vars: this.object.variables,
});
}
}
attachLinks(parentNode) {
const links = parentNode.querySelectorAll("a[href]:not([data-ui-live-event]):not([data-ui-live]):not([target='_blank'])");
for (const link of links) {
const href = link.getAttribute('href');
if (href.length && href.substring(0, 1) !== '#') {
this.registerEventFrom(link, 'click', (ev) => {
ev.preventDefault();
ev.stopPropagation();
this.object.emit("live:navigate", {
element: this.object,
url: href,
link: link,
sourceEvent: ev
});
})
}
}
}
attachForms(parentNode) {
const forms = parentNode.querySelectorAll("form:not([data-ui-live-event]):not([target='blank'])");
for (const form of forms) { for (const form of forms) {
const method = form.hasAttribute('method') ? form.getAttribute('method').toUpperCase() : "GET"; const method = form.hasAttribute('method') ? form.getAttribute('method').toUpperCase() : "GET";
form.addEventListener("submit", (ev) => { this.registerEventFrom(form, 'submit', (ev) => {
ev.preventDefault(); ev.preventDefault();
const name = ev.submitter.getAttribute('name'), const name = ev.submitter.getAttribute('name'),
@ -67,28 +140,28 @@ export class Events {
if (method === "GET") { if (method === "GET") {
form.querySelectorAll("input,select").forEach((ele) => { form.querySelectorAll("input,select").forEach((ele) => {
ele.addEventListener("change", (ev) => form.requestSubmit()); this.registerEventFrom(ele, 'change', (ev) => form.requestSubmit());
}); });
} }
} }
} }
attachDataLiveUi() { attachDataLiveUi(parentNode) {
const elements = this.object.querySelectorAll("[data-ui-live]"); const elements = parentNode.querySelectorAll("[data-ui-live]");
for (const element of elements) { for (const element of elements) {
switch(element.tagName.toUpperCase()) { switch(element.tagName.toUpperCase()) {
case "TEXTAREA": case "TEXTAREA":
case "SELECT": case "SELECT":
case "INPUT": case "INPUT":
element.addEventListener("change", (ev) => this.registerEventFrom(element, 'change', (ev) =>
this.object.emit("live:input-change", { element: element, source: this.object, submitter: element, sourceEvent: ev }) this.object.emit("live:input-change", { element: element, source: this.object, submitter: element, sourceEvent: ev })
); );
break; break;
default: default:
element.addEventListener("click", (ev) => { this.registerEventFrom(element, 'click', (ev) => {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
@ -100,6 +173,27 @@ export class Events {
} }
} }
registerEventFrom(element, eventName, callback, reset) {
if (reset ?? false) {
const currentUid = element.getAttribute('data-ui-live-event');
if (currentUid) {
this.#events[currentUid]();
}
}
const uid = eventName + ":" + Math.random().toString(16).slice(2);
element.addEventListener(eventName, callback);
element.setAttribute('data-ui-live-event', uid);
this.#uids[uid] = eventName;
this.#events[uid] = () => {
element.removeEventListener(eventName, callback);
element.removeAttribute('data-ui-live-event' + eventName)
};
}
get object() { get object() {
return this.#object; return this.#object;
} }

View File

@ -8,15 +8,15 @@ export class UiLive extends HTMLElement {
fetch : true fetch : true
} }
} }
#intervalId;
#connected = false; #connected = false;
#eventMapper;
#intervalId;
disconnectedCallback() { disconnectedCallback() {
this.intervalId && clearTimeout(this.intervalId); this.intervalId && clearTimeout(this.intervalId);
documentManipulatorObject.unregisterLiveElement(this); documentManipulatorObject.unregisterLiveElement(this);
this.#eventMapper.unregisterEvents();
console.debug("component disconnected: ", this); console.debug("component disconnected: ", this);
} }
@ -24,9 +24,8 @@ export class UiLive extends HTMLElement {
connectedCallback() { connectedCallback() {
documentManipulatorObject.registerLiveElement(this); documentManipulatorObject.registerLiveElement(this);
this.on("live:swap", () => new Events(this, true)); this.on("live:swap", () => this.mapEvents(true));
this.mapEvents(false);
new Events(this, false);
if (this.pool) { if (this.pool) {
const self = {}; const self = {};
@ -40,6 +39,14 @@ export class UiLive extends HTMLElement {
console.debug("component connected: ", this); console.debug("component connected: ", this);
} }
mapEvents(swapping) {
if (this.#eventMapper) {
this.#eventMapper.unregisterEvents();
}
this.#eventMapper = new Events(this, swapping);
}
adoptedCallback() { adoptedCallback() {
} }
@ -65,11 +72,45 @@ export class UiLive extends HTMLElement {
} }
swapHtml(html) { swapHtml(html) {
this.innerHTML = html; switch(this.action) {
case "prepend":
case "append":
let target = this.parentElement.querySelector(`ui-live[data-name="${this.name}"]:last-child`);
let elem = document.createElement("p");
target.insertAdjacentElement('afterend', elem);
elem.outerHTML = html;
break;
case "replace":
this.innerHTML = html;
break;
}
Array.from(this.querySelectorAll("script"))
.forEach( oldScriptEl => {
const newScriptEl = document.createElement("script");
Array.from(oldScriptEl.attributes).forEach( attr => {
newScriptEl.setAttribute(attr.name, attr.value)
});
const scriptText = document.createTextNode(oldScriptEl.innerHTML);
newScriptEl.appendChild(scriptText);
oldScriptEl.parentNode.replaceChild(newScriptEl, oldScriptEl);
});
this.emit("live:swap"); this.emit("live:swap");
} }
/** @return string */
get variables() {
return this.dataset.vars ? new URLSearchParams(JSON.parse(this.dataset.vars)).toString() : "";
}
/** @return string */ /** @return string */
get url() { get url() {
return this.dataset.src; return this.dataset.src;
@ -80,6 +121,21 @@ export class UiLive extends HTMLElement {
return this.dataset.src = value; return this.dataset.src = value;
} }
/** @return string */
get action() {
return this.dataset.action ?? "replace";
}
/** @return string */
set action(value) {
return this.dataset.action = value;
}
/** @return string */
get loading() {
return this.dataset.loading ?? "eager";
}
/** @return string */ /** @return string */
get selector() { get selector() {
return this.dataset.selector; return this.dataset.selector;

View File

@ -23,7 +23,7 @@ class Live
$this->params = json_decode($encodedHeader, true); $this->params = json_decode($encodedHeader, true);
foreach($this->params['vars'] ?? [] as $variables) { foreach($this->params['vars'] ?? [] as $variables) {
parse_str(implode('&', $variables['values']), $vars); parse_str(is_array($variables['values']) ? implode('&', $variables['values']) : $variables['values'], $vars);
$this->variablesObjects[$variables['source']] = new Variables($variables['source'], $vars, true); $this->variablesObjects[$variables['source']] = new Variables($variables['source'], $vars, true);
} }
@ -49,7 +49,7 @@ class Live
$node = $dom->querySelector("ui-live[data-name=\"{$name}\"]"); $node = $dom->querySelector("ui-live[data-name=\"{$name}\"]");
if ($node && !$this->isAlreadyRendering($node, $list)) { if ($node && !$this->isAlreadyRendering($node, $list)) {
$elements[$name] = $this->renderElement($node); $elements[$name] = $this->renderElement($node, $this->params['outerHtml'] ?? false);
} }
} }
} }
@ -82,8 +82,20 @@ class Live
return false; return false;
} }
protected function renderElement(\DOM\Element $element) : string protected function renderElement(\DOM\Element $element, bool $outer = false) : string
{ {
return $element->innerHTML; return $outer ? $this->outerHtml($element) : $element->innerHTML;
}
protected function outerHtml(\DOM\Element $element): string {
$dom = \DOM\HTMLDocument::createEmpty();
$cloned = $dom->importNode($element, true);
$dom->appendChild($cloned);
$html = $dom->saveHTML();
return preg_replace('/<\?xml[^?]*\?>/', '', $html);
} }
} }