UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

1,940 lines (1,566 loc) 75 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) ************************************************************************ */ /** * High-performance, high-level DOM element creation and management. * * Includes support for HTML and style attributes. Elements also have * got a powerful children and visibility management. * * Processes DOM insertion and modification with advanced logic * to reduce the real transactions. * * From the view of the parent you can use the following children management * methods: * {@link #getChildren}, {@link #indexOf}, {@link #hasChild}, {@link #add}, * {@link #addAt}, {@link #remove}, {@link #removeAt}, {@link #removeAll} * * 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 * * @require(qx.module.Animation) */ qx.Class.define("qx.html.Element", { extend : qx.core.Object, implement : [ qx.core.IDisposable ], /* ***************************************************************************** CONSTRUCTOR ***************************************************************************** */ /** * Creates a new Element * * @param tagName {String?"div"} Tag name of the element to create * @param styles {Map?null} optional map of CSS styles, where the key is the name * of the style and the value is the value to use. * @param attributes {Map?null} optional map of element attributes, where the * key is the name of the attribute and the value is the value to use. */ construct : function(tagName, styles, attributes) { this.base(arguments); // {String} Set tag name this.__nodeName = tagName || "div"; this.__styleValues = styles || null; this.__attribValues = attributes || null; }, /* ***************************************************************************** STATICS ***************************************************************************** */ statics : { /* --------------------------------------------------------------------------- STATIC DATA --------------------------------------------------------------------------- */ /** @type {Boolean} If debugging should be enabled */ DEBUG : false, /** @type {Map} Contains the modified {@link qx.html.Element}s. The key is the hash code. */ _modified : {}, /** @type {Map} Contains the {@link qx.html.Element}s which should get hidden or visible at the next flush. The key is the hash code. */ _visibility : {}, /** @type {Map} Contains the {@link qx.html.Element}s which should scrolled at the next flush */ _scroll : {}, /** @type {Array} List of post actions for elements. The key is the action name. The value the {@link qx.html.Element}. */ _actions : [], /** @type {Map} List of all selections. */ __selection : {}, __focusHandler : null, __mouseCapture : null, /* --------------------------------------------------------------------------- PUBLIC ELEMENT FLUSH --------------------------------------------------------------------------- */ /** * Schedule a deferred element queue flush. If the widget subsystem is used * this method gets overwritten by {@link qx.ui.core.queue.Manager}. * * @param job {String} The job descriptor. Should always be <code>"element"</code>. */ _scheduleFlush : function(job) { qx.html.Element.__deferredCall.schedule(); }, /** * Flush the global modified list */ flush : function() { var obj; if (qx.core.Environment.get("qx.debug")) { if (this.DEBUG) { qx.log.Logger.debug(this, "Flushing elements..."); } } // blur elements, which will be removed var focusHandler = this.__getFocusHandler(); var focusedDomElement = focusHandler.getFocus(); if (focusedDomElement && this.__willBecomeInvisible(focusedDomElement)) { focusHandler.blur(focusedDomElement); } // decativate elements, which will be removed var activeDomElement = focusHandler.getActive(); if (activeDomElement && this.__willBecomeInvisible(activeDomElement)) { qx.bom.Element.deactivate(activeDomElement); } // release capture for elements, which will be removed var captureDomElement = this.__getCaptureElement(); if (captureDomElement && this.__willBecomeInvisible(captureDomElement)) { qx.bom.Element.releaseCapture(captureDomElement); } var later = []; var modified = this._modified; for (var hc in modified) { obj = modified[hc]; // Ignore all hidden elements except iframes // but keep them until they get visible (again) if (obj.__willBeSeeable() || obj.classname == "qx.html.Iframe") { // Separately queue rendered elements if (obj.__element && qx.dom.Hierarchy.isRendered(obj.__element)) { later.push(obj); } // Flush invisible elements first else { if (qx.core.Environment.get("qx.debug")) { if (this.DEBUG) { obj.debug("Flush invisible element"); } } obj.__flush(); } // Cleanup modification list delete modified[hc]; } } for (var i=0, l=later.length; i<l; i++) { obj = later[i]; if (qx.core.Environment.get("qx.debug")) { if (this.DEBUG) { obj.debug("Flush rendered element"); } } obj.__flush(); } // Process visibility list var visibility = this._visibility; for (var hc in visibility) { obj = visibility[hc]; var element = obj.__element; if (!element) { delete visibility[hc]; continue; } if (qx.core.Environment.get("qx.debug")) { if (this.DEBUG) { qx.log.Logger.debug(this, "Switching visibility to: " + obj.__visible); } } // hiding or showing an object and deleting it right after that may // cause an disposed object in the visibility queue [BUG #3607] if (!obj.$$disposed) { element.style.display = obj.__visible ? "" : "none"; // also hide the element (fixed some rendering problem in IE<8 & IE8 quirks) if ((qx.core.Environment.get("engine.name") == "mshtml")) { if (!(document.documentMode >= 8)) { element.style.visibility = obj.__visible ? "visible" : "hidden"; } } } delete visibility[hc]; } // Process scroll list var scroll = this._scroll; for (var hc in scroll) { obj = scroll[hc]; var elem = obj.__element; if (elem && elem.offsetWidth) { var done = true; // ScrollToX if (obj.__lazyScrollX != null) { obj.__element.scrollLeft = obj.__lazyScrollX; delete obj.__lazyScrollX; } // ScrollToY if (obj.__lazyScrollY != null) { obj.__element.scrollTop = obj.__lazyScrollY; delete obj.__lazyScrollY; } // ScrollIntoViewX var intoViewX = obj.__lazyScrollIntoViewX; if (intoViewX != null) { var child = intoViewX.element.getDomElement(); if (child && child.offsetWidth) { qx.bom.element.Scroll.intoViewX(child, elem, intoViewX.align); delete obj.__lazyScrollIntoViewX; } else { done = false; } } // ScrollIntoViewY var intoViewY = obj.__lazyScrollIntoViewY; if (intoViewY != null) { var child = intoViewY.element.getDomElement(); if (child && child.offsetWidth) { qx.bom.element.Scroll.intoViewY(child, elem, intoViewY.align); delete obj.__lazyScrollIntoViewY; } else { done = false; } } // Clear flag if all things are done // Otherwise wait for the next flush if (done) { delete scroll[hc]; } } } var activityEndActions = { "releaseCapture": 1, "blur": 1, "deactivate": 1 }; // Process action list for (var i=0; i<this._actions.length; i++) { var action = this._actions[i]; var element = action.element.__element; if (!element || !activityEndActions[action.type] && !action.element.__willBeSeeable()) { continue; } var args = action.args; args.unshift(element); qx.bom.Element[action.type].apply(qx.bom.Element, args); } this._actions = []; // Process selection for (var hc in this.__selection) { var selection = this.__selection[hc]; var elem = selection.element.__element; if (elem) { qx.bom.Selection.set(elem, selection.start, selection.end); delete this.__selection[hc]; } } // Fire appear/disappear events qx.event.handler.Appear.refresh(); }, /** * Get the focus handler * * @return {qx.event.handler.Focus} The focus handler */ __getFocusHandler : function() { if (!this.__focusHandler) { var eventManager = qx.event.Registration.getManager(window); this.__focusHandler = eventManager.getHandler(qx.event.handler.Focus); } return this.__focusHandler; }, /** * Get the mouse capture element * * @return {Element} The mouse capture DOM element */ __getCaptureElement : function() { if (!this.__mouseCapture) { var eventManager = qx.event.Registration.getManager(window); this.__mouseCapture = eventManager.getDispatcher(qx.event.dispatch.MouseCapture); } return this.__mouseCapture.getCaptureElement(); }, /** * Whether the given DOM element will become invisible after the flush * * @param domElement {Element} The DOM element to check * @return {Boolean} Whether the element will become invisible */ __willBecomeInvisible : function(domElement) { var element = this.fromDomElement(domElement); return element && !element.__willBeSeeable(); }, /** * 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 */ fromDomElement: function(domElement) { if (qx.core.Environment.get("qx.debug")) { qx.core.Assert.assertTrue((!domElement.$$element && !domElement.$$elementObject) || domElement.$$element === domElement.$$elementObject.toHashCode()); } return domElement.$$elementObject; } }, /* ***************************************************************************** MEMBERS ***************************************************************************** */ members : { /* --------------------------------------------------------------------------- PROTECTED HELPERS/DATA --------------------------------------------------------------------------- */ __nodeName : null, /** @type {Element} DOM element of this object */ __element : null, /** @type {qx.ui.core.Widget} the Widget this element is attached to */ __widget : null, /** @type {Boolean} Marker for always visible root nodes (often the body node) */ __root : false, /** @type {Boolean} Whether the element should be included in the render result */ __included : true, /** @type {Boolean} Whether the element should be visible in the render result */ __visible : true, __lazyScrollIntoViewX : null, __lazyScrollIntoViewY : null, __lazyScrollX : null, __lazyScrollY : null, __styleJobs : null, __attribJobs : null, __propertyJobs : null, __styleValues : null, __attribValues : null, __propertyValues : null, __eventValues : null, __children : null, __modifiedChildren : null, __parent : null, /** * Add the element to the global modification list. * */ _scheduleChildrenUpdate : function() { if (this.__modifiedChildren) { return; } this.__modifiedChildren = true; qx.html.Element._modified[this.$$hash] = this; qx.html.Element._scheduleFlush("element"); }, /** * Internal helper to generate the DOM element * * @return {Element} DOM element */ _createDomElement : function() { return qx.dom.Element.create(this.__nodeName); }, /** * Connects a widget to this element, and to the DOM element in this Element. They * remain associated until disposed or disconnectWidget is called * * @param widget {qx.ui.core.Widget} the widget */ connectWidget: function(widget) { if (qx.core.Environment.get("qx.debug")) { qx.core.Assert.assertTrue(!this.__widget || this.__widget === widget); } this.__widget = widget; if (this.__element) { if (qx.core.Environment.get("qx.debug")) { qx.core.Assert.assertTrue((!this.__element.$$widget && !this.__element.$$widgetObject) || (this.__element.$$widgetObject === widget && this.__element.$$widget === widget.toHashCode())); } this.__element.$$widget = widget.toHashCode(); this.__element.$$widgetObject = widget; } 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 widget {qx.ui.core.Widget} the Widget */ disconnectWidget: function(widget) { if (qx.core.Environment.get("qx.debug")) { qx.core.Assert.assertTrue(this.__widget === widget); } delete this.__widget; if (this.__element) { if (qx.core.Environment.get("qx.debug")) { qx.core.Assert.assertTrue((!this.__element.$$widget && !this.__element.$$widgetObject) || (this.__element.$$widgetObject === widget && this.__element.$$widget === widget.toHashCode())); } this.__element.$$widget = ""; delete this.__element.$$widgetObject; } if (qx.core.Environment.get("module.objectid")) { this.updateObjectId(); } }, /** * Connects a DOM element to this Element; if this Element is already connected to a Widget * then the Widget is also connected. * * @param domElement {DOM} the DOM element to associate */ __connectDomElement: function(domElement) { if (qx.core.Environment.get("qx.debug")) { qx.core.Assert.assertTrue(!this.__element || this.__element === domElement); qx.core.Assert.assertTrue((domElement.$$elementObject === this && domElement.$$element === this.toHashCode()) || (!domElement.$$elementObject && !domElement.$$element)); }; this.__element = domElement; domElement.$$elementObject = this; domElement.$$element = this.toHashCode(); if (this.__widget) { domElement.$$widget = this.__widget.toHashCode(); domElement.$$widgetObject = this.__widget; } }, /* --------------------------------------------------------------------------- FLUSH OBJECT --------------------------------------------------------------------------- */ /** * Syncs data of an HtmlElement object to the DOM. * */ __flush : function() { 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.__visible && child.__included && !child.__element) { child.__flush(); } } } if (!this.__element) { this.__connectDomElement(this._createDomElement()); this._copyData(false); if (children && length > 0) { this._insertChildren(); } } else { this._syncData(); if (this.__modifiedChildren) { this._syncChildren(); } } delete this.__modifiedChildren; }, /* --------------------------------------------------------------------------- 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 : function() { var children = this.__children; 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.__element && child.__included) { domElement.appendChild(child.__element); } } this.__element.appendChild(domElement); } else { var domElement = this.__element; for (var i=0; i<length; i++) { child = children[i]; if (child.__element && child.__included) { domElement.appendChild(child.__element); } } } }, /** * Synchronize internal children hierarchy to the DOM. This is used * for further runtime updates after the element has been created * initially. * */ _syncChildren : function() { var dataChildren = this.__children; var dataLength = dataChildren.length; var dataChild; var dataEl; var domParent = this.__element; 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.Element.fromDomElement(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.__element; 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"); } } }, /* --------------------------------------------------------------------------- SUPPORT FOR ATTRIBUTE/STYLE/EVENT FLUSH --------------------------------------------------------------------------- */ updateObjectId: function() { // Copy Object Id if (qx.core.Environment.get("module.objectid")) { var id = null; if (this.__widget && this.__widget.getQxObjectId()) { id = qx.core.Id.getAbsoluteIdOf(this.__widget, true) || null; } this.setAttribute("data-qx-object-id", id, true); } }, /** * 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 */ _copyData : function(fromMarkup) { var elem = this.__element; // Copy attributes var data = this.__attribValues; if (data) { var Attribute = qx.bom.element.Attribute; for (var key in data) { Attribute.set(elem, key, data[key]); } } // Copy styles var data = this.__styleValues; if (data) { var Style = qx.bom.element.Style; if (fromMarkup) { Style.setStyles(elem, data); } else { // Set styles at once which is a lot faster in most browsers // compared to separate modifications of many single style properties. Style.setCss(elem, Style.compile(data)); } } // Copy properties var data = this.__propertyValues; if (data) { for (var key in data) { this._applyProperty(key, data[key]); } } // Attach events var data = this.__eventValues; if (data) { // Import listeners qx.event.Registration.getManager(elem).importListeners(elem, data); // 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; } }, /** * 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 : function() { var elem = this.__element; var Attribute = qx.bom.element.Attribute; var Style = qx.bom.element.Style; // Sync attributes var jobs = this.__attribJobs; if (jobs) { var data = this.__attribValues; if (data) { var value; for (var key in jobs) { value = data[key]; if (value !== undefined) { Attribute.set(elem, key, value); } else { Attribute.reset(elem, key); } } } this.__attribJobs = null; } // Sync styles var jobs = this.__styleJobs; if (jobs) { var data = this.__styleValues; if (data) { var styles = {}; for (var key in jobs) { styles[key] = data[key]; } Style.setStyles(elem, styles); } this.__styleJobs = null; } // Sync misc var jobs = this.__propertyJobs; if (jobs) { var data = this.__propertyValues; if (data) { var value; for (var key in jobs) { this._applyProperty(key, data[key]); } } this.__propertyJobs = null; } // Note: Events are directly kept in sync }, /* --------------------------------------------------------------------------- PRIVATE HELPERS/DATA --------------------------------------------------------------------------- */ /** * 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 : function() { var pa = this; // Any chance to cache this information in the parents? while(pa) { if (pa.__root) { return true; } if (!pa.__included || !pa.__visible) { return false; } pa = pa.__parent; } return false; }, /** * 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 */ __addChildHelper : function(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.__element) { 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 */ __removeChildHelper : function(child) { if (child.__parent !== this) { throw new Error("Has no child: " + child); } // Schedule children update if (this.__element) { 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 */ __moveChildHelper : function(child) { if (child.__parent !== this) { throw new Error("Has no child: " + child); } // Schedule children update if (this.__element) { 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 : function() { 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 : function(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 : function() { 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 : function(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 : function(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 : function(varargs) { if (arguments[1]) { for (var i=0, l=arguments.length; i<l; i++) { this.__addChildHelper(arguments[i]); } this.__children.push.apply(this.__children, arguments); } else { this.__addChildHelper(varargs); this.__children.push(varargs); } // 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 : function(child, index) { this.__addChildHelper(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 : function(childs) { var children = this.__children; if (!children) { return this; } if (arguments[1]) { var child; for (var i=0, l=arguments.length; i<l; i++) { child = arguments[i]; this.__removeChildHelper(child); qx.lang.Array.remove(children, child); } } else { this.__removeChildHelper(childs); qx.lang.Array.remove(children, childs); } // 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 : function(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.__removeChildHelper(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 : function() { var children = this.__children; if (children) { for (var i=0, l=children.length; i<l; i++) { this.__removeChildHelper(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 : function() { 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 : function(parent, index) { parent.__addChildHelper(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 : function(rel) { var parent = rel.__parent; parent.__addChildHelper(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 : function(rel) { var parent = rel.__parent; parent.__addChildHelper(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 : function(index) { var parent = this.__parent; parent.__moveChildHelper(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 : function(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 : function(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 : function() { var parent = this.__parent; if (!parent) { throw new Error("Has no parent to remove from."); } if (!parent.__children) { return this; } parent.__removeChildHelper(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. * * @return {Element|null} The DOM element node, if available. */ getDomElement : function() { return this.__element || null; }, /** * Returns the nodeName of the DOM element. * * @return {String} The node name */ getNodeName : function() { return this.__nodeName; }, /** * Sets the nodeName of the DOM element. * * @param name {String} The node name */ setNodeName : function(name) { this.__nodeName = name; }, /** * Sets the element's root flag, which indicates * whether the element should be a root element or not. * @param root {Boolean} The root flag. */ setRoot : function(root) { this.__root = root; }, /** * Uses existing markup for this element. This is mainly used * to insert pre-built markup blocks into the element hierarchy. * * @param html {String} HTML markup with one root element * which is used as the main element for this instance. * @return {Element} The created DOM element */ useMarkup : function(html) { if (this.__element) { throw new Error("Could not overwrite existing element!"); } // Prepare extraction // We have a IE specific issue with "Unknown error" messages // when we try to use the same DOM node again. I am not sure // why this happens. Would be a good performance improvement, // but does not seem to work. if (qx.core.Environment.get("engine.name") == "mshtml") { var helper = document.createElement("div"); } else { var helper = qx.dom.Element.getHelperElement(); } // Extract first element helper.innerHTML = html; this.useElement(helper.firstChild); return this.__element; }, /** * Uses an existing element instead of creating one. This may be interesting * when the DOM element is directly needed to add content etc. * * @param elem {Element} Element to reuse */ useElement : function(elem) { if (this.__element) { throw new Error("Could not overwrite existing element!"); } // Use incoming element this.__connectDomElement(elem); // Copy currently existing data over to element this._copyData(true); }, /** * Whether the element is focusable (or will be when created) * * @return {Boolean} <code>true</code> when the element is focusable. */ isFocusable : function() { var tabIndex = this.getAttribute("tabIndex"); if (tabIndex >= 1) { return true; } var focusable = qx.event.handler.Focus.FOCUSABLE_ELEMENTS; if (tabIndex >= 0 && focusable[this.__nodeName]) { return true; } return false; }, /** * Set whether the element is selectable. It uses the qooxdoo attribute * qxSelectable with the values 'on' or 'off'. * In webkit, a special css property will be used (-webkit-user-select). * * @param value {Boolean} True, if the element should be selectable. */ setSelectable : function(value) { this.setAttribute("qxSelectable", value ? "on" : "off"); var userSelect = qx.core.Environment.get("css.userselect"); if (userSelect) { this.setStyle(userSelect, value ? "text" : qx.core.Environment.get("css.userselect.none")); } }, /** * Whether the element is natively focusable (or will be when created) * * This ignores the configured tabIndex. * * @return {Boolean} <code>true</code> when the element is focusable. */ isNativelyFocusable : function() { return !!qx.event.handler.Focus.FOCUSABLE_ELEMENTS[this.__nodeName]; }, /* --------------------------------------------------------------------------- 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 {qx.html.Element} this object (for chaining support) */ include : function() { 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 : function() { 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 : function() { return this.__included === true; }, /* --------------------------------------------------------------------------- ANIMATION SUPPORT --------------------------------------------------------------------------- */ /** * Fades in the element. * @param duration {Number} Time in ms. * @return {qx.bom.element.AnimationHandle} The animation handle to react for * the fade animation. */ fadeIn : function(duration) { var col = qxWeb(this.__element); if (col.isPlaying()) { col.stop(); } // create the element right away if (!this.__element) { this.__flush(); col.push(this.__element); } if (this.__element) { col.fadeIn(duration).once("animationEnd", function() { this.show(); qx.html.Element.flush(); }, this); return col.getAnimationHandles()[0]; } }, /** * Fades out the element. * @param duration {Number} Time in ms. * @return {qx.bom.element.AnimationHandle} The animation handle to react for * the fade animation. */ fadeOut : function(duration) { var col = qxWeb(this.__element); if (col.isPlaying()) { col.stop(); } if (this.__element) { col.fadeOut(duration).once("animationEnd", function() { this.hide(); qx.html.Element.flush(); }, this); return col.getAnimationHandles()[0]; } }, /* --------------------------------------------------------------------------- VISIBILITY SUPPORT --------------------------------------------------------------------------- */ /** * Marks the element as 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. * * @return {qx.html.Element} this object (for chaining support) */ show : function() { if (this.__visible) { return this; } if (this.__element) { qx.html.Element._visibility[this.$$hash] = this; qx.html.Element._scheduleFlush("element"); } // Must be sure that the element gets included into the DOM. if (this.__parent) { this.__parent._scheduleChildrenUpdate(); } delete this.__visible; return this; }, /** * Marks the element as hidden which means it will kept in DOM (if it * is already there, but configured hidden using a CSS style of display=none). * * @return {qx.html.Element} this object (for chaining support) */ hide : function() { if (!this.__visible) { return this; } if (this.__element) { qx.html.Element._visibility[this.$$hash] = this; qx.html.Element._scheduleFlush("element"); } this.__visible = false; return this; }, /** * Whether the element is visible. * * Please note: This does not control the visibility or parent inclusion recursively. * * @return {Boolean} Returns <code>true</code> when the element is configured * to be visible. */ isVisible : function() { return this.__visible === true; }, /* --------------------------------------------------------------------------- SCROLL SUPPORT --------------------------------------------------------------------------- */ /** * Scrolls the given child element into view. Only scrolls children. * Do not influence elements on top of this element. * * If the element is currently invisible it gets scrolled automatically * at the next time it is visible again (queued). * * @param elem {qx.html.Element} The element to scroll into the viewport. * @param align {String?null} Alignment of the element. Allowed values: * <code>left</code> or <code>right</code>. Could also be null. * Without a given alignment the method tries to scroll the widget * with the minimum effort needed. * @param direct {Boolean?true} Whether the execution should be made * directly when possible */ scrollChildIntoViewX : function(elem, align, direct) { var thisEl = this.__element; var childEl = elem.getDomElement(); if (direct !== false && thisEl && thisEl.offsetWidth && childEl && childEl.offsetWidth) { qx.bom.element.Scroll.intoViewX(childEl, thisEl, align); } else { this.__lazyScrollIntoViewX = { element : elem, align : align }; qx.html.Element._scroll[this.$$hash] = this; qx.html.Element._scheduleFlush("element"); } delete this.__lazyScrollX; }, /** * Scrolls the given child element into view. Only scrolls children. * Do not influence elements on top of this element. * * If the element is currently invisible it gets scrolled automatically * at the next time it is visible again (queued). * * @param elem {qx.html.Element} The element to scroll into the viewport. * @param align {String?null} Alignment of the element. Allowed values: * <code>top</code> or <code>bottom</code>. Could also be null. * Without a given alignment the method tries to scroll the widget * with the minimum effort needed. * @param direct {Boolean?true} Whether the execution should be made * directly when possible */ scrollChildIntoViewY : function(elem, align, direct) { var thisEl = this.__element; var childEl = elem.getDomElement(); if (direct !== false && thisEl && thisEl.offsetWidth && childEl && childEl.offsetWidth) { qx.bom.element.Scroll.intoViewY(childEl, thisEl, align); } else { this.__lazyScrollIntoViewY = { element : elem, align : align }; qx.html.Element._scroll[this.$$hash] = this; qx.html.Element._scheduleFlush("element"); } delete this.__lazyScrollY; }, /** * Scrolls the element to the given left position. * * @param x {Integer} Horizontal scroll position * @param lazy {Boolean?false} Whether the scrolling should be performed * during element flush. */ scrollToX : function(x, lazy) { var thisEl = this.__element; if (lazy !== true && thisEl && thisEl.offsetWidth) { thisEl.scrollLeft = x; delete this.__lazyScrollX; } else { this.__lazyScrollX = x; qx.html.Element._scroll[this.$$hash] = this; qx.html.Element._scheduleFlush("element"); } delete this.__lazyScrollIntoViewX; }, /** * Get the horizontal scroll position. * * @return {Integer} Horizontal scroll position */ getScrollX : function() { var thisEl = this.__element; if (thisEl) { return thisEl.scrollLeft; } return this.__lazyScrollX || 0; }, /** * Scrolls the element to the given top position. * * @param y {Integer} Vertical scroll position * @param lazy {Boolean?false} Whether the scrolling should be performed * during element flush. */ scrollToY : function(y, lazy) { var thisEl = this.__element; if (lazy !== true && thisEl && thisEl.offsetWidth) { thisEl.scrollTop = y; delete this.__lazyScrollY; } else { this.__lazyScrollY = y; qx.html.Element._scroll[this.$$hash] = this; qx.html.Element._scheduleFlush("element"); } delete this.__lazyScrollIntoViewY; }, /** * Get the vertical scroll position. * * @return