- First working version, tagging to 1.0
This commit is contained in:
1 parent
af4fa89127
commit
21ebb12a4d
12 files changed
+446
-113
No files matched your search
@@ -1,2 +1,2 @@
|
||||
import {UiLive} from "./ui-live.mjs";
|
||||
window.customElements.define("ui-live", UiLive);
|
||||
window.customElements.get('ui-live') || window.customElements.define("ui-live", UiLive);
|
||||
@@ -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);
|
||||
this.fetchFromUrl(url.toString(), [ event.detail.element.name ], options, {}, true);
|
||||
}
|
||||
|
||||
if (window.document.location !== url) {
|
||||
window.history.pushState({ wasFetched: true }, "", url);
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchFromUrl(url, elements, options, liveHeaders) {
|
||||
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}"]`);
|
||||
}
|
||||
|
||||
/* Making sure we're not updating a component which is already updated by a parent node */
|
||||
parentsOnly(elements) {
|
||||
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)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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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
Line truncated
|
||||
});
|
||||
}
|
||||
|
||||
get elements() {
|
||||
|
||||
@@ -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) {
|
||||
if (link.dataset.uiLive === undefined) {
|
||||
const href = link.getAttribute('href');
|
||||
|
||||
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', `<input type='hidden' name='${name}' value='${value}'>` );
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Ulmus\ConnectionAdapter;
|
||||
use Lean\Live\Live;
|
||||
|
||||
use function DI\autowire;
|
||||
|
||||
putenv('LEAN_API_PROJECT_PATH=' . $path = dirname(__DIR__, 2));
|
||||
|
||||
@@ -22,10 +24,16 @@ return [
|
||||
'order' => 10
|
||||
],
|
||||
],
|
||||
|
||||
'extensions' => [
|
||||
\Lean\Live\Picea\Live::class,
|
||||
],
|
||||
],
|
||||
|
||||
'routes' => [
|
||||
#'Lean\\Live\\Controller' => implode(DIRECTORY_SEPARATOR, [ $path, "src", "Controller", "" ]),
|
||||
],
|
||||
],
|
||||
|
||||
Live::class => autowire(Live::class),
|
||||
];
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Lean\Live;
|
||||
|
||||
class Html
|
||||
{
|
||||
public static function nodeToArray(\Dom\Node $node) : array
|
||||
{
|
||||
$output = [
|
||||
'nodeName' => $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);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Lean\Live\Lib;
|
||||
|
||||
class Html
|
||||
{
|
||||
|
||||
public static function nodeToArray(\Dom\Node $node) : array
|
||||
{
|
||||
|
||||
|
||||
return $list ?? [];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Lean\Live;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class Live
|
||||
{
|
||||
public array $variablesObjects = [];
|
||||
|
||||
protected array $options = [];
|
||||
|
||||
public readonly array $params;
|
||||
|
||||
public bool $ignoreHistoryModification {
|
||||
get => $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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Lean\Live;
|
||||
|
||||
use DI\Attribute\Inject;
|
||||
|
||||
trait LiveControllerTrait
|
||||
{
|
||||
#[Inject]
|
||||
protected Live $live;
|
||||
}
|
||||
@@ -4,6 +4,8 @@ namespace Lean\Live\Middleware;
|
||||
|
||||
use DI\Attribute\Inject;
|
||||
use Lean\Factory\HttpFactoryInterface;
|
||||
use Lean\Live\Html;
|
||||
use Lean\Live\Live;
|
||||
use Psr\Http\{Message\ResponseInterface,
|
||||
Message\ServerRequestInterface,
|
||||
Server\MiddlewareInterface,
|
||||
@@ -14,75 +16,25 @@ class RenderPartOf implements MiddlewareInterface
|
||||
#[Inject]
|
||||
protected HttpFactoryInterface $factory;
|
||||
|
||||
#[Inject]
|
||||
protected Live $live;
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$this->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());
|
||||
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;
|
||||
}
|
||||
}
|
||||
+3
-7
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Lean\Live;
|
||||
|
||||
class Variables implements \ArrayAccess
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
public readonly array $variables,
|
||||
public readonly bool $active,
|
||||
) {}
|
||||
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
return array_key_exists($offset, $this->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]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user