UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

1,748 lines (1,527 loc) 49 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2004-2008 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Sebastian Werner (wpbasti) * John Spackman (https://github.com/johnspackman) ************************************************************************ */ /** * High-performance, high-level DOM element creation and management. * * Mirrors the DOM structure of Node (see also Element and Text) so to provide * DOM insertion and modification with advanced logic to reduce the real transactions. * * Each child itself also has got some powerful methods to control its * position: * {@link #getParent}, {@link #free}, * {@link #insertInto}, {@link #insertBefore}, {@link #insertAfter}, * {@link #moveTo}, {@link #moveBefore}, {@link #moveAfter}, * * NOTE: Instances of this class must be disposed of after use * * NOTE:: This class used to require `qx.module.Animation` but that brings in a huge * list of dependencies, so the require has been moved to the `qx.application.AbstractGui` * class * */ qx.Class.define("qx.html.Node", { extend: qx.core.Object, implement: [qx.core.IDisposable], /** * Creates a new Element * * @param nodeName {String} name of the node; will be a tag name for Elements, otherwise it's a reserved * name eg "#text" */ construct(nodeName) { super(); this._nodeName = nodeName; }, /* ***************************************************************************** STATICS ***************************************************************************** */ statics: { /** * Finds the Widget for a given DOM element * * @param domElement {DOM} the DOM element * @return {qx.ui.core.Widget} the Widget that created the DOM element */ fromDomNode(domNode) { if (qx.core.Environment.get("qx.debug")) { qx.core.Assert.assertTrue( (!domNode.$$element && !domNode.$$elementObject) || domNode.$$element === domNode.$$elementObject.toHashCode() ); } return domNode.$$elementObject; }, /** * Converts a DOM node into a qx.html.Node, providing the existing instance if * there is one * * @param {Node} domNode * @returns {qx.html.Node} */ toVirtualNode(domNode) { if (domNode.$$elementObject) { return domNode.$$elementObject; } let html = qx.html.Factory.getInstance().createElement( domNode.nodeName, domNode.attributes ); html.useNode(domNode); return html; } }, /* ***************************************************************************** PROPERTIES ***************************************************************************** */ properties: { /** * Controls whether the element is visible which means that a previously applied * CSS style of display=none gets removed and the element will inserted into the DOM, * when this had not already happened before. * * If the element already exists in the DOM then it will kept in DOM, but configured * hidden using a CSS style of display=none. * * Please note: This does not control the visibility or parent inclusion recursively. * * @type {Boolean} Whether the element should be visible in the render result */ visible: { init: true, nullable: true, check: "Boolean", apply: "_applyVisible", event: "changeVisible" } }, /* ***************************************************************************** MEMBERS ***************************************************************************** */ members: { /* --------------------------------------------------------------------------- PROTECTED HELPERS/DATA --------------------------------------------------------------------------- */ /** @type {String} the name of the node */ _nodeName: null, /** @type {Node} DOM node of this object */ _domNode: null, /** @type {qx.html.Element} parent element */ _parent: null, /** @type {qx.core.Object} the Qooxdoo object this node is attached to */ _qxObject: null, /** @type {Boolean} Whether the element should be included in the render result */ _included: true, _children: null, _modifiedChildren: null, _propertyJobs: null, _properties: null, /** @type {Map} map of event handlers */ __eventValues: null, /** * Connects a widget to this element, and to the DOM element in this Element. They * remain associated until disposed or disconnectObject is called * * @param qxObject {qx.core.Object} the object to associate */ connectObject(qxObject) { if (qx.core.Environment.get("qx.debug")) { qx.core.Assert.assertTrue( !this._qxObject || this._qxObject === qxObject ); } this._qxObject = qxObject; if (this._domNode) { if (qx.core.Environment.get("qx.debug")) { qx.core.Assert.assertTrue( (!this._domNode.$$qxObjectHash && !this._domNode.$$qxObject) || (this._domNode.$$qxObject === qxObject && this._domNode.$$qxObjectHash === qxObject.toHashCode()) ); } this._domNode.$$qxObjectHash = qxObject.toHashCode(); this._domNode.$$qxObject = qxObject; } if (qx.core.Environment.get("module.objectid")) { this.updateObjectId(); } }, /** * Disconnects a widget from this element and the DOM element. The DOM element remains * untouched, except that it can no longer be used to find the Widget. * * @param qxObject {qx.core.Object} the Widget */ disconnectObject(qxObject) { if (qx.core.Environment.get("qx.debug")) { qx.core.Assert.assertTrue(this._qxObject === qxObject); } delete this._qxObject; if (this._domNode) { if (qx.core.Environment.get("qx.debug")) { qx.core.Assert.assertTrue( (!this._domNode.$$qxObjectHash && !this._domNode.$$qxObject) || (this._domNode.$$qxObject === qxObject && this._domNode.$$qxObjectHash === qxObject.toHashCode()) ); } this._domNode.$$qxObjectHash = ""; delete this._domNode.$$qxObject; } if (qx.core.Environment.get("module.objectid")) { this.updateObjectId(); } }, /** * Internal helper to generate the DOM element * * @return {Element} DOM element */ _createDomElement() { throw new Error( "No implementation for " + this.classname + "._createDomElement" ); }, /** * Serializes the virtual DOM element to a string * * @param pretty {Boolean?} whether to pretty print the output. Defaults to `false` * @return {String} the serialised version */ serialize(pretty = false) { let serializer = new qx.html.Serializer(); serializer.setPrettyPrint(!!pretty); this._serializeImpl(serializer); return serializer.getOutput(); }, /** * Serializes the virtual DOM element to a writer; the `writer` function accepts * an varargs, which can be joined with an empty string or streamed. * * @param serializer {qx.html.Serializer} the serializer */ _serializeImpl(serializer) { throw new Error( "No implementation for " + this.classname + ".serializeImpl" ); }, /** * Uses an existing element instead of creating one. This may be interesting * when the DOM element is directly needed to add content etc. * * @param domNode {Node} DOM Node to reuse */ useNode(domNode) { if (this._domNode) { throw new Error("Could not overwrite existing element!"); } const removeAllChildren = parentElement => { if (parentElement._children) { qx.lang.Array.clone(parentElement._children).forEach(node => { parentElement._removeChildImpl(node); node._disconnectDomNode(); }); parentElement._children = null; } }; const scanDomNode = (parentElement, domNode, idx) => { if (domNode.nodeType == window.Node.TEXT_NODE) { let newChild = qx.html.Factory.getInstance().createElement("#text"); newChild._useNodeImpl(domNode); parentElement._addChildImpl(newChild); if (parentElement._children[idx]?.classname === "qx.html.Text") { parentElement._children[idx] = newChild; } else { parentElement._children.push(newChild); } return; } let id = domNode.getAttribute("data-qx-object-id"); let element = null; if (id) { try { element = parentElement.getQxObject(id); } catch (ex) { element = null; } } if (!element) { element = qx.html.Factory.getInstance().createElement( domNode.nodeName, domNode.attributes ); } if (element._parent !== parentElement) { parentElement._addChildImpl(element); parentElement._children.push(element); } element._connectDomNode(domNode); element._copyData(true, true); qx.lang.Array.fromCollection(domNode.childNodes).forEach( (childDomNode, idx) => scanDomNode(element, childDomNode, idx) ); parentElement._scheduleChildrenUpdate(); }; removeAllChildren(this); this._connectDomNode(domNode); this._copyData(true, true); qx.lang.Array.fromCollection(domNode.childNodes).forEach( (childDomNode, idx) => scanDomNode(this, childDomNode, idx) ); this.flush(); this._insertChildren(); this._scheduleChildrenUpdate(); }, /** * Connects a DOM element to this Node; if this Node is already connected to a Widget * then the Widget is also connected. * * @param domNode {DOM} the DOM Node to associate */ _connectDomNode(domNode) { if (qx.core.Environment.get("qx.debug")) { qx.core.Assert.assertTrue(!this._domNode || this._domNode === domNode); qx.core.Assert.assertTrue( (domNode.$$elementObject === this && domNode.$$element === this.toHashCode()) || (!domNode.$$elementObject && !domNode.$$element) ); } this._domNode = domNode; domNode.$$elementObject = this; domNode.$$element = this.toHashCode(); if (this._qxObject) { domNode.$$qxObjectHash = this._qxObject.toHashCode(); domNode.$$qxObject = this._qxObject; } }, /** * Disconnects the DOM node */ _disconnectDomNode() { if (this._domNode && this._domNode.parentElement) { this._domNode.parentElement.removeChild(this._domNode); } this._domNode = null; }, /** * Detects whether the DOM node has been created and is in the document * * @return {Boolean} */ isInDocument() { if (!this._domNode) { return false; } if (document.body) { for ( var domNode = this._domNode; domNode != null; domNode = domNode.parentElement ) { if (domNode === document.body) { return true; } } } return false; }, /** * Updates the Object ID on the element to match the QxObjectId */ updateObjectId() { // Copy Object Id if (qx.core.Environment.get("module.objectid")) { if (this._domNode) { qx.bom.element.Attribute.set( "data-qx-object-id", this._getApplicableQxObjectId() ); } } }, /** * @Override */ _cascadeQxObjectIdChanges() { if (qx.core.Environment.get("module.objectid")) { this.updateObjectId(); } super._cascadeQxObjectIdChanges(); }, /* --------------------------------------------------------------------------- FLUSH OBJECT --------------------------------------------------------------------------- */ /** * Add the element to the global modification list. * */ _scheduleChildrenUpdate() { if (this._modifiedChildren) { return; } if (this._domNode) { this._modifiedChildren = true; qx.html.Element._modified[this.toHashCode()] = this; qx.html.Element._scheduleFlush("element"); } }, /** * Syncs data of an HtmlElement object to the DOM. * * This is just a public wrapper around `flush`, because the scope has changed * * @deprecated {6.0} Please use `.flush()` instead */ _flush() { this.flush(); }, /** * Syncs data of an HtmlElement object to the DOM. * */ flush() { if (qx.core.Environment.get("qx.debug")) { if (this.DEBUG) { this.debug("Flush: " + this.getAttribute("id")); } } var length; var children = this._children; if (children) { length = children.length; var child; for (var i = 0; i < length; i++) { child = children[i]; if (child.isVisible() && child._included && !child._domNode) { child.flush(); } } } if (!this._domNode) { this._connectDomNode(this._createDomElement()); this._copyData(false, false); if (children && length > 0) { this._insertChildren(); } } else { this._syncData(); if (this._modifiedChildren) { this._syncChildren(); } } delete this._modifiedChildren; }, /** * Returns this element's root flag * * @return {Boolean} */ isRoot() { throw new Error("No implementation for " + this.classname + ".isRoot"); }, /** * Detects whether this element is inside a root element * * @return {Boolean} */ isInRoot() { var tmp = this; while (tmp) { if (tmp.isRoot()) { return true; } tmp = tmp._parent; } return false; }, /** * Walk up the internal children hierarchy and * look if one of the children is marked as root. * * This method is quite performance hungry as it * really walks up recursively. * @return {Boolean} <code>true</code> if the element will be seeable */ _willBeSeeable() { if (!qx.html.Element._hasRoots) { return false; } var pa = this; // Any chance to cache this information in the parents? while (pa) { if (pa.isRoot()) { return true; } if (!pa._included || !pa.isVisible()) { return false; } pa = pa._parent; } return false; }, /* --------------------------------------------------------------------------- SUPPORT FOR CHILDREN FLUSH --------------------------------------------------------------------------- */ /** * Append all child nodes to the DOM * element. This function is used when the element is initially * created. After this initial apply {@link #_syncChildren} is used * instead. * */ _insertChildren() { var children = this._children; if (!children) { return; } var length = children.length; var child; if (length > 2) { var domElement = document.createDocumentFragment(); for (var i = 0; i < length; i++) { child = children[i]; if (child._domNode && child._included) { domElement.appendChild(child._domNode); } } this._domNode.appendChild(domElement); } else { var domElement = this._domNode; for (var i = 0; i < length; i++) { child = children[i]; if (child._domNode && child._included) { domElement.appendChild(child._domNode); } } } }, /** * Synchronize internal children hierarchy to the DOM. This is used * for further runtime updates after the element has been created * initially. * */ _syncChildren() { var dataChildren = this._children || []; var dataLength = dataChildren.length; var dataChild; var dataEl; var domParent = this._domNode; var domChildren = domParent.childNodes; var domPos = 0; var domEl; if (qx.core.Environment.get("qx.debug")) { var domOperations = 0; } // Remove children from DOM which are excluded or remove first for (var i = domChildren.length - 1; i >= 0; i--) { domEl = domChildren[i]; dataEl = qx.html.Node.fromDomNode(domEl); if (!dataEl || !dataEl._included || dataEl._parent !== this) { domParent.removeChild(domEl); if (qx.core.Environment.get("qx.debug")) { domOperations++; } } } // Start from beginning and bring DOM in sync // with the data structure for (var i = 0; i < dataLength; i++) { dataChild = dataChildren[i]; // Only process visible childs if (dataChild._included) { dataEl = dataChild._domNode; domEl = domChildren[domPos]; if (!dataEl) { continue; } // Only do something when out of sync // If the data element is not there it may mean that it is still // marked as visible=false if (dataEl != domEl) { if (domEl) { domParent.insertBefore(dataEl, domEl); } else { domParent.appendChild(dataEl); } if (qx.core.Environment.get("qx.debug")) { domOperations++; } } // Increase counter domPos++; } } // User feedback if (qx.core.Environment.get("qx.debug")) { if (qx.html.Element.DEBUG) { this.debug("Synced DOM with " + domOperations + " operations"); } } }, /** * Copies data between the internal representation and the DOM. This * simply copies all the data and only works well directly after * element creation. After this the data must be synced using {@link #_syncData} * * @param fromMarkup {Boolean} Whether the copy should respect styles * given from markup * @param propertiesFromDom {Boolean} whether the copy should respect the property * values in the dom */ _copyData(fromMarkup, propertiesFromDom) { var elem = this._domNode; // Attach events var data = this.__eventValues; if (data) { // Import listeners let domEvents = {}; let manager = qx.event.Registration.getManager(elem); for (let id in data) { if (manager.findHandler(elem, data[id].type)) { domEvents[id] = data[id]; } } qx.event.Registration.getManager(elem).importListeners(elem, domEvents); // Cleanup event map // Events are directly attached through event manager // after initial creation. This differs from the // handling of styles and attributes where queuing happens // through the complete runtime of the application. delete this.__eventValues; } // Copy properties if (this._properties) { for (var key in this._properties) { var prop = this._properties[key]; if (propertiesFromDom) { if (prop.get) { prop.value = prop.get.call(this, key); } } else if (prop.value !== undefined) { prop.set.call(this, prop.value, key); } } } }, /** * Synchronizes data between the internal representation and the DOM. This * is the counterpart of {@link #_copyData} and is used for further updates * after the element has been created. * */ _syncData() { // Sync misc var jobs = this._propertyJobs; if (jobs && this._properties) { for (var key in jobs) { var prop = this._properties[key]; if (prop.value !== undefined) { prop.set.call(this, prop.value, key); } } this._propertyJobs = null; } }, /* --------------------------------------------------------------------------- PRIVATE HELPERS/DATA --------------------------------------------------------------------------- */ /** * Internal helper for all children addition needs * * @param child {var} the element to add * @throws {Error} if the given element is already a child * of this element */ _addChildImpl(child) { if (child._parent === this) { throw new Error("Child is already in: " + child); } if (child.__root) { throw new Error("Root elements could not be inserted into other ones."); } // Remove from previous parent if (child._parent) { child._parent.remove(child); } // Convert to child of this object child._parent = this; // Prepare array if (!this._children) { this._children = []; } // Schedule children update if (this._domNode) { this._scheduleChildrenUpdate(); } }, /** * Internal helper for all children removal needs * * @param child {qx.html.Element} the removed element * @throws {Error} if the given element is not a child * of this element */ _removeChildImpl(child) { if (child._parent !== this) { throw new Error("Has no child: " + child); } // Schedule children update if (this._domNode) { this._scheduleChildrenUpdate(); } // Remove reference to old parent delete child._parent; }, /** * Internal helper for all children move needs * * @param child {qx.html.Element} the moved element * @throws {Error} if the given element is not a child * of this element */ _moveChildImpl(child) { if (child._parent !== this) { throw new Error("Has no child: " + child); } // Schedule children update if (this._domNode) { this._scheduleChildrenUpdate(); } }, /* --------------------------------------------------------------------------- CHILDREN MANAGEMENT (EXECUTED ON THE PARENT) --------------------------------------------------------------------------- */ /** * Returns a copy of the internal children structure. * * Please do not modify the array in place. If you need * to work with the data in such a way make yourself * a copy of the data first. * * @return {Array} the children list */ getChildren() { return this._children || null; }, /** * Get a child element at the given index * * @param index {Integer} child index * @return {qx.html.Element|null} The child element or <code>null</code> if * no child is found at that index. */ getChild(index) { var children = this._children; return (children && children[index]) || null; }, /** * Returns whether the element has any child nodes * * @return {Boolean} Whether the element has any child nodes */ hasChildren() { var children = this._children; return children && children[0] !== undefined; }, /** * Find the position of the given child * * @param child {qx.html.Element} the child * @return {Integer} returns the position. If the element * is not a child <code>-1</code> will be returned. */ indexOf(child) { var children = this._children; return children ? children.indexOf(child) : -1; }, /** * Whether the given element is a child of this element. * * @param child {qx.html.Element} the child * @return {Boolean} Returns <code>true</code> when the given * element is a child of this element. */ hasChild(child) { var children = this._children; return children && children.indexOf(child) !== -1; }, /** * Append all given children at the end of this element. * * @param varargs {qx.html.Element} elements to insert * @return {qx.html.Element} this object (for chaining support) */ add(varargs) { var self = this; function addImpl(arr) { arr.forEach(function (child) { if (["string", "number", "boolean"].includes(typeof child)) { child = new qx.html.Text(`${child}`); } else if ( child instanceof qx.data.Array || qx.lang.Type.isArray(child) ) { addImpl(child); } if (child == null) { if (qx.core.Environment.get("qx.debug")) { console.error( `Tried to add a child of ${child} to ${self.classname}` ); } child = new qx.html.Text(`[${child}]`); } self._addChildImpl(child); self._children.push(child); }); } addImpl(qx.lang.Array.fromArguments(arguments)); // Chaining support return this; }, /** * Inserts a new element into this element at the given position. * * @param child {qx.html.Element} the element to insert * @param index {Integer} the index (starts at 0 for the * first child) to insert (the index of the following * children will be increased by one) * @return {qx.html.Element} this object (for chaining support) */ addAt(child, index) { if (typeof child == "string") { child = new qx.html.Text(child); } else if (typeof child == "number") { child = new qx.html.Text("" + child); } this._addChildImpl(child); qx.lang.Array.insertAt(this._children, child, index); // Chaining support return this; }, /** * Removes all given children * * @param childs {qx.html.Element} children to remove * @return {qx.html.Element} this object (for chaining support) */ remove(childs) { var children = this._children; if (!children) { return this; } var self = this; function removeImpl(arr) { arr.forEach(function (child) { if (child instanceof qx.data.Array || qx.lang.Type.isArray(child)) { removeImpl(child); } else { self._removeChildImpl(child); qx.lang.Array.remove(children, child); } }); } removeImpl(qx.lang.Array.fromArguments(arguments)); // Chaining support return this; }, /** * Removes the child at the given index * * @param index {Integer} the position of the * child (starts at 0 for the first child) * @return {qx.html.Element} this object (for chaining support) */ removeAt(index) { var children = this._children; if (!children) { throw new Error("Has no children!"); } var child = children[index]; if (!child) { throw new Error("Has no child at this position!"); } this._removeChildImpl(child); qx.lang.Array.removeAt(this._children, index); // Chaining support return this; }, /** * Remove all children from this element. * * @return {qx.html.Element} A reference to this. */ removeAll() { var children = this._children; if (children) { for (var i = 0, l = children.length; i < l; i++) { this._removeChildImpl(children[i]); } // Clear array children.length = 0; } // Chaining support return this; }, /* --------------------------------------------------------------------------- CHILDREN MANAGEMENT (EXECUTED ON THE CHILD) --------------------------------------------------------------------------- */ /** * Returns the parent of this element. * * @return {qx.html.Element|null} The parent of this element */ getParent() { return this._parent || null; }, /** * Insert self into the given parent. Normally appends self to the end, * but optionally a position can be defined. With index <code>0</code> it * will be inserted at the begin. * * @param parent {qx.html.Element} The new parent of this element * @param index {Integer?null} Optional position * @return {qx.html.Element} this object (for chaining support) */ insertInto(parent, index) { parent._addChildImpl(this); if (index == null) { parent._children.push(this); } else { qx.lang.Array.insertAt(this._children, this, index); } return this; }, /** * Insert self before the given (related) element * * @param rel {qx.html.Element} the related element * @return {qx.html.Element} this object (for chaining support) */ insertBefore(rel) { var parent = rel._parent; parent._addChildImpl(this); qx.lang.Array.insertBefore(parent._children, this, rel); return this; }, /** * Insert self after the given (related) element * * @param rel {qx.html.Element} the related element * @return {qx.html.Element} this object (for chaining support) */ insertAfter(rel) { var parent = rel._parent; parent._addChildImpl(this); qx.lang.Array.insertAfter(parent._children, this, rel); return this; }, /** * Move self to the given index in the current parent. * * @param index {Integer} the index (starts at 0 for the first child) * @return {qx.html.Element} this object (for chaining support) * @throws {Error} when the given element is not child * of this element. */ moveTo(index) { var parent = this._parent; parent._moveChildImpl(this); var oldIndex = parent._children.indexOf(this); if (oldIndex === index) { throw new Error("Could not move to same index!"); } else if (oldIndex < index) { index--; } qx.lang.Array.removeAt(parent._children, oldIndex); qx.lang.Array.insertAt(parent._children, this, index); return this; }, /** * Move self before the given (related) child. * * @param rel {qx.html.Element} the related child * @return {qx.html.Element} this object (for chaining support) */ moveBefore(rel) { var parent = this._parent; return this.moveTo(parent._children.indexOf(rel)); }, /** * Move self after the given (related) child. * * @param rel {qx.html.Element} the related child * @return {qx.html.Element} this object (for chaining support) */ moveAfter(rel) { var parent = this._parent; return this.moveTo(parent._children.indexOf(rel) + 1); }, /** * Remove self from the current parent. * * @return {qx.html.Element} this object (for chaining support) */ free() { var parent = this._parent; if (!parent) { throw new Error("Has no parent to remove from."); } if (!parent._children) { return this; } parent._removeChildImpl(this); qx.lang.Array.remove(parent._children, this); return this; }, /* --------------------------------------------------------------------------- DOM ELEMENT ACCESS --------------------------------------------------------------------------- */ /** * Returns the DOM element (if created). Please use this with caution. * It is better to make all changes to the object itself using the public * API rather than to the underlying DOM element. * * @param create {Boolean?} if true, the DOM node will be created if it does * not exist * @return {Element|null} The DOM element node, if available. */ getDomElement(create) { if (create && !this._domNode) { this.flush(); } return this._domNode || null; }, /** * Returns the nodeName of the DOM element. * * @return {String} The node name */ getNodeName() { return this._nodeName; }, /** * Sets the nodeName of the DOM element. * * @param name {String} The node name */ setNodeName(name) { if ( this._domNode && name.toLowerCase() !== this._nodeName.toLowerCase() ) { throw new Error( "Cannot change the name of the node after the DOM node has been created" ); } this._nodeName = name; }, /* --------------------------------------------------------------------------- EXCLUDE SUPPORT --------------------------------------------------------------------------- */ /** * Marks the element as included which means it will be moved into * the DOM again and synced with the internal data representation. * * @return {Node} this object (for chaining support) */ include() { if (this._included) { return this; } delete this._included; if (this._parent) { this._parent._scheduleChildrenUpdate(); } return this; }, /** * Marks the element as excluded which means it will be removed * from the DOM and ignored for updates until it gets included again. * * @return {qx.html.Element} this object (for chaining support) */ exclude() { if (!this._included) { return this; } this._included = false; if (this._parent) { this._parent._scheduleChildrenUpdate(); } return this; }, /** * Whether the element is part of the DOM * * @return {Boolean} Whether the element is part of the DOM. */ isIncluded() { return this._included === true; }, /** * Apply method for visible property */ _applyVisible(value) { // Nothing - to be overridden }, /* --------------------------------------------------------------------------- PROPERTY SUPPORT --------------------------------------------------------------------------- */ /** * Registers a property and the implementations used to read the property value * from the DOM and to set the property value onto the DOM. This allows the element * to have a simple `setProperty` method that knows how to read and write the value. * * You do not have to specify a getter or a setter - by default the setter will use * `_applyProperty` for backwards compatibility, and there is no getter implementation. * * The functions are called with `this` set to this Element. The getter takes * the property name as a parameter and is expected to return a value, the setter takes * the property name and value as parameters, and returns nothing. * * @param key {String} the property name * @param getter {Function?} function to read from the DOM * @param setter {Function?} function to copy to the DOM * @param serialize {Function?} function to serialize the value to HTML */ registerProperty(key, get, set, serialize) { if (!this._properties) { this._properties = {}; } if (this._properties[key]) { this.debug( "Overridding property " + key + " in " + this + "[" + this.classname + "]" ); } if (!set) { set = qx.lang.Function.bind(function (value, key) { this._applyProperty(key, value); }, this); qx.log.Logger.deprecatedMethodWarning( this._applyProperty, "The method '_applyProperty' is deprecated. Please use `registerProperty` instead (property '" + key + "' in " + this.classname + ")" ); } this._properties[key] = { get: get, set: set, serialize: serialize, value: undefined }; }, /** * Applies a special property with the given value. * * This property apply routine can be easily overwritten and * extended by sub classes to add new low level features which * are not easily possible using styles and attributes. * * Note that this implementation is for backwards compatibility and * implementations * * @param name {String} Unique property identifier * @param value {var} Any valid value (depends on the property) * @return {qx.html.Element} this object (for chaining support) * @abstract * @deprecated {6.0} please use `registerProperty` instead */ _applyProperty(name, value) { // empty implementation }, /** * Set up the given property. * * @param key {String} the name of the property * @param value {var} the value * @param direct {Boolean?false} Whether the value should be applied * directly (without queuing) * @return {qx.html.Element} this object (for chaining support) */ _setProperty(key, value, direct) { if (!this._properties || !this._properties[key]) { this.registerProperty(key, null, null); } if (this._properties[key].value == value) { return this; } this._properties[key].value = value; // Uncreated elements simply copy all data // on creation. We don't need to remember any // jobs. It is a simple full list copy. if (this._domNode) { // Omit queuing in direct mode if (direct) { this._properties[key].set.call(this, value, key); return this; } // Dynamically create if needed if (!this._propertyJobs) { this._propertyJobs = {}; } // Store job info this._propertyJobs[key] = true; // Register modification qx.html.Element._modified[this.toHashCode()] = this; qx.html.Element._scheduleFlush("element"); } return this; }, /** * Removes the given misc * * @param key {String} the name of the misc * @param direct {Boolean?false} Whether the value should be removed * directly (without queuing) * @return {qx.html.Element} this object (for chaining support) */ _removeProperty(key, direct) { return this._setProperty(key, null, direct); }, /** * Get the value of the given misc. * * @param key {String} name of the misc * @param direct {Boolean?false} Whether the value should be obtained directly (without queuing) * @return {var} the value of the misc */ _getProperty(key, direct) { if (!this._properties || !this._properties[key]) { return null; } var value = this._properties[key].value; if (this._domNode) { if (direct || value === undefined) { var fn = this._properties[key].get; if (fn) { this._properties[key].value = value = fn.call(this, key); } } } return value === undefined ? null : value; }, /* --------------------------------------------------------------------------- EVENT SUPPORT --------------------------------------------------------------------------- */ /** * Adds an event listener to the element. * * @param type {String} Name of the event * @param listener {Function} Function to execute on event * @param self {Object ? null} Reference to the 'this' variable inside * the event listener. When not given, the corresponding dispatcher * usually falls back to a default, which is the target * by convention. Note this is not a strict requirement, i.e. * custom dispatchers can follow a different strategy. * @param capture {Boolean ? false} Whether capturing should be enabled * @return {var} An opaque id, which can be used to remove the event listener * using the {@link #removeListenerById} method. */ addListener(type, listener, self, capture) { if (this.$$disposed) { return null; } if (qx.core.Environment.get("qx.debug")) { var msg = "Failed to add event listener for type '" + type + "'" + " to the target '" + this + "': "; this.assertString(type, msg + "Invalid event type."); this.assertFunction(listener, msg + "Invalid callback function"); if (self !== undefined) { this.assertObject(self, "Invalid context for callback."); } if (capture !== undefined) { this.assertBoolean(capture, "Invalid capture flag."); } } const registerDomEvent = () => { if (this._domNode) { return qx.event.Registration.addListener( this._domNode, type, listener, self, capture ); } if (!this.__eventValues) { this.__eventValues = {}; } if (capture == null) { capture = false; } var unique = qx.event.Manager.getNextUniqueId(); var id = type + (capture ? "|capture|" : "|bubble|") + unique; this.__eventValues[id] = { type: type, listener: listener, self: self, capture: capture, unique: unique }; return id; }; if (qx.Class.supportsEvent(this, type)) { let id = super.addListener(type, listener, self, capture); id.domEventId = registerDomEvent(); return id; } return registerDomEvent(); }, /** * Removes an event listener from the element. * * @param type {String} Name of the event * @param listener {Function} Function to execute on event * @param self {Object} Execution context of given function * @param capture {Boolean ? false} Whether capturing should be enabled * @return {qx.html.Element} this object (for chaining support) */ removeListener(type, listener, self, capture) { if (this.$$disposed) { return null; } if (qx.core.Environment.get("qx.debug")) { var msg = "Failed to remove event listener for type '" + type + "'" + " from the target '" + this + "': "; this.assertString(type, msg + "Invalid event type."); this.assertFunction(listener, msg + "Invalid callback function"); if (self !== undefined) { this.assertObject(self, "Invalid context for callback."); } if (capture !== undefined) { this.assertBoolean(capture, "Invalid capture flag."); } } if (qx.Class.supportsEvent(this, type)) { super.removeListener(type, listener, self, capture); } if (this._domNode) { if ( listener.$$wrapped_callback && listener.$$wrapped_callback[type + this.toHashCode()] ) { var callback = listener.$$wrapped_callback[type + this.toHashCode()]; delete listener.$$wrapped_callback[type + this.toHashCode()]; listener = callback; } qx.event.Registration.removeListener( this._domNode, type, listener, self, capture ); } else { var values = this.__eventValues; var entry; if (capture == null) { capture = false; } for (var key in values) { entry = values[key]; // Optimized for performance: Testing references first if ( entry.listener === listener && entry.self === self && entry.capture === capture && entry.type === type ) { delete values[key]; break; } } } return this; }, /** * Removes an event listener from an event target by an id returned by * {@link #addListener} * * @param id {var} The id returned by {@link #addListener} * @return {qx.html.Element} this object (for chaining support) */ removeListenerById(id) { if (this.$$disposed) { return null; } if (id.domEventId) { if (this._domNode) { qx.event.Registration.removeListenerById( this._domNode, id.domEventId ); } delete id.domEventId; super.removeListenerById(id); } else { if (this._domNode) { qx.event.Registration.removeListenerById(this._domNode, id); } else { delete this.__eventValues[id]; } } return this; }, /** * Check if there are one or more listeners for an event type. * * @param type {String} name of the event type * @param capture {Boolean ? false} Whether to check for listeners of * the bubbling or of the capturing phase. * @return {Boolean} Whether the object has a listener of the given type. */ hasListener(type, capture) { if (this.$$disposed) { return false; } if (qx.Class.supportsEvent(this, type)) { let has = super.hasListener(type, capture); if (has) { return true; } } if (this._domNode) { if (qx.event.Registration.hasListener(this._domNode, type, capture)) { return true; } } else { var values = this.__eventValues; var entry; if (capture == null) { capture = false; } for (var key in values) { entry = values[key]; // Optimized for performance: Testing fast types first if (entry.capture === capture && entry.type === type) { return true; } } } return false; }, /** * Serializes and returns all event listeners attached to this element * @return {Map[]} an Array containing a map for each listener. The maps * have the following keys: * <ul> * <li><code>type</code> (String): Event name</li> * <li><code>handler</code> (Function): Callback function</li> * <li><code>self</code> (Object): The callback's context</li> * <li><code>capture</code> (Boolean): If <code>true</code>, the listener is * attached to the capturing phase</li> * </ul> */ getListeners() { if (this.$$disposed) { return null; } var listeners = []; qx.lang.Array.append( listeners, qx.event.Registration.serializeListeners(this) || [] ); if (this._domNode) { qx.lang.Array.append( listeners, qx.event.Registration.serializeListeners(this._domNode) || [] ); } for (var id in this.__eventValues) { var listenerData = this.__eventValues[id]; listeners.push({ type: listenerData.type, handler: listenerData.listener, self: listenerData.self, capture: listenerData.capture }); } return listeners; } }, /* ***************************************************************************** DESTRUCT ***************************************************************************** */ destruct() { var el = this._domNode; if (el) { qx.event.Registration.getManager(el).removeAllListeners(el); el.$$element = ""; delete el.$$elementObject; el.$$qxObjectHash = ""; delete el.$$qxObject; } if (!qx.core.ObjectRegistry.inShutDown) { var parent = this._parent; if (parent && !parent.$$disposed) { parent.remove(this); } } this._disposeArray("_children"); this._properties = this._propertyJobs = this._domNode = this._parent = this.__eventValues = null; } });