tabview.js revision ee84d6929652e6eb1aba183971322a1fc4bfa607
YUI.add('tabview', function(Y) {
/**
* The TabView module
*
* @module tabview
*/
var _queries = Y.TabviewBase._queries,
_classNames = Y.TabviewBase._classNames,
DOT = '.',
getClassName = Y.ClassNameManager.getClassName,
/**
* Provides a tabbed widget interface
* @param config {Object} Object literal specifying tabview configuration properties.
*
* @class TabView
* @constructor
* @extends Widget
* @uses WidgetParent
*/
TabView = Y.Base.create('tabView', Y.Widget, [Y.WidgetParent], {
_afterChildAdded: function(e) {
this.get('contentBox').focusManager.refresh();
},
_defListNodeValueFn: function() {
return Y.Node.create(TabView.LIST_TEMPLATE);
},
_defPanelNodeValueFn: function() {
return Y.Node.create(TabView.PANEL_TEMPLATE);
},
_afterChildRemoved: function(e) { // update the selected tab when removed
var i = e.index,
selection = this.get('selection');
if (!selection) { // select previous item if selection removed
selection = this.item(i - 1) || this.item(0);
if (selection) {
selection.set('selected', 1);
}
}
this.get('contentBox').focusManager.refresh();
},
_initAria: function() {
var contentBox = this.get('contentBox'),
tablist = contentBox.one(_queries.tabviewList);
if (tablist) {
tablist.setAttrs({
//'aria-labelledby':
role: tablist
});
}
},
bindUI: function() {
// Use the Node Focus Manager to add keyboard support:
// Pressing the left and right arrow keys will move focus
// among each of the tabs.
this.get('contentBox').plug(Y.Plugin.NodeFocusManager, {
descendants: DOT + _classNames.tabLabel,
keys: { next: 'down:39', // Right arrow
previous: 'down:37' }, // Left arrow
circular: true
});
this.after('render', this._setDefSelection);
this.after('addChild', this._afterChildAdded);
this.after('removeChild', this._afterChildRemoved);
},
renderUI: function() {
var contentBox = this.get('contentBox');
this._renderListBox(contentBox);
this._renderPanelBox(contentBox);
this._childrenContainer = this.get('listNode');
this._renderTabs(contentBox);
},
_setDefSelection: function(contentBox) {
// If no tab is selected, select the first tab.
var selection = this.get('selection') || this.item(0);
this.some(function(tab) {
if (tab.get('selected')) {
selection = tab;
return true;
}
});
if (selection) {
// TODO: why both needed? (via widgetParent/Child)?
this.set('selection', selection);
selection.set('selected', 1);
}
},
_renderListBox: function(contentBox) {
var node = this.get('listNode');
if (!node.inDoc()) {
contentBox.append(node);
}
},
_renderPanelBox: function(contentBox) {
var node = this.get('panelNode');
if (!node.inDoc()) {
contentBox.append(node);
}
},
_renderTabs: function(contentBox) {
var tabs = contentBox.all(_queries.tab),
panelNode = this.get('panelNode'),
panels = (panelNode) ? this.get('panelNode').get('children') : null,
tabview = this;
if (tabs) { // add classNames and fill in Tab fields from markup when possible
tabs.addClass(_classNames.tab);
contentBox.all(_queries.tabLabel).addClass(_classNames.tabLabel);
contentBox.all(_queries.tabPanel).addClass(_classNames.tabPanel);
tabs.each(function(node, i) {
var panelNode = (panels) ? panels.item(i) : null;
tabview.add({
boundingBox: node,
contentBox: node.one(DOT + _classNames.tabLabel),
label: node.one(DOT + _classNames.tabLabel).get('text'),
panelNode: panelNode
});
});
}
}
}, {
LIST_TEMPLATE: '<ul class="' + _classNames.tabviewList + '"></ul>',
PANEL_TEMPLATE: '<div class="' + _classNames.tabviewPanel + '"></div>',
ATTRS: {
defaultChildType: {
value: 'Tab'
},
listNode: {
setter: function(node) {
node = Y.one(node);
if (node) {
node.addClass(_classNames.tabviewList);
}
return node;
},
valueFn: '_defListNodeValueFn'
},
panelNode: {
setter: function(node) {
node = Y.one(node);
if (node) {
node.addClass(_classNames.tabviewPanel);
}
return node;
},
valueFn: '_defPanelNodeValueFn'
},
tabIndex: {
value: null
//validator: '_validTabIndex'
}
},
HTML_PARSER: {
listNode: _queries.tabviewList,
panelNode: _queries.tabviewPanel
}
});
Y.TabView = TabView;
var Lang = Y.Lang,
_queries = Y.TabviewBase._queries,
_classNames = Y.TabviewBase._classNames,
getClassName = Y.ClassNameManager.getClassName;
/**
* Provides Tab instances for use with TabView
* @param config {Object} Object literal specifying tabview configuration properties.
*
* @class Tab
* @constructor
* @extends Widget
* @uses WidgetChild
*/
Y.Tab = Y.Base.create('tab', Y.Widget, [Y.WidgetChild], {
BOUNDING_TEMPLATE: '<li class="' + _classNames.tab + '"></li>',
CONTENT_TEMPLATE: '<a class="' + _classNames.tabLabel + '"></a>',
PANEL_TEMPLATE: '<div class="' + _classNames.tabPanel + '"></div>',
_uiSetSelectedPanel: function(selected) {
this.get('panelNode').toggleClass(_classNames.selectedPanel, selected);
},
_afterTabSelectedChange: function(event) {
this._uiSetSelectedPanel(event.newVal);
},
_afterParentChange: function(e) {
if (!e.newVal) {
this._remove();
} else {
this._add();
}
},
_initAria: function() {
var anchor = this.get('contentBox'),
id = anchor.get('id'),
panel = this.get('panelNode');
if (!id) {
id = Y.guid();
anchor.set('id', id);
}
// Apply the ARIA roles, states and properties to each tab
anchor.set('role', 'tab');
anchor.get('parentNode').set('role', 'presentation');
// Apply the ARIA roles, states and properties to each panel
panel.setAttrs({
role: 'tabpanel',
'aria-labelledby': id
});
},
syncUI: function() {
this.set('label', this.get('label'));
this.set('content', this.get('content'));
this._uiSetSelectedPanel(this.get('selected'));
},
bindUI: function() {
this.after('selectedChange', this._afterTabSelectedChange);
this.after('parentChange', this._afterParentChange);
},
renderUI: function() {
this._renderPanel();
this._initAria();
},
_renderPanel: function() {
this.get('parent').get('panelNode')
.appendChild(this.get('panelNode'));
},
_add: function() {
var parent = this.get('parent').get('contentBox'),
list = parent.get('listNode'),
panel = parent.get('panelNode');
if (list) {
list.appendChild(this.get('boundingBox'));
}
if (panel) {
panel.appendChild(this.get('panelNode'));
}
},
_remove: function() {
this.get('boundingBox').remove();
this.get('panelNode').remove();
},
_onActivate: function(e) {
if (e.target === this) {
// Prevent the browser from navigating to the URL specified by the
// anchor's href attribute.
e.domEvent.preventDefault();
e.target.set('selected', 1);
}
},
initializer: function() {
this.publish(this.get('triggerEvent'), {
defaultFn: this._onActivate
});
},
_defLabelSetter: function(label) {
this.get('contentBox').setContent(label);
return label;
},
_defContentSetter: function(content) {
this.get('panelNode').setContent(content);
return content;
},
// find panel by ID mapping from label href
_defPanelNodeValueFn: function() {
var id,
href = this.get('contentBox').get('href') || '',
parent = this.get('parent'),
hashIndex = href.indexOf('#'),
panel;
href = href.substr(hashIndex);
if (href.charAt(0) === '#') {
id = href.substr(1);
panel = Y.one(href).addClass(_classNames.tabPanel);
} else {
id = Y.guid();
}
// use the one found by id, or else try matching indices
if (parent) {
panel = panel ||
parent.get('panelNode').get('children').item(this.get('index'));
}
if (!panel) {
panel = Y.Node.create(this.PANEL_TEMPLATE);
panel.set('id', id);
}
return panel;
}
}, {
ATTRS: {
/**
* @attribute triggerEvent
* @default "click"
* @type String
*/
triggerEvent: {
value: 'click'
},
/**
* @attribute label
* @type String
*/
label: {
setter: '_defLabelSetter',
validator: Lang.isString
},
/**
* @attribute content
* @type String
*/
content: {
setter: '_defContentSetter',
validator: Lang.isString
},
/**
* @attribute panelNode
* @type Y.Node
*/
panelNode: {
setter: function(node) {
node = Y.one(node);
if (node) {
node.addClass(_classNames.tabPanel);
}
return node;
},
valueFn: '_defPanelNodeValueFn'
},
tabIndex: {
value: null,
validator: '_validTabIndex'
}
},
HTML_PARSER: {
selected: function(contentBox) {
return this.get('boundingBox').hasClass(_classNames.selectedTab);
}
}
});
}, '@VERSION@' ,{requires:['substitute', 'node-focusmanager', 'tabview-base', 'widget', 'widget-parent', 'widget-child']});