- First working version, tagging to 1.0
This commit is contained in:
parent
af4fa89127
commit
e0e53e1aa2
@ -1,2 +1,2 @@
|
|||||||
import { UiLive } from "./ui-live.mjs";
|
import {UiLive} from "./ui-live.mjs";
|
||||||
window.customElements.define("ui-live", UiLive);
|
window.customElements.get('ui-live') || window.customElements.define("ui-live", UiLive);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1,27 +1,47 @@
|
|||||||
export class Events {
|
export class Events {
|
||||||
#object;
|
#object;
|
||||||
|
|
||||||
constructor(object) {
|
constructor(object, swapping) {
|
||||||
this.#object = object;
|
this.#object = object;
|
||||||
|
|
||||||
if ( null === this.object.parentElement.closest('ui-live') ) {
|
if ( object.url || null === this.object.parentElement.closest('ui-live') ) {
|
||||||
this.attachLinks();
|
this.attachLinks();
|
||||||
this.attachForms();
|
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() {
|
attachLinks() {
|
||||||
const links = this.object.querySelectorAll("a[href]");
|
const links = this.object.querySelectorAll("a[href]");
|
||||||
|
|
||||||
for (const link of links) {
|
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) !== '#') {
|
if (href.length && href.substring(0, 1) !== '#') {
|
||||||
link.addEventListener("click", (ev) => {
|
link.addEventListener("click", (ev) => {
|
||||||
ev.preventDefault();
|
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) => {
|
form.addEventListener("submit", (ev) => {
|
||||||
ev.preventDefault();
|
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") {
|
if (method === "GET") {
|
||||||
form.querySelectorAll("input,select").forEach((ele) => {
|
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() {
|
get object() {
|
||||||
return this.#object;
|
return this.#object;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,10 @@ export class UiLive extends HTMLElement {
|
|||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
documentManipulatorObject.registerLiveElement(this);
|
documentManipulatorObject.registerLiveElement(this);
|
||||||
|
|
||||||
|
this.on("live:swap", () => new Events(this, true));
|
||||||
|
|
||||||
|
new Events(this, false);
|
||||||
|
|
||||||
if (this.pool) {
|
if (this.pool) {
|
||||||
const self = {};
|
const self = {};
|
||||||
self[this.name] = this;
|
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.intervalId = setInterval(() => documentManipulatorObject.fetchFromUrl(this.config.url, self), this.pool * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.on("live:swap", () => new Events(this));
|
|
||||||
|
|
||||||
new Events(this);
|
|
||||||
|
|
||||||
this.#connected = true;
|
this.#connected = true;
|
||||||
|
|
||||||
console.debug("component connected: ", this);
|
console.debug("component connected: ", this);
|
||||||
}
|
}
|
||||||
|
|
||||||
adoptedCallback() {
|
adoptedCallback() {
|
||||||
console.log("I was adopted ?");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
on(type, listener, options) {
|
on(type, listener, options) {
|
||||||
@ -71,9 +70,24 @@ export class UiLive extends HTMLElement {
|
|||||||
this.emit("live:swap");
|
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 */
|
/** @return string */
|
||||||
get name() {
|
get name() {
|
||||||
return this.dataset["name"];
|
return this.dataset.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return string */
|
/** @return string */
|
||||||
@ -98,10 +112,10 @@ export class UiLive extends HTMLElement {
|
|||||||
|
|
||||||
/** @return int | undefined */
|
/** @return int | undefined */
|
||||||
get pool() {
|
get pool() {
|
||||||
if (isNaN(this.dataset['pool'])) {
|
if (isNaN(this.dataset.pool)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.dataset["pool"];
|
return this.dataset.pool;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Ulmus\ConnectionAdapter;
|
use Lean\Live\Live;
|
||||||
|
|
||||||
|
use function DI\autowire;
|
||||||
|
|
||||||
putenv('LEAN_API_PROJECT_PATH=' . $path = dirname(__DIR__, 2));
|
putenv('LEAN_API_PROJECT_PATH=' . $path = dirname(__DIR__, 2));
|
||||||
|
|
||||||
@ -22,10 +24,16 @@ return [
|
|||||||
'order' => 10
|
'order' => 10
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'extensions' => [
|
||||||
|
\Lean\Live\Picea\Live::class,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'routes' => [
|
'routes' => [
|
||||||
#'Lean\\Live\\Controller' => implode(DIRECTORY_SEPARATOR, [ $path, "src", "Controller", "" ]),
|
#'Lean\\Live\\Controller' => implode(DIRECTORY_SEPARATOR, [ $path, "src", "Controller", "" ]),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
Live::class => autowire(Live::class),
|
||||||
];
|
];
|
||||||
34
src/Html.php
Normal file
34
src/Html.php
Normal file
@ -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 ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
89
src/Live.php
Normal file
89
src/Live.php
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/LiveControllerTrait.php
Normal file
11
src/LiveControllerTrait.php
Normal file
@ -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 DI\Attribute\Inject;
|
||||||
use Lean\Factory\HttpFactoryInterface;
|
use Lean\Factory\HttpFactoryInterface;
|
||||||
|
use Lean\Live\Html;
|
||||||
|
use Lean\Live\Live;
|
||||||
use Psr\Http\{Message\ResponseInterface,
|
use Psr\Http\{Message\ResponseInterface,
|
||||||
Message\ServerRequestInterface,
|
Message\ServerRequestInterface,
|
||||||
Server\MiddlewareInterface,
|
Server\MiddlewareInterface,
|
||||||
@ -14,75 +16,25 @@ class RenderPartOf implements MiddlewareInterface
|
|||||||
#[Inject]
|
#[Inject]
|
||||||
protected HttpFactoryInterface $factory;
|
protected HttpFactoryInterface $factory;
|
||||||
|
|
||||||
|
#[Inject]
|
||||||
|
protected Live $live;
|
||||||
|
|
||||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
{
|
{
|
||||||
|
$this->live->fromResponse($request);
|
||||||
|
|
||||||
$handling = $handler->handle($request);
|
$handling = $handler->handle($request);
|
||||||
|
|
||||||
if (
|
if ($this->live->catch($handling)) {
|
||||||
in_array($handling->getStatusCode(), [ 200, 201, ], true) &&
|
if ( $response = $this->live->render($handling->getBody())) {
|
||||||
str_starts_with($handling->getHeaderLine('content-type'), 'text/html')
|
return $this->factory->createJsonResponse($response, $handling->getStatusCode());
|
||||||
) {
|
}
|
||||||
# Subject to change
|
else {
|
||||||
$uiLiveRequest = $request->getHeaderLine("X-Ui-Live");
|
return $this->factory->createEmptyResponse();
|
||||||
|
|
||||||
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;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,8 @@ class Live implements Extension, FunctionExtension {
|
|||||||
use ExtensionTrait;
|
use ExtensionTrait;
|
||||||
|
|
||||||
public function __construct(
|
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 { }
|
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
|
public function exportFunctions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
"live" => [ $this, 'liveElement' ],
|
"live" => fn(string $source) => $this->live->getVariableBag($source),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function liveElement(string $inputName) : void
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/Variables.php
Normal file
32
src/Variables.php
Normal file
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user