]>
git.wh0rd.org - tt-rss.git/blob - lib/dijit/form/_FormSelectWidget.js.uncompressed.js
1 define("dijit/form/_FormSelectWidget", [
2 "dojo/_base/array", // array.filter array.forEach array.map array.some
4 "dojo/aspect", // aspect.after
5 "dojo/data/util/sorter", // util.sorter.createSortFunction
6 "dojo/_base/declare", // declare
7 "dojo/dom", // dom.setSelectable
8 "dojo/dom-class", // domClass.toggle
9 "dojo/_base/kernel", // _scopeName
10 "dojo/_base/lang", // lang.delegate lang.isArray lang.isObject lang.hitch
11 "dojo/query", // query
13 "dojo/store/util/QueryResults",
15 ], function(array
, Deferred
, aspect
, sorter
, declare
, dom
, domClass
, kernel
, lang
, query
, when
,
16 QueryResults
, _FormValueWidget
){
19 // dijit/form/_FormSelectWidget
22 var __SelectOption = {
24 // The value of the option. Setting to empty (or missing) will
25 // place a separator at that location
27 // The label for our option. It can contain html tags.
29 // Whether or not we are a selected option
31 // Whether or not this specific option is disabled
35 var _FormSelectWidget
= declare("dijit.form._FormSelectWidget", _FormValueWidget
, {
37 // Extends _FormValueWidget in order to provide "select-specific"
38 // values - i.e., those values that are unique to `<select>` elements.
39 // This also provides the mechanism for reading the elements from
40 // a store, if desired.
42 // multiple: [const] Boolean
43 // Whether or not we are multi-valued
46 // options: __SelectOption[]
47 // The set of options for our select item. Roughly corresponds to
48 // the html `<option>` tag.
51 // store: dojo/store/api/Store
52 // A store to use for getting our list of options - rather than reading them
53 // from the `<option>` html tags. Should support getIdentity().
54 // For back-compat store can also be a dojo/data/api/Identity.
58 // A query to use when fetching items from our store
61 // queryOptions: object
62 // Query options to use when fetching from the store
66 // The entries in the drop down list come from this attribute in the dojo.store items.
67 // If ``store`` is set, labelAttr must be set too, unless store is an old-style
68 // dojo.data store rather than a new dojo/store.
72 // A callback to do with an onFetch - but before any items are actually
73 // iterated over (i.e. to filter even further what you want to add)
76 // sortByLabel: Boolean
77 // Flag to sort the options returned from a store by the label of
82 // loadChildrenOnOpen: Boolean
83 // By default loadChildren is called when the items are fetched from the
84 // store. This property allows delaying loadChildren (and the creation
85 // of the options/menuitems) until the user clicks the button to open the
87 loadChildrenOnOpen
: false,
89 // onLoadDeferred: [readonly] dojo.Deferred
90 // This is the `dojo.Deferred` returned by setStore().
91 // Calling onLoadDeferred.then() registers your
92 // callback to be called only once, when the prior setStore completes.
95 getOptions: function(/*anything*/ valueOrIdx
){
97 // Returns a given option (or options).
99 // If passed in as a string, that string is used to look up the option
100 // in the array of options - based on the value property.
101 // (See dijit/form/_FormSelectWidget.__SelectOption).
103 // If passed in a number, then the option with the given index (0-based)
104 // within this select will be returned.
106 // If passed in a dijit/form/_FormSelectWidget.__SelectOption, the same option will be
107 // returned if and only if it exists within this select.
109 // If passed an array, then an array will be returned with each element
110 // in the array being looked up.
112 // If not passed a value, then all options will be returned
115 // The option corresponding with the given value or index. null
116 // is returned if any of the following are true:
118 // - A string value is passed in which doesn't exist
119 // - An index is passed in which is outside the bounds of the array of options
120 // - A dijit/form/_FormSelectWidget.__SelectOption is passed in which is not a part of the select
122 // NOTE: the compare for passing in a dijit/form/_FormSelectWidget.__SelectOption checks
123 // if the value property matches - NOT if the exact option exists
124 // NOTE: if passing in an array, null elements will be placed in the returned
125 // array when a value is not found.
126 var lookupValue
= valueOrIdx
, opts
= this.options
|| [], l
= opts
.length
;
128 if(lookupValue
=== undefined){
129 return opts
; // __SelectOption[]
131 if(lang
.isArray(lookupValue
)){
132 return array
.map(lookupValue
, "return this.getOptions(item);", this); // __SelectOption[]
134 if(lang
.isObject(valueOrIdx
)){
135 // We were passed an option - so see if it's in our array (directly),
136 // and if it's not, try and find it by value.
137 if(!array
.some(this.options
, function(o
, idx
){
138 if(o
=== lookupValue
||
139 (o
.value
&& o
.value
=== lookupValue
.value
)){
148 if(typeof lookupValue
== "string"){
149 for(var i
=0; i
<l
; i
++){
150 if(opts
[i
].value
=== lookupValue
){
156 if(typeof lookupValue
== "number" && lookupValue
>= 0 && lookupValue
< l
){
157 return this.options
[lookupValue
]; // __SelectOption
162 addOption: function(/*__SelectOption|__SelectOption[]*/ option
){
164 // Adds an option or options to the end of the select. If value
165 // of the option is empty or missing, a separator is created instead.
166 // Passing in an array of options will yield slightly better performance
167 // since the children are only loaded once.
168 if(!lang
.isArray(option
)){ option
= [option
]; }
169 array
.forEach(option
, function(i
){
170 if(i
&& lang
.isObject(i
)){
171 this.options
.push(i
);
174 this._loadChildren();
177 removeOption: function(/*String|__SelectOption|Number|Array*/ valueOrIdx
){
179 // Removes the given option or options. You can remove by string
180 // (in which case the value is removed), number (in which case the
181 // index in the options array is removed), or select option (in
182 // which case, the select option with a matching value is removed).
183 // You can also pass in an array of those values for a slightly
184 // better performance since the children are only loaded once.
185 if(!lang
.isArray(valueOrIdx
)){ valueOrIdx
= [valueOrIdx
]; }
186 var oldOpts
= this.getOptions(valueOrIdx
);
187 array
.forEach(oldOpts
, function(i
){
188 // We can get null back in our array - if our option was not found. In
189 // that case, we don't want to blow up...
191 this.options
= array
.filter(this.options
, function(node
){
192 return (node
.value
!== i
.value
|| node
.label
!== i
.label
);
194 this._removeOptionItem(i
);
197 this._loadChildren();
200 updateOption: function(/*__SelectOption|__SelectOption[]*/ newOption
){
202 // Updates the values of the given option. The option to update
203 // is matched based on the value of the entered option. Passing
204 // in an array of new options will yield better performance since
205 // the children will only be loaded once.
206 if(!lang
.isArray(newOption
)){ newOption
= [newOption
]; }
207 array
.forEach(newOption
, function(i
){
208 var oldOpt
= this.getOptions(i
), k
;
210 for(k
in i
){ oldOpt
[k
] = i
[k
]; }
213 this._loadChildren();
216 setStore: function(store
,
220 // Sets the store you would like to use with this select widget.
221 // The selected value is the value of the new store to set. This
222 // function returns the original store, in case you want to reuse
224 // store: dojo/store/api/Store
225 // The dojo.store you would like to use - it MUST implement getIdentity()
226 // and MAY implement observe().
227 // For backwards-compatibility this can also be a data.data store, in which case
228 // it MUST implement dojo/data/api/Identity,
229 // and MAY implement dojo/data/api/Notification.
230 // selectedValue: anything?
231 // The value that this widget should set itself to *after* the store
233 // fetchArgs: Object?
234 // Hash of parameters to set filter on store, etc.
236 // - query: new value for Select.query,
237 // - queryOptions: new value for Select.queryOptions,
238 // - onFetch: callback function for each item in data (Deprecated)
239 var oStore
= this.store
;
240 fetchArgs
= fetchArgs
|| {};
242 if(oStore
!== store
){
243 // Our store has changed, so cancel any listeners on old store (remove for 2.0)
245 while((h
= this._notifyConnections
.pop())){ h
.remove(); }
247 // For backwards-compatibility, accept dojo.data store in addition to dojo.store.store. Remove in 2.0.
253 // Retrieves an object by it's identity. This will trigger a fetchItemByIdentity.
254 // Like dojo.store.DataStore.get() except returns native item.
255 var deferred
= new Deferred();
256 this.fetchItemByIdentity({
258 onItem: function(object
){
259 deferred
.resolve(object
);
261 onError: function(error
){
262 deferred
.reject(error
);
265 return deferred
.promise
;
267 query: function(query
, options
){
269 // Queries the store for objects. Like dojo/store/DataStore.query()
270 // except returned Deferred contains array of native items.
271 var deferred
= new Deferred(function(){ if(fetchHandle
.abort
){ fetchHandle
.abort(); } } );
272 deferred
.total
= new Deferred();
273 var fetchHandle
= this.fetch(lang
.mixin({
275 onBegin: function(count
){
276 deferred
.total
.resolve(count
);
278 onComplete: function(results
){
279 deferred
.resolve(results
);
281 onError: function(error
){
282 deferred
.reject(error
);
285 return new QueryResults(deferred
);
289 if(store
.getFeatures()["dojo.data.api.Notification"]){
290 this._notifyConnections
= [
291 aspect
.after(store
, "onNew", lang
.hitch(this, "_onNewItem"), true),
292 aspect
.after(store
, "onDelete", lang
.hitch(this, "_onDeleteItem"), true),
293 aspect
.after(store
, "onSet", lang
.hitch(this, "_onSetItem"), true)
297 this._set("store", store
); // Our store has changed, so update our notifications
300 // Remove existing options (if there are any)
301 if(this.options
&& this.options
.length
){
302 this.removeOption(this.options
);
305 // Cancel listener for updates to old store
306 if(this._queryRes
&& this._queryRes
.close
){
307 this._queryRes
.close();
310 // If user has specified new query and query options along with this new store, then use them.
312 this._set("query", fetchArgs
.query
);
313 this._set("queryOptions", fetchArgs
.queryOptions
);
316 // Add our new options
318 this._loadingStore
= true;
319 this.onLoadDeferred
= new Deferred();
322 // Save result in this._queryRes so we can cancel the listeners we register below
323 this._queryRes
= store
.query(this.query
, this.queryOptions
);
324 when(this._queryRes
, lang
.hitch(this, function(items
){
326 if(this.sortByLabel
&& !fetchArgs
.sort
&& items
.length
){
327 if(items
[0].getValue
){
328 // Old dojo.data API to access items, remove for 2.0
329 items
.sort(sorter
.createSortFunction([{
330 attribute
: store
.getLabelAttributes(items
[0])[0]
333 var labelAttr
= this.labelAttr
;
334 items
.sort(function(a
, b
){
335 return a
[labelAttr
] > b
[labelAttr
] ? 1 : b
[labelAttr
] > a
[labelAttr
] ? -1 : 0;
340 if(fetchArgs
.onFetch
){
341 items
= fetchArgs
.onFetch
.call(this, items
, fetchArgs
);
344 // TODO: Add these guys as a batch, instead of separately
345 array
.forEach(items
, function(i
){
346 this._addOptionForItem(i
);
349 // Register listener for store updates
350 if(this._queryRes
.observe
){
351 this._queryRes
.observe(lang
.hitch(this, function(object
, deletedFrom
, insertedInto
){
352 if(deletedFrom
== insertedInto
){
353 this._onSetItem(object
);
355 if(deletedFrom
!= -1){
356 this._onDeleteItem(object
);
358 if(insertedInto
!= -1){
359 this._onNewItem(object
);
365 // Set our value (which might be undefined), and then tweak
366 // it to send a change event with the real value
367 this._loadingStore
= false;
368 this.set("value", "_pendingValue" in this ? this._pendingValue
: selectedValue
);
369 delete this._pendingValue
;
371 if(!this.loadChildrenOnOpen
){
372 this._loadChildren();
374 this._pseudoLoadChildren(items
);
376 this.onLoadDeferred
.resolve(true);
379 console
.error('dijit.form.Select: ' + err
.toString());
380 this.onLoadDeferred
.reject(err
);
383 return oStore
; // dojo/data/api/Identity
386 // TODO: implement set() and watch() for store and query, although not sure how to handle
387 // setting them individually rather than together (as in setStore() above)
389 _setValueAttr: function(/*anything*/ newValue
, /*Boolean?*/ priorityChange
){
391 // set the value of the widget.
392 // If a string is passed, then we set our value from looking it up.
393 if(!this._onChangeActive
){ priorityChange
= null; }
394 if(this._loadingStore
){
395 // Our store is loading - so save our value, and we'll set it when
397 this._pendingValue
= newValue
;
400 var opts
= this.getOptions() || [];
401 if(!lang
.isArray(newValue
)){
402 newValue
= [newValue
];
404 array
.forEach(newValue
, function(i
, idx
){
405 if(!lang
.isObject(i
)){
408 if(typeof i
=== "string"){
409 newValue
[idx
] = array
.filter(opts
, function(node
){
410 return node
.value
=== i
;
411 })[0] || {value
: "", label
: ""};
415 // Make sure some sane default is set
416 newValue
= array
.filter(newValue
, function(i
){ return i
&& i
.value
; });
417 if(!this.multiple
&& (!newValue
[0] || !newValue
[0].value
) && opts
.length
){
418 newValue
[0] = opts
[0];
420 array
.forEach(opts
, function(i
){
421 i
.selected
= array
.some(newValue
, function(v
){ return v
.value
=== i
.value
; });
423 var val
= array
.map(newValue
, function(i
){ return i
.value
; }),
424 disp
= array
.map(newValue
, function(i
){ return i
.label
; });
426 if(typeof val
== "undefined" || typeof val
[0] == "undefined"){ return; } // not fully initialized yet or a failed value lookup
427 this._setDisplay(this.multiple
? disp
: disp
[0]);
428 this.inherited(arguments
, [ this.multiple
? val
: val
[0], priorityChange
]);
429 this._updateSelection();
432 _getDisplayedValueAttr: function(){
434 // returns the displayed value of the widget
435 var val
= this.get("value");
436 if(!lang
.isArray(val
)){
439 var ret
= array
.map(this.getOptions(val
), function(v
){
440 if(v
&& "label" in v
){
447 return this.multiple
? ret
: ret
[0];
450 _loadChildren: function(){
452 // Loads the children represented by this widget's options.
453 // reset the menu to make it populatable on the next click
454 if(this._loadingStore
){ return; }
455 array
.forEach(this._getChildren(), function(child
){
456 child
.destroyRecursive();
458 // Add each menu item
459 array
.forEach(this.options
, this._addOptionItem
, this);
462 this._updateSelection();
465 _updateSelection: function(){
467 // Sets the "selected" class on the item for styling purposes
468 this._set("value", this._getValueFromOpts());
469 var val
= this.value
;
470 if(!lang
.isArray(val
)){
474 array
.forEach(this._getChildren(), function(child
){
475 var isSelected
= array
.some(val
, function(v
){
476 return child
.option
&& (v
=== child
.option
.value
);
478 domClass
.toggle(child
.domNode
, this.baseClass
.replace(/\s+|$/g, "SelectedOption "), isSelected
);
479 child
.domNode
.setAttribute("aria-selected", isSelected
? "true" : "false");
484 _getValueFromOpts: function(){
486 // Returns the value of the widget by reading the options for
488 var opts
= this.getOptions() || [];
489 if(!this.multiple
&& opts
.length
){
490 // Mirror what a select does - choose the first one
491 var opt
= array
.filter(opts
, function(i
){
494 if(opt
&& opt
.value
){
497 opts
[0].selected
= true;
498 return opts
[0].value
;
500 }else if(this.multiple
){
501 // Set value to be the sum of all selected
502 return array
.map(array
.filter(opts
, function(i
){
511 // Internal functions to call when we have store notifications come in
512 _onNewItem: function(/*item*/ item
, /*Object?*/ parentInfo
){
513 if(!parentInfo
|| !parentInfo
.parent
){
514 // Only add it if we are top-level
515 this._addOptionForItem(item
);
518 _onDeleteItem: function(/*item*/ item
){
519 var store
= this.store
;
520 this.removeOption(store
.getIdentity(item
));
522 _onSetItem: function(/*item*/ item
){
523 this.updateOption(this._getOptionObjForItem(item
));
526 _getOptionObjForItem: function(item
){
528 // Returns an option object based off the given item. The "value"
529 // of the option item will be the identity of the item, the "label"
530 // of the option will be the label of the item.
532 // remove getLabel() call for 2.0 (it's to support the old dojo.data API)
533 var store
= this.store
,
534 label
= (this.labelAttr
&& this.labelAttr
in item
) ? item
[this.labelAttr
] : store
.getLabel(item
),
535 value
= (label
? store
.getIdentity(item
) : null);
536 return {value
: value
, label
: label
, item
: item
}; // __SelectOption
539 _addOptionForItem: function(/*item*/ item
){
541 // Creates (and adds) the option for the given item
542 var store
= this.store
;
543 if(store
.isItemLoaded
&& !store
.isItemLoaded(item
)){
544 // We are not loaded - so let's load it and add later.
545 // Remove for 2.0 (it's the old dojo.data API)
546 store
.loadItem({item
: item
, onItem: function(i
){
547 this._addOptionForItem(i
);
552 var newOpt
= this._getOptionObjForItem(item
);
553 this.addOption(newOpt
);
556 constructor: function(params
/*===== , srcNodeRef =====*/){
558 // Create the widget.
559 // params: Object|null
560 // Hash of initialization parameters for widget, including scalar values (like title, duration etc.)
561 // and functions, typically callbacks like onClick.
562 // The hash can contain any of the widget's properties, excluding read-only properties.
563 // srcNodeRef: DOMNode|String?
564 // If a srcNodeRef (DOM node) is specified, replace srcNodeRef with my generated DOM tree
566 // Saves off our value, if we have an initial one set so we
567 // can use it if we have a store as well (see startup())
568 this._oValue
= (params
|| {}).value
|| null;
569 this._notifyConnections
= []; // remove for 2.0
572 buildRendering: function(){
573 this.inherited(arguments
);
574 dom
.setSelectable(this.focusNode
, false);
577 _fillContent: function(){
579 // Loads our options and sets up our dropdown correctly. We
580 // don't want any content, so we don't call any inherit chain
585 ? query("> *", this.srcNodeRef
).map(
587 if(node
.getAttribute("type") === "separator"){
588 return { value
: "", label
: "", selected
: false, disabled
: false };
591 value
: (node
.getAttribute("data-" + kernel
._scopeName
+ "-value") || node
.getAttribute("value")),
592 label
: String(node
.innerHTML
),
593 // FIXME: disabled and selected are not valid on complex markup children (which is why we're
594 // looking for data-dojo-value above. perhaps we should data-dojo-props="" this whole thing?)
596 selected
: node
.getAttribute("selected") || false,
597 disabled
: node
.getAttribute("disabled") || false
604 this._set("value", this._getValueFromOpts());
605 }else if(this.multiple
&& typeof this.value
== "string"){
606 this._set("value", this.value
.split(","));
610 postCreate: function(){
612 // sets up our event handling that we need for functioning
614 this.inherited(arguments
);
616 // Make our event connections for updating state
617 this.connect(this, "onChange", "_updateSelection");
619 // moved from startup
620 // Connects in our store, if we have one defined
621 var store
= this.store
;
622 if(store
&& (store
.getIdentity
|| store
.getFeatures()["dojo.data.api.Identity"])){
623 // Temporarily set our store to null so that it will get set
624 // and connected appropriately
626 this.setStore(store
, this._oValue
);
632 this._loadChildren();
633 this.inherited(arguments
);
638 // Clean up our connections
641 while((h
= this._notifyConnections
.pop())){ h
.remove(); }
643 // Cancel listener for store updates
644 if(this._queryRes
&& this._queryRes
.close
){
645 this._queryRes
.close();
648 this.inherited(arguments
);
651 _addOptionItem: function(/*__SelectOption*/ /*===== option =====*/){
653 // User-overridable function which, for the given option, adds an
654 // item to the select. If the option doesn't have a value, then a
655 // separator is added in that place. Make sure to store the option
656 // in the created option widget.
659 _removeOptionItem: function(/*__SelectOption*/ /*===== option =====*/){
661 // User-overridable function which, for the given option, removes
662 // its item from the select.
665 _setDisplay: function(/*String or String[]*/ /*===== newDisplay =====*/){
667 // Overridable function which will set the display for the
668 // widget. newDisplay is either a string (in the case of
669 // single selects) or array of strings (in the case of multi-selects)
672 _getChildren: function(){
674 // Overridable function to return the children that this widget contains.
678 _getSelectedOptionsAttr: function(){
680 // hooks into this.attr to provide a mechanism for getting the
681 // option items for the current value of the widget.
682 return this.getOptions(this.get("value"));
685 _pseudoLoadChildren: function(/*item[]*/ /*===== items =====*/){
687 // a function that will "fake" loading children, if needed, and
688 // if we have set to not load children until the widget opens.
690 // An array of items that will be loaded, when needed
693 onSetStore: function(){
695 // a function that can be connected to in order to receive a
696 // notification that the store has finished loading and all options
697 // from that store are available
702 _FormSelectWidget.__SelectOption = __SelectOption;
705 return _FormSelectWidget
;