]> git.wh0rd.org - tt-rss.git/blob - lib/dijit/form/_FormSelectWidget.js.uncompressed.js
upgrade dojo to 1.8.3 (refs #570)
[tt-rss.git] / 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/_base/Deferred",
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
12 "dojo/when",
13 "dojo/store/util/QueryResults",
14 "./_FormValueWidget"
15 ], function(array, Deferred, aspect, sorter, declare, dom, domClass, kernel, lang, query, when,
16 QueryResults, _FormValueWidget){
17
18 // module:
19 // dijit/form/_FormSelectWidget
20
21 /*=====
22 var __SelectOption = {
23 // value: String
24 // The value of the option. Setting to empty (or missing) will
25 // place a separator at that location
26 // label: String
27 // The label for our option. It can contain html tags.
28 // selected: Boolean
29 // Whether or not we are a selected option
30 // disabled: Boolean
31 // Whether or not this specific option is disabled
32 };
33 =====*/
34
35 var _FormSelectWidget = declare("dijit.form._FormSelectWidget", _FormValueWidget, {
36 // summary:
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.
41
42 // multiple: [const] Boolean
43 // Whether or not we are multi-valued
44 multiple: false,
45
46 // options: __SelectOption[]
47 // The set of options for our select item. Roughly corresponds to
48 // the html `<option>` tag.
49 options: null,
50
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.
55 store: null,
56
57 // query: object
58 // A query to use when fetching items from our store
59 query: null,
60
61 // queryOptions: object
62 // Query options to use when fetching from the store
63 queryOptions: null,
64
65 // labelAttr: String?
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.
69 labelAttr: "",
70
71 // onFetch: Function
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)
74 onFetch: null,
75
76 // sortByLabel: Boolean
77 // Flag to sort the options returned from a store by the label of
78 // the store.
79 sortByLabel: true,
80
81
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
86 // dropdown.
87 loadChildrenOnOpen: false,
88
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.
93 onLoadDeferred: null,
94
95 getOptions: function(/*anything*/ valueOrIdx){
96 // summary:
97 // Returns a given option (or options).
98 // valueOrIdx:
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).
102 //
103 // If passed in a number, then the option with the given index (0-based)
104 // within this select will be returned.
105 //
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.
108 //
109 // If passed an array, then an array will be returned with each element
110 // in the array being looked up.
111 //
112 // If not passed a value, then all options will be returned
113 //
114 // returns:
115 // The option corresponding with the given value or index. null
116 // is returned if any of the following are true:
117 //
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
121
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;
127
128 if(lookupValue === undefined){
129 return opts; // __SelectOption[]
130 }
131 if(lang.isArray(lookupValue)){
132 return array.map(lookupValue, "return this.getOptions(item);", this); // __SelectOption[]
133 }
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)){
140 lookupValue = idx;
141 return true;
142 }
143 return false;
144 })){
145 lookupValue = -1;
146 }
147 }
148 if(typeof lookupValue == "string"){
149 for(var i=0; i<l; i++){
150 if(opts[i].value === lookupValue){
151 lookupValue = i;
152 break;
153 }
154 }
155 }
156 if(typeof lookupValue == "number" && lookupValue >= 0 && lookupValue < l){
157 return this.options[lookupValue]; // __SelectOption
158 }
159 return null; // null
160 },
161
162 addOption: function(/*__SelectOption|__SelectOption[]*/ option){
163 // summary:
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);
172 }
173 }, this);
174 this._loadChildren();
175 },
176
177 removeOption: function(/*String|__SelectOption|Number|Array*/ valueOrIdx){
178 // summary:
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...
190 if(i){
191 this.options = array.filter(this.options, function(node){
192 return (node.value !== i.value || node.label !== i.label);
193 });
194 this._removeOptionItem(i);
195 }
196 }, this);
197 this._loadChildren();
198 },
199
200 updateOption: function(/*__SelectOption|__SelectOption[]*/ newOption){
201 // summary:
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;
209 if(oldOpt){
210 for(k in i){ oldOpt[k] = i[k]; }
211 }
212 }, this);
213 this._loadChildren();
214 },
215
216 setStore: function(store,
217 selectedValue,
218 fetchArgs){
219 // summary:
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
223 // it or something.
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
232 // has been loaded
233 // fetchArgs: Object?
234 // Hash of parameters to set filter on store, etc.
235 //
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 || {};
241
242 if(oStore !== store){
243 // Our store has changed, so cancel any listeners on old store (remove for 2.0)
244 var h;
245 while((h = this._notifyConnections.pop())){ h.remove(); }
246
247 // For backwards-compatibility, accept dojo.data store in addition to dojo.store.store. Remove in 2.0.
248 if(!store.get){
249 lang.mixin(store, {
250 _oldAPI: true,
251 get: function(id){
252 // summary:
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({
257 identity: id,
258 onItem: function(object){
259 deferred.resolve(object);
260 },
261 onError: function(error){
262 deferred.reject(error);
263 }
264 });
265 return deferred.promise;
266 },
267 query: function(query, options){
268 // summary:
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({
274 query: query,
275 onBegin: function(count){
276 deferred.total.resolve(count);
277 },
278 onComplete: function(results){
279 deferred.resolve(results);
280 },
281 onError: function(error){
282 deferred.reject(error);
283 }
284 }, options));
285 return new QueryResults(deferred);
286 }
287 });
288
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)
294 ];
295 }
296 }
297 this._set("store", store); // Our store has changed, so update our notifications
298 }
299
300 // Remove existing options (if there are any)
301 if(this.options && this.options.length){
302 this.removeOption(this.options);
303 }
304
305 // Cancel listener for updates to old store
306 if(this._queryRes && this._queryRes.close){
307 this._queryRes.close();
308 }
309
310 // If user has specified new query and query options along with this new store, then use them.
311 if(fetchArgs.query){
312 this._set("query", fetchArgs.query);
313 this._set("queryOptions", fetchArgs.queryOptions);
314 }
315
316 // Add our new options
317 if(store){
318 this._loadingStore = true;
319 this.onLoadDeferred = new Deferred();
320
321 // Run query
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){
325
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]
331 }], store));
332 }else{
333 var labelAttr = this.labelAttr;
334 items.sort(function(a, b){
335 return a[labelAttr] > b[labelAttr] ? 1 : b[labelAttr] > a[labelAttr] ? -1 : 0;
336 });
337 }
338 }
339
340 if(fetchArgs.onFetch){
341 items = fetchArgs.onFetch.call(this, items, fetchArgs);
342 }
343
344 // TODO: Add these guys as a batch, instead of separately
345 array.forEach(items, function(i){
346 this._addOptionForItem(i);
347 }, this);
348
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);
354 }else{
355 if(deletedFrom != -1){
356 this._onDeleteItem(object);
357 }
358 if(insertedInto != -1){
359 this._onNewItem(object);
360 }
361 }
362 }), true);
363 }
364
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;
370
371 if(!this.loadChildrenOnOpen){
372 this._loadChildren();
373 }else{
374 this._pseudoLoadChildren(items);
375 }
376 this.onLoadDeferred.resolve(true);
377 this.onSetStore();
378 }), function(err){
379 console.error('dijit.form.Select: ' + err.toString());
380 this.onLoadDeferred.reject(err);
381 });
382 }
383 return oStore; // dojo/data/api/Identity
384 },
385
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)
388
389 _setValueAttr: function(/*anything*/ newValue, /*Boolean?*/ priorityChange){
390 // summary:
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
396 // we're done
397 this._pendingValue = newValue;
398 return;
399 }
400 var opts = this.getOptions() || [];
401 if(!lang.isArray(newValue)){
402 newValue = [newValue];
403 }
404 array.forEach(newValue, function(i, idx){
405 if(!lang.isObject(i)){
406 i = i + "";
407 }
408 if(typeof i === "string"){
409 newValue[idx] = array.filter(opts, function(node){
410 return node.value === i;
411 })[0] || {value: "", label: ""};
412 }
413 }, this);
414
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];
419 }
420 array.forEach(opts, function(i){
421 i.selected = array.some(newValue, function(v){ return v.value === i.value; });
422 });
423 var val = array.map(newValue, function(i){ return i.value; }),
424 disp = array.map(newValue, function(i){ return i.label; });
425
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();
430 },
431
432 _getDisplayedValueAttr: function(){
433 // summary:
434 // returns the displayed value of the widget
435 var val = this.get("value");
436 if(!lang.isArray(val)){
437 val = [val];
438 }
439 var ret = array.map(this.getOptions(val), function(v){
440 if(v && "label" in v){
441 return v.label;
442 }else if(v){
443 return v.value;
444 }
445 return null;
446 }, this);
447 return this.multiple ? ret : ret[0];
448 },
449
450 _loadChildren: function(){
451 // summary:
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();
457 });
458 // Add each menu item
459 array.forEach(this.options, this._addOptionItem, this);
460
461 // Update states
462 this._updateSelection();
463 },
464
465 _updateSelection: function(){
466 // summary:
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)){
471 val = [val];
472 }
473 if(val && val[0]){
474 array.forEach(this._getChildren(), function(child){
475 var isSelected = array.some(val, function(v){
476 return child.option && (v === child.option.value);
477 });
478 domClass.toggle(child.domNode, this.baseClass.replace(/\s+|$/g, "SelectedOption "), isSelected);
479 child.domNode.setAttribute("aria-selected", isSelected ? "true" : "false");
480 }, this);
481 }
482 },
483
484 _getValueFromOpts: function(){
485 // summary:
486 // Returns the value of the widget by reading the options for
487 // the selected flag
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){
492 return i.selected;
493 })[0];
494 if(opt && opt.value){
495 return opt.value;
496 }else{
497 opts[0].selected = true;
498 return opts[0].value;
499 }
500 }else if(this.multiple){
501 // Set value to be the sum of all selected
502 return array.map(array.filter(opts, function(i){
503 return i.selected;
504 }), function(i){
505 return i.value;
506 }) || [];
507 }
508 return "";
509 },
510
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);
516 }
517 },
518 _onDeleteItem: function(/*item*/ item){
519 var store = this.store;
520 this.removeOption(store.getIdentity(item));
521 },
522 _onSetItem: function(/*item*/ item){
523 this.updateOption(this._getOptionObjForItem(item));
524 },
525
526 _getOptionObjForItem: function(item){
527 // summary:
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.
531
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
537 },
538
539 _addOptionForItem: function(/*item*/ item){
540 // summary:
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);
548 },
549 scope: this});
550 return;
551 }
552 var newOpt = this._getOptionObjForItem(item);
553 this.addOption(newOpt);
554 },
555
556 constructor: function(params /*===== , srcNodeRef =====*/){
557 // summary:
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
565
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
570 },
571
572 buildRendering: function(){
573 this.inherited(arguments);
574 dom.setSelectable(this.focusNode, false);
575 },
576
577 _fillContent: function(){
578 // summary:
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
581 // function.
582 if(!this.options){
583 this.options =
584 this.srcNodeRef
585 ? query("> *", this.srcNodeRef).map(
586 function(node){
587 if(node.getAttribute("type") === "separator"){
588 return { value: "", label: "", selected: false, disabled: false };
589 }
590 return {
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?)
595 // decide before 1.6
596 selected: node.getAttribute("selected") || false,
597 disabled: node.getAttribute("disabled") || false
598 };
599 },
600 this)
601 : [];
602 }
603 if(!this.value){
604 this._set("value", this._getValueFromOpts());
605 }else if(this.multiple && typeof this.value == "string"){
606 this._set("value", this.value.split(","));
607 }
608 },
609
610 postCreate: function(){
611 // summary:
612 // sets up our event handling that we need for functioning
613 // as a select
614 this.inherited(arguments);
615
616 // Make our event connections for updating state
617 this.connect(this, "onChange", "_updateSelection");
618
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
625 this.store = null;
626 this.setStore(store, this._oValue);
627 }
628 },
629
630 startup: function(){
631 // summary:
632 this._loadChildren();
633 this.inherited(arguments);
634 },
635
636 destroy: function(){
637 // summary:
638 // Clean up our connections
639
640 var h;
641 while((h = this._notifyConnections.pop())){ h.remove(); }
642
643 // Cancel listener for store updates
644 if(this._queryRes && this._queryRes.close){
645 this._queryRes.close();
646 }
647
648 this.inherited(arguments);
649 },
650
651 _addOptionItem: function(/*__SelectOption*/ /*===== option =====*/){
652 // summary:
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.
657 },
658
659 _removeOptionItem: function(/*__SelectOption*/ /*===== option =====*/){
660 // summary:
661 // User-overridable function which, for the given option, removes
662 // its item from the select.
663 },
664
665 _setDisplay: function(/*String or String[]*/ /*===== newDisplay =====*/){
666 // summary:
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)
670 },
671
672 _getChildren: function(){
673 // summary:
674 // Overridable function to return the children that this widget contains.
675 return [];
676 },
677
678 _getSelectedOptionsAttr: function(){
679 // summary:
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"));
683 },
684
685 _pseudoLoadChildren: function(/*item[]*/ /*===== items =====*/){
686 // summary:
687 // a function that will "fake" loading children, if needed, and
688 // if we have set to not load children until the widget opens.
689 // items:
690 // An array of items that will be loaded, when needed
691 },
692
693 onSetStore: function(){
694 // summary:
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
698 }
699 });
700
701 /*=====
702 _FormSelectWidget.__SelectOption = __SelectOption;
703 =====*/
704
705 return _FormSelectWidget;
706
707 });