]> git.wh0rd.org Git - tt-rss.git/blob - lib/dijit/layout/StackController.js.uncompressed.js
47e09ed468a1f32e34d3192fa436ece824363240
[tt-rss.git] / lib / dijit / layout / StackController.js.uncompressed.js
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 });