/**
* Provides Accordion widget
*
* @module gallery-accordion
*/
(function(){
// Local constants
var Lang = Y.Lang,
Node = Y.Node,
Anim = Y.Anim,
Easing = Y.Easing,
AccName = "accordion",
WidgetStdMod = Y.WidgetStdMod,
QuirksMode = document.compatMode == "BackCompat",
IEQuirksMode = QuirksMode && Y.UA.ie > 0,
COLLAPSE_HEIGHT = IEQuirksMode ? 1 : 0,
getCN = Y.ClassNameManager.getClassName,
C_ITEM = "yui3-accordion-item",
C_PROXY_VISIBLE = getCN( AccName, "proxyel", "visible" ),
DRAGGROUP = getCN( AccName, "graggroup" ),
BEFOREITEMADD = "beforeItemAdd",
ITEMADDED = "itemAdded",
BEFOREITEMREMOVE = "beforeItemRemove",
ITEMREMOVED = "itemRemoved",
BEFOREITEMERESIZED = "beforeItemResized",
ITEMERESIZED = "itemResized",
BEFOREITEMEXPAND = "beforeItemExpand",
BEFOREITEMCOLLAPSE = "beforeItemCollapse",
ITEMEXPANDED = "itemExpanded",
ITEMCOLLAPSED = "itemCollapsed",
BEFOREITEMREORDER = "beforeItemReorder",
BEFOREENDITEMREORDER = "beforeEndItemReorder",
ITEMREORDERED = "itemReordered",
DEFAULT = "default",
ANIMATION = "animation",
ALWAYSVISIBLE = "alwaysVisible",
EXPANDED = "expanded",
COLLAPSEOTHERSONEXPAND = "collapseOthersOnExpand",
ITEMS = "items",
CONTENT_HEIGHT = "contentHeight",
ICON_CLOSE = "iconClose",
ICON_ALWAYSVISIBLE = "iconAlwaysVisible",
STRETCH = "stretch",
PX = "px",
CONTENT_BOX = "contentBox",
BOUNDING_BOX = "boundingBox",
SRCNODE = "srcNode",
RENDERED = "rendered",
BODYCONTENT = "bodyContent",
CHILDREN = "children",
PARENT_NODE = "parentNode",
NODE = "node",
DATA = "data";
/**
* Accordion creates an widget, consists of one or more items, which can be collapsed, expanded,
* set as always visible and reordered by using Drag&Drop. Collapsing/expanding might be animated.
*
* @class Accordion
* @extends Widget
*/
Y.Accordion = Y.Base.create( AccName, Y.Widget, [], {
/**
* Signals the beginning of adding an item to the Accordion.
*
* @event beforeItemAdd
* @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
* <dl>
* <dt>item</dt>
* <dd>An <code>AccordionItem</code> instance of the item being added</dd>
* </dl>
*/
/**
* Signals an item has been added to the Accordion.
*
* @event itemAdded
* @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
* <dl>
* <dt>item</dt>
* <dd>An <code>AccordionItem</code> instance of the item that has been added</dd>
* </dl>
*/
/**
* Signals the beginning of removing an item.
*
* @event beforeItemRemove
* @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
* <dl>
* <dt>item</dt>
* <dd>An <code>AccordionItem</code> instance of the item being removed</dd>
* </dl>
*/
/**
* Signals an item has been removed from Accordion.
*
* @event itemRemoved
* @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
* <dl>
* <dt>item</dt>
* <dd>An <code>AccordionItem</code> instance of the item that has been removed</dd>
* </dl>
*/
/**
* Signals the beginning of resizing an item.
*
* @event beforeItemResized
* @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
* <dl>
* <dt>item</dt>
* <dd>An <code>AccordionItem</code> instance of the item being resized</dd>
* </dl>
*/
/**
* Signals an item has been resized.
*
* @event itemResized
* @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
* <dl>
* <dt>item</dt>
* <dd>An <code>AccordionItem</code> instance of the item that has been resized</dd>
* </dl>
*/
/**
* Signals the beginning of expanding an item
*
* @event beforeItemExpand
* @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
* <dl>
* <dt>item</dt>
* <dd>An <code>AccordionItem</code> instance of the item being expanded</dd>
* </dl>
*/
/**
* Signals the beginning of collapsing an item
*
* @event beforeItemCollapse
* @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
* <dl>
* <dt>item</dt>
* <dd>An <code>AccordionItem</code> instance of the item being collapsed</dd>
* </dl>
*/
/**
* Signals an item has been expanded
*
* @event itemExpanded
* @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
* <dl>
* <dt>item</dt>
* <dd>An <code>AccordionItem</code> instance of the item that has been expanded</dd>
* </dl>
*/
/**
* Signals an item has been collapsed
*
* @event itemCollapsed
* @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
* <dl>
* <dt>item</dt>
* <dd>An <code>AccordionItem</code> instance of the item that has been collapsed</dd>
* </dl>
*/
/**
* Signals the beginning of reordering an item
*
* @event beforeItemReorder
* @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
* <dl>
* <dt>item</dt>
* <dd>An <code>AccordionItem</code> instance of the item being reordered</dd>
* </dl>
*/
/**
* Fires before the end of item reordering
*
* @event beforeEndItemReorder
* @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
* <dl>
* <dt>item</dt>
* <dd>An <code>AccordionItem</code> instance of the item being reordered</dd>
* </dl>
*/
/**
* Signals an item has been reordered
*
* @event itemReordered
* @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
* <dl>
* <dt>item</dt>
* <dd>An <code>AccordionItem</code> instance of the item that has been reordered</dd>
* </dl>
*/
/**
* Initializer lifecycle implementation for the Accordion class. Publishes events,
* initializes internal properties and subscribes for resize event.
*
* @method initializer
* @protected
* @param config {Object} Configuration object literal for the Accordion
*/
initializer: function( config ) {
this.after( "render", Y.bind( this._afterRender, this ) );
},
/**
* Destructor lifecycle implementation for the Accordion class.
* Removes and destroys all registered items.
*
* @method destructor
* @protected
*/
destructor: function() {
var items, item, i, length;
items = this.get( ITEMS );
length = items.length;
for( i = length - 1; i >= 0; i-- ){
item = items[ i ];
items.splice( i, 1 );
this._removeItemHandles( item );
item.destroy();
}
},
/**
* Binds an event to Accordion's contentBox.
*
* @method _bindItemChosenEvent
* @protected
*/
_bindItemChosenEvent: function(itemChosenEvent) {
var contentBox;
contentBox = this.get( CONTENT_BOX );
contentBox.delegate( itemChosenEvent, Y.bind( this._onItemChosenEvent, this ), '.yui3-widget-hd' );
},
/**
* Contains items for collapsing
* @property _forCollapsing
* @protected
* @type Object
*/
_forCollapsing : {},
/**
* Contains items for expanding
* @property _forExpanding
* @protected
* @type Object
*/
_forExpanding : {},
/**
* Contains currently running animations
* @property _animations
* @protected
* @type Object
*/
_animations : {},
/**
* Collection of items handles.
* Keeps track of each items's event handle, as returned from <code>Y.on</code> or <code>Y.after</code>.
* @property _itemHandles
* @private
* @type Object
*/
_itemsHandles: {},
/**
* Removes all handles, attched to given item
*
* @method _removeItemHandles
* @protected
* @param item {Y.AccordionItem} The item, which handles to remove
*/
_removeItemHandles: function( item ){
var itemHandles, itemHandle;
itemHandles = this._itemsHandles[ item ];
for( itemHandle in itemHandles ){
if( itemHandles.hasOwnProperty( itemHandle ) ){
itemHandle = itemHandles[ itemHandle ];
itemHandle.detach();
}
}
delete this._itemsHandles[ item ];
},
/**
* Obtains the precise height of the node provided, including padding and border.
*
* @method _getNodeOffsetHeight
* @protected
* @param node {Node|HTMLElement} The node to gather the height from
* @return {Number} The calculated height or zero in case of failure
*/
_getNodeOffsetHeight: function( node ){
var height, preciseRegion;
if( node instanceof Node ){
if( node.hasMethod( "getBoundingClientRect" ) ){
preciseRegion = node.invoke( "getBoundingClientRect" );
if( preciseRegion ){
height = preciseRegion.bottom - preciseRegion.top;
return height;
}
} else {
height = node.get( "offsetHeight" );
return Y.Lang.isValue( height ) ? height : 0;
}
} else if( node ){
height = node.offsetHeight;
return Y.Lang.isValue( height ) ? height : 0;
}
return 0;
},
/**
* Updates expand and alwaysVisible properties of given item with the values provided.
* The properties will be updated only if needed.
*
* @method _setItemProperties
* @protected
* @param item {Y.AccordionItem} The item, which properties should be updated
* @param expanding {Boolean} The new value of "expanded" property
* @param alwaysVisible {Boolean} The new value of "alwaysVisible" property
*/
_setItemProperties: function( item, expanding, alwaysVisible ){
var curAlwaysVisible, curExpanded;
curAlwaysVisible = item.get( ALWAYSVISIBLE );
curExpanded = item.get( EXPANDED );
if( expanding != curExpanded ){
item.set( EXPANDED, expanding, {
internalCall: true
});
}
if( alwaysVisible !== curAlwaysVisible ){
item.set( ALWAYSVISIBLE, alwaysVisible, {
internalCall: true
});
}
},
/**
* Updates user interface of an item and marks it as expanded, alwaysVisible or both
*
* @method _setItemUI
* @protected
* @param item {Y.AccordionItem} The item, which user interface should be updated
* @param expanding {Boolean} If true, the item will be marked as expanded.
* If false, the item will be marked as collapsed
* @param alwaysVisible {Boolean} If true, the item will be marked as always visible.
* If false, the always visible mark will be removed
*/
_setItemUI: function( item, expanding, alwaysVisible ){
item.markAsExpanded( expanding );
item.markAsAlwaysVisible( alwaysVisible );
},
/**
* Sets listener to resize event
*
* @method _afterRender
* @protected
* @param e {Event} after render custom event
*/
_afterRender: function( e ){
var resizeEvent;
resizeEvent = this.get( "resizeEvent" );
this._setUpResizing( resizeEvent );
this.after( "resizeEventChange", Y.bind( this._afterResizeEventChange, this ) );
},
/**
* Set up resizing with the new value provided
*
* @method _afterResizeEventChange
* @protected
* @param params {Event} after resizeEventChange custom event
*/
_afterResizeEventChange: function( params ){
this._setUpResizing( params.newVal );
},
/**
* Distributes the involved items as result of user interaction on item header.
* Some items might be stored in the list for collapsing, other in the list for expanding.
* Finally, invokes <code>_processItems</code> function, except if item has been expanded and
* user has clicked on always visible icon.
* If the user clicked on close icon, the item will be closed.
*
* @method _onItemChosen
* @protected
* @param item {Y.AccordionItem} The item on which user has clicked or pressed key
* @param srcIconAlwaysVisible {Boolean} True if the user has clicked on always visible icon
* @param srcIconClose {Boolean} True if the user has clicked on close icon
*/
_onItemChosen: function( item, srcIconAlwaysVisible, srcIconClose ){
var toBeExcluded, alwaysVisible, expanded, collapseOthersOnExpand;
toBeExcluded = {};
collapseOthersOnExpand = this.get( COLLAPSEOTHERSONEXPAND );
alwaysVisible = item.get( ALWAYSVISIBLE );
expanded = item.get( EXPANDED );
if( srcIconClose ){
this.removeItem( item );
return;
} else if( srcIconAlwaysVisible ){
if( expanded ){
alwaysVisible = !alwaysVisible;
expanded = alwaysVisible ? true : expanded;
this._setItemProperties( item, expanded, alwaysVisible );
this._setItemUI( item, expanded, alwaysVisible );
return;
} else {
this._forExpanding[ item ] = {
'item': item,
alwaysVisible: true
};
if( collapseOthersOnExpand ){
toBeExcluded[ item ] = {
'item': item
};
this._storeItemsForCollapsing( toBeExcluded );
}
}
} else {
/*
* Do the opposite
*/
if( expanded ){
this._forCollapsing[ item ] = {
'item': item
};
} else {
this._forExpanding[ item ] = {
'item': item,
'alwaysVisible': alwaysVisible
};
if( collapseOthersOnExpand ){
toBeExcluded[ item ] = {
'item': item
};
this._storeItemsForCollapsing( toBeExcluded );
}
}
}
this._processItems();
},
/**
* Helper method to adjust the height of all items, which <code>contentHeight</code> property is set as "stretch".
* If some item has animation running, it will be stopped before running another one.
*
* @method adjustStretchItems
* @protected
* @return {Number} The calculated height per strech item
*/
_adjustStretchItems: function(){
var items = this.get( ITEMS ), heightPerStretchItem, forExpanding;
heightPerStretchItem = this._getHeightPerStretchItem();
forExpanding = this._forExpanding;
Y.Array.each( items, function( item, index, items ){
var body, bodyHeight, anim, heightSettings, expanded;
heightSettings = item.get( CONTENT_HEIGHT );
expanded = item.get( EXPANDED );
if( !forExpanding[ item ] && heightSettings.method === STRETCH && expanded ){
anim = this._animations[ item ];
// stop waiting animation
if( anim ){
anim.stop();
}
body = item.getStdModNode( WidgetStdMod.BODY );
bodyHeight = this._getNodeOffsetHeight( body );
if( heightPerStretchItem < bodyHeight ){
this._processCollapsing( item, heightPerStretchItem );
} else if( heightPerStretchItem > bodyHeight ){
this._processExpanding( item, heightPerStretchItem );
}
}
}, this );
return heightPerStretchItem;
},
/**
* Calculates the height per strech item.
*
* @method _getHeightPerStretchItem
* @protected
* @return {Number} The calculated height per strech item
*/
_getHeightPerStretchItem: function(){
var height, items, stretchCounter = 0;
items = this.get( ITEMS );
height = this.get( BOUNDING_BOX ).get( "clientHeight" );
Y.Array.each( items, function( item, index, items ){
var collapsed, itemContentHeight, header, heightSettings, headerHeight;
header = item.getStdModNode( WidgetStdMod.HEADER );
heightSettings = item.get( CONTENT_HEIGHT );
headerHeight = this._getNodeOffsetHeight( header );
height -= headerHeight;
collapsed = !item.get( EXPANDED );
if( collapsed ){
height -= COLLAPSE_HEIGHT;
return;
}
if( heightSettings.method === STRETCH ){
stretchCounter++;
} else {
itemContentHeight = this._getItemContentHeight( item );
height -= itemContentHeight;
}
}, this );
if( stretchCounter > 0 ){
height /= stretchCounter;
}
if( height < 0 ){
height = 0;
}
return height;
},
/**
* Calculates the height of given item depending on its "contentHeight" property.
*
* @method _getItemContentHeight
* @protected
* @param item {Y.AccordionItem} The item, which height should be calculated
* @return {Number} The calculated item's height
*/
_getItemContentHeight: function( item ){
var heightSettings, height = 0, body, bodyContent;
heightSettings = item.get( CONTENT_HEIGHT );
if( heightSettings.method === "auto" ){
body = item.getStdModNode( WidgetStdMod.BODY );
bodyContent = body.get( CHILDREN ).item(0);
height = bodyContent ? this._getNodeOffsetHeight( bodyContent ) : 0;
} else if( heightSettings.method === "fixed" ) {
height = heightSettings.height;
} else {
height = this._getHeightPerStretchItem();
}
return height;
},
/**
* Stores all items, which are expanded and not set as always visible in list
* in order to be collapsed later.
*
* @method _storeItemsForCollapsing
* @protected
* @param itemsToBeExcluded {Object} (optional) Contains one or more <code>Y.AccordionItem</code> instances,
* which should be not included in the list
*/
_storeItemsForCollapsing: function( itemsToBeExcluded ){
var items;
itemsToBeExcluded = itemsToBeExcluded || {};
items = this.get( ITEMS );
Y.Array.each( items, function( item, index, items ){
var expanded, alwaysVisible;
expanded = item.get( EXPANDED );
alwaysVisible = item.get( ALWAYSVISIBLE );
if( expanded && !alwaysVisible && !itemsToBeExcluded[ item ] ){
this._forCollapsing[ item ] = {
'item': item
};
}
}, this );
},
/**
* Expands an item to given height. This includes also an update to item's user interface
*
* @method _expandItem
* @protected
* @param item {Y.AccordionItem} The item, which should be expanded.
* @param height {Number} The height to which we should expand the item
*/
_expandItem: function( item, height ){
var alwaysVisible = item.get( ALWAYSVISIBLE );
this._processExpanding( item, height );
this._setItemUI( item, true, alwaysVisible );
},
/**
* Expands an item to given height. Depending on the <code>useAnimation</code> setting,
* the process of expanding might be animated. This setting will be ignored, if <code>forceSkipAnimation</code> param
* is <code>true</code>.
*
* @method _processExpanding
* @protected
* @param item {Y.AccordionItem} An <code>Y.AccordionItem</code> instance to be expanded
* @param forceSkipAnimation {Boolean} If true, the animation will be skipped,
* without taking in consideration Accordion's <code>useAnimation</code> setting
* @param height {Number} The height to which item should be expanded
*/
_processExpanding: function( item, height, forceSkipAnimation ){
var anim, curAnim, animSettings, notifyOthers = false,
accAnimationSettings, body;
body = item.getStdModNode( WidgetStdMod.BODY );
this.fire( BEFOREITEMERESIZED, {
'item': item
});
if( body.get( "clientHeight" ) <= COLLAPSE_HEIGHT ){
notifyOthers = true;
this.fire( BEFOREITEMEXPAND, {
'item': item
});
}
if( !forceSkipAnimation && this.get( "useAnimation" ) ){
animSettings = item.get( ANIMATION ) || {};
anim = new Anim( {
node: body,
to: {
'height': height
}
});
anim.on( "end", Y.bind( this._onExpandComplete, this, item, notifyOthers ) );
accAnimationSettings = this.get( ANIMATION );
anim.set( "duration", animSettings.duration || accAnimationSettings.duration );
anim.set( "easing" , animSettings.easing || accAnimationSettings.easing );
curAnim = this._animations[ item ];
if( curAnim ){
curAnim.stop();
}
item.markAsExpanding( true );
this._animations[ item ] = anim;
anim.run();
} else {
body.setStyle( "height", height + PX );
this.fire( ITEMERESIZED, {
'item': item
});
if( notifyOthers ){
this.fire( ITEMEXPANDED, {
'item': item
});
}
}
},
/**
* Executes when animated expanding completes
*
* @method _onExpandComplete
* @protected
* @param item {Y.AccordionItem} An <code>Y.AccordionItem</code> instance which has been expanded
* @param notifyOthers {Boolean} If true, itemExpanded event will be fired
*/
_onExpandComplete: function( item, notifyOthers ){
delete this._animations[ item ];
item.markAsExpanding( false );
this.fire( ITEMERESIZED, {
'item': item
});
if( notifyOthers ){
this.fire( ITEMEXPANDED, {
'item': item
});
}
},
/**
* Collapse an item and update its user interface
*
* @method _collapseItem
* @protected
* @param item {Y.AccordionItem} The item, which should be collapsed
*/
_collapseItem: function( item ){
this._processCollapsing( item, COLLAPSE_HEIGHT );
this._setItemUI( item, false, false );
},
/**
* Collapse an item to given height. Depending on the <code>useAnimation</code> setting,
* the process of collapsing might be animated. This setting will be ignored, if <code>forceSkipAnimation</code> param
* is <code>true</code>.
*
* @method _processCollapsing
* @protected
* @param item {Y.AccordionItem} An <code>Y.AccordionItem</code> instance to be collapsed
* @param height {Number} The height to which item should be collapsed
* @param forceSkipAnimation {Boolean} If true, the animation will be skipped,
* without taking in consideration Accordion's <code>useAnimation</code> setting
*/
_processCollapsing: function( item, height, forceSkipAnimation ){
var anim, curAnim, animSettings, accAnimationSettings, body,
notifyOthers = (height === COLLAPSE_HEIGHT);
body = item.getStdModNode( WidgetStdMod.BODY );
this.fire( BEFOREITEMERESIZED, {
'item': item
});
if( notifyOthers ){
this.fire( BEFOREITEMCOLLAPSE, {
'item': item
});
}
if( !forceSkipAnimation && this.get( "useAnimation" ) ){
animSettings = item.get( ANIMATION ) || {};
anim = new Anim( {
node: body,
to: {
'height': height
}
});
anim.on( "end", Y.bind( this._onCollapseComplete, this, item, notifyOthers ) );
accAnimationSettings = this.get( ANIMATION );
anim.set( "duration", animSettings.duration || accAnimationSettings.duration );
anim.set( "easing" , animSettings.easing || accAnimationSettings.easing );
curAnim = this._animations[ item ];
if( curAnim ){
curAnim.stop();
}
item.markAsCollapsing( true );
this._animations[ item ] = anim;
anim.run();
} else {
body.setStyle( "height", height + PX );
this.fire( ITEMERESIZED, {
'item': item
});
if( notifyOthers ){
this.fire( ITEMCOLLAPSED, {
'item': item
});
}
}
},
/**
* Executes when animated collapsing completes
*
* @method _onCollapseComplete
* @protected
* @param item {Y.AccordionItem} An <code>Y.AccordionItem</code> instance which has been collapsed
* @param notifyOthers {Boolean} If true, itemCollapsed event will be fired
*/
_onCollapseComplete: function( item, notifyOthers ){
delete this._animations[ item ];
item.markAsCollapsing( false );
this.fire( ITEMERESIZED, {
item: item
});
if( notifyOthers ){
this.fire( ITEMCOLLAPSED, {
'item': item
});
}
},
/**
* Make an item draggable. The item can be reordered later.
*
* @method _initItemDragDrop
* @protected
* @param item {Y.AccordionItem} An <code>Y.AccordionItem</code> instance to be set as draggable
*/
_initItemDragDrop: function( item ){
var itemHeader, dd, bb, itemBB, ddrop;
itemHeader = item.getStdModNode( WidgetStdMod.HEADER );
if( itemHeader.dd ){
return;
}
bb = this.get( BOUNDING_BOX );
itemBB = item.get( BOUNDING_BOX );
dd = new Y.DD.Drag({
node: itemHeader,
groups: [ DRAGGROUP ]
}).plug(Y.Plugin.DDProxy, {
moveOnEnd: false
}).plug(Y.Plugin.DDConstrained, {
constrain2node: bb
});
ddrop = new Y.DD.Drop({
node: itemBB,
groups: [ DRAGGROUP ]
});
dd.on ( "drag:start", Y.bind( this._onDragStart, this, dd ) );
dd.on ( "drag:end" , Y.bind( this._onDragEnd, this, dd ) );
dd.after( "drag:end" , Y.bind( this._afterDragEnd, this, dd ) );
dd.on ( "drag:drophit", Y.bind( this._onDropHit, this, dd ) );
},
/**
* Sets the label of the item being dragged on the drag proxy.
* Fires beforeItemReorder event - returning false will cancel reordering
*
* @method _onDragStart
* @protected
* @param dd {Y.DD.Drag} The drag instance of the item
* @param e {Event} the DD instance's drag:start custom event
*/
_onDragStart: function( dd, e ){
var dragNode, item;
item = this.getItem( dd.get( NODE ).get( PARENT_NODE ) );
dragNode = dd.get( "dragNode" );
dragNode.addClass( C_PROXY_VISIBLE );
dragNode.set( "innerHTML", item.get( "label" ) );
return this.fire( BEFOREITEMREORDER, { 'item': item } );
},
/**
* Restores HTML structure of the drag proxy.
* Fires beforeEndItemReorder event - returning false will cancel reordering
*
* @method _onDragEnd
* @protected
* @param dd {Y.DD.Drag} The drag instance of the item
* @param e {Event} the DD instance's drag:end custom event
*/
_onDragEnd: function( dd, e ){
var dragNode, item;
dragNode = dd.get( "dragNode" );
dragNode.removeClass( C_PROXY_VISIBLE );
dragNode.set( "innerHTML", "" );
item = this.getItem( dd.get( NODE ).get( PARENT_NODE ) );
return this.fire( BEFOREENDITEMREORDER, { 'item': item } );
},
/**
* Set drophit to false in dragdrop instance's custom value (if there has been drophit) and fires itemReordered event
*
* @method _afterDragEnd
* @protected
* @param dd {Y.DD.Drag} The drag instance of the item
* @param e {Event} the DD instance's drag:end custom event
*/
_afterDragEnd: function( dd, e ){
var item, data;
data = dd.get( DATA );
if( data.drophit ){
item = this.getItem( dd.get( NODE ).get( PARENT_NODE ) );
dd.set( DATA, {
drophit: false
} );
return this.fire( ITEMREORDERED, { 'item': item } );
}
return true;
},
/**
* Moves the source item before or after target item.
*
* @method _onDropHit
* @protected
* @param dd {Y.DD.Drag} The drag instance of the item
* @param e {Event} the DD instance's drag:drophit custom event
*/
_onDropHit: function( dd, e) {
var mineIndex, targetItemIndex, targetItemBB, itemBB, cb,
goingUp, items, targetItem, item;
item = this.getItem( dd.get( NODE ).get( PARENT_NODE ) );
targetItem = this.getItem( e.drop.get( NODE ) );
if( targetItem === item ){
return false;
}
mineIndex = this.getItemIndex( item );
targetItemIndex = this.getItemIndex( targetItem );
targetItemBB = targetItem.get( BOUNDING_BOX );
itemBB = item.get( BOUNDING_BOX );
cb = this.get( CONTENT_BOX );
goingUp = false;
items = this.get( ITEMS );
if( targetItemIndex < mineIndex ){
goingUp = true;
}
cb.removeChild( itemBB );
if( goingUp ){
cb. insertBefore( itemBB, targetItemBB );
items.splice( mineIndex, 1 );
items.splice( targetItemIndex, 0, item );
} else {
cb. insertBefore( itemBB, targetItemBB.next( C_ITEM ) );
items.splice( targetItemIndex + 1, 0, item );
items.splice( mineIndex, 1 );
}
dd.set( DATA, {
drophit: true
});
return true;
},
/**
* Process items as result of user interaction or properties change.
* This includes four steps:
* 1. Update the properties of the items
* 2. Collapse all items stored in the list for collapsing
* 3. Adjust all stretch items
* 4. Expand items stored in the list for expanding
*
* @method _processItems
* @protected
*/
_processItems: function(){
var forCollapsing, forExpanding, itemCont, heightPerStretchItem,
height, heightSettings, item;
forCollapsing = this._forCollapsing;
forExpanding = this._forExpanding;
this._setItemsProperties();
for( item in forCollapsing ){
if( forCollapsing.hasOwnProperty( item ) ){
itemCont = forCollapsing[ item ];
this._collapseItem( itemCont.item );
}
}
heightPerStretchItem = this._adjustStretchItems();
for( item in forExpanding ){
if( forExpanding.hasOwnProperty( item ) ){
itemCont = forExpanding[ item ];
item = itemCont.item;
height = heightPerStretchItem;
heightSettings = item.get( CONTENT_HEIGHT );
if( heightSettings.method !== STRETCH ){
height = this._getItemContentHeight( item );
}
this._expandItem( item, height );
}
}
this._forCollapsing = {};
this._forExpanding = {};
},
/**
* Update properties of items, which were stored in the lists for collapsing or expanding
*
* @method _setItemsProperties
* @protected
*/
_setItemsProperties: function (){
var forCollapsing, forExpanding, itemData;
forCollapsing = this._forCollapsing;
forExpanding = this._forExpanding;
for( itemData in forCollapsing ){
if( forCollapsing.hasOwnProperty( itemData ) ){
itemData = forCollapsing[ itemData ];
this._setItemProperties( itemData.item, false, false );
}
}
for( itemData in forExpanding ){
if( forExpanding.hasOwnProperty( itemData ) ){
itemData = forExpanding[ itemData ];
this._setItemProperties( itemData.item, true, itemData.alwaysVisible );
}
}
},
/**
* Handles the change of "expand" property of given item
*
* @method _afterItemExpand
* @protected
* @param params {EventFacade} The event facade for the attribute change
*/
_afterItemExpand: function( params ){
var expanded, item, alwaysVisible, collapseOthersOnExpand;
if( params.internalCall ){
return;
}
expanded = params.newVal;
item = params.currentTarget;
alwaysVisible = item.get( ALWAYSVISIBLE );
collapseOthersOnExpand = this.get( COLLAPSEOTHERSONEXPAND );
if( expanded ){
this._forExpanding[ item ] = {
'item': item,
'alwaysVisible': alwaysVisible
};
if( collapseOthersOnExpand ){
this._storeItemsForCollapsing();
}
} else {
this._forCollapsing[ item ] = {
'item': item
};
}
this._processItems();
},
/**
* Handles the change of "alwaysVisible" property of given item
*
* @method _afterItemAlwaysVisible
* @protected
* @param params {EventFacade} The event facade for the attribute change
*/
_afterItemAlwaysVisible: function( params ){
var item, alwaysVisible, expanded;
if( params.internalCall ){
return;
}
alwaysVisible = params.newVal;
item = params.currentTarget;
expanded = item.get( EXPANDED );
if( alwaysVisible ){
if( expanded ){
this._setItemProperties( item, true, true );
this._setItemUI( item, true, true );
return;
} else {
this._forExpanding[ item ] = {
'item': item,
'alwaysVisible': true
};
this._storeItemsForCollapsing();
}
} else {
if( expanded ){
this._setItemUI( item, true, false );
return;
} else {
return;
}
}
this._processItems();
},
/**
* Handles the change of "contentHeight" property of given item
*
* @method _afterContentHeight
* @protected
* @param params {EventFacade} The event facade for the attribute change
*/
_afterContentHeight: function( params ){
var item, itemContentHeight, body, bodyHeight, expanded;
item = params.currentTarget;
this._adjustStretchItems();
if( params.newVal.method !== STRETCH ){
expanded = item.get( EXPANDED );
itemContentHeight = this._getItemContentHeight( item );
body = item.getStdModNode( WidgetStdMod.BODY );
bodyHeight = this._getNodeOffsetHeight( body );
if( itemContentHeight < bodyHeight ){
this._processCollapsing( item, itemContentHeight, !expanded );
} else if( itemContentHeight > bodyHeight ){
this._processExpanding( item, itemContentHeight, !expanded );
}
}
},
/**
* Handles the change of "contentUpdate" property of given item
*
* @method _afterContentUpdate
* @protected
* @param params {EventFacade} The event facade for the attribute change
*/
_afterContentUpdate : function( params ){
var item, body, bodyHeight, expanded, auto, anim;
item = params.currentTarget;
auto = item.get( "contentHeight" ).method === "auto";
expanded = item.get( EXPANDED );
body = item.getStdModNode( WidgetStdMod.BODY );
bodyHeight = this._getNodeOffsetHeight( body );
if( auto && expanded && params.src !== Y.Widget.UI_SRC ){
Y.later( 0, this, function(){
var itemContentHeight = this._getItemContentHeight( item );
if( itemContentHeight !== bodyHeight ){
anim = this._animations[ item ];
// stop waiting animation
if( anim ){
anim.stop();
}
this._adjustStretchItems();
if( itemContentHeight < bodyHeight ){
this._processCollapsing( item, itemContentHeight, !expanded );
} else if( itemContentHeight > bodyHeight ){
this._processExpanding( item, itemContentHeight, !expanded );
}
}
} );
}
},
/**
* Subscribe for resize event, which could be provided from the browser or from an arbitrary object.
* For example, if there is LayoutManager in the page, it is preferable to subscribe to its resize event,
* instead to those, which browser provides.
*
* @method _setUpResizing
* @protected
* @param value {String|Object} String "default" or object with the following properties:
* <dl>
* <dt>sourceObject</dt>
* <dd>An abbitrary object</dd>
* <dt>resizeEvent</dt>
* <dd>The name of its resize event</dd>
* </dl>
*/
_setUpResizing: function( value ){
if( this._resizeEventHandle ){
this._resizeEventHandle.detach();
}
if( value === DEFAULT ){
this._resizeEventHandle = Y.on( 'windowresize', Y.bind( this._adjustStretchItems, this ) );
} else {
this._resizeEventHandle = value.sourceObject.on( value.resizeEvent, Y.bind( this._adjustStretchItems, this ) );
}
},
/**
* Creates one or more items found in Accordion's <code>contentBox</code>
*
* @method renderUI
* @protected
*/
renderUI: function(){
var srcNode, itemsDom, contentBox, srcNodeId;
srcNode = this.get( SRCNODE );
contentBox = this.get( CONTENT_BOX );
srcNodeId = srcNode.get( "id" );
/*
* Widget 3.1 workaround - the Id of contentBox is generated by YUI, instead to keep srcNode's Id, so we set it manually
*/
contentBox.set( "id", srcNodeId );
itemsDom = srcNode.all( "> ." + C_ITEM );
itemsDom.each( function( itemNode, index, itemsDom ){
var newItem;
if( !this.getItem( itemNode ) ){
newItem = new Y.AccordionItem({
srcNode: itemNode,
id : itemNode.get( "id" )
});
this.addItem( newItem );
}
}, this );
},
/**
* Add listener(s) to <code>itemChosen</code> event in Accordion's content box.
* If itemChosen is an Array, this function will invoke multiple times _bindItemChosenEvent
*
* @method bindUI
* @protected
*/
bindUI: function(){
var i, itemChosenEvent, length;
itemChosenEvent = this.get( 'itemChosen' );
if( Lang.isArray(itemChosenEvent) ){
length = itemChosenEvent.length;
for( i = 0; i < length; i++ ) {
this._bindItemChosenEvent(itemChosenEvent[i]);
}
} else {
this._bindItemChosenEvent(itemChosenEvent);
}
},
/**
* Listening for itemChosen event, determines the source (is that iconClose, iconAlwaysVisisble, etc.) and
* invokes this._onItemChosen for further processing
*
* @method _onItemChosenEvent
* @protected
*
* @param e {Event} The itemChosen event
*/
_onItemChosenEvent: function(e){
var header, itemNode, item, iconAlwaysVisible,
iconClose, srcIconAlwaysVisible, srcIconClose;
header = e.currentTarget;
itemNode = header.get( PARENT_NODE );
item = this.getItem( itemNode );
iconAlwaysVisible = item.get( ICON_ALWAYSVISIBLE );
iconClose = item.get( ICON_CLOSE );
srcIconAlwaysVisible = (iconAlwaysVisible === e.target);
srcIconClose = (iconClose === e.target);
this._onItemChosen( item, srcIconAlwaysVisible, srcIconClose );
},
/**
* Add an item to Accordion. Items could be added/removed multiple times and they
* will be rendered in the process of adding, if not.
* The item will be expanded, collapsed, or set as always visible depending on the
* settings. Item's properties will be also updated, if they are incomplete.
* For example, if <code>alwaysVisible</code> is true, but <code>expanded</code>
* property is false, it will be set to true also.
*
* If the second param, <code>parentItem</code> is an <code>Y.AccordionItem</code> instance,
* registered in Accordion, the item will be added as child of the <code>parentItem</code>
*
* @method addItem
* @param item {Y.AccordionItem} The item to be added in Accordion
* @param parentItem {Y.AccordionItem} (optional) This item will be the parent of the item being added
*
* @return {Boolean} True in case of successfully added item, false otherwise
*/
addItem: function( item, parentItem ){
var expanded, alwaysVisible, itemBody, itemBodyContent, itemIndex, items, contentBox,
itemHandles, itemContentBox, res, children;
res = this.fire( BEFOREITEMADD, {
'item': item
});
if( !res ){
return false;
}
items = this.get( ITEMS );
contentBox = this.get( CONTENT_BOX );
itemContentBox = item.get( CONTENT_BOX );
if( !itemContentBox.inDoc() ){
if( parentItem ){
itemIndex = this.getItemIndex( parentItem );
if( itemIndex < 0 ){
return false;
}
items.splice( itemIndex, 0, item );
contentBox.insertBefore( itemContentBox, parentItem.get( BOUNDING_BOX ) );
} else {
items.push( item );
contentBox.insertBefore( itemContentBox, null );
}
} else {
children = contentBox.get( CHILDREN );
res = children.some( function( node, index, nodeList ){
if( node === itemContentBox ){
items.splice( index, 0, item );
return true;
} else {
return false;
}
}, this );
if( !res ){
return false;
}
}
itemBody = item.getStdModNode( WidgetStdMod.BODY );
itemBodyContent = item.get( BODYCONTENT );
if( !itemBody && !itemBodyContent ){
item.set( BODYCONTENT, "" );
}
if( !item.get( RENDERED ) ){
item.render();
}
expanded = item.get( EXPANDED );
alwaysVisible = item.get( ALWAYSVISIBLE );
expanded = expanded || alwaysVisible;
if( expanded ){
this._forExpanding[ item ] = {
'item': item,
'alwaysVisible': alwaysVisible
};
} else {
this._forCollapsing[ item ] = {
'item': item
};
}
this._processItems();
if( this.get( "reorderItems" ) ){
this._initItemDragDrop( item );
}
itemHandles = this._itemsHandles[ item ];
if( !itemHandles ){
itemHandles = {};
}
itemHandles = {
"expandedChange" : item.after( "expandedChange", Y.bind( this._afterItemExpand, this ) ),
"alwaysVisibleChange" : item.after( "alwaysVisibleChange", Y.bind( this._afterItemAlwaysVisible, this ) ),
"contentHeightChange" : item.after( "contentHeightChange", Y.bind( this._afterContentHeight, this ) ),
"contentUpdate" : item.after( "contentUpdate", Y.bind( this._afterContentUpdate, this ) )
};
this._itemsHandles[ item ] = itemHandles;
this.fire( ITEMADDED, {
'item': item
});
return true;
},
/**
* Removes an previously registered item in Accordion
*
* @method removeItem
* @param p_item {Y.AccordionItem|Number} The item to be removed, or its index
* @return {Y.AccordionItem} The removed item or null if not found
*/
removeItem: function( p_item ){
var items, bb, item = null, itemIndex, allowed;
items = this.get( ITEMS );
if( Lang.isNumber( p_item ) ){
itemIndex = p_item;
} else if( p_item instanceof Y.AccordionItem ){
itemIndex = this.getItemIndex( p_item );
} else {
return null;
}
if( itemIndex >= 0 ){
allowed = this.fire( BEFOREITEMREMOVE, {
item: p_item
});
if( !allowed ){
return null;
}
item = items.splice( itemIndex, 1 )[0];
this._removeItemHandles( item );
bb = item.get( BOUNDING_BOX );
bb.remove();
this._adjustStretchItems();
this.fire( ITEMREMOVED, {
item: p_item
});
}
return item;
},
/**
* Searching for item, previously registered in Accordion
*
* @method getItem
* @param param {Number|Y.Node} If number, this must be item's index.
* If Node, it should be the value of item's <code>contentBox</code> or <code>boundingBox</code> properties
*
* @return {Y.AccordionItem} The found item or null
*/
getItem: function( param ){
var items = this.get( ITEMS ), item = null;
if( Lang.isNumber( param ) ){
item = items[ param ];
return (item instanceof Y.AccordionItem) ? item : null;
} else if( param instanceof Node ){
Y.Array.some( items, function( tmpItem, index, items ){
var contentBox = tmpItem.get( CONTENT_BOX );
/*
* Both contentBox and boundingBox point to same node, so it is safe to check only one of them
*/
if( contentBox === param ){
item = tmpItem;
return true;
} else {
return false;
}
}, this );
}
return item;
},
/**
* Looking for the index of previously registered item
*
* @method getItemIndex
* @param item {Y.AccordionItem} The item which index should be returned
* @return {Number} Item index or <code>-1</code> if item has been not found
*/
getItemIndex: function( item ){
var res = -1, items;
if( item instanceof Y.AccordionItem ){
items = this.get( ITEMS );
Y.Array.some( items, function( tmpItem, index, items ){
if( tmpItem === item ){
res = index;
return true;
} else {
return false;
}
}, this );
}
return res;
},
/**
* Overwrites Y.WidgetStdMod fuction in order to resolve Widget 3.1 issue:<br>
* If CONTENT_TEMPLATE is null, in renderUI the result of the following code:
* <code>this.getStdModNode( Y.WidgetStdMod.HEADER );</code> is null.
* The same is with <code>this.getStdModNode( Y.WidgetStdMod.BODY );</code>.
*
* @method _findStdModSection
* @protected
* @param {String} section The section for which the render Node is to be found. Either WidgetStdMod.HEADER, WidgetStdMod.BODY or WidgetStdMod.FOOTER.
* @return {Node} The rendered node for the given section, or null if not found.
*/
_findStdModSection: function(section) {
return this.get(SRCNODE).one("> ." + Y.WidgetStdMod.SECTION_CLASS_NAMES[section]);
},
CONTENT_TEMPLATE : null
}, {
/**
* Static property provides a string to identify the class.
*
* @property Accordion.NAME
* @type String
* @static
*/
NAME : AccName,
/**
* Static property used to define the default attribute
* configuration for the Accordion.
*
* @property Accordion.ATTRS
* @type Object
* @static
*/
ATTRS : {
/**
* @description The event on which Accordion should listen for user interactions.
* The value can be also 'mousedown', 'mouseup' or ['mouseenter','click'].
* Mousedown event can be used if drag&drop is not enabled.
*
* @attribute itemChosen
* @default click
* @type String|Array
*/
itemChosen: {
value: "click",
validator: function( value ) {
return Lang.isString(value) || Lang.isArray(value);
}
},
/**
* @description Contains the items, currently added to Accordion
*
* @attribute items
* @readOnly
* @default []
* @type Array
*/
items: {
value: [],
readOnly: true,
validator: Lang.isArray
},
/**
* @attribute resizeEvent
*
* @description The event on which Accordion should listen for resizing.
* The value must be one of these:
* <ul>
* <li> String "default" - the Accordion will subscribe to Y.windowresize event
* </li>
* <li> An object in the following form:
* {
* sourceObject: some_javascript_object,
* resizeEvent: an_event_to_subscribe
* }
* </li>
* </ul>
* For example, if we are using LayoutManager's instance as sourceObject, we will have to use its "resize" event as resizeEvent
*
* @default "default"
* @type String or Object
*/
resizeEvent: {
value: DEFAULT,
validator: function( value ){
if( value === DEFAULT ){
return true;
} else if( Lang.isObject(value) ){
if( Lang.isValue( value.sourceObject ) && Lang.isValue( value.resizeEvent ) ){
return true;
}
}
return false;
}
},
/**
* @attribute useAnimation
* @description Boolean indicating that Accordion should use animation when expanding or collapsing items.
*
* @default true
* @type Boolean
*/
useAnimation: {
value: true,
validator: Lang.isBoolean
},
/**
* @attribute animation
* @description Animation config values, see Y.Animation
*
* @default <code> {
* duration: 1,
* easing: Easing.easeOutStrong
* }
* </code>
*
* @type Object
*/
animation: {
value: {
duration: 1,
easing: Easing.easeOutStrong
},
validator: function( value ){
return Lang.isObject( value ) && Lang.isNumber( value.duration ) &&
Lang.isFunction( value.easing );
}
},
/**
* @attribute reorderItems
* @description Boolean indicating that items can be reordered via drag and drop.<br>
*
* Enabling items reordering requires also including the optional drag and drop modules in YUI instance:<br>
* 'dd-constrain', 'dd-proxy', 'dd-drop', or just 'dd'
*
* @default false
* @type Boolean
*/
reorderItems: {
value: false,
validator: function(value){
return Lang.isBoolean(value) && !Lang.isUndefined( Y.DD );
}
},
/**
* @attribute collapseOthersOnExpand
* @description If true, on item expanding, all other expanded and not set as always visible items, will be collapsed
* Otherwise, they will stay open
*
* @default true
* @type Boolean
*/
collapseOthersOnExpand: {
value: true,
validator: Lang.isBoolean
}
}
});
}());