- /**
- * Extension enabling a Widget to be a parent of another Widget.
- *
- * @module widget-parent
- */
-
- var Lang = Y.Lang,
- RENDERED = "rendered",
- BOUNDING_BOX = "boundingBox";
-
- /**
- * Widget extension providing functionality enabling a Widget to be a
- * parent of another Widget.
- *
- * <p>In addition to the set of attributes supported by WidgetParent, the constructor
- * configuration object can also contain a <code>children</code> which can be used
- * to add child widgets to the parent during construction. The <code>children</code>
- * property is an array of either child widget instances or child widget configuration
- * objects, and is sugar for the <a href="#method_add">add</a> method. See the
- * <a href="#method_add">add</a> for details on the structure of the child widget
- * configuration object.
- * @class WidgetParent
- * @constructor
- * @uses ArrayList
- * @param {Object} config User configuration object.
- */
- function Parent(config) {
-
- /**
- * Fires when a Widget is add as a child. The event object will have a
- * 'child' property that returns a reference to the child Widget, as well
- * as an 'index' property that returns a reference to the index specified
- * when the add() method was called.
- * <p>
- * Subscribers to the "on" moment of this event, will be notified
- * before a child is added.
- * </p>
- * <p>
- * Subscribers to the "after" moment of this event, will be notified
- * after a child is added.
- * </p>
- *
- * @event addChild
- * @preventable _defAddChildFn
- * @param {EventFacade} e The Event Facade
- */
- this.publish("addChild", {
- defaultTargetOnly: true,
- defaultFn: this._defAddChildFn
- });
-
-
- /**
- * Fires when a child Widget is removed. The event object will have a
- * 'child' property that returns a reference to the child Widget, as well
- * as an 'index' property that returns a reference child's ordinal position.
- * <p>
- * Subscribers to the "on" moment of this event, will be notified
- * before a child is removed.
- * </p>
- * <p>
- * Subscribers to the "after" moment of this event, will be notified
- * after a child is removed.
- * </p>
- *
- * @event removeChild
- * @preventable _defRemoveChildFn
- * @param {EventFacade} e The Event Facade
- */
- this.publish("removeChild", {
- defaultTargetOnly: true,
- defaultFn: this._defRemoveChildFn
- });
-
- this._items = [];
-
- var children,
- handle;
-
- if (config && config.children) {
-
- children = config.children;
-
- handle = this.after("initializedChange", function (e) {
- this._add(children);
- handle.detach();
- });
-
- }
-
- // Widget method overlap
- Y.after(this._renderChildren, this, "renderUI");
- Y.after(this._bindUIParent, this, "bindUI");
-
- this.after("selectionChange", this._afterSelectionChange);
- this.after("selectedChange", this._afterParentSelectedChange);
- this.after("activeDescendantChange", this._afterActiveDescendantChange);
-
- this._hDestroyChild = this.after("*:destroy", this._afterDestroyChild);
- this.after("*:focusedChange", this._updateActiveDescendant);
-
- }
-
- Parent.ATTRS = {
-
- /**
- * @attribute defaultChildType
- * @type {String|Object}
- *
- * @description String representing the default type of the children
- * managed by this Widget. Can also supply default type as a constructor
- * reference.
- */
- defaultChildType: {
- setter: function (val) {
-
- var returnVal = Y.Attribute.INVALID_VALUE,
- FnConstructor = Lang.isString(val) ? Y[val] : val;
-
- if (Lang.isFunction(FnConstructor)) {
- returnVal = FnConstructor;
- }
-
- return returnVal;
- }
- },
-
- /**
- * @attribute activeDescendant
- * @type Widget
- * @readOnly
- *
- * @description Returns the Widget's currently focused descendant Widget.
- */
- activeDescendant: {
- readOnly: true
- },
-
- /**
- * @attribute multiple
- * @type Boolean
- * @default false
- * @writeOnce
- *
- * @description Boolean indicating if multiple children can be selected at
- * once. Whether or not multiple selection is enabled is always delegated
- * to the value of the <code>multiple</code> attribute of the root widget
- * in the object hierarchy.
- */
- multiple: {
- value: false,
- validator: Lang.isBoolean,
- writeOnce: true,
- getter: function (value) {
- var root = this.get("root");
- return (root && root != this) ? root.get("multiple") : value;
- }
- },
-
-
- /**
- * @attribute selection
- * @type {ArrayList|Widget}
- * @readOnly
- *
- * @description Returns the currently selected child Widget. If the
- * <code>mulitple</code> attribte is set to <code>true</code> will
- * return an Y.ArrayList instance containing the currently selected
- * children. If no children are selected, will return null.
- */
- selection: {
- readOnly: true,
- setter: "_setSelection",
- getter: function (value) {
- var selection = Lang.isArray(value) ?
- (new Y.ArrayList(value)) : value;
- return selection;
- }
- },
-
- selected: {
- setter: function (value) {
-
- // Enforces selection behavior on for parent Widgets. Parent's
- // selected attribute can be set to the following:
- // 0 - Not selected
- // 1 - Fully selected (all children are selected). In order for
- // all children to be selected, multiple selection must be
- // enabled. Therefore, you cannot set the "selected" attribute
- // on a parent Widget to 1 unless multiple selection is enabled.
- // 2 - Partially selected, meaning one ore more (but not all)
- // children are selected.
-
- var returnVal = value;
-
- if (value === 1 && !this.get("multiple")) {
- Y.log('The selected attribute can only be set to 1 if the "multiple" attribute is set to true.', "error", "widget");
- returnVal = Y.Attribute.INVALID_VALUE;
- }
-
- return returnVal;
- }
- }
-
- };
-
- Parent.prototype = {
-
- /**
- * The destructor implementation for Parent widgets. Destroys all children.
- * @method destructor
- */
- destructor: function() {
- this._destroyChildren();
- },
-
- /**
- * Destroy event listener for each child Widget, responsible for removing
- * the destroyed child Widget from the parent's internal array of children
- * (_items property).
- *
- * @method _afterDestroyChild
- * @protected
- * @param {EventFacade} event The event facade for the attribute change.
- */
- _afterDestroyChild: function (event) {
- var child = event.target;
-
- if (child.get("parent") == this) {
- child.remove();
- }
- },
-
- /**
- * Attribute change listener for the <code>selection</code>
- * attribute, responsible for setting the value of the
- * parent's <code>selected</code> attribute.
- *
- * @method _afterSelectionChange
- * @protected
- * @param {EventFacade} event The event facade for the attribute change.
- */
- _afterSelectionChange: function (event) {
-
- if (event.target == this && event.src != this) {
-
- var selection = event.newVal,
- selectedVal = 0; // Not selected
-
-
- if (selection) {
-
- selectedVal = 2; // Assume partially selected, confirm otherwise
-
-
- if (Y.instanceOf(selection, Y.ArrayList) &&
- (selection.size() === this.size())) {
-
- selectedVal = 1; // Fully selected
-
- }
-
- }
-
- this.set("selected", selectedVal, { src: this });
-
- }
- },
-
-
- /**
- * Attribute change listener for the <code>activeDescendant</code>
- * attribute, responsible for setting the value of the
- * parent's <code>activeDescendant</code> attribute.
- *
- * @method _afterActiveDescendantChange
- * @protected
- * @param {EventFacade} event The event facade for the attribute change.
- */
- _afterActiveDescendantChange: function (event) {
- var parent = this.get("parent");
-
- if (parent) {
- parent._set("activeDescendant", event.newVal);
- }
- },
-
- /**
- * Attribute change listener for the <code>selected</code>
- * attribute, responsible for syncing the selected state of all children to
- * match that of their parent Widget.
- *
- *
- * @method _afterParentSelectedChange
- * @protected
- * @param {EventFacade} event The event facade for the attribute change.
- */
- _afterParentSelectedChange: function (event) {
-
- var value = event.newVal;
-
- if (this == event.target && event.src != this &&
- (value === 0 || value === 1)) {
-
- this.each(function (child) {
-
- // Specify the source of this change as the parent so that
- // value of the parent's "selection" attribute isn't
- // recalculated
-
- child.set("selected", value, { src: this });
-
- }, this);
-
- }
-
- },
-
-
- /**
- * Default setter for <code>selection</code> attribute changes.
- *
- * @method _setSelection
- * @protected
- * @param child {Widget|Array} Widget or Array of Widget instances.
- * @return {Widget|Array} Widget or Array of Widget instances.
- */
- _setSelection: function (child) {
-
- var selection = null,
- selected;
-
- if (this.get("multiple") && !this.isEmpty()) {
-
- selected = [];
-
- this.each(function (v) {
-
- if (v.get("selected") > 0) {
- selected.push(v);
- }
-
- });
-
- if (selected.length > 0) {
- selection = selected;
- }
-
- }
- else {
-
- if (child.get("selected") > 0) {
- selection = child;
- }
-
- }
-
- return selection;
-
- },
-
-
- /**
- * Attribute change listener for the <code>selected</code>
- * attribute of child Widgets, responsible for setting the value of the
- * parent's <code>selection</code> attribute.
- *
- * @method _updateSelection
- * @protected
- * @param {EventFacade} event The event facade for the attribute change.
- */
- _updateSelection: function (event) {
-
- var child = event.target,
- selection;
-
- if (child.get("parent") == this) {
-
- if (event.src != "_updateSelection") {
-
- selection = this.get("selection");
-
- if (!this.get("multiple") && selection && event.newVal > 0) {
-
- // Deselect the previously selected child.
- // Set src equal to the current context to prevent
- // unnecessary re-calculation of the selection.
-
- selection.set("selected", 0, { src: "_updateSelection" });
-
- }
-
- this._set("selection", child);
-
- }
-
- if (event.src == this) {
- this._set("selection", child, { src: this });
- }
-
- }
-
- },
-
- /**
- * Attribute change listener for the <code>focused</code>
- * attribute of child Widgets, responsible for setting the value of the
- * parent's <code>activeDescendant</code> attribute.
- *
- * @method _updateActiveDescendant
- * @protected
- * @param {EventFacade} event The event facade for the attribute change.
- */
- _updateActiveDescendant: function (event) {
- var activeDescendant = (event.newVal === true) ? event.target : null;
- this._set("activeDescendant", activeDescendant);
- },
-
- /**
- * Creates an instance of a child Widget using the specified configuration.
- * By default Widget instances will be created of the type specified
- * by the <code>defaultChildType</code> attribute. Types can be explicitly
- * defined via the <code>childType</code> property of the configuration object
- * literal. The use of the <code>type</code> property has been deprecated, but
- * will still be used as a fallback, if <code>childType</code> is not defined,
- * for backwards compatibility.
- *
- * @method _createChild
- * @protected
- * @param config {Object} Object literal representing the configuration
- * used to create an instance of a Widget.
- */
- _createChild: function (config) {
-
- var defaultType = this.get("defaultChildType"),
- altType = config.childType || config.type,
- child,
- Fn,
- FnConstructor;
-
- if (altType) {
- Fn = Lang.isString(altType) ? Y[altType] : altType;
- }
-
- if (Lang.isFunction(Fn)) {
- FnConstructor = Fn;
- } else if (defaultType) {
- // defaultType is normalized to a function in it's setter
- FnConstructor = defaultType;
- }
-
- if (FnConstructor) {
- child = new FnConstructor(config);
- } else {
- Y.error("Could not create a child instance because its constructor is either undefined or invalid.");
- }
-
- return child;
-
- },
-
- /**
- * Default addChild handler
- *
- * @method _defAddChildFn
- * @protected
- * @param event {EventFacade} The Event object
- * @param child {Widget} The Widget instance, or configuration
- * object for the Widget to be added as a child.
- * @param index {Number} Number representing the position at
- * which the child will be inserted.
- */
- _defAddChildFn: function (event) {
-
- var child = event.child,
- index = event.index,
- children = this._items;
-
- if (child.get("parent")) {
- child.remove();
- }
-
- if (Lang.isNumber(index)) {
- children.splice(index, 0, child);
- }
- else {
- children.push(child);
- }
-
- child._set("parent", this);
- child.addTarget(this);
-
- // Update index in case it got normalized after addition
- // (e.g. user passed in 10, and there are only 3 items, the actual index would be 3. We don't want to pass 10 around in the event facade).
- event.index = child.get("index");
-
- // TO DO: Remove in favor of using event bubbling
- child.after("selectedChange", Y.bind(this._updateSelection, this));
- },
-
-
- /**
- * Default removeChild handler
- *
- * @method _defRemoveChildFn
- * @protected
- * @param event {EventFacade} The Event object
- * @param child {Widget} The Widget instance to be removed.
- * @param index {Number} Number representing the index of the Widget to
- * be removed.
- */
- _defRemoveChildFn: function (event) {
-
- var child = event.child,
- index = event.index,
- children = this._items;
-
- if (child.get("focused")) {
- child.blur(); // focused is readOnly, so use the public i/f to unset it
- }
-
- if (child.get("selected")) {
- child.set("selected", 0);
- }
-
- children.splice(index, 1);
-
- child.removeTarget(this);
- child._oldParent = child.get("parent");
- child._set("parent", null);
- },
-
- /**
- * @method _add
- * @protected
- * @param child {Widget|Object} The Widget instance, or configuration
- * object for the Widget to be added as a child.
- * @param child {Array} Array of Widget instances, or configuration
- * objects for the Widgets to be added as a children.
- * @param index {Number} (Optional.) Number representing the position at
- * which the child should be inserted.
- * @description Adds a Widget as a child. If the specified Widget already
- * has a parent it will be removed from its current parent before
- * being added as a child.
- * @return {Widget|Array} Successfully added Widget or Array containing the
- * successfully added Widget instance(s). If no children where added, will
- * will return undefined.
- */
- _add: function (child, index) {
-
- var children,
- oChild,
- returnVal;
-
-
- if (Lang.isArray(child)) {
-
- children = [];
-
- Y.each(child, function (v, k) {
-
- oChild = this._add(v, (index + k));
-
- if (oChild) {
- children.push(oChild);
- }
-
- }, this);
-
-
- if (children.length > 0) {
- returnVal = children;
- }
-
- }
- else {
-
- if (Y.instanceOf(child, Y.Widget)) {
- oChild = child;
- }
- else {
- oChild = this._createChild(child);
- }
-
- if (oChild && this.fire("addChild", { child: oChild, index: index })) {
- returnVal = oChild;
- }
-
- }
-
- return returnVal;
-
- },
-
-
- /**
- * @method add
- * @param child {Widget|Object} The Widget instance, or configuration
- * object for the Widget to be added as a child. The configuration object
- * for the child can include a <code>childType</code> property, which is either
- * a constructor function or a string which names a constructor function on the
- * Y instance (e.g. "Tab" would refer to Y.Tab) (<code>childType</code> used to be
- * named <code>type</code>, support for which has been deprecated, but is still
- * maintained for backward compatibility. <code>childType</code> takes precedence
- * over <code>type</code> if both are defined.
- * @param child {Array} Array of Widget instances, or configuration
- * objects for the Widgets to be added as a children.
- * @param index {Number} (Optional.) Number representing the position at
- * which the child should be inserted.
- * @description Adds a Widget as a child. If the specified Widget already
- * has a parent it will be removed from its current parent before
- * being added as a child.
- * @return {ArrayList} Y.ArrayList containing the successfully added
- * Widget instance(s). If no children where added, will return an empty
- * Y.ArrayList instance.
- */
- add: function () {
-
- var added = this._add.apply(this, arguments),
- children = added ? (Lang.isArray(added) ? added : [added]) : [];
-
- return (new Y.ArrayList(children));
-
- },
-
-
- /**
- * @method remove
- * @param index {Number} (Optional.) Number representing the index of the
- * child to be removed.
- * @description Removes the Widget from its parent. Optionally, can remove
- * a child by specifying its index.
- * @return {Widget} Widget instance that was successfully removed, otherwise
- * undefined.
- */
- remove: function (index) {
-
- var child = this._items[index],
- returnVal;
-
- if (child && this.fire("removeChild", { child: child, index: index })) {
- returnVal = child;
- }
-
- return returnVal;
-
- },
-
-
- /**
- * @method removeAll
- * @description Removes all of the children from the Widget.
- * @return {ArrayList} Y.ArrayList instance containing Widgets that were
- * successfully removed. If no children where removed, will return an empty
- * Y.ArrayList instance.
- */
- removeAll: function () {
-
- var removed = [],
- child;
-
- Y.each(this._items.concat(), function () {
-
- child = this.remove(0);
-
- if (child) {
- removed.push(child);
- }
-
- }, this);
-
- return (new Y.ArrayList(removed));
-
- },
-
- /**
- * Selects the child at the given index (zero-based).
- *
- * @method selectChild
- * @param {Number} i the index of the child to be selected
- */
- selectChild: function(i) {
- this.item(i).set('selected', 1);
- },
-
- /**
- * Selects all children.
- *
- * @method selectAll
- */
- selectAll: function () {
- this.set("selected", 1);
- },
-
- /**
- * Deselects all children.
- *
- * @method deselectAll
- */
- deselectAll: function () {
- this.set("selected", 0);
- },
-
- /**
- * Updates the UI in response to a child being added.
- *
- * @method _uiAddChild
- * @protected
- * @param child {Widget} The child Widget instance to render.
- * @param parentNode {Object} The Node under which the
- * child Widget is to be rendered.
- */
- _uiAddChild: function (child, parentNode) {
-
- child.render(parentNode);
-
- // TODO: Ideally this should be in Child's render UI.
-
- var childBB = child.get("boundingBox"),
- siblingBB,
- nextSibling = child.next(false),
- prevSibling;
-
- // Insert or Append to last child.
-
- // Avoiding index, and using the current sibling
- // state (which should be accurate), means we don't have
- // to worry about decorator elements which may be added
- // to the _childContainer node.
-
- if (nextSibling && nextSibling.get(RENDERED)) {
-
- siblingBB = nextSibling.get(BOUNDING_BOX);
- siblingBB.insert(childBB, "before");
-
- } else {
-
- prevSibling = child.previous(false);
-
- if (prevSibling && prevSibling.get(RENDERED)) {
-
- siblingBB = prevSibling.get(BOUNDING_BOX);
- siblingBB.insert(childBB, "after");
-
- } else if (!parentNode.contains(childBB)) {
-
- // Based on pull request from andreas-karlsson
- // https://github.com/yui/yui3/pull/25#issuecomment-2103536
-
- // Account for case where a child was rendered independently of the
- // parent-child framework, to a node outside of the parentNode,
- // and there are no siblings.
-
- parentNode.appendChild(childBB);
- }
- }
-
- },
-
- /**
- * Updates the UI in response to a child being removed.
- *
- * @method _uiRemoveChild
- * @protected
- * @param child {Widget} The child Widget instance to render.
- */
- _uiRemoveChild: function (child) {
- child.get("boundingBox").remove();
- },
-
- _afterAddChild: function (event) {
- var child = event.child;
-
- if (child.get("parent") == this) {
- this._uiAddChild(child, this._childrenContainer);
- }
- },
-
- _afterRemoveChild: function (event) {
- var child = event.child;
-
- if (child._oldParent == this) {
- this._uiRemoveChild(child);
- }
- },
-
- /**
- * Sets up DOM and CustomEvent listeners for the parent widget.
- * <p>
- * This method in invoked after bindUI is invoked for the Widget class
- * using YUI's aop infrastructure.
- * </p>
- *
- * @method _bindUIParent
- * @protected
- */
- _bindUIParent: function () {
- this.after("addChild", this._afterAddChild);
- this.after("removeChild", this._afterRemoveChild);
- },
-
- /**
- * Renders all child Widgets for the parent.
- * <p>
- * This method in invoked after renderUI is invoked for the Widget class
- * using YUI's aop infrastructure.
- * </p>
- * @method _renderChildren
- * @protected
- */
- _renderChildren: function () {
-
- /**
- * <p>By default WidgetParent will render it's children to the parent's content box.</p>
- *
- * <p>If the children need to be rendered somewhere else, the _childrenContainer property
- * can be set to the Node which the children should be rendered to. This property should be
- * set before the _renderChildren method is invoked, ideally in your renderUI method,
- * as soon as you create the element to be rendered to.</p>
- *
- * @protected
- * @property _childrenContainer
- * @value The content box
- * @type Node
- */
- var renderTo = this._childrenContainer || this.get("contentBox");
-
- this._childrenContainer = renderTo;
-
- this.each(function (child) {
- child.render(renderTo);
- });
- },
-
- /**
- * Destroys all child Widgets for the parent.
- * <p>
- * This method is invoked before the destructor is invoked for the Widget
- * class using YUI's aop infrastructure.
- * </p>
- * @method _destroyChildren
- * @protected
- */
- _destroyChildren: function () {
-
- // Detach the handler responsible for removing children in
- // response to destroying them since:
- // 1) It is unnecessary/inefficient at this point since we are doing
- // a batch destroy of all children.
- // 2) Removing each child will affect our ability to iterate the
- // children since the size of _items will be changing as we
- // iterate.
- this._hDestroyChild.detach();
-
- // Need to clone the _items array since
- this.each(function (child) {
- child.destroy();
- });
- }
-
- };
-
- Y.augment(Parent, Y.ArrayList);
-
- Y.WidgetParent = Parent;
-
-