1 Commits
Author SHA1 Message Date
mcndave ab335bf919 - WIP on lean-live, working version 2026-10-19 19:12:41 +00:00
4 changed files with 212 additions and 40 deletions

No files matched your search

+13 -3
View File
@@ -30,7 +30,7 @@ class DocumentManipulator {
}
followUrl(event) {
this.fetchFromUrl(event.detail.url, [ event.detail.element.name ], {}, { head: false }, false);
this.fetchFromUrl(event.detail.url, [ event.detail.element.name ], {}, { includeHead: false, outerHtml: event.detail.element.action !== "replace", vars: [{ values: event.detail.element.variables, source: event.detail.element.name }] }, false);
}
navigateTo(event) {
@@ -144,7 +144,13 @@ class DocumentManipulator {
method: "GET",
}, ...( options ? options : {} ) })
.then(response => {
if (response.redirected) {
if (response.status === 204) {
console.error(`Received an empty response for your header, see log for more details.`);
console.log(liveHeaders);
return false;
}
else if (response.redirected) {
this.navigateTo({detail: { url : response.url, }});
return false;
@@ -163,8 +169,10 @@ class DocumentManipulator {
return this.render(json)
}
})
.catch((error) => {
.catch(error => {
this.showErrorMessage(error)
throw error;
});
}
@@ -269,6 +277,8 @@ class DocumentManipulator {
Notification.requestPermission().then((result) => {
});
// console.error(error);
const n = new Notification("An error occured trying to load a page", {
body: error,
icon: "data:image/png;base64,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 Line truncated
+121 -27
View File
@@ -1,36 +1,110 @@
export class Events {
#object;
#mutationObserver;
#events = [];
#uids = [];
constructor(object, swapping) {
this.#object = object;
if ( object.url || null === this.object.parentElement.closest('ui-live') ) {
this.attachLinks();
this.attachForms();
this.attachDataLiveUi();
}
console.log(this.object.parentElement.closest('ui-live'))
if (! swapping) {
this.autoloadUrl();
}
}
if ( (this.object.url && this.object.action === 'replace') || null === this.object.parentElement.closest('ui-live') ) {
this.registerEvents(this.object);
autoloadUrl() {
this.object.url && this.object.emit("live:autoload", {
element: this.object,
url: this.object.url,
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
});
}
attachLinks() {
const links = this.object.querySelectorAll("a[href]");
if (! swapping) {
switch(object.loading) {
case "eager":
this.autoloadUrl();
break;
case "lazy":
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.autoloadUrl();
observer.disconnect();
}
});
},
{
threshold: 0.1,
rootMargin: '100px'
}
);
observer.observe(object);
break;
}
}
}
registerEvents(parentNode) {
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) {
if (link.dataset.uiLive === undefined) {
const href = link.getAttribute('href');
if (href.length && href.substring(0, 1) !== '#') {
link.addEventListener("click", (ev) => {
this.registerEventFrom(link, 'click', (ev) => {
ev.preventDefault();
ev.stopPropagation();
@@ -40,19 +114,18 @@ export class Events {
link: link,
sourceEvent: ev
});
});
}
})
}
}
}
attachForms() {
const forms = this.object.querySelectorAll("form");
attachForms(parentNode) {
const forms = parentNode.querySelectorAll("form:not([data-ui-live-event]):not([target='blank'])");
for (const form of forms) {
const method = form.hasAttribute('method') ? form.getAttribute('method').toUpperCase() : "GET";
form.addEventListener("submit", (ev) => {
this.registerEventFrom(form, 'submit', (ev) => {
ev.preventDefault();
const name = ev.submitter.getAttribute('name'),
@@ -67,28 +140,28 @@ export class Events {
if (method === "GET") {
form.querySelectorAll("input,select").forEach((ele) => {
ele.addEventListener("change", (ev) => form.requestSubmit());
this.registerEventFrom(ele, 'change', (ev) => form.requestSubmit());
});
}
}
}
attachDataLiveUi() {
const elements = this.object.querySelectorAll("[data-ui-live]");
attachDataLiveUi(parentNode) {
const elements = parentNode.querySelectorAll("[data-ui-live]");
for (const element of elements) {
switch(element.tagName.toUpperCase()) {
case "TEXTAREA":
case "SELECT":
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 })
);
break;
default:
element.addEventListener("click", (ev) => {
this.registerEventFrom(element, 'click', (ev) => {
ev.stopPropagation();
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() {
return this.#object;
}
+62 -6
View File
@@ -8,15 +8,15 @@ export class UiLive extends HTMLElement {
fetch : true
}
}
#intervalId;
#connected = false;
#eventMapper;
#intervalId;
disconnectedCallback() {
this.intervalId && clearTimeout(this.intervalId);
documentManipulatorObject.unregisterLiveElement(this);
this.#eventMapper.unregisterEvents();
console.debug("component disconnected: ", this);
}
@@ -24,9 +24,8 @@ export class UiLive extends HTMLElement {
connectedCallback() {
documentManipulatorObject.registerLiveElement(this);
this.on("live:swap", () => new Events(this, true));
new Events(this, false);
this.on("live:swap", () => this.mapEvents(true));
this.mapEvents(false);
if (this.pool) {
const self = {};
@@ -40,6 +39,14 @@ export class UiLive extends HTMLElement {
console.debug("component connected: ", this);
}
mapEvents(swapping) {
if (this.#eventMapper) {
this.#eventMapper.unregisterEvents();
}
this.#eventMapper = new Events(this, swapping);
}
adoptedCallback() {
}
@@ -65,11 +72,45 @@ export class UiLive extends HTMLElement {
}
swapHtml(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");
}
/** @return string */
get variables() {
return this.dataset.vars ? new URLSearchParams(JSON.parse(this.dataset.vars)).toString() : "";
}
/** @return string */
get url() {
return this.dataset.src;
@@ -80,6 +121,21 @@ export class UiLive extends HTMLElement {
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 */
get selector() {
return this.dataset.selector;
+16 -4
View File
@@ -23,7 +23,7 @@ class Live
$this->params = json_decode($encodedHeader, true);
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);
}
@@ -49,7 +49,7 @@ class Live
$node = $dom->querySelector("ui-live[data-name=\"{$name}\"]");
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;
}
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);
}
}