- WIP still on the live module
This commit is contained in:
parent
b7cd369c12
commit
af4fa89127
137
asset/lean-live/document-manipulator.mjs
Normal file
137
asset/lean-live/document-manipulator.mjs
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
class DocumentManipulator {
|
||||||
|
#elements = {};
|
||||||
|
#window;
|
||||||
|
|
||||||
|
constructor(window) {
|
||||||
|
this.window = window;
|
||||||
|
|
||||||
|
this.window.addEventListener("popstate", (event) => {
|
||||||
|
if (event.state && event.state.wasFetched) {
|
||||||
|
this.fetchFromUrl(window.location.href, this.elements);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
registerLiveElement(element) {
|
||||||
|
if (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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unregisterLiveElement(element) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateTo(event) {
|
||||||
|
this.fetchFromUrl(event.detail.url, this.elements, {}, { includeHead: true });
|
||||||
|
|
||||||
|
window.history.pushState({ wasFetched: true },"", event.detail.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
formSubmit(event) {
|
||||||
|
const url = new URL(event.detail.form.getAttribute('action') ? event.detail.form.getAttribute('action') : window.document.location);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: event.detail.method,
|
||||||
|
headers: event.detail.headers ? event.detail.headers : {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const formData = new FormData(event.detail.form);
|
||||||
|
|
||||||
|
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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (const [key, value] of formData.entries()) {
|
||||||
|
url.searchParams.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetchFromUrl(url.toString(), [ event.detail.element.name ], options);
|
||||||
|
|
||||||
|
if (window.document.location !== url) {
|
||||||
|
window.history.pushState({ wasFetched: true }, "", url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchFromUrl(url, elements, options, liveHeaders) {
|
||||||
|
liveHeaders = liveHeaders ? liveHeaders : {};
|
||||||
|
options = options ? options : {};
|
||||||
|
liveHeaders.elements = liveHeaders.elements ? liveHeaders.elements : [];
|
||||||
|
|
||||||
|
if (Array.isArray(elements)) {
|
||||||
|
for (const key of elements) {
|
||||||
|
liveHeaders.elements.push({name: key});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (const key in elements) {
|
||||||
|
liveHeaders.elements.push({name: key});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options.headers = options.headers ? options.headers : {};
|
||||||
|
options.headers['X-Ui-Live'] = JSON.stringify(liveHeaders);
|
||||||
|
|
||||||
|
fetch(url, {... {
|
||||||
|
method: "GET",
|
||||||
|
}, ...( options ? options : {} ) })
|
||||||
|
.then(response => {
|
||||||
|
if (response.redirected) {
|
||||||
|
this.navigateTo({detail: { url : response.url, }});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(json => {
|
||||||
|
if (json) {
|
||||||
|
return this.render(json, url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render(json, url) {
|
||||||
|
for (let key in json.elements) {
|
||||||
|
const elem = this.elements[key];
|
||||||
|
|
||||||
|
if (elem) {
|
||||||
|
elem.swapHtml(json.elements[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Making sure we're not updating a component which is already updated by a parent node */
|
||||||
|
parentsOnly(elements) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
get elements() {
|
||||||
|
return this.#elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
set elements(value) {
|
||||||
|
return this.#elements = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get window() {
|
||||||
|
return this.#window;
|
||||||
|
}
|
||||||
|
|
||||||
|
set window(value) {
|
||||||
|
return this.#window = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const documentManipulatorObject = new DocumentManipulator(window);
|
||||||
52
asset/lean-live/events.mjs
Normal file
52
asset/lean-live/events.mjs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
export class Events {
|
||||||
|
#object;
|
||||||
|
|
||||||
|
constructor(object) {
|
||||||
|
this.#object = object;
|
||||||
|
|
||||||
|
if ( null === this.object.parentElement.closest('ui-live') ) {
|
||||||
|
this.attachLinks();
|
||||||
|
this.attachForms();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attachLinks() {
|
||||||
|
const links = this.object.querySelectorAll("a[href]");
|
||||||
|
|
||||||
|
for (const link of links) {
|
||||||
|
const href = link.getAttribute('href');
|
||||||
|
|
||||||
|
if (href.length && href.substring(0, 1) !== '#') {
|
||||||
|
link.addEventListener("click", (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
this.object.emit("live:navigate", { element: this.object, "url" : href, "link" : link });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attachForms() {
|
||||||
|
const forms = this.object.querySelectorAll("form");
|
||||||
|
|
||||||
|
for (const form of forms) {
|
||||||
|
const method = form.hasAttribute('method') ? form.getAttribute('method').toUpperCase() : "GET";
|
||||||
|
|
||||||
|
form.addEventListener("submit", (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
this.object.emit("live:form-submit", { element: this.object, "form" : form, "method": method, submitter: ev.submitter });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (method === "GET") {
|
||||||
|
form.querySelectorAll("input,select").forEach((ele) => {
|
||||||
|
ele.addEventListener("change", (ev) => form.submit());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get object() {
|
||||||
|
return this.#object;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,54 +1,74 @@
|
|||||||
import { Webcomponent } from "../../webcomponent/module/webcomponent.js";
|
import { Events } from "./events.mjs";
|
||||||
import { Hourglass } from "../../webcomponent/hourglass.js";
|
import { documentManipulatorObject } from "./document-manipulator.mjs";
|
||||||
|
|
||||||
const config = {
|
export class UiLive extends HTMLElement {
|
||||||
url: window.location.href,
|
#config = {
|
||||||
template: {
|
url: window.location.href,
|
||||||
fetch : false
|
template: {
|
||||||
}
|
fetch : true
|
||||||
};
|
|
||||||
|
|
||||||
class UiLive extends Webcomponent {
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super(config);
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (this.pool) {
|
|
||||||
new Hourglass(this.fetchAndRender.bind(this), {
|
|
||||||
timer: this.pool * 1000,
|
|
||||||
start: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchAndRender() {
|
#intervalId;
|
||||||
this.classList.add("live-loading");
|
|
||||||
|
|
||||||
fetch(this.config.url, {
|
#connected = false;
|
||||||
method: "GET",
|
|
||||||
headers: {
|
disconnectedCallback() {
|
||||||
'X-Ui-Live': JSON.stringify([ { name: this.name } ])
|
this.intervalId && clearTimeout(this.intervalId);
|
||||||
}
|
|
||||||
}).then( response => response.text() )
|
documentManipulatorObject.unregisterLiveElement(this);
|
||||||
.then(html => {
|
|
||||||
this.swapHtml(html);
|
console.debug("component disconnected: ", this);
|
||||||
})
|
}
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
connectedCallback() {
|
||||||
});
|
documentManipulatorObject.registerLiveElement(this);
|
||||||
|
|
||||||
|
if (this.pool) {
|
||||||
|
const self = {};
|
||||||
|
self[this.name] = this;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
this.addEventListener(type, listener, options);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
off(type, listener, options) {
|
||||||
|
this.removeEventListener(type, listener, options);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(name, detail, bubbles, cancelable, composed) {
|
||||||
|
this.dispatchEvent(new CustomEvent(name, {
|
||||||
|
detail: detail ? detail : null,
|
||||||
|
bubbles: !! bubbles,
|
||||||
|
cancelable: !! cancelable,
|
||||||
|
composed: !! composed,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
swapHtml(html) {
|
swapHtml(html) {
|
||||||
this.innerHTML = ( new DOMParser() )
|
this.innerHTML = html;
|
||||||
.parseFromString(html, "text/html")
|
|
||||||
.querySelector('ui-live[data-name="' + this.name + '"]')
|
|
||||||
.innerHTML;
|
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent("live:swap"));
|
this.emit("live:swap");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return string */
|
/** @return string */
|
||||||
@ -56,6 +76,26 @@ class UiLive extends Webcomponent {
|
|||||||
return this.dataset["name"];
|
return this.dataset["name"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return string */
|
||||||
|
get config() {
|
||||||
|
return this.#config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return string */
|
||||||
|
set config(value) {
|
||||||
|
return this.#config = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return int | undefined */
|
||||||
|
get intervalId() {
|
||||||
|
return this.#intervalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return int | undefined */
|
||||||
|
set intervalId(value) {
|
||||||
|
return this.#intervalId = value;
|
||||||
|
}
|
||||||
|
|
||||||
/** @return int | undefined */
|
/** @return int | undefined */
|
||||||
get pool() {
|
get pool() {
|
||||||
if (isNaN(this.dataset['pool'])) {
|
if (isNaN(this.dataset['pool'])) {
|
||||||
@ -64,6 +104,4 @@ class UiLive extends Webcomponent {
|
|||||||
|
|
||||||
return this.dataset["pool"];
|
return this.dataset["pool"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { UiLive };
|
|
||||||
@ -5,9 +5,9 @@ use Ulmus\ConnectionAdapter;
|
|||||||
putenv('LEAN_API_PROJECT_PATH=' . $path = dirname(__DIR__, 2));
|
putenv('LEAN_API_PROJECT_PATH=' . $path = dirname(__DIR__, 2));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'lean.api' => [
|
'lean.live' => [
|
||||||
'picea' => [
|
'picea' => [
|
||||||
'context' => Lean\Api\View::class,
|
'context' => Lean\Live\View::class,
|
||||||
|
|
||||||
'view' => [
|
'view' => [
|
||||||
[
|
[
|
||||||
@ -24,37 +24,8 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'ulmus' => [
|
|
||||||
'entities' => [ 'Lean\\Api\\Entity' => implode(DIRECTORY_SEPARATOR, [ $path, 'src', 'Entity', '' ]) ],
|
|
||||||
'adapters' => [
|
|
||||||
DI\get('lean.api:storage'),
|
|
||||||
]
|
|
||||||
],
|
|
||||||
|
|
||||||
'tell' => [
|
|
||||||
'json' => [
|
|
||||||
[
|
|
||||||
'path' => implode(DIRECTORY_SEPARATOR, [ $path, "meta", "i18n", "" ]),
|
|
||||||
'order' => 99,
|
|
||||||
],
|
|
||||||
]
|
|
||||||
],
|
|
||||||
|
|
||||||
'routes' => [
|
'routes' => [
|
||||||
'Lean\\Api\\Controller' => implode(DIRECTORY_SEPARATOR, [ $path, "src", "Controller", "" ]),
|
#'Lean\\Live\\Controller' => implode(DIRECTORY_SEPARATOR, [ $path, "src", "Controller", "" ]),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'lean.api:storage' => function($c) {
|
|
||||||
$adapter = new ConnectionAdapter('lean.api', $c->get('config')['ulmus'], false);
|
|
||||||
$adapter->resolveConfiguration();
|
|
||||||
|
|
||||||
return $adapter;
|
|
||||||
},
|
|
||||||
|
|
||||||
Lean\ApplicationStrategy\NotFoundDecoratorInterface::class => DI\autowire(Lean\Api\ApplicationStrategy\NotFoundDecorator::class),
|
|
||||||
Lean\ApplicationStrategy\MethodNotAllowedInterface::class => DI\autowire(Lean\Api\ApplicationStrategy\MethodNotAllowedDecorator::class),
|
|
||||||
Lean\Api\Factory\MessageFactoryInterface::class => DI\autowire(Lean\Api\Lib\Message::class),
|
|
||||||
Lean\Api\Factory\DebugFormFactoryInterface::class => DI\autowire(Lean\Api\Factory\DebugFormFactory::class),
|
|
||||||
# League\Route\Strategy\ApplicationStrategy::class => DI\autowire(Lean\Api\ApplicationStrategy::class),
|
|
||||||
];
|
];
|
||||||
15
src/Lib/Html.php
Normal file
15
src/Lib/Html.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Lean\Live\Lib;
|
||||||
|
|
||||||
|
class Html
|
||||||
|
{
|
||||||
|
|
||||||
|
public static function nodeToArray(\Dom\Node $node) : array
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
return $list ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
88
src/Middleware/RenderPartOf.php
Normal file
88
src/Middleware/RenderPartOf.php
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Lean\Live\Middleware;
|
||||||
|
|
||||||
|
use DI\Attribute\Inject;
|
||||||
|
use Lean\Factory\HttpFactoryInterface;
|
||||||
|
use Psr\Http\{Message\ResponseInterface,
|
||||||
|
Message\ServerRequestInterface,
|
||||||
|
Server\MiddlewareInterface,
|
||||||
|
Server\RequestHandlerInterface};
|
||||||
|
|
||||||
|
class RenderPartOf implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
#[Inject]
|
||||||
|
protected HttpFactoryInterface $factory;
|
||||||
|
|
||||||
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
|
{
|
||||||
|
$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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/Picea/Live.php
Normal file
33
src/Picea/Live.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Lean\Live\Picea;
|
||||||
|
|
||||||
|
use Picea\Extension\Extension,
|
||||||
|
Picea\Extension\ExtensionTrait;
|
||||||
|
|
||||||
|
use Picea\Compiler\Context;
|
||||||
|
|
||||||
|
use Picea\Extension\FunctionExtension;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
class Live implements Extension, FunctionExtension {
|
||||||
|
use ExtensionTrait;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected ServerRequestInterface $request
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token, array $options = []) : string { }
|
||||||
|
|
||||||
|
public function exportFunctions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
"live" => [ $this, 'liveElement' ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function liveElement(string $inputName) : void
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user