UNPKG

dijit

Version:

Dijit provides a complete collection of user interface controls based on Dojo, giving you the power to create web applications that are highly optimized for usability, performance, internationalization, accessibility, but above all deliver an incredible u

412 lines (345 loc) 13.8 kB
define([ "dojo/_base/array", // array.forEach array.indexOf array.some "dojo/cookie", // cookie "dojo/_base/declare", // declare "dojo/dom-class", // domClass.add domClass.replace "dojo/dom-construct", "dojo/has", // has("dijit-legacy-requires") "dojo/_base/lang", // lang.extend "dojo/on", "dojo/ready", "dojo/topic", // publish "dojo/when", "../registry", // registry.byId "../_WidgetBase", "./_LayoutWidget" ], function(array, cookie, declare, domClass, domConstruct, has, lang, on, ready, topic, when, registry, _WidgetBase, _LayoutWidget){ // module: // dijit/layout/StackContainer // Back compat w/1.6, remove for 2.0 if(has("dijit-legacy-requires")){ ready(0, function(){ var requires = ["dijit/layout/StackController"]; require(requires); // use indirection so modules not rolled into a build }); } var StackContainer = declare("dijit.layout.StackContainer", _LayoutWidget, { // summary: // A container that has multiple children, but shows only // one child at a time // // description: // A container for widgets (ContentPanes, for example) That displays // only one Widget at a time. // // Publishes topics [widgetId]-addChild, [widgetId]-removeChild, and [widgetId]-selectChild // // Can be base class for container, Wizard, Show, etc. // // See `StackContainer.ChildWidgetProperties` for details on the properties that can be set on // children of a `StackContainer`. // doLayout: Boolean // If true, change the size of my currently displayed child to match my size doLayout: true, // persist: Boolean // Remembers the selected child across sessions persist: false, baseClass: "dijitStackContainer", /*===== // selectedChildWidget: [readonly] dijit._Widget // References the currently selected child widget, if any. // Adjust selected child with selectChild() method. selectedChildWidget: null, =====*/ buildRendering: function(){ this.inherited(arguments); domClass.add(this.domNode, "dijitLayoutContainer"); }, postCreate: function(){ this.inherited(arguments); this.own( on(this.domNode, "keydown", lang.hitch(this, "_onKeyDown")) ); }, startup: function(){ if(this._started){ return; } var children = this.getChildren(); // Setup each page panel to be initially hidden array.forEach(children, this._setupChild, this); // Figure out which child to initially display, defaulting to first one if(this.persist){ this.selectedChildWidget = registry.byId(cookie(this.id + "_selectedChild")); }else{ array.some(children, function(child){ if(child.selected){ this.selectedChildWidget = child; } return child.selected; }, this); } var selected = this.selectedChildWidget; if(!selected && children[0]){ selected = this.selectedChildWidget = children[0]; selected.selected = true; } // Publish information about myself so any StackControllers can initialize. // This needs to happen before this.inherited(arguments) so that for // TabContainer, this._contentBox doesn't include the space for the tab labels. topic.publish(this.id + "-startup", {children: children, selected: selected, textDir: this.textDir}); // Startup each child widget, and do initial layout like setting this._contentBox, // then calls this.resize() which does the initial sizing on the selected child. this.inherited(arguments); }, resize: function(){ // Overrides _LayoutWidget.resize() // Resize is called when we are first made visible (it's called from startup() // if we are initially visible). If this is the first time we've been made // visible then show our first child. if(!this._hasBeenShown){ this._hasBeenShown = true; var selected = this.selectedChildWidget; if(selected){ this._showChild(selected); } } this.inherited(arguments); }, _setupChild: function(/*dijit/_WidgetBase*/ child){ // Overrides _LayoutWidget._setupChild() // For aria support, wrap child widget in a <div role="tabpanel"> var childNode = child.domNode, wrapper = domConstruct.place( "<div role='tabpanel' class='" + this.baseClass + "ChildWrapper dijitHidden'>", child.domNode, "replace"), label = child["aria-label"] || child.title || child.label; if(label){ // setAttribute() escapes special chars, and if() statement avoids setting aria-label="undefined" wrapper.setAttribute("aria-label", label); } domConstruct.place(childNode, wrapper); child._wrapper = wrapper; // to set the aria-labelledby in StackController this.inherited(arguments); // child may have style="display: none" (at least our test cases do), so remove that if(childNode.style.display == "none"){ childNode.style.display = "block"; } // remove the title attribute so it doesn't show up when i hover over a node child.domNode.removeAttribute("title"); }, addChild: function(/*dijit/_WidgetBase*/ child, /*Integer?*/ insertIndex){ // Overrides _Container.addChild() to do layout and publish events this.inherited(arguments); if(this._started){ topic.publish(this.id + "-addChild", child, insertIndex); // publish // in case the tab titles have overflowed from one line to two lines // (or, if this if first child, from zero lines to one line) // TODO: w/ScrollingTabController this is no longer necessary, although // ScrollTabController.resize() does need to get called to show/hide // the navigation buttons as appropriate, but that's handled in ScrollingTabController.onAddChild(). // If this is updated to not layout [except for initial child added / last child removed], update // "childless startup" test in StackContainer.html to check for no resize event after second addChild() this.layout(); // if this is the first child, then select it if(!this.selectedChildWidget){ this.selectChild(child); } } }, removeChild: function(/*dijit/_WidgetBase*/ page){ // Overrides _Container.removeChild() to do layout and publish events var idx = array.indexOf(this.getChildren(), page); this.inherited(arguments); // Remove the child widget wrapper we use to set aria roles. This won't affect the page itself since it's // already been detached from page._wrapper via the this.inherited(arguments) call above. domConstruct.destroy(page._wrapper); delete page._wrapper; if(this._started){ // This will notify any tablists to remove a button; do this first because it may affect sizing. topic.publish(this.id + "-removeChild", page); } // If all our children are being destroyed than don't run the code below (to select another page), // because we are deleting every page one by one if(this._descendantsBeingDestroyed){ return; } // Select new page to display, also updating TabController to show the respective tab. // Do this before layout call because it can affect the height of the TabController. if(this.selectedChildWidget === page){ this.selectedChildWidget = undefined; if(this._started){ var children = this.getChildren(); if(children.length){ this.selectChild(children[Math.max(idx - 1, 0)]); } } } if(this._started){ // In case the tab titles now take up one line instead of two lines // (note though that ScrollingTabController never overflows to multiple lines), // or the height has changed slightly because of addition/removal of tab which close icon this.layout(); } }, selectChild: function(/*dijit/_WidgetBase|String*/ page, /*Boolean*/ animate){ // summary: // Show the given widget (which must be one of my children) // page: // Reference to child widget or id of child widget var d; page = registry.byId(page); if(this.selectedChildWidget != page){ // Deselect old page and select new one d = this._transition(page, this.selectedChildWidget, animate); this._set("selectedChildWidget", page); topic.publish(this.id + "-selectChild", page, this._focused); // publish if(this.persist){ cookie(this.id + "_selectedChild", this.selectedChildWidget.id); } } // d may be null, or a scalar like true. Return a promise in all cases return when(d || true); // Promise }, _transition: function(newWidget, oldWidget /*===== , animate =====*/){ // summary: // Hide the old widget and display the new widget. // Subclasses should override this. // newWidget: dijit/_WidgetBase // The newly selected widget. // oldWidget: dijit/_WidgetBase // The previously selected widget. // animate: Boolean // Used by AccordionContainer to turn on/off slide effect. // tags: // protected extension if(oldWidget){ this._hideChild(oldWidget); } var d = this._showChild(newWidget); // Size the new widget, in case this is the first time it's being shown, // or I have been resized since the last time it was shown. // Note that page must be visible for resizing to work. if(newWidget.resize){ if(this.doLayout){ newWidget.resize(this._containerContentBox || this._contentBox); }else{ // the child should pick it's own size but we still need to call resize() // (with no arguments) to let the widget lay itself out newWidget.resize(); } } return d; // If child has an href, promise that fires when the child's href finishes loading }, _adjacent: function(/*Boolean*/ forward){ // summary: // Gets the next/previous child widget in this container from the current selection. // TODO: remove for 2.0 if this isn't being used. Otherwise, fix to skip disabled tabs. var children = this.getChildren(); var index = array.indexOf(children, this.selectedChildWidget); index += forward ? 1 : children.length - 1; return children[ index % children.length ]; // dijit/_WidgetBase }, forward: function(){ // summary: // Advance to next page. return this.selectChild(this._adjacent(true), true); }, back: function(){ // summary: // Go back to previous page. return this.selectChild(this._adjacent(false), true); }, _onKeyDown: function(e){ topic.publish(this.id + "-containerKeyDown", { e: e, page: this}); // publish }, layout: function(){ // Implement _LayoutWidget.layout() virtual method. var child = this.selectedChildWidget; if(child && child.resize){ if(this.doLayout){ child.resize(this._containerContentBox || this._contentBox); }else{ child.resize(); } } }, _showChild: function(/*dijit/_WidgetBase*/ page){ // summary: // Show the specified child by changing it's CSS, and call _onShow()/onShow() so // it can do any updates it needs regarding loading href's etc. // returns: // Promise that fires when page has finished showing, or true if there's no href var children = this.getChildren(); page.isFirstChild = (page == children[0]); page.isLastChild = (page == children[children.length - 1]); page._set("selected", true); if(page._wrapper){ // false if not started yet domClass.replace(page._wrapper, "dijitVisible", "dijitHidden"); } return (page._onShow && page._onShow()) || true; }, _hideChild: function(/*dijit/_WidgetBase*/ page){ // summary: // Hide the specified child by changing it's CSS, and call _onHide() so // it's notified. page._set("selected", false); if(page._wrapper){ // false if not started yet domClass.replace(page._wrapper, "dijitHidden", "dijitVisible"); } page.onHide && page.onHide(); }, closeChild: function(/*dijit/_WidgetBase*/ page){ // summary: // Callback when user clicks the [X] to remove a page. // If onClose() returns true then remove and destroy the child. // tags: // private var remove = !page.onClose || page.onClose(this, page); if(remove){ this.removeChild(page); // makes sure we can clean up executeScripts in ContentPane onUnLoad page.destroyRecursive(); } }, destroyDescendants: function(/*Boolean*/ preserveDom){ this._descendantsBeingDestroyed = true; this.selectedChildWidget = undefined; array.forEach(this.getChildren(), function(child){ if(!preserveDom){ this.removeChild(child); } child.destroyRecursive(preserveDom); }, this); this._descendantsBeingDestroyed = false; } }); StackContainer.ChildWidgetProperties = { // summary: // These properties can be specified for the children of a StackContainer. // selected: Boolean // Specifies that this widget should be the initially displayed pane. // Note: to change the selected child use `dijit/layout/StackContainer.selectChild` selected: false, // disabled: Boolean // Specifies that the button to select this pane should be disabled. // Doesn't affect programmatic selection of the pane, nor does it deselect the pane if it is currently selected. disabled: false, // closable: Boolean // True if user can close (destroy) this child, such as (for example) clicking the X on the tab. closable: false, // iconClass: String // CSS Class specifying icon to use in label associated with this pane. iconClass: "dijitNoIcon", // showTitle: Boolean // When true, display title of this widget as tab label etc., rather than just using // icon specified in iconClass showTitle: true }; // Since any widget can be specified as a StackContainer child, mix them // into the base widget class. (This is a hack, but it's effective.) // This is for the benefit of the parser. Remove for 2.0. Also, hide from doc viewer. lang.extend(_WidgetBase, /*===== {} || =====*/ StackContainer.ChildWidgetProperties); return StackContainer; });