]>
git.wh0rd.org - tt-rss.git/blob - 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
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
;