/**
* The Toggler Component
*
* @module aui-toggler
*/
var Lang = A.Lang,
isBoolean = Lang.isBoolean,
isObject = Lang.isObject,
isString = Lang.isString,
isUndefined = Lang.isUndefined,
toInt = Lang.toInt,
CUBIC_BEZIER = 'cubic-bezier(0, 0.1, 0, 1.0)',
getCN = A.getClassName,
CSS_TOGGLER_CONTENT = getCN('toggler', 'content'),
CSS_TOGGLER_CONTENT_COLLAPSED = getCN('toggler', 'content', 'collapsed'),
CSS_TOGGLER_CONTENT_EXPANDED = getCN('toggler', 'content', 'expanded'),
CSS_TOGGLER_CONTENT_WRAPPER = getCN('toggler', 'content', 'wrapper'),
CSS_TOGGLER_HEADER = getCN('toggler', 'header'),
CSS_TOGGLER_HEADER_COLLAPSED = getCN('toggler', 'header', 'collapsed'),
CSS_TOGGLER_HEADER_EXPANDED = getCN('toggler', 'header', 'expanded'),
CSS_TOGGLER_CONTENT_STATE = {
'false': CSS_TOGGLER_CONTENT_COLLAPSED,
'true': CSS_TOGGLER_CONTENT_EXPANDED
},
CSS_TOGGLER_HEADER_STATE = {
'false': CSS_TOGGLER_HEADER_COLLAPSED,
'true': CSS_TOGGLER_HEADER_EXPANDED
},
TPL_CONTENT_WRAPPER = '<div class="' + CSS_TOGGLER_CONTENT_WRAPPER + '"></div>';
/**
* A base class for Toggler.
*
* Check the [live demo](http://alloyui.com/examples/toggler/).
*
* @class A.Toggler
* @extends Base
* @param {Object} config Object literal specifying widget configuration
* properties.
* @constructor
*/
var Toggler = A.Component.create({
/**
* Static property provides a string to identify the class.
*
* @property NAME
* @type String
* @static
*/
NAME: 'toggler',
/**
* Static property used to define the default attribute
* configuration for the `A.Toggler`.
*
* @property ATTRS
* @type Object
* @static
*/
ATTRS: {
/**
* Determine if the `A.Toggler` transitions will animate.
*
* @attribute animated
* @default false
* @type Boolean
* @writeOnce
*/
animated: {
validator: isBoolean,
value: false,
writeOnce: true
},
/**
* Determine if the `A.Toggler` transitions are being animated in that
* moment.
*
* @attribute animating
* @default false
* @type Boolean
*/
animating: {
validator: isBoolean,
value: false
},
/**
* Determine if the `A.Toggler` should bind DOM events or not.
*
* @attribute bindDOMEvents
* @default true
* @type Boolean
* @writeOnce
*/
bindDOMEvents: {
validator: isBoolean,
value: true,
writeOnce: true
},
/**
* The content of a Toogler instance.
*
* @attribute content
*/
content: {
setter: A.one
},
/**
* Determine if the content starts as toggled on/off on page load.
*
* @attribute expanded
* @default true
* @type Boolean
*/
expanded: {
validator: isBoolean,
value: true
},
/**
* The header of a Toogler instance.
*
* @attribute header
*/
header: {
setter: A.one
},
/**
* User interaction that triggers the Toggler instance.
*
* @attribute toggleEvent
* @type String
* @writeOnce
*/
toggleEvent: {
validator: isString,
value: 'tap',
writeOnce: true
},
/**
* Transition definitions such as duration and type of easing effect.
*
* @attribute transition
* @type Object
*/
transition: {
validator: isObject,
value: {
duration: 0.4,
easing: CUBIC_BEZIER
}
}
},
/**
* Static property used to define which component it extends.
*
* @property EXTENDS
* @type Object
* @static
*/
EXTENDS: A.Base,
/**
* Handle header events.
*
* @method headerEventHandler
* @param event
* @param instance
*/
headerEventHandler: function(event, instance) {
if (event.type === instance.get('toggleEvent') || event.isKey('enter') || event.isKey('space')) {
return instance.toggle();
}
else if (event.isKey('down') || event.isKey('right') || event.isKey('num_plus')) {
return instance.expand();
}
else if (event.isKey('up') || event.isKey('left') || event.isKey('esc') || event.isKey('num_minus')) {
return instance.collapse();
}
},
prototype: {
/**
* Construction logic executed during `A.Toggler` instantiation. Lifecycle.
*
* @method initializer
* @protected
*/
initializer: function() {
var instance = this;
instance.bindUI();
instance.syncUI();
instance._uiSetExpanded(instance.get('expanded'));
},
/**
* Bind the events on the `A.Toggler` UI. Lifecycle.
*
* @method bindUI
* @protected
*/
bindUI: function() {
var instance = this;
var header = instance.get('header');
header.setData('toggler', instance);
var eventHandles = [
instance.on('expandedChange', A.bind(instance._onExpandedChange, instance))
];
if (instance.get('bindDOMEvents')) {
eventHandles.push(
header.on([instance.get('toggleEvent'), 'keydown'], A.rbind(Toggler.headerEventHandler, null, instance))
);
}
instance._eventHandles = eventHandles;
},
/**
* Sync the events on the `A.Toggler` UI. Lifecycle.
*
* @method syncUI
* @protected
*/
syncUI: function() {
var instance = this;
instance.get('content').addClass(CSS_TOGGLER_CONTENT);
instance.get('header').addClass(CSS_TOGGLER_HEADER);
},
/**
* Destructor lifecycle implementation for the `A.Toggler` class.
*
* @method destructor
* @protected
*/
destructor: function() {
var instance = this,
content,
wrapper;
instance.get('header').setData('toggler', null);
if (instance.wrapped) {
content = instance.get('content');
wrapper = content.get('parentNode');
wrapper.insert(content, 'before');
wrapper.remove();
}
(new A.EventHandle(instance._eventHandles)).detach();
},
/**
* Expand/collapse `A.Toggler` with an animation.
*
* @method animate
* @param config
* @param fn
*/
animate: function(config, fn) {
var instance = this;
instance._uiSetExpanded(true);
var transition = A.merge(config, instance.get('transition'));
instance.get('content').transition(transition, A.bind(fn, instance));
},
/**
* Hide `A.Toggler` content.
*
* @method collapse
* @param {Object} payload
* @return {Boolean} expand
*/
collapse: function(payload) {
var instance = this;
return instance.toggle(false, payload);
},
/**
* Show `A.Toggler` content.
*
* @method expand
* @param {Object} payload
* @return {Boolean} expand
*/
expand: function(payload) {
var instance = this;
return instance.toggle(true, payload);
},
/**
* Return the height of content.
*
* @method getContentHeight
* @return {Number} height
*/
getContentHeight: function() {
var instance = this;
var content = instance.get('content');
var expanded = instance.get('expanded'),
height;
if (!expanded) {
instance._uiSetExpanded(true);
}
if (content.hasMethod('getBoundingClientRect')) {
var preciseRegion = content.invoke('getBoundingClientRect');
if (preciseRegion) {
height = preciseRegion.bottom - preciseRegion.top;
}
}
else {
height = content.get('offsetHeight');
}
if (!expanded) {
instance._uiSetExpanded(false);
}
return height;
},
/**
* Show or hide content.
*
* @method toggle
* @param {Boolean} expand
* @param {Object} payload
* @return {Boolean} expand
*/
toggle: function(expand, payload) {
var instance = this,
header = instance.get('header'),
expanded;
if (!header.test(':visible')) {
return;
}
expanded = instance.get('expanded');
if (isUndefined(expand)) {
expand = !expanded;
}
if (expand !== expanded) {
if (instance.get('animated')) {
if (instance.get('animating')) {
return expand;
}
instance._animation(expand, payload);
}
else {
instance.set('expanded', expand, payload);
}
}
return expand;
},
/**
* Apply animation on `toggle`.
*
* @method _animation
* @param {Boolean} expand
* @param {Object} payload
* @protected
*/
_animation: function(expand, payload) {
var instance = this,
content = instance.get('content'),
gutter = instance.contentGutter,
height = instance.getContentHeight();
if (isUndefined(gutter)) {
gutter = instance.contentGutter = toInt(content.getStyle('marginTop'));
}
if (!instance.wrapped) {
instance._uiSetExpandedContent();
if (expand) {
content.setStyle('marginTop', -(height + gutter));
}
}
instance.set('animating', true);
instance.animate(
{
marginTop: (expand ? gutter : -(height + gutter)) + 'px'
},
function() {
instance.set('animating', false);
instance.set('expanded', expand, payload);
}
);
},
/**
* Trigger when the `expanded` attribute change its value.
*
* @method _onExpandedChange
* @param event
* @protected
*/
_onExpandedChange: function(event) {
var instance = this;
instance._uiSetExpanded(event.newVal);
},
/**
* Set the "expanded"/"collapsed" classes on the UI.
*
* @method _uiSetExpanded
* @param {Boolean} val
* @protected
*/
_uiSetExpanded: function(val) {
var instance = this,
expanded = instance.get('expanded');
if (expanded && !instance.wrapped) {
instance._uiSetExpandedContent();
}
instance.get('content').replaceClass(CSS_TOGGLER_CONTENT_STATE[!val], CSS_TOGGLER_CONTENT_STATE[val]);
instance.get('header').replaceClass(CSS_TOGGLER_HEADER_STATE[!val], CSS_TOGGLER_HEADER_STATE[val]);
},
/**
* Wrap the content HTML if `expanded` attribute is true.
*
* @method _uiSetExpandedContent
* @protected
*/
_uiSetExpandedContent: function() {
var instance = this;
instance.get('content').wrap(TPL_CONTENT_WRAPPER);
instance.wrapped = true;
}
}
});
A.Toggler = Toggler;