(function() {
/**
*
* By Marco van Hylckama Vlieg (marco@i-marco.nl)
*
* THIS IS A WORK IN PROGRESS
*
* Many, many thanks go out to Daniel Satyam Barreiro!
* Please read his article about YUI widget development
* http://yuiblog.com/blog/2008/06/24/buildingwidgets/
* Without his excellent help and advice this widget would not
* be half as good as it is now.
*/
/**
* The accordionview module provides a widget for managing content bound to an 'accordion'.
* @module accordionview
* @requires yahoo, dom, event, element, animation
*/
var YUD = YAHOO.util.Dom, YUE = YAHOO.util.Event, YUA = YAHOO.util.Anim;
/**
* A widget to control accordion views.
* @namespace YAHOO.widget
* @class AccordionView
* @extends YAHOO.util.Element
* @constructor
* @param {HTMLElement | String} el The id of the html element that represents the AccordionView.
* @param {Object} oAttr (optional) A key map of the AccordionView's
* initial oAttributes.
*/
var AccordionView = function(el, oAttr) {
el = YUD.get(el);
// some sensible defaults
oAttr = oAttr || {};
if(!el) {
el = document.createElement(this.CONFIG.TAG_NAME);
}
if (el.id) {oAttr.id = el.id; }
YAHOO.widget.AccordionView.superclass.constructor.call(this, el, oAttr);
this.initList(el, oAttr);
// This refresh forces all defaults to be set
this.refresh(['id', 'width','hoverActivated'],true);
};
/**
* @event panelClose
* @description Fires before a panel closes.
* See Element.addListener
* for more information on listening for this event.
* @type YAHOO.util.CustomEvent
*/
var panelCloseEvent = 'panelClose';
/**
* @event panelOpen
* @description Fires before a panel opens.
* See Element.addListener
* for more information on listening for this event.
* @type YAHOO.util.CustomEvent
*/
var panelOpenEvent = 'panelOpen';
/**
* @event afterPanelClose
* @description Fires after a panel has finished closing.
* See Element.addListener
* for more information on listening for this event.
* @type YAHOO.util.CustomEvent
*/
var afterPanelCloseEvent = 'afterPanelClose';
/**
* @event afterPanelOpen
* @description Fires after a panel has finished opening.
* See Element.addListener
* for more information on listening for this event.
* @type YAHOO.util.CustomEvent
*/
var afterPanelOpenEvent = 'afterPanelOpen';
/**
* @event stateChanged
* @description Fires after the accordion has fully changed state (after opening and/or closing (a) panel(s)).
* See Element.addListener
* for more information on listening for this event.
* @type YAHOO.util.CustomEvent
*/
var stateChangedEvent = 'stateChanged';
/**
* @event beforeStateChange
* @description Fires before a state change.
* This is useful to cancel an entire change operation
* See Element.addListener
* for more information on listening for this event.
* @type YAHOO.util.CustomEvent
*/
var beforeStateChangeEvent = 'beforeStateChange';
YAHOO.widget.AccordionView = AccordionView;
YAHOO.extend(AccordionView, YAHOO.util.Element, {
/**
* Initialize attributes for the Accordion
* @param {Object} oAttr attributes key map
* @method initAttributes
*/
initAttributes: function (oAttr) {
AccordionView.superclass.initAttributes.call(this, oAttr);
var bAnimate = (YAHOO.env.modules.animation) ? true : false;
this.setAttributeConfig('id', {
writeOnce: true,
validator: function (value) {
return (/^[a-zA-Z][\w0-9\-_.:]*$/.test(value));
},
value: YUD.generateId(),
method: function (value) {
this.get('element').id = value;
}
});
this.setAttributeConfig('width', {
value: '400px',
method: function (value) {
this.setStyle('width', value);
}
}
);
this.setAttributeConfig('animationSpeed', {
value: 0.7
}
);
this.setAttributeConfig('animate', {
value: bAnimate,
validator: YAHOO.lang.isBoolean
}
);
this.setAttributeConfig('collapsible', {
value: false,
validator: YAHOO.lang.isBoolean
}
);
this.setAttributeConfig('expandable', {
value: false,
validator: YAHOO.lang.isBoolean
}
);
this.setAttributeConfig('effect', {
value: YAHOO.util.Easing.easeBoth,
validator: YAHOO.lang.isString
}
);
this.setAttributeConfig('hoverActivated', {
value: false,
validator: YAHOO.lang.isBoolean,
method: function (value) {
if (value) {
YUE.on(this, 'mouseover', this._onMouseOver, this, true);
} else {
YUE.removeListener(this, 'mouseover', this._onMouseOver);
}
}
});
this.setAttributeConfig('_hoverTimeout', {
value: 500,
validator: YAHOO.lang.isInteger
}
);
},
/**
* Configuration object containing tag names used in the AccordionView component.
* See sourcecode for explanation in case you need to change this
* @property CONFIG
* @public
* @type Object
*/
CONFIG : {
// tag name for the entire accordion
TAG_NAME : 'UL',
// tag name for the wrapper around a toggle + content pair
ITEM_WRAPPER_TAG_NAME : 'LI',
// tag name for the wrapper around the content for a panel
CONTENT_WRAPPER_TAG_NAME : 'DIV'
},
/**
* Configuration object containing classes used in the AccordionView component.
* See sourcecode for explanation in case you need to change this
* @property CLASSES
* @public
* @type Object
*/
CLASSES : {
// the entire accordion
ACCORDION : 'yui-accordionview',
// the wrapper around a toggle + content pair
PANEL : 'yui-accordion-panel',
// the element that toggles a panel
TOGGLE : 'yui-accordion-toggle',
// the element that contains the content of a panel
CONTENT : 'yui-accordion-content',
// to indicate that a toggle is active
ACTIVE : 'active',
// to indicate that content is hidden
HIDDEN : 'hidden',
// the opened/closed indicator
INDICATOR : 'indicator'
},
/**
* Internal counter to make sure id's are unique
* @property _idCounter
* @private
* @type Integer
*/
_idCounter : '1',
/**
* Holds the timer for hover activated accordions
* @property _hoverTimer
* @private
*/
_hoverTimer : null,
/**
* Holds references to all accordion panels (list elements) in an array
* @property _panels
* @private
* @type Array
*/
_panels : null,
/**
* Keeps track of whether a panel is currently in the process of opening.
* Used to time when a full change is finished (open and close panel)
* @property _opening
* @private
* @type Boolean
*/
_opening : false,
/**
* Keeps track of whether a panel is currently in the process of closing.
* Used to time when a full change is finished (open and close panel)
* @property _closing
* @private
* @type Boolean
*/
_closing : false,
/**
* Whether we're running FF2 or older (or another derivate of Gecko < 1.9)
* @property _ff2
* @private
* @type Boolean
*/
_ff2 : (YAHOO.env.ua.gecko > 0 && YAHOO.env.ua.gecko < 1.9),
/**
* Whether we're running IE6 or IE7
* @property _ie
* @private
* @type Boolean
*/
_ie : (YAHOO.env.ua.ie < 8 && YAHOO.env.ua.ie > 0),
/**
* Whether we're ARIA capable (currently only IE8 ad FF3)
* @property _ARIACapable
* @private
* @type Boolean
*/
_ARIACapable : (YAHOO.env.ua.ie > 7 || YAHOO.env.ua.gecko >= 1.9),
/**
* Initialize the list / accordion
* @param {HTMLElement} el The element for the accordion
* @param {Object} oAttr attributes key map
* @method initList
* @public
*/
initList : function(el, oAttr) {
YUD.addClass(el, this.CLASSES.ACCORDION);
this._setARIA(el, 'role', 'tree');
var aCollectedItems = [];
var aListItems = el.getElementsByTagName(this.CONFIG.ITEM_WRAPPER_TAG_NAME);
for(var i=0;i 0) {
this.addPanels(aCollectedItems);
}
if((oAttr.expandItem === 0) || (oAttr.expandItem > 0)) {
var eLink = this._panels[oAttr.expandItem].firstChild;
var eContent = this._panels[oAttr.expandItem].firstChild.nextSibling;
YUD.removeClass(eContent, this.CLASSES.HIDDEN);
if(eLink && eContent) {
YUD.addClass(eLink, this.CLASSES.ACTIVE);
eLink.tabIndex = 0;
this._setARIA(eLink, 'aria-expanded', 'true');
this._setARIA(eContent, 'aria-hidden', 'false');
}
}
this.initEvents();
},
/**
* Attach all event listeners
* @method initEvents
* @public
*/
initEvents : function() {
if(true === this.get('hoverActivated')) {
this.on('mouseover', this._onMouseOver, this, true);
this.on('mouseout', this._onMouseOut, this, true);
}
this.on('click', this._onClick, this, true);
this.on('keydown', this._onKeydown, this, true);
// set this._opening and this._closing before open/close operations begin
this.on('panelOpen', function(){this._opening = true;}, this, true);
this.on('panelClose', function(){this._closing = true;}, this, true);
// This makes sure that this._fixTabindexes is called after a change has
// fully completed
this.on('afterPanelClose', function(){
this._closing = false;
if(!this._closing && !this._opening) {
this._fixTabIndexes();
}
}, this, true);
this.on('afterPanelOpen', function(){
this._opening = false;
if(!this._closing && !this._opening) {
this._fixTabIndexes();
}
}, this, true);
/*
This is needed when the hrefs are removed from links
to be able to still hit enter to follow the link
We only do this when we have ARIA support
*/
if(this._ARIACapable) {
this.on('keypress', function(ev){
var eCurrentPanel = YUD.getAncestorByClassName(YUE.getTarget(ev), this.CLASSES.PANEL);
var keyCode = YUE.getCharCode(ev);
if(keyCode === 13) {
this._onClick(eCurrentPanel.firstChild);
return false;
}
});
}
},
/**
* Wrapper around setAttribute to make sure we only set ARIA roles and states
* in browsers that support it
* @ethod _setARIA
* @param {HTMLElement} el the element to set the attribute on
* @param {String} sAttr the attribute name
* @param {String} sValue the value for the attribute
* @private
*/
_setARIA : function(el, sAttr, sValue) {
if(this._ARIACapable) {
el.setAttribute(sAttr, sValue);
}
},
/**
* Closes all panels
* @method _collapseAccordion
* @private
*/
_collapseAccordion : function() {
YUD.batch(this._panels, function(e) {
var elContent = this.firstChild.nextSibling;
if(elContent) {
YUD.removeClass(e.firstChild, this.CLASSES.ACTIVE);
YUD.addClass(elContent, this.CLASSES.HIDDEN);
this._setARIA(elContent, 'aria-hidden', 'true');
}
}, this);
},
/**
* Set tabIndex to 0 on the first item in case all panels are closed
* or active. Otherwise set it to -1
* @method _fixTabIndexes
* @private
*/
_fixTabIndexes : function() {
var aLength = this._panels.length;
var bAllClosed = true;
for(var i=0;i0) {
this._panels[i-1].firstChild.focus();
return;
}
}
}
if(nKeyCode === 39 || nKeyCode === 40) {
for(var i=0;i