@qooxdoo/framework
Version:
The JS Framework for Coders
1,940 lines (1,566 loc) • 75 kB
JavaScript
/* ************************************************************************
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