_FormSelectWidget.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. /*
  2. Copyright (c) 2004-2012, The Dojo Foundation All Rights Reserved.
  3. Available via Academic Free License >= 2.1 OR the modified BSD license.
  4. see: http://dojotoolkit.org/license for details
  5. */
  6. if(!dojo._hasResource["dijit.form._FormSelectWidget"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dijit.form._FormSelectWidget"] = true;
  8. dojo.provide("dijit.form._FormSelectWidget");
  9. dojo.require("dijit.form._FormWidget");
  10. dojo.require("dojo.data.util.sorter");
  11. /*=====
  12. dijit.form.__SelectOption = function(){
  13. // value: String
  14. // The value of the option. Setting to empty (or missing) will
  15. // place a separator at that location
  16. // label: String
  17. // The label for our option. It can contain html tags.
  18. // selected: Boolean
  19. // Whether or not we are a selected option
  20. // disabled: Boolean
  21. // Whether or not this specific option is disabled
  22. this.value = value;
  23. this.label = label;
  24. this.selected = selected;
  25. this.disabled = disabled;
  26. }
  27. =====*/
  28. dojo.declare("dijit.form._FormSelectWidget", dijit.form._FormValueWidget, {
  29. // summary:
  30. // Extends _FormValueWidget in order to provide "select-specific"
  31. // values - i.e., those values that are unique to <select> elements.
  32. // This also provides the mechanism for reading the elements from
  33. // a store, if desired.
  34. // multiple: [const] Boolean
  35. // Whether or not we are multi-valued
  36. multiple: false,
  37. // options: dijit.form.__SelectOption[]
  38. // The set of options for our select item. Roughly corresponds to
  39. // the html <option> tag.
  40. options: null,
  41. // store: dojo.data.api.Identity
  42. // A store which, at the very least impelements dojo.data.api.Identity
  43. // to use for getting our list of options - rather than reading them
  44. // from the <option> html tags.
  45. store: null,
  46. // query: object
  47. // A query to use when fetching items from our store
  48. query: null,
  49. // queryOptions: object
  50. // Query options to use when fetching from the store
  51. queryOptions: null,
  52. // onFetch: Function
  53. // A callback to do with an onFetch - but before any items are actually
  54. // iterated over (i.e. to filter even futher what you want to add)
  55. onFetch: null,
  56. // sortByLabel: Boolean
  57. // Flag to sort the options returned from a store by the label of
  58. // the store.
  59. sortByLabel: true,
  60. // loadChildrenOnOpen: Boolean
  61. // By default loadChildren is called when the items are fetched from the
  62. // store. This property allows delaying loadChildren (and the creation
  63. // of the options/menuitems) until the user clicks the button to open the
  64. // dropdown.
  65. loadChildrenOnOpen: false,
  66. getOptions: function(/*anything*/ valueOrIdx){
  67. // summary:
  68. // Returns a given option (or options).
  69. // valueOrIdx:
  70. // If passed in as a string, that string is used to look up the option
  71. // in the array of options - based on the value property.
  72. // (See dijit.form.__SelectOption).
  73. //
  74. // If passed in a number, then the option with the given index (0-based)
  75. // within this select will be returned.
  76. //
  77. // If passed in a dijit.form.__SelectOption, the same option will be
  78. // returned if and only if it exists within this select.
  79. //
  80. // If passed an array, then an array will be returned with each element
  81. // in the array being looked up.
  82. //
  83. // If not passed a value, then all options will be returned
  84. //
  85. // returns:
  86. // The option corresponding with the given value or index. null
  87. // is returned if any of the following are true:
  88. // - A string value is passed in which doesn't exist
  89. // - An index is passed in which is outside the bounds of the array of options
  90. // - A dijit.form.__SelectOption is passed in which is not a part of the select
  91. // NOTE: the compare for passing in a dijit.form.__SelectOption checks
  92. // if the value property matches - NOT if the exact option exists
  93. // NOTE: if passing in an array, null elements will be placed in the returned
  94. // array when a value is not found.
  95. var lookupValue = valueOrIdx, opts = this.options || [], l = opts.length;
  96. if(lookupValue === undefined){
  97. return opts; // dijit.form.__SelectOption[]
  98. }
  99. if(dojo.isArray(lookupValue)){
  100. return dojo.map(lookupValue, "return this.getOptions(item);", this); // dijit.form.__SelectOption[]
  101. }
  102. if(dojo.isObject(valueOrIdx)){
  103. // We were passed an option - so see if it's in our array (directly),
  104. // and if it's not, try and find it by value.
  105. if(!dojo.some(this.options, function(o, idx){
  106. if(o === lookupValue ||
  107. (o.value && o.value === lookupValue.value)){
  108. lookupValue = idx;
  109. return true;
  110. }
  111. return false;
  112. })){
  113. lookupValue = -1;
  114. }
  115. }
  116. if(typeof lookupValue == "string"){
  117. for(var i=0; i<l; i++){
  118. if(opts[i].value === lookupValue){
  119. lookupValue = i;
  120. break;
  121. }
  122. }
  123. }
  124. if(typeof lookupValue == "number" && lookupValue >= 0 && lookupValue < l){
  125. return this.options[lookupValue] // dijit.form.__SelectOption
  126. }
  127. return null; // null
  128. },
  129. addOption: function(/*dijit.form.__SelectOption|dijit.form.__SelectOption[]*/ option){
  130. // summary:
  131. // Adds an option or options to the end of the select. If value
  132. // of the option is empty or missing, a separator is created instead.
  133. // Passing in an array of options will yield slightly better performance
  134. // since the children are only loaded once.
  135. if(!dojo.isArray(option)){ option = [option]; }
  136. dojo.forEach(option, function(i){
  137. if(i && dojo.isObject(i)){
  138. this.options.push(i);
  139. }
  140. }, this);
  141. this._loadChildren();
  142. },
  143. removeOption: function(/*String|dijit.form.__SelectOption|Number|Array*/ valueOrIdx){
  144. // summary:
  145. // Removes the given option or options. You can remove by string
  146. // (in which case the value is removed), number (in which case the
  147. // index in the options array is removed), or select option (in
  148. // which case, the select option with a matching value is removed).
  149. // You can also pass in an array of those values for a slightly
  150. // better performance since the children are only loaded once.
  151. if(!dojo.isArray(valueOrIdx)){ valueOrIdx = [valueOrIdx]; }
  152. var oldOpts = this.getOptions(valueOrIdx);
  153. dojo.forEach(oldOpts, function(i){
  154. // We can get null back in our array - if our option was not found. In
  155. // that case, we don't want to blow up...
  156. if(i){
  157. this.options = dojo.filter(this.options, function(node, idx){
  158. return (node.value !== i.value || node.label !== i.label);
  159. });
  160. this._removeOptionItem(i);
  161. }
  162. }, this);
  163. this._loadChildren();
  164. },
  165. updateOption: function(/*dijit.form.__SelectOption|dijit.form.__SelectOption[]*/ newOption){
  166. // summary:
  167. // Updates the values of the given option. The option to update
  168. // is matched based on the value of the entered option. Passing
  169. // in an array of new options will yeild better performance since
  170. // the children will only be loaded once.
  171. if(!dojo.isArray(newOption)){ newOption = [newOption]; }
  172. dojo.forEach(newOption, function(i){
  173. var oldOpt = this.getOptions(i), k;
  174. if(oldOpt){
  175. for(k in i){ oldOpt[k] = i[k]; }
  176. }
  177. }, this);
  178. this._loadChildren();
  179. },
  180. setStore: function(/*dojo.data.api.Identity*/ store,
  181. /*anything?*/ selectedValue,
  182. /*Object?*/ fetchArgs){
  183. // summary:
  184. // Sets the store you would like to use with this select widget.
  185. // The selected value is the value of the new store to set. This
  186. // function returns the original store, in case you want to reuse
  187. // it or something.
  188. // store: dojo.data.api.Identity
  189. // The store you would like to use - it MUST implement Identity,
  190. // and MAY implement Notification.
  191. // selectedValue: anything?
  192. // The value that this widget should set itself to *after* the store
  193. // has been loaded
  194. // fetchArgs: Object?
  195. // The arguments that will be passed to the store's fetch() function
  196. var oStore = this.store;
  197. fetchArgs = fetchArgs || {};
  198. if(oStore !== store){
  199. // Our store has changed, so update our notifications
  200. dojo.forEach(this._notifyConnections || [], dojo.disconnect);
  201. delete this._notifyConnections;
  202. if(store && store.getFeatures()["dojo.data.api.Notification"]){
  203. this._notifyConnections = [
  204. dojo.connect(store, "onNew", this, "_onNewItem"),
  205. dojo.connect(store, "onDelete", this, "_onDeleteItem"),
  206. dojo.connect(store, "onSet", this, "_onSetItem")
  207. ];
  208. }
  209. this._set("store", store);
  210. }
  211. // Turn off change notifications while we make all these changes
  212. this._onChangeActive = false;
  213. // Remove existing options (if there are any)
  214. if(this.options && this.options.length){
  215. this.removeOption(this.options);
  216. }
  217. // Add our new options
  218. if(store){
  219. this._loadingStore = true;
  220. store.fetch(dojo.delegate(fetchArgs, {
  221. onComplete: function(items, opts){
  222. if(this.sortByLabel && !fetchArgs.sort && items.length){
  223. items.sort(dojo.data.util.sorter.createSortFunction([{
  224. attribute: store.getLabelAttributes(items[0])[0]
  225. }], store));
  226. }
  227. if(fetchArgs.onFetch){
  228. items = fetchArgs.onFetch.call(this, items, opts);
  229. }
  230. // TODO: Add these guys as a batch, instead of separately
  231. dojo.forEach(items, function(i){
  232. this._addOptionForItem(i);
  233. }, this);
  234. // Set our value (which might be undefined), and then tweak
  235. // it to send a change event with the real value
  236. this._loadingStore = false;
  237. this.set("value", "_pendingValue" in this ? this._pendingValue : selectedValue);
  238. delete this._pendingValue;
  239. if(!this.loadChildrenOnOpen){
  240. this._loadChildren();
  241. }else{
  242. this._pseudoLoadChildren(items);
  243. }
  244. this._fetchedWith = opts;
  245. this._lastValueReported = this.multiple ? [] : null;
  246. this._onChangeActive = true;
  247. this.onSetStore();
  248. this._handleOnChange(this.value);
  249. },
  250. scope: this
  251. }));
  252. }else{
  253. delete this._fetchedWith;
  254. }
  255. return oStore; // dojo.data.api.Identity
  256. },
  257. // TODO: implement set() and watch() for store and query, although not sure how to handle
  258. // setting them individually rather than together (as in setStore() above)
  259. _setValueAttr: function(/*anything*/ newValue, /*Boolean?*/ priorityChange){
  260. // summary:
  261. // set the value of the widget.
  262. // If a string is passed, then we set our value from looking it up.
  263. if(this._loadingStore){
  264. // Our store is loading - so save our value, and we'll set it when
  265. // we're done
  266. this._pendingValue = newValue;
  267. return;
  268. }
  269. var opts = this.getOptions() || [];
  270. if(!dojo.isArray(newValue)){
  271. newValue = [newValue];
  272. }
  273. dojo.forEach(newValue, function(i, idx){
  274. if(!dojo.isObject(i)){
  275. i = i + "";
  276. }
  277. if(typeof i === "string"){
  278. newValue[idx] = dojo.filter(opts, function(node){
  279. return node.value === i;
  280. })[0] || {value: "", label: ""};
  281. }
  282. }, this);
  283. // Make sure some sane default is set
  284. newValue = dojo.filter(newValue, function(i){ return i && i.value; });
  285. if(!this.multiple && (!newValue[0] || !newValue[0].value) && opts.length){
  286. newValue[0] = opts[0];
  287. }
  288. dojo.forEach(opts, function(i){
  289. i.selected = dojo.some(newValue, function(v){ return v.value === i.value; });
  290. });
  291. var val = dojo.map(newValue, function(i){ return i.value; }),
  292. disp = dojo.map(newValue, function(i){ return i.label; });
  293. this._set("value", this.multiple ? val : val[0]);
  294. this._setDisplay(this.multiple ? disp : disp[0]);
  295. this._updateSelection();
  296. this._handleOnChange(this.value, priorityChange);
  297. },
  298. _getDisplayedValueAttr: function(){
  299. // summary:
  300. // returns the displayed value of the widget
  301. var val = this.get("value");
  302. if(!dojo.isArray(val)){
  303. val = [val];
  304. }
  305. var ret = dojo.map(this.getOptions(val), function(v){
  306. if(v && "label" in v){
  307. return v.label;
  308. }else if(v){
  309. return v.value;
  310. }
  311. return null;
  312. }, this);
  313. return this.multiple ? ret : ret[0];
  314. },
  315. _loadChildren: function(){
  316. // summary:
  317. // Loads the children represented by this widget's options.
  318. // reset the menu to make it populatable on the next click
  319. if(this._loadingStore){ return; }
  320. dojo.forEach(this._getChildren(), function(child){
  321. child.destroyRecursive();
  322. });
  323. // Add each menu item
  324. dojo.forEach(this.options, this._addOptionItem, this);
  325. // Update states
  326. this._updateSelection();
  327. },
  328. _updateSelection: function(){
  329. // summary:
  330. // Sets the "selected" class on the item for styling purposes
  331. this._set("value", this._getValueFromOpts());
  332. var val = this.value;
  333. if(!dojo.isArray(val)){
  334. val = [val];
  335. }
  336. if(val && val[0]){
  337. dojo.forEach(this._getChildren(), function(child){
  338. var isSelected = dojo.some(val, function(v){
  339. return child.option && (v === child.option.value);
  340. });
  341. dojo.toggleClass(child.domNode, this.baseClass + "SelectedOption", isSelected);
  342. dijit.setWaiState(child.domNode, "selected", isSelected);
  343. }, this);
  344. }
  345. },
  346. _getValueFromOpts: function(){
  347. // summary:
  348. // Returns the value of the widget by reading the options for
  349. // the selected flag
  350. var opts = this.getOptions() || [];
  351. if(!this.multiple && opts.length){
  352. // Mirror what a select does - choose the first one
  353. var opt = dojo.filter(opts, function(i){
  354. return i.selected;
  355. })[0];
  356. if(opt && opt.value){
  357. return opt.value
  358. }else{
  359. opts[0].selected = true;
  360. return opts[0].value;
  361. }
  362. }else if(this.multiple){
  363. // Set value to be the sum of all selected
  364. return dojo.map(dojo.filter(opts, function(i){
  365. return i.selected;
  366. }), function(i){
  367. return i.value;
  368. }) || [];
  369. }
  370. return "";
  371. },
  372. // Internal functions to call when we have store notifications come in
  373. _onNewItem: function(/*item*/ item, /*Object?*/ parentInfo){
  374. if(!parentInfo || !parentInfo.parent){
  375. // Only add it if we are top-level
  376. this._addOptionForItem(item);
  377. }
  378. },
  379. _onDeleteItem: function(/*item*/ item){
  380. var store = this.store;
  381. this.removeOption(store.getIdentity(item));
  382. },
  383. _onSetItem: function(/*item*/ item){
  384. this.updateOption(this._getOptionObjForItem(item));
  385. },
  386. _getOptionObjForItem: function(item){
  387. // summary:
  388. // Returns an option object based off the given item. The "value"
  389. // of the option item will be the identity of the item, the "label"
  390. // of the option will be the label of the item. If the item contains
  391. // children, the children value of the item will be set
  392. var store = this.store, label = store.getLabel(item),
  393. value = (label ? store.getIdentity(item) : null);
  394. return {value: value, label: label, item:item}; // dijit.form.__SelectOption
  395. },
  396. _addOptionForItem: function(/*item*/ item){
  397. // summary:
  398. // Creates (and adds) the option for the given item
  399. var store = this.store;
  400. if(!store.isItemLoaded(item)){
  401. // We are not loaded - so let's load it and add later
  402. store.loadItem({item: item, onComplete: function(i){
  403. this._addOptionForItem(item);
  404. },
  405. scope: this});
  406. return;
  407. }
  408. var newOpt = this._getOptionObjForItem(item);
  409. this.addOption(newOpt);
  410. },
  411. constructor: function(/*Object*/ keywordArgs){
  412. // summary:
  413. // Saves off our value, if we have an initial one set so we
  414. // can use it if we have a store as well (see startup())
  415. this._oValue = (keywordArgs || {}).value || null;
  416. },
  417. buildRendering: function(){
  418. this.inherited(arguments);
  419. dojo.setSelectable(this.focusNode, false);
  420. },
  421. _fillContent: function(){
  422. // summary:
  423. // Loads our options and sets up our dropdown correctly. We
  424. // don't want any content, so we don't call any inherit chain
  425. // function.
  426. var opts = this.options;
  427. if(!opts){
  428. opts = this.options = this.srcNodeRef ? dojo.query(">",
  429. this.srcNodeRef).map(function(node){
  430. if(node.getAttribute("type") === "separator"){
  431. return { value: "", label: "", selected: false, disabled: false };
  432. }
  433. return {
  434. value: (node.getAttribute("data-" + dojo._scopeName + "-value") || node.getAttribute("value")),
  435. label: String(node.innerHTML),
  436. // FIXME: disabled and selected are not valid on complex markup children (which is why we're
  437. // looking for data-dojo-value above. perhaps we should data-dojo-props="" this whole thing?)
  438. // decide before 1.6
  439. selected: node.getAttribute("selected") || false,
  440. disabled: node.getAttribute("disabled") || false
  441. };
  442. }, this) : [];
  443. }
  444. if(!this.value){
  445. this._set("value", this._getValueFromOpts());
  446. }else if(this.multiple && typeof this.value == "string"){
  447. this._set("value", this.value.split(","));
  448. }
  449. },
  450. postCreate: function(){
  451. // summary:
  452. // sets up our event handling that we need for functioning
  453. // as a select
  454. this.inherited(arguments);
  455. // Make our event connections for updating state
  456. this.connect(this, "onChange", "_updateSelection");
  457. this.connect(this, "startup", "_loadChildren");
  458. this._setValueAttr(this.value, null);
  459. },
  460. startup: function(){
  461. // summary:
  462. // Connects in our store, if we have one defined
  463. this.inherited(arguments);
  464. var store = this.store, fetchArgs = {};
  465. dojo.forEach(["query", "queryOptions", "onFetch"], function(i){
  466. if(this[i]){
  467. fetchArgs[i] = this[i];
  468. }
  469. delete this[i];
  470. }, this);
  471. if(store && store.getFeatures()["dojo.data.api.Identity"]){
  472. // Temporarily set our store to null so that it will get set
  473. // and connected appropriately
  474. this.store = null;
  475. this.setStore(store, this._oValue, fetchArgs);
  476. }
  477. },
  478. destroy: function(){
  479. // summary:
  480. // Clean up our connections
  481. dojo.forEach(this._notifyConnections || [], dojo.disconnect);
  482. this.inherited(arguments);
  483. },
  484. _addOptionItem: function(/*dijit.form.__SelectOption*/ option){
  485. // summary:
  486. // User-overridable function which, for the given option, adds an
  487. // item to the select. If the option doesn't have a value, then a
  488. // separator is added in that place. Make sure to store the option
  489. // in the created option widget.
  490. },
  491. _removeOptionItem: function(/*dijit.form.__SelectOption*/ option){
  492. // summary:
  493. // User-overridable function which, for the given option, removes
  494. // its item from the select.
  495. },
  496. _setDisplay: function(/*String or String[]*/ newDisplay){
  497. // summary:
  498. // Overridable function which will set the display for the
  499. // widget. newDisplay is either a string (in the case of
  500. // single selects) or array of strings (in the case of multi-selects)
  501. },
  502. _getChildren: function(){
  503. // summary:
  504. // Overridable function to return the children that this widget contains.
  505. return [];
  506. },
  507. _getSelectedOptionsAttr: function(){
  508. // summary:
  509. // hooks into this.attr to provide a mechanism for getting the
  510. // option items for the current value of the widget.
  511. return this.getOptions(this.get("value"));
  512. },
  513. _pseudoLoadChildren: function(/*item[]*/ items){
  514. // summary:
  515. // a function that will "fake" loading children, if needed, and
  516. // if we have set to not load children until the widget opens.
  517. // items:
  518. // An array of items that will be loaded, when needed
  519. },
  520. onSetStore: function(){
  521. // summary:
  522. // a function that can be connected to in order to receive a
  523. // notification that the store has finished loading and all options
  524. // from that store are available
  525. }
  526. });
  527. }