1 | define([ |
---|
2 | "dojo/_base/array", // array.forEach array.indexOf array.some |
---|
3 | "dojo/cookie", // cookie |
---|
4 | "dojo/_base/declare", // declare |
---|
5 | "dojo/dom-class", // domClass.add domClass.replace |
---|
6 | "dojo/dom-construct", |
---|
7 | "dojo/has", // has("dijit-legacy-requires") |
---|
8 | "dojo/_base/lang", // lang.extend |
---|
9 | "dojo/on", |
---|
10 | "dojo/ready", |
---|
11 | "dojo/topic", // publish |
---|
12 | "dojo/when", |
---|
13 | "../registry", // registry.byId |
---|
14 | "../_WidgetBase", |
---|
15 | "./_LayoutWidget", |
---|
16 | "dojo/i18n!../nls/common" |
---|
17 | ], function(array, cookie, declare, domClass, domConstruct, has, lang, on, ready, topic, when, registry, _WidgetBase, _LayoutWidget){ |
---|
18 | |
---|
19 | // module: |
---|
20 | // dijit/layout/StackContainer |
---|
21 | |
---|
22 | // Back compat w/1.6, remove for 2.0 |
---|
23 | if(has("dijit-legacy-requires")){ |
---|
24 | ready(0, function(){ |
---|
25 | var requires = ["dijit/layout/StackController"]; |
---|
26 | require(requires); // use indirection so modules not rolled into a build |
---|
27 | }); |
---|
28 | } |
---|
29 | |
---|
30 | var StackContainer = declare("dijit.layout.StackContainer", _LayoutWidget, { |
---|
31 | // summary: |
---|
32 | // A container that has multiple children, but shows only |
---|
33 | // one child at a time |
---|
34 | // |
---|
35 | // description: |
---|
36 | // A container for widgets (ContentPanes, for example) That displays |
---|
37 | // only one Widget at a time. |
---|
38 | // |
---|
39 | // Publishes topics [widgetId]-addChild, [widgetId]-removeChild, and [widgetId]-selectChild |
---|
40 | // |
---|
41 | // Can be base class for container, Wizard, Show, etc. |
---|
42 | // |
---|
43 | // See `StackContainer.ChildWidgetProperties` for details on the properties that can be set on |
---|
44 | // children of a `StackContainer`. |
---|
45 | |
---|
46 | // doLayout: Boolean |
---|
47 | // If true, change the size of my currently displayed child to match my size |
---|
48 | doLayout: true, |
---|
49 | |
---|
50 | // persist: Boolean |
---|
51 | // Remembers the selected child across sessions |
---|
52 | persist: false, |
---|
53 | |
---|
54 | baseClass: "dijitStackContainer", |
---|
55 | |
---|
56 | /*===== |
---|
57 | // selectedChildWidget: [readonly] dijit._Widget |
---|
58 | // References the currently selected child widget, if any. |
---|
59 | // Adjust selected child with selectChild() method. |
---|
60 | selectedChildWidget: null, |
---|
61 | =====*/ |
---|
62 | |
---|
63 | buildRendering: function(){ |
---|
64 | this.inherited(arguments); |
---|
65 | domClass.add(this.domNode, "dijitLayoutContainer"); |
---|
66 | }, |
---|
67 | |
---|
68 | postCreate: function(){ |
---|
69 | this.inherited(arguments); |
---|
70 | this.own( |
---|
71 | on(this.domNode, "keydown", lang.hitch(this, "_onKeyDown")) |
---|
72 | ); |
---|
73 | }, |
---|
74 | |
---|
75 | startup: function(){ |
---|
76 | if(this._started){ |
---|
77 | return; |
---|
78 | } |
---|
79 | |
---|
80 | var children = this.getChildren(); |
---|
81 | |
---|
82 | // Setup each page panel to be initially hidden |
---|
83 | array.forEach(children, this._setupChild, this); |
---|
84 | |
---|
85 | // Figure out which child to initially display, defaulting to first one |
---|
86 | if(this.persist){ |
---|
87 | this.selectedChildWidget = registry.byId(cookie(this.id + "_selectedChild")); |
---|
88 | }else{ |
---|
89 | array.some(children, function(child){ |
---|
90 | if(child.selected){ |
---|
91 | this.selectedChildWidget = child; |
---|
92 | } |
---|
93 | return child.selected; |
---|
94 | }, this); |
---|
95 | } |
---|
96 | var selected = this.selectedChildWidget; |
---|
97 | if(!selected && children[0]){ |
---|
98 | selected = this.selectedChildWidget = children[0]; |
---|
99 | selected.selected = true; |
---|
100 | } |
---|
101 | |
---|
102 | // Publish information about myself so any StackControllers can initialize. |
---|
103 | // This needs to happen before this.inherited(arguments) so that for |
---|
104 | // TabContainer, this._contentBox doesn't include the space for the tab labels. |
---|
105 | topic.publish(this.id + "-startup", {children: children, selected: selected, textDir: this.textDir}); |
---|
106 | |
---|
107 | // Startup each child widget, and do initial layout like setting this._contentBox, |
---|
108 | // then calls this.resize() which does the initial sizing on the selected child. |
---|
109 | this.inherited(arguments); |
---|
110 | }, |
---|
111 | |
---|
112 | resize: function(){ |
---|
113 | // Overrides _LayoutWidget.resize() |
---|
114 | // Resize is called when we are first made visible (it's called from startup() |
---|
115 | // if we are initially visible). If this is the first time we've been made |
---|
116 | // visible then show our first child. |
---|
117 | if(!this._hasBeenShown){ |
---|
118 | this._hasBeenShown = true; |
---|
119 | var selected = this.selectedChildWidget; |
---|
120 | if(selected){ |
---|
121 | this._showChild(selected); |
---|
122 | } |
---|
123 | } |
---|
124 | this.inherited(arguments); |
---|
125 | }, |
---|
126 | |
---|
127 | _setupChild: function(/*dijit/_WidgetBase*/ child){ |
---|
128 | // Overrides _LayoutWidget._setupChild() |
---|
129 | |
---|
130 | // For aria support, wrap child widget in a <div role="tabpanel"> |
---|
131 | var childNode = child.domNode, |
---|
132 | wrapper = domConstruct.place( |
---|
133 | "<div role='tabpanel' class='" + this.baseClass + "ChildWrapper dijitHidden'>", |
---|
134 | child.domNode, |
---|
135 | "replace"), |
---|
136 | label = child["aria-label"] || child.title || child.label; |
---|
137 | if(label){ |
---|
138 | // setAttribute() escapes special chars, and if() statement avoids setting aria-label="undefined" |
---|
139 | wrapper.setAttribute("aria-label", label); |
---|
140 | } |
---|
141 | domConstruct.place(childNode, wrapper); |
---|
142 | child._wrapper = wrapper; // to set the aria-labelledby in StackController |
---|
143 | |
---|
144 | this.inherited(arguments); |
---|
145 | |
---|
146 | // child may have style="display: none" (at least our test cases do), so remove that |
---|
147 | if(childNode.style.display == "none"){ |
---|
148 | childNode.style.display = "block"; |
---|
149 | } |
---|
150 | |
---|
151 | // remove the title attribute so it doesn't show up when i hover over a node |
---|
152 | child.domNode.title = ""; |
---|
153 | }, |
---|
154 | |
---|
155 | addChild: function(/*dijit/_WidgetBase*/ child, /*Integer?*/ insertIndex){ |
---|
156 | // Overrides _Container.addChild() to do layout and publish events |
---|
157 | |
---|
158 | this.inherited(arguments); |
---|
159 | |
---|
160 | if(this._started){ |
---|
161 | topic.publish(this.id + "-addChild", child, insertIndex); // publish |
---|
162 | |
---|
163 | // in case the tab titles have overflowed from one line to two lines |
---|
164 | // (or, if this if first child, from zero lines to one line) |
---|
165 | // TODO: w/ScrollingTabController this is no longer necessary, although |
---|
166 | // ScrollTabController.resize() does need to get called to show/hide |
---|
167 | // the navigation buttons as appropriate, but that's handled in ScrollingTabController.onAddChild(). |
---|
168 | // If this is updated to not layout [except for initial child added / last child removed], update |
---|
169 | // "childless startup" test in StackContainer.html to check for no resize event after second addChild() |
---|
170 | this.layout(); |
---|
171 | |
---|
172 | // if this is the first child, then select it |
---|
173 | if(!this.selectedChildWidget){ |
---|
174 | this.selectChild(child); |
---|
175 | } |
---|
176 | } |
---|
177 | }, |
---|
178 | |
---|
179 | removeChild: function(/*dijit/_WidgetBase*/ page){ |
---|
180 | // Overrides _Container.removeChild() to do layout and publish events |
---|
181 | |
---|
182 | var idx = array.indexOf(this.getChildren(), page); |
---|
183 | |
---|
184 | this.inherited(arguments); |
---|
185 | |
---|
186 | // Remove the child widget wrapper we use to set aria roles. This won't affect the page itself since it's |
---|
187 | // already been detached from page._wrapper via the this.inherited(arguments) call above. |
---|
188 | domConstruct.destroy(page._wrapper); |
---|
189 | delete page._wrapper; |
---|
190 | |
---|
191 | if(this._started){ |
---|
192 | // This will notify any tablists to remove a button; do this first because it may affect sizing. |
---|
193 | topic.publish(this.id + "-removeChild", page); |
---|
194 | } |
---|
195 | |
---|
196 | // If all our children are being destroyed than don't run the code below (to select another page), |
---|
197 | // because we are deleting every page one by one |
---|
198 | if(this._descendantsBeingDestroyed){ |
---|
199 | return; |
---|
200 | } |
---|
201 | |
---|
202 | // Select new page to display, also updating TabController to show the respective tab. |
---|
203 | // Do this before layout call because it can affect the height of the TabController. |
---|
204 | if(this.selectedChildWidget === page){ |
---|
205 | this.selectedChildWidget = undefined; |
---|
206 | if(this._started){ |
---|
207 | var children = this.getChildren(); |
---|
208 | if(children.length){ |
---|
209 | this.selectChild(children[Math.max(idx - 1, 0)]); |
---|
210 | } |
---|
211 | } |
---|
212 | } |
---|
213 | |
---|
214 | if(this._started){ |
---|
215 | // In case the tab titles now take up one line instead of two lines |
---|
216 | // (note though that ScrollingTabController never overflows to multiple lines), |
---|
217 | // or the height has changed slightly because of addition/removal of tab which close icon |
---|
218 | this.layout(); |
---|
219 | } |
---|
220 | }, |
---|
221 | |
---|
222 | selectChild: function(/*dijit/_WidgetBase|String*/ page, /*Boolean*/ animate){ |
---|
223 | // summary: |
---|
224 | // Show the given widget (which must be one of my children) |
---|
225 | // page: |
---|
226 | // Reference to child widget or id of child widget |
---|
227 | |
---|
228 | var d; |
---|
229 | |
---|
230 | page = registry.byId(page); |
---|
231 | |
---|
232 | if(this.selectedChildWidget != page){ |
---|
233 | // Deselect old page and select new one |
---|
234 | d = this._transition(page, this.selectedChildWidget, animate); |
---|
235 | this._set("selectedChildWidget", page); |
---|
236 | topic.publish(this.id + "-selectChild", page); // publish |
---|
237 | |
---|
238 | if(this.persist){ |
---|
239 | cookie(this.id + "_selectedChild", this.selectedChildWidget.id); |
---|
240 | } |
---|
241 | } |
---|
242 | |
---|
243 | // d may be null, or a scalar like true. Return a promise in all cases |
---|
244 | return when(d || true); // Promise |
---|
245 | }, |
---|
246 | |
---|
247 | _transition: function(newWidget, oldWidget /*===== , animate =====*/){ |
---|
248 | // summary: |
---|
249 | // Hide the old widget and display the new widget. |
---|
250 | // Subclasses should override this. |
---|
251 | // newWidget: dijit/_WidgetBase |
---|
252 | // The newly selected widget. |
---|
253 | // oldWidget: dijit/_WidgetBase |
---|
254 | // The previously selected widget. |
---|
255 | // animate: Boolean |
---|
256 | // Used by AccordionContainer to turn on/off slide effect. |
---|
257 | // tags: |
---|
258 | // protected extension |
---|
259 | if(oldWidget){ |
---|
260 | this._hideChild(oldWidget); |
---|
261 | } |
---|
262 | var d = this._showChild(newWidget); |
---|
263 | |
---|
264 | // Size the new widget, in case this is the first time it's being shown, |
---|
265 | // or I have been resized since the last time it was shown. |
---|
266 | // Note that page must be visible for resizing to work. |
---|
267 | if(newWidget.resize){ |
---|
268 | if(this.doLayout){ |
---|
269 | newWidget.resize(this._containerContentBox || this._contentBox); |
---|
270 | }else{ |
---|
271 | // the child should pick it's own size but we still need to call resize() |
---|
272 | // (with no arguments) to let the widget lay itself out |
---|
273 | newWidget.resize(); |
---|
274 | } |
---|
275 | } |
---|
276 | |
---|
277 | return d; // If child has an href, promise that fires when the child's href finishes loading |
---|
278 | }, |
---|
279 | |
---|
280 | _adjacent: function(/*Boolean*/ forward){ |
---|
281 | // summary: |
---|
282 | // Gets the next/previous child widget in this container from the current selection. |
---|
283 | |
---|
284 | // TODO: remove for 2.0 if this isn't being used. Otherwise, fix to skip disabled tabs. |
---|
285 | |
---|
286 | var children = this.getChildren(); |
---|
287 | var index = array.indexOf(children, this.selectedChildWidget); |
---|
288 | index += forward ? 1 : children.length - 1; |
---|
289 | return children[ index % children.length ]; // dijit/_WidgetBase |
---|
290 | }, |
---|
291 | |
---|
292 | forward: function(){ |
---|
293 | // summary: |
---|
294 | // Advance to next page. |
---|
295 | return this.selectChild(this._adjacent(true), true); |
---|
296 | }, |
---|
297 | |
---|
298 | back: function(){ |
---|
299 | // summary: |
---|
300 | // Go back to previous page. |
---|
301 | return this.selectChild(this._adjacent(false), true); |
---|
302 | }, |
---|
303 | |
---|
304 | _onKeyDown: function(e){ |
---|
305 | topic.publish(this.id + "-containerKeyDown", { e: e, page: this}); // publish |
---|
306 | }, |
---|
307 | |
---|
308 | layout: function(){ |
---|
309 | // Implement _LayoutWidget.layout() virtual method. |
---|
310 | var child = this.selectedChildWidget; |
---|
311 | if(child && child.resize){ |
---|
312 | if(this.doLayout){ |
---|
313 | child.resize(this._containerContentBox || this._contentBox); |
---|
314 | }else{ |
---|
315 | child.resize(); |
---|
316 | } |
---|
317 | } |
---|
318 | }, |
---|
319 | |
---|
320 | _showChild: function(/*dijit/_WidgetBase*/ page){ |
---|
321 | // summary: |
---|
322 | // Show the specified child by changing it's CSS, and call _onShow()/onShow() so |
---|
323 | // it can do any updates it needs regarding loading href's etc. |
---|
324 | // returns: |
---|
325 | // Promise that fires when page has finished showing, or true if there's no href |
---|
326 | var children = this.getChildren(); |
---|
327 | page.isFirstChild = (page == children[0]); |
---|
328 | page.isLastChild = (page == children[children.length - 1]); |
---|
329 | page._set("selected", true); |
---|
330 | |
---|
331 | if(page._wrapper){ // false if not started yet |
---|
332 | domClass.replace(page._wrapper, "dijitVisible", "dijitHidden"); |
---|
333 | } |
---|
334 | |
---|
335 | return (page._onShow && page._onShow()) || true; |
---|
336 | }, |
---|
337 | |
---|
338 | _hideChild: function(/*dijit/_WidgetBase*/ page){ |
---|
339 | // summary: |
---|
340 | // Hide the specified child by changing it's CSS, and call _onHide() so |
---|
341 | // it's notified. |
---|
342 | page._set("selected", false); |
---|
343 | |
---|
344 | if(page._wrapper){ // false if not started yet |
---|
345 | domClass.replace(page._wrapper, "dijitHidden", "dijitVisible"); |
---|
346 | } |
---|
347 | |
---|
348 | page.onHide && page.onHide(); |
---|
349 | }, |
---|
350 | |
---|
351 | closeChild: function(/*dijit/_WidgetBase*/ page){ |
---|
352 | // summary: |
---|
353 | // Callback when user clicks the [X] to remove a page. |
---|
354 | // If onClose() returns true then remove and destroy the child. |
---|
355 | // tags: |
---|
356 | // private |
---|
357 | var remove = page.onClose && page.onClose(this, page); |
---|
358 | if(remove){ |
---|
359 | this.removeChild(page); |
---|
360 | // makes sure we can clean up executeScripts in ContentPane onUnLoad |
---|
361 | page.destroyRecursive(); |
---|
362 | } |
---|
363 | }, |
---|
364 | |
---|
365 | destroyDescendants: function(/*Boolean*/ preserveDom){ |
---|
366 | this._descendantsBeingDestroyed = true; |
---|
367 | this.selectedChildWidget = undefined; |
---|
368 | array.forEach(this.getChildren(), function(child){ |
---|
369 | if(!preserveDom){ |
---|
370 | this.removeChild(child); |
---|
371 | } |
---|
372 | child.destroyRecursive(preserveDom); |
---|
373 | }, this); |
---|
374 | this._descendantsBeingDestroyed = false; |
---|
375 | } |
---|
376 | }); |
---|
377 | |
---|
378 | StackContainer.ChildWidgetProperties = { |
---|
379 | // summary: |
---|
380 | // These properties can be specified for the children of a StackContainer. |
---|
381 | |
---|
382 | // selected: Boolean |
---|
383 | // Specifies that this widget should be the initially displayed pane. |
---|
384 | // Note: to change the selected child use `dijit/layout/StackContainer.selectChild` |
---|
385 | selected: false, |
---|
386 | |
---|
387 | // disabled: Boolean |
---|
388 | // Specifies that the button to select this pane should be disabled. |
---|
389 | // Doesn't affect programmatic selection of the pane, nor does it deselect the pane if it is currently selected. |
---|
390 | disabled: false, |
---|
391 | |
---|
392 | // closable: Boolean |
---|
393 | // True if user can close (destroy) this child, such as (for example) clicking the X on the tab. |
---|
394 | closable: false, |
---|
395 | |
---|
396 | // iconClass: String |
---|
397 | // CSS Class specifying icon to use in label associated with this pane. |
---|
398 | iconClass: "dijitNoIcon", |
---|
399 | |
---|
400 | // showTitle: Boolean |
---|
401 | // When true, display title of this widget as tab label etc., rather than just using |
---|
402 | // icon specified in iconClass |
---|
403 | showTitle: true |
---|
404 | }; |
---|
405 | |
---|
406 | // Since any widget can be specified as a StackContainer child, mix them |
---|
407 | // into the base widget class. (This is a hack, but it's effective.) |
---|
408 | // This is for the benefit of the parser. Remove for 2.0. Also, hide from doc viewer. |
---|
409 | lang.extend(_WidgetBase, /*===== {} || =====*/ StackContainer.ChildWidgetProperties); |
---|
410 | |
---|
411 | return StackContainer; |
---|
412 | }); |
---|