_FormSelectWidget.js 19 KB

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