From a089699c8915636ba4f158d77dba9b012bc93208 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Fri, 4 Mar 2011 19:02:28 +0300 Subject: build custom layer of Dojo to speed up loading of tt-rss (refs #293) --- lib/dojo/tt-rss-layer.js.uncompressed.js | 26844 +++++++++++++++++++++++++++++ 1 file changed, 26844 insertions(+) create mode 100644 lib/dojo/tt-rss-layer.js.uncompressed.js (limited to 'lib/dojo/tt-rss-layer.js.uncompressed.js') diff --git a/lib/dojo/tt-rss-layer.js.uncompressed.js b/lib/dojo/tt-rss-layer.js.uncompressed.js new file mode 100644 index 000000000..fa2e12157 --- /dev/null +++ b/lib/dojo/tt-rss-layer.js.uncompressed.js @@ -0,0 +1,26844 @@ +/* + Copyright (c) 2004-2010, The Dojo Foundation All Rights Reserved. + Available via Academic Free License >= 2.1 OR the modified BSD license. + see: http://dojotoolkit.org/license for details +*/ + +/* + This is an optimized version of Dojo, built for deployment and not for + development. To get sources and documentation, please visit: + + http://dojotoolkit.org +*/ + +dojo.provide("tt-rss-layer"); +if(!dojo._hasResource["dojo.date.stamp"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.date.stamp"] = true; +dojo.provide("dojo.date.stamp"); + +// Methods to convert dates to or from a wire (string) format using well-known conventions + +dojo.date.stamp.fromISOString = function(/*String*/formattedString, /*Number?*/defaultTime){ + // summary: + // Returns a Date object given a string formatted according to a subset of the ISO-8601 standard. + // + // description: + // Accepts a string formatted according to a profile of ISO8601 as defined by + // [RFC3339](http://www.ietf.org/rfc/rfc3339.txt), except that partial input is allowed. + // Can also process dates as specified [by the W3C](http://www.w3.org/TR/NOTE-datetime) + // The following combinations are valid: + // + // * dates only + // | * yyyy + // | * yyyy-MM + // | * yyyy-MM-dd + // * times only, with an optional time zone appended + // | * THH:mm + // | * THH:mm:ss + // | * THH:mm:ss.SSS + // * and "datetimes" which could be any combination of the above + // + // timezones may be specified as Z (for UTC) or +/- followed by a time expression HH:mm + // Assumes the local time zone if not specified. Does not validate. Improperly formatted + // input may return null. Arguments which are out of bounds will be handled + // by the Date constructor (e.g. January 32nd typically gets resolved to February 1st) + // Only years between 100 and 9999 are supported. + // + // formattedString: + // A string such as 2005-06-30T08:05:00-07:00 or 2005-06-30 or T08:05:00 + // + // defaultTime: + // Used for defaults for fields omitted in the formattedString. + // Uses 1970-01-01T00:00:00.0Z by default. + + if(!dojo.date.stamp._isoRegExp){ + dojo.date.stamp._isoRegExp = +//TODO: could be more restrictive and check for 00-59, etc. + /^(?:(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(.\d+)?)?((?:[+-](\d{2}):(\d{2}))|Z)?)?$/; + } + + var match = dojo.date.stamp._isoRegExp.exec(formattedString), + result = null; + + if(match){ + match.shift(); + if(match[1]){match[1]--;} // Javascript Date months are 0-based + if(match[6]){match[6] *= 1000;} // Javascript Date expects fractional seconds as milliseconds + + if(defaultTime){ + // mix in defaultTime. Relatively expensive, so use || operators for the fast path of defaultTime === 0 + defaultTime = new Date(defaultTime); + dojo.forEach(dojo.map(["FullYear", "Month", "Date", "Hours", "Minutes", "Seconds", "Milliseconds"], function(prop){ + return defaultTime["get" + prop](); + }), function(value, index){ + match[index] = match[index] || value; + }); + } + result = new Date(match[0]||1970, match[1]||0, match[2]||1, match[3]||0, match[4]||0, match[5]||0, match[6]||0); //TODO: UTC defaults + if(match[0] < 100){ + result.setFullYear(match[0] || 1970); + } + + var offset = 0, + zoneSign = match[7] && match[7].charAt(0); + if(zoneSign != 'Z'){ + offset = ((match[8] || 0) * 60) + (Number(match[9]) || 0); + if(zoneSign != '-'){ offset *= -1; } + } + if(zoneSign){ + offset -= result.getTimezoneOffset(); + } + if(offset){ + result.setTime(result.getTime() + offset * 60000); + } + } + + return result; // Date or null +} + +/*===== + dojo.date.stamp.__Options = function(){ + // selector: String + // "date" or "time" for partial formatting of the Date object. + // Both date and time will be formatted by default. + // zulu: Boolean + // if true, UTC/GMT is used for a timezone + // milliseconds: Boolean + // if true, output milliseconds + this.selector = selector; + this.zulu = zulu; + this.milliseconds = milliseconds; + } +=====*/ + +dojo.date.stamp.toISOString = function(/*Date*/dateObject, /*dojo.date.stamp.__Options?*/options){ + // summary: + // Format a Date object as a string according a subset of the ISO-8601 standard + // + // description: + // When options.selector is omitted, output follows [RFC3339](http://www.ietf.org/rfc/rfc3339.txt) + // The local time zone is included as an offset from GMT, except when selector=='time' (time without a date) + // Does not check bounds. Only years between 100 and 9999 are supported. + // + // dateObject: + // A Date object + + var _ = function(n){ return (n < 10) ? "0" + n : n; }; + options = options || {}; + var formattedDate = [], + getter = options.zulu ? "getUTC" : "get", + date = ""; + if(options.selector != "time"){ + var year = dateObject[getter+"FullYear"](); + date = ["0000".substr((year+"").length)+year, _(dateObject[getter+"Month"]()+1), _(dateObject[getter+"Date"]())].join('-'); + } + formattedDate.push(date); + if(options.selector != "date"){ + var time = [_(dateObject[getter+"Hours"]()), _(dateObject[getter+"Minutes"]()), _(dateObject[getter+"Seconds"]())].join(':'); + var millis = dateObject[getter+"Milliseconds"](); + if(options.milliseconds){ + time += "."+ (millis < 100 ? "0" : "") + _(millis); + } + if(options.zulu){ + time += "Z"; + }else if(options.selector != "time"){ + var timezoneOffset = dateObject.getTimezoneOffset(); + var absOffset = Math.abs(timezoneOffset); + time += (timezoneOffset > 0 ? "-" : "+") + + _(Math.floor(absOffset/60)) + ":" + _(absOffset%60); + } + formattedDate.push(time); + } + return formattedDate.join('T'); // String +} + +} + +if(!dojo._hasResource["dojo.parser"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.parser"] = true; +dojo.provide("dojo.parser"); + + +new Date("X"); // workaround for #11279, new Date("") == NaN + +dojo.parser = new function(){ + // summary: The Dom/Widget parsing package + + var d = dojo; + this._attrName = d._scopeName + "Type"; + this._query = "[" + this._attrName + "]"; + + function val2type(/*Object*/ value){ + // summary: + // Returns name of type of given value. + + if(d.isString(value)){ return "string"; } + if(typeof value == "number"){ return "number"; } + if(typeof value == "boolean"){ return "boolean"; } + if(d.isFunction(value)){ return "function"; } + if(d.isArray(value)){ return "array"; } // typeof [] == "object" + if(value instanceof Date) { return "date"; } // assume timestamp + if(value instanceof d._Url){ return "url"; } + return "object"; + } + + function str2obj(/*String*/ value, /*String*/ type){ + // summary: + // Convert given string value to given type + switch(type){ + case "string": + return value; + case "number": + return value.length ? Number(value) : NaN; + case "boolean": + // for checked/disabled value might be "" or "checked". interpret as true. + return typeof value == "boolean" ? value : !(value.toLowerCase()=="false"); + case "function": + if(d.isFunction(value)){ + // IE gives us a function, even when we say something like onClick="foo" + // (in which case it gives us an invalid function "function(){ foo }"). + // Therefore, convert to string + value=value.toString(); + value=d.trim(value.substring(value.indexOf('{')+1, value.length-1)); + } + try{ + if(value === "" || value.search(/[^\w\.]+/i) != -1){ + // The user has specified some text for a function like "return x+5" + return new Function(value); + }else{ + // The user has specified the name of a function like "myOnClick" + // or a single word function "return" + return d.getObject(value, false) || new Function(value); + } + }catch(e){ return new Function(); } + case "array": + return value ? value.split(/\s*,\s*/) : []; + case "date": + switch(value){ + case "": return new Date(""); // the NaN of dates + case "now": return new Date(); // current date + default: return d.date.stamp.fromISOString(value); + } + case "url": + return d.baseUrl + value; + default: + return d.fromJson(value); + } + } + + var instanceClasses = { + // map from fully qualified name (like "dijit.Button") to structure like + // { cls: dijit.Button, params: {label: "string", disabled: "boolean"} } + }; + + // Widgets like BorderContainer add properties to _Widget via dojo.extend(). + // If BorderContainer is loaded after _Widget's parameter list has been cached, + // we need to refresh that parameter list (for _Widget and all widgets that extend _Widget). + dojo.connect(dojo, "extend", function(){ + instanceClasses = {}; + }); + + function getClassInfo(/*String*/ className){ + // className: + // fully qualified name (like "dijit.form.Button") + // returns: + // structure like + // { + // cls: dijit.Button, + // params: { label: "string", disabled: "boolean"} + // } + + if(!instanceClasses[className]){ + // get pointer to widget class + var cls = d.getObject(className); + if(!cls){ return null; } // class not defined [yet] + + var proto = cls.prototype; + + // get table of parameter names & types + var params = {}, dummyClass = {}; + for(var name in proto){ + if(name.charAt(0)=="_"){ continue; } // skip internal properties + if(name in dummyClass){ continue; } // skip "constructor" and "toString" + var defVal = proto[name]; + params[name]=val2type(defVal); + } + + instanceClasses[className] = { cls: cls, params: params }; + } + return instanceClasses[className]; + } + + this._functionFromScript = function(script){ + var preamble = ""; + var suffix = ""; + var argsStr = script.getAttribute("args"); + if(argsStr){ + d.forEach(argsStr.split(/\s*,\s*/), function(part, idx){ + preamble += "var "+part+" = arguments["+idx+"]; "; + }); + } + var withStr = script.getAttribute("with"); + if(withStr && withStr.length){ + d.forEach(withStr.split(/\s*,\s*/), function(part){ + preamble += "with("+part+"){"; + suffix += "}"; + }); + } + return new Function(preamble+script.innerHTML+suffix); + } + + this.instantiate = function(/* Array */nodes, /* Object? */mixin, /* Object? */args){ + // summary: + // Takes array of nodes, and turns them into class instances and + // potentially calls a startup method to allow them to connect with + // any children. + // nodes: Array + // Array of nodes or objects like + // | { + // | type: "dijit.form.Button", + // | node: DOMNode, + // | scripts: [ ... ], // array of + // +}; +=====*/ + +// All the stuff in _base (these are the function that are guaranteed available without an explicit dojo.require) + + +// And some other stuff that we tend to pull in all the time anyway + + + + + + + +} + +if(!dojo._hasResource["dojo.fx.Toggler"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.fx.Toggler"] = true; +dojo.provide("dojo.fx.Toggler"); + +dojo.declare("dojo.fx.Toggler", null, { + // summary: + // A simple `dojo.Animation` toggler API. + // + // description: + // class constructor for an animation toggler. It accepts a packed + // set of arguments about what type of animation to use in each + // direction, duration, etc. All available members are mixed into + // these animations from the constructor (for example, `node`, + // `showDuration`, `hideDuration`). + // + // example: + // | var t = new dojo.fx.Toggler({ + // | node: "nodeId", + // | showDuration: 500, + // | // hideDuration will default to "200" + // | showFunc: dojo.fx.wipeIn, + // | // hideFunc will default to "fadeOut" + // | }); + // | t.show(100); // delay showing for 100ms + // | // ...time passes... + // | t.hide(); + + // node: DomNode + // the node to target for the showing and hiding animations + node: null, + + // showFunc: Function + // The function that returns the `dojo.Animation` to show the node + showFunc: dojo.fadeIn, + + // hideFunc: Function + // The function that returns the `dojo.Animation` to hide the node + hideFunc: dojo.fadeOut, + + // showDuration: + // Time in milliseconds to run the show Animation + showDuration: 200, + + // hideDuration: + // Time in milliseconds to run the hide Animation + hideDuration: 200, + + // FIXME: need a policy for where the toggler should "be" the next + // time show/hide are called if we're stopped somewhere in the + // middle. + // FIXME: also would be nice to specify individual showArgs/hideArgs mixed into + // each animation individually. + // FIXME: also would be nice to have events from the animations exposed/bridged + + /*===== + _showArgs: null, + _showAnim: null, + + _hideArgs: null, + _hideAnim: null, + + _isShowing: false, + _isHiding: false, + =====*/ + + constructor: function(args){ + var _t = this; + + dojo.mixin(_t, args); + _t.node = args.node; + _t._showArgs = dojo.mixin({}, args); + _t._showArgs.node = _t.node; + _t._showArgs.duration = _t.showDuration; + _t.showAnim = _t.showFunc(_t._showArgs); + + _t._hideArgs = dojo.mixin({}, args); + _t._hideArgs.node = _t.node; + _t._hideArgs.duration = _t.hideDuration; + _t.hideAnim = _t.hideFunc(_t._hideArgs); + + dojo.connect(_t.showAnim, "beforeBegin", dojo.hitch(_t.hideAnim, "stop", true)); + dojo.connect(_t.hideAnim, "beforeBegin", dojo.hitch(_t.showAnim, "stop", true)); + }, + + show: function(delay){ + // summary: Toggle the node to showing + // delay: Integer? + // Ammount of time to stall playing the show animation + return this.showAnim.play(delay || 0); + }, + + hide: function(delay){ + // summary: Toggle the node to hidden + // delay: Integer? + // Ammount of time to stall playing the hide animation + return this.hideAnim.play(delay || 0); + } +}); + +} + +if(!dojo._hasResource["dojo.fx"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.fx"] = true; +dojo.provide("dojo.fx"); + // FIXME: remove this back-compat require in 2.0 +/*===== +dojo.fx = { + // summary: Effects library on top of Base animations +}; +=====*/ +(function(){ + + var d = dojo, + _baseObj = { + _fire: function(evt, args){ + if(this[evt]){ + this[evt].apply(this, args||[]); + } + return this; + } + }; + + var _chain = function(animations){ + this._index = -1; + this._animations = animations||[]; + this._current = this._onAnimateCtx = this._onEndCtx = null; + + this.duration = 0; + d.forEach(this._animations, function(a){ + this.duration += a.duration; + if(a.delay){ this.duration += a.delay; } + }, this); + }; + d.extend(_chain, { + _onAnimate: function(){ + this._fire("onAnimate", arguments); + }, + _onEnd: function(){ + d.disconnect(this._onAnimateCtx); + d.disconnect(this._onEndCtx); + this._onAnimateCtx = this._onEndCtx = null; + if(this._index + 1 == this._animations.length){ + this._fire("onEnd"); + }else{ + // switch animations + this._current = this._animations[++this._index]; + this._onAnimateCtx = d.connect(this._current, "onAnimate", this, "_onAnimate"); + this._onEndCtx = d.connect(this._current, "onEnd", this, "_onEnd"); + this._current.play(0, true); + } + }, + play: function(/*int?*/ delay, /*Boolean?*/ gotoStart){ + if(!this._current){ this._current = this._animations[this._index = 0]; } + if(!gotoStart && this._current.status() == "playing"){ return this; } + var beforeBegin = d.connect(this._current, "beforeBegin", this, function(){ + this._fire("beforeBegin"); + }), + onBegin = d.connect(this._current, "onBegin", this, function(arg){ + this._fire("onBegin", arguments); + }), + onPlay = d.connect(this._current, "onPlay", this, function(arg){ + this._fire("onPlay", arguments); + d.disconnect(beforeBegin); + d.disconnect(onBegin); + d.disconnect(onPlay); + }); + if(this._onAnimateCtx){ + d.disconnect(this._onAnimateCtx); + } + this._onAnimateCtx = d.connect(this._current, "onAnimate", this, "_onAnimate"); + if(this._onEndCtx){ + d.disconnect(this._onEndCtx); + } + this._onEndCtx = d.connect(this._current, "onEnd", this, "_onEnd"); + this._current.play.apply(this._current, arguments); + return this; + }, + pause: function(){ + if(this._current){ + var e = d.connect(this._current, "onPause", this, function(arg){ + this._fire("onPause", arguments); + d.disconnect(e); + }); + this._current.pause(); + } + return this; + }, + gotoPercent: function(/*Decimal*/percent, /*Boolean?*/ andPlay){ + this.pause(); + var offset = this.duration * percent; + this._current = null; + d.some(this._animations, function(a){ + if(a.duration <= offset){ + this._current = a; + return true; + } + offset -= a.duration; + return false; + }); + if(this._current){ + this._current.gotoPercent(offset / this._current.duration, andPlay); + } + return this; + }, + stop: function(/*boolean?*/ gotoEnd){ + if(this._current){ + if(gotoEnd){ + for(; this._index + 1 < this._animations.length; ++this._index){ + this._animations[this._index].stop(true); + } + this._current = this._animations[this._index]; + } + var e = d.connect(this._current, "onStop", this, function(arg){ + this._fire("onStop", arguments); + d.disconnect(e); + }); + this._current.stop(); + } + return this; + }, + status: function(){ + return this._current ? this._current.status() : "stopped"; + }, + destroy: function(){ + if(this._onAnimateCtx){ d.disconnect(this._onAnimateCtx); } + if(this._onEndCtx){ d.disconnect(this._onEndCtx); } + } + }); + d.extend(_chain, _baseObj); + + dojo.fx.chain = function(/*dojo.Animation[]*/ animations){ + // summary: + // Chain a list of `dojo.Animation`s to run in sequence + // + // description: + // Return a `dojo.Animation` which will play all passed + // `dojo.Animation` instances in sequence, firing its own + // synthesized events simulating a single animation. (eg: + // onEnd of this animation means the end of the chain, + // not the individual animations within) + // + // example: + // Once `node` is faded out, fade in `otherNode` + // | dojo.fx.chain([ + // | dojo.fadeIn({ node:node }), + // | dojo.fadeOut({ node:otherNode }) + // | ]).play(); + // + return new _chain(animations) // dojo.Animation + }; + + var _combine = function(animations){ + this._animations = animations||[]; + this._connects = []; + this._finished = 0; + + this.duration = 0; + d.forEach(animations, function(a){ + var duration = a.duration; + if(a.delay){ duration += a.delay; } + if(this.duration < duration){ this.duration = duration; } + this._connects.push(d.connect(a, "onEnd", this, "_onEnd")); + }, this); + + this._pseudoAnimation = new d.Animation({curve: [0, 1], duration: this.duration}); + var self = this; + d.forEach(["beforeBegin", "onBegin", "onPlay", "onAnimate", "onPause", "onStop", "onEnd"], + function(evt){ + self._connects.push(d.connect(self._pseudoAnimation, evt, + function(){ self._fire(evt, arguments); } + )); + } + ); + }; + d.extend(_combine, { + _doAction: function(action, args){ + d.forEach(this._animations, function(a){ + a[action].apply(a, args); + }); + return this; + }, + _onEnd: function(){ + if(++this._finished > this._animations.length){ + this._fire("onEnd"); + } + }, + _call: function(action, args){ + var t = this._pseudoAnimation; + t[action].apply(t, args); + }, + play: function(/*int?*/ delay, /*Boolean?*/ gotoStart){ + this._finished = 0; + this._doAction("play", arguments); + this._call("play", arguments); + return this; + }, + pause: function(){ + this._doAction("pause", arguments); + this._call("pause", arguments); + return this; + }, + gotoPercent: function(/*Decimal*/percent, /*Boolean?*/ andPlay){ + var ms = this.duration * percent; + d.forEach(this._animations, function(a){ + a.gotoPercent(a.duration < ms ? 1 : (ms / a.duration), andPlay); + }); + this._call("gotoPercent", arguments); + return this; + }, + stop: function(/*boolean?*/ gotoEnd){ + this._doAction("stop", arguments); + this._call("stop", arguments); + return this; + }, + status: function(){ + return this._pseudoAnimation.status(); + }, + destroy: function(){ + d.forEach(this._connects, dojo.disconnect); + } + }); + d.extend(_combine, _baseObj); + + dojo.fx.combine = function(/*dojo.Animation[]*/ animations){ + // summary: + // Combine a list of `dojo.Animation`s to run in parallel + // + // description: + // Combine an array of `dojo.Animation`s to run in parallel, + // providing a new `dojo.Animation` instance encompasing each + // animation, firing standard animation events. + // + // example: + // Fade out `node` while fading in `otherNode` simultaneously + // | dojo.fx.combine([ + // | dojo.fadeIn({ node:node }), + // | dojo.fadeOut({ node:otherNode }) + // | ]).play(); + // + // example: + // When the longest animation ends, execute a function: + // | var anim = dojo.fx.combine([ + // | dojo.fadeIn({ node: n, duration:700 }), + // | dojo.fadeOut({ node: otherNode, duration: 300 }) + // | ]); + // | dojo.connect(anim, "onEnd", function(){ + // | // overall animation is done. + // | }); + // | anim.play(); // play the animation + // + return new _combine(animations); // dojo.Animation + }; + + dojo.fx.wipeIn = function(/*Object*/ args){ + // summary: + // Expand a node to it's natural height. + // + // description: + // Returns an animation that will expand the + // node defined in 'args' object from it's current height to + // it's natural height (with no scrollbar). + // Node must have no margin/border/padding. + // + // args: Object + // A hash-map of standard `dojo.Animation` constructor properties + // (such as easing: node: duration: and so on) + // + // example: + // | dojo.fx.wipeIn({ + // | node:"someId" + // | }).play() + var node = args.node = d.byId(args.node), s = node.style, o; + + var anim = d.animateProperty(d.mixin({ + properties: { + height: { + // wrapped in functions so we wait till the last second to query (in case value has changed) + start: function(){ + // start at current [computed] height, but use 1px rather than 0 + // because 0 causes IE to display the whole panel + o = s.overflow; + s.overflow = "hidden"; + if(s.visibility == "hidden" || s.display == "none"){ + s.height = "1px"; + s.display = ""; + s.visibility = ""; + return 1; + }else{ + var height = d.style(node, "height"); + return Math.max(height, 1); + } + }, + end: function(){ + return node.scrollHeight; + } + } + } + }, args)); + + d.connect(anim, "onEnd", function(){ + s.height = "auto"; + s.overflow = o; + }); + + return anim; // dojo.Animation + } + + dojo.fx.wipeOut = function(/*Object*/ args){ + // summary: + // Shrink a node to nothing and hide it. + // + // description: + // Returns an animation that will shrink node defined in "args" + // from it's current height to 1px, and then hide it. + // + // args: Object + // A hash-map of standard `dojo.Animation` constructor properties + // (such as easing: node: duration: and so on) + // + // example: + // | dojo.fx.wipeOut({ node:"someId" }).play() + + var node = args.node = d.byId(args.node), s = node.style, o; + + var anim = d.animateProperty(d.mixin({ + properties: { + height: { + end: 1 // 0 causes IE to display the whole panel + } + } + }, args)); + + d.connect(anim, "beforeBegin", function(){ + o = s.overflow; + s.overflow = "hidden"; + s.display = ""; + }); + d.connect(anim, "onEnd", function(){ + s.overflow = o; + s.height = "auto"; + s.display = "none"; + }); + + return anim; // dojo.Animation + } + + dojo.fx.slideTo = function(/*Object*/ args){ + // summary: + // Slide a node to a new top/left position + // + // description: + // Returns an animation that will slide "node" + // defined in args Object from its current position to + // the position defined by (args.left, args.top). + // + // args: Object + // A hash-map of standard `dojo.Animation` constructor properties + // (such as easing: node: duration: and so on). Special args members + // are `top` and `left`, which indicate the new position to slide to. + // + // example: + // | dojo.fx.slideTo({ node: node, left:"40", top:"50", units:"px" }).play() + + var node = args.node = d.byId(args.node), + top = null, left = null; + + var init = (function(n){ + return function(){ + var cs = d.getComputedStyle(n); + var pos = cs.position; + top = (pos == 'absolute' ? n.offsetTop : parseInt(cs.top) || 0); + left = (pos == 'absolute' ? n.offsetLeft : parseInt(cs.left) || 0); + if(pos != 'absolute' && pos != 'relative'){ + var ret = d.position(n, true); + top = ret.y; + left = ret.x; + n.style.position="absolute"; + n.style.top=top+"px"; + n.style.left=left+"px"; + } + }; + })(node); + init(); + + var anim = d.animateProperty(d.mixin({ + properties: { + top: args.top || 0, + left: args.left || 0 + } + }, args)); + d.connect(anim, "beforeBegin", anim, init); + + return anim; // dojo.Animation + } + +})(); + +} + +if(!dojo._hasResource["dojo.NodeList-fx"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.NodeList-fx"] = true; +dojo.provide("dojo.NodeList-fx"); + + +/*===== +dojo["NodeList-fx"] = { + // summary: Adds dojo.fx animation support to dojo.query() +}; +=====*/ + +dojo.extend(dojo.NodeList, { + _anim: function(obj, method, args){ + args = args||{}; + var a = dojo.fx.combine( + this.map(function(item){ + var tmpArgs = { node: item }; + dojo.mixin(tmpArgs, args); + return obj[method](tmpArgs); + }) + ); + return args.auto ? a.play() && this : a; // dojo.Animation|dojo.NodeList + }, + + wipeIn: function(args){ + // summary: + // wipe in all elements of this NodeList via `dojo.fx.wipeIn` + // + // args: Object? + // Additional dojo.Animation arguments to mix into this set with the addition of + // an `auto` parameter. + // + // returns: dojo.Animation|dojo.NodeList + // A special args member `auto` can be passed to automatically play the animation. + // If args.auto is present, the original dojo.NodeList will be returned for further + // chaining. Otherwise the dojo.Animation instance is returned and must be .play()'ed + // + // example: + // Fade in all tables with class "blah": + // | dojo.query("table.blah").wipeIn().play(); + // + // example: + // Utilizing `auto` to get the NodeList back: + // | dojo.query(".titles").wipeIn({ auto:true }).onclick(someFunction); + // + return this._anim(dojo.fx, "wipeIn", args); // dojo.Animation|dojo.NodeList + }, + + wipeOut: function(args){ + // summary: + // wipe out all elements of this NodeList via `dojo.fx.wipeOut` + // + // args: Object? + // Additional dojo.Animation arguments to mix into this set with the addition of + // an `auto` parameter. + // + // returns: dojo.Animation|dojo.NodeList + // A special args member `auto` can be passed to automatically play the animation. + // If args.auto is present, the original dojo.NodeList will be returned for further + // chaining. Otherwise the dojo.Animation instance is returned and must be .play()'ed + // + // example: + // Wipe out all tables with class "blah": + // | dojo.query("table.blah").wipeOut().play(); + return this._anim(dojo.fx, "wipeOut", args); // dojo.Animation|dojo.NodeList + }, + + slideTo: function(args){ + // summary: + // slide all elements of the node list to the specified place via `dojo.fx.slideTo` + // + // args: Object? + // Additional dojo.Animation arguments to mix into this set with the addition of + // an `auto` parameter. + // + // returns: dojo.Animation|dojo.NodeList + // A special args member `auto` can be passed to automatically play the animation. + // If args.auto is present, the original dojo.NodeList will be returned for further + // chaining. Otherwise the dojo.Animation instance is returned and must be .play()'ed + // + // example: + // | Move all tables with class "blah" to 300/300: + // | dojo.query("table.blah").slideTo({ + // | left: 40, + // | top: 50 + // | }).play(); + return this._anim(dojo.fx, "slideTo", args); // dojo.Animation|dojo.NodeList + }, + + + fadeIn: function(args){ + // summary: + // fade in all elements of this NodeList via `dojo.fadeIn` + // + // args: Object? + // Additional dojo.Animation arguments to mix into this set with the addition of + // an `auto` parameter. + // + // returns: dojo.Animation|dojo.NodeList + // A special args member `auto` can be passed to automatically play the animation. + // If args.auto is present, the original dojo.NodeList will be returned for further + // chaining. Otherwise the dojo.Animation instance is returned and must be .play()'ed + // + // example: + // Fade in all tables with class "blah": + // | dojo.query("table.blah").fadeIn().play(); + return this._anim(dojo, "fadeIn", args); // dojo.Animation|dojo.NodeList + }, + + fadeOut: function(args){ + // summary: + // fade out all elements of this NodeList via `dojo.fadeOut` + // + // args: Object? + // Additional dojo.Animation arguments to mix into this set with the addition of + // an `auto` parameter. + // + // returns: dojo.Animation|dojo.NodeList + // A special args member `auto` can be passed to automatically play the animation. + // If args.auto is present, the original dojo.NodeList will be returned for further + // chaining. Otherwise the dojo.Animation instance is returned and must be .play()'ed + // + // example: + // Fade out all elements with class "zork": + // | dojo.query(".zork").fadeOut().play(); + // example: + // Fade them on a delay and do something at the end: + // | var fo = dojo.query(".zork").fadeOut(); + // | dojo.connect(fo, "onEnd", function(){ /*...*/ }); + // | fo.play(); + // example: + // Using `auto`: + // | dojo.query("li").fadeOut({ auto:true }).filter(filterFn).forEach(doit); + // + return this._anim(dojo, "fadeOut", args); // dojo.Animation|dojo.NodeList + }, + + animateProperty: function(args){ + // summary: + // Animate all elements of this NodeList across the properties specified. + // syntax identical to `dojo.animateProperty` + // + // returns: dojo.Animation|dojo.NodeList + // A special args member `auto` can be passed to automatically play the animation. + // If args.auto is present, the original dojo.NodeList will be returned for further + // chaining. Otherwise the dojo.Animation instance is returned and must be .play()'ed + // + // example: + // | dojo.query(".zork").animateProperty({ + // | duration: 500, + // | properties: { + // | color: { start: "black", end: "white" }, + // | left: { end: 300 } + // | } + // | }).play(); + // + // example: + // | dojo.query(".grue").animateProperty({ + // | auto:true, + // | properties: { + // | height:240 + // | } + // | }).onclick(handler); + return this._anim(dojo, "animateProperty", args); // dojo.Animation|dojo.NodeList + }, + + anim: function( /*Object*/ properties, + /*Integer?*/ duration, + /*Function?*/ easing, + /*Function?*/ onEnd, + /*Integer?*/ delay){ + // summary: + // Animate one or more CSS properties for all nodes in this list. + // The returned animation object will already be playing when it + // is returned. See the docs for `dojo.anim` for full details. + // properties: Object + // the properties to animate. does NOT support the `auto` parameter like other + // NodeList-fx methods. + // duration: Integer? + // Optional. The time to run the animations for + // easing: Function? + // Optional. The easing function to use. + // onEnd: Function? + // A function to be called when the animation ends + // delay: + // how long to delay playing the returned animation + // example: + // Another way to fade out: + // | dojo.query(".thinger").anim({ opacity: 0 }); + // example: + // animate all elements with the "thigner" class to a width of 500 + // pixels over half a second + // | dojo.query(".thinger").anim({ width: 500 }, 700); + var canim = dojo.fx.combine( + this.map(function(item){ + return dojo.animateProperty({ + node: item, + properties: properties, + duration: duration||350, + easing: easing + }); + }) + ); + if(onEnd){ + dojo.connect(canim, "onEnd", onEnd); + } + return canim.play(delay||0); // dojo.Animation + } +}); + +} + +if(!dojo._hasResource["dojo.colors"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.colors"] = true; +dojo.provide("dojo.colors"); + +//TODO: this module appears to break naming conventions + +/*===== +dojo.colors = { + // summary: Color utilities +} +=====*/ + +(function(){ + // this is a standard conversion prescribed by the CSS3 Color Module + var hue2rgb = function(m1, m2, h){ + if(h < 0){ ++h; } + if(h > 1){ --h; } + var h6 = 6 * h; + if(h6 < 1){ return m1 + (m2 - m1) * h6; } + if(2 * h < 1){ return m2; } + if(3 * h < 2){ return m1 + (m2 - m1) * (2 / 3 - h) * 6; } + return m1; + }; + + dojo.colorFromRgb = function(/*String*/ color, /*dojo.Color?*/ obj){ + // summary: + // get rgb(a) array from css-style color declarations + // description: + // this function can handle all 4 CSS3 Color Module formats: rgb, + // rgba, hsl, hsla, including rgb(a) with percentage values. + var m = color.toLowerCase().match(/^(rgba?|hsla?)\(([\s\.\-,%0-9]+)\)/); + if(m){ + var c = m[2].split(/\s*,\s*/), l = c.length, t = m[1], a; + if((t == "rgb" && l == 3) || (t == "rgba" && l == 4)){ + var r = c[0]; + if(r.charAt(r.length - 1) == "%"){ + // 3 rgb percentage values + a = dojo.map(c, function(x){ + return parseFloat(x) * 2.56; + }); + if(l == 4){ a[3] = c[3]; } + return dojo.colorFromArray(a, obj); // dojo.Color + } + return dojo.colorFromArray(c, obj); // dojo.Color + } + if((t == "hsl" && l == 3) || (t == "hsla" && l == 4)){ + // normalize hsl values + var H = ((parseFloat(c[0]) % 360) + 360) % 360 / 360, + S = parseFloat(c[1]) / 100, + L = parseFloat(c[2]) / 100, + // calculate rgb according to the algorithm + // recommended by the CSS3 Color Module + m2 = L <= 0.5 ? L * (S + 1) : L + S - L * S, + m1 = 2 * L - m2; + a = [ + hue2rgb(m1, m2, H + 1 / 3) * 256, + hue2rgb(m1, m2, H) * 256, + hue2rgb(m1, m2, H - 1 / 3) * 256, + 1 + ]; + if(l == 4){ a[3] = c[3]; } + return dojo.colorFromArray(a, obj); // dojo.Color + } + } + return null; // dojo.Color + }; + + var confine = function(c, low, high){ + // summary: + // sanitize a color component by making sure it is a number, + // and clamping it to valid values + c = Number(c); + return isNaN(c) ? high : c < low ? low : c > high ? high : c; // Number + }; + + dojo.Color.prototype.sanitize = function(){ + // summary: makes sure that the object has correct attributes + var t = this; + t.r = Math.round(confine(t.r, 0, 255)); + t.g = Math.round(confine(t.g, 0, 255)); + t.b = Math.round(confine(t.b, 0, 255)); + t.a = confine(t.a, 0, 1); + return this; // dojo.Color + }; +})(); + + +dojo.colors.makeGrey = function(/*Number*/ g, /*Number?*/ a){ + // summary: creates a greyscale color with an optional alpha + return dojo.colorFromArray([g, g, g, a]); +}; + +// mixin all CSS3 named colors not already in _base, along with SVG 1.0 variant spellings +dojo.mixin(dojo.Color.named, { + aliceblue: [240,248,255], + antiquewhite: [250,235,215], + aquamarine: [127,255,212], + azure: [240,255,255], + beige: [245,245,220], + bisque: [255,228,196], + blanchedalmond: [255,235,205], + blueviolet: [138,43,226], + brown: [165,42,42], + burlywood: [222,184,135], + cadetblue: [95,158,160], + chartreuse: [127,255,0], + chocolate: [210,105,30], + coral: [255,127,80], + cornflowerblue: [100,149,237], + cornsilk: [255,248,220], + crimson: [220,20,60], + cyan: [0,255,255], + darkblue: [0,0,139], + darkcyan: [0,139,139], + darkgoldenrod: [184,134,11], + darkgray: [169,169,169], + darkgreen: [0,100,0], + darkgrey: [169,169,169], + darkkhaki: [189,183,107], + darkmagenta: [139,0,139], + darkolivegreen: [85,107,47], + darkorange: [255,140,0], + darkorchid: [153,50,204], + darkred: [139,0,0], + darksalmon: [233,150,122], + darkseagreen: [143,188,143], + darkslateblue: [72,61,139], + darkslategray: [47,79,79], + darkslategrey: [47,79,79], + darkturquoise: [0,206,209], + darkviolet: [148,0,211], + deeppink: [255,20,147], + deepskyblue: [0,191,255], + dimgray: [105,105,105], + dimgrey: [105,105,105], + dodgerblue: [30,144,255], + firebrick: [178,34,34], + floralwhite: [255,250,240], + forestgreen: [34,139,34], + gainsboro: [220,220,220], + ghostwhite: [248,248,255], + gold: [255,215,0], + goldenrod: [218,165,32], + greenyellow: [173,255,47], + grey: [128,128,128], + honeydew: [240,255,240], + hotpink: [255,105,180], + indianred: [205,92,92], + indigo: [75,0,130], + ivory: [255,255,240], + khaki: [240,230,140], + lavender: [230,230,250], + lavenderblush: [255,240,245], + lawngreen: [124,252,0], + lemonchiffon: [255,250,205], + lightblue: [173,216,230], + lightcoral: [240,128,128], + lightcyan: [224,255,255], + lightgoldenrodyellow: [250,250,210], + lightgray: [211,211,211], + lightgreen: [144,238,144], + lightgrey: [211,211,211], + lightpink: [255,182,193], + lightsalmon: [255,160,122], + lightseagreen: [32,178,170], + lightskyblue: [135,206,250], + lightslategray: [119,136,153], + lightslategrey: [119,136,153], + lightsteelblue: [176,196,222], + lightyellow: [255,255,224], + limegreen: [50,205,50], + linen: [250,240,230], + magenta: [255,0,255], + mediumaquamarine: [102,205,170], + mediumblue: [0,0,205], + mediumorchid: [186,85,211], + mediumpurple: [147,112,219], + mediumseagreen: [60,179,113], + mediumslateblue: [123,104,238], + mediumspringgreen: [0,250,154], + mediumturquoise: [72,209,204], + mediumvioletred: [199,21,133], + midnightblue: [25,25,112], + mintcream: [245,255,250], + mistyrose: [255,228,225], + moccasin: [255,228,181], + navajowhite: [255,222,173], + oldlace: [253,245,230], + olivedrab: [107,142,35], + orange: [255,165,0], + orangered: [255,69,0], + orchid: [218,112,214], + palegoldenrod: [238,232,170], + palegreen: [152,251,152], + paleturquoise: [175,238,238], + palevioletred: [219,112,147], + papayawhip: [255,239,213], + peachpuff: [255,218,185], + peru: [205,133,63], + pink: [255,192,203], + plum: [221,160,221], + powderblue: [176,224,230], + rosybrown: [188,143,143], + royalblue: [65,105,225], + saddlebrown: [139,69,19], + salmon: [250,128,114], + sandybrown: [244,164,96], + seagreen: [46,139,87], + seashell: [255,245,238], + sienna: [160,82,45], + skyblue: [135,206,235], + slateblue: [106,90,205], + slategray: [112,128,144], + slategrey: [112,128,144], + snow: [255,250,250], + springgreen: [0,255,127], + steelblue: [70,130,180], + tan: [210,180,140], + thistle: [216,191,216], + tomato: [255,99,71], + transparent: [0, 0, 0, 0], + turquoise: [64,224,208], + violet: [238,130,238], + wheat: [245,222,179], + whitesmoke: [245,245,245], + yellowgreen: [154,205,50] +}); + +} + +if(!dojo._hasResource["dojo.i18n"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.i18n"] = true; +dojo.provide("dojo.i18n"); + +/*===== +dojo.i18n = { + // summary: Utility classes to enable loading of resources for internationalization (i18n) +}; +=====*/ + +dojo.i18n.getLocalization = function(/*String*/packageName, /*String*/bundleName, /*String?*/locale){ + // summary: + // Returns an Object containing the localization for a given resource + // bundle in a package, matching the specified locale. + // description: + // Returns a hash containing name/value pairs in its prototypesuch + // that values can be easily overridden. Throws an exception if the + // bundle is not found. Bundle must have already been loaded by + // `dojo.requireLocalization()` or by a build optimization step. NOTE: + // try not to call this method as part of an object property + // definition (`var foo = { bar: dojo.i18n.getLocalization() }`). In + // some loading situations, the bundle may not be available in time + // for the object definition. Instead, call this method inside a + // function that is run after all modules load or the page loads (like + // in `dojo.addOnLoad()`), or in a widget lifecycle method. + // packageName: + // package which is associated with this resource + // bundleName: + // the base filename of the resource bundle (without the ".js" suffix) + // locale: + // the variant to load (optional). By default, the locale defined by + // the host environment: dojo.locale + + locale = dojo.i18n.normalizeLocale(locale); + + // look for nearest locale match + var elements = locale.split('-'); + var module = [packageName,"nls",bundleName].join('.'); + var bundle = dojo._loadedModules[module]; + if(bundle){ + var localization; + for(var i = elements.length; i > 0; i--){ + var loc = elements.slice(0, i).join('_'); + if(bundle[loc]){ + localization = bundle[loc]; + break; + } + } + if(!localization){ + localization = bundle.ROOT; + } + + // make a singleton prototype so that the caller won't accidentally change the values globally + if(localization){ + var clazz = function(){}; + clazz.prototype = localization; + return new clazz(); // Object + } + } + + throw new Error("Bundle not found: " + bundleName + " in " + packageName+" , locale=" + locale); +}; + +dojo.i18n.normalizeLocale = function(/*String?*/locale){ + // summary: + // Returns canonical form of locale, as used by Dojo. + // + // description: + // All variants are case-insensitive and are separated by '-' as specified in [RFC 3066](http://www.ietf.org/rfc/rfc3066.txt). + // If no locale is specified, the dojo.locale is returned. dojo.locale is defined by + // the user agent's locale unless overridden by djConfig. + + var result = locale ? locale.toLowerCase() : dojo.locale; + if(result == "root"){ + result = "ROOT"; + } + return result; // String +}; + +dojo.i18n._requireLocalization = function(/*String*/moduleName, /*String*/bundleName, /*String?*/locale, /*String?*/availableFlatLocales){ + // summary: + // See dojo.requireLocalization() + // description: + // Called by the bootstrap, but factored out so that it is only + // included in the build when needed. + + var targetLocale = dojo.i18n.normalizeLocale(locale); + var bundlePackage = [moduleName, "nls", bundleName].join("."); + // NOTE: + // When loading these resources, the packaging does not match what is + // on disk. This is an implementation detail, as this is just a + // private data structure to hold the loaded resources. e.g. + // `tests/hello/nls/en-us/salutations.js` is loaded as the object + // `tests.hello.nls.salutations.en_us={...}` The structure on disk is + // intended to be most convenient for developers and translators, but + // in memory it is more logical and efficient to store in a different + // order. Locales cannot use dashes, since the resulting path will + // not evaluate as valid JS, so we translate them to underscores. + + //Find the best-match locale to load if we have available flat locales. + var bestLocale = ""; + if(availableFlatLocales){ + var flatLocales = availableFlatLocales.split(","); + for(var i = 0; i < flatLocales.length; i++){ + //Locale must match from start of string. + //Using ["indexOf"] so customBase builds do not see + //this as a dojo._base.array dependency. + if(targetLocale["indexOf"](flatLocales[i]) == 0){ + if(flatLocales[i].length > bestLocale.length){ + bestLocale = flatLocales[i]; + } + } + } + if(!bestLocale){ + bestLocale = "ROOT"; + } + } + + //See if the desired locale is already loaded. + var tempLocale = availableFlatLocales ? bestLocale : targetLocale; + var bundle = dojo._loadedModules[bundlePackage]; + var localizedBundle = null; + if(bundle){ + if(dojo.config.localizationComplete && bundle._built){return;} + var jsLoc = tempLocale.replace(/-/g, '_'); + var translationPackage = bundlePackage+"."+jsLoc; + localizedBundle = dojo._loadedModules[translationPackage]; + } + + if(!localizedBundle){ + bundle = dojo["provide"](bundlePackage); + var syms = dojo._getModuleSymbols(moduleName); + var modpath = syms.concat("nls").join("/"); + var parent; + + dojo.i18n._searchLocalePath(tempLocale, availableFlatLocales, function(loc){ + var jsLoc = loc.replace(/-/g, '_'); + var translationPackage = bundlePackage + "." + jsLoc; + var loaded = false; + if(!dojo._loadedModules[translationPackage]){ + // Mark loaded whether it's found or not, so that further load attempts will not be made + dojo["provide"](translationPackage); + var module = [modpath]; + if(loc != "ROOT"){module.push(loc);} + module.push(bundleName); + var filespec = module.join("/") + '.js'; + loaded = dojo._loadPath(filespec, null, function(hash){ + // Use singleton with prototype to point to parent bundle, then mix-in result from loadPath + var clazz = function(){}; + clazz.prototype = parent; + bundle[jsLoc] = new clazz(); + for(var j in hash){ bundle[jsLoc][j] = hash[j]; } + }); + }else{ + loaded = true; + } + if(loaded && bundle[jsLoc]){ + parent = bundle[jsLoc]; + }else{ + bundle[jsLoc] = parent; + } + + if(availableFlatLocales){ + //Stop the locale path searching if we know the availableFlatLocales, since + //the first call to this function will load the only bundle that is needed. + return true; + } + }); + } + + //Save the best locale bundle as the target locale bundle when we know the + //the available bundles. + if(availableFlatLocales && targetLocale != bestLocale){ + bundle[targetLocale.replace(/-/g, '_')] = bundle[bestLocale.replace(/-/g, '_')]; + } +}; + +(function(){ + // If other locales are used, dojo.requireLocalization should load them as + // well, by default. + // + // Override dojo.requireLocalization to do load the default bundle, then + // iterate through the extraLocale list and load those translations as + // well, unless a particular locale was requested. + + var extra = dojo.config.extraLocale; + if(extra){ + if(!extra instanceof Array){ + extra = [extra]; + } + + var req = dojo.i18n._requireLocalization; + dojo.i18n._requireLocalization = function(m, b, locale, availableFlatLocales){ + req(m,b,locale, availableFlatLocales); + if(locale){return;} + for(var i=0; i 0; i--){ + searchlist.push(elements.slice(0, i).join('-')); + } + searchlist.push(false); + if(down){searchlist.reverse();} + + for(var j = searchlist.length - 1; j >= 0; j--){ + var loc = searchlist[j] || "ROOT"; + var stop = searchFunc(loc); + if(stop){ break; } + } +}; + +dojo.i18n._preloadLocalizations = function(/*String*/bundlePrefix, /*Array*/localesGenerated){ + // summary: + // Load built, flattened resource bundles, if available for all + // locales used in the page. Only called by built layer files. + + function preload(locale){ + locale = dojo.i18n.normalizeLocale(locale); + dojo.i18n._searchLocalePath(locale, true, function(loc){ + for(var i=0; i= 0){ + dojo.removeClass(this._cells[this._selectedCell].node, "dijitPaletteCellSelected"); + } + this._selectedCell = -1; + + // search for cell matching specified value + if(value){ + for(var i = 0; i < this._cells.length; i++){ + if(value == this._cells[i].dye.getValue()){ + this._selectedCell = i; + this.value = value; + + dojo.addClass(this._cells[i].node, "dijitPaletteCellSelected"); + + if(priorityChange || priorityChange === undefined){ + this.onChange(value); + } + + break; + } + } + } + }, + + onChange: function(value){ + // summary: + // Callback when a cell is selected. + // value: String + // Value corresponding to cell. + }, + + _navigateByKey: function(increment, typeCount){ + // summary: + // This is the callback for typematic. + // It changes the focus and the highlighed cell. + // increment: + // How much the key is navigated. + // typeCount: + // How many times typematic has fired. + // tags: + // private + + // typecount == -1 means the key is released. + if(typeCount == -1){ return; } + + var newFocusIndex = this._currentFocus.index + increment; + if(newFocusIndex < this._cells.length && newFocusIndex > -1){ + var focusNode = this._cells[newFocusIndex].node; + this._setCurrent(focusNode); + + // Actually focus the node, for the benefit of screen readers. + // Use setTimeout because IE doesn't like changing focus inside of an event handler + setTimeout(dojo.hitch(dijit, "focus", focusNode), 0); + } + }, + + _getDye: function(/*DomNode*/ cell){ + // summary: + // Get JS object for given cell DOMNode + + return this._cells[cell.index].dye; + } +}); + +/*===== +dojo.declare("dijit.Dye", + null, + { + // summary: + // Interface for the JS Object associated with a palette cell (i.e. DOMNode) + + constructor: function(alias){ + // summary: + // Initialize according to value or alias like "white" + // alias: String + }, + + getValue: function(){ + // summary: + // Return "value" of cell; meaning of "value" varies by subclass. + // description: + // For example color hex value, emoticon ascii value etc, entity hex value. + }, + + fillCell: function(cell, blankGif){ + // summary: + // Add cell DOMNode inner structure + // cell: DomNode + // The surrounding cell + // blankGif: String + // URL for blank cell image + } + } +); +=====*/ + +} + +if(!dojo._hasResource["dijit.ColorPalette"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.ColorPalette"] = true; +dojo.provide("dijit.ColorPalette"); + + + + + + + + + + +dojo.declare("dijit.ColorPalette", + [dijit._Widget, dijit._Templated, dijit._PaletteMixin], + { + // summary: + // A keyboard accessible color-picking widget + // description: + // Grid showing various colors, so the user can pick a certain color. + // Can be used standalone, or as a popup. + // + // example: + // |
+ // + // example: + // | var picker = new dijit.ColorPalette({ },srcNode); + // | picker.startup(); + + + // palette: String + // Size of grid, either "7x10" or "3x4". + palette: "7x10", + + // _palettes: [protected] Map + // This represents the value of the colors. + // The first level is a hashmap of the different palettes available. + // The next two dimensions represent the columns and rows of colors. + _palettes: { + "7x10": [["white", "seashell", "cornsilk", "lemonchiffon","lightyellow", "palegreen", "paleturquoise", "lightcyan", "lavender", "plum"], + ["lightgray", "pink", "bisque", "moccasin", "khaki", "lightgreen", "lightseagreen", "lightskyblue", "cornflowerblue", "violet"], + ["silver", "lightcoral", "sandybrown", "orange", "palegoldenrod", "chartreuse", "mediumturquoise", "skyblue", "mediumslateblue","orchid"], + ["gray", "red", "orangered", "darkorange", "yellow", "limegreen", "darkseagreen", "royalblue", "slateblue", "mediumorchid"], + ["dimgray", "crimson", "chocolate", "coral", "gold", "forestgreen", "seagreen", "blue", "blueviolet", "darkorchid"], + ["darkslategray","firebrick","saddlebrown", "sienna", "olive", "green", "darkcyan", "mediumblue","darkslateblue", "darkmagenta" ], + ["black", "darkred", "maroon", "brown", "darkolivegreen", "darkgreen", "midnightblue", "navy", "indigo", "purple"]], + + "3x4": [["white", "lime", "green", "blue"], + ["silver", "yellow", "fuchsia", "navy"], + ["gray", "red", "purple", "black"]] + }, + + // _imagePaths: [protected] Map + // This is stores the path to the palette images + _imagePaths: { + "7x10": dojo.moduleUrl("dijit.themes", "a11y/colors7x10.png"), + "3x4": dojo.moduleUrl("dijit.themes", "a11y/colors3x4.png"), + "7x10-rtl": dojo.moduleUrl("dijit.themes", "a11y/colors7x10-rtl.png"), + "3x4-rtl": dojo.moduleUrl("dijit.themes", "a11y/colors3x4-rtl.png") + }, + + // templateString: String + // The template of this widget. + templateString: dojo.cache("dijit", "templates/ColorPalette.html", "
\n\t\"\"/\n\t\n\t\t\n\t
\n
\n"), + + baseClass: "dijitColorPalette", + + dyeClass: 'dijit._Color', + + buildRendering: function(){ + // Instantiate the template, which makes a skeleton into which we'll insert a bunch of + // nodes + + this.inherited(arguments); + + this.imageNode.setAttribute("src", this._imagePaths[this.palette + (this.isLeftToRight() ? "" : "-rtl")].toString()); + + var i18nColorNames = dojo.i18n.getLocalization("dojo", "colors", this.lang); + this._preparePalette( + this._palettes[this.palette], + i18nColorNames + ); + } +}); + +dojo.declare("dijit._Color", dojo.Color, + // summary: + // Object associated with each cell in a ColorPalette palette. + // Implements dijit.Dye. + { + constructor: function(/*String*/alias){ + this._alias = alias; + this.setColor(dojo.Color.named[alias]); + }, + + getValue: function(){ + // summary: + // Note that although dijit._Color is initialized with a value like "white" getValue() always + // returns a hex value + return this.toHex(); + }, + + fillCell: function(/*DOMNode*/ cell, /*String*/ blankGif){ + dojo.create("img", { + src: blankGif, + "class": "dijitPaletteImg", + alt: this._alias + }, cell); + } + } +); + +} + +if(!dojo._hasResource["dojo.dnd.common"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.dnd.common"] = true; +dojo.provide("dojo.dnd.common"); + +dojo.dnd.getCopyKeyState = dojo.isCopyKey; + +dojo.dnd._uniqueId = 0; +dojo.dnd.getUniqueId = function(){ + // summary: + // returns a unique string for use with any DOM element + var id; + do{ + id = dojo._scopeName + "Unique" + (++dojo.dnd._uniqueId); + }while(dojo.byId(id)); + return id; +}; + +dojo.dnd._empty = {}; + +dojo.dnd.isFormElement = function(/*Event*/ e){ + // summary: + // returns true if user clicked on a form element + var t = e.target; + if(t.nodeType == 3 /*TEXT_NODE*/){ + t = t.parentNode; + } + return " button textarea input select option ".indexOf(" " + t.tagName.toLowerCase() + " ") >= 0; // Boolean +}; + +} + +if(!dojo._hasResource["dojo.dnd.autoscroll"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.dnd.autoscroll"] = true; +dojo.provide("dojo.dnd.autoscroll"); + +dojo.dnd.getViewport = function(){ + // summary: + // Returns a viewport size (visible part of the window) + + // TODO: remove this when getViewport() moved to dojo core, see #7028 + + // FIXME: need more docs!! + var d = dojo.doc, dd = d.documentElement, w = window, b = dojo.body(); + if(dojo.isMozilla){ + return {w: dd.clientWidth, h: w.innerHeight}; // Object + }else if(!dojo.isOpera && w.innerWidth){ + return {w: w.innerWidth, h: w.innerHeight}; // Object + }else if (!dojo.isOpera && dd && dd.clientWidth){ + return {w: dd.clientWidth, h: dd.clientHeight}; // Object + }else if (b.clientWidth){ + return {w: b.clientWidth, h: b.clientHeight}; // Object + } + return null; // Object +}; + +dojo.dnd.V_TRIGGER_AUTOSCROLL = 32; +dojo.dnd.H_TRIGGER_AUTOSCROLL = 32; + +dojo.dnd.V_AUTOSCROLL_VALUE = 16; +dojo.dnd.H_AUTOSCROLL_VALUE = 16; + +dojo.dnd.autoScroll = function(e){ + // summary: + // a handler for onmousemove event, which scrolls the window, if + // necesary + // e: Event + // onmousemove event + + // FIXME: needs more docs! + var v = dojo.dnd.getViewport(), dx = 0, dy = 0; + if(e.clientX < dojo.dnd.H_TRIGGER_AUTOSCROLL){ + dx = -dojo.dnd.H_AUTOSCROLL_VALUE; + }else if(e.clientX > v.w - dojo.dnd.H_TRIGGER_AUTOSCROLL){ + dx = dojo.dnd.H_AUTOSCROLL_VALUE; + } + if(e.clientY < dojo.dnd.V_TRIGGER_AUTOSCROLL){ + dy = -dojo.dnd.V_AUTOSCROLL_VALUE; + }else if(e.clientY > v.h - dojo.dnd.V_TRIGGER_AUTOSCROLL){ + dy = dojo.dnd.V_AUTOSCROLL_VALUE; + } + window.scrollBy(dx, dy); +}; + +dojo.dnd._validNodes = {"div": 1, "p": 1, "td": 1}; +dojo.dnd._validOverflow = {"auto": 1, "scroll": 1}; + +dojo.dnd.autoScrollNodes = function(e){ + // summary: + // a handler for onmousemove event, which scrolls the first avaialble + // Dom element, it falls back to dojo.dnd.autoScroll() + // e: Event + // onmousemove event + + // FIXME: needs more docs! + for(var n = e.target; n;){ + if(n.nodeType == 1 && (n.tagName.toLowerCase() in dojo.dnd._validNodes)){ + var s = dojo.getComputedStyle(n); + if(s.overflow.toLowerCase() in dojo.dnd._validOverflow){ + var b = dojo._getContentBox(n, s), t = dojo.position(n, true); + //console.log(b.l, b.t, t.x, t.y, n.scrollLeft, n.scrollTop); + var w = Math.min(dojo.dnd.H_TRIGGER_AUTOSCROLL, b.w / 2), + h = Math.min(dojo.dnd.V_TRIGGER_AUTOSCROLL, b.h / 2), + rx = e.pageX - t.x, ry = e.pageY - t.y, dx = 0, dy = 0; + if(dojo.isWebKit || dojo.isOpera){ + // FIXME: this code should not be here, it should be taken into account + // either by the event fixing code, or the dojo.position() + // FIXME: this code doesn't work on Opera 9.5 Beta + rx += dojo.body().scrollLeft, ry += dojo.body().scrollTop; + } + if(rx > 0 && rx < b.w){ + if(rx < w){ + dx = -w; + }else if(rx > b.w - w){ + dx = w; + } + } + //console.log("ry =", ry, "b.h =", b.h, "h =", h); + if(ry > 0 && ry < b.h){ + if(ry < h){ + dy = -h; + }else if(ry > b.h - h){ + dy = h; + } + } + var oldLeft = n.scrollLeft, oldTop = n.scrollTop; + n.scrollLeft = n.scrollLeft + dx; + n.scrollTop = n.scrollTop + dy; + if(oldLeft != n.scrollLeft || oldTop != n.scrollTop){ return; } + } + } + try{ + n = n.parentNode; + }catch(x){ + n = null; + } + } + dojo.dnd.autoScroll(e); +}; + +} + +if(!dojo._hasResource["dojo.dnd.Mover"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.dnd.Mover"] = true; +dojo.provide("dojo.dnd.Mover"); + + + + +dojo.declare("dojo.dnd.Mover", null, { + constructor: function(node, e, host){ + // summary: + // an object, which makes a node follow the mouse. + // Used as a default mover, and as a base class for custom movers. + // node: Node + // a node (or node's id) to be moved + // e: Event + // a mouse event, which started the move; + // only pageX and pageY properties are used + // host: Object? + // object which implements the functionality of the move, + // and defines proper events (onMoveStart and onMoveStop) + this.node = dojo.byId(node); + this.marginBox = {l: e.pageX, t: e.pageY}; + this.mouseButton = e.button; + var h = this.host = host, d = node.ownerDocument, + firstEvent = dojo.connect(d, "onmousemove", this, "onFirstMove"); + this.events = [ + dojo.connect(d, "onmousemove", this, "onMouseMove"), + dojo.connect(d, "onmouseup", this, "onMouseUp"), + // cancel text selection and text dragging + dojo.connect(d, "ondragstart", dojo.stopEvent), + dojo.connect(d.body, "onselectstart", dojo.stopEvent), + firstEvent + ]; + // notify that the move has started + if(h && h.onMoveStart){ + h.onMoveStart(this); + } + }, + // mouse event processors + onMouseMove: function(e){ + // summary: + // event processor for onmousemove + // e: Event + // mouse event + dojo.dnd.autoScroll(e); + var m = this.marginBox; + this.host.onMove(this, {l: m.l + e.pageX, t: m.t + e.pageY}, e); + dojo.stopEvent(e); + }, + onMouseUp: function(e){ + if(dojo.isWebKit && dojo.isMac && this.mouseButton == 2 ? + e.button == 0 : this.mouseButton == e.button){ + this.destroy(); + } + dojo.stopEvent(e); + }, + // utilities + onFirstMove: function(e){ + // summary: + // makes the node absolute; it is meant to be called only once. + // relative and absolutely positioned nodes are assumed to use pixel units + var s = this.node.style, l, t, h = this.host; + switch(s.position){ + case "relative": + case "absolute": + // assume that left and top values are in pixels already + l = Math.round(parseFloat(s.left)) || 0; + t = Math.round(parseFloat(s.top)) || 0; + break; + default: + s.position = "absolute"; // enforcing the absolute mode + var m = dojo.marginBox(this.node); + // event.pageX/pageY (which we used to generate the initial + // margin box) includes padding and margin set on the body. + // However, setting the node's position to absolute and then + // doing dojo.marginBox on it *doesn't* take that additional + // space into account - so we need to subtract the combined + // padding and margin. We use getComputedStyle and + // _getMarginBox/_getContentBox to avoid the extra lookup of + // the computed style. + var b = dojo.doc.body; + var bs = dojo.getComputedStyle(b); + var bm = dojo._getMarginBox(b, bs); + var bc = dojo._getContentBox(b, bs); + l = m.l - (bc.l - bm.l); + t = m.t - (bc.t - bm.t); + break; + } + this.marginBox.l = l - this.marginBox.l; + this.marginBox.t = t - this.marginBox.t; + if(h && h.onFirstMove){ + h.onFirstMove(this, e); + } + dojo.disconnect(this.events.pop()); + }, + destroy: function(){ + // summary: + // stops the move, deletes all references, so the object can be garbage-collected + dojo.forEach(this.events, dojo.disconnect); + // undo global settings + var h = this.host; + if(h && h.onMoveStop){ + h.onMoveStop(this); + } + // destroy objects + this.events = this.node = this.host = null; + } +}); + +} + +if(!dojo._hasResource["dojo.dnd.Moveable"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.dnd.Moveable"] = true; +dojo.provide("dojo.dnd.Moveable"); + + + +/*===== +dojo.declare("dojo.dnd.__MoveableArgs", [], { + // handle: Node||String + // A node (or node's id), which is used as a mouse handle. + // If omitted, the node itself is used as a handle. + handle: null, + + // delay: Number + // delay move by this number of pixels + delay: 0, + + // skip: Boolean + // skip move of form elements + skip: false, + + // mover: Object + // a constructor of custom Mover + mover: dojo.dnd.Mover +}); +=====*/ + +dojo.declare("dojo.dnd.Moveable", null, { + // object attributes (for markup) + handle: "", + delay: 0, + skip: false, + + constructor: function(node, params){ + // summary: + // an object, which makes a node moveable + // node: Node + // a node (or node's id) to be moved + // params: dojo.dnd.__MoveableArgs? + // optional parameters + this.node = dojo.byId(node); + if(!params){ params = {}; } + this.handle = params.handle ? dojo.byId(params.handle) : null; + if(!this.handle){ this.handle = this.node; } + this.delay = params.delay > 0 ? params.delay : 0; + this.skip = params.skip; + this.mover = params.mover ? params.mover : dojo.dnd.Mover; + this.events = [ + dojo.connect(this.handle, "onmousedown", this, "onMouseDown"), + // cancel text selection and text dragging + dojo.connect(this.handle, "ondragstart", this, "onSelectStart"), + dojo.connect(this.handle, "onselectstart", this, "onSelectStart") + ]; + }, + + // markup methods + markupFactory: function(params, node){ + return new dojo.dnd.Moveable(node, params); + }, + + // methods + destroy: function(){ + // summary: + // stops watching for possible move, deletes all references, so the object can be garbage-collected + dojo.forEach(this.events, dojo.disconnect); + this.events = this.node = this.handle = null; + }, + + // mouse event processors + onMouseDown: function(e){ + // summary: + // event processor for onmousedown, creates a Mover for the node + // e: Event + // mouse event + if(this.skip && dojo.dnd.isFormElement(e)){ return; } + if(this.delay){ + this.events.push( + dojo.connect(this.handle, "onmousemove", this, "onMouseMove"), + dojo.connect(this.handle, "onmouseup", this, "onMouseUp") + ); + this._lastX = e.pageX; + this._lastY = e.pageY; + }else{ + this.onDragDetected(e); + } + dojo.stopEvent(e); + }, + onMouseMove: function(e){ + // summary: + // event processor for onmousemove, used only for delayed drags + // e: Event + // mouse event + if(Math.abs(e.pageX - this._lastX) > this.delay || Math.abs(e.pageY - this._lastY) > this.delay){ + this.onMouseUp(e); + this.onDragDetected(e); + } + dojo.stopEvent(e); + }, + onMouseUp: function(e){ + // summary: + // event processor for onmouseup, used only for delayed drags + // e: Event + // mouse event + for(var i = 0; i < 2; ++i){ + dojo.disconnect(this.events.pop()); + } + dojo.stopEvent(e); + }, + onSelectStart: function(e){ + // summary: + // event processor for onselectevent and ondragevent + // e: Event + // mouse event + if(!this.skip || !dojo.dnd.isFormElement(e)){ + dojo.stopEvent(e); + } + }, + + // local events + onDragDetected: function(/* Event */ e){ + // summary: + // called when the drag is detected; + // responsible for creation of the mover + new this.mover(this.node, e, this); + }, + onMoveStart: function(/* dojo.dnd.Mover */ mover){ + // summary: + // called before every move operation + dojo.publish("/dnd/move/start", [mover]); + dojo.addClass(dojo.body(), "dojoMove"); + dojo.addClass(this.node, "dojoMoveItem"); + }, + onMoveStop: function(/* dojo.dnd.Mover */ mover){ + // summary: + // called after every move operation + dojo.publish("/dnd/move/stop", [mover]); + dojo.removeClass(dojo.body(), "dojoMove"); + dojo.removeClass(this.node, "dojoMoveItem"); + }, + onFirstMove: function(/* dojo.dnd.Mover */ mover, /* Event */ e){ + // summary: + // called during the very first move notification; + // can be used to initialize coordinates, can be overwritten. + + // default implementation does nothing + }, + onMove: function(/* dojo.dnd.Mover */ mover, /* Object */ leftTop, /* Event */ e){ + // summary: + // called during every move notification; + // should actually move the node; can be overwritten. + this.onMoving(mover, leftTop); + var s = mover.node.style; + s.left = leftTop.l + "px"; + s.top = leftTop.t + "px"; + this.onMoved(mover, leftTop); + }, + onMoving: function(/* dojo.dnd.Mover */ mover, /* Object */ leftTop){ + // summary: + // called before every incremental move; can be overwritten. + + // default implementation does nothing + }, + onMoved: function(/* dojo.dnd.Mover */ mover, /* Object */ leftTop){ + // summary: + // called after every incremental move; can be overwritten. + + // default implementation does nothing + } +}); + +} + +if(!dojo._hasResource["dojo.dnd.move"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.dnd.move"] = true; +dojo.provide("dojo.dnd.move"); + + + + +/*===== +dojo.declare("dojo.dnd.move.__constrainedMoveableArgs", [dojo.dnd.__MoveableArgs], { + // constraints: Function + // Calculates a constraint box. + // It is called in a context of the moveable object. + constraints: function(){}, + + // within: Boolean + // restrict move within boundaries. + within: false +}); +=====*/ + +dojo.declare("dojo.dnd.move.constrainedMoveable", dojo.dnd.Moveable, { + // object attributes (for markup) + constraints: function(){}, + within: false, + + // markup methods + markupFactory: function(params, node){ + return new dojo.dnd.move.constrainedMoveable(node, params); + }, + + constructor: function(node, params){ + // summary: + // an object that makes a node moveable + // node: Node + // a node (or node's id) to be moved + // params: dojo.dnd.move.__constrainedMoveableArgs? + // an optional object with additional parameters; + // the rest is passed to the base class + if(!params){ params = {}; } + this.constraints = params.constraints; + this.within = params.within; + }, + onFirstMove: function(/* dojo.dnd.Mover */ mover){ + // summary: + // called during the very first move notification; + // can be used to initialize coordinates, can be overwritten. + var c = this.constraintBox = this.constraints.call(this, mover); + c.r = c.l + c.w; + c.b = c.t + c.h; + if(this.within){ + var mb = dojo.marginBox(mover.node); + c.r -= mb.w; + c.b -= mb.h; + } + }, + onMove: function(/* dojo.dnd.Mover */ mover, /* Object */ leftTop){ + // summary: + // called during every move notification; + // should actually move the node; can be overwritten. + var c = this.constraintBox, s = mover.node.style; + s.left = (leftTop.l < c.l ? c.l : c.r < leftTop.l ? c.r : leftTop.l) + "px"; + s.top = (leftTop.t < c.t ? c.t : c.b < leftTop.t ? c.b : leftTop.t) + "px"; + } +}); + +/*===== +dojo.declare("dojo.dnd.move.__boxConstrainedMoveableArgs", [dojo.dnd.move.__constrainedMoveableArgs], { + // box: Object + // a constraint box + box: {} +}); +=====*/ + +dojo.declare("dojo.dnd.move.boxConstrainedMoveable", dojo.dnd.move.constrainedMoveable, { + // box: + // object attributes (for markup) + box: {}, + + // markup methods + markupFactory: function(params, node){ + return new dojo.dnd.move.boxConstrainedMoveable(node, params); + }, + + constructor: function(node, params){ + // summary: + // an object, which makes a node moveable + // node: Node + // a node (or node's id) to be moved + // params: dojo.dnd.move.__boxConstrainedMoveableArgs? + // an optional object with parameters + var box = params && params.box; + this.constraints = function(){ return box; }; + } +}); + +/*===== +dojo.declare("dojo.dnd.move.__parentConstrainedMoveableArgs", [dojo.dnd.move.__constrainedMoveableArgs], { + // area: String + // A parent's area to restrict the move. + // Can be "margin", "border", "padding", or "content". + area: "" +}); +=====*/ + +dojo.declare("dojo.dnd.move.parentConstrainedMoveable", dojo.dnd.move.constrainedMoveable, { + // area: + // object attributes (for markup) + area: "content", + + // markup methods + markupFactory: function(params, node){ + return new dojo.dnd.move.parentConstrainedMoveable(node, params); + }, + + constructor: function(node, params){ + // summary: + // an object, which makes a node moveable + // node: Node + // a node (or node's id) to be moved + // params: dojo.dnd.move.__parentConstrainedMoveableArgs? + // an optional object with parameters + var area = params && params.area; + this.constraints = function(){ + var n = this.node.parentNode, + s = dojo.getComputedStyle(n), + mb = dojo._getMarginBox(n, s); + if(area == "margin"){ + return mb; // Object + } + var t = dojo._getMarginExtents(n, s); + mb.l += t.l, mb.t += t.t, mb.w -= t.w, mb.h -= t.h; + if(area == "border"){ + return mb; // Object + } + t = dojo._getBorderExtents(n, s); + mb.l += t.l, mb.t += t.t, mb.w -= t.w, mb.h -= t.h; + if(area == "padding"){ + return mb; // Object + } + t = dojo._getPadExtents(n, s); + mb.l += t.l, mb.t += t.t, mb.w -= t.w, mb.h -= t.h; + return mb; // Object + }; + } +}); + +// WARNING: below are obsolete objects, instead of custom movers use custom moveables (above) + +dojo.dnd.move.constrainedMover = function(fun, within){ + // summary: + // returns a constrained version of dojo.dnd.Mover + // description: + // this function produces n object, which will put a constraint on + // the margin box of dragged object in absolute coordinates + // fun: Function + // called on drag, and returns a constraint box + // within: Boolean + // if true, constraints the whole dragged object withtin the rectangle, + // otherwise the constraint is applied to the left-top corner + + dojo.deprecated("dojo.dnd.move.constrainedMover, use dojo.dnd.move.constrainedMoveable instead"); + var mover = function(node, e, notifier){ + dojo.dnd.Mover.call(this, node, e, notifier); + }; + dojo.extend(mover, dojo.dnd.Mover.prototype); + dojo.extend(mover, { + onMouseMove: function(e){ + // summary: event processor for onmousemove + // e: Event: mouse event + dojo.dnd.autoScroll(e); + var m = this.marginBox, c = this.constraintBox, + l = m.l + e.pageX, t = m.t + e.pageY; + l = l < c.l ? c.l : c.r < l ? c.r : l; + t = t < c.t ? c.t : c.b < t ? c.b : t; + this.host.onMove(this, {l: l, t: t}); + }, + onFirstMove: function(){ + // summary: called once to initialize things; it is meant to be called only once + dojo.dnd.Mover.prototype.onFirstMove.call(this); + var c = this.constraintBox = fun.call(this); + c.r = c.l + c.w; + c.b = c.t + c.h; + if(within){ + var mb = dojo.marginBox(this.node); + c.r -= mb.w; + c.b -= mb.h; + } + } + }); + return mover; // Object +}; + +dojo.dnd.move.boxConstrainedMover = function(box, within){ + // summary: + // a specialization of dojo.dnd.constrainedMover, which constrains to the specified box + // box: Object + // a constraint box (l, t, w, h) + // within: Boolean + // if true, constraints the whole dragged object withtin the rectangle, + // otherwise the constraint is applied to the left-top corner + + dojo.deprecated("dojo.dnd.move.boxConstrainedMover, use dojo.dnd.move.boxConstrainedMoveable instead"); + return dojo.dnd.move.constrainedMover(function(){ return box; }, within); // Object +}; + +dojo.dnd.move.parentConstrainedMover = function(area, within){ + // summary: + // a specialization of dojo.dnd.constrainedMover, which constrains to the parent node + // area: String + // "margin" to constrain within the parent's margin box, "border" for the border box, + // "padding" for the padding box, and "content" for the content box; "content" is the default value. + // within: Boolean + // if true, constraints the whole dragged object within the rectangle, + // otherwise the constraint is applied to the left-top corner + + dojo.deprecated("dojo.dnd.move.parentConstrainedMover, use dojo.dnd.move.parentConstrainedMoveable instead"); + var fun = function(){ + var n = this.node.parentNode, + s = dojo.getComputedStyle(n), + mb = dojo._getMarginBox(n, s); + if(area == "margin"){ + return mb; // Object + } + var t = dojo._getMarginExtents(n, s); + mb.l += t.l, mb.t += t.t, mb.w -= t.w, mb.h -= t.h; + if(area == "border"){ + return mb; // Object + } + t = dojo._getBorderExtents(n, s); + mb.l += t.l, mb.t += t.t, mb.w -= t.w, mb.h -= t.h; + if(area == "padding"){ + return mb; // Object + } + t = dojo._getPadExtents(n, s); + mb.l += t.l, mb.t += t.t, mb.w -= t.w, mb.h -= t.h; + return mb; // Object + }; + return dojo.dnd.move.constrainedMover(fun, within); // Object +}; + +// patching functions one level up for compatibility + +dojo.dnd.constrainedMover = dojo.dnd.move.constrainedMover; +dojo.dnd.boxConstrainedMover = dojo.dnd.move.boxConstrainedMover; +dojo.dnd.parentConstrainedMover = dojo.dnd.move.parentConstrainedMover; + +} + +if(!dojo._hasResource["dojo.dnd.TimedMoveable"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.dnd.TimedMoveable"] = true; +dojo.provide("dojo.dnd.TimedMoveable"); + + + +/*===== +dojo.declare("dojo.dnd.__TimedMoveableArgs", [dojo.dnd.__MoveableArgs], { + // timeout: Number + // delay move by this number of ms, + // accumulating position changes during the timeout + timeout: 0 +}); +=====*/ + +(function(){ + // precalculate long expressions + var oldOnMove = dojo.dnd.Moveable.prototype.onMove; + + dojo.declare("dojo.dnd.TimedMoveable", dojo.dnd.Moveable, { + // summary: + // A specialized version of Moveable to support an FPS throttling. + // This class puts an upper restriction on FPS, which may reduce + // the CPU load. The additional parameter "timeout" regulates + // the delay before actually moving the moveable object. + + // object attributes (for markup) + timeout: 40, // in ms, 40ms corresponds to 25 fps + + constructor: function(node, params){ + // summary: + // an object that makes a node moveable with a timer + // node: Node||String + // a node (or node's id) to be moved + // params: dojo.dnd.__TimedMoveableArgs + // object with additional parameters. + + // sanitize parameters + if(!params){ params = {}; } + if(params.timeout && typeof params.timeout == "number" && params.timeout >= 0){ + this.timeout = params.timeout; + } + }, + + // markup methods + markupFactory: function(params, node){ + return new dojo.dnd.TimedMoveable(node, params); + }, + + onMoveStop: function(/* dojo.dnd.Mover */ mover){ + if(mover._timer){ + // stop timer + clearTimeout(mover._timer) + // reflect the last received position + oldOnMove.call(this, mover, mover._leftTop) + } + dojo.dnd.Moveable.prototype.onMoveStop.apply(this, arguments); + }, + onMove: function(/* dojo.dnd.Mover */ mover, /* Object */ leftTop){ + mover._leftTop = leftTop; + if(!mover._timer){ + var _t = this; // to avoid using dojo.hitch() + mover._timer = setTimeout(function(){ + // we don't have any pending requests + mover._timer = null; + // reflect the last received position + oldOnMove.call(_t, mover, mover._leftTop); + }, this.timeout); + } + } + }); +})(); + +} + +if(!dojo._hasResource["dijit.form._FormMixin"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.form._FormMixin"] = true; +dojo.provide("dijit.form._FormMixin"); + + + +dojo.declare("dijit.form._FormMixin", null, + { + // summary: + // Mixin for containers of form widgets (i.e. widgets that represent a single value + // and can be children of a
node or dijit.form.Form widget) + // description: + // Can extract all the form widgets + // values and combine them into a single javascript object, or alternately + // take such an object and set the values for all the contained + // form widgets + +/*===== + // value: Object + // Name/value hash for each child widget with a name and value. + // Child widgets without names are not part of the hash. + // + // If there are multiple child widgets w/the same name, value is an array, + // unless they are radio buttons in which case value is a scalar (since only + // one radio button can be checked at a time). + // + // If a child widget's name is a dot separated list (like a.b.c.d), it's a nested structure. + // + // Example: + // | { name: "John Smith", interests: ["sports", "movies"] } +=====*/ + + // TODO: + // * Repeater + // * better handling for arrays. Often form elements have names with [] like + // * people[3].sex (for a list of people [{name: Bill, sex: M}, ...]) + // + // + + reset: function(){ + dojo.forEach(this.getDescendants(), function(widget){ + if(widget.reset){ + widget.reset(); + } + }); + }, + + validate: function(){ + // summary: + // returns if the form is valid - same as isValid - but + // provides a few additional (ui-specific) features. + // 1 - it will highlight any sub-widgets that are not + // valid + // 2 - it will call focus() on the first invalid + // sub-widget + var didFocus = false; + return dojo.every(dojo.map(this.getDescendants(), function(widget){ + // Need to set this so that "required" widgets get their + // state set. + widget._hasBeenBlurred = true; + var valid = widget.disabled || !widget.validate || widget.validate(); + if(!valid && !didFocus){ + // Set focus of the first non-valid widget + dojo.window.scrollIntoView(widget.containerNode || widget.domNode); + widget.focus(); + didFocus = true; + } + return valid; + }), function(item){ return item; }); + }, + + setValues: function(val){ + dojo.deprecated(this.declaredClass+"::setValues() is deprecated. Use set('value', val) instead.", "", "2.0"); + return this.set('value', val); + }, + _setValueAttr: function(/*object*/obj){ + // summary: + // Fill in form values from according to an Object (in the format returned by attr('value')) + + // generate map from name --> [list of widgets with that name] + var map = { }; + dojo.forEach(this.getDescendants(), function(widget){ + if(!widget.name){ return; } + var entry = map[widget.name] || (map[widget.name] = [] ); + entry.push(widget); + }); + + for(var name in map){ + if(!map.hasOwnProperty(name)){ + continue; + } + var widgets = map[name], // array of widgets w/this name + values = dojo.getObject(name, false, obj); // list of values for those widgets + + if(values === undefined){ + continue; + } + if(!dojo.isArray(values)){ + values = [ values ]; + } + if(typeof widgets[0].checked == 'boolean'){ + // for checkbox/radio, values is a list of which widgets should be checked + dojo.forEach(widgets, function(w, i){ + w.set('value', dojo.indexOf(values, w.value) != -1); + }); + }else if(widgets[0].multiple){ + // it takes an array (e.g. multi-select) + widgets[0].set('value', values); + }else{ + // otherwise, values is a list of values to be assigned sequentially to each widget + dojo.forEach(widgets, function(w, i){ + w.set('value', values[i]); + }); + } + } + + /*** + * TODO: code for plain input boxes (this shouldn't run for inputs that are part of widgets) + + dojo.forEach(this.containerNode.elements, function(element){ + if(element.name == ''){return}; // like "continue" + var namePath = element.name.split("."); + var myObj=obj; + var name=namePath[namePath.length-1]; + for(var j=1,len2=namePath.length;j 1){ + if(typeof(myObj[nameA[0]]) == "undefined"){ + myObj[nameA[0]]=[ ]; + } // if + + nameIndex=parseInt(nameA[1]); + if(typeof(myObj[nameA[0]][nameIndex]) == "undefined"){ + myObj[nameA[0]][nameIndex] = { }; + } + myObj=myObj[nameA[0]][nameIndex]; + continue; + } // repeater support ends + + if(typeof(myObj[p]) == "undefined"){ + myObj=undefined; + break; + }; + myObj=myObj[p]; + } + + if(typeof(myObj) == "undefined"){ + return; // like "continue" + } + if(typeof(myObj[name]) == "undefined" && this.ignoreNullValues){ + return; // like "continue" + } + + // TODO: widget values (just call attr('value', ...) on the widget) + + // TODO: maybe should call dojo.getNodeProp() instead + switch(element.type){ + case "checkbox": + element.checked = (name in myObj) && + dojo.some(myObj[name], function(val){ return val == element.value; }); + break; + case "radio": + element.checked = (name in myObj) && myObj[name] == element.value; + break; + case "select-multiple": + element.selectedIndex=-1; + dojo.forEach(element.options, function(option){ + option.selected = dojo.some(myObj[name], function(val){ return option.value == val; }); + }); + break; + case "select-one": + element.selectedIndex="0"; + dojo.forEach(element.options, function(option){ + option.selected = option.value == myObj[name]; + }); + break; + case "hidden": + case "text": + case "textarea": + case "password": + element.value = myObj[name] || ""; + break; + } + }); + */ + }, + + getValues: function(){ + dojo.deprecated(this.declaredClass+"::getValues() is deprecated. Use get('value') instead.", "", "2.0"); + return this.get('value'); + }, + _getValueAttr: function(){ + // summary: + // Returns Object representing form values. + // description: + // Returns name/value hash for each form element. + // If there are multiple elements w/the same name, value is an array, + // unless they are radio buttons in which case value is a scalar since only + // one can be checked at a time. + // + // If the name is a dot separated list (like a.b.c.d), creates a nested structure. + // Only works on widget form elements. + // example: + // | { name: "John Smith", interests: ["sports", "movies"] } + + // get widget values + var obj = { }; + dojo.forEach(this.getDescendants(), function(widget){ + var name = widget.name; + if(!name || widget.disabled){ return; } + + // Single value widget (checkbox, radio, or plain type widget + var value = widget.get('value'); + + // Store widget's value(s) as a scalar, except for checkboxes which are automatically arrays + if(typeof widget.checked == 'boolean'){ + if(/Radio/.test(widget.declaredClass)){ + // radio button + if(value !== false){ + dojo.setObject(name, value, obj); + }else{ + // give radio widgets a default of null + value = dojo.getObject(name, false, obj); + if(value === undefined){ + dojo.setObject(name, null, obj); + } + } + }else{ + // checkbox/toggle button + var ary=dojo.getObject(name, false, obj); + if(!ary){ + ary=[]; + dojo.setObject(name, ary, obj); + } + if(value !== false){ + ary.push(value); + } + } + }else{ + var prev=dojo.getObject(name, false, obj); + if(typeof prev != "undefined"){ + if(dojo.isArray(prev)){ + prev.push(value); + }else{ + dojo.setObject(name, [prev, value], obj); + } + }else{ + // unique name + dojo.setObject(name, value, obj); + } + } + }); + + /*** + * code for plain input boxes (see also dojo.formToObject, can we use that instead of this code? + * but it doesn't understand [] notation, presumably) + var obj = { }; + dojo.forEach(this.containerNode.elements, function(elm){ + if(!elm.name) { + return; // like "continue" + } + var namePath = elm.name.split("."); + var myObj=obj; + var name=namePath[namePath.length-1]; + for(var j=1,len2=namePath.length;j 1){ + if(typeof(myObj[nameA[0]]) == "undefined"){ + myObj[nameA[0]]=[ ]; + } // if + nameIndex=parseInt(nameA[1]); + if(typeof(myObj[nameA[0]][nameIndex]) == "undefined"){ + myObj[nameA[0]][nameIndex] = { }; + } + } else if(typeof(myObj[nameA[0]]) == "undefined"){ + myObj[nameA[0]] = { } + } // if + + if(nameA.length == 1){ + myObj=myObj[nameA[0]]; + } else{ + myObj=myObj[nameA[0]][nameIndex]; + } // if + } // for + + if((elm.type != "select-multiple" && elm.type != "checkbox" && elm.type != "radio") || (elm.type == "radio" && elm.checked)){ + if(name == name.split("[")[0]){ + myObj[name]=elm.value; + } else{ + // can not set value when there is no name + } + } else if(elm.type == "checkbox" && elm.checked){ + if(typeof(myObj[name]) == 'undefined'){ + myObj[name]=[ ]; + } + myObj[name].push(elm.value); + } else if(elm.type == "select-multiple"){ + if(typeof(myObj[name]) == 'undefined'){ + myObj[name]=[ ]; + } + for(var jdx=0,len3=elm.options.length; jdx
", + + // Parameters on creation or updatable later + + // dialogId: String + // Id of the dialog.... DialogUnderlay's id is based on this id + dialogId: "", + + // class: String + // This class name is used on the DialogUnderlay node, in addition to dijitDialogUnderlay + "class": "", + + attributeMap: { id: "domNode" }, + + _setDialogIdAttr: function(id){ + dojo.attr(this.node, "id", id + "_underlay"); + }, + + _setClassAttr: function(clazz){ + this.node.className = "dijitDialogUnderlay " + clazz; + }, + + postCreate: function(){ + // summary: + // Append the underlay to the body + dojo.body().appendChild(this.domNode); + }, + + layout: function(){ + // summary: + // Sets the background to the size of the viewport + // + // description: + // Sets the background to the size of the viewport (rather than the size + // of the document) since we need to cover the whole browser window, even + // if the document is only a few lines long. + // tags: + // private + + var is = this.node.style, + os = this.domNode.style; + + // hide the background temporarily, so that the background itself isn't + // causing scrollbars to appear (might happen when user shrinks browser + // window and then we are called to resize) + os.display = "none"; + + // then resize and show + var viewport = dojo.window.getBox(); + os.top = viewport.t + "px"; + os.left = viewport.l + "px"; + is.width = viewport.w + "px"; + is.height = viewport.h + "px"; + os.display = "block"; + }, + + show: function(){ + // summary: + // Show the dialog underlay + this.domNode.style.display = "block"; + this.layout(); + this.bgIframe = new dijit.BackgroundIframe(this.domNode); + }, + + hide: function(){ + // summary: + // Hides the dialog underlay + this.bgIframe.destroy(); + this.domNode.style.display = "none"; + }, + + uninitialize: function(){ + if(this.bgIframe){ + this.bgIframe.destroy(); + } + this.inherited(arguments); + } + } +); + +} + +if(!dojo._hasResource["dojo.html"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.html"] = true; +dojo.provide("dojo.html"); + +// the parser might be needed.. + + +(function(){ // private scope, sort of a namespace + + // idCounter is incremented with each instantiation to allow asignment of a unique id for tracking, logging purposes + var idCounter = 0, + d = dojo; + + dojo.html._secureForInnerHtml = function(/*String*/ cont){ + // summary: + // removes !DOCTYPE and title elements from the html string. + // + // khtml is picky about dom faults, you can't attach a style or node as child of body + // must go into head, so we need to cut out those tags + // cont: + // An html string for insertion into the dom + // + return cont.replace(/(?:\s*<!DOCTYPE\s[^>]+>|<title[^>]*>[\s\S]*?<\/title>)/ig, ""); // String + }; + +/*==== + dojo.html._emptyNode = function(node){ + // summary: + // removes all child nodes from the given node + // node: DOMNode + // the parent element + }; +=====*/ + dojo.html._emptyNode = dojo.empty; + + dojo.html._setNodeContent = function(/* DomNode */ node, /* String|DomNode|NodeList */ cont){ + // summary: + // inserts the given content into the given node + // node: + // the parent element + // content: + // the content to be set on the parent element. + // This can be an html string, a node reference or a NodeList, dojo.NodeList, Array or other enumerable list of nodes + + // always empty + d.empty(node); + + if(cont) { + if(typeof cont == "string") { + cont = d._toDom(cont, node.ownerDocument); + } + if(!cont.nodeType && d.isArrayLike(cont)) { + // handle as enumerable, but it may shrink as we enumerate it + for(var startlen=cont.length, i=0; i<cont.length; i=startlen==cont.length ? i+1 : 0) { + d.place( cont[i], node, "last"); + } + } else { + // pass nodes, documentFragments and unknowns through to dojo.place + d.place(cont, node, "last"); + } + } + + // return DomNode + return node; + }; + + // we wrap up the content-setting operation in a object + dojo.declare("dojo.html._ContentSetter", null, + { + // node: DomNode|String + // An node which will be the parent element that we set content into + node: "", + + // content: String|DomNode|DomNode[] + // The content to be placed in the node. Can be an HTML string, a node reference, or a enumerable list of nodes + content: "", + + // id: String? + // Usually only used internally, and auto-generated with each instance + id: "", + + // cleanContent: Boolean + // Should the content be treated as a full html document, + // and the real content stripped of <html>, <body> wrapper before injection + cleanContent: false, + + // extractContent: Boolean + // Should the content be treated as a full html document, and the real content stripped of <html>, <body> wrapper before injection + extractContent: false, + + // parseContent: Boolean + // Should the node by passed to the parser after the new content is set + parseContent: false, + + // lifecyle methods + constructor: function(/* Object */params, /* String|DomNode */node){ + // summary: + // Provides a configurable, extensible object to wrap the setting on content on a node + // call the set() method to actually set the content.. + + // the original params are mixed directly into the instance "this" + dojo.mixin(this, params || {}); + + // give precedence to params.node vs. the node argument + // and ensure its a node, not an id string + node = this.node = dojo.byId( this.node || node ); + + if(!this.id){ + this.id = [ + "Setter", + (node) ? node.id || node.tagName : "", + idCounter++ + ].join("_"); + } + }, + set: function(/* String|DomNode|NodeList? */ cont, /* Object? */ params){ + // summary: + // front-end to the set-content sequence + // cont: + // An html string, node or enumerable list of nodes for insertion into the dom + // If not provided, the object's content property will be used + if(undefined !== cont){ + this.content = cont; + } + // in the re-use scenario, set needs to be able to mixin new configuration + if(params){ + this._mixin(params); + } + + this.onBegin(); + this.setContent(); + this.onEnd(); + + return this.node; + }, + setContent: function(){ + // summary: + // sets the content on the node + + var node = this.node; + if(!node) { + // can't proceed + throw new Error(this.declaredClass + ": setContent given no node"); + } + try{ + node = dojo.html._setNodeContent(node, this.content); + }catch(e){ + // check if a domfault occurs when we are appending this.errorMessage + // like for instance if domNode is a UL and we try append a DIV + + // FIXME: need to allow the user to provide a content error message string + var errMess = this.onContentError(e); + try{ + node.innerHTML = errMess; + }catch(e){ + console.error('Fatal ' + this.declaredClass + '.setContent could not change content due to '+e.message, e); + } + } + // always put back the node for the next method + this.node = node; // DomNode + }, + + empty: function() { + // summary + // cleanly empty out existing content + + // destroy any widgets from a previous run + // NOTE: if you dont want this you'll need to empty + // the parseResults array property yourself to avoid bad things happenning + if(this.parseResults && this.parseResults.length) { + dojo.forEach(this.parseResults, function(w) { + if(w.destroy){ + w.destroy(); + } + }); + delete this.parseResults; + } + // this is fast, but if you know its already empty or safe, you could + // override empty to skip this step + dojo.html._emptyNode(this.node); + }, + + onBegin: function(){ + // summary + // Called after instantiation, but before set(); + // It allows modification of any of the object properties + // - including the node and content provided - before the set operation actually takes place + // This default implementation checks for cleanContent and extractContent flags to + // optionally pre-process html string content + var cont = this.content; + + if(dojo.isString(cont)){ + if(this.cleanContent){ + cont = dojo.html._secureForInnerHtml(cont); + } + + if(this.extractContent){ + var match = cont.match(/<body[^>]*>\s*([\s\S]+)\s*<\/body>/im); + if(match){ cont = match[1]; } + } + } + + // clean out the node and any cruft associated with it - like widgets + this.empty(); + + this.content = cont; + return this.node; /* DomNode */ + }, + + onEnd: function(){ + // summary + // Called after set(), when the new content has been pushed into the node + // It provides an opportunity for post-processing before handing back the node to the caller + // This default implementation checks a parseContent flag to optionally run the dojo parser over the new content + if(this.parseContent){ + // populates this.parseResults if you need those.. + this._parse(); + } + return this.node; /* DomNode */ + }, + + tearDown: function(){ + // summary + // manually reset the Setter instance if its being re-used for example for another set() + // description + // tearDown() is not called automatically. + // In normal use, the Setter instance properties are simply allowed to fall out of scope + // but the tearDown method can be called to explicitly reset this instance. + delete this.parseResults; + delete this.node; + delete this.content; + }, + + onContentError: function(err){ + return "Error occured setting content: " + err; + }, + + _mixin: function(params){ + // mix properties/methods into the instance + // TODO: the intention with tearDown is to put the Setter's state + // back to that of the original constructor (vs. deleting/resetting everything regardless of ctor params) + // so we could do something here to move the original properties aside for later restoration + var empty = {}, key; + for(key in params){ + if(key in empty){ continue; } + // TODO: here's our opportunity to mask the properties we dont consider configurable/overridable + // .. but history shows we'll almost always guess wrong + this[key] = params[key]; + } + }, + _parse: function(){ + // summary: + // runs the dojo parser over the node contents, storing any results in this.parseResults + // Any errors resulting from parsing are passed to _onError for handling + + var rootNode = this.node; + try{ + // store the results (widgets, whatever) for potential retrieval + this.parseResults = dojo.parser.parse({ + rootNode: rootNode, + dir: this.dir, + lang: this.lang + }); + }catch(e){ + this._onError('Content', e, "Error parsing in _ContentSetter#"+this.id); + } + }, + + _onError: function(type, err, consoleText){ + // summary: + // shows user the string that is returned by on[type]Error + // overide/implement on[type]Error and return your own string to customize + var errText = this['on' + type + 'Error'].call(this, err); + if(consoleText){ + console.error(consoleText, err); + }else if(errText){ // a empty string won't change current content + dojo.html._setNodeContent(this.node, errText, true); + } + } + }); // end dojo.declare() + + dojo.html.set = function(/* DomNode */ node, /* String|DomNode|NodeList */ cont, /* Object? */ params){ + // summary: + // inserts (replaces) the given content into the given node. dojo.place(cont, node, "only") + // may be a better choice for simple HTML insertion. + // description: + // Unless you need to use the params capabilities of this method, you should use + // dojo.place(cont, node, "only"). dojo.place() has more robust support for injecting + // an HTML string into the DOM, but it only handles inserting an HTML string as DOM + // elements, or inserting a DOM node. dojo.place does not handle NodeList insertions + // or the other capabilities as defined by the params object for this method. + // node: + // the parent element that will receive the content + // cont: + // the content to be set on the parent element. + // This can be an html string, a node reference or a NodeList, dojo.NodeList, Array or other enumerable list of nodes + // params: + // Optional flags/properties to configure the content-setting. See dojo.html._ContentSetter + // example: + // A safe string/node/nodelist content replacement/injection with hooks for extension + // Example Usage: + // dojo.html.set(node, "some string"); + // dojo.html.set(node, contentNode, {options}); + // dojo.html.set(node, myNode.childNodes, {options}); + if(undefined == cont){ + console.warn("dojo.html.set: no cont argument provided, using empty string"); + cont = ""; + } + if(!params){ + // simple and fast + return dojo.html._setNodeContent(node, cont, true); + }else{ + // more options but slower + // note the arguments are reversed in order, to match the convention for instantiation via the parser + var op = new dojo.html._ContentSetter(dojo.mixin( + params, + { content: cont, node: node } + )); + return op.set(); + } + }; +})(); + +} + +if(!dojo._hasResource["dijit.layout.ContentPane"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.layout.ContentPane"] = true; +dojo.provide("dijit.layout.ContentPane"); + + + + // for dijit.layout.marginBox2contentBox() + + + + + + +dojo.declare( + "dijit.layout.ContentPane", dijit._Widget, +{ + // summary: + // A widget that acts as a container for mixed HTML and widgets, and includes an Ajax interface + // description: + // A widget that can be used as a stand alone widget + // or as a base class for other widgets. + // + // Handles replacement of document fragment using either external uri or javascript + // generated markup or DOM content, instantiating widgets within that content. + // Don't confuse it with an iframe, it only needs/wants document fragments. + // It's useful as a child of LayoutContainer, SplitContainer, or TabContainer. + // But note that those classes can contain any widget as a child. + // example: + // Some quick samples: + // To change the innerHTML use .set('content', '<b>new content</b>') + // + // Or you can send it a NodeList, .set('content', dojo.query('div [class=selected]', userSelection)) + // please note that the nodes in NodeList will copied, not moved + // + // To do a ajax update use .set('href', url) + + // href: String + // The href of the content that displays now. + // Set this at construction if you want to load data externally when the + // pane is shown. (Set preload=true to load it immediately.) + // Changing href after creation doesn't have any effect; Use set('href', ...); + href: "", + +/*===== + // content: String || DomNode || NodeList || dijit._Widget + // The innerHTML of the ContentPane. + // Note that the initialization parameter / argument to attr("content", ...) + // can be a String, DomNode, Nodelist, or _Widget. + content: "", +=====*/ + + // extractContent: Boolean + // Extract visible content from inside of <body> .... </body>. + // I.e., strip <html> and <head> (and it's contents) from the href + extractContent: false, + + // parseOnLoad: Boolean + // Parse content and create the widgets, if any. + parseOnLoad: true, + + // preventCache: Boolean + // Prevent caching of data from href's by appending a timestamp to the href. + preventCache: false, + + // preload: Boolean + // Force load of data on initialization even if pane is hidden. + preload: false, + + // refreshOnShow: Boolean + // Refresh (re-download) content when pane goes from hidden to shown + refreshOnShow: false, + + // loadingMessage: String + // Message that shows while downloading + loadingMessage: "<span class='dijitContentPaneLoading'>${loadingState}</span>", + + // errorMessage: String + // Message that shows if an error occurs + errorMessage: "<span class='dijitContentPaneError'>${errorState}</span>", + + // isLoaded: [readonly] Boolean + // True if the ContentPane has data in it, either specified + // during initialization (via href or inline content), or set + // via attr('content', ...) / attr('href', ...) + // + // False if it doesn't have any content, or if ContentPane is + // still in the process of downloading href. + isLoaded: false, + + baseClass: "dijitContentPane", + + // doLayout: Boolean + // - false - don't adjust size of children + // - true - if there is a single visible child widget, set it's size to + // however big the ContentPane is + doLayout: true, + + // ioArgs: Object + // Parameters to pass to xhrGet() request, for example: + // | <div dojoType="dijit.layout.ContentPane" href="./bar" ioArgs="{timeout: 500}"> + ioArgs: {}, + + // isContainer: [protected] Boolean + // Indicates that this widget acts as a "parent" to the descendant widgets. + // When the parent is started it will call startup() on the child widgets. + // See also `isLayoutContainer`. + isContainer: true, + + // isLayoutContainer: [protected] Boolean + // Indicates that this widget will call resize() on it's child widgets + // when they become visible. + isLayoutContainer: true, + + // onLoadDeferred: [readonly] dojo.Deferred + // This is the `dojo.Deferred` returned by attr('href', ...) and refresh(). + // Calling onLoadDeferred.addCallback() or addErrback() registers your + // callback to be called only once, when the prior attr('href', ...) call or + // the initial href parameter to the constructor finishes loading. + // + // This is different than an onLoad() handler which gets called any time any href is loaded. + onLoadDeferred: null, + + // Override _Widget's attributeMap because we don't want the title attribute (used to specify + // tab labels) to be copied to ContentPane.domNode... otherwise a tooltip shows up over the + // entire pane. + attributeMap: dojo.delegate(dijit._Widget.prototype.attributeMap, { + title: [] + }), + + postMixInProperties: function(){ + this.inherited(arguments); + var messages = dojo.i18n.getLocalization("dijit", "loading", this.lang); + this.loadingMessage = dojo.string.substitute(this.loadingMessage, messages); + this.errorMessage = dojo.string.substitute(this.errorMessage, messages); + + // Detect if we were initialized with data + if(!this.href && this.srcNodeRef && this.srcNodeRef.innerHTML){ + this.isLoaded = true; + } + }, + + buildRendering: function(){ + // Overrides Widget.buildRendering(). + // Since we have no template we need to set this.containerNode ourselves. + // For subclasses of ContentPane do have a template, does nothing. + this.inherited(arguments); + if(!this.containerNode){ + // make getDescendants() work + this.containerNode = this.domNode; + } + }, + + postCreate: function(){ + // remove the title attribute so it doesn't show up when hovering + // over a node + this.domNode.title = ""; + + if(!dojo.attr(this.domNode,"role")){ + dijit.setWaiRole(this.domNode, "group"); + } + + dojo.addClass(this.domNode, this.baseClass); + }, + + startup: function(){ + // summary: + // See `dijit.layout._LayoutWidget.startup` for description. + // Although ContentPane doesn't extend _LayoutWidget, it does implement + // the same API. + if(this._started){ return; } + + var parent = dijit._Contained.prototype.getParent.call(this); + this._childOfLayoutWidget = parent && parent.isLayoutContainer; + + // I need to call resize() on my child/children (when I become visible), unless + // I'm the child of a layout widget in which case my parent will call resize() on me and I'll do it then. + this._needLayout = !this._childOfLayoutWidget; + + if(this.isLoaded){ + dojo.forEach(this.getChildren(), function(child){ + child.startup(); + }); + } + + if(this._isShown() || this.preload){ + this._onShow(); + } + + this.inherited(arguments); + }, + + _checkIfSingleChild: function(){ + // summary: + // Test if we have exactly one visible widget as a child, + // and if so assume that we are a container for that widget, + // and should propogate startup() and resize() calls to it. + // Skips over things like data stores since they aren't visible. + + var childNodes = dojo.query("> *", this.containerNode).filter(function(node){ + return node.tagName !== "SCRIPT"; // or a regexp for hidden elements like script|area|map|etc.. + }), + childWidgetNodes = childNodes.filter(function(node){ + return dojo.hasAttr(node, "dojoType") || dojo.hasAttr(node, "widgetId"); + }), + candidateWidgets = dojo.filter(childWidgetNodes.map(dijit.byNode), function(widget){ + return widget && widget.domNode && widget.resize; + }); + + if( + // all child nodes are widgets + childNodes.length == childWidgetNodes.length && + + // all but one are invisible (like dojo.data) + candidateWidgets.length == 1 + ){ + this._singleChild = candidateWidgets[0]; + }else{ + delete this._singleChild; + } + + // So we can set overflow: hidden to avoid a safari bug w/scrollbars showing up (#9449) + dojo.toggleClass(this.containerNode, this.baseClass + "SingleChild", !!this._singleChild); + }, + + setHref: function(/*String|Uri*/ href){ + // summary: + // Deprecated. Use set('href', ...) instead. + dojo.deprecated("dijit.layout.ContentPane.setHref() is deprecated. Use set('href', ...) instead.", "", "2.0"); + return this.set("href", href); + }, + _setHrefAttr: function(/*String|Uri*/ href){ + // summary: + // Hook so attr("href", ...) works. + // description: + // Reset the (external defined) content of this pane and replace with new url + // Note: It delays the download until widget is shown if preload is false. + // href: + // url to the page you want to get, must be within the same domain as your mainpage + + // Cancel any in-flight requests (an attr('href') will cancel any in-flight attr('href', ...)) + this.cancel(); + + this.onLoadDeferred = new dojo.Deferred(dojo.hitch(this, "cancel")); + + this.href = href; + + // _setHrefAttr() is called during creation and by the user, after creation. + // only in the second case do we actually load the URL; otherwise it's done in startup() + if(this._created && (this.preload || this._isShown())){ + this._load(); + }else{ + // Set flag to indicate that href needs to be loaded the next time the + // ContentPane is made visible + this._hrefChanged = true; + } + + return this.onLoadDeferred; // dojo.Deferred + }, + + setContent: function(/*String|DomNode|Nodelist*/data){ + // summary: + // Deprecated. Use set('content', ...) instead. + dojo.deprecated("dijit.layout.ContentPane.setContent() is deprecated. Use set('content', ...) instead.", "", "2.0"); + this.set("content", data); + }, + _setContentAttr: function(/*String|DomNode|Nodelist*/data){ + // summary: + // Hook to make attr("content", ...) work. + // Replaces old content with data content, include style classes from old content + // data: + // the new Content may be String, DomNode or NodeList + // + // if data is a NodeList (or an array of nodes) nodes are copied + // so you can import nodes from another document implicitly + + // clear href so we can't run refresh and clear content + // refresh should only work if we downloaded the content + this.href = ""; + + // Cancel any in-flight requests (an attr('content') will cancel any in-flight attr('href', ...)) + this.cancel(); + + // Even though user is just setting content directly, still need to define an onLoadDeferred + // because the _onLoadHandler() handler is still getting called from setContent() + this.onLoadDeferred = new dojo.Deferred(dojo.hitch(this, "cancel")); + + this._setContent(data || ""); + + this._isDownloaded = false; // mark that content is from a attr('content') not an attr('href') + + return this.onLoadDeferred; // dojo.Deferred + }, + _getContentAttr: function(){ + // summary: + // Hook to make attr("content") work + return this.containerNode.innerHTML; + }, + + cancel: function(){ + // summary: + // Cancels an in-flight download of content + if(this._xhrDfd && (this._xhrDfd.fired == -1)){ + this._xhrDfd.cancel(); + } + delete this._xhrDfd; // garbage collect + + this.onLoadDeferred = null; + }, + + uninitialize: function(){ + if(this._beingDestroyed){ + this.cancel(); + } + this.inherited(arguments); + }, + + destroyRecursive: function(/*Boolean*/ preserveDom){ + // summary: + // Destroy the ContentPane and its contents + + // if we have multiple controllers destroying us, bail after the first + if(this._beingDestroyed){ + return; + } + this.inherited(arguments); + }, + + resize: function(changeSize, resultSize){ + // summary: + // See `dijit.layout._LayoutWidget.resize` for description. + // Although ContentPane doesn't extend _LayoutWidget, it does implement + // the same API. + + // For the TabContainer --> BorderContainer --> ContentPane case, _onShow() is + // never called, so resize() is our trigger to do the initial href download. + if(!this._wasShown){ + this._onShow(); + } + + this._resizeCalled = true; + + // Set margin box size, unless it wasn't specified, in which case use current size. + if(changeSize){ + dojo.marginBox(this.domNode, changeSize); + } + + // Compute content box size of containerNode in case we [later] need to size our single child. + var cn = this.containerNode; + if(cn === this.domNode){ + // If changeSize or resultSize was passed to this method and this.containerNode == + // this.domNode then we can compute the content-box size without querying the node, + // which is more reliable (similar to LayoutWidget.resize) (see for example #9449). + var mb = resultSize || {}; + dojo.mixin(mb, changeSize || {}); // changeSize overrides resultSize + if(!("h" in mb) || !("w" in mb)){ + mb = dojo.mixin(dojo.marginBox(cn), mb); // just use dojo.marginBox() to fill in missing values + } + this._contentBox = dijit.layout.marginBox2contentBox(cn, mb); + }else{ + this._contentBox = dojo.contentBox(cn); + } + + // Make my children layout, or size my single child widget + this._layoutChildren(); + }, + + _isShown: function(){ + // summary: + // Returns true if the content is currently shown. + // description: + // If I am a child of a layout widget then it actually returns true if I've ever been visible, + // not whether I'm currently visible, since that's much faster than tracing up the DOM/widget + // tree every call, and at least solves the performance problem on page load by deferring loading + // hidden ContentPanes until they are first shown + + if(this._childOfLayoutWidget){ + // If we are TitlePane, etc - we return that only *IF* we've been resized + if(this._resizeCalled && "open" in this){ + return this.open; + } + return this._resizeCalled; + }else if("open" in this){ + return this.open; // for TitlePane, etc. + }else{ + // TODO: with _childOfLayoutWidget check maybe this branch no longer necessary? + var node = this.domNode; + return (node.style.display != 'none') && (node.style.visibility != 'hidden') && !dojo.hasClass(node, "dijitHidden"); + } + }, + + _onShow: function(){ + // summary: + // Called when the ContentPane is made visible + // description: + // For a plain ContentPane, this is called on initialization, from startup(). + // If the ContentPane is a hidden pane of a TabContainer etc., then it's + // called whenever the pane is made visible. + // + // Does necessary processing, including href download and layout/resize of + // child widget(s) + + if(this.href){ + if(!this._xhrDfd && // if there's an href that isn't already being loaded + (!this.isLoaded || this._hrefChanged || this.refreshOnShow) + ){ + this.refresh(); + } + }else{ + // If we are the child of a layout widget then the layout widget will call resize() on + // us, and then we will size our child/children. Otherwise, we need to do it now. + if(!this._childOfLayoutWidget && this._needLayout){ + // If a layout has been scheduled for when we become visible, do it now + this._layoutChildren(); + } + } + + this.inherited(arguments); + + // Need to keep track of whether ContentPane has been shown (which is different than + // whether or not it's currently visible). + this._wasShown = true; + }, + + refresh: function(){ + // summary: + // [Re]download contents of href and display + // description: + // 1. cancels any currently in-flight requests + // 2. posts "loading..." message + // 3. sends XHR to download new data + + // Cancel possible prior in-flight request + this.cancel(); + + this.onLoadDeferred = new dojo.Deferred(dojo.hitch(this, "cancel")); + this._load(); + return this.onLoadDeferred; + }, + + _load: function(){ + // summary: + // Load/reload the href specified in this.href + + // display loading message + this._setContent(this.onDownloadStart(), true); + + var self = this; + var getArgs = { + preventCache: (this.preventCache || this.refreshOnShow), + url: this.href, + handleAs: "text" + }; + if(dojo.isObject(this.ioArgs)){ + dojo.mixin(getArgs, this.ioArgs); + } + + var hand = (this._xhrDfd = (this.ioMethod || dojo.xhrGet)(getArgs)); + + hand.addCallback(function(html){ + try{ + self._isDownloaded = true; + self._setContent(html, false); + self.onDownloadEnd(); + }catch(err){ + self._onError('Content', err); // onContentError + } + delete self._xhrDfd; + return html; + }); + + hand.addErrback(function(err){ + if(!hand.canceled){ + // show error message in the pane + self._onError('Download', err); // onDownloadError + } + delete self._xhrDfd; + return err; + }); + + // Remove flag saying that a load is needed + delete this._hrefChanged; + }, + + _onLoadHandler: function(data){ + // summary: + // This is called whenever new content is being loaded + this.isLoaded = true; + try{ + this.onLoadDeferred.callback(data); + this.onLoad(data); + }catch(e){ + console.error('Error '+this.widgetId+' running custom onLoad code: ' + e.message); + } + }, + + _onUnloadHandler: function(){ + // summary: + // This is called whenever the content is being unloaded + this.isLoaded = false; + try{ + this.onUnload(); + }catch(e){ + console.error('Error '+this.widgetId+' running custom onUnload code: ' + e.message); + } + }, + + destroyDescendants: function(){ + // summary: + // Destroy all the widgets inside the ContentPane and empty containerNode + + // Make sure we call onUnload (but only when the ContentPane has real content) + if(this.isLoaded){ + this._onUnloadHandler(); + } + + // Even if this.isLoaded == false there might still be a "Loading..." message + // to erase, so continue... + + // For historical reasons we need to delete all widgets under this.containerNode, + // even ones that the user has created manually. + var setter = this._contentSetter; + dojo.forEach(this.getChildren(), function(widget){ + if(widget.destroyRecursive){ + widget.destroyRecursive(); + } + }); + if(setter){ + // Most of the widgets in setter.parseResults have already been destroyed, but + // things like Menu that have been moved to <body> haven't yet + dojo.forEach(setter.parseResults, function(widget){ + if(widget.destroyRecursive && widget.domNode && widget.domNode.parentNode == dojo.body()){ + widget.destroyRecursive(); + } + }); + delete setter.parseResults; + } + + // And then clear away all the DOM nodes + dojo.html._emptyNode(this.containerNode); + + // Delete any state information we have about current contents + delete this._singleChild; + }, + + _setContent: function(cont, isFakeContent){ + // summary: + // Insert the content into the container node + + // first get rid of child widgets + this.destroyDescendants(); + + // dojo.html.set will take care of the rest of the details + // we provide an override for the error handling to ensure the widget gets the errors + // configure the setter instance with only the relevant widget instance properties + // NOTE: unless we hook into attr, or provide property setters for each property, + // we need to re-configure the ContentSetter with each use + var setter = this._contentSetter; + if(! (setter && setter instanceof dojo.html._ContentSetter)){ + setter = this._contentSetter = new dojo.html._ContentSetter({ + node: this.containerNode, + _onError: dojo.hitch(this, this._onError), + onContentError: dojo.hitch(this, function(e){ + // fires if a domfault occurs when we are appending this.errorMessage + // like for instance if domNode is a UL and we try append a DIV + var errMess = this.onContentError(e); + try{ + this.containerNode.innerHTML = errMess; + }catch(e){ + console.error('Fatal '+this.id+' could not change content due to '+e.message, e); + } + })/*, + _onError */ + }); + }; + + var setterParams = dojo.mixin({ + cleanContent: this.cleanContent, + extractContent: this.extractContent, + parseContent: this.parseOnLoad, + dir: this.dir, + lang: this.lang + }, this._contentSetterParams || {}); + + dojo.mixin(setter, setterParams); + + setter.set( (dojo.isObject(cont) && cont.domNode) ? cont.domNode : cont ); + + // setter params must be pulled afresh from the ContentPane each time + delete this._contentSetterParams; + + if(!isFakeContent){ + // Startup each top level child widget (and they will start their children, recursively) + dojo.forEach(this.getChildren(), function(child){ + // The parser has already called startup on all widgets *without* a getParent() method + if(!this.parseOnLoad || child.getParent){ + child.startup(); + } + }, this); + + // Call resize() on each of my child layout widgets, + // or resize() on my single child layout widget... + // either now (if I'm currently visible) + // or when I become visible + this._scheduleLayout(); + + this._onLoadHandler(cont); + } + }, + + _onError: function(type, err, consoleText){ + this.onLoadDeferred.errback(err); + + // shows user the string that is returned by on[type]Error + // overide on[type]Error and return your own string to customize + var errText = this['on' + type + 'Error'].call(this, err); + if(consoleText){ + console.error(consoleText, err); + }else if(errText){// a empty string won't change current content + this._setContent(errText, true); + } + }, + + _scheduleLayout: function(){ + // summary: + // Call resize() on each of my child layout widgets, either now + // (if I'm currently visible) or when I become visible + if(this._isShown()){ + this._layoutChildren(); + }else{ + this._needLayout = true; + } + }, + + _layoutChildren: function(){ + // summary: + // Since I am a Container widget, each of my children expects me to + // call resize() or layout() on them. + // description: + // Should be called on initialization and also whenever we get new content + // (from an href, or from attr('content', ...))... but deferred until + // the ContentPane is visible + + if(this.doLayout){ + this._checkIfSingleChild(); + } + + if(this._singleChild && this._singleChild.resize){ + var cb = this._contentBox || dojo.contentBox(this.containerNode); + + // note: if widget has padding this._contentBox will have l and t set, + // but don't pass them to resize() or it will doubly-offset the child + this._singleChild.resize({w: cb.w, h: cb.h}); + }else{ + // All my child widgets are independently sized (rather than matching my size), + // but I still need to call resize() on each child to make it layout. + dojo.forEach(this.getChildren(), function(widget){ + if(widget.resize){ + widget.resize(); + } + }); + } + delete this._needLayout; + }, + + // EVENT's, should be overide-able + onLoad: function(data){ + // summary: + // Event hook, is called after everything is loaded and widgetified + // tags: + // callback + }, + + onUnload: function(){ + // summary: + // Event hook, is called before old content is cleared + // tags: + // callback + }, + + onDownloadStart: function(){ + // summary: + // Called before download starts. + // description: + // The string returned by this function will be the html + // that tells the user we are loading something. + // Override with your own function if you want to change text. + // tags: + // extension + return this.loadingMessage; + }, + + onContentError: function(/*Error*/ error){ + // summary: + // Called on DOM faults, require faults etc. in content. + // + // In order to display an error message in the pane, return + // the error message from this method, as an HTML string. + // + // By default (if this method is not overriden), it returns + // nothing, so the error message is just printed to the console. + // tags: + // extension + }, + + onDownloadError: function(/*Error*/ error){ + // summary: + // Called when download error occurs. + // + // In order to display an error message in the pane, return + // the error message from this method, as an HTML string. + // + // Default behavior (if this method is not overriden) is to display + // the error message inside the pane. + // tags: + // extension + return this.errorMessage; + }, + + onDownloadEnd: function(){ + // summary: + // Called when download is finished. + // tags: + // callback + } +}); + +} + +if(!dojo._hasResource["dijit.TooltipDialog"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.TooltipDialog"] = true; +dojo.provide("dijit.TooltipDialog"); + + + + + + +dojo.declare( + "dijit.TooltipDialog", + [dijit.layout.ContentPane, dijit._Templated, dijit.form._FormMixin, dijit._DialogMixin], + { + // summary: + // Pops up a dialog that appears like a Tooltip + + // title: String + // Description of tooltip dialog (required for a11y) + title: "", + + // doLayout: [protected] Boolean + // Don't change this parameter from the default value. + // This ContentPane parameter doesn't make sense for TooltipDialog, since TooltipDialog + // is never a child of a layout container, nor can you specify the size of + // TooltipDialog in order to control the size of an inner widget. + doLayout: false, + + // autofocus: Boolean + // A Toggle to modify the default focus behavior of a Dialog, which + // is to focus on the first dialog element after opening the dialog. + // False will disable autofocusing. Default: true + autofocus: true, + + // baseClass: [protected] String + // The root className to use for the various states of this widget + baseClass: "dijitTooltipDialog", + + // _firstFocusItem: [private] [readonly] DomNode + // The pointer to the first focusable node in the dialog. + // Set by `dijit._DialogMixin._getFocusItems`. + _firstFocusItem: null, + + // _lastFocusItem: [private] [readonly] DomNode + // The pointer to which node has focus prior to our dialog. + // Set by `dijit._DialogMixin._getFocusItems`. + _lastFocusItem: null, + + templateString: dojo.cache("dijit", "templates/TooltipDialog.html", "<div waiRole=\"presentation\">\n\t<div class=\"dijitTooltipContainer\" waiRole=\"presentation\">\n\t\t<div class =\"dijitTooltipContents dijitTooltipFocusNode\" dojoAttachPoint=\"containerNode\" tabindex=\"-1\" waiRole=\"dialog\"></div>\n\t</div>\n\t<div class=\"dijitTooltipConnector\" waiRole=\"presentation\"></div>\n</div>\n"), + + postCreate: function(){ + this.inherited(arguments); + this.connect(this.containerNode, "onkeypress", "_onKey"); + this.containerNode.title = this.title; + }, + + orient: function(/*DomNode*/ node, /*String*/ aroundCorner, /*String*/ corner){ + // summary: + // Configure widget to be displayed in given position relative to the button. + // This is called from the dijit.popup code, and should not be called + // directly. + // tags: + // protected + var c = this._currentOrientClass; + if(c){ + dojo.removeClass(this.domNode, c); + } + c = "dijitTooltipAB"+(corner.charAt(1) == 'L'?"Left":"Right")+" dijitTooltip"+(corner.charAt(0) == 'T' ? "Below" : "Above"); + dojo.addClass(this.domNode, c); + this._currentOrientClass = c; + }, + + onOpen: function(/*Object*/ pos){ + // summary: + // Called when dialog is displayed. + // This is called from the dijit.popup code, and should not be called directly. + // tags: + // protected + + this.orient(this.domNode,pos.aroundCorner, pos.corner); + this._onShow(); // lazy load trigger + + if(this.autofocus){ + this._getFocusItems(this.containerNode); + dijit.focus(this._firstFocusItem); + } + }, + + onClose: function(){ + // summary: + // Called when dialog is hidden. + // This is called from the dijit.popup code, and should not be called directly. + // tags: + // protected + this.onHide(); + }, + + _onKey: function(/*Event*/ evt){ + // summary: + // Handler for keyboard events + // description: + // Keep keyboard focus in dialog; close dialog on escape key + // tags: + // private + + var node = evt.target; + var dk = dojo.keys; + if(evt.charOrCode === dk.TAB){ + this._getFocusItems(this.containerNode); + } + var singleFocusItem = (this._firstFocusItem == this._lastFocusItem); + if(evt.charOrCode == dk.ESCAPE){ + // Use setTimeout to avoid crash on IE, see #10396. + setTimeout(dojo.hitch(this, "onCancel"), 0); + dojo.stopEvent(evt); + }else if(node == this._firstFocusItem && evt.shiftKey && evt.charOrCode === dk.TAB){ + if(!singleFocusItem){ + dijit.focus(this._lastFocusItem); // send focus to last item in dialog + } + dojo.stopEvent(evt); + }else if(node == this._lastFocusItem && evt.charOrCode === dk.TAB && !evt.shiftKey){ + if(!singleFocusItem){ + dijit.focus(this._firstFocusItem); // send focus to first item in dialog + } + dojo.stopEvent(evt); + }else if(evt.charOrCode === dk.TAB){ + // we want the browser's default tab handling to move focus + // but we don't want the tab to propagate upwards + evt.stopPropagation(); + } + } + } + ); + +} + +if(!dojo._hasResource["dijit.Dialog"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.Dialog"] = true; +dojo.provide("dijit.Dialog"); + + + + + + + + + + + + + + + +/*===== +dijit._underlay = function(kwArgs){ + // summary: + // A shared instance of a `dijit.DialogUnderlay` + // + // description: + // A shared instance of a `dijit.DialogUnderlay` created and + // used by `dijit.Dialog`, though never created until some Dialog + // or subclass thereof is shown. +}; +=====*/ + +dojo.declare( + "dijit._DialogBase", + [dijit._Templated, dijit.form._FormMixin, dijit._DialogMixin, dijit._CssStateMixin], + { + // summary: + // A modal dialog Widget + // + // description: + // Pops up a modal dialog window, blocking access to the screen + // and also graying out the screen Dialog is extended from + // ContentPane so it supports all the same parameters (href, etc.) + // + // example: + // | <div dojoType="dijit.Dialog" href="test.html"></div> + // + // example: + // | var foo = new dijit.Dialog({ title: "test dialog", content: "test content" }; + // | dojo.body().appendChild(foo.domNode); + // | foo.startup(); + + templateString: dojo.cache("dijit", "templates/Dialog.html", "<div class=\"dijitDialog\" tabindex=\"-1\" waiRole=\"dialog\" waiState=\"labelledby-${id}_title\">\n\t<div dojoAttachPoint=\"titleBar\" class=\"dijitDialogTitleBar\">\n\t<span dojoAttachPoint=\"titleNode\" class=\"dijitDialogTitle\" id=\"${id}_title\"></span>\n\t<span dojoAttachPoint=\"closeButtonNode\" class=\"dijitDialogCloseIcon\" dojoAttachEvent=\"onclick: onCancel\" title=\"${buttonCancel}\">\n\t\t<span dojoAttachPoint=\"closeText\" class=\"closeText\" title=\"${buttonCancel}\">x</span>\n\t</span>\n\t</div>\n\t\t<div dojoAttachPoint=\"containerNode\" class=\"dijitDialogPaneContent\"></div>\n</div>\n"), + + baseClass: "dijitDialog", + + cssStateNodes: { + closeButtonNode: "dijitDialogCloseIcon" + }, + + attributeMap: dojo.delegate(dijit._Widget.prototype.attributeMap, { + title: [ + { node: "titleNode", type: "innerHTML" }, + { node: "titleBar", type: "attribute" } + ], + "aria-describedby":"" + }), + + // open: Boolean + // True if Dialog is currently displayed on screen. + open: false, + + // duration: Integer + // The time in milliseconds it takes the dialog to fade in and out + duration: dijit.defaultDuration, + + // refocus: Boolean + // A Toggle to modify the default focus behavior of a Dialog, which + // is to re-focus the element which had focus before being opened. + // False will disable refocusing. Default: true + refocus: true, + + // autofocus: Boolean + // A Toggle to modify the default focus behavior of a Dialog, which + // is to focus on the first dialog element after opening the dialog. + // False will disable autofocusing. Default: true + autofocus: true, + + // _firstFocusItem: [private] [readonly] DomNode + // The pointer to the first focusable node in the dialog. + // Set by `dijit._DialogMixin._getFocusItems`. + _firstFocusItem: null, + + // _lastFocusItem: [private] [readonly] DomNode + // The pointer to which node has focus prior to our dialog. + // Set by `dijit._DialogMixin._getFocusItems`. + _lastFocusItem: null, + + // doLayout: [protected] Boolean + // Don't change this parameter from the default value. + // This ContentPane parameter doesn't make sense for Dialog, since Dialog + // is never a child of a layout container, nor can you specify the size of + // Dialog in order to control the size of an inner widget. + doLayout: false, + + // draggable: Boolean + // Toggles the moveable aspect of the Dialog. If true, Dialog + // can be dragged by it's title. If false it will remain centered + // in the viewport. + draggable: true, + + //aria-describedby: String + // Allows the user to add an aria-describedby attribute onto the dialog. The value should + // be the id of the container element of text that describes the dialog purpose (usually + // the first text in the dialog). + // <div dojoType="dijit.Dialog" aria-describedby="intro" .....> + // <div id="intro">Introductory text</div> + // <div>rest of dialog contents</div> + // </div> + "aria-describedby":"", + + postMixInProperties: function(){ + var _nlsResources = dojo.i18n.getLocalization("dijit", "common"); + dojo.mixin(this, _nlsResources); + this.inherited(arguments); + }, + + postCreate: function(){ + dojo.style(this.domNode, { + display: "none", + position:"absolute" + }); + dojo.body().appendChild(this.domNode); + + this.inherited(arguments); + + this.connect(this, "onExecute", "hide"); + this.connect(this, "onCancel", "hide"); + this._modalconnects = []; + }, + + onLoad: function(){ + // summary: + // Called when data has been loaded from an href. + // Unlike most other callbacks, this function can be connected to (via `dojo.connect`) + // but should *not* be overriden. + // tags: + // callback + + // when href is specified we need to reposition the dialog after the data is loaded + // and find the focusable elements + this._position(); + if(this.autofocus){ + this._getFocusItems(this.domNode); + dijit.focus(this._firstFocusItem); + } + this.inherited(arguments); + }, + + _endDrag: function(e){ + // summary: + // Called after dragging the Dialog. Saves the position of the dialog in the viewport. + // tags: + // private + if(e && e.node && e.node === this.domNode){ + this._relativePosition = dojo.position(e.node); + } + }, + + _setup: function(){ + // summary: + // Stuff we need to do before showing the Dialog for the first + // time (but we defer it until right beforehand, for + // performance reasons). + // tags: + // private + + var node = this.domNode; + + if(this.titleBar && this.draggable){ + this._moveable = (dojo.isIE == 6) ? + new dojo.dnd.TimedMoveable(node, { handle: this.titleBar }) : // prevent overload, see #5285 + new dojo.dnd.Moveable(node, { handle: this.titleBar, timeout: 0 }); + dojo.subscribe("/dnd/move/stop",this,"_endDrag"); + }else{ + dojo.addClass(node,"dijitDialogFixed"); + } + + this.underlayAttrs = { + dialogId: this.id, + "class": dojo.map(this["class"].split(/\s/), function(s){ return s+"_underlay"; }).join(" ") + }; + + this._fadeIn = dojo.fadeIn({ + node: node, + duration: this.duration, + beforeBegin: dojo.hitch(this, function(){ + var underlay = dijit._underlay; + if(!underlay){ + underlay = dijit._underlay = new dijit.DialogUnderlay(this.underlayAttrs); + }else{ + underlay.set(this.underlayAttrs); + } + + var ds = dijit._dialogStack, + zIndex = 948 + ds.length*2; + if(ds.length == 1){ // first dialog + underlay.show(); + } + dojo.style(dijit._underlay.domNode, 'zIndex', zIndex); + dojo.style(this.domNode, 'zIndex', zIndex + 1); + }), + onEnd: dojo.hitch(this, function(){ + if(this.autofocus){ + // find focusable Items each time dialog is shown since if dialog contains a widget the + // first focusable items can change + this._getFocusItems(this.domNode); + dijit.focus(this._firstFocusItem); + } + }) + }); + + this._fadeOut = dojo.fadeOut({ + node: node, + duration: this.duration, + onEnd: dojo.hitch(this, function(){ + node.style.display = "none"; + + // Restore the previous dialog in the stack, or if this is the only dialog + // then restore to original page + var ds = dijit._dialogStack; + if(ds.length == 0){ + dijit._underlay.hide(); + }else{ + dojo.style(dijit._underlay.domNode, 'zIndex', 948 + ds.length*2); + dijit._underlay.set(ds[ds.length-1].underlayAttrs); + } + + // Restore focus to wherever it was before this dialog was displayed + if(this.refocus){ + var focus = this._savedFocus; + + // If we are returning control to a previous dialog but for some reason + // that dialog didn't have a focused field, set focus to first focusable item. + // This situation could happen if two dialogs appeared at nearly the same time, + // since a dialog doesn't set it's focus until the fade-in is finished. + if(ds.length > 0){ + var pd = ds[ds.length-1]; + if(!dojo.isDescendant(focus.node, pd.domNode)){ + pd._getFocusItems(pd.domNode); + focus = pd._firstFocusItem; + } + } + + dijit.focus(focus); + } + }) + }); + }, + + uninitialize: function(){ + var wasPlaying = false; + if(this._fadeIn && this._fadeIn.status() == "playing"){ + wasPlaying = true; + this._fadeIn.stop(); + } + if(this._fadeOut && this._fadeOut.status() == "playing"){ + wasPlaying = true; + this._fadeOut.stop(); + } + + // Hide the underlay, unless the underlay widget has already been destroyed + // because we are being called during page unload (when all widgets are destroyed) + if((this.open || wasPlaying) && !dijit._underlay._destroyed){ + dijit._underlay.hide(); + } + + if(this._moveable){ + this._moveable.destroy(); + } + this.inherited(arguments); + }, + + _size: function(){ + // summary: + // If necessary, shrink dialog contents so dialog fits in viewport + // tags: + // private + + this._checkIfSingleChild(); + + // If we resized the dialog contents earlier, reset them back to original size, so + // that if the user later increases the viewport size, the dialog can display w/out a scrollbar. + // Need to do this before the dojo.marginBox(this.domNode) call below. + if(this._singleChild){ + if(this._singleChildOriginalStyle){ + this._singleChild.domNode.style.cssText = this._singleChildOriginalStyle; + } + delete this._singleChildOriginalStyle; + }else{ + dojo.style(this.containerNode, { + width:"auto", + height:"auto" + }); + } + + var mb = dojo.marginBox(this.domNode); + var viewport = dojo.window.getBox(); + if(mb.w >= viewport.w || mb.h >= viewport.h){ + // Reduce size of dialog contents so that dialog fits in viewport + + var w = Math.min(mb.w, Math.floor(viewport.w * 0.75)), + h = Math.min(mb.h, Math.floor(viewport.h * 0.75)); + + if(this._singleChild && this._singleChild.resize){ + this._singleChildOriginalStyle = this._singleChild.domNode.style.cssText; + this._singleChild.resize({w: w, h: h}); + }else{ + dojo.style(this.containerNode, { + width: w + "px", + height: h + "px", + overflow: "auto", + position: "relative" // workaround IE bug moving scrollbar or dragging dialog + }); + } + }else{ + if(this._singleChild && this._singleChild.resize){ + this._singleChild.resize(); + } + } + }, + + _position: function(){ + // summary: + // Position modal dialog in the viewport. If no relative offset + // in the viewport has been determined (by dragging, for instance), + // center the node. Otherwise, use the Dialog's stored relative offset, + // and position the node to top: left: values based on the viewport. + // tags: + // private + if(!dojo.hasClass(dojo.body(),"dojoMove")){ + var node = this.domNode, + viewport = dojo.window.getBox(), + p = this._relativePosition, + bb = p ? null : dojo._getBorderBox(node), + l = Math.floor(viewport.l + (p ? p.x : (viewport.w - bb.w) / 2)), + t = Math.floor(viewport.t + (p ? p.y : (viewport.h - bb.h) / 2)) + ; + dojo.style(node,{ + left: l + "px", + top: t + "px" + }); + } + }, + + _onKey: function(/*Event*/ evt){ + // summary: + // Handles the keyboard events for accessibility reasons + // tags: + // private + + var ds = dijit._dialogStack; + if(ds[ds.length-1] != this){ + // console.debug(this.id + ': skipping because', this, 'is not the active dialog'); + return; + } + + if(evt.charOrCode){ + var dk = dojo.keys; + var node = evt.target; + if(evt.charOrCode === dk.TAB){ + this._getFocusItems(this.domNode); + } + var singleFocusItem = (this._firstFocusItem == this._lastFocusItem); + // see if we are shift-tabbing from first focusable item on dialog + if(node == this._firstFocusItem && evt.shiftKey && evt.charOrCode === dk.TAB){ + if(!singleFocusItem){ + dijit.focus(this._lastFocusItem); // send focus to last item in dialog + } + dojo.stopEvent(evt); + }else if(node == this._lastFocusItem && evt.charOrCode === dk.TAB && !evt.shiftKey){ + if(!singleFocusItem){ + dijit.focus(this._firstFocusItem); // send focus to first item in dialog + } + dojo.stopEvent(evt); + }else{ + // see if the key is for the dialog + while(node){ + if(node == this.domNode || dojo.hasClass(node, "dijitPopup")){ + if(evt.charOrCode == dk.ESCAPE){ + this.onCancel(); + }else{ + return; // just let it go + } + } + node = node.parentNode; + } + // this key is for the disabled document window + if(evt.charOrCode !== dk.TAB){ // allow tabbing into the dialog for a11y + dojo.stopEvent(evt); + // opera won't tab to a div + }else if(!dojo.isOpera){ + try{ + this._firstFocusItem.focus(); + }catch(e){ /*squelch*/ } + } + } + } + }, + + show: function(){ + // summary: + // Display the dialog + if(this.open){ return; } + + // first time we show the dialog, there's some initialization stuff to do + if(!this._alreadyInitialized){ + this._setup(); + this._alreadyInitialized=true; + } + + if(this._fadeOut.status() == "playing"){ + this._fadeOut.stop(); + } + + this._modalconnects.push(dojo.connect(window, "onscroll", this, "layout")); + this._modalconnects.push(dojo.connect(window, "onresize", this, function(){ + // IE gives spurious resize events and can actually get stuck + // in an infinite loop if we don't ignore them + var viewport = dojo.window.getBox(); + if(!this._oldViewport || + viewport.h != this._oldViewport.h || + viewport.w != this._oldViewport.w){ + this.layout(); + this._oldViewport = viewport; + } + })); + this._modalconnects.push(dojo.connect(dojo.doc.documentElement, "onkeypress", this, "_onKey")); + + dojo.style(this.domNode, { + opacity:0, + display:"" + }); + + this.open = true; + this._onShow(); // lazy load trigger + + this._size(); + this._position(); + dijit._dialogStack.push(this); + this._fadeIn.play(); + + this._savedFocus = dijit.getFocus(this); + }, + + hide: function(){ + // summary: + // Hide the dialog + + // if we haven't been initialized yet then we aren't showing and we can just return + // or if we aren't the active dialog, don't allow us to close yet + var ds = dijit._dialogStack; + if(!this._alreadyInitialized || this != ds[ds.length-1]){ + return; + } + + if(this._fadeIn.status() == "playing"){ + this._fadeIn.stop(); + } + + // throw away current active dialog from stack -- making the previous dialog or the node on the original page active + ds.pop(); + + this._fadeOut.play(); + + if(this._scrollConnected){ + this._scrollConnected = false; + } + dojo.forEach(this._modalconnects, dojo.disconnect); + this._modalconnects = []; + + if(this._relativePosition){ + delete this._relativePosition; + } + this.open = false; + + this.onHide(); + }, + + layout: function(){ + // summary: + // Position the Dialog and the underlay + // tags: + // private + if(this.domNode.style.display != "none"){ + if(dijit._underlay){ // avoid race condition during show() + dijit._underlay.layout(); + } + this._position(); + } + }, + + destroy: function(){ + dojo.forEach(this._modalconnects, dojo.disconnect); + if(this.refocus && this.open){ + setTimeout(dojo.hitch(dijit,"focus",this._savedFocus), 25); + } + this.inherited(arguments); + } + } +); + +dojo.declare( + "dijit.Dialog", + [dijit.layout.ContentPane, dijit._DialogBase], + {} +); + +// Stack of currenctly displayed dialogs, layered on top of each other +dijit._dialogStack = []; + +// For back-compat. TODO: remove in 2.0 + + +} + +if(!dojo._hasResource["dijit._HasDropDown"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit._HasDropDown"] = true; +dojo.provide("dijit._HasDropDown"); + + + + +dojo.declare("dijit._HasDropDown", + null, + { + // summary: + // Mixin for widgets that need drop down ability. + + // _buttonNode: [protected] DomNode + // The button/icon/node to click to display the drop down. + // Can be set via a dojoAttachPoint assignment. + // If missing, then either focusNode or domNode (if focusNode is also missing) will be used. + _buttonNode: null, + + // _arrowWrapperNode: [protected] DomNode + // Will set CSS class dijitUpArrow, dijitDownArrow, dijitRightArrow etc. on this node depending + // on where the drop down is set to be positioned. + // Can be set via a dojoAttachPoint assignment. + // If missing, then _buttonNode will be used. + _arrowWrapperNode: null, + + // _popupStateNode: [protected] DomNode + // The node to set the popupActive class on. + // Can be set via a dojoAttachPoint assignment. + // If missing, then focusNode or _buttonNode (if focusNode is missing) will be used. + _popupStateNode: null, + + // _aroundNode: [protected] DomNode + // The node to display the popup around. + // Can be set via a dojoAttachPoint assignment. + // If missing, then domNode will be used. + _aroundNode: null, + + // dropDown: [protected] Widget + // The widget to display as a popup. This widget *must* be + // defined before the startup function is called. + dropDown: null, + + // autoWidth: [protected] Boolean + // Set to true to make the drop down at least as wide as this + // widget. Set to false if the drop down should just be its + // default width + autoWidth: true, + + // forceWidth: [protected] Boolean + // Set to true to make the drop down exactly as wide as this + // widget. Overrides autoWidth. + forceWidth: false, + + // maxHeight: [protected] Integer + // The max height for our dropdown. Set to 0 for no max height. + // any dropdown taller than this will have scrollbars + maxHeight: 0, + + // dropDownPosition: [const] String[] + // This variable controls the position of the drop down. + // It's an array of strings with the following values: + // + // * before: places drop down to the left of the target node/widget, or to the right in + // the case of RTL scripts like Hebrew and Arabic + // * after: places drop down to the right of the target node/widget, or to the left in + // the case of RTL scripts like Hebrew and Arabic + // * above: drop down goes above target node + // * below: drop down goes below target node + // + // The list is positions is tried, in order, until a position is found where the drop down fits + // within the viewport. + // + dropDownPosition: ["below","above"], + + // _stopClickEvents: Boolean + // When set to false, the click events will not be stopped, in + // case you want to use them in your subwidget + _stopClickEvents: true, + + _onDropDownMouseDown: function(/*Event*/ e){ + // summary: + // Callback when the user mousedown's on the arrow icon + + if(this.disabled || this.readOnly){ return; } + + this._docHandler = this.connect(dojo.doc, "onmouseup", "_onDropDownMouseUp"); + + this.toggleDropDown(); + }, + + _onDropDownMouseUp: function(/*Event?*/ e){ + // summary: + // Callback when the user lifts their mouse after mouse down on the arrow icon. + // If the drop is a simple menu and the mouse is over the menu, we execute it, otherwise, we focus our + // dropDown node. If the event is missing, then we are not + // a mouseup event. + // + // This is useful for the common mouse movement pattern + // with native browser <select> nodes: + // 1. mouse down on the select node (probably on the arrow) + // 2. move mouse to a menu item while holding down the mouse button + // 3. mouse up. this selects the menu item as though the user had clicked it. + if(e && this._docHandler){ + this.disconnect(this._docHandler); + } + var dropDown = this.dropDown, overMenu = false; + + if(e && this._opened){ + // This code deals with the corner-case when the drop down covers the original widget, + // because it's so large. In that case mouse-up shouldn't select a value from the menu. + // Find out if our target is somewhere in our dropdown widget, + // but not over our _buttonNode (the clickable node) + var c = dojo.position(this._buttonNode, true); + if(!(e.pageX >= c.x && e.pageX <= c.x + c.w) || + !(e.pageY >= c.y && e.pageY <= c.y + c.h)){ + var t = e.target; + while(t && !overMenu){ + if(dojo.hasClass(t, "dijitPopup")){ + overMenu = true; + }else{ + t = t.parentNode; + } + } + if(overMenu){ + t = e.target; + if(dropDown.onItemClick){ + var menuItem; + while(t && !(menuItem = dijit.byNode(t))){ + t = t.parentNode; + } + if(menuItem && menuItem.onClick && menuItem.getParent){ + menuItem.getParent().onItemClick(menuItem, e); + } + } + return; + } + } + } + if(this._opened && dropDown.focus){ + // Focus the dropdown widget - do it on a delay so that we + // don't steal our own focus. + window.setTimeout(dojo.hitch(dropDown, "focus"), 1); + } + }, + + _onDropDownClick: function(/*Event*/ e){ + // the drop down was already opened on mousedown/keydown; just need to call stopEvent() + if(this._stopClickEvents){ + dojo.stopEvent(e); + } + }, + + _setupDropdown: function(){ + // summary: + // set up nodes and connect our mouse and keypress events + this._buttonNode = this._buttonNode || this.focusNode || this.domNode; + this._popupStateNode = this._popupStateNode || this.focusNode || this._buttonNode; + this._aroundNode = this._aroundNode || this.domNode; + this.connect(this._buttonNode, "onmousedown", "_onDropDownMouseDown"); + this.connect(this._buttonNode, "onclick", "_onDropDownClick"); + this.connect(this._buttonNode, "onkeydown", "_onDropDownKeydown"); + this.connect(this._buttonNode, "onkeyup", "_onKey"); + + // If we have a _setStateClass function (which happens when + // we are a form widget), then we need to connect our open/close + // functions to it + if(this._setStateClass){ + this.connect(this, "openDropDown", "_setStateClass"); + this.connect(this, "closeDropDown", "_setStateClass"); + } + + // Add a class to the "dijitDownArrowButton" type class to _buttonNode so theme can set direction of arrow + // based on where drop down will normally appear + var defaultPos = { + "after" : this.isLeftToRight() ? "Right" : "Left", + "before" : this.isLeftToRight() ? "Left" : "Right", + "above" : "Up", + "below" : "Down", + "left" : "Left", + "right" : "Right" + }[this.dropDownPosition[0]] || this.dropDownPosition[0] || "Down"; + dojo.addClass(this._arrowWrapperNode || this._buttonNode, "dijit" + defaultPos + "ArrowButton"); + }, + + postCreate: function(){ + this._setupDropdown(); + this.inherited(arguments); + }, + + destroyDescendants: function(){ + if(this.dropDown){ + // Destroy the drop down, unless it's already been destroyed. This can happen because + // the drop down is a direct child of <body> even though it's logically my child. + if(!this.dropDown._destroyed){ + this.dropDown.destroyRecursive(); + } + delete this.dropDown; + } + this.inherited(arguments); + }, + + _onDropDownKeydown: function(/*Event*/ e){ + if(e.keyCode == dojo.keys.DOWN_ARROW || e.keyCode == dojo.keys.ENTER || e.keyCode == dojo.keys.SPACE){ + e.preventDefault(); // stop IE screen jump + } + }, + + _onKey: function(/*Event*/ e){ + // summary: + // Callback when the user presses a key while focused on the button node + + if(this.disabled || this.readOnly){ return; } + var d = this.dropDown; + if(d && this._opened && d.handleKey){ + if(d.handleKey(e) === false){ return; } + } + if(d && this._opened && e.keyCode == dojo.keys.ESCAPE){ + this.toggleDropDown(); + }else if(d && !this._opened && + (e.keyCode == dojo.keys.DOWN_ARROW || e.keyCode == dojo.keys.ENTER || e.keyCode == dojo.keys.SPACE)){ + this.toggleDropDown(); + if(d.focus){ + setTimeout(dojo.hitch(d, "focus"), 1); + } + } + }, + + _onBlur: function(){ + // summary: + // Called magically when focus has shifted away from this widget and it's dropdown + + this.closeDropDown(); + // don't focus on button. the user has explicitly focused on something else. + this.inherited(arguments); + }, + + isLoaded: function(){ + // summary: + // Returns whether or not the dropdown is loaded. This can + // be overridden in order to force a call to loadDropDown(). + // tags: + // protected + + return true; + }, + + loadDropDown: function(/* Function */ loadCallback){ + // summary: + // Loads the data for the dropdown, and at some point, calls + // the given callback + // tags: + // protected + + loadCallback(); + }, + + toggleDropDown: function(){ + // summary: + // Toggle the drop-down widget; if it is up, close it, if not, open it + // tags: + // protected + + if(this.disabled || this.readOnly){ return; } + this.focus(); + var dropDown = this.dropDown; + if(!dropDown){ return; } + if(!this._opened){ + // If we aren't loaded, load it first so there isn't a flicker + if(!this.isLoaded()){ + this.loadDropDown(dojo.hitch(this, "openDropDown")); + return; + }else{ + this.openDropDown(); + } + }else{ + this.closeDropDown(); + } + }, + + openDropDown: function(){ + // summary: + // Opens the dropdown for this widget - it returns the + // return value of dijit.popup.open + // tags: + // protected + + var dropDown = this.dropDown; + var ddNode = dropDown.domNode; + var self = this; + + // Prepare our popup's height and honor maxHeight if it exists. + + // TODO: isn't maxHeight dependent on the return value from dijit.popup.open(), + // ie, dependent on how much space is available (BK) + + if(!this._preparedNode){ + dijit.popup.moveOffScreen(ddNode); + this._preparedNode = true; + // Check if we have explicitly set width and height on the dropdown widget dom node + if(ddNode.style.width){ + this._explicitDDWidth = true; + } + if(ddNode.style.height){ + this._explicitDDHeight = true; + } + } + + // Code for resizing dropdown (height limitation, or increasing width to match my width) + if(this.maxHeight || this.forceWidth || this.autoWidth){ + var myStyle = { + display: "", + visibility: "hidden" + }; + if(!this._explicitDDWidth){ + myStyle.width = ""; + } + if(!this._explicitDDHeight){ + myStyle.height = ""; + } + dojo.style(ddNode, myStyle); + + // Get size of drop down, and determine if vertical scroll bar needed + var mb = dojo.marginBox(ddNode); + var overHeight = (this.maxHeight && mb.h > this.maxHeight); + dojo.style(ddNode, { + overflowX: "hidden", + overflowY: overHeight ? "auto" : "hidden" + }); + if(overHeight){ + mb.h = this.maxHeight; + if("w" in mb){ + mb.w += 16; // room for vertical scrollbar + } + }else{ + delete mb.h; + } + delete mb.t; + delete mb.l; + + // Adjust dropdown width to match or be larger than my width + if(this.forceWidth){ + mb.w = this.domNode.offsetWidth; + }else if(this.autoWidth){ + mb.w = Math.max(mb.w, this.domNode.offsetWidth); + }else{ + delete mb.w; + } + + // And finally, resize the dropdown to calculated height and width + if(dojo.isFunction(dropDown.resize)){ + dropDown.resize(mb); + }else{ + dojo.marginBox(ddNode, mb); + } + } + + var retVal = dijit.popup.open({ + parent: this, + popup: dropDown, + around: this._aroundNode, + orient: dijit.getPopupAroundAlignment((this.dropDownPosition && this.dropDownPosition.length) ? this.dropDownPosition : ["below"],this.isLeftToRight()), + onExecute: function(){ + self.closeDropDown(true); + }, + onCancel: function(){ + self.closeDropDown(true); + }, + onClose: function(){ + dojo.attr(self._popupStateNode, "popupActive", false); + dojo.removeClass(self._popupStateNode, "dijitHasDropDownOpen"); + self._opened = false; + self.state = ""; + } + }); + dojo.attr(this._popupStateNode, "popupActive", "true"); + dojo.addClass(self._popupStateNode, "dijitHasDropDownOpen"); + this._opened=true; + this.state="Opened"; + // TODO: set this.checked and call setStateClass(), to affect button look while drop down is shown + return retVal; + }, + + closeDropDown: function(/*Boolean*/ focus){ + // summary: + // Closes the drop down on this widget + // tags: + // protected + + if(this._opened){ + if(focus){ this.focus(); } + dijit.popup.close(this.dropDown); + this._opened = false; + this.state = ""; + } + } + + } +); + +} + +if(!dojo._hasResource["dijit.form.Button"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.form.Button"] = true; +dojo.provide("dijit.form.Button"); + + + + + +dojo.declare("dijit.form.Button", + dijit.form._FormWidget, + { + // summary: + // Basically the same thing as a normal HTML button, but with special styling. + // description: + // Buttons can display a label, an icon, or both. + // A label should always be specified (through innerHTML) or the label + // attribute. It can be hidden via showLabel=false. + // example: + // | <button dojoType="dijit.form.Button" onClick="...">Hello world</button> + // + // example: + // | var button1 = new dijit.form.Button({label: "hello world", onClick: foo}); + // | dojo.body().appendChild(button1.domNode); + + // label: HTML String + // Text to display in button. + // If the label is hidden (showLabel=false) then and no title has + // been specified, then label is also set as title attribute of icon. + label: "", + + // showLabel: Boolean + // Set this to true to hide the label text and display only the icon. + // (If showLabel=false then iconClass must be specified.) + // Especially useful for toolbars. + // If showLabel=true, the label will become the title (a.k.a. tooltip/hint) of the icon. + // + // The exception case is for computers in high-contrast mode, where the label + // will still be displayed, since the icon doesn't appear. + showLabel: true, + + // iconClass: String + // Class to apply to DOMNode in button to make it display an icon + iconClass: "", + + // type: String + // Defines the type of button. "button", "submit", or "reset". + type: "button", + + baseClass: "dijitButton", + + templateString: dojo.cache("dijit.form", "templates/Button.html", "<span class=\"dijit dijitReset dijitInline\"\n\t><span class=\"dijitReset dijitInline dijitButtonNode\"\n\t\tdojoAttachEvent=\"ondijitclick:_onButtonClick\"\n\t\t><span class=\"dijitReset dijitStretch dijitButtonContents\"\n\t\t\tdojoAttachPoint=\"titleNode,focusNode\"\n\t\t\twaiRole=\"button\" waiState=\"labelledby-${id}_label\"\n\t\t\t><span class=\"dijitReset dijitInline dijitIcon\" dojoAttachPoint=\"iconNode\"></span\n\t\t\t><span class=\"dijitReset dijitToggleButtonIconChar\">●</span\n\t\t\t><span class=\"dijitReset dijitInline dijitButtonText\"\n\t\t\t\tid=\"${id}_label\"\n\t\t\t\tdojoAttachPoint=\"containerNode\"\n\t\t\t></span\n\t\t></span\n\t></span\n\t><input ${!nameAttrSetting} type=\"${type}\" value=\"${value}\" class=\"dijitOffScreen\"\n\t\tdojoAttachPoint=\"valueNode\"\n/></span>\n"), + + attributeMap: dojo.delegate(dijit.form._FormWidget.prototype.attributeMap, { + value: "valueNode", + iconClass: { node: "iconNode", type: "class" } + }), + + + _onClick: function(/*Event*/ e){ + // summary: + // Internal function to handle click actions + if(this.disabled){ + return false; + } + this._clicked(); // widget click actions + return this.onClick(e); // user click actions + }, + + _onButtonClick: function(/*Event*/ e){ + // summary: + // Handler when the user activates the button portion. + if(this._onClick(e) === false){ // returning nothing is same as true + e.preventDefault(); // needed for checkbox + }else if(this.type == "submit" && !(this.valueNode||this.focusNode).form){ // see if a nonform widget needs to be signalled + for(var node=this.domNode; node.parentNode/*#5935*/; node=node.parentNode){ + var widget=dijit.byNode(node); + if(widget && typeof widget._onSubmit == "function"){ + widget._onSubmit(e); + break; + } + } + }else if(this.valueNode){ + this.valueNode.click(); + e.preventDefault(); // cancel BUTTON click and continue with hidden INPUT click + } + }, + + _fillContent: function(/*DomNode*/ source){ + // Overrides _Templated._fillContent(). + // If button label is specified as srcNodeRef.innerHTML rather than + // this.params.label, handle it here. + if(source && (!this.params || !("label" in this.params))){ + this.set('label', source.innerHTML); + } + }, + + postCreate: function(){ + dojo.setSelectable(this.focusNode, false); + this.inherited(arguments); + }, + + _setShowLabelAttr: function(val){ + if(this.containerNode){ + dojo.toggleClass(this.containerNode, "dijitDisplayNone", !val); + } + this.showLabel = val; + }, + + onClick: function(/*Event*/ e){ + // summary: + // Callback for when button is clicked. + // If type="submit", return true to perform submit, or false to cancel it. + // type: + // callback + return true; // Boolean + }, + + _clicked: function(/*Event*/ e){ + // summary: + // Internal overridable function for when the button is clicked + }, + + setLabel: function(/*String*/ content){ + // summary: + // Deprecated. Use set('label', ...) instead. + dojo.deprecated("dijit.form.Button.setLabel() is deprecated. Use set('label', ...) instead.", "", "2.0"); + this.set("label", content); + }, + + _setLabelAttr: function(/*String*/ content){ + // summary: + // Hook for attr('label', ...) to work. + // description: + // Set the label (text) of the button; takes an HTML string. + this.containerNode.innerHTML = this.label = content; + if(this.showLabel == false && !this.params.title){ + this.titleNode.title = dojo.trim(this.containerNode.innerText || this.containerNode.textContent || ''); + } + } +}); + + +dojo.declare("dijit.form.DropDownButton", [dijit.form.Button, dijit._Container, dijit._HasDropDown], { + // summary: + // A button with a drop down + // + // example: + // | <button dojoType="dijit.form.DropDownButton" label="Hello world"> + // | <div dojotype="dijit.Menu">...</div> + // | </button> + // + // example: + // | var button1 = new dijit.form.DropDownButton({ label: "hi", dropDown: new dijit.Menu(...) }); + // | dojo.body().appendChild(button1); + // + + baseClass : "dijitDropDownButton", + + templateString: dojo.cache("dijit.form", "templates/DropDownButton.html", "<span class=\"dijit dijitReset dijitInline\"\n\t><span class='dijitReset dijitInline dijitButtonNode'\n\t\tdojoAttachEvent=\"ondijitclick:_onButtonClick\" dojoAttachPoint=\"_buttonNode\"\n\t\t><span class=\"dijitReset dijitStretch dijitButtonContents\"\n\t\t\tdojoAttachPoint=\"focusNode,titleNode,_arrowWrapperNode\"\n\t\t\twaiRole=\"button\" waiState=\"haspopup-true,labelledby-${id}_label\"\n\t\t\t><span class=\"dijitReset dijitInline dijitIcon\"\n\t\t\t\tdojoAttachPoint=\"iconNode\"\n\t\t\t></span\n\t\t\t><span class=\"dijitReset dijitInline dijitButtonText\"\n\t\t\t\tdojoAttachPoint=\"containerNode,_popupStateNode\"\n\t\t\t\tid=\"${id}_label\"\n\t\t\t></span\n\t\t\t><span class=\"dijitReset dijitInline dijitArrowButtonInner\"></span\n\t\t\t><span class=\"dijitReset dijitInline dijitArrowButtonChar\">▼</span\n\t\t></span\n\t></span\n\t><input ${!nameAttrSetting} type=\"${type}\" value=\"${value}\" class=\"dijitOffScreen\"\n\t\tdojoAttachPoint=\"valueNode\"\n/></span>\n"), + + _fillContent: function(){ + // Overrides Button._fillContent(). + // + // My inner HTML contains both the button contents and a drop down widget, like + // <DropDownButton> <span>push me</span> <Menu> ... </Menu> </DropDownButton> + // The first node is assumed to be the button content. The widget is the popup. + + if(this.srcNodeRef){ // programatically created buttons might not define srcNodeRef + //FIXME: figure out how to filter out the widget and use all remaining nodes as button + // content, not just nodes[0] + var nodes = dojo.query("*", this.srcNodeRef); + dijit.form.DropDownButton.superclass._fillContent.call(this, nodes[0]); + + // save pointer to srcNode so we can grab the drop down widget after it's instantiated + this.dropDownContainer = this.srcNodeRef; + } + }, + + startup: function(){ + if(this._started){ return; } + + // the child widget from srcNodeRef is the dropdown widget. Insert it in the page DOM, + // make it invisible, and store a reference to pass to the popup code. + if(!this.dropDown){ + var dropDownNode = dojo.query("[widgetId]", this.dropDownContainer)[0]; + this.dropDown = dijit.byNode(dropDownNode); + delete this.dropDownContainer; + } + dijit.popup.moveOffScreen(this.dropDown.domNode); + + this.inherited(arguments); + }, + + isLoaded: function(){ + // Returns whether or not we are loaded - if our dropdown has an href, + // then we want to check that. + var dropDown = this.dropDown; + return (!dropDown.href || dropDown.isLoaded); + }, + + loadDropDown: function(){ + // Loads our dropdown + var dropDown = this.dropDown; + if(!dropDown){ return; } + if(!this.isLoaded()){ + var handler = dojo.connect(dropDown, "onLoad", this, function(){ + dojo.disconnect(handler); + this.openDropDown(); + }); + dropDown.refresh(); + }else{ + this.openDropDown(); + } + }, + + isFocusable: function(){ + // Overridden so that focus is handled by the _HasDropDown mixin, not by + // the _FormWidget mixin. + return this.inherited(arguments) && !this._mouseDown; + } +}); + +dojo.declare("dijit.form.ComboButton", dijit.form.DropDownButton, { + // summary: + // A combination button and drop-down button. + // Users can click one side to "press" the button, or click an arrow + // icon to display the drop down. + // + // example: + // | <button dojoType="dijit.form.ComboButton" onClick="..."> + // | <span>Hello world</span> + // | <div dojoType="dijit.Menu">...</div> + // | </button> + // + // example: + // | var button1 = new dijit.form.ComboButton({label: "hello world", onClick: foo, dropDown: "myMenu"}); + // | dojo.body().appendChild(button1.domNode); + // + + templateString: dojo.cache("dijit.form", "templates/ComboButton.html", "<table class=\"dijit dijitReset dijitInline dijitLeft\"\n\tcellspacing='0' cellpadding='0' waiRole=\"presentation\"\n\t><tbody waiRole=\"presentation\"><tr waiRole=\"presentation\"\n\t\t><td class=\"dijitReset dijitStretch dijitButtonNode\" dojoAttachPoint=\"buttonNode\" dojoAttachEvent=\"ondijitclick:_onButtonClick,onkeypress:_onButtonKeyPress\"\n\t\t><div id=\"${id}_button\" class=\"dijitReset dijitButtonContents\"\n\t\t\tdojoAttachPoint=\"titleNode\"\n\t\t\twaiRole=\"button\" waiState=\"labelledby-${id}_label\"\n\t\t\t><div class=\"dijitReset dijitInline dijitIcon\" dojoAttachPoint=\"iconNode\" waiRole=\"presentation\"></div\n\t\t\t><div class=\"dijitReset dijitInline dijitButtonText\" id=\"${id}_label\" dojoAttachPoint=\"containerNode\" waiRole=\"presentation\"></div\n\t\t></div\n\t\t></td\n\t\t><td id=\"${id}_arrow\" class='dijitReset dijitRight dijitButtonNode dijitArrowButton'\n\t\t\tdojoAttachPoint=\"_popupStateNode,focusNode,_buttonNode\"\n\t\t\tdojoAttachEvent=\"onkeypress:_onArrowKeyPress\"\n\t\t\ttitle=\"${optionsTitle}\"\n\t\t\twaiRole=\"button\" waiState=\"haspopup-true\"\n\t\t\t><div class=\"dijitReset dijitArrowButtonInner\" waiRole=\"presentation\"></div\n\t\t\t><div class=\"dijitReset dijitArrowButtonChar\" waiRole=\"presentation\">▼</div\n\t\t></td\n\t\t><td style=\"display:none !important;\"\n\t\t\t><input ${!nameAttrSetting} type=\"${type}\" value=\"${value}\" dojoAttachPoint=\"valueNode\"\n\t\t/></td></tr></tbody\n></table>\n"), + + attributeMap: dojo.mixin(dojo.clone(dijit.form.Button.prototype.attributeMap), { + id: "", + tabIndex: ["focusNode", "titleNode"], + title: "titleNode" + }), + + // optionsTitle: String + // Text that describes the options menu (accessibility) + optionsTitle: "", + + baseClass: "dijitComboButton", + + // Set classes like dijitButtonContentsHover or dijitArrowButtonActive depending on + // mouse action over specified node + cssStateNodes: { + "buttonNode": "dijitButtonNode", + "titleNode": "dijitButtonContents", + "_popupStateNode": "dijitDownArrowButton" + }, + + _focusedNode: null, + + _onButtonKeyPress: function(/*Event*/ evt){ + // summary: + // Handler for right arrow key when focus is on left part of button + if(evt.charOrCode == dojo.keys[this.isLeftToRight() ? "RIGHT_ARROW" : "LEFT_ARROW"]){ + dijit.focus(this._popupStateNode); + dojo.stopEvent(evt); + } + }, + + _onArrowKeyPress: function(/*Event*/ evt){ + // summary: + // Handler for left arrow key when focus is on right part of button + if(evt.charOrCode == dojo.keys[this.isLeftToRight() ? "LEFT_ARROW" : "RIGHT_ARROW"]){ + dijit.focus(this.titleNode); + dojo.stopEvent(evt); + } + }, + + focus: function(/*String*/ position){ + // summary: + // Focuses this widget to according to position, if specified, + // otherwise on arrow node + // position: + // "start" or "end" + + dijit.focus(position == "start" ? this.titleNode : this._popupStateNode); + } +}); + +dojo.declare("dijit.form.ToggleButton", dijit.form.Button, { + // summary: + // A button that can be in two states (checked or not). + // Can be base class for things like tabs or checkbox or radio buttons + + baseClass: "dijitToggleButton", + + // checked: Boolean + // Corresponds to the native HTML <input> element's attribute. + // In markup, specified as "checked='checked'" or just "checked". + // True if the button is depressed, or the checkbox is checked, + // or the radio button is selected, etc. + checked: false, + + attributeMap: dojo.mixin(dojo.clone(dijit.form.Button.prototype.attributeMap), { + checked:"focusNode" + }), + + _clicked: function(/*Event*/ evt){ + this.set('checked', !this.checked); + }, + + _setCheckedAttr: function(/*Boolean*/ value, /* Boolean? */ priorityChange){ + this.checked = value; + dojo.attr(this.focusNode || this.domNode, "checked", value); + dijit.setWaiState(this.focusNode || this.domNode, "pressed", value); + this._handleOnChange(value, priorityChange); + }, + + setChecked: function(/*Boolean*/ checked){ + // summary: + // Deprecated. Use set('checked', true/false) instead. + dojo.deprecated("setChecked("+checked+") is deprecated. Use set('checked',"+checked+") instead.", "", "2.0"); + this.set('checked', checked); + }, + + reset: function(){ + // summary: + // Reset the widget's value to what it was at initialization time + + this._hasBeenBlurred = false; + + // set checked state to original setting + this.set('checked', this.params.checked || false); + } +}); + +} + +if(!dojo._hasResource["dijit.form.ToggleButton"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.form.ToggleButton"] = true; +dojo.provide("dijit.form.ToggleButton"); + + +} + +if(!dojo._hasResource["dijit.form.CheckBox"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.form.CheckBox"] = true; +dojo.provide("dijit.form.CheckBox"); + + + +dojo.declare( + "dijit.form.CheckBox", + dijit.form.ToggleButton, + { + // summary: + // Same as an HTML checkbox, but with fancy styling. + // + // description: + // User interacts with real html inputs. + // On onclick (which occurs by mouse click, space-bar, or + // using the arrow keys to switch the selected radio button), + // we update the state of the checkbox/radio. + // + // There are two modes: + // 1. High contrast mode + // 2. Normal mode + // + // In case 1, the regular html inputs are shown and used by the user. + // In case 2, the regular html inputs are invisible but still used by + // the user. They are turned quasi-invisible and overlay the background-image. + + templateString: dojo.cache("dijit.form", "templates/CheckBox.html", "<div class=\"dijit dijitReset dijitInline\" waiRole=\"presentation\"\n\t><input\n\t \t${!nameAttrSetting} type=\"${type}\" ${checkedAttrSetting}\n\t\tclass=\"dijitReset dijitCheckBoxInput\"\n\t\tdojoAttachPoint=\"focusNode\"\n\t \tdojoAttachEvent=\"onclick:_onClick\"\n/></div>\n"), + + baseClass: "dijitCheckBox", + + // type: [private] String + // type attribute on <input> node. + // Overrides `dijit.form.Button.type`. Users should not change this value. + type: "checkbox", + + // value: String + // As an initialization parameter, equivalent to value field on normal checkbox + // (if checked, the value is passed as the value when form is submitted). + // + // However, attr('value') will return either the string or false depending on + // whether or not the checkbox is checked. + // + // attr('value', string) will check the checkbox and change the value to the + // specified string + // + // attr('value', boolean) will change the checked state. + value: "on", + + // readOnly: Boolean + // Should this widget respond to user input? + // In markup, this is specified as "readOnly". + // Similar to disabled except readOnly form values are submitted. + readOnly: false, + + // the attributeMap should inherit from dijit.form._FormWidget.prototype.attributeMap + // instead of ToggleButton as the icon mapping has no meaning for a CheckBox + attributeMap: dojo.delegate(dijit.form._FormWidget.prototype.attributeMap, { + readOnly: "focusNode" + }), + + _setReadOnlyAttr: function(/*Boolean*/ value){ + this.readOnly = value; + dojo.attr(this.focusNode, 'readOnly', value); + dijit.setWaiState(this.focusNode, "readonly", value); + }, + + _setValueAttr: function(/*String or Boolean*/ newValue, /*Boolean*/ priorityChange){ + // summary: + // Handler for value= attribute to constructor, and also calls to + // attr('value', val). + // description: + // During initialization, just saves as attribute to the <input type=checkbox>. + // + // After initialization, + // when passed a boolean, controls whether or not the CheckBox is checked. + // If passed a string, changes the value attribute of the CheckBox (the one + // specified as "value" when the CheckBox was constructed (ex: <input + // dojoType="dijit.CheckBox" value="chicken">) + if(typeof newValue == "string"){ + this.value = newValue; + dojo.attr(this.focusNode, 'value', newValue); + newValue = true; + } + if(this._created){ + this.set('checked', newValue, priorityChange); + } + }, + _getValueAttr: function(){ + // summary: + // Hook so attr('value') works. + // description: + // If the CheckBox is checked, returns the value attribute. + // Otherwise returns false. + return (this.checked ? this.value : false); + }, + + // Override dijit.form.Button._setLabelAttr() since we don't even have a containerNode. + // Normally users won't try to set label, except when CheckBox or RadioButton is the child of a dojox.layout.TabContainer + _setLabelAttr: undefined, + + postMixInProperties: function(){ + if(this.value == ""){ + this.value = "on"; + } + + // Need to set initial checked state as part of template, so that form submit works. + // dojo.attr(node, "checked", bool) doesn't work on IEuntil node has been attached + // to <body>, see #8666 + this.checkedAttrSetting = this.checked ? "checked" : ""; + + this.inherited(arguments); + }, + + _fillContent: function(/*DomNode*/ source){ + // Override Button::_fillContent() since it doesn't make sense for CheckBox, + // since CheckBox doesn't even have a container + }, + + reset: function(){ + // Override ToggleButton.reset() + + this._hasBeenBlurred = false; + + this.set('checked', this.params.checked || false); + + // Handle unlikely event that the <input type=checkbox> value attribute has changed + this.value = this.params.value || "on"; + dojo.attr(this.focusNode, 'value', this.value); + }, + + _onFocus: function(){ + if(this.id){ + dojo.query("label[for='"+this.id+"']").addClass("dijitFocusedLabel"); + } + this.inherited(arguments); + }, + + _onBlur: function(){ + if(this.id){ + dojo.query("label[for='"+this.id+"']").removeClass("dijitFocusedLabel"); + } + this.inherited(arguments); + }, + + _onClick: function(/*Event*/ e){ + // summary: + // Internal function to handle click actions - need to check + // readOnly, since button no longer does that check. + if(this.readOnly){ + return false; + } + return this.inherited(arguments); + } + } +); + +dojo.declare( + "dijit.form.RadioButton", + dijit.form.CheckBox, + { + // summary: + // Same as an HTML radio, but with fancy styling. + + type: "radio", + baseClass: "dijitRadio", + + _setCheckedAttr: function(/*Boolean*/ value){ + // If I am being checked then have to deselect currently checked radio button + this.inherited(arguments); + if(!this._created){ return; } + if(value){ + var _this = this; + // search for radio buttons with the same name that need to be unchecked + dojo.query("INPUT[type=radio]", this.focusNode.form || dojo.doc).forEach( // can't use name= since dojo.query doesn't support [] in the name + function(inputNode){ + if(inputNode.name == _this.name && inputNode != _this.focusNode && inputNode.form == _this.focusNode.form){ + var widget = dijit.getEnclosingWidget(inputNode); + if(widget && widget.checked){ + widget.set('checked', false); + } + } + } + ); + } + }, + + _clicked: function(/*Event*/ e){ + if(!this.checked){ + this.set('checked', true); + } + } + } +); + +} + +if(!dojo._hasResource["dijit.form.DropDownButton"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.form.DropDownButton"] = true; +dojo.provide("dijit.form.DropDownButton"); + + + +} + +if(!dojo._hasResource["dojo.regexp"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.regexp"] = true; +dojo.provide("dojo.regexp"); + +/*===== +dojo.regexp = { + // summary: Regular expressions and Builder resources +}; +=====*/ + +dojo.regexp.escapeString = function(/*String*/str, /*String?*/except){ + // summary: + // Adds escape sequences for special characters in regular expressions + // except: + // a String with special characters to be left unescaped + + return str.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, function(ch){ + if(except && except.indexOf(ch) != -1){ + return ch; + } + return "\\" + ch; + }); // String +} + +dojo.regexp.buildGroupRE = function(/*Object|Array*/arr, /*Function*/re, /*Boolean?*/nonCapture){ + // summary: + // Builds a regular expression that groups subexpressions + // description: + // A utility function used by some of the RE generators. The + // subexpressions are constructed by the function, re, in the second + // parameter. re builds one subexpression for each elem in the array + // a, in the first parameter. Returns a string for a regular + // expression that groups all the subexpressions. + // arr: + // A single value or an array of values. + // re: + // A function. Takes one parameter and converts it to a regular + // expression. + // nonCapture: + // If true, uses non-capturing match, otherwise matches are retained + // by regular expression. Defaults to false + + // case 1: a is a single value. + if(!(arr instanceof Array)){ + return re(arr); // String + } + + // case 2: a is an array + var b = []; + for(var i = 0; i < arr.length; i++){ + // convert each elem to a RE + b.push(re(arr[i])); + } + + // join the REs as alternatives in a RE group. + return dojo.regexp.group(b.join("|"), nonCapture); // String +} + +dojo.regexp.group = function(/*String*/expression, /*Boolean?*/nonCapture){ + // summary: + // adds group match to expression + // nonCapture: + // If true, uses non-capturing match, otherwise matches are retained + // by regular expression. + return "(" + (nonCapture ? "?:":"") + expression + ")"; // String +} + +} + +if(!dojo._hasResource["dojo.data.util.sorter"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.data.util.sorter"] = true; +dojo.provide("dojo.data.util.sorter"); + +dojo.data.util.sorter.basicComparator = function( /*anything*/ a, + /*anything*/ b){ + // summary: + // Basic comparision function that compares if an item is greater or less than another item + // description: + // returns 1 if a > b, -1 if a < b, 0 if equal. + // 'null' values (null, undefined) are treated as larger values so that they're pushed to the end of the list. + // And compared to each other, null is equivalent to undefined. + + //null is a problematic compare, so if null, we set to undefined. + //Makes the check logic simple, compact, and consistent + //And (null == undefined) === true, so the check later against null + //works for undefined and is less bytes. + var r = -1; + if(a === null){ + a = undefined; + } + if(b === null){ + b = undefined; + } + if(a == b){ + r = 0; + }else if(a > b || a == null){ + r = 1; + } + return r; //int {-1,0,1} +}; + +dojo.data.util.sorter.createSortFunction = function( /* attributes array */sortSpec, + /*dojo.data.core.Read*/ store){ + // summary: + // Helper function to generate the sorting function based off the list of sort attributes. + // description: + // The sort function creation will look for a property on the store called 'comparatorMap'. If it exists + // it will look in the mapping for comparisons function for the attributes. If one is found, it will + // use it instead of the basic comparator, which is typically used for strings, ints, booleans, and dates. + // Returns the sorting function for this particular list of attributes and sorting directions. + // + // sortSpec: array + // A JS object that array that defines out what attribute names to sort on and whether it should be descenting or asending. + // The objects should be formatted as follows: + // { + // attribute: "attributeName-string" || attribute, + // descending: true|false; // Default is false. + // } + // store: object + // The datastore object to look up item values from. + // + var sortFunctions=[]; + + function createSortFunction(attr, dir, comp, s){ + //Passing in comp and s (comparator and store), makes this + //function much faster. + return function(itemA, itemB){ + var a = s.getValue(itemA, attr); + var b = s.getValue(itemB, attr); + return dir * comp(a,b); //int + }; + } + var sortAttribute; + var map = store.comparatorMap; + var bc = dojo.data.util.sorter.basicComparator; + for(var i = 0; i < sortSpec.length; i++){ + sortAttribute = sortSpec[i]; + var attr = sortAttribute.attribute; + if(attr){ + var dir = (sortAttribute.descending) ? -1 : 1; + var comp = bc; + if(map){ + if(typeof attr !== "string" && ("toString" in attr)){ + attr = attr.toString(); + } + comp = map[attr] || bc; + } + sortFunctions.push(createSortFunction(attr, + dir, comp, store)); + } + } + return function(rowA, rowB){ + var i=0; + while(i < sortFunctions.length){ + var ret = sortFunctions[i++](rowA, rowB); + if(ret !== 0){ + return ret;//int + } + } + return 0; //int + }; // Function +}; + +} + +if(!dojo._hasResource["dojo.data.util.simpleFetch"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.data.util.simpleFetch"] = true; +dojo.provide("dojo.data.util.simpleFetch"); + + +dojo.data.util.simpleFetch.fetch = function(/* Object? */ request){ + // summary: + // The simpleFetch mixin is designed to serve as a set of function(s) that can + // be mixed into other datastore implementations to accelerate their development. + // The simpleFetch mixin should work well for any datastore that can respond to a _fetchItems() + // call by returning an array of all the found items that matched the query. The simpleFetch mixin + // is not designed to work for datastores that respond to a fetch() call by incrementally + // loading items, or sequentially loading partial batches of the result + // set. For datastores that mixin simpleFetch, simpleFetch + // implements a fetch method that automatically handles eight of the fetch() + // arguments -- onBegin, onItem, onComplete, onError, start, count, sort and scope + // The class mixing in simpleFetch should not implement fetch(), + // but should instead implement a _fetchItems() method. The _fetchItems() + // method takes three arguments, the keywordArgs object that was passed + // to fetch(), a callback function to be called when the result array is + // available, and an error callback to be called if something goes wrong. + // The _fetchItems() method should ignore any keywordArgs parameters for + // start, count, onBegin, onItem, onComplete, onError, sort, and scope. + // The _fetchItems() method needs to correctly handle any other keywordArgs + // parameters, including the query parameter and any optional parameters + // (such as includeChildren). The _fetchItems() method should create an array of + // result items and pass it to the fetchHandler along with the original request object + // -- or, the _fetchItems() method may, if it wants to, create an new request object + // with other specifics about the request that are specific to the datastore and pass + // that as the request object to the handler. + // + // For more information on this specific function, see dojo.data.api.Read.fetch() + request = request || {}; + if(!request.store){ + request.store = this; + } + var self = this; + + var _errorHandler = function(errorData, requestObject){ + if(requestObject.onError){ + var scope = requestObject.scope || dojo.global; + requestObject.onError.call(scope, errorData, requestObject); + } + }; + + var _fetchHandler = function(items, requestObject){ + var oldAbortFunction = requestObject.abort || null; + var aborted = false; + + var startIndex = requestObject.start?requestObject.start:0; + var endIndex = (requestObject.count && (requestObject.count !== Infinity))?(startIndex + requestObject.count):items.length; + + requestObject.abort = function(){ + aborted = true; + if(oldAbortFunction){ + oldAbortFunction.call(requestObject); + } + }; + + var scope = requestObject.scope || dojo.global; + if(!requestObject.store){ + requestObject.store = self; + } + if(requestObject.onBegin){ + requestObject.onBegin.call(scope, items.length, requestObject); + } + if(requestObject.sort){ + items.sort(dojo.data.util.sorter.createSortFunction(requestObject.sort, self)); + } + if(requestObject.onItem){ + for(var i = startIndex; (i < items.length) && (i < endIndex); ++i){ + var item = items[i]; + if(!aborted){ + requestObject.onItem.call(scope, item, requestObject); + } + } + } + if(requestObject.onComplete && !aborted){ + var subset = null; + if(!requestObject.onItem){ + subset = items.slice(startIndex, endIndex); + } + requestObject.onComplete.call(scope, subset, requestObject); + } + }; + this._fetchItems(request, _fetchHandler, _errorHandler); + return request; // Object +}; + +} + +if(!dojo._hasResource["dojo.data.util.filter"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.data.util.filter"] = true; +dojo.provide("dojo.data.util.filter"); + +dojo.data.util.filter.patternToRegExp = function(/*String*/pattern, /*boolean?*/ ignoreCase){ + // summary: + // Helper function to convert a simple pattern to a regular expression for matching. + // description: + // Returns a regular expression object that conforms to the defined conversion rules. + // For example: + // ca* -> /^ca.*$/ + // *ca* -> /^.*ca.*$/ + // *c\*a* -> /^.*c\*a.*$/ + // *c\*a?* -> /^.*c\*a..*$/ + // and so on. + // + // pattern: string + // A simple matching pattern to convert that follows basic rules: + // * Means match anything, so ca* means match anything starting with ca + // ? Means match single character. So, b?b will match to bob and bab, and so on. + // \ is an escape character. So for example, \* means do not treat * as a match, but literal character *. + // To use a \ as a character in the string, it must be escaped. So in the pattern it should be + // represented by \\ to be treated as an ordinary \ character instead of an escape. + // + // ignoreCase: + // An optional flag to indicate if the pattern matching should be treated as case-sensitive or not when comparing + // By default, it is assumed case sensitive. + + var rxp = "^"; + var c = null; + for(var i = 0; i < pattern.length; i++){ + c = pattern.charAt(i); + switch(c){ + case '\\': + rxp += c; + i++; + rxp += pattern.charAt(i); + break; + case '*': + rxp += ".*"; break; + case '?': + rxp += "."; break; + case '$': + case '^': + case '/': + case '+': + case '.': + case '|': + case '(': + case ')': + case '{': + case '}': + case '[': + case ']': + rxp += "\\"; //fallthrough + default: + rxp += c; + } + } + rxp += "$"; + if(ignoreCase){ + return new RegExp(rxp,"mi"); //RegExp + }else{ + return new RegExp(rxp,"m"); //RegExp + } + +}; + +} + +if(!dojo._hasResource["dijit.form.TextBox"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.form.TextBox"] = true; +dojo.provide("dijit.form.TextBox"); + + + +dojo.declare( + "dijit.form.TextBox", + dijit.form._FormValueWidget, + { + // summary: + // A base class for textbox form inputs + + // trim: Boolean + // Removes leading and trailing whitespace if true. Default is false. + trim: false, + + // uppercase: Boolean + // Converts all characters to uppercase if true. Default is false. + uppercase: false, + + // lowercase: Boolean + // Converts all characters to lowercase if true. Default is false. + lowercase: false, + + // propercase: Boolean + // Converts the first character of each word to uppercase if true. + propercase: false, + + // maxLength: String + // HTML INPUT tag maxLength declaration. + maxLength: "", + + // selectOnClick: [const] Boolean + // If true, all text will be selected when focused with mouse + selectOnClick: false, + + // placeHolder: String + // Defines a hint to help users fill out the input field (as defined in HTML 5). + // This should only contain plain text (no html markup). + placeHolder: "", + + templateString: dojo.cache("dijit.form", "templates/TextBox.html", "<div class=\"dijit dijitReset dijitInline dijitLeft\" id=\"widget_${id}\" waiRole=\"presentation\"\n\t><div class=\"dijitReset dijitInputField dijitInputContainer\"\n\t\t><input class=\"dijitReset dijitInputInner\" dojoAttachPoint='textbox,focusNode' autocomplete=\"off\"\n\t\t\t${!nameAttrSetting} type='${type}'\n\t/></div\n></div>\n"), + _singleNodeTemplate: '<input class="dijit dijitReset dijitLeft dijitInputField" dojoAttachPoint="textbox,focusNode" autocomplete="off" type="${type}" ${!nameAttrSetting} />', + + _buttonInputDisabled: dojo.isIE ? "disabled" : "", // allows IE to disallow focus, but Firefox cannot be disabled for mousedown events + + baseClass: "dijitTextBox", + + attributeMap: dojo.delegate(dijit.form._FormValueWidget.prototype.attributeMap, { + maxLength: "focusNode" + }), + + postMixInProperties: function(){ + var type = this.type.toLowerCase(); + if(this.templateString.toLowerCase() == "input" || ((type == "hidden" || type == "file") && this.templateString == dijit.form.TextBox.prototype.templateString)){ + this.templateString = this._singleNodeTemplate; + } + this.inherited(arguments); + }, + + _setPlaceHolderAttr: function(v){ + this.placeHolder = v; + if(!this._phspan){ + this._attachPoints.push('_phspan'); + /* dijitInputField class gives placeHolder same padding as the input field + * parent node already has dijitInputField class but it doesn't affect this <span> + * since it's position: absolute. + */ + this._phspan = dojo.create('span',{className:'dijitPlaceHolder dijitInputField'},this.textbox,'after'); + } + this._phspan.innerHTML=""; + this._phspan.appendChild(document.createTextNode(v)); + + this._updatePlaceHolder(); + }, + + _updatePlaceHolder: function(){ + if(this._phspan){ + this._phspan.style.display=(this.placeHolder&&!this._focused&&!this.textbox.value)?"":"none"; + } + }, + + _getValueAttr: function(){ + // summary: + // Hook so attr('value') works as we like. + // description: + // For `dijit.form.TextBox` this basically returns the value of the <input>. + // + // For `dijit.form.MappedTextBox` subclasses, which have both + // a "displayed value" and a separate "submit value", + // This treats the "displayed value" as the master value, computing the + // submit value from it via this.parse(). + return this.parse(this.get('displayedValue'), this.constraints); + }, + + _setValueAttr: function(value, /*Boolean?*/ priorityChange, /*String?*/ formattedValue){ + // summary: + // Hook so attr('value', ...) works. + // + // description: + // Sets the value of the widget to "value" which can be of + // any type as determined by the widget. + // + // value: + // The visual element value is also set to a corresponding, + // but not necessarily the same, value. + // + // formattedValue: + // If specified, used to set the visual element value, + // otherwise a computed visual value is used. + // + // priorityChange: + // If true, an onChange event is fired immediately instead of + // waiting for the next blur event. + + var filteredValue; + if(value !== undefined){ + // TODO: this is calling filter() on both the display value and the actual value. + // I added a comment to the filter() definition about this, but it should be changed. + filteredValue = this.filter(value); + if(typeof formattedValue != "string"){ + if(filteredValue !== null && ((typeof filteredValue != "number") || !isNaN(filteredValue))){ + formattedValue = this.filter(this.format(filteredValue, this.constraints)); + }else{ formattedValue = ''; } + } + } + if(formattedValue != null && formattedValue != undefined && ((typeof formattedValue) != "number" || !isNaN(formattedValue)) && this.textbox.value != formattedValue){ + this.textbox.value = formattedValue; + } + + this._updatePlaceHolder(); + + this.inherited(arguments, [filteredValue, priorityChange]); + }, + + // displayedValue: String + // For subclasses like ComboBox where the displayed value + // (ex: Kentucky) and the serialized value (ex: KY) are different, + // this represents the displayed value. + // + // Setting 'displayedValue' through attr('displayedValue', ...) + // updates 'value', and vice-versa. Otherwise 'value' is updated + // from 'displayedValue' periodically, like onBlur etc. + // + // TODO: move declaration to MappedTextBox? + // Problem is that ComboBox references displayedValue, + // for benefit of FilteringSelect. + displayedValue: "", + + getDisplayedValue: function(){ + // summary: + // Deprecated. Use set('displayedValue') instead. + // tags: + // deprecated + dojo.deprecated(this.declaredClass+"::getDisplayedValue() is deprecated. Use set('displayedValue') instead.", "", "2.0"); + return this.get('displayedValue'); + }, + + _getDisplayedValueAttr: function(){ + // summary: + // Hook so attr('displayedValue') works. + // description: + // Returns the displayed value (what the user sees on the screen), + // after filtering (ie, trimming spaces etc.). + // + // For some subclasses of TextBox (like ComboBox), the displayed value + // is different from the serialized value that's actually + // sent to the server (see dijit.form.ValidationTextBox.serialize) + + return this.filter(this.textbox.value); + }, + + setDisplayedValue: function(/*String*/value){ + // summary: + // Deprecated. Use set('displayedValue', ...) instead. + // tags: + // deprecated + dojo.deprecated(this.declaredClass+"::setDisplayedValue() is deprecated. Use set('displayedValue', ...) instead.", "", "2.0"); + this.set('displayedValue', value); + }, + + _setDisplayedValueAttr: function(/*String*/value){ + // summary: + // Hook so attr('displayedValue', ...) works. + // description: + // Sets the value of the visual element to the string "value". + // The widget value is also set to a corresponding, + // but not necessarily the same, value. + + if(value === null || value === undefined){ value = '' } + else if(typeof value != "string"){ value = String(value) } + this.textbox.value = value; + this._setValueAttr(this.get('value'), undefined, value); + }, + + format: function(/* String */ value, /* Object */ constraints){ + // summary: + // Replacable function to convert a value to a properly formatted string. + // tags: + // protected extension + return ((value == null || value == undefined) ? "" : (value.toString ? value.toString() : value)); + }, + + parse: function(/* String */ value, /* Object */ constraints){ + // summary: + // Replacable function to convert a formatted string to a value + // tags: + // protected extension + + return value; // String + }, + + _refreshState: function(){ + // summary: + // After the user types some characters, etc., this method is + // called to check the field for validity etc. The base method + // in `dijit.form.TextBox` does nothing, but subclasses override. + // tags: + // protected + }, + + _onInput: function(e){ + if(e && e.type && /key/i.test(e.type) && e.keyCode){ + switch(e.keyCode){ + case dojo.keys.SHIFT: + case dojo.keys.ALT: + case dojo.keys.CTRL: + case dojo.keys.TAB: + return; + } + } + if(this.intermediateChanges){ + var _this = this; + // the setTimeout allows the key to post to the widget input box + setTimeout(function(){ _this._handleOnChange(_this.get('value'), false); }, 0); + } + this._refreshState(); + }, + + postCreate: function(){ + // setting the value here is needed since value="" in the template causes "undefined" + // and setting in the DOM (instead of the JS object) helps with form reset actions + if(dojo.isIE){ // IE INPUT tag fontFamily has to be set directly using STYLE + var s = dojo.getComputedStyle(this.domNode); + if(s){ + var ff = s.fontFamily; + if(ff){ + var inputs = this.domNode.getElementsByTagName("INPUT"); + if(inputs){ + for(var i=0; i < inputs.length; i++){ + inputs[i].style.fontFamily = ff; + } + } + } + } + } + this.textbox.setAttribute("value", this.textbox.value); // DOM and JS values shuld be the same + this.inherited(arguments); + if(dojo.isMoz || dojo.isOpera){ + this.connect(this.textbox, "oninput", this._onInput); + }else{ + this.connect(this.textbox, "onkeydown", this._onInput); + this.connect(this.textbox, "onkeyup", this._onInput); + this.connect(this.textbox, "onpaste", this._onInput); + this.connect(this.textbox, "oncut", this._onInput); + } + }, + + _blankValue: '', // if the textbox is blank, what value should be reported + filter: function(val){ + // summary: + // Auto-corrections (such as trimming) that are applied to textbox + // value on blur or form submit. + // description: + // For MappedTextBox subclasses, this is called twice + // - once with the display value + // - once the value as set/returned by attr('value', ...) + // and attr('value'), ex: a Number for NumberTextBox. + // + // In the latter case it does corrections like converting null to NaN. In + // the former case the NumberTextBox.filter() method calls this.inherited() + // to execute standard trimming code in TextBox.filter(). + // + // TODO: break this into two methods in 2.0 + // + // tags: + // protected extension + if(val === null){ return this._blankValue; } + if(typeof val != "string"){ return val; } + if(this.trim){ + val = dojo.trim(val); + } + if(this.uppercase){ + val = val.toUpperCase(); + } + if(this.lowercase){ + val = val.toLowerCase(); + } + if(this.propercase){ + val = val.replace(/[^\s]+/g, function(word){ + return word.substring(0,1).toUpperCase() + word.substring(1); + }); + } + return val; + }, + + _setBlurValue: function(){ + this._setValueAttr(this.get('value'), true); + }, + + _onBlur: function(e){ + if(this.disabled){ return; } + this._setBlurValue(); + this.inherited(arguments); + + if(this._selectOnClickHandle){ + this.disconnect(this._selectOnClickHandle); + } + if(this.selectOnClick && dojo.isMoz){ + this.textbox.selectionStart = this.textbox.selectionEnd = undefined; // clear selection so that the next mouse click doesn't reselect + } + + this._updatePlaceHolder(); + }, + + _onFocus: function(/*String*/ by){ + if(this.disabled || this.readOnly){ return; } + + // Select all text on focus via click if nothing already selected. + // Since mouse-up will clear the selection need to defer selection until after mouse-up. + // Don't do anything on focus by tabbing into the widget since there's no associated mouse-up event. + if(this.selectOnClick && by == "mouse"){ + this._selectOnClickHandle = this.connect(this.domNode, "onmouseup", function(){ + // Only select all text on first click; otherwise users would have no way to clear + // the selection. + this.disconnect(this._selectOnClickHandle); + + // Check if the user selected some text manually (mouse-down, mouse-move, mouse-up) + // and if not, then select all the text + var textIsNotSelected; + if(dojo.isIE){ + var range = dojo.doc.selection.createRange(); + var parent = range.parentElement(); + textIsNotSelected = parent == this.textbox && range.text.length == 0; + }else{ + textIsNotSelected = this.textbox.selectionStart == this.textbox.selectionEnd; + } + if(textIsNotSelected){ + dijit.selectInputText(this.textbox); + } + }); + } + + this._updatePlaceHolder(); + + this._refreshState(); + this.inherited(arguments); + }, + + reset: function(){ + // Overrides dijit._FormWidget.reset(). + // Additionally resets the displayed textbox value to '' + this.textbox.value = ''; + this.inherited(arguments); + } + } +); + +dijit.selectInputText = function(/*DomNode*/element, /*Number?*/ start, /*Number?*/ stop){ + // summary: + // Select text in the input element argument, from start (default 0), to stop (default end). + + // TODO: use functions in _editor/selection.js? + var _window = dojo.global; + var _document = dojo.doc; + element = dojo.byId(element); + if(isNaN(start)){ start = 0; } + if(isNaN(stop)){ stop = element.value ? element.value.length : 0; } + dijit.focus(element); + if(_document["selection"] && dojo.body()["createTextRange"]){ // IE + if(element.createTextRange){ + var range = element.createTextRange(); + with(range){ + collapse(true); + moveStart("character", -99999); // move to 0 + moveStart("character", start); // delta from 0 is the correct position + moveEnd("character", stop-start); + select(); + } + } + }else if(_window["getSelection"]){ + if(element.setSelectionRange){ + element.setSelectionRange(start, stop); + } + } +}; + +} + +if(!dojo._hasResource["dijit.Tooltip"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.Tooltip"] = true; +dojo.provide("dijit.Tooltip"); + + + + +dojo.declare( + "dijit._MasterTooltip", + [dijit._Widget, dijit._Templated], + { + // summary: + // Internal widget that holds the actual tooltip markup, + // which occurs once per page. + // Called by Tooltip widgets which are just containers to hold + // the markup + // tags: + // protected + + // duration: Integer + // Milliseconds to fade in/fade out + duration: dijit.defaultDuration, + + templateString: dojo.cache("dijit", "templates/Tooltip.html", "<div class=\"dijitTooltip dijitTooltipLeft\" id=\"dojoTooltip\">\n\t<div class=\"dijitTooltipContainer dijitTooltipContents\" dojoAttachPoint=\"containerNode\" waiRole='alert'></div>\n\t<div class=\"dijitTooltipConnector\"></div>\n</div>\n"), + + postCreate: function(){ + dojo.body().appendChild(this.domNode); + + this.bgIframe = new dijit.BackgroundIframe(this.domNode); + + // Setup fade-in and fade-out functions. + this.fadeIn = dojo.fadeIn({ node: this.domNode, duration: this.duration, onEnd: dojo.hitch(this, "_onShow") }); + this.fadeOut = dojo.fadeOut({ node: this.domNode, duration: this.duration, onEnd: dojo.hitch(this, "_onHide") }); + + }, + + show: function(/*String*/ innerHTML, /*DomNode*/ aroundNode, /*String[]?*/ position, /*Boolean*/ rtl){ + // summary: + // Display tooltip w/specified contents to right of specified node + // (To left if there's no space on the right, or if rtl == true) + + if(this.aroundNode && this.aroundNode === aroundNode){ + return; + } + + if(this.fadeOut.status() == "playing"){ + // previous tooltip is being hidden; wait until the hide completes then show new one + this._onDeck=arguments; + return; + } + this.containerNode.innerHTML=innerHTML; + + var pos = dijit.placeOnScreenAroundElement(this.domNode, aroundNode, dijit.getPopupAroundAlignment((position && position.length) ? position : dijit.Tooltip.defaultPosition, !rtl), dojo.hitch(this, "orient")); + + // show it + dojo.style(this.domNode, "opacity", 0); + this.fadeIn.play(); + this.isShowingNow = true; + this.aroundNode = aroundNode; + }, + + orient: function(/* DomNode */ node, /* String */ aroundCorner, /* String */ tooltipCorner){ + // summary: + // Private function to set CSS for tooltip node based on which position it's in. + // This is called by the dijit popup code. + // tags: + // protected + + node.className = "dijitTooltip " + + { + "BL-TL": "dijitTooltipBelow dijitTooltipABLeft", + "TL-BL": "dijitTooltipAbove dijitTooltipABLeft", + "BR-TR": "dijitTooltipBelow dijitTooltipABRight", + "TR-BR": "dijitTooltipAbove dijitTooltipABRight", + "BR-BL": "dijitTooltipRight", + "BL-BR": "dijitTooltipLeft" + }[aroundCorner + "-" + tooltipCorner]; + }, + + _onShow: function(){ + // summary: + // Called at end of fade-in operation + // tags: + // protected + if(dojo.isIE){ + // the arrow won't show up on a node w/an opacity filter + this.domNode.style.filter=""; + } + }, + + hide: function(aroundNode){ + // summary: + // Hide the tooltip + if(this._onDeck && this._onDeck[1] == aroundNode){ + // this hide request is for a show() that hasn't even started yet; + // just cancel the pending show() + this._onDeck=null; + }else if(this.aroundNode === aroundNode){ + // this hide request is for the currently displayed tooltip + this.fadeIn.stop(); + this.isShowingNow = false; + this.aroundNode = null; + this.fadeOut.play(); + }else{ + // just ignore the call, it's for a tooltip that has already been erased + } + }, + + _onHide: function(){ + // summary: + // Called at end of fade-out operation + // tags: + // protected + + this.domNode.style.cssText=""; // to position offscreen again + this.containerNode.innerHTML=""; + if(this._onDeck){ + // a show request has been queued up; do it now + this.show.apply(this, this._onDeck); + this._onDeck=null; + } + } + + } +); + +dijit.showTooltip = function(/*String*/ innerHTML, /*DomNode*/ aroundNode, /*String[]?*/ position, /*Boolean*/ rtl){ + // summary: + // Display tooltip w/specified contents in specified position. + // See description of dijit.Tooltip.defaultPosition for details on position parameter. + // If position is not specified then dijit.Tooltip.defaultPosition is used. + if(!dijit._masterTT){ dijit._masterTT = new dijit._MasterTooltip(); } + return dijit._masterTT.show(innerHTML, aroundNode, position, rtl); +}; + +dijit.hideTooltip = function(aroundNode){ + // summary: + // Hide the tooltip + if(!dijit._masterTT){ dijit._masterTT = new dijit._MasterTooltip(); } + return dijit._masterTT.hide(aroundNode); +}; + +dojo.declare( + "dijit.Tooltip", + dijit._Widget, + { + // summary: + // Pops up a tooltip (a help message) when you hover over a node. + + // label: String + // Text to display in the tooltip. + // Specified as innerHTML when creating the widget from markup. + label: "", + + // showDelay: Integer + // Number of milliseconds to wait after hovering over/focusing on the object, before + // the tooltip is displayed. + showDelay: 400, + + // connectId: [const] String[] + // Id's of domNodes to attach the tooltip to. + // When user hovers over any of the specified dom nodes, the tooltip will appear. + // + // Note: Currently connectId can only be specified on initialization, it cannot + // be changed via attr('connectId', ...) + // + // Note: in 2.0 this will be renamed to connectIds for less confusion. + connectId: [], + + // position: String[] + // See description of `dijit.Tooltip.defaultPosition` for details on position parameter. + position: [], + + constructor: function(){ + // Map id's of nodes I'm connected to to a list of the this.connect() handles + this._nodeConnectionsById = {}; + }, + + _setConnectIdAttr: function(newIds){ + for(var oldId in this._nodeConnectionsById){ + this.removeTarget(oldId); + } + dojo.forEach(dojo.isArrayLike(newIds) ? newIds : [newIds], this.addTarget, this); + }, + + _getConnectIdAttr: function(){ + var ary = []; + for(var id in this._nodeConnectionsById){ + ary.push(id); + } + return ary; + }, + + addTarget: function(/*DOMNODE || String*/ id){ + // summary: + // Attach tooltip to specified node, if it's not already connected + var node = dojo.byId(id); + if(!node){ return; } + if(node.id in this._nodeConnectionsById){ return; }//Already connected + + this._nodeConnectionsById[node.id] = [ + this.connect(node, "onmouseenter", "_onTargetMouseEnter"), + this.connect(node, "onmouseleave", "_onTargetMouseLeave"), + this.connect(node, "onfocus", "_onTargetFocus"), + this.connect(node, "onblur", "_onTargetBlur") + ]; + }, + + removeTarget: function(/*DOMNODE || String*/ node){ + // summary: + // Detach tooltip from specified node + + // map from DOMNode back to plain id string + var id = node.id || node; + + if(id in this._nodeConnectionsById){ + dojo.forEach(this._nodeConnectionsById[id], this.disconnect, this); + delete this._nodeConnectionsById[id]; + } + }, + + postCreate: function(){ + dojo.addClass(this.domNode,"dijitTooltipData"); + }, + + startup: function(){ + this.inherited(arguments); + + // If this tooltip was created in a template, or for some other reason the specified connectId[s] + // didn't exist during the widget's initialization, then connect now. + var ids = this.connectId; + dojo.forEach(dojo.isArrayLike(ids) ? ids : [ids], this.addTarget, this); + }, + + _onTargetMouseEnter: function(/*Event*/ e){ + // summary: + // Handler for mouseenter event on the target node + // tags: + // private + this._onHover(e); + }, + + _onTargetMouseLeave: function(/*Event*/ e){ + // summary: + // Handler for mouseleave event on the target node + // tags: + // private + this._onUnHover(e); + }, + + _onTargetFocus: function(/*Event*/ e){ + // summary: + // Handler for focus event on the target node + // tags: + // private + + this._focus = true; + this._onHover(e); + }, + + _onTargetBlur: function(/*Event*/ e){ + // summary: + // Handler for blur event on the target node + // tags: + // private + + this._focus = false; + this._onUnHover(e); + }, + + _onHover: function(/*Event*/ e){ + // summary: + // Despite the name of this method, it actually handles both hover and focus + // events on the target node, setting a timer to show the tooltip. + // tags: + // private + if(!this._showTimer){ + var target = e.target; + this._showTimer = setTimeout(dojo.hitch(this, function(){this.open(target)}), this.showDelay); + } + }, + + _onUnHover: function(/*Event*/ e){ + // summary: + // Despite the name of this method, it actually handles both mouseleave and blur + // events on the target node, hiding the tooltip. + // tags: + // private + + // keep a tooltip open if the associated element still has focus (even though the + // mouse moved away) + if(this._focus){ return; } + + if(this._showTimer){ + clearTimeout(this._showTimer); + delete this._showTimer; + } + this.close(); + }, + + open: function(/*DomNode*/ target){ + // summary: + // Display the tooltip; usually not called directly. + // tags: + // private + + if(this._showTimer){ + clearTimeout(this._showTimer); + delete this._showTimer; + } + dijit.showTooltip(this.label || this.domNode.innerHTML, target, this.position, !this.isLeftToRight()); + + this._connectNode = target; + this.onShow(target, this.position); + }, + + close: function(){ + // summary: + // Hide the tooltip or cancel timer for show of tooltip + // tags: + // private + + if(this._connectNode){ + // if tooltip is currently shown + dijit.hideTooltip(this._connectNode); + delete this._connectNode; + this.onHide(); + } + if(this._showTimer){ + // if tooltip is scheduled to be shown (after a brief delay) + clearTimeout(this._showTimer); + delete this._showTimer; + } + }, + + onShow: function(target, position){ + // summary: + // Called when the tooltip is shown + // tags: + // callback + }, + + onHide: function(){ + // summary: + // Called when the tooltip is hidden + // tags: + // callback + }, + + uninitialize: function(){ + this.close(); + this.inherited(arguments); + } + } +); + +// dijit.Tooltip.defaultPosition: String[] +// This variable controls the position of tooltips, if the position is not specified to +// the Tooltip widget or *TextBox widget itself. It's an array of strings with the following values: +// +// * before: places tooltip to the left of the target node/widget, or to the right in +// the case of RTL scripts like Hebrew and Arabic +// * after: places tooltip to the right of the target node/widget, or to the left in +// the case of RTL scripts like Hebrew and Arabic +// * above: tooltip goes above target node +// * below: tooltip goes below target node +// +// The list is positions is tried, in order, until a position is found where the tooltip fits +// within the viewport. +// +// Be careful setting this parameter. A value of "above" may work fine until the user scrolls +// the screen so that there's no room above the target node. Nodes with drop downs, like +// DropDownButton or FilteringSelect, are especially problematic, in that you need to be sure +// that the drop down and tooltip don't overlap, even when the viewport is scrolled so that there +// is only room below (or above) the target node, but not both. +dijit.Tooltip.defaultPosition = ["after", "before"]; + +} + +if(!dojo._hasResource["dijit.form.ValidationTextBox"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.form.ValidationTextBox"] = true; +dojo.provide("dijit.form.ValidationTextBox"); + + + + + + + + +/*===== + dijit.form.ValidationTextBox.__Constraints = function(){ + // locale: String + // locale used for validation, picks up value from this widget's lang attribute + // _flags_: anything + // various flags passed to regExpGen function + this.locale = ""; + this._flags_ = ""; + } +=====*/ + +dojo.declare( + "dijit.form.ValidationTextBox", + dijit.form.TextBox, + { + // summary: + // Base class for textbox widgets with the ability to validate content of various types and provide user feedback. + // tags: + // protected + + templateString: dojo.cache("dijit.form", "templates/ValidationTextBox.html", "<div class=\"dijit dijitReset dijitInlineTable dijitLeft\"\n\tid=\"widget_${id}\" waiRole=\"presentation\"\n\t><div class='dijitReset dijitValidationContainer'\n\t\t><input class=\"dijitReset dijitInputField dijitValidationIcon dijitValidationInner\" value=\"Χ \" type=\"text\" tabIndex=\"-1\" readOnly waiRole=\"presentation\"\n\t/></div\n\t><div class=\"dijitReset dijitInputField dijitInputContainer\"\n\t\t><input class=\"dijitReset dijitInputInner\" dojoAttachPoint='textbox,focusNode' autocomplete=\"off\"\n\t\t\t${!nameAttrSetting} type='${type}'\n\t/></div\n></div>\n"), + baseClass: "dijitTextBox dijitValidationTextBox", + + // required: Boolean + // User is required to enter data into this field. + required: false, + + // promptMessage: String + // If defined, display this hint string immediately on focus to the textbox, if empty. + // Think of this like a tooltip that tells the user what to do, not an error message + // that tells the user what they've done wrong. + // + // Message disappears when user starts typing. + promptMessage: "", + + // invalidMessage: String + // The message to display if value is invalid. + // The translated string value is read from the message file by default. + // Set to "" to use the promptMessage instead. + invalidMessage: "$_unset_$", + + // missingMessage: String + // The message to display if value is empty and the field is required. + // The translated string value is read from the message file by default. + // Set to "" to use the invalidMessage instead. + missingMessage: "$_unset_$", + + // constraints: dijit.form.ValidationTextBox.__Constraints + // user-defined object needed to pass parameters to the validator functions + constraints: {}, + + // regExp: [extension protected] String + // regular expression string used to validate the input + // Do not specify both regExp and regExpGen + regExp: ".*", + + regExpGen: function(/*dijit.form.ValidationTextBox.__Constraints*/constraints){ + // summary: + // Overridable function used to generate regExp when dependent on constraints. + // Do not specify both regExp and regExpGen. + // tags: + // extension protected + return this.regExp; // String + }, + + // state: [readonly] String + // Shows current state (ie, validation result) of input (Normal, Warning, or Error) + state: "", + + // tooltipPosition: String[] + // See description of `dijit.Tooltip.defaultPosition` for details on this parameter. + tooltipPosition: [], + + _setValueAttr: function(){ + // summary: + // Hook so attr('value', ...) works. + this.inherited(arguments); + this.validate(this._focused); + }, + + validator: function(/*anything*/value, /*dijit.form.ValidationTextBox.__Constraints*/constraints){ + // summary: + // Overridable function used to validate the text input against the regular expression. + // tags: + // protected + return (new RegExp("^(?:" + this.regExpGen(constraints) + ")"+(this.required?"":"?")+"$")).test(value) && + (!this.required || !this._isEmpty(value)) && + (this._isEmpty(value) || this.parse(value, constraints) !== undefined); // Boolean + }, + + _isValidSubset: function(){ + // summary: + // Returns true if the value is either already valid or could be made valid by appending characters. + // This is used for validation while the user [may be] still typing. + return this.textbox.value.search(this._partialre) == 0; + }, + + isValid: function(/*Boolean*/ isFocused){ + // summary: + // Tests if value is valid. + // Can override with your own routine in a subclass. + // tags: + // protected + return this.validator(this.textbox.value, this.constraints); + }, + + _isEmpty: function(value){ + // summary: + // Checks for whitespace + return /^\s*$/.test(value); // Boolean + }, + + getErrorMessage: function(/*Boolean*/ isFocused){ + // summary: + // Return an error message to show if appropriate + // tags: + // protected + return (this.required && this._isEmpty(this.textbox.value)) ? this.missingMessage : this.invalidMessage; // String + }, + + getPromptMessage: function(/*Boolean*/ isFocused){ + // summary: + // Return a hint message to show when widget is first focused + // tags: + // protected + return this.promptMessage; // String + }, + + _maskValidSubsetError: true, + validate: function(/*Boolean*/ isFocused){ + // summary: + // Called by oninit, onblur, and onkeypress. + // description: + // Show missing or invalid messages if appropriate, and highlight textbox field. + // tags: + // protected + var message = ""; + var isValid = this.disabled || this.isValid(isFocused); + if(isValid){ this._maskValidSubsetError = true; } + var isEmpty = this._isEmpty(this.textbox.value); + var isValidSubset = !isValid && !isEmpty && isFocused && this._isValidSubset(); + this.state = ((isValid || ((!this._hasBeenBlurred || isFocused) && isEmpty) || isValidSubset) && this._maskValidSubsetError) ? "" : "Error"; + if(this.state == "Error"){ this._maskValidSubsetError = isFocused; } // we want the error to show up afer a blur and refocus + this._setStateClass(); + dijit.setWaiState(this.focusNode, "invalid", isValid ? "false" : "true"); + if(isFocused){ + if(this.state == "Error"){ + message = this.getErrorMessage(true); + }else{ + message = this.getPromptMessage(true); // show the prompt whever there's no error + } + this._maskValidSubsetError = true; // since we're focused, always mask warnings + } + this.displayMessage(message); + return isValid; + }, + + // _message: String + // Currently displayed message + _message: "", + + displayMessage: function(/*String*/ message){ + // summary: + // Overridable method to display validation errors/hints. + // By default uses a tooltip. + // tags: + // extension + if(this._message == message){ return; } + this._message = message; + dijit.hideTooltip(this.domNode); + if(message){ + dijit.showTooltip(message, this.domNode, this.tooltipPosition, !this.isLeftToRight()); + } + }, + + _refreshState: function(){ + // Overrides TextBox._refreshState() + this.validate(this._focused); + this.inherited(arguments); + }, + + //////////// INITIALIZATION METHODS /////////////////////////////////////// + + constructor: function(){ + this.constraints = {}; + }, + + _setConstraintsAttr: function(/* Object */ constraints){ + if(!constraints.locale && this.lang){ + constraints.locale = this.lang; + } + this.constraints = constraints; + this._computePartialRE(); + }, + + _computePartialRE: function(){ + var p = this.regExpGen(this.constraints); + this.regExp = p; + var partialre = ""; + // parse the regexp and produce a new regexp that matches valid subsets + // if the regexp is .* then there's no use in matching subsets since everything is valid + if(p != ".*"){ this.regExp.replace(/\\.|\[\]|\[.*?[^\\]{1}\]|\{.*?\}|\(\?[=:!]|./g, + function (re){ + switch(re.charAt(0)){ + case '{': + case '+': + case '?': + case '*': + case '^': + case '$': + case '|': + case '(': + partialre += re; + break; + case ")": + partialre += "|$)"; + break; + default: + partialre += "(?:"+re+"|$)"; + break; + } + } + );} + try{ // this is needed for now since the above regexp parsing needs more test verification + "".search(partialre); + }catch(e){ // should never be here unless the original RE is bad or the parsing is bad + partialre = this.regExp; + console.warn('RegExp error in ' + this.declaredClass + ': ' + this.regExp); + } // should never be here unless the original RE is bad or the parsing is bad + this._partialre = "^(?:" + partialre + ")$"; + }, + + postMixInProperties: function(){ + this.inherited(arguments); + this.messages = dojo.i18n.getLocalization("dijit.form", "validate", this.lang); + if(this.invalidMessage == "$_unset_$"){ this.invalidMessage = this.messages.invalidMessage; } + if(!this.invalidMessage){ this.invalidMessage = this.promptMessage; } + if(this.missingMessage == "$_unset_$"){ this.missingMessage = this.messages.missingMessage; } + if(!this.missingMessage){ this.missingMessage = this.invalidMessage; } + this._setConstraintsAttr(this.constraints); // this needs to happen now (and later) due to codependency on _set*Attr calls attachPoints + }, + + _setDisabledAttr: function(/*Boolean*/ value){ + this.inherited(arguments); // call FormValueWidget._setDisabledAttr() + this._refreshState(); + }, + + _setRequiredAttr: function(/*Boolean*/ value){ + this.required = value; + dijit.setWaiState(this.focusNode, "required", value); + this._refreshState(); + }, + + reset:function(){ + // Overrides dijit.form.TextBox.reset() by also + // hiding errors about partial matches + this._maskValidSubsetError = true; + this.inherited(arguments); + }, + + _onBlur: function(){ + this.displayMessage(''); + this.inherited(arguments); + } + } +); + +dojo.declare( + "dijit.form.MappedTextBox", + dijit.form.ValidationTextBox, + { + // summary: + // A dijit.form.ValidationTextBox subclass which provides a base class for widgets that have + // a visible formatted display value, and a serializable + // value in a hidden input field which is actually sent to the server. + // description: + // The visible display may + // be locale-dependent and interactive. The value sent to the server is stored in a hidden + // input field which uses the `name` attribute declared by the original widget. That value sent + // to the server is defined by the dijit.form.MappedTextBox.serialize method and is typically + // locale-neutral. + // tags: + // protected + + postMixInProperties: function(){ + this.inherited(arguments); + + // we want the name attribute to go to the hidden <input>, not the displayed <input>, + // so override _FormWidget.postMixInProperties() setting of nameAttrSetting + this.nameAttrSetting = ""; + }, + + serialize: function(/*anything*/val, /*Object?*/options){ + // summary: + // Overridable function used to convert the attr('value') result to a canonical + // (non-localized) string. For example, will print dates in ISO format, and + // numbers the same way as they are represented in javascript. + // tags: + // protected extension + return val.toString ? val.toString() : ""; // String + }, + + toString: function(){ + // summary: + // Returns widget as a printable string using the widget's value + // tags: + // protected + var val = this.filter(this.get('value')); // call filter in case value is nonstring and filter has been customized + return val != null ? (typeof val == "string" ? val : this.serialize(val, this.constraints)) : ""; // String + }, + + validate: function(){ + // Overrides `dijit.form.TextBox.validate` + this.valueNode.value = this.toString(); + return this.inherited(arguments); + }, + + buildRendering: function(){ + // Overrides `dijit._Templated.buildRendering` + + this.inherited(arguments); + + // Create a hidden <input> node with the serialized value used for submit + // (as opposed to the displayed value). + // Passing in name as markup rather than calling dojo.create() with an attrs argument + // to make dojo.query(input[name=...]) work on IE. (see #8660) + this.valueNode = dojo.place("<input type='hidden'" + (this.name ? " name='" + this.name + "'" : "") + ">", this.textbox, "after"); + }, + + reset:function(){ + // Overrides `dijit.form.ValidationTextBox.reset` to + // reset the hidden textbox value to '' + this.valueNode.value = ''; + this.inherited(arguments); + } + } +); + +/*===== + dijit.form.RangeBoundTextBox.__Constraints = function(){ + // min: Number + // Minimum signed value. Default is -Infinity + // max: Number + // Maximum signed value. Default is +Infinity + this.min = min; + this.max = max; + } +=====*/ + +dojo.declare( + "dijit.form.RangeBoundTextBox", + dijit.form.MappedTextBox, + { + // summary: + // Base class for textbox form widgets which defines a range of valid values. + + // rangeMessage: String + // The message to display if value is out-of-range + rangeMessage: "", + + /*===== + // constraints: dijit.form.RangeBoundTextBox.__Constraints + constraints: {}, + ======*/ + + rangeCheck: function(/*Number*/ primitive, /*dijit.form.RangeBoundTextBox.__Constraints*/ constraints){ + // summary: + // Overridable function used to validate the range of the numeric input value. + // tags: + // protected + return ("min" in constraints? (this.compare(primitive,constraints.min) >= 0) : true) && + ("max" in constraints? (this.compare(primitive,constraints.max) <= 0) : true); // Boolean + }, + + isInRange: function(/*Boolean*/ isFocused){ + // summary: + // Tests if the value is in the min/max range specified in constraints + // tags: + // protected + return this.rangeCheck(this.get('value'), this.constraints); + }, + + _isDefinitelyOutOfRange: function(){ + // summary: + // Returns true if the value is out of range and will remain + // out of range even if the user types more characters + var val = this.get('value'); + var isTooLittle = false; + var isTooMuch = false; + if("min" in this.constraints){ + var min = this.constraints.min; + min = this.compare(val, ((typeof min == "number") && min >= 0 && val !=0) ? 0 : min); + isTooLittle = (typeof min == "number") && min < 0; + } + if("max" in this.constraints){ + var max = this.constraints.max; + max = this.compare(val, ((typeof max != "number") || max > 0) ? max : 0); + isTooMuch = (typeof max == "number") && max > 0; + } + return isTooLittle || isTooMuch; + }, + + _isValidSubset: function(){ + // summary: + // Overrides `dijit.form.ValidationTextBox._isValidSubset`. + // Returns true if the input is syntactically valid, and either within + // range or could be made in range by more typing. + return this.inherited(arguments) && !this._isDefinitelyOutOfRange(); + }, + + isValid: function(/*Boolean*/ isFocused){ + // Overrides dijit.form.ValidationTextBox.isValid to check that the value is also in range. + return this.inherited(arguments) && + ((this._isEmpty(this.textbox.value) && !this.required) || this.isInRange(isFocused)); // Boolean + }, + + getErrorMessage: function(/*Boolean*/ isFocused){ + // Overrides dijit.form.ValidationTextBox.getErrorMessage to print "out of range" message if appropriate + var v = this.get('value'); + if(v !== null && v !== '' && v !== undefined && (typeof v != "number" || !isNaN(v)) && !this.isInRange(isFocused)){ // don't check isInRange w/o a real value + return this.rangeMessage; // String + } + return this.inherited(arguments); + }, + + postMixInProperties: function(){ + this.inherited(arguments); + if(!this.rangeMessage){ + this.messages = dojo.i18n.getLocalization("dijit.form", "validate", this.lang); + this.rangeMessage = this.messages.rangeMessage; + } + }, + + _setConstraintsAttr: function(/* Object */ constraints){ + this.inherited(arguments); + if(this.focusNode){ // not set when called from postMixInProperties + if(this.constraints.min !== undefined){ + dijit.setWaiState(this.focusNode, "valuemin", this.constraints.min); + }else{ + dijit.removeWaiState(this.focusNode, "valuemin"); + } + if(this.constraints.max !== undefined){ + dijit.setWaiState(this.focusNode, "valuemax", this.constraints.max); + }else{ + dijit.removeWaiState(this.focusNode, "valuemax"); + } + } + }, + + _setValueAttr: function(/*Number*/ value, /*Boolean?*/ priorityChange){ + // summary: + // Hook so attr('value', ...) works. + + dijit.setWaiState(this.focusNode, "valuenow", value); + this.inherited(arguments); + } + } +); + +} + +if(!dojo._hasResource["dijit.form.ComboBox"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.form.ComboBox"] = true; +dojo.provide("dijit.form.ComboBox"); + + + + + + + + + + + + +dojo.declare( + "dijit.form.ComboBoxMixin", + null, + { + // summary: + // Implements the base functionality for `dijit.form.ComboBox`/`dijit.form.FilteringSelect` + // description: + // All widgets that mix in dijit.form.ComboBoxMixin must extend `dijit.form._FormValueWidget`. + // tags: + // protected + + // item: Object + // This is the item returned by the dojo.data.store implementation that + // provides the data for this ComboBox, it's the currently selected item. + item: null, + + // pageSize: Integer + // Argument to data provider. + // Specifies number of search results per page (before hitting "next" button) + pageSize: Infinity, + + // store: Object + // Reference to data provider object used by this ComboBox + store: null, + + // fetchProperties: Object + // Mixin to the dojo.data store's fetch. + // For example, to set the sort order of the ComboBox menu, pass: + // | { sort: {attribute:"name",descending: true} } + // To override the default queryOptions so that deep=false, do: + // | { queryOptions: {ignoreCase: true, deep: false} } + fetchProperties:{}, + + // query: Object + // A query that can be passed to 'store' to initially filter the items, + // before doing further filtering based on `searchAttr` and the key. + // Any reference to the `searchAttr` is ignored. + query: {}, + + // autoComplete: Boolean + // If user types in a partial string, and then tab out of the `<input>` box, + // automatically copy the first entry displayed in the drop down list to + // the `<input>` field + autoComplete: true, + + // highlightMatch: String + // One of: "first", "all" or "none". + // + // If the ComboBox/FilteringSelect opens with the search results and the searched + // string can be found, it will be highlighted. If set to "all" + // then will probably want to change `queryExpr` parameter to '*${0}*' + // + // Highlighting is only performed when `labelType` is "text", so as to not + // interfere with any HTML markup an HTML label might contain. + highlightMatch: "first", + + // searchDelay: Integer + // Delay in milliseconds between when user types something and we start + // searching based on that value + searchDelay: 100, + + // searchAttr: String + // Search for items in the data store where this attribute (in the item) + // matches what the user typed + searchAttr: "name", + + // labelAttr: String? + // The entries in the drop down list come from this attribute in the + // dojo.data items. + // If not specified, the searchAttr attribute is used instead. + labelAttr: "", + + // labelType: String + // Specifies how to interpret the labelAttr in the data store items. + // Can be "html" or "text". + labelType: "text", + + // queryExpr: String + // This specifies what query ComboBox/FilteringSelect sends to the data store, + // based on what the user has typed. Changing this expression will modify + // whether the drop down shows only exact matches, a "starting with" match, + // etc. Use it in conjunction with highlightMatch. + // dojo.data query expression pattern. + // `${0}` will be substituted for the user text. + // `*` is used for wildcards. + // `${0}*` means "starts with", `*${0}*` means "contains", `${0}` means "is" + queryExpr: "${0}*", + + // ignoreCase: Boolean + // Set true if the ComboBox/FilteringSelect should ignore case when matching possible items + ignoreCase: true, + + // hasDownArrow: [const] Boolean + // Set this textbox to have a down arrow button, to display the drop down list. + // Defaults to true. + hasDownArrow: true, + + templateString: dojo.cache("dijit.form", "templates/ComboBox.html", "<div class=\"dijit dijitReset dijitInlineTable dijitLeft\"\n\tid=\"widget_${id}\"\n\tdojoAttachPoint=\"comboNode\" waiRole=\"combobox\"\n\t><div class='dijitReset dijitRight dijitButtonNode dijitArrowButton dijitDownArrowButton dijitArrowButtonContainer'\n\t\tdojoAttachPoint=\"downArrowNode\" waiRole=\"presentation\"\n\t\tdojoAttachEvent=\"onmousedown:_onArrowMouseDown\"\n\t\t><input class=\"dijitReset dijitInputField dijitArrowButtonInner\" value=\"▼ \" type=\"text\" tabIndex=\"-1\" readOnly waiRole=\"presentation\"\n\t\t\t${_buttonInputDisabled}\n\t/></div\n\t><div class='dijitReset dijitValidationContainer'\n\t\t><input class=\"dijitReset dijitInputField dijitValidationIcon dijitValidationInner\" value=\"Χ \" type=\"text\" tabIndex=\"-1\" readOnly waiRole=\"presentation\"\n\t/></div\n\t><div class=\"dijitReset dijitInputField dijitInputContainer\"\n\t\t><input class='dijitReset dijitInputInner' ${!nameAttrSetting} type=\"text\" autocomplete=\"off\"\n\t\t\tdojoAttachEvent=\"onkeypress:_onKeyPress,compositionend\"\n\t\t\tdojoAttachPoint=\"textbox,focusNode\" waiRole=\"textbox\" waiState=\"haspopup-true,autocomplete-list\"\n\t/></div\n></div>\n"), + + baseClass: "dijitTextBox dijitComboBox", + + // Set classes like dijitDownArrowButtonHover depending on + // mouse action over button node + cssStateNodes: { + "downArrowNode": "dijitDownArrowButton" + }, + + _getCaretPos: function(/*DomNode*/ element){ + // khtml 3.5.2 has selection* methods as does webkit nightlies from 2005-06-22 + var pos = 0; + if(typeof(element.selectionStart) == "number"){ + // FIXME: this is totally borked on Moz < 1.3. Any recourse? + pos = element.selectionStart; + }else if(dojo.isIE){ + // in the case of a mouse click in a popup being handled, + // then the dojo.doc.selection is not the textarea, but the popup + // var r = dojo.doc.selection.createRange(); + // hack to get IE 6 to play nice. What a POS browser. + var tr = dojo.doc.selection.createRange().duplicate(); + var ntr = element.createTextRange(); + tr.move("character",0); + ntr.move("character",0); + try{ + // If control doesnt have focus, you get an exception. + // Seems to happen on reverse-tab, but can also happen on tab (seems to be a race condition - only happens sometimes). + // There appears to be no workaround for this - googled for quite a while. + ntr.setEndPoint("EndToEnd", tr); + pos = String(ntr.text).replace(/\r/g,"").length; + }catch(e){ + // If focus has shifted, 0 is fine for caret pos. + } + } + return pos; + }, + + _setCaretPos: function(/*DomNode*/ element, /*Number*/ location){ + location = parseInt(location); + dijit.selectInputText(element, location, location); + }, + + _setDisabledAttr: function(/*Boolean*/ value){ + // Additional code to set disabled state of ComboBox node. + // Overrides _FormValueWidget._setDisabledAttr() or ValidationTextBox._setDisabledAttr(). + this.inherited(arguments); + dijit.setWaiState(this.comboNode, "disabled", value); + }, + + _abortQuery: function(){ + // stop in-progress query + if(this.searchTimer){ + clearTimeout(this.searchTimer); + this.searchTimer = null; + } + if(this._fetchHandle){ + if(this._fetchHandle.abort){ this._fetchHandle.abort(); } + this._fetchHandle = null; + } + }, + + _onInput: function(/*Event*/ evt){ + // summary: + // Handles paste events + if(!this.searchTimer && (evt.type == 'paste'/*IE|WebKit*/ || evt.type == 'input'/*Firefox*/) && this._lastInput != this.textbox.value){ + this.searchTimer = setTimeout(dojo.hitch(this, function(){ + this._onKeyPress({charOrCode: 229}); // fake IME key to cause a search + }), 100); // long delay that will probably be preempted by keyboard input + } + this.inherited(arguments); + }, + + _onKeyPress: function(/*Event*/ evt){ + // summary: + // Handles keyboard events + var key = evt.charOrCode; + // except for cutting/pasting case - ctrl + x/v + if(evt.altKey || ((evt.ctrlKey || evt.metaKey) && (key != 'x' && key != 'v')) || key == dojo.keys.SHIFT){ + return; // throw out weird key combinations and spurious events + } + var doSearch = false; + var searchFunction = "_startSearchFromInput"; + var pw = this._popupWidget; + var dk = dojo.keys; + var highlighted = null; + this._prev_key_backspace = false; + this._abortQuery(); + if(this._isShowingNow){ + pw.handleKey(key); + highlighted = pw.getHighlightedOption(); + } + switch(key){ + case dk.PAGE_DOWN: + case dk.DOWN_ARROW: + case dk.PAGE_UP: + case dk.UP_ARROW: + if(!this._isShowingNow){ + doSearch = true; + searchFunction = "_startSearchAll"; + }else{ + this._announceOption(highlighted); + } + dojo.stopEvent(evt); + break; + + case dk.ENTER: + // prevent submitting form if user presses enter. Also + // prevent accepting the value if either Next or Previous + // are selected + if(highlighted){ + // only stop event on prev/next + if(highlighted == pw.nextButton){ + this._nextSearch(1); + dojo.stopEvent(evt); + break; + }else if(highlighted == pw.previousButton){ + this._nextSearch(-1); + dojo.stopEvent(evt); + break; + } + }else{ + // Update 'value' (ex: KY) according to currently displayed text + this._setBlurValue(); // set value if needed + this._setCaretPos(this.focusNode, this.focusNode.value.length); // move cursor to end and cancel highlighting + } + // default case: + // prevent submit, but allow event to bubble + evt.preventDefault(); + // fall through + + case dk.TAB: + var newvalue = this.get('displayedValue'); + // if the user had More Choices selected fall into the + // _onBlur handler + if(pw && ( + newvalue == pw._messages["previousMessage"] || + newvalue == pw._messages["nextMessage"]) + ){ + break; + } + if(highlighted){ + this._selectOption(); + } + if(this._isShowingNow){ + this._lastQuery = null; // in case results come back later + this._hideResultList(); + } + break; + + case ' ': + if(highlighted){ + dojo.stopEvent(evt); + this._selectOption(); + this._hideResultList(); + }else{ + doSearch = true; + } + break; + + case dk.ESCAPE: + if(this._isShowingNow){ + dojo.stopEvent(evt); + this._hideResultList(); + } + break; + + case dk.DELETE: + case dk.BACKSPACE: + this._prev_key_backspace = true; + doSearch = true; + break; + + default: + // Non char keys (F1-F12 etc..) shouldn't open list. + // Ascii characters and IME input (Chinese, Japanese etc.) should. + // On IE and safari, IME input produces keycode == 229, and we simulate + // it on firefox by attaching to compositionend event (see compositionend method) + doSearch = typeof key == 'string' || key == 229; + } + if(doSearch){ + // need to wait a tad before start search so that the event + // bubbles through DOM and we have value visible + this.item = undefined; // undefined means item needs to be set + this.searchTimer = setTimeout(dojo.hitch(this, searchFunction),1); + } + }, + + _autoCompleteText: function(/*String*/ text){ + // summary: + // Fill in the textbox with the first item from the drop down + // list, and highlight the characters that were + // auto-completed. For example, if user typed "CA" and the + // drop down list appeared, the textbox would be changed to + // "California" and "ifornia" would be highlighted. + + var fn = this.focusNode; + + // IE7: clear selection so next highlight works all the time + dijit.selectInputText(fn, fn.value.length); + // does text autoComplete the value in the textbox? + var caseFilter = this.ignoreCase? 'toLowerCase' : 'substr'; + if(text[caseFilter](0).indexOf(this.focusNode.value[caseFilter](0)) == 0){ + var cpos = this._getCaretPos(fn); + // only try to extend if we added the last character at the end of the input + if((cpos+1) > fn.value.length){ + // only add to input node as we would overwrite Capitalisation of chars + // actually, that is ok + fn.value = text;//.substr(cpos); + // visually highlight the autocompleted characters + dijit.selectInputText(fn, cpos); + } + }else{ + // text does not autoComplete; replace the whole value and highlight + fn.value = text; + dijit.selectInputText(fn); + } + }, + + _openResultList: function(/*Object*/ results, /*Object*/ dataObject){ + this._fetchHandle = null; + if( this.disabled || + this.readOnly || + (dataObject.query[this.searchAttr] != this._lastQuery) + ){ + return; + } + this._popupWidget.clearResultList(); + if(!results.length && !this._maxOptions){ // this condition needs to match !this._isvalid set in FilteringSelect::_openResultList + this._hideResultList(); + return; + } + + + // Fill in the textbox with the first item from the drop down list, + // and highlight the characters that were auto-completed. For + // example, if user typed "CA" and the drop down list appeared, the + // textbox would be changed to "California" and "ifornia" would be + // highlighted. + + dataObject._maxOptions = this._maxOptions; + var nodes = this._popupWidget.createOptions( + results, + dataObject, + dojo.hitch(this, "_getMenuLabelFromItem") + ); + + // show our list (only if we have content, else nothing) + this._showResultList(); + + // #4091: + // tell the screen reader that the paging callback finished by + // shouting the next choice + if(dataObject.direction){ + if(1 == dataObject.direction){ + this._popupWidget.highlightFirstOption(); + }else if(-1 == dataObject.direction){ + this._popupWidget.highlightLastOption(); + } + this._announceOption(this._popupWidget.getHighlightedOption()); + }else if(this.autoComplete && !this._prev_key_backspace /*&& !dataObject.direction*/ + // when the user clicks the arrow button to show the full list, + // startSearch looks for "*". + // it does not make sense to autocomplete + // if they are just previewing the options available. + && !/^[*]+$/.test(dataObject.query[this.searchAttr])){ + this._announceOption(nodes[1]); // 1st real item + } + }, + + _showResultList: function(){ + this._hideResultList(); + // hide the tooltip + this.displayMessage(""); + + // Position the list and if it's too big to fit on the screen then + // size it to the maximum possible height + // Our dear friend IE doesnt take max-height so we need to + // calculate that on our own every time + + // TODO: want to redo this, see + // http://trac.dojotoolkit.org/ticket/3272 + // and + // http://trac.dojotoolkit.org/ticket/4108 + + + // natural size of the list has changed, so erase old + // width/height settings, which were hardcoded in a previous + // call to this function (via dojo.marginBox() call) + dojo.style(this._popupWidget.domNode, {width: "", height: ""}); + + var best = this.open(); + // #3212: + // only set auto scroll bars if necessary prevents issues with + // scroll bars appearing when they shouldn't when node is made + // wider (fractional pixels cause this) + var popupbox = dojo.marginBox(this._popupWidget.domNode); + this._popupWidget.domNode.style.overflow = + ((best.h == popupbox.h) && (best.w == popupbox.w)) ? "hidden" : "auto"; + // #4134: + // borrow TextArea scrollbar test so content isn't covered by + // scrollbar and horizontal scrollbar doesn't appear + var newwidth = best.w; + if(best.h < this._popupWidget.domNode.scrollHeight){ + newwidth += 16; + } + dojo.marginBox(this._popupWidget.domNode, { + h: best.h, + w: Math.max(newwidth, this.domNode.offsetWidth) + }); + + // If we increased the width of drop down to match the width of ComboBox.domNode, + // then need to reposition the drop down (wrapper) so (all of) the drop down still + // appears underneath the ComboBox.domNode + if(newwidth < this.domNode.offsetWidth){ + this._popupWidget.domNode.parentNode.style.left = dojo.position(this.domNode, true).x + "px"; + } + + dijit.setWaiState(this.comboNode, "expanded", "true"); + }, + + _hideResultList: function(){ + this._abortQuery(); + if(this._isShowingNow){ + dijit.popup.close(this._popupWidget); + this._isShowingNow=false; + dijit.setWaiState(this.comboNode, "expanded", "false"); + dijit.removeWaiState(this.focusNode,"activedescendant"); + } + }, + + _setBlurValue: function(){ + // if the user clicks away from the textbox OR tabs away, set the + // value to the textbox value + // #4617: + // if value is now more choices or previous choices, revert + // the value + var newvalue = this.get('displayedValue'); + var pw = this._popupWidget; + if(pw && ( + newvalue == pw._messages["previousMessage"] || + newvalue == pw._messages["nextMessage"] + ) + ){ + this._setValueAttr(this._lastValueReported, true); + }else if(typeof this.item == "undefined"){ + // Update 'value' (ex: KY) according to currently displayed text + this.item = null; + this.set('displayedValue', newvalue); + }else{ + if(this.value != this._lastValueReported){ + dijit.form._FormValueWidget.prototype._setValueAttr.call(this, this.value, true); + } + this._refreshState(); + } + }, + + _onBlur: function(){ + // summary: + // Called magically when focus has shifted away from this widget and it's drop down + this._hideResultList(); + this.inherited(arguments); + }, + + _setItemAttr: function(/*item*/ item, /*Boolean?*/ priorityChange, /*String?*/ displayedValue){ + // summary: + // Set the displayed valued in the input box, and the hidden value + // that gets submitted, based on a dojo.data store item. + // description: + // Users shouldn't call this function; they should be calling + // attr('item', value) + // tags: + // private + if(!displayedValue){ displayedValue = this.labelFunc(item, this.store); } + this.value = this._getValueField() != this.searchAttr? this.store.getIdentity(item) : displayedValue; + this.item = item; + dijit.form.ComboBox.superclass._setValueAttr.call(this, this.value, priorityChange, displayedValue); + }, + + _announceOption: function(/*Node*/ node){ + // summary: + // a11y code that puts the highlighted option in the textbox. + // This way screen readers will know what is happening in the + // menu. + + if(!node){ + return; + } + // pull the text value from the item attached to the DOM node + var newValue; + if(node == this._popupWidget.nextButton || + node == this._popupWidget.previousButton){ + newValue = node.innerHTML; + this.item = undefined; + this.value = ''; + }else{ + newValue = this.labelFunc(node.item, this.store); + this.set('item', node.item, false, newValue); + } + // get the text that the user manually entered (cut off autocompleted text) + this.focusNode.value = this.focusNode.value.substring(0, this._lastInput.length); + // set up ARIA activedescendant + dijit.setWaiState(this.focusNode, "activedescendant", dojo.attr(node, "id")); + // autocomplete the rest of the option to announce change + this._autoCompleteText(newValue); + }, + + _selectOption: function(/*Event*/ evt){ + // summary: + // Menu callback function, called when an item in the menu is selected. + if(evt){ + this._announceOption(evt.target); + } + this._hideResultList(); + this._setCaretPos(this.focusNode, this.focusNode.value.length); + dijit.form._FormValueWidget.prototype._setValueAttr.call(this, this.value, true); // set this.value and fire onChange + }, + + _onArrowMouseDown: function(evt){ + // summary: + // Callback when arrow is clicked + if(this.disabled || this.readOnly){ + return; + } + dojo.stopEvent(evt); + this.focus(); + if(this._isShowingNow){ + this._hideResultList(); + }else{ + // forces full population of results, if they click + // on the arrow it means they want to see more options + this._startSearchAll(); + } + }, + + _startSearchAll: function(){ + this._startSearch(''); + }, + + _startSearchFromInput: function(){ + this._startSearch(this.focusNode.value.replace(/([\\\*\?])/g, "\\$1")); + }, + + _getQueryString: function(/*String*/ text){ + return dojo.string.substitute(this.queryExpr, [text]); + }, + + _startSearch: function(/*String*/ key){ + if(!this._popupWidget){ + var popupId = this.id + "_popup"; + this._popupWidget = new dijit.form._ComboBoxMenu({ + onChange: dojo.hitch(this, this._selectOption), + id: popupId, + dir: this.dir + }); + dijit.removeWaiState(this.focusNode,"activedescendant"); + dijit.setWaiState(this.textbox,"owns",popupId); // associate popup with textbox + } + // create a new query to prevent accidentally querying for a hidden + // value from FilteringSelect's keyField + var query = dojo.clone(this.query); // #5970 + this._lastInput = key; // Store exactly what was entered by the user. + this._lastQuery = query[this.searchAttr] = this._getQueryString(key); + // #5970: set _lastQuery, *then* start the timeout + // otherwise, if the user types and the last query returns before the timeout, + // _lastQuery won't be set and their input gets rewritten + this.searchTimer=setTimeout(dojo.hitch(this, function(query, _this){ + this.searchTimer = null; + var fetch = { + queryOptions: { + ignoreCase: this.ignoreCase, + deep: true + }, + query: query, + onBegin: dojo.hitch(this, "_setMaxOptions"), + onComplete: dojo.hitch(this, "_openResultList"), + onError: function(errText){ + _this._fetchHandle = null; + console.error('dijit.form.ComboBox: ' + errText); + dojo.hitch(_this, "_hideResultList")(); + }, + start: 0, + count: this.pageSize + }; + dojo.mixin(fetch, _this.fetchProperties); + this._fetchHandle = _this.store.fetch(fetch); + + var nextSearch = function(dataObject, direction){ + dataObject.start += dataObject.count*direction; + // #4091: + // tell callback the direction of the paging so the screen + // reader knows which menu option to shout + dataObject.direction = direction; + this._fetchHandle = this.store.fetch(dataObject); + }; + this._nextSearch = this._popupWidget.onPage = dojo.hitch(this, nextSearch, this._fetchHandle); + }, query, this), this.searchDelay); + }, + + _setMaxOptions: function(size, request){ + this._maxOptions = size; + }, + + _getValueField: function(){ + // summmary: + // Helper for postMixInProperties() to set this.value based on data inlined into the markup. + // Returns the attribute name in the item (in dijit.form._ComboBoxDataStore) to use as the value. + return this.searchAttr; + }, + + /////////////// Event handlers ///////////////////// + + // FIXME: For 2.0, rename to "_compositionEnd" + compositionend: function(/*Event*/ evt){ + // summary: + // When inputting characters using an input method, such as + // Asian languages, it will generate this event instead of + // onKeyDown event. + // Note: this event is only triggered in FF (not in IE/safari) + // tags: + // private + + // 229 is the code produced by IE and safari while pressing keys during + // IME input mode + this._onKeyPress({charOrCode: 229}); + }, + + //////////// INITIALIZATION METHODS /////////////////////////////////////// + + constructor: function(){ + this.query={}; + this.fetchProperties={}; + }, + + postMixInProperties: function(){ + if(!this.store){ + var srcNodeRef = this.srcNodeRef; + + // if user didn't specify store, then assume there are option tags + this.store = new dijit.form._ComboBoxDataStore(srcNodeRef); + + // if there is no value set and there is an option list, set + // the value to the first value to be consistent with native + // Select + + // Firefox and Safari set value + // IE6 and Opera set selectedIndex, which is automatically set + // by the selected attribute of an option tag + // IE6 does not set value, Opera sets value = selectedIndex + if(!("value" in this.params)){ + var item = this.store.fetchSelectedItem(); + if(item){ + var valueField = this._getValueField(); + this.value = valueField != this.searchAttr? this.store.getValue(item, valueField) : this.labelFunc(item, this.store); + } + } + } + this.inherited(arguments); + }, + + postCreate: function(){ + // summary: + // Subclasses must call this method from their postCreate() methods + // tags: + // protected + + if(!this.hasDownArrow){ + this.downArrowNode.style.display = "none"; + } + + // find any associated label element and add to ComboBox node. + var label=dojo.query('label[for="'+this.id+'"]'); + if(label.length){ + label[0].id = (this.id+"_label"); + var cn=this.comboNode; + dijit.setWaiState(cn, "labelledby", label[0].id); + + } + this.inherited(arguments); + }, + + uninitialize: function(){ + if(this._popupWidget && !this._popupWidget._destroyed){ + this._hideResultList(); + this._popupWidget.destroy(); + } + this.inherited(arguments); + }, + + _getMenuLabelFromItem: function(/*Item*/ item){ + var label = this.labelAttr? this.store.getValue(item, this.labelAttr) : this.labelFunc(item, this.store); + var labelType = this.labelType; + // If labelType is not "text" we don't want to screw any markup ot whatever. + if(this.highlightMatch != "none" && this.labelType == "text" && this._lastInput){ + label = this.doHighlight(label, this._escapeHtml(this._lastInput)); + labelType = "html"; + } + return {html: labelType == "html", label: label}; + }, + + doHighlight: function(/*String*/label, /*String*/find){ + // summary: + // Highlights the string entered by the user in the menu. By default this + // highlights the first occurence found. Override this method + // to implement your custom highlighing. + // tags: + // protected + + // Add greedy when this.highlightMatch == "all" + var modifiers = "i"+(this.highlightMatch == "all"?"g":""); + var escapedLabel = this._escapeHtml(label); + find = dojo.regexp.escapeString(find); // escape regexp special chars + var ret = escapedLabel.replace(new RegExp("(^|\\s)("+ find +")", modifiers), + '$1<span class="dijitComboBoxHighlightMatch">$2</span>'); + return ret;// returns String, (almost) valid HTML (entities encoded) + }, + + _escapeHtml: function(/*string*/str){ + // TODO Should become dojo.html.entities(), when exists use instead + // summary: + // Adds escape sequences for special characters in XML: &<>"' + str = String(str).replace(/&/gm, "&").replace(/</gm, "<") + .replace(/>/gm, ">").replace(/"/gm, """); + return str; // string + }, + + open: function(){ + // summary: + // Opens the drop down menu. TODO: rename to _open. + // tags: + // private + this._isShowingNow=true; + return dijit.popup.open({ + popup: this._popupWidget, + around: this.domNode, + parent: this + }); + }, + + reset: function(){ + // Overrides the _FormWidget.reset(). + // Additionally reset the .item (to clean up). + this.item = null; + this.inherited(arguments); + }, + + labelFunc: function(/*item*/ item, /*dojo.data.store*/ store){ + // summary: + // Computes the label to display based on the dojo.data store item. + // returns: + // The label that the ComboBox should display + // tags: + // private + + // Use toString() because XMLStore returns an XMLItem whereas this + // method is expected to return a String (#9354) + return store.getValue(item, this.searchAttr).toString(); // String + } + } +); + +dojo.declare( + "dijit.form._ComboBoxMenu", + [dijit._Widget, dijit._Templated, dijit._CssStateMixin], + { + // summary: + // Focus-less menu for internal use in `dijit.form.ComboBox` + // tags: + // private + + templateString: "<ul class='dijitReset dijitMenu' dojoAttachEvent='onmousedown:_onMouseDown,onmouseup:_onMouseUp,onmouseover:_onMouseOver,onmouseout:_onMouseOut' tabIndex='-1' style='overflow: \"auto\"; overflow-x: \"hidden\";'>" + +"<li class='dijitMenuItem dijitMenuPreviousButton' dojoAttachPoint='previousButton' waiRole='option'></li>" + +"<li class='dijitMenuItem dijitMenuNextButton' dojoAttachPoint='nextButton' waiRole='option'></li>" + +"</ul>", + + // _messages: Object + // Holds "next" and "previous" text for paging buttons on drop down + _messages: null, + + baseClass: "dijitComboBoxMenu", + + postMixInProperties: function(){ + this._messages = dojo.i18n.getLocalization("dijit.form", "ComboBox", this.lang); + this.inherited(arguments); + }, + + _setValueAttr: function(/*Object*/ value){ + this.value = value; + this.onChange(value); + }, + + // stubs + onChange: function(/*Object*/ value){ + // summary: + // Notifies ComboBox/FilteringSelect that user clicked an option in the drop down menu. + // Probably should be called onSelect. + // tags: + // callback + }, + onPage: function(/*Number*/ direction){ + // summary: + // Notifies ComboBox/FilteringSelect that user clicked to advance to next/previous page. + // tags: + // callback + }, + + postCreate: function(){ + // fill in template with i18n messages + this.previousButton.innerHTML = this._messages["previousMessage"]; + this.nextButton.innerHTML = this._messages["nextMessage"]; + this.inherited(arguments); + }, + + onClose: function(){ + // summary: + // Callback from dijit.popup code to this widget, notifying it that it closed + // tags: + // private + this._blurOptionNode(); + }, + + _createOption: function(/*Object*/ item, labelFunc){ + // summary: + // Creates an option to appear on the popup menu subclassed by + // `dijit.form.FilteringSelect`. + + var labelObject = labelFunc(item); + var menuitem = dojo.doc.createElement("li"); + dijit.setWaiRole(menuitem, "option"); + if(labelObject.html){ + menuitem.innerHTML = labelObject.label; + }else{ + menuitem.appendChild( + dojo.doc.createTextNode(labelObject.label) + ); + } + // #3250: in blank options, assign a normal height + if(menuitem.innerHTML == ""){ + menuitem.innerHTML = " "; + } + menuitem.item=item; + return menuitem; + }, + + createOptions: function(results, dataObject, labelFunc){ + // summary: + // Fills in the items in the drop down list + // results: + // Array of dojo.data items + // dataObject: + // dojo.data store + // labelFunc: + // Function to produce a label in the drop down list from a dojo.data item + + //this._dataObject=dataObject; + //this._dataObject.onComplete=dojo.hitch(comboBox, comboBox._openResultList); + // display "Previous . . ." button + this.previousButton.style.display = (dataObject.start == 0) ? "none" : ""; + dojo.attr(this.previousButton, "id", this.id + "_prev"); + // create options using _createOption function defined by parent + // ComboBox (or FilteringSelect) class + // #2309: + // iterate over cache nondestructively + dojo.forEach(results, function(item, i){ + var menuitem = this._createOption(item, labelFunc); + menuitem.className = "dijitReset dijitMenuItem" + + (this.isLeftToRight() ? "" : " dijitMenuItemRtl"); + dojo.attr(menuitem, "id", this.id + i); + this.domNode.insertBefore(menuitem, this.nextButton); + }, this); + // display "Next . . ." button + var displayMore = false; + //Try to determine if we should show 'more'... + if(dataObject._maxOptions && dataObject._maxOptions != -1){ + if((dataObject.start + dataObject.count) < dataObject._maxOptions){ + displayMore = true; + }else if((dataObject.start + dataObject.count) > dataObject._maxOptions && dataObject.count == results.length){ + //Weird return from a datastore, where a start + count > maxOptions + // implies maxOptions isn't really valid and we have to go into faking it. + //And more or less assume more if count == results.length + displayMore = true; + } + }else if(dataObject.count == results.length){ + //Don't know the size, so we do the best we can based off count alone. + //So, if we have an exact match to count, assume more. + displayMore = true; + } + + this.nextButton.style.display = displayMore ? "" : "none"; + dojo.attr(this.nextButton,"id", this.id + "_next"); + return this.domNode.childNodes; + }, + + clearResultList: function(){ + // summary: + // Clears the entries in the drop down list, but of course keeps the previous and next buttons. + while(this.domNode.childNodes.length>2){ + this.domNode.removeChild(this.domNode.childNodes[this.domNode.childNodes.length-2]); + } + }, + + _onMouseDown: function(/*Event*/ evt){ + dojo.stopEvent(evt); + }, + + _onMouseUp: function(/*Event*/ evt){ + if(evt.target === this.domNode || !this._highlighted_option){ + return; + }else if(evt.target == this.previousButton){ + this.onPage(-1); + }else if(evt.target == this.nextButton){ + this.onPage(1); + }else{ + var tgt = evt.target; + // while the clicked node is inside the div + while(!tgt.item){ + // recurse to the top + tgt = tgt.parentNode; + } + this._setValueAttr({ target: tgt }, true); + } + }, + + _onMouseOver: function(/*Event*/ evt){ + if(evt.target === this.domNode){ return; } + var tgt = evt.target; + if(!(tgt == this.previousButton || tgt == this.nextButton)){ + // while the clicked node is inside the div + while(!tgt.item){ + // recurse to the top + tgt = tgt.parentNode; + } + } + this._focusOptionNode(tgt); + }, + + _onMouseOut: function(/*Event*/ evt){ + if(evt.target === this.domNode){ return; } + this._blurOptionNode(); + }, + + _focusOptionNode: function(/*DomNode*/ node){ + // summary: + // Does the actual highlight. + if(this._highlighted_option != node){ + this._blurOptionNode(); + this._highlighted_option = node; + dojo.addClass(this._highlighted_option, "dijitMenuItemSelected"); + } + }, + + _blurOptionNode: function(){ + // summary: + // Removes highlight on highlighted option. + if(this._highlighted_option){ + dojo.removeClass(this._highlighted_option, "dijitMenuItemSelected"); + this._highlighted_option = null; + } + }, + + _highlightNextOption: function(){ + // summary: + // Highlight the item just below the current selection. + // If nothing selected, highlight first option. + + // because each press of a button clears the menu, + // the highlighted option sometimes becomes detached from the menu! + // test to see if the option has a parent to see if this is the case. + if(!this.getHighlightedOption()){ + var fc = this.domNode.firstChild; + this._focusOptionNode(fc.style.display == "none" ? fc.nextSibling : fc); + }else{ + var ns = this._highlighted_option.nextSibling; + if(ns && ns.style.display != "none"){ + this._focusOptionNode(ns); + }else{ + this.highlightFirstOption(); + } + } + // scrollIntoView is called outside of _focusOptionNode because in IE putting it inside causes the menu to scroll up on mouseover + dojo.window.scrollIntoView(this._highlighted_option); + }, + + highlightFirstOption: function(){ + // summary: + // Highlight the first real item in the list (not Previous Choices). + var first = this.domNode.firstChild; + var second = first.nextSibling; + this._focusOptionNode(second.style.display == "none" ? first : second); // remotely possible that Previous Choices is the only thing in the list + dojo.window.scrollIntoView(this._highlighted_option); + }, + + highlightLastOption: function(){ + // summary: + // Highlight the last real item in the list (not More Choices). + this._focusOptionNode(this.domNode.lastChild.previousSibling); + dojo.window.scrollIntoView(this._highlighted_option); + }, + + _highlightPrevOption: function(){ + // summary: + // Highlight the item just above the current selection. + // If nothing selected, highlight last option (if + // you select Previous and try to keep scrolling up the list). + if(!this.getHighlightedOption()){ + var lc = this.domNode.lastChild; + this._focusOptionNode(lc.style.display == "none" ? lc.previousSibling : lc); + }else{ + var ps = this._highlighted_option.previousSibling; + if(ps && ps.style.display != "none"){ + this._focusOptionNode(ps); + }else{ + this.highlightLastOption(); + } + } + dojo.window.scrollIntoView(this._highlighted_option); + }, + + _page: function(/*Boolean*/ up){ + // summary: + // Handles page-up and page-down keypresses + + var scrollamount = 0; + var oldscroll = this.domNode.scrollTop; + var height = dojo.style(this.domNode, "height"); + // if no item is highlighted, highlight the first option + if(!this.getHighlightedOption()){ + this._highlightNextOption(); + } + while(scrollamount<height){ + if(up){ + // stop at option 1 + if(!this.getHighlightedOption().previousSibling || + this._highlighted_option.previousSibling.style.display == "none"){ + break; + } + this._highlightPrevOption(); + }else{ + // stop at last option + if(!this.getHighlightedOption().nextSibling || + this._highlighted_option.nextSibling.style.display == "none"){ + break; + } + this._highlightNextOption(); + } + // going backwards + var newscroll=this.domNode.scrollTop; + scrollamount+=(newscroll-oldscroll)*(up ? -1:1); + oldscroll=newscroll; + } + }, + + pageUp: function(){ + // summary: + // Handles pageup keypress. + // TODO: just call _page directly from handleKey(). + // tags: + // private + this._page(true); + }, + + pageDown: function(){ + // summary: + // Handles pagedown keypress. + // TODO: just call _page directly from handleKey(). + // tags: + // private + this._page(false); + }, + + getHighlightedOption: function(){ + // summary: + // Returns the highlighted option. + var ho = this._highlighted_option; + return (ho && ho.parentNode) ? ho : null; + }, + + handleKey: function(key){ + switch(key){ + case dojo.keys.DOWN_ARROW: + this._highlightNextOption(); + break; + case dojo.keys.PAGE_DOWN: + this.pageDown(); + break; + case dojo.keys.UP_ARROW: + this._highlightPrevOption(); + break; + case dojo.keys.PAGE_UP: + this.pageUp(); + break; + } + } + } +); + +dojo.declare( + "dijit.form.ComboBox", + [dijit.form.ValidationTextBox, dijit.form.ComboBoxMixin], + { + // summary: + // Auto-completing text box, and base class for dijit.form.FilteringSelect. + // + // description: + // The drop down box's values are populated from an class called + // a data provider, which returns a list of values based on the characters + // that the user has typed into the input box. + // If OPTION tags are used as the data provider via markup, + // then the OPTION tag's child text node is used as the widget value + // when selected. The OPTION tag's value attribute is ignored. + // To set the default value when using OPTION tags, specify the selected + // attribute on 1 of the child OPTION tags. + // + // Some of the options to the ComboBox are actually arguments to the data + // provider. + + _setValueAttr: function(/*String*/ value, /*Boolean?*/ priorityChange, /*String?*/ displayedValue){ + // summary: + // Hook so attr('value', value) works. + // description: + // Sets the value of the select. + this.item = null; // value not looked up in store + if(!value){ value = ''; } // null translates to blank + dijit.form.ValidationTextBox.prototype._setValueAttr.call(this, value, priorityChange, displayedValue); + } + } +); + +dojo.declare("dijit.form._ComboBoxDataStore", null, { + // summary: + // Inefficient but small data store specialized for inlined `dijit.form.ComboBox` data + // + // description: + // Provides a store for inlined data like: + // + // | <select> + // | <option value="AL">Alabama</option> + // | ... + // + // Actually. just implements the subset of dojo.data.Read/Notification + // needed for ComboBox and FilteringSelect to work. + // + // Note that an item is just a pointer to the <option> DomNode. + + constructor: function( /*DomNode*/ root){ + this.root = root; + if(root.tagName != "SELECT" && root.firstChild){ + root = dojo.query("select", root); + if(root.length > 0){ // SELECT is a child of srcNodeRef + root = root[0]; + }else{ // no select, so create 1 to parent the option tags to define selectedIndex + this.root.innerHTML = "<SELECT>"+this.root.innerHTML+"</SELECT>"; + root = this.root.firstChild; + } + this.root = root; + } + dojo.query("> option", root).forEach(function(node){ + // TODO: this was added in #3858 but unclear why/if it's needed; doesn't seem to be. + // If it is needed then can we just hide the select itself instead? + //node.style.display="none"; + node.innerHTML = dojo.trim(node.innerHTML); + }); + + }, + + getValue: function( /* item */ item, + /* attribute-name-string */ attribute, + /* value? */ defaultValue){ + return (attribute == "value") ? item.value : (item.innerText || item.textContent || ''); + }, + + isItemLoaded: function(/* anything */ something){ + return true; + }, + + getFeatures: function(){ + return {"dojo.data.api.Read": true, "dojo.data.api.Identity": true}; + }, + + _fetchItems: function( /* Object */ args, + /* Function */ findCallback, + /* Function */ errorCallback){ + // summary: + // See dojo.data.util.simpleFetch.fetch() + if(!args.query){ args.query = {}; } + if(!args.query.name){ args.query.name = ""; } + if(!args.queryOptions){ args.queryOptions = {}; } + var matcher = dojo.data.util.filter.patternToRegExp(args.query.name, args.queryOptions.ignoreCase), + items = dojo.query("> option", this.root).filter(function(option){ + return (option.innerText || option.textContent || '').match(matcher); + } ); + if(args.sort){ + items.sort(dojo.data.util.sorter.createSortFunction(args.sort, this)); + } + findCallback(items, args); + }, + + close: function(/*dojo.data.api.Request || args || null */ request){ + return; + }, + + getLabel: function(/* item */ item){ + return item.innerHTML; + }, + + getIdentity: function(/* item */ item){ + return dojo.attr(item, "value"); + }, + + fetchItemByIdentity: function(/* Object */ args){ + // summary: + // Given the identity of an item, this method returns the item that has + // that identity through the onItem callback. + // Refer to dojo.data.api.Identity.fetchItemByIdentity() for more details. + // + // description: + // Given arguments like: + // + // | {identity: "CA", onItem: function(item){...} + // + // Call `onItem()` with the DOM node `<option value="CA">California</option>` + var item = dojo.query("> option[value='" + args.identity + "']", this.root)[0]; + args.onItem(item); + }, + + fetchSelectedItem: function(){ + // summary: + // Get the option marked as selected, like `<option selected>`. + // Not part of dojo.data API. + var root = this.root, + si = root.selectedIndex; + return typeof si == "number" + ? dojo.query("> option:nth-child(" + (si != -1 ? si+1 : 1) + ")", root)[0] + : null; // dojo.data.Item + } +}); +//Mix in the simple fetch implementation to this class. +dojo.extend(dijit.form._ComboBoxDataStore,dojo.data.util.simpleFetch); + +} + +if(!dojo._hasResource["dijit.form.FilteringSelect"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.form.FilteringSelect"] = true; +dojo.provide("dijit.form.FilteringSelect"); + + + +dojo.declare( + "dijit.form.FilteringSelect", + [dijit.form.MappedTextBox, dijit.form.ComboBoxMixin], + { + // summary: + // An enhanced version of the HTML SELECT tag, populated dynamically + // + // description: + // An enhanced version of the HTML SELECT tag, populated dynamically. It works + // very nicely with very large data sets because it can load and page data as needed. + // It also resembles ComboBox, but does not allow values outside of the provided ones. + // If OPTION tags are used as the data provider via markup, then the + // OPTION tag's child text node is used as the displayed value when selected + // while the OPTION tag's value attribute is used as the widget value on form submit. + // To set the default value when using OPTION tags, specify the selected + // attribute on 1 of the child OPTION tags. + // + // Similar features: + // - There is a drop down list of possible values. + // - You can only enter a value from the drop down list. (You can't + // enter an arbitrary value.) + // - The value submitted with the form is the hidden value (ex: CA), + // not the displayed value a.k.a. label (ex: California) + // + // Enhancements over plain HTML version: + // - If you type in some text then it will filter down the list of + // possible values in the drop down list. + // - List can be specified either as a static list or via a javascript + // function (that can get the list from a server) + + _isvalid: true, + + // required: Boolean + // True (default) if user is required to enter a value into this field. + required: true, + + _lastDisplayedValue: "", + + isValid: function(){ + // Overrides ValidationTextBox.isValid() + return this._isvalid || (!this.required && this.get('displayedValue') == ""); // #5974 + }, + + _refreshState: function(){ + if(!this.searchTimer){ // state will be refreshed after results are returned + this.inherited(arguments); + } + }, + + _callbackSetLabel: function( /*Array*/ result, + /*Object*/ dataObject, + /*Boolean?*/ priorityChange){ + // summary: + // Callback function that dynamically sets the label of the + // ComboBox + + // setValue does a synchronous lookup, + // so it calls _callbackSetLabel directly, + // and so does not pass dataObject + // still need to test against _lastQuery in case it came too late + if((dataObject && dataObject.query[this.searchAttr] != this._lastQuery) || (!dataObject && result.length && this.store.getIdentity(result[0]) != this._lastQuery)){ + return; + } + if(!result.length){ + //#3268: do nothing on bad input + //#3285: change CSS to indicate error + this.valueNode.value = ""; + dijit.form.TextBox.superclass._setValueAttr.call(this, "", priorityChange || (priorityChange === undefined && !this._focused)); + this._isvalid = false; + this.validate(this._focused); + this.item = null; + }else{ + this.set('item', result[0], priorityChange); + } + }, + + _openResultList: function(/*Object*/ results, /*Object*/ dataObject){ + // Overrides ComboBox._openResultList() + + // #3285: tap into search callback to see if user's query resembles a match + if(dataObject.query[this.searchAttr] != this._lastQuery){ + return; + } + if(this.item === undefined){ // item == undefined for keyboard search + this._isvalid = results.length != 0 || this._maxOptions != 0; // result.length==0 && maxOptions != 0 implies the nextChoices item selected but then the datastore returned 0 more entries + this.validate(true); + } + dijit.form.ComboBoxMixin.prototype._openResultList.apply(this, arguments); + }, + + _getValueAttr: function(){ + // summary: + // Hook for attr('value') to work. + + // don't get the textbox value but rather the previously set hidden value. + // Use this.valueNode.value which isn't always set for other MappedTextBox widgets until blur + return this.valueNode.value; + }, + + _getValueField: function(){ + // Overrides ComboBox._getValueField() + return "value"; + }, + + _setValueAttr: function(/*String*/ value, /*Boolean?*/ priorityChange){ + // summary: + // Hook so attr('value', value) works. + // description: + // Sets the value of the select. + // Also sets the label to the corresponding value by reverse lookup. + if(!this._onChangeActive){ priorityChange = null; } + this._lastQuery = value; + + if(value === null || value === ''){ + this._setDisplayedValueAttr('', priorityChange); + return; + } + + //#3347: fetchItemByIdentity if no keyAttr specified + var self = this; + this.store.fetchItemByIdentity({ + identity: value, + onItem: function(item){ + self._callbackSetLabel(item? [item] : [], undefined, priorityChange); + } + }); + }, + + _setItemAttr: function(/*item*/ item, /*Boolean?*/ priorityChange, /*String?*/ displayedValue){ + // summary: + // Set the displayed valued in the input box, and the hidden value + // that gets submitted, based on a dojo.data store item. + // description: + // Users shouldn't call this function; they should be calling + // attr('item', value) + // tags: + // private + this._isvalid = true; + this.inherited(arguments); + this.valueNode.value = this.value; + this._lastDisplayedValue = this.textbox.value; + }, + + _getDisplayQueryString: function(/*String*/ text){ + return text.replace(/([\\\*\?])/g, "\\$1"); + }, + + _setDisplayedValueAttr: function(/*String*/ label, /*Boolean?*/ priorityChange){ + // summary: + // Hook so attr('displayedValue', label) works. + // description: + // Sets textbox to display label. Also performs reverse lookup + // to set the hidden value. + + // When this is called during initialization it'll ping the datastore + // for reverse lookup, and when that completes (after an XHR request) + // will call setValueAttr()... but that shouldn't trigger an onChange() + // event, even when it happens after creation has finished + if(!this._created){ + priorityChange = false; + } + + if(this.store){ + this._hideResultList(); + var query = dojo.clone(this.query); // #6196: populate query with user-specifics + // escape meta characters of dojo.data.util.filter.patternToRegExp(). + this._lastQuery = query[this.searchAttr] = this._getDisplayQueryString(label); + // if the label is not valid, the callback will never set it, + // so the last valid value will get the warning textbox set the + // textbox value now so that the impending warning will make + // sense to the user + this.textbox.value = label; + this._lastDisplayedValue = label; + var _this = this; + var fetch = { + query: query, + queryOptions: { + ignoreCase: this.ignoreCase, + deep: true + }, + onComplete: function(result, dataObject){ + _this._fetchHandle = null; + dojo.hitch(_this, "_callbackSetLabel")(result, dataObject, priorityChange); + }, + onError: function(errText){ + _this._fetchHandle = null; + console.error('dijit.form.FilteringSelect: ' + errText); + dojo.hitch(_this, "_callbackSetLabel")([], undefined, false); + } + }; + dojo.mixin(fetch, this.fetchProperties); + this._fetchHandle = this.store.fetch(fetch); + } + }, + + postMixInProperties: function(){ + this.inherited(arguments); + this._isvalid = !this.required; + }, + + undo: function(){ + this.set('displayedValue', this._lastDisplayedValue); + } + } +); + +} + +if(!dojo._hasResource["dijit.form.Form"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.form.Form"] = true; +dojo.provide("dijit.form.Form"); + + + + + +dojo.declare( + "dijit.form.Form", + [dijit._Widget, dijit._Templated, dijit.form._FormMixin], + { + // summary: + // Widget corresponding to HTML form tag, for validation and serialization + // + // example: + // | <form dojoType="dijit.form.Form" id="myForm"> + // | Name: <input type="text" name="name" /> + // | </form> + // | myObj = {name: "John Doe"}; + // | dijit.byId('myForm').set('value', myObj); + // | + // | myObj=dijit.byId('myForm').get('value'); + + // HTML <FORM> attributes + + // name: String? + // Name of form for scripting. + name: "", + + // action: String? + // Server-side form handler. + action: "", + + // method: String? + // HTTP method used to submit the form, either "GET" or "POST". + method: "", + + // encType: String? + // Encoding type for the form, ex: application/x-www-form-urlencoded. + encType: "", + + // accept-charset: String? + // List of supported charsets. + "accept-charset": "", + + // accept: String? + // List of MIME types for file upload. + accept: "", + + // target: String? + // Target frame for the document to be opened in. + target: "", + + templateString: "<form dojoAttachPoint='containerNode' dojoAttachEvent='onreset:_onReset,onsubmit:_onSubmit' ${!nameAttrSetting}></form>", + + attributeMap: dojo.delegate(dijit._Widget.prototype.attributeMap, { + action: "", + method: "", + encType: "", + "accept-charset": "", + accept: "", + target: "" + }), + + postMixInProperties: function(){ + // Setup name=foo string to be referenced from the template (but only if a name has been specified) + // Unfortunately we can't use attributeMap to set the name due to IE limitations, see #8660 + this.nameAttrSetting = this.name ? ("name='" + this.name + "'") : ""; + this.inherited(arguments); + }, + + execute: function(/*Object*/ formContents){ + // summary: + // Deprecated: use submit() + // tags: + // deprecated + }, + + onExecute: function(){ + // summary: + // Deprecated: use onSubmit() + // tags: + // deprecated + }, + + _setEncTypeAttr: function(/*String*/ value){ + this.encType = value; + dojo.attr(this.domNode, "encType", value); + if(dojo.isIE){ this.domNode.encoding = value; } + }, + + postCreate: function(){ + // IE tries to hide encType + // TODO: this code should be in parser, not here. + if(dojo.isIE && this.srcNodeRef && this.srcNodeRef.attributes){ + var item = this.srcNodeRef.attributes.getNamedItem('encType'); + if(item && !item.specified && (typeof item.value == "string")){ + this.set('encType', item.value); + } + } + this.inherited(arguments); + }, + + reset: function(/*Event?*/ e){ + // summary: + // restores all widget values back to their init values, + // calls onReset() which can cancel the reset by returning false + + // create fake event so we can know if preventDefault() is called + var faux = { + returnValue: true, // the IE way + preventDefault: function(){ // not IE + this.returnValue = false; + }, + stopPropagation: function(){}, + currentTarget: e ? e.target : this.domNode, + target: e ? e.target : this.domNode + }; + // if return value is not exactly false, and haven't called preventDefault(), then reset + if(!(this.onReset(faux) === false) && faux.returnValue){ + this.inherited(arguments, []); + } + }, + + onReset: function(/*Event?*/ e){ + // summary: + // Callback when user resets the form. This method is intended + // to be over-ridden. When the `reset` method is called + // programmatically, the return value from `onReset` is used + // to compute whether or not resetting should proceed + // tags: + // callback + return true; // Boolean + }, + + _onReset: function(e){ + this.reset(e); + dojo.stopEvent(e); + return false; + }, + + _onSubmit: function(e){ + var fp = dijit.form.Form.prototype; + // TODO: remove this if statement beginning with 2.0 + if(this.execute != fp.execute || this.onExecute != fp.onExecute){ + dojo.deprecated("dijit.form.Form:execute()/onExecute() are deprecated. Use onSubmit() instead.", "", "2.0"); + this.onExecute(); + this.execute(this.getValues()); + } + if(this.onSubmit(e) === false){ // only exactly false stops submit + dojo.stopEvent(e); + } + }, + + onSubmit: function(/*Event?*/e){ + // summary: + // Callback when user submits the form. + // description: + // This method is intended to be over-ridden, but by default it checks and + // returns the validity of form elements. When the `submit` + // method is called programmatically, the return value from + // `onSubmit` is used to compute whether or not submission + // should proceed + // tags: + // extension + + return this.isValid(); // Boolean + }, + + submit: function(){ + // summary: + // programmatically submit form if and only if the `onSubmit` returns true + if(!(this.onSubmit() === false)){ + this.containerNode.submit(); + } + } + } +); + +} + +if(!dojo._hasResource["dijit.form.RadioButton"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.form.RadioButton"] = true; +dojo.provide("dijit.form.RadioButton"); + + +// TODO: for 2.0, move the RadioButton code into this file + +} + +if(!dojo._hasResource["dijit.form._FormSelectWidget"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.form._FormSelectWidget"] = true; +dojo.provide("dijit.form._FormSelectWidget"); + + + + +/*===== +dijit.form.__SelectOption = function(){ + // value: String + // The value of the option. Setting to empty (or missing) will + // place a separator at that location + // label: String + // The label for our option. It can contain html tags. + // selected: Boolean + // Whether or not we are a selected option + // disabled: Boolean + // Whether or not this specific option is disabled + this.value = value; + this.label = label; + this.selected = selected; + this.disabled = disabled; +} +=====*/ + +dojo.declare("dijit.form._FormSelectWidget", dijit.form._FormValueWidget, { + // summary: + // Extends _FormValueWidget in order to provide "select-specific" + // values - i.e., those values that are unique to <select> elements. + // This also provides the mechanism for reading the elements from + // a store, if desired. + + // multiple: Boolean + // Whether or not we are multi-valued + multiple: false, + + // options: dijit.form.__SelectOption[] + // The set of options for our select item. Roughly corresponds to + // the html <option> tag. + options: null, + + // store: dojo.data.api.Identity + // A store which, at the very least impelements dojo.data.api.Identity + // to use for getting our list of options - rather than reading them + // from the <option> html tags. + store: null, + + // query: object + // A query to use when fetching items from our store + query: null, + + // queryOptions: object + // Query options to use when fetching from the store + queryOptions: null, + + // onFetch: Function + // A callback to do with an onFetch - but before any items are actually + // iterated over (i.e. to filter even futher what you want to add) + onFetch: null, + + // sortByLabel: boolean + // Flag to sort the options returned from a store by the label of + // the store. + sortByLabel: true, + + + // loadChildrenOnOpen: boolean + // By default loadChildren is called when the items are fetched from the + // store. This property allows delaying loadChildren (and the creation + // of the options/menuitems) until the user opens the click the button. + // dropdown + loadChildrenOnOpen: false, + + getOptions: function(/* anything */ valueOrIdx){ + // summary: + // Returns a given option (or options). + // valueOrIdx: + // If passed in as a string, that string is used to look up the option + // in the array of options - based on the value property. + // (See dijit.form.__SelectOption). + // + // If passed in a number, then the option with the given index (0-based) + // within this select will be returned. + // + // If passed in a dijit.form.__SelectOption, the same option will be + // returned if and only if it exists within this select. + // + // If passed an array, then an array will be returned with each element + // in the array being looked up. + // + // If not passed a value, then all options will be returned + // + // returns: + // The option corresponding with the given value or index. null + // is returned if any of the following are true: + // - A string value is passed in which doesn't exist + // - An index is passed in which is outside the bounds of the array of options + // - A dijit.form.__SelectOption is passed in which is not a part of the select + + // NOTE: the compare for passing in a dijit.form.__SelectOption checks + // if the value property matches - NOT if the exact option exists + // NOTE: if passing in an array, null elements will be placed in the returned + // array when a value is not found. + var lookupValue = valueOrIdx, opts = this.options || [], l = opts.length; + + if(lookupValue === undefined){ + return opts; // dijit.form.__SelectOption[] + } + if(dojo.isArray(lookupValue)){ + return dojo.map(lookupValue, "return this.getOptions(item);", this); // dijit.form.__SelectOption[] + } + if(dojo.isObject(valueOrIdx)){ + // We were passed an option - so see if it's in our array (directly), + // and if it's not, try and find it by value. + if(!dojo.some(this.options, function(o, idx){ + if(o === lookupValue || + (o.value && o.value === lookupValue.value)){ + lookupValue = idx; + return true; + } + return false; + })){ + lookupValue = -1; + } + } + if(typeof lookupValue == "string"){ + for(var i=0; i<l; i++){ + if(opts[i].value === lookupValue){ + lookupValue = i; + break; + } + } + } + if(typeof lookupValue == "number" && lookupValue >= 0 && lookupValue < l){ + return this.options[lookupValue] // dijit.form.__SelectOption + } + return null; // null + }, + + addOption: function(/* dijit.form.__SelectOption, dijit.form.__SelectOption[] */ option){ + // summary: + // Adds an option or options to the end of the select. If value + // of the option is empty or missing, a separator is created instead. + // Passing in an array of options will yield slightly better performance + // since the children are only loaded once. + if(!dojo.isArray(option)){ option = [option]; } + dojo.forEach(option, function(i){ + if(i && dojo.isObject(i)){ + this.options.push(i); + } + }, this); + this._loadChildren(); + }, + + removeOption: function(/* string, dijit.form.__SelectOption, number, or array */ valueOrIdx){ + // summary: + // Removes the given option or options. You can remove by string + // (in which case the value is removed), number (in which case the + // index in the options array is removed), or select option (in + // which case, the select option with a matching value is removed). + // You can also pass in an array of those values for a slightly + // better performance since the children are only loaded once. + if(!dojo.isArray(valueOrIdx)){ valueOrIdx = [valueOrIdx]; } + var oldOpts = this.getOptions(valueOrIdx); + dojo.forEach(oldOpts, function(i){ + // We can get null back in our array - if our option was not found. In + // that case, we don't want to blow up... + if(i){ + this.options = dojo.filter(this.options, function(node, idx){ + return (node.value !== i.value); + }); + this._removeOptionItem(i); + } + }, this); + this._loadChildren(); + }, + + updateOption: function(/* dijit.form.__SelectOption, dijit.form.__SelectOption[] */ newOption){ + // summary: + // Updates the values of the given option. The option to update + // is matched based on the value of the entered option. Passing + // in an array of new options will yeild better performance since + // the children will only be loaded once. + if(!dojo.isArray(newOption)){ newOption = [newOption]; } + dojo.forEach(newOption, function(i){ + var oldOpt = this.getOptions(i), k; + if(oldOpt){ + for(k in i){ oldOpt[k] = i[k]; } + } + }, this); + this._loadChildren(); + }, + + setStore: function(/* dojo.data.api.Identity */ store, + /* anything? */ selectedValue, + /* Object? */ fetchArgs){ + // summary: + // Sets the store you would like to use with this select widget. + // The selected value is the value of the new store to set. This + // function returns the original store, in case you want to reuse + // it or something. + // store: dojo.data.api.Identity + // The store you would like to use - it MUST implement Identity, + // and MAY implement Notification. + // selectedValue: anything? + // The value that this widget should set itself to *after* the store + // has been loaded + // fetchArgs: Object? + // The arguments that will be passed to the store's fetch() function + var oStore = this.store; + fetchArgs = fetchArgs || {}; + if(oStore !== store){ + // Our store has changed, so update our notifications + dojo.forEach(this._notifyConnections || [], dojo.disconnect); + delete this._notifyConnections; + if(store && store.getFeatures()["dojo.data.api.Notification"]){ + this._notifyConnections = [ + dojo.connect(store, "onNew", this, "_onNewItem"), + dojo.connect(store, "onDelete", this, "_onDeleteItem"), + dojo.connect(store, "onSet", this, "_onSetItem") + ]; + } + this.store = store; + } + + // Turn off change notifications while we make all these changes + this._onChangeActive = false; + + // Remove existing options (if there are any) + if(this.options && this.options.length){ + this.removeOption(this.options); + } + + // Add our new options + if(store){ + var cb = function(items){ + if(this.sortByLabel && !fetchArgs.sort && items.length){ + items.sort(dojo.data.util.sorter.createSortFunction([{ + attribute: store.getLabelAttributes(items[0])[0] + }], store)); + } + + if(fetchArgs.onFetch){ + items = fetchArgs.onFetch(items); + } + // TODO: Add these guys as a batch, instead of separately + dojo.forEach(items, function(i){ + this._addOptionForItem(i); + }, this); + + // Set our value (which might be undefined), and then tweak + // it to send a change event with the real value + this._loadingStore = false; + this.set("value", (("_pendingValue" in this) ? this._pendingValue : selectedValue)); + delete this._pendingValue; + + if(!this.loadChildrenOnOpen){ + this._loadChildren(); + }else{ + this._pseudoLoadChildren(items); + } + this._fetchedWith = opts; + this._lastValueReported = this.multiple ? [] : null; + this._onChangeActive = true; + this.onSetStore(); + this._handleOnChange(this.value); + }; + var opts = dojo.mixin({onComplete:cb, scope: this}, fetchArgs); + this._loadingStore = true; + store.fetch(opts); + }else{ + delete this._fetchedWith; + } + return oStore; // dojo.data.api.Identity + }, + + _setValueAttr: function(/*anything*/ newValue, /*Boolean, optional*/ priorityChange){ + // summary: + // set the value of the widget. + // If a string is passed, then we set our value from looking it up. + if(this._loadingStore){ + // Our store is loading - so save our value, and we'll set it when + // we're done + this._pendingValue = newValue; + return; + } + var opts = this.getOptions() || []; + if(!dojo.isArray(newValue)){ + newValue = [newValue]; + } + dojo.forEach(newValue, function(i, idx){ + if(!dojo.isObject(i)){ + i = i + ""; + } + if(typeof i === "string"){ + newValue[idx] = dojo.filter(opts, function(node){ + return node.value === i; + })[0] || {value: "", label: ""}; + } + }, this); + + // Make sure some sane default is set + newValue = dojo.filter(newValue, function(i){ return i && i.value; }); + if(!this.multiple && (!newValue[0] || !newValue[0].value) && opts.length){ + newValue[0] = opts[0]; + } + dojo.forEach(opts, function(i){ + i.selected = dojo.some(newValue, function(v){ return v.value === i.value; }); + }); + var val = dojo.map(newValue, function(i){ return i.value; }), + disp = dojo.map(newValue, function(i){ return i.label; }); + + this.value = this.multiple ? val : val[0]; + this._setDisplay(this.multiple ? disp : disp[0]); + this._updateSelection(); + this._handleOnChange(this.value, priorityChange); + }, + + _getDisplayedValueAttr: function(){ + // summary: + // returns the displayed value of the widget + var val = this.get("value"); + if(!dojo.isArray(val)){ + val = [val]; + } + var ret = dojo.map(this.getOptions(val), function(v){ + if(v && "label" in v){ + return v.label; + }else if(v){ + return v.value; + } + return null; + }, this); + return this.multiple ? ret : ret[0]; + }, + + _getValueDeprecated: false, // remove when _FormWidget:getValue is removed + getValue: function(){ + // summary: + // get the value of the widget. + return this._lastValue; + }, + + undo: function(){ + // summary: + // restore the value to the last value passed to onChange + this._setValueAttr(this._lastValueReported, false); + }, + + _loadChildren: function(){ + // summary: + // Loads the children represented by this widget's options. + // reset the menu to make it "populatable on the next click + if(this._loadingStore){ return; } + dojo.forEach(this._getChildren(), function(child){ + child.destroyRecursive(); + }); + // Add each menu item + dojo.forEach(this.options, this._addOptionItem, this); + + // Update states + this._updateSelection(); + }, + + _updateSelection: function(){ + // summary: + // Sets the "selected" class on the item for styling purposes + this.value = this._getValueFromOpts(); + var val = this.value; + if(!dojo.isArray(val)){ + val = [val]; + } + if(val && val[0]){ + dojo.forEach(this._getChildren(), function(child){ + var isSelected = dojo.some(val, function(v){ + return child.option && (v === child.option.value); + }); + dojo.toggleClass(child.domNode, this.baseClass + "SelectedOption", isSelected); + dijit.setWaiState(child.domNode, "selected", isSelected); + }, this); + } + this._handleOnChange(this.value); + }, + + _getValueFromOpts: function(){ + // summary: + // Returns the value of the widget by reading the options for + // the selected flag + var opts = this.getOptions() || []; + if(!this.multiple && opts.length){ + // Mirror what a select does - choose the first one + var opt = dojo.filter(opts, function(i){ + return i.selected; + })[0]; + if(opt && opt.value){ + return opt.value + }else{ + opts[0].selected = true; + return opts[0].value; + } + }else if(this.multiple){ + // Set value to be the sum of all selected + return dojo.map(dojo.filter(opts, function(i){ + return i.selected; + }), function(i){ + return i.value; + }) || []; + } + return ""; + }, + + // Internal functions to call when we have store notifications come in + _onNewItem: function(/* item */ item, /* Object? */ parentInfo){ + if(!parentInfo || !parentInfo.parent){ + // Only add it if we are top-level + this._addOptionForItem(item); + } + }, + _onDeleteItem: function(/* item */ item){ + var store = this.store; + this.removeOption(store.getIdentity(item)); + }, + _onSetItem: function(/* item */ item){ + this.updateOption(this._getOptionObjForItem(item)); + }, + + _getOptionObjForItem: function(item){ + // summary: + // Returns an option object based off the given item. The "value" + // of the option item will be the identity of the item, the "label" + // of the option will be the label of the item. If the item contains + // children, the children value of the item will be set + var store = this.store, label = store.getLabel(item), + value = (label ? store.getIdentity(item) : null); + return {value: value, label: label, item:item}; // dijit.form.__SelectOption + }, + + _addOptionForItem: function(/* item */ item){ + // summary: + // Creates (and adds) the option for the given item + var store = this.store; + if(!store.isItemLoaded(item)){ + // We are not loaded - so let's load it and add later + store.loadItem({item: item, onComplete: function(i){ + this._addOptionForItem(item); + }, + scope: this}); + return; + } + var newOpt = this._getOptionObjForItem(item); + this.addOption(newOpt); + }, + + constructor: function(/* Object */ keywordArgs){ + // summary: + // Saves off our value, if we have an initial one set so we + // can use it if we have a store as well (see startup()) + this._oValue = (keywordArgs || {}).value || null; + }, + + _fillContent: function(){ + // summary: + // Loads our options and sets up our dropdown correctly. We + // don't want any content, so we don't call any inherit chain + // function. + var opts = this.options; + if(!opts){ + opts = this.options = this.srcNodeRef ? dojo.query(">", + this.srcNodeRef).map(function(node){ + if(node.getAttribute("type") === "separator"){ + return { value: "", label: "", selected: false, disabled: false }; + } + return { value: node.getAttribute("value"), + label: String(node.innerHTML), + selected: node.getAttribute("selected") || false, + disabled: node.getAttribute("disabled") || false }; + }, this) : []; + } + if(!this.value){ + this.value = this._getValueFromOpts(); + }else if(this.multiple && typeof this.value == "string"){ + this.value = this.value.split(","); + } + }, + + postCreate: function(){ + // summary: + // sets up our event handling that we need for functioning + // as a select + dojo.setSelectable(this.focusNode, false); + this.inherited(arguments); + + // Make our event connections for updating state + this.connect(this, "onChange", "_updateSelection"); + this.connect(this, "startup", "_loadChildren"); + + this._setValueAttr(this.value, null); + }, + + startup: function(){ + // summary: + // Connects in our store, if we have one defined + this.inherited(arguments); + var store = this.store, fetchArgs = {}; + dojo.forEach(["query", "queryOptions", "onFetch"], function(i){ + if(this[i]){ + fetchArgs[i] = this[i]; + } + delete this[i]; + }, this); + if(store && store.getFeatures()["dojo.data.api.Identity"]){ + // Temporarily set our store to null so that it will get set + // and connected appropriately + this.store = null; + this.setStore(store, this._oValue, fetchArgs); + } + }, + + destroy: function(){ + // summary: + // Clean up our connections + dojo.forEach(this._notifyConnections || [], dojo.disconnect); + this.inherited(arguments); + }, + + _addOptionItem: function(/* dijit.form.__SelectOption */ option){ + // summary: + // User-overridable function which, for the given option, adds an + // item to the select. If the option doesn't have a value, then a + // separator is added in that place. Make sure to store the option + // in the created option widget. + }, + + _removeOptionItem: function(/* dijit.form.__SelectOption */ option){ + // summary: + // User-overridable function which, for the given option, removes + // its item from the select. + }, + + _setDisplay: function(/*String or String[]*/ newDisplay){ + // summary: + // Overridable function which will set the display for the + // widget. newDisplay is either a string (in the case of + // single selects) or array of strings (in the case of multi-selects) + }, + + _getChildren: function(){ + // summary: + // Overridable function to return the children that this widget contains. + return []; + }, + + _getSelectedOptionsAttr: function(){ + // summary: + // hooks into this.attr to provide a mechanism for getting the + // option items for the current value of the widget. + return this.getOptions(this.get("value")); + }, + + _pseudoLoadChildren: function(/* item[] */ items){ + // summary: + // a function that will "fake" loading children, if needed, and + // if we have set to not load children until the widget opens. + // items: + // An array of items that will be loaded, when needed + }, + + onSetStore: function(){ + // summary: + // a function that can be connected to in order to receive a + // notification that the store has finished loading and all options + // from that store are available + } +}); + +} + +if(!dojo._hasResource["dijit._KeyNavContainer"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit._KeyNavContainer"] = true; +dojo.provide("dijit._KeyNavContainer"); + + +dojo.declare("dijit._KeyNavContainer", + dijit._Container, + { + + // summary: + // A _Container with keyboard navigation of its children. + // description: + // To use this mixin, call connectKeyNavHandlers() in + // postCreate() and call startupKeyNavChildren() in startup(). + // It provides normalized keyboard and focusing code for Container + // widgets. +/*===== + // focusedChild: [protected] Widget + // The currently focused child widget, or null if there isn't one + focusedChild: null, +=====*/ + + // tabIndex: Integer + // Tab index of the container; same as HTML tabIndex attribute. + // Note then when user tabs into the container, focus is immediately + // moved to the first item in the container. + tabIndex: "0", + + _keyNavCodes: {}, + + connectKeyNavHandlers: function(/*dojo.keys[]*/ prevKeyCodes, /*dojo.keys[]*/ nextKeyCodes){ + // summary: + // Call in postCreate() to attach the keyboard handlers + // to the container. + // preKeyCodes: dojo.keys[] + // Key codes for navigating to the previous child. + // nextKeyCodes: dojo.keys[] + // Key codes for navigating to the next child. + // tags: + // protected + + var keyCodes = (this._keyNavCodes = {}); + var prev = dojo.hitch(this, this.focusPrev); + var next = dojo.hitch(this, this.focusNext); + dojo.forEach(prevKeyCodes, function(code){ keyCodes[code] = prev; }); + dojo.forEach(nextKeyCodes, function(code){ keyCodes[code] = next; }); + this.connect(this.domNode, "onkeypress", "_onContainerKeypress"); + this.connect(this.domNode, "onfocus", "_onContainerFocus"); + }, + + startupKeyNavChildren: function(){ + // summary: + // Call in startup() to set child tabindexes to -1 + // tags: + // protected + dojo.forEach(this.getChildren(), dojo.hitch(this, "_startupChild")); + }, + + addChild: function(/*dijit._Widget*/ widget, /*int?*/ insertIndex){ + // summary: + // Add a child to our _Container + dijit._KeyNavContainer.superclass.addChild.apply(this, arguments); + this._startupChild(widget); + }, + + focus: function(){ + // summary: + // Default focus() implementation: focus the first child. + this.focusFirstChild(); + }, + + focusFirstChild: function(){ + // summary: + // Focus the first focusable child in the container. + // tags: + // protected + var child = this._getFirstFocusableChild(); + if(child){ // edge case: Menu could be empty or hidden + this.focusChild(child); + } + }, + + focusNext: function(){ + // summary: + // Focus the next widget + // tags: + // protected + var child = this._getNextFocusableChild(this.focusedChild, 1); + this.focusChild(child); + }, + + focusPrev: function(){ + // summary: + // Focus the last focusable node in the previous widget + // (ex: go to the ComboButton icon section rather than button section) + // tags: + // protected + var child = this._getNextFocusableChild(this.focusedChild, -1); + this.focusChild(child, true); + }, + + focusChild: function(/*dijit._Widget*/ widget, /*Boolean*/ last){ + // summary: + // Focus widget. + // widget: + // Reference to container's child widget + // last: + // If true and if widget has multiple focusable nodes, focus the + // last one instead of the first one + // tags: + // protected + + if(this.focusedChild && widget !== this.focusedChild){ + this._onChildBlur(this.focusedChild); + } + widget.focus(last ? "end" : "start"); + this.focusedChild = widget; + }, + + _startupChild: function(/*dijit._Widget*/ widget){ + // summary: + // Setup for each child widget + // description: + // Sets tabIndex=-1 on each child, so that the tab key will + // leave the container rather than visiting each child. + // tags: + // private + + widget.set("tabIndex", "-1"); + + this.connect(widget, "_onFocus", function(){ + // Set valid tabIndex so tabbing away from widget goes to right place, see #10272 + widget.set("tabIndex", this.tabIndex); + }); + this.connect(widget, "_onBlur", function(){ + widget.set("tabIndex", "-1"); + }); + }, + + _onContainerFocus: function(evt){ + // summary: + // Handler for when the container gets focus + // description: + // Initially the container itself has a tabIndex, but when it gets + // focus, switch focus to first child... + // tags: + // private + + // Note that we can't use _onFocus() because switching focus from the + // _onFocus() handler confuses the focus.js code + // (because it causes _onFocusNode() to be called recursively) + + // focus bubbles on Firefox, + // so just make sure that focus has really gone to the container + if(evt.target !== this.domNode){ return; } + + this.focusFirstChild(); + + // and then set the container's tabIndex to -1, + // (don't remove as that breaks Safari 4) + // so that tab or shift-tab will go to the fields after/before + // the container, rather than the container itself + dojo.attr(this.domNode, "tabIndex", "-1"); + }, + + _onBlur: function(evt){ + // When focus is moved away the container, and its descendant (popup) widgets, + // then restore the container's tabIndex so that user can tab to it again. + // Note that using _onBlur() so that this doesn't happen when focus is shifted + // to one of my child widgets (typically a popup) + if(this.tabIndex){ + dojo.attr(this.domNode, "tabIndex", this.tabIndex); + } + this.inherited(arguments); + }, + + _onContainerKeypress: function(evt){ + // summary: + // When a key is pressed, if it's an arrow key etc. then + // it's handled here. + // tags: + // private + if(evt.ctrlKey || evt.altKey){ return; } + var func = this._keyNavCodes[evt.charOrCode]; + if(func){ + func(); + dojo.stopEvent(evt); + } + }, + + _onChildBlur: function(/*dijit._Widget*/ widget){ + // summary: + // Called when focus leaves a child widget to go + // to a sibling widget. + // tags: + // protected + }, + + _getFirstFocusableChild: function(){ + // summary: + // Returns first child that can be focused + return this._getNextFocusableChild(null, 1); // dijit._Widget + }, + + _getNextFocusableChild: function(child, dir){ + // summary: + // Returns the next or previous focusable child, compared + // to "child" + // child: Widget + // The current widget + // dir: Integer + // * 1 = after + // * -1 = before + if(child){ + child = this._getSiblingOfChild(child, dir); + } + var children = this.getChildren(); + for(var i=0; i < children.length; i++){ + if(!child){ + child = children[(dir>0) ? 0 : (children.length-1)]; + } + if(child.isFocusable()){ + return child; // dijit._Widget + } + child = this._getSiblingOfChild(child, dir); + } + // no focusable child found + return null; // dijit._Widget + } + } +); + +} + +if(!dojo._hasResource["dijit.MenuItem"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.MenuItem"] = true; +dojo.provide("dijit.MenuItem"); + + + + + + +dojo.declare("dijit.MenuItem", + [dijit._Widget, dijit._Templated, dijit._Contained, dijit._CssStateMixin], + { + // summary: + // A line item in a Menu Widget + + // Make 3 columns + // icon, label, and expand arrow (BiDi-dependent) indicating sub-menu + templateString: dojo.cache("dijit", "templates/MenuItem.html", "<tr class=\"dijitReset dijitMenuItem\" dojoAttachPoint=\"focusNode\" waiRole=\"menuitem\" tabIndex=\"-1\"\n\t\tdojoAttachEvent=\"onmouseenter:_onHover,onmouseleave:_onUnhover,ondijitclick:_onClick\">\n\t<td class=\"dijitReset dijitMenuItemIconCell\" waiRole=\"presentation\">\n\t\t<img src=\"${_blankGif}\" alt=\"\" class=\"dijitIcon dijitMenuItemIcon\" dojoAttachPoint=\"iconNode\"/>\n\t</td>\n\t<td class=\"dijitReset dijitMenuItemLabel\" colspan=\"2\" dojoAttachPoint=\"containerNode\"></td>\n\t<td class=\"dijitReset dijitMenuItemAccelKey\" style=\"display: none\" dojoAttachPoint=\"accelKeyNode\"></td>\n\t<td class=\"dijitReset dijitMenuArrowCell\" waiRole=\"presentation\">\n\t\t<div dojoAttachPoint=\"arrowWrapper\" style=\"visibility: hidden\">\n\t\t\t<img src=\"${_blankGif}\" alt=\"\" class=\"dijitMenuExpand\"/>\n\t\t\t<span class=\"dijitMenuExpandA11y\">+</span>\n\t\t</div>\n\t</td>\n</tr>\n"), + + attributeMap: dojo.delegate(dijit._Widget.prototype.attributeMap, { + label: { node: "containerNode", type: "innerHTML" }, + iconClass: { node: "iconNode", type: "class" } + }), + + baseClass: "dijitMenuItem", + + // label: String + // Menu text + label: '', + + // iconClass: String + // Class to apply to DOMNode to make it display an icon. + iconClass: "", + + // accelKey: String + // Text for the accelerator (shortcut) key combination. + // Note that although Menu can display accelerator keys there + // is no infrastructure to actually catch and execute these + // accelerators. + accelKey: "", + + // disabled: Boolean + // If true, the menu item is disabled. + // If false, the menu item is enabled. + disabled: false, + + _fillContent: function(/*DomNode*/ source){ + // If button label is specified as srcNodeRef.innerHTML rather than + // this.params.label, handle it here. + if(source && !("label" in this.params)){ + this.set('label', source.innerHTML); + } + }, + + postCreate: function(){ + this.inherited(arguments); + dojo.setSelectable(this.domNode, false); + var label = this.id+"_text"; + dojo.attr(this.containerNode, "id", label); + if(this.accelKeyNode){ + dojo.attr(this.accelKeyNode, "id", this.id + "_accel"); + label += " " + this.id + "_accel"; + } + dijit.setWaiState(this.domNode, "labelledby", label); + }, + + _onHover: function(){ + // summary: + // Handler when mouse is moved onto menu item + // tags: + // protected + this.getParent().onItemHover(this); + }, + + _onUnhover: function(){ + // summary: + // Handler when mouse is moved off of menu item, + // possibly to a child menu, or maybe to a sibling + // menuitem or somewhere else entirely. + // tags: + // protected + + // if we are unhovering the currently selected item + // then unselect it + this.getParent().onItemUnhover(this); + + // _onUnhover() is called when the menu is hidden (collapsed), due to clicking + // a MenuItem and having it execut. When that happens, FF and IE don't generate + // an onmouseout event for the MenuItem, so give _CssStateMixin some help + this._hovering = false; + this._setStateClass(); + }, + + _onClick: function(evt){ + // summary: + // Internal handler for click events on MenuItem. + // tags: + // private + this.getParent().onItemClick(this, evt); + dojo.stopEvent(evt); + }, + + onClick: function(/*Event*/ evt){ + // summary: + // User defined function to handle clicks + // tags: + // callback + }, + + focus: function(){ + // summary: + // Focus on this MenuItem + try{ + if(dojo.isIE == 8){ + // needed for IE8 which won't scroll TR tags into view on focus yet calling scrollIntoView creates flicker (#10275) + this.containerNode.focus(); + } + dijit.focus(this.focusNode); + }catch(e){ + // this throws on IE (at least) in some scenarios + } + }, + + _onFocus: function(){ + // summary: + // This is called by the focus manager when focus + // goes to this MenuItem or a child menu. + // tags: + // protected + this._setSelected(true); + this.getParent()._onItemFocus(this); + + this.inherited(arguments); + }, + + _setSelected: function(selected){ + // summary: + // Indicate that this node is the currently selected one + // tags: + // private + + /*** + * TODO: remove this method and calls to it, when _onBlur() is working for MenuItem. + * Currently _onBlur() gets called when focus is moved from the MenuItem to a child menu. + * That's not supposed to happen, but the problem is: + * In order to allow dijit.popup's getTopPopup() to work,a sub menu's popupParent + * points to the parent Menu, bypassing the parent MenuItem... thus the + * MenuItem is not in the chain of active widgets and gets a premature call to + * _onBlur() + */ + + dojo.toggleClass(this.domNode, "dijitMenuItemSelected", selected); + }, + + setLabel: function(/*String*/ content){ + // summary: + // Deprecated. Use set('label', ...) instead. + // tags: + // deprecated + dojo.deprecated("dijit.MenuItem.setLabel() is deprecated. Use set('label', ...) instead.", "", "2.0"); + this.set("label", content); + }, + + setDisabled: function(/*Boolean*/ disabled){ + // summary: + // Deprecated. Use set('disabled', bool) instead. + // tags: + // deprecated + dojo.deprecated("dijit.Menu.setDisabled() is deprecated. Use set('disabled', bool) instead.", "", "2.0"); + this.set('disabled', disabled); + }, + _setDisabledAttr: function(/*Boolean*/ value){ + // summary: + // Hook for attr('disabled', ...) to work. + // Enable or disable this menu item. + this.disabled = value; + dijit.setWaiState(this.focusNode, 'disabled', value ? 'true' : 'false'); + }, + _setAccelKeyAttr: function(/*String*/ value){ + // summary: + // Hook for attr('accelKey', ...) to work. + // Set accelKey on this menu item. + this.accelKey=value; + + this.accelKeyNode.style.display=value?"":"none"; + this.accelKeyNode.innerHTML=value; + //have to use colSpan to make it work in IE + dojo.attr(this.containerNode,'colSpan',value?"1":"2"); + } + }); + +} + +if(!dojo._hasResource["dijit.PopupMenuItem"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.PopupMenuItem"] = true; +dojo.provide("dijit.PopupMenuItem"); + + + +dojo.declare("dijit.PopupMenuItem", + dijit.MenuItem, + { + _fillContent: function(){ + // summary: + // When Menu is declared in markup, this code gets the menu label and + // the popup widget from the srcNodeRef. + // description: + // srcNodeRefinnerHTML contains both the menu item text and a popup widget + // The first part holds the menu item text and the second part is the popup + // example: + // | <div dojoType="dijit.PopupMenuItem"> + // | <span>pick me</span> + // | <popup> ... </popup> + // | </div> + // tags: + // protected + + if(this.srcNodeRef){ + var nodes = dojo.query("*", this.srcNodeRef); + dijit.PopupMenuItem.superclass._fillContent.call(this, nodes[0]); + + // save pointer to srcNode so we can grab the drop down widget after it's instantiated + this.dropDownContainer = this.srcNodeRef; + } + }, + + startup: function(){ + if(this._started){ return; } + this.inherited(arguments); + + // we didn't copy the dropdown widget from the this.srcNodeRef, so it's in no-man's + // land now. move it to dojo.doc.body. + if(!this.popup){ + var node = dojo.query("[widgetId]", this.dropDownContainer)[0]; + this.popup = dijit.byNode(node); + } + dojo.body().appendChild(this.popup.domNode); + this.popup.startup(); + + this.popup.domNode.style.display="none"; + if(this.arrowWrapper){ + dojo.style(this.arrowWrapper, "visibility", ""); + } + dijit.setWaiState(this.focusNode, "haspopup", "true"); + }, + + destroyDescendants: function(){ + if(this.popup){ + // Destroy the popup, unless it's already been destroyed. This can happen because + // the popup is a direct child of <body> even though it's logically my child. + if(!this.popup._destroyed){ + this.popup.destroyRecursive(); + } + delete this.popup; + } + this.inherited(arguments); + } + }); + + +} + +if(!dojo._hasResource["dijit.CheckedMenuItem"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.CheckedMenuItem"] = true; +dojo.provide("dijit.CheckedMenuItem"); + + + +dojo.declare("dijit.CheckedMenuItem", + dijit.MenuItem, + { + // summary: + // A checkbox-like menu item for toggling on and off + + templateString: dojo.cache("dijit", "templates/CheckedMenuItem.html", "<tr class=\"dijitReset dijitMenuItem\" dojoAttachPoint=\"focusNode\" waiRole=\"menuitemcheckbox\" tabIndex=\"-1\"\n\t\tdojoAttachEvent=\"onmouseenter:_onHover,onmouseleave:_onUnhover,ondijitclick:_onClick\">\n\t<td class=\"dijitReset dijitMenuItemIconCell\" waiRole=\"presentation\">\n\t\t<img src=\"${_blankGif}\" alt=\"\" class=\"dijitMenuItemIcon dijitCheckedMenuItemIcon\" dojoAttachPoint=\"iconNode\"/>\n\t\t<span class=\"dijitCheckedMenuItemIconChar\">✓</span>\n\t</td>\n\t<td class=\"dijitReset dijitMenuItemLabel\" colspan=\"2\" dojoAttachPoint=\"containerNode,labelNode\"></td>\n\t<td class=\"dijitReset dijitMenuItemAccelKey\" style=\"display: none\" dojoAttachPoint=\"accelKeyNode\"></td>\n\t<td class=\"dijitReset dijitMenuArrowCell\" waiRole=\"presentation\"> </td>\n</tr>\n"), + + // checked: Boolean + // Our checked state + checked: false, + _setCheckedAttr: function(/*Boolean*/ checked){ + // summary: + // Hook so attr('checked', bool) works. + // Sets the class and state for the check box. + dojo.toggleClass(this.domNode, "dijitCheckedMenuItemChecked", checked); + dijit.setWaiState(this.domNode, "checked", checked); + this.checked = checked; + }, + + onChange: function(/*Boolean*/ checked){ + // summary: + // User defined function to handle check/uncheck events + // tags: + // callback + }, + + _onClick: function(/*Event*/ e){ + // summary: + // Clicking this item just toggles its state + // tags: + // private + if(!this.disabled){ + this.set("checked", !this.checked); + this.onChange(this.checked); + } + this.inherited(arguments); + } + }); + +} + +if(!dojo._hasResource["dijit.MenuSeparator"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.MenuSeparator"] = true; +dojo.provide("dijit.MenuSeparator"); + + + + + +dojo.declare("dijit.MenuSeparator", + [dijit._Widget, dijit._Templated, dijit._Contained], + { + // summary: + // A line between two menu items + + templateString: dojo.cache("dijit", "templates/MenuSeparator.html", "<tr class=\"dijitMenuSeparator\">\n\t<td class=\"dijitMenuSeparatorIconCell\">\n\t\t<div class=\"dijitMenuSeparatorTop\"></div>\n\t\t<div class=\"dijitMenuSeparatorBottom\"></div>\n\t</td>\n\t<td colspan=\"3\" class=\"dijitMenuSeparatorLabelCell\">\n\t\t<div class=\"dijitMenuSeparatorTop dijitMenuSeparatorLabel\"></div>\n\t\t<div class=\"dijitMenuSeparatorBottom\"></div>\n\t</td>\n</tr>\n"), + + postCreate: function(){ + dojo.setSelectable(this.domNode, false); + }, + + isFocusable: function(){ + // summary: + // Override to always return false + // tags: + // protected + + return false; // Boolean + } + }); + + +} + +if(!dojo._hasResource["dijit.Menu"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.Menu"] = true; +dojo.provide("dijit.Menu"); + + + + + + + +dojo.declare("dijit._MenuBase", + [dijit._Widget, dijit._Templated, dijit._KeyNavContainer], +{ + // summary: + // Base class for Menu and MenuBar + + // parentMenu: [readonly] Widget + // pointer to menu that displayed me + parentMenu: null, + + // popupDelay: Integer + // number of milliseconds before hovering (without clicking) causes the popup to automatically open. + popupDelay: 500, + + startup: function(){ + if(this._started){ return; } + + dojo.forEach(this.getChildren(), function(child){ child.startup(); }); + this.startupKeyNavChildren(); + + this.inherited(arguments); + }, + + onExecute: function(){ + // summary: + // Attach point for notification about when a menu item has been executed. + // This is an internal mechanism used for Menus to signal to their parent to + // close them, because they are about to execute the onClick handler. In + // general developers should not attach to or override this method. + // tags: + // protected + }, + + onCancel: function(/*Boolean*/ closeAll){ + // summary: + // Attach point for notification about when the user cancels the current menu + // This is an internal mechanism used for Menus to signal to their parent to + // close them. In general developers should not attach to or override this method. + // tags: + // protected + }, + + _moveToPopup: function(/*Event*/ evt){ + // summary: + // This handles the right arrow key (left arrow key on RTL systems), + // which will either open a submenu, or move to the next item in the + // ancestor MenuBar + // tags: + // private + + if(this.focusedChild && this.focusedChild.popup && !this.focusedChild.disabled){ + this.focusedChild._onClick(evt); + }else{ + var topMenu = this._getTopMenu(); + if(topMenu && topMenu._isMenuBar){ + topMenu.focusNext(); + } + } + }, + + _onPopupHover: function(/*Event*/ evt){ + // summary: + // This handler is called when the mouse moves over the popup. + // tags: + // private + + // if the mouse hovers over a menu popup that is in pending-close state, + // then stop the close operation. + // This can't be done in onItemHover since some popup targets don't have MenuItems (e.g. ColorPicker) + if(this.currentPopup && this.currentPopup._pendingClose_timer){ + var parentMenu = this.currentPopup.parentMenu; + // highlight the parent menu item pointing to this popup + if(parentMenu.focusedChild){ + parentMenu.focusedChild._setSelected(false); + } + parentMenu.focusedChild = this.currentPopup.from_item; + parentMenu.focusedChild._setSelected(true); + // cancel the pending close + this._stopPendingCloseTimer(this.currentPopup); + } + }, + + onItemHover: function(/*MenuItem*/ item){ + // summary: + // Called when cursor is over a MenuItem. + // tags: + // protected + + // Don't do anything unless user has "activated" the menu by: + // 1) clicking it + // 2) opening it from a parent menu (which automatically focuses it) + if(this.isActive){ + this.focusChild(item); + if(this.focusedChild.popup && !this.focusedChild.disabled && !this.hover_timer){ + this.hover_timer = setTimeout(dojo.hitch(this, "_openPopup"), this.popupDelay); + } + } + // if the user is mixing mouse and keyboard navigation, + // then the menu may not be active but a menu item has focus, + // but it's not the item that the mouse just hovered over. + // To avoid both keyboard and mouse selections, use the latest. + if(this.focusedChild){ + this.focusChild(item); + } + this._hoveredChild = item; + }, + + _onChildBlur: function(item){ + // summary: + // Called when a child MenuItem becomes inactive because focus + // has been removed from the MenuItem *and* it's descendant menus. + // tags: + // private + this._stopPopupTimer(); + item._setSelected(false); + // Close all popups that are open and descendants of this menu + var itemPopup = item.popup; + if(itemPopup){ + this._stopPendingCloseTimer(itemPopup); + itemPopup._pendingClose_timer = setTimeout(function(){ + itemPopup._pendingClose_timer = null; + if(itemPopup.parentMenu){ + itemPopup.parentMenu.currentPopup = null; + } + dijit.popup.close(itemPopup); // this calls onClose + }, this.popupDelay); + } + }, + + onItemUnhover: function(/*MenuItem*/ item){ + // summary: + // Callback fires when mouse exits a MenuItem + // tags: + // protected + + if(this.isActive){ + this._stopPopupTimer(); + } + if(this._hoveredChild == item){ this._hoveredChild = null; } + }, + + _stopPopupTimer: function(){ + // summary: + // Cancels the popup timer because the user has stop hovering + // on the MenuItem, etc. + // tags: + // private + if(this.hover_timer){ + clearTimeout(this.hover_timer); + this.hover_timer = null; + } + }, + + _stopPendingCloseTimer: function(/*dijit._Widget*/ popup){ + // summary: + // Cancels the pending-close timer because the close has been preempted + // tags: + // private + if(popup._pendingClose_timer){ + clearTimeout(popup._pendingClose_timer); + popup._pendingClose_timer = null; + } + }, + + _stopFocusTimer: function(){ + // summary: + // Cancels the pending-focus timer because the menu was closed before focus occured + // tags: + // private + if(this._focus_timer){ + clearTimeout(this._focus_timer); + this._focus_timer = null; + } + }, + + _getTopMenu: function(){ + // summary: + // Returns the top menu in this chain of Menus + // tags: + // private + for(var top=this; top.parentMenu; top=top.parentMenu); + return top; + }, + + onItemClick: function(/*dijit._Widget*/ item, /*Event*/ evt){ + // summary: + // Handle clicks on an item. + // tags: + // private + + // this can't be done in _onFocus since the _onFocus events occurs asynchronously + if(typeof this.isShowingNow == 'undefined'){ // non-popup menu + this._markActive(); + } + + this.focusChild(item); + + if(item.disabled){ return false; } + + if(item.popup){ + this._openPopup(); + }else{ + // before calling user defined handler, close hierarchy of menus + // and restore focus to place it was when menu was opened + this.onExecute(); + + // user defined handler for click + item.onClick(evt); + } + }, + + _openPopup: function(){ + // summary: + // Open the popup to the side of/underneath the current menu item + // tags: + // protected + + this._stopPopupTimer(); + var from_item = this.focusedChild; + if(!from_item){ return; } // the focused child lost focus since the timer was started + var popup = from_item.popup; + if(popup.isShowingNow){ return; } + if(this.currentPopup){ + this._stopPendingCloseTimer(this.currentPopup); + dijit.popup.close(this.currentPopup); + } + popup.parentMenu = this; + popup.from_item = from_item; // helps finding the parent item that should be focused for this popup + var self = this; + dijit.popup.open({ + parent: this, + popup: popup, + around: from_item.domNode, + orient: this._orient || (this.isLeftToRight() ? + {'TR': 'TL', 'TL': 'TR', 'BR': 'BL', 'BL': 'BR'} : + {'TL': 'TR', 'TR': 'TL', 'BL': 'BR', 'BR': 'BL'}), + onCancel: function(){ // called when the child menu is canceled + // set isActive=false (_closeChild vs _cleanUp) so that subsequent hovering will NOT open child menus + // which seems aligned with the UX of most applications (e.g. notepad, wordpad, paint shop pro) + self.focusChild(from_item); // put focus back on my node + self._cleanUp(); // close the submenu (be sure this is done _after_ focus is moved) + from_item._setSelected(true); // oops, _cleanUp() deselected the item + self.focusedChild = from_item; // and unset focusedChild + }, + onExecute: dojo.hitch(this, "_cleanUp") + }); + + this.currentPopup = popup; + // detect mouseovers to handle lazy mouse movements that temporarily focus other menu items + popup.connect(popup.domNode, "onmouseenter", dojo.hitch(self, "_onPopupHover")); // cleaned up when the popped-up widget is destroyed on close + + if(popup.focus){ + // If user is opening the popup via keyboard (right arrow, or down arrow for MenuBar), + // if the cursor happens to collide with the popup, it will generate an onmouseover event + // even though the mouse wasn't moved. Use a setTimeout() to call popup.focus so that + // our focus() call overrides the onmouseover event, rather than vice-versa. (#8742) + popup._focus_timer = setTimeout(dojo.hitch(popup, function(){ + this._focus_timer = null; + this.focus(); + }), 0); + } + }, + + _markActive: function(){ + // summary: + // Mark this menu's state as active. + // Called when this Menu gets focus from: + // 1) clicking it (mouse or via space/arrow key) + // 2) being opened by a parent menu. + // This is not called just from mouse hover. + // Focusing a menu via TAB does NOT automatically set isActive + // since TAB is a navigation operation and not a selection one. + // For Windows apps, pressing the ALT key focuses the menubar + // menus (similar to TAB navigation) but the menu is not active + // (ie no dropdown) until an item is clicked. + this.isActive = true; + dojo.addClass(this.domNode, "dijitMenuActive"); + dojo.removeClass(this.domNode, "dijitMenuPassive"); + }, + + onOpen: function(/*Event*/ e){ + // summary: + // Callback when this menu is opened. + // This is called by the popup manager as notification that the menu + // was opened. + // tags: + // private + + this.isShowingNow = true; + this._markActive(); + }, + + _markInactive: function(){ + // summary: + // Mark this menu's state as inactive. + this.isActive = false; // don't do this in _onBlur since the state is pending-close until we get here + dojo.removeClass(this.domNode, "dijitMenuActive"); + dojo.addClass(this.domNode, "dijitMenuPassive"); + }, + + onClose: function(){ + // summary: + // Callback when this menu is closed. + // This is called by the popup manager as notification that the menu + // was closed. + // tags: + // private + + this._stopFocusTimer(); + this._markInactive(); + this.isShowingNow = false; + this.parentMenu = null; + }, + + _closeChild: function(){ + // summary: + // Called when submenu is clicked or focus is lost. Close hierarchy of menus. + // tags: + // private + this._stopPopupTimer(); + if(this.focusedChild){ // unhighlight the focused item + this.focusedChild._setSelected(false); + this.focusedChild._onUnhover(); + this.focusedChild = null; + } + if(this.currentPopup){ + // Close all popups that are open and descendants of this menu + dijit.popup.close(this.currentPopup); + this.currentPopup = null; + } + }, + + _onItemFocus: function(/*MenuItem*/ item){ + // summary: + // Called when child of this Menu gets focus from: + // 1) clicking it + // 2) tabbing into it + // 3) being opened by a parent menu. + // This is not called just from mouse hover. + if(this._hoveredChild && this._hoveredChild != item){ + this._hoveredChild._onUnhover(); // any previous mouse movement is trumped by focus selection + } + }, + + _onBlur: function(){ + // summary: + // Called when focus is moved away from this Menu and it's submenus. + // tags: + // protected + this._cleanUp(); + this.inherited(arguments); + }, + + _cleanUp: function(){ + // summary: + // Called when the user is done with this menu. Closes hierarchy of menus. + // tags: + // private + + this._closeChild(); // don't call this.onClose since that's incorrect for MenuBar's that never close + if(typeof this.isShowingNow == 'undefined'){ // non-popup menu doesn't call onClose + this._markInactive(); + } + } +}); + +dojo.declare("dijit.Menu", + dijit._MenuBase, + { + // summary + // A context menu you can assign to multiple elements + + // TODO: most of the code in here is just for context menu (right-click menu) + // support. In retrospect that should have been a separate class (dijit.ContextMenu). + // Split them for 2.0 + + constructor: function(){ + this._bindings = []; + }, + + templateString: dojo.cache("dijit", "templates/Menu.html", "<table class=\"dijit dijitMenu dijitMenuPassive dijitReset dijitMenuTable\" waiRole=\"menu\" tabIndex=\"${tabIndex}\" dojoAttachEvent=\"onkeypress:_onKeyPress\" cellspacing=0>\n\t<tbody class=\"dijitReset\" dojoAttachPoint=\"containerNode\"></tbody>\n</table>\n"), + + baseClass: "dijitMenu", + + // targetNodeIds: [const] String[] + // Array of dom node ids of nodes to attach to. + // Fill this with nodeIds upon widget creation and it becomes context menu for those nodes. + targetNodeIds: [], + + // contextMenuForWindow: [const] Boolean + // If true, right clicking anywhere on the window will cause this context menu to open. + // If false, must specify targetNodeIds. + contextMenuForWindow: false, + + // leftClickToOpen: [const] Boolean + // If true, menu will open on left click instead of right click, similiar to a file menu. + leftClickToOpen: false, + + // refocus: Boolean + // When this menu closes, re-focus the element which had focus before it was opened. + refocus: true, + + postCreate: function(){ + if(this.contextMenuForWindow){ + this.bindDomNode(dojo.body()); + }else{ + // TODO: should have _setTargetNodeIds() method to handle initialization and a possible + // later attr('targetNodeIds', ...) call. There's also a problem that targetNodeIds[] + // gets stale after calls to bindDomNode()/unBindDomNode() as it still is just the original list (see #9610) + dojo.forEach(this.targetNodeIds, this.bindDomNode, this); + } + var k = dojo.keys, l = this.isLeftToRight(); + this._openSubMenuKey = l ? k.RIGHT_ARROW : k.LEFT_ARROW; + this._closeSubMenuKey = l ? k.LEFT_ARROW : k.RIGHT_ARROW; + this.connectKeyNavHandlers([k.UP_ARROW], [k.DOWN_ARROW]); + }, + + _onKeyPress: function(/*Event*/ evt){ + // summary: + // Handle keyboard based menu navigation. + // tags: + // protected + + if(evt.ctrlKey || evt.altKey){ return; } + + switch(evt.charOrCode){ + case this._openSubMenuKey: + this._moveToPopup(evt); + dojo.stopEvent(evt); + break; + case this._closeSubMenuKey: + if(this.parentMenu){ + if(this.parentMenu._isMenuBar){ + this.parentMenu.focusPrev(); + }else{ + this.onCancel(false); + } + }else{ + dojo.stopEvent(evt); + } + break; + } + }, + + // thanks burstlib! + _iframeContentWindow: function(/* HTMLIFrameElement */iframe_el){ + // summary: + // Returns the window reference of the passed iframe + // tags: + // private + var win = dojo.window.get(this._iframeContentDocument(iframe_el)) || + // Moz. TODO: is this available when defaultView isn't? + this._iframeContentDocument(iframe_el)['__parent__'] || + (iframe_el.name && dojo.doc.frames[iframe_el.name]) || null; + return win; // Window + }, + + _iframeContentDocument: function(/* HTMLIFrameElement */iframe_el){ + // summary: + // Returns a reference to the document object inside iframe_el + // tags: + // protected + var doc = iframe_el.contentDocument // W3 + || (iframe_el.contentWindow && iframe_el.contentWindow.document) // IE + || (iframe_el.name && dojo.doc.frames[iframe_el.name] && dojo.doc.frames[iframe_el.name].document) + || null; + return doc; // HTMLDocument + }, + + bindDomNode: function(/*String|DomNode*/ node){ + // summary: + // Attach menu to given node + node = dojo.byId(node); + + var cn; // Connect node + + // Support context menus on iframes. Rather than binding to the iframe itself we need + // to bind to the <body> node inside the iframe. + if(node.tagName.toLowerCase() == "iframe"){ + var iframe = node, + win = this._iframeContentWindow(iframe); + cn = dojo.withGlobal(win, dojo.body); + }else{ + + // To capture these events at the top level, attach to <html>, not <body>. + // Otherwise right-click context menu just doesn't work. + cn = (node == dojo.body() ? dojo.doc.documentElement : node); + } + + + // "binding" is the object to track our connection to the node (ie, the parameter to bindDomNode()) + var binding = { + node: node, + iframe: iframe + }; + + // Save info about binding in _bindings[], and make node itself record index(+1) into + // _bindings[] array. Prefix w/_dijitMenu to avoid setting an attribute that may + // start with a number, which fails on FF/safari. + dojo.attr(node, "_dijitMenu" + this.id, this._bindings.push(binding)); + + // Setup the connections to monitor click etc., unless we are connecting to an iframe which hasn't finished + // loading yet, in which case we need to wait for the onload event first, and then connect + // On linux Shift-F10 produces the oncontextmenu event, but on Windows it doesn't, so + // we need to monitor keyboard events in addition to the oncontextmenu event. + var doConnects = dojo.hitch(this, function(cn){ + return [ + // TODO: when leftClickToOpen is true then shouldn't space/enter key trigger the menu, + // rather than shift-F10? + dojo.connect(cn, this.leftClickToOpen ? "onclick" : "oncontextmenu", this, function(evt){ + // Schedule context menu to be opened unless it's already been scheduled from onkeydown handler + dojo.stopEvent(evt); + this._scheduleOpen(evt.target, iframe, {x: evt.pageX, y: evt.pageY}); + }), + dojo.connect(cn, "onkeydown", this, function(evt){ + if(evt.shiftKey && evt.keyCode == dojo.keys.F10){ + dojo.stopEvent(evt); + this._scheduleOpen(evt.target, iframe); // no coords - open near target node + } + }) + ]; + }); + binding.connects = cn ? doConnects(cn) : []; + + if(iframe){ + // Setup handler to [re]bind to the iframe when the contents are initially loaded, + // and every time the contents change. + // Need to do this b/c we are actually binding to the iframe's <body> node. + // Note: can't use dojo.connect(), see #9609. + + binding.onloadHandler = dojo.hitch(this, function(){ + // want to remove old connections, but IE throws exceptions when trying to + // access the <body> node because it's already gone, or at least in a state of limbo + + var win = this._iframeContentWindow(iframe); + cn = dojo.withGlobal(win, dojo.body); + binding.connects = doConnects(cn); + }); + if(iframe.addEventListener){ + iframe.addEventListener("load", binding.onloadHandler, false); + }else{ + iframe.attachEvent("onload", binding.onloadHandler); + } + } + }, + + unBindDomNode: function(/*String|DomNode*/ nodeName){ + // summary: + // Detach menu from given node + + var node; + try{ + node = dojo.byId(nodeName); + }catch(e){ + // On IE the dojo.byId() call will get an exception if the attach point was + // the <body> node of an <iframe> that has since been reloaded (and thus the + // <body> node is in a limbo state of destruction. + return; + } + + // node["_dijitMenu" + this.id] contains index(+1) into my _bindings[] array + var attrName = "_dijitMenu" + this.id; + if(node && dojo.hasAttr(node, attrName)){ + var bid = dojo.attr(node, attrName)-1, b = this._bindings[bid]; + dojo.forEach(b.connects, dojo.disconnect); + + // Remove listener for iframe onload events + var iframe = b.iframe; + if(iframe){ + if(iframe.removeEventListener){ + iframe.removeEventListener("load", b.onloadHandler, false); + }else{ + iframe.detachEvent("onload", b.onloadHandler); + } + } + + dojo.removeAttr(node, attrName); + delete this._bindings[bid]; + } + }, + + _scheduleOpen: function(/*DomNode?*/ target, /*DomNode?*/ iframe, /*Object?*/ coords){ + // summary: + // Set timer to display myself. Using a timer rather than displaying immediately solves + // two problems: + // + // 1. IE: without the delay, focus work in "open" causes the system + // context menu to appear in spite of stopEvent. + // + // 2. Avoid double-shows on linux, where shift-F10 generates an oncontextmenu event + // even after a dojo.stopEvent(e). (Shift-F10 on windows doesn't generate the + // oncontextmenu event.) + + if(!this._openTimer){ + this._openTimer = setTimeout(dojo.hitch(this, function(){ + delete this._openTimer; + this._openMyself({ + target: target, + iframe: iframe, + coords: coords + }); + }), 1); + } + }, + + _openMyself: function(args){ + // summary: + // Internal function for opening myself when the user does a right-click or something similar. + // args: + // This is an Object containing: + // * target: + // The node that is being clicked + // * iframe: + // If an <iframe> is being clicked, iframe points to that iframe + // * coords: + // Put menu at specified x/y position in viewport, or if iframe is + // specified, then relative to iframe. + // + // _openMyself() formerly took the event object, and since various code references + // evt.target (after connecting to _openMyself()), using an Object for parameters + // (so that old code still works). + + var target = args.target, + iframe = args.iframe, + coords = args.coords; + + // Get coordinates to open menu, either at specified (mouse) position or (if triggered via keyboard) + // then near the node the menu is assigned to. + if(coords){ + if(iframe){ + // Specified coordinates are on <body> node of an <iframe>, convert to match main document + var od = target.ownerDocument, + ifc = dojo.position(iframe, true), + win = this._iframeContentWindow(iframe), + scroll = dojo.withGlobal(win, "_docScroll", dojo); + + var cs = dojo.getComputedStyle(iframe), + tp = dojo._toPixelValue, + left = (dojo.isIE && dojo.isQuirks ? 0 : tp(iframe, cs.paddingLeft)) + (dojo.isIE && dojo.isQuirks ? tp(iframe, cs.borderLeftWidth) : 0), + top = (dojo.isIE && dojo.isQuirks ? 0 : tp(iframe, cs.paddingTop)) + (dojo.isIE && dojo.isQuirks ? tp(iframe, cs.borderTopWidth) : 0); + + coords.x += ifc.x + left - scroll.x; + coords.y += ifc.y + top - scroll.y; + } + }else{ + coords = dojo.position(target, true); + coords.x += 10; + coords.y += 10; + } + + var self=this; + var savedFocus = dijit.getFocus(this); + function closeAndRestoreFocus(){ + // user has clicked on a menu or popup + if(self.refocus){ + dijit.focus(savedFocus); + } + dijit.popup.close(self); + } + dijit.popup.open({ + popup: this, + x: coords.x, + y: coords.y, + onExecute: closeAndRestoreFocus, + onCancel: closeAndRestoreFocus, + orient: this.isLeftToRight() ? 'L' : 'R' + }); + this.focus(); + + this._onBlur = function(){ + this.inherited('_onBlur', arguments); + // Usually the parent closes the child widget but if this is a context + // menu then there is no parent + dijit.popup.close(this); + // don't try to restore focus; user has clicked another part of the screen + // and set focus there + }; + }, + + uninitialize: function(){ + dojo.forEach(this._bindings, function(b){ if(b){ this.unBindDomNode(b.node); } }, this); + this.inherited(arguments); + } +} +); + +// Back-compat (TODO: remove in 2.0) + + + + + + +} + +if(!dojo._hasResource["dijit.form.Select"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.form.Select"] = true; +dojo.provide("dijit.form.Select"); + + + + + + + + +dojo.declare("dijit.form._SelectMenu", dijit.Menu, { + // summary: + // An internally-used menu for dropdown that allows us a vertical scrollbar + buildRendering: function(){ + // summary: + // Stub in our own changes, so that our domNode is not a table + // otherwise, we won't respond correctly to heights/overflows + this.inherited(arguments); + var o = (this.menuTableNode = this.domNode); + var n = (this.domNode = dojo.create("div", {style: {overflowX: "hidden", overflowY: "scroll"}})); + if(o.parentNode){ + o.parentNode.replaceChild(n, o); + } + dojo.removeClass(o, "dijitMenuTable"); + n.className = o.className + " dijitSelectMenu"; + o.className = "dijitReset dijitMenuTable"; + dijit.setWaiRole(o,"listbox"); + dijit.setWaiRole(n,"presentation"); + n.appendChild(o); + }, + resize: function(/*Object*/ mb){ + // summary: + // Overridden so that we are able to handle resizing our + // internal widget. Note that this is not a "full" resize + // implementation - it only works correctly if you pass it a + // marginBox. + // + // mb: Object + // The margin box to set this dropdown to. + if(mb){ + dojo.marginBox(this.domNode, mb); + if("w" in mb){ + // We've explicitly set the wrapper <div>'s width, so set <table> width to match. + // 100% is safer than a pixel value because there may be a scroll bar with + // browser/OS specific width. + this.menuTableNode.style.width = "100%"; + } + } + } +}); + +dojo.declare("dijit.form.Select", [dijit.form._FormSelectWidget, dijit._HasDropDown], { + // summary: + // This is a "styleable" select box - it is basically a DropDownButton which + // can take a <select> as its input. + + baseClass: "dijitSelect", + + templateString: dojo.cache("dijit.form", "templates/Select.html", "<table class=\"dijit dijitReset dijitInline dijitLeft\"\n\tdojoAttachPoint=\"_buttonNode,tableNode,focusNode\" cellspacing='0' cellpadding='0'\n\twaiRole=\"combobox\" waiState=\"haspopup-true\"\n\t><tbody waiRole=\"presentation\"><tr waiRole=\"presentation\"\n\t\t><td class=\"dijitReset dijitStretch dijitButtonContents dijitButtonNode\" waiRole=\"presentation\"\n\t\t\t><span class=\"dijitReset dijitInline dijitButtonText\" dojoAttachPoint=\"containerNode,_popupStateNode\"></span\n\t\t\t><input type=\"hidden\" ${!nameAttrSetting} dojoAttachPoint=\"valueNode\" value=\"${value}\" waiState=\"hidden-true\"\n\t\t/></td><td class=\"dijitReset dijitRight dijitButtonNode dijitArrowButton dijitDownArrowButton\"\n\t\t\t\tdojoAttachPoint=\"titleNode\" waiRole=\"presentation\"\n\t\t\t><div class=\"dijitReset dijitArrowButtonInner\" waiRole=\"presentation\"></div\n\t\t\t><div class=\"dijitReset dijitArrowButtonChar\" waiRole=\"presentation\">▼</div\n\t\t></td\n\t></tr></tbody\n></table>\n"), + + // attributeMap: Object + // Add in our style to be applied to the focus node + attributeMap: dojo.mixin(dojo.clone(dijit.form._FormSelectWidget.prototype.attributeMap),{style:"tableNode"}), + + // required: Boolean + // Can be true or false, default is false. + required: false, + + // state: String + // Shows current state (ie, validation result) of input (Normal, Warning, or Error) + state: "", + + // tooltipPosition: String[] + // See description of dijit.Tooltip.defaultPosition for details on this parameter. + tooltipPosition: [], + + // emptyLabel: string + // What to display in an "empty" dropdown + emptyLabel: "", + + // _isLoaded: Boolean + // Whether or not we have been loaded + _isLoaded: false, + + // _childrenLoaded: Boolean + // Whether or not our children have been loaded + _childrenLoaded: false, + + _fillContent: function(){ + // summary: + // Set the value to be the first, or the selected index + this.inherited(arguments); + if(this.options.length && !this.value && this.srcNodeRef){ + var si = this.srcNodeRef.selectedIndex; + this.value = this.options[si != -1 ? si : 0].value; + } + + // Create the dropDown widget + this.dropDown = new dijit.form._SelectMenu({id: this.id + "_menu"}); + dojo.addClass(this.dropDown.domNode, this.baseClass + "Menu"); + }, + + _getMenuItemForOption: function(/*dijit.form.__SelectOption*/ option){ + // summary: + // For the given option, return the menu item that should be + // used to display it. This can be overridden as needed + if(!option.value){ + // We are a separator (no label set for it) + return new dijit.MenuSeparator(); + }else{ + // Just a regular menu option + var click = dojo.hitch(this, "_setValueAttr", option); + var item = new dijit.MenuItem({ + option: option, + label: option.label, + onClick: click, + disabled: option.disabled || false + }); + dijit.setWaiRole(item.focusNode, "listitem"); + return item; + } + }, + + _addOptionItem: function(/*dijit.form.__SelectOption*/ option){ + // summary: + // For the given option, add an option to our dropdown. + // If the option doesn't have a value, then a separator is added + // in that place. + if(this.dropDown){ + this.dropDown.addChild(this._getMenuItemForOption(option)); + } + }, + + _getChildren: function(){ + if(!this.dropDown){ + return []; + } + return this.dropDown.getChildren(); + }, + + _loadChildren: function(/*Boolean*/ loadMenuItems){ + // summary: + // Resets the menu and the length attribute of the button - and + // ensures that the label is appropriately set. + // loadMenuItems: Boolean + // actually loads the child menu items - we only do this when we are + // populating for showing the dropdown. + + if(loadMenuItems === true){ + // this.inherited destroys this.dropDown's child widgets (MenuItems). + // Avoid this.dropDown (Menu widget) having a pointer to a destroyed widget (which will cause + // issues later in _setSelected). (see #10296) + if(this.dropDown){ + delete this.dropDown.focusedChild; + } + if(this.options.length){ + this.inherited(arguments); + }else{ + // Drop down menu is blank but add one blank entry just so something appears on the screen + // to let users know that they are no choices (mimicing native select behavior) + dojo.forEach(this._getChildren(), function(child){ child.destroyRecursive(); }); + var item = new dijit.MenuItem({label: " "}); + this.dropDown.addChild(item); + } + }else{ + this._updateSelection(); + } + + var len = this.options.length; + this._isLoaded = false; + this._childrenLoaded = true; + + if(!this._loadingStore){ + // Don't call this if we are loading - since we will handle it later + this._setValueAttr(this.value); + } + }, + + _setValueAttr: function(value){ + this.inherited(arguments); + dojo.attr(this.valueNode, "value", this.get("value")); + }, + + _setDisplay: function(/*String*/ newDisplay){ + // summary: + // sets the display for the given value (or values) + this.containerNode.innerHTML = '<span class="dijitReset dijitInline ' + this.baseClass + 'Label">' + + (newDisplay || this.emptyLabel || " ") + + '</span>'; + dijit.setWaiState(this.focusNode, "valuetext", (newDisplay || this.emptyLabel || " ") ); + }, + + validate: function(/*Boolean*/ isFocused){ + // summary: + // Called by oninit, onblur, and onkeypress. + // description: + // Show missing or invalid messages if appropriate, and highlight textbox field. + // Used when a select is initially set to no value and the user is required to + // set the value. + + var isValid = this.isValid(isFocused); + this.state = isValid ? "" : "Error"; + this._setStateClass(); + dijit.setWaiState(this.focusNode, "invalid", isValid ? "false" : "true"); + var message = isValid ? "" : this._missingMsg; + if(this._message !== message){ + this._message = message; + dijit.hideTooltip(this.domNode); + if(message){ + dijit.showTooltip(message, this.domNode, this.tooltipPosition, !this.isLeftToRight()); + } + } + return isValid; + }, + + isValid: function(/*Boolean*/ isFocused){ + // summary: + // Whether or not this is a valid value. The only way a Select + // can be invalid is when it's required but nothing is selected. + return (!this.required || !(/^\s*$/.test(this.value))); + }, + + reset: function(){ + // summary: + // Overridden so that the state will be cleared. + this.inherited(arguments); + dijit.hideTooltip(this.domNode); + this.state = ""; + this._setStateClass(); + delete this._message; + }, + + postMixInProperties: function(){ + // summary: + // set the missing message + this.inherited(arguments); + this._missingMsg = dojo.i18n.getLocalization("dijit.form", "validate", + this.lang).missingMessage; + }, + + postCreate: function(){ + this.inherited(arguments); + if(this.tableNode.style.width){ + dojo.addClass(this.domNode, this.baseClass + "FixedWidth"); + } + }, + + isLoaded: function(){ + return this._isLoaded; + }, + + loadDropDown: function(/*Function*/ loadCallback){ + // summary: + // populates the menu + this._loadChildren(true); + this._isLoaded = true; + loadCallback(); + }, + + closeDropDown: function(){ + // overriding _HasDropDown.closeDropDown() + this.inherited(arguments); + + if(this.dropDown && this.dropDown.menuTableNode){ + // Erase possible width: 100% setting from _SelectMenu.resize(). + // Leaving it would interfere with the next openDropDown() call, which + // queries the natural size of the drop down. + this.dropDown.menuTableNode.style.width = ""; + } + }, + + uninitialize: function(preserveDom){ + if(this.dropDown && !this.dropDown._destroyed){ + this.dropDown.destroyRecursive(preserveDom); + delete this.dropDown; + } + this.inherited(arguments); + } +}); + +} + +if(!dojo._hasResource["dijit.form.SimpleTextarea"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.form.SimpleTextarea"] = true; +dojo.provide("dijit.form.SimpleTextarea"); + + + +dojo.declare("dijit.form.SimpleTextarea", + dijit.form.TextBox, + { + // summary: + // A simple textarea that degrades, and responds to + // minimal LayoutContainer usage, and works with dijit.form.Form. + // Doesn't automatically size according to input, like Textarea. + // + // example: + // | <textarea dojoType="dijit.form.SimpleTextarea" name="foo" value="bar" rows=30 cols=40></textarea> + // + // example: + // | new dijit.form.SimpleTextarea({ rows:20, cols:30 }, "foo"); + + baseClass: "dijitTextBox dijitTextArea", + + attributeMap: dojo.delegate(dijit.form._FormValueWidget.prototype.attributeMap, { + rows:"textbox", cols: "textbox" + }), + + // rows: Number + // The number of rows of text. + rows: "3", + + // rows: Number + // The number of characters per line. + cols: "20", + + templateString: "<textarea ${!nameAttrSetting} dojoAttachPoint='focusNode,containerNode,textbox' autocomplete='off'></textarea>", + + postMixInProperties: function(){ + // Copy value from srcNodeRef, unless user specified a value explicitly (or there is no srcNodeRef) + if(!this.value && this.srcNodeRef){ + this.value = this.srcNodeRef.value; + } + this.inherited(arguments); + }, + + filter: function(/*String*/ value){ + // Override TextBox.filter to deal with newlines... specifically (IIRC) this is for IE which writes newlines + // as \r\n instead of just \n + if(value){ + value = value.replace(/\r/g,""); + } + return this.inherited(arguments); + }, + + postCreate: function(){ + this.inherited(arguments); + if(dojo.isIE && this.cols){ // attribute selectors is not supported in IE6 + dojo.addClass(this.textbox, "dijitTextAreaCols"); + } + }, + + _previousValue: "", + _onInput: function(/*Event?*/ e){ + // Override TextBox._onInput() to enforce maxLength restriction + if(this.maxLength){ + var maxLength = parseInt(this.maxLength); + var value = this.textbox.value.replace(/\r/g,''); + var overflow = value.length - maxLength; + if(overflow > 0){ + if(e){ dojo.stopEvent(e); } + var textarea = this.textbox; + if(textarea.selectionStart){ + var pos = textarea.selectionStart; + var cr = 0; + if(dojo.isOpera){ + cr = (this.textbox.value.substring(0,pos).match(/\r/g) || []).length; + } + this.textbox.value = value.substring(0,pos-overflow-cr)+value.substring(pos-cr); + textarea.setSelectionRange(pos-overflow, pos-overflow); + }else if(dojo.doc.selection){ //IE + textarea.focus(); + var range = dojo.doc.selection.createRange(); + // delete overflow characters + range.moveStart("character", -overflow); + range.text = ''; + // show cursor + range.select(); + } + } + this._previousValue = this.textbox.value; + } + this.inherited(arguments); + } +}); + +} + +if(!dojo._hasResource["dijit.InlineEditBox"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.InlineEditBox"] = true; +dojo.provide("dijit.InlineEditBox"); + + + + + + + + + + +dojo.declare("dijit.InlineEditBox", + dijit._Widget, + { + // summary: + // An element with in-line edit capabilites + // + // description: + // Behavior for an existing node (`<p>`, `<div>`, `<span>`, etc.) so that + // when you click it, an editor shows up in place of the original + // text. Optionally, Save and Cancel button are displayed below the edit widget. + // When Save is clicked, the text is pulled from the edit + // widget and redisplayed and the edit widget is again hidden. + // By default a plain Textarea widget is used as the editor (or for + // inline values a TextBox), but you can specify an editor such as + // dijit.Editor (for editing HTML) or a Slider (for adjusting a number). + // An edit widget must support the following API to be used: + // - displayedValue or value as initialization parameter, + // and available through set('displayedValue') / set('value') + // - void focus() + // - DOM-node focusNode = node containing editable text + + // editing: [readonly] Boolean + // Is the node currently in edit mode? + editing: false, + + // autoSave: Boolean + // Changing the value automatically saves it; don't have to push save button + // (and save button isn't even displayed) + autoSave: true, + + // buttonSave: String + // Save button label + buttonSave: "", + + // buttonCancel: String + // Cancel button label + buttonCancel: "", + + // renderAsHtml: Boolean + // Set this to true if the specified Editor's value should be interpreted as HTML + // rather than plain text (ex: `dijit.Editor`) + renderAsHtml: false, + + // editor: String + // Class name for Editor widget + editor: "dijit.form.TextBox", + + // editorWrapper: String + // Class name for widget that wraps the editor widget, displaying save/cancel + // buttons. + editorWrapper: "dijit._InlineEditor", + + // editorParams: Object + // Set of parameters for editor, like {required: true} + editorParams: {}, + + onChange: function(value){ + // summary: + // Set this handler to be notified of changes to value. + // tags: + // callback + }, + + onCancel: function(){ + // summary: + // Set this handler to be notified when editing is cancelled. + // tags: + // callback + }, + + // width: String + // Width of editor. By default it's width=100% (ie, block mode). + width: "100%", + + // value: String + // The display value of the widget in read-only mode + value: "", + + // noValueIndicator: [const] String + // The text that gets displayed when there is no value (so that the user has a place to click to edit) + noValueIndicator: dojo.isIE <= 6 ? // font-family needed on IE6 but it messes up IE8 + "<span style='font-family: wingdings; text-decoration: underline;'>    ✍    </span>" : + "<span style='text-decoration: underline;'>    ✍    </span>", + + constructor: function(){ + // summary: + // Sets up private arrays etc. + // tags: + // private + this.editorParams = {}; + }, + + postMixInProperties: function(){ + this.inherited(arguments); + + // save pointer to original source node, since Widget nulls-out srcNodeRef + this.displayNode = this.srcNodeRef; + + // connect handlers to the display node + var events = { + ondijitclick: "_onClick", + onmouseover: "_onMouseOver", + onmouseout: "_onMouseOut", + onfocus: "_onMouseOver", + onblur: "_onMouseOut" + }; + for(var name in events){ + this.connect(this.displayNode, name, events[name]); + } + dijit.setWaiRole(this.displayNode, "button"); + if(!this.displayNode.getAttribute("tabIndex")){ + this.displayNode.setAttribute("tabIndex", 0); + } + + if(!this.value && !("value" in this.params)){ // "" is a good value if specified directly so check params){ + this.value = dojo.trim(this.renderAsHtml ? this.displayNode.innerHTML : + (this.displayNode.innerText||this.displayNode.textContent||"")); + } + if(!this.value){ + this.displayNode.innerHTML = this.noValueIndicator; + } + + dojo.addClass(this.displayNode, 'dijitInlineEditBoxDisplayMode'); + }, + + setDisabled: function(/*Boolean*/ disabled){ + // summary: + // Deprecated. Use set('disabled', ...) instead. + // tags: + // deprecated + dojo.deprecated("dijit.InlineEditBox.setDisabled() is deprecated. Use set('disabled', bool) instead.", "", "2.0"); + this.set('disabled', disabled); + }, + + _setDisabledAttr: function(/*Boolean*/ disabled){ + // summary: + // Hook to make set("disabled", ...) work. + // Set disabled state of widget. + this.disabled = disabled; + dijit.setWaiState(this.domNode, "disabled", disabled); + if(disabled){ + this.displayNode.removeAttribute("tabIndex"); + }else{ + this.displayNode.setAttribute("tabIndex", 0); + } + dojo.toggleClass(this.displayNode, "dijitInlineEditBoxDisplayModeDisabled", disabled); + }, + + _onMouseOver: function(){ + // summary: + // Handler for onmouseover and onfocus event. + // tags: + // private + if(!this.disabled){ + dojo.addClass(this.displayNode, "dijitInlineEditBoxDisplayModeHover"); + } + }, + + _onMouseOut: function(){ + // summary: + // Handler for onmouseout and onblur event. + // tags: + // private + dojo.removeClass(this.displayNode, "dijitInlineEditBoxDisplayModeHover"); + }, + + _onClick: function(/*Event*/ e){ + // summary: + // Handler for onclick event. + // tags: + // private + if(this.disabled){ return; } + if(e){ dojo.stopEvent(e); } + this._onMouseOut(); + + // Since FF gets upset if you move a node while in an event handler for that node... + setTimeout(dojo.hitch(this, "edit"), 0); + }, + + edit: function(){ + // summary: + // Display the editor widget in place of the original (read only) markup. + // tags: + // private + + if(this.disabled || this.editing){ return; } + this.editing = true; + + // save some display node values that can be restored later + this._savedPosition = dojo.style(this.displayNode, "position") || "static"; + this._savedOpacity = dojo.style(this.displayNode, "opacity") || "1"; + this._savedTabIndex = dojo.attr(this.displayNode, "tabIndex") || "0"; + + if(this.wrapperWidget){ + var ew = this.wrapperWidget.editWidget; + ew.set("displayedValue" in ew ? "displayedValue" : "value", this.value); + }else{ + // Placeholder for edit widget + // Put place holder (and eventually editWidget) before the display node so that it's positioned correctly + // when Calendar dropdown appears, which happens automatically on focus. + var placeholder = dojo.create("span", null, this.domNode, "before"); + + // Create the editor wrapper (the thing that holds the editor widget and the save/cancel buttons) + var ewc = dojo.getObject(this.editorWrapper); + this.wrapperWidget = new ewc({ + value: this.value, + buttonSave: this.buttonSave, + buttonCancel: this.buttonCancel, + dir: this.dir, + lang: this.lang, + tabIndex: this._savedTabIndex, + editor: this.editor, + inlineEditBox: this, + sourceStyle: dojo.getComputedStyle(this.displayNode), + save: dojo.hitch(this, "save"), + cancel: dojo.hitch(this, "cancel") + }, placeholder); + } + var ww = this.wrapperWidget; + + if(dojo.isIE){ + dijit.focus(dijit.getFocus()); // IE (at least 8) needs help with tab order changes + } + // to avoid screen jitter, we first create the editor with position:absolute, visibility:hidden, + // and then when it's finished rendering, we switch from display mode to editor + // position:absolute releases screen space allocated to the display node + // opacity:0 is the same as visibility:hidden but is still focusable + // visiblity:hidden removes focus outline + + dojo.style(this.displayNode, { position: "absolute", opacity: "0", display: "none" }); // makes display node invisible, display style used for focus-ability + dojo.style(ww.domNode, { position: this._savedPosition, visibility: "visible", opacity: "1" }); + dojo.attr(this.displayNode, "tabIndex", "-1"); // needed by WebKit for TAB from editor to skip displayNode + + // Replace the display widget with edit widget, leaving them both displayed for a brief time so that + // focus can be shifted without incident. (browser may needs some time to render the editor.) + setTimeout(dojo.hitch(this, function(){ + ww.focus(); // both nodes are showing, so we can switch focus safely + ww._resetValue = ww.getValue(); + }), 0); + }, + + _onBlur: function(){ + // summary: + // Called when focus moves outside the InlineEditBox. + // Performs garbage collection. + // tags: + // private + + this.inherited(arguments); + if(!this.editing){ + /* causes IE focus problems, see TooltipDialog_a11y.html... + setTimeout(dojo.hitch(this, function(){ + if(this.wrapperWidget){ + this.wrapperWidget.destroy(); + delete this.wrapperWidget; + } + }), 0); + */ + } + }, + + destroy: function(){ + if(this.wrapperWidget){ + this.wrapperWidget.destroy(); + delete this.wrapperWidget; + } + this.inherited(arguments); + }, + + _showText: function(/*Boolean*/ focus){ + // summary: + // Revert to display mode, and optionally focus on display node + // tags: + // private + + var ww = this.wrapperWidget; + dojo.style(ww.domNode, { position: "absolute", visibility: "hidden", opacity: "0" }); // hide the editor from mouse/keyboard events + dojo.style(this.displayNode, { position: this._savedPosition, opacity: this._savedOpacity, display: "" }); // make the original text visible + dojo.attr(this.displayNode, "tabIndex", this._savedTabIndex); + if(focus){ + dijit.focus(this.displayNode); + } + }, + + save: function(/*Boolean*/ focus){ + // summary: + // Save the contents of the editor and revert to display mode. + // focus: Boolean + // Focus on the display mode text + // tags: + // private + + if(this.disabled || !this.editing){ return; } + this.editing = false; + + var ww = this.wrapperWidget; + var value = ww.getValue(); + this.set('value', value); // display changed, formatted value + + // tell the world that we have changed + setTimeout(dojo.hitch(this, "onChange", value), 0); // setTimeout prevents browser freeze for long-running event handlers + + this._showText(focus); // set focus as needed + }, + + setValue: function(/*String*/ val){ + // summary: + // Deprecated. Use set('value', ...) instead. + // tags: + // deprecated + dojo.deprecated("dijit.InlineEditBox.setValue() is deprecated. Use set('value', ...) instead.", "", "2.0"); + return this.set("value", val); + }, + + _setValueAttr: function(/*String*/ val){ + // summary: + // Hook to make set("value", ...) work. + // Inserts specified HTML value into this node, or an "input needed" character if node is blank. + + this.value = val = dojo.trim(val); + if(!this.renderAsHtml){ + val = val.replace(/&/gm, "&").replace(/</gm, "<").replace(/>/gm, ">").replace(/"/gm, """).replace(/\n/g, "<br>"); + } + this.displayNode.innerHTML = val || this.noValueIndicator; + }, + + getValue: function(){ + // summary: + // Deprecated. Use get('value') instead. + // tags: + // deprecated + dojo.deprecated("dijit.InlineEditBox.getValue() is deprecated. Use get('value') instead.", "", "2.0"); + return this.get("value"); + }, + + cancel: function(/*Boolean*/ focus){ + // summary: + // Revert to display mode, discarding any changes made in the editor + // tags: + // private + + if(this.disabled || !this.editing){ return; } + this.editing = false; + + // tell the world that we have no changes + setTimeout(dojo.hitch(this, "onCancel"), 0); // setTimeout prevents browser freeze for long-running event handlers + + this._showText(focus); + } +}); + +dojo.declare( + "dijit._InlineEditor", + [dijit._Widget, dijit._Templated], +{ + // summary: + // Internal widget used by InlineEditBox, displayed when in editing mode + // to display the editor and maybe save/cancel buttons. Calling code should + // connect to save/cancel methods to detect when editing is finished + // + // Has mainly the same parameters as InlineEditBox, plus these values: + // + // style: Object + // Set of CSS attributes of display node, to replicate in editor + // + // value: String + // Value as an HTML string or plain text string, depending on renderAsHTML flag + + templateString: dojo.cache("dijit", "templates/InlineEditBox.html", "<span dojoAttachPoint=\"editNode\" waiRole=\"presentation\" style=\"position: absolute; visibility:hidden\" class=\"dijitReset dijitInline\"\n\tdojoAttachEvent=\"onkeypress: _onKeyPress\"\n\t><span dojoAttachPoint=\"editorPlaceholder\"></span\n\t><span dojoAttachPoint=\"buttonContainer\"\n\t\t><button class='saveButton' dojoAttachPoint=\"saveButton\" dojoType=\"dijit.form.Button\" dojoAttachEvent=\"onClick:save\" label=\"${buttonSave}\"></button\n\t\t><button class='cancelButton' dojoAttachPoint=\"cancelButton\" dojoType=\"dijit.form.Button\" dojoAttachEvent=\"onClick:cancel\" label=\"${buttonCancel}\"></button\n\t></span\n></span>\n"), + widgetsInTemplate: true, + + postMixInProperties: function(){ + this.inherited(arguments); + this.messages = dojo.i18n.getLocalization("dijit", "common", this.lang); + dojo.forEach(["buttonSave", "buttonCancel"], function(prop){ + if(!this[prop]){ this[prop] = this.messages[prop]; } + }, this); + }, + + postCreate: function(){ + // Create edit widget in place in the template + var cls = dojo.getObject(this.editor); + + // Copy the style from the source + // Don't copy ALL properties though, just the necessary/applicable ones. + // wrapperStyle/destStyle code is to workaround IE bug where getComputedStyle().fontSize + // is a relative value like 200%, rather than an absolute value like 24px, and + // the 200% can refer *either* to a setting on the node or it's ancestor (see #11175) + var srcStyle = this.sourceStyle, + editStyle = "line-height:" + srcStyle.lineHeight + ";", + destStyle = dojo.getComputedStyle(this.domNode); + dojo.forEach(["Weight","Family","Size","Style"], function(prop){ + var textStyle = srcStyle["font"+prop], + wrapperStyle = destStyle["font"+prop]; + if(wrapperStyle != textStyle){ + editStyle += "font-"+prop+":"+srcStyle["font"+prop]+";"; + } + }, this); + dojo.forEach(["marginTop","marginBottom","marginLeft", "marginRight"], function(prop){ + this.domNode.style[prop] = srcStyle[prop]; + }, this); + var width = this.inlineEditBox.width; + if(width == "100%"){ + // block mode + editStyle += "width:100%;"; + this.domNode.style.display = "block"; + }else{ + // inline-block mode + editStyle += "width:" + (width + (Number(width) == width ? "px" : "")) + ";"; + } + var editorParams = dojo.delegate(this.inlineEditBox.editorParams, { + style: editStyle, + dir: this.dir, + lang: this.lang + }); + editorParams[ "displayedValue" in cls.prototype ? "displayedValue" : "value"] = this.value; + var ew = (this.editWidget = new cls(editorParams, this.editorPlaceholder)); + + if(this.inlineEditBox.autoSave){ + // Remove the save/cancel buttons since saving is done by simply tabbing away or + // selecting a value from the drop down list + dojo.destroy(this.buttonContainer); + + // Selecting a value from a drop down list causes an onChange event and then we save + this.connect(ew, "onChange", "_onChange"); + + // ESC and TAB should cancel and save. Note that edit widgets do a stopEvent() on ESC key (to + // prevent Dialog from closing when the user just wants to revert the value in the edit widget), + // so this is the only way we can see the key press event. + this.connect(ew, "onKeyPress", "_onKeyPress"); + }else{ + // If possible, enable/disable save button based on whether the user has changed the value + if("intermediateChanges" in cls.prototype){ + ew.set("intermediateChanges", true); + this.connect(ew, "onChange", "_onIntermediateChange"); + this.saveButton.set("disabled", true); + } + } + }, + + _onIntermediateChange: function(val){ + // summary: + // Called for editor widgets that support the intermediateChanges=true flag as a way + // to detect when to enable/disabled the save button + this.saveButton.set("disabled", (this.getValue() == this._resetValue) || !this.enableSave()); + }, + + destroy: function(){ + this.editWidget.destroy(true); // let the parent wrapper widget clean up the DOM + this.inherited(arguments); + }, + + getValue: function(){ + // summary: + // Return the [display] value of the edit widget + var ew = this.editWidget; + return String(ew.get("displayedValue" in ew ? "displayedValue" : "value")); + }, + + _onKeyPress: function(e){ + // summary: + // Handler for keypress in the edit box in autoSave mode. + // description: + // For autoSave widgets, if Esc/Enter, call cancel/save. + // tags: + // private + + if(this.inlineEditBox.autoSave && this.inlineEditBox.editing){ + if(e.altKey || e.ctrlKey){ return; } + // If Enter/Esc pressed, treat as save/cancel. + if(e.charOrCode == dojo.keys.ESCAPE){ + dojo.stopEvent(e); + this.cancel(true); // sets editing=false which short-circuits _onBlur processing + }else if(e.charOrCode == dojo.keys.ENTER && e.target.tagName == "INPUT"){ + dojo.stopEvent(e); + this._onChange(); // fire _onBlur and then save + } + + // _onBlur will handle TAB automatically by allowing + // the TAB to change focus before we mess with the DOM: #6227 + // Expounding by request: + // The current focus is on the edit widget input field. + // save() will hide and destroy this widget. + // We want the focus to jump from the currently hidden + // displayNode, but since it's hidden, it's impossible to + // unhide it, focus it, and then have the browser focus + // away from it to the next focusable element since each + // of these events is asynchronous and the focus-to-next-element + // is already queued. + // So we allow the browser time to unqueue the move-focus event + // before we do all the hide/show stuff. + } + }, + + _onBlur: function(){ + // summary: + // Called when focus moves outside the editor + // tags: + // private + + this.inherited(arguments); + if(this.inlineEditBox.autoSave && this.inlineEditBox.editing){ + if(this.getValue() == this._resetValue){ + this.cancel(false); + }else if(this.enableSave()){ + this.save(false); + } + } + }, + + _onChange: function(){ + // summary: + // Called when the underlying widget fires an onChange event, + // such as when the user selects a value from the drop down list of a ComboBox, + // which means that the user has finished entering the value and we should save. + // tags: + // private + + if(this.inlineEditBox.autoSave && this.inlineEditBox.editing && this.enableSave()){ + dojo.style(this.inlineEditBox.displayNode, { display: "" }); + dijit.focus(this.inlineEditBox.displayNode); // fires _onBlur which will save the formatted value + } + }, + + enableSave: function(){ + // summary: + // User overridable function returning a Boolean to indicate + // if the Save button should be enabled or not - usually due to invalid conditions + // tags: + // extension + return ( + this.editWidget.isValid + ? this.editWidget.isValid() + : true + ); + }, + + focus: function(){ + // summary: + // Focus the edit widget. + // tags: + // protected + + this.editWidget.focus(); + setTimeout(dojo.hitch(this, function(){ + if(this.editWidget.focusNode && this.editWidget.focusNode.tagName == "INPUT"){ + dijit.selectInputText(this.editWidget.focusNode); + } + }), 0); + } +}); + +} + +if(!dojo._hasResource["dojo.cookie"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.cookie"] = true; +dojo.provide("dojo.cookie"); + + + +/*===== +dojo.__cookieProps = function(){ + // expires: Date|String|Number? + // If a number, the number of days from today at which the cookie + // will expire. If a date, the date past which the cookie will expire. + // If expires is in the past, the cookie will be deleted. + // If expires is omitted or is 0, the cookie will expire when the browser closes. << FIXME: 0 seems to disappear right away? FF3. + // path: String? + // The path to use for the cookie. + // domain: String? + // The domain to use for the cookie. + // secure: Boolean? + // Whether to only send the cookie on secure connections + this.expires = expires; + this.path = path; + this.domain = domain; + this.secure = secure; +} +=====*/ + + +dojo.cookie = function(/*String*/name, /*String?*/value, /*dojo.__cookieProps?*/props){ + // summary: + // Get or set a cookie. + // description: + // If one argument is passed, returns the value of the cookie + // For two or more arguments, acts as a setter. + // name: + // Name of the cookie + // value: + // Value for the cookie + // props: + // Properties for the cookie + // example: + // set a cookie with the JSON-serialized contents of an object which + // will expire 5 days from now: + // | dojo.cookie("configObj", dojo.toJson(config), { expires: 5 }); + // + // example: + // de-serialize a cookie back into a JavaScript object: + // | var config = dojo.fromJson(dojo.cookie("configObj")); + // + // example: + // delete a cookie: + // | dojo.cookie("configObj", null, {expires: -1}); + var c = document.cookie; + if(arguments.length == 1){ + var matches = c.match(new RegExp("(?:^|; )" + dojo.regexp.escapeString(name) + "=([^;]*)")); + return matches ? decodeURIComponent(matches[1]) : undefined; // String or undefined + }else{ + props = props || {}; +// FIXME: expires=0 seems to disappear right away, not on close? (FF3) Change docs? + var exp = props.expires; + if(typeof exp == "number"){ + var d = new Date(); + d.setTime(d.getTime() + exp*24*60*60*1000); + exp = props.expires = d; + } + if(exp && exp.toUTCString){ props.expires = exp.toUTCString(); } + + value = encodeURIComponent(value); + var updatedCookie = name + "=" + value, propName; + for(propName in props){ + updatedCookie += "; " + propName; + var propValue = props[propName]; + if(propValue !== true){ updatedCookie += "=" + propValue; } + } + document.cookie = updatedCookie; + } +}; + +dojo.cookie.isSupported = function(){ + // summary: + // Use to determine if the current browser supports cookies or not. + // + // Returns true if user allows cookies. + // Returns false if user doesn't allow cookies. + + if(!("cookieEnabled" in navigator)){ + this("__djCookieTest__", "CookiesAllowed"); + navigator.cookieEnabled = this("__djCookieTest__") == "CookiesAllowed"; + if(navigator.cookieEnabled){ + this("__djCookieTest__", "", {expires: -1}); + } + } + return navigator.cookieEnabled; +}; + +} + +if(!dojo._hasResource["dijit.layout.StackController"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.layout.StackController"] = true; +dojo.provide("dijit.layout.StackController"); + + + + + + + +dojo.declare( + "dijit.layout.StackController", + [dijit._Widget, dijit._Templated, dijit._Container], + { + // summary: + // Set of buttons to select a page in a page list. + // description: + // Monitors the specified StackContainer, and whenever a page is + // added, deleted, or selected, updates itself accordingly. + + templateString: "<span wairole='tablist' dojoAttachEvent='onkeypress' class='dijitStackController'></span>", + + // containerId: [const] String + // The id of the page container that I point to + containerId: "", + + // buttonWidget: [const] String + // The name of the button widget to create to correspond to each page + buttonWidget: "dijit.layout._StackButton", + + postCreate: function(){ + dijit.setWaiRole(this.domNode, "tablist"); + + this.pane2button = {}; // mapping from pane id to buttons + this.pane2handles = {}; // mapping from pane id to this.connect() handles + + // Listen to notifications from StackContainer + this.subscribe(this.containerId+"-startup", "onStartup"); + this.subscribe(this.containerId+"-addChild", "onAddChild"); + this.subscribe(this.containerId+"-removeChild", "onRemoveChild"); + this.subscribe(this.containerId+"-selectChild", "onSelectChild"); + this.subscribe(this.containerId+"-containerKeyPress", "onContainerKeyPress"); + }, + + onStartup: function(/*Object*/ info){ + // summary: + // Called after StackContainer has finished initializing + // tags: + // private + dojo.forEach(info.children, this.onAddChild, this); + if(info.selected){ + // Show button corresponding to selected pane (unless selected + // is null because there are no panes) + this.onSelectChild(info.selected); + } + }, + + destroy: function(){ + for(var pane in this.pane2button){ + this.onRemoveChild(dijit.byId(pane)); + } + this.inherited(arguments); + }, + + onAddChild: function(/*dijit._Widget*/ page, /*Integer?*/ insertIndex){ + // summary: + // Called whenever a page is added to the container. + // Create button corresponding to the page. + // tags: + // private + + // create an instance of the button widget + var cls = dojo.getObject(this.buttonWidget); + var button = new cls({ + id: this.id + "_" + page.id, + label: page.title, + dir: page.dir, + lang: page.lang, + showLabel: page.showTitle, + iconClass: page.iconClass, + closeButton: page.closable, + title: page.tooltip + }); + dijit.setWaiState(button.focusNode,"selected", "false"); + this.pane2handles[page.id] = [ + this.connect(page, 'set', function(name, value){ + var buttonAttr = { + title: 'label', + showTitle: 'showLabel', + iconClass: 'iconClass', + closable: 'closeButton', + tooltip: 'title' + }[name]; + if(buttonAttr){ + button.set(buttonAttr, value); + } + }), + this.connect(button, 'onClick', dojo.hitch(this,"onButtonClick", page)), + this.connect(button, 'onClickCloseButton', dojo.hitch(this,"onCloseButtonClick", page)) + ]; + this.addChild(button, insertIndex); + this.pane2button[page.id] = button; + page.controlButton = button; // this value might be overwritten if two tabs point to same container + if(!this._currentChild){ // put the first child into the tab order + button.focusNode.setAttribute("tabIndex", "0"); + dijit.setWaiState(button.focusNode, "selected", "true"); + this._currentChild = page; + } + // make sure all tabs have the same length + if(!this.isLeftToRight() && dojo.isIE && this._rectifyRtlTabList){ + this._rectifyRtlTabList(); + } + }, + + onRemoveChild: function(/*dijit._Widget*/ page){ + // summary: + // Called whenever a page is removed from the container. + // Remove the button corresponding to the page. + // tags: + // private + + if(this._currentChild === page){ this._currentChild = null; } + dojo.forEach(this.pane2handles[page.id], this.disconnect, this); + delete this.pane2handles[page.id]; + var button = this.pane2button[page.id]; + if(button){ + this.removeChild(button); + delete this.pane2button[page.id]; + button.destroy(); + } + delete page.controlButton; + }, + + onSelectChild: function(/*dijit._Widget*/ page){ + // summary: + // Called when a page has been selected in the StackContainer, either by me or by another StackController + // tags: + // private + + if(!page){ return; } + + if(this._currentChild){ + var oldButton=this.pane2button[this._currentChild.id]; + oldButton.set('checked', false); + dijit.setWaiState(oldButton.focusNode, "selected", "false"); + oldButton.focusNode.setAttribute("tabIndex", "-1"); + } + + var newButton=this.pane2button[page.id]; + newButton.set('checked', true); + dijit.setWaiState(newButton.focusNode, "selected", "true"); + this._currentChild = page; + newButton.focusNode.setAttribute("tabIndex", "0"); + var container = dijit.byId(this.containerId); + dijit.setWaiState(container.containerNode, "labelledby", newButton.id); + }, + + onButtonClick: function(/*dijit._Widget*/ page){ + // summary: + // Called whenever one of my child buttons is pressed in an attempt to select a page + // tags: + // private + + var container = dijit.byId(this.containerId); + container.selectChild(page); + }, + + onCloseButtonClick: function(/*dijit._Widget*/ page){ + // summary: + // Called whenever one of my child buttons [X] is pressed in an attempt to close a page + // tags: + // private + + var container = dijit.byId(this.containerId); + container.closeChild(page); + if(this._currentChild){ + var b = this.pane2button[this._currentChild.id]; + if(b){ + dijit.focus(b.focusNode || b.domNode); + } + } + }, + + // TODO: this is a bit redundant with forward, back api in StackContainer + adjacent: function(/*Boolean*/ forward){ + // summary: + // Helper for onkeypress to find next/previous button + // tags: + // private + + if(!this.isLeftToRight() && (!this.tabPosition || /top|bottom/.test(this.tabPosition))){ forward = !forward; } + // find currently focused button in children array + var children = this.getChildren(); + var current = dojo.indexOf(children, this.pane2button[this._currentChild.id]); + // pick next button to focus on + var offset = forward ? 1 : children.length - 1; + return children[ (current + offset) % children.length ]; // dijit._Widget + }, + + onkeypress: function(/*Event*/ e){ + // summary: + // Handle keystrokes on the page list, for advancing to next/previous button + // and closing the current page if the page is closable. + // tags: + // private + + if(this.disabled || e.altKey ){ return; } + var forward = null; + if(e.ctrlKey || !e._djpage){ + var k = dojo.keys; + switch(e.charOrCode){ + case k.LEFT_ARROW: + case k.UP_ARROW: + if(!e._djpage){ forward = false; } + break; + case k.PAGE_UP: + if(e.ctrlKey){ forward = false; } + break; + case k.RIGHT_ARROW: + case k.DOWN_ARROW: + if(!e._djpage){ forward = true; } + break; + case k.PAGE_DOWN: + if(e.ctrlKey){ forward = true; } + break; + case k.DELETE: + if(this._currentChild.closable){ + this.onCloseButtonClick(this._currentChild); + } + dojo.stopEvent(e); + break; + default: + if(e.ctrlKey){ + if(e.charOrCode === k.TAB){ + this.adjacent(!e.shiftKey).onClick(); + dojo.stopEvent(e); + }else if(e.charOrCode == "w"){ + if(this._currentChild.closable){ + this.onCloseButtonClick(this._currentChild); + } + dojo.stopEvent(e); // avoid browser tab closing. + } + } + } + // handle page navigation + if(forward !== null){ + this.adjacent(forward).onClick(); + dojo.stopEvent(e); + } + } + }, + + onContainerKeyPress: function(/*Object*/ info){ + // summary: + // Called when there was a keypress on the container + // tags: + // private + info.e._djpage = info.page; + this.onkeypress(info.e); + } + }); + + +dojo.declare("dijit.layout._StackButton", + dijit.form.ToggleButton, + { + // summary: + // Internal widget used by StackContainer. + // description: + // The button-like or tab-like object you click to select or delete a page + // tags: + // private + + // Override _FormWidget.tabIndex. + // StackContainer buttons are not in the tab order by default. + // Probably we should be calling this.startupKeyNavChildren() instead. + tabIndex: "-1", + + postCreate: function(/*Event*/ evt){ + dijit.setWaiRole((this.focusNode || this.domNode), "tab"); + this.inherited(arguments); + }, + + onClick: function(/*Event*/ evt){ + // summary: + // This is for TabContainer where the tabs are <span> rather than button, + // so need to set focus explicitly (on some browsers) + // Note that you shouldn't override this method, but you can connect to it. + dijit.focus(this.focusNode); + + // ... now let StackController catch the event and tell me what to do + }, + + onClickCloseButton: function(/*Event*/ evt){ + // summary: + // StackContainer connects to this function; if your widget contains a close button + // then clicking it should call this function. + // Note that you shouldn't override this method, but you can connect to it. + evt.stopPropagation(); + } + }); + + +} + +if(!dojo._hasResource["dijit.layout.StackContainer"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.layout.StackContainer"] = true; +dojo.provide("dijit.layout.StackContainer"); + + + + + + +dojo.declare( + "dijit.layout.StackContainer", + dijit.layout._LayoutWidget, + { + // summary: + // A container that has multiple children, but shows only + // one child at a time + // + // description: + // A container for widgets (ContentPanes, for example) That displays + // only one Widget at a time. + // + // Publishes topics [widgetId]-addChild, [widgetId]-removeChild, and [widgetId]-selectChild + // + // Can be base class for container, Wizard, Show, etc. + + // doLayout: Boolean + // If true, change the size of my currently displayed child to match my size + doLayout: true, + + // persist: Boolean + // Remembers the selected child across sessions + persist: false, + + baseClass: "dijitStackContainer", + +/*===== + // selectedChildWidget: [readonly] dijit._Widget + // References the currently selected child widget, if any. + // Adjust selected child with selectChild() method. + selectedChildWidget: null, +=====*/ + + postCreate: function(){ + this.inherited(arguments); + dojo.addClass(this.domNode, "dijitLayoutContainer"); + dijit.setWaiRole(this.containerNode, "tabpanel"); + this.connect(this.domNode, "onkeypress", this._onKeyPress); + }, + + startup: function(){ + if(this._started){ return; } + + var children = this.getChildren(); + + // Setup each page panel to be initially hidden + dojo.forEach(children, this._setupChild, this); + + // Figure out which child to initially display, defaulting to first one + if(this.persist){ + this.selectedChildWidget = dijit.byId(dojo.cookie(this.id + "_selectedChild")); + }else{ + dojo.some(children, function(child){ + if(child.selected){ + this.selectedChildWidget = child; + } + return child.selected; + }, this); + } + var selected = this.selectedChildWidget; + if(!selected && children[0]){ + selected = this.selectedChildWidget = children[0]; + selected.selected = true; + } + + // Publish information about myself so any StackControllers can initialize. + // This needs to happen before this.inherited(arguments) so that for + // TabContainer, this._contentBox doesn't include the space for the tab labels. + dojo.publish(this.id+"-startup", [{children: children, selected: selected}]); + + // Startup each child widget, and do initial layout like setting this._contentBox, + // then calls this.resize() which does the initial sizing on the selected child. + this.inherited(arguments); + }, + + resize: function(){ + // Resize is called when we are first made visible (it's called from startup() + // if we are initially visible). If this is the first time we've been made + // visible then show our first child. + var selected = this.selectedChildWidget; + if(selected && !this._hasBeenShown){ + this._hasBeenShown = true; + this._showChild(selected); + } + this.inherited(arguments); + }, + + _setupChild: function(/*dijit._Widget*/ child){ + // Overrides _LayoutWidget._setupChild() + + this.inherited(arguments); + + dojo.removeClass(child.domNode, "dijitVisible"); + dojo.addClass(child.domNode, "dijitHidden"); + + // remove the title attribute so it doesn't show up when i hover + // over a node + child.domNode.title = ""; + }, + + addChild: function(/*dijit._Widget*/ child, /*Integer?*/ insertIndex){ + // Overrides _Container.addChild() to do layout and publish events + + this.inherited(arguments); + + if(this._started){ + dojo.publish(this.id+"-addChild", [child, insertIndex]); + + // in case the tab titles have overflowed from one line to two lines + // (or, if this if first child, from zero lines to one line) + // TODO: w/ScrollingTabController this is no longer necessary, although + // ScrollTabController.resize() does need to get called to show/hide + // the navigation buttons as appropriate, but that's handled in ScrollingTabController.onAddChild() + this.layout(); + + // if this is the first child, then select it + if(!this.selectedChildWidget){ + this.selectChild(child); + } + } + }, + + removeChild: function(/*dijit._Widget*/ page){ + // Overrides _Container.removeChild() to do layout and publish events + + this.inherited(arguments); + + if(this._started){ + // this will notify any tablists to remove a button; do this first because it may affect sizing + dojo.publish(this.id + "-removeChild", [page]); + } + + // If we are being destroyed than don't run the code below (to select another page), because we are deleting + // every page one by one + if(this._beingDestroyed){ return; } + + // Select new page to display, also updating TabController to show the respective tab. + // Do this before layout call because it can affect the height of the TabController. + if(this.selectedChildWidget === page){ + this.selectedChildWidget = undefined; + if(this._started){ + var children = this.getChildren(); + if(children.length){ + this.selectChild(children[0]); + } + } + } + + if(this._started){ + // In case the tab titles now take up one line instead of two lines + // (note though that ScrollingTabController never overflows to multiple lines), + // or the height has changed slightly because of addition/removal of tab which close icon + this.layout(); + } + }, + + selectChild: function(/*dijit._Widget|String*/ page, /*Boolean*/ animate){ + // summary: + // Show the given widget (which must be one of my children) + // page: + // Reference to child widget or id of child widget + + page = dijit.byId(page); + + if(this.selectedChildWidget != page){ + // Deselect old page and select new one + this._transition(page, this.selectedChildWidget, animate); + this.selectedChildWidget = page; + dojo.publish(this.id+"-selectChild", [page]); + + if(this.persist){ + dojo.cookie(this.id + "_selectedChild", this.selectedChildWidget.id); + } + } + }, + + _transition: function(/*dijit._Widget*/newWidget, /*dijit._Widget*/oldWidget){ + // summary: + // Hide the old widget and display the new widget. + // Subclasses should override this. + // tags: + // protected extension + if(oldWidget){ + this._hideChild(oldWidget); + } + this._showChild(newWidget); + + // Size the new widget, in case this is the first time it's being shown, + // or I have been resized since the last time it was shown. + // Note that page must be visible for resizing to work. + if(newWidget.resize){ + if(this.doLayout){ + newWidget.resize(this._containerContentBox || this._contentBox); + }else{ + // the child should pick it's own size but we still need to call resize() + // (with no arguments) to let the widget lay itself out + newWidget.resize(); + } + } + }, + + _adjacent: function(/*Boolean*/ forward){ + // summary: + // Gets the next/previous child widget in this container from the current selection. + var children = this.getChildren(); + var index = dojo.indexOf(children, this.selectedChildWidget); + index += forward ? 1 : children.length - 1; + return children[ index % children.length ]; // dijit._Widget + }, + + forward: function(){ + // summary: + // Advance to next page. + this.selectChild(this._adjacent(true), true); + }, + + back: function(){ + // summary: + // Go back to previous page. + this.selectChild(this._adjacent(false), true); + }, + + _onKeyPress: function(e){ + dojo.publish(this.id+"-containerKeyPress", [{ e: e, page: this}]); + }, + + layout: function(){ + // Implement _LayoutWidget.layout() virtual method. + if(this.doLayout && this.selectedChildWidget && this.selectedChildWidget.resize){ + this.selectedChildWidget.resize(this._containerContentBox || this._contentBox); + } + }, + + _showChild: function(/*dijit._Widget*/ page){ + // summary: + // Show the specified child by changing it's CSS, and call _onShow()/onShow() so + // it can do any updates it needs regarding loading href's etc. + var children = this.getChildren(); + page.isFirstChild = (page == children[0]); + page.isLastChild = (page == children[children.length-1]); + page.selected = true; + + dojo.removeClass(page.domNode, "dijitHidden"); + dojo.addClass(page.domNode, "dijitVisible"); + + page._onShow(); + }, + + _hideChild: function(/*dijit._Widget*/ page){ + // summary: + // Hide the specified child by changing it's CSS, and call _onHide() so + // it's notified. + page.selected=false; + dojo.removeClass(page.domNode, "dijitVisible"); + dojo.addClass(page.domNode, "dijitHidden"); + + page.onHide(); + }, + + closeChild: function(/*dijit._Widget*/ page){ + // summary: + // Callback when user clicks the [X] to remove a page. + // If onClose() returns true then remove and destroy the child. + // tags: + // private + var remove = page.onClose(this, page); + if(remove){ + this.removeChild(page); + // makes sure we can clean up executeScripts in ContentPane onUnLoad + page.destroyRecursive(); + } + }, + + destroyDescendants: function(/*Boolean*/preserveDom){ + dojo.forEach(this.getChildren(), function(child){ + this.removeChild(child); + child.destroyRecursive(preserveDom); + }, this); + } +}); + +// For back-compat, remove for 2.0 + + + +// These arguments can be specified for the children of a StackContainer. +// Since any widget can be specified as a StackContainer child, mix them +// into the base widget class. (This is a hack, but it's effective.) +dojo.extend(dijit._Widget, { + // selected: Boolean + // Parameter for children of `dijit.layout.StackContainer` or subclasses. + // Specifies that this widget should be the initially displayed pane. + // Note: to change the selected child use `dijit.layout.StackContainer.selectChild` + selected: false, + + // closable: Boolean + // Parameter for children of `dijit.layout.StackContainer` or subclasses. + // True if user can close (destroy) this child, such as (for example) clicking the X on the tab. + closable: false, + + // iconClass: String + // Parameter for children of `dijit.layout.StackContainer` or subclasses. + // CSS Class specifying icon to use in label associated with this pane. + iconClass: "", + + // showTitle: Boolean + // Parameter for children of `dijit.layout.StackContainer` or subclasses. + // When true, display title of this widget as tab label etc., rather than just using + // icon specified in iconClass + showTitle: true +}); + +} + +if(!dojo._hasResource["dijit.layout.AccordionPane"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.layout.AccordionPane"] = true; +dojo.provide("dijit.layout.AccordionPane"); + + + +dojo.declare("dijit.layout.AccordionPane", dijit.layout.ContentPane, { + // summary: + // Deprecated widget. Use `dijit.layout.ContentPane` instead. + // tags: + // deprecated + + constructor: function(){ + dojo.deprecated("dijit.layout.AccordionPane deprecated, use ContentPane instead", "", "2.0"); + }, + + onSelected: function(){ + // summary: + // called when this pane is selected + } +}); + +} + +if(!dojo._hasResource["dijit.layout.AccordionContainer"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.layout.AccordionContainer"] = true; +dojo.provide("dijit.layout.AccordionContainer"); + + + + + + + + + + // for back compat, remove for 2.0 + +dojo.declare( + "dijit.layout.AccordionContainer", + dijit.layout.StackContainer, + { + // summary: + // Holds a set of panes where every pane's title is visible, but only one pane's content is visible at a time, + // and switching between panes is visualized by sliding the other panes up/down. + // example: + // | <div dojoType="dijit.layout.AccordionContainer"> + // | <div dojoType="dijit.layout.ContentPane" title="pane 1"> + // | </div> + // | <div dojoType="dijit.layout.ContentPane" title="pane 2"> + // | <p>This is some text</p> + // | </div> + // | </div> + + // duration: Integer + // Amount of time (in ms) it takes to slide panes + duration: dijit.defaultDuration, + + // buttonWidget: [const] String + // The name of the widget used to display the title of each pane + buttonWidget: "dijit.layout._AccordionButton", + + // _verticalSpace: Number + // Pixels of space available for the open pane + // (my content box size minus the cumulative size of all the title bars) + _verticalSpace: 0, + + baseClass: "dijitAccordionContainer", + + postCreate: function(){ + this.domNode.style.overflow = "hidden"; + this.inherited(arguments); + dijit.setWaiRole(this.domNode, "tablist"); + }, + + startup: function(){ + if(this._started){ return; } + this.inherited(arguments); + if(this.selectedChildWidget){ + var style = this.selectedChildWidget.containerNode.style; + style.display = ""; + style.overflow = "auto"; + this.selectedChildWidget._wrapperWidget.set("selected", true); + } + }, + + _getTargetHeight: function(/* Node */ node){ + // summary: + // For the given node, returns the height that should be + // set to achieve our vertical space (subtract any padding + // we may have). + // + // This is used by the animations. + // + // TODO: I don't think this works correctly in IE quirks when an elements + // style.height including padding and borders + var cs = dojo.getComputedStyle(node); + return Math.max(this._verticalSpace - dojo._getPadBorderExtents(node, cs).h - dojo._getMarginExtents(node, cs).h, 0); + }, + + layout: function(){ + // Implement _LayoutWidget.layout() virtual method. + // Set the height of the open pane based on what room remains. + + var openPane = this.selectedChildWidget; + + if(!openPane){ return;} + + var openPaneContainer = openPane._wrapperWidget.domNode, + openPaneContainerMargin = dojo._getMarginExtents(openPaneContainer), + openPaneContainerPadBorder = dojo._getPadBorderExtents(openPaneContainer), + mySize = this._contentBox; + + // get cumulative height of all the unselected title bars + var totalCollapsedHeight = 0; + dojo.forEach(this.getChildren(), function(child){ + if(child != openPane){ + totalCollapsedHeight += dojo.marginBox(child._wrapperWidget.domNode).h; + } + }); + this._verticalSpace = mySize.h - totalCollapsedHeight - openPaneContainerMargin.h + - openPaneContainerPadBorder.h - openPane._buttonWidget.getTitleHeight(); + + // Memo size to make displayed child + this._containerContentBox = { + h: this._verticalSpace, + w: this._contentBox.w - openPaneContainerMargin.w - openPaneContainerPadBorder.w + }; + + if(openPane){ + openPane.resize(this._containerContentBox); + } + }, + + _setupChild: function(child){ + // Overrides _LayoutWidget._setupChild(). + // Put wrapper widget around the child widget, showing title + + child._wrapperWidget = new dijit.layout._AccordionInnerContainer({ + contentWidget: child, + buttonWidget: this.buttonWidget, + id: child.id + "_wrapper", + dir: child.dir, + lang: child.lang, + parent: this + }); + + this.inherited(arguments); + }, + + addChild: function(/*dijit._Widget*/ child, /*Integer?*/ insertIndex){ + if(this._started){ + // Adding a child to a started Accordion is complicated because children have + // wrapper widgets. Default code path (calling this.inherited()) would add + // the new child inside another child's wrapper. + + // First add in child as a direct child of this AccordionContainer + dojo.place(child.domNode, this.containerNode, insertIndex); + + if(!child._started){ + child.startup(); + } + + // Then stick the wrapper widget around the child widget + this._setupChild(child); + + // Code below copied from StackContainer + dojo.publish(this.id+"-addChild", [child, insertIndex]); + this.layout(); + if(!this.selectedChildWidget){ + this.selectChild(child); + } + }else{ + // We haven't been started yet so just add in the child widget directly, + // and the wrapper will be created on startup() + this.inherited(arguments); + } + }, + + removeChild: function(child){ + // Overrides _LayoutWidget.removeChild(). + + // destroy wrapper widget first, before StackContainer.getChildren() call + child._wrapperWidget.destroy(); + delete child._wrapperWidget; + dojo.removeClass(child.domNode, "dijitHidden"); + + this.inherited(arguments); + }, + + getChildren: function(){ + // Overrides _Container.getChildren() to return content panes rather than internal AccordionInnerContainer panes + return dojo.map(this.inherited(arguments), function(child){ + return child.declaredClass == "dijit.layout._AccordionInnerContainer" ? child.contentWidget : child; + }, this); + }, + + destroy: function(){ + dojo.forEach(this.getChildren(), function(child){ + child._wrapperWidget.destroy(); + }); + this.inherited(arguments); + }, + + _transition: function(/*dijit._Widget?*/newWidget, /*dijit._Widget?*/oldWidget, /*Boolean*/ animate){ + // Overrides StackContainer._transition() to provide sliding of title bars etc. + +//TODO: should be able to replace this with calls to slideIn/slideOut + if(this._inTransition){ return; } + var animations = []; + var paneHeight = this._verticalSpace; + if(newWidget){ + newWidget._wrapperWidget.set("selected", true); + + this._showChild(newWidget); // prepare widget to be slid in + + // Size the new widget, in case this is the first time it's being shown, + // or I have been resized since the last time it was shown. + // Note that page must be visible for resizing to work. + if(this.doLayout && newWidget.resize){ + newWidget.resize(this._containerContentBox); + } + + var newContents = newWidget.domNode; + dojo.addClass(newContents, "dijitVisible"); + dojo.removeClass(newContents, "dijitHidden"); + + if(animate){ + var newContentsOverflow = newContents.style.overflow; + newContents.style.overflow = "hidden"; + animations.push(dojo.animateProperty({ + node: newContents, + duration: this.duration, + properties: { + height: { start: 1, end: this._getTargetHeight(newContents) } + }, + onEnd: function(){ + newContents.style.overflow = newContentsOverflow; + + // Kick IE to workaround layout bug, see #11415 + if(dojo.isIE){ + setTimeout(function(){ + dojo.removeClass(newContents.parentNode, "dijitAccordionInnerContainerFocused"); + setTimeout(function(){ + dojo.addClass(newContents.parentNode, "dijitAccordionInnerContainerFocused"); + }, 0); + }, 0); + } + } + })); + } + } + if(oldWidget){ + oldWidget._wrapperWidget.set("selected", false); + var oldContents = oldWidget.domNode; + if(animate){ + var oldContentsOverflow = oldContents.style.overflow; + oldContents.style.overflow = "hidden"; + animations.push(dojo.animateProperty({ + node: oldContents, + duration: this.duration, + properties: { + height: { start: this._getTargetHeight(oldContents), end: 1 } + }, + onEnd: function(){ + dojo.addClass(oldContents, "dijitHidden"); + dojo.removeClass(oldContents, "dijitVisible"); + oldContents.style.overflow = oldContentsOverflow; + if(oldWidget.onHide){ + oldWidget.onHide(); + } + } + })); + }else{ + dojo.addClass(oldContents, "dijitHidden"); + dojo.removeClass(oldContents, "dijitVisible"); + if(oldWidget.onHide){ + oldWidget.onHide(); + } + } + } + + if(animate){ + this._inTransition = true; + var combined = dojo.fx.combine(animations); + combined.onEnd = dojo.hitch(this, function(){ + delete this._inTransition; + }); + combined.play(); + } + }, + + // note: we are treating the container as controller here + _onKeyPress: function(/*Event*/ e, /*dijit._Widget*/ fromTitle){ + // summary: + // Handle keypress events + // description: + // This is called from a handler on AccordionContainer.domNode + // (setup in StackContainer), and is also called directly from + // the click handler for accordion labels + if(this._inTransition || this.disabled || e.altKey || !(fromTitle || e.ctrlKey)){ + if(this._inTransition){ + dojo.stopEvent(e); + } + return; + } + var k = dojo.keys, + c = e.charOrCode; + if((fromTitle && (c == k.LEFT_ARROW || c == k.UP_ARROW)) || + (e.ctrlKey && c == k.PAGE_UP)){ + this._adjacent(false)._buttonWidget._onTitleClick(); + dojo.stopEvent(e); + }else if((fromTitle && (c == k.RIGHT_ARROW || c == k.DOWN_ARROW)) || + (e.ctrlKey && (c == k.PAGE_DOWN || c == k.TAB))){ + this._adjacent(true)._buttonWidget._onTitleClick(); + dojo.stopEvent(e); + } + } + } +); + +dojo.declare("dijit.layout._AccordionInnerContainer", + [dijit._Widget, dijit._CssStateMixin], { + // summary: + // Internal widget placed as direct child of AccordionContainer.containerNode. + // When other widgets are added as children to an AccordionContainer they are wrapped in + // this widget. + + // buttonWidget: String + // Name of class to use to instantiate title + // (Wish we didn't have a separate widget for just the title but maintaining it + // for backwards compatibility, is it worth it?) +/*===== + buttonWidget: null, +=====*/ + // contentWidget: dijit._Widget + // Pointer to the real child widget +/*===== + contentWidget: null, +=====*/ + + baseClass: "dijitAccordionInnerContainer", + + // tell nested layout widget that we will take care of sizing + isContainer: true, + isLayoutContainer: true, + + buildRendering: function(){ + // Create wrapper div, placed where the child is now + this.domNode = dojo.place("<div class='" + this.baseClass + "'>", 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) + dojo.place(this.contentWidget.domNode, this.domNode); + }, + + postCreate: function(){ + this.inherited(arguments); + this.connect(this.contentWidget, 'set', function(name, value){ + var mappedName = {title: "label", tooltip: "title", iconClass: "iconClass"}[name]; + if(mappedName){ + this.button.set(mappedName, value); + } + }, this); + }, + + _setSelectedAttr: function(/*Boolean*/ isSelected){ + this.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(); + + 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", "<div dojoAttachEvent='onclick:_onTitleClick' class='dijitAccordionTitle'>\n\t<div dojoAttachPoint='titleNode,focusNode' dojoAttachEvent='onkeypress:_onTitleKeyPress'\n\t\t\tclass='dijitAccordionTitleFocus' wairole=\"tab\" waiState=\"expanded-false\"\n\t\t><span class='dijitInline dijitAccordionArrow' waiRole=\"presentation\"></span\n\t\t><span class='arrowTextUp' waiRole=\"presentation\">+</span\n\t\t><span class='arrowTextDown' waiRole=\"presentation\">-</span\n\t\t><img src=\"${_blankGif}\" alt=\"\" class=\"dijitIcon\" dojoAttachPoint='iconNode' style=\"vertical-align: middle\" waiRole=\"presentation\"/>\n\t\t<span waiRole=\"presentation\" dojoAttachPoint='titleTextNode' class='dijitAccordionText'></span>\n\t</div>\n</div>\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; + }, + + postCreate: function(){ + this.inherited(arguments); + dojo.setSelectable(this.domNode, false); + var titleTextNodeId = dojo.attr(this.domNode,'id').replace(' ','_'); + dojo.attr(this.titleTextNode, "id", titleTextNodeId+"_title"); + dijit.setWaiState(this.focusNode, "labelledby", dojo.attr(this.titleTextNode, "id")); + }, + + getTitleHeight: function(){ + // summary: + // Returns the height of the title dom node. + return dojo.marginBox(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(); + if(!parent._inTransition){ + 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.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. + // + // NOTE: Splitters must not be more than 50 pixels in width. + // + // 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. + // + // example: + // | <div dojoType="dijit.layout.BorderContainer" design="sidebar" gutters="false" + // | style="width: 400px; height: 300px;"> + // | <div dojoType="ContentPane" region="top">header text</div> + // | <div dojoType="ContentPane" region="right" splitter="true" style="width: 200px;">table of contents</div> + // | <div dojoType="ContentPane" region="center">client area</div> + // | </div> + + // 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: 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: 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); + }, + + postCreate: function(){ + this.inherited(arguments); + + this._splitters = {}; + this._splitterThickness = {}; + }, + + 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"; } + + //FIXME: redundant? + this["_"+region] = child.domNode; + this["_"+region+"Widget"] = child; + + // Create draggable splitter for resizing pane, + // or alternately if splitter=false but BorderContainer.gutters=true then + // insert dummy div just for spacing + if((child.splitter || this.gutters) && !this._splitters[region]){ + 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; + this._splitters[region] = splitter.domNode; + dojo.place(this._splitters[region], child.domNode, "after"); + + // Splitters arent added as Contained children, so we need to call startup explicitly + splitter.startup(); + } + child.region = region; + } + }, + + _computeSplitterThickness: function(region){ + this._splitterThickness[region] = this._splitterThickness[region] || + dojo.marginBox(this._splitters[region])[(/top|bottom/.test(region) ? 'h' : 'w')]; + }, + + layout: function(){ + // Implement _LayoutWidget.layout() virtual method. + for(var region in this._splitters){ this._computeSplitterThickness(region); } + 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 = this._splitters[region]; + if(splitter){ + dijit.byNode(splitter).destroy(); + delete this._splitters[region]; + delete this._splitterThickness[region]; + } + this.inherited(arguments); + delete this["_"+region]; + delete this["_" +region+"Widget"]; + if(this._started){ + this._layoutChildren(); + } + dojo.removeClass(child.domNode, this.baseClass+"Pane"); + }, + + getChildren: function(){ + // Override _LayoutWidget.getChildren() to only return real children, not the splitters. + return dojo.filter(this.inherited(arguments), function(widget){ + return !widget.isSplitter; + }); + }, + + getSplitter: function(/*String*/region){ + // summary: + // Returns the widget responsible for rendering the splitter associated with region + var splitter = this._splitters[region]; + return splitter ? dijit.byNode(splitter) : null; + }, + + 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?*/changedRegion, /*Number?*/ changedRegionSize){ + // 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. + // changedRegion: + // The region should be changed because splitter was dragged. + // "left", "right", "top", or "bottom". + // changedRegionSize: + // The new width/height (in pixels) to make changedRegion + + 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; + } + + var sidebarLayout = (this.design == "sidebar"); + var topHeight = 0, bottomHeight = 0, leftWidth = 0, rightWidth = 0; + var topStyle = {}, leftStyle = {}, rightStyle = {}, bottomStyle = {}, + centerStyle = (this._center && this._center.style) || {}; + + var changedSide = /left|right/.test(changedRegion); + + var layoutSides = !changedRegion || (!changedSide && !sidebarLayout); + var layoutTopBottom = !changedRegion || (changedSide && sidebarLayout); + + // Ask browser for width/height of side panes. + // Would be nice to cache this but height can change according to width + // (because words wrap around). I don't think width will ever change though + // (except when the user drags a splitter). + if(this._top){ + topStyle = (changedRegion == "top" || layoutTopBottom) && this._top.style; + topHeight = changedRegion == "top" ? changedRegionSize : dojo.marginBox(this._top).h; + } + if(this._left){ + leftStyle = (changedRegion == "left" || layoutSides) && this._left.style; + leftWidth = changedRegion == "left" ? changedRegionSize : dojo.marginBox(this._left).w; + } + if(this._right){ + rightStyle = (changedRegion == "right" || layoutSides) && this._right.style; + rightWidth = changedRegion == "right" ? changedRegionSize : dojo.marginBox(this._right).w; + } + if(this._bottom){ + bottomStyle = (changedRegion == "bottom" || layoutTopBottom) && this._bottom.style; + bottomHeight = changedRegion == "bottom" ? changedRegionSize : dojo.marginBox(this._bottom).h; + } + + var splitters = this._splitters; + var topSplitter = splitters.top, bottomSplitter = splitters.bottom, + leftSplitter = splitters.left, rightSplitter = splitters.right; + var splitterThickness = this._splitterThickness; + var topSplitterThickness = splitterThickness.top || 0, + leftSplitterThickness = splitterThickness.left || 0, + rightSplitterThickness = splitterThickness.right || 0, + bottomSplitterThickness = splitterThickness.bottom || 0; + + // Check for race condition where CSS hasn't finished loading, so + // the splitter width == the viewport width (#5824) + if(leftSplitterThickness > 50 || rightSplitterThickness > 50){ + setTimeout(dojo.hitch(this, function(){ + // Results are invalid. Clear them out. + this._splitterThickness = {}; + + for(var region in this._splitters){ + this._computeSplitterThickness(region); + } + this._layoutChildren(); + }), 50); + return false; + } + + var pe = this.pe; + + var splitterBounds = { + left: (sidebarLayout ? leftWidth + leftSplitterThickness: 0) + pe.l + "px", + right: (sidebarLayout ? rightWidth + rightSplitterThickness: 0) + pe.r + "px" + }; + + if(topSplitter){ + dojo.mixin(topSplitter.style, splitterBounds); + topSplitter.style.top = topHeight + pe.t + "px"; + } + + if(bottomSplitter){ + dojo.mixin(bottomSplitter.style, splitterBounds); + bottomSplitter.style.bottom = bottomHeight + pe.b + "px"; + } + + splitterBounds = { + top: (sidebarLayout ? 0 : topHeight + topSplitterThickness) + pe.t + "px", + bottom: (sidebarLayout ? 0 : bottomHeight + bottomSplitterThickness) + pe.b + "px" + }; + + if(leftSplitter){ + dojo.mixin(leftSplitter.style, splitterBounds); + leftSplitter.style.left = leftWidth + pe.l + "px"; + } + + if(rightSplitter){ + dojo.mixin(rightSplitter.style, splitterBounds); + rightSplitter.style.right = rightWidth + pe.r + "px"; + } + + dojo.mixin(centerStyle, { + top: pe.t + topHeight + topSplitterThickness + "px", + left: pe.l + leftWidth + leftSplitterThickness + "px", + right: pe.r + rightWidth + rightSplitterThickness + "px", + bottom: pe.b + bottomHeight + bottomSplitterThickness + "px" + }); + + var bounds = { + top: sidebarLayout ? pe.t + "px" : centerStyle.top, + bottom: sidebarLayout ? pe.b + "px" : centerStyle.bottom + }; + dojo.mixin(leftStyle, bounds); + dojo.mixin(rightStyle, bounds); + leftStyle.left = pe.l + "px"; rightStyle.right = pe.r + "px"; topStyle.top = pe.t + "px"; bottomStyle.bottom = pe.b + "px"; + if(sidebarLayout){ + topStyle.left = bottomStyle.left = leftWidth + leftSplitterThickness + pe.l + "px"; + topStyle.right = bottomStyle.right = rightWidth + rightSplitterThickness + pe.r + "px"; + }else{ + topStyle.left = bottomStyle.left = pe.l + "px"; + topStyle.right = bottomStyle.right = pe.r + "px"; + } + + // More calculations about sizes of panes + var containerHeight = this._borderBox.h - pe.t - pe.b, + middleHeight = containerHeight - ( topHeight + topSplitterThickness + bottomHeight + bottomSplitterThickness), + sidebarHeight = sidebarLayout ? containerHeight : middleHeight; + + var containerWidth = this._borderBox.w - pe.l - pe.r, + middleWidth = containerWidth - (leftWidth + leftSplitterThickness + rightWidth + rightSplitterThickness), + sidebarWidth = sidebarLayout ? middleWidth : containerWidth; + + // New margin-box size of each pane + var dim = { + top: { w: sidebarWidth, h: topHeight }, + bottom: { w: sidebarWidth, h: bottomHeight }, + left: { w: leftWidth, h: sidebarHeight }, + right: { w: rightWidth, h: sidebarHeight }, + center: { h: middleHeight, w: middleWidth } + }; + + if(changedRegion){ + // Respond to splitter drag event by changing changedRegion's width or height + var child = this["_" + changedRegion + "Widget"], + mb = {}; + mb[ /top|bottom/.test(changedRegion) ? "h" : "w"] = changedRegionSize; + child.resize ? child.resize(mb, dim[child.region]) : dojo.marginBox(child.domNode, mb); + } + + // Nodes in IE<8 don't respond to t/l/b/r, and TEXTAREA doesn't respond in any browser + var janky = dojo.isIE < 8 || (dojo.isIE && dojo.isQuirks) || dojo.some(this.getChildren(), function(child){ + return child.domNode.tagName == "TEXTAREA" || child.domNode.tagName == "INPUT"; + }); + if(janky){ + // Set the size of the children the old fashioned way, by setting + // CSS width and height + + var resizeWidget = function(widget, changes, result){ + if(widget){ + (widget.resize ? widget.resize(changes, result) : dojo.marginBox(widget.domNode, changes)); + } + }; + + if(leftSplitter){ leftSplitter.style.height = sidebarHeight; } + if(rightSplitter){ rightSplitter.style.height = sidebarHeight; } + resizeWidget(this._leftWidget, {h: sidebarHeight}, dim.left); + resizeWidget(this._rightWidget, {h: sidebarHeight}, dim.right); + + if(topSplitter){ topSplitter.style.width = sidebarWidth; } + if(bottomSplitter){ bottomSplitter.style.width = sidebarWidth; } + resizeWidget(this._topWidget, {w: sidebarWidth}, dim.top); + resizeWidget(this._bottomWidget, {w: sidebarWidth}, dim.bottom); + + resizeWidget(this._centerWidget, dim.center); + }else{ + // Calculate which panes need a notification that their size has been changed + // (we've already set style.top/bottom/left/right on those other panes). + var notifySides = !changedRegion || (/top|bottom/.test(changedRegion) && this.design != "sidebar"), + notifyTopBottom = !changedRegion || (/left|right/.test(changedRegion) && this.design == "sidebar"), + notifyList = { + center: true, + left: notifySides, + right: notifySides, + top: notifyTopBottom, + bottom: notifyTopBottom + }; + + // Send notification to those panes that have changed size + dojo.forEach(this.getChildren(), function(child){ + if(child.resize && notifyList[child.region]){ + child.resize(null, dim[child.region]); + } + }, this); + } + }, + + destroy: function(){ + for(var region in this._splitters){ + var splitter = this._splitters[region]; + dijit.byNode(splitter).destroy(); + dojo.destroy(splitter); + } + delete this._splitters; + delete this._splitterThickness; + this.inherited(arguments); + } +}); + +// This argument can be specified for the children of a BorderContainer. +// Since any widget can be specified as a LayoutContainer child, mix it +// into the base widget class. (This is a hack, but it's effective.) +dojo.extend(dijit._Widget, { + // region: [const] String + // Parameter for children of `dijit.layout.BorderContainer`. + // Values: "top", "bottom", "leading", "trailing", "left", "right", "center". + // See the `dijit.layout.BorderContainer` description for details. + region: '', + + // splitter: [const] Boolean + // Parameter for child of `dijit.layout.BorderContainer` where region != "center". + // If true, enables user to resize the widget by putting a draggable splitter between + // this widget and the region=center widget. + splitter: false, + + // minSize: [const] Number + // Parameter for children of `dijit.layout.BorderContainer`. + // Specifies a minimum size (in pixels) for this widget when resized by a splitter. + minSize: 0, + + // maxSize: [const] Number + // Parameter for children of `dijit.layout.BorderContainer`. + // Specifies a maximum size (in pixels) for this widget when resized by a splitter. + maxSize: Infinity +}); + + + +dojo.declare("dijit.layout._Splitter", [ dijit._Widget, dijit._Templated ], +{ + // summary: + // A draggable spacer between two items in a `dijit.layout.BorderContainer`. + // description: + // This is instantiated by `dijit.layout.BorderContainer`. Users should not + // create it directly. + // tags: + // private + +/*===== + // container: [const] dijit.layout.BorderContainer + // Pointer to the parent BorderContainer + container: null, + + // child: [const] dijit.layout._LayoutWidget + // Pointer to the pane associated with this splitter + child: null, + + // region: String + // Region of pane associated with this splitter. + // "top", "bottom", "left", "right". + region: null, +=====*/ + + // live: [const] Boolean + // If true, the child's size changes and the child widget is redrawn as you drag the splitter; + // otherwise, the size doesn't change until you drop the splitter (by mouse-up) + live: true, + + templateString: '<div class="dijitSplitter" dojoAttachEvent="onkeypress:_onKeyPress,onmousedown:_startDrag,onmouseenter:_onMouse,onmouseleave:_onMouse" tabIndex="0" waiRole="separator"><div class="dijitSplitterThumb"></div></div>', + + postCreate: function(){ + this.inherited(arguments); + this.horizontal = /top|bottom/.test(this.region); + dojo.addClass(this.domNode, "dijitSplitter" + (this.horizontal ? "H" : "V")); +// dojo.addClass(this.child.domNode, "dijitSplitterPane"); +// dojo.setSelectable(this.domNode, false); //TODO is this necessary? + + this._factor = /top|left/.test(this.region) ? 1 : -1; + + this._cookieName = this.container.id + "_" + this.region; + 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: + // Compute the maximum size that my corresponding pane can be set to + + var dim = this.horizontal ? 'h' : 'w', + thickness = this.container._splitterThickness[this.region]; + + // Get DOMNode of opposite pane, if an opposite pane exists. + // Ex: if I am the _Splitter for the left pane, then get the right pane. + var flip = {left:'right', right:'left', top:'bottom', bottom:'top', leading:'trailing', trailing:'leading'}, + oppNode = this.container["_" + flip[this.region]]; + + // I can expand up to the edge of the opposite pane, or if there's no opposite pane, then to + // edge of BorderContainer + var available = dojo.contentBox(this.container.domNode)[dim] - + (oppNode ? dojo.marginBox(oppNode)[dim] : 0) - + 20 - thickness * 2; + + return Math.min(this.child.maxSize, available); + }, + + _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"); + dojo.addClass(this.domNode, "dijitSplitter" + (this.horizontal ? "H" : "V") + "Active"); + if(this.fake){ + dojo.removeClass(this.fake, "dijitSplitterHover"); + dojo.removeClass(this.fake, "dijitSplitter" + (this.horizontal ? "H" : "V") + "Hover"); + } + + //Performance: load data info local vars for onmousevent function closure + var factor = this._factor, + max = this._computeMaxSize(), + min = this.child.minSize || 20, + 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], + region = this.region, + splitterStart = parseInt(this.domNode.style[region], 10), + resize = this._resize, + childNode = this.child.domNode, + layoutFunc = dojo.hitch(this.container, this.container._layoutChildren), + 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(region, boundChildSize); + } + splitterStyle[region] = factor * delta + splitterStart + (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"); + dojo.removeClass(this.domNode, "dijitSplitter" + (this.horizontal ? "H" : "V") + "Active"); + dojo.removeClass(this.domNode, "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.marginBox(this.child.domNode)[ horizontal ? 'h' : 'w' ] + this._factor * tick; + this.container._layoutChildren(this.region, 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: '<div class="dijitGutter" waiRole="presentation"></div>', + + postCreate: function(){ + this.horizontal = /top|bottom/.test(this.region); + 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: Boolean + // Defines whether the tablist gets an extra class for layouting, putting a border/shading + // around the set of tabs. + tabStrip: false, + + // nested: 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", "<div class=\"dijitTabContainer\">\n\t<div class=\"dijitTabListWrapper\" dojoAttachPoint=\"tablistNode\"></div>\n\t<div dojoAttachPoint=\"tablistSpacer\" class=\"dijitTabSpacer ${baseClass}-spacer\"></div>\n\t<div class=\"dijitTabPaneWrapper ${baseClass}-container\" dojoAttachPoint=\"containerNode\"></div>\n</div>\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); + }, + + postCreate: 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){ + this.tablist.resize({w: dojo.contentBox(this.domNode).w}); + } + + // 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: "<div wairole='tablist' dojoAttachEvent='onkeypress:onkeypress'></div>", + + // 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", "<div waiRole=\"presentation\" dojoAttachPoint=\"titleNode\" dojoAttachEvent='onclick:onClick'>\n <div waiRole=\"presentation\" class='dijitTabInnerDiv' dojoAttachPoint='innerDiv'>\n <div waiRole=\"presentation\" class='dijitTabContent' dojoAttachPoint='tabContent'>\n \t<div waiRole=\"presentation\" dojoAttachPoint='focusNode'>\n\t\t <img src=\"${_blankGif}\" alt=\"\" class=\"dijitIcon\" dojoAttachPoint='iconNode' />\n\t\t <span dojoAttachPoint='containerNode' class='tabLabel'></span>\n\t\t <span class=\"dijitInline dijitTabCloseButton dijitTabCloseIcon\" dojoAttachPoint='closeNode'\n\t\t \t\tdojoAttachEvent='onclick: onClickCloseButton' waiRole=\"presentation\">\n\t\t <span dojoAttachPoint='closeText' class='dijitTabCloseText'>x</span\n\t\t ></span>\n\t\t\t</div>\n </div>\n </div>\n</div>\n"), + + // Override _FormWidget.scrollOnFocus. + // Don't scroll the whole tab container into view when the button is focused. + scrollOnFocus: false, + + postMixInProperties: function(){ + // Override blank iconClass from Button to do tab height adjustment on IE6, + // to make sure that tabs with and w/out close icons are same height + if(!this.iconClass){ + this.iconClass = "dijitTabButtonIcon"; + } + }, + + postCreate: function(){ + this.inherited(arguments); + dojo.setSelectable(this.containerNode, false); + + // If a custom icon class has not been set for the + // tab icon, set its width to one pixel. This ensures + // that the height styling of the tab is maintained, + // as it is based on the height of the icon. + // TODO: I still think we can just set dijitTabButtonIcon to 1px in CSS <Bill> + if(this.iconNode.className == "dijitTabButtonIcon"){ + dojo.style(this.iconNode, "width", "1px"); + } + }, + + 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(disp){ + this.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 attr('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", "<div class=\"dijitTabListContainer-${tabPosition}\" style=\"visibility:hidden\">\n\t<div dojoType=\"dijit.layout._ScrollingTabControllerButton\"\n\t\t\tclass=\"tabStripButton-${tabPosition}\"\n\t\t\tid=\"${id}_menuBtn\" iconClass=\"dijitTabStripMenuIcon\"\n\t\t\tdojoAttachPoint=\"_menuBtn\" showLabel=false>▼</div>\n\t<div dojoType=\"dijit.layout._ScrollingTabControllerButton\"\n\t\t\tclass=\"tabStripButton-${tabPosition}\"\n\t\t\tid=\"${id}_leftBtn\" iconClass=\"dijitTabStripSlideLeftIcon\"\n\t\t\tdojoAttachPoint=\"_leftBtn\" dojoAttachEvent=\"onClick: doSlideLeft\" showLabel=false>◀</div>\n\t<div dojoType=\"dijit.layout._ScrollingTabControllerButton\"\n\t\t\tclass=\"tabStripButton-${tabPosition}\"\n\t\t\tid=\"${id}_rightBtn\" iconClass=\"dijitTabStripSlideRightIcon\"\n\t\t\tdojoAttachPoint=\"_rightBtn\" dojoAttachEvent=\"onClick: doSlideRight\" showLabel=false>▶</div>\n\t<div class='dijitTabListWrapper' dojoAttachPoint='tablistWrapper'>\n\t\t<div wairole='tablist' dojoAttachEvent='onkeypress:onkeypress'\n\t\t\t\tdojoAttachPoint='containerNode' class='nowrapTabStrip'></div>\n\t</div>\n</div>\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: 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" + }), + + postCreate: 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); + var menuItem; + if(this.useMenu){ + var containerId = this.containerId; + menuItem = new dijit.MenuItem({ + id: page.id + "_stcMi", + label: page.title, + dir: page.dir, + lang: page.lang, + onClick: dojo.hitch(this, function(){ + var container = dijit.byId(containerId); + container.selectChild(page); + }) + }); + this._menuChildren[page.id] = menuItem; + this._menu.addChild(menuItem, insertIndex); + } + + // update the menuItem label when the button label is updated + this.pane2handles[page.id].push( + this.connect(this.pane2button[page.id], "set", function(name, value){ + if(this._postStartup){ + if(name == "label"){ + if(menuItem){ + menuItem.set(name, value); + } + + // The changed label will have changed the width of the + // buttons, so do a resize + if(this._dim){ + this.resize(this._dim); + } + } + } + }) + ); + + // 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; + } + + // delete menu entry corresponding to pane that was removed from TabContainer + if(this.useMenu && page && page.id && this._menuChildren[page.id]){ + this._menu.removeChild(this._menuChildren[page.id]); + this._menuChildren[page.id].destroy(); + delete this._menuChildren[page.id]; + } + + 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. + this._menuChildren = {}; + + // 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.marginBox(btn).w; + return true; + }else{ + dojo.style(btn, "display", "none"); + return false; + } + }, this); + + if(this.useMenu){ + // Create the menu that is used to select tabs. + this._menu = new dijit.Menu({ + id: this.id + "_menu", + dir: this.dir, + lang: this.lang, + targetNodeIds: [this._menuBtn.domNode], + leftClickToOpen: true, + refocus: false // selecting a menu item sets focus to a TabButton + }); + this._supportingWidgets.push(this._menu); + } + }, + + _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; + }, + + _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(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(e){ + // summary: + // Scrolls the menu to the right. + // e: + // The mouse click event. + this.doSlide(1, this._getBtnNode(e)); + }, + + doSlideLeft: function(e){ + // summary: + // Scrolls the menu to the left. + // e: + // The mouse click event. + this.doSlide(-1,this._getBtnNode(e)); + }, + + doSlide: function(direction, 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(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._ScrollingTabControllerButton", + dijit.form.Button, + { + baseClass: "dijitTab tabStripButton", + + templateString: dojo.cache("dijit.layout", "templates/_ScrollingTabControllerButton.html", "<div dojoAttachEvent=\"onclick:_onButtonClick\">\n\t<div waiRole=\"presentation\" class=\"dijitTabInnerDiv\" dojoattachpoint=\"innerDiv,focusNode\">\n\t\t<div waiRole=\"presentation\" class=\"dijitTabContent dijitButtonContents\" dojoattachpoint=\"tabContent\">\n\t\t\t<img waiRole=\"presentation\" alt=\"\" src=\"${_blankGif}\" class=\"dijitTabStripIcon\" dojoAttachPoint=\"iconNode\"/>\n\t\t\t<span dojoAttachPoint=\"containerNode,titleNode\" class=\"dijitButtonText\"></span>\n\t\t</div>\n\t</div>\n</div>\n"), + + // Override inherited tabIndex: 0 from dijit.form.Button, because user shouldn't be + // able to tab to the left/right/menu buttons + tabIndex: "-1" + } +); + +} + +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.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: + // | <div dojoType="ProgressBar" + // | places="0" + // | progress="..." maximum="..."> + // | </div> + // + // description: + // Note that the progress bar is updated via (a non-standard) + // update() method, rather than via attr() like other widgets. + + // progress: [const] 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 + // TODO: rename to value for 2.0 + progress: "0", + + // 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. + indeterminate: false, + + // 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", "<div class=\"dijitProgressBar dijitProgressBarEmpty\"\n\t><div waiRole=\"progressbar\" dojoAttachPoint=\"internalProgress\" class=\"dijitProgressBarFull\"\n\t\t><div class=\"dijitProgressBarTile\"></div\n\t\t><span style=\"visibility:hidden\"> </span\n\t></div\n\t><div dojoAttachPoint=\"label\" class=\"dijitProgressBarLabel\" id=\"${id}_label\"> </div\n\t><img dojoAttachPoint=\"indeterminateHighContrastImage\" class=\"dijitProgressBarIndeterminateHighContrastImage\" alt=\"\"\n/></div>\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"), + + // public functions + postCreate: function(){ + this.inherited(arguments); + this.indeterminateHighContrastImage.setAttribute("src", + this._indeterminateHighContrastImagePath.toString()); + this.update(); + }, + + update: function(/*Object?*/attributes){ + // summary: + // Change attributes of ProgressBar, similar to attr(hash). + // + // 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}); + + // TODO: deprecate this method and use set() instead + + dojo.mixin(this, attributes || {}); + var tip = this.internalProgress; + var percent = 1, classFunc; + if(this.indeterminate){ + classFunc = "addClass"; + dijit.removeWaiState(tip, "valuenow"); + dijit.removeWaiState(tip, "valuemin"); + dijit.removeWaiState(tip, "valuemax"); + }else{ + classFunc = "removeClass"; + 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; + } + var text = this.report(percent); + this.label.firstChild.nodeValue = text; + dijit.setWaiState(tip, "describedby", this.label.id); + dijit.setWaiState(tip, "valuenow", this.progress); + dijit.setWaiState(tip, "valuemin", 0); + dijit.setWaiState(tip, "valuemax", this.maximum); + } + dojo[classFunc](this.domNode, "dijitProgressBarIndeterminate"); + tip.style.width = (percent * 100) + "%"; + this.onChange(); + }, + + _setValueAttr: function(v){ + if(v == Infinity){ + this.update({indeterminate:true}); + }else{ + this.update({indeterminate:false, progress:v}); + } + }, + + _getValueAttr: function(){ + return this.progress; + }, + + report: function(/*float*/percent){ + // summary: + // Generates message to show inside progress bar (normally indicating amount of task completed). + // May be overridden. + // tags: + // extension + + return dojo.number.format(percent, { type: "percent", places: this.places, locale: this.lang }); + }, + + onChange: function(){ + // summary: + // Callback fired when progress updates. + // tags: + // progress + } +}); + +} + +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: '<div class="dijitToolbarSeparator dijitInline" waiRole="presentation"></div>', + postCreate: function(){ 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"); + + + + + +dojo.declare("dijit.Toolbar", + [dijit._Widget, dijit._Templated, dijit._KeyNavContainer], + { + // summary: + // A Toolbar widget, used to hold things like `dijit.Editor` buttons + + templateString: + '<div class="dijit" waiRole="toolbar" tabIndex="${tabIndex}" dojoAttachPoint="containerNode">' + + // '<table style="table-layout: fixed" class="dijitReset dijitToolbarTable">' + // factor out style + // '<tr class="dijitReset" dojoAttachPoint="containerNode"></tr>'+ + // '</table>' + + '</div>', + + baseClass: "dijitToolbar", + + postCreate: function(){ + this.connectKeyNavHandlers( + this.isLeftToRight() ? [dojo.keys.LEFT_ARROW] : [dojo.keys.RIGHT_ARROW], + this.isLeftToRight() ? [dojo.keys.RIGHT_ARROW] : [dojo.keys.LEFT_ARROW] + ); + this.inherited(arguments); + }, + + startup: function(){ + if(this._started){ return; } + + this.startupKeyNavChildren(); + + this.inherited(arguments); + } +} +); + +// For back-compat, remove for 2.0 + + +} + +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<this.childrenAttrs.length; i++){ + var vals = store.getValues(parentItem, this.childrenAttrs[i]); + childItems = childItems.concat(vals); + } + + // count how many items need to be loaded + var _waitCount = 0; + if(!this.deferItemLoadingUntilExpand){ + dojo.forEach(childItems, function(item){ if(!store.isItemLoaded(item)){ _waitCount++; } }); + } + + if(_waitCount == 0){ + // all items are already loaded (or we aren't loading them). proceed... + onComplete(childItems); + }else{ + // still waiting for some or all of the items to load + dojo.forEach(childItems, function(item, idx){ + if(!store.isItemLoaded(item)){ + store.loadItem({ + item: item, + onItem: function(item){ + childItems[idx] = item; + if(--_waitCount == 0){ + // all nodes have been loaded, send them to the tree + onComplete(childItems); + } + }, + onError: onError + }); + } + }); + } + }, + + // ======================================================================= + // Inspecting items + + isItem: function(/* anything */ something){ + return this.store.isItem(something); // Boolean + }, + + fetchItemByIdentity: function(/* object */ keywordArgs){ + this.store.fetchItemByIdentity(keywordArgs); + }, + + getIdentity: function(/* item */ item){ + return this.store.getIdentity(item); // Object + }, + + getLabel: function(/*dojo.data.Item*/ item){ + // summary: + // Get the label for an item + if(this.labelAttr){ + return this.store.getValue(item,this.labelAttr); // String + }else{ + return this.store.getLabel(item); // String + } + }, + + // ======================================================================= + // Write interface + + newItem: function(/* dojo.dnd.Item */ args, /*Item*/ parent, /*int?*/ insertIndex){ + // summary: + // Creates a new item. See `dojo.data.api.Write` for details on args. + // Used in drag & drop when item from external source dropped onto tree. + // description: + // Developers will need to override this method if new items get added + // to parents with multiple children attributes, in order to define which + // children attribute points to the new item. + + var pInfo = {parent: parent, attribute: this.childrenAttrs[0], insertIndex: insertIndex}; + + if(this.newItemIdAttr && args[this.newItemIdAttr]){ + // Maybe there's already a corresponding item in the store; if so, reuse it. + this.fetchItemByIdentity({identity: args[this.newItemIdAttr], scope: this, onItem: function(item){ + if(item){ + // There's already a matching item in store, use it + this.pasteItem(item, null, parent, true, insertIndex); + }else{ + // Create new item in the tree, based on the drag source. + this.store.newItem(args, pInfo); + } + }}); + }else{ + // [as far as we know] there is no id so we must assume this is a new item + this.store.newItem(args, pInfo); + } + }, + + pasteItem: function(/*Item*/ childItem, /*Item*/ oldParentItem, /*Item*/ newParentItem, /*Boolean*/ bCopy, /*int?*/ insertIndex){ + // summary: + // Move or copy an item from one parent item to another. + // Used in drag & drop + var store = this.store, + parentAttr = this.childrenAttrs[0]; // name of "children" attr in parent item + + // remove child from source item, and record the attribute that child occurred in + if(oldParentItem){ + dojo.forEach(this.childrenAttrs, function(attr){ + if(store.containsValue(oldParentItem, attr, childItem)){ + if(!bCopy){ + var values = dojo.filter(store.getValues(oldParentItem, attr), function(x){ + return x != childItem; + }); + store.setValues(oldParentItem, attr, values); + } + parentAttr = attr; + } + }); + } + + // modify target item's children attribute to include this item + if(newParentItem){ + if(typeof insertIndex == "number"){ + // call slice() to avoid modifying the original array, confusing the data store + var childItems = store.getValues(newParentItem, parentAttr).slice(); + childItems.splice(insertIndex, 0, childItem); + store.setValues(newParentItem, parentAttr, childItems); + }else{ + store.setValues(newParentItem, parentAttr, + store.getValues(newParentItem, parentAttr).concat(childItem)); + } + } + }, + + // ======================================================================= + // Callbacks + + onChange: function(/*dojo.data.Item*/ item){ + // summary: + // Callback whenever an item has changed, so that Tree + // can update the label, icon, etc. Note that changes + // to an item's children or parent(s) will trigger an + // onChildrenChange() so you can ignore those changes here. + // tags: + // callback + }, + + onChildrenChange: function(/*dojo.data.Item*/ parent, /*dojo.data.Item[]*/ newChildrenList){ + // summary: + // Callback to do notifications about new, updated, or deleted items. + // tags: + // callback + }, + + onDelete: function(/*dojo.data.Item*/ parent, /*dojo.data.Item[]*/ newChildrenList){ + // summary: + // Callback when an item has been deleted. + // description: + // Note that there will also be an onChildrenChange() callback for the parent + // of this item. + // tags: + // callback + }, + + // ======================================================================= + // Events from data store + + onNewItem: function(/* dojo.data.Item */ item, /* Object */ parentInfo){ + // summary: + // Handler for when new items appear in the store, either from a drop operation + // or some other way. Updates the tree view (if necessary). + // description: + // If the new item is a child of an existing item, + // calls onChildrenChange() with the new list of children + // for that existing item. + // + // tags: + // extension + + // We only care about the new item if it has a parent that corresponds to a TreeNode + // we are currently displaying + if(!parentInfo){ + return; + } + + // Call onChildrenChange() on parent (ie, existing) item with new list of children + // In the common case, the new list of children is simply parentInfo.newValue or + // [ parentInfo.newValue ], although if items in the store has multiple + // child attributes (see `childrenAttr`), then it's a superset of parentInfo.newValue, + // so call getChildren() to be sure to get right answer. + this.getChildren(parentInfo.item, dojo.hitch(this, function(children){ + this.onChildrenChange(parentInfo.item, children); + })); + }, + + onDeleteItem: function(/*Object*/ item){ + // summary: + // Handler for delete notifications from underlying store + this.onDelete(item); + }, + + onSetItem: function(/* item */ item, + /* attribute-name-string */ attribute, + /* object | array */ oldValue, + /* object | array */ newValue){ + // summary: + // Updates the tree view according to changes in the data store. + // description: + // Handles updates to an item's children by calling onChildrenChange(), and + // other updates to an item by calling onChange(). + // + // See `onNewItem` for more details on handling updates to an item's children. + // tags: + // extension + + if(dojo.indexOf(this.childrenAttrs, attribute) != -1){ + // item's children list changed + this.getChildren(item, dojo.hitch(this, function(children){ + // See comments in onNewItem() about calling getChildren() + this.onChildrenChange(item, children); + })); + }else{ + // item's label/icon/etc. changed. + this.onChange(item); + } + } + }); + + + +} + +if(!dojo._hasResource["dijit.tree.ForestStoreModel"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.tree.ForestStoreModel"] = true; +dojo.provide("dijit.tree.ForestStoreModel"); + + + +dojo.declare("dijit.tree.ForestStoreModel", dijit.tree.TreeStoreModel, { + // summary: + // Interface between Tree and a dojo.store that doesn't have a root item, + // i.e. has multiple "top level" items. + // + // description + // Use this class to wrap a dojo.store, making all the items matching the specified query + // appear as children of a fabricated "root item". If no query is specified then all the + // items returned by fetch() on the underlying store become children of the root item. + // It allows dijit.Tree to assume a single root item, even if the store doesn't have one. + + // Parameters to constructor + + // rootId: String + // ID of fabricated root item + rootId: "$root$", + + // rootLabel: String + // Label of fabricated root item + rootLabel: "ROOT", + + // query: String + // Specifies the set of children of the root item. + // example: + // | {type:'continent'} + query: null, + + // End of parameters to constructor + + constructor: function(params){ + // summary: + // Sets up variables, etc. + // tags: + // private + + // Make dummy root item + this.root = { + store: this, + root: true, + id: params.rootId, + label: params.rootLabel, + children: params.rootChildren // optional param + }; + }, + + // ======================================================================= + // Methods for traversing hierarchy + + 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) + // tags: + // extension + return item === this.root || this.inherited(arguments); + }, + + getChildren: function(/*dojo.data.Item*/ parentItem, /*function(items)*/ callback, /*function*/ onError){ + // summary: + // Calls onComplete() with array of child items of given parent item, all loaded. + if(parentItem === this.root){ + if(this.root.children){ + // already loaded, just return + callback(this.root.children); + }else{ + this.store.fetch({ + query: this.query, + onComplete: dojo.hitch(this, function(items){ + this.root.children = items; + callback(items); + }), + onError: onError + }); + } + }else{ + this.inherited(arguments); + } + }, + + // ======================================================================= + // Inspecting items + + isItem: function(/* anything */ something){ + return (something === this.root) ? true : this.inherited(arguments); + }, + + fetchItemByIdentity: function(/* object */ keywordArgs){ + if(keywordArgs.identity == this.root.id){ + var scope = keywordArgs.scope?keywordArgs.scope:dojo.global; + if(keywordArgs.onItem){ + keywordArgs.onItem.call(scope, this.root); + } + }else{ + this.inherited(arguments); + } + }, + + getIdentity: function(/* item */ item){ + return (item === this.root) ? this.root.id : this.inherited(arguments); + }, + + getLabel: function(/* item */ item){ + return (item === this.root) ? this.root.label : this.inherited(arguments); + }, + + // ======================================================================= + // Write interface + + newItem: function(/* dojo.dnd.Item */ args, /*Item*/ parent, /*int?*/ insertIndex){ + // summary: + // Creates a new item. See dojo.data.api.Write for details on args. + // Used in drag & drop when item from external source dropped onto tree. + if(parent === this.root){ + this.onNewRootItem(args); + return this.store.newItem(args); + }else{ + return this.inherited(arguments); + } + }, + + onNewRootItem: function(args){ + // summary: + // User can override this method to modify a new element that's being + // added to the root of the tree, for example to add a flag like root=true + }, + + pasteItem: function(/*Item*/ childItem, /*Item*/ oldParentItem, /*Item*/ newParentItem, /*Boolean*/ bCopy, /*int?*/ insertIndex){ + // summary: + // Move or copy an item from one parent item to another. + // Used in drag & drop + if(oldParentItem === this.root){ + if(!bCopy){ + // It's onLeaveRoot()'s responsibility to modify the item so it no longer matches + // this.query... thus triggering an onChildrenChange() event to notify the Tree + // that this element is no longer a child of the root node + this.onLeaveRoot(childItem); + } + } + dijit.tree.TreeStoreModel.prototype.pasteItem.call(this, childItem, + oldParentItem === this.root ? null : oldParentItem, + newParentItem === this.root ? null : newParentItem, + bCopy, + insertIndex + ); + if(newParentItem === this.root){ + // It's onAddToRoot()'s responsibility to modify the item so it matches + // this.query... thus triggering an onChildrenChange() event to notify the Tree + // that this element is now a child of the root node + this.onAddToRoot(childItem); + } + }, + + // ======================================================================= + // Handling for top level children + + onAddToRoot: function(/* item */ item){ + // summary: + // Called when item added to root of tree; user must override this method + // to modify the item so that it matches the query for top level items + // example: + // | store.setValue(item, "root", true); + // tags: + // extension + console.log(this, ": item ", item, " added to root"); + }, + + onLeaveRoot: function(/* item */ item){ + // summary: + // Called when item removed from root of tree; user must override this method + // to modify the item so it doesn't match the query for top level items + // example: + // | store.unsetAttribute(item, "root"); + // tags: + // extension + console.log(this, ": item ", item, " removed from root"); + }, + + // ======================================================================= + // Events from data store + + _requeryTop: function(){ + // reruns the query for the children of the root node, + // sending out an onSet notification if those children have changed + var oldChildren = this.root.children || []; + this.store.fetch({ + query: this.query, + onComplete: dojo.hitch(this, function(newChildren){ + this.root.children = newChildren; + + // If the list of children or the order of children has changed... + if(oldChildren.length != newChildren.length || + dojo.some(oldChildren, function(item, idx){ return newChildren[idx] != item;})){ + this.onChildrenChange(this.root, newChildren); + } + }) + }); + }, + + onNewItem: function(/* dojo.data.Item */ item, /* Object */ parentInfo){ + // summary: + // Handler for when new items appear in the store. Developers should override this + // method to be more efficient based on their app/data. + // description: + // Note that the default implementation requeries the top level items every time + // a new item is created, since any new item could be a top level item (even in + // addition to being a child of another item, since items can have multiple parents). + // + // Developers can override this function to do something more efficient if they can + // detect which items are possible top level items (based on the item and the + // parentInfo parameters). Often all top level items have parentInfo==null, but + // that will depend on which store you use and what your data is like. + // tags: + // extension + this._requeryTop(); + + this.inherited(arguments); + }, + + onDeleteItem: function(/*Object*/ item){ + // summary: + // Handler for delete notifications from underlying store + + // check if this was a child of root, and if so send notification that root's children + // have changed + if(dojo.indexOf(this.root.children, item) != -1){ + this._requeryTop(); + } + + this.inherited(arguments); + } +}); + + + +} + +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: 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", "<div class=\"dijitTreeNode\" waiRole=\"presentation\"\n\t><div dojoAttachPoint=\"rowNode\" class=\"dijitTreeRow\" waiRole=\"presentation\" dojoAttachEvent=\"onmouseenter:_onMouseEnter, onmouseleave:_onMouseLeave, onclick:_onClick, ondblclick:_onDblClick\"\n\t\t><img src=\"${_blankGif}\" alt=\"\" dojoAttachPoint=\"expandoNode\" class=\"dijitTreeExpando\" waiRole=\"presentation\"\n\t\t/><span dojoAttachPoint=\"expandoNodeText\" class=\"dijitExpandoText\" waiRole=\"presentation\"\n\t\t></span\n\t\t><span dojoAttachPoint=\"contentNode\"\n\t\t\tclass=\"dijitTreeContent\" waiRole=\"presentation\">\n\t\t\t<img src=\"${_blankGif}\" alt=\"\" dojoAttachPoint=\"iconNode\" class=\"dijitIcon dijitTreeIcon\" waiRole=\"presentation\"\n\t\t\t/><span dojoAttachPoint=\"labelNode\" class=\"dijitTreeLabel\" wairole=\"treeitem\" tabindex=\"-1\" waiState=\"selected-false\" dojoAttachEvent=\"onfocus:_onLabelFocus\"></span>\n\t\t</span\n\t></div>\n\t<div dojoAttachPoint=\"containerNode\" class=\"dijitTreeContainer\" waiRole=\"presentation\" style=\"display: none;\"></div>\n</div>\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"} + }), + + postCreate: 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); + } + }, + + _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. + this.indent = indent; + + // 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); + }); + }, + + 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"; + + if(this[clsName]){ + dojo.removeClass(this[nodeName], this[clsName]); + } + this[clsName] = this.tree["get" + upper + "Class"](item, this.isExpanded); + if(this[clsName]){ + dojo.addClass(this[nodeName], this[clsName]); + } + 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.removeClass(this.expandoNode, styles); + dojo.addClass(this.expandoNode, styles[idx]); + + // 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"); + 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<existingNodes.length;i++){ + if(existingNodes[i] && !existingNodes[i].getParent()){ + node = existingNodes[i]; + node.set('indent', this.indent+1); + break; + } + } + } + if(!node){ + node = this.tree._createTreeNode({ + item: item, + tree: tree, + isExpandable: model.mayHaveChildren(item), + label: tree.getLabel(item), + tooltip: tree.getTooltip(item), + dir: tree.dir, + lang: tree.lang, + indent: this.indent + 1 + }); + if(existingNodes){ + existingNodes.push(node); + }else{ + tree._itemNodesMap[id] = [node]; + } + } + this.addChild(node); + + // If node was previously opened then open it again now (this may trigger + // more data store accesses, recursively) + if(this.tree.autoExpand || this.tree._state(item)){ + defs.push(tree._expandNode(node)); + } + }, this); + + // note that updateLayout() needs to be called on each child after + // _all_ the children exist + dojo.forEach(this.getChildren(), function(child, idx){ + child._updateLayout(); + }); + }else{ + this.isExpandable=false; + } + + if(this._setExpando){ + // change expando to/from dot or + icon, as appropriate + this._setExpando(false); + } + + // Set leaf icon or folder icon, as appropriate + this._updateItemClasses(this.item); + + // On initial tree show, make the selected TreeNode as either the root node of the tree, + // or the first child, if the root node is hidden + if(this == tree.rootNode){ + var fc = this.tree.showRoot ? this : this.getChildren()[0]; + if(fc){ + fc.setFocusable(true); + tree.lastFocused = fc; + }else{ + // fallback: no nodes in tree so focus on Tree <div> itself + tree.domNode.setAttribute("tabIndex", "0"); + } + } + + return new dojo.DeferredList(defs); // dojo.Deferred + }, + + 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"], + + // path: String[] or Item[] + // Full path from rootNode to selected node expressed as array of items or array of ids. + // Since setting the path may be asynchronous (because ofwaiting on dojo.data), set("path", ...) + // returns a Deferred to indicate when the set is complete. + path: [], + + // selectedItem: [readonly] Item + // The currently selected item in this tree. + // This property can only be set (via set('selectedItem', ...)) when that item is already + // visible in the tree. (I.e. the tree has already been expanded to show that node.) + // Should generally use `path` attribute to set the selected item instead. + 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", "<div class=\"dijitTree dijitTreeContainer\" waiRole=\"tree\"\n\tdojoAttachEvent=\"onkeypress:_onKeyPress\">\n\t<div class=\"dijitInline dijitTreeIndent\" style=\"position: absolute; top: -9999px\" dojoAttachPoint=\"indentDetector\"></div>\n</div>\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". + dndController: null, + + // 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<this.dndParams.length;i++){ + if(this[this.dndParams[i]]){ + params[this.dndParams[i]] = this[this.dndParams[i]]; + } + } + this.dndController = new this.dndController(this, params); + } + }, + + _store2model: function(){ + // summary: + // User specified a store&query rather than model, so create model from store/query + this._v10Compat = true; + dojo.deprecated("Tree: from version 2.0, should specify a model object rather than a store/query"); + + var modelParams = { + id: this.id + "_ForestStoreModel", + store: this.store, + query: this.query, + childrenAttrs: this.childrenAttr + }; + + // Only override the model's mayHaveChildren() method if the user has specified an override + if(this.params.mayHaveChildren){ + modelParams.mayHaveChildren = dojo.hitch(this, "mayHaveChildren"); + } + + if(this.params.getItemChildren){ + modelParams.getChildren = dojo.hitch(this, function(item, onComplete, onError){ + this.getItemChildren((this._v10Compat && item === this.model.root) ? null : item, onComplete, onError); + }); + } + this.model = new dijit.tree.ForestStoreModel(modelParams); + + // For backwards compatibility, the visibility of the root node is controlled by + // whether or not the user has specified a label + this.showRoot = Boolean(this.label); + }, + + onLoad: function(){ + // summary: + // Called when tree finishes loading and expanding. + // description: + // If persist == true the loading may encompass many levels of fetches + // from the data store, each asynchronous. Waits for all to finish. + // tags: + // callback + }, + + _load: function(){ + // summary: + // Initial load of the tree. + // Load root node (possibly hidden) and it's children. + this.model.getRoot( + dojo.hitch(this, function(item){ + var rn = (this.rootNode = this.tree._createTreeNode({ + item: item, + tree: this, + isExpandable: true, + label: this.label || this.getLabel(item), + indent: this.showRoot ? 0 : -1 + })); + if(!this.showRoot){ + rn.rowNode.style.display="none"; + } + this.domNode.appendChild(rn.domNode); + var identity = this.model.getIdentity(item); + if(this._itemNodesMap[identity]){ + this._itemNodesMap[identity].push(rn); + }else{ + this._itemNodesMap[identity] = [rn]; + } + + rn._updateLayout(); // sets "dijitTreeIsRoot" CSS classname + + // load top level children and then fire onLoad() event + this._expandNode(rn).addCallback(dojo.hitch(this, function(){ + this._loadDeferred.callback(true); + this.onLoad(); + })); + }), + function(err){ + console.error(this, ": error loading root: ", err); + } + ); + }, + + getNodesByItem: function(/*dojo.data.Item or id*/ item){ + // summary: + // Returns all tree nodes that refer to an item + // returns: + // Array of tree nodes that refer to passed item + + if(!item){ return []; } + var identity = dojo.isString(item) ? item : this.model.getIdentity(item); + // return a copy so widget don't get messed up by changes to returned array + return [].concat(this._itemNodesMap[identity]); + }, + + _setSelectedItemAttr: function(/*dojo.data.Item or id*/ item){ + // summary: + // Select a tree node related to passed item. + // WARNING: if model use multi-parented items or desired tree node isn't already loaded + // behavior is undefined. Use set('path', ...) instead. + + var oldValue = this.get("selectedItem"); + var identity = (!item || dojo.isString(item)) ? item : this.model.getIdentity(item); + if(identity == oldValue ? this.model.getIdentity(oldValue) : null){ return; } + var nodes = this._itemNodesMap[identity]; + this._selectNode((nodes && nodes[0]) || null); //select the first item + }, + + _getSelectedItemAttr: function(){ + // summary: + // Return item related to selected tree node. + return this.selectedNode && this.selectedNode.item; + }, + + _setPathAttr: function(/*Item[] || String[]*/ path){ + // summary: + // Select the tree node identified by passed path. + // path: + // Array of items or item id's + // returns: + // Deferred to indicate when the set is complete + + var d = new dojo.Deferred(); + + this._selectNode(null); + if(!path || !path.length){ + d.resolve(true); + return d; + } + + // If this is called during initialization, defer running until Tree has finished loading + this._loadDeferred.addCallback(dojo.hitch(this, function(){ + if(!this.rootNode){ + d.reject(new Error("!this.rootNode")); + return; + } + if(path[0] !== this.rootNode.item && (dojo.isString(path[0]) && path[0] != this.model.getIdentity(this.rootNode.item))){ + d.reject(new Error(this.id + ":path[0] doesn't match this.rootNode.item. Maybe you are using the wrong tree.")); + return; + } + path.shift(); + + var node = this.rootNode; + + function advance(){ + // summary: + // Called when "node" has completed loading and expanding. Pop the next item from the path + // (which must be a child of "node") and advance to it, and then recurse. + + // Set item and identity to next item in path (node is pointing to the item that was popped + // from the path _last_ time. + var item = path.shift(), + identity = dojo.isString(item) ? item : this.model.getIdentity(item); + + // Change "node" from previous item in path to the item we just popped from path + dojo.some(this._itemNodesMap[identity], function(n){ + if(n.getParent() == node){ + node = n; + return true; + } + return false; + }); + + if(path.length){ + // Need to do more expanding + this._expandNode(node).addCallback(dojo.hitch(this, advance)); + }else{ + // Final destination node, select it + this._selectNode(node); + + // signal that path setting is finished + d.resolve(true); + } + } + + this._expandNode(node).addCallback(dojo.hitch(this, advance)); + })); + + return d; + }, + + _getPathAttr: function(){ + // summary: + // Return an array of items that is the path to selected tree node. + if(!this.selectedNode){ return; } + var res = []; + var treeNode = this.selectedNode; + while(treeNode && treeNode !== this.rootNode){ + res.unshift(treeNode.item); + treeNode = treeNode.getParent(); + } + res.unshift(this.rootNode.item); + return res; + }, + + ////////////// Data store related functions ////////////////////// + // These just get passed to the model; they are here for back-compat + + mayHaveChildren: function(/*dojo.data.Item*/ item){ + // summary: + // Deprecated. This should be specified on the model itself. + // + // Overridable function to tell if an item has or may have children. + // Controls whether or not +/- expando icon is shown. + // (For efficiency reasons we may not want to check if an element actually + // has children until user clicks the expando node) + // tags: + // deprecated + }, + + getItemChildren: function(/*dojo.data.Item*/ parentItem, /*function(items)*/ onComplete){ + // summary: + // Deprecated. This should be specified on the model itself. + // + // Overridable function that return array of child items of given parent item, + // or if parentItem==null then return top items in tree + // tags: + // deprecated + }, + + /////////////////////////////////////////////////////// + // Functions for converting an item to a TreeNode + getLabel: function(/*dojo.data.Item*/ item){ + // summary: + // Overridable function to get the label for a tree node (given the item) + // tags: + // extension + return this.model.getLabel(item); // String + }, + + getIconClass: function(/*dojo.data.Item*/ item, /*Boolean*/ opened){ + // summary: + // Overridable function to return CSS class name to display icon + // tags: + // extension + return (!item || this.model.mayHaveChildren(item)) ? (opened ? "dijitFolderOpened" : "dijitFolderClosed") : "dijitLeaf" + }, + + getLabelClass: function(/*dojo.data.Item*/ item, /*Boolean*/ opened){ + // summary: + // Overridable function to return CSS class name to display label + // tags: + // extension + }, + + getRowClass: function(/*dojo.data.Item*/ item, /*Boolean*/ opened){ + // summary: + // Overridable function to return CSS class name to display row + // tags: + // extension + }, + + getIconStyle: function(/*dojo.data.Item*/ item, /*Boolean*/ opened){ + // summary: + // Overridable function to return CSS styles to display icon + // returns: + // Object suitable for input to dojo.style() like {backgroundImage: "url(...)"} + // tags: + // extension + }, + + getLabelStyle: function(/*dojo.data.Item*/ item, /*Boolean*/ opened){ + // summary: + // Overridable function to return CSS styles to display label + // returns: + // Object suitable for input to dojo.style() like {color: "red", background: "green"} + // tags: + // extension + }, + + getRowStyle: function(/*dojo.data.Item*/ item, /*Boolean*/ opened){ + // summary: + // Overridable function to return CSS styles to display row + // returns: + // Object suitable for input to dojo.style() like {background-color: "#bbb"} + // tags: + // extension + }, + + getTooltip: function(/*dojo.data.Item*/ item){ + // summary: + // Overridable function to get the tooltip for a tree node (given the item) + // tags: + // extension + return ""; // String + }, + + /////////// Keyboard and Mouse handlers //////////////////// + + _onKeyPress: function(/*Event*/ e){ + // summary: + // Translates keypress events into commands for the controller + if(e.altKey){ return; } + var dk = dojo.keys; + var treeNode = dijit.getEnclosingWidget(e.target); + if(!treeNode){ return; } + + var key = e.charOrCode; + if(typeof key == "string"){ // handle printables (letter navigation) + // Check for key navigation. + if(!e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey){ + this._onLetterKeyNav( { node: treeNode, key: key.toLowerCase() } ); + dojo.stopEvent(e); + } + }else{ // handle non-printables (arrow keys) + // clear record of recent printables (being saved for multi-char letter navigation), + // because "a", down-arrow, "b" shouldn't search for "ab" + if(this._curSearch){ + clearTimeout(this._curSearch.timer); + delete this._curSearch; + } + + var map = this._keyHandlerMap; + if(!map){ + // setup table mapping keys to events + map = {}; + map[dk.ENTER]="_onEnterKey"; + map[this.isLeftToRight() ? dk.LEFT_ARROW : dk.RIGHT_ARROW]="_onLeftArrow"; + map[this.isLeftToRight() ? dk.RIGHT_ARROW : dk.LEFT_ARROW]="_onRightArrow"; + map[dk.UP_ARROW]="_onUpArrow"; + map[dk.DOWN_ARROW]="_onDownArrow"; + map[dk.HOME]="_onHomeKey"; + map[dk.END]="_onEndKey"; + this._keyHandlerMap = map; + } + if(this._keyHandlerMap[key]){ + this[this._keyHandlerMap[key]]( { node: treeNode, item: treeNode.item, evt: e } ); + dojo.stopEvent(e); + } + } + }, + + _onEnterKey: function(/*Object*/ message, /*Event*/ evt){ + this._publish("execute", { item: message.item, node: message.node } ); + this._selectNode(message.node); + this.onClick(message.item, message.node, evt); + }, + + _onDownArrow: function(/*Object*/ message){ + // summary: + // down arrow pressed; get next visible node, set focus there + var node = this._getNextNode(message.node); + if(node && node.isTreeNode){ + this.focusNode(node); + } + }, + + _onUpArrow: function(/*Object*/ message){ + // summary: + // Up arrow pressed; move to previous visible node + + var node = message.node; + + // if younger siblings + var previousSibling = node.getPreviousSibling(); + if(previousSibling){ + node = previousSibling; + // if the previous node is expanded, dive in deep + while(node.isExpandable && node.isExpanded && node.hasChildren()){ + // move to the last child + var children = node.getChildren(); + node = children[children.length-1]; + } + }else{ + // if this is the first child, return the parent + // unless the parent is the root of a tree with a hidden root + var parent = node.getParent(); + if(!(!this.showRoot && parent === this.rootNode)){ + node = parent; + } + } + + if(node && node.isTreeNode){ + this.focusNode(node); + } + }, + + _onRightArrow: function(/*Object*/ message){ + // summary: + // Right arrow pressed; go to child node + var node = message.node; + + // if not expanded, expand, else move to 1st child + if(node.isExpandable && !node.isExpanded){ + this._expandNode(node); + }else if(node.hasChildren()){ + node = node.getChildren()[0]; + if(node && node.isTreeNode){ + this.focusNode(node); + } + } + }, + + _onLeftArrow: function(/*Object*/ message){ + // summary: + // Left arrow pressed. + // If not collapsed, collapse, else move to parent. + + var node = message.node; + + if(node.isExpandable && node.isExpanded){ + this._collapseNode(node); + }else{ + var parent = node.getParent(); + if(parent && parent.isTreeNode && !(!this.showRoot && parent === this.rootNode)){ + this.focusNode(parent); + } + } + }, + + _onHomeKey: function(){ + // summary: + // Home key pressed; get first visible node, and set focus there + var node = this._getRootOrFirstNode(); + if(node){ + this.focusNode(node); + } + }, + + _onEndKey: function(/*Object*/ message){ + // summary: + // End key pressed; go to last visible node. + + var node = this.rootNode; + while(node.isExpanded){ + var c = node.getChildren(); + node = c[c.length - 1]; + } + + if(node && node.isTreeNode){ + this.focusNode(node); + } + }, + + // multiCharSearchDuration: Number + // If multiple characters are typed where each keystroke happens within + // multiCharSearchDuration of the previous keystroke, + // search for nodes matching all the keystrokes. + // + // For example, typing "ab" will search for entries starting with + // "ab" unless the delay between "a" and "b" is greater than multiCharSearchDuration. + multiCharSearchDuration: 250, + + _onLetterKeyNav: function(message){ + // summary: + // Called when user presses a prinatable key; search for node starting with recently typed letters. + // message: Object + // Like { node: TreeNode, key: 'a' } where key is the key the user pressed. + + // Branch depending on whether this key starts a new search, or modifies an existing search + var cs = this._curSearch; + if(cs){ + // We are continuing a search. Ex: user has pressed 'a', and now has pressed + // 'b', so we want to search for nodes starting w/"ab". + cs.pattern = cs.pattern + message.key; + clearTimeout(cs.timer); + }else{ + // We are starting a new search + cs = this._curSearch = { + pattern: message.key, + startNode: message.node + }; + } + + // set/reset timer to forget recent keystrokes + var self = this; + cs.timer = setTimeout(function(){ + delete self._curSearch; + }, this.multiCharSearchDuration); + + // Navigate to TreeNode matching keystrokes [entered so far]. + var node = cs.startNode; + do{ + node = this._getNextNode(node); + //check for last node, jump to first node if necessary + if(!node){ + node = this._getRootOrFirstNode(); + } + }while(node !== cs.startNode && (node.label.toLowerCase().substr(0, cs.pattern.length) != cs.pattern)); + if(node && node.isTreeNode){ + // no need to set focus if back where we started + if(node !== cs.startNode){ + this.focusNode(node); + } + } + }, + + _onClick: function(/*TreeNode*/ nodeWidget, /*Event*/ e){ + // summary: + // Translates click events into commands for the controller to process + + var domElement = e.target, + isExpandoClick = (domElement == nodeWidget.expandoNode || domElement == nodeWidget.expandoNodeText); + + if( (this.openOnClick && nodeWidget.isExpandable) || isExpandoClick ){ + // expando node was clicked, or label of a folder node was clicked; open it + if(nodeWidget.isExpandable){ + this._onExpandoClick({node:nodeWidget}); + } + }else{ + this._publish("execute", { item: nodeWidget.item, node: nodeWidget, evt: e } ); + this.onClick(nodeWidget.item, nodeWidget, e); + this.focusNode(nodeWidget); + } + if(!isExpandoClick){ + this._selectNode(nodeWidget); + } + dojo.stopEvent(e); + }, + _onDblClick: function(/*TreeNode*/ nodeWidget, /*Event*/ e){ + // summary: + // Translates double-click events into commands for the controller to process + + var domElement = e.target, + isExpandoClick = (domElement == nodeWidget.expandoNode || domElement == nodeWidget.expandoNodeText); + + if( (this.openOnDblClick && nodeWidget.isExpandable) ||isExpandoClick ){ + // expando node was clicked, or label of a folder node was clicked; open it + if(nodeWidget.isExpandable){ + this._onExpandoClick({node:nodeWidget}); + } + }else{ + this._publish("execute", { item: nodeWidget.item, node: nodeWidget, evt: e } ); + this.onDblClick(nodeWidget.item, nodeWidget, e); + this.focusNode(nodeWidget); + } + if(!isExpandoClick){ + this._selectNode(nodeWidget); + } + dojo.stopEvent(e); + }, + + _onExpandoClick: function(/*Object*/ message){ + // summary: + // User clicked the +/- icon; expand or collapse my children. + var node = message.node; + + // If we are collapsing, we might be hiding the currently focused node. + // Also, clicking the expando node might have erased focus from the current node. + // For simplicity's sake just focus on the node with the expando. + this.focusNode(node); + + if(node.isExpanded){ + this._collapseNode(node); + }else{ + this._expandNode(node); + } + }, + + onClick: function(/* dojo.data */ item, /*TreeNode*/ node, /*Event*/ evt){ + // summary: + // Callback when a tree node is clicked + // tags: + // callback + }, + onDblClick: function(/* dojo.data */ item, /*TreeNode*/ node, /*Event*/ evt){ + // summary: + // Callback when a tree node is double-clicked + // tags: + // callback + }, + onOpen: function(/* dojo.data */ item, /*TreeNode*/ node){ + // summary: + // Callback when a node is opened + // tags: + // callback + }, + onClose: function(/* dojo.data */ item, /*TreeNode*/ node){ + // summary: + // Callback when a node is closed + // tags: + // callback + }, + + _getNextNode: function(node){ + // summary: + // Get next visible node + + if(node.isExpandable && node.isExpanded && node.hasChildren()){ + // if this is an expanded node, get the first child + return node.getChildren()[0]; // _TreeNode + }else{ + // find a parent node with a sibling + while(node && node.isTreeNode){ + var returnNode = node.getNextSibling(); + if(returnNode){ + return returnNode; // _TreeNode + } + node = node.getParent(); + } + return null; + } + }, + + _getRootOrFirstNode: function(){ + // summary: + // Get first visible node + return this.showRoot ? this.rootNode : this.rootNode.getChildren()[0]; + }, + + _collapseNode: function(/*_TreeNode*/ node){ + // summary: + // Called when the user has requested to collapse the node + + if(node._expandNodeDeferred){ + delete node._expandNodeDeferred; + } + + if(node.isExpandable){ + if(node.state == "LOADING"){ + // ignore clicks while we are in the process of loading data + return; + } + + node.collapse(); + this.onClose(node.item, node); + + if(node.item){ + this._state(node.item,false); + this._saveState(); + } + } + }, + + _expandNode: function(/*_TreeNode*/ node, /*Boolean?*/ recursive){ + // summary: + // Called when the user has requested to expand the node + // recursive: + // Internal flag used when _expandNode() calls itself, don't set. + // returns: + // Deferred that fires when the node is loaded and opened and (if persist=true) all it's descendants + // that were previously opened too + + if(node._expandNodeDeferred && !recursive){ + // there's already an expand in progress (or completed), so just return + return node._expandNodeDeferred; // dojo.Deferred + } + + var model = this.model, + item = node.item, + _this = this; + + switch(node.state){ + case "UNCHECKED": + // need to load all the children, and then expand + node.markProcessing(); + + // Setup deferred to signal when the load and expand are finished. + // Save that deferred in this._expandDeferred as a flag that operation is in progress. + var def = (node._expandNodeDeferred = new dojo.Deferred()); + + // Get the children + model.getChildren( + item, + function(items){ + node.unmarkProcessing(); + + // Display the children and also start expanding any children that were previously expanded + // (if this.persist == true). The returned Deferred will fire when those expansions finish. + var scid = node.setChildItems(items); + + // Call _expandNode() again but this time it will just to do the animation (default branch). + // The returned Deferred will fire when the animation completes. + // TODO: seems like I can avoid recursion and just use a deferred to sequence the events? + var ed = _this._expandNode(node, true); + + // After the above two tasks (setChildItems() and recursive _expandNode()) finish, + // signal that I am done. + scid.addCallback(function(){ + ed.addCallback(function(){ + def.callback(); + }) + }); + }, + function(err){ + console.error(_this, ": error loading root children: ", err); + } + ); + break; + + default: // "LOADED" + // data is already loaded; just expand node + def = (node._expandNodeDeferred = node.expand()); + + this.onOpen(node.item, node); + + if(item){ + this._state(item, true); + this._saveState(); + } + } + + return def; // dojo.Deferred + }, + + ////////////////// Miscellaneous functions //////////////// + + focusNode: function(/* _tree.Node */ node){ + // summary: + // Focus on the specified node (which must be visible) + // tags: + // protected + + // set focus so that the label will be voiced using screen readers + dijit.focus(node.labelNode); + }, + + _selectNode: function(/*_tree.Node*/ node){ + // summary: + // Mark specified node as select, and unmark currently selected node. + // tags: + // protected + + if(this.selectedNode && !this.selectedNode._destroyed){ + this.selectedNode.setSelected(false); + } + if(node){ + node.setSelected(true); + } + this.selectedNode = node; + }, + + _onNodeFocus: function(/*dijit._Widget*/ node){ + // summary: + // Called when a TreeNode gets focus, either by user clicking + // it, or programatically by arrow key handling code. + // description: + // It marks that the current node is the selected one, and the previously + // selected node no longer is. + + if(node && node != this.lastFocused){ + if(this.lastFocused && !this.lastFocused._destroyed){ + // mark that the previously focsable node is no longer focusable + this.lastFocused.setFocusable(false); + } + + // mark that the new node is the currently selected one + node.setFocusable(true); + this.lastFocused = node; + } + }, + + _onNodeMouseEnter: function(/*dijit._Widget*/ node){ + // summary: + // Called when mouse is over a node (onmouseenter event), + // this is monitored by the DND code + }, + + _onNodeMouseLeave: function(/*dijit._Widget*/ node){ + // summary: + // Called when mouse leaves a node (onmouseleave event), + // this is monitored by the DND code + }, + + //////////////// Events from the model ////////////////////////// + + _onItemChange: function(/*Item*/ item){ + // summary: + // Processes notification of a change to an item's scalar values like label + var model = this.model, + identity = model.getIdentity(item), + nodes = this._itemNodesMap[identity]; + + if(nodes){ + var label = this.getLabel(item), + tooltip = this.getTooltip(item); + dojo.forEach(nodes, function(node){ + node.set({ + item: item, // theoretically could be new JS Object representing same item + label: label, + tooltip: tooltip + }); + node._updateItemClasses(item); + }); + } + }, + + _onItemChildrenChange: function(/*dojo.data.Item*/ parent, /*dojo.data.Item[]*/ newChildrenList){ + // summary: + // Processes notification of a change to an item's children + var model = this.model, + identity = model.getIdentity(parent), + parentNodes = this._itemNodesMap[identity]; + + if(parentNodes){ + dojo.forEach(parentNodes,function(parentNode){ + parentNode.setChildItems(newChildrenList); + }); + } + }, + + _onItemDelete: function(/*Item*/ item){ + // summary: + // Processes notification of a deletion of an item + var model = this.model, + identity = model.getIdentity(item), + nodes = this._itemNodesMap[identity]; + + if(nodes){ + dojo.forEach(nodes,function(node){ + var parent = node.getParent(); + if(parent){ + // if node has not already been orphaned from a _onSetItem(parent, "children", ..) call... + parent.removeChild(node); + } + node.destroyRecursive(); + }); + delete this._itemNodesMap[identity]; + } + }, + + /////////////// Miscellaneous funcs + + _initState: function(){ + // summary: + // Load in which nodes should be opened automatically + if(this.persist){ + var cookie = dojo.cookie(this.cookieName); + this._openedItemIds = {}; + if(cookie){ + dojo.forEach(cookie.split(','), function(item){ + this._openedItemIds[item] = true; + }, this); + } + } + }, + _state: function(item,expanded){ + // summary: + // Query or set expanded state for an item, + if(!this.persist){ + return false; + } + var id=this.model.getIdentity(item); + if(arguments.length === 1){ + return this._openedItemIds[id]; + } + if(expanded){ + this._openedItemIds[id] = true; + }else{ + delete this._openedItemIds[id]; + } + }, + _saveState: function(){ + // summary: + // Create and save a cookie with the currently expanded nodes identifiers + if(!this.persist){ + return; + } + var ary = []; + for(var id in this._openedItemIds){ + ary.push(id); + } + dojo.cookie(this.cookieName, ary.join(","), {expires:365}); + }, + + destroy: function(){ + if(this._curSearch){ + clearTimeout(this._curSearch.timer); + delete this._curSearch; + } + if(this.rootNode){ + this.rootNode.destroyRecursive(); + } + if(this.dndController && !dojo.isString(this.dndController)){ + this.dndController.destroy(); + } + this.rootNode = null; + this.inherited(arguments); + }, + + destroyRecursive: function(){ + // A tree is treated as a leaf, not as a node with children (like a grid), + // but defining destroyRecursive for back-compat. + this.destroy(); + }, + + resize: function(changeSize){ + if(changeSize){ + dojo.marginBox(this.domNode, changeSize); + dojo.style(this.domNode, "overflow", "auto"); // for scrollbars + } + + // The only JS sizing involved w/tree is the indentation, which is specified + // in CSS and read in through this dummy indentDetector node (tree must be + // visible and attached to the DOM to read this) + this._nodePixelIndent = dojo.marginBox(this.tree.indentDetector).w; + + if(this.tree.rootNode){ + // If tree has already loaded, then reset indent for all the nodes + this.tree.rootNode.set('indent', this.showRoot ? 0 : -1); + } + }, + + _createTreeNode: function(/*Object*/ args){ + // summary: + // creates a TreeNode + // description: + // Developers can override this method to define their own TreeNode class; + // However it will probably be removed in a future release in favor of a way + // of just specifying a widget for the label, rather than one that contains + // the children too. + return new dijit._TreeNode(args); + } +}); + +// For back-compat. TODO: remove in 2.0 + + + +} + +if(!dojo._hasResource["dojo.dnd.Container"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.dnd.Container"] = true; +dojo.provide("dojo.dnd.Container"); + + + + +/* + Container states: + "" - normal state + "Over" - mouse over a container + Container item states: + "" - normal state + "Over" - mouse over a container item +*/ + +/*===== +dojo.declare("dojo.dnd.__ContainerArgs", [], { + creator: function(){ + // summary: + // a creator function, which takes a data item, and returns an object like that: + // {node: newNode, data: usedData, type: arrayOfStrings} + }, + + // skipForm: Boolean + // don't start the drag operation, if clicked on form elements + skipForm: false, + + // dropParent: Node||String + // node or node's id to use as the parent node for dropped items + // (must be underneath the 'node' parameter in the DOM) + dropParent: null, + + // _skipStartup: Boolean + // skip startup(), which collects children, for deferred initialization + // (this is used in the markup mode) + _skipStartup: false +}); + +dojo.dnd.Item = function(){ + // summary: + // Represents (one of) the source node(s) being dragged. + // Contains (at least) the "type" and "data" attributes. + // type: String[] + // Type(s) of this item, by default this is ["text"] + // data: Object + // Logical representation of the object being dragged. + // If the drag object's type is "text" then data is a String, + // if it's another type then data could be a different Object, + // perhaps a name/value hash. + + this.type = type; + this.data = data; +} +=====*/ + +dojo.declare("dojo.dnd.Container", null, { + // summary: + // a Container object, which knows when mouse hovers over it, + // and over which element it hovers + + // object attributes (for markup) + skipForm: false, + + /*===== + // current: DomNode + // The DOM node the mouse is currently hovered over + current: null, + + // map: Hash<String, dojo.dnd.Item> + // 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.removeClass(this.node, prefix + this[state]); + dojo.addClass(this.node, prefix + newState); + 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.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 node = this.selection[key], + ret = { + data: dijit.getEnclosingWidget(node), + 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.rowNode; + this.currentWidget = widget; + }, + + onMouseOut: function(/*TreeNode*/ widget, /*Event*/ evt){ + // summary: + // Called when mouse is moved away from a TreeNode + // tags: + // protected + this.current = null; + this.currentWidget = 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.removeClass(this.node, prefix + this[state]); + dojo.addClass(this.node, prefix + newState); + 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<String, DomNode> + // (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; + this.simpleSelection=false; + + 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 + + getSelectedNodes: function(){ + // summary: + // Returns the set of selected nodes. + // Used by dndSource on the start of a drag. + // tags: + // protected + return this.selection; + }, + + selectNone: function(){ + // summary: + // Unselects all items + // tags: + // private + + return this._removeSelection()._removeAnchor(); // self + }, + + destroy: function(){ + // summary: + // Prepares the object to be garbage-collected + this.inherited(arguments); + this.selection = this.anchor = null; + }, + + // mouse events + onMouseDown: function(e){ + // summary: + // Event processor for onmousedown + // e: Event + // mouse event + // tags: + // protected + + if(!this.current){ return; } + + if(e.button == dojo.mouseButtons.RIGHT){ return; } // ignore right-click + + var treeNode = dijit.getEnclosingWidget(this.current), + id = treeNode.id + "-dnd" // so id doesn't conflict w/widget + + if(!dojo.hasAttr(this.current, "id")){ + dojo.attr(this.current, "id", id); + } + + if(!this.singular && !dojo.isCopyKey(e) && !e.shiftKey && (this.current.id in this.selection)){ + this.simpleSelection = true; + dojo.stopEvent(e); + return; + } + if(this.singular){ + if(this.anchor == this.current){ + if(dojo.isCopyKey(e)){ + this.selectNone(); + } + }else{ + this.selectNone(); + this.anchor = this.current; + this._addItemClass(this.anchor, "Anchor"); + + this.selection[this.current.id] = this.current; + } + }else{ + if(!this.singular && e.shiftKey){ + if(dojo.isCopyKey(e)){ + //TODO add range to selection + }else{ + //TODO select new range from anchor + } + }else{ + if(dojo.isCopyKey(e)){ + if(this.anchor == this.current){ + delete this.selection[this.anchor.id]; + this._removeAnchor(); + }else{ + if(this.current.id in this.selection){ + this._removeItemClass(this.current, "Selected"); + delete this.selection[this.current.id]; + }else{ + if(this.anchor){ + this._removeItemClass(this.anchor, "Anchor"); + this._addItemClass(this.anchor, "Selected"); + } + this.anchor = this.current; + this._addItemClass(this.current, "Anchor"); + this.selection[this.current.id] = this.current; + } + } + }else{ + if(!(id in this.selection)){ + this.selectNone(); + this.anchor = this.current; + this._addItemClass(this.current, "Anchor"); + this.selection[id] = this.current; + } + } + } + } + + dojo.stopEvent(e); + }, + + onMouseUp: function(e){ + // summary: + // Event processor for onmouseup + // e: Event + // mouse event + // tags: + // protected + + // TODO: this code is apparently for handling an edge case when the user is selecting + // multiple nodes and then mousedowns on a node by accident... it lets the user keep the + // current selection by moving the mouse away (or something like that). It doesn't seem + // to work though and requires a lot of plumbing (including this code, the onmousemove + // handler, and the this.simpleSelection attribute. Consider getting rid of all of it. + + if(!this.simpleSelection){ return; } + this.simpleSelection = false; + this.selectNone(); + if(this.current){ + this.anchor = this.current; + this._addItemClass(this.anchor, "Anchor"); + this.selection[this.current.id] = this.current; + } + }, + onMouseMove: function(e){ + // summary + // event processor for onmousemove + // e: Event + // mouse event + this.simpleSelection = false; + }, + + _removeSelection: function(){ + // summary: + // Unselects all items + // tags: + // private + var e = dojo.dnd._empty; + for(var i in this.selection){ + if(i in e){ continue; } + var node = dojo.byId(i); + if(node){ this._removeItemClass(node, "Selected"); } + } + this.selection = {}; + return this; // self + }, + + _removeAnchor: function(){ + // summary: + // Removes the Anchor CSS class from a node. + // According to `dojo.dnd.Selector`, anchor means that + // "an item is selected, and is an anchor for a 'shift' selection". + // It's not relevant for Tree at this point, since we don't support multiple selection. + // tags: + // private + if(this.anchor){ + this._removeItemClass(this.anchor, "Anchor"); + this.anchor = null; + } + return this; // self + }, + + 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["dojo.dnd.Avatar"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.dnd.Avatar"] = true; +dojo.provide("dojo.dnd.Avatar"); + + + +dojo.declare("dojo.dnd.Avatar", null, { + // summary: + // Object that represents transferred DnD items visually + // manager: Object + // a DnD manager object + + constructor: function(manager){ + this.manager = manager; + this.construct(); + }, + + // methods + construct: function(){ + // summary: + // constructor function; + // it is separate so it can be (dynamically) overwritten in case of need + this.isA11y = dojo.hasClass(dojo.body(),"dijit_a11y"); + var a = dojo.create("table", { + "class": "dojoDndAvatar", + style: { + position: "absolute", + zIndex: "1999", + margin: "0px" + } + }), + source = this.manager.source, node, + b = dojo.create("tbody", null, a), + tr = dojo.create("tr", null, b), + td = dojo.create("td", null, tr), + icon = this.isA11y ? dojo.create("span", { + id : "a11yIcon", + innerHTML : this.manager.copy ? '+' : "<" + }, td) : null, + span = dojo.create("span", { + innerHTML: source.generateText ? this._generateText() : "" + }, td), + k = Math.min(5, this.manager.nodes.length), i = 0; + // we have to set the opacity on IE only after the node is live + dojo.attr(tr, { + "class": "dojoDndAvatarHeader", + style: {opacity: 0.9} + }); + for(; i < k; ++i){ + if(source.creator){ + // create an avatar representation of the node + node = source._normalizedCreator(source.getItem(this.manager.nodes[i].id).data, "avatar").node; + }else{ + // or just clone the node and hope it works + node = this.manager.nodes[i].cloneNode(true); + if(node.tagName.toLowerCase() == "tr"){ + // insert extra table nodes + var table = dojo.create("table"), + tbody = dojo.create("tbody", null, table); + tbody.appendChild(node); + node = table; + } + } + node.id = ""; + tr = dojo.create("tr", null, b); + td = dojo.create("td", null, tr); + td.appendChild(node); + dojo.attr(tr, { + "class": "dojoDndAvatarItem", + style: {opacity: (9 - i) / 10} + }); + } + this.node = a; + }, + destroy: function(){ + // summary: + // destructor for the avatar; called to remove all references so it can be garbage-collected + dojo.destroy(this.node); + this.node = false; + }, + update: function(){ + // summary: + // updates the avatar to reflect the current DnD state + dojo[(this.manager.canDropFlag ? "add" : "remove") + "Class"](this.node, "dojoDndAvatarCanDrop"); + if (this.isA11y){ + var icon = dojo.byId("a11yIcon"); + var text = '+'; // assume canDrop && copy + if (this.manager.canDropFlag && !this.manager.copy) { + text = '< '; // canDrop && move + }else if (!this.manager.canDropFlag && !this.manager.copy) { + text = "o"; //!canDrop && move + }else if(!this.manager.canDropFlag){ + text = 'x'; // !canDrop && copy + } + icon.innerHTML=text; + } + // replace text + dojo.query(("tr.dojoDndAvatarHeader td span" +(this.isA11y ? " span" : "")), this.node).forEach( + function(node){ + node.innerHTML = this._generateText(); + }, this); + }, + _generateText: function(){ + // summary: generates a proper text to reflect copying or moving of items + return this.manager.nodes.length.toString(); + } +}); + +} + +if(!dojo._hasResource["dojo.dnd.Manager"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojo.dnd.Manager"] = true; +dojo.provide("dojo.dnd.Manager"); + + + + + +dojo.declare("dojo.dnd.Manager", null, { + // summary: + // the manager of DnD operations (usually a singleton) + constructor: function(){ + this.avatar = null; + this.source = null; + this.nodes = []; + this.copy = true; + this.target = null; + this.canDropFlag = false; + this.events = []; + }, + + // avatar's offset from the mouse + OFFSET_X: 16, + OFFSET_Y: 16, + + // methods + overSource: function(source){ + // summary: + // called when a source detected a mouse-over condition + // source: Object + // the reporter + if(this.avatar){ + this.target = (source && source.targetState != "Disabled") ? source : null; + this.canDropFlag = Boolean(this.target); + this.avatar.update(); + } + dojo.publish("/dnd/source/over", [source]); + }, + outSource: function(source){ + // summary: + // called when a source detected a mouse-out condition + // source: Object + // the reporter + if(this.avatar){ + if(this.target == source){ + this.target = null; + this.canDropFlag = false; + this.avatar.update(); + dojo.publish("/dnd/source/over", [null]); + } + }else{ + dojo.publish("/dnd/source/over", [null]); + } + }, + startDrag: function(source, nodes, copy){ + // summary: + // called to initiate the DnD operation + // source: Object + // the source which provides items + // nodes: Array + // the list of transferred items + // copy: Boolean + // copy items, if true, move items otherwise + this.source = source; + this.nodes = nodes; + this.copy = Boolean(copy); // normalizing to true boolean + this.avatar = this.makeAvatar(); + dojo.body().appendChild(this.avatar.node); + dojo.publish("/dnd/start", [source, nodes, this.copy]); + this.events = [ + dojo.connect(dojo.doc, "onmousemove", this, "onMouseMove"), + dojo.connect(dojo.doc, "onmouseup", this, "onMouseUp"), + dojo.connect(dojo.doc, "onkeydown", this, "onKeyDown"), + dojo.connect(dojo.doc, "onkeyup", this, "onKeyUp"), + // cancel text selection and text dragging + dojo.connect(dojo.doc, "ondragstart", dojo.stopEvent), + dojo.connect(dojo.body(), "onselectstart", dojo.stopEvent) + ]; + var c = "dojoDnd" + (copy ? "Copy" : "Move"); + dojo.addClass(dojo.body(), c); + }, + canDrop: function(flag){ + // summary: + // called to notify if the current target can accept items + var canDropFlag = Boolean(this.target && flag); + if(this.canDropFlag != canDropFlag){ + this.canDropFlag = canDropFlag; + this.avatar.update(); + } + }, + stopDrag: function(){ + // summary: + // stop the DnD in progress + dojo.removeClass(dojo.body(), "dojoDndCopy"); + dojo.removeClass(dojo.body(), "dojoDndMove"); + dojo.forEach(this.events, dojo.disconnect); + this.events = []; + this.avatar.destroy(); + this.avatar = null; + this.source = this.target = null; + this.nodes = []; + }, + makeAvatar: function(){ + // summary: + // makes the avatar; it is separate to be overwritten dynamically, if needed + return new dojo.dnd.Avatar(this); + }, + updateAvatar: function(){ + // summary: + // updates the avatar; it is separate to be overwritten dynamically, if needed + this.avatar.update(); + }, + + // mouse event processors + onMouseMove: function(e){ + // summary: + // event processor for onmousemove + // e: Event + // mouse event + var a = this.avatar; + if(a){ + dojo.dnd.autoScrollNodes(e); + //dojo.dnd.autoScroll(e); + var s = a.node.style; + s.left = (e.pageX + this.OFFSET_X) + "px"; + s.top = (e.pageY + this.OFFSET_Y) + "px"; + var copy = Boolean(this.source.copyState(dojo.isCopyKey(e))); + if(this.copy != copy){ + this._setCopyStatus(copy); + } + } + }, + onMouseUp: function(e){ + // summary: + // event processor for onmouseup + // e: Event + // mouse event + if(this.avatar){ + if(this.target && this.canDropFlag){ + var copy = Boolean(this.source.copyState(dojo.isCopyKey(e))), + params = [this.source, this.nodes, copy, this.target, e]; + dojo.publish("/dnd/drop/before", params); + dojo.publish("/dnd/drop", params); + }else{ + dojo.publish("/dnd/cancel"); + } + this.stopDrag(); + } + }, + + // keyboard event processors + onKeyDown: function(e){ + // summary: + // event processor for onkeydown: + // watching for CTRL for copy/move status, watching for ESCAPE to cancel the drag + // e: Event + // keyboard event + if(this.avatar){ + switch(e.keyCode){ + case dojo.keys.CTRL: + var copy = Boolean(this.source.copyState(true)); + if(this.copy != copy){ + this._setCopyStatus(copy); + } + break; + case dojo.keys.ESCAPE: + dojo.publish("/dnd/cancel"); + this.stopDrag(); + break; + } + } + }, + onKeyUp: function(e){ + // summary: + // event processor for onkeyup, watching for CTRL for copy/move status + // e: Event + // keyboard event + if(this.avatar && e.keyCode == dojo.keys.CTRL){ + var copy = Boolean(this.source.copyState(false)); + if(this.copy != copy){ + this._setCopyStatus(copy); + } + } + }, + + // utilities + _setCopyStatus: function(copy){ + // summary: + // changes the copy status + // copy: Boolean + // the copy status + this.copy = copy; + this.source._markDndStatus(this.copy); + this.updateAvatar(); + dojo.removeClass(dojo.body(), "dojoDnd" + (this.copy ? "Move" : "Copy")); + dojo.addClass(dojo.body(), "dojoDnd" + (this.copy ? "Copy" : "Move")); + } +}); + +// dojo.dnd._manager: +// The manager singleton variable. Can be overwritten if needed. +dojo.dnd._manager = null; + +dojo.dnd.manager = function(){ + // summary: + // Returns the current DnD manager. Creates one if it is not created yet. + if(!dojo.dnd._manager){ + dojo.dnd._manager = new dojo.dnd.Manager(); + } + return dojo.dnd._manager; // Object +}; + +} + +if(!dojo._hasResource["dijit.tree.dndSource"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.tree.dndSource"] = true; +dojo.provide("dijit.tree.dndSource"); + + + + +/*===== +dijit.tree.__SourceArgs = function(){ + // summary: + // A dict of parameters for Tree source configuration. + // isSource: Boolean? + // Can be used as a DnD source. Defaults to true. + // accept: String[] + // List of accepted types (text strings) for a target; defaults to + // ["text", "treeNode"] + // copyOnly: Boolean? + // Copy items, if true, use a state of Ctrl key otherwise, + // dragThreshold: Number + // The move delay in pixels before detecting a drag; 0 by default + // betweenThreshold: Integer + // Distance from upper/lower edge of node to allow drop to reorder nodes + this.isSource = isSource; + this.accept = accept; + this.autoSync = autoSync; + this.copyOnly = copyOnly; + this.dragThreshold = dragThreshold; + this.betweenThreshold = betweenThreshold; +} +=====*/ + +dojo.declare("dijit.tree.dndSource", dijit.tree._dndSelector, { + // summary: + // Handles drag and drop operations (as a source or a target) for `dijit.Tree` + + // isSource: [private] Boolean + // Can be used as a DnD source. + isSource: true, + + // accept: String[] + // List of accepted types (text strings) for the Tree; defaults to + // ["text"] + accept: ["text", "treeNode"], + + // copyOnly: [private] Boolean + // Copy items, if true, use a state of Ctrl key otherwise + copyOnly: false, + + // dragThreshold: Number + // The move delay in pixels before detecting a drag; 5 by default + dragThreshold: 5, + + // betweenThreshold: Integer + // Distance from upper/lower edge of node to allow drop to reorder nodes + betweenThreshold: 0, + + constructor: function(/*dijit.Tree*/ tree, /*dijit.tree.__SourceArgs*/ params){ + // summary: + // a constructor of the Tree DnD Source + // tags: + // private + if(!params){ params = {}; } + dojo.mixin(this, params); + this.isSource = typeof params.isSource == "undefined" ? true : params.isSource; + var type = params.accept instanceof Array ? params.accept : ["text", "treeNode"]; + this.accept = null; + if(type.length){ + this.accept = {}; + for(var i = 0; i < type.length; ++i){ + this.accept[type[i]] = 1; + } + } + + // class-specific variables + this.isDragging = false; + this.mouseDown = false; + this.targetAnchor = null; // DOMNode corresponding to the currently moused over TreeNode + this.targetBox = null; // coordinates of this.targetAnchor + this.dropPosition = ""; // whether mouse is over/after/before this.targetAnchor + this._lastX = 0; + this._lastY = 0; + + // states + this.sourceState = ""; + if(this.isSource){ + dojo.addClass(this.node, "dojoDndSource"); + } + this.targetState = ""; + if(this.accept){ + dojo.addClass(this.node, "dojoDndTarget"); + } + + // set up events + this.topics = [ + dojo.subscribe("/dnd/source/over", this, "onDndSourceOver"), + dojo.subscribe("/dnd/start", this, "onDndStart"), + dojo.subscribe("/dnd/drop", this, "onDndDrop"), + dojo.subscribe("/dnd/cancel", this, "onDndCancel") + ]; + }, + + // methods + checkAcceptance: function(source, nodes){ + // summary: + // Checks if the target 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 + }, + + copyState: function(keyPressed){ + // summary: + // Returns true, if we need to copy items, false to move. + // It is separated to be overwritten dynamically, if needed. + // keyPressed: Boolean + // The "copy" control key was pressed + // tags: + // protected + return this.copyOnly || keyPressed; // Boolean + }, + destroy: function(){ + // summary: + // Prepares the object to be garbage-collected. + this.inherited("destroy",arguments); + dojo.forEach(this.topics, dojo.unsubscribe); + this.targetAnchor = null; + }, + + _onDragMouse: function(e){ + // summary: + // Helper method for processing onmousemove/onmouseover events while drag is in progress. + // Keeps track of current drop target. + + var m = dojo.dnd.manager(), + oldTarget = this.targetAnchor, // the DOMNode corresponding to TreeNode mouse was previously over + newTarget = this.current, // DOMNode corresponding to TreeNode mouse is currently over + newTargetWidget = this.currentWidget, // the TreeNode itself + oldDropPosition = this.dropPosition; // the previous drop position (over/before/after) + + // calculate if user is indicating to drop the dragged node before, after, or over + // (i.e., to become a child of) the target node + var newDropPosition = "Over"; + if(newTarget && this.betweenThreshold > 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, 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, oldDropPosition); + } + if(newTarget){ + this._addItemClass(newTarget, newDropPosition); + } + + // Check if it's ok to drop the dragged node on/before/after the target node. + if(!newTarget){ + m.canDrop(false); + }else if(newTargetWidget == 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, m.source, newDropPosition.toLowerCase()) + && !this._isParentChildDrop(m.source, newTarget)){ + 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 n = this.getSelectedNodes(); + var nodes=[]; + for(var i in n){ + nodes.push(n[i]); + } + if(nodes.length){ + 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("onMouseDown",arguments); + }, + + onMouseUp: function(e){ + // summary: + // Event processor for onmouseup + // e: Event + // onmouseup event + // tags: + // private + if(this.mouseDown){ + this.mouseDown = false; + this.inherited("onMouseUp",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 = dijit.getEnclosingWidget(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, 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 = {}; + for(var x in source.selection){ + ids[source.selection[x].parentNode.id] = true; + } + + 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 && (!node.id || !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, 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 == 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:'May 14, 1802'}); + // | 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(!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:'July 18, 1918'}] } + // into this: + // { name:['Kermit'], born:(new Date('July 18, 1918')) } + // + // 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){ + item = this._itemsByIdentity[identity]; + }else{ + item = this._arrayOfAllItems[identity]; + } + if(item === undefined){ + item = null; + } + return item; // Object + }, + + getIdentityAttributes: function(/* item */ item){ + // summary: + // See dojo.data.api.Identity.getIdentifierAttributes() + + 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 == 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"]); -- cgit v1.2.3