(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