/* Copyright (c) 2004-2011, 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"); dojo.getObject("date.stamp", true, dojo); // 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; 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 dummyClass = {}, 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). // TODO: remove this in 2.0, when we stop caching parameters. d.connect(d, "extend", function(){ instanceClasses = {}; }); function getProtoInfo(cls, params){ // cls: A prototype // The prototype of the class to check props on // params: Object // The parameters object to mix found parameters onto. for(var name in cls){ if(name.charAt(0)=="_"){ continue; } // skip internal properties if(name in dummyClass){ continue; } // skip "constructor" and "toString" params[name] = val2type(cls[name]); } return params; } function getClassInfo(/*String*/ className, /*Boolean*/ skipParamsLookup){ // summary: // Maps a widget name string like "dijit.form.Button" to the widget constructor itself, // and a list of that widget's parameters and their types // className: // fully qualified name (like "dijit.form.Button") // returns: // structure like // { // cls: dijit.Button, // params: { label: "string", disabled: "boolean"} // } var c = instanceClasses[className]; if(!c){ // get pointer to widget class var cls = d.getObject(className), params = null; if(!cls){ return null; } // class not defined [yet] if(!skipParamsLookup){ // from fastpath, we don't need to lookup the attrs on the proto because they are explicit params = getProtoInfo(cls.prototype, {}) } c = { cls: cls, params: params }; }else if(!skipParamsLookup && !c.params){ // if we're calling getClassInfo and have a cls proto, but no params info, scan that cls for params now // and update the pointer in instanceClasses[className]. This happens when a widget appears in another // widget's template which still uses dojoType, but an instance of the widget appears prior with a data-dojo-type, // skipping this lookup the first time. c.params = getProtoInfo(c.cls.prototype, {}); } return c; } this._functionFromScript = function(script, attrData){ // summary: // Convert a // into a function // script: DOMNode // The // }; =====*/ // 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"); /*===== 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"); dojo.getObject("colors", true, dojo); //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.getObject("i18n", true, dojo); /*===== dojo.i18n = { // summary: Utility classes to enable loading of resources for internationalization (i18n) }; =====*/ // when using a real AMD loader, dojo.i18n.getLocalization is already defined by dojo/lib/backCompat dojo.i18n.getLocalization = 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){ hash = hash.root || 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; dojo.addClass(this._cells[i].node, "dijitPaletteCellSelected"); break; } } } // record new value, or null if no matching cell this._set("value", this._selectedCell >= 0 ? value : null); if(priorityChange || priorityChange === undefined){ this.onChange(value); } }, 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, row, col){ // 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: [const] 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"]] }, // templateString: String // The template of this widget. templateString: dojo.cache("dijit", "templates/ColorPalette.html", "
\n\t\n\t\t\n\t
\n
\n"), baseClass: "dijitColorPalette", buildRendering: function(){ // Instantiate the template, which makes a skeleton into which we'll insert a bunch of // nodes this.inherited(arguments); // Creates nodes in each cell of the template. // Pass in "customized" dijit._Color constructor for specified palette and high-contrast vs. normal mode this._preparePalette( this._palettes[this.palette], dojo.i18n.getLocalization("dojo", "colors", this.lang), dojo.declare(dijit._Color, { hc: dojo.hasClass(dojo.body(), "dijit_a11y"), palette: this.palette }) ); } }); dojo.declare("dijit._Color", dojo.Color, { // summary: // Object associated with each cell in a ColorPalette palette. // Implements dijit.Dye. // Template for each cell in normal (non-high-contrast mode). Each cell contains a wrapper // node for showing the border (called dijitPaletteImg for back-compat), and dijitColorPaletteSwatch // for showing the color. template: "" + "${alt}" + "", // Template for each cell in high contrast mode. Each cell contains an image with the whole palette, // but scrolled and clipped to show the correct color only hcTemplate: "" + "${alt}" + "", // _imagePaths: [protected] Map // This is stores the path to the palette images used for high-contrast mode display _imagePaths: { "7x10": dojo.moduleUrl("dijit.themes", "a11y/colors7x10.png"), "3x4": dojo.moduleUrl("dijit.themes", "a11y/colors3x4.png") }, constructor: function(/*String*/alias, /*Number*/ row, /*Number*/ col){ this._alias = alias; this._row = row; this._col = col; 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){ var html = dojo.string.substitute(this.hc ? this.hcTemplate : this.template, { // substitution variables for normal mode color: this.toHex(), blankGif: blankGif, alt: this._alias, // variables used for high contrast mode image: this._imagePaths[this.palette].toString(), left: this._col * -20 - 5, top: this._row * -20 - 5, size: this.palette == "7x10" ? "height: 145px; width: 206px" : "height: 64px; width: 86px" }); dojo.place(html, 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.getObject("dnd", true, dojo); 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.getObject("dnd", true, dojo); dojo.dnd.getViewport = dojo.window.getBox; 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.window.getBox(), 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, or touch-drag on touch devices. // 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); var pos = e.touches ? e.touches[0] : e; this.marginBox = {l: pos.pageX, t: pos.pageY}; this.mouseButton = e.button; var h = (this.host = host), d = node.ownerDocument; this.events = [ // At the start of a drag, onFirstMove is called, and then the following two // connects are disconnected dojo.connect(d, "onmousemove", this, "onFirstMove"), dojo.connect(d, "ontouchmove", this, "onFirstMove"), // These are called continually during the drag dojo.connect(d, "onmousemove", this, "onMouseMove"), dojo.connect(d, "ontouchmove", this, "onMouseMove"), // And these are called at the end of the drag dojo.connect(d, "onmouseup", this, "onMouseUp"), dojo.connect(d, "ontouchend", this, "onMouseUp"), // cancel text selection and text dragging dojo.connect(d, "ondragstart", dojo.stopEvent), dojo.connect(d.body, "onselectstart", dojo.stopEvent) ]; // notify that the move has started if(h && h.onMoveStart){ h.onMoveStart(this); } }, // mouse event processors onMouseMove: function(e){ // summary: // event processor for onmousemove/ontouchmove // e: Event // mouse/touch event dojo.dnd.autoScroll(e); var m = this.marginBox, pos = e.touches ? e.touches[0] : e; this.host.onMove(this, {l: m.l + pos.pageX, t: m.t + pos.pageY}, e); dojo.stopEvent(e); }, onMouseUp: function(e){ if(dojo.isWebKit && dojo.isMac && this.mouseButton == 2 ? e.button == 0 : this.mouseButton == e.button){ // TODO Should condition be met for touch devices, too? 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); } // Disconnect onmousemove and ontouchmove events that call this function dojo.disconnect(this.events.shift()); dojo.disconnect(this.events.shift()); }, 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"), dojo.connect(this.handle, "ontouchstart", 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/ontouchstart, creates a Mover for the node // e: Event // mouse/touch 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, "ontouchmove", this, "onMouseMove"), dojo.connect(this.handle, "onmouseup", this, "onMouseUp"), dojo.connect(this.handle, "ontouchend", this, "onMouseUp") ); var pos = e.touches ? e.touches[0] : e; this._lastX = pos.pageX; this._lastY = pos.pageY; }else{ this.onDragDetected(e); } dojo.stopEvent(e); }, onMouseMove: function(e){ // summary: // event processor for onmousemove/ontouchmove, used only for delayed drags // e: Event // mouse/touch event var pos = e.touches ? e.touches[0] : e; if(Math.abs(pos.pageX - this._lastX) > this.delay || Math.abs(pos.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._getMarginSize(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; this.onMoving(mover, leftTop); leftTop.l = leftTop.l < c.l ? c.l : c.r < leftTop.l ? c.r : leftTop.l; leftTop.t = leftTop.t < c.t ? c.t : c.b < leftTop.t ? c.b : leftTop.t; s.left = leftTop.l + "px"; s.top = leftTop.t + "px"; this.onMoved(mover, leftTop); } }); /*===== 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 }; } }); // 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"] } =====*/ // state: [readonly] String // Will be "Error" if one or more of the child widgets has an invalid value, // "Incomplete" if not all of the required child widgets are filled in. Otherwise, "", // which indicates that the form is ready to be submitted. state: "", // 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 get('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 set('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; } }); */ // Note: no need to call this._set("value", ...) as the child updates will trigger onChange events // which I am monitoring. }, 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. See description of `value` for details. // description: // The value is updated into this.value every time a child has an onChange event, // so in the common case this function could just return this.value. However, // that wouldn't work when: // // 1. User presses return key to submit a form. That doesn't fire an onchange event, // and even if it did it would come too late due to the setTimout(..., 0) in _handleOnChange() // // 2. app for some reason calls this.get("value") while the user is typing into a // form field. Not sure if that case needs to be supported or not. // 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= 0 ? "Error" : dojo.indexOf(states, "Incomplete") >= 0 ? "Incomplete" : ""; }, disconnectChildren: function(){ // summary: // Remove connections to monitor changes to children's value, error state, and disabled state, // in order to update Form.value and Form.state. dojo.forEach(this._childConnections || [], dojo.hitch(this, "disconnect")); dojo.forEach(this._childWatches || [], function(w){ w.unwatch(); }); }, connectChildren: function(/*Boolean*/ inStartup){ // summary: // Setup connections to monitor changes to children's value, error state, and disabled state, // in order to update Form.value and Form.state. // // You can call this function directly, ex. in the event that you // programmatically add a widget to the form *after* the form has been // initialized. var _this = this; // Remove old connections, if any this.disconnectChildren(); this._descendants = this.getDescendants(); // (Re)set this.value and this.state. Send watch() notifications but not on startup. var set = inStartup ? function(name, val){ _this[name] = val; } : dojo.hitch(this, "_set"); set("value", this.get("value")); set("state", this._getState()); // Monitor changes to error state and disabled state in order to update // Form.state var conns = (this._childConnections = []), watches = (this._childWatches = []); dojo.forEach(dojo.filter(this._descendants, function(item){ return item.validate; } ), function(widget){ // We are interested in whenever the widget changes validity state - or // whenever the disabled attribute on that widget is changed. dojo.forEach(["state", "disabled"], function(attr){ watches.push(widget.watch(attr, function(attr, oldVal, newVal){ _this.set("state", _this._getState()); })); }); }); // And monitor calls to child.onChange so we can update this.value var onChange = function(){ // summary: // Called when child's value or disabled state changes // Use setTimeout() to collapse value changes in multiple children into a single // update to my value. Multiple updates will occur on: // 1. Form.set() // 2. Form.reset() // 3. user selecting a radio button (which will de-select another radio button, // causing two onChange events) if(_this._onChangeDelayTimer){ clearTimeout(_this._onChangeDelayTimer); } _this._onChangeDelayTimer = setTimeout(function(){ delete _this._onChangeDelayTimer; _this._set("value", _this.get("value")); }, 10); }; dojo.forEach( dojo.filter(this._descendants, function(item){ return item.onChange; } ), function(widget){ // When a child widget's value changes, // the efficient thing to do is to just update that one attribute in this.value, // but that gets a little complicated when a checkbox is checked/unchecked // since this.value["checkboxName"] contains an array of all the checkboxes w/the same name. // Doing simple thing for now. conns.push(_this.connect(widget, "onChange", onChange)); // Disabling/enabling a child widget should remove it's value from this.value. // Again, this code could be more efficient, doing simple thing for now. watches.push(widget.watch("disabled", onChange)); } ); }, startup: function(){ this.inherited(arguments); // Initialize value and valid/invalid state tracking. Needs to be done in startup() // so that children are initialized. this.connectChildren(true); // Make state change call onValidStateChange(), will be removed in 2.0 this.watch("state", function(attr, oldVal, newVal){ this.onValidStateChange(newVal == ""); }); }, destroy: function(){ this.disconnectChildren(); this.inherited(arguments); } }); } if(!dojo._hasResource["dijit._DialogMixin"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dijit._DialogMixin"] = true; dojo.provide("dijit._DialogMixin"); dojo.declare("dijit._DialogMixin", null, { // summary: // This provides functions useful to Dialog and TooltipDialog attributeMap: dijit._Widget.prototype.attributeMap, execute: function(/*Object*/ formContents){ // summary: // Callback when the user hits the submit button. // Override this method to handle Dialog execution. // description: // After the user has pressed the submit button, the Dialog // first calls onExecute() to notify the container to hide the // dialog and restore focus to wherever it used to be. // // *Then* this method is called. // type: // callback }, onCancel: function(){ // summary: // Called when user has pressed the Dialog's cancel button, to notify container. // description: // Developer shouldn't override or connect to this method; // it's a private communication device between the TooltipDialog // and the thing that opened it (ex: `dijit.form.DropDownButton`) // type: // protected }, onExecute: function(){ // summary: // Called when user has pressed the dialog's OK button, to notify container. // description: // Developer shouldn't override or connect to this method; // it's a private communication device between the TooltipDialog // and the thing that opened it (ex: `dijit.form.DropDownButton`) // type: // protected }, _onSubmit: function(){ // summary: // Callback when user hits submit button // type: // protected this.onExecute(); // notify container that we are about to execute this.execute(this.get('value')); }, _getFocusItems: function(){ // summary: // Finds focusable items in dialog, // and sets this._firstFocusItem and this._lastFocusItem // tags: // protected var elems = dijit._getTabNavigable(this.containerNode); this._firstFocusItem = elems.lowest || elems.first || this.closeButtonNode || this.domNode; this._lastFocusItem = elems.last || elems.highest || this._firstFocusItem; } } ); } if(!dojo._hasResource["dijit.DialogUnderlay"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dijit.DialogUnderlay"] = true; dojo.provide("dijit.DialogUnderlay"); dojo.declare( "dijit.DialogUnderlay", [dijit._Widget, dijit._Templated], { // summary: // The component that blocks the screen behind a `dijit.Dialog` // // description: // A component used to block input behind a `dijit.Dialog`. Only a single // instance of this widget is created by `dijit.Dialog`, and saved as // a reference to be shared between all Dialogs as `dijit._underlay` // // The underlay itself can be styled based on and id: // | #myDialog_underlay { background-color:red; } // // In the case of `dijit.Dialog`, this id is based on the id of the Dialog, // suffixed with _underlay. // Template has two divs; outer div is used for fade-in/fade-out, and also to hold background iframe. // Inner div has opacity specified in CSS file. templateString: "
", // 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"); this._set("dialogId", id); }, _setClassAttr: function(clazz){ this.node.className = "dijitDialogUnderlay " + clazz; this._set("class", 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(); delete this.bgIframe; this.domNode.style.display = "none"; } } ); } if(!dojo._hasResource["dijit.layout._ContentPaneResizeMixin"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dijit.layout._ContentPaneResizeMixin"] = true; dojo.provide("dijit.layout._ContentPaneResizeMixin"); dojo.declare("dijit.layout._ContentPaneResizeMixin", null, { // summary: // Resize() functionality of ContentPane. If there's a single layout widget // child then it will call resize() with the same dimensions as the ContentPane. // Otherwise just calls resize on each child. // // Also implements basic startup() functionality, where starting the parent // will start the children // 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, // 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, _startChildren: function(){ // summary: // Call startup() on all children including non _Widget ones like dojo.dnd.Source objects // This starts all the widgets dojo.forEach(this.getChildren(), function(child){ child.startup(); child._started = true; }); }, 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; this.inherited(arguments); this._startChildren(); if(this._isShown()){ this._onShow(); } if(!this._childOfLayoutWidget){ // If my parent isn't a layout container, since my style *may be* width=height=100% // or something similar (either set directly or via a CSS class), // monitor when my size changes so that I can re-layout. // For browsers where I can't directly monitor when my size changes, // monitor when the viewport changes size, which *may* indicate a size change for me. this.connect(dojo.isIE ? this.domNode : dojo.global, 'onresize', function(){ // Using function(){} closure to ensure no arguments to resize. this._needLayout = !this._childOfLayoutWidget; this.resize(); }); } }, _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 propagate 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, "data-dojo-type") || 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); }, 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 (see [20099]). // However, don't load href for closed TitlePanes. if(!this._wasShown && this.open !== false){ this._onShow(); } this._resizeCalled = true; this._scheduleLayout(changeSize, resultSize); }, _scheduleLayout: function(changeSize, resultSize){ // summary: // Resize myself, and 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._layout(changeSize, resultSize); }else{ this._needLayout = true; this._changeSize = changeSize; this._resultSize = resultSize; } }, _layout: function(changeSize, resultSize){ // summary: // Resize myself according to optional changeSize/resultSize parameters, like a layout widget. // Also, since I am a Container widget, each of my children expects me to // call resize() or layout() on them. // // Should be called on initialization and also whenever we get new content // (from an href, or from set('content', ...))... but deferred until // the ContentPane is visible // 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); } this._layoutChildren(); delete this._needLayout; }, _layoutChildren: function(){ // Call _checkIfSingleChild() again in case app has manually mucked w/the content // of the ContentPane (rather than changing it through the set("content", ...) API. 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(); } }); } }, _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{ var node = this.domNode, parent = this.domNode.parentNode; return (node.style.display != 'none') && (node.style.visibility != 'hidden') && !dojo.hasClass(node, "dijitHidden") && parent && parent.style && (parent.style.display != 'none'); } }, _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 layout/resize of child widget(s) if(this._needLayout){ // If a layout has been scheduled for when we become visible, do it now this._layout(this._changeSize, this._resultSize); } 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; } }); } 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"); dojo.getObject("html", true, dojo); // 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, // parserScope: String // Flag passed to parser. Root for attribute names to search for. If scopeName is dojo, // will search for data-dojo-type (or dojoType). For backwards compatibility // reasons defaults to dojo._scopeName (which is "dojo" except when // multi-version support is used, when it will be something like dojo16, dojo20, etc.) parserScope: dojo._scopeName, // startup: Boolean // Start the child widgets after parsing them. Only obeyed if parseContent is true. startup: true, // 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 var inherited = {}; dojo.forEach(["dir", "lang", "textDir"], function(name){ if(this[name]){ inherited[name] = this[name]; } }, this); this.parseResults = dojo.parser.parse({ rootNode: rootNode, noStart: !this.startup, inherited: inherited, scope: this.parserScope }); }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"); dojo.declare( "dijit.layout.ContentPane", [dijit._Widget, dijit.layout._ContentPaneResizeMixin], { // summary: // A widget containing an HTML fragment, specified inline // or by uri. Fragment may include widgets. // // description: // This widget embeds a document fragment in the page, specified // either by uri, javascript generated markup or DOM reference. // Any widgets within this content are instantiated and managed, // but laid out according to the HTML structure. Unlike IFRAME, // ContentPane embeds a document fragment as would be found // inside the BODY tag of a full HTML document. It should not // contain the HTML, HEAD, or BODY tags. // For more advanced functionality with scripts and // stylesheets, see dojox.layout.ContentPane. This widget may be // used stand alone or as a base class for other widgets. // ContentPane is useful as a child of other layout containers // such as BorderContainer or TabContainer, but note that those // widgets can contain any widget as a child. // // example: // Some quick samples: // To change the innerHTML: cp.set('content', '<b>new content</b>') // // Or you can send it a NodeList: cp.set('content', dojo.query('div [class=selected]', userSelection)) // // To do an ajax update: cp.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 set("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, // parserScope: String // Flag passed to parser. Root for attribute names to search for. If scopeName is dojo, // will search for data-dojo-type (or dojoType). For backwards compatibility // reasons defaults to dojo._scopeName (which is "dojo" except when // multi-version support is used, when it will be something like dojo16, dojo20, etc.) parserScope: dojo._scopeName, // 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 set('content', ...) / set('href', ...) // // False if it doesn't have any content, or if ContentPane is // still in the process of downloading href. isLoaded: false, baseClass: "dijitContentPane", // ioArgs: Object // Parameters to pass to xhrGet() request, for example: // | <div dojoType="dijit.layout.ContentPane" href="./bar" ioArgs="{timeout: 500}"> ioArgs: {}, // onLoadDeferred: [readonly] dojo.Deferred // This is the `dojo.Deferred` returned by set('href', ...) and refresh(). // Calling onLoadDeferred.addCallback() or addErrback() registers your // callback to be called only once, when the prior set('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 // or content 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: [] }), // Flag to parser that I'll parse my contents, so it shouldn't. stopParser: true, // template: [private] Boolean // Flag from the parser that this ContentPane is inside a template // so the contents are pre-parsed. // (TODO: this declaration can be commented out in 2.0) template: false, create: function(params, srcNodeRef){ // Convert a srcNodeRef argument into a content parameter, so that the original contents are // processed in the same way as contents set via set("content", ...), calling the parser etc. // Avoid modifying original params object since that breaks NodeList instantiation, see #11906. if((!params || !params.template) && srcNodeRef && !("href" in params) && !("content" in params)){ var df = dojo.doc.createDocumentFragment(); srcNodeRef = dojo.byId(srcNodeRef) while(srcNodeRef.firstChild){ df.appendChild(srcNodeRef.firstChild); } params = dojo.delegate(params, {content: df}); } this.inherited(arguments, [params, srcNodeRef]); }, 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); }, buildRendering: function(){ this.inherited(arguments); // Since we have no template we need to set this.containerNode ourselves, to make getChildren() work. // For subclasses of ContentPane that do have a template, does nothing. if(!this.containerNode){ this.containerNode = this.domNode; } // remove the title attribute so it doesn't show up when hovering // over a node (TODO: remove in 2.0, no longer needed after #11490) this.domNode.title = ""; if(!dojo.attr(this.domNode,"role")){ dijit.setWaiRole(this.domNode, "group"); } }, _startChildren: function(){ // summary: // Call startup() on all children including non _Widget ones like dojo.dnd.Source objects // This starts all the widgets this.inherited(arguments); // And this catches stuff like dojo.dnd.Source if(this._contentSetter){ dojo.forEach(this._contentSetter.parseResults, function(obj){ if(!obj._started && !obj._destroyed && dojo.isFunction(obj.startup)){ obj.startup(); obj._started = true; } }, this); } }, 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 set("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 (a set('href', ...) will cancel any in-flight set('href', ...)) this.cancel(); this.onLoadDeferred = new dojo.Deferred(dojo.hitch(this, "cancel")); this.onLoadDeferred.addCallback(dojo.hitch(this, "onLoad")); this._set("href", href); // _setHrefAttr() is called during creation and by the user, after creation. // Assuming preload == false, only in the second case do we actually load the URL; // otherwise it's done in startup(), and only if this widget is shown. if(this.preload || (this._created && 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 set("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._set("href", ""); // Cancel any in-flight requests (a set('content', ...) will cancel any in-flight set('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")); if(this._created){ // For back-compat reasons, call onLoad() for set('content', ...) // calls but not for content specified in srcNodeRef (ie: <div dojoType=ContentPane>...</div>) // or as initialization parameter (ie: new ContentPane({content: ...}) this.onLoadDeferred.addCallback(dojo.hitch(this, "onLoad")); } this._setContent(data || ""); this._isDownloaded = false; // mark that content is from a set('content') not a set('href') return this.onLoadDeferred; // dojo.Deferred }, _getContentAttr: function(){ // summary: // Hook to make get("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); }, _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) this.inherited(arguments); if(this.href){ if(!this._xhrDfd && // if there's an href that isn't already being loaded (!this.isLoaded || this._hrefChanged || this.refreshOnShow) ){ return this.refresh(); // If child has an href, promise that fires when the load is complete } } }, 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.onLoadDeferred.addCallback(dojo.hitch(this, "onLoad")); this._load(); return this.onLoadDeferred; // If child has an href, promise that fires when refresh is complete }, _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._set("isLoaded", true); try{ this.onLoadDeferred.callback(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._set("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(/*String|DocumentFragment*/ cont, /*Boolean*/ 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, parserScope: this.parserScope, startup: false, dir: this.dir, lang: this.lang }, this._contentSetterParams || {}); setter.set( (dojo.isObject(cont) && cont.domNode) ? cont.domNode : cont, setterParams ); // setter params must be pulled afresh from the ContentPane each time delete this._contentSetterParams; if(this.doLayout){ this._checkIfSingleChild(); } if(!isFakeContent){ if(this._started){ // Startup each top level child widget (and they will start their children, recursively) this._startChildren(); // 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 // override 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); } }, // 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 role=\"presentation\" tabIndex=\"-1\">\n\t<div class=\"dijitTooltipContainer\" role=\"presentation\">\n\t\t<div class =\"dijitTooltipContents dijitTooltipFocusNode\" dojoAttachPoint=\"containerNode\" role=\"dialog\"></div>\n\t</div>\n\t<div class=\"dijitTooltipConnector\" role=\"presentation\"></div>\n</div>\n"), _setTitleAttr: function(/*String*/ title){ this.containerNode.title = title; this._set("title", title) }, postCreate: function(){ this.inherited(arguments); this.connect(this.containerNode, "onkeypress", "_onKey"); }, 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 newC = "dijitTooltipAB" + (corner.charAt(1) == 'L' ? "Left" : "Right") + " dijitTooltip" + (corner.charAt(0) == 'T' ? "Below" : "Above"); dojo.replaceClass(this.domNode, newC, this._currentOrientClass || ""); this._currentOrientClass = newC; }, focus: function(){ // summary: // Focus on first field this._getFocusItems(this.containerNode); dijit.focus(this._firstFocusItem); }, 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 }, 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/TooltipDialog required for back-compat. TODO: remove in 2.0 /*===== 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\" role=\"dialog\" aria-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=\"ondijitclick: onCancel\" title=\"${buttonCancel}\" role=\"button\" tabIndex=\"-1\">\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: [readonly] 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 overridden. // 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 && dijit._DialogLevelManager.isTop(this)){ 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 }); this._dndListener = 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(" ") }; }, _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._getMarginSize(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 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 // returns: dojo.Deferred // Deferred object that resolves when the display animation is complete if(this.open){ return; } if(!this._started){ this.startup(); } // first time we show the dialog, there's some initialization stuff to do if(!this._alreadyInitialized){ this._setup(); this._alreadyInitialized=true; } if(this._fadeOutDeferred){ this._fadeOutDeferred.cancel(); } 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(this.domNode, "onkeypress", this, "_onKey")); dojo.style(this.domNode, { opacity:0, display:"" }); this._set("open", true); this._onShow(); // lazy load trigger this._size(); this._position(); // fade-in Animation object, setup below var fadeIn; this._fadeInDeferred = new dojo.Deferred(dojo.hitch(this, function(){ fadeIn.stop(); delete this._fadeInDeferred; })); fadeIn = dojo.fadeIn({ node: this.domNode, duration: this.duration, beforeBegin: dojo.hitch(this, function(){ dijit._DialogLevelManager.show(this, this.underlayAttrs); }), onEnd: dojo.hitch(this, function(){ if(this.autofocus && dijit._DialogLevelManager.isTop(this)){ // 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._fadeInDeferred.callback(true); delete this._fadeInDeferred; }) }).play(); return this._fadeInDeferred; }, hide: function(){ // summary: // Hide the dialog // returns: dojo.Deferred // Deferred object that resolves when the hide animation is complete // if we haven't been initialized yet then we aren't showing and we can just return if(!this._alreadyInitialized){ return; } if(this._fadeInDeferred){ this._fadeInDeferred.cancel(); } // fade-in Animation object, setup below var fadeOut; this._fadeOutDeferred = new dojo.Deferred(dojo.hitch(this, function(){ fadeOut.stop(); delete this._fadeOutDeferred; })); fadeOut = dojo.fadeOut({ node: this.domNode, duration: this.duration, onEnd: dojo.hitch(this, function(){ this.domNode.style.display = "none"; dijit._DialogLevelManager.hide(this); this.onHide(); this._fadeOutDeferred.callback(true); delete this._fadeOutDeferred; }) }).play(); if(this._scrollConnected){ this._scrollConnected = false; } dojo.forEach(this._modalconnects, dojo.disconnect); this._modalconnects = []; if(this._relativePosition){ delete this._relativePosition; } this._set("open", false); return this._fadeOutDeferred; }, 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(){ if(this._fadeInDeferred){ this._fadeInDeferred.cancel(); } if(this._fadeOutDeferred){ this._fadeOutDeferred.cancel(); } if(this._moveable){ this._moveable.destroy(); } if(this._dndListener){ dojo.unsubscribe(this._dndListener); } dojo.forEach(this._modalconnects, dojo.disconnect); dijit._DialogLevelManager.hide(this); this.inherited(arguments); } } ); dojo.declare( "dijit.Dialog", [dijit.layout.ContentPane, dijit._DialogBase], {} ); dijit._DialogLevelManager = { // summary: // Controls the various active "levels" on the page, starting with the // stuff initially visible on the page (at z-index 0), and then having an entry for // each Dialog shown. show: function(/*dijit._Widget*/ dialog, /*Object*/ underlayAttrs){ // summary: // Call right before fade-in animation for new dialog. // Saves current focus, displays/adjusts underlay for new dialog, // and sets the z-index of the dialog itself. // // New dialog will be displayed on top of all currently displayed dialogs. // // Caller is responsible for setting focus in new dialog after the fade-in // animation completes. var ds = dijit._dialogStack; // Save current focus ds[ds.length-1].focus = dijit.getFocus(dialog); // Display the underlay, or if already displayed then adjust for this new dialog var underlay = dijit._underlay; if(!underlay || underlay._destroyed){ underlay = dijit._underlay = new dijit.DialogUnderlay(underlayAttrs); }else{ underlay.set(dialog.underlayAttrs); } // Set z-index a bit above previous dialog var zIndex = ds[ds.length-1].dialog ? ds[ds.length-1].zIndex + 2 : 950; if(ds.length == 1){ // first dialog underlay.show(); } dojo.style(dijit._underlay.domNode, 'zIndex', zIndex - 1); // Dialog dojo.style(dialog.domNode, 'zIndex', zIndex); ds.push({dialog: dialog, underlayAttrs: underlayAttrs, zIndex: zIndex}); }, hide: function(/*dijit._Widget*/ dialog){ // summary: // Called when the specified dialog is hidden/destroyed, after the fade-out // animation ends, in order to reset page focus, fix the underlay, etc. // If the specified dialog isn't open then does nothing. // // Caller is responsible for either setting display:none on the dialog domNode, // or calling dijit.popup.hide(), or removing it from the page DOM. var ds = dijit._dialogStack; if(ds[ds.length-1].dialog == dialog){ // Removing the top (or only) dialog in the stack, return focus // to previous dialog ds.pop(); var pd = ds[ds.length-1]; // the new active dialog (or the base page itself) // Adjust underlay if(ds.length == 1){ // Returning to original page. // 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(!dijit._underlay._destroyed){ dijit._underlay.hide(); } }else{ // Popping back to previous dialog, adjust underlay dojo.style(dijit._underlay.domNode, 'zIndex', pd.zIndex - 1); dijit._underlay.set(pd.underlayAttrs); } // Adjust focus if(dialog.refocus){ // 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. var focus = pd.focus; if(!focus || (pd.dialog && !dojo.isDescendant(focus.node, pd.dialog.domNode))){ pd.dialog._getFocusItems(pd.dialog.domNode); focus = pd.dialog._firstFocusItem; } try{ dijit.focus(focus); }catch(e){ /* focus() will fail if user opened the dialog by clicking a non-focusable element */ } } }else{ // Removing a dialog out of order (#9944, #10705). // Don't need to mess with underlay or z-index or anything. var idx = dojo.indexOf(dojo.map(ds, function(elem){return elem.dialog}), dialog); if(idx != -1){ ds.splice(idx, 1); } } }, isTop: function(/*dijit._Widget*/ dialog){ // summary: // Returns true if specified Dialog is the top in the task var ds = dijit._dialogStack; return ds[ds.length-1].dialog == dialog; } }; // Stack representing the various active "levels" on the page, starting with the // stuff initially visible on the page (at z-index 0), and then having an entry for // each Dialog shown. // Each element in stack has form { // dialog: dialogWidget, // focus: returnFromGetFocus(), // underlayAttrs: attributes to set on underlay (when this widget is active) // } dijit._dialogStack = [ {dialog: null, focus: null, underlayAttrs: null} // entry for stuff at z-index: 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. // Any dropdown taller than this will have scrollbars. // Set to 0 for no max height, or -1 to limit height to available space in viewport 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; } dojo.stopEvent(e); 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 && dropDown.autoFocus !== false){ // 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); } }, buildRendering: function(){ this.inherited(arguments); this._buttonNode = this._buttonNode || this.focusNode || this.domNode; this._popupStateNode = this._popupStateNode || this.focusNode || this._buttonNode; // 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(){ // summary: // set up nodes and connect our mouse and keypress events this.inherited(arguments); this.connect(this._buttonNode, "onmousedown", "_onDropDownMouseDown"); this.connect(this._buttonNode, "onclick", "_onDropDownClick"); this.connect(this.focusNode, "onkeypress", "_onKey"); this.connect(this.focusNode, "onkeyup", "_onKeyUp"); }, destroy: 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); }, _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, target = e.target; if(d && this._opened && d.handleKey){ if(d.handleKey(e) === false){ /* false return code means that the drop down handled the key */ dojo.stopEvent(e); return; } } if(d && this._opened && e.charOrCode == dojo.keys.ESCAPE){ this.closeDropDown(); dojo.stopEvent(e); }else if(!this._opened && (e.charOrCode == dojo.keys.DOWN_ARROW || ( (e.charOrCode == dojo.keys.ENTER || e.charOrCode == " ") && //ignore enter and space if the event is for a text input ((target.tagName || "").toLowerCase() !== 'input' || (target.type && target.type.toLowerCase() !== 'text'))))){ // Toggle the drop down, but wait until keyup so that the drop down doesn't // get a stray keyup event, or in the case of key-repeat (because user held // down key for too long), stray keydown events this._toggleOnKeyUp = true; dojo.stopEvent(e); } }, _onKeyUp: function(){ if(this._toggleOnKeyUp){ delete this._toggleOnKeyUp; this.toggleDropDown(); var d = this.dropDown; // drop down may not exist until toggleDropDown() call if(d && 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 // Don't focus on button if the user has explicitly focused on something else (happens // when user clicks another control causing the current popup to close).. // But if focus is inside of the drop down then reset focus to me, because IE doesn't like // it when you display:none a node with focus. var focusMe = dijit._curFocus && this.dropDown && dojo.isDescendant(dijit._curFocus, this.dropDown.domNode); this.closeDropDown(focusMe); 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. This is basically a callback when the // user presses the down arrow button to open the drop down. // tags: // protected loadCallback(); }, toggleDropDown: function(){ // summary: // Callback when the user presses the down arrow button or presses // the down arrow key to open/close the drop down. // Toggle the drop-down widget; if it is up, close it, if not, open it // tags: // protected if(this.disabled || this.readOnly){ 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. To be called only when this.dropDown // has been created and is ready to display (ie, it's data is loaded). // returns: // return value of dijit.popup.open() // tags: // protected var dropDown = this.dropDown, ddNode = dropDown.domNode, aroundNode = this._aroundNode || this.domNode, 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){ 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); // Figure out maximum height allowed (if there is a height restriction) var maxHeight = this.maxHeight; if(maxHeight == -1){ // limit height to space available in viewport either above or below my domNode // (whichever side has more room) var viewport = dojo.window.getBox(), position = dojo.position(aroundNode, false); maxHeight = Math.floor(Math.max(position.y, viewport.h - (position.y + position.h))); } // Attach dropDown to DOM and make make visibility:hidden rather than display:none // so we call startup() and also get the size if(dropDown.startup && !dropDown._started){ dropDown.startup(); } dijit.popup.moveOffScreen(dropDown); // Get size of drop down, and determine if vertical scroll bar needed var mb = dojo._getMarginSize(ddNode); var overHeight = (maxHeight && mb.h > maxHeight); dojo.style(ddNode, { overflowX: "hidden", overflowY: overHeight ? "auto" : "hidden" }); if(overHeight){ mb.h = maxHeight; if("w" in mb){ mb.w += 16; // room for vertical scrollbar } }else{ delete mb.h; } // Adjust dropdown width to match or be larger than my width if(this.forceWidth){ mb.w = aroundNode.offsetWidth; }else if(this.autoWidth){ mb.w = Math.max(mb.w, aroundNode.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: 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; } }); dojo.attr(this._popupStateNode, "popupActive", "true"); dojo.addClass(self._popupStateNode, "dijitHasDropDownOpen"); this._opened=true; // 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 // focus: // If true, refocuses the button widget // tags: // protected if(this._opened){ if(focus){ this.focus(); } dijit.popup.close(this.dropDown); this._opened = false; } } } ); } 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\trole=\"button\" aria-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\" tabIndex=\"-1\"\n\t\tdojoAttachPoint=\"valueNode\"\n/></span>\n"), attributeMap: dojo.delegate(dijit.form._FormWidget.prototype.attributeMap, { value: "valueNode" }), _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 } }, buildRendering: function(){ this.inherited(arguments); dojo.setSelectable(this.focusNode, false); }, _fillContent: function(/*DomNode*/ source){ // Overrides _Templated._fillContent(). // If button label is specified as srcNodeRef.innerHTML rather than // this.params.label, handle it here. // TODO: remove the method in 2.0, parser will do it all for me if(source && (!this.params || !("label" in this.params))){ this.set('label', source.innerHTML); } }, _setShowLabelAttr: function(val){ if(this.containerNode){ dojo.toggleClass(this.containerNode, "dijitDisplayNone", !val); } this._set("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 set('label', ...) to work. // description: // Set the label (text) of the button; takes an HTML string. this._set("label", content); this.containerNode.innerHTML = content; if(this.showLabel == false && !this.params.title){ this.titleNode.title = dojo.trim(this.containerNode.innerText || this.containerNode.textContent || ''); } }, _setIconClassAttr: function(/*String*/ val){ // Custom method so that icon node is hidden when not in use, to avoid excess padding/margin // appearing around it (even if it's a 0x0 sized <img> node) var oldVal = this.iconClass || "dijitNoIcon", newVal = val || "dijitNoIcon"; dojo.replaceClass(this.iconNode, newVal, oldVal); this._set("iconClass", val); } }); 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\trole=\"button\" aria-haspopup=\"true\" aria-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\" tabIndex=\"-1\"\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 && this.dropDownContainer){ var dropDownNode = dojo.query("[widgetId]", this.dropDownContainer)[0]; this.dropDown = dijit.byNode(dropDownNode); delete this.dropDownContainer; } if(this.dropDown){ dijit.popup.hide(this.dropDown); } 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 && (!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' role=\"presentation\"\n\t><tbody role=\"presentation\"><tr role=\"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\trole=\"button\" aria-labelledby=\"${id}_label\"\n\t\t\t><div class=\"dijitReset dijitInline dijitIcon\" dojoAttachPoint=\"iconNode\" role=\"presentation\"></div\n\t\t\t><div class=\"dijitReset dijitInline dijitButtonText\" id=\"${id}_label\" dojoAttachPoint=\"containerNode\" role=\"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\trole=\"button\" aria-haspopup=\"true\"\n\t\t\t><div class=\"dijitReset dijitArrowButtonInner\" role=\"presentation\"></div\n\t\t\t><div class=\"dijitReset dijitArrowButtonChar\" role=\"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" if(!this.disabled){ 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._set("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\" role=\"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, get('value') will return either the string or false depending on // whether or not the checkbox is checked. // // set('value', string) will check the checkbox and change the value to the // specified string // // set('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._set("readOnly", value); dojo.attr(this.focusNode, 'readOnly', value); dijit.setWaiState(this.focusNode, "readonly", value); }, _setValueAttr: function(/*String|Boolean*/ newValue, /*Boolean*/ priorityChange){ // summary: // Handler for value= attribute to constructor, and also calls to // set('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._set("value", newValue); dojo.attr(this.focusNode, 'value', newValue); newValue = true; } if(this._created){ this.set('checked', newValue, priorityChange); } }, _getValueAttr: function(){ // summary: // Hook so get('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._set("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){ dojo.stopEvent(e); 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.getObject("regexp", true, dojo); /*===== 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.getObject("data.util.sorter", true, dojo); 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.getObject("data.util.simpleFetch", true, dojo); 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.getObject("data.util.filter", true, dojo); 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}\" role=\"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 && 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._set("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 get('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 set('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._set("displayedValue", this.get("displayedValue")); } 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 set('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 get('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 get('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) // TODO: maybe we should update this.displayedValue on every keystroke so that we don't need // this method // TODO: this isn't really the displayed value when the user is typing 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 set('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; // sets the serialized value to something corresponding to specified displayedValue // (if possible), and also updates the textbox.value, for example converting "123" // to "123.00" this._setValueAttr(this.get('value'), undefined); this._set("displayedValue", this.get('displayedValue')); }, 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(); // In case someone is watch()'ing for changes to displayedValue this._set("displayedValue", this.get("displayedValue")); }, postCreate: function(){ if(dojo.isIE){ // IE INPUT tag fontFamily has to be set directly using STYLE // the setTimeout gives IE a chance to render the TextBox and to deal with font inheritance setTimeout(dojo.hitch(this, function(){ 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; } } } } }), 0); } // 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 this.textbox.setAttribute("value", this.textbox.value); // DOM and JS values should be the same this.inherited(arguments); if(dojo.isMoz || dojo.isOpera){ this.connect(this.textbox, "oninput", "_onInput"); }else{ this.connect(this.textbox, "onkeydown", "_onInput"); this.connect(this.textbox, "onkeyup", "_onInput"); this.connect(this.textbox, "onpaste", "_onInput"); this.connect(this.textbox, "oncut", "_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 set('value', ...) // and get('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(); // call this.inherited() before refreshState(), since this.inherited() will possibly scroll the viewport // (to scroll the TextBox into view), which will affect how _refreshState() positions the tooltip this.inherited(arguments); this._refreshState(); }, 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 r = element.createTextRange(); r.collapse(true); r.moveStart("character", -99999); // move to 0 r.moveStart("character", start); // delta from 0 is the correct position r.moveEnd("character", stop-start); r.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\" role='alert'></div\n\t><div class=\"dijitTooltipConnector\" dojoAttachPoint=\"connectorNode\"></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; } // reset width; it may have been set by orient() on a previous tooltip show() this.domNode.width = "auto"; 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, /*Object*/ spaceAvailable, /*Object*/ aroundNodeCoords){ // summary: // Private function to set CSS for tooltip node based on which position it's in. // This is called by the dijit popup code. It will also reduce the tooltip's // width to whatever width is available // tags: // protected this.connectorNode.style.top = ""; //reset to default //Adjust the spaceAvailable width, without changing the spaceAvailable object var tooltipSpaceAvaliableWidth = spaceAvailable.w - this.connectorNode.offsetWidth; 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]; // reduce tooltip's width to the amount of width available, so that it doesn't overflow screen this.domNode.style.width = "auto"; var size = dojo.contentBox(this.domNode); var width = Math.min((Math.max(tooltipSpaceAvaliableWidth,1)), size.w); var widthWasReduced = width < size.w; this.domNode.style.width = width+"px"; //Adjust width for tooltips that have a really long word or a nowrap setting if(widthWasReduced){ this.containerNode.style.overflow = "auto"; //temp change to overflow to detect if our tooltip needs to be wider to support the content var scrollWidth = this.containerNode.scrollWidth; this.containerNode.style.overflow = "visible"; //change it back if(scrollWidth > width){ scrollWidth = scrollWidth + dojo.style(this.domNode,"paddingLeft") + dojo.style(this.domNode,"paddingRight"); this.domNode.style.width = scrollWidth + "px"; } } // Reposition the tooltip connector. if(tooltipCorner.charAt(0) == 'B' && aroundCorner.charAt(0) == 'B'){ var mb = dojo.marginBox(node); var tooltipConnectorHeight = this.connectorNode.offsetHeight; if(mb.h > spaceAvailable.h){ // The tooltip starts at the top of the page and will extend past the aroundNode var aroundNodePlacement = spaceAvailable.h - (aroundNodeCoords.h / 2) - (tooltipConnectorHeight / 2); this.connectorNode.style.top = aroundNodePlacement + "px"; this.connectorNode.style.bottom = ""; }else{ // Align center of connector with center of aroundNode, except don't let bottom // of connector extend below bottom of tooltip content, or top of connector // extend past top of tooltip content this.connectorNode.style.bottom = Math.min( Math.max(aroundNodeCoords.h/2 - tooltipConnectorHeight/2, 0), mb.h - tooltipConnectorHeight) + "px"; this.connectorNode.style.top = ""; } }else{ // reset the tooltip back to the defaults this.connectorNode.style.top = ""; this.connectorNode.style.bottom = ""; } return Math.max(0, size.w - tooltipSpaceAvaliableWidth); }, _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: String|String[] // Id of domNode(s) to attach the tooltip to. // When user hovers over specified dom node, the tooltip will appear. connectId: [], // position: String[] // See description of `dijit.Tooltip.defaultPosition` for details on position parameter. position: [], _setConnectIdAttr: function(/*String*/ newId){ // summary: // Connect to node(s) (specified by id) // Remove connections to old nodes (if there are any) dojo.forEach(this._connections || [], function(nested){ dojo.forEach(nested, dojo.hitch(this, "disconnect")); }, this); // Make connections to nodes in newIds. var ary = dojo.isArrayLike(newId) ? newId : (newId ? [newId] : []); this._connections = dojo.map(ary, function(id){ var node = dojo.byId(id); return node ? [ this.connect(node, "onmouseenter", "_onTargetMouseEnter"), this.connect(node, "onmouseleave", "_onTargetMouseLeave"), this.connect(node, "onfocus", "_onTargetFocus"), this.connect(node, "onblur", "_onTargetBlur") ] : []; }, this); this._set("connectId", newId); this._connectIds = ary; // save as array }, addTarget: function(/*DOMNODE || String*/ node){ // summary: // Attach tooltip to specified node if it's not already connected // TODO: remove in 2.0 and just use set("connectId", ...) interface var id = node.id || node; if(dojo.indexOf(this._connectIds, id) == -1){ this.set("connectId", this._connectIds.concat(id)); } }, removeTarget: function(/*DOMNODE || String*/ node){ // summary: // Detach tooltip from specified node // TODO: remove in 2.0 and just use set("connectId", ...) interface var id = node.id || node, // map from DOMNode back to plain id string idx = dojo.indexOf(this._connectIds, id); if(idx >= 0){ // remove id (modifies original this._connectIds but that's OK in this case) this._connectIds.splice(idx, 1); this.set("connectId", this._connectIds); } }, buildRendering: function(){ this.inherited(arguments); 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}\" role=\"presentation\"\n\t><div class='dijitReset dijitValidationContainer'\n\t\t><input class=\"dijitReset dijitInputField dijitValidationIcon dijitValidationInner\" value=\"Χ \" type=\"text\" tabIndex=\"-1\" readonly=\"readonly\" role=\"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. // Also displays if the textbox value is Incomplete (not yet valid but will be with additional input). // 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_$", // message: String // Currently error/prompt message. // When using the default tooltip implementation, this will only be // displayed when the field is focused. message: "", // 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, Incomplete, or Error) state: "", // tooltipPosition: String[] // See description of `dijit.Tooltip.defaultPosition` for details on this parameter. tooltipPosition: [], _setValueAttr: function(){ // summary: // Hook so set('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 (this.trim ? /^\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 && isFocused && this._isValidSubset(); this._set("state", isValid ? "" : (((((!this._hasBeenBlurred || isFocused) && isEmpty) || isValidSubset) && this._maskValidSubsetError) ? "Incomplete" : "Error")); dijit.setWaiState(this.focusNode, "invalid", isValid ? "false" : "true"); if(this.state == "Error"){ this._maskValidSubsetError = isFocused && isValidSubset; // we want the error to show up after a blur and refocus message = this.getErrorMessage(isFocused); }else if(this.state == "Incomplete"){ message = this.getPromptMessage(isFocused); // show the prompt whenever the value is not yet complete this._maskValidSubsetError = !this._hasBeenBlurred || isFocused; // no Incomplete warnings while focused }else if(isEmpty){ message = this.getPromptMessage(isFocused); // show the prompt whenever there's no error and no text } this.set("message", message); return isValid; }, displayMessage: function(/*String*/ message){ // summary: // Overridable method to display validation errors/hints. // By default uses a tooltip. // tags: // extension dijit.hideTooltip(this.domNode); if(message && this._focused){ 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._set("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._set("required", value); dijit.setWaiState(this.focusNode, "required", value); this._refreshState(); }, _setMessageAttr: function(/*String*/ message){ this._set("message", message); this.displayMessage(message); }, reset:function(){ // Overrides dijit.form.TextBox.reset() by also // hiding errors about partial matches this._maskValidSubsetError = true; this.inherited(arguments); }, _onBlur: function(){ // the message still exists but for back-compat, and to erase the tooltip // (if the message is being displayed as a tooltip), call displayMessage('') 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 get('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.replace(/'/g, """) + "'" : "") + "/>", 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 set('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", dijit._HasDropDown, { // 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: [const] 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: 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/DropDownBox.html", "<div class=\"dijit dijitReset dijitInlineTable dijitLeft\"\n\tid=\"widget_${id}\"\n\trole=\"combobox\"\n\t><div class='dijitReset dijitRight dijitButtonNode dijitArrowButton dijitDownArrowButton dijitArrowButtonContainer'\n\t\tdojoAttachPoint=\"_buttonNode, _popupStateNode\" role=\"presentation\"\n\t\t><input class=\"dijitReset dijitInputField dijitArrowButtonInner\" value=\"▼ \" type=\"text\" tabIndex=\"-1\" readonly=\"readonly\" role=\"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=\"readonly\" role=\"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\tdojoAttachPoint=\"textbox,focusNode\" role=\"textbox\" aria-haspopup=\"true\"\n\t/></div\n></div>\n"), baseClass: "dijitTextBox dijitComboBox", // dropDownClass: [protected extension] String // Name of the dropdown widget class used to select a date/time. // Subclasses should specify this. dropDownClass: "dijit.form._ComboBoxMenu", // Set classes like dijitDownArrowButtonHover depending on // mouse action over button node cssStateNodes: { "_buttonNode": "dijitDownArrowButton" }, // Flags to _HasDropDown to limit height of drop down to make it fit in viewport maxHeight: -1, // For backwards compatibility let onClick events propagate, even clicks on the down arrow button _stopClickEvents: false, _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 doesn't 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.domNode, "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._onKey({charOrCode: 229}); // fake IME key to cause a search }), 100); // long delay that will probably be preempted by keyboard input } this.inherited(arguments); }, _onKey: 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 pw = this.dropDown; var dk = dojo.keys; var highlighted = null; this._prev_key_backspace = false; this._abortQuery(); // _HasDropDown will do some of the work: // 1. when drop down is not yet shown: // - if user presses the down arrow key, call loadDropDown() // 2. when drop down is already displayed: // - on ESC key, call closeDropDown() // - otherwise, call dropDown.handleKey() to process the keystroke this.inherited(arguments); if(this._opened){ highlighted = pw.getHighlightedOption(); } switch(key){ case dk.PAGE_DOWN: case dk.DOWN_ARROW: case dk.PAGE_UP: case dk.UP_ARROW: // Keystroke caused ComboBox_menu to move to a different item. // Copy new item to <input> box. if(this._opened){ 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: // if enter pressed while drop down is open, or for FilteringSelect, // if we are in the middle of a query to convert a directly typed in value to an item, // prevent submit, but allow event to bubble if(this._opened || this._fetchHandle){ 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._opened){ this._lastQuery = null; // in case results come back later this.closeDropDown(); } break; case ' ': if(highlighted){ // user is effectively clicking a choice in the drop down menu dojo.stopEvent(evt); this._selectOption(); this.closeDropDown(); }else{ // user typed a space into the input box, treat as normal character doSearch = true; } 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. //IME input produces keycode == 229. 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, "_startSearchFromInput"),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){ // summary: // Callback when a search completes. // description: // 1. generates drop-down list and calls _showResultList() to display it // 2. if this result list is from user pressing "more choices"/"previous choices" // then tell screen reader to announce new option this._fetchHandle = null; if( this.disabled || this.readOnly || (dataObject.query[this.searchAttr] != this._lastQuery) ){ return; } var wasSelected = this.dropDown._highlighted_option && dojo.hasClass(this.dropDown._highlighted_option, "dijitMenuItemSelected"); this.dropDown.clearResultList(); if(!results.length && !this._maxOptions){ // if no results and not just the previous choices button this.closeDropDown(); 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.dropDown.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.dropDown.highlightFirstOption(); }else if(-1 == dataObject.direction){ this.dropDown.highlightLastOption(); } if(wasSelected){ this._announceOption(this.dropDown.getHighlightedOption()); } }else if(this.autoComplete && !this._prev_key_backspace // 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(){ // summary: // Display the drop down if not already displayed, or if it is displayed, then // reposition it if necessary (reposition may be necessary if drop down's height changed). this.closeDropDown(true); // hide the tooltip this.displayMessage(""); this.openDropDown(); dijit.setWaiState(this.domNode, "expanded", "true"); }, loadDropDown: function(/*Function*/ callback){ // Overrides _HasDropDown.loadDropDown(). // This is called when user has pressed button icon or pressed the down arrow key // to open the drop down. this._startSearchAll(); }, isLoaded: function(){ // signal to _HasDropDown that it needs to call loadDropDown() to load the // drop down asynchronously before displaying it return false; }, closeDropDown: function(){ // Overrides _HasDropDown.closeDropDown(). Closes the drop down (assuming that it's open). // This method is the callback when the user types ESC or clicking // the button icon while the drop down is open. It's also called by other code. this._abortQuery(); if(this._opened){ this.inherited(arguments); dijit.setWaiState(this.domNode, "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.dropDown; 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.closeDropDown(); 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 // set('item', value) // tags: // private if(!displayedValue){ displayedValue = this.store.getValue(item, this.searchAttr); } var value = this._getValueField() != this.searchAttr? this.store.getIdentity(item) : displayedValue; this._set("item", item); dijit.form.ComboBox.superclass._setValueAttr.call(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.dropDown.nextButton || node == this.dropDown.previousButton){ newValue = node.innerHTML; this.item = undefined; this.value = ''; }else{ newValue = this.store.getValue(node.item, this.searchAttr).toString(); 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.closeDropDown(); this._setCaretPos(this.focusNode, this.focusNode.value.length); dijit.form._FormValueWidget.prototype._setValueAttr.call(this, this.value, true); // set this.value and fire onChange }, _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){ // summary: // Starts a search for elements matching key (key=="" means to return all items), // and calls _openResultList() when the search completes, to display the results. if(!this.dropDown){ var popupId = this.id + "_popup", dropDownConstructor = dojo.getObject(this.dropDownClass, false); this.dropDown = new dropDownConstructor({ 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); _this.closeDropDown(); }, 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.focus(); }; this._nextSearch = this.dropDown.onPage = dojo.hitch(this, nextSearch, this._fetchHandle); }, query, this), this.searchDelay); }, _setMaxOptions: function(size, request){ this._maxOptions = size; }, _getValueField: function(){ // summary: // 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; }, //////////// 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.item = this.store.fetchSelectedItem()); if(item){ var valueField = this._getValueField(); this.value = this.store.getValue(item, valueField); } } } this.inherited(arguments); }, postCreate: function(){ // summary: // Subclasses must call this method from their postCreate() methods // tags: // protected // 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"); dijit.setWaiState(this.domNode, "labelledby", label[0].id); } this.inherited(arguments); }, _setHasDownArrowAttr: function(val){ this.hasDownArrow = val; this._buttonNode.style.display = val ? "" : "none"; }, _getMenuLabelFromItem: function(/*Item*/ item){ var label = this.labelFunc(item, this.store), 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 occurrence found. Override this method // to implement your custom highlighting. // tags: // protected var // Add (g)lobal modifier when this.highlightMatch == "all" and (i)gnorecase when this.ignoreCase == true modifiers = (this.ignoreCase ? "i" : "") + (this.highlightMatch == "all" ? "g" : ""), i = this.queryExpr.indexOf("${0}"); find = dojo.regexp.escapeString(find); // escape regexp special chars return this._escapeHtml(label).replace( // prepend ^ when this.queryExpr == "${0}*" and append $ when this.queryExpr == "*${0}" new RegExp((i == 0 ? "^" : "") + "("+ find +")" + (i == (this.queryExpr.length - 4) ? "$" : ""), modifiers), '<span class="dijitComboBoxHighlightMatch">$1</span>' ); // 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 }, 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.labelAttr || 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' style='overflow: \"auto\"; overflow-x: \"hidden\";'>" +"<li class='dijitMenuItem dijitMenuPreviousButton' dojoAttachPoint='previousButton' role='option'></li>" +"<li class='dijitMenuItem dijitMenuNextButton' dojoAttachPoint='nextButton' role='option'></li>" +"</ul>", // _messages: Object // Holds "next" and "previous" text for paging buttons on drop down _messages: null, baseClass: "dijitComboBoxMenu", postMixInProperties: function(){ this.inherited(arguments); this._messages = dojo.i18n.getLocalization("dijit.form", "ComboBox", this.lang); }, buildRendering: function(){ this.inherited(arguments); // fill in template with i18n messages this.previousButton.innerHTML = this._messages["previousMessage"]; this.nextButton.innerHTML = this._messages["nextMessage"]; }, _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 }, 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 menuitem = dojo.create("li", { "class": "dijitReset dijitMenuItem" +(this.isLeftToRight() ? "" : " dijitMenuItemRtl"), role: "option" }); var labelObject = labelFunc(item); 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); 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]); } this._blurOptionNode(); }, _onMouseDown: function(/*Event*/ evt){ dojo.stopEvent(evt); }, _onMouseUp: function(/*Event*/ evt){ if(evt.target === this.domNode || !this._highlighted_option){ // !this._highlighted_option check to prevent immediate selection when menu appears on top // of <input>, see #9898. Note that _HasDropDown also has code to prevent this. return; }else if(evt.target == this.previousButton){ this._blurOptionNode(); this.onPage(-1); }else if(evt.target == this.nextButton){ this._blurOptionNode(); 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(evt){ // summary: // Handle keystroke event forwarded from ComboBox, returning false if it's // a keystroke I recognize and process, true otherwise. switch(evt.charOrCode){ case dojo.keys.DOWN_ARROW: this._highlightNextOption(); return false; case dojo.keys.PAGE_DOWN: this.pageDown(); return false; case dojo.keys.UP_ARROW: this._highlightPrevOption(); return false; case dojo.keys.PAGE_UP: this.pageUp(); return false; default: return true; } } } ); 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 set('value', value) works. // description: // Sets the value of the select. this._set("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) // required: Boolean // True (default) if user is required to enter a value into this field. required: true, _lastDisplayedValue: "", _isValidSubset: function(){ return this._opened; }, isValid: function(){ // Overrides ValidationTextBox.isValid() return this.item || (!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 from dojo.data after lookup of user entered value finishes // 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: don't modify display value 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._set("item", null); this.validate(this._focused); }else{ this.set('item', result[0], priorityChange); } }, _openResultList: function(/*Object*/ results, /*Object*/ dataObject){ // Callback when a data store query completes. // 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; } dijit.form.ComboBoxMixin.prototype._openResultList.apply(this, arguments); if(this.item === undefined){ // item == undefined for keyboard search // If the search returned no items that means that the user typed // in something invalid (and they can't make it valid by typing more characters), // so flag the FilteringSelect as being in an invalid state this.validate(true); } }, _getValueAttr: function(){ // summary: // Hook for get('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 set('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 // set('item', value) // tags: // private 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 set('displayedValue', label) works. // description: // Sets textbox to display label. Also performs reverse lookup // to set the hidden value. label should corresponding to item.searchAttr. if(label == null){ label = ''; } // This is called at initialization along with every custom setter. // Usually (or always?) the call can be ignored. If it needs to be // processed then at least make sure that the XHR request doesn't trigger an onChange() // event, even if it returns after creation has finished if(!this._created){ if(!("displayedValue" in this.params)){ return; } priorityChange = false; } // Do a reverse lookup to map the specified displayedValue to the hidden value. // Note that if there's a custom labelFunc() this code if(this.store){ this.closeDropDown(); 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; this._set("displayedValue", label); // for watch("displayedValue") notification 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); } }, 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, dijit.layout._ContentPaneResizeMixin], { // 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: remove in 2.0, no longer necessary with data-dojo-params 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: [const] 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 clicks the button to open the // 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|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 || node.label !== i.label); }); 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._set("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){ this._loadingStore = true; store.fetch(dojo.delegate(fetchArgs, { onComplete: function(items, opts){ 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.call(this, items, opts); } // 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); }, scope: this })); }else{ delete this._fetchedWith; } return oStore; // dojo.data.api.Identity }, // TODO: implement set() and watch() for store and query, although not sure how to handle // setting them individually rather than together (as in setStore() above) _setValueAttr: function(/*anything*/ newValue, /*Boolean?*/ 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._set("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]; }, _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._set("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); } }, _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; }, buildRendering: function(){ this.inherited(arguments); dojo.setSelectable(this.focusNode, false); }, _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("data-" + dojo._scopeName + "-value") || node.getAttribute("value")), label: String(node.innerHTML), // FIXME: disabled and selected are not valid on complex markup children (which is why we're // looking for data-dojo-value above. perhaps we should data-dojo-props="" this whole thing?) // decide before 1.6 selected: node.getAttribute("selected") || false, disabled: node.getAttribute("disabled") || false }; }, this) : []; } if(!this.value){ this._set("value", this._getValueFromOpts()); }else if(this.multiple && typeof this.value == "string"){ this_set("value", this.value.split(",")); } }, postCreate: function(){ // summary: // sets up our event handling that we need for functioning // as a select 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; }); keyCodes[dojo.keys.HOME] = dojo.hitch(this, "focusFirstChild"); keyCodes[dojo.keys.END] = dojo.hitch(this, "focusLastChild"); 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); } }, focusLastChild: function(){ // summary: // Focus the last focusable child in the container. // tags: // protected var child = this._getLastFocusableChild(); 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.set("tabIndex", this.tabIndex); // for IE focus outline to appear, must set tabIndex before focs widget.focus(last ? "end" : "start"); this._set("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 }, _getLastFocusableChild: function(){ // summary: // Returns last 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\" role=\"menuitem\" tabIndex=\"-1\"\n\t\tdojoAttachEvent=\"onmouseenter:_onHover,onmouseleave:_onUnhover,ondijitclick:_onClick\">\n\t<td class=\"dijitReset dijitMenuItemIconCell\" role=\"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\" role=\"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); } }, buildRendering: function(){ this.inherited(arguments); 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); dojo.setSelectable(this.domNode, false); }, _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); // When menu is hidden (collapsed) due to clicking a MenuItem and having it execute, // FF and IE don't generate an onmouseout event for the MenuItem. // So, help out _CssStateMixin in this case. this._set("hovering", false); }, _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. dijit.setWaiState(this.focusNode, 'disabled', value ? 'true' : 'false'); this._set("disabled", value); }, _setAccelKeyAttr: function(/*String*/ value){ // summary: // Hook for attr('accelKey', ...) to work. // Set accelKey on this menu item. 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"); this._set("accelKey", value); } }); } 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\" role=\"menuitemcheckbox\" tabIndex=\"-1\"\n\t\tdojoAttachEvent=\"onmouseenter:_onHover,onmouseleave:_onUnhover,ondijitclick:_onClick\">\n\t<td class=\"dijitReset dijitMenuItemIconCell\" role=\"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\" role=\"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._set("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"), buildRendering: function(){ this.inherited(arguments); 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"); // "dijit/MenuItem", "dijit/PopupMenuItem", "dijit/CheckedMenuItem", "dijit/MenuSeparator" for Back-compat (TODO: remove in 2.0) 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.replaceClass(this.domNode, "dijitMenuActive", "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.replaceClass(this.domNode, "dijitMenuPassive", "dijitMenuActive"); }, 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(); var fromItem = this.focusedChild && this.focusedChild.from_item; if(this.currentPopup){ // If focus is on my child menu then move focus to me, // because IE doesn't like it when you display:none a node with focus if(dijit._curFocus && dojo.isDescendant(dijit._curFocus, this.currentPopup.domNode)){ this.focusedChild.focusNode.focus(); } // Close all popups that are open and descendants of this menu dijit.popup.close(this.currentPopup); this.currentPopup = null; } if(this.focusedChild){ // unhighlight the focused item this.focusedChild._setSelected(false); this.focusedChild._onUnhover(); this.focusedChild = 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\" role=\"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 set('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); } } ); } 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); }, postCreate: function(){ // summary: // stop mousemove from selecting text on IE to be consistent with other browsers this.inherited(arguments); this.connect(this.domNode, "onmousemove", dojo.stopEvent); }, 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\trole=\"combobox\" aria-haspopup=\"true\"\n\t><tbody role=\"presentation\"><tr role=\"presentation\"\n\t\t><td class=\"dijitReset dijitStretch dijitButtonContents dijitButtonNode\" role=\"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}\" aria-hidden=\"true\"\n\t\t/></td><td class=\"dijitReset dijitRight dijitButtonNode dijitArrowButton dijitDownArrowButton\"\n\t\t\t\tdojoAttachPoint=\"titleNode\" role=\"presentation\"\n\t\t\t><div class=\"dijitReset dijitArrowButtonInner\" role=\"presentation\"></div\n\t\t\t><div class=\"dijitReset dijitArrowButtonChar\" role=\"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: "", // message: String // Currently displayed error/prompt message message: "", // 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); // set value from selected option if(this.options.length && !this.value && this.srcNodeRef){ var si = this.srcNodeRef.selectedIndex || 0; // || 0 needed for when srcNodeRef is not a SELECT this.value = this.options[si >= 0 ? 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 && !option.label){ // 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 || this.emptyLabel, 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(); } 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) var lbl = newDisplay || this.emptyLabel; this.containerNode.innerHTML = '<span class="dijitReset dijitInline ' + this.baseClass + 'Label">' + lbl + '</span>'; dijit.setWaiState(this.focusNode, "valuetext", lbl); }, 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._set("state", isValid ? "" : "Error"); dijit.setWaiState(this.focusNode, "invalid", isValid ? "false" : "true"); var message = isValid ? "" : this._missingMsg; if(this.message !== message){ this._set("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 || this.value === 0 || !(/^\s*$/.test(this.value || ""))); // handle value is null or undefined }, reset: function(){ // summary: // Overridden so that the state will be cleared. this.inherited(arguments); dijit.hideTooltip(this.domNode); this._set("state", ""); this._set("message", "") }, postMixInProperties: function(){ // summary: // set the missing message this.inherited(arguments); this._missingMsg = dojo.i18n.getLocalization("dijit.form", "validate", this.lang).missingMessage; }, postCreate: function(){ // summary: // stop mousemove from selecting text on IE to be consistent with other browsers this.inherited(arguments); this.connect(this.domNode, "onmousemove", dojo.stopEvent); }, _setStyleAttr: function(/*String||Object*/ value){ this.inherited(arguments); dojo.toggleClass(this.domNode, this.baseClass + "FixedWidth", !!this.tableNode.style.width); }, 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) // TODO: parser will handle this in 2.0 if(!this.value && this.srcNodeRef){ this.value = this.srcNodeRef.value; } this.inherited(arguments); }, buildRendering: function(){ this.inherited(arguments); if(dojo.isIE && this.cols){ // attribute selectors is not supported in IE6 dojo.addClass(this.textbox, "dijitTextAreaCols"); } }, 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); }, _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|Function // Class name (or reference to the Class) for Editor widget editor: "dijit.form.TextBox", // editorWrapper: String|Function // Class name (or reference to the Class) 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: {}, // disabled: Boolean // If true, clicking the InlineEditBox to edit it will have no effect. disabled: false, 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. dijit.setWaiState(this.domNode, "disabled", disabled); if(disabled){ this.displayNode.removeAttribute("tabIndex"); }else{ this.displayNode.setAttribute("tabIndex", 0); } dojo.toggleClass(this.displayNode, "dijitInlineEditBoxDisplayModeDisabled", disabled); this._set("disabled", 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 = typeof this.editorWrapper == "string" ? dojo.getObject(this.editorWrapper) : 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); if(!this._started){ this.startup(); } } 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._destroyed){ 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 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. val = dojo.trim(val); var renderVal = this.renderAsHtml ? val : val.replace(/&/gm, "&").replace(/</gm, "<").replace(/>/gm, ">").replace(/"/gm, """).replace(/\n/g, "<br>"); this.displayNode.innerHTML = renderVal || this.noValueIndicator; this._set("value", val); if(this._started){ // tell the world that we have changed setTimeout(dojo.hitch(this, "onChange", val), 0); // setTimeout prevents browser freeze for long-running event handlers } }, 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 data-dojo-attach-point=\"editNode\" role=\"presentation\" style=\"position: absolute; visibility:hidden\" class=\"dijitReset dijitInline\"\n\tdata-dojo-attach-event=\"onkeypress: _onKeyPress\"\n\t><span data-dojo-attach-point=\"editorPlaceholder\"></span\n\t><span data-dojo-attach-point=\"buttonContainer\"\n\t\t><button data-dojo-type=\"dijit.form.Button\" data-dojo-props=\"label: '${buttonSave}', 'class': 'saveButton'\"\n\t\t\tdata-dojo-attach-point=\"saveButton\" data-dojo-attach-event=\"onClick:save\"></button\n\t\t><button data-dojo-type=\"dijit.form.Button\" data-dojo-props=\"label: '${buttonCancel}', 'class': 'cancelButton'\"\n\t\t\tdata-dojo-attach-point=\"cancelButton\" data-dojo-attach-event=\"onClick:cancel\"></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); }, buildRendering: function(){ this.inherited(arguments); // Create edit widget in place in the template var cls = typeof this.editor == "string" ? dojo.getObject(this.editor) : 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; 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); } }, postCreate: function(){ this.inherited(arguments); var ew = this.editWidget; if(this.inlineEditBox.autoSave){ // 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 ew){ 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 role='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", constructor: function(){ this.pane2button = {}; // mapping from pane id to buttons this.pane2connects = {}; // mapping from pane id to this.connect() handles this.pane2watches = {}; // mapping from pane id to watch() handles }, buildRendering: function(){ this.inherited(arguments); dijit.setWaiRole(this.domNode, "tablist"); // TODO: unneeded? it's in template above. }, postCreate: function(){ this.inherited(arguments); // 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"); // map from page attribute to corresponding tab button attribute var pageAttrList = ["title", "showTitle", "iconClass", "closable", "tooltip"], buttonAttrList = ["label", "showLabel", "iconClass", "closeButton", "title"]; // watch() so events like page title changes are reflected in tab button this.pane2watches[page.id] = dojo.map(pageAttrList, function(pageAttr, idx){ return page.watch(pageAttr, function(name, oldVal, newVal){ button.set(buttonAttrList[idx], newVal); }); }); // connections so that clicking a tab button selects the corresponding page this.pane2connects[page.id] = [ 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; } // disconnect/unwatch connections/watches related to page being removed dojo.forEach(this.pane2connects[page.id], dojo.hitch(this, "disconnect")); delete this.pane2connects[page.id]; dojo.forEach(this.pane2watches[page.id], function(w){ w.unwatch(); }); delete this.pane2watches[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.HOME: case k.END: var children = this.getChildren(); if(children && children.length){ children[e.charOrCode == k.HOME ? 0 : children.length-1].onClick(); } dojo.stopEvent(e); 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 next/previous page navigation (left/right arrow, etc.) 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", buildRendering: function(/*Event*/ evt){ this.inherited(arguments); dijit.setWaiRole((this.focusNode || this.domNode), "tab"); }, 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, =====*/ buildRendering: function(){ this.inherited(arguments); dojo.addClass(this.domNode, "dijitLayoutContainer"); dijit.setWaiRole(this.containerNode, "tabpanel"); }, postCreate: function(){ this.inherited(arguments); 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.replaceClass(child.domNode, "dijitHidden", "dijitVisible"); // 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 var d = this._transition(page, this.selectedChildWidget, animate); this._set("selectedChildWidget", page); dojo.publish(this.id+"-selectChild", [page]); if(this.persist){ dojo.cookie(this.id + "_selectedChild", this.selectedChildWidget.id); } } return d; // If child has an href, promise that fires when the child's href finishes loading }, _transition: function(/*dijit._Widget*/ newWidget, /*dijit._Widget*/ oldWidget, /*Boolean*/ animate){ // summary: // Hide the old widget and display the new widget. // Subclasses should override this. // tags: // protected extension if(oldWidget){ this._hideChild(oldWidget); } var d = this._showChild(newWidget); // Size the new widget, in case this is the first time it's being shown, // or I have been resized since the last time it was shown. // Note that page must be visible for resizing to work. if(newWidget.resize){ if(this.doLayout){ newWidget.resize(this._containerContentBox || this._contentBox); }else{ // the child should pick it's own size but we still need to call resize() // (with no arguments) to let the widget lay itself out newWidget.resize(); } } return d; // If child has an href, promise that fires when the child's href finishes loading }, _adjacent: function(/*Boolean*/ forward){ // summary: // Gets the next/previous child widget in this container from the current selection. 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. return this.selectChild(this._adjacent(true), true); }, back: function(){ // summary: // Go back to previous page. return 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. // returns: // Promise that fires when page has finished showing, or true if there's no href var children = this.getChildren(); page.isFirstChild = (page == children[0]); page.isLastChild = (page == children[children.length-1]); page._set("selected", true); dojo.replaceClass(page.domNode, "dijitVisible", "dijitHidden"); return page._onShow() || true; }, _hideChild: function(/*dijit._Widget*/ page){ // summary: // Hide the specified child by changing it's CSS, and call _onHide() so // it's notified. page._set("selected", false); dojo.replaceClass(page.domNode, "dijitHidden", "dijitVisible"); 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"); //dojo.require("dijit.layout.AccordionPane "); // for back compat, remove for 2.0 // Design notes: // // An AccordionContainer is a StackContainer, but each child (typically ContentPane) // is wrapped in a _AccordionInnerContainer. This is hidden from the caller. // // The resulting markup will look like: // // <div class=dijitAccordionContainer> // <div class=dijitAccordionInnerContainer> (one pane) // <div class=dijitAccordionTitle> (title bar) ... </div> // <div class=dijtAccordionChildWrapper> (content pane) </div> // </div> // </div> // // Normally the dijtAccordionChildWrapper is hidden for all but one child (the shown // child), so the space for the content pane is all the title bars + the one dijtAccordionChildWrapper, // which on claro has a 1px border plus a 2px bottom margin. // // During animation there are two dijtAccordionChildWrapper's shown, so we need // to compensate for that. 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", buildRendering: function(){ this.inherited(arguments); this.domNode.style.overflow = "hidden"; // TODO: put this in dijit.css dijit.setWaiRole(this.domNode, "tablist"); // TODO: put this in template }, 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); } }, 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;} // space taken up by title, plus wrapper div (with border/margin) for open pane var wrapperDomNode = openPane._wrapperWidget.domNode, wrapperDomNodeMargin = dojo._getMarginExtents(wrapperDomNode), wrapperDomNodePadBorder = dojo._getPadBorderExtents(wrapperDomNode), wrapperContainerNode = openPane._wrapperWidget.containerNode, wrapperContainerNodeMargin = dojo._getMarginExtents(wrapperContainerNode), wrapperContainerNodePadBorder = dojo._getPadBorderExtents(wrapperContainerNode), 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._getMarginSize(child._wrapperWidget.domNode).h; } }); this._verticalSpace = mySize.h - totalCollapsedHeight - wrapperDomNodeMargin.h - wrapperDomNodePadBorder.h - wrapperContainerNodeMargin.h - wrapperContainerNodePadBorder.h - openPane._buttonWidget.getTitleHeight(); // Memo size to make displayed child this._containerContentBox = { h: this._verticalSpace, w: this._contentBox.w - wrapperDomNodeMargin.w - wrapperDomNodePadBorder.w - wrapperContainerNodeMargin.w - wrapperContainerNodePadBorder.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. // Replace wrapper widget with true child widget (ContentPane etc.). // This step only happens if the AccordionContainer has been started; otherwise there's no wrapper. if(child._wrapperWidget){ dojo.place(child.domNode, child._wrapperWidget.domNode, "after"); 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(){ if(this._animation){ this._animation.stop(); } dojo.forEach(this.getChildren(), function(child){ // If AccordionContainer has been started, then each child has a wrapper widget which // also needs to be destroyed. if(child._wrapperWidget){ child._wrapperWidget.destroy(); }else{ child.destroyRecursive(); } }); this.inherited(arguments); }, _showChild: function(child){ // Override StackContainer._showChild() to set visibility of _wrapperWidget.containerNode child._wrapperWidget.containerNode.style.display="block"; return this.inherited(arguments); }, _hideChild: function(child){ // Override StackContainer._showChild() to set visibility of _wrapperWidget.containerNode child._wrapperWidget.containerNode.style.display="none"; this.inherited(arguments); }, _transition: function(/*dijit._Widget?*/ newWidget, /*dijit._Widget?*/ oldWidget, /*Boolean*/ animate){ // Overrides StackContainer._transition() to provide sliding of title bars etc. if(dojo.isIE < 8){ // workaround animation bugs by not animating; not worth supporting animation for IE6 & 7 animate = false; } if(this._animation){ // there's an in-progress animation. speedily end it so we can do the newly requested one this._animation.stop(true); delete this._animation; } var self = this; if(newWidget){ newWidget._wrapperWidget.set("selected", true); var d = 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); } } if(oldWidget){ oldWidget._wrapperWidget.set("selected", false); if(!animate){ this._hideChild(oldWidget); } } if(animate){ var newContents = newWidget._wrapperWidget.containerNode, oldContents = oldWidget._wrapperWidget.containerNode; // During the animation we will be showing two dijitAccordionChildWrapper nodes at once, // which on claro takes up 4px extra space (compared to stable AccordionContainer). // Have to compensate for that by immediately shrinking the pane being closed. var wrapperContainerNode = newWidget._wrapperWidget.containerNode, wrapperContainerNodeMargin = dojo._getMarginExtents(wrapperContainerNode), wrapperContainerNodePadBorder = dojo._getPadBorderExtents(wrapperContainerNode), animationHeightOverhead = wrapperContainerNodeMargin.h + wrapperContainerNodePadBorder.h; oldContents.style.height = (self._verticalSpace - animationHeightOverhead) + "px"; this._animation = new dojo.Animation({ node: newContents, duration: this.duration, curve: [1, this._verticalSpace - animationHeightOverhead - 1], onAnimate: function(value){ value = Math.floor(value); // avoid fractional values newContents.style.height = value + "px"; oldContents.style.height = (self._verticalSpace - animationHeightOverhead - value) + "px"; }, onEnd: function(){ delete self._animation; newContents.style.height = "auto"; oldWidget._wrapperWidget.containerNode.style.display = "none"; oldContents.style.height = "auto"; self._hideChild(oldWidget); } }); this._animation.onStop = this._animation.onEnd; this._animation.play(); } return d; // If child has an href, promise that fires when the widget has finished loading }, // 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.disabled || e.altKey || !(fromTitle || e.ctrlKey)){ 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(){ // Builds a template like: // <div class=dijitAccordionInnerContainer> // Button // <div class=dijitAccordionChildWrapper> // ContentPane // </div> // </div> // 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), // wrapped by a <div class=dijitAccordionChildWrapper> this.containerNode = dojo.place("<div class='dijitAccordionChildWrapper' style='display:none'>", this.domNode); dojo.place(this.contentWidget.domNode, this.containerNode); }, postCreate: function(){ this.inherited(arguments); // Map changes in content widget's title etc. to changes in the button var button = this.button; this._contentWidgetWatches = [ this.contentWidget.watch('title', dojo.hitch(this, function(name, oldValue, newValue){ button.set("label", newValue); })), this.contentWidget.watch('tooltip', dojo.hitch(this, function(name, oldValue, newValue){ button.set("title", newValue); })), this.contentWidget.watch('iconClass', dojo.hitch(this, function(name, oldValue, newValue){ button.set("iconClass", newValue); })) ]; }, _setSelectedAttr: function(/*Boolean*/ isSelected){ this._set("selected", isSelected); this.button.set("selected", isSelected); if(isSelected){ var cw = this.contentWidget; if(cw.onSelected){ cw.onSelected(); } } }, startup: function(){ // Called by _Container.addChild() this.contentWidget.startup(); }, destroy: function(){ this.button.destroyRecursive(); dojo.forEach(this._contentWidgetWatches || [], function(w){ w.unwatch(); }); delete this.contentWidget._buttonWidget; delete this.contentWidget._wrapperWidget; this.inherited(arguments); }, destroyDescendants: function(){ // since getChildren isn't working for me, have to code this manually this.contentWidget.destroyRecursive(); } }); dojo.declare("dijit.layout._AccordionButton", [dijit._Widget, dijit._Templated, dijit._CssStateMixin], { // summary: // The title bar to click to open up an accordion pane. // Internal widget used by AccordionContainer. // tags: // private templateString: dojo.cache("dijit.layout", "templates/AccordionButton.html", "<div dojoAttachEvent='onclick:_onTitleClick' class='dijitAccordionTitle'>\n\t<div dojoAttachPoint='titleNode,focusNode' dojoAttachEvent='onkeypress:_onTitleKeyPress'\n\t\t\tclass='dijitAccordionTitleFocus' role=\"tab\" aria-expanded=\"false\"\n\t\t><span class='dijitInline dijitAccordionArrow' role=\"presentation\"></span\n\t\t><span class='arrowTextUp' role=\"presentation\">+</span\n\t\t><span class='arrowTextDown' role=\"presentation\">-</span\n\t\t><img src=\"${_blankGif}\" alt=\"\" class=\"dijitIcon\" dojoAttachPoint='iconNode' style=\"vertical-align: middle\" role=\"presentation\"/>\n\t\t<span role=\"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; }, buildRendering: function(){ this.inherited(arguments); var titleTextNodeId = this.id.replace(' ','_'); dojo.attr(this.titleTextNode, "id", titleTextNodeId+"_title"); dijit.setWaiState(this.focusNode, "labelledby", dojo.attr(this.titleTextNode, "id")); dojo.setSelectable(this.domNode, false); }, getTitleHeight: function(){ // summary: // Returns the height of the title dom node. return dojo._getMarginSize(this.domNode).h; // Integer }, // TODO: maybe the parent should set these methods directly rather than forcing the code // into the button widget? _onTitleClick: function(){ // summary: // Callback when someone clicks my title. var parent = this.getParent(); parent.selectChild(this.contentWidget, true); dijit.focus(this.focusNode); }, _onTitleKeyPress: function(/*Event*/ evt){ return this.getParent()._onKeyPress(evt, this.contentWidget); }, _setSelectedAttr: function(/*Boolean*/ isSelected){ this._set("selected", isSelected); dijit.setWaiState(this.focusNode, "expanded", isSelected); dijit.setWaiState(this.focusNode, "selected", isSelected); this.focusNode.setAttribute("tabIndex", isSelected ? "0" : "-1"); } }); } if(!dojo._hasResource["dijit.layout.BorderContainer"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dijit.layout.BorderContainer"] = true; dojo.provide("dijit.layout.BorderContainer"); dojo.declare( "dijit.layout.BorderContainer", dijit.layout._LayoutWidget, { // summary: // Provides layout in up to 5 regions, a mandatory center with optional borders along its 4 sides. // // description: // A BorderContainer is a box with a specified size, such as style="width: 500px; height: 500px;", // that contains a child widget marked region="center" and optionally children widgets marked // region equal to "top", "bottom", "leading", "trailing", "left" or "right". // Children along the edges will be laid out according to width or height dimensions and may // include optional splitters (splitter="true") to make them resizable by the user. The remaining // space is designated for the center region. // // The outer size must be specified on the BorderContainer node. Width must be specified for the sides // and height for the top and bottom, respectively. No dimensions should be specified on the center; // it will fill the remaining space. Regions named "leading" and "trailing" may be used just like // "left" and "right" except that they will be reversed in right-to-left environments. // // For complex layouts, multiple children can be specified for a single region. In this case, the // layoutPriority flag on the children determines which child is closer to the edge (low layoutPriority) // and which child is closer to the center (high layoutPriority). layoutPriority can also be used // instead of the design attribute to conrol layout precedence of horizontal vs. vertical panes. // example: // | <div dojoType="dijit.layout.BorderContainer" design="sidebar" gutters="false" // | style="width: 400px; height: 300px;"> // | <div dojoType="dijit.layout.ContentPane" region="top">header text</div> // | <div dojoType="dijit.layout.ContentPane" region="right" splitter="true" style="width: 200px;">table of contents</div> // | <div dojoType="dijit.layout.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: [const] Boolean // Give each pane a border and margin. // Margin determined by domNode.paddingLeft. // When false, only resizable panes have a gutter (i.e. draggable splitter) for resizing. gutters: true, // liveSplitters: [const] Boolean // Specifies whether splitters resize as you drag (true) or only upon mouseup (false) liveSplitters: true, // persist: Boolean // Save splitter positions in a cookie. persist: false, baseClass: "dijitBorderContainer", // _splitterClass: String // Optional hook to override the default Splitter widget used by BorderContainer _splitterClass: "dijit.layout._Splitter", postMixInProperties: function(){ // change class name to indicate that BorderContainer is being used purely for // layout (like LayoutContainer) rather than for pretty formatting. if(!this.gutters){ this.baseClass += "NoGutter"; } this.inherited(arguments); }, startup: function(){ if(this._started){ return; } dojo.forEach(this.getChildren(), this._setupChild, this); this.inherited(arguments); }, _setupChild: function(/*dijit._Widget*/ child){ // Override _LayoutWidget._setupChild(). var region = child.region; if(region){ this.inherited(arguments); dojo.addClass(child.domNode, this.baseClass+"Pane"); var ltr = this.isLeftToRight(); if(region == "leading"){ region = ltr ? "left" : "right"; } if(region == "trailing"){ region = ltr ? "right" : "left"; } // Create draggable splitter for resizing pane, // or alternately if splitter=false but BorderContainer.gutters=true then // insert dummy div just for spacing if(region != "center" && (child.splitter || this.gutters) && !child._splitterWidget){ var _Splitter = dojo.getObject(child.splitter ? this._splitterClass : "dijit.layout._Gutter"); var splitter = new _Splitter({ id: child.id + "_splitter", container: this, child: child, region: region, live: this.liveSplitters }); splitter.isSplitter = true; child._splitterWidget = splitter; dojo.place(splitter.domNode, child.domNode, "after"); // Splitters aren't added as Contained children, so we need to call startup explicitly splitter.startup(); } child.region = region; // TODO: technically wrong since it overwrites "trailing" with "left" etc. } }, layout: function(){ // Implement _LayoutWidget.layout() virtual method. this._layoutChildren(); }, addChild: function(/*dijit._Widget*/ child, /*Integer?*/ insertIndex){ // Override _LayoutWidget.addChild(). this.inherited(arguments); if(this._started){ this.layout(); //OPT } }, removeChild: function(/*dijit._Widget*/ child){ // Override _LayoutWidget.removeChild(). var region = child.region; var splitter = child._splitterWidget if(splitter){ splitter.destroy(); delete child._splitterWidget; } this.inherited(arguments); if(this._started){ this._layoutChildren(); } // Clean up whatever style changes we made to the child pane. // Unclear how height and width should be handled. dojo.removeClass(child.domNode, this.baseClass+"Pane"); dojo.style(child.domNode, { top: "auto", bottom: "auto", left: "auto", right: "auto", position: "static" }); dojo.style(child.domNode, region == "top" || region == "bottom" ? "width" : "height", "auto"); }, getChildren: function(){ // Override _LayoutWidget.getChildren() to only return real children, not the splitters. return dojo.filter(this.inherited(arguments), function(widget){ return !widget.isSplitter; }); }, // TODO: remove in 2.0 getSplitter: function(/*String*/region){ // summary: // Returns the widget responsible for rendering the splitter associated with region // tags: // deprecated return dojo.filter(this.getChildren(), function(child){ return child.region == region; })[0]._splitterWidget; }, resize: function(newSize, currentSize){ // Overrides _LayoutWidget.resize(). // resetting potential padding to 0px to provide support for 100% width/height + padding // TODO: this hack doesn't respect the box model and is a temporary fix if(!this.cs || !this.pe){ var node = this.domNode; this.cs = dojo.getComputedStyle(node); this.pe = dojo._getPadExtents(node, this.cs); this.pe.r = dojo._toPixelValue(node, this.cs.paddingRight); this.pe.b = dojo._toPixelValue(node, this.cs.paddingBottom); dojo.style(node, "padding", "0px"); } this.inherited(arguments); }, _layoutChildren: function(/*String?*/ changedChildId, /*Number?*/ changedChildSize){ // summary: // This is the main routine for setting size/position of each child. // description: // With no arguments, measures the height of top/bottom panes, the width // of left/right panes, and then sizes all panes accordingly. // // With changedRegion specified (as "left", "top", "bottom", or "right"), // it changes that region's width/height to changedRegionSize and // then resizes other regions that were affected. // changedChildId: // Id of the child which should be resized because splitter was dragged. // changedChildSize: // The new width/height (in pixels) to make specified child if(!this._borderBox || !this._borderBox.h){ // We are currently hidden, or we haven't been sized by our parent yet. // Abort. Someone will resize us later. return; } // Generate list of wrappers of my children in the order that I want layoutChildren() // to process them (i.e. from the outside to the inside) var wrappers = dojo.map(this.getChildren(), function(child, idx){ return { pane: child, weight: [ child.region == "center" ? Infinity : 0, child.layoutPriority, (this.design == "sidebar" ? 1 : -1) * (/top|bottom/.test(child.region) ? 1 : -1), idx ] }; }, this); wrappers.sort(function(a, b){ var aw = a.weight, bw = b.weight; for(var i=0; i<aw.length; i++){ if(aw[i] != bw[i]){ return aw[i] - bw[i]; } } return 0; }); // Make new list, combining the externally specified children with splitters and gutters var childrenAndSplitters = []; dojo.forEach(wrappers, function(wrapper){ var pane = wrapper.pane; childrenAndSplitters.push(pane); if(pane._splitterWidget){ childrenAndSplitters.push(pane._splitterWidget); } }); // Compute the box in which to lay out my children var dim = { l: this.pe.l, t: this.pe.t, w: this._borderBox.w - this.pe.w, h: this._borderBox.h - this.pe.h }; // Layout the children, possibly changing size due to a splitter drag dijit.layout.layoutChildren(this.domNode, dim, childrenAndSplitters, changedChildId, changedChildSize); }, destroyRecursive: function(){ // Destroy splitters first, while getChildren() still works dojo.forEach(this.getChildren(), function(child){ var splitter = child._splitterWidget; if(splitter){ splitter.destroy(); } delete child._splitterWidget; }); // Then destroy the real children, and myself 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: '', // layoutPriority: [const] Number // Parameter for children of `dijit.layout.BorderContainer`. // Children with a higher layoutPriority will be placed closer to the BorderContainer center, // between children with a lower layoutPriority. layoutPriority: 0, // 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: [const] 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" role="separator"><div class="dijitSplitterThumb"></div></div>', postMixInProperties: function(){ this.inherited(arguments); this.horizontal = /top|bottom/.test(this.region); this._factor = /top|left/.test(this.region) ? 1 : -1; this._cookieName = this.container.id + "_" + this.region; }, buildRendering: function(){ this.inherited(arguments); dojo.addClass(this.domNode, "dijitSplitter" + (this.horizontal ? "H" : "V")); if(this.container.persist){ // restore old size var persistSize = dojo.cookie(this._cookieName); if(persistSize){ this.child.domNode.style[this.horizontal ? "height" : "width"] = persistSize; } } }, _computeMaxSize: function(){ // summary: // Return the maximum size that my corresponding pane can be set to var dim = this.horizontal ? 'h' : 'w', childSize = dojo.marginBox(this.child.domNode)[dim], center = dojo.filter(this.container.getChildren(), function(child){ return child.region == "center";})[0], spaceAvailable = dojo.marginBox(center.domNode)[dim]; // can expand until center is crushed to 0 return Math.min(this.child.maxSize, childSize + spaceAvailable); }, _startDrag: function(e){ if(!this.cover){ this.cover = dojo.doc.createElement('div'); dojo.addClass(this.cover, "dijitSplitterCover"); dojo.place(this.cover, this.child.domNode, "after"); } dojo.addClass(this.cover, "dijitSplitterCoverActive"); // Safeguard in case the stop event was missed. Shouldn't be necessary if we always get the mouse up. if(this.fake){ dojo.destroy(this.fake); } if(!(this._resize = this.live)){ //TODO: disable live for IE6? // create fake splitter to display at old position while we drag (this.fake = this.domNode.cloneNode(true)).removeAttribute("id"); dojo.addClass(this.domNode, "dijitSplitterShadow"); dojo.place(this.fake, this.domNode, "after"); } dojo.addClass(this.domNode, "dijitSplitterActive dijitSplitter" + (this.horizontal ? "H" : "V") + "Active"); if(this.fake){ dojo.removeClass(this.fake, "dijitSplitterHover dijitSplitter" + (this.horizontal ? "H" : "V") + "Hover"); } //Performance: load data info local vars for onmousevent function closure var factor = this._factor, isHorizontal = this.horizontal, axis = isHorizontal ? "pageY" : "pageX", pageStart = e[axis], splitterStyle = this.domNode.style, dim = isHorizontal ? 'h' : 'w', childStart = dojo.marginBox(this.child.domNode)[dim], max = this._computeMaxSize(), min = this.child.minSize || 20, region = this.region, splitterAttr = region == "top" || region == "bottom" ? "top" : "left", // style attribute of splitter to adjust splitterStart = parseInt(splitterStyle[splitterAttr], 10), resize = this._resize, layoutFunc = dojo.hitch(this.container, "_layoutChildren", this.child.id), de = dojo.doc; this._handlers = (this._handlers || []).concat([ dojo.connect(de, "onmousemove", this._drag = function(e, forceResize){ var delta = e[axis] - pageStart, childSize = factor * delta + childStart, boundChildSize = Math.max(Math.min(childSize, max), min); if(resize || forceResize){ layoutFunc(boundChildSize); } // TODO: setting style directly (usually) sets content box size, need to set margin box size splitterStyle[splitterAttr] = delta + splitterStart + factor*(boundChildSize - childSize) + "px"; }), dojo.connect(de, "ondragstart", dojo.stopEvent), dojo.connect(dojo.body(), "onselectstart", dojo.stopEvent), dojo.connect(de, "onmouseup", this, "_stopDrag") ]); dojo.stopEvent(e); }, _onMouse: function(e){ var o = (e.type == "mouseover" || e.type == "mouseenter"); dojo.toggleClass(this.domNode, "dijitSplitterHover", o); dojo.toggleClass(this.domNode, "dijitSplitter" + (this.horizontal ? "H" : "V") + "Hover", o); }, _stopDrag: function(e){ try{ if(this.cover){ dojo.removeClass(this.cover, "dijitSplitterCoverActive"); } if(this.fake){ dojo.destroy(this.fake); } dojo.removeClass(this.domNode, "dijitSplitterActive dijitSplitter" + (this.horizontal ? "H" : "V") + "Active dijitSplitterShadow"); this._drag(e); //TODO: redundant with onmousemove? this._drag(e, true); }finally{ this._cleanupHandlers(); delete this._drag; } if(this.container.persist){ dojo.cookie(this._cookieName, this.child.domNode.style[this.horizontal ? "height" : "width"], {expires:365}); } }, _cleanupHandlers: function(){ dojo.forEach(this._handlers, dojo.disconnect); delete this._handlers; }, _onKeyPress: function(/*Event*/ e){ // should we apply typematic to this? this._resize = true; var horizontal = this.horizontal; var tick = 1; var dk = dojo.keys; switch(e.charOrCode){ case horizontal ? dk.UP_ARROW : dk.LEFT_ARROW: tick *= -1; // break; case horizontal ? dk.DOWN_ARROW : dk.RIGHT_ARROW: break; default: // this.inherited(arguments); return; } var childSize = dojo._getMarginSize(this.child.domNode)[ horizontal ? 'h' : 'w' ] + this._factor * tick; this.container._layoutChildren(this.child.id, Math.max(Math.min(childSize, this._computeMaxSize()), this.child.minSize)); dojo.stopEvent(e); }, destroy: function(){ this._cleanupHandlers(); delete this.child; delete this.container; delete this.cover; delete this.fake; this.inherited(arguments); } }); dojo.declare("dijit.layout._Gutter", [dijit._Widget, dijit._Templated], { // summary: // Just a spacer div to separate side pane from center pane. // Basically a trick to lookup the gutter/splitter width from the theme. // description: // Instantiated by `dijit.layout.BorderContainer`. Users should not // create directly. // tags: // private templateString: '<div class="dijitGutter" role="presentation"></div>', postMixInProperties: function(){ this.inherited(arguments); this.horizontal = /top|bottom/.test(this.region); }, buildRendering: function(){ this.inherited(arguments); dojo.addClass(this.domNode, "dijitGutter" + (this.horizontal ? "H" : "V")); } }); } if(!dojo._hasResource["dijit.layout._TabContainerBase"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dijit.layout._TabContainerBase"] = true; dojo.provide("dijit.layout._TabContainerBase"); dojo.declare("dijit.layout._TabContainerBase", [dijit.layout.StackContainer, dijit._Templated], { // summary: // Abstract base class for TabContainer. Must define _makeController() to instantiate // and return the widget that displays the tab labels // description: // A TabContainer is a container that has multiple panes, but shows only // one pane at a time. There are a set of tabs corresponding to each pane, // where each tab has the name (aka title) of the pane, and optionally a close button. // tabPosition: String // Defines where tabs go relative to tab content. // "top", "bottom", "left-h", "right-h" tabPosition: "top", baseClass: "dijitTabContainer", // tabStrip: [const] Boolean // Defines whether the tablist gets an extra class for layouting, putting a border/shading // around the set of tabs. Not supported by claro theme. tabStrip: false, // nested: [const] Boolean // If true, use styling for a TabContainer nested inside another TabContainer. // For tundra etc., makes tabs look like links, and hides the outer // border since the outer TabContainer already has a border. nested: false, templateString: dojo.cache("dijit.layout", "templates/TabContainer.html", "<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); }, buildRendering: function(){ this.inherited(arguments); // Create the tab list that will have a tab (a.k.a. tab button) for each tab panel this.tablist = this._makeController(this.tablistNode); if(!this.doLayout){ dojo.addClass(this.domNode, "dijitTabContainerNoLayout"); } if(this.nested){ /* workaround IE's lack of support for "a > b" selectors by * tagging each node in the template. */ dojo.addClass(this.domNode, "dijitTabContainerNested"); dojo.addClass(this.tablist.containerNode, "dijitTabContainerTabListNested"); dojo.addClass(this.tablistSpacer, "dijitTabContainerSpacerNested"); dojo.addClass(this.containerNode, "dijitTabPaneWrapperNested"); }else{ dojo.addClass(this.domNode, "tabStrip-" + (this.tabStrip ? "enabled" : "disabled")); } }, _setupChild: function(/*dijit._Widget*/ tab){ // Overrides StackContainer._setupChild(). dojo.addClass(tab.domNode, "dijitTabPane"); this.inherited(arguments); }, startup: function(){ if(this._started){ return; } // wire up the tablist and its tabs this.tablist.startup(); this.inherited(arguments); }, layout: function(){ // Overrides StackContainer.layout(). // Configure the content pane to take up all the space except for where the tabs are if(!this._contentBox || typeof(this._contentBox.l) == "undefined"){return;} var sc = this.selectedChildWidget; if(this.doLayout){ // position and size the titles and the container node var titleAlign = this.tabPosition.replace(/-h/, ""); this.tablist.layoutAlign = titleAlign; var children = [this.tablist, { domNode: this.tablistSpacer, layoutAlign: titleAlign }, { domNode: this.containerNode, layoutAlign: "client" }]; dijit.layout.layoutChildren(this.domNode, this._contentBox, children); // Compute size to make each of my children. // children[2] is the margin-box size of this.containerNode, set by layoutChildren() call above this._containerContentBox = dijit.layout.marginBox2contentBox(this.containerNode, children[2]); if(sc && sc.resize){ sc.resize(this._containerContentBox); } }else{ // just layout the tab controller, so it can position left/right buttons etc. if(this.tablist.resize){ //make the tabs zero width so that they don't interfere with width calc, then reset var s = this.tablist.domNode.style; s.width="0"; var width = dojo.contentBox(this.domNode).w; s.width=""; this.tablist.resize({w: width}); } // and call resize() on the selected pane just to tell it that it's been made visible if(sc && sc.resize){ sc.resize(); } } }, destroy: function(){ if(this.tablist){ this.tablist.destroy(); } this.inherited(arguments); } }); } if(!dojo._hasResource["dijit.layout.TabController"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dijit.layout.TabController"] = true; dojo.provide("dijit.layout.TabController"); // Menu is used for an accessible close button, would be nice to have a lighter-weight solution dojo.declare("dijit.layout.TabController", dijit.layout.StackController, { // summary: // Set of tabs (the things with titles and a close button, that you click to show a tab panel). // Used internally by `dijit.layout.TabContainer`. // description: // Lets the user select the currently shown pane in a TabContainer or StackContainer. // TabController also monitors the TabContainer, and whenever a pane is // added or deleted updates itself accordingly. // tags: // private templateString: "<div role='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 role=\"presentation\" dojoAttachPoint=\"titleNode\" dojoAttachEvent='onclick:onClick'>\n <div role=\"presentation\" class='dijitTabInnerDiv' dojoAttachPoint='innerDiv'>\n <div role=\"presentation\" class='dijitTabContent' dojoAttachPoint='tabContent'>\n \t<div role=\"presentation\" dojoAttachPoint='focusNode'>\n\t\t <img src=\"${_blankGif}\" alt=\"\" class=\"dijitIcon dijitTabButtonIcon\" 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' role=\"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, buildRendering: function(){ this.inherited(arguments); dojo.setSelectable(this.containerNode, false); }, startup: function(){ this.inherited(arguments); var n = this.domNode; // Required to give IE6 a kick, as it initially hides the // tabs until they are focused on. setTimeout(function(){ n.className = n.className; }, 1); }, _setCloseButtonAttr: function(/*Boolean*/ disp){ // summary: // Hide/show close button this._set("closeButton", disp); dojo.toggleClass(this.innerDiv, "dijitClosable", disp); this.closeNode.style.display = disp ? "" : "none"; if(disp){ var _nlsResources = dojo.i18n.getLocalization("dijit", "common"); if(this.closeNode){ dojo.attr(this.closeNode,"title", _nlsResources.itemClose); } // add context menu onto title button var _nlsResources = dojo.i18n.getLocalization("dijit", "common"); this._closeMenu = new dijit.Menu({ id: this.id+"_Menu", dir: this.dir, lang: this.lang, targetNodeIds: [this.domNode] }); this._closeMenu.addChild(new dijit.MenuItem({ label: _nlsResources.itemClose, dir: this.dir, lang: this.lang, onClick: dojo.hitch(this, "onClickCloseButton") })); }else{ if(this._closeMenu){ this._closeMenu.destroyRecursive(); delete this._closeMenu; } } }, _setLabelAttr: function(/*String*/ content){ // summary: // Hook for set('label', ...) to work. // description: // takes an HTML string. // Inherited ToggleButton implementation will Set the label (text) of the button; // Need to set the alt attribute of icon on tab buttons if no label displayed this.inherited(arguments); if(this.showLabel == false && !this.params.title){ this.iconNode.alt = dojo.trim(this.containerNode.innerText || this.containerNode.textContent || ''); } }, destroy: function(){ if(this._closeMenu){ this._closeMenu.destroyRecursive(); delete this._closeMenu; } this.inherited(arguments); } }); } if(!dojo._hasResource["dijit.layout.ScrollingTabController"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dijit.layout.ScrollingTabController"] = true; dojo.provide("dijit.layout.ScrollingTabController"); dojo.declare("dijit.layout.ScrollingTabController", dijit.layout.TabController, { // summary: // Set of tabs with left/right arrow keys and a menu to switch between tabs not // all fitting on a single row. // Works only for horizontal tabs (either above or below the content, not to the left // or right). // tags: // private templateString: dojo.cache("dijit.layout", "templates/ScrollingTabController.html", "<div class=\"dijitTabListContainer-${tabPosition}\" style=\"visibility:hidden\">\n\t<div dojoType=\"dijit.layout._ScrollingTabControllerMenuButton\"\n\t\t\tclass=\"tabStripButton-${tabPosition}\"\n\t\t\tid=\"${id}_menuBtn\" containerId=\"${containerId}\" iconClass=\"dijitTabStripMenuIcon\"\n\t\t\tdropDownPosition=\"below-alt, above-alt\"\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 role='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: [const] String // The css class to apply to the tab strip, if it is visible. tabStripClass: "", widgetsInTemplate: true, // _minScroll: Number // The distance in pixels from the edge of the tab strip which, // if a scroll animation is less than, forces the scroll to // go all the way to the left/right. _minScroll: 5, attributeMap: dojo.delegate(dijit._Widget.prototype.attributeMap, { "class": "containerNode" }), buildRendering: function(){ this.inherited(arguments); var n = this.domNode; this.scrollNode = this.tablistWrapper; this._initButtons(); if(!this.tabStripClass){ this.tabStripClass = "dijitTabContainer" + this.tabPosition.charAt(0).toUpperCase() + this.tabPosition.substr(1).replace(/-.*/, "") + "None"; dojo.addClass(n, "tabStrip-disabled") } dojo.addClass(this.tablistWrapper, this.tabStripClass); }, onStartup: function(){ this.inherited(arguments); // Do not show the TabController until the related // StackController has added it's children. This gives // a less visually jumpy instantiation. dojo.style(this.domNode, "visibility", "visible"); this._postStartup = true; }, onAddChild: function(page, insertIndex){ this.inherited(arguments); // changes to the tab button label or iconClass will have changed the width of the // buttons, so do a resize dojo.forEach(["label", "iconClass"], function(attr){ this.pane2watches[page.id].push( this.pane2button[page.id].watch(attr, dojo.hitch(this, function(name, oldValue, newValue){ if(this._postStartup && this._dim){ this.resize(this._dim); } })) ); }, this); // Increment the width of the wrapper when a tab is added // This makes sure that the buttons never wrap. // The value 200 is chosen as it should be bigger than most // Tab button widths. dojo.style(this.containerNode, "width", (dojo.style(this.containerNode, "width") + 200) + "px"); }, onRemoveChild: function(page, insertIndex){ // null out _selectedTab because we are about to delete that dom node var button = this.pane2button[page.id]; if(this._selectedTab === button.domNode){ this._selectedTab = null; } this.inherited(arguments); }, _initButtons: function(){ // summary: // Creates the buttons used to scroll to view tabs that // may not be visible if the TabContainer is too narrow. // Make a list of the buttons to display when the tab labels become // wider than the TabContainer, and hide the other buttons. // Also gets the total width of the displayed buttons. this._btnWidth = 0; this._buttons = dojo.query("> .tabStripButton", this.domNode).filter(function(btn){ if((this.useMenu && btn == this._menuBtn.domNode) || (this.useSlider && (btn == this._rightBtn.domNode || btn == this._leftBtn.domNode))){ this._btnWidth += dojo._getMarginSize(btn).w; return true; }else{ dojo.style(btn, "display", "none"); return false; } }, this); }, _getTabsWidth: function(){ var children = this.getChildren(); if(children.length){ var leftTab = children[this.isLeftToRight() ? 0 : children.length - 1].domNode, rightTab = children[this.isLeftToRight() ? children.length - 1 : 0].domNode; return rightTab.offsetLeft + dojo.style(rightTab, "width") - leftTab.offsetLeft; }else{ return 0; } }, _enableBtn: function(width){ // summary: // Determines if the tabs are wider than the width of the TabContainer, and // thus that we need to display left/right/menu navigation buttons. var tabsWidth = this._getTabsWidth(); width = width || dojo.style(this.scrollNode, "width"); return tabsWidth > 0 && width < tabsWidth; }, resize: function(dim){ // summary: // Hides or displays the buttons used to scroll the tab list and launch the menu // that selects tabs. if(this.domNode.offsetWidth == 0){ return; } // Save the dimensions to be used when a child is renamed. this._dim = dim; // Set my height to be my natural height (tall enough for one row of tab labels), // and my content-box width based on margin-box width specified in dim parameter. // But first reset scrollNode.height in case it was set by layoutChildren() call // in a previous run of this method. this.scrollNode.style.height = "auto"; this._contentBox = dijit.layout.marginBox2contentBox(this.domNode, {h: 0, w: dim.w}); this._contentBox.h = this.scrollNode.offsetHeight; dojo.contentBox(this.domNode, this._contentBox); // Show/hide the left/right/menu navigation buttons depending on whether or not they // are needed. var enable = this._enableBtn(this._contentBox.w); this._buttons.style("display", enable ? "" : "none"); // Position and size the navigation buttons and the tablist this._leftBtn.layoutAlign = "left"; this._rightBtn.layoutAlign = "right"; this._menuBtn.layoutAlign = this.isLeftToRight() ? "right" : "left"; dijit.layout.layoutChildren(this.domNode, this._contentBox, [this._menuBtn, this._leftBtn, this._rightBtn, {domNode: this.scrollNode, layoutAlign: "client"}]); // set proper scroll so that selected tab is visible if(this._selectedTab){ if(this._anim && this._anim.status() == "playing"){ this._anim.stop(); } var w = this.scrollNode, sl = this._convertToScrollLeft(this._getScrollForSelectedTab()); w.scrollLeft = sl; } // Enable/disabled left right buttons depending on whether or not user can scroll to left or right this._setButtonClass(this._getScroll()); this._postResize = true; // Return my size so layoutChildren() can use it. // Also avoids IE9 layout glitch on browser resize when scroll buttons present return {h: this._contentBox.h, w: dim.w}; }, _getScroll: function(){ // summary: // Returns the current scroll of the tabs where 0 means // "scrolled all the way to the left" and some positive number, based on # // of pixels of possible scroll (ex: 1000) means "scrolled all the way to the right" var sl = (this.isLeftToRight() || dojo.isIE < 8 || (dojo.isIE && dojo.isQuirks) || dojo.isWebKit) ? this.scrollNode.scrollLeft : dojo.style(this.containerNode, "width") - dojo.style(this.scrollNode, "width") + (dojo.isIE == 8 ? -1 : 1) * this.scrollNode.scrollLeft; return sl; }, _convertToScrollLeft: function(val){ // summary: // Given a scroll value where 0 means "scrolled all the way to the left" // and some positive number, based on # of pixels of possible scroll (ex: 1000) // means "scrolled all the way to the right", return value to set this.scrollNode.scrollLeft // to achieve that scroll. // // This method is to adjust for RTL funniness in various browsers and versions. if(this.isLeftToRight() || dojo.isIE < 8 || (dojo.isIE && dojo.isQuirks) || dojo.isWebKit){ return val; }else{ var maxScroll = dojo.style(this.containerNode, "width") - dojo.style(this.scrollNode, "width"); return (dojo.isIE == 8 ? -1 : 1) * (val - maxScroll); } }, onSelectChild: function(/*dijit._Widget*/ page){ // summary: // Smoothly scrolls to a tab when it is selected. var tab = this.pane2button[page.id]; if(!tab || !page){return;} // Scroll to the selected tab, except on startup, when scrolling is handled in resize() var node = tab.domNode; if(this._postResize && node != this._selectedTab){ this._selectedTab = node; var sl = this._getScroll(); if(sl > node.offsetLeft || sl + dojo.style(this.scrollNode, "width") < node.offsetLeft + dojo.style(node, "width")){ this.createSmoothScroll().play(); } } this.inherited(arguments); }, _getScrollBounds: function(){ // summary: // Returns the minimum and maximum scroll setting to show the leftmost and rightmost // tabs (respectively) var children = this.getChildren(), scrollNodeWidth = dojo.style(this.scrollNode, "width"), // about 500px containerWidth = dojo.style(this.containerNode, "width"), // 50,000px maxPossibleScroll = containerWidth - scrollNodeWidth, // scrolling until right edge of containerNode visible tabsWidth = this._getTabsWidth(); if(children.length && tabsWidth > scrollNodeWidth){ // Scrolling should happen return { min: this.isLeftToRight() ? 0 : children[children.length-1].domNode.offsetLeft, max: this.isLeftToRight() ? (children[children.length-1].domNode.offsetLeft + dojo.style(children[children.length-1].domNode, "width")) - scrollNodeWidth : maxPossibleScroll }; }else{ // No scrolling needed, all tabs visible, we stay either scrolled to far left or far right (depending on dir) var onlyScrollPosition = this.isLeftToRight() ? 0 : maxPossibleScroll; return { min: onlyScrollPosition, max: onlyScrollPosition }; } }, _getScrollForSelectedTab: function(){ // summary: // Returns the scroll value setting so that the selected tab // will appear in the center var w = this.scrollNode, n = this._selectedTab, scrollNodeWidth = dojo.style(this.scrollNode, "width"), scrollBounds = this._getScrollBounds(); // TODO: scroll minimal amount (to either right or left) so that // selected tab is fully visible, and just return if it's already visible? var pos = (n.offsetLeft + dojo.style(n, "width")/2) - scrollNodeWidth/2; pos = Math.min(Math.max(pos, scrollBounds.min), scrollBounds.max); // TODO: // If scrolling close to the left side or right side, scroll // all the way to the left or right. See this._minScroll. // (But need to make sure that doesn't scroll the tab out of view...) return pos; }, createSmoothScroll: function(x){ // summary: // Creates a dojo._Animation object that smoothly scrolls the tab list // either to a fixed horizontal pixel value, or to the selected tab. // description: // If an number argument is passed to the function, that horizontal // pixel position is scrolled to. Otherwise the currently selected // tab is scrolled to. // x: Integer? // An optional pixel value to scroll to, indicating distance from left. // Calculate position to scroll to if(arguments.length > 0){ // position specified by caller, just make sure it's within bounds var scrollBounds = this._getScrollBounds(); x = Math.min(Math.max(x, scrollBounds.min), scrollBounds.max); }else{ // scroll to center the current tab x = this._getScrollForSelectedTab(); } if(this._anim && this._anim.status() == "playing"){ this._anim.stop(); } var self = this, w = this.scrollNode, anim = new dojo._Animation({ beforeBegin: function(){ if(this.curve){ delete this.curve; } var oldS = w.scrollLeft, newS = self._convertToScrollLeft(x); anim.curve = new dojo._Line(oldS, newS); }, onAnimate: function(val){ w.scrollLeft = val; } }); this._anim = anim; // Disable/enable left/right buttons according to new scroll position this._setButtonClass(x); return anim; // dojo._Animation }, _getBtnNode: function(/*Event*/ e){ // summary: // Gets a button DOM node from a mouse click event. // e: // The mouse click event. var n = e.target; while(n && !dojo.hasClass(n, "tabStripButton")){ n = n.parentNode; } return n; }, doSlideRight: function(/*Event*/ e){ // summary: // Scrolls the menu to the right. // e: // The mouse click event. this.doSlide(1, this._getBtnNode(e)); }, doSlideLeft: function(/*Event*/ e){ // summary: // Scrolls the menu to the left. // e: // The mouse click event. this.doSlide(-1,this._getBtnNode(e)); }, doSlide: function(/*Number*/ direction, /*DomNode*/ node){ // summary: // Scrolls the tab list to the left or right by 75% of the widget width. // direction: // If the direction is 1, the widget scrolls to the right, if it is // -1, it scrolls to the left. if(node && dojo.hasClass(node, "dijitTabDisabled")){return;} var sWidth = dojo.style(this.scrollNode, "width"); var d = (sWidth * 0.75) * direction; var to = this._getScroll() + d; this._setButtonClass(to); this.createSmoothScroll(to).play(); }, _setButtonClass: function(/*Number*/ scroll){ // summary: // Disables the left scroll button if the tabs are scrolled all the way to the left, // or the right scroll button in the opposite case. // scroll: Integer // amount of horizontal scroll var scrollBounds = this._getScrollBounds(); this._leftBtn.set("disabled", scroll <= scrollBounds.min); this._rightBtn.set("disabled", scroll >= scrollBounds.max); } }); dojo.declare("dijit.layout._ScrollingTabControllerButtonMixin", null, { baseClass: "dijitTab tabStripButton", templateString: dojo.cache("dijit.layout", "templates/_ScrollingTabControllerButton.html", "<div dojoAttachEvent=\"onclick:_onButtonClick\">\n\t<div role=\"presentation\" class=\"dijitTabInnerDiv\" dojoattachpoint=\"innerDiv,focusNode\">\n\t\t<div role=\"presentation\" class=\"dijitTabContent dijitButtonContents\" dojoattachpoint=\"tabContent\">\n\t\t\t<img role=\"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: "", // Similarly, override FormWidget.isFocusable() because clicking a button shouldn't focus it // either (this override avoids focus() call in FormWidget.js) isFocusable: function(){ return false; } }); dojo.declare("dijit.layout._ScrollingTabControllerButton", [dijit.form.Button, dijit.layout._ScrollingTabControllerButtonMixin]); dojo.declare( "dijit.layout._ScrollingTabControllerMenuButton", [dijit.form.Button, dijit._HasDropDown, dijit.layout._ScrollingTabControllerButtonMixin], { // id of the TabContainer itself containerId: "", // -1 so user can't tab into the button, but so that button can still be focused programatically. // Because need to move focus to the button (or somewhere) before the menu is hidden or IE6 will crash. tabIndex: "-1", isLoaded: function(){ // recreate menu every time, in case the TabContainer's list of children (or their icons/labels) have changed return false; }, loadDropDown: function(callback){ this.dropDown = new dijit.Menu({ id: this.containerId + "_menu", dir: this.dir, lang: this.lang }); var container = dijit.byId(this.containerId); dojo.forEach(container.getChildren(), function(page){ var menuItem = new dijit.MenuItem({ id: page.id + "_stcMi", label: page.title, iconClass: page.iconClass, dir: page.dir, lang: page.lang, onClick: function(){ container.selectChild(page); } }); this.dropDown.addChild(menuItem); }, this); callback(); }, closeDropDown: function(/*Boolean*/ focus){ this.inherited(arguments); if(this.dropDown){ this.dropDown.destroyRecursive(); delete this.dropDown; } } }); } if(!dojo._hasResource["dijit.layout.TabContainer"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dijit.layout.TabContainer"] = true; dojo.provide("dijit.layout.TabContainer"); dojo.declare("dijit.layout.TabContainer", dijit.layout._TabContainerBase, { // summary: // A Container with tabs to select each child (only one of which is displayed at a time). // description: // A TabContainer is a container that has multiple panes, but shows only // one pane at a time. There are a set of tabs corresponding to each pane, // where each tab has the name (aka title) of the pane, and optionally a close button. // useMenu: [const] Boolean // True if a menu should be used to select tabs when they are too // wide to fit the TabContainer, false otherwise. useMenu: true, // useSlider: [const] Boolean // True if a slider should be used to select tabs when they are too // wide to fit the TabContainer, false otherwise. useSlider: true, // controllerWidget: String // An optional parameter to override the widget used to display the tab labels controllerWidget: "", _makeController: function(/*DomNode*/ srcNode){ // summary: // Instantiate tablist controller widget and return reference to it. // Callback from _TabContainerBase.postCreate(). // tags: // protected extension var cls = this.baseClass + "-tabs" + (this.doLayout ? "" : " dijitTabNoLayout"), TabController = dojo.getObject(this.controllerWidget); return new TabController({ id: this.id + "_tablist", dir: this.dir, lang: this.lang, tabPosition: this.tabPosition, doLayout: this.doLayout, containerId: this.id, "class": cls, nested: this.nested, useMenu: this.useMenu, useSlider: this.useSlider, tabStripClass: this.tabStrip ? this.baseClass + (this.tabStrip ? "":"No") + "Strip": null }, srcNode); }, postMixInProperties: function(){ this.inherited(arguments); // Scrolling controller only works for horizontal non-nested tabs if(!this.controllerWidget){ this.controllerWidget = (this.tabPosition == "top" || this.tabPosition == "bottom") && !this.nested ? "dijit.layout.ScrollingTabController" : "dijit.layout.TabController"; } } }); } if(!dojo._hasResource["dojo.number"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dojo.number"] = true; dojo.provide("dojo.number"); dojo.getObject("number", true, dojo); /*===== dojo.number = { // summary: localized formatting and parsing routines for Number } dojo.number.__FormatOptions = function(){ // pattern: String? // override [formatting pattern](http://www.unicode.org/reports/tr35/#Number_Format_Patterns) // with this string. Default value is based on locale. Overriding this property will defeat // localization. Literal characters in patterns are not supported. // type: String? // choose a format type based on the locale from the following: // decimal, scientific (not yet supported), percent, currency. decimal by default. // places: Number? // fixed number of decimal places to show. This overrides any // information in the provided pattern. // round: Number? // 5 rounds to nearest .5; 0 rounds to nearest whole (default). -1 // means do not round. // locale: String? // override the locale used to determine formatting rules // fractional: Boolean? // If false, show no decimal places, overriding places and pattern settings. this.pattern = pattern; this.type = type; this.places = places; this.round = round; this.locale = locale; this.fractional = fractional; } =====*/ dojo.number.format = function(/*Number*/value, /*dojo.number.__FormatOptions?*/options){ // summary: // Format a Number as a String, using locale-specific settings // description: // Create a string from a Number using a known localized pattern. // Formatting patterns appropriate to the locale are chosen from the // [Common Locale Data Repository](http://unicode.org/cldr) as well as the appropriate symbols and // delimiters. // If value is Infinity, -Infinity, or is not a valid JavaScript number, return null. // value: // the number to be formatted options = dojo.mixin({}, options || {}); var locale = dojo.i18n.normalizeLocale(options.locale), bundle = dojo.i18n.getLocalization("dojo.cldr", "number", locale); options.customs = bundle; var pattern = options.pattern || bundle[(options.type || "decimal") + "Format"]; if(isNaN(value) || Math.abs(value) == Infinity){ return null; } // null return dojo.number._applyPattern(value, pattern, options); // String }; //dojo.number._numberPatternRE = /(?:[#0]*,?)*[#0](?:\.0*#*)?/; // not precise, but good enough dojo.number._numberPatternRE = /[#0,]*[#0](?:\.0*#*)?/; // not precise, but good enough dojo.number._applyPattern = function(/*Number*/value, /*String*/pattern, /*dojo.number.__FormatOptions?*/options){ // summary: // Apply pattern to format value as a string using options. Gives no // consideration to local customs. // value: // the number to be formatted. // pattern: // a pattern string as described by // [unicode.org TR35](http://www.unicode.org/reports/tr35/#Number_Format_Patterns) // options: dojo.number.__FormatOptions? // _applyPattern is usually called via `dojo.number.format()` which // populates an extra property in the options parameter, "customs". // The customs object specifies group and decimal parameters if set. //TODO: support escapes options = options || {}; var group = options.customs.group, decimal = options.customs.decimal, patternList = pattern.split(';'), positivePattern = patternList[0]; pattern = patternList[(value < 0) ? 1 : 0] || ("-" + positivePattern); //TODO: only test against unescaped if(pattern.indexOf('%') != -1){ value *= 100; }else if(pattern.indexOf('\u2030') != -1){ value *= 1000; // per mille }else if(pattern.indexOf('\u00a4') != -1){ group = options.customs.currencyGroup || group;//mixins instead? decimal = options.customs.currencyDecimal || decimal;// Should these be mixins instead? pattern = pattern.replace(/\u00a4{1,3}/, function(match){ var prop = ["symbol", "currency", "displayName"][match.length-1]; return options[prop] || options.currency || ""; }); }else if(pattern.indexOf('E') != -1){ throw new Error("exponential notation not supported"); } //TODO: support @ sig figs? var numberPatternRE = dojo.number._numberPatternRE; var numberPattern = positivePattern.match(numberPatternRE); if(!numberPattern){ throw new Error("unable to find a number expression in pattern: "+pattern); } if(options.fractional === false){ options.places = 0; } return pattern.replace(numberPatternRE, dojo.number._formatAbsolute(value, numberPattern[0], {decimal: decimal, group: group, places: options.places, round: options.round})); }; dojo.number.round = function(/*Number*/value, /*Number?*/places, /*Number?*/increment){ // summary: // Rounds to the nearest value with the given number of decimal places, away from zero // description: // Rounds to the nearest value with the given number of decimal places, away from zero if equal. // Similar to Number.toFixed(), but compensates for browser quirks. Rounding can be done by // fractional increments also, such as the nearest quarter. // NOTE: Subject to floating point errors. See dojox.math.round for experimental workaround. // value: // The number to round // places: // The number of decimal places where rounding takes place. Defaults to 0 for whole rounding. // Must be non-negative. // increment: // Rounds next place to nearest value of increment/10. 10 by default. // example: // >>> dojo.number.round(-0.5) // -1 // >>> dojo.number.round(162.295, 2) // 162.29 // note floating point error. Should be 162.3 // >>> dojo.number.round(10.71, 0, 2.5) // 10.75 var factor = 10 / (increment || 10); return (factor * +value).toFixed(places) / factor; // Number }; if((0.9).toFixed() == 0){ // (isIE) toFixed() bug workaround: Rounding fails on IE when most significant digit // is just after the rounding place and is >=5 (function(){ var round = dojo.number.round; dojo.number.round = function(v, p, m){ var d = Math.pow(10, -p || 0), a = Math.abs(v); if(!v || a >= d || a * Math.pow(10, p + 1) < 5){ d = 0; } return round(v, p, m) + (v > 0 ? d : -d); }; })(); } /*===== dojo.number.__FormatAbsoluteOptions = function(){ // decimal: String? // the decimal separator // group: String? // the group separator // places: Number?|String? // number of decimal places. the range "n,m" will format to m places. // round: Number? // 5 rounds to nearest .5; 0 rounds to nearest whole (default). -1 // means don't round. this.decimal = decimal; this.group = group; this.places = places; this.round = round; } =====*/ dojo.number._formatAbsolute = function(/*Number*/value, /*String*/pattern, /*dojo.number.__FormatAbsoluteOptions?*/options){ // summary: // Apply numeric pattern to absolute value using options. Gives no // consideration to local customs. // value: // the number to be formatted, ignores sign // pattern: // the number portion of a pattern (e.g. `#,##0.00`) options = options || {}; if(options.places === true){options.places=0;} if(options.places === Infinity){options.places=6;} // avoid a loop; pick a limit var patternParts = pattern.split("."), comma = typeof options.places == "string" && options.places.indexOf(","), maxPlaces = options.places; if(comma){ maxPlaces = options.places.substring(comma + 1); }else if(!(maxPlaces >= 0)){ maxPlaces = (patternParts[1] || []).length; } if(!(options.round < 0)){ value = dojo.number.round(value, maxPlaces, options.round); } var valueParts = String(Math.abs(value)).split("."), fractional = valueParts[1] || ""; if(patternParts[1] || options.places){ if(comma){ options.places = options.places.substring(0, comma); } // Pad fractional with trailing zeros var pad = options.places !== undefined ? options.places : (patternParts[1] && patternParts[1].lastIndexOf("0") + 1); if(pad > fractional.length){ valueParts[1] = dojo.string.pad(fractional, pad, '0', true); } // Truncate fractional if(maxPlaces < fractional.length){ valueParts[1] = fractional.substr(0, maxPlaces); } }else{ if(valueParts[1]){ valueParts.pop(); } } // Pad whole with leading zeros var patternDigits = patternParts[0].replace(',', ''); pad = patternDigits.indexOf("0"); if(pad != -1){ pad = patternDigits.length - pad; if(pad > valueParts[0].length){ valueParts[0] = dojo.string.pad(valueParts[0], pad); } // Truncate whole if(patternDigits.indexOf("#") == -1){ valueParts[0] = valueParts[0].substr(valueParts[0].length - pad); } } // Add group separators var index = patternParts[0].lastIndexOf(','), groupSize, groupSize2; if(index != -1){ groupSize = patternParts[0].length - index - 1; var remainder = patternParts[0].substr(0, index); index = remainder.lastIndexOf(','); if(index != -1){ groupSize2 = remainder.length - index - 1; } } var pieces = []; for(var whole = valueParts[0]; whole;){ var off = whole.length - groupSize; pieces.push((off > 0) ? whole.substr(off) : whole); whole = (off > 0) ? whole.slice(0, off) : ""; if(groupSize2){ groupSize = groupSize2; delete groupSize2; } } valueParts[0] = pieces.reverse().join(options.group || ","); return valueParts.join(options.decimal || "."); }; /*===== dojo.number.__RegexpOptions = function(){ // pattern: String? // override [formatting pattern](http://www.unicode.org/reports/tr35/#Number_Format_Patterns) // with this string. Default value is based on locale. Overriding this property will defeat // localization. // type: String? // choose a format type based on the locale from the following: // decimal, scientific (not yet supported), percent, currency. decimal by default. // locale: String? // override the locale used to determine formatting rules // strict: Boolean? // strict parsing, false by default. Strict parsing requires input as produced by the format() method. // Non-strict is more permissive, e.g. flexible on white space, omitting thousands separators // places: Number|String? // number of decimal places to accept: Infinity, a positive number, or // a range "n,m". Defined by pattern or Infinity if pattern not provided. this.pattern = pattern; this.type = type; this.locale = locale; this.strict = strict; this.places = places; } =====*/ dojo.number.regexp = function(/*dojo.number.__RegexpOptions?*/options){ // summary: // Builds the regular needed to parse a number // description: // Returns regular expression with positive and negative match, group // and decimal separators return dojo.number._parseInfo(options).regexp; // String }; dojo.number._parseInfo = function(/*Object?*/options){ options = options || {}; var locale = dojo.i18n.normalizeLocale(options.locale), bundle = dojo.i18n.getLocalization("dojo.cldr", "number", locale), pattern = options.pattern || bundle[(options.type || "decimal") + "Format"], //TODO: memoize? group = bundle.group, decimal = bundle.decimal, factor = 1; if(pattern.indexOf('%') != -1){ factor /= 100; }else if(pattern.indexOf('\u2030') != -1){ factor /= 1000; // per mille }else{ var isCurrency = pattern.indexOf('\u00a4') != -1; if(isCurrency){ group = bundle.currencyGroup || group; decimal = bundle.currencyDecimal || decimal; } } //TODO: handle quoted escapes var patternList = pattern.split(';'); if(patternList.length == 1){ patternList.push("-" + patternList[0]); } var re = dojo.regexp.buildGroupRE(patternList, function(pattern){ pattern = "(?:"+dojo.regexp.escapeString(pattern, '.')+")"; return pattern.replace(dojo.number._numberPatternRE, function(format){ var flags = { signed: false, separator: options.strict ? group : [group,""], fractional: options.fractional, decimal: decimal, exponent: false }, parts = format.split('.'), places = options.places; // special condition for percent (factor != 1) // allow decimal places even if not specified in pattern if(parts.length == 1 && factor != 1){ parts[1] = "###"; } if(parts.length == 1 || places === 0){ flags.fractional = false; }else{ if(places === undefined){ places = options.pattern ? parts[1].lastIndexOf('0') + 1 : Infinity; } if(places && options.fractional == undefined){flags.fractional = true;} // required fractional, unless otherwise specified if(!options.places && (places < parts[1].length)){ places += "," + parts[1].length; } flags.places = places; } var groups = parts[0].split(','); if(groups.length > 1){ flags.groupSize = groups.pop().length; if(groups.length > 1){ flags.groupSize2 = groups.pop().length; } } return "("+dojo.number._realNumberRegexp(flags)+")"; }); }, true); if(isCurrency){ // substitute the currency symbol for the placeholder in the pattern re = re.replace(/([\s\xa0]*)(\u00a4{1,3})([\s\xa0]*)/g, function(match, before, target, after){ var prop = ["symbol", "currency", "displayName"][target.length-1], symbol = dojo.regexp.escapeString(options[prop] || options.currency || ""); before = before ? "[\\s\\xa0]" : ""; after = after ? "[\\s\\xa0]" : ""; if(!options.strict){ if(before){before += "*";} if(after){after += "*";} return "(?:"+before+symbol+after+")?"; } return before+symbol+after; }); } //TODO: substitute localized sign/percent/permille/etc.? // normalize whitespace and return return {regexp: re.replace(/[\xa0 ]/g, "[\\s\\xa0]"), group: group, decimal: decimal, factor: factor}; // Object }; /*===== dojo.number.__ParseOptions = function(){ // pattern: String? // override [formatting pattern](http://www.unicode.org/reports/tr35/#Number_Format_Patterns) // with this string. Default value is based on locale. Overriding this property will defeat // localization. Literal characters in patterns are not supported. // type: String? // choose a format type based on the locale from the following: // decimal, scientific (not yet supported), percent, currency. decimal by default. // locale: String? // override the locale used to determine formatting rules // strict: Boolean? // strict parsing, false by default. Strict parsing requires input as produced by the format() method. // Non-strict is more permissive, e.g. flexible on white space, omitting thousands separators // fractional: Boolean?|Array? // Whether to include the fractional portion, where the number of decimal places are implied by pattern // or explicit 'places' parameter. The value [true,false] makes the fractional portion optional. this.pattern = pattern; this.type = type; this.locale = locale; this.strict = strict; this.fractional = fractional; } =====*/ dojo.number.parse = function(/*String*/expression, /*dojo.number.__ParseOptions?*/options){ // summary: // Convert a properly formatted string to a primitive Number, using // locale-specific settings. // description: // Create a Number from a string using a known localized pattern. // Formatting patterns are chosen appropriate to the locale // and follow the syntax described by // [unicode.org TR35](http://www.unicode.org/reports/tr35/#Number_Format_Patterns) // Note that literal characters in patterns are not supported. // expression: // A string representation of a Number var info = dojo.number._parseInfo(options), results = (new RegExp("^"+info.regexp+"$")).exec(expression); if(!results){ return NaN; //NaN } var absoluteMatch = results[1]; // match for the positive expression if(!results[1]){ if(!results[2]){ return NaN; //NaN } // matched the negative pattern absoluteMatch =results[2]; info.factor *= -1; } // Transform it to something Javascript can parse as a number. Normalize // decimal point and strip out group separators or alternate forms of whitespace absoluteMatch = absoluteMatch. replace(new RegExp("["+info.group + "\\s\\xa0"+"]", "g"), ""). replace(info.decimal, "."); // Adjust for negative sign, percent, etc. as necessary return absoluteMatch * info.factor; //Number }; /*===== dojo.number.__RealNumberRegexpFlags = function(){ // places: Number? // The integer number of decimal places or a range given as "n,m". If // not given, the decimal part is optional and the number of places is // unlimited. // decimal: String? // A string for the character used as the decimal point. Default // is ".". // fractional: Boolean?|Array? // Whether decimal places are used. Can be true, false, or [true, // false]. Default is [true, false] which means optional. // exponent: Boolean?|Array? // Express in exponential notation. Can be true, false, or [true, // false]. Default is [true, false], (i.e. will match if the // exponential part is present are not). // eSigned: Boolean?|Array? // The leading plus-or-minus sign on the exponent. Can be true, // false, or [true, false]. Default is [true, false], (i.e. will // match if it is signed or unsigned). flags in regexp.integer can be // applied. this.places = places; this.decimal = decimal; this.fractional = fractional; this.exponent = exponent; this.eSigned = eSigned; } =====*/ dojo.number._realNumberRegexp = function(/*dojo.number.__RealNumberRegexpFlags?*/flags){ // summary: // Builds a regular expression to match a real number in exponential // notation // assign default values to missing parameters flags = flags || {}; //TODO: use mixin instead? if(!("places" in flags)){ flags.places = Infinity; } if(typeof flags.decimal != "string"){ flags.decimal = "."; } if(!("fractional" in flags) || /^0/.test(flags.places)){ flags.fractional = [true, false]; } if(!("exponent" in flags)){ flags.exponent = [true, false]; } if(!("eSigned" in flags)){ flags.eSigned = [true, false]; } var integerRE = dojo.number._integerRegexp(flags), decimalRE = dojo.regexp.buildGroupRE(flags.fractional, function(q){ var re = ""; if(q && (flags.places!==0)){ re = "\\" + flags.decimal; if(flags.places == Infinity){ re = "(?:" + re + "\\d+)?"; }else{ re += "\\d{" + flags.places + "}"; } } return re; }, true ); var exponentRE = dojo.regexp.buildGroupRE(flags.exponent, function(q){ if(q){ return "([eE]" + dojo.number._integerRegexp({ signed: flags.eSigned}) + ")"; } return ""; } ); var realRE = integerRE + decimalRE; // allow for decimals without integers, e.g. .25 if(decimalRE){realRE = "(?:(?:"+ realRE + ")|(?:" + decimalRE + "))";} return realRE + exponentRE; // String }; /*===== dojo.number.__IntegerRegexpFlags = function(){ // signed: Boolean? // The leading plus-or-minus sign. Can be true, false, or `[true,false]`. // Default is `[true, false]`, (i.e. will match if it is signed // or unsigned). // separator: String? // The character used as the thousands separator. Default is no // separator. For more than one symbol use an array, e.g. `[",", ""]`, // makes ',' optional. // groupSize: Number? // group size between separators // groupSize2: Number? // second grouping, where separators 2..n have a different interval than the first separator (for India) this.signed = signed; this.separator = separator; this.groupSize = groupSize; this.groupSize2 = groupSize2; } =====*/ dojo.number._integerRegexp = function(/*dojo.number.__IntegerRegexpFlags?*/flags){ // summary: // Builds a regular expression that matches an integer // assign default values to missing parameters flags = flags || {}; if(!("signed" in flags)){ flags.signed = [true, false]; } if(!("separator" in flags)){ flags.separator = ""; }else if(!("groupSize" in flags)){ flags.groupSize = 3; } var signRE = dojo.regexp.buildGroupRE(flags.signed, function(q){ return q ? "[-+]" : ""; }, true ); var numberRE = dojo.regexp.buildGroupRE(flags.separator, function(sep){ if(!sep){ return "(?:\\d+)"; } sep = dojo.regexp.escapeString(sep); if(sep == " "){ sep = "\\s"; } else if(sep == "\xa0"){ sep = "\\s\\xa0"; } var grp = flags.groupSize, grp2 = flags.groupSize2; //TODO: should we continue to enforce that numbers with separators begin with 1-9? See #6933 if(grp2){ var grp2RE = "(?:0|[1-9]\\d{0," + (grp2-1) + "}(?:[" + sep + "]\\d{" + grp2 + "})*[" + sep + "]\\d{" + grp + "})"; return ((grp-grp2) > 0) ? "(?:" + grp2RE + "|(?:0|[1-9]\\d{0," + (grp-1) + "}))" : grp2RE; } return "(?:0|[1-9]\\d{0," + (grp-1) + "}(?:[" + sep + "]\\d{" + grp + "})*)"; }, true ); return signRE + numberRE; // String }; } if(!dojo._hasResource["dijit.ProgressBar"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dijit.ProgressBar"] = true; dojo.provide("dijit.ProgressBar"); dojo.declare("dijit.ProgressBar", [dijit._Widget, dijit._Templated], { // summary: // A progress indication widget, showing the amount completed // (often the percentage completed) of a task. // // example: // | <div dojoType="ProgressBar" // | places="0" // | value="..." maximum="..."> // | </div> // progress: [const] String (Percentage or Number) // Number or percentage indicating amount of task completed. // Deprecated. Use "value" instead. progress: "0", // value: String (Percentage or Number) // Number or percentage indicating amount of task completed. // With "%": percentage value, 0% <= progress <= 100%, or // without "%": absolute value, 0 <= progress <= maximum. // Infinity means that the progress bar is indeterminate. value: "", // maximum: [const] Float // Max sample number maximum: 100, // places: [const] Number // Number of places to show in values; 0 by default places: 0, // indeterminate: [const] Boolean // If false: show progress value (number or percentage). // If true: show that a process is underway but that the amount completed is unknown. // Deprecated. Use "value" instead. indeterminate: false, // label: String? // Label on progress bar. Defaults to percentage for determinate progress bar and // blank for indeterminate progress bar. label:"", // name: String // this is the field name (for a form) if set. This needs to be set if you want to use // this widget in a dijit.form.Form widget (such as dijit.Dialog) name: '', templateString: dojo.cache("dijit", "templates/ProgressBar.html", "<div class=\"dijitProgressBar dijitProgressBarEmpty\" role=\"progressbar\"\n\t><div dojoAttachPoint=\"internalProgress\" class=\"dijitProgressBarFull\"\n\t\t><div class=\"dijitProgressBarTile\" role=\"presentation\"></div\n\t\t><span style=\"visibility:hidden\"> </span\n\t></div\n\t><div dojoAttachPoint=\"labelNode\" 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"), postMixInProperties: function(){ this.inherited(arguments); if(!("value" in this.params)){ this.value = this.indeterminate ? Infinity : this.progress; } }, buildRendering: function(){ this.inherited(arguments); this.indeterminateHighContrastImage.setAttribute("src", this._indeterminateHighContrastImagePath.toString()); this.update(); }, update: function(/*Object?*/attributes){ // summary: // Internal method to change attributes of ProgressBar, similar to set(hash). Users should call // set("value", ...) rather than calling this method directly. // attributes: // May provide progress and/or maximum properties on this parameter; // see attribute specs for details. // example: // | myProgressBar.update({'indeterminate': true}); // | myProgressBar.update({'progress': 80}); // | myProgressBar.update({'indeterminate': true, label:"Loading ..." }) // tags: // private // TODO: deprecate this method and use set() instead dojo.mixin(this, attributes || {}); var tip = this.internalProgress, ap = this.domNode; var percent = 1; if(this.indeterminate){ dijit.removeWaiState(ap, "valuenow"); dijit.removeWaiState(ap, "valuemin"); dijit.removeWaiState(ap, "valuemax"); }else{ if(String(this.progress).indexOf("%") != -1){ percent = Math.min(parseFloat(this.progress)/100, 1); this.progress = percent * this.maximum; }else{ this.progress = Math.min(this.progress, this.maximum); percent = this.progress / this.maximum; } dijit.setWaiState(ap, "describedby", this.labelNode.id); dijit.setWaiState(ap, "valuenow", this.progress); dijit.setWaiState(ap, "valuemin", 0); dijit.setWaiState(ap, "valuemax", this.maximum); } this.labelNode.innerHTML = this.report(percent); dojo.toggleClass(this.domNode, "dijitProgressBarIndeterminate", this.indeterminate); tip.style.width = (percent * 100) + "%"; this.onChange(); }, _setValueAttr: function(v){ this._set("value", v); if(v == Infinity){ this.update({indeterminate:true}); }else{ this.update({indeterminate:false, progress:v}); } }, _setLabelAttr: function(label){ this._set("label", label); this.update(); }, _setIndeterminateAttr: function(indeterminate){ // Deprecated, use set("value", ...) instead this.indeterminate = indeterminate; this.update(); }, report: function(/*float*/percent){ // summary: // Generates message to show inside progress bar (normally indicating amount of task completed). // May be overridden. // tags: // extension return this.label ? this.label : (this.indeterminate ? " " : dojo.number.format(percent, { type: "percent", places: this.places, locale: this.lang })); }, onChange: function(){ // summary: // Callback fired when progress updates. // tags: // extension } }); } if(!dojo._hasResource["dijit.ToolbarSeparator"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dijit.ToolbarSeparator"] = true; dojo.provide("dijit.ToolbarSeparator"); dojo.declare("dijit.ToolbarSeparator", [ dijit._Widget, dijit._Templated ], { // summary: // A spacer between two `dijit.Toolbar` items templateString: '<div class="dijitToolbarSeparator dijitInline" role="presentation"></div>', buildRendering: function(){ this.inherited(arguments); dojo.setSelectable(this.domNode, false); }, isFocusable: function(){ // summary: // This widget isn't focusable, so pass along that fact. // tags: // protected return false; } }); } if(!dojo._hasResource["dijit.Toolbar"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dijit.Toolbar"] = true; dojo.provide("dijit.Toolbar"); // Note: require of ToolbarSeparator is for back-compat, remove for 2.0 dojo.declare("dijit.Toolbar", [dijit._Widget, dijit._Templated, dijit._KeyNavContainer], { // summary: // A Toolbar widget, used to hold things like `dijit.Editor` buttons templateString: '<div class="dijit" role="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.inherited(arguments); this.connectKeyNavHandlers( this.isLeftToRight() ? [dojo.keys.LEFT_ARROW] : [dojo.keys.RIGHT_ARROW], this.isLeftToRight() ? [dojo.keys.RIGHT_ARROW] : [dojo.keys.LEFT_ARROW] ); }, startup: function(){ if(this._started){ return; } this.startupKeyNavChildren(); this.inherited(arguments); } } ); } if(!dojo._hasResource["dojo.DeferredList"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dojo.DeferredList"] = true; dojo.provide("dojo.DeferredList"); dojo.DeferredList = function(/*Array*/ list, /*Boolean?*/ fireOnOneCallback, /*Boolean?*/ fireOnOneErrback, /*Boolean?*/ consumeErrors, /*Function?*/ canceller){ // summary: // Provides event handling for a group of Deferred objects. // description: // DeferredList takes an array of existing deferreds and returns a new deferred of its own // this new deferred will typically have its callback fired when all of the deferreds in // the given list have fired their own deferreds. The parameters `fireOnOneCallback` and // fireOnOneErrback, will fire before all the deferreds as appropriate // // list: // The list of deferreds to be synchronizied with this DeferredList // fireOnOneCallback: // Will cause the DeferredLists callback to be fired as soon as any // of the deferreds in its list have been fired instead of waiting until // the entire list has finished // fireonOneErrback: // Will cause the errback to fire upon any of the deferreds errback // canceller: // A deferred canceller function, see dojo.Deferred var resultList = []; dojo.Deferred.call(this); var self = this; if(list.length === 0 && !fireOnOneCallback){ this.resolve([0, []]); } var finished = 0; dojo.forEach(list, function(item, i){ item.then(function(result){ if(fireOnOneCallback){ self.resolve([i, result]); }else{ addResult(true, result); } },function(error){ if(fireOnOneErrback){ self.reject(error); }else{ addResult(false, error); } if(consumeErrors){ return null; } throw error; }); function addResult(succeeded, result){ resultList[i] = [succeeded, result]; finished++; if(finished === list.length){ self.resolve(resultList); } } }); }; dojo.DeferredList.prototype = new dojo.Deferred(); dojo.DeferredList.prototype.gatherResults= function(deferredList){ // summary: // Gathers the results of the deferreds for packaging // as the parameters to the Deferred Lists' callback var d = new dojo.DeferredList(deferredList, false, true, false); d.addCallback(function(results){ var ret = []; dojo.forEach(results, function(result){ ret.push(result[1]); }); return ret; }); return d; }; } if(!dojo._hasResource["dijit.tree.TreeStoreModel"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dijit.tree.TreeStoreModel"] = true; dojo.provide("dijit.tree.TreeStoreModel"); dojo.declare( "dijit.tree.TreeStoreModel", null, { // summary: // Implements dijit.Tree.model connecting to a store with a single // root item. Any methods passed into the constructor will override // the ones defined here. // store: dojo.data.Store // Underlying store store: null, // childrenAttrs: String[] // One or more attribute names (attributes in the dojo.data item) that specify that item's children childrenAttrs: ["children"], // newItemIdAttr: String // Name of attribute in the Object passed to newItem() that specifies the id. // // If newItemIdAttr is set then it's used when newItem() is called to see if an // item with the same id already exists, and if so just links to the old item // (so that the old item ends up with two parents). // // Setting this to null or "" will make every drop create a new item. newItemIdAttr: "id", // labelAttr: String // If specified, get label for tree node from this attribute, rather // than by calling store.getLabel() labelAttr: "", // root: [readonly] dojo.data.Item // Pointer to the root item (read only, not a parameter) root: null, // query: anything // Specifies datastore query to return the root item for the tree. // Must only return a single item. Alternately can just pass in pointer // to root item. // example: // | {id:'ROOT'} query: null, // deferItemLoadingUntilExpand: Boolean // Setting this to true will cause the TreeStoreModel to defer calling loadItem on nodes // until they are expanded. This allows for lazying loading where only one // loadItem (and generally one network call, consequently) per expansion // (rather than one for each child). // This relies on partial loading of the children items; each children item of a // fully loaded item should contain the label and info about having children. deferItemLoadingUntilExpand: false, constructor: function(/* Object */ args){ // summary: // Passed the arguments listed above (store, etc) // tags: // private dojo.mixin(this, args); this.connects = []; var store = this.store; if(!store.getFeatures()['dojo.data.api.Identity']){ throw new Error("dijit.Tree: store must support dojo.data.Identity"); } // if the store supports Notification, subscribe to the notification events if(store.getFeatures()['dojo.data.api.Notification']){ this.connects = this.connects.concat([ dojo.connect(store, "onNew", this, "onNewItem"), dojo.connect(store, "onDelete", this, "onDeleteItem"), dojo.connect(store, "onSet", this, "onSetItem") ]); } }, destroy: function(){ dojo.forEach(this.connects, dojo.disconnect); // TODO: should cancel any in-progress processing of getRoot(), getChildren() }, // ======================================================================= // Methods for traversing hierarchy getRoot: function(onItem, onError){ // summary: // Calls onItem with the root item for the tree, possibly a fabricated item. // Calls onError on error. if(this.root){ onItem(this.root); }else{ this.store.fetch({ query: this.query, onComplete: dojo.hitch(this, function(items){ if(items.length != 1){ throw new Error(this.declaredClass + ": query " + dojo.toJson(this.query) + " returned " + items.length + " items, but must return exactly one item"); } this.root = items[0]; onItem(this.root); }), onError: onError }); } }, mayHaveChildren: function(/*dojo.data.Item*/ item){ // summary: // Tells if an item has or may have children. Implementing logic here // avoids showing +/- expando icon for nodes that we know don't have children. // (For efficiency reasons we may not want to check if an element actually // has children until user clicks the expando node) return dojo.some(this.childrenAttrs, function(attr){ return this.store.hasAttribute(item, attr); }, this); }, getChildren: function(/*dojo.data.Item*/ parentItem, /*function(items)*/ onComplete, /*function*/ onError){ // summary: // Calls onComplete() with array of child items of given parent item, all loaded. var store = this.store; if(!store.isItemLoaded(parentItem)){ // The parent is not loaded yet, we must be in deferItemLoadingUntilExpand // mode, so we will load it and just return the children (without loading each // child item) var getChildren = dojo.hitch(this, arguments.callee); store.loadItem({ item: parentItem, onItem: function(parentItem){ getChildren(parentItem, onComplete, onError); }, onError: onError }); return; } // get children of specified item var childItems = []; for(var i=0; i<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]}, LnewItem; 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. LnewItem=this.store.newItem(args, pInfo); if (LnewItem && (insertIndex!=undefined)){ // Move new item to desired position this.pasteItem(LnewItem, parent, parent, false, insertIndex); } } }}); }else{ // [as far as we know] there is no id so we must assume this is a new item LnewItem=this.store.newItem(args, pInfo); if (LnewItem && (insertIndex!=undefined)){ // Move new item to desired position this.pasteItem(LnewItem, parent, parent, false, insertIndex); } } }, 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 a dijit.Tree and a dojo.data store that doesn't have a root item, // a.k.a. a store that has multiple "top level" items. // // description // Use this class to wrap a dojo.data 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. // This class allows dijit.Tree to assume a single root item, even if the store doesn't have one. // // When using this class the developer must override a number of methods according to their app and // data, including: // - onNewRootItem // - onAddToRoot // - onLeaveRoot // - onNewItem // - onSetItem // 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). // // If developers can detect which items are possible top level items (based on the item and the // parentInfo parameters), they should override this method to only call _requeryTop() for top // level items. 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); }, onSetItem: function(/* item */ item, /* attribute-name-string */ attribute, /* object | array */ oldValue, /* object | array */ newValue){ // summary: // Updates the tree view according to changes to an item in the data store. // Developers should override this method to be more efficient based on their app/data. // description: // Handles updates to an item's children by calling onChildrenChange(), and // other updates to an item by calling onChange(). // // Also, any change to any item re-executes the query for the tree's top-level items, // since this modified item may have started/stopped matching the query for top level items. // // If possible, developers should override this function to only call _requeryTop() when // the change to the item has caused it to stop/start being a top level item in the tree. // tags: // extension this._requeryTop(); this.inherited(arguments); } }); } 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.replaceClass(this.node, prefix + newState, prefix + this[state]); this[state] = newState; }, _addItemClass: function(node, type){ // summary: // adds a class with prefix "dojoDndItem" // node: Node // a node // type: String // a variable suffix for a class name dojo.addClass(node, "dojoDndItem" + type); }, _removeItemClass: function(node, type){ // summary: // removes a class with prefix "dojoDndItem" // node: Node // a node // type: String // a variable suffix for a class name dojo.removeClass(node, "dojoDndItem" + type); }, _getChildByEvent: function(e){ // summary: // gets a child, which is under the mouse at the moment, or null // e: Event // a mouse event var node = e.target; if(node){ for(var parent = node.parentNode; parent; node = parent, parent = node.parentNode){ if(parent == this.parent && dojo.hasClass(node, "dojoDndItem")){ return node; } } } return null; }, _normalizedCreator: function(/*dojo.dnd.Item*/ item, /*String*/ hint){ // summary: // adds all necessary data to the output of the user-supplied creator function var t = (this.creator || this.defaultCreator).call(this, item, hint); if(!dojo.isArray(t.type)){ t.type = ["text"]; } if(!t.node.id){ t.node.id = dojo.dnd.getUniqueId(); } dojo.addClass(t.node, "dojoDndItem"); return t; } }); dojo.dnd._createNode = function(tag){ // summary: // returns a function, which creates an element of given tag // (SPAN by default) and sets its innerHTML to given text // tag: String // a tag name or empty for SPAN if(!tag){ return dojo.dnd._createSpan; } return function(text){ // Function return dojo.create(tag, {innerHTML: text}); // Node }; }; dojo.dnd._createTrTd = function(text){ // summary: // creates a TR/TD structure with given text as an innerHTML of TD // text: String // a text for TD var tr = dojo.create("tr"); dojo.create("td", {innerHTML: text}, tr); return tr; // Node }; dojo.dnd._createSpan = function(text){ // summary: // creates a SPAN element with given text as its innerHTML // text: String // a text for SPAN return dojo.create("span", {innerHTML: text}); // Node }; // dojo.dnd._defaultCreatorNodes: Object // a dictionary that maps container tag names to child tag names dojo.dnd._defaultCreatorNodes = {ul: "li", ol: "li", div: "div", p: "div"}; dojo.dnd._defaultCreator = function(node){ // summary: // takes a parent node, and returns an appropriate creator function // node: Node // a container node var tag = node.tagName.toLowerCase(); var c = tag == "tbody" || tag == "thead" ? dojo.dnd._createTrTd : dojo.dnd._createNode(dojo.dnd._defaultCreatorNodes[tag]); return function(item, hint){ // Function var isObj = item && dojo.isObject(item), data, type, n; if(isObj && item.tagName && item.nodeType && item.getAttribute){ // process a DOM node data = item.getAttribute("dndData") || item.innerHTML; type = item.getAttribute("dndType"); type = type ? type.split(/\s*,\s*/) : ["text"]; n = item; // this node is going to be moved rather than copied }else{ // process a DnD item object or a string data = (isObj && item.data) ? item.data : item; type = (isObj && item.type) ? item.type : ["text"]; n = (hint == "avatar" ? dojo.dnd._createSpan : c)(String(data)); } if(!n.id){ n.id = dojo.dnd.getUniqueId(); } return {node: n, data: data, type: type}; }; }; } if(!dojo._hasResource["dijit.tree._dndContainer"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dijit.tree._dndContainer"] = true; dojo.provide("dijit.tree._dndContainer"); dojo.getObject("tree", true, dojo); dijit.tree._compareNodes = function(n1, n2){ if(n1 === n2){ return 0; } if('sourceIndex' in document.documentElement){ //IE //TODO: does not yet work if n1 and/or n2 is a text node return n1.sourceIndex - n2.sourceIndex; }else if('compareDocumentPosition' in document.documentElement){ //FF, Opera return n1.compareDocumentPosition(n2) & 2 ? 1: -1; }else if(document.createRange){ //Webkit var r1 = doc.createRange(); r1.setStartBefore(n1); var r2 = doc.createRange(); r2.setStartBefore(n2); return r1.compareBoundaryPoints(r1.END_TO_END, r2); }else{ throw Error("dijit.tree._compareNodes don't know how to compare two different nodes in this browser"); } }; dojo.declare("dijit.tree._dndContainer", null, { // summary: // This is a base class for `dijit.tree._dndSelector`, and isn't meant to be used directly. // It's modeled after `dojo.dnd.Container`. // tags: // protected /*===== // current: DomNode // The currently hovered TreeNode.rowNode (which is the DOM node // associated w/a given node in the tree, excluding it's descendants) current: null, =====*/ constructor: function(tree, params){ // summary: // A constructor of the Container // tree: Node // Node or node's id to build the container on // params: dijit.tree.__SourceArgs // A dict of parameters, which gets mixed into the object // tags: // private this.tree = tree; this.node = tree.domNode; // TODO: rename; it's not a TreeNode but the whole Tree dojo.mixin(this, params); // class-specific variables this.map = {}; this.current = null; // current TreeNode's DOM node // states this.containerState = ""; dojo.addClass(this.node, "dojoDndContainer"); // set up events this.events = [ // container level events dojo.connect(this.node, "onmouseenter", this, "onOverEvent"), dojo.connect(this.node, "onmouseleave", this, "onOutEvent"), // switching between TreeNodes dojo.connect(this.tree, "_onNodeMouseEnter", this, "onMouseOver"), dojo.connect(this.tree, "_onNodeMouseLeave", this, "onMouseOut"), // cancel text selection and text dragging dojo.connect(this.node, "ondragstart", dojo, "stopEvent"), dojo.connect(this.node, "onselectstart", dojo, "stopEvent") ]; }, getItem: function(/*String*/ key){ // summary: // Returns the dojo.dnd.Item (representing a dragged node) by it's key (id). // Called by dojo.dnd.Source.checkAcceptance(). // tags: // protected var widget = this.selection[key], ret = { data: widget, type: ["treeNode"] }; return ret; // dojo.dnd.Item }, destroy: function(){ // summary: // Prepares this object to be garbage-collected dojo.forEach(this.events, dojo.disconnect); // this.clearItems(); this.node = this.parent = null; }, // mouse events onMouseOver: function(/*TreeNode*/ widget, /*Event*/ evt){ // summary: // Called when mouse is moved over a TreeNode // tags: // protected this.current = widget; }, onMouseOut: function(/*TreeNode*/ widget, /*Event*/ evt){ // summary: // Called when mouse is moved away from a TreeNode // tags: // protected this.current = null; }, _changeState: function(type, newState){ // summary: // Changes a named state to new state value // type: String // A name of the state to change // newState: String // new state var prefix = "dojoDnd" + type; var state = type.toLowerCase() + "State"; //dojo.replaceClass(this.node, prefix + newState, prefix + this[state]); dojo.replaceClass(this.node, prefix + newState, prefix + this[state]); this[state] = newState; }, _addItemClass: function(node, type){ // summary: // Adds a class with prefix "dojoDndItem" // node: Node // A node // type: String // A variable suffix for a class name dojo.addClass(node, "dojoDndItem" + type); }, _removeItemClass: function(node, type){ // summary: // Removes a class with prefix "dojoDndItem" // node: Node // A node // type: String // A variable suffix for a class name dojo.removeClass(node, "dojoDndItem" + type); }, onOverEvent: function(){ // summary: // This function is called once, when mouse is over our container // tags: // protected this._changeState("Container", "Over"); }, onOutEvent: function(){ // summary: // This function is called once, when mouse is out of our container // tags: // protected this._changeState("Container", ""); } }); } if(!dojo._hasResource["dijit.tree._dndSelector"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dijit.tree._dndSelector"] = true; dojo.provide("dijit.tree._dndSelector"); dojo.declare("dijit.tree._dndSelector", dijit.tree._dndContainer, { // summary: // This is a base class for `dijit.tree.dndSource` , and isn't meant to be used directly. // It's based on `dojo.dnd.Selector`. // tags: // protected /*===== // selection: Hash<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; dijit.setWaiState(this.tree.domNode, "multiselect", !this.singular); this.events.push( dojo.connect(this.tree.domNode, "onmousedown", this,"onMouseDown"), dojo.connect(this.tree.domNode, "onmouseup", this,"onMouseUp"), dojo.connect(this.tree.domNode, "onmousemove", this,"onMouseMove") ); }, // singular: Boolean // Allows selection of only one element, if true. // Tree hasn't been tested in singular=true mode, unclear if it works. singular: false, // methods getSelectedTreeNodes: function(){ // summary: // Returns a list of selected node(s). // Used by dndSource on the start of a drag. // tags: // protected var nodes=[], sel = this.selection; for(var i in sel){ nodes.push(sel[i]); } return nodes; }, selectNone: function(){ // summary: // Unselects all items // tags: // private this.setSelection([]); return this; // self }, destroy: function(){ // summary: // Prepares the object to be garbage-collected this.inherited(arguments); this.selection = this.anchor = null; }, addTreeNode: function(/*dijit._TreeNode*/node, /*Boolean?*/isAnchor){ // summary // add node to current selection // node: Node // node to add // isAnchor: Boolean // Whether the node should become anchor. this.setSelection(this.getSelectedTreeNodes().concat( [node] )); if(isAnchor){ this.anchor = node; } return node; }, removeTreeNode: function(/*dijit._TreeNode*/node){ // summary // remove node from current selection // node: Node // node to remove this.setSelection(this._setDifference(this.getSelectedTreeNodes(), [node])) return node; }, isTreeNodeSelected: function(/*dijit._TreeNode*/node){ // summary // return true if node is currently selected // node: Node // the node to check whether it's in the current selection return node.id && !!this.selection[node.id]; }, setSelection: function(/*dijit._treeNode[]*/ newSelection){ // summary // set the list of selected nodes to be exactly newSelection. All changes to the // selection should be passed through this function, which ensures that derived // attributes are kept up to date. Anchor will be deleted if it has been removed // from the selection, but no new anchor will be added by this function. // newSelection: Node[] // list of tree nodes to make selected var oldSelection = this.getSelectedTreeNodes(); dojo.forEach(this._setDifference(oldSelection, newSelection), dojo.hitch(this, function(node){ node.setSelected(false); if(this.anchor == node){ delete this.anchor; } delete this.selection[node.id]; })); dojo.forEach(this._setDifference(newSelection, oldSelection), dojo.hitch(this, function(node){ node.setSelected(true); this.selection[node.id] = node; })); this._updateSelectionProperties(); }, _setDifference: function(xs,ys){ // summary // Returns a copy of xs which lacks any objects // occurring in ys. Checks for membership by // modifying and then reading the object, so it will // not properly handle sets of numbers or strings. dojo.forEach(ys, function(y){ y.__exclude__ = true; }); var ret = dojo.filter(xs, function(x){ return !x.__exclude__; }); // clean up after ourselves. dojo.forEach(ys, function(y){ delete y['__exclude__'] }); return ret; }, _updateSelectionProperties: function() { // summary // Update the following tree properties from the current selection: // path[s], selectedItem[s], selectedNode[s] var selected = this.getSelectedTreeNodes(); var paths = [], nodes = []; dojo.forEach(selected, function(node) { nodes.push(node); paths.push(node.getTreePath()); }); var items = dojo.map(nodes,function(node) { return node.item; }); this.tree._set("paths", paths); this.tree._set("path", paths[0] || []); this.tree._set("selectedNodes", nodes); this.tree._set("selectedNode", nodes[0] || null); this.tree._set("selectedItems", items); this.tree._set("selectedItem", items[0] || null); }, // mouse events onMouseDown: function(e){ // summary: // Event processor for onmousedown // e: Event // mouse event // tags: // protected // ignore click on expando node if(!this.current || this.tree.isExpandoNode( e.target, this.current)){ return; } if(e.button == dojo.mouseButtons.RIGHT){ return; } // ignore right-click dojo.stopEvent(e); var treeNode = this.current, copy = dojo.isCopyKey(e), id = treeNode.id; // if shift key is not pressed, and the node is already in the selection, // delay deselection until onmouseup so in the case of DND, deselection // will be canceled by onmousemove. if(!this.singular && !e.shiftKey && this.selection[id]){ this._doDeselect = true; return; }else{ this._doDeselect = false; } this.userSelect(treeNode, copy, e.shiftKey); }, onMouseUp: function(e){ // summary: // Event processor for onmouseup // e: Event // mouse event // tags: // protected // _doDeselect is the flag to indicate that the user wants to either ctrl+click on // a already selected item (to deselect the item), or click on a not-yet selected item // (which should remove all current selection, and add the clicked item). This can not // be done in onMouseDown, because the user may start a drag after mousedown. By moving // the deselection logic here, the user can drags an already selected item. if(!this._doDeselect){ return; } this._doDeselect = false; this.userSelect(this.current, dojo.isCopyKey( e ), e.shiftKey); }, onMouseMove: function(e){ // summary // event processor for onmousemove // e: Event // mouse event this._doDeselect = false; }, userSelect: function(node, multi, range){ // summary: // Add or remove the given node from selection, responding // to a user action such as a click or keypress. // multi: Boolean // Indicates whether this is meant to be a multi-select action (e.g. ctrl-click) // range: Boolean // Indicates whether this is meant to be a ranged action (e.g. shift-click) // tags: // protected if(this.singular){ if(this.anchor == node && multi){ this.selectNone(); }else{ this.setSelection([node]); this.anchor = node; } }else{ if(range && this.anchor){ var cr = dijit.tree._compareNodes(this.anchor.rowNode, node.rowNode), begin, end, anchor = this.anchor; if(cr < 0){ //current is after anchor begin = anchor; end = node; }else{ //current is before anchor begin = node; end = anchor; } nodes = []; //add everything betweeen begin and end inclusively while(begin != end) { nodes.push(begin) begin = this.tree._getNextNode(begin); } nodes.push(end) this.setSelection(nodes); }else{ if( this.selection[ node.id ] && multi ) { this.removeTreeNode( node ); } else if(multi) { this.addTreeNode(node, true); } else { this.setSelection([node]); this.anchor = node; } } } }, forInSelectedItems: function(/*Function*/ f, /*Object?*/ o){ // summary: // Iterates over selected items; // see `dojo.dnd.Container.forInItems()` for details o = o || dojo.global; for(var id in this.selection){ // console.log("selected item id: " + id); f.call(o, this.getItem(id), id, this); } } }); } if(!dojo._hasResource["dijit.Tree"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dijit.Tree"] = true; dojo.provide("dijit.Tree"); dojo.declare( "dijit._TreeNode", [dijit._Widget, dijit._Templated, dijit._Container, dijit._Contained, dijit._CssStateMixin], { // summary: // Single node within a tree. This class is used internally // by Tree and should not be accessed directly. // tags: // private // item: [const] dojo.data.Item // the dojo.data entry this tree represents item: null, // isTreeNode: [protected] Boolean // Indicates that this is a TreeNode. Used by `dijit.Tree` only, // should not be accessed directly. isTreeNode: true, // label: String // Text of this tree node label: "", // isExpandable: [private] Boolean // This node has children, so show the expando node (+ sign) isExpandable: null, // isExpanded: [readonly] Boolean // This node is currently expanded (ie, opened) isExpanded: false, // state: [private] String // Dynamic loading-related stuff. // When an empty folder node appears, it is "UNCHECKED" first, // then after dojo.data query it becomes "LOADING" and, finally "LOADED" state: "UNCHECKED", templateString: dojo.cache("dijit", "templates/TreeNode.html", "<div class=\"dijitTreeNode\" role=\"presentation\"\n\t><div dojoAttachPoint=\"rowNode\" class=\"dijitTreeRow\" role=\"presentation\" dojoAttachEvent=\"onmouseenter:_onMouseEnter, onmouseleave:_onMouseLeave, onclick:_onClick, ondblclick:_onDblClick\"\n\t\t><img src=\"${_blankGif}\" alt=\"\" dojoAttachPoint=\"expandoNode\" class=\"dijitTreeExpando\" role=\"presentation\"\n\t\t/><span dojoAttachPoint=\"expandoNodeText\" class=\"dijitExpandoText\" role=\"presentation\"\n\t\t></span\n\t\t><span dojoAttachPoint=\"contentNode\"\n\t\t\tclass=\"dijitTreeContent\" role=\"presentation\">\n\t\t\t<img src=\"${_blankGif}\" alt=\"\" dojoAttachPoint=\"iconNode\" class=\"dijitIcon dijitTreeIcon\" role=\"presentation\"\n\t\t\t/><span dojoAttachPoint=\"labelNode\" class=\"dijitTreeLabel\" role=\"treeitem\" tabindex=\"-1\" aria-selected=\"false\" dojoAttachEvent=\"onfocus:_onLabelFocus\"></span>\n\t\t</span\n\t></div>\n\t<div dojoAttachPoint=\"containerNode\" class=\"dijitTreeContainer\" role=\"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"} }), buildRendering: function(){ this.inherited(arguments); // set expand icon for leaf this._setExpando(); // set icon and label class based on item this._updateItemClasses(this.item); if(this.isExpandable){ dijit.setWaiState(this.labelNode, "expanded", this.isExpanded); } //aria-selected should be false on all selectable elements. this.setSelected(false); }, _setIndentAttr: function(indent){ // summary: // Tell this node how many levels it should be indented // description: // 0 for top level nodes, 1 for their children, 2 for their // grandchildren, etc. // Math.max() is to prevent negative padding on hidden root node (when indent == -1) var pixels = (Math.max(indent, 0) * this.tree._nodePixelIndent) + "px"; dojo.style(this.domNode, "backgroundPosition", pixels + " 0px"); dojo.style(this.rowNode, this.isLeftToRight() ? "paddingLeft" : "paddingRight", pixels); dojo.forEach(this.getChildren(), function(child){ child.set("indent", indent+1); }); this._set("indent", indent); }, markProcessing: function(){ // summary: // Visually denote that tree is loading data, etc. // tags: // private this.state = "LOADING"; this._setExpando(true); }, unmarkProcessing: function(){ // summary: // Clear markup from markProcessing() call // tags: // private this._setExpando(false); }, _updateItemClasses: function(item){ // summary: // Set appropriate CSS classes for icon and label dom node // (used to allow for item updates to change respective CSS) // tags: // private var tree = this.tree, model = tree.model; if(tree._v10Compat && item === model.root){ // For back-compat with 1.0, need to use null to specify root item (TODO: remove in 2.0) item = null; } this._applyClassAndStyle(item, "icon", "Icon"); this._applyClassAndStyle(item, "label", "Label"); this._applyClassAndStyle(item, "row", "Row"); }, _applyClassAndStyle: function(item, lower, upper){ // summary: // Set the appropriate CSS classes and styles for labels, icons and rows. // // item: // The data item. // // lower: // The lower case attribute to use, e.g. 'icon', 'label' or 'row'. // // upper: // The upper case attribute to use, e.g. 'Icon', 'Label' or 'Row'. // // tags: // private var clsName = "_" + lower + "Class"; var nodeName = lower + "Node"; var oldCls = this[clsName]; this[clsName] = this.tree["get" + upper + "Class"](item, this.isExpanded); dojo.replaceClass(this[nodeName], this[clsName] || "", oldCls || ""); dojo.style(this[nodeName], this.tree["get" + upper + "Style"](item, this.isExpanded) || {}); }, _updateLayout: function(){ // summary: // Set appropriate CSS classes for this.domNode // tags: // private var parent = this.getParent(); if(!parent || parent.rowNode.style.display == "none"){ /* if we are hiding the root node then make every first level child look like a root node */ dojo.addClass(this.domNode, "dijitTreeIsRoot"); }else{ dojo.toggleClass(this.domNode, "dijitTreeIsLast", !this.getNextSibling()); } }, _setExpando: function(/*Boolean*/ processing){ // summary: // Set the right image for the expando node // tags: // private var styles = ["dijitTreeExpandoLoading", "dijitTreeExpandoOpened", "dijitTreeExpandoClosed", "dijitTreeExpandoLeaf"], _a11yStates = ["*","-","+","*"], idx = processing ? 0 : (this.isExpandable ? (this.isExpanded ? 1 : 2) : 3); // apply the appropriate class to the expando node dojo.replaceClass(this.expandoNode, styles[idx], styles); // provide a non-image based indicator for images-off mode this.expandoNodeText.innerHTML = _a11yStates[idx]; }, expand: function(){ // summary: // Show my children // returns: // Deferred that fires when expansion is complete // If there's already an expand in progress or we are already expanded, just return if(this._expandDeferred){ return this._expandDeferred; // dojo.Deferred } // cancel in progress collapse operation this._wipeOut && this._wipeOut.stop(); // All the state information for when a node is expanded, maybe this should be // set when the animation completes instead this.isExpanded = true; dijit.setWaiState(this.labelNode, "expanded", "true"); if(this.tree.showRoot || this !== this.tree.rootNode){ dijit.setWaiRole(this.containerNode, "group"); } dojo.addClass(this.contentNode,'dijitTreeContentExpanded'); this._setExpando(); this._updateItemClasses(this.item); if(this == this.tree.rootNode){ dijit.setWaiState(this.tree.domNode, "expanded", "true"); } var def, wipeIn = dojo.fx.wipeIn({ node: this.containerNode, duration: dijit.defaultDuration, onEnd: function(){ def.callback(true); } }); // Deferred that fires when expand is complete def = (this._expandDeferred = new dojo.Deferred(function(){ // Canceller wipeIn.stop(); })); wipeIn.play(); return def; // dojo.Deferred }, collapse: function(){ // summary: // Collapse this node (if it's expanded) if(!this.isExpanded){ return; } // cancel in progress expand operation if(this._expandDeferred){ this._expandDeferred.cancel(); delete this._expandDeferred; } this.isExpanded = false; dijit.setWaiState(this.labelNode, "expanded", "false"); if(this == this.tree.rootNode){ dijit.setWaiState(this.tree.domNode, "expanded", "false"); } dojo.removeClass(this.contentNode,'dijitTreeContentExpanded'); this._setExpando(); this._updateItemClasses(this.item); if(!this._wipeOut){ this._wipeOut = dojo.fx.wipeOut({ node: this.containerNode, duration: dijit.defaultDuration }); } this._wipeOut.play(); }, // indent: Integer // Levels from this node to the root node indent: 0, setChildItems: function(/* Object[] */ items){ // summary: // Sets the child items of this node, removing/adding nodes // from current children to match specified items[] array. // Also, if this.persist == true, expands any children that were previously // opened. // returns: // Deferred object that fires after all previously opened children // have been expanded again (or fires instantly if there are no such children). var tree = this.tree, model = tree.model, defs = []; // list of deferreds that need to fire before I am complete // Orphan all my existing children. // If items contains some of the same items as before then we will reattach them. // Don't call this.removeChild() because that will collapse the tree etc. dojo.forEach(this.getChildren(), function(child){ dijit._Container.prototype.removeChild.call(this, child); }, this); this.state = "LOADED"; if(items && items.length > 0){ this.isExpandable = true; // Create _TreeNode widget for each specified tree node, unless one already // exists and isn't being used (presumably it's from a DnD move and was recently // released dojo.forEach(items, function(item){ var id = model.getIdentity(item), existingNodes = tree._itemNodesMap[id], node; if(existingNodes){ for(var i=0;i<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 }, getTreePath: function(){ var node = this; var path = []; while(node && node !== this.tree.rootNode){ path.unshift(node.item); node = node.getParent(); } path.unshift(this.tree.rootNode.item); return path; }, getIdentity: function() { return this.tree.model.getIdentity(this.item); }, removeChild: function(/* treeNode */ node){ this.inherited(arguments); var children = this.getChildren(); if(children.length == 0){ this.isExpandable = false; this.collapse(); } dojo.forEach(children, function(child){ child._updateLayout(); }); }, makeExpandable: function(){ // summary: // if this node wasn't already showing the expando node, // turn it into one and call _setExpando() // TODO: hmm this isn't called from anywhere, maybe should remove it for 2.0 this.isExpandable = true; this._setExpando(false); }, _onLabelFocus: function(evt){ // summary: // Called when this row is focused (possibly programatically) // Note that we aren't using _onFocus() builtin to dijit // because it's called when focus is moved to a descendant TreeNode. // tags: // private this.tree._onNodeFocus(this); }, setSelected: function(/*Boolean*/ selected){ // summary: // A Tree has a (single) currently selected node. // Mark that this node is/isn't that currently selected node. // description: // In particular, setting a node as selected involves setting tabIndex // so that when user tabs to the tree, focus will go to that node (only). dijit.setWaiState(this.labelNode, "selected", selected); dojo.toggleClass(this.rowNode, "dijitTreeRowSelected", selected); }, setFocusable: function(/*Boolean*/ selected){ // summary: // A Tree has a (single) node that's focusable. // Mark that this node is/isn't that currently focsuable node. // description: // In particular, setting a node as selected involves setting tabIndex // so that when user tabs to the tree, focus will go to that node (only). this.labelNode.setAttribute("tabIndex", selected ? "0" : "-1"); }, _onClick: function(evt){ // summary: // Handler for onclick event on a node // tags: // private this.tree._onClick(this, evt); }, _onDblClick: function(evt){ // summary: // Handler for ondblclick event on a node // tags: // private this.tree._onDblClick(this, evt); }, _onMouseEnter: function(evt){ // summary: // Handler for onmouseenter event on a node // tags: // private this.tree._onNodeMouseEnter(this, evt); }, _onMouseLeave: function(evt){ // summary: // Handler for onmouseenter event on a node // tags: // private this.tree._onNodeMouseLeave(this, evt); } }); dojo.declare( "dijit.Tree", [dijit._Widget, dijit._Templated], { // summary: // This widget displays hierarchical data from a store. // store: [deprecated] String||dojo.data.Store // Deprecated. Use "model" parameter instead. // The store to get data to display in the tree. store: null, // model: dijit.Tree.model // Interface to read tree data, get notifications of changes to tree data, // and for handling drop operations (i.e drag and drop onto the tree) model: null, // query: [deprecated] anything // Deprecated. User should specify query to the model directly instead. // Specifies datastore query to return the root item or top items for the tree. query: null, // label: [deprecated] String // Deprecated. Use dijit.tree.ForestStoreModel directly instead. // Used in conjunction with query parameter. // If a query is specified (rather than a root node id), and a label is also specified, // then a fake root node is created and displayed, with this label. label: "", // showRoot: [const] Boolean // Should the root node be displayed, or hidden? showRoot: true, // childrenAttr: [deprecated] String[] // Deprecated. This information should be specified in the model. // One ore more attributes that holds children of a tree node childrenAttr: ["children"], // paths: String[][] or Item[][] // Full paths from rootNode to selected nodes expressed as array of items or array of ids. // Since setting the paths may be asynchronous (because ofwaiting on dojo.data), set("paths", ...) // returns a Deferred to indicate when the set is complete. paths: [], // path: String[] or Item[] // Backward compatible singular variant of paths. path: [], // selectedItems: [readonly] Item[] // The currently selected items in this tree. // This property can only be set (via set('selectedItems', ...)) when that item is already // visible in the tree. (I.e. the tree has already been expanded to show that node.) // Should generally use `paths` attribute to set the selected items instead. selectedItems: null, // selectedItem: [readonly] Item // Backward compatible singular variant of selectedItems. selectedItem: null, // openOnClick: Boolean // If true, clicking a folder node's label will open it, rather than calling onClick() openOnClick: false, // openOnDblClick: Boolean // If true, double-clicking a folder node's label will open it, rather than calling onDblClick() openOnDblClick: false, templateString: dojo.cache("dijit", "templates/Tree.html", "<div class=\"dijitTree dijitTreeContainer\" role=\"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". // Default of "dijit.tree._dndSelector" handles selection only (no actual DnD). dndController: "dijit.tree._dndSelector", // parameters to pull off of the tree and pass on to the dndController as its params dndParams: ["onDndDrop","itemCreator","onDndCancel","checkAcceptance", "checkItemAcceptance", "dragThreshold", "betweenThreshold"], //declare the above items so they can be pulled from the tree's markup // onDndDrop: [protected] Function // Parameter to dndController, see `dijit.tree.dndSource.onDndDrop`. // Generally this doesn't need to be set. onDndDrop: null, /*===== itemCreator: function(nodes, target, source){ // summary: // Returns objects passed to `Tree.model.newItem()` based on DnD nodes // dropped onto the tree. Developer must override this method to enable // dropping from external sources onto this Tree, unless the Tree.model's items // happen to look like {id: 123, name: "Apple" } with no other attributes. // description: // For each node in nodes[], which came from source, create a hash of name/value // pairs to be passed to Tree.model.newItem(). Returns array of those hashes. // nodes: DomNode[] // The DOMNodes dragged from the source container // target: DomNode // The target TreeNode.rowNode // source: dojo.dnd.Source // The source container the nodes were dragged from, perhaps another Tree or a plain dojo.dnd.Source // returns: Object[] // Array of name/value hashes for each new item to be added to the Tree, like: // | [ // | { id: 123, label: "apple", foo: "bar" }, // | { id: 456, label: "pear", zaz: "bam" } // | ] // tags: // extension return [{}]; }, =====*/ itemCreator: null, // onDndCancel: [protected] Function // Parameter to dndController, see `dijit.tree.dndSource.onDndCancel`. // Generally this doesn't need to be set. onDndCancel: null, /*===== checkAcceptance: function(source, nodes){ // summary: // Checks if the Tree itself can accept nodes from this source // source: dijit.tree._dndSource // The source which provides items // nodes: DOMNode[] // Array of DOM nodes corresponding to nodes being dropped, dijitTreeRow nodes if // source is a dijit.Tree. // tags: // extension return true; // Boolean }, =====*/ checkAcceptance: null, /*===== checkItemAcceptance: function(target, source, position){ // summary: // Stub function to be overridden if one wants to check for the ability to drop at the node/item level // description: // In the base case, this is called to check if target can become a child of source. // When betweenThreshold is set, position="before" or "after" means that we // are asking if the source node can be dropped before/after the target node. // target: DOMNode // The dijitTreeRoot DOM node inside of the TreeNode that we are dropping on to // Use dijit.getEnclosingWidget(target) to get the TreeNode. // source: dijit.tree.dndSource // The (set of) nodes we are dropping // position: String // "over", "before", or "after" // tags: // extension return true; // Boolean }, =====*/ checkItemAcceptance: null, // dragThreshold: Integer // Number of pixels mouse moves before it's considered the start of a drag operation dragThreshold: 5, // betweenThreshold: Integer // Set to a positive value to allow drag and drop "between" nodes. // // If during DnD mouse is over a (target) node but less than betweenThreshold // pixels from the bottom edge, dropping the the dragged node will make it // the next sibling of the target node, rather than the child. // // Similarly, if mouse is over a target node but less that betweenThreshold // pixels from the top edge, dropping the dragged node will make it // the target node's previous sibling rather than the target node's child. betweenThreshold: 0, // _nodePixelIndent: Integer // Number of pixels to indent tree nodes (relative to parent node). // Default is 19 but can be overridden by setting CSS class dijitTreeIndent // and calling resize() or startup() on tree after it's in the DOM. _nodePixelIndent: 19, _publish: function(/*String*/ topicName, /*Object*/ message){ // summary: // Publish a message for this widget/topic dojo.publish(this.id, [dojo.mixin({tree: this, event: topicName}, message || {})]); }, postMixInProperties: function(){ this.tree = this; if(this.autoExpand){ // There's little point in saving opened/closed state of nodes for a Tree // that initially opens all it's nodes. this.persist = false; } this._itemNodesMap={}; if(!this.cookieName){ this.cookieName = this.id + "SaveStateCookie"; } this._loadDeferred = new dojo.Deferred(); this.inherited(arguments); }, postCreate: function(){ this._initState(); // Create glue between store and Tree, if not specified directly by user if(!this.model){ this._store2model(); } // monitor changes to items this.connect(this.model, "onChange", "_onItemChange"); this.connect(this.model, "onChildrenChange", "_onItemChildrenChange"); this.connect(this.model, "onDelete", "_onItemDelete"); this._load(); this.inherited(arguments); if(this.dndController){ if(dojo.isString(this.dndController)){ this.dndController = dojo.getObject(this.dndController); } var params={}; for(var i=0; i<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"; // if root is not visible, move tree role to the invisible // root node's containerNode, see #12135 dijit.setWaiRole(this.domNode, 'presentation'); dijit.setWaiRole(rn.labelNode, 'presentation'); dijit.setWaiRole(rn.containerNode, 'tree'); } 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){ this.set('selectedItems', [item]); }, _setSelectedItemsAttr: function(/*dojo.data.Items or ids*/ items){ // summary: // Select tree nodes related to passed items. // WARNING: if model use multi-parented items or desired tree node isn't already loaded // behavior is undefined. Use set('paths', ...) instead. var tree = this; this._loadDeferred.addCallback( dojo.hitch(this, function(){ var identities = dojo.map(items, function(item){ return (!item || dojo.isString(item)) ? item : tree.model.getIdentity(item); }); var nodes = []; dojo.forEach(identities, function(id){ nodes = nodes.concat(tree._itemNodesMap[id] || []); }); this.set('selectedNodes', nodes); })); }, _setPathAttr: function(/*Item[] || String[]*/ path){ // summary: // Singular variant of _setPathsAttr if(path.length) { return this.set("paths", [path]); } else { //Empty list is interpreted as "select nothing" return this.set("paths", []); } }, _setPathsAttr: function(/*Item[][] || String[][]*/ paths){ // summary: // Select the tree nodes identified by passed paths. // paths: // Array of arrays of items or item id's // returns: // Deferred to indicate when the set is complete var tree = this; // We may need to wait for some nodes to expand, so setting // each path will involve a Deferred. We bring those deferreds // together witha DeferredList. return new dojo.DeferredList(dojo.map(paths, function(path){ var d = new dojo.Deferred(); // normalize path to use identity path = dojo.map(path, function(item){ return dojo.isString(item) ? item : tree.model.getIdentity(item); }); if(path.length){ // Wait for the tree to load, if it hasn't already. tree._loadDeferred.addCallback(function(){ selectPath(path, [tree.rootNode], d); }); }else{ d.errback("Empty path"); } return d; })).addCallback(setNodes); function selectPath(path, nodes, def){ // Traverse path; the next path component should be among "nodes". var nextPath = path.shift(); var nextNode = dojo.filter(nodes, function(node){ return node.getIdentity() == nextPath; })[0]; if(!!nextNode){ if(path.length){ tree._expandNode(nextNode).addCallback(function(){ selectPath(path, nextNode.getChildren(), def); }); }else{ //Successfully reached the end of this path def.callback(nextNode); } } else { def.errback("Could not expand path at " + nextPath); } } function setNodes(newNodes){ //After all expansion is finished, set the selection to //the set of nodes successfully found. tree.set("selectedNodes", dojo.map( dojo.filter(newNodes,function(x){return x[0];}), function(x){return x[1];})); } }, _setSelectedNodeAttr: function(node){ this.set('selectedNodes', [node]); }, _setSelectedNodesAttr: function(nodes){ this._loadDeferred.addCallback( dojo.hitch(this, function(){ this.dndController.setSelection(nodes); })); }, ////////////// 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" && key != " "){ // 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"; //On WebKit based browsers, the combination ctrl-enter //does not get passed through. To allow accessible //multi-select on those browsers, the space key is //also used for selection. map[dk.SPACE]= map[" "] = "_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){ this._publish("execute", { item: message.item, node: message.node } ); this.dndController.userSelect(message.node, dojo.isCopyKey( message.evt ), message.evt.shiftKey); this.onClick(message.item, message.node, message.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); } } }, isExpandoNode: function(node, widget){ // summary: // check whether a dom node is the expandoNode for a particular TreeNode widget return dojo.isDescendant(node, widget.expandoNode); }, _onClick: function(/*TreeNode*/ nodeWidget, /*Event*/ e){ // summary: // Translates click events into commands for the controller to process var domElement = e.target, isExpandoClick = this.isExpandoNode(domElement, nodeWidget); 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); } 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); } 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); }, _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){ // Remove node from set of selected nodes (if it's selected) this.dndController.removeTreeNode(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(); }, this); 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); } // 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._getMarginSize(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.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", "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.replaceClass(dojo.body(), "dojoDnd" + (this.copy ? "Copy" : "Move"), "dojoDnd" + (this.copy ? "Move" : "Copy")); } }); // 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 TreeNode corresponding to TreeNode mouse was previously over newTarget = this.current, // TreeNode corresponding to TreeNode mouse is currently over 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.rowNode, true); } if((e.pageY - this.targetBox.y) <= this.betweenThreshold){ newDropPosition = "Before"; }else if((e.pageY - this.targetBox.y) >= (this.targetBox.h - this.betweenThreshold)){ newDropPosition = "After"; } } if(newTarget != oldTarget || newDropPosition != oldDropPosition){ if(oldTarget){ this._removeItemClass(oldTarget.rowNode, oldDropPosition); } if(newTarget){ this._addItemClass(newTarget.rowNode, newDropPosition); } // Check if it's ok to drop the dragged node on/before/after the target node. if(!newTarget){ m.canDrop(false); }else if(newTarget == this.tree.rootNode && newDropPosition != "Over"){ // Can't drop before or after tree's root node; the dropped node would just disappear (at least visually) m.canDrop(false); }else if(m.source == this && (newTarget.id in this.selection)){ // Guard against dropping onto yourself (TODO: guard against dropping onto your descendant, #7140) m.canDrop(false); }else if(this.checkItemAcceptance(newTarget.rowNode, m.source, newDropPosition.toLowerCase()) && !this._isParentChildDrop(m.source, newTarget.rowNode)){ m.canDrop(true); }else{ m.canDrop(false); } this.targetAnchor = newTarget; this.dropPosition = newDropPosition; } }, onMouseMove: function(e){ // summary: // Called for any onmousemove events over the Tree // e: Event // onmousemouse event // tags: // private if(this.isDragging && this.targetState == "Disabled"){ return; } this.inherited(arguments); var m = dojo.dnd.manager(); if(this.isDragging){ this._onDragMouse(e); }else{ if(this.mouseDown && this.isSource && (Math.abs(e.pageX-this._lastX)>=this.dragThreshold || Math.abs(e.pageY-this._lastY)>=this.dragThreshold)){ var nodes = this.getSelectedTreeNodes(); if(nodes.length){ if(nodes.length > 1){ //filter out all selected items which has one of their ancestor selected as well var seen = this.selection, i = 0, r = [], n, p; nextitem: while((n = nodes[i++])){ for(p = n.getParent(); p && p !== this.tree; p = p.getParent()){ if(seen[p.id]){ //parent is already selected, skip this node continue nextitem; } } //this node does not have any ancestors selected, add it r.push(n); } nodes = r; } nodes = dojo.map(nodes, function(n){return n.domNode}); m.startDrag(this, nodes, this.copyState(dojo.isCopyKey(e))); } } } }, onMouseDown: function(e){ // summary: // Event processor for onmousedown // e: Event // onmousedown event // tags: // private this.mouseDown = true; this.mouseButton = e.button; this._lastX = e.pageX; this._lastY = e.pageY; this.inherited(arguments); }, onMouseUp: function(e){ // summary: // Event processor for onmouseup // e: Event // onmouseup event // tags: // private if(this.mouseDown){ this.mouseDown = false; this.inherited(arguments); } }, onMouseOut: function(){ // summary: // Event processor for when mouse is moved away from a TreeNode // tags: // private this.inherited(arguments); this._unmarkTargetAnchor(); }, checkItemAcceptance: function(target, source, position){ // summary: // Stub function to be overridden if one wants to check for the ability to drop at the node/item level // description: // In the base case, this is called to check if target can become a child of source. // When betweenThreshold is set, position="before" or "after" means that we // are asking if the source node can be dropped before/after the target node. // target: DOMNode // The dijitTreeRoot DOM node inside of the TreeNode that we are dropping on to // Use dijit.getEnclosingWidget(target) to get the TreeNode. // source: dijit.tree.dndSource // The (set of) nodes we are dropping // position: String // "over", "before", or "after" // tags: // extension return true; }, // topic event processors onDndSourceOver: function(source){ // summary: // Topic event processor for /dnd/source/over, called when detected a current source. // source: Object // The dijit.tree.dndSource / dojo.dnd.Source which has the mouse over it // tags: // private if(this != source){ this.mouseDown = false; this._unmarkTargetAnchor(); }else if(this.isDragging){ var m = dojo.dnd.manager(); m.canDrop(false); } }, onDndStart: function(source, nodes, copy){ // summary: // Topic event processor for /dnd/start, called to initiate the DnD operation // source: Object // The dijit.tree.dndSource / dojo.dnd.Source which is providing the items // nodes: DomNode[] // The list of transferred items, dndTreeNode nodes if dragging from a Tree // copy: Boolean // Copy items, if true, move items otherwise // tags: // private if(this.isSource){ this._changeState("Source", this == source ? (copy ? "Copied" : "Moved") : ""); } var accepted = this.checkAcceptance(source, nodes); this._changeState("Target", accepted ? "" : "Disabled"); if(this == source){ dojo.dnd.manager().overSource(this); } this.isDragging = true; }, itemCreator: function(/*DomNode[]*/ nodes, target, /*dojo.dnd.Source*/ source){ // summary: // Returns objects passed to `Tree.model.newItem()` based on DnD nodes // dropped onto the tree. Developer must override this method to enable // dropping from external sources onto this Tree, unless the Tree.model's items // happen to look like {id: 123, name: "Apple" } with no other attributes. // description: // For each node in nodes[], which came from source, create a hash of name/value // pairs to be passed to Tree.model.newItem(). Returns array of those hashes. // returns: Object[] // Array of name/value hashes for each new item to be added to the Tree, like: // | [ // | { id: 123, label: "apple", foo: "bar" }, // | { id: 456, label: "pear", zaz: "bam" } // | ] // tags: // extension // TODO: for 2.0 refactor so itemCreator() is called once per drag node, and // make signature itemCreator(sourceItem, node, target) (or similar). return dojo.map(nodes, function(node){ return { "id": node.id, "name": node.textContent || node.innerText || "" }; }); // Object[] }, onDndDrop: function(source, nodes, copy){ // summary: // Topic event processor for /dnd/drop, called to finish the DnD operation. // description: // Updates data store items according to where node was dragged from and dropped // to. The tree will then respond to those data store updates and redraw itself. // source: Object // The dijit.tree.dndSource / dojo.dnd.Source which is providing the items // nodes: DomNode[] // The list of transferred items, dndTreeNode nodes if dragging from a Tree // copy: Boolean // Copy items, if true, move items otherwise // tags: // protected if(this.containerState == "Over"){ var tree = this.tree, model = tree.model, target = this.targetAnchor, requeryRoot = false; // set to true iff top level items change this.isDragging = false; // Compute the new parent item var targetWidget = target; var newParentItem; var insertIndex; newParentItem = (targetWidget && targetWidget.item) || tree.item; if(this.dropPosition == "Before" || this.dropPosition == "After"){ // TODO: if there is no parent item then disallow the drop. // Actually this should be checked during onMouseMove too, to make the drag icon red. newParentItem = (targetWidget.getParent() && targetWidget.getParent().item) || tree.item; // Compute the insert index for reordering insertIndex = targetWidget.getIndexInParent(); if(this.dropPosition == "After"){ insertIndex = targetWidget.getIndexInParent() + 1; } }else{ newParentItem = (targetWidget && targetWidget.item) || tree.item; } // If necessary, use this variable to hold array of hashes to pass to model.newItem() // (one entry in the array for each dragged node). var newItemsParams; dojo.forEach(nodes, function(node, idx){ // dojo.dnd.Item representing the thing being dropped. // Don't confuse the use of item here (meaning a DnD item) with the // uses below where item means dojo.data item. var sourceItem = source.getItem(node.id); // Information that's available if the source is another Tree // (possibly but not necessarily this tree, possibly but not // necessarily the same model as this Tree) if(dojo.indexOf(sourceItem.type, "treeNode") != -1){ var childTreeNode = sourceItem.data, childItem = childTreeNode.item, oldParentItem = childTreeNode.getParent().item; } if(source == this){ // This is a node from my own tree, and we are moving it, not copying. // Remove item from old parent's children attribute. // TODO: dijit.tree.dndSelector should implement deleteSelectedNodes() // and this code should go there. if(typeof insertIndex == "number"){ if(newParentItem == oldParentItem && childTreeNode.getIndexInParent() < insertIndex){ insertIndex -= 1; } } model.pasteItem(childItem, oldParentItem, newParentItem, copy, insertIndex); }else if(model.isItem(childItem)){ // Item from same model // (maybe we should only do this branch if the source is a tree?) model.pasteItem(childItem, oldParentItem, newParentItem, copy, insertIndex); }else{ // Get the hash to pass to model.newItem(). A single call to // itemCreator() returns an array of hashes, one for each drag source node. if(!newItemsParams){ newItemsParams = this.itemCreator(nodes, target.rowNode, source); } // Create new item in the tree, based on the drag source. model.newItem(newItemsParams[idx], newParentItem, insertIndex); } }, this); // Expand the target node (if it's currently collapsed) so the user can see // where their node was dropped. In particular since that node is still selected. this.tree._expandNode(targetWidget); } this.onDndCancel(); }, onDndCancel: function(){ // summary: // Topic event processor for /dnd/cancel, called to cancel the DnD operation // tags: // private this._unmarkTargetAnchor(); this.isDragging = false; this.mouseDown = false; delete this.mouseButton; this._changeState("Source", ""); this._changeState("Target", ""); }, // When focus moves in/out of the entire Tree onOverEvent: function(){ // summary: // This method is called when mouse is moved over our container (like onmouseenter) // tags: // private this.inherited(arguments); dojo.dnd.manager().overSource(this); }, onOutEvent: function(){ // summary: // This method is called when mouse is moved out of our container (like onmouseleave) // tags: // private this._unmarkTargetAnchor(); var m = dojo.dnd.manager(); if(this.isDragging){ m.canDrop(false); } m.outSource(this); this.inherited(arguments); }, _isParentChildDrop: function(source, targetRow){ // summary: // Checks whether the dragged items are parent rows in the tree which are being // dragged into their own children. // // source: // The DragSource object. // // targetRow: // The tree row onto which the dragged nodes are being dropped. // // tags: // private // If the dragged object is not coming from the tree this widget belongs to, // it cannot be invalid. if(!source.tree || source.tree != this.tree){ return false; } var root = source.tree.domNode; var ids = source.selection; var node = targetRow.parentNode; // Iterate up the DOM hierarchy from the target drop row, // checking of any of the dragged nodes have the same ID. while(node != root && !ids[node.id]){ node = node.parentNode; } return node.id && ids[node.id]; }, _unmarkTargetAnchor: function(){ // summary: // Removes hover class of the current target anchor // tags: // private if(!this.targetAnchor){ return; } this._removeItemClass(this.targetAnchor.rowNode, this.dropPosition); this.targetAnchor = null; this.targetBox = null; this.dropPosition = null; }, _markDndStatus: function(copy){ // summary: // Changes source's state based on "copy" status this._changeState("Source", copy ? "Copied" : "Moved"); } }); } if(!dojo._hasResource["dojo.data.ItemFileReadStore"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dojo.data.ItemFileReadStore"] = true; dojo.provide("dojo.data.ItemFileReadStore"); dojo.declare("dojo.data.ItemFileReadStore", null,{ // summary: // The ItemFileReadStore implements the dojo.data.api.Read API and reads // data from JSON files that have contents in this format -- // { items: [ // { name:'Kermit', color:'green', age:12, friends:['Gonzo', {_reference:{name:'Fozzie Bear'}}]}, // { name:'Fozzie Bear', wears:['hat', 'tie']}, // { name:'Miss Piggy', pets:'Foo-Foo'} // ]} // Note that it can also contain an 'identifer' property that specified which attribute on the items // in the array of items that acts as the unique identifier for that item. // constructor: function(/* Object */ keywordParameters){ // summary: constructor // keywordParameters: {url: String} // keywordParameters: {data: jsonObject} // keywordParameters: {typeMap: object) // The structure of the typeMap object is as follows: // { // type0: function || object, // type1: function || object, // ... // typeN: function || object // } // Where if it is a function, it is assumed to be an object constructor that takes the // value of _value as the initialization parameters. If it is an object, then it is assumed // to be an object of general form: // { // type: function, //constructor. // deserialize: function(value) //The function that parses the value and constructs the object defined by type appropriately. // } this._arrayOfAllItems = []; this._arrayOfTopLevelItems = []; this._loadFinished = false; this._jsonFileUrl = keywordParameters.url; this._ccUrl = keywordParameters.url; this.url = keywordParameters.url; this._jsonData = keywordParameters.data; this.data = null; this._datatypeMap = keywordParameters.typeMap || {}; if(!this._datatypeMap['Date']){ //If no default mapping for dates, then set this as default. //We use the dojo.date.stamp here because the ISO format is the 'dojo way' //of generically representing dates. this._datatypeMap['Date'] = { type: Date, deserialize: function(value){ return dojo.date.stamp.fromISOString(value); } }; } this._features = {'dojo.data.api.Read':true, 'dojo.data.api.Identity':true}; this._itemsByIdentity = null; this._storeRefPropName = "_S"; // Default name for the store reference to attach to every item. this._itemNumPropName = "_0"; // Default Item Id for isItem to attach to every item. this._rootItemPropName = "_RI"; // Default Item Id for isItem to attach to every item. this._reverseRefMap = "_RRM"; // Default attribute for constructing a reverse reference map for use with reference integrity this._loadInProgress = false; //Got to track the initial load to prevent duelling loads of the dataset. this._queuedFetches = []; if(keywordParameters.urlPreventCache !== undefined){ this.urlPreventCache = keywordParameters.urlPreventCache?true:false; } if(keywordParameters.hierarchical !== undefined){ this.hierarchical = keywordParameters.hierarchical?true:false; } if(keywordParameters.clearOnClose){ this.clearOnClose = true; } if("failOk" in keywordParameters){ this.failOk = keywordParameters.failOk?true:false; } }, url: "", // use "" rather than undefined for the benefit of the parser (#3539) //Internal var, crossCheckUrl. Used so that setting either url or _jsonFileUrl, can still trigger a reload //when clearOnClose and close is used. _ccUrl: "", data: null, // define this so that the parser can populate it typeMap: null, //Define so parser can populate. //Parameter to allow users to specify if a close call should force a reload or not. //By default, it retains the old behavior of not clearing if close is called. But //if set true, the store will be reset to default state. Note that by doing this, //all item handles will become invalid and a new fetch must be issued. clearOnClose: false, //Parameter to allow specifying if preventCache should be passed to the xhrGet call or not when loading data from a url. //Note this does not mean the store calls the server on each fetch, only that the data load has preventCache set as an option. //Added for tracker: #6072 urlPreventCache: false, //Parameter for specifying that it is OK for the xhrGet call to fail silently. failOk: false, //Parameter to indicate to process data from the url as hierarchical //(data items can contain other data items in js form). Default is true //for backwards compatibility. False means only root items are processed //as items, all child objects outside of type-mapped objects and those in //specific reference format, are left straight JS data objects. hierarchical: true, _assertIsItem: function(/* item */ item){ // summary: // This function tests whether the item passed in is indeed an item in the store. // item: // The item to test for being contained by the store. if(!this.isItem(item)){ throw new Error("dojo.data.ItemFileReadStore: Invalid item argument."); } }, _assertIsAttribute: function(/* attribute-name-string */ attribute){ // summary: // This function tests whether the item passed in is indeed a valid 'attribute' like type for the store. // attribute: // The attribute to test for being contained by the store. if(typeof attribute !== "string"){ throw new Error("dojo.data.ItemFileReadStore: Invalid attribute argument."); } }, getValue: function( /* item */ item, /* attribute-name-string */ attribute, /* value? */ defaultValue){ // summary: // See dojo.data.api.Read.getValue() var values = this.getValues(item, attribute); return (values.length > 0)?values[0]:defaultValue; // mixed }, getValues: function(/* item */ item, /* attribute-name-string */ attribute){ // summary: // See dojo.data.api.Read.getValues() this._assertIsItem(item); this._assertIsAttribute(attribute); // Clone it before returning. refs: #10474 return (item[attribute] || []).slice(0); // Array }, getAttributes: function(/* item */ item){ // summary: // See dojo.data.api.Read.getAttributes() this._assertIsItem(item); var attributes = []; for(var key in item){ // Save off only the real item attributes, not the special id marks for O(1) isItem. if((key !== this._storeRefPropName) && (key !== this._itemNumPropName) && (key !== this._rootItemPropName) && (key !== this._reverseRefMap)){ attributes.push(key); } } return attributes; // Array }, hasAttribute: function( /* item */ item, /* attribute-name-string */ attribute){ // summary: // See dojo.data.api.Read.hasAttribute() this._assertIsItem(item); this._assertIsAttribute(attribute); return (attribute in item); }, containsValue: function(/* item */ item, /* attribute-name-string */ attribute, /* anything */ value){ // summary: // See dojo.data.api.Read.containsValue() var regexp = undefined; if(typeof value === "string"){ regexp = dojo.data.util.filter.patternToRegExp(value, false); } return this._containsValue(item, attribute, value, regexp); //boolean. }, _containsValue: function( /* item */ item, /* attribute-name-string */ attribute, /* anything */ value, /* RegExp?*/ regexp){ // summary: // Internal function for looking at the values contained by the item. // description: // Internal function for looking at the values contained by the item. This // function allows for denoting if the comparison should be case sensitive for // strings or not (for handling filtering cases where string case should not matter) // // item: // The data item to examine for attribute values. // attribute: // The attribute to inspect. // value: // The value to match. // regexp: // Optional regular expression generated off value if value was of string type to handle wildcarding. // If present and attribute values are string, then it can be used for comparison instead of 'value' return dojo.some(this.getValues(item, attribute), function(possibleValue){ if(possibleValue !== null && !dojo.isObject(possibleValue) && regexp){ if(possibleValue.toString().match(regexp)){ return true; // Boolean } }else if(value === possibleValue){ return true; // Boolean } }); }, isItem: function(/* anything */ something){ // summary: // See dojo.data.api.Read.isItem() if(something && something[this._storeRefPropName] === this){ if(this._arrayOfAllItems[something[this._itemNumPropName]] === something){ return true; } } return false; // Boolean }, isItemLoaded: function(/* anything */ something){ // summary: // See dojo.data.api.Read.isItemLoaded() return this.isItem(something); //boolean }, loadItem: function(/* object */ keywordArgs){ // summary: // See dojo.data.api.Read.loadItem() this._assertIsItem(keywordArgs.item); }, getFeatures: function(){ // summary: // See dojo.data.api.Read.getFeatures() return this._features; //Object }, getLabel: function(/* item */ item){ // summary: // See dojo.data.api.Read.getLabel() if(this._labelAttr && this.isItem(item)){ return this.getValue(item,this._labelAttr); //String } return undefined; //undefined }, getLabelAttributes: function(/* item */ item){ // summary: // See dojo.data.api.Read.getLabelAttributes() if(this._labelAttr){ return [this._labelAttr]; //array } return null; //null }, _fetchItems: function( /* Object */ keywordArgs, /* Function */ findCallback, /* Function */ errorCallback){ // summary: // See dojo.data.util.simpleFetch.fetch() var self = this, filter = function(requestArgs, arrayOfItems){ var items = [], i, key; if(requestArgs.query){ var value, ignoreCase = requestArgs.queryOptions ? requestArgs.queryOptions.ignoreCase : false; //See if there are any string values that can be regexp parsed first to avoid multiple regexp gens on the //same value for each item examined. Much more efficient. var regexpList = {}; for(key in requestArgs.query){ value = requestArgs.query[key]; if(typeof value === "string"){ regexpList[key] = dojo.data.util.filter.patternToRegExp(value, ignoreCase); }else if(value instanceof RegExp){ regexpList[key] = value; } } for(i = 0; i < arrayOfItems.length; ++i){ var match = true; var candidateItem = arrayOfItems[i]; if(candidateItem === null){ match = false; }else{ for(key in requestArgs.query){ value = requestArgs.query[key]; if(!self._containsValue(candidateItem, key, value, regexpList[key])){ match = false; } } } if(match){ items.push(candidateItem); } } findCallback(items, requestArgs); }else{ // We want a copy to pass back in case the parent wishes to sort the array. // We shouldn't allow resort of the internal list, so that multiple callers // can get lists and sort without affecting each other. We also need to // filter out any null values that have been left as a result of deleteItem() // calls in ItemFileWriteStore. for(i = 0; i < arrayOfItems.length; ++i){ var item = arrayOfItems[i]; if(item !== null){ items.push(item); } } findCallback(items, requestArgs); } }; if(this._loadFinished){ filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions)); }else{ //Do a check on the JsonFileUrl and crosscheck it. //If it doesn't match the cross-check, it needs to be updated //This allows for either url or _jsonFileUrl to he changed to //reset the store load location. Done this way for backwards //compatibility. People use _jsonFileUrl (even though officially //private. if(this._jsonFileUrl !== this._ccUrl){ dojo.deprecated("dojo.data.ItemFileReadStore: ", "To change the url, set the url property of the store," + " not _jsonFileUrl. _jsonFileUrl support will be removed in 2.0"); this._ccUrl = this._jsonFileUrl; this.url = this._jsonFileUrl; }else if(this.url !== this._ccUrl){ this._jsonFileUrl = this.url; this._ccUrl = this.url; } //See if there was any forced reset of data. if(this.data != null){ this._jsonData = this.data; this.data = null; } if(this._jsonFileUrl){ //If fetches come in before the loading has finished, but while //a load is in progress, we have to defer the fetching to be //invoked in the callback. if(this._loadInProgress){ this._queuedFetches.push({args: keywordArgs, filter: filter}); }else{ this._loadInProgress = true; var getArgs = { url: self._jsonFileUrl, handleAs: "json-comment-optional", preventCache: this.urlPreventCache, failOk: this.failOk }; var getHandler = dojo.xhrGet(getArgs); getHandler.addCallback(function(data){ try{ self._getItemsFromLoadedData(data); self._loadFinished = true; self._loadInProgress = false; filter(keywordArgs, self._getItemsArray(keywordArgs.queryOptions)); self._handleQueuedFetches(); }catch(e){ self._loadFinished = true; self._loadInProgress = false; errorCallback(e, keywordArgs); } }); getHandler.addErrback(function(error){ self._loadInProgress = false; errorCallback(error, keywordArgs); }); //Wire up the cancel to abort of the request //This call cancel on the deferred if it hasn't been called //yet and then will chain to the simple abort of the //simpleFetch keywordArgs var oldAbort = null; if(keywordArgs.abort){ oldAbort = keywordArgs.abort; } keywordArgs.abort = function(){ var df = getHandler; if(df && df.fired === -1){ df.cancel(); df = null; } if(oldAbort){ oldAbort.call(keywordArgs); } }; } }else if(this._jsonData){ try{ this._loadFinished = true; this._getItemsFromLoadedData(this._jsonData); this._jsonData = null; filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions)); }catch(e){ errorCallback(e, keywordArgs); } }else{ errorCallback(new Error("dojo.data.ItemFileReadStore: No JSON source data was provided as either URL or a nested Javascript object."), keywordArgs); } } }, _handleQueuedFetches: function(){ // summary: // Internal function to execute delayed request in the store. //Execute any deferred fetches now. if(this._queuedFetches.length > 0){ for(var i = 0; i < this._queuedFetches.length; i++){ var fData = this._queuedFetches[i], delayedQuery = fData.args, delayedFilter = fData.filter; if(delayedFilter){ delayedFilter(delayedQuery, this._getItemsArray(delayedQuery.queryOptions)); }else{ this.fetchItemByIdentity(delayedQuery); } } this._queuedFetches = []; } }, _getItemsArray: function(/*object?*/queryOptions){ // summary: // Internal function to determine which list of items to search over. // queryOptions: The query options parameter, if any. if(queryOptions && queryOptions.deep){ return this._arrayOfAllItems; } return this._arrayOfTopLevelItems; }, close: function(/*dojo.data.api.Request || keywordArgs || null */ request){ // summary: // See dojo.data.api.Read.close() if(this.clearOnClose && this._loadFinished && !this._loadInProgress){ //Reset all internalsback to default state. This will force a reload //on next fetch. This also checks that the data or url param was set //so that the store knows it can get data. Without one of those being set, //the next fetch will trigger an error. if(((this._jsonFileUrl == "" || this._jsonFileUrl == null) && (this.url == "" || this.url == null) ) && this.data == null){ console.debug("dojo.data.ItemFileReadStore: WARNING! Data reload " + " information has not been provided." + " Please set 'url' or 'data' to the appropriate value before" + " the next fetch"); } this._arrayOfAllItems = []; this._arrayOfTopLevelItems = []; this._loadFinished = false; this._itemsByIdentity = null; this._loadInProgress = false; this._queuedFetches = []; } }, _getItemsFromLoadedData: function(/* Object */ dataObject){ // summary: // Function to parse the loaded data into item format and build the internal items array. // description: // Function to parse the loaded data into item format and build the internal items array. // // dataObject: // The JS data object containing the raw data to convery into item format. // // returns: array // Array of items in store item format. // First, we define a couple little utility functions... var addingArrays = false, self = this; function valueIsAnItem(/* anything */ aValue){ // summary: // Given any sort of value that could be in the raw json data, // return true if we should interpret the value as being an // item itself, rather than a literal value or a reference. // example: // | false == valueIsAnItem("Kermit"); // | false == valueIsAnItem(42); // | false == valueIsAnItem(new Date()); // | false == valueIsAnItem({_type:'Date', _value:'1802-05-14'}); // | false == valueIsAnItem({_reference:'Kermit'}); // | true == valueIsAnItem({name:'Kermit', color:'green'}); // | true == valueIsAnItem({iggy:'pop'}); // | true == valueIsAnItem({foo:42}); var isItem = ( (aValue !== null) && (typeof aValue === "object") && (!dojo.isArray(aValue) || addingArrays) && (!dojo.isFunction(aValue)) && (aValue.constructor == Object || dojo.isArray(aValue)) && (typeof aValue._reference === "undefined") && (typeof aValue._type === "undefined") && (typeof aValue._value === "undefined") && self.hierarchical ); return isItem; } function addItemAndSubItemsToArrayOfAllItems(/* Item */ anItem){ self._arrayOfAllItems.push(anItem); for(var attribute in anItem){ var valueForAttribute = anItem[attribute]; if(valueForAttribute){ if(dojo.isArray(valueForAttribute)){ var valueArray = valueForAttribute; for(var k = 0; k < valueArray.length; ++k){ var singleValue = valueArray[k]; if(valueIsAnItem(singleValue)){ addItemAndSubItemsToArrayOfAllItems(singleValue); } } }else{ if(valueIsAnItem(valueForAttribute)){ addItemAndSubItemsToArrayOfAllItems(valueForAttribute); } } } } } this._labelAttr = dataObject.label; // We need to do some transformations to convert the data structure // that we read from the file into a format that will be convenient // to work with in memory. // Step 1: Walk through the object hierarchy and build a list of all items var i, item; this._arrayOfAllItems = []; this._arrayOfTopLevelItems = dataObject.items; for(i = 0; i < this._arrayOfTopLevelItems.length; ++i){ item = this._arrayOfTopLevelItems[i]; if(dojo.isArray(item)){ addingArrays = true; } addItemAndSubItemsToArrayOfAllItems(item); item[this._rootItemPropName]=true; } // Step 2: Walk through all the attribute values of all the items, // and replace single values with arrays. For example, we change this: // { name:'Miss Piggy', pets:'Foo-Foo'} // into this: // { name:['Miss Piggy'], pets:['Foo-Foo']} // // We also store the attribute names so we can validate our store // reference and item id special properties for the O(1) isItem var allAttributeNames = {}, key; for(i = 0; i < this._arrayOfAllItems.length; ++i){ item = this._arrayOfAllItems[i]; for(key in item){ if(key !== this._rootItemPropName){ var value = item[key]; if(value !== null){ if(!dojo.isArray(value)){ item[key] = [value]; } }else{ item[key] = [null]; } } allAttributeNames[key]=key; } } // Step 3: Build unique property names to use for the _storeRefPropName and _itemNumPropName // This should go really fast, it will generally never even run the loop. while(allAttributeNames[this._storeRefPropName]){ this._storeRefPropName += "_"; } while(allAttributeNames[this._itemNumPropName]){ this._itemNumPropName += "_"; } while(allAttributeNames[this._reverseRefMap]){ this._reverseRefMap += "_"; } // Step 4: Some data files specify an optional 'identifier', which is // the name of an attribute that holds the identity of each item. // If this data file specified an identifier attribute, then build a // hash table of items keyed by the identity of the items. var arrayOfValues; var identifier = dataObject.identifier; if(identifier){ this._itemsByIdentity = {}; this._features['dojo.data.api.Identity'] = identifier; for(i = 0; i < this._arrayOfAllItems.length; ++i){ item = this._arrayOfAllItems[i]; arrayOfValues = item[identifier]; var identity = arrayOfValues[0]; if(!Object.hasOwnProperty.call(this._itemsByIdentity, identity)){ this._itemsByIdentity[identity] = item; }else{ if(this._jsonFileUrl){ throw new Error("dojo.data.ItemFileReadStore: The json data as specified by: [" + this._jsonFileUrl + "] is malformed. Items within the list have identifier: [" + identifier + "]. Value collided: [" + identity + "]"); }else if(this._jsonData){ throw new Error("dojo.data.ItemFileReadStore: The json data provided by the creation arguments is malformed. Items within the list have identifier: [" + identifier + "]. Value collided: [" + identity + "]"); } } } }else{ this._features['dojo.data.api.Identity'] = Number; } // Step 5: Walk through all the items, and set each item's properties // for _storeRefPropName and _itemNumPropName, so that store.isItem() will return true. for(i = 0; i < this._arrayOfAllItems.length; ++i){ item = this._arrayOfAllItems[i]; item[this._storeRefPropName] = this; item[this._itemNumPropName] = i; } // Step 6: We walk through all the attribute values of all the items, // looking for type/value literals and item-references. // // We replace item-references with pointers to items. For example, we change: // { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] } // into this: // { name:['Kermit'], friends:[miss_piggy] } // (where miss_piggy is the object representing the 'Miss Piggy' item). // // We replace type/value pairs with typed-literals. For example, we change: // { name:['Nelson Mandela'], born:[{_type:'Date', _value:'1918-07-18'}] } // into this: // { name:['Kermit'], born:(new Date(1918, 6, 18)) } // // We also generate the associate map for all items for the O(1) isItem function. for(i = 0; i < this._arrayOfAllItems.length; ++i){ item = this._arrayOfAllItems[i]; // example: { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] } for(key in item){ arrayOfValues = item[key]; // example: [{_reference:{name:'Miss Piggy'}}] for(var j = 0; j < arrayOfValues.length; ++j){ value = arrayOfValues[j]; // example: {_reference:{name:'Miss Piggy'}} if(value !== null && typeof value == "object"){ if(("_type" in value) && ("_value" in value)){ var type = value._type; // examples: 'Date', 'Color', or 'ComplexNumber' var mappingObj = this._datatypeMap[type]; // examples: Date, dojo.Color, foo.math.ComplexNumber, {type: dojo.Color, deserialize(value){ return new dojo.Color(value)}} if(!mappingObj){ throw new Error("dojo.data.ItemFileReadStore: in the typeMap constructor arg, no object class was specified for the datatype '" + type + "'"); }else if(dojo.isFunction(mappingObj)){ arrayOfValues[j] = new mappingObj(value._value); }else if(dojo.isFunction(mappingObj.deserialize)){ arrayOfValues[j] = mappingObj.deserialize(value._value); }else{ throw new Error("dojo.data.ItemFileReadStore: Value provided in typeMap was neither a constructor, nor a an object with a deserialize function"); } } if(value._reference){ var referenceDescription = value._reference; // example: {name:'Miss Piggy'} if(!dojo.isObject(referenceDescription)){ // example: 'Miss Piggy' // from an item like: { name:['Kermit'], friends:[{_reference:'Miss Piggy'}]} arrayOfValues[j] = this._getItemByIdentity(referenceDescription); }else{ // example: {name:'Miss Piggy'} // from an item like: { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] } for(var k = 0; k < this._arrayOfAllItems.length; ++k){ var candidateItem = this._arrayOfAllItems[k], found = true; for(var refKey in referenceDescription){ if(candidateItem[refKey] != referenceDescription[refKey]){ found = false; } } if(found){ arrayOfValues[j] = candidateItem; } } } if(this.referenceIntegrity){ var refItem = arrayOfValues[j]; if(this.isItem(refItem)){ this._addReferenceToMap(refItem, item, key); } } }else if(this.isItem(value)){ //It's a child item (not one referenced through _reference). //We need to treat this as a referenced item, so it can be cleaned up //in a write store easily. if(this.referenceIntegrity){ this._addReferenceToMap(value, item, key); } } } } } } }, _addReferenceToMap: function(/*item*/ refItem, /*item*/ parentItem, /*string*/ attribute){ // summary: // Method to add an reference map entry for an item and attribute. // description: // Method to add an reference map entry for an item and attribute. // // refItem: // The item that is referenced. // parentItem: // The item that holds the new reference to refItem. // attribute: // The attribute on parentItem that contains the new reference. //Stub function, does nothing. Real processing is in ItemFileWriteStore. }, getIdentity: function(/* item */ item){ // summary: // See dojo.data.api.Identity.getIdentity() var identifier = this._features['dojo.data.api.Identity']; if(identifier === Number){ return item[this._itemNumPropName]; // Number }else{ var arrayOfValues = item[identifier]; if(arrayOfValues){ return arrayOfValues[0]; // Object || String } } return null; // null }, fetchItemByIdentity: function(/* Object */ keywordArgs){ // summary: // See dojo.data.api.Identity.fetchItemByIdentity() // Hasn't loaded yet, we have to trigger the load. var item, scope; if(!this._loadFinished){ var self = this; //Do a check on the JsonFileUrl and crosscheck it. //If it doesn't match the cross-check, it needs to be updated //This allows for either url or _jsonFileUrl to he changed to //reset the store load location. Done this way for backwards //compatibility. People use _jsonFileUrl (even though officially //private. if(this._jsonFileUrl !== this._ccUrl){ dojo.deprecated("dojo.data.ItemFileReadStore: ", "To change the url, set the url property of the store," + " not _jsonFileUrl. _jsonFileUrl support will be removed in 2.0"); this._ccUrl = this._jsonFileUrl; this.url = this._jsonFileUrl; }else if(this.url !== this._ccUrl){ this._jsonFileUrl = this.url; this._ccUrl = this.url; } //See if there was any forced reset of data. if(this.data != null && this._jsonData == null){ this._jsonData = this.data; this.data = null; } if(this._jsonFileUrl){ if(this._loadInProgress){ this._queuedFetches.push({args: keywordArgs}); }else{ this._loadInProgress = true; var getArgs = { url: self._jsonFileUrl, handleAs: "json-comment-optional", preventCache: this.urlPreventCache, failOk: this.failOk }; var getHandler = dojo.xhrGet(getArgs); getHandler.addCallback(function(data){ var scope = keywordArgs.scope?keywordArgs.scope:dojo.global; try{ self._getItemsFromLoadedData(data); self._loadFinished = true; self._loadInProgress = false; item = self._getItemByIdentity(keywordArgs.identity); if(keywordArgs.onItem){ keywordArgs.onItem.call(scope, item); } self._handleQueuedFetches(); }catch(error){ self._loadInProgress = false; if(keywordArgs.onError){ keywordArgs.onError.call(scope, error); } } }); getHandler.addErrback(function(error){ self._loadInProgress = false; if(keywordArgs.onError){ var scope = keywordArgs.scope?keywordArgs.scope:dojo.global; keywordArgs.onError.call(scope, error); } }); } }else if(this._jsonData){ // Passed in data, no need to xhr. self._getItemsFromLoadedData(self._jsonData); self._jsonData = null; self._loadFinished = true; item = self._getItemByIdentity(keywordArgs.identity); if(keywordArgs.onItem){ scope = keywordArgs.scope?keywordArgs.scope:dojo.global; keywordArgs.onItem.call(scope, item); } } }else{ // Already loaded. We can just look it up and call back. item = this._getItemByIdentity(keywordArgs.identity); if(keywordArgs.onItem){ scope = keywordArgs.scope?keywordArgs.scope:dojo.global; keywordArgs.onItem.call(scope, item); } } }, _getItemByIdentity: function(/* Object */ identity){ // summary: // Internal function to look an item up by its identity map. var item = null; if(this._itemsByIdentity && Object.hasOwnProperty.call(this._itemsByIdentity, identity)){ item = this._itemsByIdentity[identity]; }else if (Object.hasOwnProperty.call(this._arrayOfAllItems, identity)){ item = this._arrayOfAllItems[identity]; } if(item === undefined){ item = null; } return item; // Object }, getIdentityAttributes: function(/* item */ item){ // summary: // See dojo.data.api.Identity.getIdentityAttributes() var identifier = this._features['dojo.data.api.Identity']; if(identifier === Number){ // If (identifier === Number) it means getIdentity() just returns // an integer item-number for each item. The dojo.data.api.Identity // spec says we need to return null if the identity is not composed // of attributes return null; // null }else{ return [identifier]; // Array } }, _forceLoad: function(){ // summary: // Internal function to force a load of the store if it hasn't occurred yet. This is required // for specific functions to work properly. var self = this; //Do a check on the JsonFileUrl and crosscheck it. //If it doesn't match the cross-check, it needs to be updated //This allows for either url or _jsonFileUrl to he changed to //reset the store load location. Done this way for backwards //compatibility. People use _jsonFileUrl (even though officially //private. if(this._jsonFileUrl !== this._ccUrl){ dojo.deprecated("dojo.data.ItemFileReadStore: ", "To change the url, set the url property of the store," + " not _jsonFileUrl. _jsonFileUrl support will be removed in 2.0"); this._ccUrl = this._jsonFileUrl; this.url = this._jsonFileUrl; }else if(this.url !== this._ccUrl){ this._jsonFileUrl = this.url; this._ccUrl = this.url; } //See if there was any forced reset of data. if(this.data != null){ this._jsonData = this.data; this.data = null; } if(this._jsonFileUrl){ var getArgs = { url: this._jsonFileUrl, handleAs: "json-comment-optional", preventCache: this.urlPreventCache, failOk: this.failOk, sync: true }; var getHandler = dojo.xhrGet(getArgs); getHandler.addCallback(function(data){ try{ //Check to be sure there wasn't another load going on concurrently //So we don't clobber data that comes in on it. If there is a load going on //then do not save this data. It will potentially clobber current data. //We mainly wanted to sync/wait here. //TODO: Revisit the loading scheme of this store to improve multi-initial //request handling. if(self._loadInProgress !== true && !self._loadFinished){ self._getItemsFromLoadedData(data); self._loadFinished = true; }else if(self._loadInProgress){ //Okay, we hit an error state we can't recover from. A forced load occurred //while an async load was occurring. Since we cannot block at this point, the best //that can be managed is to throw an error. throw new Error("dojo.data.ItemFileReadStore: Unable to perform a synchronous load, an async load is in progress."); } }catch(e){ console.log(e); throw e; } }); getHandler.addErrback(function(error){ throw error; }); }else if(this._jsonData){ self._getItemsFromLoadedData(self._jsonData); self._jsonData = null; self._loadFinished = true; } } }); //Mix in the simple fetch implementation to this class. dojo.extend(dojo.data.ItemFileReadStore,dojo.data.util.simpleFetch); } if(!dojo._hasResource["dojo.data.ItemFileWriteStore"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dojo.data.ItemFileWriteStore"] = true; dojo.provide("dojo.data.ItemFileWriteStore"); dojo.declare("dojo.data.ItemFileWriteStore", dojo.data.ItemFileReadStore, { constructor: function(/* object */ keywordParameters){ // keywordParameters: {typeMap: object) // The structure of the typeMap object is as follows: // { // type0: function || object, // type1: function || object, // ... // typeN: function || object // } // Where if it is a function, it is assumed to be an object constructor that takes the // value of _value as the initialization parameters. It is serialized assuming object.toString() // serialization. If it is an object, then it is assumed // to be an object of general form: // { // type: function, //constructor. // deserialize: function(value) //The function that parses the value and constructs the object defined by type appropriately. // serialize: function(object) //The function that converts the object back into the proper file format form. // } // ItemFileWriteStore extends ItemFileReadStore to implement these additional dojo.data APIs this._features['dojo.data.api.Write'] = true; this._features['dojo.data.api.Notification'] = true; // For keeping track of changes so that we can implement isDirty and revert this._pending = { _newItems:{}, _modifiedItems:{}, _deletedItems:{} }; if(!this._datatypeMap['Date'].serialize){ this._datatypeMap['Date'].serialize = function(obj){ return dojo.date.stamp.toISOString(obj, {zulu:true}); }; } //Disable only if explicitly set to false. if(keywordParameters && (keywordParameters.referenceIntegrity === false)){ this.referenceIntegrity = false; } // this._saveInProgress is set to true, briefly, from when save() is first called to when it completes this._saveInProgress = false; }, referenceIntegrity: true, //Flag that defaultly enabled reference integrity tracking. This way it can also be disabled pogrammatially or declaratively. _assert: function(/* boolean */ condition){ if(!condition){ throw new Error("assertion failed in ItemFileWriteStore"); } }, _getIdentifierAttribute: function(){ var identifierAttribute = this.getFeatures()['dojo.data.api.Identity']; // this._assert((identifierAttribute === Number) || (dojo.isString(identifierAttribute))); return identifierAttribute; }, /* dojo.data.api.Write */ newItem: function(/* Object? */ keywordArgs, /* Object? */ parentInfo){ // summary: See dojo.data.api.Write.newItem() this._assert(!this._saveInProgress); if(!this._loadFinished){ // We need to do this here so that we'll be able to find out what // identifierAttribute was specified in the data file. this._forceLoad(); } if(typeof keywordArgs != "object" && typeof keywordArgs != "undefined"){ throw new Error("newItem() was passed something other than an object"); } var newIdentity = null; var identifierAttribute = this._getIdentifierAttribute(); if(identifierAttribute === Number){ newIdentity = this._arrayOfAllItems.length; }else{ newIdentity = keywordArgs[identifierAttribute]; if(typeof newIdentity === "undefined"){ throw new Error("newItem() was not passed an identity for the new item"); } if(dojo.isArray(newIdentity)){ throw new Error("newItem() was not passed an single-valued identity"); } } // make sure this identity is not already in use by another item, if identifiers were // defined in the file. Otherwise it would be the item count, // which should always be unique in this case. if(this._itemsByIdentity){ this._assert(typeof this._itemsByIdentity[newIdentity] === "undefined"); } this._assert(typeof this._pending._newItems[newIdentity] === "undefined"); this._assert(typeof this._pending._deletedItems[newIdentity] === "undefined"); var newItem = {}; newItem[this._storeRefPropName] = this; newItem[this._itemNumPropName] = this._arrayOfAllItems.length; if(this._itemsByIdentity){ this._itemsByIdentity[newIdentity] = newItem; //We have to set the identifier now, otherwise we can't look it //up at calls to setValueorValues in parentInfo handling. newItem[identifierAttribute] = [newIdentity]; } this._arrayOfAllItems.push(newItem); //We need to construct some data for the onNew call too... var pInfo = null; // Now we need to check to see where we want to assign this thingm if any. if(parentInfo && parentInfo.parent && parentInfo.attribute){ pInfo = { item: parentInfo.parent, attribute: parentInfo.attribute, oldValue: undefined }; //See if it is multi-valued or not and handle appropriately //Generally, all attributes are multi-valued for this store //So, we only need to append if there are already values present. var values = this.getValues(parentInfo.parent, parentInfo.attribute); if(values && values.length > 0){ var tempValues = values.slice(0, values.length); if(values.length === 1){ pInfo.oldValue = values[0]; }else{ pInfo.oldValue = values.slice(0, values.length); } tempValues.push(newItem); this._setValueOrValues(parentInfo.parent, parentInfo.attribute, tempValues, false); pInfo.newValue = this.getValues(parentInfo.parent, parentInfo.attribute); }else{ this._setValueOrValues(parentInfo.parent, parentInfo.attribute, newItem, false); pInfo.newValue = newItem; } }else{ //Toplevel item, add to both top list as well as all list. newItem[this._rootItemPropName]=true; this._arrayOfTopLevelItems.push(newItem); } this._pending._newItems[newIdentity] = newItem; //Clone over the properties to the new item for(var key in keywordArgs){ if(key === this._storeRefPropName || key === this._itemNumPropName){ // Bummer, the user is trying to do something like // newItem({_S:"foo"}). Unfortunately, our superclass, // ItemFileReadStore, is already using _S in each of our items // to hold private info. To avoid a naming collision, we // need to move all our private info to some other property // of all the items/objects. So, we need to iterate over all // the items and do something like: // item.__S = item._S; // item._S = undefined; // But first we have to make sure the new "__S" variable is // not in use, which means we have to iterate over all the // items checking for that. throw new Error("encountered bug in ItemFileWriteStore.newItem"); } var value = keywordArgs[key]; if(!dojo.isArray(value)){ value = [value]; } newItem[key] = value; if(this.referenceIntegrity){ for(var i = 0; i < value.length; i++){ var val = value[i]; if(this.isItem(val)){ this._addReferenceToMap(val, newItem, key); } } } } this.onNew(newItem, pInfo); // dojo.data.api.Notification call return newItem; // item }, _removeArrayElement: function(/* Array */ array, /* anything */ element){ var index = dojo.indexOf(array, element); if(index != -1){ array.splice(index, 1); return true; } return false; }, deleteItem: function(/* item */ item){ // summary: See dojo.data.api.Write.deleteItem() this._assert(!this._saveInProgress); this._assertIsItem(item); // Remove this item from the _arrayOfAllItems, but leave a null value in place // of the item, so as not to change the length of the array, so that in newItem() // we can still safely do: newIdentity = this._arrayOfAllItems.length; var indexInArrayOfAllItems = item[this._itemNumPropName]; var identity = this.getIdentity(item); //If we have reference integrity on, we need to do reference cleanup for the deleted item if(this.referenceIntegrity){ //First scan all the attributes of this items for references and clean them up in the map //As this item is going away, no need to track its references anymore. //Get the attributes list before we generate the backup so it //doesn't pollute the attributes list. var attributes = this.getAttributes(item); //Backup the map, we'll have to restore it potentially, in a revert. if(item[this._reverseRefMap]){ item["backup_" + this._reverseRefMap] = dojo.clone(item[this._reverseRefMap]); } //TODO: This causes a reversion problem. This list won't be restored on revert since it is //attached to the 'value'. item, not ours. Need to back tese up somehow too. //Maybe build a map of the backup of the entries and attach it to the deleted item to be restored //later. Or just record them and call _addReferenceToMap on them in revert. dojo.forEach(attributes, function(attribute){ dojo.forEach(this.getValues(item, attribute), function(value){ if(this.isItem(value)){ //We have to back up all the references we had to others so they can be restored on a revert. if(!item["backupRefs_" + this._reverseRefMap]){ item["backupRefs_" + this._reverseRefMap] = []; } item["backupRefs_" + this._reverseRefMap].push({id: this.getIdentity(value), attr: attribute}); this._removeReferenceFromMap(value, item, attribute); } }, this); }, this); //Next, see if we have references to this item, if we do, we have to clean them up too. var references = item[this._reverseRefMap]; if(references){ //Look through all the items noted as references to clean them up. for(var itemId in references){ var containingItem = null; if(this._itemsByIdentity){ containingItem = this._itemsByIdentity[itemId]; }else{ containingItem = this._arrayOfAllItems[itemId]; } //We have a reference to a containing item, now we have to process the //attributes and clear all references to the item being deleted. if(containingItem){ for(var attribute in references[itemId]){ var oldValues = this.getValues(containingItem, attribute) || []; var newValues = dojo.filter(oldValues, function(possibleItem){ return !(this.isItem(possibleItem) && this.getIdentity(possibleItem) == identity); }, this); //Remove the note of the reference to the item and set the values on the modified attribute. this._removeReferenceFromMap(item, containingItem, attribute); if(newValues.length < oldValues.length){ this._setValueOrValues(containingItem, attribute, newValues, true); } } } } } } this._arrayOfAllItems[indexInArrayOfAllItems] = null; item[this._storeRefPropName] = null; if(this._itemsByIdentity){ delete this._itemsByIdentity[identity]; } this._pending._deletedItems[identity] = item; //Remove from the toplevel items, if necessary... if(item[this._rootItemPropName]){ this._removeArrayElement(this._arrayOfTopLevelItems, item); } this.onDelete(item); // dojo.data.api.Notification call return true; }, setValue: function(/* item */ item, /* attribute-name-string */ attribute, /* almost anything */ value){ // summary: See dojo.data.api.Write.set() return this._setValueOrValues(item, attribute, value, true); // boolean }, setValues: function(/* item */ item, /* attribute-name-string */ attribute, /* array */ values){ // summary: See dojo.data.api.Write.setValues() return this._setValueOrValues(item, attribute, values, true); // boolean }, unsetAttribute: function(/* item */ item, /* attribute-name-string */ attribute){ // summary: See dojo.data.api.Write.unsetAttribute() return this._setValueOrValues(item, attribute, [], true); }, _setValueOrValues: function(/* item */ item, /* attribute-name-string */ attribute, /* anything */ newValueOrValues, /*boolean?*/ callOnSet){ this._assert(!this._saveInProgress); // Check for valid arguments this._assertIsItem(item); this._assert(dojo.isString(attribute)); this._assert(typeof newValueOrValues !== "undefined"); // Make sure the user isn't trying to change the item's identity var identifierAttribute = this._getIdentifierAttribute(); if(attribute == identifierAttribute){ throw new Error("ItemFileWriteStore does not have support for changing the value of an item's identifier."); } // To implement the Notification API, we need to make a note of what // the old attribute value was, so that we can pass that info when // we call the onSet method. var oldValueOrValues = this._getValueOrValues(item, attribute); var identity = this.getIdentity(item); if(!this._pending._modifiedItems[identity]){ // Before we actually change the item, we make a copy of it to // record the original state, so that we'll be able to revert if // the revert method gets called. If the item has already been // modified then there's no need to do this now, since we already // have a record of the original state. var copyOfItemState = {}; for(var key in item){ if((key === this._storeRefPropName) || (key === this._itemNumPropName) || (key === this._rootItemPropName)){ copyOfItemState[key] = item[key]; }else if(key === this._reverseRefMap){ copyOfItemState[key] = dojo.clone(item[key]); }else{ copyOfItemState[key] = item[key].slice(0, item[key].length); } } // Now mark the item as dirty, and save the copy of the original state this._pending._modifiedItems[identity] = copyOfItemState; } // Okay, now we can actually change this attribute on the item var success = false; if(dojo.isArray(newValueOrValues) && newValueOrValues.length === 0){ // If we were passed an empty array as the value, that counts // as "unsetting" the attribute, so we need to remove this // attribute from the item. success = delete item[attribute]; newValueOrValues = undefined; // used in the onSet Notification call below if(this.referenceIntegrity && oldValueOrValues){ var oldValues = oldValueOrValues; if(!dojo.isArray(oldValues)){ oldValues = [oldValues]; } for(var i = 0; i < oldValues.length; i++){ var value = oldValues[i]; if(this.isItem(value)){ this._removeReferenceFromMap(value, item, attribute); } } } }else{ var newValueArray; if(dojo.isArray(newValueOrValues)){ var newValues = newValueOrValues; // Unfortunately, it's not safe to just do this: // newValueArray = newValues; // Instead, we need to copy the array, which slice() does very nicely. // This is so that our internal data structure won't // get corrupted if the user mucks with the values array *after* // calling setValues(). newValueArray = newValueOrValues.slice(0, newValueOrValues.length); }else{ newValueArray = [newValueOrValues]; } //We need to handle reference integrity if this is on. //In the case of set, we need to see if references were added or removed //and update the reference tracking map accordingly. if(this.referenceIntegrity){ if(oldValueOrValues){ var oldValues = oldValueOrValues; if(!dojo.isArray(oldValues)){ oldValues = [oldValues]; } //Use an associative map to determine what was added/removed from the list. //Should be O(n) performant. First look at all the old values and make a list of them //Then for any item not in the old list, we add it. If it was already present, we remove it. //Then we pass over the map and any references left it it need to be removed (IE, no match in //the new values list). var map = {}; dojo.forEach(oldValues, function(possibleItem){ if(this.isItem(possibleItem)){ var id = this.getIdentity(possibleItem); map[id.toString()] = true; } }, this); dojo.forEach(newValueArray, function(possibleItem){ if(this.isItem(possibleItem)){ var id = this.getIdentity(possibleItem); if(map[id.toString()]){ delete map[id.toString()]; }else{ this._addReferenceToMap(possibleItem, item, attribute); } } }, this); for(var rId in map){ var removedItem; if(this._itemsByIdentity){ removedItem = this._itemsByIdentity[rId]; }else{ removedItem = this._arrayOfAllItems[rId]; } this._removeReferenceFromMap(removedItem, item, attribute); } }else{ //Everything is new (no old values) so we have to just //insert all the references, if any. for(var i = 0; i < newValueArray.length; i++){ var value = newValueArray[i]; if(this.isItem(value)){ this._addReferenceToMap(value, item, attribute); } } } } item[attribute] = newValueArray; success = true; } // Now we make the dojo.data.api.Notification call if(callOnSet){ this.onSet(item, attribute, oldValueOrValues, newValueOrValues); } return success; // boolean }, _addReferenceToMap: function(/*item*/ refItem, /*item*/ parentItem, /*string*/ attribute){ // summary: // Method to add an reference map entry for an item and attribute. // description: // Method to add an reference map entry for an item and attribute. // // refItem: // The item that is referenced. // parentItem: // The item that holds the new reference to refItem. // attribute: // The attribute on parentItem that contains the new reference. var parentId = this.getIdentity(parentItem); var references = refItem[this._reverseRefMap]; if(!references){ references = refItem[this._reverseRefMap] = {}; } var itemRef = references[parentId]; if(!itemRef){ itemRef = references[parentId] = {}; } itemRef[attribute] = true; }, _removeReferenceFromMap: function(/* item */ refItem, /* item */ parentItem, /*strin*/ attribute){ // summary: // Method to remove an reference map entry for an item and attribute. // description: // Method to remove an reference map entry for an item and attribute. This will // also perform cleanup on the map such that if there are no more references at all to // the item, its reference object and entry are removed. // // refItem: // The item that is referenced. // parentItem: // The item holding a reference to refItem. // attribute: // The attribute on parentItem that contains the reference. var identity = this.getIdentity(parentItem); var references = refItem[this._reverseRefMap]; var itemId; if(references){ for(itemId in references){ if(itemId == identity){ delete references[itemId][attribute]; if(this._isEmpty(references[itemId])){ delete references[itemId]; } } } if(this._isEmpty(references)){ delete refItem[this._reverseRefMap]; } } }, _dumpReferenceMap: function(){ // summary: // Function to dump the reverse reference map of all items in the store for debug purposes. // description: // Function to dump the reverse reference map of all items in the store for debug purposes. var i; for(i = 0; i < this._arrayOfAllItems.length; i++){ var item = this._arrayOfAllItems[i]; if(item && item[this._reverseRefMap]){ console.log("Item: [" + this.getIdentity(item) + "] is referenced by: " + dojo.toJson(item[this._reverseRefMap])); } } }, _getValueOrValues: function(/* item */ item, /* attribute-name-string */ attribute){ var valueOrValues = undefined; if(this.hasAttribute(item, attribute)){ var valueArray = this.getValues(item, attribute); if(valueArray.length == 1){ valueOrValues = valueArray[0]; }else{ valueOrValues = valueArray; } } return valueOrValues; }, _flatten: function(/* anything */ value){ if(this.isItem(value)){ var item = value; // Given an item, return an serializable object that provides a // reference to the item. // For example, given kermit: // var kermit = store.newItem({id:2, name:"Kermit"}); // we want to return // {_reference:2} var identity = this.getIdentity(item); var referenceObject = {_reference: identity}; return referenceObject; }else{ if(typeof value === "object"){ for(var type in this._datatypeMap){ var typeMap = this._datatypeMap[type]; if(dojo.isObject(typeMap) && !dojo.isFunction(typeMap)){ if(value instanceof typeMap.type){ if(!typeMap.serialize){ throw new Error("ItemFileWriteStore: No serializer defined for type mapping: [" + type + "]"); } return {_type: type, _value: typeMap.serialize(value)}; } } else if(value instanceof typeMap){ //SImple mapping, therefore, return as a toString serialization. return {_type: type, _value: value.toString()}; } } } return value; } }, _getNewFileContentString: function(){ // summary: // Generate a string that can be saved to a file. // The result should look similar to: // http://trac.dojotoolkit.org/browser/dojo/trunk/tests/data/countries.json var serializableStructure = {}; var identifierAttribute = this._getIdentifierAttribute(); if(identifierAttribute !== Number){ serializableStructure.identifier = identifierAttribute; } if(this._labelAttr){ serializableStructure.label = this._labelAttr; } serializableStructure.items = []; for(var i = 0; i < this._arrayOfAllItems.length; ++i){ var item = this._arrayOfAllItems[i]; if(item !== null){ var serializableItem = {}; for(var key in item){ if(key !== this._storeRefPropName && key !== this._itemNumPropName && key !== this._reverseRefMap && key !== this._rootItemPropName){ var attribute = key; var valueArray = this.getValues(item, attribute); if(valueArray.length == 1){ serializableItem[attribute] = this._flatten(valueArray[0]); }else{ var serializableArray = []; for(var j = 0; j < valueArray.length; ++j){ serializableArray.push(this._flatten(valueArray[j])); serializableItem[attribute] = serializableArray; } } } } serializableStructure.items.push(serializableItem); } } var prettyPrint = true; return dojo.toJson(serializableStructure, prettyPrint); }, _isEmpty: function(something){ // summary: // Function to determine if an array or object has no properties or values. // something: // The array or object to examine. var empty = true; if(dojo.isObject(something)){ var i; for(i in something){ empty = false; break; } }else if(dojo.isArray(something)){ if(something.length > 0){ empty = false; } } return empty; //boolean }, save: function(/* object */ keywordArgs){ // summary: See dojo.data.api.Write.save() this._assert(!this._saveInProgress); // this._saveInProgress is set to true, briefly, from when save is first called to when it completes this._saveInProgress = true; var self = this; var saveCompleteCallback = function(){ self._pending = { _newItems:{}, _modifiedItems:{}, _deletedItems:{} }; self._saveInProgress = false; // must come after this._pending is cleared, but before any callbacks if(keywordArgs && keywordArgs.onComplete){ var scope = keywordArgs.scope || dojo.global; keywordArgs.onComplete.call(scope); } }; var saveFailedCallback = function(err){ self._saveInProgress = false; if(keywordArgs && keywordArgs.onError){ var scope = keywordArgs.scope || dojo.global; keywordArgs.onError.call(scope, err); } }; if(this._saveEverything){ var newFileContentString = this._getNewFileContentString(); this._saveEverything(saveCompleteCallback, saveFailedCallback, newFileContentString); } if(this._saveCustom){ this._saveCustom(saveCompleteCallback, saveFailedCallback); } if(!this._saveEverything && !this._saveCustom){ // Looks like there is no user-defined save-handler function. // That's fine, it just means the datastore is acting as a "mock-write" // store -- changes get saved in memory but don't get saved to disk. saveCompleteCallback(); } }, revert: function(){ // summary: See dojo.data.api.Write.revert() this._assert(!this._saveInProgress); var identity; for(identity in this._pending._modifiedItems){ // find the original item and the modified item that replaced it var copyOfItemState = this._pending._modifiedItems[identity]; var modifiedItem = null; if(this._itemsByIdentity){ modifiedItem = this._itemsByIdentity[identity]; }else{ modifiedItem = this._arrayOfAllItems[identity]; } // Restore the original item into a full-fledged item again, we want to try to // keep the same object instance as if we don't it, causes bugs like #9022. copyOfItemState[this._storeRefPropName] = this; for(key in modifiedItem){ delete modifiedItem[key]; } dojo.mixin(modifiedItem, copyOfItemState); } var deletedItem; for(identity in this._pending._deletedItems){ deletedItem = this._pending._deletedItems[identity]; deletedItem[this._storeRefPropName] = this; var index = deletedItem[this._itemNumPropName]; //Restore the reverse refererence map, if any. if(deletedItem["backup_" + this._reverseRefMap]){ deletedItem[this._reverseRefMap] = deletedItem["backup_" + this._reverseRefMap]; delete deletedItem["backup_" + this._reverseRefMap]; } this._arrayOfAllItems[index] = deletedItem; if(this._itemsByIdentity){ this._itemsByIdentity[identity] = deletedItem; } if(deletedItem[this._rootItemPropName]){ this._arrayOfTopLevelItems.push(deletedItem); } } //We have to pass through it again and restore the reference maps after all the //undeletes have occurred. for(identity in this._pending._deletedItems){ deletedItem = this._pending._deletedItems[identity]; if(deletedItem["backupRefs_" + this._reverseRefMap]){ dojo.forEach(deletedItem["backupRefs_" + this._reverseRefMap], function(reference){ var refItem; if(this._itemsByIdentity){ refItem = this._itemsByIdentity[reference.id]; }else{ refItem = this._arrayOfAllItems[reference.id]; } this._addReferenceToMap(refItem, deletedItem, reference.attr); }, this); delete deletedItem["backupRefs_" + this._reverseRefMap]; } } for(identity in this._pending._newItems){ var newItem = this._pending._newItems[identity]; newItem[this._storeRefPropName] = null; // null out the new item, but don't change the array index so // so we can keep using _arrayOfAllItems.length. this._arrayOfAllItems[newItem[this._itemNumPropName]] = null; if(newItem[this._rootItemPropName]){ this._removeArrayElement(this._arrayOfTopLevelItems, newItem); } if(this._itemsByIdentity){ delete this._itemsByIdentity[identity]; } } this._pending = { _newItems:{}, _modifiedItems:{}, _deletedItems:{} }; return true; // boolean }, isDirty: function(/* item? */ item){ // summary: See dojo.data.api.Write.isDirty() if(item){ // return true if the item is dirty var identity = this.getIdentity(item); return new Boolean(this._pending._newItems[identity] || this._pending._modifiedItems[identity] || this._pending._deletedItems[identity]).valueOf(); // boolean }else{ // return true if the store is dirty -- which means return true // if there are any new items, dirty items, or modified items if(!this._isEmpty(this._pending._newItems) || !this._isEmpty(this._pending._modifiedItems) || !this._isEmpty(this._pending._deletedItems)){ return true; } return false; // boolean } }, /* dojo.data.api.Notification */ onSet: function(/* item */ item, /*attribute-name-string*/ attribute, /*object | array*/ oldValue, /*object | array*/ newValue){ // summary: See dojo.data.api.Notification.onSet() // No need to do anything. This method is here just so that the // client code can connect observers to it. }, onNew: function(/* item */ newItem, /*object?*/ parentInfo){ // summary: See dojo.data.api.Notification.onNew() // No need to do anything. This method is here just so that the // client code can connect observers to it. }, onDelete: function(/* item */ deletedItem){ // summary: See dojo.data.api.Notification.onDelete() // No need to do anything. This method is here just so that the // client code can connect observers to it. }, close: function(/* object? */ request){ // summary: // Over-ride of base close function of ItemFileReadStore to add in check for store state. // description: // Over-ride of base close function of ItemFileReadStore to add in check for store state. // If the store is still dirty (unsaved changes), then an error will be thrown instead of // clearing the internal state for reload from the url. //Clear if not dirty ... or throw an error if(this.clearOnClose){ if(!this.isDirty()){ this.inherited(arguments); }else{ //Only throw an error if the store was dirty and we were loading from a url (cannot reload from url until state is saved). throw new Error("dojo.data.ItemFileWriteStore: There are unsaved changes present in the store. Please save or revert the changes before invoking close."); } } } }); } dojo.i18n._preloadLocalizations("dojo.nls.tt-rss-layer", ["ROOT","ar","ca","cs","da","de","de-de","el","en","en-gb","en-us","es","es-es","fi","fi-fi","fr","fr-fr","he","he-il","hu","it","it-it","ja","ja-jp","ko","ko-kr","nb","nl","nl-nl","pl","pt","pt-br","pt-pt","ru","sk","sl","sv","th","tr","xx","zh","zh-cn","zh-tw"]);