Source: delite/Container.js

/** @module delite/Container */
define([
	"dcl/dcl",
	"./Widget"
], function (dcl, Widget) {

	/**
	 * Base class for widgets that contain content.
	 * Useful both for widgets that contain free-form markup (ex: ContentPane),
	 * and widgets that contain an ordered list of children (ex: Toolbar).
	 *
	 * Note that Container is not meant to be used for widgets that just internally create child
	 * widgets (ex: a StarRating widget creates stars), but rather for when the widget has children from
	 * the application's perspective (i.e. from the perspective of the widget *user* rather
	 * than the widget *author*).
	 *
	 * @mixin module:delite/Container
	 * @augments module:delite/Widget
	 */
	return dcl(Widget, /** @lends module:delite/Container# */{
		/**
		 * Designates where children of the source DOM node will be placed,
		 * and also the target for nodes inserted via `.appendChild()`, `.insertBefore()`, etc.
		 * "Children" in this case refers to both DOM nodes and widgets.
		 *
		 * @member {Element}
		 * @default Widget root node itself.
		 * @protected
		 */
		containerNode: undefined,

		render: dcl.advise({
			before: function () {
				// Save original markup to put into this.containerNode.
				var srcDom = this._srcDom = this.ownerDocument.createDocumentFragment();
				while (this.firstChild) {
					srcDom.appendChild(this.firstChild);
				}
			},

			after: function () {
				if (!this.containerNode) {
					// All widgets with descendants must set containerNode.
					this.containerNode = this;
				}

				// Put original markup into this.containerNode.  Note that appendChild() on a DocumentFragment will
				// loop through all the Elements in the document fragment, adding each one.
				this.containerNode.appendChild(this._srcDom);
			}
		}),

		appendChild: dcl.superCall(function (sup) {
			return function (child) {
				if (this.created) {
					var res = sup.call(this.containerNode, child);
					this.onAddChild(child);
					return res;
				} else {
					return sup.call(this, child);
				}
			};
		}),

		insertBefore: dcl.superCall(function (sup) {
			return function (newChild, refChild) {
				if (this.created) {
					var res = sup.call(this.containerNode, newChild, refChild);
					this.onAddChild(newChild);
					return res;
				} else {
					return sup.call(this, newChild, refChild);
				}
			};
		}),

		/**
		 * Callback whenever a child element is added to this widget via `appendChild()`, `insertBefore()`,
		 * or a method like `addChild()` that internally calls `appendChild()` and/or `insertBefore()`.
		 * @param {Element} node
		 */
		onAddChild: function (node) {
			// If I've been started but the child widget hasn't been started,
			// start it now.  Make sure to do this after widget has been
			// inserted into the DOM tree, so it can see that it's being controlled by me,
			// so it doesn't try to size itself.
			if (this.started && !node.started && node.startup) {
				node.startup();
			}
		},

		/**
		 * Inserts the specified Element at the specified index.
		 * For example, `.addChild(node, 3)` sets this widget's fourth child to node.
		 * @param {Element} node - Element to add as a child.
		 * @param {number} [insertIndex] - Position the child as at the specified position relative to other children.
		 */
		addChild: function (node, insertIndex) {
			// Note: insertBefore(node, null) equivalent to appendChild().  Null arg is needed (only) on IE.
			var cn = this.containerNode, nextSibling = cn.children[insertIndex];
			cn.insertBefore(node, nextSibling || null);
		},

		/**
		 * Detaches the specified node instance from this widget but does
		 * not destroy it.  You can also pass in an integer indicating
		 * the index within the container to remove (ie, removeChild(5) removes the sixth node).
		 * @param {Element|number} node
		 */
		removeChild: function (node) {
			if (typeof node === "number") {
				node = this.getChildren()[node];
			}

			if (node && node.parentNode) {
				HTMLElement.prototype.removeChild.call(node.parentNode, node); // detach but don't destroy
			}
		},

		/**
		 * Returns all direct children of this widget, i.e. all widgets or DOM nodes underneath
		 * `this.containerNode`.  Note that it does not return all
		 * descendants, but rather just direct children.
		 *
		 * The result intentionally excludes element outside off `this.containerNode`.  So, it is different than
		 * accessing the `children` or `childNode` properties.
		 *
		 * @returns {Element[]}
		 */
		getChildren: function () {
			// use Array.prototype.slice to transform the live HTMLCollection into an Array
			return Array.prototype.slice.call(this.containerNode.children);
		},

		/**
		 * Returns true if widget has child nodes, i.e. if `this.containerNode` contains widgets.
		 * @returns {boolean}
		 */
		hasChildren: function () {
			return this.getChildren().length > 0;
		},

		/**
		 * Returns the index of the child in this container or -1 if not found.
		 * @param {Element} child
		 * @returns {number}
		 */
		getIndexOfChild: function (child) {
			return this.getChildren().indexOf(child);
		}
	});
});