2023-06-11 18:29:31 +02:00
|
|
|
const { getBooleanAttribute } = require("../misc");
|
|
|
|
|
2023-06-09 00:37:29 +02:00
|
|
|
class View extends HTMLElement {
|
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this._model = null;
|
2023-06-09 02:03:57 +02:00
|
|
|
this._listener = () => this.render();
|
2023-06-10 13:08:08 +02:00
|
|
|
this._disableChangeEvent = false;
|
|
|
|
}
|
|
|
|
|
2023-06-11 02:37:35 +02:00
|
|
|
static traverse(element, parents, filter, childrenProcessed) {
|
|
|
|
if (!element) return;
|
|
|
|
|
|
|
|
if (element.getAttribute("x-id")) {
|
|
|
|
if (filter(parents, element)) parents.push(element);
|
|
|
|
else return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const childNodes = element.childNodes;
|
|
|
|
for (let i = 0; i < childNodes.length; i++) {
|
|
|
|
const child = childNodes[i];
|
|
|
|
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
|
|
View.traverse(child, parents, filter, childrenProcessed);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (parents.length > 0) parents.pop();
|
|
|
|
childrenProcessed(parents, element);
|
|
|
|
}
|
|
|
|
|
|
|
|
static elements(view) {
|
2023-06-11 18:29:31 +02:00
|
|
|
let elements = [...view.querySelectorAll("[x-id]")];
|
|
|
|
elements = elements.filter((el) => {
|
|
|
|
let parent = el.parentElement;
|
|
|
|
while (parent != view) {
|
|
|
|
if (parent instanceof View) return false;
|
|
|
|
if (getBooleanAttribute(parent, "x-notraverse")) return false;
|
|
|
|
parent = parent.parentElement;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
});
|
2023-06-10 13:08:08 +02:00
|
|
|
const result = {};
|
2023-06-11 18:29:31 +02:00
|
|
|
elements.forEach((element) => {
|
|
|
|
if (result[element.getAttribute("x-id")]) {
|
|
|
|
console.log(`Duplicate element x-id ${element.getAttribute("x-id")} in ${view.localName}`);
|
|
|
|
}
|
|
|
|
result[element.getAttribute("x-id")] = element;
|
|
|
|
});
|
2023-06-10 13:08:08 +02:00
|
|
|
return result;
|
2023-06-09 00:37:29 +02:00
|
|
|
}
|
|
|
|
|
2023-06-11 02:37:35 +02:00
|
|
|
get elements() {
|
|
|
|
return View.elements(this);
|
|
|
|
}
|
|
|
|
|
2023-06-09 00:37:29 +02:00
|
|
|
set model(model) {
|
|
|
|
if (this._model) this._model.removeListener(this._listener);
|
|
|
|
this._model = model;
|
|
|
|
this._model.addListener(this._listener);
|
2023-06-10 13:08:08 +02:00
|
|
|
this.render();
|
|
|
|
}
|
|
|
|
|
|
|
|
get model() {
|
|
|
|
return this._model;
|
|
|
|
}
|
|
|
|
|
|
|
|
get state() {
|
|
|
|
const elements = this.elements;
|
|
|
|
const properties = ["checked", "value"];
|
|
|
|
const state = {};
|
|
|
|
for (const key of Object.keys(elements)) {
|
|
|
|
const element = elements[key];
|
|
|
|
if (!element.hasAttribute("x-state")) continue;
|
|
|
|
const elementState = {};
|
|
|
|
for (const property of properties) {
|
|
|
|
if (property in element) {
|
|
|
|
elementState[property] = element[property];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
state[key] = elementState;
|
|
|
|
}
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
set state(state) {
|
|
|
|
const elements = this.elements;
|
|
|
|
this._disableChangeEvent = true;
|
|
|
|
for (const key of Object.keys(elements)) {
|
|
|
|
const elementState = state[key];
|
|
|
|
if (elementState) {
|
|
|
|
const element = elements[key];
|
|
|
|
for (const property in elementState) {
|
|
|
|
element[property] = elementState[property];
|
|
|
|
if (element.localName === "input" && element.getAttribute("type") === "radio") {
|
2023-06-11 21:57:01 +02:00
|
|
|
const changeEvent = new CustomEvent("x-change", {
|
2023-06-10 13:08:08 +02:00
|
|
|
bubbles: true,
|
|
|
|
cancelable: true,
|
|
|
|
});
|
|
|
|
element.dispatchEvent(changeEvent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this._disableChangeEvent = false;
|
|
|
|
this.fireChangeEvent();
|
2023-06-09 00:37:29 +02:00
|
|
|
}
|
|
|
|
|
2023-06-09 02:03:57 +02:00
|
|
|
render() {}
|
2023-06-10 13:08:08 +02:00
|
|
|
|
|
|
|
setupEventHandlers() {
|
2023-06-11 21:57:01 +02:00
|
|
|
const handler = (event) => this.fireChangeEvent();
|
2023-06-10 13:08:08 +02:00
|
|
|
|
|
|
|
const elements = this.elements;
|
|
|
|
for (const key of Object.keys(elements)) {
|
|
|
|
const element = elements[key];
|
2023-06-11 02:37:35 +02:00
|
|
|
if (element._handlerSet) continue;
|
2023-06-10 13:08:08 +02:00
|
|
|
if (element.hasAttribute("x-change")) {
|
|
|
|
element.addEventListener("change", handler);
|
2023-06-11 02:37:35 +02:00
|
|
|
element._handlerSet = true;
|
2023-06-10 13:08:08 +02:00
|
|
|
}
|
|
|
|
if (element.hasAttribute("x-click")) {
|
|
|
|
element.addEventListener("click", handler);
|
2023-06-11 02:37:35 +02:00
|
|
|
element._handlerSet = true;
|
2023-06-10 13:08:08 +02:00
|
|
|
}
|
|
|
|
if (element.hasAttribute("x-input")) {
|
2023-06-10 22:34:28 +02:00
|
|
|
element.addEventListener("input", handler);
|
2023-06-11 02:37:35 +02:00
|
|
|
element._handlerSet = true;
|
2023-06-10 22:34:28 +02:00
|
|
|
}
|
|
|
|
if (element.hasAttribute("x-input-debounce")) {
|
|
|
|
const DEBOUNCE_MS = 50;
|
|
|
|
let timeoutId = 0;
|
|
|
|
const debounceHandler = (event) => {
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
timeoutId = setTimeout(() => {
|
|
|
|
this.fireChangeEvent();
|
|
|
|
}, DEBOUNCE_MS);
|
|
|
|
};
|
|
|
|
element.addEventListener("input", debounceHandler);
|
2023-06-11 02:37:35 +02:00
|
|
|
element._handlerSet = true;
|
2023-06-10 13:08:08 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fireChangeEvent() {
|
|
|
|
if (this._disableChangeEvent) return;
|
2023-06-11 21:57:01 +02:00
|
|
|
const event = new CustomEvent("x-change", {
|
2023-06-10 13:08:08 +02:00
|
|
|
bubbles: true,
|
|
|
|
cancelable: true,
|
|
|
|
});
|
|
|
|
this.dispatchEvent(event);
|
|
|
|
}
|
2023-06-09 00:37:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
exports.View = View;
|