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
JavaScript
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;
});