", this.contentWidget.domNode, "after");
// wrapper div's first child is the button widget (ie, the title bar)
var child = this.contentWidget,
cls = dojo.getObject(this.buttonWidget);
this.button = child._buttonWidget = (new cls({
contentWidget: child,
label: child.title,
title: child.tooltip,
dir: child.dir,
lang: child.lang,
iconClass: child.iconClass,
id: child.id + "_button",
parent: this.parent
})).placeAt(this.domNode);
// and then the actual content widget (changing it from prior-sibling to last-child),
// wrapped by a
this.containerNode = dojo.place("
", this.domNode);
dojo.place(this.contentWidget.domNode, this.containerNode);
},
postCreate: function(){
this.inherited(arguments);
// Map changes in content widget's title etc. to changes in the button
var button = this.button;
this._contentWidgetWatches = [
this.contentWidget.watch('title', dojo.hitch(this, function(name, oldValue, newValue){
button.set("label", newValue);
})),
this.contentWidget.watch('tooltip', dojo.hitch(this, function(name, oldValue, newValue){
button.set("title", newValue);
})),
this.contentWidget.watch('iconClass', dojo.hitch(this, function(name, oldValue, newValue){
button.set("iconClass", newValue);
}))
];
},
_setSelectedAttr: function(/*Boolean*/ isSelected){
this._set("selected", isSelected);
this.button.set("selected", isSelected);
if(isSelected){
var cw = this.contentWidget;
if(cw.onSelected){ cw.onSelected(); }
}
},
startup: function(){
// Called by _Container.addChild()
this.contentWidget.startup();
},
destroy: function(){
this.button.destroyRecursive();
dojo.forEach(this._contentWidgetWatches || [], function(w){ w.unwatch(); });
delete this.contentWidget._buttonWidget;
delete this.contentWidget._wrapperWidget;
this.inherited(arguments);
},
destroyDescendants: function(){
// since getChildren isn't working for me, have to code this manually
this.contentWidget.destroyRecursive();
}
});
dojo.declare("dijit.layout._AccordionButton",
[dijit._Widget, dijit._Templated, dijit._CssStateMixin],
{
// summary:
// The title bar to click to open up an accordion pane.
// Internal widget used by AccordionContainer.
// tags:
// private
templateString: dojo.cache("dijit.layout", "templates/AccordionButton.html", "
\n\t
+-\n\t\t
\n\t
\n
\n"),
attributeMap: dojo.mixin(dojo.clone(dijit.layout.ContentPane.prototype.attributeMap), {
label: {node: "titleTextNode", type: "innerHTML" },
title: {node: "titleTextNode", type: "attribute", attribute: "title"},
iconClass: { node: "iconNode", type: "class" }
}),
baseClass: "dijitAccordionTitle",
getParent: function(){
// summary:
// Returns the AccordionContainer parent.
// tags:
// private
return this.parent;
},
buildRendering: function(){
this.inherited(arguments);
var titleTextNodeId = this.id.replace(' ','_');
dojo.attr(this.titleTextNode, "id", titleTextNodeId+"_title");
dijit.setWaiState(this.focusNode, "labelledby", dojo.attr(this.titleTextNode, "id"));
dojo.setSelectable(this.domNode, false);
},
getTitleHeight: function(){
// summary:
// Returns the height of the title dom node.
return dojo._getMarginSize(this.domNode).h; // Integer
},
// TODO: maybe the parent should set these methods directly rather than forcing the code
// into the button widget?
_onTitleClick: function(){
// summary:
// Callback when someone clicks my title.
var parent = this.getParent();
parent.selectChild(this.contentWidget, true);
dijit.focus(this.focusNode);
},
_onTitleKeyPress: function(/*Event*/ evt){
return this.getParent()._onKeyPress(evt, this.contentWidget);
},
_setSelectedAttr: function(/*Boolean*/ isSelected){
this._set("selected", isSelected);
dijit.setWaiState(this.focusNode, "expanded", isSelected);
dijit.setWaiState(this.focusNode, "selected", isSelected);
this.focusNode.setAttribute("tabIndex", isSelected ? "0" : "-1");
}
});
}
if(!dojo._hasResource["dijit.layout.BorderContainer"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dijit.layout.BorderContainer"] = true;
dojo.provide("dijit.layout.BorderContainer");
dojo.declare(
"dijit.layout.BorderContainer",
dijit.layout._LayoutWidget,
{
// summary:
// Provides layout in up to 5 regions, a mandatory center with optional borders along its 4 sides.
//
// description:
// A BorderContainer is a box with a specified size, such as style="width: 500px; height: 500px;",
// that contains a child widget marked region="center" and optionally children widgets marked
// region equal to "top", "bottom", "leading", "trailing", "left" or "right".
// Children along the edges will be laid out according to width or height dimensions and may
// include optional splitters (splitter="true") to make them resizable by the user. The remaining
// space is designated for the center region.
//
// The outer size must be specified on the BorderContainer node. Width must be specified for the sides
// and height for the top and bottom, respectively. No dimensions should be specified on the center;
// it will fill the remaining space. Regions named "leading" and "trailing" may be used just like
// "left" and "right" except that they will be reversed in right-to-left environments.
//
// For complex layouts, multiple children can be specified for a single region. In this case, the
// layoutPriority flag on the children determines which child is closer to the edge (low layoutPriority)
// and which child is closer to the center (high layoutPriority). layoutPriority can also be used
// instead of the design attribute to conrol layout precedence of horizontal vs. vertical panes.
// example:
// |
// |
header text
// |
table of contents
// |
client area
// |
// design: String
// Which design is used for the layout:
// - "headline" (default) where the top and bottom extend
// the full width of the container
// - "sidebar" where the left and right sides extend from top to bottom.
design: "headline",
// gutters: [const] Boolean
// Give each pane a border and margin.
// Margin determined by domNode.paddingLeft.
// When false, only resizable panes have a gutter (i.e. draggable splitter) for resizing.
gutters: true,
// liveSplitters: [const] Boolean
// Specifies whether splitters resize as you drag (true) or only upon mouseup (false)
liveSplitters: true,
// persist: Boolean
// Save splitter positions in a cookie.
persist: false,
baseClass: "dijitBorderContainer",
// _splitterClass: String
// Optional hook to override the default Splitter widget used by BorderContainer
_splitterClass: "dijit.layout._Splitter",
postMixInProperties: function(){
// change class name to indicate that BorderContainer is being used purely for
// layout (like LayoutContainer) rather than for pretty formatting.
if(!this.gutters){
this.baseClass += "NoGutter";
}
this.inherited(arguments);
},
startup: function(){
if(this._started){ return; }
dojo.forEach(this.getChildren(), this._setupChild, this);
this.inherited(arguments);
},
_setupChild: function(/*dijit._Widget*/ child){
// Override _LayoutWidget._setupChild().
var region = child.region;
if(region){
this.inherited(arguments);
dojo.addClass(child.domNode, this.baseClass+"Pane");
var ltr = this.isLeftToRight();
if(region == "leading"){ region = ltr ? "left" : "right"; }
if(region == "trailing"){ region = ltr ? "right" : "left"; }
// Create draggable splitter for resizing pane,
// or alternately if splitter=false but BorderContainer.gutters=true then
// insert dummy div just for spacing
if(region != "center" && (child.splitter || this.gutters) && !child._splitterWidget){
var _Splitter = dojo.getObject(child.splitter ? this._splitterClass : "dijit.layout._Gutter");
var splitter = new _Splitter({
id: child.id + "_splitter",
container: this,
child: child,
region: region,
live: this.liveSplitters
});
splitter.isSplitter = true;
child._splitterWidget = splitter;
dojo.place(splitter.domNode, child.domNode, "after");
// Splitters aren't added as Contained children, so we need to call startup explicitly
splitter.startup();
}
child.region = region; // TODO: technically wrong since it overwrites "trailing" with "left" etc.
}
},
layout: function(){
// Implement _LayoutWidget.layout() virtual method.
this._layoutChildren();
},
addChild: function(/*dijit._Widget*/ child, /*Integer?*/ insertIndex){
// Override _LayoutWidget.addChild().
this.inherited(arguments);
if(this._started){
this.layout(); //OPT
}
},
removeChild: function(/*dijit._Widget*/ child){
// Override _LayoutWidget.removeChild().
var region = child.region;
var splitter = child._splitterWidget
if(splitter){
splitter.destroy();
delete child._splitterWidget;
}
this.inherited(arguments);
if(this._started){
this._layoutChildren();
}
// Clean up whatever style changes we made to the child pane.
// Unclear how height and width should be handled.
dojo.removeClass(child.domNode, this.baseClass+"Pane");
dojo.style(child.domNode, {
top: "auto",
bottom: "auto",
left: "auto",
right: "auto",
position: "static"
});
dojo.style(child.domNode, region == "top" || region == "bottom" ? "width" : "height", "auto");
},
getChildren: function(){
// Override _LayoutWidget.getChildren() to only return real children, not the splitters.
return dojo.filter(this.inherited(arguments), function(widget){
return !widget.isSplitter;
});
},
// TODO: remove in 2.0
getSplitter: function(/*String*/region){
// summary:
// Returns the widget responsible for rendering the splitter associated with region
// tags:
// deprecated
return dojo.filter(this.getChildren(), function(child){
return child.region == region;
})[0]._splitterWidget;
},
resize: function(newSize, currentSize){
// Overrides _LayoutWidget.resize().
// resetting potential padding to 0px to provide support for 100% width/height + padding
// TODO: this hack doesn't respect the box model and is a temporary fix
if(!this.cs || !this.pe){
var node = this.domNode;
this.cs = dojo.getComputedStyle(node);
this.pe = dojo._getPadExtents(node, this.cs);
this.pe.r = dojo._toPixelValue(node, this.cs.paddingRight);
this.pe.b = dojo._toPixelValue(node, this.cs.paddingBottom);
dojo.style(node, "padding", "0px");
}
this.inherited(arguments);
},
_layoutChildren: function(/*String?*/ changedChildId, /*Number?*/ changedChildSize){
// summary:
// This is the main routine for setting size/position of each child.
// description:
// With no arguments, measures the height of top/bottom panes, the width
// of left/right panes, and then sizes all panes accordingly.
//
// With changedRegion specified (as "left", "top", "bottom", or "right"),
// it changes that region's width/height to changedRegionSize and
// then resizes other regions that were affected.
// changedChildId:
// Id of the child which should be resized because splitter was dragged.
// changedChildSize:
// The new width/height (in pixels) to make specified child
if(!this._borderBox || !this._borderBox.h){
// We are currently hidden, or we haven't been sized by our parent yet.
// Abort. Someone will resize us later.
return;
}
// Generate list of wrappers of my children in the order that I want layoutChildren()
// to process them (i.e. from the outside to the inside)
var wrappers = dojo.map(this.getChildren(), function(child, idx){
return {
pane: child,
weight: [
child.region == "center" ? Infinity : 0,
child.layoutPriority,
(this.design == "sidebar" ? 1 : -1) * (/top|bottom/.test(child.region) ? 1 : -1),
idx
]
};
}, this);
wrappers.sort(function(a, b){
var aw = a.weight, bw = b.weight;
for(var i=0; i
',
postMixInProperties: function(){
this.inherited(arguments);
this.horizontal = /top|bottom/.test(this.region);
this._factor = /top|left/.test(this.region) ? 1 : -1;
this._cookieName = this.container.id + "_" + this.region;
},
buildRendering: function(){
this.inherited(arguments);
dojo.addClass(this.domNode, "dijitSplitter" + (this.horizontal ? "H" : "V"));
if(this.container.persist){
// restore old size
var persistSize = dojo.cookie(this._cookieName);
if(persistSize){
this.child.domNode.style[this.horizontal ? "height" : "width"] = persistSize;
}
}
},
_computeMaxSize: function(){
// summary:
// Return the maximum size that my corresponding pane can be set to
var dim = this.horizontal ? 'h' : 'w',
childSize = dojo.marginBox(this.child.domNode)[dim],
center = dojo.filter(this.container.getChildren(), function(child){ return child.region == "center";})[0],
spaceAvailable = dojo.marginBox(center.domNode)[dim]; // can expand until center is crushed to 0
return Math.min(this.child.maxSize, childSize + spaceAvailable);
},
_startDrag: function(e){
if(!this.cover){
this.cover = dojo.doc.createElement('div');
dojo.addClass(this.cover, "dijitSplitterCover");
dojo.place(this.cover, this.child.domNode, "after");
}
dojo.addClass(this.cover, "dijitSplitterCoverActive");
// Safeguard in case the stop event was missed. Shouldn't be necessary if we always get the mouse up.
if(this.fake){ dojo.destroy(this.fake); }
if(!(this._resize = this.live)){ //TODO: disable live for IE6?
// create fake splitter to display at old position while we drag
(this.fake = this.domNode.cloneNode(true)).removeAttribute("id");
dojo.addClass(this.domNode, "dijitSplitterShadow");
dojo.place(this.fake, this.domNode, "after");
}
dojo.addClass(this.domNode, "dijitSplitterActive dijitSplitter" + (this.horizontal ? "H" : "V") + "Active");
if(this.fake){
dojo.removeClass(this.fake, "dijitSplitterHover dijitSplitter" + (this.horizontal ? "H" : "V") + "Hover");
}
//Performance: load data info local vars for onmousevent function closure
var factor = this._factor,
isHorizontal = this.horizontal,
axis = isHorizontal ? "pageY" : "pageX",
pageStart = e[axis],
splitterStyle = this.domNode.style,
dim = isHorizontal ? 'h' : 'w',
childStart = dojo.marginBox(this.child.domNode)[dim],
max = this._computeMaxSize(),
min = this.child.minSize || 20,
region = this.region,
splitterAttr = region == "top" || region == "bottom" ? "top" : "left", // style attribute of splitter to adjust
splitterStart = parseInt(splitterStyle[splitterAttr], 10),
resize = this._resize,
layoutFunc = dojo.hitch(this.container, "_layoutChildren", this.child.id),
de = dojo.doc;
this._handlers = (this._handlers || []).concat([
dojo.connect(de, "onmousemove", this._drag = function(e, forceResize){
var delta = e[axis] - pageStart,
childSize = factor * delta + childStart,
boundChildSize = Math.max(Math.min(childSize, max), min);
if(resize || forceResize){
layoutFunc(boundChildSize);
}
// TODO: setting style directly (usually) sets content box size, need to set margin box size
splitterStyle[splitterAttr] = delta + splitterStart + factor*(boundChildSize - childSize) + "px";
}),
dojo.connect(de, "ondragstart", dojo.stopEvent),
dojo.connect(dojo.body(), "onselectstart", dojo.stopEvent),
dojo.connect(de, "onmouseup", this, "_stopDrag")
]);
dojo.stopEvent(e);
},
_onMouse: function(e){
var o = (e.type == "mouseover" || e.type == "mouseenter");
dojo.toggleClass(this.domNode, "dijitSplitterHover", o);
dojo.toggleClass(this.domNode, "dijitSplitter" + (this.horizontal ? "H" : "V") + "Hover", o);
},
_stopDrag: function(e){
try{
if(this.cover){
dojo.removeClass(this.cover, "dijitSplitterCoverActive");
}
if(this.fake){ dojo.destroy(this.fake); }
dojo.removeClass(this.domNode, "dijitSplitterActive dijitSplitter"
+ (this.horizontal ? "H" : "V") + "Active dijitSplitterShadow");
this._drag(e); //TODO: redundant with onmousemove?
this._drag(e, true);
}finally{
this._cleanupHandlers();
delete this._drag;
}
if(this.container.persist){
dojo.cookie(this._cookieName, this.child.domNode.style[this.horizontal ? "height" : "width"], {expires:365});
}
},
_cleanupHandlers: function(){
dojo.forEach(this._handlers, dojo.disconnect);
delete this._handlers;
},
_onKeyPress: function(/*Event*/ e){
// should we apply typematic to this?
this._resize = true;
var horizontal = this.horizontal;
var tick = 1;
var dk = dojo.keys;
switch(e.charOrCode){
case horizontal ? dk.UP_ARROW : dk.LEFT_ARROW:
tick *= -1;
// break;
case horizontal ? dk.DOWN_ARROW : dk.RIGHT_ARROW:
break;
default:
// this.inherited(arguments);
return;
}
var childSize = dojo._getMarginSize(this.child.domNode)[ horizontal ? 'h' : 'w' ] + this._factor * tick;
this.container._layoutChildren(this.child.id, Math.max(Math.min(childSize, this._computeMaxSize()), this.child.minSize));
dojo.stopEvent(e);
},
destroy: function(){
this._cleanupHandlers();
delete this.child;
delete this.container;
delete this.cover;
delete this.fake;
this.inherited(arguments);
}
});
dojo.declare("dijit.layout._Gutter", [dijit._Widget, dijit._Templated],
{
// summary:
// Just a spacer div to separate side pane from center pane.
// Basically a trick to lookup the gutter/splitter width from the theme.
// description:
// Instantiated by `dijit.layout.BorderContainer`. Users should not
// create directly.
// tags:
// private
templateString: '
',
postMixInProperties: function(){
this.inherited(arguments);
this.horizontal = /top|bottom/.test(this.region);
},
buildRendering: function(){
this.inherited(arguments);
dojo.addClass(this.domNode, "dijitGutter" + (this.horizontal ? "H" : "V"));
}
});
}
if(!dojo._hasResource["dijit.layout._TabContainerBase"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dijit.layout._TabContainerBase"] = true;
dojo.provide("dijit.layout._TabContainerBase");
dojo.declare("dijit.layout._TabContainerBase",
[dijit.layout.StackContainer, dijit._Templated],
{
// summary:
// Abstract base class for TabContainer. Must define _makeController() to instantiate
// and return the widget that displays the tab labels
// description:
// A TabContainer is a container that has multiple panes, but shows only
// one pane at a time. There are a set of tabs corresponding to each pane,
// where each tab has the name (aka title) of the pane, and optionally a close button.
// tabPosition: String
// Defines where tabs go relative to tab content.
// "top", "bottom", "left-h", "right-h"
tabPosition: "top",
baseClass: "dijitTabContainer",
// tabStrip: [const] Boolean
// Defines whether the tablist gets an extra class for layouting, putting a border/shading
// around the set of tabs. Not supported by claro theme.
tabStrip: false,
// nested: [const] Boolean
// If true, use styling for a TabContainer nested inside another TabContainer.
// For tundra etc., makes tabs look like links, and hides the outer
// border since the outer TabContainer already has a border.
nested: false,
templateString: dojo.cache("dijit.layout", "templates/TabContainer.html", "
\n"),
postMixInProperties: function(){
// set class name according to tab position, ex: dijitTabContainerTop
this.baseClass += this.tabPosition.charAt(0).toUpperCase() + this.tabPosition.substr(1).replace(/-.*/, "");
this.srcNodeRef && dojo.style(this.srcNodeRef, "visibility", "hidden");
this.inherited(arguments);
},
buildRendering: function(){
this.inherited(arguments);
// Create the tab list that will have a tab (a.k.a. tab button) for each tab panel
this.tablist = this._makeController(this.tablistNode);
if(!this.doLayout){ dojo.addClass(this.domNode, "dijitTabContainerNoLayout"); }
if(this.nested){
/* workaround IE's lack of support for "a > b" selectors by
* tagging each node in the template.
*/
dojo.addClass(this.domNode, "dijitTabContainerNested");
dojo.addClass(this.tablist.containerNode, "dijitTabContainerTabListNested");
dojo.addClass(this.tablistSpacer, "dijitTabContainerSpacerNested");
dojo.addClass(this.containerNode, "dijitTabPaneWrapperNested");
}else{
dojo.addClass(this.domNode, "tabStrip-" + (this.tabStrip ? "enabled" : "disabled"));
}
},
_setupChild: function(/*dijit._Widget*/ tab){
// Overrides StackContainer._setupChild().
dojo.addClass(tab.domNode, "dijitTabPane");
this.inherited(arguments);
},
startup: function(){
if(this._started){ return; }
// wire up the tablist and its tabs
this.tablist.startup();
this.inherited(arguments);
},
layout: function(){
// Overrides StackContainer.layout().
// Configure the content pane to take up all the space except for where the tabs are
if(!this._contentBox || typeof(this._contentBox.l) == "undefined"){return;}
var sc = this.selectedChildWidget;
if(this.doLayout){
// position and size the titles and the container node
var titleAlign = this.tabPosition.replace(/-h/, "");
this.tablist.layoutAlign = titleAlign;
var children = [this.tablist, {
domNode: this.tablistSpacer,
layoutAlign: titleAlign
}, {
domNode: this.containerNode,
layoutAlign: "client"
}];
dijit.layout.layoutChildren(this.domNode, this._contentBox, children);
// Compute size to make each of my children.
// children[2] is the margin-box size of this.containerNode, set by layoutChildren() call above
this._containerContentBox = dijit.layout.marginBox2contentBox(this.containerNode, children[2]);
if(sc && sc.resize){
sc.resize(this._containerContentBox);
}
}else{
// just layout the tab controller, so it can position left/right buttons etc.
if(this.tablist.resize){
//make the tabs zero width so that they don't interfere with width calc, then reset
var s = this.tablist.domNode.style;
s.width="0";
var width = dojo.contentBox(this.domNode).w;
s.width="";
this.tablist.resize({w: width});
}
// and call resize() on the selected pane just to tell it that it's been made visible
if(sc && sc.resize){
sc.resize();
}
}
},
destroy: function(){
if(this.tablist){
this.tablist.destroy();
}
this.inherited(arguments);
}
});
}
if(!dojo._hasResource["dijit.layout.TabController"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dijit.layout.TabController"] = true;
dojo.provide("dijit.layout.TabController");
// Menu is used for an accessible close button, would be nice to have a lighter-weight solution
dojo.declare("dijit.layout.TabController",
dijit.layout.StackController,
{
// summary:
// Set of tabs (the things with titles and a close button, that you click to show a tab panel).
// Used internally by `dijit.layout.TabContainer`.
// description:
// Lets the user select the currently shown pane in a TabContainer or StackContainer.
// TabController also monitors the TabContainer, and whenever a pane is
// added or deleted updates itself accordingly.
// tags:
// private
templateString: "
",
// tabPosition: String
// Defines where tabs go relative to the content.
// "top", "bottom", "left-h", "right-h"
tabPosition: "top",
// buttonWidget: String
// The name of the tab widget to create to correspond to each page
buttonWidget: "dijit.layout._TabButton",
_rectifyRtlTabList: function(){
// summary:
// For left/right TabContainer when page is RTL mode, rectify the width of all tabs to be equal, otherwise the tab widths are different in IE
if(0 >= this.tabPosition.indexOf('-h')){ return; }
if(!this.pane2button){ return; }
var maxWidth = 0;
for(var pane in this.pane2button){
var ow = this.pane2button[pane].innerDiv.scrollWidth;
maxWidth = Math.max(maxWidth, ow);
}
//unify the length of all the tabs
for(pane in this.pane2button){
this.pane2button[pane].innerDiv.style.width = maxWidth + 'px';
}
}
});
dojo.declare("dijit.layout._TabButton",
dijit.layout._StackButton,
{
// summary:
// A tab (the thing you click to select a pane).
// description:
// Contains the title of the pane, and optionally a close-button to destroy the pane.
// This is an internal widget and should not be instantiated directly.
// tags:
// private
// baseClass: String
// The CSS class applied to the domNode.
baseClass: "dijitTab",
// Apply dijitTabCloseButtonHover when close button is hovered
cssStateNodes: {
closeNode: "dijitTabCloseButton"
},
templateString: dojo.cache("dijit.layout", "templates/_TabButton.html", "
\n
\n
\n \t
\n\t\t
\n\t\t
\n\t\t
\n\t\t [x]\n\t\t\t
\n
\n
\n
\n"),
// Override _FormWidget.scrollOnFocus.
// Don't scroll the whole tab container into view when the button is focused.
scrollOnFocus: false,
buildRendering: function(){
this.inherited(arguments);
dojo.setSelectable(this.containerNode, false);
},
startup: function(){
this.inherited(arguments);
var n = this.domNode;
// Required to give IE6 a kick, as it initially hides the
// tabs until they are focused on.
setTimeout(function(){
n.className = n.className;
}, 1);
},
_setCloseButtonAttr: function(/*Boolean*/ disp){
// summary:
// Hide/show close button
this._set("closeButton", disp);
dojo.toggleClass(this.innerDiv, "dijitClosable", disp);
this.closeNode.style.display = disp ? "" : "none";
if(disp){
var _nlsResources = dojo.i18n.getLocalization("dijit", "common");
if(this.closeNode){
dojo.attr(this.closeNode,"title", _nlsResources.itemClose);
}
// add context menu onto title button
var _nlsResources = dojo.i18n.getLocalization("dijit", "common");
this._closeMenu = new dijit.Menu({
id: this.id+"_Menu",
dir: this.dir,
lang: this.lang,
targetNodeIds: [this.domNode]
});
this._closeMenu.addChild(new dijit.MenuItem({
label: _nlsResources.itemClose,
dir: this.dir,
lang: this.lang,
onClick: dojo.hitch(this, "onClickCloseButton")
}));
}else{
if(this._closeMenu){
this._closeMenu.destroyRecursive();
delete this._closeMenu;
}
}
},
_setLabelAttr: function(/*String*/ content){
// summary:
// Hook for set('label', ...) to work.
// description:
// takes an HTML string.
// Inherited ToggleButton implementation will Set the label (text) of the button;
// Need to set the alt attribute of icon on tab buttons if no label displayed
this.inherited(arguments);
if(this.showLabel == false && !this.params.title){
this.iconNode.alt = dojo.trim(this.containerNode.innerText || this.containerNode.textContent || '');
}
},
destroy: function(){
if(this._closeMenu){
this._closeMenu.destroyRecursive();
delete this._closeMenu;
}
this.inherited(arguments);
}
});
}
if(!dojo._hasResource["dijit.layout.ScrollingTabController"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dijit.layout.ScrollingTabController"] = true;
dojo.provide("dijit.layout.ScrollingTabController");
dojo.declare("dijit.layout.ScrollingTabController",
dijit.layout.TabController,
{
// summary:
// Set of tabs with left/right arrow keys and a menu to switch between tabs not
// all fitting on a single row.
// Works only for horizontal tabs (either above or below the content, not to the left
// or right).
// tags:
// private
templateString: dojo.cache("dijit.layout", "templates/ScrollingTabController.html", "
\n"),
// useMenu: [const] Boolean
// True if a menu should be used to select tabs when they are too
// wide to fit the TabContainer, false otherwise.
useMenu: true,
// useSlider: [const] Boolean
// True if a slider should be used to select tabs when they are too
// wide to fit the TabContainer, false otherwise.
useSlider: true,
// tabStripClass: [const] String
// The css class to apply to the tab strip, if it is visible.
tabStripClass: "",
widgetsInTemplate: true,
// _minScroll: Number
// The distance in pixels from the edge of the tab strip which,
// if a scroll animation is less than, forces the scroll to
// go all the way to the left/right.
_minScroll: 5,
attributeMap: dojo.delegate(dijit._Widget.prototype.attributeMap, {
"class": "containerNode"
}),
buildRendering: function(){
this.inherited(arguments);
var n = this.domNode;
this.scrollNode = this.tablistWrapper;
this._initButtons();
if(!this.tabStripClass){
this.tabStripClass = "dijitTabContainer" +
this.tabPosition.charAt(0).toUpperCase() +
this.tabPosition.substr(1).replace(/-.*/, "") +
"None";
dojo.addClass(n, "tabStrip-disabled")
}
dojo.addClass(this.tablistWrapper, this.tabStripClass);
},
onStartup: function(){
this.inherited(arguments);
// Do not show the TabController until the related
// StackController has added it's children. This gives
// a less visually jumpy instantiation.
dojo.style(this.domNode, "visibility", "visible");
this._postStartup = true;
},
onAddChild: function(page, insertIndex){
this.inherited(arguments);
// changes to the tab button label or iconClass will have changed the width of the
// buttons, so do a resize
dojo.forEach(["label", "iconClass"], function(attr){
this.pane2watches[page.id].push(
this.pane2button[page.id].watch(attr, dojo.hitch(this, function(name, oldValue, newValue){
if(this._postStartup && this._dim){
this.resize(this._dim);
}
}))
);
}, this);
// Increment the width of the wrapper when a tab is added
// This makes sure that the buttons never wrap.
// The value 200 is chosen as it should be bigger than most
// Tab button widths.
dojo.style(this.containerNode, "width",
(dojo.style(this.containerNode, "width") + 200) + "px");
},
onRemoveChild: function(page, insertIndex){
// null out _selectedTab because we are about to delete that dom node
var button = this.pane2button[page.id];
if(this._selectedTab === button.domNode){
this._selectedTab = null;
}
this.inherited(arguments);
},
_initButtons: function(){
// summary:
// Creates the buttons used to scroll to view tabs that
// may not be visible if the TabContainer is too narrow.
// Make a list of the buttons to display when the tab labels become
// wider than the TabContainer, and hide the other buttons.
// Also gets the total width of the displayed buttons.
this._btnWidth = 0;
this._buttons = dojo.query("> .tabStripButton", this.domNode).filter(function(btn){
if((this.useMenu && btn == this._menuBtn.domNode) ||
(this.useSlider && (btn == this._rightBtn.domNode || btn == this._leftBtn.domNode))){
this._btnWidth += dojo._getMarginSize(btn).w;
return true;
}else{
dojo.style(btn, "display", "none");
return false;
}
}, this);
},
_getTabsWidth: function(){
var children = this.getChildren();
if(children.length){
var leftTab = children[this.isLeftToRight() ? 0 : children.length - 1].domNode,
rightTab = children[this.isLeftToRight() ? children.length - 1 : 0].domNode;
return rightTab.offsetLeft + dojo.style(rightTab, "width") - leftTab.offsetLeft;
}else{
return 0;
}
},
_enableBtn: function(width){
// summary:
// Determines if the tabs are wider than the width of the TabContainer, and
// thus that we need to display left/right/menu navigation buttons.
var tabsWidth = this._getTabsWidth();
width = width || dojo.style(this.scrollNode, "width");
return tabsWidth > 0 && width < tabsWidth;
},
resize: function(dim){
// summary:
// Hides or displays the buttons used to scroll the tab list and launch the menu
// that selects tabs.
if(this.domNode.offsetWidth == 0){
return;
}
// Save the dimensions to be used when a child is renamed.
this._dim = dim;
// Set my height to be my natural height (tall enough for one row of tab labels),
// and my content-box width based on margin-box width specified in dim parameter.
// But first reset scrollNode.height in case it was set by layoutChildren() call
// in a previous run of this method.
this.scrollNode.style.height = "auto";
this._contentBox = dijit.layout.marginBox2contentBox(this.domNode, {h: 0, w: dim.w});
this._contentBox.h = this.scrollNode.offsetHeight;
dojo.contentBox(this.domNode, this._contentBox);
// Show/hide the left/right/menu navigation buttons depending on whether or not they
// are needed.
var enable = this._enableBtn(this._contentBox.w);
this._buttons.style("display", enable ? "" : "none");
// Position and size the navigation buttons and the tablist
this._leftBtn.layoutAlign = "left";
this._rightBtn.layoutAlign = "right";
this._menuBtn.layoutAlign = this.isLeftToRight() ? "right" : "left";
dijit.layout.layoutChildren(this.domNode, this._contentBox,
[this._menuBtn, this._leftBtn, this._rightBtn, {domNode: this.scrollNode, layoutAlign: "client"}]);
// set proper scroll so that selected tab is visible
if(this._selectedTab){
if(this._anim && this._anim.status() == "playing"){
this._anim.stop();
}
var w = this.scrollNode,
sl = this._convertToScrollLeft(this._getScrollForSelectedTab());
w.scrollLeft = sl;
}
// Enable/disabled left right buttons depending on whether or not user can scroll to left or right
this._setButtonClass(this._getScroll());
this._postResize = true;
// Return my size so layoutChildren() can use it.
// Also avoids IE9 layout glitch on browser resize when scroll buttons present
return {h: this._contentBox.h, w: dim.w};
},
_getScroll: function(){
// summary:
// Returns the current scroll of the tabs where 0 means
// "scrolled all the way to the left" and some positive number, based on #
// of pixels of possible scroll (ex: 1000) means "scrolled all the way to the right"
var sl = (this.isLeftToRight() || dojo.isIE < 8 || (dojo.isIE && dojo.isQuirks) || dojo.isWebKit) ? this.scrollNode.scrollLeft :
dojo.style(this.containerNode, "width") - dojo.style(this.scrollNode, "width")
+ (dojo.isIE == 8 ? -1 : 1) * this.scrollNode.scrollLeft;
return sl;
},
_convertToScrollLeft: function(val){
// summary:
// Given a scroll value where 0 means "scrolled all the way to the left"
// and some positive number, based on # of pixels of possible scroll (ex: 1000)
// means "scrolled all the way to the right", return value to set this.scrollNode.scrollLeft
// to achieve that scroll.
//
// This method is to adjust for RTL funniness in various browsers and versions.
if(this.isLeftToRight() || dojo.isIE < 8 || (dojo.isIE && dojo.isQuirks) || dojo.isWebKit){
return val;
}else{
var maxScroll = dojo.style(this.containerNode, "width") - dojo.style(this.scrollNode, "width");
return (dojo.isIE == 8 ? -1 : 1) * (val - maxScroll);
}
},
onSelectChild: function(/*dijit._Widget*/ page){
// summary:
// Smoothly scrolls to a tab when it is selected.
var tab = this.pane2button[page.id];
if(!tab || !page){return;}
// Scroll to the selected tab, except on startup, when scrolling is handled in resize()
var node = tab.domNode;
if(this._postResize && node != this._selectedTab){
this._selectedTab = node;
var sl = this._getScroll();
if(sl > node.offsetLeft ||
sl + dojo.style(this.scrollNode, "width") <
node.offsetLeft + dojo.style(node, "width")){
this.createSmoothScroll().play();
}
}
this.inherited(arguments);
},
_getScrollBounds: function(){
// summary:
// Returns the minimum and maximum scroll setting to show the leftmost and rightmost
// tabs (respectively)
var children = this.getChildren(),
scrollNodeWidth = dojo.style(this.scrollNode, "width"), // about 500px
containerWidth = dojo.style(this.containerNode, "width"), // 50,000px
maxPossibleScroll = containerWidth - scrollNodeWidth, // scrolling until right edge of containerNode visible
tabsWidth = this._getTabsWidth();
if(children.length && tabsWidth > scrollNodeWidth){
// Scrolling should happen
return {
min: this.isLeftToRight() ? 0 : children[children.length-1].domNode.offsetLeft,
max: this.isLeftToRight() ?
(children[children.length-1].domNode.offsetLeft + dojo.style(children[children.length-1].domNode, "width")) - scrollNodeWidth :
maxPossibleScroll
};
}else{
// No scrolling needed, all tabs visible, we stay either scrolled to far left or far right (depending on dir)
var onlyScrollPosition = this.isLeftToRight() ? 0 : maxPossibleScroll;
return {
min: onlyScrollPosition,
max: onlyScrollPosition
};
}
},
_getScrollForSelectedTab: function(){
// summary:
// Returns the scroll value setting so that the selected tab
// will appear in the center
var w = this.scrollNode,
n = this._selectedTab,
scrollNodeWidth = dojo.style(this.scrollNode, "width"),
scrollBounds = this._getScrollBounds();
// TODO: scroll minimal amount (to either right or left) so that
// selected tab is fully visible, and just return if it's already visible?
var pos = (n.offsetLeft + dojo.style(n, "width")/2) - scrollNodeWidth/2;
pos = Math.min(Math.max(pos, scrollBounds.min), scrollBounds.max);
// TODO:
// If scrolling close to the left side or right side, scroll
// all the way to the left or right. See this._minScroll.
// (But need to make sure that doesn't scroll the tab out of view...)
return pos;
},
createSmoothScroll: function(x){
// summary:
// Creates a dojo._Animation object that smoothly scrolls the tab list
// either to a fixed horizontal pixel value, or to the selected tab.
// description:
// If an number argument is passed to the function, that horizontal
// pixel position is scrolled to. Otherwise the currently selected
// tab is scrolled to.
// x: Integer?
// An optional pixel value to scroll to, indicating distance from left.
// Calculate position to scroll to
if(arguments.length > 0){
// position specified by caller, just make sure it's within bounds
var scrollBounds = this._getScrollBounds();
x = Math.min(Math.max(x, scrollBounds.min), scrollBounds.max);
}else{
// scroll to center the current tab
x = this._getScrollForSelectedTab();
}
if(this._anim && this._anim.status() == "playing"){
this._anim.stop();
}
var self = this,
w = this.scrollNode,
anim = new dojo._Animation({
beforeBegin: function(){
if(this.curve){ delete this.curve; }
var oldS = w.scrollLeft,
newS = self._convertToScrollLeft(x);
anim.curve = new dojo._Line(oldS, newS);
},
onAnimate: function(val){
w.scrollLeft = val;
}
});
this._anim = anim;
// Disable/enable left/right buttons according to new scroll position
this._setButtonClass(x);
return anim; // dojo._Animation
},
_getBtnNode: function(/*Event*/ e){
// summary:
// Gets a button DOM node from a mouse click event.
// e:
// The mouse click event.
var n = e.target;
while(n && !dojo.hasClass(n, "tabStripButton")){
n = n.parentNode;
}
return n;
},
doSlideRight: function(/*Event*/ e){
// summary:
// Scrolls the menu to the right.
// e:
// The mouse click event.
this.doSlide(1, this._getBtnNode(e));
},
doSlideLeft: function(/*Event*/ e){
// summary:
// Scrolls the menu to the left.
// e:
// The mouse click event.
this.doSlide(-1,this._getBtnNode(e));
},
doSlide: function(/*Number*/ direction, /*DomNode*/ node){
// summary:
// Scrolls the tab list to the left or right by 75% of the widget width.
// direction:
// If the direction is 1, the widget scrolls to the right, if it is
// -1, it scrolls to the left.
if(node && dojo.hasClass(node, "dijitTabDisabled")){return;}
var sWidth = dojo.style(this.scrollNode, "width");
var d = (sWidth * 0.75) * direction;
var to = this._getScroll() + d;
this._setButtonClass(to);
this.createSmoothScroll(to).play();
},
_setButtonClass: function(/*Number*/ scroll){
// summary:
// Disables the left scroll button if the tabs are scrolled all the way to the left,
// or the right scroll button in the opposite case.
// scroll: Integer
// amount of horizontal scroll
var scrollBounds = this._getScrollBounds();
this._leftBtn.set("disabled", scroll <= scrollBounds.min);
this._rightBtn.set("disabled", scroll >= scrollBounds.max);
}
});
dojo.declare("dijit.layout._ScrollingTabControllerButtonMixin", null, {
baseClass: "dijitTab tabStripButton",
templateString: dojo.cache("dijit.layout", "templates/_ScrollingTabControllerButton.html", "
\n\t
\n\t\t
\n\t\t\t
\n\t\t\t
\n\t\t
\n\t
\n
\n"),
// Override inherited tabIndex: 0 from dijit.form.Button, because user shouldn't be
// able to tab to the left/right/menu buttons
tabIndex: "",
// Similarly, override FormWidget.isFocusable() because clicking a button shouldn't focus it
// either (this override avoids focus() call in FormWidget.js)
isFocusable: function(){ return false; }
});
dojo.declare("dijit.layout._ScrollingTabControllerButton",
[dijit.form.Button, dijit.layout._ScrollingTabControllerButtonMixin]);
dojo.declare(
"dijit.layout._ScrollingTabControllerMenuButton",
[dijit.form.Button, dijit._HasDropDown, dijit.layout._ScrollingTabControllerButtonMixin],
{
// id of the TabContainer itself
containerId: "",
// -1 so user can't tab into the button, but so that button can still be focused programatically.
// Because need to move focus to the button (or somewhere) before the menu is hidden or IE6 will crash.
tabIndex: "-1",
isLoaded: function(){
// recreate menu every time, in case the TabContainer's list of children (or their icons/labels) have changed
return false;
},
loadDropDown: function(callback){
this.dropDown = new dijit.Menu({
id: this.containerId + "_menu",
dir: this.dir,
lang: this.lang
});
var container = dijit.byId(this.containerId);
dojo.forEach(container.getChildren(), function(page){
var menuItem = new dijit.MenuItem({
id: page.id + "_stcMi",
label: page.title,
iconClass: page.iconClass,
dir: page.dir,
lang: page.lang,
onClick: function(){
container.selectChild(page);
}
});
this.dropDown.addChild(menuItem);
}, this);
callback();
},
closeDropDown: function(/*Boolean*/ focus){
this.inherited(arguments);
if(this.dropDown){
this.dropDown.destroyRecursive();
delete this.dropDown;
}
}
});
}
if(!dojo._hasResource["dijit.layout.TabContainer"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dijit.layout.TabContainer"] = true;
dojo.provide("dijit.layout.TabContainer");
dojo.declare("dijit.layout.TabContainer",
dijit.layout._TabContainerBase,
{
// summary:
// A Container with tabs to select each child (only one of which is displayed at a time).
// description:
// A TabContainer is a container that has multiple panes, but shows only
// one pane at a time. There are a set of tabs corresponding to each pane,
// where each tab has the name (aka title) of the pane, and optionally a close button.
// useMenu: [const] Boolean
// True if a menu should be used to select tabs when they are too
// wide to fit the TabContainer, false otherwise.
useMenu: true,
// useSlider: [const] Boolean
// True if a slider should be used to select tabs when they are too
// wide to fit the TabContainer, false otherwise.
useSlider: true,
// controllerWidget: String
// An optional parameter to override the widget used to display the tab labels
controllerWidget: "",
_makeController: function(/*DomNode*/ srcNode){
// summary:
// Instantiate tablist controller widget and return reference to it.
// Callback from _TabContainerBase.postCreate().
// tags:
// protected extension
var cls = this.baseClass + "-tabs" + (this.doLayout ? "" : " dijitTabNoLayout"),
TabController = dojo.getObject(this.controllerWidget);
return new TabController({
id: this.id + "_tablist",
dir: this.dir,
lang: this.lang,
tabPosition: this.tabPosition,
doLayout: this.doLayout,
containerId: this.id,
"class": cls,
nested: this.nested,
useMenu: this.useMenu,
useSlider: this.useSlider,
tabStripClass: this.tabStrip ? this.baseClass + (this.tabStrip ? "":"No") + "Strip": null
}, srcNode);
},
postMixInProperties: function(){
this.inherited(arguments);
// Scrolling controller only works for horizontal non-nested tabs
if(!this.controllerWidget){
this.controllerWidget = (this.tabPosition == "top" || this.tabPosition == "bottom") && !this.nested ?
"dijit.layout.ScrollingTabController" : "dijit.layout.TabController";
}
}
});
}
if(!dojo._hasResource["dojo.number"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dojo.number"] = true;
dojo.provide("dojo.number");
dojo.getObject("number", true, dojo);
/*=====
dojo.number = {
// summary: localized formatting and parsing routines for Number
}
dojo.number.__FormatOptions = function(){
// pattern: String?
// override [formatting pattern](http://www.unicode.org/reports/tr35/#Number_Format_Patterns)
// with this string. Default value is based on locale. Overriding this property will defeat
// localization. Literal characters in patterns are not supported.
// type: String?
// choose a format type based on the locale from the following:
// decimal, scientific (not yet supported), percent, currency. decimal by default.
// places: Number?
// fixed number of decimal places to show. This overrides any
// information in the provided pattern.
// round: Number?
// 5 rounds to nearest .5; 0 rounds to nearest whole (default). -1
// means do not round.
// locale: String?
// override the locale used to determine formatting rules
// fractional: Boolean?
// If false, show no decimal places, overriding places and pattern settings.
this.pattern = pattern;
this.type = type;
this.places = places;
this.round = round;
this.locale = locale;
this.fractional = fractional;
}
=====*/
dojo.number.format = function(/*Number*/value, /*dojo.number.__FormatOptions?*/options){
// summary:
// Format a Number as a String, using locale-specific settings
// description:
// Create a string from a Number using a known localized pattern.
// Formatting patterns appropriate to the locale are chosen from the
// [Common Locale Data Repository](http://unicode.org/cldr) as well as the appropriate symbols and
// delimiters.
// If value is Infinity, -Infinity, or is not a valid JavaScript number, return null.
// value:
// the number to be formatted
options = dojo.mixin({}, options || {});
var locale = dojo.i18n.normalizeLocale(options.locale),
bundle = dojo.i18n.getLocalization("dojo.cldr", "number", locale);
options.customs = bundle;
var pattern = options.pattern || bundle[(options.type || "decimal") + "Format"];
if(isNaN(value) || Math.abs(value) == Infinity){ return null; } // null
return dojo.number._applyPattern(value, pattern, options); // String
};
//dojo.number._numberPatternRE = /(?:[#0]*,?)*[#0](?:\.0*#*)?/; // not precise, but good enough
dojo.number._numberPatternRE = /[#0,]*[#0](?:\.0*#*)?/; // not precise, but good enough
dojo.number._applyPattern = function(/*Number*/value, /*String*/pattern, /*dojo.number.__FormatOptions?*/options){
// summary:
// Apply pattern to format value as a string using options. Gives no
// consideration to local customs.
// value:
// the number to be formatted.
// pattern:
// a pattern string as described by
// [unicode.org TR35](http://www.unicode.org/reports/tr35/#Number_Format_Patterns)
// options: dojo.number.__FormatOptions?
// _applyPattern is usually called via `dojo.number.format()` which
// populates an extra property in the options parameter, "customs".
// The customs object specifies group and decimal parameters if set.
//TODO: support escapes
options = options || {};
var group = options.customs.group,
decimal = options.customs.decimal,
patternList = pattern.split(';'),
positivePattern = patternList[0];
pattern = patternList[(value < 0) ? 1 : 0] || ("-" + positivePattern);
//TODO: only test against unescaped
if(pattern.indexOf('%') != -1){
value *= 100;
}else if(pattern.indexOf('\u2030') != -1){
value *= 1000; // per mille
}else if(pattern.indexOf('\u00a4') != -1){
group = options.customs.currencyGroup || group;//mixins instead?
decimal = options.customs.currencyDecimal || decimal;// Should these be mixins instead?
pattern = pattern.replace(/\u00a4{1,3}/, function(match){
var prop = ["symbol", "currency", "displayName"][match.length-1];
return options[prop] || options.currency || "";
});
}else if(pattern.indexOf('E') != -1){
throw new Error("exponential notation not supported");
}
//TODO: support @ sig figs?
var numberPatternRE = dojo.number._numberPatternRE;
var numberPattern = positivePattern.match(numberPatternRE);
if(!numberPattern){
throw new Error("unable to find a number expression in pattern: "+pattern);
}
if(options.fractional === false){ options.places = 0; }
return pattern.replace(numberPatternRE,
dojo.number._formatAbsolute(value, numberPattern[0], {decimal: decimal, group: group, places: options.places, round: options.round}));
};
dojo.number.round = function(/*Number*/value, /*Number?*/places, /*Number?*/increment){
// summary:
// Rounds to the nearest value with the given number of decimal places, away from zero
// description:
// Rounds to the nearest value with the given number of decimal places, away from zero if equal.
// Similar to Number.toFixed(), but compensates for browser quirks. Rounding can be done by
// fractional increments also, such as the nearest quarter.
// NOTE: Subject to floating point errors. See dojox.math.round for experimental workaround.
// value:
// The number to round
// places:
// The number of decimal places where rounding takes place. Defaults to 0 for whole rounding.
// Must be non-negative.
// increment:
// Rounds next place to nearest value of increment/10. 10 by default.
// example:
// >>> dojo.number.round(-0.5)
// -1
// >>> dojo.number.round(162.295, 2)
// 162.29 // note floating point error. Should be 162.3
// >>> dojo.number.round(10.71, 0, 2.5)
// 10.75
var factor = 10 / (increment || 10);
return (factor * +value).toFixed(places) / factor; // Number
};
if((0.9).toFixed() == 0){
// (isIE) toFixed() bug workaround: Rounding fails on IE when most significant digit
// is just after the rounding place and is >=5
(function(){
var round = dojo.number.round;
dojo.number.round = function(v, p, m){
var d = Math.pow(10, -p || 0), a = Math.abs(v);
if(!v || a >= d || a * Math.pow(10, p + 1) < 5){
d = 0;
}
return round(v, p, m) + (v > 0 ? d : -d);
};
})();
}
/*=====
dojo.number.__FormatAbsoluteOptions = function(){
// decimal: String?
// the decimal separator
// group: String?
// the group separator
// places: Number?|String?
// number of decimal places. the range "n,m" will format to m places.
// round: Number?
// 5 rounds to nearest .5; 0 rounds to nearest whole (default). -1
// means don't round.
this.decimal = decimal;
this.group = group;
this.places = places;
this.round = round;
}
=====*/
dojo.number._formatAbsolute = function(/*Number*/value, /*String*/pattern, /*dojo.number.__FormatAbsoluteOptions?*/options){
// summary:
// Apply numeric pattern to absolute value using options. Gives no
// consideration to local customs.
// value:
// the number to be formatted, ignores sign
// pattern:
// the number portion of a pattern (e.g. `#,##0.00`)
options = options || {};
if(options.places === true){options.places=0;}
if(options.places === Infinity){options.places=6;} // avoid a loop; pick a limit
var patternParts = pattern.split("."),
comma = typeof options.places == "string" && options.places.indexOf(","),
maxPlaces = options.places;
if(comma){
maxPlaces = options.places.substring(comma + 1);
}else if(!(maxPlaces >= 0)){
maxPlaces = (patternParts[1] || []).length;
}
if(!(options.round < 0)){
value = dojo.number.round(value, maxPlaces, options.round);
}
var valueParts = String(Math.abs(value)).split("."),
fractional = valueParts[1] || "";
if(patternParts[1] || options.places){
if(comma){
options.places = options.places.substring(0, comma);
}
// Pad fractional with trailing zeros
var pad = options.places !== undefined ? options.places : (patternParts[1] && patternParts[1].lastIndexOf("0") + 1);
if(pad > fractional.length){
valueParts[1] = dojo.string.pad(fractional, pad, '0', true);
}
// Truncate fractional
if(maxPlaces < fractional.length){
valueParts[1] = fractional.substr(0, maxPlaces);
}
}else{
if(valueParts[1]){ valueParts.pop(); }
}
// Pad whole with leading zeros
var patternDigits = patternParts[0].replace(',', '');
pad = patternDigits.indexOf("0");
if(pad != -1){
pad = patternDigits.length - pad;
if(pad > valueParts[0].length){
valueParts[0] = dojo.string.pad(valueParts[0], pad);
}
// Truncate whole
if(patternDigits.indexOf("#") == -1){
valueParts[0] = valueParts[0].substr(valueParts[0].length - pad);
}
}
// Add group separators
var index = patternParts[0].lastIndexOf(','),
groupSize, groupSize2;
if(index != -1){
groupSize = patternParts[0].length - index - 1;
var remainder = patternParts[0].substr(0, index);
index = remainder.lastIndexOf(',');
if(index != -1){
groupSize2 = remainder.length - index - 1;
}
}
var pieces = [];
for(var whole = valueParts[0]; whole;){
var off = whole.length - groupSize;
pieces.push((off > 0) ? whole.substr(off) : whole);
whole = (off > 0) ? whole.slice(0, off) : "";
if(groupSize2){
groupSize = groupSize2;
delete groupSize2;
}
}
valueParts[0] = pieces.reverse().join(options.group || ",");
return valueParts.join(options.decimal || ".");
};
/*=====
dojo.number.__RegexpOptions = function(){
// pattern: String?
// override [formatting pattern](http://www.unicode.org/reports/tr35/#Number_Format_Patterns)
// with this string. Default value is based on locale. Overriding this property will defeat
// localization.
// type: String?
// choose a format type based on the locale from the following:
// decimal, scientific (not yet supported), percent, currency. decimal by default.
// locale: String?
// override the locale used to determine formatting rules
// strict: Boolean?
// strict parsing, false by default. Strict parsing requires input as produced by the format() method.
// Non-strict is more permissive, e.g. flexible on white space, omitting thousands separators
// places: Number|String?
// number of decimal places to accept: Infinity, a positive number, or
// a range "n,m". Defined by pattern or Infinity if pattern not provided.
this.pattern = pattern;
this.type = type;
this.locale = locale;
this.strict = strict;
this.places = places;
}
=====*/
dojo.number.regexp = function(/*dojo.number.__RegexpOptions?*/options){
// summary:
// Builds the regular needed to parse a number
// description:
// Returns regular expression with positive and negative match, group
// and decimal separators
return dojo.number._parseInfo(options).regexp; // String
};
dojo.number._parseInfo = function(/*Object?*/options){
options = options || {};
var locale = dojo.i18n.normalizeLocale(options.locale),
bundle = dojo.i18n.getLocalization("dojo.cldr", "number", locale),
pattern = options.pattern || bundle[(options.type || "decimal") + "Format"],
//TODO: memoize?
group = bundle.group,
decimal = bundle.decimal,
factor = 1;
if(pattern.indexOf('%') != -1){
factor /= 100;
}else if(pattern.indexOf('\u2030') != -1){
factor /= 1000; // per mille
}else{
var isCurrency = pattern.indexOf('\u00a4') != -1;
if(isCurrency){
group = bundle.currencyGroup || group;
decimal = bundle.currencyDecimal || decimal;
}
}
//TODO: handle quoted escapes
var patternList = pattern.split(';');
if(patternList.length == 1){
patternList.push("-" + patternList[0]);
}
var re = dojo.regexp.buildGroupRE(patternList, function(pattern){
pattern = "(?:"+dojo.regexp.escapeString(pattern, '.')+")";
return pattern.replace(dojo.number._numberPatternRE, function(format){
var flags = {
signed: false,
separator: options.strict ? group : [group,""],
fractional: options.fractional,
decimal: decimal,
exponent: false
},
parts = format.split('.'),
places = options.places;
// special condition for percent (factor != 1)
// allow decimal places even if not specified in pattern
if(parts.length == 1 && factor != 1){
parts[1] = "###";
}
if(parts.length == 1 || places === 0){
flags.fractional = false;
}else{
if(places === undefined){ places = options.pattern ? parts[1].lastIndexOf('0') + 1 : Infinity; }
if(places && options.fractional == undefined){flags.fractional = true;} // required fractional, unless otherwise specified
if(!options.places && (places < parts[1].length)){ places += "," + parts[1].length; }
flags.places = places;
}
var groups = parts[0].split(',');
if(groups.length > 1){
flags.groupSize = groups.pop().length;
if(groups.length > 1){
flags.groupSize2 = groups.pop().length;
}
}
return "("+dojo.number._realNumberRegexp(flags)+")";
});
}, true);
if(isCurrency){
// substitute the currency symbol for the placeholder in the pattern
re = re.replace(/([\s\xa0]*)(\u00a4{1,3})([\s\xa0]*)/g, function(match, before, target, after){
var prop = ["symbol", "currency", "displayName"][target.length-1],
symbol = dojo.regexp.escapeString(options[prop] || options.currency || "");
before = before ? "[\\s\\xa0]" : "";
after = after ? "[\\s\\xa0]" : "";
if(!options.strict){
if(before){before += "*";}
if(after){after += "*";}
return "(?:"+before+symbol+after+")?";
}
return before+symbol+after;
});
}
//TODO: substitute localized sign/percent/permille/etc.?
// normalize whitespace and return
return {regexp: re.replace(/[\xa0 ]/g, "[\\s\\xa0]"), group: group, decimal: decimal, factor: factor}; // Object
};
/*=====
dojo.number.__ParseOptions = function(){
// pattern: String?
// override [formatting pattern](http://www.unicode.org/reports/tr35/#Number_Format_Patterns)
// with this string. Default value is based on locale. Overriding this property will defeat
// localization. Literal characters in patterns are not supported.
// type: String?
// choose a format type based on the locale from the following:
// decimal, scientific (not yet supported), percent, currency. decimal by default.
// locale: String?
// override the locale used to determine formatting rules
// strict: Boolean?
// strict parsing, false by default. Strict parsing requires input as produced by the format() method.
// Non-strict is more permissive, e.g. flexible on white space, omitting thousands separators
// fractional: Boolean?|Array?
// Whether to include the fractional portion, where the number of decimal places are implied by pattern
// or explicit 'places' parameter. The value [true,false] makes the fractional portion optional.
this.pattern = pattern;
this.type = type;
this.locale = locale;
this.strict = strict;
this.fractional = fractional;
}
=====*/
dojo.number.parse = function(/*String*/expression, /*dojo.number.__ParseOptions?*/options){
// summary:
// Convert a properly formatted string to a primitive Number, using
// locale-specific settings.
// description:
// Create a Number from a string using a known localized pattern.
// Formatting patterns are chosen appropriate to the locale
// and follow the syntax described by
// [unicode.org TR35](http://www.unicode.org/reports/tr35/#Number_Format_Patterns)
// Note that literal characters in patterns are not supported.
// expression:
// A string representation of a Number
var info = dojo.number._parseInfo(options),
results = (new RegExp("^"+info.regexp+"$")).exec(expression);
if(!results){
return NaN; //NaN
}
var absoluteMatch = results[1]; // match for the positive expression
if(!results[1]){
if(!results[2]){
return NaN; //NaN
}
// matched the negative pattern
absoluteMatch =results[2];
info.factor *= -1;
}
// Transform it to something Javascript can parse as a number. Normalize
// decimal point and strip out group separators or alternate forms of whitespace
absoluteMatch = absoluteMatch.
replace(new RegExp("["+info.group + "\\s\\xa0"+"]", "g"), "").
replace(info.decimal, ".");
// Adjust for negative sign, percent, etc. as necessary
return absoluteMatch * info.factor; //Number
};
/*=====
dojo.number.__RealNumberRegexpFlags = function(){
// places: Number?
// The integer number of decimal places or a range given as "n,m". If
// not given, the decimal part is optional and the number of places is
// unlimited.
// decimal: String?
// A string for the character used as the decimal point. Default
// is ".".
// fractional: Boolean?|Array?
// Whether decimal places are used. Can be true, false, or [true,
// false]. Default is [true, false] which means optional.
// exponent: Boolean?|Array?
// Express in exponential notation. Can be true, false, or [true,
// false]. Default is [true, false], (i.e. will match if the
// exponential part is present are not).
// eSigned: Boolean?|Array?
// The leading plus-or-minus sign on the exponent. Can be true,
// false, or [true, false]. Default is [true, false], (i.e. will
// match if it is signed or unsigned). flags in regexp.integer can be
// applied.
this.places = places;
this.decimal = decimal;
this.fractional = fractional;
this.exponent = exponent;
this.eSigned = eSigned;
}
=====*/
dojo.number._realNumberRegexp = function(/*dojo.number.__RealNumberRegexpFlags?*/flags){
// summary:
// Builds a regular expression to match a real number in exponential
// notation
// assign default values to missing parameters
flags = flags || {};
//TODO: use mixin instead?
if(!("places" in flags)){ flags.places = Infinity; }
if(typeof flags.decimal != "string"){ flags.decimal = "."; }
if(!("fractional" in flags) || /^0/.test(flags.places)){ flags.fractional = [true, false]; }
if(!("exponent" in flags)){ flags.exponent = [true, false]; }
if(!("eSigned" in flags)){ flags.eSigned = [true, false]; }
var integerRE = dojo.number._integerRegexp(flags),
decimalRE = dojo.regexp.buildGroupRE(flags.fractional,
function(q){
var re = "";
if(q && (flags.places!==0)){
re = "\\" + flags.decimal;
if(flags.places == Infinity){
re = "(?:" + re + "\\d+)?";
}else{
re += "\\d{" + flags.places + "}";
}
}
return re;
},
true
);
var exponentRE = dojo.regexp.buildGroupRE(flags.exponent,
function(q){
if(q){ return "([eE]" + dojo.number._integerRegexp({ signed: flags.eSigned}) + ")"; }
return "";
}
);
var realRE = integerRE + decimalRE;
// allow for decimals without integers, e.g. .25
if(decimalRE){realRE = "(?:(?:"+ realRE + ")|(?:" + decimalRE + "))";}
return realRE + exponentRE; // String
};
/*=====
dojo.number.__IntegerRegexpFlags = function(){
// signed: Boolean?
// The leading plus-or-minus sign. Can be true, false, or `[true,false]`.
// Default is `[true, false]`, (i.e. will match if it is signed
// or unsigned).
// separator: String?
// The character used as the thousands separator. Default is no
// separator. For more than one symbol use an array, e.g. `[",", ""]`,
// makes ',' optional.
// groupSize: Number?
// group size between separators
// groupSize2: Number?
// second grouping, where separators 2..n have a different interval than the first separator (for India)
this.signed = signed;
this.separator = separator;
this.groupSize = groupSize;
this.groupSize2 = groupSize2;
}
=====*/
dojo.number._integerRegexp = function(/*dojo.number.__IntegerRegexpFlags?*/flags){
// summary:
// Builds a regular expression that matches an integer
// assign default values to missing parameters
flags = flags || {};
if(!("signed" in flags)){ flags.signed = [true, false]; }
if(!("separator" in flags)){
flags.separator = "";
}else if(!("groupSize" in flags)){
flags.groupSize = 3;
}
var signRE = dojo.regexp.buildGroupRE(flags.signed,
function(q){ return q ? "[-+]" : ""; },
true
);
var numberRE = dojo.regexp.buildGroupRE(flags.separator,
function(sep){
if(!sep){
return "(?:\\d+)";
}
sep = dojo.regexp.escapeString(sep);
if(sep == " "){ sep = "\\s"; }
else if(sep == "\xa0"){ sep = "\\s\\xa0"; }
var grp = flags.groupSize, grp2 = flags.groupSize2;
//TODO: should we continue to enforce that numbers with separators begin with 1-9? See #6933
if(grp2){
var grp2RE = "(?:0|[1-9]\\d{0," + (grp2-1) + "}(?:[" + sep + "]\\d{" + grp2 + "})*[" + sep + "]\\d{" + grp + "})";
return ((grp-grp2) > 0) ? "(?:" + grp2RE + "|(?:0|[1-9]\\d{0," + (grp-1) + "}))" : grp2RE;
}
return "(?:0|[1-9]\\d{0," + (grp-1) + "}(?:[" + sep + "]\\d{" + grp + "})*)";
},
true
);
return signRE + numberRE; // String
};
}
if(!dojo._hasResource["dijit.ProgressBar"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dijit.ProgressBar"] = true;
dojo.provide("dijit.ProgressBar");
dojo.declare("dijit.ProgressBar", [dijit._Widget, dijit._Templated], {
// summary:
// A progress indication widget, showing the amount completed
// (often the percentage completed) of a task.
//
// example:
// |
// |
// progress: [const] String (Percentage or Number)
// Number or percentage indicating amount of task completed.
// Deprecated. Use "value" instead.
progress: "0",
// value: String (Percentage or Number)
// Number or percentage indicating amount of task completed.
// With "%": percentage value, 0% <= progress <= 100%, or
// without "%": absolute value, 0 <= progress <= maximum.
// Infinity means that the progress bar is indeterminate.
value: "",
// maximum: [const] Float
// Max sample number
maximum: 100,
// places: [const] Number
// Number of places to show in values; 0 by default
places: 0,
// indeterminate: [const] Boolean
// If false: show progress value (number or percentage).
// If true: show that a process is underway but that the amount completed is unknown.
// Deprecated. Use "value" instead.
indeterminate: false,
// label: String?
// Label on progress bar. Defaults to percentage for determinate progress bar and
// blank for indeterminate progress bar.
label:"",
// name: String
// this is the field name (for a form) if set. This needs to be set if you want to use
// this widget in a dijit.form.Form widget (such as dijit.Dialog)
name: '',
templateString: dojo.cache("dijit", "templates/ProgressBar.html", "
\n"),
// _indeterminateHighContrastImagePath: [private] dojo._URL
// URL to image to use for indeterminate progress bar when display is in high contrast mode
_indeterminateHighContrastImagePath:
dojo.moduleUrl("dijit", "themes/a11y/indeterminate_progress.gif"),
postMixInProperties: function(){
this.inherited(arguments);
if(!("value" in this.params)){
this.value = this.indeterminate ? Infinity : this.progress;
}
},
buildRendering: function(){
this.inherited(arguments);
this.indeterminateHighContrastImage.setAttribute("src",
this._indeterminateHighContrastImagePath.toString());
this.update();
},
update: function(/*Object?*/attributes){
// summary:
// Internal method to change attributes of ProgressBar, similar to set(hash). Users should call
// set("value", ...) rather than calling this method directly.
// attributes:
// May provide progress and/or maximum properties on this parameter;
// see attribute specs for details.
// example:
// | myProgressBar.update({'indeterminate': true});
// | myProgressBar.update({'progress': 80});
// | myProgressBar.update({'indeterminate': true, label:"Loading ..." })
// tags:
// private
// TODO: deprecate this method and use set() instead
dojo.mixin(this, attributes || {});
var tip = this.internalProgress, ap = this.domNode;
var percent = 1;
if(this.indeterminate){
dijit.removeWaiState(ap, "valuenow");
dijit.removeWaiState(ap, "valuemin");
dijit.removeWaiState(ap, "valuemax");
}else{
if(String(this.progress).indexOf("%") != -1){
percent = Math.min(parseFloat(this.progress)/100, 1);
this.progress = percent * this.maximum;
}else{
this.progress = Math.min(this.progress, this.maximum);
percent = this.progress / this.maximum;
}
dijit.setWaiState(ap, "describedby", this.labelNode.id);
dijit.setWaiState(ap, "valuenow", this.progress);
dijit.setWaiState(ap, "valuemin", 0);
dijit.setWaiState(ap, "valuemax", this.maximum);
}
this.labelNode.innerHTML = this.report(percent);
dojo.toggleClass(this.domNode, "dijitProgressBarIndeterminate", this.indeterminate);
tip.style.width = (percent * 100) + "%";
this.onChange();
},
_setValueAttr: function(v){
this._set("value", v);
if(v == Infinity){
this.update({indeterminate:true});
}else{
this.update({indeterminate:false, progress:v});
}
},
_setLabelAttr: function(label){
this._set("label", label);
this.update();
},
_setIndeterminateAttr: function(indeterminate){
// Deprecated, use set("value", ...) instead
this.indeterminate = indeterminate;
this.update();
},
report: function(/*float*/percent){
// summary:
// Generates message to show inside progress bar (normally indicating amount of task completed).
// May be overridden.
// tags:
// extension
return this.label ? this.label :
(this.indeterminate ? " " : dojo.number.format(percent, { type: "percent", places: this.places, locale: this.lang }));
},
onChange: function(){
// summary:
// Callback fired when progress updates.
// tags:
// extension
}
});
}
if(!dojo._hasResource["dijit.ToolbarSeparator"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dijit.ToolbarSeparator"] = true;
dojo.provide("dijit.ToolbarSeparator");
dojo.declare("dijit.ToolbarSeparator",
[ dijit._Widget, dijit._Templated ],
{
// summary:
// A spacer between two `dijit.Toolbar` items
templateString: '
',
buildRendering: function(){
this.inherited(arguments);
dojo.setSelectable(this.domNode, false);
},
isFocusable: function(){
// summary:
// This widget isn't focusable, so pass along that fact.
// tags:
// protected
return false;
}
});
}
if(!dojo._hasResource["dijit.Toolbar"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dijit.Toolbar"] = true;
dojo.provide("dijit.Toolbar");
// Note: require of ToolbarSeparator is for back-compat, remove for 2.0
dojo.declare("dijit.Toolbar",
[dijit._Widget, dijit._Templated, dijit._KeyNavContainer],
{
// summary:
// A Toolbar widget, used to hold things like `dijit.Editor` buttons
templateString:
'
',
baseClass: "dijitToolbar",
postCreate: function(){
this.inherited(arguments);
this.connectKeyNavHandlers(
this.isLeftToRight() ? [dojo.keys.LEFT_ARROW] : [dojo.keys.RIGHT_ARROW],
this.isLeftToRight() ? [dojo.keys.RIGHT_ARROW] : [dojo.keys.LEFT_ARROW]
);
},
startup: function(){
if(this._started){ return; }
this.startupKeyNavChildren();
this.inherited(arguments);
}
}
);
}
if(!dojo._hasResource["dojo.DeferredList"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dojo.DeferredList"] = true;
dojo.provide("dojo.DeferredList");
dojo.DeferredList = function(/*Array*/ list, /*Boolean?*/ fireOnOneCallback, /*Boolean?*/ fireOnOneErrback, /*Boolean?*/ consumeErrors, /*Function?*/ canceller){
// summary:
// Provides event handling for a group of Deferred objects.
// description:
// DeferredList takes an array of existing deferreds and returns a new deferred of its own
// this new deferred will typically have its callback fired when all of the deferreds in
// the given list have fired their own deferreds. The parameters `fireOnOneCallback` and
// fireOnOneErrback, will fire before all the deferreds as appropriate
//
// list:
// The list of deferreds to be synchronizied with this DeferredList
// fireOnOneCallback:
// Will cause the DeferredLists callback to be fired as soon as any
// of the deferreds in its list have been fired instead of waiting until
// the entire list has finished
// fireonOneErrback:
// Will cause the errback to fire upon any of the deferreds errback
// canceller:
// A deferred canceller function, see dojo.Deferred
var resultList = [];
dojo.Deferred.call(this);
var self = this;
if(list.length === 0 && !fireOnOneCallback){
this.resolve([0, []]);
}
var finished = 0;
dojo.forEach(list, function(item, i){
item.then(function(result){
if(fireOnOneCallback){
self.resolve([i, result]);
}else{
addResult(true, result);
}
},function(error){
if(fireOnOneErrback){
self.reject(error);
}else{
addResult(false, error);
}
if(consumeErrors){
return null;
}
throw error;
});
function addResult(succeeded, result){
resultList[i] = [succeeded, result];
finished++;
if(finished === list.length){
self.resolve(resultList);
}
}
});
};
dojo.DeferredList.prototype = new dojo.Deferred();
dojo.DeferredList.prototype.gatherResults= function(deferredList){
// summary:
// Gathers the results of the deferreds for packaging
// as the parameters to the Deferred Lists' callback
var d = new dojo.DeferredList(deferredList, false, true, false);
d.addCallback(function(results){
var ret = [];
dojo.forEach(results, function(result){
ret.push(result[1]);
});
return ret;
});
return d;
};
}
if(!dojo._hasResource["dijit.tree.TreeStoreModel"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dijit.tree.TreeStoreModel"] = true;
dojo.provide("dijit.tree.TreeStoreModel");
dojo.declare(
"dijit.tree.TreeStoreModel",
null,
{
// summary:
// Implements dijit.Tree.model connecting to a store with a single
// root item. Any methods passed into the constructor will override
// the ones defined here.
// store: dojo.data.Store
// Underlying store
store: null,
// childrenAttrs: String[]
// One or more attribute names (attributes in the dojo.data item) that specify that item's children
childrenAttrs: ["children"],
// newItemIdAttr: String
// Name of attribute in the Object passed to newItem() that specifies the id.
//
// If newItemIdAttr is set then it's used when newItem() is called to see if an
// item with the same id already exists, and if so just links to the old item
// (so that the old item ends up with two parents).
//
// Setting this to null or "" will make every drop create a new item.
newItemIdAttr: "id",
// labelAttr: String
// If specified, get label for tree node from this attribute, rather
// than by calling store.getLabel()
labelAttr: "",
// root: [readonly] dojo.data.Item
// Pointer to the root item (read only, not a parameter)
root: null,
// query: anything
// Specifies datastore query to return the root item for the tree.
// Must only return a single item. Alternately can just pass in pointer
// to root item.
// example:
// | {id:'ROOT'}
query: null,
// deferItemLoadingUntilExpand: Boolean
// Setting this to true will cause the TreeStoreModel to defer calling loadItem on nodes
// until they are expanded. This allows for lazying loading where only one
// loadItem (and generally one network call, consequently) per expansion
// (rather than one for each child).
// This relies on partial loading of the children items; each children item of a
// fully loaded item should contain the label and info about having children.
deferItemLoadingUntilExpand: false,
constructor: function(/* Object */ args){
// summary:
// Passed the arguments listed above (store, etc)
// tags:
// private
dojo.mixin(this, args);
this.connects = [];
var store = this.store;
if(!store.getFeatures()['dojo.data.api.Identity']){
throw new Error("dijit.Tree: store must support dojo.data.Identity");
}
// if the store supports Notification, subscribe to the notification events
if(store.getFeatures()['dojo.data.api.Notification']){
this.connects = this.connects.concat([
dojo.connect(store, "onNew", this, "onNewItem"),
dojo.connect(store, "onDelete", this, "onDeleteItem"),
dojo.connect(store, "onSet", this, "onSetItem")
]);
}
},
destroy: function(){
dojo.forEach(this.connects, dojo.disconnect);
// TODO: should cancel any in-progress processing of getRoot(), getChildren()
},
// =======================================================================
// Methods for traversing hierarchy
getRoot: function(onItem, onError){
// summary:
// Calls onItem with the root item for the tree, possibly a fabricated item.
// Calls onError on error.
if(this.root){
onItem(this.root);
}else{
this.store.fetch({
query: this.query,
onComplete: dojo.hitch(this, function(items){
if(items.length != 1){
throw new Error(this.declaredClass + ": query " + dojo.toJson(this.query) + " returned " + items.length +
" items, but must return exactly one item");
}
this.root = items[0];
onItem(this.root);
}),
onError: onError
});
}
},
mayHaveChildren: function(/*dojo.data.Item*/ item){
// summary:
// Tells if an item has or may have children. Implementing logic here
// avoids showing +/- expando icon for nodes that we know don't have children.
// (For efficiency reasons we may not want to check if an element actually
// has children until user clicks the expando node)
return dojo.some(this.childrenAttrs, function(attr){
return this.store.hasAttribute(item, attr);
}, this);
},
getChildren: function(/*dojo.data.Item*/ parentItem, /*function(items)*/ onComplete, /*function*/ onError){
// summary:
// Calls onComplete() with array of child items of given parent item, all loaded.
var store = this.store;
if(!store.isItemLoaded(parentItem)){
// The parent is not loaded yet, we must be in deferItemLoadingUntilExpand
// mode, so we will load it and just return the children (without loading each
// child item)
var getChildren = dojo.hitch(this, arguments.callee);
store.loadItem({
item: parentItem,
onItem: function(parentItem){
getChildren(parentItem, onComplete, onError);
},
onError: onError
});
return;
}
// get children of specified item
var childItems = [];
for(var i=0; i
// Map from an item's id (which is also the DOMNode's id) to
// the dojo.dnd.Item itself.
map: {},
=====*/
constructor: function(node, params){
// summary:
// a constructor of the Container
// node: Node
// node or node's id to build the container on
// params: dojo.dnd.__ContainerArgs
// a dictionary of parameters
this.node = dojo.byId(node);
if(!params){ params = {}; }
this.creator = params.creator || null;
this.skipForm = params.skipForm;
this.parent = params.dropParent && dojo.byId(params.dropParent);
// class-specific variables
this.map = {};
this.current = null;
// states
this.containerState = "";
dojo.addClass(this.node, "dojoDndContainer");
// mark up children
if(!(params && params._skipStartup)){
this.startup();
}
// set up events
this.events = [
dojo.connect(this.node, "onmouseover", this, "onMouseOver"),
dojo.connect(this.node, "onmouseout", this, "onMouseOut"),
// cancel text selection and text dragging
dojo.connect(this.node, "ondragstart", this, "onSelectStart"),
dojo.connect(this.node, "onselectstart", this, "onSelectStart")
];
},
// object attributes (for markup)
creator: function(){
// summary:
// creator function, dummy at the moment
},
// abstract access to the map
getItem: function(/*String*/ key){
// summary:
// returns a data item by its key (id)
return this.map[key]; // dojo.dnd.Item
},
setItem: function(/*String*/ key, /*dojo.dnd.Item*/ data){
// summary:
// associates a data item with its key (id)
this.map[key] = data;
},
delItem: function(/*String*/ key){
// summary:
// removes a data item from the map by its key (id)
delete this.map[key];
},
forInItems: function(/*Function*/ f, /*Object?*/ o){
// summary:
// iterates over a data map skipping members that
// are present in the empty object (IE and/or 3rd-party libraries).
o = o || dojo.global;
var m = this.map, e = dojo.dnd._empty;
for(var i in m){
if(i in e){ continue; }
f.call(o, m[i], i, this);
}
return o; // Object
},
clearItems: function(){
// summary:
// removes all data items from the map
this.map = {};
},
// methods
getAllNodes: function(){
// summary:
// returns a list (an array) of all valid child nodes
return dojo.query("> .dojoDndItem", this.parent); // NodeList
},
sync: function(){
// summary:
// sync up the node list with the data map
var map = {};
this.getAllNodes().forEach(function(node){
if(node.id){
var item = this.getItem(node.id);
if(item){
map[node.id] = item;
return;
}
}else{
node.id = dojo.dnd.getUniqueId();
}
var type = node.getAttribute("dndType"),
data = node.getAttribute("dndData");
map[node.id] = {
data: data || node.innerHTML,
type: type ? type.split(/\s*,\s*/) : ["text"]
};
}, this);
this.map = map;
return this; // self
},
insertNodes: function(data, before, anchor){
// summary:
// inserts an array of new nodes before/after an anchor node
// data: Array
// a list of data items, which should be processed by the creator function
// before: Boolean
// insert before the anchor, if true, and after the anchor otherwise
// anchor: Node
// the anchor node to be used as a point of insertion
if(!this.parent.firstChild){
anchor = null;
}else if(before){
if(!anchor){
anchor = this.parent.firstChild;
}
}else{
if(anchor){
anchor = anchor.nextSibling;
}
}
if(anchor){
for(var i = 0; i < data.length; ++i){
var t = this._normalizedCreator(data[i]);
this.setItem(t.node.id, {data: t.data, type: t.type});
this.parent.insertBefore(t.node, anchor);
}
}else{
for(var i = 0; i < data.length; ++i){
var t = this._normalizedCreator(data[i]);
this.setItem(t.node.id, {data: t.data, type: t.type});
this.parent.appendChild(t.node);
}
}
return this; // self
},
destroy: function(){
// summary:
// prepares this object to be garbage-collected
dojo.forEach(this.events, dojo.disconnect);
this.clearItems();
this.node = this.parent = this.current = null;
},
// markup methods
markupFactory: function(params, node){
params._skipStartup = true;
return new dojo.dnd.Container(node, params);
},
startup: function(){
// summary:
// collects valid child items and populate the map
// set up the real parent node
if(!this.parent){
// use the standard algorithm, if not assigned
this.parent = this.node;
if(this.parent.tagName.toLowerCase() == "table"){
var c = this.parent.getElementsByTagName("tbody");
if(c && c.length){ this.parent = c[0]; }
}
}
this.defaultCreator = dojo.dnd._defaultCreator(this.parent);
// process specially marked children
this.sync();
},
// mouse events
onMouseOver: function(e){
// summary:
// event processor for onmouseover
// e: Event
// mouse event
var n = e.relatedTarget;
while(n){
if(n == this.node){ break; }
try{
n = n.parentNode;
}catch(x){
n = null;
}
}
if(!n){
this._changeState("Container", "Over");
this.onOverEvent();
}
n = this._getChildByEvent(e);
if(this.current == n){ return; }
if(this.current){ this._removeItemClass(this.current, "Over"); }
if(n){ this._addItemClass(n, "Over"); }
this.current = n;
},
onMouseOut: function(e){
// summary:
// event processor for onmouseout
// e: Event
// mouse event
for(var n = e.relatedTarget; n;){
if(n == this.node){ return; }
try{
n = n.parentNode;
}catch(x){
n = null;
}
}
if(this.current){
this._removeItemClass(this.current, "Over");
this.current = null;
}
this._changeState("Container", "");
this.onOutEvent();
},
onSelectStart: function(e){
// summary:
// event processor for onselectevent and ondragevent
// e: Event
// mouse event
if(!this.skipForm || !dojo.dnd.isFormElement(e)){
dojo.stopEvent(e);
}
},
// utilities
onOverEvent: function(){
// summary:
// this function is called once, when mouse is over our container
},
onOutEvent: function(){
// summary:
// this function is called once, when mouse is out of our container
},
_changeState: function(type, newState){
// summary:
// changes a named state to new state value
// type: String
// a name of the state to change
// newState: String
// new state
var prefix = "dojoDnd" + type;
var state = type.toLowerCase() + "State";
//dojo.replaceClass(this.node, prefix + newState, prefix + this[state]);
dojo.replaceClass(this.node, prefix + newState, prefix + this[state]);
this[state] = newState;
},
_addItemClass: function(node, type){
// summary:
// adds a class with prefix "dojoDndItem"
// node: Node
// a node
// type: String
// a variable suffix for a class name
dojo.addClass(node, "dojoDndItem" + type);
},
_removeItemClass: function(node, type){
// summary:
// removes a class with prefix "dojoDndItem"
// node: Node
// a node
// type: String
// a variable suffix for a class name
dojo.removeClass(node, "dojoDndItem" + type);
},
_getChildByEvent: function(e){
// summary:
// gets a child, which is under the mouse at the moment, or null
// e: Event
// a mouse event
var node = e.target;
if(node){
for(var parent = node.parentNode; parent; node = parent, parent = node.parentNode){
if(parent == this.parent && dojo.hasClass(node, "dojoDndItem")){ return node; }
}
}
return null;
},
_normalizedCreator: function(/*dojo.dnd.Item*/ item, /*String*/ hint){
// summary:
// adds all necessary data to the output of the user-supplied creator function
var t = (this.creator || this.defaultCreator).call(this, item, hint);
if(!dojo.isArray(t.type)){ t.type = ["text"]; }
if(!t.node.id){ t.node.id = dojo.dnd.getUniqueId(); }
dojo.addClass(t.node, "dojoDndItem");
return t;
}
});
dojo.dnd._createNode = function(tag){
// summary:
// returns a function, which creates an element of given tag
// (SPAN by default) and sets its innerHTML to given text
// tag: String
// a tag name or empty for SPAN
if(!tag){ return dojo.dnd._createSpan; }
return function(text){ // Function
return dojo.create(tag, {innerHTML: text}); // Node
};
};
dojo.dnd._createTrTd = function(text){
// summary:
// creates a TR/TD structure with given text as an innerHTML of TD
// text: String
// a text for TD
var tr = dojo.create("tr");
dojo.create("td", {innerHTML: text}, tr);
return tr; // Node
};
dojo.dnd._createSpan = function(text){
// summary:
// creates a SPAN element with given text as its innerHTML
// text: String
// a text for SPAN
return dojo.create("span", {innerHTML: text}); // Node
};
// dojo.dnd._defaultCreatorNodes: Object
// a dictionary that maps container tag names to child tag names
dojo.dnd._defaultCreatorNodes = {ul: "li", ol: "li", div: "div", p: "div"};
dojo.dnd._defaultCreator = function(node){
// summary:
// takes a parent node, and returns an appropriate creator function
// node: Node
// a container node
var tag = node.tagName.toLowerCase();
var c = tag == "tbody" || tag == "thead" ? dojo.dnd._createTrTd :
dojo.dnd._createNode(dojo.dnd._defaultCreatorNodes[tag]);
return function(item, hint){ // Function
var isObj = item && dojo.isObject(item), data, type, n;
if(isObj && item.tagName && item.nodeType && item.getAttribute){
// process a DOM node
data = item.getAttribute("dndData") || item.innerHTML;
type = item.getAttribute("dndType");
type = type ? type.split(/\s*,\s*/) : ["text"];
n = item; // this node is going to be moved rather than copied
}else{
// process a DnD item object or a string
data = (isObj && item.data) ? item.data : item;
type = (isObj && item.type) ? item.type : ["text"];
n = (hint == "avatar" ? dojo.dnd._createSpan : c)(String(data));
}
if(!n.id){
n.id = dojo.dnd.getUniqueId();
}
return {node: n, data: data, type: type};
};
};
}
if(!dojo._hasResource["dijit.tree._dndContainer"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dijit.tree._dndContainer"] = true;
dojo.provide("dijit.tree._dndContainer");
dojo.getObject("tree", true, dojo);
dijit.tree._compareNodes = function(n1, n2){
if(n1 === n2){
return 0;
}
if('sourceIndex' in document.documentElement){ //IE
//TODO: does not yet work if n1 and/or n2 is a text node
return n1.sourceIndex - n2.sourceIndex;
}else if('compareDocumentPosition' in document.documentElement){ //FF, Opera
return n1.compareDocumentPosition(n2) & 2 ? 1: -1;
}else if(document.createRange){ //Webkit
var r1 = doc.createRange();
r1.setStartBefore(n1);
var r2 = doc.createRange();
r2.setStartBefore(n2);
return r1.compareBoundaryPoints(r1.END_TO_END, r2);
}else{
throw Error("dijit.tree._compareNodes don't know how to compare two different nodes in this browser");
}
};
dojo.declare("dijit.tree._dndContainer",
null,
{
// summary:
// This is a base class for `dijit.tree._dndSelector`, and isn't meant to be used directly.
// It's modeled after `dojo.dnd.Container`.
// tags:
// protected
/*=====
// current: DomNode
// The currently hovered TreeNode.rowNode (which is the DOM node
// associated w/a given node in the tree, excluding it's descendants)
current: null,
=====*/
constructor: function(tree, params){
// summary:
// A constructor of the Container
// tree: Node
// Node or node's id to build the container on
// params: dijit.tree.__SourceArgs
// A dict of parameters, which gets mixed into the object
// tags:
// private
this.tree = tree;
this.node = tree.domNode; // TODO: rename; it's not a TreeNode but the whole Tree
dojo.mixin(this, params);
// class-specific variables
this.map = {};
this.current = null; // current TreeNode's DOM node
// states
this.containerState = "";
dojo.addClass(this.node, "dojoDndContainer");
// set up events
this.events = [
// container level events
dojo.connect(this.node, "onmouseenter", this, "onOverEvent"),
dojo.connect(this.node, "onmouseleave", this, "onOutEvent"),
// switching between TreeNodes
dojo.connect(this.tree, "_onNodeMouseEnter", this, "onMouseOver"),
dojo.connect(this.tree, "_onNodeMouseLeave", this, "onMouseOut"),
// cancel text selection and text dragging
dojo.connect(this.node, "ondragstart", dojo, "stopEvent"),
dojo.connect(this.node, "onselectstart", dojo, "stopEvent")
];
},
getItem: function(/*String*/ key){
// summary:
// Returns the dojo.dnd.Item (representing a dragged node) by it's key (id).
// Called by dojo.dnd.Source.checkAcceptance().
// tags:
// protected
var widget = this.selection[key],
ret = {
data: widget,
type: ["treeNode"]
};
return ret; // dojo.dnd.Item
},
destroy: function(){
// summary:
// Prepares this object to be garbage-collected
dojo.forEach(this.events, dojo.disconnect);
// this.clearItems();
this.node = this.parent = null;
},
// mouse events
onMouseOver: function(/*TreeNode*/ widget, /*Event*/ evt){
// summary:
// Called when mouse is moved over a TreeNode
// tags:
// protected
this.current = widget;
},
onMouseOut: function(/*TreeNode*/ widget, /*Event*/ evt){
// summary:
// Called when mouse is moved away from a TreeNode
// tags:
// protected
this.current = null;
},
_changeState: function(type, newState){
// summary:
// Changes a named state to new state value
// type: String
// A name of the state to change
// newState: String
// new state
var prefix = "dojoDnd" + type;
var state = type.toLowerCase() + "State";
//dojo.replaceClass(this.node, prefix + newState, prefix + this[state]);
dojo.replaceClass(this.node, prefix + newState, prefix + this[state]);
this[state] = newState;
},
_addItemClass: function(node, type){
// summary:
// Adds a class with prefix "dojoDndItem"
// node: Node
// A node
// type: String
// A variable suffix for a class name
dojo.addClass(node, "dojoDndItem" + type);
},
_removeItemClass: function(node, type){
// summary:
// Removes a class with prefix "dojoDndItem"
// node: Node
// A node
// type: String
// A variable suffix for a class name
dojo.removeClass(node, "dojoDndItem" + type);
},
onOverEvent: function(){
// summary:
// This function is called once, when mouse is over our container
// tags:
// protected
this._changeState("Container", "Over");
},
onOutEvent: function(){
// summary:
// This function is called once, when mouse is out of our container
// tags:
// protected
this._changeState("Container", "");
}
});
}
if(!dojo._hasResource["dijit.tree._dndSelector"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dijit.tree._dndSelector"] = true;
dojo.provide("dijit.tree._dndSelector");
dojo.declare("dijit.tree._dndSelector",
dijit.tree._dndContainer,
{
// summary:
// This is a base class for `dijit.tree.dndSource` , and isn't meant to be used directly.
// It's based on `dojo.dnd.Selector`.
// tags:
// protected
/*=====
// selection: Hash
// (id, DomNode) map for every TreeNode that's currently selected.
// The DOMNode is the TreeNode.rowNode.
selection: {},
=====*/
constructor: function(tree, params){
// summary:
// Initialization
// tags:
// private
this.selection={};
this.anchor = null;
dijit.setWaiState(this.tree.domNode, "multiselect", !this.singular);
this.events.push(
dojo.connect(this.tree.domNode, "onmousedown", this,"onMouseDown"),
dojo.connect(this.tree.domNode, "onmouseup", this,"onMouseUp"),
dojo.connect(this.tree.domNode, "onmousemove", this,"onMouseMove")
);
},
// singular: Boolean
// Allows selection of only one element, if true.
// Tree hasn't been tested in singular=true mode, unclear if it works.
singular: false,
// methods
getSelectedTreeNodes: function(){
// summary:
// Returns a list of selected node(s).
// Used by dndSource on the start of a drag.
// tags:
// protected
var nodes=[], sel = this.selection;
for(var i in sel){
nodes.push(sel[i]);
}
return nodes;
},
selectNone: function(){
// summary:
// Unselects all items
// tags:
// private
this.setSelection([]);
return this; // self
},
destroy: function(){
// summary:
// Prepares the object to be garbage-collected
this.inherited(arguments);
this.selection = this.anchor = null;
},
addTreeNode: function(/*dijit._TreeNode*/node, /*Boolean?*/isAnchor){
// summary
// add node to current selection
// node: Node
// node to add
// isAnchor: Boolean
// Whether the node should become anchor.
this.setSelection(this.getSelectedTreeNodes().concat( [node] ));
if(isAnchor){ this.anchor = node; }
return node;
},
removeTreeNode: function(/*dijit._TreeNode*/node){
// summary
// remove node from current selection
// node: Node
// node to remove
this.setSelection(this._setDifference(this.getSelectedTreeNodes(), [node]))
return node;
},
isTreeNodeSelected: function(/*dijit._TreeNode*/node){
// summary
// return true if node is currently selected
// node: Node
// the node to check whether it's in the current selection
return node.id && !!this.selection[node.id];
},
setSelection: function(/*dijit._treeNode[]*/ newSelection){
// summary
// set the list of selected nodes to be exactly newSelection. All changes to the
// selection should be passed through this function, which ensures that derived
// attributes are kept up to date. Anchor will be deleted if it has been removed
// from the selection, but no new anchor will be added by this function.
// newSelection: Node[]
// list of tree nodes to make selected
var oldSelection = this.getSelectedTreeNodes();
dojo.forEach(this._setDifference(oldSelection, newSelection), dojo.hitch(this, function(node){
node.setSelected(false);
if(this.anchor == node){
delete this.anchor;
}
delete this.selection[node.id];
}));
dojo.forEach(this._setDifference(newSelection, oldSelection), dojo.hitch(this, function(node){
node.setSelected(true);
this.selection[node.id] = node;
}));
this._updateSelectionProperties();
},
_setDifference: function(xs,ys){
// summary
// Returns a copy of xs which lacks any objects
// occurring in ys. Checks for membership by
// modifying and then reading the object, so it will
// not properly handle sets of numbers or strings.
dojo.forEach(ys, function(y){ y.__exclude__ = true; });
var ret = dojo.filter(xs, function(x){ return !x.__exclude__; });
// clean up after ourselves.
dojo.forEach(ys, function(y){ delete y['__exclude__'] });
return ret;
},
_updateSelectionProperties: function() {
// summary
// Update the following tree properties from the current selection:
// path[s], selectedItem[s], selectedNode[s]
var selected = this.getSelectedTreeNodes();
var paths = [], nodes = [];
dojo.forEach(selected, function(node) {
nodes.push(node);
paths.push(node.getTreePath());
});
var items = dojo.map(nodes,function(node) { return node.item; });
this.tree._set("paths", paths);
this.tree._set("path", paths[0] || []);
this.tree._set("selectedNodes", nodes);
this.tree._set("selectedNode", nodes[0] || null);
this.tree._set("selectedItems", items);
this.tree._set("selectedItem", items[0] || null);
},
// mouse events
onMouseDown: function(e){
// summary:
// Event processor for onmousedown
// e: Event
// mouse event
// tags:
// protected
// ignore click on expando node
if(!this.current || this.tree.isExpandoNode( e.target, this.current)){ return; }
if(e.button == dojo.mouseButtons.RIGHT){ return; } // ignore right-click
dojo.stopEvent(e);
var treeNode = this.current,
copy = dojo.isCopyKey(e), id = treeNode.id;
// if shift key is not pressed, and the node is already in the selection,
// delay deselection until onmouseup so in the case of DND, deselection
// will be canceled by onmousemove.
if(!this.singular && !e.shiftKey && this.selection[id]){
this._doDeselect = true;
return;
}else{
this._doDeselect = false;
}
this.userSelect(treeNode, copy, e.shiftKey);
},
onMouseUp: function(e){
// summary:
// Event processor for onmouseup
// e: Event
// mouse event
// tags:
// protected
// _doDeselect is the flag to indicate that the user wants to either ctrl+click on
// a already selected item (to deselect the item), or click on a not-yet selected item
// (which should remove all current selection, and add the clicked item). This can not
// be done in onMouseDown, because the user may start a drag after mousedown. By moving
// the deselection logic here, the user can drags an already selected item.
if(!this._doDeselect){ return; }
this._doDeselect = false;
this.userSelect(this.current, dojo.isCopyKey( e ), e.shiftKey);
},
onMouseMove: function(e){
// summary
// event processor for onmousemove
// e: Event
// mouse event
this._doDeselect = false;
},
userSelect: function(node, multi, range){
// summary:
// Add or remove the given node from selection, responding
// to a user action such as a click or keypress.
// multi: Boolean
// Indicates whether this is meant to be a multi-select action (e.g. ctrl-click)
// range: Boolean
// Indicates whether this is meant to be a ranged action (e.g. shift-click)
// tags:
// protected
if(this.singular){
if(this.anchor == node && multi){
this.selectNone();
}else{
this.setSelection([node]);
this.anchor = node;
}
}else{
if(range && this.anchor){
var cr = dijit.tree._compareNodes(this.anchor.rowNode, node.rowNode),
begin, end, anchor = this.anchor;
if(cr < 0){ //current is after anchor
begin = anchor;
end = node;
}else{ //current is before anchor
begin = node;
end = anchor;
}
nodes = [];
//add everything betweeen begin and end inclusively
while(begin != end) {
nodes.push(begin)
begin = this.tree._getNextNode(begin);
}
nodes.push(end)
this.setSelection(nodes);
}else{
if( this.selection[ node.id ] && multi ) {
this.removeTreeNode( node );
} else if(multi) {
this.addTreeNode(node, true);
} else {
this.setSelection([node]);
this.anchor = node;
}
}
}
},
forInSelectedItems: function(/*Function*/ f, /*Object?*/ o){
// summary:
// Iterates over selected items;
// see `dojo.dnd.Container.forInItems()` for details
o = o || dojo.global;
for(var id in this.selection){
// console.log("selected item id: " + id);
f.call(o, this.getItem(id), id, this);
}
}
});
}
if(!dojo._hasResource["dijit.Tree"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dijit.Tree"] = true;
dojo.provide("dijit.Tree");
dojo.declare(
"dijit._TreeNode",
[dijit._Widget, dijit._Templated, dijit._Container, dijit._Contained, dijit._CssStateMixin],
{
// summary:
// Single node within a tree. This class is used internally
// by Tree and should not be accessed directly.
// tags:
// private
// item: [const] dojo.data.Item
// the dojo.data entry this tree represents
item: null,
// isTreeNode: [protected] Boolean
// Indicates that this is a TreeNode. Used by `dijit.Tree` only,
// should not be accessed directly.
isTreeNode: true,
// label: String
// Text of this tree node
label: "",
// isExpandable: [private] Boolean
// This node has children, so show the expando node (+ sign)
isExpandable: null,
// isExpanded: [readonly] Boolean
// This node is currently expanded (ie, opened)
isExpanded: false,
// state: [private] String
// Dynamic loading-related stuff.
// When an empty folder node appears, it is "UNCHECKED" first,
// then after dojo.data query it becomes "LOADING" and, finally "LOADED"
state: "UNCHECKED",
templateString: dojo.cache("dijit", "templates/TreeNode.html", "\n\t\t\t\n\t\t \n\t
\n
\n"),
baseClass: "dijitTreeNode",
// For hover effect for tree node, and focus effect for label
cssStateNodes: {
rowNode: "dijitTreeRow",
labelNode: "dijitTreeLabel"
},
attributeMap: dojo.delegate(dijit._Widget.prototype.attributeMap, {
label: {node: "labelNode", type: "innerText"},
tooltip: {node: "rowNode", type: "attribute", attribute: "title"}
}),
buildRendering: function(){
this.inherited(arguments);
// set expand icon for leaf
this._setExpando();
// set icon and label class based on item
this._updateItemClasses(this.item);
if(this.isExpandable){
dijit.setWaiState(this.labelNode, "expanded", this.isExpanded);
}
//aria-selected should be false on all selectable elements.
this.setSelected(false);
},
_setIndentAttr: function(indent){
// summary:
// Tell this node how many levels it should be indented
// description:
// 0 for top level nodes, 1 for their children, 2 for their
// grandchildren, etc.
// Math.max() is to prevent negative padding on hidden root node (when indent == -1)
var pixels = (Math.max(indent, 0) * this.tree._nodePixelIndent) + "px";
dojo.style(this.domNode, "backgroundPosition", pixels + " 0px");
dojo.style(this.rowNode, this.isLeftToRight() ? "paddingLeft" : "paddingRight", pixels);
dojo.forEach(this.getChildren(), function(child){
child.set("indent", indent+1);
});
this._set("indent", indent);
},
markProcessing: function(){
// summary:
// Visually denote that tree is loading data, etc.
// tags:
// private
this.state = "LOADING";
this._setExpando(true);
},
unmarkProcessing: function(){
// summary:
// Clear markup from markProcessing() call
// tags:
// private
this._setExpando(false);
},
_updateItemClasses: function(item){
// summary:
// Set appropriate CSS classes for icon and label dom node
// (used to allow for item updates to change respective CSS)
// tags:
// private
var tree = this.tree, model = tree.model;
if(tree._v10Compat && item === model.root){
// For back-compat with 1.0, need to use null to specify root item (TODO: remove in 2.0)
item = null;
}
this._applyClassAndStyle(item, "icon", "Icon");
this._applyClassAndStyle(item, "label", "Label");
this._applyClassAndStyle(item, "row", "Row");
},
_applyClassAndStyle: function(item, lower, upper){
// summary:
// Set the appropriate CSS classes and styles for labels, icons and rows.
//
// item:
// The data item.
//
// lower:
// The lower case attribute to use, e.g. 'icon', 'label' or 'row'.
//
// upper:
// The upper case attribute to use, e.g. 'Icon', 'Label' or 'Row'.
//
// tags:
// private
var clsName = "_" + lower + "Class";
var nodeName = lower + "Node";
var oldCls = this[clsName];
this[clsName] = this.tree["get" + upper + "Class"](item, this.isExpanded);
dojo.replaceClass(this[nodeName], this[clsName] || "", oldCls || "");
dojo.style(this[nodeName], this.tree["get" + upper + "Style"](item, this.isExpanded) || {});
},
_updateLayout: function(){
// summary:
// Set appropriate CSS classes for this.domNode
// tags:
// private
var parent = this.getParent();
if(!parent || parent.rowNode.style.display == "none"){
/* if we are hiding the root node then make every first level child look like a root node */
dojo.addClass(this.domNode, "dijitTreeIsRoot");
}else{
dojo.toggleClass(this.domNode, "dijitTreeIsLast", !this.getNextSibling());
}
},
_setExpando: function(/*Boolean*/ processing){
// summary:
// Set the right image for the expando node
// tags:
// private
var styles = ["dijitTreeExpandoLoading", "dijitTreeExpandoOpened",
"dijitTreeExpandoClosed", "dijitTreeExpandoLeaf"],
_a11yStates = ["*","-","+","*"],
idx = processing ? 0 : (this.isExpandable ? (this.isExpanded ? 1 : 2) : 3);
// apply the appropriate class to the expando node
dojo.replaceClass(this.expandoNode, styles[idx], styles);
// provide a non-image based indicator for images-off mode
this.expandoNodeText.innerHTML = _a11yStates[idx];
},
expand: function(){
// summary:
// Show my children
// returns:
// Deferred that fires when expansion is complete
// If there's already an expand in progress or we are already expanded, just return
if(this._expandDeferred){
return this._expandDeferred; // dojo.Deferred
}
// cancel in progress collapse operation
this._wipeOut && this._wipeOut.stop();
// All the state information for when a node is expanded, maybe this should be
// set when the animation completes instead
this.isExpanded = true;
dijit.setWaiState(this.labelNode, "expanded", "true");
if(this.tree.showRoot || this !== this.tree.rootNode){
dijit.setWaiRole(this.containerNode, "group");
}
dojo.addClass(this.contentNode,'dijitTreeContentExpanded');
this._setExpando();
this._updateItemClasses(this.item);
if(this == this.tree.rootNode){
dijit.setWaiState(this.tree.domNode, "expanded", "true");
}
var def,
wipeIn = dojo.fx.wipeIn({
node: this.containerNode, duration: dijit.defaultDuration,
onEnd: function(){
def.callback(true);
}
});
// Deferred that fires when expand is complete
def = (this._expandDeferred = new dojo.Deferred(function(){
// Canceller
wipeIn.stop();
}));
wipeIn.play();
return def; // dojo.Deferred
},
collapse: function(){
// summary:
// Collapse this node (if it's expanded)
if(!this.isExpanded){ return; }
// cancel in progress expand operation
if(this._expandDeferred){
this._expandDeferred.cancel();
delete this._expandDeferred;
}
this.isExpanded = false;
dijit.setWaiState(this.labelNode, "expanded", "false");
if(this == this.tree.rootNode){
dijit.setWaiState(this.tree.domNode, "expanded", "false");
}
dojo.removeClass(this.contentNode,'dijitTreeContentExpanded');
this._setExpando();
this._updateItemClasses(this.item);
if(!this._wipeOut){
this._wipeOut = dojo.fx.wipeOut({
node: this.containerNode, duration: dijit.defaultDuration
});
}
this._wipeOut.play();
},
// indent: Integer
// Levels from this node to the root node
indent: 0,
setChildItems: function(/* Object[] */ items){
// summary:
// Sets the child items of this node, removing/adding nodes
// from current children to match specified items[] array.
// Also, if this.persist == true, expands any children that were previously
// opened.
// returns:
// Deferred object that fires after all previously opened children
// have been expanded again (or fires instantly if there are no such children).
var tree = this.tree,
model = tree.model,
defs = []; // list of deferreds that need to fire before I am complete
// Orphan all my existing children.
// If items contains some of the same items as before then we will reattach them.
// Don't call this.removeChild() because that will collapse the tree etc.
dojo.forEach(this.getChildren(), function(child){
dijit._Container.prototype.removeChild.call(this, child);
}, this);
this.state = "LOADED";
if(items && items.length > 0){
this.isExpandable = true;
// Create _TreeNode widget for each specified tree node, unless one already
// exists and isn't being used (presumably it's from a DnD move and was recently
// released
dojo.forEach(items, function(item){
var id = model.getIdentity(item),
existingNodes = tree._itemNodesMap[id],
node;
if(existingNodes){
for(var i=0;i itself
tree.domNode.setAttribute("tabIndex", "0");
}
}
return new dojo.DeferredList(defs); // dojo.Deferred
},
getTreePath: function(){
var node = this;
var path = [];
while(node && node !== this.tree.rootNode){
path.unshift(node.item);
node = node.getParent();
}
path.unshift(this.tree.rootNode.item);
return path;
},
getIdentity: function() {
return this.tree.model.getIdentity(this.item);
},
removeChild: function(/* treeNode */ node){
this.inherited(arguments);
var children = this.getChildren();
if(children.length == 0){
this.isExpandable = false;
this.collapse();
}
dojo.forEach(children, function(child){
child._updateLayout();
});
},
makeExpandable: function(){
// summary:
// if this node wasn't already showing the expando node,
// turn it into one and call _setExpando()
// TODO: hmm this isn't called from anywhere, maybe should remove it for 2.0
this.isExpandable = true;
this._setExpando(false);
},
_onLabelFocus: function(evt){
// summary:
// Called when this row is focused (possibly programatically)
// Note that we aren't using _onFocus() builtin to dijit
// because it's called when focus is moved to a descendant TreeNode.
// tags:
// private
this.tree._onNodeFocus(this);
},
setSelected: function(/*Boolean*/ selected){
// summary:
// A Tree has a (single) currently selected node.
// Mark that this node is/isn't that currently selected node.
// description:
// In particular, setting a node as selected involves setting tabIndex
// so that when user tabs to the tree, focus will go to that node (only).
dijit.setWaiState(this.labelNode, "selected", selected);
dojo.toggleClass(this.rowNode, "dijitTreeRowSelected", selected);
},
setFocusable: function(/*Boolean*/ selected){
// summary:
// A Tree has a (single) node that's focusable.
// Mark that this node is/isn't that currently focsuable node.
// description:
// In particular, setting a node as selected involves setting tabIndex
// so that when user tabs to the tree, focus will go to that node (only).
this.labelNode.setAttribute("tabIndex", selected ? "0" : "-1");
},
_onClick: function(evt){
// summary:
// Handler for onclick event on a node
// tags:
// private
this.tree._onClick(this, evt);
},
_onDblClick: function(evt){
// summary:
// Handler for ondblclick event on a node
// tags:
// private
this.tree._onDblClick(this, evt);
},
_onMouseEnter: function(evt){
// summary:
// Handler for onmouseenter event on a node
// tags:
// private
this.tree._onNodeMouseEnter(this, evt);
},
_onMouseLeave: function(evt){
// summary:
// Handler for onmouseenter event on a node
// tags:
// private
this.tree._onNodeMouseLeave(this, evt);
}
});
dojo.declare(
"dijit.Tree",
[dijit._Widget, dijit._Templated],
{
// summary:
// This widget displays hierarchical data from a store.
// store: [deprecated] String||dojo.data.Store
// Deprecated. Use "model" parameter instead.
// The store to get data to display in the tree.
store: null,
// model: dijit.Tree.model
// Interface to read tree data, get notifications of changes to tree data,
// and for handling drop operations (i.e drag and drop onto the tree)
model: null,
// query: [deprecated] anything
// Deprecated. User should specify query to the model directly instead.
// Specifies datastore query to return the root item or top items for the tree.
query: null,
// label: [deprecated] String
// Deprecated. Use dijit.tree.ForestStoreModel directly instead.
// Used in conjunction with query parameter.
// If a query is specified (rather than a root node id), and a label is also specified,
// then a fake root node is created and displayed, with this label.
label: "",
// showRoot: [const] Boolean
// Should the root node be displayed, or hidden?
showRoot: true,
// childrenAttr: [deprecated] String[]
// Deprecated. This information should be specified in the model.
// One ore more attributes that holds children of a tree node
childrenAttr: ["children"],
// paths: String[][] or Item[][]
// Full paths from rootNode to selected nodes expressed as array of items or array of ids.
// Since setting the paths may be asynchronous (because ofwaiting on dojo.data), set("paths", ...)
// returns a Deferred to indicate when the set is complete.
paths: [],
// path: String[] or Item[]
// Backward compatible singular variant of paths.
path: [],
// selectedItems: [readonly] Item[]
// The currently selected items in this tree.
// This property can only be set (via set('selectedItems', ...)) when that item is already
// visible in the tree. (I.e. the tree has already been expanded to show that node.)
// Should generally use `paths` attribute to set the selected items instead.
selectedItems: null,
// selectedItem: [readonly] Item
// Backward compatible singular variant of selectedItems.
selectedItem: null,
// openOnClick: Boolean
// If true, clicking a folder node's label will open it, rather than calling onClick()
openOnClick: false,
// openOnDblClick: Boolean
// If true, double-clicking a folder node's label will open it, rather than calling onDblClick()
openOnDblClick: false,
templateString: dojo.cache("dijit", "templates/Tree.html", "\n"),
// persist: Boolean
// Enables/disables use of cookies for state saving.
persist: true,
// autoExpand: Boolean
// Fully expand the tree on load. Overrides `persist`.
autoExpand: false,
// dndController: [protected] String
// Class name to use as as the dnd controller. Specifying this class enables DnD.
// Generally you should specify this as "dijit.tree.dndSource".
// Default of "dijit.tree._dndSelector" handles selection only (no actual DnD).
dndController: "dijit.tree._dndSelector",
// parameters to pull off of the tree and pass on to the dndController as its params
dndParams: ["onDndDrop","itemCreator","onDndCancel","checkAcceptance", "checkItemAcceptance", "dragThreshold", "betweenThreshold"],
//declare the above items so they can be pulled from the tree's markup
// onDndDrop: [protected] Function
// Parameter to dndController, see `dijit.tree.dndSource.onDndDrop`.
// Generally this doesn't need to be set.
onDndDrop: null,
/*=====
itemCreator: function(nodes, target, source){
// summary:
// Returns objects passed to `Tree.model.newItem()` based on DnD nodes
// dropped onto the tree. Developer must override this method to enable
// dropping from external sources onto this Tree, unless the Tree.model's items
// happen to look like {id: 123, name: "Apple" } with no other attributes.
// description:
// For each node in nodes[], which came from source, create a hash of name/value
// pairs to be passed to Tree.model.newItem(). Returns array of those hashes.
// nodes: DomNode[]
// The DOMNodes dragged from the source container
// target: DomNode
// The target TreeNode.rowNode
// source: dojo.dnd.Source
// The source container the nodes were dragged from, perhaps another Tree or a plain dojo.dnd.Source
// returns: Object[]
// Array of name/value hashes for each new item to be added to the Tree, like:
// | [
// | { id: 123, label: "apple", foo: "bar" },
// | { id: 456, label: "pear", zaz: "bam" }
// | ]
// tags:
// extension
return [{}];
},
=====*/
itemCreator: null,
// onDndCancel: [protected] Function
// Parameter to dndController, see `dijit.tree.dndSource.onDndCancel`.
// Generally this doesn't need to be set.
onDndCancel: null,
/*=====
checkAcceptance: function(source, nodes){
// summary:
// Checks if the Tree itself can accept nodes from this source
// source: dijit.tree._dndSource
// The source which provides items
// nodes: DOMNode[]
// Array of DOM nodes corresponding to nodes being dropped, dijitTreeRow nodes if
// source is a dijit.Tree.
// tags:
// extension
return true; // Boolean
},
=====*/
checkAcceptance: null,
/*=====
checkItemAcceptance: function(target, source, position){
// summary:
// Stub function to be overridden if one wants to check for the ability to drop at the node/item level
// description:
// In the base case, this is called to check if target can become a child of source.
// When betweenThreshold is set, position="before" or "after" means that we
// are asking if the source node can be dropped before/after the target node.
// target: DOMNode
// The dijitTreeRoot DOM node inside of the TreeNode that we are dropping on to
// Use dijit.getEnclosingWidget(target) to get the TreeNode.
// source: dijit.tree.dndSource
// The (set of) nodes we are dropping
// position: String
// "over", "before", or "after"
// tags:
// extension
return true; // Boolean
},
=====*/
checkItemAcceptance: null,
// dragThreshold: Integer
// Number of pixels mouse moves before it's considered the start of a drag operation
dragThreshold: 5,
// betweenThreshold: Integer
// Set to a positive value to allow drag and drop "between" nodes.
//
// If during DnD mouse is over a (target) node but less than betweenThreshold
// pixels from the bottom edge, dropping the the dragged node will make it
// the next sibling of the target node, rather than the child.
//
// Similarly, if mouse is over a target node but less that betweenThreshold
// pixels from the top edge, dropping the dragged node will make it
// the target node's previous sibling rather than the target node's child.
betweenThreshold: 0,
// _nodePixelIndent: Integer
// Number of pixels to indent tree nodes (relative to parent node).
// Default is 19 but can be overridden by setting CSS class dijitTreeIndent
// and calling resize() or startup() on tree after it's in the DOM.
_nodePixelIndent: 19,
_publish: function(/*String*/ topicName, /*Object*/ message){
// summary:
// Publish a message for this widget/topic
dojo.publish(this.id, [dojo.mixin({tree: this, event: topicName}, message || {})]);
},
postMixInProperties: function(){
this.tree = this;
if(this.autoExpand){
// There's little point in saving opened/closed state of nodes for a Tree
// that initially opens all it's nodes.
this.persist = false;
}
this._itemNodesMap={};
if(!this.cookieName){
this.cookieName = this.id + "SaveStateCookie";
}
this._loadDeferred = new dojo.Deferred();
this.inherited(arguments);
},
postCreate: function(){
this._initState();
// Create glue between store and Tree, if not specified directly by user
if(!this.model){
this._store2model();
}
// monitor changes to items
this.connect(this.model, "onChange", "_onItemChange");
this.connect(this.model, "onChildrenChange", "_onItemChildrenChange");
this.connect(this.model, "onDelete", "_onItemDelete");
this._load();
this.inherited(arguments);
if(this.dndController){
if(dojo.isString(this.dndController)){
this.dndController = dojo.getObject(this.dndController);
}
var params={};
for(var i=0; i 0){
// If mouse is over a new TreeNode, then get new TreeNode's position and size
if(!this.targetBox || oldTarget != newTarget){
this.targetBox = dojo.position(newTarget.rowNode, true);
}
if((e.pageY - this.targetBox.y) <= this.betweenThreshold){
newDropPosition = "Before";
}else if((e.pageY - this.targetBox.y) >= (this.targetBox.h - this.betweenThreshold)){
newDropPosition = "After";
}
}
if(newTarget != oldTarget || newDropPosition != oldDropPosition){
if(oldTarget){
this._removeItemClass(oldTarget.rowNode, oldDropPosition);
}
if(newTarget){
this._addItemClass(newTarget.rowNode, newDropPosition);
}
// Check if it's ok to drop the dragged node on/before/after the target node.
if(!newTarget){
m.canDrop(false);
}else if(newTarget == this.tree.rootNode && newDropPosition != "Over"){
// Can't drop before or after tree's root node; the dropped node would just disappear (at least visually)
m.canDrop(false);
}else if(m.source == this && (newTarget.id in this.selection)){
// Guard against dropping onto yourself (TODO: guard against dropping onto your descendant, #7140)
m.canDrop(false);
}else if(this.checkItemAcceptance(newTarget.rowNode, m.source, newDropPosition.toLowerCase())
&& !this._isParentChildDrop(m.source, newTarget.rowNode)){
m.canDrop(true);
}else{
m.canDrop(false);
}
this.targetAnchor = newTarget;
this.dropPosition = newDropPosition;
}
},
onMouseMove: function(e){
// summary:
// Called for any onmousemove events over the Tree
// e: Event
// onmousemouse event
// tags:
// private
if(this.isDragging && this.targetState == "Disabled"){ return; }
this.inherited(arguments);
var m = dojo.dnd.manager();
if(this.isDragging){
this._onDragMouse(e);
}else{
if(this.mouseDown && this.isSource &&
(Math.abs(e.pageX-this._lastX)>=this.dragThreshold || Math.abs(e.pageY-this._lastY)>=this.dragThreshold)){
var nodes = this.getSelectedTreeNodes();
if(nodes.length){
if(nodes.length > 1){
//filter out all selected items which has one of their ancestor selected as well
var seen = this.selection, i = 0, r = [], n, p;
nextitem: while((n = nodes[i++])){
for(p = n.getParent(); p && p !== this.tree; p = p.getParent()){
if(seen[p.id]){ //parent is already selected, skip this node
continue nextitem;
}
}
//this node does not have any ancestors selected, add it
r.push(n);
}
nodes = r;
}
nodes = dojo.map(nodes, function(n){return n.domNode});
m.startDrag(this, nodes, this.copyState(dojo.isCopyKey(e)));
}
}
}
},
onMouseDown: function(e){
// summary:
// Event processor for onmousedown
// e: Event
// onmousedown event
// tags:
// private
this.mouseDown = true;
this.mouseButton = e.button;
this._lastX = e.pageX;
this._lastY = e.pageY;
this.inherited(arguments);
},
onMouseUp: function(e){
// summary:
// Event processor for onmouseup
// e: Event
// onmouseup event
// tags:
// private
if(this.mouseDown){
this.mouseDown = false;
this.inherited(arguments);
}
},
onMouseOut: function(){
// summary:
// Event processor for when mouse is moved away from a TreeNode
// tags:
// private
this.inherited(arguments);
this._unmarkTargetAnchor();
},
checkItemAcceptance: function(target, source, position){
// summary:
// Stub function to be overridden if one wants to check for the ability to drop at the node/item level
// description:
// In the base case, this is called to check if target can become a child of source.
// When betweenThreshold is set, position="before" or "after" means that we
// are asking if the source node can be dropped before/after the target node.
// target: DOMNode
// The dijitTreeRoot DOM node inside of the TreeNode that we are dropping on to
// Use dijit.getEnclosingWidget(target) to get the TreeNode.
// source: dijit.tree.dndSource
// The (set of) nodes we are dropping
// position: String
// "over", "before", or "after"
// tags:
// extension
return true;
},
// topic event processors
onDndSourceOver: function(source){
// summary:
// Topic event processor for /dnd/source/over, called when detected a current source.
// source: Object
// The dijit.tree.dndSource / dojo.dnd.Source which has the mouse over it
// tags:
// private
if(this != source){
this.mouseDown = false;
this._unmarkTargetAnchor();
}else if(this.isDragging){
var m = dojo.dnd.manager();
m.canDrop(false);
}
},
onDndStart: function(source, nodes, copy){
// summary:
// Topic event processor for /dnd/start, called to initiate the DnD operation
// source: Object
// The dijit.tree.dndSource / dojo.dnd.Source which is providing the items
// nodes: DomNode[]
// The list of transferred items, dndTreeNode nodes if dragging from a Tree
// copy: Boolean
// Copy items, if true, move items otherwise
// tags:
// private
if(this.isSource){
this._changeState("Source", this == source ? (copy ? "Copied" : "Moved") : "");
}
var accepted = this.checkAcceptance(source, nodes);
this._changeState("Target", accepted ? "" : "Disabled");
if(this == source){
dojo.dnd.manager().overSource(this);
}
this.isDragging = true;
},
itemCreator: function(/*DomNode[]*/ nodes, target, /*dojo.dnd.Source*/ source){
// summary:
// Returns objects passed to `Tree.model.newItem()` based on DnD nodes
// dropped onto the tree. Developer must override this method to enable
// dropping from external sources onto this Tree, unless the Tree.model's items
// happen to look like {id: 123, name: "Apple" } with no other attributes.
// description:
// For each node in nodes[], which came from source, create a hash of name/value
// pairs to be passed to Tree.model.newItem(). Returns array of those hashes.
// returns: Object[]
// Array of name/value hashes for each new item to be added to the Tree, like:
// | [
// | { id: 123, label: "apple", foo: "bar" },
// | { id: 456, label: "pear", zaz: "bam" }
// | ]
// tags:
// extension
// TODO: for 2.0 refactor so itemCreator() is called once per drag node, and
// make signature itemCreator(sourceItem, node, target) (or similar).
return dojo.map(nodes, function(node){
return {
"id": node.id,
"name": node.textContent || node.innerText || ""
};
}); // Object[]
},
onDndDrop: function(source, nodes, copy){
// summary:
// Topic event processor for /dnd/drop, called to finish the DnD operation.
// description:
// Updates data store items according to where node was dragged from and dropped
// to. The tree will then respond to those data store updates and redraw itself.
// source: Object
// The dijit.tree.dndSource / dojo.dnd.Source which is providing the items
// nodes: DomNode[]
// The list of transferred items, dndTreeNode nodes if dragging from a Tree
// copy: Boolean
// Copy items, if true, move items otherwise
// tags:
// protected
if(this.containerState == "Over"){
var tree = this.tree,
model = tree.model,
target = this.targetAnchor,
requeryRoot = false; // set to true iff top level items change
this.isDragging = false;
// Compute the new parent item
var targetWidget = target;
var newParentItem;
var insertIndex;
newParentItem = (targetWidget && targetWidget.item) || tree.item;
if(this.dropPosition == "Before" || this.dropPosition == "After"){
// TODO: if there is no parent item then disallow the drop.
// Actually this should be checked during onMouseMove too, to make the drag icon red.
newParentItem = (targetWidget.getParent() && targetWidget.getParent().item) || tree.item;
// Compute the insert index for reordering
insertIndex = targetWidget.getIndexInParent();
if(this.dropPosition == "After"){
insertIndex = targetWidget.getIndexInParent() + 1;
}
}else{
newParentItem = (targetWidget && targetWidget.item) || tree.item;
}
// If necessary, use this variable to hold array of hashes to pass to model.newItem()
// (one entry in the array for each dragged node).
var newItemsParams;
dojo.forEach(nodes, function(node, idx){
// dojo.dnd.Item representing the thing being dropped.
// Don't confuse the use of item here (meaning a DnD item) with the
// uses below where item means dojo.data item.
var sourceItem = source.getItem(node.id);
// Information that's available if the source is another Tree
// (possibly but not necessarily this tree, possibly but not
// necessarily the same model as this Tree)
if(dojo.indexOf(sourceItem.type, "treeNode") != -1){
var childTreeNode = sourceItem.data,
childItem = childTreeNode.item,
oldParentItem = childTreeNode.getParent().item;
}
if(source == this){
// This is a node from my own tree, and we are moving it, not copying.
// Remove item from old parent's children attribute.
// TODO: dijit.tree.dndSelector should implement deleteSelectedNodes()
// and this code should go there.
if(typeof insertIndex == "number"){
if(newParentItem == oldParentItem && childTreeNode.getIndexInParent() < insertIndex){
insertIndex -= 1;
}
}
model.pasteItem(childItem, oldParentItem, newParentItem, copy, insertIndex);
}else if(model.isItem(childItem)){
// Item from same model
// (maybe we should only do this branch if the source is a tree?)
model.pasteItem(childItem, oldParentItem, newParentItem, copy, insertIndex);
}else{
// Get the hash to pass to model.newItem(). A single call to
// itemCreator() returns an array of hashes, one for each drag source node.
if(!newItemsParams){
newItemsParams = this.itemCreator(nodes, target.rowNode, source);
}
// Create new item in the tree, based on the drag source.
model.newItem(newItemsParams[idx], newParentItem, insertIndex);
}
}, this);
// Expand the target node (if it's currently collapsed) so the user can see
// where their node was dropped. In particular since that node is still selected.
this.tree._expandNode(targetWidget);
}
this.onDndCancel();
},
onDndCancel: function(){
// summary:
// Topic event processor for /dnd/cancel, called to cancel the DnD operation
// tags:
// private
this._unmarkTargetAnchor();
this.isDragging = false;
this.mouseDown = false;
delete this.mouseButton;
this._changeState("Source", "");
this._changeState("Target", "");
},
// When focus moves in/out of the entire Tree
onOverEvent: function(){
// summary:
// This method is called when mouse is moved over our container (like onmouseenter)
// tags:
// private
this.inherited(arguments);
dojo.dnd.manager().overSource(this);
},
onOutEvent: function(){
// summary:
// This method is called when mouse is moved out of our container (like onmouseleave)
// tags:
// private
this._unmarkTargetAnchor();
var m = dojo.dnd.manager();
if(this.isDragging){
m.canDrop(false);
}
m.outSource(this);
this.inherited(arguments);
},
_isParentChildDrop: function(source, targetRow){
// summary:
// Checks whether the dragged items are parent rows in the tree which are being
// dragged into their own children.
//
// source:
// The DragSource object.
//
// targetRow:
// The tree row onto which the dragged nodes are being dropped.
//
// tags:
// private
// If the dragged object is not coming from the tree this widget belongs to,
// it cannot be invalid.
if(!source.tree || source.tree != this.tree){
return false;
}
var root = source.tree.domNode;
var ids = source.selection;
var node = targetRow.parentNode;
// Iterate up the DOM hierarchy from the target drop row,
// checking of any of the dragged nodes have the same ID.
while(node != root && !ids[node.id]){
node = node.parentNode;
}
return node.id && ids[node.id];
},
_unmarkTargetAnchor: function(){
// summary:
// Removes hover class of the current target anchor
// tags:
// private
if(!this.targetAnchor){ return; }
this._removeItemClass(this.targetAnchor.rowNode, this.dropPosition);
this.targetAnchor = null;
this.targetBox = null;
this.dropPosition = null;
},
_markDndStatus: function(copy){
// summary:
// Changes source's state based on "copy" status
this._changeState("Source", copy ? "Copied" : "Moved");
}
});
}
if(!dojo._hasResource["dojo.data.ItemFileReadStore"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dojo.data.ItemFileReadStore"] = true;
dojo.provide("dojo.data.ItemFileReadStore");
dojo.declare("dojo.data.ItemFileReadStore", null,{
// summary:
// The ItemFileReadStore implements the dojo.data.api.Read API and reads
// data from JSON files that have contents in this format --
// { items: [
// { name:'Kermit', color:'green', age:12, friends:['Gonzo', {_reference:{name:'Fozzie Bear'}}]},
// { name:'Fozzie Bear', wears:['hat', 'tie']},
// { name:'Miss Piggy', pets:'Foo-Foo'}
// ]}
// Note that it can also contain an 'identifer' property that specified which attribute on the items
// in the array of items that acts as the unique identifier for that item.
//
constructor: function(/* Object */ keywordParameters){
// summary: constructor
// keywordParameters: {url: String}
// keywordParameters: {data: jsonObject}
// keywordParameters: {typeMap: object)
// The structure of the typeMap object is as follows:
// {
// type0: function || object,
// type1: function || object,
// ...
// typeN: function || object
// }
// Where if it is a function, it is assumed to be an object constructor that takes the
// value of _value as the initialization parameters. If it is an object, then it is assumed
// to be an object of general form:
// {
// type: function, //constructor.
// deserialize: function(value) //The function that parses the value and constructs the object defined by type appropriately.
// }
this._arrayOfAllItems = [];
this._arrayOfTopLevelItems = [];
this._loadFinished = false;
this._jsonFileUrl = keywordParameters.url;
this._ccUrl = keywordParameters.url;
this.url = keywordParameters.url;
this._jsonData = keywordParameters.data;
this.data = null;
this._datatypeMap = keywordParameters.typeMap || {};
if(!this._datatypeMap['Date']){
//If no default mapping for dates, then set this as default.
//We use the dojo.date.stamp here because the ISO format is the 'dojo way'
//of generically representing dates.
this._datatypeMap['Date'] = {
type: Date,
deserialize: function(value){
return dojo.date.stamp.fromISOString(value);
}
};
}
this._features = {'dojo.data.api.Read':true, 'dojo.data.api.Identity':true};
this._itemsByIdentity = null;
this._storeRefPropName = "_S"; // Default name for the store reference to attach to every item.
this._itemNumPropName = "_0"; // Default Item Id for isItem to attach to every item.
this._rootItemPropName = "_RI"; // Default Item Id for isItem to attach to every item.
this._reverseRefMap = "_RRM"; // Default attribute for constructing a reverse reference map for use with reference integrity
this._loadInProgress = false; //Got to track the initial load to prevent duelling loads of the dataset.
this._queuedFetches = [];
if(keywordParameters.urlPreventCache !== undefined){
this.urlPreventCache = keywordParameters.urlPreventCache?true:false;
}
if(keywordParameters.hierarchical !== undefined){
this.hierarchical = keywordParameters.hierarchical?true:false;
}
if(keywordParameters.clearOnClose){
this.clearOnClose = true;
}
if("failOk" in keywordParameters){
this.failOk = keywordParameters.failOk?true:false;
}
},
url: "", // use "" rather than undefined for the benefit of the parser (#3539)
//Internal var, crossCheckUrl. Used so that setting either url or _jsonFileUrl, can still trigger a reload
//when clearOnClose and close is used.
_ccUrl: "",
data: null, // define this so that the parser can populate it
typeMap: null, //Define so parser can populate.
//Parameter to allow users to specify if a close call should force a reload or not.
//By default, it retains the old behavior of not clearing if close is called. But
//if set true, the store will be reset to default state. Note that by doing this,
//all item handles will become invalid and a new fetch must be issued.
clearOnClose: false,
//Parameter to allow specifying if preventCache should be passed to the xhrGet call or not when loading data from a url.
//Note this does not mean the store calls the server on each fetch, only that the data load has preventCache set as an option.
//Added for tracker: #6072
urlPreventCache: false,
//Parameter for specifying that it is OK for the xhrGet call to fail silently.
failOk: false,
//Parameter to indicate to process data from the url as hierarchical
//(data items can contain other data items in js form). Default is true
//for backwards compatibility. False means only root items are processed
//as items, all child objects outside of type-mapped objects and those in
//specific reference format, are left straight JS data objects.
hierarchical: true,
_assertIsItem: function(/* item */ item){
// summary:
// This function tests whether the item passed in is indeed an item in the store.
// item:
// The item to test for being contained by the store.
if(!this.isItem(item)){
throw new Error("dojo.data.ItemFileReadStore: Invalid item argument.");
}
},
_assertIsAttribute: function(/* attribute-name-string */ attribute){
// summary:
// This function tests whether the item passed in is indeed a valid 'attribute' like type for the store.
// attribute:
// The attribute to test for being contained by the store.
if(typeof attribute !== "string"){
throw new Error("dojo.data.ItemFileReadStore: Invalid attribute argument.");
}
},
getValue: function( /* item */ item,
/* attribute-name-string */ attribute,
/* value? */ defaultValue){
// summary:
// See dojo.data.api.Read.getValue()
var values = this.getValues(item, attribute);
return (values.length > 0)?values[0]:defaultValue; // mixed
},
getValues: function(/* item */ item,
/* attribute-name-string */ attribute){
// summary:
// See dojo.data.api.Read.getValues()
this._assertIsItem(item);
this._assertIsAttribute(attribute);
// Clone it before returning. refs: #10474
return (item[attribute] || []).slice(0); // Array
},
getAttributes: function(/* item */ item){
// summary:
// See dojo.data.api.Read.getAttributes()
this._assertIsItem(item);
var attributes = [];
for(var key in item){
// Save off only the real item attributes, not the special id marks for O(1) isItem.
if((key !== this._storeRefPropName) && (key !== this._itemNumPropName) && (key !== this._rootItemPropName) && (key !== this._reverseRefMap)){
attributes.push(key);
}
}
return attributes; // Array
},
hasAttribute: function( /* item */ item,
/* attribute-name-string */ attribute){
// summary:
// See dojo.data.api.Read.hasAttribute()
this._assertIsItem(item);
this._assertIsAttribute(attribute);
return (attribute in item);
},
containsValue: function(/* item */ item,
/* attribute-name-string */ attribute,
/* anything */ value){
// summary:
// See dojo.data.api.Read.containsValue()
var regexp = undefined;
if(typeof value === "string"){
regexp = dojo.data.util.filter.patternToRegExp(value, false);
}
return this._containsValue(item, attribute, value, regexp); //boolean.
},
_containsValue: function( /* item */ item,
/* attribute-name-string */ attribute,
/* anything */ value,
/* RegExp?*/ regexp){
// summary:
// Internal function for looking at the values contained by the item.
// description:
// Internal function for looking at the values contained by the item. This
// function allows for denoting if the comparison should be case sensitive for
// strings or not (for handling filtering cases where string case should not matter)
//
// item:
// The data item to examine for attribute values.
// attribute:
// The attribute to inspect.
// value:
// The value to match.
// regexp:
// Optional regular expression generated off value if value was of string type to handle wildcarding.
// If present and attribute values are string, then it can be used for comparison instead of 'value'
return dojo.some(this.getValues(item, attribute), function(possibleValue){
if(possibleValue !== null && !dojo.isObject(possibleValue) && regexp){
if(possibleValue.toString().match(regexp)){
return true; // Boolean
}
}else if(value === possibleValue){
return true; // Boolean
}
});
},
isItem: function(/* anything */ something){
// summary:
// See dojo.data.api.Read.isItem()
if(something && something[this._storeRefPropName] === this){
if(this._arrayOfAllItems[something[this._itemNumPropName]] === something){
return true;
}
}
return false; // Boolean
},
isItemLoaded: function(/* anything */ something){
// summary:
// See dojo.data.api.Read.isItemLoaded()
return this.isItem(something); //boolean
},
loadItem: function(/* object */ keywordArgs){
// summary:
// See dojo.data.api.Read.loadItem()
this._assertIsItem(keywordArgs.item);
},
getFeatures: function(){
// summary:
// See dojo.data.api.Read.getFeatures()
return this._features; //Object
},
getLabel: function(/* item */ item){
// summary:
// See dojo.data.api.Read.getLabel()
if(this._labelAttr && this.isItem(item)){
return this.getValue(item,this._labelAttr); //String
}
return undefined; //undefined
},
getLabelAttributes: function(/* item */ item){
// summary:
// See dojo.data.api.Read.getLabelAttributes()
if(this._labelAttr){
return [this._labelAttr]; //array
}
return null; //null
},
_fetchItems: function( /* Object */ keywordArgs,
/* Function */ findCallback,
/* Function */ errorCallback){
// summary:
// See dojo.data.util.simpleFetch.fetch()
var self = this,
filter = function(requestArgs, arrayOfItems){
var items = [],
i, key;
if(requestArgs.query){
var value,
ignoreCase = requestArgs.queryOptions ? requestArgs.queryOptions.ignoreCase : false;
//See if there are any string values that can be regexp parsed first to avoid multiple regexp gens on the
//same value for each item examined. Much more efficient.
var regexpList = {};
for(key in requestArgs.query){
value = requestArgs.query[key];
if(typeof value === "string"){
regexpList[key] = dojo.data.util.filter.patternToRegExp(value, ignoreCase);
}else if(value instanceof RegExp){
regexpList[key] = value;
}
}
for(i = 0; i < arrayOfItems.length; ++i){
var match = true;
var candidateItem = arrayOfItems[i];
if(candidateItem === null){
match = false;
}else{
for(key in requestArgs.query){
value = requestArgs.query[key];
if(!self._containsValue(candidateItem, key, value, regexpList[key])){
match = false;
}
}
}
if(match){
items.push(candidateItem);
}
}
findCallback(items, requestArgs);
}else{
// We want a copy to pass back in case the parent wishes to sort the array.
// We shouldn't allow resort of the internal list, so that multiple callers
// can get lists and sort without affecting each other. We also need to
// filter out any null values that have been left as a result of deleteItem()
// calls in ItemFileWriteStore.
for(i = 0; i < arrayOfItems.length; ++i){
var item = arrayOfItems[i];
if(item !== null){
items.push(item);
}
}
findCallback(items, requestArgs);
}
};
if(this._loadFinished){
filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions));
}else{
//Do a check on the JsonFileUrl and crosscheck it.
//If it doesn't match the cross-check, it needs to be updated
//This allows for either url or _jsonFileUrl to he changed to
//reset the store load location. Done this way for backwards
//compatibility. People use _jsonFileUrl (even though officially
//private.
if(this._jsonFileUrl !== this._ccUrl){
dojo.deprecated("dojo.data.ItemFileReadStore: ",
"To change the url, set the url property of the store," +
" not _jsonFileUrl. _jsonFileUrl support will be removed in 2.0");
this._ccUrl = this._jsonFileUrl;
this.url = this._jsonFileUrl;
}else if(this.url !== this._ccUrl){
this._jsonFileUrl = this.url;
this._ccUrl = this.url;
}
//See if there was any forced reset of data.
if(this.data != null){
this._jsonData = this.data;
this.data = null;
}
if(this._jsonFileUrl){
//If fetches come in before the loading has finished, but while
//a load is in progress, we have to defer the fetching to be
//invoked in the callback.
if(this._loadInProgress){
this._queuedFetches.push({args: keywordArgs, filter: filter});
}else{
this._loadInProgress = true;
var getArgs = {
url: self._jsonFileUrl,
handleAs: "json-comment-optional",
preventCache: this.urlPreventCache,
failOk: this.failOk
};
var getHandler = dojo.xhrGet(getArgs);
getHandler.addCallback(function(data){
try{
self._getItemsFromLoadedData(data);
self._loadFinished = true;
self._loadInProgress = false;
filter(keywordArgs, self._getItemsArray(keywordArgs.queryOptions));
self._handleQueuedFetches();
}catch(e){
self._loadFinished = true;
self._loadInProgress = false;
errorCallback(e, keywordArgs);
}
});
getHandler.addErrback(function(error){
self._loadInProgress = false;
errorCallback(error, keywordArgs);
});
//Wire up the cancel to abort of the request
//This call cancel on the deferred if it hasn't been called
//yet and then will chain to the simple abort of the
//simpleFetch keywordArgs
var oldAbort = null;
if(keywordArgs.abort){
oldAbort = keywordArgs.abort;
}
keywordArgs.abort = function(){
var df = getHandler;
if(df && df.fired === -1){
df.cancel();
df = null;
}
if(oldAbort){
oldAbort.call(keywordArgs);
}
};
}
}else if(this._jsonData){
try{
this._loadFinished = true;
this._getItemsFromLoadedData(this._jsonData);
this._jsonData = null;
filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions));
}catch(e){
errorCallback(e, keywordArgs);
}
}else{
errorCallback(new Error("dojo.data.ItemFileReadStore: No JSON source data was provided as either URL or a nested Javascript object."), keywordArgs);
}
}
},
_handleQueuedFetches: function(){
// summary:
// Internal function to execute delayed request in the store.
//Execute any deferred fetches now.
if(this._queuedFetches.length > 0){
for(var i = 0; i < this._queuedFetches.length; i++){
var fData = this._queuedFetches[i],
delayedQuery = fData.args,
delayedFilter = fData.filter;
if(delayedFilter){
delayedFilter(delayedQuery, this._getItemsArray(delayedQuery.queryOptions));
}else{
this.fetchItemByIdentity(delayedQuery);
}
}
this._queuedFetches = [];
}
},
_getItemsArray: function(/*object?*/queryOptions){
// summary:
// Internal function to determine which list of items to search over.
// queryOptions: The query options parameter, if any.
if(queryOptions && queryOptions.deep){
return this._arrayOfAllItems;
}
return this._arrayOfTopLevelItems;
},
close: function(/*dojo.data.api.Request || keywordArgs || null */ request){
// summary:
// See dojo.data.api.Read.close()
if(this.clearOnClose &&
this._loadFinished &&
!this._loadInProgress){
//Reset all internalsback to default state. This will force a reload
//on next fetch. This also checks that the data or url param was set
//so that the store knows it can get data. Without one of those being set,
//the next fetch will trigger an error.
if(((this._jsonFileUrl == "" || this._jsonFileUrl == null) &&
(this.url == "" || this.url == null)
) && this.data == null){
console.debug("dojo.data.ItemFileReadStore: WARNING! Data reload " +
" information has not been provided." +
" Please set 'url' or 'data' to the appropriate value before" +
" the next fetch");
}
this._arrayOfAllItems = [];
this._arrayOfTopLevelItems = [];
this._loadFinished = false;
this._itemsByIdentity = null;
this._loadInProgress = false;
this._queuedFetches = [];
}
},
_getItemsFromLoadedData: function(/* Object */ dataObject){
// summary:
// Function to parse the loaded data into item format and build the internal items array.
// description:
// Function to parse the loaded data into item format and build the internal items array.
//
// dataObject:
// The JS data object containing the raw data to convery into item format.
//
// returns: array
// Array of items in store item format.
// First, we define a couple little utility functions...
var addingArrays = false,
self = this;
function valueIsAnItem(/* anything */ aValue){
// summary:
// Given any sort of value that could be in the raw json data,
// return true if we should interpret the value as being an
// item itself, rather than a literal value or a reference.
// example:
// | false == valueIsAnItem("Kermit");
// | false == valueIsAnItem(42);
// | false == valueIsAnItem(new Date());
// | false == valueIsAnItem({_type:'Date', _value:'1802-05-14'});
// | false == valueIsAnItem({_reference:'Kermit'});
// | true == valueIsAnItem({name:'Kermit', color:'green'});
// | true == valueIsAnItem({iggy:'pop'});
// | true == valueIsAnItem({foo:42});
var isItem = (
(aValue !== null) &&
(typeof aValue === "object") &&
(!dojo.isArray(aValue) || addingArrays) &&
(!dojo.isFunction(aValue)) &&
(aValue.constructor == Object || dojo.isArray(aValue)) &&
(typeof aValue._reference === "undefined") &&
(typeof aValue._type === "undefined") &&
(typeof aValue._value === "undefined") &&
self.hierarchical
);
return isItem;
}
function addItemAndSubItemsToArrayOfAllItems(/* Item */ anItem){
self._arrayOfAllItems.push(anItem);
for(var attribute in anItem){
var valueForAttribute = anItem[attribute];
if(valueForAttribute){
if(dojo.isArray(valueForAttribute)){
var valueArray = valueForAttribute;
for(var k = 0; k < valueArray.length; ++k){
var singleValue = valueArray[k];
if(valueIsAnItem(singleValue)){
addItemAndSubItemsToArrayOfAllItems(singleValue);
}
}
}else{
if(valueIsAnItem(valueForAttribute)){
addItemAndSubItemsToArrayOfAllItems(valueForAttribute);
}
}
}
}
}
this._labelAttr = dataObject.label;
// We need to do some transformations to convert the data structure
// that we read from the file into a format that will be convenient
// to work with in memory.
// Step 1: Walk through the object hierarchy and build a list of all items
var i,
item;
this._arrayOfAllItems = [];
this._arrayOfTopLevelItems = dataObject.items;
for(i = 0; i < this._arrayOfTopLevelItems.length; ++i){
item = this._arrayOfTopLevelItems[i];
if(dojo.isArray(item)){
addingArrays = true;
}
addItemAndSubItemsToArrayOfAllItems(item);
item[this._rootItemPropName]=true;
}
// Step 2: Walk through all the attribute values of all the items,
// and replace single values with arrays. For example, we change this:
// { name:'Miss Piggy', pets:'Foo-Foo'}
// into this:
// { name:['Miss Piggy'], pets:['Foo-Foo']}
//
// We also store the attribute names so we can validate our store
// reference and item id special properties for the O(1) isItem
var allAttributeNames = {},
key;
for(i = 0; i < this._arrayOfAllItems.length; ++i){
item = this._arrayOfAllItems[i];
for(key in item){
if(key !== this._rootItemPropName){
var value = item[key];
if(value !== null){
if(!dojo.isArray(value)){
item[key] = [value];
}
}else{
item[key] = [null];
}
}
allAttributeNames[key]=key;
}
}
// Step 3: Build unique property names to use for the _storeRefPropName and _itemNumPropName
// This should go really fast, it will generally never even run the loop.
while(allAttributeNames[this._storeRefPropName]){
this._storeRefPropName += "_";
}
while(allAttributeNames[this._itemNumPropName]){
this._itemNumPropName += "_";
}
while(allAttributeNames[this._reverseRefMap]){
this._reverseRefMap += "_";
}
// Step 4: Some data files specify an optional 'identifier', which is
// the name of an attribute that holds the identity of each item.
// If this data file specified an identifier attribute, then build a
// hash table of items keyed by the identity of the items.
var arrayOfValues;
var identifier = dataObject.identifier;
if(identifier){
this._itemsByIdentity = {};
this._features['dojo.data.api.Identity'] = identifier;
for(i = 0; i < this._arrayOfAllItems.length; ++i){
item = this._arrayOfAllItems[i];
arrayOfValues = item[identifier];
var identity = arrayOfValues[0];
if(!Object.hasOwnProperty.call(this._itemsByIdentity, identity)){
this._itemsByIdentity[identity] = item;
}else{
if(this._jsonFileUrl){
throw new Error("dojo.data.ItemFileReadStore: The json data as specified by: [" + this._jsonFileUrl + "] is malformed. Items within the list have identifier: [" + identifier + "]. Value collided: [" + identity + "]");
}else if(this._jsonData){
throw new Error("dojo.data.ItemFileReadStore: The json data provided by the creation arguments is malformed. Items within the list have identifier: [" + identifier + "]. Value collided: [" + identity + "]");
}
}
}
}else{
this._features['dojo.data.api.Identity'] = Number;
}
// Step 5: Walk through all the items, and set each item's properties
// for _storeRefPropName and _itemNumPropName, so that store.isItem() will return true.
for(i = 0; i < this._arrayOfAllItems.length; ++i){
item = this._arrayOfAllItems[i];
item[this._storeRefPropName] = this;
item[this._itemNumPropName] = i;
}
// Step 6: We walk through all the attribute values of all the items,
// looking for type/value literals and item-references.
//
// We replace item-references with pointers to items. For example, we change:
// { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] }
// into this:
// { name:['Kermit'], friends:[miss_piggy] }
// (where miss_piggy is the object representing the 'Miss Piggy' item).
//
// We replace type/value pairs with typed-literals. For example, we change:
// { name:['Nelson Mandela'], born:[{_type:'Date', _value:'1918-07-18'}] }
// into this:
// { name:['Kermit'], born:(new Date(1918, 6, 18)) }
//
// We also generate the associate map for all items for the O(1) isItem function.
for(i = 0; i < this._arrayOfAllItems.length; ++i){
item = this._arrayOfAllItems[i]; // example: { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] }
for(key in item){
arrayOfValues = item[key]; // example: [{_reference:{name:'Miss Piggy'}}]
for(var j = 0; j < arrayOfValues.length; ++j){
value = arrayOfValues[j]; // example: {_reference:{name:'Miss Piggy'}}
if(value !== null && typeof value == "object"){
if(("_type" in value) && ("_value" in value)){
var type = value._type; // examples: 'Date', 'Color', or 'ComplexNumber'
var mappingObj = this._datatypeMap[type]; // examples: Date, dojo.Color, foo.math.ComplexNumber, {type: dojo.Color, deserialize(value){ return new dojo.Color(value)}}
if(!mappingObj){
throw new Error("dojo.data.ItemFileReadStore: in the typeMap constructor arg, no object class was specified for the datatype '" + type + "'");
}else if(dojo.isFunction(mappingObj)){
arrayOfValues[j] = new mappingObj(value._value);
}else if(dojo.isFunction(mappingObj.deserialize)){
arrayOfValues[j] = mappingObj.deserialize(value._value);
}else{
throw new Error("dojo.data.ItemFileReadStore: Value provided in typeMap was neither a constructor, nor a an object with a deserialize function");
}
}
if(value._reference){
var referenceDescription = value._reference; // example: {name:'Miss Piggy'}
if(!dojo.isObject(referenceDescription)){
// example: 'Miss Piggy'
// from an item like: { name:['Kermit'], friends:[{_reference:'Miss Piggy'}]}
arrayOfValues[j] = this._getItemByIdentity(referenceDescription);
}else{
// example: {name:'Miss Piggy'}
// from an item like: { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] }
for(var k = 0; k < this._arrayOfAllItems.length; ++k){
var candidateItem = this._arrayOfAllItems[k],
found = true;
for(var refKey in referenceDescription){
if(candidateItem[refKey] != referenceDescription[refKey]){
found = false;
}
}
if(found){
arrayOfValues[j] = candidateItem;
}
}
}
if(this.referenceIntegrity){
var refItem = arrayOfValues[j];
if(this.isItem(refItem)){
this._addReferenceToMap(refItem, item, key);
}
}
}else if(this.isItem(value)){
//It's a child item (not one referenced through _reference).
//We need to treat this as a referenced item, so it can be cleaned up
//in a write store easily.
if(this.referenceIntegrity){
this._addReferenceToMap(value, item, key);
}
}
}
}
}
}
},
_addReferenceToMap: function(/*item*/ refItem, /*item*/ parentItem, /*string*/ attribute){
// summary:
// Method to add an reference map entry for an item and attribute.
// description:
// Method to add an reference map entry for an item and attribute. //
// refItem:
// The item that is referenced.
// parentItem:
// The item that holds the new reference to refItem.
// attribute:
// The attribute on parentItem that contains the new reference.
//Stub function, does nothing. Real processing is in ItemFileWriteStore.
},
getIdentity: function(/* item */ item){
// summary:
// See dojo.data.api.Identity.getIdentity()
var identifier = this._features['dojo.data.api.Identity'];
if(identifier === Number){
return item[this._itemNumPropName]; // Number
}else{
var arrayOfValues = item[identifier];
if(arrayOfValues){
return arrayOfValues[0]; // Object || String
}
}
return null; // null
},
fetchItemByIdentity: function(/* Object */ keywordArgs){
// summary:
// See dojo.data.api.Identity.fetchItemByIdentity()
// Hasn't loaded yet, we have to trigger the load.
var item,
scope;
if(!this._loadFinished){
var self = this;
//Do a check on the JsonFileUrl and crosscheck it.
//If it doesn't match the cross-check, it needs to be updated
//This allows for either url or _jsonFileUrl to he changed to
//reset the store load location. Done this way for backwards
//compatibility. People use _jsonFileUrl (even though officially
//private.
if(this._jsonFileUrl !== this._ccUrl){
dojo.deprecated("dojo.data.ItemFileReadStore: ",
"To change the url, set the url property of the store," +
" not _jsonFileUrl. _jsonFileUrl support will be removed in 2.0");
this._ccUrl = this._jsonFileUrl;
this.url = this._jsonFileUrl;
}else if(this.url !== this._ccUrl){
this._jsonFileUrl = this.url;
this._ccUrl = this.url;
}
//See if there was any forced reset of data.
if(this.data != null && this._jsonData == null){
this._jsonData = this.data;
this.data = null;
}
if(this._jsonFileUrl){
if(this._loadInProgress){
this._queuedFetches.push({args: keywordArgs});
}else{
this._loadInProgress = true;
var getArgs = {
url: self._jsonFileUrl,
handleAs: "json-comment-optional",
preventCache: this.urlPreventCache,
failOk: this.failOk
};
var getHandler = dojo.xhrGet(getArgs);
getHandler.addCallback(function(data){
var scope = keywordArgs.scope?keywordArgs.scope:dojo.global;
try{
self._getItemsFromLoadedData(data);
self._loadFinished = true;
self._loadInProgress = false;
item = self._getItemByIdentity(keywordArgs.identity);
if(keywordArgs.onItem){
keywordArgs.onItem.call(scope, item);
}
self._handleQueuedFetches();
}catch(error){
self._loadInProgress = false;
if(keywordArgs.onError){
keywordArgs.onError.call(scope, error);
}
}
});
getHandler.addErrback(function(error){
self._loadInProgress = false;
if(keywordArgs.onError){
var scope = keywordArgs.scope?keywordArgs.scope:dojo.global;
keywordArgs.onError.call(scope, error);
}
});
}
}else if(this._jsonData){
// Passed in data, no need to xhr.
self._getItemsFromLoadedData(self._jsonData);
self._jsonData = null;
self._loadFinished = true;
item = self._getItemByIdentity(keywordArgs.identity);
if(keywordArgs.onItem){
scope = keywordArgs.scope?keywordArgs.scope:dojo.global;
keywordArgs.onItem.call(scope, item);
}
}
}else{
// Already loaded. We can just look it up and call back.
item = this._getItemByIdentity(keywordArgs.identity);
if(keywordArgs.onItem){
scope = keywordArgs.scope?keywordArgs.scope:dojo.global;
keywordArgs.onItem.call(scope, item);
}
}
},
_getItemByIdentity: function(/* Object */ identity){
// summary:
// Internal function to look an item up by its identity map.
var item = null;
if(this._itemsByIdentity &&
Object.hasOwnProperty.call(this._itemsByIdentity, identity)){
item = this._itemsByIdentity[identity];
}else if (Object.hasOwnProperty.call(this._arrayOfAllItems, identity)){
item = this._arrayOfAllItems[identity];
}
if(item === undefined){
item = null;
}
return item; // Object
},
getIdentityAttributes: function(/* item */ item){
// summary:
// See dojo.data.api.Identity.getIdentityAttributes()
var identifier = this._features['dojo.data.api.Identity'];
if(identifier === Number){
// If (identifier === Number) it means getIdentity() just returns
// an integer item-number for each item. The dojo.data.api.Identity
// spec says we need to return null if the identity is not composed
// of attributes
return null; // null
}else{
return [identifier]; // Array
}
},
_forceLoad: function(){
// summary:
// Internal function to force a load of the store if it hasn't occurred yet. This is required
// for specific functions to work properly.
var self = this;
//Do a check on the JsonFileUrl and crosscheck it.
//If it doesn't match the cross-check, it needs to be updated
//This allows for either url or _jsonFileUrl to he changed to
//reset the store load location. Done this way for backwards
//compatibility. People use _jsonFileUrl (even though officially
//private.
if(this._jsonFileUrl !== this._ccUrl){
dojo.deprecated("dojo.data.ItemFileReadStore: ",
"To change the url, set the url property of the store," +
" not _jsonFileUrl. _jsonFileUrl support will be removed in 2.0");
this._ccUrl = this._jsonFileUrl;
this.url = this._jsonFileUrl;
}else if(this.url !== this._ccUrl){
this._jsonFileUrl = this.url;
this._ccUrl = this.url;
}
//See if there was any forced reset of data.
if(this.data != null){
this._jsonData = this.data;
this.data = null;
}
if(this._jsonFileUrl){
var getArgs = {
url: this._jsonFileUrl,
handleAs: "json-comment-optional",
preventCache: this.urlPreventCache,
failOk: this.failOk,
sync: true
};
var getHandler = dojo.xhrGet(getArgs);
getHandler.addCallback(function(data){
try{
//Check to be sure there wasn't another load going on concurrently
//So we don't clobber data that comes in on it. If there is a load going on
//then do not save this data. It will potentially clobber current data.
//We mainly wanted to sync/wait here.
//TODO: Revisit the loading scheme of this store to improve multi-initial
//request handling.
if(self._loadInProgress !== true && !self._loadFinished){
self._getItemsFromLoadedData(data);
self._loadFinished = true;
}else if(self._loadInProgress){
//Okay, we hit an error state we can't recover from. A forced load occurred
//while an async load was occurring. Since we cannot block at this point, the best
//that can be managed is to throw an error.
throw new Error("dojo.data.ItemFileReadStore: Unable to perform a synchronous load, an async load is in progress.");
}
}catch(e){
console.log(e);
throw e;
}
});
getHandler.addErrback(function(error){
throw error;
});
}else if(this._jsonData){
self._getItemsFromLoadedData(self._jsonData);
self._jsonData = null;
self._loadFinished = true;
}
}
});
//Mix in the simple fetch implementation to this class.
dojo.extend(dojo.data.ItemFileReadStore,dojo.data.util.simpleFetch);
}
if(!dojo._hasResource["dojo.data.ItemFileWriteStore"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dojo.data.ItemFileWriteStore"] = true;
dojo.provide("dojo.data.ItemFileWriteStore");
dojo.declare("dojo.data.ItemFileWriteStore", dojo.data.ItemFileReadStore, {
constructor: function(/* object */ keywordParameters){
// keywordParameters: {typeMap: object)
// The structure of the typeMap object is as follows:
// {
// type0: function || object,
// type1: function || object,
// ...
// typeN: function || object
// }
// Where if it is a function, it is assumed to be an object constructor that takes the
// value of _value as the initialization parameters. It is serialized assuming object.toString()
// serialization. If it is an object, then it is assumed
// to be an object of general form:
// {
// type: function, //constructor.
// deserialize: function(value) //The function that parses the value and constructs the object defined by type appropriately.
// serialize: function(object) //The function that converts the object back into the proper file format form.
// }
// ItemFileWriteStore extends ItemFileReadStore to implement these additional dojo.data APIs
this._features['dojo.data.api.Write'] = true;
this._features['dojo.data.api.Notification'] = true;
// For keeping track of changes so that we can implement isDirty and revert
this._pending = {
_newItems:{},
_modifiedItems:{},
_deletedItems:{}
};
if(!this._datatypeMap['Date'].serialize){
this._datatypeMap['Date'].serialize = function(obj){
return dojo.date.stamp.toISOString(obj, {zulu:true});
};
}
//Disable only if explicitly set to false.
if(keywordParameters && (keywordParameters.referenceIntegrity === false)){
this.referenceIntegrity = false;
}
// this._saveInProgress is set to true, briefly, from when save() is first called to when it completes
this._saveInProgress = false;
},
referenceIntegrity: true, //Flag that defaultly enabled reference integrity tracking. This way it can also be disabled pogrammatially or declaratively.
_assert: function(/* boolean */ condition){
if(!condition){
throw new Error("assertion failed in ItemFileWriteStore");
}
},
_getIdentifierAttribute: function(){
var identifierAttribute = this.getFeatures()['dojo.data.api.Identity'];
// this._assert((identifierAttribute === Number) || (dojo.isString(identifierAttribute)));
return identifierAttribute;
},
/* dojo.data.api.Write */
newItem: function(/* Object? */ keywordArgs, /* Object? */ parentInfo){
// summary: See dojo.data.api.Write.newItem()
this._assert(!this._saveInProgress);
if(!this._loadFinished){
// We need to do this here so that we'll be able to find out what
// identifierAttribute was specified in the data file.
this._forceLoad();
}
if(typeof keywordArgs != "object" && typeof keywordArgs != "undefined"){
throw new Error("newItem() was passed something other than an object");
}
var newIdentity = null;
var identifierAttribute = this._getIdentifierAttribute();
if(identifierAttribute === Number){
newIdentity = this._arrayOfAllItems.length;
}else{
newIdentity = keywordArgs[identifierAttribute];
if(typeof newIdentity === "undefined"){
throw new Error("newItem() was not passed an identity for the new item");
}
if(dojo.isArray(newIdentity)){
throw new Error("newItem() was not passed an single-valued identity");
}
}
// make sure this identity is not already in use by another item, if identifiers were
// defined in the file. Otherwise it would be the item count,
// which should always be unique in this case.
if(this._itemsByIdentity){
this._assert(typeof this._itemsByIdentity[newIdentity] === "undefined");
}
this._assert(typeof this._pending._newItems[newIdentity] === "undefined");
this._assert(typeof this._pending._deletedItems[newIdentity] === "undefined");
var newItem = {};
newItem[this._storeRefPropName] = this;
newItem[this._itemNumPropName] = this._arrayOfAllItems.length;
if(this._itemsByIdentity){
this._itemsByIdentity[newIdentity] = newItem;
//We have to set the identifier now, otherwise we can't look it
//up at calls to setValueorValues in parentInfo handling.
newItem[identifierAttribute] = [newIdentity];
}
this._arrayOfAllItems.push(newItem);
//We need to construct some data for the onNew call too...
var pInfo = null;
// Now we need to check to see where we want to assign this thingm if any.
if(parentInfo && parentInfo.parent && parentInfo.attribute){
pInfo = {
item: parentInfo.parent,
attribute: parentInfo.attribute,
oldValue: undefined
};
//See if it is multi-valued or not and handle appropriately
//Generally, all attributes are multi-valued for this store
//So, we only need to append if there are already values present.
var values = this.getValues(parentInfo.parent, parentInfo.attribute);
if(values && values.length > 0){
var tempValues = values.slice(0, values.length);
if(values.length === 1){
pInfo.oldValue = values[0];
}else{
pInfo.oldValue = values.slice(0, values.length);
}
tempValues.push(newItem);
this._setValueOrValues(parentInfo.parent, parentInfo.attribute, tempValues, false);
pInfo.newValue = this.getValues(parentInfo.parent, parentInfo.attribute);
}else{
this._setValueOrValues(parentInfo.parent, parentInfo.attribute, newItem, false);
pInfo.newValue = newItem;
}
}else{
//Toplevel item, add to both top list as well as all list.
newItem[this._rootItemPropName]=true;
this._arrayOfTopLevelItems.push(newItem);
}
this._pending._newItems[newIdentity] = newItem;
//Clone over the properties to the new item
for(var key in keywordArgs){
if(key === this._storeRefPropName || key === this._itemNumPropName){
// Bummer, the user is trying to do something like
// newItem({_S:"foo"}). Unfortunately, our superclass,
// ItemFileReadStore, is already using _S in each of our items
// to hold private info. To avoid a naming collision, we
// need to move all our private info to some other property
// of all the items/objects. So, we need to iterate over all
// the items and do something like:
// item.__S = item._S;
// item._S = undefined;
// But first we have to make sure the new "__S" variable is
// not in use, which means we have to iterate over all the
// items checking for that.
throw new Error("encountered bug in ItemFileWriteStore.newItem");
}
var value = keywordArgs[key];
if(!dojo.isArray(value)){
value = [value];
}
newItem[key] = value;
if(this.referenceIntegrity){
for(var i = 0; i < value.length; i++){
var val = value[i];
if(this.isItem(val)){
this._addReferenceToMap(val, newItem, key);
}
}
}
}
this.onNew(newItem, pInfo); // dojo.data.api.Notification call
return newItem; // item
},
_removeArrayElement: function(/* Array */ array, /* anything */ element){
var index = dojo.indexOf(array, element);
if(index != -1){
array.splice(index, 1);
return true;
}
return false;
},
deleteItem: function(/* item */ item){
// summary: See dojo.data.api.Write.deleteItem()
this._assert(!this._saveInProgress);
this._assertIsItem(item);
// Remove this item from the _arrayOfAllItems, but leave a null value in place
// of the item, so as not to change the length of the array, so that in newItem()
// we can still safely do: newIdentity = this._arrayOfAllItems.length;
var indexInArrayOfAllItems = item[this._itemNumPropName];
var identity = this.getIdentity(item);
//If we have reference integrity on, we need to do reference cleanup for the deleted item
if(this.referenceIntegrity){
//First scan all the attributes of this items for references and clean them up in the map
//As this item is going away, no need to track its references anymore.
//Get the attributes list before we generate the backup so it
//doesn't pollute the attributes list.
var attributes = this.getAttributes(item);
//Backup the map, we'll have to restore it potentially, in a revert.
if(item[this._reverseRefMap]){
item["backup_" + this._reverseRefMap] = dojo.clone(item[this._reverseRefMap]);
}
//TODO: This causes a reversion problem. This list won't be restored on revert since it is
//attached to the 'value'. item, not ours. Need to back tese up somehow too.
//Maybe build a map of the backup of the entries and attach it to the deleted item to be restored
//later. Or just record them and call _addReferenceToMap on them in revert.
dojo.forEach(attributes, function(attribute){
dojo.forEach(this.getValues(item, attribute), function(value){
if(this.isItem(value)){
//We have to back up all the references we had to others so they can be restored on a revert.
if(!item["backupRefs_" + this._reverseRefMap]){
item["backupRefs_" + this._reverseRefMap] = [];
}
item["backupRefs_" + this._reverseRefMap].push({id: this.getIdentity(value), attr: attribute});
this._removeReferenceFromMap(value, item, attribute);
}
}, this);
}, this);
//Next, see if we have references to this item, if we do, we have to clean them up too.
var references = item[this._reverseRefMap];
if(references){
//Look through all the items noted as references to clean them up.
for(var itemId in references){
var containingItem = null;
if(this._itemsByIdentity){
containingItem = this._itemsByIdentity[itemId];
}else{
containingItem = this._arrayOfAllItems[itemId];
}
//We have a reference to a containing item, now we have to process the
//attributes and clear all references to the item being deleted.
if(containingItem){
for(var attribute in references[itemId]){
var oldValues = this.getValues(containingItem, attribute) || [];
var newValues = dojo.filter(oldValues, function(possibleItem){
return !(this.isItem(possibleItem) && this.getIdentity(possibleItem) == identity);
}, this);
//Remove the note of the reference to the item and set the values on the modified attribute.
this._removeReferenceFromMap(item, containingItem, attribute);
if(newValues.length < oldValues.length){
this._setValueOrValues(containingItem, attribute, newValues, true);
}
}
}
}
}
}
this._arrayOfAllItems[indexInArrayOfAllItems] = null;
item[this._storeRefPropName] = null;
if(this._itemsByIdentity){
delete this._itemsByIdentity[identity];
}
this._pending._deletedItems[identity] = item;
//Remove from the toplevel items, if necessary...
if(item[this._rootItemPropName]){
this._removeArrayElement(this._arrayOfTopLevelItems, item);
}
this.onDelete(item); // dojo.data.api.Notification call
return true;
},
setValue: function(/* item */ item, /* attribute-name-string */ attribute, /* almost anything */ value){
// summary: See dojo.data.api.Write.set()
return this._setValueOrValues(item, attribute, value, true); // boolean
},
setValues: function(/* item */ item, /* attribute-name-string */ attribute, /* array */ values){
// summary: See dojo.data.api.Write.setValues()
return this._setValueOrValues(item, attribute, values, true); // boolean
},
unsetAttribute: function(/* item */ item, /* attribute-name-string */ attribute){
// summary: See dojo.data.api.Write.unsetAttribute()
return this._setValueOrValues(item, attribute, [], true);
},
_setValueOrValues: function(/* item */ item, /* attribute-name-string */ attribute, /* anything */ newValueOrValues, /*boolean?*/ callOnSet){
this._assert(!this._saveInProgress);
// Check for valid arguments
this._assertIsItem(item);
this._assert(dojo.isString(attribute));
this._assert(typeof newValueOrValues !== "undefined");
// Make sure the user isn't trying to change the item's identity
var identifierAttribute = this._getIdentifierAttribute();
if(attribute == identifierAttribute){
throw new Error("ItemFileWriteStore does not have support for changing the value of an item's identifier.");
}
// To implement the Notification API, we need to make a note of what
// the old attribute value was, so that we can pass that info when
// we call the onSet method.
var oldValueOrValues = this._getValueOrValues(item, attribute);
var identity = this.getIdentity(item);
if(!this._pending._modifiedItems[identity]){
// Before we actually change the item, we make a copy of it to
// record the original state, so that we'll be able to revert if
// the revert method gets called. If the item has already been
// modified then there's no need to do this now, since we already
// have a record of the original state.
var copyOfItemState = {};
for(var key in item){
if((key === this._storeRefPropName) || (key === this._itemNumPropName) || (key === this._rootItemPropName)){
copyOfItemState[key] = item[key];
}else if(key === this._reverseRefMap){
copyOfItemState[key] = dojo.clone(item[key]);
}else{
copyOfItemState[key] = item[key].slice(0, item[key].length);
}
}
// Now mark the item as dirty, and save the copy of the original state
this._pending._modifiedItems[identity] = copyOfItemState;
}
// Okay, now we can actually change this attribute on the item
var success = false;
if(dojo.isArray(newValueOrValues) && newValueOrValues.length === 0){
// If we were passed an empty array as the value, that counts
// as "unsetting" the attribute, so we need to remove this
// attribute from the item.
success = delete item[attribute];
newValueOrValues = undefined; // used in the onSet Notification call below
if(this.referenceIntegrity && oldValueOrValues){
var oldValues = oldValueOrValues;
if(!dojo.isArray(oldValues)){
oldValues = [oldValues];
}
for(var i = 0; i < oldValues.length; i++){
var value = oldValues[i];
if(this.isItem(value)){
this._removeReferenceFromMap(value, item, attribute);
}
}
}
}else{
var newValueArray;
if(dojo.isArray(newValueOrValues)){
var newValues = newValueOrValues;
// Unfortunately, it's not safe to just do this:
// newValueArray = newValues;
// Instead, we need to copy the array, which slice() does very nicely.
// This is so that our internal data structure won't
// get corrupted if the user mucks with the values array *after*
// calling setValues().
newValueArray = newValueOrValues.slice(0, newValueOrValues.length);
}else{
newValueArray = [newValueOrValues];
}
//We need to handle reference integrity if this is on.
//In the case of set, we need to see if references were added or removed
//and update the reference tracking map accordingly.
if(this.referenceIntegrity){
if(oldValueOrValues){
var oldValues = oldValueOrValues;
if(!dojo.isArray(oldValues)){
oldValues = [oldValues];
}
//Use an associative map to determine what was added/removed from the list.
//Should be O(n) performant. First look at all the old values and make a list of them
//Then for any item not in the old list, we add it. If it was already present, we remove it.
//Then we pass over the map and any references left it it need to be removed (IE, no match in
//the new values list).
var map = {};
dojo.forEach(oldValues, function(possibleItem){
if(this.isItem(possibleItem)){
var id = this.getIdentity(possibleItem);
map[id.toString()] = true;
}
}, this);
dojo.forEach(newValueArray, function(possibleItem){
if(this.isItem(possibleItem)){
var id = this.getIdentity(possibleItem);
if(map[id.toString()]){
delete map[id.toString()];
}else{
this._addReferenceToMap(possibleItem, item, attribute);
}
}
}, this);
for(var rId in map){
var removedItem;
if(this._itemsByIdentity){
removedItem = this._itemsByIdentity[rId];
}else{
removedItem = this._arrayOfAllItems[rId];
}
this._removeReferenceFromMap(removedItem, item, attribute);
}
}else{
//Everything is new (no old values) so we have to just
//insert all the references, if any.
for(var i = 0; i < newValueArray.length; i++){
var value = newValueArray[i];
if(this.isItem(value)){
this._addReferenceToMap(value, item, attribute);
}
}
}
}
item[attribute] = newValueArray;
success = true;
}
// Now we make the dojo.data.api.Notification call
if(callOnSet){
this.onSet(item, attribute, oldValueOrValues, newValueOrValues);
}
return success; // boolean
},
_addReferenceToMap: function(/*item*/ refItem, /*item*/ parentItem, /*string*/ attribute){
// summary:
// Method to add an reference map entry for an item and attribute.
// description:
// Method to add an reference map entry for an item and attribute. //
// refItem:
// The item that is referenced.
// parentItem:
// The item that holds the new reference to refItem.
// attribute:
// The attribute on parentItem that contains the new reference.
var parentId = this.getIdentity(parentItem);
var references = refItem[this._reverseRefMap];
if(!references){
references = refItem[this._reverseRefMap] = {};
}
var itemRef = references[parentId];
if(!itemRef){
itemRef = references[parentId] = {};
}
itemRef[attribute] = true;
},
_removeReferenceFromMap: function(/* item */ refItem, /* item */ parentItem, /*strin*/ attribute){
// summary:
// Method to remove an reference map entry for an item and attribute.
// description:
// Method to remove an reference map entry for an item and attribute. This will
// also perform cleanup on the map such that if there are no more references at all to
// the item, its reference object and entry are removed.
//
// refItem:
// The item that is referenced.
// parentItem:
// The item holding a reference to refItem.
// attribute:
// The attribute on parentItem that contains the reference.
var identity = this.getIdentity(parentItem);
var references = refItem[this._reverseRefMap];
var itemId;
if(references){
for(itemId in references){
if(itemId == identity){
delete references[itemId][attribute];
if(this._isEmpty(references[itemId])){
delete references[itemId];
}
}
}
if(this._isEmpty(references)){
delete refItem[this._reverseRefMap];
}
}
},
_dumpReferenceMap: function(){
// summary:
// Function to dump the reverse reference map of all items in the store for debug purposes.
// description:
// Function to dump the reverse reference map of all items in the store for debug purposes.
var i;
for(i = 0; i < this._arrayOfAllItems.length; i++){
var item = this._arrayOfAllItems[i];
if(item && item[this._reverseRefMap]){
console.log("Item: [" + this.getIdentity(item) + "] is referenced by: " + dojo.toJson(item[this._reverseRefMap]));
}
}
},
_getValueOrValues: function(/* item */ item, /* attribute-name-string */ attribute){
var valueOrValues = undefined;
if(this.hasAttribute(item, attribute)){
var valueArray = this.getValues(item, attribute);
if(valueArray.length == 1){
valueOrValues = valueArray[0];
}else{
valueOrValues = valueArray;
}
}
return valueOrValues;
},
_flatten: function(/* anything */ value){
if(this.isItem(value)){
var item = value;
// Given an item, return an serializable object that provides a
// reference to the item.
// For example, given kermit:
// var kermit = store.newItem({id:2, name:"Kermit"});
// we want to return
// {_reference:2}
var identity = this.getIdentity(item);
var referenceObject = {_reference: identity};
return referenceObject;
}else{
if(typeof value === "object"){
for(var type in this._datatypeMap){
var typeMap = this._datatypeMap[type];
if(dojo.isObject(typeMap) && !dojo.isFunction(typeMap)){
if(value instanceof typeMap.type){
if(!typeMap.serialize){
throw new Error("ItemFileWriteStore: No serializer defined for type mapping: [" + type + "]");
}
return {_type: type, _value: typeMap.serialize(value)};
}
} else if(value instanceof typeMap){
//SImple mapping, therefore, return as a toString serialization.
return {_type: type, _value: value.toString()};
}
}
}
return value;
}
},
_getNewFileContentString: function(){
// summary:
// Generate a string that can be saved to a file.
// The result should look similar to:
// http://trac.dojotoolkit.org/browser/dojo/trunk/tests/data/countries.json
var serializableStructure = {};
var identifierAttribute = this._getIdentifierAttribute();
if(identifierAttribute !== Number){
serializableStructure.identifier = identifierAttribute;
}
if(this._labelAttr){
serializableStructure.label = this._labelAttr;
}
serializableStructure.items = [];
for(var i = 0; i < this._arrayOfAllItems.length; ++i){
var item = this._arrayOfAllItems[i];
if(item !== null){
var serializableItem = {};
for(var key in item){
if(key !== this._storeRefPropName && key !== this._itemNumPropName && key !== this._reverseRefMap && key !== this._rootItemPropName){
var attribute = key;
var valueArray = this.getValues(item, attribute);
if(valueArray.length == 1){
serializableItem[attribute] = this._flatten(valueArray[0]);
}else{
var serializableArray = [];
for(var j = 0; j < valueArray.length; ++j){
serializableArray.push(this._flatten(valueArray[j]));
serializableItem[attribute] = serializableArray;
}
}
}
}
serializableStructure.items.push(serializableItem);
}
}
var prettyPrint = true;
return dojo.toJson(serializableStructure, prettyPrint);
},
_isEmpty: function(something){
// summary:
// Function to determine if an array or object has no properties or values.
// something:
// The array or object to examine.
var empty = true;
if(dojo.isObject(something)){
var i;
for(i in something){
empty = false;
break;
}
}else if(dojo.isArray(something)){
if(something.length > 0){
empty = false;
}
}
return empty; //boolean
},
save: function(/* object */ keywordArgs){
// summary: See dojo.data.api.Write.save()
this._assert(!this._saveInProgress);
// this._saveInProgress is set to true, briefly, from when save is first called to when it completes
this._saveInProgress = true;
var self = this;
var saveCompleteCallback = function(){
self._pending = {
_newItems:{},
_modifiedItems:{},
_deletedItems:{}
};
self._saveInProgress = false; // must come after this._pending is cleared, but before any callbacks
if(keywordArgs && keywordArgs.onComplete){
var scope = keywordArgs.scope || dojo.global;
keywordArgs.onComplete.call(scope);
}
};
var saveFailedCallback = function(err){
self._saveInProgress = false;
if(keywordArgs && keywordArgs.onError){
var scope = keywordArgs.scope || dojo.global;
keywordArgs.onError.call(scope, err);
}
};
if(this._saveEverything){
var newFileContentString = this._getNewFileContentString();
this._saveEverything(saveCompleteCallback, saveFailedCallback, newFileContentString);
}
if(this._saveCustom){
this._saveCustom(saveCompleteCallback, saveFailedCallback);
}
if(!this._saveEverything && !this._saveCustom){
// Looks like there is no user-defined save-handler function.
// That's fine, it just means the datastore is acting as a "mock-write"
// store -- changes get saved in memory but don't get saved to disk.
saveCompleteCallback();
}
},
revert: function(){
// summary: See dojo.data.api.Write.revert()
this._assert(!this._saveInProgress);
var identity;
for(identity in this._pending._modifiedItems){
// find the original item and the modified item that replaced it
var copyOfItemState = this._pending._modifiedItems[identity];
var modifiedItem = null;
if(this._itemsByIdentity){
modifiedItem = this._itemsByIdentity[identity];
}else{
modifiedItem = this._arrayOfAllItems[identity];
}
// Restore the original item into a full-fledged item again, we want to try to
// keep the same object instance as if we don't it, causes bugs like #9022.
copyOfItemState[this._storeRefPropName] = this;
for(key in modifiedItem){
delete modifiedItem[key];
}
dojo.mixin(modifiedItem, copyOfItemState);
}
var deletedItem;
for(identity in this._pending._deletedItems){
deletedItem = this._pending._deletedItems[identity];
deletedItem[this._storeRefPropName] = this;
var index = deletedItem[this._itemNumPropName];
//Restore the reverse refererence map, if any.
if(deletedItem["backup_" + this._reverseRefMap]){
deletedItem[this._reverseRefMap] = deletedItem["backup_" + this._reverseRefMap];
delete deletedItem["backup_" + this._reverseRefMap];
}
this._arrayOfAllItems[index] = deletedItem;
if(this._itemsByIdentity){
this._itemsByIdentity[identity] = deletedItem;
}
if(deletedItem[this._rootItemPropName]){
this._arrayOfTopLevelItems.push(deletedItem);
}
}
//We have to pass through it again and restore the reference maps after all the
//undeletes have occurred.
for(identity in this._pending._deletedItems){
deletedItem = this._pending._deletedItems[identity];
if(deletedItem["backupRefs_" + this._reverseRefMap]){
dojo.forEach(deletedItem["backupRefs_" + this._reverseRefMap], function(reference){
var refItem;
if(this._itemsByIdentity){
refItem = this._itemsByIdentity[reference.id];
}else{
refItem = this._arrayOfAllItems[reference.id];
}
this._addReferenceToMap(refItem, deletedItem, reference.attr);
}, this);
delete deletedItem["backupRefs_" + this._reverseRefMap];
}
}
for(identity in this._pending._newItems){
var newItem = this._pending._newItems[identity];
newItem[this._storeRefPropName] = null;
// null out the new item, but don't change the array index so
// so we can keep using _arrayOfAllItems.length.
this._arrayOfAllItems[newItem[this._itemNumPropName]] = null;
if(newItem[this._rootItemPropName]){
this._removeArrayElement(this._arrayOfTopLevelItems, newItem);
}
if(this._itemsByIdentity){
delete this._itemsByIdentity[identity];
}
}
this._pending = {
_newItems:{},
_modifiedItems:{},
_deletedItems:{}
};
return true; // boolean
},
isDirty: function(/* item? */ item){
// summary: See dojo.data.api.Write.isDirty()
if(item){
// return true if the item is dirty
var identity = this.getIdentity(item);
return new Boolean(this._pending._newItems[identity] ||
this._pending._modifiedItems[identity] ||
this._pending._deletedItems[identity]).valueOf(); // boolean
}else{
// return true if the store is dirty -- which means return true
// if there are any new items, dirty items, or modified items
if(!this._isEmpty(this._pending._newItems) ||
!this._isEmpty(this._pending._modifiedItems) ||
!this._isEmpty(this._pending._deletedItems)){
return true;
}
return false; // boolean
}
},
/* dojo.data.api.Notification */
onSet: function(/* item */ item,
/*attribute-name-string*/ attribute,
/*object | array*/ oldValue,
/*object | array*/ newValue){
// summary: See dojo.data.api.Notification.onSet()
// No need to do anything. This method is here just so that the
// client code can connect observers to it.
},
onNew: function(/* item */ newItem, /*object?*/ parentInfo){
// summary: See dojo.data.api.Notification.onNew()
// No need to do anything. This method is here just so that the
// client code can connect observers to it.
},
onDelete: function(/* item */ deletedItem){
// summary: See dojo.data.api.Notification.onDelete()
// No need to do anything. This method is here just so that the
// client code can connect observers to it.
},
close: function(/* object? */ request){
// summary:
// Over-ride of base close function of ItemFileReadStore to add in check for store state.
// description:
// Over-ride of base close function of ItemFileReadStore to add in check for store state.
// If the store is still dirty (unsaved changes), then an error will be thrown instead of
// clearing the internal state for reload from the url.
//Clear if not dirty ... or throw an error
if(this.clearOnClose){
if(!this.isDirty()){
this.inherited(arguments);
}else{
//Only throw an error if the store was dirty and we were loading from a url (cannot reload from url until state is saved).
throw new Error("dojo.data.ItemFileWriteStore: There are unsaved changes present in the store. Please save or revert the changes before invoking close.");
}
}
}
});
}
dojo.i18n._preloadLocalizations("dojo.nls.tt-rss-layer", ["ROOT","ar","ca","cs","da","de","de-de","el","en","en-gb","en-us","es","es-es","fi","fi-fi","fr","fr-fr","he","he-il","hu","it","it-it","ja","ja-jp","ko","ko-kr","nb","nl","nl-nl","pl","pt","pt-br","pt-pt","ru","sk","sl","sv","th","tr","xx","zh","zh-cn","zh-tw"]);