2 Copyright (c) 2010, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.com/yui/license.html
8 YUI.add('widget-parent', function(Y) {
11 * Extension enabling a Widget to be a parent of another Widget.
13 * @module widget-parent
19 * Widget extension providing functionality enabling a Widget to be a
20 * parent of another Widget.
22 * <p>In addition to the set of attributes supported by WidgetParent, the constructor
23 * configuration object can also contain a <code>children</code> which can be used
24 * to add child widgets to the parent during construction. The <code>children</code>
25 * property is an array of either child widget instances or child widget configuration
26 * objects, and is sugar for the <a href="#method_add">add</a> method. See the
27 * <a href="#method_add">add</a> for details on the structure of the child widget
28 * configuration object.
32 * @param {Object} config User configuration object.
34 function Parent(config) {
37 * Fires when a Widget is add as a child. The event object will have a
38 * 'child' property that returns a reference to the child Widget, as well
39 * as an 'index' property that returns a reference to the index specified
40 * when the add() method was called.
42 * Subscribers to the "on" moment of this event, will be notified
43 * before a child is added.
46 * Subscribers to the "after" moment of this event, will be notified
47 * after a child is added.
51 * @preventable _defAddChildFn
52 * @param {EventFacade} e The Event Facade
54 this.publish("addChild", {
55 defaultTargetOnly: true,
56 defaultFn: this._defAddChildFn
61 * Fires when a child Widget is removed. The event object will have a
62 * 'child' property that returns a reference to the child Widget, as well
63 * as an 'index' property that returns a reference child's ordinal position.
65 * Subscribers to the "on" moment of this event, will be notified
66 * before a child is removed.
69 * Subscribers to the "after" moment of this event, will be notified
70 * after a child is removed.
74 * @preventable _defRemoveChildFn
75 * @param {EventFacade} e The Event Facade
77 this.publish("removeChild", {
78 defaultTargetOnly: true,
79 defaultFn: this._defRemoveChildFn
87 if (config && config.children) {
89 children = config.children;
91 handle = this.after("initializedChange", function (e) {
98 // Widget method overlap
99 Y.after(this._renderChildren, this, "renderUI");
100 Y.after(this._bindUIParent, this, "bindUI");
101 Y.before(this._destroyChildren, this, "destructor");
103 this.after("selectionChange", this._afterSelectionChange);
104 this.after("selectedChange", this._afterParentSelectedChange);
105 this.after("activeDescendantChange", this._afterActiveDescendantChange);
107 this._hDestroyChild = this.after("*:destroy", this._afterDestroyChild);
108 this.after("*:focusedChange", this._updateActiveDescendant);
115 * @attribute defaultChildType
116 * @type {String|Object}
118 * @description String representing the default type of the children
119 * managed by this Widget. Can also supply default type as a constructor
123 setter: function (val) {
125 var returnVal = Y.Attribute.INVALID_VALUE,
126 FnConstructor = Lang.isString(val) ? Y[val] : val;
128 if (Lang.isFunction(FnConstructor)) {
129 returnVal = FnConstructor;
137 * @attribute activeDescendant
141 * @description Returns the Widget's currently focused descendant Widget.
148 * @attribute multiple
153 * @description Boolean indicating if multiple children can be selected at
154 * once. Whether or not multiple selection is enabled is always delegated
155 * to the value of the <code>multiple</code> attribute of the root widget
156 * in the object hierarchy.
160 validator: Lang.isBoolean,
162 getter: function (value) {
163 var root = this.get("root");
164 return (root && root != this) ? root.get("multiple") : value;
170 * @attribute selection
171 * @type {Y.ArrayList|Widget}
174 * @description Returns the currently selected child Widget. If the
175 * <code>mulitple</code> attribte is set to <code>true</code> will
176 * return an Y.ArrayList instance containing the currently selected
177 * children. If no children are selected, will return null.
181 setter: "_setSelection",
182 getter: function (value) {
183 var selection = Lang.isArray(value) ?
184 (new Y.ArrayList(value)) : value;
190 setter: function (value) {
192 // Enforces selection behavior on for parent Widgets. Parent's
193 // selected attribute can be set to the following:
195 // 1 - Fully selected (all children are selected). In order for
196 // all children to be selected, multiple selection must be
197 // enabled. Therefore, you cannot set the "selected" attribute
198 // on a parent Widget to 1 unless multiple selection is enabled.
199 // 2 - Partially selected, meaning one ore more (but not all)
200 // children are selected.
202 var returnVal = value;
204 if (value === 1 && !this.get("multiple")) {
205 returnVal = Y.Attribute.INVALID_VALUE;
217 * Destroy event listener for each child Widget, responsible for removing
218 * the destroyed child Widget from the parent's internal array of children
221 * @method _afterDestroyChild
223 * @param {EventFacade} event The event facade for the attribute change.
225 _afterDestroyChild: function (event) {
226 var child = event.target;
228 if (child.get("parent") == this) {
235 * Attribute change listener for the <code>selection</code>
236 * attribute, responsible for setting the value of the
237 * parent's <code>selected</code> attribute.
239 * @method _afterSelectionChange
241 * @param {EventFacade} event The event facade for the attribute change.
243 _afterSelectionChange: function (event) {
245 if (event.target == this && event.src != this) {
247 var selection = event.newVal,
248 selectedVal = 0; // Not selected
253 selectedVal = 2; // Assume partially selected, confirm otherwise
256 if (Y.instanceOf(selection, Y.ArrayList) &&
257 (selection.size() === this.size())) {
259 selectedVal = 1; // Fully selected
265 this.set("selected", selectedVal, { src: this });
272 * Attribute change listener for the <code>activeDescendant</code>
273 * attribute, responsible for setting the value of the
274 * parent's <code>activeDescendant</code> attribute.
276 * @method _afterActiveDescendantChange
278 * @param {EventFacade} event The event facade for the attribute change.
280 _afterActiveDescendantChange: function (event) {
281 var parent = this.get("parent");
284 parent._set("activeDescendant", event.newVal);
289 * Attribute change listener for the <code>selected</code>
290 * attribute, responsible for syncing the selected state of all children to
291 * match that of their parent Widget.
294 * @method _afterParentSelectedChange
296 * @param {EventFacade} event The event facade for the attribute change.
298 _afterParentSelectedChange: function (event) {
300 var value = event.newVal;
302 if (this == event.target && event.src != this &&
303 (value === 0 || value === 1)) {
305 this.each(function (child) {
307 // Specify the source of this change as the parent so that
308 // value of the parent's "selection" attribute isn't
311 child.set("selected", value, { src: this });
321 * Default setter for <code>selection</code> attribute changes.
323 * @method _setSelection
325 * @param child {Widget|Array} Widget or Array of Widget instances.
326 * @return {Widget|Array} Widget or Array of Widget instances.
328 _setSelection: function (child) {
330 var selection = null,
333 if (this.get("multiple") && !this.isEmpty()) {
337 this.each(function (v) {
339 if (v.get("selected") > 0) {
345 if (selected.length > 0) {
346 selection = selected;
352 if (child.get("selected") > 0) {
364 * Attribute change listener for the <code>selected</code>
365 * attribute of child Widgets, responsible for setting the value of the
366 * parent's <code>selection</code> attribute.
368 * @method _updateSelection
370 * @param {EventFacade} event The event facade for the attribute change.
372 _updateSelection: function (event) {
374 var child = event.target,
377 if (child.get("parent") == this) {
379 if (event.src != "_updateSelection") {
381 selection = this.get("selection");
383 if (!this.get("multiple") && selection && event.newVal > 0) {
385 // Deselect the previously selected child.
386 // Set src equal to the current context to prevent
387 // unnecessary re-calculation of the selection.
389 selection.set("selected", 0, { src: "_updateSelection" });
393 this._set("selection", child);
397 if (event.src == this) {
398 this._set("selection", child, { src: this });
406 * Attribute change listener for the <code>focused</code>
407 * attribute of child Widgets, responsible for setting the value of the
408 * parent's <code>activeDescendant</code> attribute.
410 * @method _updateActiveDescendant
412 * @param {EventFacade} event The event facade for the attribute change.
414 _updateActiveDescendant: function (event) {
415 var activeDescendant = (event.newVal === true) ? event.target : null;
416 this._set("activeDescendant", activeDescendant);
420 * Creates an instance of a child Widget using the specified configuration.
421 * By default Widget instances will be created of the type specified
422 * by the <code>defaultChildType</code> attribute. Types can be explicitly
423 * defined via the <code>childType</code> property of the configuration object
424 * literal. The use of the <code>type</code> property has been deprecated, but
425 * will still be used as a fallback, if <code>childType</code> is not defined,
426 * for backwards compatibility.
428 * @method _createChild
430 * @param config {Object} Object literal representing the configuration
431 * used to create an instance of a Widget.
433 _createChild: function (config) {
435 var defaultType = this.get("defaultChildType"),
436 altType = config.childType || config.type,
442 Fn = Lang.isString(altType) ? Y[altType] : altType;
445 if (Lang.isFunction(Fn)) {
447 } else if (defaultType) {
448 // defaultType is normalized to a function in it's setter
449 FnConstructor = defaultType;
453 child = new FnConstructor(config);
455 Y.error("Could not create a child instance because its constructor is either undefined or invalid.");
463 * Default addChild handler
465 * @method _defAddChildFn
467 * @param event {EventFacade} The Event object
468 * @param child {Widget} The Widget instance, or configuration
469 * object for the Widget to be added as a child.
470 * @param index {Number} Number representing the position at
471 * which the child will be inserted.
473 _defAddChildFn: function (event) {
475 var child = event.child,
477 children = this._items;
479 if (child.get("parent")) {
483 if (Lang.isNumber(index)) {
484 children.splice(index, 0, child);
487 children.push(child);
490 child._set("parent", this);
491 child.addTarget(this);
493 // Update index in case it got normalized after addition
494 // (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).
495 event.index = child.get("index");
497 // TO DO: Remove in favor of using event bubbling
498 child.after("selectedChange", Y.bind(this._updateSelection, this));
503 * Default removeChild handler
505 * @method _defRemoveChildFn
507 * @param event {EventFacade} The Event object
508 * @param child {Widget} The Widget instance to be removed.
509 * @param index {Number} Number representing the index of the Widget to
512 _defRemoveChildFn: function (event) {
514 var child = event.child,
516 children = this._items;
518 if (child.get("focused")) {
519 child.set("focused", false);
522 if (child.get("selected")) {
523 child.set("selected", 0);
526 children.splice(index, 1);
528 child.removeTarget(this);
529 child._oldParent = child.get("parent");
530 child._set("parent", null);
536 * @param child {Widget|Object} The Widget instance, or configuration
537 * object for the Widget to be added as a child.
538 * @param child {Array} Array of Widget instances, or configuration
539 * objects for the Widgets to be added as a children.
540 * @param index {Number} (Optional.) Number representing the position at
541 * which the child should be inserted.
542 * @description Adds a Widget as a child. If the specified Widget already
543 * has a parent it will be removed from its current parent before
544 * being added as a child.
545 * @return {Widget|Array} Successfully added Widget or Array containing the
546 * successfully added Widget instance(s). If no children where added, will
547 * will return undefined.
549 _add: function (child, index) {
556 if (Lang.isArray(child)) {
560 Y.each(child, function (v, k) {
562 oChild = this._add(v, (index + k));
565 children.push(oChild);
571 if (children.length > 0) {
572 returnVal = children;
578 if (Y.instanceOf(child, Y.Widget)) {
582 oChild = this._createChild(child);
585 if (oChild && this.fire("addChild", { child: oChild, index: index })) {
598 * @param child {Widget|Object} The Widget instance, or configuration
599 * object for the Widget to be added as a child. The configuration object
600 * for the child can include a <code>childType</code> property, which is either
601 * a constructor function or a string which names a constructor function on the
602 * Y instance (e.g. "Tab" would refer to Y.Tab) (<code>childType</code> used to be
603 * named <code>type</code>, support for which has been deprecated, but is still
604 * maintained for backward compatibility. <code>childType</code> takes precedence
605 * over <code>type</code> if both are defined.
606 * @param child {Array} Array of Widget instances, or configuration
607 * objects for the Widgets to be added as a children.
608 * @param index {Number} (Optional.) Number representing the position at
609 * which the child should be inserted.
610 * @description Adds a Widget as a child. If the specified Widget already
611 * has a parent it will be removed from its current parent before
612 * being added as a child.
613 * @return {Y.ArrayList} Y.ArrayList containing the successfully added
614 * Widget instance(s). If no children where added, will return an empty
615 * Y.ArrayList instance.
619 var added = this._add.apply(this, arguments),
620 children = added ? (Lang.isArray(added) ? added : [added]) : [];
622 return (new Y.ArrayList(children));
629 * @param index {Number} (Optional.) Number representing the index of the
630 * child to be removed.
631 * @description Removes the Widget from its parent. Optionally, can remove
632 * a child by specifying its index.
633 * @return {Widget} Widget instance that was successfully removed, otherwise
636 remove: function (index) {
638 var child = this._items[index],
641 if (child && this.fire("removeChild", { child: child, index: index })) {
652 * @description Removes all of the children from the Widget.
653 * @return {Y.ArrayList} Y.ArrayList instance containing Widgets that were
654 * successfully removed. If no children where removed, will return an empty
655 * Y.ArrayList instance.
657 removeAll: function () {
662 Y.each(this._items.concat(), function () {
664 child = this.remove(0);
672 return (new Y.ArrayList(removed));
677 * Selects the child at the given index (zero-based).
679 * @method selectChild
680 * @param {Number} i the index of the child to be selected
682 selectChild: function(i) {
683 this.item(i).set('selected', 1);
687 * Selects all children.
691 selectAll: function () {
692 this.set("selected", 1);
696 * Deselects all children.
698 * @method deselectAll
700 deselectAll: function () {
701 this.set("selected", 0);
705 * Updates the UI in response to a child being added.
707 * @method _uiAddChild
709 * @param child {Widget} The child Widget instance to render.
710 * @param parentNode {Object} The Node under which the
711 * child Widget is to be rendered.
713 _uiAddChild: function (child, parentNode) {
715 child.render(parentNode);
717 // TODO: Ideally this should be in Child's render UI.
719 var childBB = child.get("boundingBox"),
721 nextSibling = child.next(false),
724 // Insert or Append to last child.
726 // Avoiding index, and using the current sibling
727 // state (which should be accurate), means we don't have
728 // to worry about decorator elements which may be added
729 // to the _childContainer node.
732 siblingBB = nextSibling.get("boundingBox");
733 siblingBB.insert(childBB, "before");
735 prevSibling = child.previous(false);
737 siblingBB = prevSibling.get("boundingBox");
738 siblingBB.insert(childBB, "after");
744 * Updates the UI in response to a child being removed.
746 * @method _uiRemoveChild
748 * @param child {Widget} The child Widget instance to render.
750 _uiRemoveChild: function (child) {
751 child.get("boundingBox").remove();
754 _afterAddChild: function (event) {
755 var child = event.child;
757 if (child.get("parent") == this) {
758 this._uiAddChild(child, this._childrenContainer);
762 _afterRemoveChild: function (event) {
763 var child = event.child;
765 if (child._oldParent == this) {
766 this._uiRemoveChild(child);
771 * Sets up DOM and CustomEvent listeners for the parent widget.
773 * This method in invoked after bindUI is invoked for the Widget class
774 * using YUI's aop infrastructure.
777 * @method _bindUIParent
780 _bindUIParent: function () {
781 this.after("addChild", this._afterAddChild);
782 this.after("removeChild", this._afterRemoveChild);
786 * Renders all child Widgets for the parent.
788 * This method in invoked after renderUI is invoked for the Widget class
789 * using YUI's aop infrastructure.
791 * @method _renderChildren
794 _renderChildren: function () {
797 * <p>By default WidgetParent will render it's children to the parent's content box.</p>
799 * <p>If the children need to be rendered somewhere else, the _childrenContainer property
800 * can be set to the Node which the children should be rendered to. This property should be
801 * set before the _renderChildren method is invoked, ideally in your renderUI method,
802 * as soon as you create the element to be rendered to.</p>
805 * @property _childrenContainer
806 * @value The content box
809 var renderTo = this._childrenContainer || this.get("contentBox");
811 this._childrenContainer = renderTo;
813 this.each(function (child) {
814 child.render(renderTo);
819 * Destroys all child Widgets for the parent.
821 * This method is invoked before the destructor is invoked for the Widget
822 * class using YUI's aop infrastructure.
824 * @method _destroyChildren
827 _destroyChildren: function () {
829 // Detach the handler responsible for removing children in
830 // response to destroying them since:
831 // 1) It is unnecessary/inefficient at this point since we are doing
832 // a batch destroy of all children.
833 // 2) Removing each child will affect our ability to iterate the
834 // children since the size of _items will be changing as we
836 this._hDestroyChild.detach();
838 // Need to clone the _items array since
839 this.each(function (child) {
846 Y.augment(Parent, Y.ArrayList);
848 Y.WidgetParent = Parent;
851 }, '3.3.0' ,{requires:['base-build', 'arraylist', 'widget']});