2023-06-18 16:24:32 +02:00
|
|
|
const { getBooleanAttribute, log, isMobile } = require("../js/misc");
|
2023-06-11 18:29:31 +02:00
|
|
|
|
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")]) {
|
2023-06-13 01:04:12 +02:00
|
|
|
log(`View - Duplicate element x-id ${element.getAttribute("x-id")} in ${view.localName}`);
|
2023-06-11 18:29:31 +02:00
|
|
|
}
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-06-11 23:49:18 +02:00
|
|
|
static getStateProperty(element) {
|
|
|
|
if (element instanceof HTMLInputElement) {
|
|
|
|
if (element.type === "checkbox" || element.type === "radio") {
|
|
|
|
return "checked";
|
|
|
|
} else {
|
|
|
|
return "value";
|
|
|
|
}
|
|
|
|
} else if (element instanceof HTMLOptionElement) {
|
|
|
|
return "selected";
|
|
|
|
} else if (element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
|
|
|
|
return "value";
|
|
|
|
} else if (element.localName === "custom-checkbox") {
|
|
|
|
return "checked";
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-10 13:08:08 +02:00
|
|
|
get state() {
|
|
|
|
const elements = this.elements;
|
|
|
|
const state = {};
|
|
|
|
for (const key of Object.keys(elements)) {
|
|
|
|
const element = elements[key];
|
|
|
|
if (!element.hasAttribute("x-state")) continue;
|
2023-06-11 23:49:18 +02:00
|
|
|
const property = View.getStateProperty(element);
|
|
|
|
if (property == null) {
|
|
|
|
log(`View.state() - Unknown state property for element ${element.getAttribute("x-id")} in ${this.localName}`);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (property in element) {
|
|
|
|
state[key] = element[property];
|
2023-06-10 13:08:08 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
set state(state) {
|
|
|
|
const elements = this.elements;
|
|
|
|
this._disableChangeEvent = true;
|
2023-06-11 23:49:18 +02:00
|
|
|
for (const key of Object.keys(state)) {
|
2023-06-10 13:08:08 +02:00
|
|
|
const elementState = state[key];
|
2023-06-11 23:49:18 +02:00
|
|
|
const element = elements[key];
|
|
|
|
if (element) {
|
|
|
|
const property = View.getStateProperty(element);
|
|
|
|
element[property] = elementState;
|
2023-06-10 13:08:08 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
this._disableChangeEvent = false;
|
|
|
|
this.fireChangeEvent();
|
2023-06-09 00:37:29 +02:00
|
|
|
}
|
|
|
|
|
2023-06-12 11:46:59 +02:00
|
|
|
get shareableState() {
|
|
|
|
const state = this.state;
|
|
|
|
const shareableState = Object.keys(state)
|
|
|
|
.sort()
|
|
|
|
.map((el) => {
|
|
|
|
let value = state[el];
|
|
|
|
if (value === true) value = ".";
|
|
|
|
if (value === false) value = "-";
|
|
|
|
return value;
|
|
|
|
})
|
|
|
|
.join(";");
|
|
|
|
return shareableState;
|
|
|
|
}
|
|
|
|
|
|
|
|
set shareableState(shareableState) {
|
|
|
|
const values = shareableState.split(";");
|
|
|
|
const state = this.state;
|
|
|
|
Object.keys(state)
|
|
|
|
.sort()
|
|
|
|
.forEach((el, index) => {
|
|
|
|
if (values[index] === ".") state[el] = true;
|
|
|
|
else if (values[index] === "-") state[el] = false;
|
|
|
|
else state[el] = values[index];
|
|
|
|
});
|
|
|
|
this.state = state;
|
|
|
|
}
|
|
|
|
|
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")) {
|
2023-06-18 16:24:32 +02:00
|
|
|
const DEBOUNCE_MS = isMobile() ? 150 : 50;
|
2023-06-10 22:34:28 +02:00
|
|
|
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;
|