]> git.wh0rd.org - tt-rss.git/blob - lib/dijit/form/_FormSelectWidget.js.uncompressed.js
update dojo to 1.7.3
[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/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
11 "./_FormValueWidget"
12 ], function(array, aspect, sorter, declare, dom, domClass, kernel, lang, query, _FormValueWidget){
13
14 /*=====
15 var _FormValueWidget = dijit.form._FormValueWidget;
16 =====*/
17
18 // module:
19 // dijit/form/_FormSelectWidget
20 // summary:
21 // Extends _FormValueWidget in order to provide "select-specific"
22 // values - i.e., those values that are unique to <select> elements.
23
24
25 /*=====
26 dijit.form.__SelectOption = function(){
27 // value: String
28 // The value of the option. Setting to empty (or missing) will
29 // place a separator at that location
30 // label: String
31 // The label for our option. It can contain html tags.
32 // selected: Boolean
33 // Whether or not we are a selected option
34 // disabled: Boolean
35 // Whether or not this specific option is disabled
36 this.value = value;
37 this.label = label;
38 this.selected = selected;
39 this.disabled = disabled;
40 }
41 =====*/
42
43 return declare("dijit.form._FormSelectWidget", _FormValueWidget, {
44 // summary:
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.
49
50 // multiple: [const] Boolean
51 // Whether or not we are multi-valued
52 multiple: false,
53
54 // options: dijit.form.__SelectOption[]
55 // The set of options for our select item. Roughly corresponds to
56 // the html <option> tag.
57 options: null,
58
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.
63 store: null,
64
65 // query: object
66 // A query to use when fetching items from our store
67 query: null,
68
69 // queryOptions: object
70 // Query options to use when fetching from the store
71 queryOptions: null,
72
73 // onFetch: Function
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)
76 onFetch: null,
77
78 // sortByLabel: Boolean
79 // Flag to sort the options returned from a store by the label of
80 // the store.
81 sortByLabel: true,
82
83
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
88 // dropdown.
89 loadChildrenOnOpen: false,
90
91 getOptions: function(/*anything*/ valueOrIdx){
92 // summary:
93 // Returns a given option (or options).
94 // valueOrIdx:
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).
98 //
99 // If passed in a number, then the option with the given index (0-based)
100 // within this select will be returned.
101 //
102 // If passed in a dijit.form.__SelectOption, the same option will be
103 // returned if and only if it exists within this select.
104 //
105 // If passed an array, then an array will be returned with each element
106 // in the array being looked up.
107 //
108 // If not passed a value, then all options will be returned
109 //
110 // returns:
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
116
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;
122
123 if(lookupValue === undefined){
124 return opts; // dijit.form.__SelectOption[]
125 }
126 if(lang.isArray(lookupValue)){
127 return array.map(lookupValue, "return this.getOptions(item);", this); // dijit.form.__SelectOption[]
128 }
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)){
135 lookupValue = idx;
136 return true;
137 }
138 return false;
139 })){
140 lookupValue = -1;
141 }
142 }
143 if(typeof lookupValue == "string"){
144 for(var i=0; i<l; i++){
145 if(opts[i].value === lookupValue){
146 lookupValue = i;
147 break;
148 }
149 }
150 }
151 if(typeof lookupValue == "number" && lookupValue >= 0 && lookupValue < l){
152 return this.options[lookupValue]; // dijit.form.__SelectOption
153 }
154 return null; // null
155 },
156
157 addOption: function(/*dijit.form.__SelectOption|dijit.form.__SelectOption[]*/ option){
158 // summary:
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);
167 }
168 }, this);
169 this._loadChildren();
170 },
171
172 removeOption: function(/*String|dijit.form.__SelectOption|Number|Array*/ valueOrIdx){
173 // summary:
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...
185 if(i){
186 this.options = array.filter(this.options, function(node){
187 return (node.value !== i.value || node.label !== i.label);
188 });
189 this._removeOptionItem(i);
190 }
191 }, this);
192 this._loadChildren();
193 },
194
195 updateOption: function(/*dijit.form.__SelectOption|dijit.form.__SelectOption[]*/ newOption){
196 // summary:
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;
204 if(oldOpt){
205 for(k in i){ oldOpt[k] = i[k]; }
206 }
207 }, this);
208 this._loadChildren();
209 },
210
211 setStore: function(/*dojo.data.api.Identity*/ store,
212 /*anything?*/ selectedValue,
213 /*Object?*/ fetchArgs){
214 // summary:
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
218 // it or something.
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
224 // has been loaded
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
231 var h;
232 while(h = this._notifyConnections.pop()){ h.remove(); }
233
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)
239 ];
240 }
241 this._set("store", store);
242 }
243
244 // Turn off change notifications while we make all these changes
245 this._onChangeActive = false;
246
247 // Remove existing options (if there are any)
248 if(this.options && this.options.length){
249 this.removeOption(this.options);
250 }
251
252 // Add our new options
253 if(store){
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]
260 }], store));
261 }
262
263 if(fetchArgs.onFetch){
264 items = fetchArgs.onFetch.call(this, items, opts);
265 }
266 // TODO: Add these guys as a batch, instead of separately
267 array.forEach(items, function(i){
268 this._addOptionForItem(i);
269 }, this);
270
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;
276
277 if(!this.loadChildrenOnOpen){
278 this._loadChildren();
279 }else{
280 this._pseudoLoadChildren(items);
281 }
282 this._fetchedWith = opts;
283 this._lastValueReported = this.multiple ? [] : null;
284 this._onChangeActive = true;
285 this.onSetStore();
286 this._handleOnChange(this.value);
287 },
288 scope: this
289 }));
290 }else{
291 delete this._fetchedWith;
292 }
293 return oStore; // dojo.data.api.Identity
294 },
295
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)
298
299 _setValueAttr: function(/*anything*/ newValue, /*Boolean?*/ priorityChange){
300 // summary:
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
305 // we're done
306 this._pendingValue = newValue;
307 return;
308 }
309 var opts = this.getOptions() || [];
310 if(!lang.isArray(newValue)){
311 newValue = [newValue];
312 }
313 array.forEach(newValue, function(i, idx){
314 if(!lang.isObject(i)){
315 i = i + "";
316 }
317 if(typeof i === "string"){
318 newValue[idx] = array.filter(opts, function(node){
319 return node.value === i;
320 })[0] || {value: "", label: ""};
321 }
322 }, this);
323
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];
328 }
329 array.forEach(opts, function(i){
330 i.selected = array.some(newValue, function(v){ return v.value === i.value; });
331 });
332 var val = array.map(newValue, function(i){ return i.value; }),
333 disp = array.map(newValue, function(i){ return i.label; });
334
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);
339 },
340
341 _getDisplayedValueAttr: function(){
342 // summary:
343 // returns the displayed value of the widget
344 var val = this.get("value");
345 if(!lang.isArray(val)){
346 val = [val];
347 }
348 var ret = array.map(this.getOptions(val), function(v){
349 if(v && "label" in v){
350 return v.label;
351 }else if(v){
352 return v.value;
353 }
354 return null;
355 }, this);
356 return this.multiple ? ret : ret[0];
357 },
358
359 _loadChildren: function(){
360 // summary:
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();
366 });
367 // Add each menu item
368 array.forEach(this.options, this._addOptionItem, this);
369
370 // Update states
371 this._updateSelection();
372 },
373
374 _updateSelection: function(){
375 // summary:
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)){
380 val = [val];
381 }
382 if(val && val[0]){
383 array.forEach(this._getChildren(), function(child){
384 var isSelected = array.some(val, function(v){
385 return child.option && (v === child.option.value);
386 });
387 domClass.toggle(child.domNode, this.baseClass + "SelectedOption", isSelected);
388 child.domNode.setAttribute("aria-selected", isSelected);
389 }, this);
390 }
391 },
392
393 _getValueFromOpts: function(){
394 // summary:
395 // Returns the value of the widget by reading the options for
396 // the selected flag
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){
401 return i.selected;
402 })[0];
403 if(opt && opt.value){
404 return opt.value
405 }else{
406 opts[0].selected = true;
407 return opts[0].value;
408 }
409 }else if(this.multiple){
410 // Set value to be the sum of all selected
411 return array.map(array.filter(opts, function(i){
412 return i.selected;
413 }), function(i){
414 return i.value;
415 }) || [];
416 }
417 return "";
418 },
419
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);
425 }
426 },
427 _onDeleteItem: function(/*item*/ item){
428 var store = this.store;
429 this.removeOption(store.getIdentity(item));
430 },
431 _onSetItem: function(/*item*/ item){
432 this.updateOption(this._getOptionObjForItem(item));
433 },
434
435 _getOptionObjForItem: function(item){
436 // summary:
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
444 },
445
446 _addOptionForItem: function(/*item*/ item){
447 // summary:
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);
454 },
455 scope: this});
456 return;
457 }
458 var newOpt = this._getOptionObjForItem(item);
459 this.addOption(newOpt);
460 },
461
462 constructor: function(/*Object*/ keywordArgs){
463 // summary:
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 = [];
468 },
469
470 buildRendering: function(){
471 this.inherited(arguments);
472 dom.setSelectable(this.focusNode, false);
473 },
474
475 _fillContent: function(){
476 // summary:
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
479 // function.
480 var opts = this.options;
481 if(!opts){
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 };
486 }
487 return {
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?)
492 // decide before 1.6
493 selected: node.getAttribute("selected") || false,
494 disabled: node.getAttribute("disabled") || false
495 };
496 }, this) : [];
497 }
498 if(!this.value){
499 this._set("value", this._getValueFromOpts());
500 }else if(this.multiple && typeof this.value == "string"){
501 this._set("value", this.value.split(","));
502 }
503 },
504
505 postCreate: function(){
506 // summary:
507 // sets up our event handling that we need for functioning
508 // as a select
509 this.inherited(arguments);
510
511 // Make our event connections for updating state
512 this.connect(this, "onChange", "_updateSelection");
513 this.connect(this, "startup", "_loadChildren");
514
515 this._setValueAttr(this.value, null);
516 },
517
518 startup: function(){
519 // summary:
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){
524 if(this[i]){
525 fetchArgs[i] = this[i];
526 }
527 delete this[i];
528 }, this);
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
532 this.store = null;
533 this.setStore(store, this._oValue, fetchArgs);
534 }
535 },
536
537 destroy: function(){
538 // summary:
539 // Clean up our connections
540 var h;
541 while(h = this._notifyConnections.pop()){ h.remove(); }
542 this.inherited(arguments);
543 },
544
545 _addOptionItem: function(/*dijit.form.__SelectOption*/ /*===== option =====*/){
546 // summary:
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.
551 },
552
553 _removeOptionItem: function(/*dijit.form.__SelectOption*/ /*===== option =====*/){
554 // summary:
555 // User-overridable function which, for the given option, removes
556 // its item from the select.
557 },
558
559 _setDisplay: function(/*String or String[]*/ /*===== newDisplay =====*/){
560 // summary:
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)
564 },
565
566 _getChildren: function(){
567 // summary:
568 // Overridable function to return the children that this widget contains.
569 return [];
570 },
571
572 _getSelectedOptionsAttr: function(){
573 // summary:
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"));
577 },
578
579 _pseudoLoadChildren: function(/*item[]*/ /*===== items =====*/){
580 // summary:
581 // a function that will "fake" loading children, if needed, and
582 // if we have set to not load children until the widget opens.
583 // items:
584 // An array of items that will be loaded, when needed
585 },
586
587 onSetStore: function(){
588 // summary:
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
592 }
593 });
594
595 });