_AutoCompleterMixin.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786
  1. define("dijit/form/_AutoCompleterMixin", [
  2. "dojo/_base/connect", // keys keys.SHIFT
  3. "dojo/data/util/filter", // patternToRegExp
  4. "dojo/_base/declare", // declare
  5. "dojo/_base/Deferred", // Deferred.when
  6. "dojo/dom-attr", // domAttr.get
  7. "dojo/_base/event", // event.stop
  8. "dojo/keys",
  9. "dojo/_base/lang", // lang.clone lang.hitch
  10. "dojo/on",
  11. "dojo/query", // query
  12. "dojo/regexp", // regexp.escapeString
  13. "dojo/_base/sniff", // has("ie")
  14. "dojo/string", // string.substitute
  15. "dojo/_base/window", // win.doc.selection.createRange
  16. "./DataList",
  17. "../registry", // registry.byId
  18. "./_TextBoxMixin" // defines _TextBoxMixin.selectInputText
  19. ], function(connect, filter, declare, Deferred, domAttr, event, keys, lang, on, query, regexp, has, string, win,
  20. DataList, registry, _TextBoxMixin){
  21. // module:
  22. // dijit/form/_AutoCompleterMixin
  23. // summary:
  24. // A mixin that implements the base functionality for `dijit.form.ComboBox`/`dijit.form.FilteringSelect`
  25. return declare("dijit.form._AutoCompleterMixin", null, {
  26. // summary:
  27. // A mixin that implements the base functionality for `dijit.form.ComboBox`/`dijit.form.FilteringSelect`
  28. // description:
  29. // All widgets that mix in dijit.form._AutoCompleterMixin must extend `dijit.form._FormValueWidget`.
  30. // tags:
  31. // protected
  32. // item: Object
  33. // This is the item returned by the dojo.data.store implementation that
  34. // provides the data for this ComboBox, it's the currently selected item.
  35. item: null,
  36. // pageSize: Integer
  37. // Argument to data provider.
  38. // Specifies number of search results per page (before hitting "next" button)
  39. pageSize: Infinity,
  40. // store: [const] dojo.store.api.Store
  41. // Reference to data provider object used by this ComboBox
  42. store: null,
  43. // fetchProperties: Object
  44. // Mixin to the store's fetch.
  45. // For example, to set the sort order of the ComboBox menu, pass:
  46. // | { sort: [{attribute:"name",descending: true}] }
  47. // To override the default queryOptions so that deep=false, do:
  48. // | { queryOptions: {ignoreCase: true, deep: false} }
  49. fetchProperties:{},
  50. // query: Object
  51. // A query that can be passed to 'store' to initially filter the items,
  52. // before doing further filtering based on `searchAttr` and the key.
  53. // Any reference to the `searchAttr` is ignored.
  54. query: {},
  55. // list: [const] String
  56. // Alternate to specifying a store. Id of a dijit/form/DataList widget.
  57. list: "",
  58. _setListAttr: function(list){
  59. // Avoid having list applied to the DOM node, since it has native meaning in modern browsers
  60. this._set("list", list);
  61. },
  62. // autoComplete: Boolean
  63. // If user types in a partial string, and then tab out of the `<input>` box,
  64. // automatically copy the first entry displayed in the drop down list to
  65. // the `<input>` field
  66. autoComplete: true,
  67. // highlightMatch: String
  68. // One of: "first", "all" or "none".
  69. //
  70. // If the ComboBox/FilteringSelect opens with the search results and the searched
  71. // string can be found, it will be highlighted. If set to "all"
  72. // then will probably want to change `queryExpr` parameter to '*${0}*'
  73. //
  74. // Highlighting is only performed when `labelType` is "text", so as to not
  75. // interfere with any HTML markup an HTML label might contain.
  76. highlightMatch: "first",
  77. // searchDelay: Integer
  78. // Delay in milliseconds between when user types something and we start
  79. // searching based on that value
  80. searchDelay: 100,
  81. // searchAttr: String
  82. // Search for items in the data store where this attribute (in the item)
  83. // matches what the user typed
  84. searchAttr: "name",
  85. // labelAttr: String?
  86. // The entries in the drop down list come from this attribute in the
  87. // dojo.data items.
  88. // If not specified, the searchAttr attribute is used instead.
  89. labelAttr: "",
  90. // labelType: String
  91. // Specifies how to interpret the labelAttr in the data store items.
  92. // Can be "html" or "text".
  93. labelType: "text",
  94. // queryExpr: String
  95. // This specifies what query ComboBox/FilteringSelect sends to the data store,
  96. // based on what the user has typed. Changing this expression will modify
  97. // whether the drop down shows only exact matches, a "starting with" match,
  98. // etc. Use it in conjunction with highlightMatch.
  99. // dojo.data query expression pattern.
  100. // `${0}` will be substituted for the user text.
  101. // `*` is used for wildcards.
  102. // `${0}*` means "starts with", `*${0}*` means "contains", `${0}` means "is"
  103. queryExpr: "${0}*",
  104. // ignoreCase: Boolean
  105. // Set true if the ComboBox/FilteringSelect should ignore case when matching possible items
  106. ignoreCase: true,
  107. // Flags to _HasDropDown to limit height of drop down to make it fit in viewport
  108. maxHeight: -1,
  109. // For backwards compatibility let onClick events propagate, even clicks on the down arrow button
  110. _stopClickEvents: false,
  111. _getCaretPos: function(/*DomNode*/ element){
  112. // khtml 3.5.2 has selection* methods as does webkit nightlies from 2005-06-22
  113. var pos = 0;
  114. if(typeof(element.selectionStart) == "number"){
  115. // FIXME: this is totally borked on Moz < 1.3. Any recourse?
  116. pos = element.selectionStart;
  117. }else if(has("ie")){
  118. // in the case of a mouse click in a popup being handled,
  119. // then the win.doc.selection is not the textarea, but the popup
  120. // var r = win.doc.selection.createRange();
  121. // hack to get IE 6 to play nice. What a POS browser.
  122. var tr = win.doc.selection.createRange().duplicate();
  123. var ntr = element.createTextRange();
  124. tr.move("character",0);
  125. ntr.move("character",0);
  126. try{
  127. // If control doesn't have focus, you get an exception.
  128. // Seems to happen on reverse-tab, but can also happen on tab (seems to be a race condition - only happens sometimes).
  129. // There appears to be no workaround for this - googled for quite a while.
  130. ntr.setEndPoint("EndToEnd", tr);
  131. pos = String(ntr.text).replace(/\r/g,"").length;
  132. }catch(e){
  133. // If focus has shifted, 0 is fine for caret pos.
  134. }
  135. }
  136. return pos;
  137. },
  138. _setCaretPos: function(/*DomNode*/ element, /*Number*/ location){
  139. location = parseInt(location);
  140. _TextBoxMixin.selectInputText(element, location, location);
  141. },
  142. _setDisabledAttr: function(/*Boolean*/ value){
  143. // Additional code to set disabled state of ComboBox node.
  144. // Overrides _FormValueWidget._setDisabledAttr() or ValidationTextBox._setDisabledAttr().
  145. this.inherited(arguments);
  146. this.domNode.setAttribute("aria-disabled", value);
  147. },
  148. _abortQuery: function(){
  149. // stop in-progress query
  150. if(this.searchTimer){
  151. clearTimeout(this.searchTimer);
  152. this.searchTimer = null;
  153. }
  154. if(this._fetchHandle){
  155. if(this._fetchHandle.cancel){
  156. this._cancelingQuery = true;
  157. this._fetchHandle.cancel();
  158. this._cancelingQuery = false;
  159. }
  160. this._fetchHandle = null;
  161. }
  162. },
  163. _onInput: function(/*Event*/ evt){
  164. // summary:
  165. // Handles paste events.
  166. this.inherited(arguments);
  167. if(evt.charOrCode == 229){ // IME or cut/paste event
  168. this._onKeyPress(evt);
  169. }
  170. },
  171. _onKey: function(/*Event*/ evt){
  172. // summary:
  173. // Handles keyboard events from synthetic dojo/_base/connect._keypress event
  174. if(this.disabled || this.readOnly){ return; }
  175. var key = evt.keyCode;
  176. // except for cutting/pasting case - ctrl + x/v
  177. if(evt.altKey || ((evt.ctrlKey || evt.metaKey) && (key != 86 && key != 88)) || key == keys.SHIFT){
  178. return; // throw out weird key combinations and spurious events
  179. }
  180. var doSearch = false;
  181. var pw = this.dropDown;
  182. var highlighted = null;
  183. this._prev_key_backspace = false;
  184. this._abortQuery();
  185. // _HasDropDown will do some of the work:
  186. // 1. when drop down is not yet shown:
  187. // - if user presses the down arrow key, call loadDropDown()
  188. // 2. when drop down is already displayed:
  189. // - on ESC key, call closeDropDown()
  190. // - otherwise, call dropDown.handleKey() to process the keystroke
  191. this.inherited(arguments);
  192. if(this._opened){
  193. highlighted = pw.getHighlightedOption();
  194. }
  195. switch(key){
  196. case keys.PAGE_DOWN:
  197. case keys.DOWN_ARROW:
  198. case keys.PAGE_UP:
  199. case keys.UP_ARROW:
  200. // Keystroke caused ComboBox_menu to move to a different item.
  201. // Copy new item to <input> box.
  202. if(this._opened){
  203. this._announceOption(highlighted);
  204. }
  205. event.stop(evt);
  206. break;
  207. case keys.ENTER:
  208. // prevent submitting form if user presses enter. Also
  209. // prevent accepting the value if either Next or Previous
  210. // are selected
  211. if(highlighted){
  212. // only stop event on prev/next
  213. if(highlighted == pw.nextButton){
  214. this._nextSearch(1);
  215. event.stop(evt);
  216. break;
  217. }else if(highlighted == pw.previousButton){
  218. this._nextSearch(-1);
  219. event.stop(evt);
  220. break;
  221. }
  222. }else{
  223. // Update 'value' (ex: KY) according to currently displayed text
  224. this._setBlurValue(); // set value if needed
  225. this._setCaretPos(this.focusNode, this.focusNode.value.length); // move cursor to end and cancel highlighting
  226. }
  227. // default case:
  228. // if enter pressed while drop down is open, or for FilteringSelect,
  229. // if we are in the middle of a query to convert a directly typed in value to an item,
  230. // prevent submit
  231. if(this._opened || this._fetchHandle){
  232. event.stop(evt);
  233. }
  234. // fall through
  235. case keys.TAB:
  236. var newvalue = this.get('displayedValue');
  237. // if the user had More Choices selected fall into the
  238. // _onBlur handler
  239. if(pw && (
  240. newvalue == pw._messages["previousMessage"] ||
  241. newvalue == pw._messages["nextMessage"])
  242. ){
  243. break;
  244. }
  245. if(highlighted){
  246. this._selectOption(highlighted);
  247. }
  248. // fall through
  249. case keys.ESCAPE:
  250. if(this._opened){
  251. this._lastQuery = null; // in case results come back later
  252. this.closeDropDown();
  253. }
  254. break;
  255. case ' ':
  256. if(highlighted){
  257. // user is effectively clicking a choice in the drop down menu
  258. event.stop(evt);
  259. this._selectOption(highlighted);
  260. this.closeDropDown();
  261. }else{
  262. // user typed a space into the input box, treat as normal character
  263. doSearch = true;
  264. }
  265. break;
  266. case keys.DELETE:
  267. case keys.BACKSPACE:
  268. this._prev_key_backspace = true;
  269. doSearch = true;
  270. break;
  271. }
  272. if(doSearch){
  273. // need to wait a tad before start search so that the event
  274. // bubbles through DOM and we have value visible
  275. this.item = undefined; // undefined means item needs to be set
  276. this.searchTimer = setTimeout(lang.hitch(this, "_startSearchFromInput"),1);
  277. }
  278. },
  279. _onKeyPress: function(evt){
  280. // Non char keys (F1-F12 etc..) shouldn't open list.
  281. // Ascii characters and IME input (Chinese, Japanese etc.) should.
  282. if(typeof evt.charOrCode == "string" || evt.charOrCode == 229){
  283. // need to wait a tad before start search so that the event
  284. // bubbles through DOM and we have value visible
  285. this.item = undefined; // undefined means item needs to be set
  286. this.searchTimer = setTimeout(lang.hitch(this, "_startSearchFromInput"),1);
  287. }
  288. },
  289. _autoCompleteText: function(/*String*/ text){
  290. // summary:
  291. // Fill in the textbox with the first item from the drop down
  292. // list, and highlight the characters that were
  293. // auto-completed. For example, if user typed "CA" and the
  294. // drop down list appeared, the textbox would be changed to
  295. // "California" and "ifornia" would be highlighted.
  296. var fn = this.focusNode;
  297. // IE7: clear selection so next highlight works all the time
  298. _TextBoxMixin.selectInputText(fn, fn.value.length);
  299. // does text autoComplete the value in the textbox?
  300. var caseFilter = this.ignoreCase? 'toLowerCase' : 'substr';
  301. if(text[caseFilter](0).indexOf(this.focusNode.value[caseFilter](0)) == 0){
  302. var cpos = this.autoComplete ? this._getCaretPos(fn) : fn.value.length;
  303. // only try to extend if we added the last character at the end of the input
  304. if((cpos+1) > fn.value.length){
  305. // only add to input node as we would overwrite Capitalisation of chars
  306. // actually, that is ok
  307. fn.value = text;//.substr(cpos);
  308. // visually highlight the autocompleted characters
  309. _TextBoxMixin.selectInputText(fn, cpos);
  310. }
  311. }else{
  312. // text does not autoComplete; replace the whole value and highlight
  313. fn.value = text;
  314. _TextBoxMixin.selectInputText(fn);
  315. }
  316. },
  317. _openResultList: function(/*Object*/ results, /*Object*/ query, /*Object*/ options){
  318. // summary:
  319. // Callback when a search completes.
  320. // description:
  321. // 1. generates drop-down list and calls _showResultList() to display it
  322. // 2. if this result list is from user pressing "more choices"/"previous choices"
  323. // then tell screen reader to announce new option
  324. this._fetchHandle = null;
  325. if( this.disabled ||
  326. this.readOnly ||
  327. (query[this.searchAttr] !== this._lastQuery) // TODO: better way to avoid getting unwanted notify
  328. ){
  329. return;
  330. }
  331. var wasSelected = this.dropDown.getHighlightedOption();
  332. this.dropDown.clearResultList();
  333. if(!results.length && options.start == 0){ // if no results and not just the previous choices button
  334. this.closeDropDown();
  335. return;
  336. }
  337. // Fill in the textbox with the first item from the drop down list,
  338. // and highlight the characters that were auto-completed. For
  339. // example, if user typed "CA" and the drop down list appeared, the
  340. // textbox would be changed to "California" and "ifornia" would be
  341. // highlighted.
  342. this.dropDown.createOptions(
  343. results,
  344. options,
  345. lang.hitch(this, "_getMenuLabelFromItem")
  346. );
  347. // show our list (only if we have content, else nothing)
  348. this._showResultList();
  349. // #4091:
  350. // tell the screen reader that the paging callback finished by
  351. // shouting the next choice
  352. if(options.direction){
  353. if(1 == options.direction){
  354. this.dropDown.highlightFirstOption();
  355. }else if(-1 == options.direction){
  356. this.dropDown.highlightLastOption();
  357. }
  358. if(wasSelected){
  359. this._announceOption(this.dropDown.getHighlightedOption());
  360. }
  361. }else if(this.autoComplete && !this._prev_key_backspace
  362. // when the user clicks the arrow button to show the full list,
  363. // startSearch looks for "*".
  364. // it does not make sense to autocomplete
  365. // if they are just previewing the options available.
  366. && !/^[*]+$/.test(query[this.searchAttr].toString())){
  367. this._announceOption(this.dropDown.containerNode.firstChild.nextSibling); // 1st real item
  368. }
  369. },
  370. _showResultList: function(){
  371. // summary:
  372. // Display the drop down if not already displayed, or if it is displayed, then
  373. // reposition it if necessary (reposition may be necessary if drop down's height changed).
  374. this.closeDropDown(true);
  375. this.openDropDown();
  376. this.domNode.setAttribute("aria-expanded", "true");
  377. },
  378. loadDropDown: function(/*Function*/ /*===== callback =====*/){
  379. // Overrides _HasDropDown.loadDropDown().
  380. // This is called when user has pressed button icon or pressed the down arrow key
  381. // to open the drop down.
  382. this._startSearchAll();
  383. },
  384. isLoaded: function(){
  385. // signal to _HasDropDown that it needs to call loadDropDown() to load the
  386. // drop down asynchronously before displaying it
  387. return false;
  388. },
  389. closeDropDown: function(){
  390. // Overrides _HasDropDown.closeDropDown(). Closes the drop down (assuming that it's open).
  391. // This method is the callback when the user types ESC or clicking
  392. // the button icon while the drop down is open. It's also called by other code.
  393. this._abortQuery();
  394. if(this._opened){
  395. this.inherited(arguments);
  396. this.domNode.setAttribute("aria-expanded", "false");
  397. this.focusNode.removeAttribute("aria-activedescendant");
  398. }
  399. },
  400. _setBlurValue: function(){
  401. // if the user clicks away from the textbox OR tabs away, set the
  402. // value to the textbox value
  403. // #4617:
  404. // if value is now more choices or previous choices, revert
  405. // the value
  406. var newvalue = this.get('displayedValue');
  407. var pw = this.dropDown;
  408. if(pw && (
  409. newvalue == pw._messages["previousMessage"] ||
  410. newvalue == pw._messages["nextMessage"]
  411. )
  412. ){
  413. this._setValueAttr(this._lastValueReported, true);
  414. }else if(typeof this.item == "undefined"){
  415. // Update 'value' (ex: KY) according to currently displayed text
  416. this.item = null;
  417. this.set('displayedValue', newvalue);
  418. }else{
  419. if(this.value != this._lastValueReported){
  420. this._handleOnChange(this.value, true);
  421. }
  422. this._refreshState();
  423. }
  424. },
  425. _setItemAttr: function(/*item*/ item, /*Boolean?*/ priorityChange, /*String?*/ displayedValue){
  426. // summary:
  427. // Set the displayed valued in the input box, and the hidden value
  428. // that gets submitted, based on a dojo.data store item.
  429. // description:
  430. // Users shouldn't call this function; they should be calling
  431. // set('item', value)
  432. // tags:
  433. // private
  434. var value = '';
  435. if(item){
  436. if(!displayedValue){
  437. displayedValue = this.store._oldAPI ? // remove getValue() for 2.0 (old dojo.data API)
  438. this.store.getValue(item, this.searchAttr) : item[this.searchAttr];
  439. }
  440. value = this._getValueField() != this.searchAttr ? this.store.getIdentity(item) : displayedValue;
  441. }
  442. this.set('value', value, priorityChange, displayedValue, item);
  443. },
  444. _announceOption: function(/*Node*/ node){
  445. // summary:
  446. // a11y code that puts the highlighted option in the textbox.
  447. // This way screen readers will know what is happening in the
  448. // menu.
  449. if(!node){
  450. return;
  451. }
  452. // pull the text value from the item attached to the DOM node
  453. var newValue;
  454. if(node == this.dropDown.nextButton ||
  455. node == this.dropDown.previousButton){
  456. newValue = node.innerHTML;
  457. this.item = undefined;
  458. this.value = '';
  459. }else{
  460. var item = this.dropDown.items[node.getAttribute("item")];
  461. newValue = (this.store._oldAPI ? // remove getValue() for 2.0 (old dojo.data API)
  462. this.store.getValue(item, this.searchAttr) : item[this.searchAttr]).toString();
  463. this.set('item', item, false, newValue);
  464. }
  465. // get the text that the user manually entered (cut off autocompleted text)
  466. this.focusNode.value = this.focusNode.value.substring(0, this._lastInput.length);
  467. // set up ARIA activedescendant
  468. this.focusNode.setAttribute("aria-activedescendant", domAttr.get(node, "id"));
  469. // autocomplete the rest of the option to announce change
  470. this._autoCompleteText(newValue);
  471. },
  472. _selectOption: function(/*DomNode*/ target){
  473. // summary:
  474. // Menu callback function, called when an item in the menu is selected.
  475. this.closeDropDown();
  476. if(target){
  477. this._announceOption(target);
  478. }
  479. this._setCaretPos(this.focusNode, this.focusNode.value.length);
  480. this._handleOnChange(this.value, true);
  481. },
  482. _startSearchAll: function(){
  483. this._startSearch('');
  484. },
  485. _startSearchFromInput: function(){
  486. this._startSearch(this.focusNode.value.replace(/([\\\*\?])/g, "\\$1"));
  487. },
  488. _getQueryString: function(/*String*/ text){
  489. return string.substitute(this.queryExpr, [text]);
  490. },
  491. _startSearch: function(/*String*/ key){
  492. // summary:
  493. // Starts a search for elements matching key (key=="" means to return all items),
  494. // and calls _openResultList() when the search completes, to display the results.
  495. if(!this.dropDown){
  496. var popupId = this.id + "_popup",
  497. dropDownConstructor = lang.isString(this.dropDownClass) ?
  498. lang.getObject(this.dropDownClass, false) : this.dropDownClass;
  499. this.dropDown = new dropDownConstructor({
  500. onChange: lang.hitch(this, this._selectOption),
  501. id: popupId,
  502. dir: this.dir,
  503. textDir: this.textDir
  504. });
  505. this.focusNode.removeAttribute("aria-activedescendant");
  506. this.textbox.setAttribute("aria-owns",popupId); // associate popup with textbox
  507. }
  508. this._lastInput = key; // Store exactly what was entered by the user.
  509. // Setup parameters to be passed to store.query().
  510. // Create a new query to prevent accidentally querying for a hidden
  511. // value from FilteringSelect's keyField
  512. var query = lang.clone(this.query); // #5970
  513. var options = {
  514. start: 0,
  515. count: this.pageSize,
  516. queryOptions: { // remove for 2.0
  517. ignoreCase: this.ignoreCase,
  518. deep: true
  519. }
  520. };
  521. lang.mixin(options, this.fetchProperties);
  522. // Generate query
  523. var qs = this._getQueryString(key), q;
  524. if(this.store._oldAPI){
  525. // remove this branch for 2.0
  526. q = qs;
  527. }else{
  528. // Query on searchAttr is a regex for benefit of dojo.store.Memory,
  529. // but with a toString() method to help dojo.store.JsonRest.
  530. // Search string like "Co*" converted to regex like /^Co.*$/i.
  531. q = filter.patternToRegExp(qs, this.ignoreCase);
  532. q.toString = function(){ return qs; };
  533. }
  534. this._lastQuery = query[this.searchAttr] = q;
  535. // Function to run the query, wait for the results, and then call _openResultList()
  536. var _this = this,
  537. startQuery = function(){
  538. var resPromise = _this._fetchHandle = _this.store.query(query, options);
  539. Deferred.when(resPromise, function(res){
  540. _this._fetchHandle = null;
  541. res.total = resPromise.total;
  542. _this._openResultList(res, query, options);
  543. }, function(err){
  544. _this._fetchHandle = null;
  545. if(!_this._cancelingQuery){ // don't treat canceled query as an error
  546. console.error(_this.declaredClass + ' ' + err.toString());
  547. _this.closeDropDown();
  548. }
  549. });
  550. };
  551. // #5970: set _lastQuery, *then* start the timeout
  552. // otherwise, if the user types and the last query returns before the timeout,
  553. // _lastQuery won't be set and their input gets rewritten
  554. this.searchTimer = setTimeout(lang.hitch(this, function(query, _this){
  555. this.searchTimer = null;
  556. startQuery();
  557. // Setup method to handle clicking next/previous buttons to page through results
  558. this._nextSearch = this.dropDown.onPage = function(direction){
  559. options.start += options.count * direction;
  560. // tell callback the direction of the paging so the screen
  561. // reader knows which menu option to shout
  562. options.direction = direction;
  563. startQuery();
  564. _this.focus();
  565. };
  566. }, query, this), this.searchDelay);
  567. },
  568. _getValueField: function(){
  569. // summary:
  570. // Helper for postMixInProperties() to set this.value based on data inlined into the markup.
  571. // Returns the attribute name in the item (in dijit.form._ComboBoxDataStore) to use as the value.
  572. return this.searchAttr;
  573. },
  574. //////////// INITIALIZATION METHODS ///////////////////////////////////////
  575. constructor: function(){
  576. this.query={};
  577. this.fetchProperties={};
  578. },
  579. postMixInProperties: function(){
  580. if(!this.store){
  581. var srcNodeRef = this.srcNodeRef;
  582. var list = this.list;
  583. if(list){
  584. this.store = registry.byId(list);
  585. }else{
  586. // if user didn't specify store, then assume there are option tags
  587. this.store = new DataList({}, srcNodeRef);
  588. }
  589. // if there is no value set and there is an option list, set
  590. // the value to the first value to be consistent with native Select
  591. // Firefox and Safari set value
  592. // IE6 and Opera set selectedIndex, which is automatically set
  593. // by the selected attribute of an option tag
  594. // IE6 does not set value, Opera sets value = selectedIndex
  595. if(!("value" in this.params)){
  596. var item = (this.item = this.store.fetchSelectedItem());
  597. if(item){
  598. var valueField = this._getValueField();
  599. // remove getValue() for 2.0 (old dojo.data API)
  600. this.value = this.store._oldAPI ? this.store.getValue(item, valueField) : item[valueField];
  601. }
  602. }
  603. }
  604. this.inherited(arguments);
  605. },
  606. postCreate: function(){
  607. // summary:
  608. // Subclasses must call this method from their postCreate() methods
  609. // tags:
  610. // protected
  611. // find any associated label element and add to ComboBox node.
  612. var label=query('label[for="'+this.id+'"]');
  613. if(label.length){
  614. label[0].id = (this.id+"_label");
  615. this.domNode.setAttribute("aria-labelledby", label[0].id);
  616. }
  617. this.inherited(arguments);
  618. // HasDropDown calls _onKey() on keydown but we want to be notified of the synthetic
  619. // dojo/_base/connect._keypress event too.
  620. this.connect(this.focusNode, "onkeypress", "_onKeyPress");
  621. },
  622. _getMenuLabelFromItem: function(/*Item*/ item){
  623. var label = this.labelFunc(item, this.store),
  624. labelType = this.labelType;
  625. // If labelType is not "text" we don't want to screw any markup ot whatever.
  626. if(this.highlightMatch != "none" && this.labelType == "text" && this._lastInput){
  627. label = this.doHighlight(label, this._escapeHtml(this._lastInput));
  628. labelType = "html";
  629. }
  630. return {html: labelType == "html", label: label};
  631. },
  632. doHighlight: function(/*String*/ label, /*String*/ find){
  633. // summary:
  634. // Highlights the string entered by the user in the menu. By default this
  635. // highlights the first occurrence found. Override this method
  636. // to implement your custom highlighting.
  637. // tags:
  638. // protected
  639. var
  640. // Add (g)lobal modifier when this.highlightMatch == "all" and (i)gnorecase when this.ignoreCase == true
  641. modifiers = (this.ignoreCase ? "i" : "") + (this.highlightMatch == "all" ? "g" : ""),
  642. i = this.queryExpr.indexOf("${0}");
  643. find = regexp.escapeString(find); // escape regexp special chars
  644. return this._escapeHtml(label).replace(
  645. // prepend ^ when this.queryExpr == "${0}*" and append $ when this.queryExpr == "*${0}"
  646. new RegExp((i == 0 ? "^" : "") + "("+ find +")" + (i == (this.queryExpr.length - 4) ? "$" : ""), modifiers),
  647. '<span class="dijitComboBoxHighlightMatch">$1</span>'
  648. ); // returns String, (almost) valid HTML (entities encoded)
  649. },
  650. _escapeHtml: function(/*String*/ str){
  651. // TODO Should become dojo.html.entities(), when exists use instead
  652. // summary:
  653. // Adds escape sequences for special characters in XML: &<>"'
  654. str = String(str).replace(/&/gm, "&amp;").replace(/</gm, "&lt;")
  655. .replace(/>/gm, "&gt;").replace(/"/gm, "&quot;"); //balance"
  656. return str; // string
  657. },
  658. reset: function(){
  659. // Overrides the _FormWidget.reset().
  660. // Additionally reset the .item (to clean up).
  661. this.item = null;
  662. this.inherited(arguments);
  663. },
  664. labelFunc: function(/*item*/ item, /*dojo.store.api.Store*/ store){
  665. // summary:
  666. // Computes the label to display based on the dojo.data store item.
  667. // returns:
  668. // The label that the ComboBox should display
  669. // tags:
  670. // private
  671. // Use toString() because XMLStore returns an XMLItem whereas this
  672. // method is expected to return a String (#9354).
  673. // Remove getValue() for 2.0 (old dojo.data API)
  674. return (store._oldAPI ? store.getValue(item, this.labelAttr || this.searchAttr) :
  675. item[this.labelAttr || this.searchAttr]).toString(); // String
  676. },
  677. _setValueAttr: function(/*String*/ value, /*Boolean?*/ priorityChange, /*String?*/ displayedValue, /*item?*/ item){
  678. // summary:
  679. // Hook so set('value', value) works.
  680. // description:
  681. // Sets the value of the select.
  682. this._set("item", item||null); // value not looked up in store
  683. if(!value){ value = ''; } // null translates to blank
  684. this.inherited(arguments);
  685. },
  686. _setTextDirAttr: function(/*String*/ textDir){
  687. // summary:
  688. // Setter for textDir, needed for the dropDown's textDir update.
  689. // description:
  690. // Users shouldn't call this function; they should be calling
  691. // set('textDir', value)
  692. // tags:
  693. // private
  694. this.inherited(arguments);
  695. // update the drop down also (_ComboBoxMenuMixin)
  696. if(this.dropDown){
  697. this.dropDown._set("textDir", textDir);
  698. }
  699. }
  700. });
  701. });