[483] | 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 | }); |
---|