1 define("dijit/layout/StackController", [
2 "dojo/_base/array", // array.forEach array.indexOf array.map
3 "dojo/_base/declare", // declare
5 "dojo/_base/event", // event.stop
7 "dojo/_base/lang", // lang.getObject
9 "../focus", // focus.focus()
10 "../registry", // registry.byId
14 "../form/ToggleButton",
15 "dojo/i18n!../nls/common"
16 ], function(array, declare, domClass, event, keys, lang, on,
17 focus, registry, _Widget, _TemplatedMixin, _Container, ToggleButton){
20 // dijit/layout/StackController
22 var StackButton = declare("dijit.layout._StackButton", ToggleButton, {
24 // Internal widget used by StackContainer.
26 // The button-like or tab-like object you click to select or delete a page
30 // Override _FormWidget.tabIndex.
31 // StackContainer buttons are not in the tab order by default.
32 // Probably we should be calling this.startupKeyNavChildren() instead.
35 // closeButton: Boolean
36 // When true, display close button for this tab
39 _aria_attr: "aria-selected",
41 buildRendering: function(/*Event*/ evt){
42 this.inherited(arguments);
43 (this.focusNode || this.domNode).setAttribute("role", "tab");
48 var StackController = declare("dijit.layout.StackController", [_Widget, _TemplatedMixin, _Container], {
50 // Set of buttons to select a page in a `dijit/layout/StackContainer`
52 // Monitors the specified StackContainer, and whenever a page is
53 // added, deleted, or selected, updates itself accordingly.
55 baseClass: "dijitStackController",
57 templateString: "<span role='tablist' data-dojo-attach-event='onkeypress'></span>",
59 // containerId: [const] String
60 // The id of the page container that I point to
63 // buttonWidget: [const] Constructor
64 // The button widget to create to correspond to each page
65 buttonWidget: StackButton,
67 // buttonWidgetCloseClass: String
68 // CSS class of [x] close icon, used by event delegation code to tell when close button was clicked
69 buttonWidgetCloseClass: "dijitStackCloseButton",
71 constructor: function(params /*===== , srcNodeRef =====*/){
74 // params: Object|null
75 // Hash of initialization parameters for widget, including scalar values (like title, duration etc.)
76 // and functions, typically callbacks like onClick.
77 // The hash can contain any of the widget's properties, excluding read-only properties.
78 // srcNodeRef: DOMNode|String?
79 // If a srcNodeRef (DOM node) is specified, replace srcNodeRef with my generated DOM tree
81 this.pane2button = {}; // mapping from pane id to buttons
84 postCreate: function(){
85 this.inherited(arguments);
87 // Listen to notifications from StackContainer.
88 // TODO: do this through bubbled events instead of topics
89 this.subscribe(this.containerId+"-startup", "onStartup");
90 this.subscribe(this.containerId+"-addChild", "onAddChild");
91 this.subscribe(this.containerId+"-removeChild", "onRemoveChild");
92 this.subscribe(this.containerId+"-selectChild", "onSelectChild");
93 this.subscribe(this.containerId+"-containerKeyPress", "onContainerKeyPress");
95 // Listen for click events to select or close tabs.
96 // No need to worry about ENTER/SPACE key handling: tabs are selected via left/right arrow keys,
97 // and closed via shift-F10 (to show the close menu).
98 this.connect(this.containerNode, 'click', function(evt){
99 var button = registry.getEnclosingWidget(evt.target);
100 if(button != this.containerNode && !button.disabled && button.page){
101 for(var target = evt.target; target !== this.containerNode; target = target.parentNode){
102 if(domClass.contains(target, this.buttonWidgetCloseClass)){
103 this.onCloseButtonClick(button.page);
105 }else if(target == button.domNode){
106 this.onButtonClick(button.page);
114 onStartup: function(/*Object*/ info){
116 // Called after StackContainer has finished initializing
119 array.forEach(info.children, this.onAddChild, this);
121 // Show button corresponding to selected pane (unless selected
122 // is null because there are no panes)
123 this.onSelectChild(info.selected);
126 // Reflect events like page title changes to tab buttons
127 var containerNode = registry.byId(this.containerId).containerNode,
128 pane2button = this.pane2button,
131 "showtitle": "showLabel",
132 "iconclass": "iconClass",
133 "closable": "closeButton",
135 "disabled": "disabled"
137 connectFunc = function(attr, buttonAttr){
138 return on(containerNode, "attrmodified-" + attr, function(evt){
139 var button = pane2button[evt.detail && evt.detail.widget && evt.detail.widget.id];
141 button.set(buttonAttr, evt.detail.newValue);
145 for(var attr in paneToButtonAttr){
146 this.own(connectFunc(attr, paneToButtonAttr[attr]));
151 // Since the buttons are internal to the StackController widget, destroy() should remove them, which is
152 // done by calling onRemoveChild().
153 for(var pane in this.pane2button){
154 this.onRemoveChild(registry.byId(pane));
157 // TODO: destroyRecursive() will call destroy() on each child button twice. Once from the above code,
158 // and once because _WidgetBase.destroyDescendants() deletes anything inside of this.containerNode.
159 // Probably shouldn't attach that DOMNode as this.containerNode.
161 this.inherited(arguments);
164 onAddChild: function(/*dijit/_WidgetBase*/ page, /*Integer?*/ insertIndex){
166 // Called whenever a page is added to the container.
167 // Create button corresponding to the page.
171 // create an instance of the button widget
172 // (remove typeof buttonWidget == string support in 2.0)
173 var Cls = lang.isString(this.buttonWidget) ? lang.getObject(this.buttonWidget) : this.buttonWidget;
174 var button = new Cls({
175 id: this.id + "_" + page.id,
176 name: this.id + "_" + page.id,
178 disabled: page.disabled,
179 ownerDocument: this.ownerDocument,
182 textDir: page.textDir,
183 showLabel: page.showTitle,
184 iconClass: page.iconClass,
185 closeButton: page.closable,
190 this.addChild(button, insertIndex);
191 this.pane2button[page.id] = button;
192 page.controlButton = button; // this value might be overwritten if two tabs point to same container
193 if(!this._currentChild){
194 // If this is the first child then StackContainer will soon publish that it's selected,
195 // but before that StackContainer calls layout(), and before layout() is called the
196 // StackController needs to have the proper height... which means that the button needs
197 // to be marked as selected now. See test_TabContainer_CSS.html for test.
198 this.onSelectChild(page);
202 onRemoveChild: function(/*dijit/_WidgetBase*/ page){
204 // Called whenever a page is removed from the container.
205 // Remove the button corresponding to the page.
209 if(this._currentChild === page){ this._currentChild = null; }
211 var button = this.pane2button[page.id];
213 this.removeChild(button);
214 delete this.pane2button[page.id];
217 delete page.controlButton;
220 onSelectChild: function(/*dijit/_WidgetBase*/ page){
222 // Called when a page has been selected in the StackContainer, either by me or by another StackController
228 if(this._currentChild){
229 var oldButton=this.pane2button[this._currentChild.id];
230 oldButton.set('checked', false);
231 oldButton.focusNode.setAttribute("tabIndex", "-1");
234 var newButton=this.pane2button[page.id];
235 newButton.set('checked', true);
236 this._currentChild = page;
237 newButton.focusNode.setAttribute("tabIndex", "0");
238 var container = registry.byId(this.containerId);
239 container.containerNode.setAttribute("aria-labelledby", newButton.id);
242 onButtonClick: function(/*dijit/_WidgetBase*/ page){
244 // Called whenever one of my child buttons is pressed in an attempt to select a page
248 var button = this.pane2button[page.id];
250 // For TabContainer where the tabs are <span>, need to set focus explicitly when left/right arrow
251 focus.focus(button.focusNode);
253 if(this._currentChild && this._currentChild.id === page.id) {
254 //In case the user clicked the checked button, keep it in the checked state because it remains to be the selected stack page.
255 button.set('checked', true);
257 var container = registry.byId(this.containerId);
258 container.selectChild(page);
261 onCloseButtonClick: function(/*dijit/_WidgetBase*/ page){
263 // Called whenever one of my child buttons [X] is pressed in an attempt to close a page
267 var container = registry.byId(this.containerId);
268 container.closeChild(page);
269 if(this._currentChild){
270 var b = this.pane2button[this._currentChild.id];
272 focus.focus(b.focusNode || b.domNode);
277 // TODO: this is a bit redundant with forward, back api in StackContainer
278 adjacent: function(/*Boolean*/ forward){
280 // Helper for onkeypress to find next/previous button
284 if(!this.isLeftToRight() && (!this.tabPosition || /top|bottom/.test(this.tabPosition))){ forward = !forward; }
285 // find currently focused button in children array
286 var children = this.getChildren();
287 var idx = array.indexOf(children, this.pane2button[this._currentChild.id]),
288 current = children[idx];
290 // Pick next/previous non-disabled button to focus on. If we get back to the original button it means
291 // that all buttons must be disabled, so return current child to avoid an infinite loop.
294 idx = (idx + (forward ? 1 : children.length - 1)) % children.length;
295 child = children[idx];
296 }while(child.disabled && child != current);
298 return child; // dijit/_WidgetBase
301 onkeypress: function(/*Event*/ e){
303 // Handle keystrokes on the page list, for advancing to next/previous button
304 // and closing the current page if the page is closable.
308 if(this.disabled || e.altKey ){ return; }
310 if(e.ctrlKey || !e._djpage){
311 switch(e.charOrCode){
312 case keys.LEFT_ARROW:
314 if(!e._djpage){ forward = false; }
317 if(e.ctrlKey){ forward = false; }
319 case keys.RIGHT_ARROW:
320 case keys.DOWN_ARROW:
321 if(!e._djpage){ forward = true; }
324 if(e.ctrlKey){ forward = true; }
327 // Navigate to first non-disabled child
328 var children = this.getChildren();
329 for(var idx = 0; idx < children.length; idx++){
330 var child = children[idx];
332 this.onButtonClick(child.page);
339 // Navigate to last non-disabled child
340 var children = this.getChildren();
341 for(var idx = children.length-1; idx >= 0; idx--){
342 var child = children[idx];
344 this.onButtonClick(child.page);
351 if(this._currentChild.closable){
352 this.onCloseButtonClick(this._currentChild);
358 if(e.charOrCode === keys.TAB){
359 this.onButtonClick(this.adjacent(!e.shiftKey).page);
361 }else if(e.charOrCode == "w"){
362 if(this._currentChild.closable){
363 this.onCloseButtonClick(this._currentChild);
365 event.stop(e); // avoid browser tab closing.
369 // handle next/previous page navigation (left/right arrow, etc.)
370 if(forward !== null){
371 this.onButtonClick(this.adjacent(forward).page);
377 onContainerKeyPress: function(/*Object*/ info){
379 // Called when there was a keypress on the container
382 info.e._djpage = info.page;
383 this.onkeypress(info.e);
387 StackController.StackButton = StackButton; // for monkey patching
389 return StackController;