summaryrefslogtreecommitdiff
path: root/lib/dijit/form/_FormSelectWidget.js.uncompressed.js
diff options
context:
space:
mode:
Diffstat (limited to 'lib/dijit/form/_FormSelectWidget.js.uncompressed.js')
-rw-r--r--lib/dijit/form/_FormSelectWidget.js.uncompressed.js707
1 files changed, 707 insertions, 0 deletions
diff --git a/lib/dijit/form/_FormSelectWidget.js.uncompressed.js b/lib/dijit/form/_FormSelectWidget.js.uncompressed.js
new file mode 100644
index 000000000..4a40c51d0
--- /dev/null
+++ b/lib/dijit/form/_FormSelectWidget.js.uncompressed.js
@@ -0,0 +1,707 @@
+define("dijit/form/_FormSelectWidget", [
+ "dojo/_base/array", // array.filter array.forEach array.map array.some
+ "dojo/_base/Deferred",
+ "dojo/aspect", // aspect.after
+ "dojo/data/util/sorter", // util.sorter.createSortFunction
+ "dojo/_base/declare", // declare
+ "dojo/dom", // dom.setSelectable
+ "dojo/dom-class", // domClass.toggle
+ "dojo/_base/kernel", // _scopeName
+ "dojo/_base/lang", // lang.delegate lang.isArray lang.isObject lang.hitch
+ "dojo/query", // query
+ "dojo/when",
+ "dojo/store/util/QueryResults",
+ "./_FormValueWidget"
+], function(array, Deferred, aspect, sorter, declare, dom, domClass, kernel, lang, query, when,
+ QueryResults, _FormValueWidget){
+
+// module:
+// dijit/form/_FormSelectWidget
+
+/*=====
+var __SelectOption = {
+ // 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
+};
+=====*/
+
+var _FormSelectWidget = declare("dijit.form._FormSelectWidget", _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: __SelectOption[]
+ // The set of options for our select item. Roughly corresponds to
+ // the html `<option>` tag.
+ options: null,
+
+ // store: dojo/store/api/Store
+ // A store to use for getting our list of options - rather than reading them
+ // from the `<option>` html tags. Should support getIdentity().
+ // For back-compat store can also be a dojo/data/api/Identity.
+ 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,
+
+ // labelAttr: String?
+ // The entries in the drop down list come from this attribute in the dojo.store items.
+ // If ``store`` is set, labelAttr must be set too, unless store is an old-style
+ // dojo.data store rather than a new dojo/store.
+ labelAttr: "",
+
+ // onFetch: Function
+ // A callback to do with an onFetch - but before any items are actually
+ // iterated over (i.e. to filter even further 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,
+
+ // onLoadDeferred: [readonly] dojo.Deferred
+ // This is the `dojo.Deferred` returned by setStore().
+ // Calling onLoadDeferred.then() registers your
+ // callback to be called only once, when the prior setStore completes.
+ onLoadDeferred: null,
+
+ 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/_FormSelectWidget.__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/_FormSelectWidget.__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/_FormSelectWidget.__SelectOption is passed in which is not a part of the select
+
+ // NOTE: the compare for passing in a dijit/form/_FormSelectWidget.__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; // __SelectOption[]
+ }
+ if(lang.isArray(lookupValue)){
+ return array.map(lookupValue, "return this.getOptions(item);", this); // __SelectOption[]
+ }
+ if(lang.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(!array.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]; // __SelectOption
+ }
+ return null; // null
+ },
+
+ addOption: function(/*__SelectOption|__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(!lang.isArray(option)){ option = [option]; }
+ array.forEach(option, function(i){
+ if(i && lang.isObject(i)){
+ this.options.push(i);
+ }
+ }, this);
+ this._loadChildren();
+ },
+
+ removeOption: function(/*String|__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(!lang.isArray(valueOrIdx)){ valueOrIdx = [valueOrIdx]; }
+ var oldOpts = this.getOptions(valueOrIdx);
+ array.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 = array.filter(this.options, function(node){
+ return (node.value !== i.value || node.label !== i.label);
+ });
+ this._removeOptionItem(i);
+ }
+ }, this);
+ this._loadChildren();
+ },
+
+ updateOption: function(/*__SelectOption|__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 yield better performance since
+ // the children will only be loaded once.
+ if(!lang.isArray(newOption)){ newOption = [newOption]; }
+ array.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(store,
+ selectedValue,
+ 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/store/api/Store
+ // The dojo.store you would like to use - it MUST implement getIdentity()
+ // and MAY implement observe().
+ // For backwards-compatibility this can also be a data.data store, in which case
+ // it MUST implement dojo/data/api/Identity,
+ // and MAY implement dojo/data/api/Notification.
+ // selectedValue: anything?
+ // The value that this widget should set itself to *after* the store
+ // has been loaded
+ // fetchArgs: Object?
+ // Hash of parameters to set filter on store, etc.
+ //
+ // - query: new value for Select.query,
+ // - queryOptions: new value for Select.queryOptions,
+ // - onFetch: callback function for each item in data (Deprecated)
+ var oStore = this.store;
+ fetchArgs = fetchArgs || {};
+
+ if(oStore !== store){
+ // Our store has changed, so cancel any listeners on old store (remove for 2.0)
+ var h;
+ while((h = this._notifyConnections.pop())){ h.remove(); }
+
+ // For backwards-compatibility, accept dojo.data store in addition to dojo.store.store. Remove in 2.0.
+ if(!store.get){
+ lang.mixin(store, {
+ _oldAPI: true,
+ get: function(id){
+ // summary:
+ // Retrieves an object by it's identity. This will trigger a fetchItemByIdentity.
+ // Like dojo.store.DataStore.get() except returns native item.
+ var deferred = new Deferred();
+ this.fetchItemByIdentity({
+ identity: id,
+ onItem: function(object){
+ deferred.resolve(object);
+ },
+ onError: function(error){
+ deferred.reject(error);
+ }
+ });
+ return deferred.promise;
+ },
+ query: function(query, options){
+ // summary:
+ // Queries the store for objects. Like dojo/store/DataStore.query()
+ // except returned Deferred contains array of native items.
+ var deferred = new Deferred(function(){ if(fetchHandle.abort){ fetchHandle.abort(); } } );
+ deferred.total = new Deferred();
+ var fetchHandle = this.fetch(lang.mixin({
+ query: query,
+ onBegin: function(count){
+ deferred.total.resolve(count);
+ },
+ onComplete: function(results){
+ deferred.resolve(results);
+ },
+ onError: function(error){
+ deferred.reject(error);
+ }
+ }, options));
+ return new QueryResults(deferred);
+ }
+ });
+
+ if(store.getFeatures()["dojo.data.api.Notification"]){
+ this._notifyConnections = [
+ aspect.after(store, "onNew", lang.hitch(this, "_onNewItem"), true),
+ aspect.after(store, "onDelete", lang.hitch(this, "_onDeleteItem"), true),
+ aspect.after(store, "onSet", lang.hitch(this, "_onSetItem"), true)
+ ];
+ }
+ }
+ this._set("store", store); // Our store has changed, so update our notifications
+ }
+
+ // Remove existing options (if there are any)
+ if(this.options && this.options.length){
+ this.removeOption(this.options);
+ }
+
+ // Cancel listener for updates to old store
+ if(this._queryRes && this._queryRes.close){
+ this._queryRes.close();
+ }
+
+ // If user has specified new query and query options along with this new store, then use them.
+ if(fetchArgs.query){
+ this._set("query", fetchArgs.query);
+ this._set("queryOptions", fetchArgs.queryOptions);
+ }
+
+ // Add our new options
+ if(store){
+ this._loadingStore = true;
+ this.onLoadDeferred = new Deferred();
+
+ // Run query
+ // Save result in this._queryRes so we can cancel the listeners we register below
+ this._queryRes = store.query(this.query, this.queryOptions);
+ when(this._queryRes, lang.hitch(this, function(items){
+
+ if(this.sortByLabel && !fetchArgs.sort && items.length){
+ if(items[0].getValue){
+ // Old dojo.data API to access items, remove for 2.0
+ items.sort(sorter.createSortFunction([{
+ attribute: store.getLabelAttributes(items[0])[0]
+ }], store));
+ }else{
+ var labelAttr = this.labelAttr;
+ items.sort(function(a, b){
+ return a[labelAttr] > b[labelAttr] ? 1 : b[labelAttr] > a[labelAttr] ? -1 : 0;
+ });
+ }
+ }
+
+ if(fetchArgs.onFetch){
+ items = fetchArgs.onFetch.call(this, items, fetchArgs);
+ }
+
+ // TODO: Add these guys as a batch, instead of separately
+ array.forEach(items, function(i){
+ this._addOptionForItem(i);
+ }, this);
+
+ // Register listener for store updates
+ if(this._queryRes.observe){
+ this._queryRes.observe(lang.hitch(this, function(object, deletedFrom, insertedInto){
+ if(deletedFrom == insertedInto){
+ this._onSetItem(object);
+ }else{
+ if(deletedFrom != -1){
+ this._onDeleteItem(object);
+ }
+ if(insertedInto != -1){
+ this._onNewItem(object);
+ }
+ }
+ }), true);
+ }
+
+ // 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.onLoadDeferred.resolve(true);
+ this.onSetStore();
+ }), function(err){
+ console.error('dijit.form.Select: ' + err.toString());
+ this.onLoadDeferred.reject(err);
+ });
+ }
+ 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._onChangeActive){ priorityChange = null; }
+ 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(!lang.isArray(newValue)){
+ newValue = [newValue];
+ }
+ array.forEach(newValue, function(i, idx){
+ if(!lang.isObject(i)){
+ i = i + "";
+ }
+ if(typeof i === "string"){
+ newValue[idx] = array.filter(opts, function(node){
+ return node.value === i;
+ })[0] || {value: "", label: ""};
+ }
+ }, this);
+
+ // Make sure some sane default is set
+ newValue = array.filter(newValue, function(i){ return i && i.value; });
+ if(!this.multiple && (!newValue[0] || !newValue[0].value) && opts.length){
+ newValue[0] = opts[0];
+ }
+ array.forEach(opts, function(i){
+ i.selected = array.some(newValue, function(v){ return v.value === i.value; });
+ });
+ var val = array.map(newValue, function(i){ return i.value; }),
+ disp = array.map(newValue, function(i){ return i.label; });
+
+ if(typeof val == "undefined" || typeof val[0] == "undefined"){ return; } // not fully initialized yet or a failed value lookup
+ this._setDisplay(this.multiple ? disp : disp[0]);
+ this.inherited(arguments, [ this.multiple ? val : val[0], priorityChange ]);
+ this._updateSelection();
+ },
+
+ _getDisplayedValueAttr: function(){
+ // summary:
+ // returns the displayed value of the widget
+ var val = this.get("value");
+ if(!lang.isArray(val)){
+ val = [val];
+ }
+ var ret = array.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; }
+ array.forEach(this._getChildren(), function(child){
+ child.destroyRecursive();
+ });
+ // Add each menu item
+ array.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(!lang.isArray(val)){
+ val = [val];
+ }
+ if(val && val[0]){
+ array.forEach(this._getChildren(), function(child){
+ var isSelected = array.some(val, function(v){
+ return child.option && (v === child.option.value);
+ });
+ domClass.toggle(child.domNode, this.baseClass.replace(/\s+|$/g, "SelectedOption "), isSelected);
+ child.domNode.setAttribute("aria-selected", isSelected ? "true" : "false");
+ }, 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 = array.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 array.map(array.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.
+
+ // remove getLabel() call for 2.0 (it's to support the old dojo.data API)
+ var store = this.store,
+ label = (this.labelAttr && this.labelAttr in item) ? item[this.labelAttr] : store.getLabel(item),
+ value = (label ? store.getIdentity(item) : null);
+ return {value: value, label: label, item: item}; // __SelectOption
+ },
+
+ _addOptionForItem: function(/*item*/ item){
+ // summary:
+ // Creates (and adds) the option for the given item
+ var store = this.store;
+ if(store.isItemLoaded && !store.isItemLoaded(item)){
+ // We are not loaded - so let's load it and add later.
+ // Remove for 2.0 (it's the old dojo.data API)
+ store.loadItem({item: item, onItem: function(i){
+ this._addOptionForItem(i);
+ },
+ scope: this});
+ return;
+ }
+ var newOpt = this._getOptionObjForItem(item);
+ this.addOption(newOpt);
+ },
+
+ constructor: function(params /*===== , srcNodeRef =====*/){
+ // summary:
+ // Create the widget.
+ // params: Object|null
+ // Hash of initialization parameters for widget, including scalar values (like title, duration etc.)
+ // and functions, typically callbacks like onClick.
+ // The hash can contain any of the widget's properties, excluding read-only properties.
+ // srcNodeRef: DOMNode|String?
+ // If a srcNodeRef (DOM node) is specified, replace srcNodeRef with my generated DOM tree
+
+ // 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 = (params || {}).value || null;
+ this._notifyConnections = []; // remove for 2.0
+ },
+
+ buildRendering: function(){
+ this.inherited(arguments);
+ dom.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.
+ if(!this.options){
+ this.options =
+ this.srcNodeRef
+ ? query("> *", this.srcNodeRef).map(
+ function(node){
+ if(node.getAttribute("type") === "separator"){
+ return { value: "", label: "", selected: false, disabled: false };
+ }
+ return {
+ value: (node.getAttribute("data-" + kernel._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");
+
+ // moved from startup
+ // Connects in our store, if we have one defined
+ var store = this.store;
+ if(store && (store.getIdentity || 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);
+ }
+ },
+
+ startup: function(){
+ // summary:
+ this._loadChildren();
+ this.inherited(arguments);
+ },
+
+ destroy: function(){
+ // summary:
+ // Clean up our connections
+
+ var h;
+ while((h = this._notifyConnections.pop())){ h.remove(); }
+
+ // Cancel listener for store updates
+ if(this._queryRes && this._queryRes.close){
+ this._queryRes.close();
+ }
+
+ this.inherited(arguments);
+ },
+
+ _addOptionItem: function(/*__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(/*__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
+ }
+});
+
+/*=====
+_FormSelectWidget.__SelectOption = __SelectOption;
+=====*/
+
+return _FormSelectWidget;
+
+});