]>
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
3 "dojo/aspect", // aspect.after
4 "dojo/data/util/sorter", // util.sorter.createSortFunction
5 "dojo/_base/declare", // declare
6 "dojo/dom", // dom.setSelectable
7 "dojo/dom-class", // domClass.toggle
8 "dojo/_base/kernel", // _scopeName
9 "dojo/_base/lang", // lang.delegate lang.isArray lang.isObject lang.hitch
10 "dojo/query", // query
12 ], function(array
, aspect
, sorter
, declare
, dom
, domClass
, kernel
, lang
, query
, _FormValueWidget
){
15 var _FormValueWidget = dijit.form._FormValueWidget;
19 // dijit/form/_FormSelectWidget
21 // Extends _FormValueWidget in order to provide "select-specific"
22 // values - i.e., those values that are unique to <select> elements.
26 dijit.form.__SelectOption = function(){
28 // The value of the option. Setting to empty (or missing) will
29 // place a separator at that location
31 // The label for our option. It can contain html tags.
33 // Whether or not we are a selected option
35 // Whether or not this specific option is disabled
38 this.selected = selected;
39 this.disabled = disabled;
43 return declare("dijit.form._FormSelectWidget", _FormValueWidget
, {
45 // Extends _FormValueWidget in order to provide "select-specific"
46 // values - i.e., those values that are unique to <select> elements.
47 // This also provides the mechanism for reading the elements from
48 // a store, if desired.
50 // multiple: [const] Boolean
51 // Whether or not we are multi-valued
54 // options: dijit.form.__SelectOption[]
55 // The set of options for our select item. Roughly corresponds to
56 // the html <option> tag.
59 // store: dojo.data.api.Identity
60 // A store which, at the very least implements dojo.data.api.Identity
61 // to use for getting our list of options - rather than reading them
62 // from the <option> html tags.
66 // A query to use when fetching items from our store
69 // queryOptions: object
70 // Query options to use when fetching from the store
74 // A callback to do with an onFetch - but before any items are actually
75 // iterated over (i.e. to filter even further what you want to add)
78 // sortByLabel: Boolean
79 // Flag to sort the options returned from a store by the label of
84 // loadChildrenOnOpen: Boolean
85 // By default loadChildren is called when the items are fetched from the
86 // store. This property allows delaying loadChildren (and the creation
87 // of the options/menuitems) until the user clicks the button to open the
89 loadChildrenOnOpen
: false,
91 getOptions: function(/*anything*/ valueOrIdx
){
93 // Returns a given option (or options).
95 // If passed in as a string, that string is used to look up the option
96 // in the array of options - based on the value property.
97 // (See dijit.form.__SelectOption).
99 // If passed in a number, then the option with the given index (0-based)
100 // within this select will be returned.
102 // If passed in a dijit.form.__SelectOption, the same option will be
103 // returned if and only if it exists within this select.
105 // If passed an array, then an array will be returned with each element
106 // in the array being looked up.
108 // If not passed a value, then all options will be returned
111 // The option corresponding with the given value or index. null
112 // is returned if any of the following are true:
113 // - A string value is passed in which doesn't exist
114 // - An index is passed in which is outside the bounds of the array of options
115 // - A dijit.form.__SelectOption is passed in which is not a part of the select
117 // NOTE: the compare for passing in a dijit.form.__SelectOption checks
118 // if the value property matches - NOT if the exact option exists
119 // NOTE: if passing in an array, null elements will be placed in the returned
120 // array when a value is not found.
121 var lookupValue
= valueOrIdx
, opts
= this.options
|| [], l
= opts
.length
;
123 if(lookupValue
=== undefined){
124 return opts
; // dijit.form.__SelectOption[]
126 if(lang
.isArray(lookupValue
)){
127 return array
.map(lookupValue
, "return this.getOptions(item);", this); // dijit.form.__SelectOption[]
129 if(lang
.isObject(valueOrIdx
)){
130 // We were passed an option - so see if it's in our array (directly),
131 // and if it's not, try and find it by value.
132 if(!array
.some(this.options
, function(o
, idx
){
133 if(o
=== lookupValue
||
134 (o
.value
&& o
.value
=== lookupValue
.value
)){
143 if(typeof lookupValue
== "string"){
144 for(var i
=0; i
<l
; i
++){
145 if(opts
[i
].value
=== lookupValue
){
151 if(typeof lookupValue
== "number" && lookupValue
>= 0 && lookupValue
< l
){
152 return this.options
[lookupValue
]; // dijit.form.__SelectOption
157 addOption: function(/*dijit.form.__SelectOption|dijit.form.__SelectOption[]*/ option
){
159 // Adds an option or options to the end of the select. If value
160 // of the option is empty or missing, a separator is created instead.
161 // Passing in an array of options will yield slightly better performance
162 // since the children are only loaded once.
163 if(!lang
.isArray(option
)){ option
= [option
]; }
164 array
.forEach(option
, function(i
){
165 if(i
&& lang
.isObject(i
)){
166 this.options
.push(i
);
169 this._loadChildren();
172 removeOption: function(/*String|dijit.form.__SelectOption|Number|Array*/ valueOrIdx
){
174 // Removes the given option or options. You can remove by string
175 // (in which case the value is removed), number (in which case the
176 // index in the options array is removed), or select option (in
177 // which case, the select option with a matching value is removed).
178 // You can also pass in an array of those values for a slightly
179 // better performance since the children are only loaded once.
180 if(!lang
.isArray(valueOrIdx
)){ valueOrIdx
= [valueOrIdx
]; }
181 var oldOpts
= this.getOptions(valueOrIdx
);
182 array
.forEach(oldOpts
, function(i
){
183 // We can get null back in our array - if our option was not found. In
184 // that case, we don't want to blow up...
186 this.options
= array
.filter(this.options
, function(node
){
187 return (node
.value
!== i
.value
|| node
.label
!== i
.label
);
189 this._removeOptionItem(i
);
192 this._loadChildren();
195 updateOption: function(/*dijit.form.__SelectOption|dijit.form.__SelectOption[]*/ newOption
){
197 // Updates the values of the given option. The option to update
198 // is matched based on the value of the entered option. Passing
199 // in an array of new options will yield better performance since
200 // the children will only be loaded once.
201 if(!lang
.isArray(newOption
)){ newOption
= [newOption
]; }
202 array
.forEach(newOption
, function(i
){
203 var oldOpt
= this.getOptions(i
), k
;
205 for(k
in i
){ oldOpt
[k
] = i
[k
]; }
208 this._loadChildren();
211 setStore: function(/*dojo.data.api.Identity*/ store
,
212 /*anything?*/ selectedValue
,
213 /*Object?*/ fetchArgs
){
215 // Sets the store you would like to use with this select widget.
216 // The selected value is the value of the new store to set. This
217 // function returns the original store, in case you want to reuse
219 // store: dojo.data.api.Identity
220 // The store you would like to use - it MUST implement dojo.data.api.Identity,
221 // and MAY implement dojo.data.api.Notification.
222 // selectedValue: anything?
223 // The value that this widget should set itself to *after* the store
225 // fetchArgs: Object?
226 // The arguments that will be passed to the store's fetch() function
227 var oStore
= this.store
;
228 fetchArgs
= fetchArgs
|| {};
229 if(oStore
!== store
){
230 // Our store has changed, so update our notifications
232 while(h
= this._notifyConnections
.pop()){ h
.remove(); }
234 if(store
&& store
.getFeatures()["dojo.data.api.Notification"]){
235 this._notifyConnections
= [
236 aspect
.after(store
, "onNew", lang
.hitch(this, "_onNewItem"), true),
237 aspect
.after(store
, "onDelete", lang
.hitch(this, "_onDeleteItem"), true),
238 aspect
.after(store
, "onSet", lang
.hitch(this, "_onSetItem"), true)
241 this._set("store", store
);
244 // Turn off change notifications while we make all these changes
245 this._onChangeActive
= false;
247 // Remove existing options (if there are any)
248 if(this.options
&& this.options
.length
){
249 this.removeOption(this.options
);
252 // Add our new options
254 this._loadingStore
= true;
255 store
.fetch(lang
.delegate(fetchArgs
, {
256 onComplete: function(items
, opts
){
257 if(this.sortByLabel
&& !fetchArgs
.sort
&& items
.length
){
258 items
.sort(sorter
.createSortFunction([{
259 attribute
: store
.getLabelAttributes(items
[0])[0]
263 if(fetchArgs
.onFetch
){
264 items
= fetchArgs
.onFetch
.call(this, items
, opts
);
266 // TODO: Add these guys as a batch, instead of separately
267 array
.forEach(items
, function(i
){
268 this._addOptionForItem(i
);
271 // Set our value (which might be undefined), and then tweak
272 // it to send a change event with the real value
273 this._loadingStore
= false;
274 this.set("value", "_pendingValue" in this ? this._pendingValue
: selectedValue
);
275 delete this._pendingValue
;
277 if(!this.loadChildrenOnOpen
){
278 this._loadChildren();
280 this._pseudoLoadChildren(items
);
282 this._fetchedWith
= opts
;
283 this._lastValueReported
= this.multiple
? [] : null;
284 this._onChangeActive
= true;
286 this._handleOnChange(this.value
);
291 delete this._fetchedWith
;
293 return oStore
; // dojo.data.api.Identity
296 // TODO: implement set() and watch() for store and query, although not sure how to handle
297 // setting them individually rather than together (as in setStore() above)
299 _setValueAttr: function(/*anything*/ newValue
, /*Boolean?*/ priorityChange
){
301 // set the value of the widget.
302 // If a string is passed, then we set our value from looking it up.
303 if(this._loadingStore
){
304 // Our store is loading - so save our value, and we'll set it when
306 this._pendingValue
= newValue
;
309 var opts
= this.getOptions() || [];
310 if(!lang
.isArray(newValue
)){
311 newValue
= [newValue
];
313 array
.forEach(newValue
, function(i
, idx
){
314 if(!lang
.isObject(i
)){
317 if(typeof i
=== "string"){
318 newValue
[idx
] = array
.filter(opts
, function(node
){
319 return node
.value
=== i
;
320 })[0] || {value
: "", label
: ""};
324 // Make sure some sane default is set
325 newValue
= array
.filter(newValue
, function(i
){ return i
&& i
.value
; });
326 if(!this.multiple
&& (!newValue
[0] || !newValue
[0].value
) && opts
.length
){
327 newValue
[0] = opts
[0];
329 array
.forEach(opts
, function(i
){
330 i
.selected
= array
.some(newValue
, function(v
){ return v
.value
=== i
.value
; });
332 var val
= array
.map(newValue
, function(i
){ return i
.value
; }),
333 disp
= array
.map(newValue
, function(i
){ return i
.label
; });
335 this._set("value", this.multiple
? val
: val
[0]);
336 this._setDisplay(this.multiple
? disp
: disp
[0]);
337 this._updateSelection();
338 this._handleOnChange(this.value
, priorityChange
);
341 _getDisplayedValueAttr: function(){
343 // returns the displayed value of the widget
344 var val
= this.get("value");
345 if(!lang
.isArray(val
)){
348 var ret
= array
.map(this.getOptions(val
), function(v
){
349 if(v
&& "label" in v
){
356 return this.multiple
? ret
: ret
[0];
359 _loadChildren: function(){
361 // Loads the children represented by this widget's options.
362 // reset the menu to make it populatable on the next click
363 if(this._loadingStore
){ return; }
364 array
.forEach(this._getChildren(), function(child
){
365 child
.destroyRecursive();
367 // Add each menu item
368 array
.forEach(this.options
, this._addOptionItem
, this);
371 this._updateSelection();
374 _updateSelection: function(){
376 // Sets the "selected" class on the item for styling purposes
377 this._set("value", this._getValueFromOpts());
378 var val
= this.value
;
379 if(!lang
.isArray(val
)){
383 array
.forEach(this._getChildren(), function(child
){
384 var isSelected
= array
.some(val
, function(v
){
385 return child
.option
&& (v
=== child
.option
.value
);
387 domClass
.toggle(child
.domNode
, this.baseClass
+ "SelectedOption", isSelected
);
388 child
.domNode
.setAttribute("aria-selected", isSelected
);
393 _getValueFromOpts: function(){
395 // Returns the value of the widget by reading the options for
397 var opts
= this.getOptions() || [];
398 if(!this.multiple
&& opts
.length
){
399 // Mirror what a select does - choose the first one
400 var opt
= array
.filter(opts
, function(i
){
403 if(opt
&& opt
.value
){
406 opts
[0].selected
= true;
407 return opts
[0].value
;
409 }else if(this.multiple
){
410 // Set value to be the sum of all selected
411 return array
.map(array
.filter(opts
, function(i
){
420 // Internal functions to call when we have store notifications come in
421 _onNewItem: function(/*item*/ item
, /*Object?*/ parentInfo
){
422 if(!parentInfo
|| !parentInfo
.parent
){
423 // Only add it if we are top-level
424 this._addOptionForItem(item
);
427 _onDeleteItem: function(/*item*/ item
){
428 var store
= this.store
;
429 this.removeOption(store
.getIdentity(item
));
431 _onSetItem: function(/*item*/ item
){
432 this.updateOption(this._getOptionObjForItem(item
));
435 _getOptionObjForItem: function(item
){
437 // Returns an option object based off the given item. The "value"
438 // of the option item will be the identity of the item, the "label"
439 // of the option will be the label of the item. If the item contains
440 // children, the children value of the item will be set
441 var store
= this.store
, label
= store
.getLabel(item
),
442 value
= (label
? store
.getIdentity(item
) : null);
443 return {value
: value
, label
: label
, item
:item
}; // dijit.form.__SelectOption
446 _addOptionForItem: function(/*item*/ item
){
448 // Creates (and adds) the option for the given item
449 var store
= this.store
;
450 if(!store
.isItemLoaded(item
)){
451 // We are not loaded - so let's load it and add later
452 store
.loadItem({item
: item
, onItem: function(i
){
453 this._addOptionForItem(i
);
458 var newOpt
= this._getOptionObjForItem(item
);
459 this.addOption(newOpt
);
462 constructor: function(/*Object*/ keywordArgs
){
464 // Saves off our value, if we have an initial one set so we
465 // can use it if we have a store as well (see startup())
466 this._oValue
= (keywordArgs
|| {}).value
|| null;
467 this._notifyConnections
= [];
470 buildRendering: function(){
471 this.inherited(arguments
);
472 dom
.setSelectable(this.focusNode
, false);
475 _fillContent: function(){
477 // Loads our options and sets up our dropdown correctly. We
478 // don't want any content, so we don't call any inherit chain
480 var opts
= this.options
;
482 opts
= this.options
= this.srcNodeRef
? query("> *",
483 this.srcNodeRef
).map(function(node
){
484 if(node
.getAttribute("type") === "separator"){
485 return { value
: "", label
: "", selected
: false, disabled
: false };
488 value
: (node
.getAttribute("data-" + kernel
._scopeName
+ "-value") || node
.getAttribute("value")),
489 label
: String(node
.innerHTML
),
490 // FIXME: disabled and selected are not valid on complex markup children (which is why we're
491 // looking for data-dojo-value above. perhaps we should data-dojo-props="" this whole thing?)
493 selected
: node
.getAttribute("selected") || false,
494 disabled
: node
.getAttribute("disabled") || false
499 this._set("value", this._getValueFromOpts());
500 }else if(this.multiple
&& typeof this.value
== "string"){
501 this._set("value", this.value
.split(","));
505 postCreate: function(){
507 // sets up our event handling that we need for functioning
509 this.inherited(arguments
);
511 // Make our event connections for updating state
512 this.connect(this, "onChange", "_updateSelection");
513 this.connect(this, "startup", "_loadChildren");
515 this._setValueAttr(this.value
, null);
520 // Connects in our store, if we have one defined
521 this.inherited(arguments
);
522 var store
= this.store
, fetchArgs
= {};
523 array
.forEach(["query", "queryOptions", "onFetch"], function(i
){
525 fetchArgs
[i
] = this[i
];
529 if(store
&& store
.getFeatures()["dojo.data.api.Identity"]){
530 // Temporarily set our store to null so that it will get set
531 // and connected appropriately
533 this.setStore(store
, this._oValue
, fetchArgs
);
539 // Clean up our connections
541 while(h
= this._notifyConnections
.pop()){ h
.remove(); }
542 this.inherited(arguments
);
545 _addOptionItem: function(/*dijit.form.__SelectOption*/ /*===== option =====*/){
547 // User-overridable function which, for the given option, adds an
548 // item to the select. If the option doesn't have a value, then a
549 // separator is added in that place. Make sure to store the option
550 // in the created option widget.
553 _removeOptionItem: function(/*dijit.form.__SelectOption*/ /*===== option =====*/){
555 // User-overridable function which, for the given option, removes
556 // its item from the select.
559 _setDisplay: function(/*String or String[]*/ /*===== newDisplay =====*/){
561 // Overridable function which will set the display for the
562 // widget. newDisplay is either a string (in the case of
563 // single selects) or array of strings (in the case of multi-selects)
566 _getChildren: function(){
568 // Overridable function to return the children that this widget contains.
572 _getSelectedOptionsAttr: function(){
574 // hooks into this.attr to provide a mechanism for getting the
575 // option items for the current value of the widget.
576 return this.getOptions(this.get("value"));
579 _pseudoLoadChildren: function(/*item[]*/ /*===== items =====*/){
581 // a function that will "fake" loading children, if needed, and
582 // if we have set to not load children until the widget opens.
584 // An array of items that will be loaded, when needed
587 onSetStore: function(){
589 // a function that can be connected to in order to receive a
590 // notification that the store has finished loading and all options
591 // from that store are available