2 YUI 3.13.0 (build 508226d)
3 Copyright 2013 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
8 YUI.add('widget-parent', function (Y, NAME) {
11 * Extension enabling a Widget to be a parent of another Widget.
13 * @module widget-parent
17 RENDERED = "rendered",
18 BOUNDING_BOX = "boundingBox";
21 * Widget extension providing functionality enabling a Widget to be a
22 * parent of another Widget.
24 * <p>In addition to the set of attributes supported by WidgetParent, the constructor
25 * configuration object can also contain a <code>children</code> which can be used
26 * to add child widgets to the parent during construction. The <code>children</code>
27 * property is an array of either child widget instances or child widget configuration
28 * objects, and is sugar for the <a href="#method_add">add</a> method. See the
29 * <a href="#method_add">add</a> for details on the structure of the child widget
30 * configuration object.
34 * @param {Object} config User configuration object.
36 function Parent(config) {
39 * Fires when a Widget is add as a child. The event object will have a
40 * 'child' property that returns a reference to the child Widget, as well
41 * as an 'index' property that returns a reference to the index specified
42 * when the add() method was called.
44 * Subscribers to the "on" moment of this event, will be notified
45 * before a child is added.
48 * Subscribers to the "after" moment of this event, will be notified
49 * after a child is added.
53 * @preventable _defAddChildFn
54 * @param {EventFacade} e The Event Facade
56 this.publish("addChild", {
57 defaultTargetOnly: true,
58 defaultFn: this._defAddChildFn
63 * Fires when a child Widget is removed. The event object will have a
64 * 'child' property that returns a reference to the child Widget, as well
65 * as an 'index' property that returns a reference child's ordinal position.
67 * Subscribers to the "on" moment of this event, will be notified
68 * before a child is removed.
71 * Subscribers to the "after" moment of this event, will be notified
72 * after a child is removed.
76 * @preventable _defRemoveChildFn
77 * @param {EventFacade} e The Event Facade
79 this.publish("removeChild", {
80 defaultTargetOnly: true,
81 defaultFn: this._defRemoveChildFn
89 if (config && config.children) {
91 children = config.children;
93 handle = this.after("initializedChange", function (e) {
100 // Widget method overlap
101 Y.after(this._renderChildren, this, "renderUI");
102 Y.after(this._bindUIParent, this, "bindUI");
104 this.after("selectionChange", this._afterSelectionChange);
105 this.after("selectedChange", this._afterParentSelectedChange);
106 this.after("activeDescendantChange", this._afterActiveDescendantChange);
108 this._hDestroyChild = this.after("*:destroy", this._afterDestroyChild);
109 this.after("*:focusedChange", this._updateActiveDescendant);
116 * @attribute defaultChildType
117 * @type {String|Object}
119 * @description String representing the default type of the children
120 * managed by this Widget. Can also supply default type as a constructor
124 setter: function (val) {
126 var returnVal = Y.Attribute.INVALID_VALUE,
127 FnConstructor = Lang.isString(val) ? Y[val] : val;
129 if (Lang.isFunction(FnConstructor)) {
130 returnVal = FnConstructor;
138 * @attribute activeDescendant
142 * @description Returns the Widget's currently focused descendant Widget.
149 * @attribute multiple
154 * @description Boolean indicating if multiple children can be selected at
155 * once. Whether or not multiple selection is enabled is always delegated
156 * to the value of the <code>multiple</code> attribute of the root widget
157 * in the object hierarchy.
161 validator: Lang.isBoolean,
163 getter: function (value) {
164 var root = this.get("root");
165 return (root && root != this) ? root.get("multiple") : value;
171 * @attribute selection
172 * @type {ArrayList|Widget}
175 * @description Returns the currently selected child Widget. If the
176 * <code>mulitple</code> attribte is set to <code>true</code> will
177 * return an Y.ArrayList instance containing the currently selected
178 * children. If no children are selected, will return null.
182 setter: "_setSelection",
183 getter: function (value) {
184 var selection = Lang.isArray(value) ?
185 (new Y.ArrayList(value)) : value;
191 setter: function (value) {
193 // Enforces selection behavior on for parent Widgets. Parent's
194 // selected attribute can be set to the following:
196 // 1 - Fully selected (all children are selected). In order for
197 // all children to be selected, multiple selection must be
198 // enabled. Therefore, you cannot set the "selected" attribute
199 // on a parent Widget to 1 unless multiple selection is enabled.
200 // 2 - Partially selected, meaning one ore more (but not all)
201 // children are selected.
203 var returnVal = value;
205 if (value === 1 && !this.get("multiple")) {
206 Y.log('The selected attribute can only be set to 1 if the "multiple" attribute is set to true.', "error", "widget");
207 returnVal = Y.Attribute.INVALID_VALUE;
219 * The destructor implementation for Parent widgets. Destroys all children.
222 destructor: function() {
223 this._destroyChildren();
227 * Destroy event listener for each child Widget, responsible for removing
228 * the destroyed child Widget from the parent's internal array of children
231 * @method _afterDestroyChild
233 * @param {EventFacade} event The event facade for the attribute change.
235 _afterDestroyChild: function (event) {
236 var child = event.target;
238 if (child.get("parent") == this) {
244 * Attribute change listener for the <code>selection</code>
245 * attribute, responsible for setting the value of the
246 * parent's <code>selected</code> attribute.
248 * @method _afterSelectionChange
250 * @param {EventFacade} event The event facade for the attribute change.
252 _afterSelectionChange: function (event) {
254 if (event.target == this && event.src != this) {
256 var selection = event.newVal,
257 selectedVal = 0; // Not selected
262 selectedVal = 2; // Assume partially selected, confirm otherwise
265 if (Y.instanceOf(selection, Y.ArrayList) &&
266 (selection.size() === this.size())) {
268 selectedVal = 1; // Fully selected
274 this.set("selected", selectedVal, { src: this });
281 * Attribute change listener for the <code>activeDescendant</code>
282 * attribute, responsible for setting the value of the
283 * parent's <code>activeDescendant</code> attribute.
285 * @method _afterActiveDescendantChange
287 * @param {EventFacade} event The event facade for the attribute change.
289 _afterActiveDescendantChange: function (event) {
290 var parent = this.get("parent");
293 parent._set("activeDescendant", event.newVal);
298 * Attribute change listener for the <code>selected</code>
299 * attribute, responsible for syncing the selected state of all children to
300 * match that of their parent Widget.
303 * @method _afterParentSelectedChange
305 * @param {EventFacade} event The event facade for the attribute change.
307 _afterParentSelectedChange: function (event) {
309 var value = event.newVal;
311 if (this == event.target && event.src != this &&
312 (value === 0 || value === 1)) {
314 this.each(function (child) {
316 // Specify the source of this change as the parent so that
317 // value of the parent's "selection" attribute isn't
320 child.set("selected", value, { src: this });
330 * Default setter for <code>selection</code> attribute changes.
332 * @method _setSelection
334 * @param child {Widget|Array} Widget or Array of Widget instances.
335 * @return {Widget|Array} Widget or Array of Widget instances.
337 _setSelection: function (child) {
339 var selection = null,
342 if (this.get("multiple") && !this.isEmpty()) {
346 this.each(function (v) {
348 if (v.get("selected") > 0) {
354 if (selected.length > 0) {
355 selection = selected;
361 if (child.get("selected") > 0) {
373 * Attribute change listener for the <code>selected</code>
374 * attribute of child Widgets, responsible for setting the value of the
375 * parent's <code>selection</code> attribute.
377 * @method _updateSelection
379 * @param {EventFacade} event The event facade for the attribute change.
381 _updateSelection: function (event) {
383 var child = event.target,
386 if (child.get("parent") == this) {
388 if (event.src != "_updateSelection") {
390 selection = this.get("selection");
392 if (!this.get("multiple") && selection && event.newVal > 0) {
394 // Deselect the previously selected child.
395 // Set src equal to the current context to prevent
396 // unnecessary re-calculation of the selection.
398 selection.set("selected", 0, { src: "_updateSelection" });
402 this._set("selection", child);
406 if (event.src == this) {
407 this._set("selection", child, { src: this });
415 * Attribute change listener for the <code>focused</code>
416 * attribute of child Widgets, responsible for setting the value of the
417 * parent's <code>activeDescendant</code> attribute.
419 * @method _updateActiveDescendant
421 * @param {EventFacade} event The event facade for the attribute change.
423 _updateActiveDescendant: function (event) {
424 var activeDescendant = (event.newVal === true) ? event.target : null;
425 this._set("activeDescendant", activeDescendant);
429 * Creates an instance of a child Widget using the specified configuration.
430 * By default Widget instances will be created of the type specified
431 * by the <code>defaultChildType</code> attribute. Types can be explicitly
432 * defined via the <code>childType</code> property of the configuration object
433 * literal. The use of the <code>type</code> property has been deprecated, but
434 * will still be used as a fallback, if <code>childType</code> is not defined,
435 * for backwards compatibility.
437 * @method _createChild
439 * @param config {Object} Object literal representing the configuration
440 * used to create an instance of a Widget.
442 _createChild: function (config) {
444 var defaultType = this.get("defaultChildType"),
445 altType = config.childType || config.type,
451 Fn = Lang.isString(altType) ? Y[altType] : altType;
454 if (Lang.isFunction(Fn)) {
456 } else if (defaultType) {
457 // defaultType is normalized to a function in it's setter
458 FnConstructor = defaultType;
462 child = new FnConstructor(config);
464 Y.error("Could not create a child instance because its constructor is either undefined or invalid.");
472 * Default addChild handler
474 * @method _defAddChildFn
476 * @param event {EventFacade} The Event object
477 * @param child {Widget} The Widget instance, or configuration
478 * object for the Widget to be added as a child.
479 * @param index {Number} Number representing the position at
480 * which the child will be inserted.
482 _defAddChildFn: function (event) {
484 var child = event.child,
486 children = this._items;
488 if (child.get("parent")) {
492 if (Lang.isNumber(index)) {
493 children.splice(index, 0, child);
496 children.push(child);
499 child._set("parent", this);
500 child.addTarget(this);
502 // Update index in case it got normalized after addition
503 // (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).
504 event.index = child.get("index");
506 // TO DO: Remove in favor of using event bubbling
507 child.after("selectedChange", Y.bind(this._updateSelection, this));
512 * Default removeChild handler
514 * @method _defRemoveChildFn
516 * @param event {EventFacade} The Event object
517 * @param child {Widget} The Widget instance to be removed.
518 * @param index {Number} Number representing the index of the Widget to
521 _defRemoveChildFn: function (event) {
523 var child = event.child,
525 children = this._items;
527 if (child.get("focused")) {
528 child.blur(); // focused is readOnly, so use the public i/f to unset it
531 if (child.get("selected")) {
532 child.set("selected", 0);
535 children.splice(index, 1);
537 child.removeTarget(this);
538 child._oldParent = child.get("parent");
539 child._set("parent", null);
545 * @param child {Widget|Object} The Widget instance, or configuration
546 * object for the Widget to be added as a child.
547 * @param child {Array} Array of Widget instances, or configuration
548 * objects for the Widgets to be added as a children.
549 * @param index {Number} (Optional.) Number representing the position at
550 * which the child should be inserted.
551 * @description Adds a Widget as a child. If the specified Widget already
552 * has a parent it will be removed from its current parent before
553 * being added as a child.
554 * @return {Widget|Array} Successfully added Widget or Array containing the
555 * successfully added Widget instance(s). If no children where added, will
556 * will return undefined.
558 _add: function (child, index) {
565 if (Lang.isArray(child)) {
569 Y.each(child, function (v, k) {
571 oChild = this._add(v, (index + k));
574 children.push(oChild);
580 if (children.length > 0) {
581 returnVal = children;
587 if (Y.instanceOf(child, Y.Widget)) {
591 oChild = this._createChild(child);
594 if (oChild && this.fire("addChild", { child: oChild, index: index })) {
607 * @param child {Widget|Object} The Widget instance, or configuration
608 * object for the Widget to be added as a child. The configuration object
609 * for the child can include a <code>childType</code> property, which is either
610 * a constructor function or a string which names a constructor function on the
611 * Y instance (e.g. "Tab" would refer to Y.Tab) (<code>childType</code> used to be
612 * named <code>type</code>, support for which has been deprecated, but is still
613 * maintained for backward compatibility. <code>childType</code> takes precedence
614 * over <code>type</code> if both are defined.
615 * @param child {Array} Array of Widget instances, or configuration
616 * objects for the Widgets to be added as a children.
617 * @param index {Number} (Optional.) Number representing the position at
618 * which the child should be inserted.
619 * @description Adds a Widget as a child. If the specified Widget already
620 * has a parent it will be removed from its current parent before
621 * being added as a child.
622 * @return {ArrayList} Y.ArrayList containing the successfully added
623 * Widget instance(s). If no children where added, will return an empty
624 * Y.ArrayList instance.
628 var added = this._add.apply(this, arguments),
629 children = added ? (Lang.isArray(added) ? added : [added]) : [];
631 return (new Y.ArrayList(children));
638 * @param index {Number} (Optional.) Number representing the index of the
639 * child to be removed.
640 * @description Removes the Widget from its parent. Optionally, can remove
641 * a child by specifying its index.
642 * @return {Widget} Widget instance that was successfully removed, otherwise
645 remove: function (index) {
647 var child = this._items[index],
650 if (child && this.fire("removeChild", { child: child, index: index })) {
661 * @description Removes all of the children from the Widget.
662 * @return {ArrayList} Y.ArrayList instance containing Widgets that were
663 * successfully removed. If no children where removed, will return an empty
664 * Y.ArrayList instance.
666 removeAll: function () {
671 Y.each(this._items.concat(), function () {
673 child = this.remove(0);
681 return (new Y.ArrayList(removed));
686 * Selects the child at the given index (zero-based).
688 * @method selectChild
689 * @param {Number} i the index of the child to be selected
691 selectChild: function(i) {
692 this.item(i).set('selected', 1);
696 * Selects all children.
700 selectAll: function () {
701 this.set("selected", 1);
705 * Deselects all children.
707 * @method deselectAll
709 deselectAll: function () {
710 this.set("selected", 0);
714 * Updates the UI in response to a child being added.
716 * @method _uiAddChild
718 * @param child {Widget} The child Widget instance to render.
719 * @param parentNode {Object} The Node under which the
720 * child Widget is to be rendered.
722 _uiAddChild: function (child, parentNode) {
724 child.render(parentNode);
726 // TODO: Ideally this should be in Child's render UI.
728 var childBB = child.get("boundingBox"),
730 nextSibling = child.next(false),
733 // Insert or Append to last child.
735 // Avoiding index, and using the current sibling
736 // state (which should be accurate), means we don't have
737 // to worry about decorator elements which may be added
738 // to the _childContainer node.
740 if (nextSibling && nextSibling.get(RENDERED)) {
742 siblingBB = nextSibling.get(BOUNDING_BOX);
743 siblingBB.insert(childBB, "before");
747 prevSibling = child.previous(false);
749 if (prevSibling && prevSibling.get(RENDERED)) {
751 siblingBB = prevSibling.get(BOUNDING_BOX);
752 siblingBB.insert(childBB, "after");
754 } else if (!parentNode.contains(childBB)) {
756 // Based on pull request from andreas-karlsson
757 // https://github.com/yui/yui3/pull/25#issuecomment-2103536
759 // Account for case where a child was rendered independently of the
760 // parent-child framework, to a node outside of the parentNode,
761 // and there are no siblings.
763 parentNode.appendChild(childBB);
770 * Updates the UI in response to a child being removed.
772 * @method _uiRemoveChild
774 * @param child {Widget} The child Widget instance to render.
776 _uiRemoveChild: function (child) {
777 child.get("boundingBox").remove();
780 _afterAddChild: function (event) {
781 var child = event.child;
783 if (child.get("parent") == this) {
784 this._uiAddChild(child, this._childrenContainer);
788 _afterRemoveChild: function (event) {
789 var child = event.child;
791 if (child._oldParent == this) {
792 this._uiRemoveChild(child);
797 * Sets up DOM and CustomEvent listeners for the parent widget.
799 * This method in invoked after bindUI is invoked for the Widget class
800 * using YUI's aop infrastructure.
803 * @method _bindUIParent
806 _bindUIParent: function () {
807 this.after("addChild", this._afterAddChild);
808 this.after("removeChild", this._afterRemoveChild);
812 * Renders all child Widgets for the parent.
814 * This method in invoked after renderUI is invoked for the Widget class
815 * using YUI's aop infrastructure.
817 * @method _renderChildren
820 _renderChildren: function () {
823 * <p>By default WidgetParent will render it's children to the parent's content box.</p>
825 * <p>If the children need to be rendered somewhere else, the _childrenContainer property
826 * can be set to the Node which the children should be rendered to. This property should be
827 * set before the _renderChildren method is invoked, ideally in your renderUI method,
828 * as soon as you create the element to be rendered to.</p>
831 * @property _childrenContainer
832 * @value The content box
835 var renderTo = this._childrenContainer || this.get("contentBox");
837 this._childrenContainer = renderTo;
839 this.each(function (child) {
840 child.render(renderTo);
845 * Destroys all child Widgets for the parent.
847 * This method is invoked before the destructor is invoked for the Widget
848 * class using YUI's aop infrastructure.
850 * @method _destroyChildren
853 _destroyChildren: function () {
855 // Detach the handler responsible for removing children in
856 // response to destroying them since:
857 // 1) It is unnecessary/inefficient at this point since we are doing
858 // a batch destroy of all children.
859 // 2) Removing each child will affect our ability to iterate the
860 // children since the size of _items will be changing as we
862 this._hDestroyChild.detach();
864 // Need to clone the _items array since
865 this.each(function (child) {
872 Y.augment(Parent, Y.ArrayList);
874 Y.WidgetParent = Parent;
877 }, '3.13.0', {"requires": ["arraylist", "base-build", "widget"]});