source: Dev/trunk/src/client/dijit/layout/StackController.js @ 529

Last change on this file since 529 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 13.0 KB
Line 
1define([
2        "dojo/_base/array", // array.forEach array.indexOf array.map
3        "dojo/_base/declare", // declare
4        "dojo/dom-class",
5        "dojo/dom-construct",
6        "dojo/keys", // keys
7        "dojo/_base/lang", // lang.getObject
8        "dojo/on",
9        "dojo/topic",
10        "../focus", // focus.focus()
11        "../registry", // registry.byId
12        "../_Widget",
13        "../_TemplatedMixin",
14        "../_Container",
15        "../form/ToggleButton",
16        "dojo/touch",   // for normalized click handling, see dojoClick property setting in postCreate()
17        "dojo/i18n!../nls/common"
18], function(array, declare, domClass, domConstruct, keys, lang, on, topic, focus, registry, _Widget, _TemplatedMixin, _Container, ToggleButton){
19
20        // module:
21        //              dijit/layout/StackController
22
23        var StackButton = declare("dijit.layout._StackButton", ToggleButton, {
24                // summary:
25                //              Internal widget used by StackContainer.
26                // description:
27                //              The button-like or tab-like object you click to select or delete a page
28                // tags:
29                //              private
30
31                // Override _FormWidget.tabIndex.
32                // StackContainer buttons are not in the tab order by default.
33                // Probably we should be calling this.startupKeyNavChildren() instead.
34                tabIndex: "-1",
35
36                // closeButton: Boolean
37                //              When true, display close button for this tab
38                closeButton: false,
39
40                _aria_attr: "aria-selected",
41
42                buildRendering: function(/*Event*/ evt){
43                        this.inherited(arguments);
44                        (this.focusNode || this.domNode).setAttribute("role", "tab");
45                }
46        });
47
48
49        var StackController = declare("dijit.layout.StackController", [_Widget, _TemplatedMixin, _Container], {
50                // summary:
51                //              Set of buttons to select a page in a `dijit/layout/StackContainer`
52                // description:
53                //              Monitors the specified StackContainer, and whenever a page is
54                //              added, deleted, or selected, updates itself accordingly.
55
56                baseClass: "dijitStackController",
57
58                templateString: "<span role='tablist' data-dojo-attach-event='onkeydown'></span>",
59
60                // containerId: [const] String
61                //              The id of the page container that I point to
62                containerId: "",
63
64                // buttonWidget: [const] Constructor
65                //              The button widget to create to correspond to each page
66                buttonWidget: StackButton,
67
68                // buttonWidgetCloseClass: String
69                //              CSS class of [x] close icon, used by event delegation code to tell when close button was clicked
70                buttonWidgetCloseClass: "dijitStackCloseButton",
71
72                pane2button: function(/*String*/ id){
73                        // summary:
74                        //              Returns the button corresponding to the pane w/the given id.
75                        // tags:
76                        //              protected
77                        return registry.byId(this.id + "_" + id);
78                },
79
80                postCreate: function(){
81                        this.inherited(arguments);
82
83                        // Listen to notifications from StackContainer.  This is tricky because the StackContainer may not have
84                        // been created yet, so abstracting it through topics.
85                        // Note: for TabContainer we can do this through bubbled events instead of topics; maybe that's
86                        // all we support for 2.0?
87                        this.own(
88                                topic.subscribe(this.containerId + "-startup", lang.hitch(this, "onStartup")),
89                                topic.subscribe(this.containerId + "-addChild", lang.hitch(this, "onAddChild")),
90                                topic.subscribe(this.containerId + "-removeChild", lang.hitch(this, "onRemoveChild")),
91                                topic.subscribe(this.containerId + "-selectChild", lang.hitch(this, "onSelectChild")),
92                                topic.subscribe(this.containerId + "-containerKeyDown", lang.hitch(this, "onContainerKeyDown"))
93                        );
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                        // Also, add flag to use normalized click handling from dojo/touch
99                        this.containerNode.dojoClick = true;
100                        this.own(on(this.containerNode, 'click', lang.hitch(this, function(evt){
101                                var button = registry.getEnclosingWidget(evt.target);
102                                if(button != this.containerNode && !button.disabled && button.page){
103                                        for(var target = evt.target; target !== this.containerNode; target = target.parentNode){
104                                                if(domClass.contains(target, this.buttonWidgetCloseClass)){
105                                                        this.onCloseButtonClick(button.page);
106                                                        break;
107                                                }else if(target == button.domNode){
108                                                        this.onButtonClick(button.page);
109                                                        break;
110                                                }
111                                        }
112                                }
113                        })));
114                },
115
116                onStartup: function(/*Object*/ info){
117                        // summary:
118                        //              Called after StackContainer has finished initializing
119                        // tags:
120                        //              private
121                        this.textDir = info.textDir;
122                        array.forEach(info.children, this.onAddChild, this);
123                        if(info.selected){
124                                // Show button corresponding to selected pane (unless selected
125                                // is null because there are no panes)
126                                this.onSelectChild(info.selected);
127                        }
128
129                        // Reflect events like page title changes to tab buttons
130                        var containerNode = registry.byId(this.containerId).containerNode,
131                                pane2button = lang.hitch(this, "pane2button"),
132                                paneToButtonAttr = {
133                                        "title": "label",
134                                        "showtitle": "showLabel",
135                                        "iconclass": "iconClass",
136                                        "closable": "closeButton",
137                                        "tooltip": "title",
138                                        "disabled": "disabled",
139                                        "textdir": "textdir"
140                                },
141                                connectFunc = function(attr, buttonAttr){
142                                        return on(containerNode, "attrmodified-" + attr, function(evt){
143                                                var button = pane2button(evt.detail && evt.detail.widget && evt.detail.widget.id);
144                                                if(button){
145                                                        button.set(buttonAttr, evt.detail.newValue);
146                                                }
147                                        });
148                                };
149                        for(var attr in paneToButtonAttr){
150                                this.own(connectFunc(attr, paneToButtonAttr[attr]));
151                        }
152                },
153
154                destroy: function(preserveDom){
155                        // Since the buttons are internal to the StackController widget, destroy() should remove them.
156                        // When #5796 is fixed for 2.0 can get rid of this function completely.
157                        this.destroyDescendants(preserveDom);
158                        this.inherited(arguments);
159                },
160
161                onAddChild: function(/*dijit/_WidgetBase*/ page, /*Integer?*/ insertIndex){
162                        // summary:
163                        //              Called whenever a page is added to the container.
164                        //              Create button corresponding to the page.
165                        // tags:
166                        //              private
167
168                        // create an instance of the button widget
169                        // (remove typeof buttonWidget == string support in 2.0)
170                        var Cls = lang.isString(this.buttonWidget) ? lang.getObject(this.buttonWidget) : this.buttonWidget;
171                        var button = new Cls({
172                                id: this.id + "_" + page.id,
173                                name: this.id + "_" + page.id, // note: must match id used in pane2button()
174                                label: page.title,
175                                disabled: page.disabled,
176                                ownerDocument: this.ownerDocument,
177                                dir: page.dir,
178                                lang: page.lang,
179                                textDir: page.textDir || this.textDir,
180                                showLabel: page.showTitle,
181                                iconClass: page.iconClass,
182                                closeButton: page.closable,
183                                title: page.tooltip,
184                                page: page
185                        });
186
187                        this.addChild(button, insertIndex);
188                        page.controlButton = button;    // this value might be overwritten if two tabs point to same container
189                        if(!this._currentChild){
190                                // If this is the first child then StackContainer will soon publish that it's selected,
191                                // but before that StackContainer calls layout(), and before layout() is called the
192                                // StackController needs to have the proper height... which means that the button needs
193                                // to be marked as selected now.   See test_TabContainer_CSS.html for test.
194                                this.onSelectChild(page);
195                        }
196
197                        // Add this StackController button to the list of things that labels that StackContainer pane.
198                        // Also, if there's an aria-labelledby parameter for the pane, then the aria-label parameter is unneeded.
199                        var labelledby = page._wrapper.getAttribute("aria-labelledby") ?
200                                page._wrapper.getAttribute("aria-labelledby") + " " + button.id : button.id;
201                        page._wrapper.removeAttribute("aria-label");
202                        page._wrapper.setAttribute("aria-labelledby", labelledby);
203                },
204
205                onRemoveChild: function(/*dijit/_WidgetBase*/ page){
206                        // summary:
207                        //              Called whenever a page is removed from the container.
208                        //              Remove the button corresponding to the page.
209                        // tags:
210                        //              private
211
212                        if(this._currentChild === page){
213                                this._currentChild = null;
214                        }
215
216                        var button = this.pane2button(page.id);
217                        if(button){
218                                this.removeChild(button);
219                                button.destroy();
220                        }
221                        delete page.controlButton;
222                },
223
224                onSelectChild: function(/*dijit/_WidgetBase*/ page){
225                        // summary:
226                        //              Called when a page has been selected in the StackContainer, either by me or by another StackController
227                        // tags:
228                        //              private
229
230                        if(!page){
231                                return;
232                        }
233
234                        if(this._currentChild){
235                                var oldButton = this.pane2button(this._currentChild.id);
236                                oldButton.set('checked', false);
237                                oldButton.focusNode.setAttribute("tabIndex", "-1");
238                        }
239
240                        var newButton = this.pane2button(page.id);
241                        newButton.set('checked', true);
242                        this._currentChild = page;
243                        newButton.focusNode.setAttribute("tabIndex", "0");
244                        var container = registry.byId(this.containerId);
245                },
246
247                onButtonClick: function(/*dijit/_WidgetBase*/ page){
248                        // summary:
249                        //              Called whenever one of my child buttons is pressed in an attempt to select a page
250                        // tags:
251                        //              private
252
253                        var button = this.pane2button(page.id);
254
255                        // For TabContainer where the tabs are <span>, need to set focus explicitly when left/right arrow
256                        focus.focus(button.focusNode);
257
258                        if(this._currentChild && this._currentChild.id === page.id){
259                                //In case the user clicked the checked button, keep it in the checked state because it remains to be the selected stack page.
260                                button.set('checked', true);
261                        }
262                        var container = registry.byId(this.containerId);
263                        container.selectChild(page);
264                },
265
266                onCloseButtonClick: function(/*dijit/_WidgetBase*/ page){
267                        // summary:
268                        //              Called whenever one of my child buttons [X] is pressed in an attempt to close a page
269                        // tags:
270                        //              private
271
272                        var container = registry.byId(this.containerId);
273                        container.closeChild(page);
274                        if(this._currentChild){
275                                var b = this.pane2button(this._currentChild.id);
276                                if(b){
277                                        focus.focus(b.focusNode || b.domNode);
278                                }
279                        }
280                },
281
282                // TODO: this is a bit redundant with forward, back api in StackContainer
283                adjacent: function(/*Boolean*/ forward){
284                        // summary:
285                        //              Helper for onkeydown to find next/previous button
286                        // tags:
287                        //              private
288
289                        if(!this.isLeftToRight() && (!this.tabPosition || /top|bottom/.test(this.tabPosition))){
290                                forward = !forward;
291                        }
292                        // find currently focused button in children array
293                        var children = this.getChildren();
294                        var idx = array.indexOf(children, this.pane2button(this._currentChild.id)),
295                                current = children[idx];
296
297                        // Pick next/previous non-disabled button to focus on.   If we get back to the original button it means
298                        // that all buttons must be disabled, so return current child to avoid an infinite loop.
299                        var child;
300                        do{
301                                idx = (idx + (forward ? 1 : children.length - 1)) % children.length;
302                                child = children[idx];
303                        }while(child.disabled && child != current);
304
305                        return child; // dijit/_WidgetBase
306                },
307
308                onkeydown: function(/*Event*/ e, /*Boolean?*/ fromContainer){
309                        // summary:
310                        //              Handle keystrokes on the page list, for advancing to next/previous button
311                        //              and closing the current page if the page is closable.
312                        // tags:
313                        //              private
314
315                        if(this.disabled || e.altKey){
316                                return;
317                        }
318                        var forward = null;
319                        if(e.ctrlKey || !e._djpage){
320                                switch(e.keyCode){
321                                        case keys.LEFT_ARROW:
322                                        case keys.UP_ARROW:
323                                                if(!e._djpage){
324                                                        forward = false;
325                                                }
326                                                break;
327                                        case keys.PAGE_UP:
328                                                if(e.ctrlKey){
329                                                        forward = false;
330                                                }
331                                                break;
332                                        case keys.RIGHT_ARROW:
333                                        case keys.DOWN_ARROW:
334                                                if(!e._djpage){
335                                                        forward = true;
336                                                }
337                                                break;
338                                        case keys.PAGE_DOWN:
339                                                if(e.ctrlKey){
340                                                        forward = true;
341                                                }
342                                                break;
343                                        case keys.HOME:
344                                                // Navigate to first non-disabled child
345                                                var children = this.getChildren();
346                                                for(var idx = 0; idx < children.length; idx++){
347                                                        var child = children[idx];
348                                                        if(!child.disabled){
349                                                                this.onButtonClick(child.page);
350                                                                break;
351                                                        }
352                                                }
353                                                e.stopPropagation();
354                                                e.preventDefault();
355                                                break;
356                                        case keys.END:
357                                                // Navigate to last non-disabled child
358                                                var children = this.getChildren();
359                                                for(var idx = children.length - 1; idx >= 0; idx--){
360                                                        var child = children[idx];
361                                                        if(!child.disabled){
362                                                                this.onButtonClick(child.page);
363                                                                break;
364                                                        }
365                                                }
366                                                e.stopPropagation();
367                                                e.preventDefault();
368                                                break;
369                                        case keys.DELETE:
370                                        case "W".charCodeAt(0):    // ctrl-W
371                                                if(this._currentChild.closable &&
372                                                        (e.keyCode == keys.DELETE || e.ctrlKey)){
373                                                        this.onCloseButtonClick(this._currentChild);
374
375                                                        // avoid browser tab closing
376                                                        e.stopPropagation();
377                                                        e.preventDefault();
378                                                }
379                                                break;
380                                        case keys.TAB:
381                                                if(e.ctrlKey){
382                                                        this.onButtonClick(this.adjacent(!e.shiftKey).page);
383                                                        e.stopPropagation();
384                                                        e.preventDefault();
385                                                }
386                                                break;
387                                }
388                                // handle next/previous page navigation (left/right arrow, etc.)
389                                if(forward !== null){
390                                        this.onButtonClick(this.adjacent(forward).page);
391                                        e.stopPropagation();
392                                        e.preventDefault();
393                                }
394                        }
395                },
396
397                onContainerKeyDown: function(/*Object*/ info){
398                        // summary:
399                        //              Called when there was a keydown on the container
400                        // tags:
401                        //              private
402                        info.e._djpage = info.page;
403                        this.onkeydown(info.e);
404                }
405        });
406
407        StackController.StackButton = StackButton;      // for monkey patching
408
409        return StackController;
410});
Note: See TracBrowser for help on using the repository browser.