/**
* Provides the Accordion class
*
* @module accordion
*/
(function(){
/**
* Create an AccordionItem widget.
*
* @param config {Object} Object literal specifying AccordionItem configuration properties.
*
* @class AccordionItem
* @constructor
* @extends Widget
*/
function AccordionItem( config ){
AccordionItem.superclass.constructor.apply( this, arguments );
}
// Local constants
var Lang = Y.Lang,
Base = Y.Base,
Node = Y.Node,
JSON = Y.JSON,
WidgetStdMod = Y.WidgetStdMod,
AccItemName = "accordion-item",
getCN = Y.ClassNameManager.getClassName,
C_ICONEXPANDED_EXPANDING = getCN( AccItemName, "iconexpanded", "expanding" ),
C_ICONEXPANDED_COLLAPSING = getCN( AccItemName, "iconexpanded", "collapsing" ),
C_ICON = getCN( AccItemName, "icon" ),
C_LABEL = getCN( AccItemName, "label" ),
C_ICONALWAYSVISIBLE = getCN( AccItemName, "iconalwaysvisible" ),
C_ICONSCONTAINER = getCN( AccItemName, "icons" ),
C_ICONEXPANDED = getCN( AccItemName, "iconexpanded" ),
C_ICONCLOSE = getCN( AccItemName, "iconclose" ),
C_ICONCLOSE_HIDDEN = getCN( AccItemName, "iconclose", "hidden" ),
C_ICONEXPANDED_ON = getCN( AccItemName, "iconexpanded", "on" ),
C_ICONEXPANDED_OFF = getCN( AccItemName, "iconexpanded", "off" ),
C_ICONALWAYSVISIBLE_ON = getCN( AccItemName, "iconalwaysvisible", "on" ),
C_ICONALWAYSVISIBLE_OFF = getCN( AccItemName, "iconalwaysvisible", "off" ),
C_EXPANDED = getCN( AccItemName, "expanded" ),
C_CLOSABLE = getCN( AccItemName, "closable" ),
C_ALWAYSVISIBLE = getCN( AccItemName, "alwaysvisible" ),
C_CONTENTHEIGHT = getCN( AccItemName, "contentheight" ),
TITLE = "title",
STRINGS = "strings",
CONTENT_BOX = "contentBox",
RENDERED = "rendered",
CLASS_NAME = "className",
AUTO = "auto",
STRETCH = "stretch",
FIXED = "fixed",
HEADER_SELECTOR = ".yui-widget-hd",
DOT = ".",
HEADER_SELECTOR_SUB = ".yui-widget-hd " + DOT,
INNER_HTML = "innerHTML",
ICONS_CONTAINER = "iconsContainer",
ICON = "icon",
NODE_LABEL = "nodeLabel",
ICON_ALWAYSVISIBLE = "iconAlwaysVisible",
ICON_EXPANDED = "iconExpanded",
ICON_CLOSE = "iconClose",
HREF = "href",
HREF_VALUE = "#",
YUICONFIG = "yuiConfig",
HEADER_CONTENT = "headerContent";
/**
* Static property provides a string to identify the class.
*
* @property AccordionItem.NAME
* @type String
* @static
*/
AccordionItem.NAME = AccItemName;
/**
* Static property used to define the default attribute
* configuration for the Accordion.
*
* @property Accordion.ATTRS
* @type Object
* @static
*/
AccordionItem.ATTRS = {
/**
* @description Item's icon
*
* @attribute icon
* @default null
* @type Node
*/
icon: {
value: null,
validator: function( value ){
return value instanceof Node;
}
},
/**
* @description The label of the item
*
* @attribute label
* @default " "
* @type String
*/
label: {
value: " ",
validator: Lang.isString
},
/**
* @description The node, contains label
*
* @attribute nodeLabel
* @default null
* @type Node
*/
nodeLabel: {
value: null,
validator: function( value ){
return value instanceof Node;
}
},
/**
* @description The container of iconAlwaysVisible, iconExpanded and iconClose
*
* @attribute iconsContainer
* @default null
* @type Node
*/
iconsContainer: {
value: null,
validator: function( value ){
return value instanceof Node;
}
},
/**
* @description Icon expanded
*
* @attribute iconExpanded
* @default null
* @type Node
*/
iconExpanded: {
value: null,
validator: function( value ){
return value instanceof Node;
}
},
/**
* @description Icon always visible
*
* @attribute iconAlwaysVisible
* @default null
* @type Node
*/
iconAlwaysVisible: {
value: null,
validator: function( value ){
return value instanceof Node;
}
},
/**
* @description Icon close, or null if the item is not closable
*
* @attribute iconClose
* @default null
* @type Node
*/
iconClose: {
value: null,
validator: function( value ){
return value instanceof Node;
}
},
/**
* @description Get/Set the expanded status of the item
*
* @attribute expanded
* @default false
* @type Boolean
*/
expanded: {
value: false,
validator: Lang.isBoolean
},
/**
* @description Describe the method, which will be used when expanding/collapsing
* the item. The value should be an object with at least one property ("method"):
* <dl>
* <dt>method</dt>
* <dd>The method can be one of these: "auto", "fixed" and "stretch"</dd>
* <dt>height</dt>
* <dd>Must be set only if method's value is "fixed"</dd>
* </dl>
*
* @attribute contentHeight
* @default auto
* @type Object
*/
contentHeight: {
value: {
method: AUTO
},
validator: function( value ){
if( Lang.isObject( value ) ){
if( value.method === AUTO ){
return true;
} else if( value.method === STRETCH ){
return true;
} else if( value.method === FIXED && Lang.isNumber( value.height ) &&
value.height >= 0 ){
return true;
}
}
return false;
}
},
/**
* @description Get/Set the expanded status of the item
*
* @attribute alwaysVisible
* @default false
* @type Boolean
*/
alwaysVisible: {
value: false,
validator: Lang.isBoolean
},
/**
* @description Get/Set the animaton specific settings. By default there are no any settings.
* If set, they will overwrite Accordion's animation settings
*
* @attribute animation
* @default {}
* @type Object
*/
animation: {
value: {},
validator: Lang.isObject
},
/**
* @description Provides client side string localization support.
*
* @attribute strings
* @default Object English messages
* @type Object
*/
strings: {
value: {
title_always_visible_off: "Click to set always visible on",
title_always_visible_on: "Click to set always visible off",
title_iconexpanded_off: "Click to expand",
title_iconexpanded_on: "Click to collapse",
title_iconclose: "Click to close"
}
},
/**
* @description Flag, indicated whether the item can be closed by user, or not
* If yes, there will be placed close icon, otherwise not
*
* @attribute closable
* @default false
* @type Boolean
*/
closable: {
value: false,
validator: Lang.isBoolean
}
};
/**
* Static Object hash used to capture existing markup for progressive
* enhancement. Keys correspond to config attribute names and values
* are selectors used to inspect the contentBox for an existing node
* structure.
*
* @property AccordionItem.HTML_PARSER
* @type Object
* @protected
* @static
*/
AccordionItem.HTML_PARSER = {
icon: function( contentBox ){
var node, iconSelector;
iconSelector = HEADER_SELECTOR_SUB + C_ICON;
node = contentBox.query( iconSelector );
return node;
},
label: function( contentBox ){
var node, labelSelector, yuiConfig;
yuiConfig = this._getConfigDOMAttribute( contentBox );
if( yuiConfig && Lang.isValue( yuiConfig.label ) ){
return yuiConfig.label;
}
labelSelector = HEADER_SELECTOR_SUB + C_LABEL;
node = contentBox.query( labelSelector );
return (node) ? node.get( INNER_HTML ) : null;
},
nodeLabel: function( contentBox ){
var node, labelSelector;
labelSelector = HEADER_SELECTOR_SUB + C_LABEL;
node = contentBox.query( labelSelector );
return node;
},
iconsContainer: function( contentBox ){
var node, iconsContainer;
iconsContainer = HEADER_SELECTOR_SUB + C_ICONSCONTAINER;
node = contentBox.query( iconsContainer );
return node;
},
iconAlwaysVisible: function( contentBox ){
var node, iconAlwaysVisibleSelector;
iconAlwaysVisibleSelector = HEADER_SELECTOR_SUB + C_ICONALWAYSVISIBLE;
node = contentBox.query( iconAlwaysVisibleSelector );
return node;
},
iconExpanded: function( contentBox ){
var node, iconExpandedSelector;
iconExpandedSelector = HEADER_SELECTOR_SUB + C_ICONEXPANDED;
node = contentBox.query( iconExpandedSelector );
return node;
},
iconClose: function( contentBox ){
var node, iconCloseSelector;
iconCloseSelector = HEADER_SELECTOR_SUB + C_ICONCLOSE;
node = contentBox.query( iconCloseSelector );
return node;
},
expanded: function( contentBox ){
var yuiConfig = this._getConfigDOMAttribute( contentBox );
if( yuiConfig && Lang.isValue( yuiConfig.expanded ) ){
return yuiConfig.expanded;
}
return contentBox.hasClass( C_EXPANDED );
},
alwaysVisible: function( contentBox ){
var value, yuiConfig;
yuiConfig = this._getConfigDOMAttribute( contentBox );
if( yuiConfig && Lang.isValue( yuiConfig.alwaysVisible ) ){
value = yuiConfig.alwaysVisible;
} else {
value = contentBox.hasClass( C_ALWAYSVISIBLE );
}
if( Lang.isBoolean(value) && value ){
this.set( "expanded", true, {
internalCall: true
} );
}
return value;
},
closable: function( contentBox ){
var yuiConfig = this._getConfigDOMAttribute( contentBox );
if( yuiConfig && Lang.isValue( yuiConfig.closable ) ){
return yuiConfig.closable;
}
return contentBox.hasClass( C_CLOSABLE );
},
contentHeight: function( contentBox ){
var contentHeightClass, classValue, height = 0, i, length, index, chr, yuiConfig;
yuiConfig = this._getConfigDOMAttribute( contentBox );
if( yuiConfig && yuiConfig.contentHeight ){
return yuiConfig.contentHeight;
}
classValue = contentBox.get( CLASS_NAME );
contentHeightClass = C_CONTENTHEIGHT + '-';
index = classValue.indexOf( contentHeightClass, 0);
if( index >= 0 ){
length = classValue.length;
index += contentHeightClass.length;
classValue = classValue.substring( index );
if( classValue.match( /^auto\s*/g ) ){
return {
method: AUTO
};
} else if( classValue.match( /^stretch\s*/g ) ){
return {
method: STRETCH
};
} else if( classValue.match( /^fixed-\d+/g ) ){
for( i = 6, length = classValue.length; i < length; i++ ){ // 6 = "fixed-".length
chr = classValue.charAt(i);
chr = parseInt( chr, 10 );
if( Lang.isNumber( chr ) ){
height = (height * 10) + chr;
} else {
break;
}
}
return {
method: FIXED,
height: height
};
}
}
return null;
}
};
/**
* The template HTML strings for each of header components.
* e.g.
* <pre>
* {
* icon : '<a class="yui-accordion-item-icon"></a>',
* label: '<a href="#" class="yui-accordion-item-label"></a>',
* iconsContainer: '<div class="yui-accordion-item-icons"></div>',
* iconAlwaysVisible: '<a href="#" class="yui-accordion-item-iconalwaysvisible"></a>',
* iconExpanded: '<a href="#" class="yui-accordion-item-iconexpanded"></a>',
* iconClose: '<a href="#" class="yui-accordion-item-iconclose yui-accordion-item-iconclose-hidden"></a>'
* }
* </pre>
* @property WidgetStdMod.TEMPLATES
* @type Object
*/
AccordionItem.TEMPLATES = {
icon : '<a class="' + C_ICON + '"></a>',
label: '<a href="#" class="' + C_LABEL + '"></a>',
iconsContainer: '<div class="' + C_ICONSCONTAINER + '"></div>',
iconExpanded: ['<a href="#" class="', C_ICONEXPANDED, ' ', C_ICONEXPANDED_OFF, '"></a>'].join(''),
iconAlwaysVisible: ['<a href="#" class="', C_ICONALWAYSVISIBLE, ' ', C_ICONALWAYSVISIBLE_OFF, '"></a>'].join(''),
iconClose: ['<a href="#" class="', C_ICONCLOSE, ' ', C_ICONCLOSE_HIDDEN, '"></a>'].join('')
};
// AccordionItem extends Widget
Y.extend( AccordionItem, Y.Widget, {
/**
* Creates the header content
*
* @method _createHeader
* @protected
*/
_createHeader: function(){
var closable, templates, strings, iconsContainer,
icon, nodeLabel, iconExpanded, iconAlwaysVisible, iconClose;
icon = this.get( ICON );
nodeLabel = this.get( NODE_LABEL );
iconExpanded = this.get( ICON_EXPANDED );
iconAlwaysVisible = this.get( ICON_ALWAYSVISIBLE );
iconClose = this.get( ICON_CLOSE );
iconsContainer = this.get( ICONS_CONTAINER );
strings = this.get( STRINGS );
closable = this.get( "closable" );
templates = AccordionItem.TEMPLATES;
if( !icon ){
icon = Node.create( templates.icon );
this.set( ICON, icon );
}
if( !nodeLabel ){
nodeLabel = Node.create( templates.label );
this.set( NODE_LABEL, nodeLabel );
} else if( !nodeLabel.hasAttribute( HREF ) ){
nodeLabel.setAttribute( HREF, HREF_VALUE );
}
nodeLabel.setContent( this.get( "label" ) );
if( !iconsContainer ){
iconsContainer = Node.create( templates.iconsContainer );
this.set( ICONS_CONTAINER, iconsContainer );
}
if( !iconAlwaysVisible ){
iconAlwaysVisible = Node.create( templates.iconAlwaysVisible );
iconAlwaysVisible.setAttribute( TITLE, strings.title_always_visible_off );
this.set( ICON_ALWAYSVISIBLE, iconAlwaysVisible );
} else if( !iconAlwaysVisible.hasAttribute( HREF ) ){
iconAlwaysVisible.setAttribute( HREF, HREF_VALUE );
}
if( !iconExpanded ){
iconExpanded = Node.create( templates.iconExpanded );
iconExpanded.setAttribute( TITLE, strings.title_iconexpanded_off );
this.set( ICON_EXPANDED, iconExpanded );
} else if( !iconExpanded.hasAttribute( HREF ) ){
iconExpanded.setAttribute( HREF, HREF_VALUE );
}
if( !iconClose ){
iconClose = Node.create( templates.iconClose );
iconClose.setAttribute( TITLE, strings.title_iconclose );
this.set( ICON_CLOSE, iconClose );
} else if( !iconClose.hasAttribute( HREF ) ){
iconClose.setAttribute( HREF, HREF_VALUE );
}
if( closable ){
iconClose.removeClass( C_ICONCLOSE_HIDDEN );
} else {
iconClose.addClass( C_ICONCLOSE_HIDDEN );
}
this._addHeaderComponents();
},
/**
* Add label and icons in the header. Also, it creates header in if not set from markup
*
* @method _addHeaderComponents
* @protected
*/
_addHeaderComponents: function(){
var header, icon, nodeLabel, iconsContainer, iconExpanded,
iconAlwaysVisible, iconClose;
icon = this.get( ICON );
nodeLabel = this.get( NODE_LABEL );
iconExpanded = this.get( ICON_EXPANDED );
iconAlwaysVisible = this.get( ICON_ALWAYSVISIBLE );
iconClose = this.get( ICON_CLOSE );
iconsContainer = this.get( ICONS_CONTAINER );
header = this.get( HEADER_CONTENT );
if( !header ){
header = new Node( document.createDocumentFragment() );
header.appendChild( icon );
header.appendChild( nodeLabel );
header.appendChild( iconsContainer );
iconsContainer.appendChild( iconAlwaysVisible );
iconsContainer.appendChild( iconExpanded );
iconsContainer.appendChild( iconClose );
this.setStdModContent( WidgetStdMod.HEADER, header, WidgetStdMod.REPLACE );
} else {
if( !header.contains( icon ) ){
if( header.contains( nodeLabel ) ){
header.insertBefore( icon, nodeLabel );
} else {
header.appendChild( icon );
}
}
if( !header.contains( nodeLabel ) ){
header.appendChild( nodeLabel );
}
if( !header.contains( iconsContainer ) ){
header.appendChild( iconsContainer );
}
if( !iconsContainer.contains( iconAlwaysVisible ) ){
iconsContainer.appendChild( iconAlwaysVisible );
}
if( !iconsContainer.contains( iconExpanded ) ){
iconsContainer.appendChild( iconExpanded );
}
if( !iconsContainer.contains( iconClose ) ){
iconsContainer.appendChild( iconClose );
}
}
},
/**
* Handles the change of "labelChanged" property. Updates item's UI with the label provided
*
* @method _labelChanged
* @protected
* @param {EventFacade} params The event facade for the attribute change
*/
_labelChanged: function( params ){
var label;
if( this.get( RENDERED ) ){
label = this.get( NODE_LABEL );
label.set( INNER_HTML, ["<a href='#'>", params.newVal, "</a>" ].join('') );
}
},
/**
* Handles the change of "closableChanged" property. Hides or shows close icon
*
* @method _closableChanged
* @protected
* @param {EventFacade} params The event facade for the attribute change
*/
_closableChanged: function( params ){
var selector, node, contentBox;
if( this.get( RENDERED ) ){
contentBox = this.get( CONTENT_BOX );
selector = HEADER_SELECTOR_SUB + C_ICONCLOSE;
node = contentBox.query( selector );
if( params.newVal ){
node.removeClass( C_ICONCLOSE_HIDDEN );
} else {
node.addClass( C_ICONCLOSE_HIDDEN );
}
}
},
/**
* Initializer lifecycle implementation for the AccordionItem class.
*
* @method initializer
* @protected
* @param config {Object} Configuration object literal for the AccordionItem
*/
initializer: function( config ) {
this.after( "labelChange", Y.bind( this._labelChanged, this ) );
this.after( "closableChange", Y.bind( this._closableChanged, this ) );
},
/**
* Destructor lifecycle implementation for the AccordionItem class.
*
* @method destructor
* @protected
*/
destructor : function() {
// EMPTY
},
/**
* Creates AccordionItem's header.
*
* @method renderUI
* @protected
*/
renderUI: function(){
this._createHeader();
},
/**
* Configures/Sets up listeners to bind Widget State to UI/DOM
*
* @method bindUI
* @protected
*/
bindUI: function(){
var contentBox;
contentBox = this.get( CONTENT_BOX );
contentBox.delegate( "click", Y.bind( this._onLinkClick, this ), HEADER_SELECTOR + ' a' );
},
/**
* Prevent default action on clicking the link in the label
*
* @method _onLinkClick
* @protected
*
* @param e {Event} The click event
*/
_onLinkClick: function( e ){
e.preventDefault();
},
/**
* Marks the item as always visible by adding class to always visible icon.
* The icon will be updated only if needed.
*
* @method markAsAlwaysVisible
* @param {Boolean} alwaysVisible Whether or not the item should be marked as always visible
* @return Boolean Return true if the icon has been updated, false if there was no need to update
*/
markAsAlwaysVisible: function( alwaysVisible ){
var iconAlwaysVisisble, strings;
iconAlwaysVisisble = this.get( ICON_ALWAYSVISIBLE );
strings = this.get( STRINGS );
if( alwaysVisible ){
if( !iconAlwaysVisisble.hasClass( C_ICONALWAYSVISIBLE_ON ) ){
iconAlwaysVisisble.replaceClass( C_ICONALWAYSVISIBLE_OFF, C_ICONALWAYSVISIBLE_ON );
iconAlwaysVisisble.set( TITLE, strings.title_always_visible_on );
return true;
}
} else {
if( iconAlwaysVisisble.hasClass( C_ICONALWAYSVISIBLE_ON ) ){
iconAlwaysVisisble.replaceClass( C_ICONALWAYSVISIBLE_ON, C_ICONALWAYSVISIBLE_OFF );
iconAlwaysVisisble.set( TITLE, strings.title_always_visible_off );
return true;
}
}
return false;
},
/**
* Marks the item as expanded by adding class to expand icon.
* The icon will be updated only if needed.
*
* @method markAsExpanded
* @param {Boolean} expanded Whether or not the item should be marked as expanded
* @return Boolean Return true if the icon has been updated, false if there was no need to update
*/
markAsExpanded: function( expanded ){
var strings, iconExpanded;
iconExpanded = this.get( ICON_EXPANDED );
strings = this.get( STRINGS );
if( expanded ){
if( !iconExpanded.hasClass( C_ICONEXPANDED_ON ) ){
iconExpanded.replaceClass( C_ICONEXPANDED_OFF, C_ICONEXPANDED_ON );
iconExpanded.set( TITLE , strings.title_iconexpanded_on );
return true;
}
} else {
if( iconExpanded.hasClass( C_ICONEXPANDED_ON ) ){
iconExpanded.replaceClass( C_ICONEXPANDED_ON, C_ICONEXPANDED_OFF );
iconExpanded.set( TITLE , strings.title_iconexpanded_off );
return true;
}
}
return false;
},
/**
* Marks the item as expanding by adding class to expand icon.
* The method will update icon only if needed.
*
* @method markAsExpanding
* @param {Boolean} expanding Whether or not the item should be marked as expanding
* @return Boolean Return true if the icon has been updated, false if there was no need to update
*/
markAsExpanding: function( expanding ){
var iconExpanded = this.get( ICON_EXPANDED );
if( expanding ){
if( !iconExpanded.hasClass( C_ICONEXPANDED_EXPANDING ) ){
iconExpanded.addClass( C_ICONEXPANDED_EXPANDING );
return true;
}
} else {
if( iconExpanded.hasClass( C_ICONEXPANDED_EXPANDING ) ){
iconExpanded.removeClass( C_ICONEXPANDED_EXPANDING );
return true;
}
}
return false;
},
/**
* Marks the item as collapsing by adding class to expand icon.
* The method will update icon only if needed.
*
* @method markAsCollapsing
* @param {Boolean} collapsing Whether or not the item should be marked as collapsing
* @return Boolean Return true if the icon has been updated, false if there was no need to update
*/
markAsCollapsing: function( collapsing ){
var iconExpanded = this.get( ICON_EXPANDED );
if( collapsing ){
if( !iconExpanded.hasClass( C_ICONEXPANDED_COLLAPSING ) ){
iconExpanded.addClass( C_ICONEXPANDED_COLLAPSING );
return true;
}
} else {
if( iconExpanded.hasClass( C_ICONEXPANDED_COLLAPSING ) ){
iconExpanded.removeClass( C_ICONEXPANDED_COLLAPSING );
return true;
}
}
return false;
},
/**
* Parses and returns the yuiConfig attribute from contentBox. It must be stringified JSON object.
* This function will be replaced with more clever solution when YUI 3.1 becomes available
*
* @method _getConfigDOMAttribute
* @private
* @param {Node} contentBox Widget's contentBox
* @return {Object} The parsed yuiConfig value
*/
_getConfigDOMAttribute: function( contentBox ) {
if( !this._parsedCfg ){
this._parsedCfg = contentBox.getAttribute( YUICONFIG );
if( this._parsedCfg ){
this._parsedCfg = JSON.parse( this._parsedCfg );
}
}
return this._parsedCfg;
}
});
// Add WidgetStdMod's functionality to AccordionItem
Base.build( AccordionItem.NAME, AccordionItem, [ WidgetStdMod ], {
dynamic: false
});
Y.AccordionItem = AccordionItem;
}());