]>
Commit | Line | Data |
---|---|---|
f0cfe83e AD |
1 | define("dijit/layout/StackController", [ |
2 | "dojo/_base/array", // array.forEach array.indexOf array.map | |
3 | "dojo/_base/declare", // declare | |
4 | "dojo/dom-class", | |
5 | "dojo/_base/event", // event.stop | |
6 | "dojo/keys", // keys | |
7 | "dojo/_base/lang", // lang.getObject | |
8 | "dojo/on", | |
9 | "../focus", // focus.focus() | |
10 | "../registry", // registry.byId | |
11 | "../_Widget", | |
12 | "../_TemplatedMixin", | |
13 | "../_Container", | |
14 | "../form/ToggleButton", | |
15 | "dojo/i18n!../nls/common" | |
16 | ], function(array, declare, domClass, event, keys, lang, on, | |
17 | focus, registry, _Widget, _TemplatedMixin, _Container, ToggleButton){ | |
18 | ||
19 | // module: | |
20 | // dijit/layout/StackController | |
21 | ||
22 | var StackButton = declare("dijit.layout._StackButton", ToggleButton, { | |
23 | // summary: | |
24 | // Internal widget used by StackContainer. | |
25 | // description: | |
26 | // The button-like or tab-like object you click to select or delete a page | |
27 | // tags: | |
28 | // private | |
29 | ||
30 | // Override _FormWidget.tabIndex. | |
31 | // StackContainer buttons are not in the tab order by default. | |
32 | // Probably we should be calling this.startupKeyNavChildren() instead. | |
33 | tabIndex: "-1", | |
34 | ||
35 | // closeButton: Boolean | |
36 | // When true, display close button for this tab | |
37 | closeButton: false, | |
38 | ||
39 | _aria_attr: "aria-selected", | |
40 | ||
41 | buildRendering: function(/*Event*/ evt){ | |
42 | this.inherited(arguments); | |
43 | (this.focusNode || this.domNode).setAttribute("role", "tab"); | |
44 | } | |
45 | }); | |
46 | ||
47 | ||
48 | var StackController = declare("dijit.layout.StackController", [_Widget, _TemplatedMixin, _Container], { | |
49 | // summary: | |
50 | // Set of buttons to select a page in a `dijit/layout/StackContainer` | |
51 | // description: | |
52 | // Monitors the specified StackContainer, and whenever a page is | |
53 | // added, deleted, or selected, updates itself accordingly. | |
54 | ||
55 | baseClass: "dijitStackController", | |
56 | ||
57 | templateString: "<span role='tablist' data-dojo-attach-event='onkeypress'></span>", | |
58 | ||
59 | // containerId: [const] String | |
60 | // The id of the page container that I point to | |
61 | containerId: "", | |
62 | ||
63 | // buttonWidget: [const] Constructor | |
64 | // The button widget to create to correspond to each page | |
65 | buttonWidget: StackButton, | |
66 | ||
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", | |
70 | ||
71 | constructor: function(params /*===== , srcNodeRef =====*/){ | |
72 | // summary: | |
73 | // Create the widget. | |
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 | |
80 | ||
81 | this.pane2button = {}; // mapping from pane id to buttons | |
82 | }, | |
83 | ||
84 | postCreate: function(){ | |
85 | this.inherited(arguments); | |
86 | ||
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"); | |
94 | ||
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); | |
104 | break; | |
105 | }else if(target == button.domNode){ | |
106 | this.onButtonClick(button.page); | |
107 | break; | |
108 | } | |
109 | } | |
110 | } | |
111 | }); | |
112 | }, | |
113 | ||
114 | onStartup: function(/*Object*/ info){ | |
115 | // summary: | |
116 | // Called after StackContainer has finished initializing | |
117 | // tags: | |
118 | // private | |
119 | array.forEach(info.children, this.onAddChild, this); | |
120 | if(info.selected){ | |
121 | // Show button corresponding to selected pane (unless selected | |
122 | // is null because there are no panes) | |
123 | this.onSelectChild(info.selected); | |
124 | } | |
125 | ||
126 | // Reflect events like page title changes to tab buttons | |
127 | var containerNode = registry.byId(this.containerId).containerNode, | |
128 | pane2button = this.pane2button, | |
129 | paneToButtonAttr = { | |
130 | "title": "label", | |
131 | "showtitle": "showLabel", | |
132 | "iconclass": "iconClass", | |
133 | "closable": "closeButton", | |
134 | "tooltip": "title", | |
135 | "disabled": "disabled" | |
136 | }, | |
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]; | |
140 | if(button){ | |
141 | button.set(buttonAttr, evt.detail.newValue); | |
142 | } | |
143 | }); | |
144 | }; | |
145 | for(var attr in paneToButtonAttr){ | |
146 | this.own(connectFunc(attr, paneToButtonAttr[attr])); | |
147 | } | |
148 | }, | |
149 | ||
150 | destroy: function(){ | |
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)); | |
155 | } | |
156 | ||
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. | |
160 | ||
161 | this.inherited(arguments); | |
162 | }, | |
163 | ||
164 | onAddChild: function(/*dijit/_WidgetBase*/ page, /*Integer?*/ insertIndex){ | |
165 | // summary: | |
166 | // Called whenever a page is added to the container. | |
167 | // Create button corresponding to the page. | |
168 | // tags: | |
169 | // private | |
170 | ||
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, | |
177 | label: page.title, | |
178 | disabled: page.disabled, | |
179 | ownerDocument: this.ownerDocument, | |
180 | dir: page.dir, | |
181 | lang: page.lang, | |
182 | textDir: page.textDir, | |
183 | showLabel: page.showTitle, | |
184 | iconClass: page.iconClass, | |
185 | closeButton: page.closable, | |
186 | title: page.tooltip, | |
187 | page: page | |
188 | }); | |
189 | ||
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); | |
199 | } | |
200 | }, | |
201 | ||
202 | onRemoveChild: function(/*dijit/_WidgetBase*/ page){ | |
203 | // summary: | |
204 | // Called whenever a page is removed from the container. | |
205 | // Remove the button corresponding to the page. | |
206 | // tags: | |
207 | // private | |
208 | ||
209 | if(this._currentChild === page){ this._currentChild = null; } | |
210 | ||
211 | var button = this.pane2button[page.id]; | |
212 | if(button){ | |
213 | this.removeChild(button); | |
214 | delete this.pane2button[page.id]; | |
215 | button.destroy(); | |
216 | } | |
217 | delete page.controlButton; | |
218 | }, | |
219 | ||
220 | onSelectChild: function(/*dijit/_WidgetBase*/ page){ | |
221 | // summary: | |
222 | // Called when a page has been selected in the StackContainer, either by me or by another StackController | |
223 | // tags: | |
224 | // private | |
225 | ||
226 | if(!page){ return; } | |
227 | ||
228 | if(this._currentChild){ | |
229 | var oldButton=this.pane2button[this._currentChild.id]; | |
230 | oldButton.set('checked', false); | |
231 | oldButton.focusNode.setAttribute("tabIndex", "-1"); | |
232 | } | |
233 | ||
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); | |
240 | }, | |
241 | ||
242 | onButtonClick: function(/*dijit/_WidgetBase*/ page){ | |
243 | // summary: | |
244 | // Called whenever one of my child buttons is pressed in an attempt to select a page | |
245 | // tags: | |
246 | // private | |
247 | ||
248 | var button = this.pane2button[page.id]; | |
249 | ||
250 | // For TabContainer where the tabs are <span>, need to set focus explicitly when left/right arrow | |
251 | focus.focus(button.focusNode); | |
252 | ||
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); | |
256 | } | |
257 | var container = registry.byId(this.containerId); | |
258 | container.selectChild(page); | |
259 | }, | |
260 | ||
261 | onCloseButtonClick: function(/*dijit/_WidgetBase*/ page){ | |
262 | // summary: | |
263 | // Called whenever one of my child buttons [X] is pressed in an attempt to close a page | |
264 | // tags: | |
265 | // private | |
266 | ||
267 | var container = registry.byId(this.containerId); | |
268 | container.closeChild(page); | |
269 | if(this._currentChild){ | |
270 | var b = this.pane2button[this._currentChild.id]; | |
271 | if(b){ | |
272 | focus.focus(b.focusNode || b.domNode); | |
273 | } | |
274 | } | |
275 | }, | |
276 | ||
277 | // TODO: this is a bit redundant with forward, back api in StackContainer | |
278 | adjacent: function(/*Boolean*/ forward){ | |
279 | // summary: | |
280 | // Helper for onkeypress to find next/previous button | |
281 | // tags: | |
282 | // private | |
283 | ||
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]; | |
289 | ||
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. | |
292 | var child; | |
293 | do{ | |
294 | idx = (idx + (forward ? 1 : children.length - 1)) % children.length; | |
295 | child = children[idx]; | |
296 | }while(child.disabled && child != current); | |
297 | ||
298 | return child; // dijit/_WidgetBase | |
299 | }, | |
300 | ||
301 | onkeypress: function(/*Event*/ e){ | |
302 | // summary: | |
303 | // Handle keystrokes on the page list, for advancing to next/previous button | |
304 | // and closing the current page if the page is closable. | |
305 | // tags: | |
306 | // private | |
307 | ||
308 | if(this.disabled || e.altKey ){ return; } | |
309 | var forward = null; | |
310 | if(e.ctrlKey || !e._djpage){ | |
311 | switch(e.charOrCode){ | |
312 | case keys.LEFT_ARROW: | |
313 | case keys.UP_ARROW: | |
314 | if(!e._djpage){ forward = false; } | |
315 | break; | |
316 | case keys.PAGE_UP: | |
317 | if(e.ctrlKey){ forward = false; } | |
318 | break; | |
319 | case keys.RIGHT_ARROW: | |
320 | case keys.DOWN_ARROW: | |
321 | if(!e._djpage){ forward = true; } | |
322 | break; | |
323 | case keys.PAGE_DOWN: | |
324 | if(e.ctrlKey){ forward = true; } | |
325 | break; | |
326 | case keys.HOME: | |
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]; | |
331 | if(!child.disabled){ | |
332 | this.onButtonClick(child.page); | |
333 | break; | |
334 | } | |
335 | } | |
336 | event.stop(e); | |
337 | break; | |
338 | case keys.END: | |
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]; | |
343 | if(!child.disabled){ | |
344 | this.onButtonClick(child.page); | |
345 | break; | |
346 | } | |
347 | } | |
348 | event.stop(e); | |
349 | break; | |
350 | case keys.DELETE: | |
351 | if(this._currentChild.closable){ | |
352 | this.onCloseButtonClick(this._currentChild); | |
353 | } | |
354 | event.stop(e); | |
355 | break; | |
356 | default: | |
357 | if(e.ctrlKey){ | |
358 | if(e.charOrCode === keys.TAB){ | |
359 | this.onButtonClick(this.adjacent(!e.shiftKey).page); | |
360 | event.stop(e); | |
361 | }else if(e.charOrCode == "w"){ | |
362 | if(this._currentChild.closable){ | |
363 | this.onCloseButtonClick(this._currentChild); | |
364 | } | |
365 | event.stop(e); // avoid browser tab closing. | |
366 | } | |
367 | } | |
368 | } | |
369 | // handle next/previous page navigation (left/right arrow, etc.) | |
370 | if(forward !== null){ | |
371 | this.onButtonClick(this.adjacent(forward).page); | |
372 | event.stop(e); | |
373 | } | |
374 | } | |
375 | }, | |
376 | ||
377 | onContainerKeyPress: function(/*Object*/ info){ | |
378 | // summary: | |
379 | // Called when there was a keypress on the container | |
380 | // tags: | |
381 | // private | |
382 | info.e._djpage = info.page; | |
383 | this.onkeypress(info.e); | |
384 | } | |
385 | }); | |
386 | ||
387 | StackController.StackButton = StackButton; // for monkey patching | |
388 | ||
389 | return StackController; | |
390 | }); |