diff --git a/asset/lean-live/app.js b/asset/lean-live/app.js index b4e3b01..09eae1e 100644 --- a/asset/lean-live/app.js +++ b/asset/lean-live/app.js @@ -1,2 +1,2 @@ -import { UiLive } from "./ui-live.mjs"; -window.customElements.define("ui-live", UiLive); \ No newline at end of file +import {UiLive} from "./ui-live.mjs"; +window.customElements.get('ui-live') || window.customElements.define("ui-live", UiLive); diff --git a/asset/lean-live/document-manipulator.mjs b/asset/lean-live/document-manipulator.mjs index bbe5a96..5d0bfa2 100644 --- a/asset/lean-live/document-manipulator.mjs +++ b/asset/lean-live/document-manipulator.mjs @@ -7,22 +7,30 @@ class DocumentManipulator { this.window.addEventListener("popstate", (event) => { if (event.state && event.state.wasFetched) { - this.fetchFromUrl(window.location.href, this.elements); + this.fetchFromUrl(window.location.href, this.elements, {}, { includeHead: true }); } }); } registerLiveElement(element) { - if (this.elements[element.name] === undefined) { + if (element.name && this.elements[element.name] === undefined) { this.elements[element.name] = element; element.on("live:navigate", this.navigateTo.bind(this)) element.on("live:form-submit", this.formSubmit.bind(this)) + element.on("live:input-change", this.attachedInputChange.bind(this)) + element.on("live:click", this.attachedElementClick.bind(this)) } + + element.on("live:autoload", this.followUrl.bind(this)); } unregisterLiveElement(element) { + delete this.elements[element.name]; + } + followUrl(event) { + this.fetchFromUrl(event.detail.url, [ event.detail.element.name ], {}, { head: false }, false); } navigateTo(event) { @@ -36,7 +44,7 @@ class DocumentManipulator { const options = { method: event.detail.method, - headers: event.detail.headers ? event.detail.headers : {}, + headers: event.detail.headers ?? {}, }; const formData = new FormData(event.detail.form); @@ -44,8 +52,8 @@ class DocumentManipulator { if (options.method !== "GET") { options.body = formData; - if (event.detail.submitter.hasAttribute('name')) { - options.body.append(event.detail.submitter.getAttribute('name'), event.detail.submitter.getAttribute('value')); + if (event.detail.sourceEvent.submitter.hasAttribute('name')) { + options.body.append(event.detail.sourceEvent.submitter.getAttribute('name'), event.detail.sourceEvent.submitter.getAttribute('value')); } } else { @@ -54,17 +62,69 @@ class DocumentManipulator { } } - this.fetchFromUrl(url.toString(), [ event.detail.element.name ], options); - - if (window.document.location !== url) { - window.history.pushState({ wasFetched: true }, "", url); - } + this.fetchFromUrl(url.toString(), [ event.detail.element.name ], options, {}, true); } - fetchFromUrl(url, elements, options, liveHeaders) { + attachedInputChange(event) { + const vars = []; + const name = event.detail.element.dataset.uiLive; + const siblings = this.window.document.querySelectorAll(`[data-ui-live="${name}"]`); + + for (let idx = 0; idx < siblings.length; idx++) { + const ele = siblings[idx]; + const key = ele.name ? ele.name : ele.id + + if ([ "radio", "checkbox" ].includes(ele.getAttribute('type'))) { + if ( ele.checked ) { + vars.push({ + key: key, + value: ele.value, + }); + } + } + else { + if (ele.value !== "") { + vars.push({ + key: key, + value: ele.value, + }); + } + } + } + + this.fetchFromUrl(window.location.href, [ event.detail.source.name ], {}, { vars: [ + { source: name, values: vars.map( ({ key, value }) => `${key}=${value}`) } + ] }); + } + + attachedElementClick(event) { + const name = event.detail.element.dataset.uiLive; + const defaultVarName = event.detail.element.dataset.name ?? name; + const defaultValue = event.detail.element.dataset.value ?? event.detail.element.value; + const vars = defaultValue ? [{ + key: event.detail.element.name ?? defaultVarName, + value: event.detail.element.dataset.value ?? event.detail.element.value, + }] : []; + const elements = event.detail.element.dataset.uiElement ?? event.detail.source.name; + const elementVariable = (event.detail.element.dataset.vars ? JSON.parse(event.detail.element.dataset.vars) : {}); + const url = event.detail.element.getAttribute('href') ?? event.detail.element.dataset.uiUrl ?? window.location.href; + + for (const property in elementVariable) { + vars.push({ + key: property, + value: elementVariable[property] + }); + } + + this.fetchFromUrl(url, elements.split(','), {}, { vars: [ + { source: name, values: vars.map( ({ key, value }) => `${key}=${value}`) } + ] }); + } + + fetchFromUrl(url, elements, options, liveHeaders, pushHistory) { liveHeaders = liveHeaders ? liveHeaders : {}; options = options ? options : {}; - liveHeaders.elements = liveHeaders.elements ? liveHeaders.elements : []; + liveHeaders.elements = liveHeaders.elements ?? []; if (Array.isArray(elements)) { for (const key of elements) { @@ -77,7 +137,7 @@ class DocumentManipulator { } } - options.headers = options.headers ? options.headers : {}; + options.headers = options.headers ?? {}; options.headers['X-Ui-Live'] = JSON.stringify(liveHeaders); fetch(url, {... { @@ -94,27 +154,125 @@ class DocumentManipulator { }) .then(json => { if (json) { - return this.render(json, url) + const blockHistory = json.options ? json.options.ignoreHistoryModification ?? false : false + + if (! blockHistory && pushHistory && (window.document.location !== url)) { + window.history.pushState({ wasFetched: true }, "", url); + } + + return this.render(json) } }) .catch((error) => { - console.error(error); + this.showErrorMessage(error) }); } - render(json, url) { - for (let key in json.elements) { + render(json) { + for (const key in json.elements) { const elem = this.elements[key]; if (elem) { elem.swapHtml(json.elements[key]); } } + + if (json.head && json.head.childNodes) { + const selectors = []; + + for (const key in json.head.childNodes) { + const jsonElement = json.head.childNodes[key]; + + let attributes = (jsonElement => { + const rendered = []; + + for (let [key, value] of Object.entries(jsonElement.attributes ?? {})) { + rendered.push(`[${key}="${value}"]`); + } + + return rendered.join(''); + })(jsonElement); + + selectors.push({ + selector : jsonElement.nodeName + attributes, + element: jsonElement, + }); + } + + this.window.document.head.childNodes.forEach((child) => { + let match = false; + + if (child.nodeType === 1) { + if ( [ "TITLE", "STYLE" ].includes(child.nodeName)) { + child.remove(); + } + else if (child.nodeName === "SCRIPT" && ! child.hasAttribute('src')) { + child.remove(); + } + else { + selectors.forEach((e, idx) => { + if (child.matches(e.selector)) { + match = true; + + delete selectors[idx]; + } + }); + + if (! match) { + child.remove(); + } + } + } + }); + + selectors.forEach(ele => { + this.window.document.head.append( + this.render_node_from_array(ele.element) + ); + }); + } } - /* Making sure we're not updating a component which is already updated by a parent node */ - parentsOnly(elements) { + render_node_from_array(node) { + if (node.nodeType === 3) { + return document.createTextNode(node.nodeValue); + } + if (node.nodeType !== 1) { + return null; + } + + // Create element node/ + const element = document.createElement(node.nodeName); + + // Set attributes + for (const attr in node.attributes) { + if (Object.hasOwnProperty.call(node.attributes, attr)) { + element.setAttribute(attr, node.attributes[attr]); + } + } + + // Rebuild child nodes recursively + if (node.childNodes && node.childNodes.length > 0) { + node.childNodes.forEach(childData => { + const childElement = this.render_node_from_array(childData); + if (childElement) { + element.appendChild(childElement); + } + }); + } + + return element; + } + + showErrorMessage(error) { + Notification.requestPermission().then((result) => { + }); + + const n = new Notification("An error occured trying to load a page", { + body: error, + icon: "data:image/png;base64,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" + }); } get elements() { diff --git a/asset/lean-live/events.mjs b/asset/lean-live/events.mjs index 390232e..dc23b99 100644 --- a/asset/lean-live/events.mjs +++ b/asset/lean-live/events.mjs @@ -1,27 +1,47 @@ export class Events { #object; - constructor(object) { + constructor(object, swapping) { this.#object = object; - if ( null === this.object.parentElement.closest('ui-live') ) { + if ( object.url || null === this.object.parentElement.closest('ui-live') ) { this.attachLinks(); this.attachForms(); + this.attachDataLiveUi(); } + + if (! swapping) { + this.autoloadUrl(); + } + } + + autoloadUrl() { + 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) { - const href = link.getAttribute('href'); + if (link.dataset.uiLive === undefined) { + const href = link.getAttribute('href'); - if (href.length && href.substring(0, 1) !== '#') { - link.addEventListener("click", (ev) => { - ev.preventDefault(); + if (href.length && href.substring(0, 1) !== '#') { + link.addEventListener("click", (ev) => { + ev.preventDefault(); + ev.stopPropagation(); - this.object.emit("live:navigate", { element: this.object, "url" : href, "link" : link }); - }); + this.object.emit("live:navigate", { + element: this.object, + url: href, + link: link, + sourceEvent: ev + }); + }); + } } } } @@ -35,17 +55,51 @@ export class Events { form.addEventListener("submit", (ev) => { ev.preventDefault(); - this.object.emit("live:form-submit", { element: this.object, "form" : form, "method": method, submitter: ev.submitter }); + const name = ev.submitter.getAttribute('name'), + value = ev.submitter.getAttribute('value'); + + if (name && value) { + form.insertAdjacentHTML( 'beforeend', `` ); + } + + this.object.emit("live:form-submit", { element: this.object, form: form, method: method, sourceEvent: ev }); }); if (method === "GET") { form.querySelectorAll("input,select").forEach((ele) => { - ele.addEventListener("change", (ev) => form.submit()); + ele.addEventListener("change", (ev) => form.requestSubmit()); }); } } } + attachDataLiveUi() { + const elements = this.object.querySelectorAll("[data-ui-live]"); + + for (const element of elements) { + switch(element.tagName.toUpperCase()) { + case "TEXTAREA": + case "SELECT": + case "INPUT": + element.addEventListener("change", (ev) => + this.object.emit("live:input-change", { element: element, source: this.object, submitter: element, sourceEvent: ev }) + ); + + break; + + default: + element.addEventListener("click", (ev) => { + ev.stopPropagation(); + ev.preventDefault(); + + this.object.emit("live:click", { element: element, source: this.object, submitter: element, sourceEvent: ev }) + }); + + break; + } + } + } + get object() { return this.#object; } diff --git a/asset/lean-live/ui-live.mjs b/asset/lean-live/ui-live.mjs index 1382dd7..851f68c 100644 --- a/asset/lean-live/ui-live.mjs +++ b/asset/lean-live/ui-live.mjs @@ -24,6 +24,10 @@ export class UiLive extends HTMLElement { connectedCallback() { documentManipulatorObject.registerLiveElement(this); + this.on("live:swap", () => new Events(this, true)); + + new Events(this, false); + if (this.pool) { const self = {}; self[this.name] = this; @@ -31,17 +35,12 @@ export class UiLive extends HTMLElement { this.intervalId = setInterval(() => documentManipulatorObject.fetchFromUrl(this.config.url, self), this.pool * 1000); } - this.on("live:swap", () => new Events(this)); - - new Events(this); - this.#connected = true; console.debug("component connected: ", this); } adoptedCallback() { - console.log("I was adopted ?"); } on(type, listener, options) { @@ -71,9 +70,24 @@ export class UiLive extends HTMLElement { this.emit("live:swap"); } + /** @return string */ + get url() { + return this.dataset.src; + } + + /** @return string */ + set url(value) { + return this.dataset.src = value; + } + + /** @return string */ + get selector() { + return this.dataset.selector; + } + /** @return string */ get name() { - return this.dataset["name"]; + return this.dataset.name; } /** @return string */ @@ -98,10 +112,10 @@ export class UiLive extends HTMLElement { /** @return int | undefined */ get pool() { - if (isNaN(this.dataset['pool'])) { + if (isNaN(this.dataset.pool)) { return undefined; } - return this.dataset["pool"]; + return this.dataset.pool; } } \ No newline at end of file diff --git a/meta/definitions/software.php b/meta/definitions/software.php index 2501dc5..9d3bd26 100644 --- a/meta/definitions/software.php +++ b/meta/definitions/software.php @@ -1,6 +1,8 @@ 10 ], ], + + 'extensions' => [ + \Lean\Live\Picea\Live::class, + ], ], 'routes' => [ #'Lean\\Live\\Controller' => implode(DIRECTORY_SEPARATOR, [ $path, "src", "Controller", "" ]), ], ], + + Live::class => autowire(Live::class), ]; \ No newline at end of file diff --git a/src/Html.php b/src/Html.php new file mode 100644 index 0000000..caffaf1 --- /dev/null +++ b/src/Html.php @@ -0,0 +1,34 @@ + $node->nodeName, + 'nodeValue' => $node->nodeValue, + 'nodeType' => $node->nodeType, + 'attributes' => [], + 'childNodes' => [] + ]; + + if ($node->nodeType === XML_ELEMENT_NODE && $node->hasAttributes()) { + foreach ($node->attributes as $attribute) { + $output['attributes'][$attribute->name] = $attribute->value; + } + } + + if ($node->hasChildNodes()) { + foreach ($node->childNodes as $childNode) { + if ($childNode->nodeType === XML_TEXT_NODE && trim($childNode->nodeValue) === '') { + continue; + } + $output['childNodes'][] = static::nodeToArray($childNode); + } + } + + return array_filter($output); + } +} \ No newline at end of file diff --git a/src/Lib/Html.php b/src/Lib/Html.php deleted file mode 100644 index fd25a7e..0000000 --- a/src/Lib/Html.php +++ /dev/null @@ -1,15 +0,0 @@ - $this->options['ignoreHistoryModification'] ?? false; + set => $this->options['ignoreHistoryModification'] = $value; + } + + public function fromResponse(ServerRequestInterface $request) : void { + if ( $encodedHeader = $request->getHeaderLine("X-Ui-Live") ) { + $this->params = json_decode($encodedHeader, true); + + foreach($this->params['vars'] ?? [] as $variables) { + parse_str(implode('&', $variables['values']), $vars); + + $this->variablesObjects[$variables['source']] = new Variables($variables['source'], $vars, true); + } + } + } + + public function catch(ResponseInterface $response) : bool + { + return ! empty($this->params) && + in_array($response->getStatusCode(), [ 200, 201, ], true) && + str_starts_with($response->getHeaderLine('content-type'), 'text/html'); + } + + public function render($stream) : array + { + $dom = \DOM\HTMLDocument::createFromString($stream->getContents(), 32); + + if ($this->params['elements'] ?? false) { + $list = array_map(fn($e) => $e['name'], $this->params['elements']); + + if ($list) { + foreach ($list as $name) { + $node = $dom->querySelector("ui-live[data-name=\"{$name}\"]"); + + if ($node && !$this->isAlreadyRendering($node, $list)) { + $elements[$name] = $this->renderElement($node); + } + } + } + } + + return array_filter([ + 'head' => empty($this->params['includeHead']) ? false : Html::nodeToArray($dom->querySelector("head")), + 'elements' => $elements ?? false, + 'options' => $this->options, + ]); + } + + public function getVariableBag(string $name) : mixed + { + return $this->variablesObjects[$name] ?? new Variables("", [], false); + } + + protected function isAlreadyRendering(\DOM\Element $element, array $list) : bool + { + $parent = $element->parentElement; + + do { + if (strtolower($parent->tagName) === "ui-live") { + if ($parent->hasAttribute('data-name') && in_array($parent->getAttribute('data-name'), $list)) { + return true; + } + } + } while($parent = $parent->parentElement); + + return false; + } + + protected function renderElement(\DOM\Element $element) : string + { + return $element->innerHTML; + } +} \ No newline at end of file diff --git a/src/LiveControllerTrait.php b/src/LiveControllerTrait.php new file mode 100644 index 0000000..5fdf7ab --- /dev/null +++ b/src/LiveControllerTrait.php @@ -0,0 +1,11 @@ +live->fromResponse($request); + $handling = $handler->handle($request); - if ( - in_array($handling->getStatusCode(), [ 200, 201, ], true) && - str_starts_with($handling->getHeaderLine('content-type'), 'text/html') - ) { - # Subject to change - $uiLiveRequest = $request->getHeaderLine("X-Ui-Live"); - - if ($uiLiveRequest) { - $decoded = json_decode($uiLiveRequest, true); - - $dom = \DOM\HTMLDocument::createFromString($handling->getBody()->getContents(), 32); - $list = array_map(fn($e) => $e['name'], $decoded['elements']); - - if ($list) { - foreach ($list as $name) { - $node = $dom->querySelector("ui-live[data-name=\"{$name}\"]"); - - if ($node && !$this->isAlreadyRendering($node, $list)) { - $elements[$name] = $this->renderElement($node); - } - } - } - - if ($decoded['includeHead'] ?? false) { - foreach($dom->querySelector("head")->childNodes as $node) { - if ($node instanceof \DOM\HTMLElement) { - $head[] = $dom->saveHTML($node); - } - } - } - - $jsonResponse = array_filter([ - 'head' => $head ?? false, - 'elements' => $elements ?? false, - ]); - - if ($elements) { - return $this->factory->createJsonResponse($jsonResponse, $handling->getStatusCode()); - } - else { - return $this->factory->createEmptyResponse(); - } + if ($this->live->catch($handling)) { + if ( $response = $this->live->render($handling->getBody())) { + return $this->factory->createJsonResponse($response, $handling->getStatusCode()); + } + else { + return $this->factory->createEmptyResponse(); } } + return $handling; } - - protected function isAlreadyRendering(\DOM\Element $element, array $list) : bool - { - $parent = $element->parentElement; - - do { - if (strtolower($parent->tagName) === "ui-live") { - if ($parent->hasAttribute('data-name') && in_array($parent->getAttribute('data-name'), $list)) { - return true; - } - } - } while($parent = $parent->parentElement); - - return false; - } - - protected function renderElement(\DOM\Element $element) : string - { - return $element->innerHTML; - } } diff --git a/src/Picea/Live.php b/src/Picea/Live.php index f4d1ce4..24c5c0f 100644 --- a/src/Picea/Live.php +++ b/src/Picea/Live.php @@ -14,7 +14,8 @@ class Live implements Extension, FunctionExtension { use ExtensionTrait; public function __construct( - protected ServerRequestInterface $request + protected ServerRequestInterface $request, + protected \Lean\Live\Live $live, ) {} public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token, array $options = []) : string { } @@ -22,12 +23,7 @@ class Live implements Extension, FunctionExtension { public function exportFunctions(): array { return [ - "live" => [ $this, 'liveElement' ], + "live" => fn(string $source) => $this->live->getVariableBag($source), ]; } - - public function liveElement(string $inputName) : void - { - - } } diff --git a/src/Variables.php b/src/Variables.php new file mode 100644 index 0000000..cbf3c9a --- /dev/null +++ b/src/Variables.php @@ -0,0 +1,32 @@ +variables); + } + + public function offsetGet(mixed $offset): mixed + { + return $this->variables[$offset] ?? null; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + throw new \LogicException("Live variable are read-only and cannot be redefined"); + } + + public function offsetUnset(mixed $offset): void + { + unset($this->variables[$offset]); + } +} \ No newline at end of file