From b0322c549a3400a6f45309c8991f5b4167cf0b8c Mon Sep 17 00:00:00 2001 From: nathan Date: Wed, 14 Jul 2021 09:49:36 -0600 Subject: [PATCH] Attribute parsing & basics of WebComponents looking like et2 widgets --- api/js/etemplate/et2-button.ts | 47 ++++++-- api/js/etemplate/et2_core_inheritance.ts | 142 ++++++++++++++++++++++- api/js/etemplate/et2_core_inputWidget.ts | 103 +++++++++++++++- api/js/etemplate/et2_core_widget.ts | 50 +++++--- api/js/etemplate/etemplate2.ts | 2 +- infolog/templates/default/edit.xet | 2 +- 6 files changed, 322 insertions(+), 24 deletions(-) diff --git a/api/js/etemplate/et2-button.ts b/api/js/etemplate/et2-button.ts index 472c7b953f..7ff96a9505 100644 --- a/api/js/etemplate/et2-button.ts +++ b/api/js/etemplate/et2-button.ts @@ -8,14 +8,47 @@ * @author Nathan Gray */ -/* Commented out while we work on rollup -import {LitElement,html} from "https://cdn.skypack.dev/lit-element"; -import {SlButton} from "https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.44/dist/shoelace.js"; -export class Et2Button extends SlButton +import BXButton from "../../../node_modules/carbon-web-components/es/components/button/button" +import {css} from "../../../node_modules/lit-element/lit-element.js"; +import {Et2InputWidget} from "./et2_core_inputWidget"; +import {Et2Widget} from "./et2_core_inheritance"; + +export class Et2Button extends Et2InputWidget(Et2Widget(BXButton)) { - size='small'; + static get properties() { + return { + image: {type: String} + } + } + static get styles() + { + debugger; + return [ + super.styles, + css` + /* Custom CSS */ + ` + ]; + } + constructor() + { + super(); + this.image = ''; + } + + connectedCallback() { + super.connectedCallback(); + + this.classList.add("et2_button") + debugger; + if(this.image) + { + let icon = document.createElement("img"); + icon.src = egw.image(this.image); + icon.slot="icon"; + this.appendChild(icon); + } + } } customElements.define("et2-button",Et2Button); - - */ \ No newline at end of file diff --git a/api/js/etemplate/et2_core_inheritance.ts b/api/js/etemplate/et2_core_inheritance.ts index cdda50e7c1..566cb4ee45 100644 --- a/api/js/etemplate/et2_core_inheritance.ts +++ b/api/js/etemplate/et2_core_inheritance.ts @@ -14,7 +14,10 @@ import {egw} from "../jsapi/egw_global"; import {et2_checkType, et2_no_init, et2_validateAttrib} from "./et2_core_common"; -import {et2_implements_registry} from "./et2_core_interfaces"; +import {et2_IDOMNode, et2_IInput, et2_IInputNode, et2_implements_registry} from "./et2_core_interfaces"; +import {LitElement} from "lit-element"; +import {et2_arrayMgr} from "./et2_core_arrayMgr"; +import {et2_widget} from "./et2_core_widget"; // Needed for mixin export function mix (superclass) @@ -303,3 +306,140 @@ export class ClassWithAttributes extends ClassWithInterfaces return attributes; } } + + +/** + * This mixin will allow any LitElement to become an Et2Widget + * + * Usage: + * export class Et2Loading extends Et2Widget(BXLoading) {...} + */ + +type Constructor = new (...args: any[]) => T; +export const Et2Widget = >(superClass: T) => { + class Et2WidgetClass extends superClass implements et2_IDOMNode { + + protected _mgrs: et2_arrayMgr[] = [] ; + protected _parent: Et2WidgetClass|et2_widget|null = null; + + iterateOver(callback: Function, context, _type) + {} + loadingFinished() + {} + getWidgetById(_id) + { + if (this.id == _id) { + return this; + } + } + + setParent(new_parent: HTMLElement | et2_widget) + { + this._parent = new_parent; + } + getParent() : HTMLElement | et2_widget { + let parentNode = this.parentNode; + + // If parent is an old et2_widget, use it + if(this._parent) + { + return this._parent; + } + + return parentNode; + } + getDOMNode(): HTMLElement { + return this; + } + + /** + * Sets the array manager for the given part + * + * @param {string} _part which array mgr to set + * @param {object} _mgr + */ + setArrayMgr(_part : string, _mgr : et2_arrayMgr) + { + this._mgrs[_part] = _mgr; + } + + /** + * Returns the array manager object for the given part + * + * @param {string} managed_array_type name of array mgr to return + */ + getArrayMgr(managed_array_type : string) : et2_arrayMgr | null + { + if (this._mgrs && typeof this._mgrs[managed_array_type] != "undefined") { + return this._mgrs[managed_array_type]; + } else if (this.getParent()) { + return this.getParent().getArrayMgr(managed_array_type); + } + + return null; + } + + /** + * Returns an associative array containing the top-most array managers. + * + * @param _mgrs is used internally and should not be supplied. + */ + getArrayMgrs(_mgrs? : object) + { + if (typeof _mgrs == "undefined") { + _mgrs = {}; + } + + // Add all managers of this object to the result, if they have not already + // been set in the result + for (var key in this._mgrs) { + if (typeof _mgrs[key] == "undefined") { + _mgrs[key] = this._mgrs[key]; + } + } + + // Recursively applies this function to the parent widget + if (this._parent) { + this._parent.getArrayMgrs(_mgrs); + } + + return _mgrs; + } + + /** + * Checks whether a namespace exists for this element in the content array. + * If yes, an own perspective of the content array is created. If not, the + * parent content manager is used. + * + * Constructor attributes are passed in case a child needs to make decisions + */ + checkCreateNamespace(_attrs? : any) + { + // Get the content manager + var mgrs = this.getArrayMgrs(); + + for (var key in mgrs) { + var mgr = mgrs[key]; + + // Get the original content manager if we have already created a + // perspective for this node + if (typeof this._mgrs[key] != "undefined" && mgr.perspectiveData.owner == this) { + mgr = mgr.parentMgr; + } + + // Check whether the manager has a namespace for the id of this object + var entry = mgr.getEntry(this.id); + if (typeof entry === 'object' && entry !== null || this.id) { + // The content manager has an own node for this object, so + // create an own perspective. + this._mgrs[key] = mgr.openPerspective(this, this.id); + } else { + // The current content manager does not have an own namespace for + // this element, so use the content manager of the parent. + delete (this._mgrs[key]); + } + } + } + }; + return Et2WidgetClass as unknown as Constructor & T; +} \ No newline at end of file diff --git a/api/js/etemplate/et2_core_inputWidget.ts b/api/js/etemplate/et2_core_inputWidget.ts index a9c6b98fac..2257efe2ba 100644 --- a/api/js/etemplate/et2_core_inputWidget.ts +++ b/api/js/etemplate/et2_core_inputWidget.ts @@ -18,10 +18,11 @@ import {et2_no_init} from "./et2_core_common"; import { ClassWithAttributes } from "./et2_core_inheritance"; import { et2_widget, WidgetConfig } from "./et2_core_widget"; import { et2_valueWidget } from './et2_core_valueWidget' -import {et2_IInput, et2_ISubmitListener} from "./et2_core_interfaces"; +import {et2_IInput, et2_IInputNode, et2_ISubmitListener} from "./et2_core_interfaces"; import {et2_compileLegacyJS} from "./et2_core_legacyJSFunctions"; // fixing circular dependencies by only importing the type (not in compiled .js) import type {et2_tabbox} from "./et2_widget_tabs"; +import {LitElement} from "lit-element"; export interface et2_input { getInputNode() : HTMLInputElement|HTMLElement; @@ -383,3 +384,103 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_ } } + +/** + * This mixin will allow any LitElement to become an Et2InputWidget + * + * Usage: + * export class Et2Button extends Et2InputWidget(BXButton) {...} + */ + +type Constructor = new (...args: any[]) => T; +export const Et2InputWidget = >(superClass: T) => { + class Et2InputWidgetClass extends superClass implements et2_IInput, et2_IInputNode { + + label: string = ''; + protected value: string | number | Object; + protected _oldValue: string | number | Object; + + + getValue() + { + var node = this.getInputNode(); + if (node) + { + var val = jQuery(node).val(); + + return val; + } + + return this._oldValue; + } + + isDirty() + { + let value = this.getValue(); + if(typeof value !== typeof this._oldValue) + { + return true; + } + if(this._oldValue === value) + { + return false; + } + switch(typeof this._oldValue) + { + case "object": + if(typeof this._oldValue.length !== "undefined" && + this._oldValue.length !== value.length + ) + { + return true; + } + for(let key in this._oldValue) + { + if(this._oldValue[key] !== value[key]) return true; + } + return false; + default: + return this._oldValue != value; + } + } + + resetDirty() + { + this._oldValue = this.getValue(); + } + + isValid(messages) + { + var ok = true; + + // Check for required + if (this.options && this.options.needed && !this.options.readonly && !this.disabled && + (this.getValue() == null || this.getValue().valueOf() == '')) + { + messages.push(this.egw().lang('Field must not be empty !!!')); + ok = false; + } + return ok; + } + + getInputNode() + { + return this.node; + } + + /** + * These belongs somewhere else/higher, I'm just getting it to work + */ + iterateOver(callback: Function, context, _type) + {} + loadingFinished() + {} + getWidgetById(_id) + { + if (this.id == _id) { + return this; + } + } + }; + return Et2InputWidgetClass as unknown as Constructor & T; +} \ No newline at end of file diff --git a/api/js/etemplate/et2_core_widget.ts b/api/js/etemplate/et2_core_widget.ts index b706e45e83..a25fbcb5bb 100644 --- a/api/js/etemplate/et2_core_widget.ts +++ b/api/js/etemplate/et2_core_widget.ts @@ -16,7 +16,7 @@ et2_core_arrayMgr; */ -import {ClassWithAttributes} from './et2_core_inheritance'; +import {ClassWithAttributes, Et2Widget} from './et2_core_inheritance'; import {et2_arrayMgr} from "./et2_core_arrayMgr"; import {egw, IegwAppLocal} from "../jsapi/egw_global"; import {et2_cloneObject, et2_csvSplit} from "./et2_core_common"; @@ -239,7 +239,7 @@ export class et2_widget extends ClassWithAttributes // The supported widget classes array defines a whitelist for all widget // classes or interfaces child widgets have to support. - this.supportedWidgetClasses = [et2_widget]; + this.supportedWidgetClasses = [et2_widget, HTMLElement]; if (_attrs["id"]) { // Create a namespace for this object @@ -714,14 +714,14 @@ export class et2_widget extends ClassWithAttributes constructor = et2_registry[_nodeName + "_ro"]; } - // Parse the attributes from the given XML attributes object - this.parseXMLAttrs(_node.attributes, attributes, constructor.prototype); - - // Do an sanity check for the attributes - ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], attributes); - if(undefined == window.customElements.get(_nodeName)) { + // Parse the attributes from the given XML attributes object + this.parseXMLAttrs(_node.attributes, attributes, constructor.prototype); + + // Do an sanity check for the attributes + ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], attributes); + // Creates the new widget, passes this widget as an instance and // passes the widgetType. Then it goes on loading the XML for it. var widget = new constructor(this, attributes); @@ -731,7 +731,7 @@ export class et2_widget extends ClassWithAttributes } else { - widget = this.loadWebComponent(_nodeName, _node, attributes); + widget = this.loadWebComponent(_nodeName, _node); if(this.addChild) { @@ -747,14 +747,38 @@ export class et2_widget extends ClassWithAttributes * @param _nodeName * @param _node */ - loadWebComponent(_nodeName : string, _node, attributes : Object) : HTMLElement + loadWebComponent(_nodeName : string, _node) : HTMLElement { - let widget = document.createElement(_nodeName); + let widget = document.createElement(_nodeName); widget.textContent = _node.textContent; - // Apply any set attributes - _node.getAttributeNames().forEach(attribute => widget.setAttribute(attribute, attributes[attribute])); + const widget_class = window.customElements.get(_nodeName); + if(!widget_class) + { + throw Error("Unknown or unregistered WebComponent '"+_nodeName+"', could not find class"); + } + widget.setParent(this); + var mgr = widget.getArrayMgr("content"); + // Apply any set attributes - widget will do its own coercion + _node.getAttributeNames().forEach(attribute => { + let attrValue = _node.getAttribute(attribute); + + // If there is not attribute set, ignore it. Widget sets its own default. + if(typeof attrValue === "undefined") return; + + // If the attribute is marked as boolean, parse the + // expression as bool expression. + if (widget_class.properties[attribute]?.type == "Boolean") { + attrValue = mgr.parseBoolExpression(attrValue); + } else { + attrValue = mgr.expandName(attrValue); + } + widget.setAttribute(attribute, attrValue); + }); + + // Children need to be loaded + //this.loadFromXML(_node); return widget; } diff --git a/api/js/etemplate/etemplate2.ts b/api/js/etemplate/etemplate2.ts index 3ec44666a9..bf62caa837 100644 --- a/api/js/etemplate/etemplate2.ts +++ b/api/js/etemplate/etemplate2.ts @@ -23,7 +23,7 @@ import {et2_nextmatch, et2_nextmatch_header_bar} from "./et2_extension_nextmatch import {et2_tabbox} from "./et2_widget_tabs"; import '../jsapi/egw_json.js'; import {egwIsMobile} from "../egw_action/egw_action_common.js"; -//import './et2-button'; +import './et2-button'; /* Include all widget classes here, we only care about them registering, not importing anything*/ import './et2_widget_vfs'; // Vfs must be first (before et2_widget_file) due to import cycle import './et2_widget_template'; diff --git a/infolog/templates/default/edit.xet b/infolog/templates/default/edit.xet index 79ca2f972e..1d1d990a08 100644 --- a/infolog/templates/default/edit.xet +++ b/infolog/templates/default/edit.xet @@ -235,7 +235,7 @@