ComboBox.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982
  1. if (!dojo._hasResource["dijit.form.ComboBox"]) { // _hasResource checks added
  2. // by build. Do not use
  3. // _hasResource directly in
  4. // your code.
  5. dojo._hasResource["dijit.form.ComboBox"] = true;
  6. dojo.provide("dijit.form.ComboBox");
  7. dojo.require("dojo.data.ItemFileReadStore");
  8. dojo.require("dijit.form.ValidationTextBox");
  9. dojo.requireLocalization("dijit.form", "ComboBox", null,
  10. "ko,zh,ja,zh-tw,ru,it,hu,ROOT,fr,pt,pl,es,de,cs");
  11. dojo.declare("dijit.form.ComboBoxMixin", null, {
  12. // summary:
  13. // Auto-completing text box, and base class for FilteringSelect widget.
  14. //
  15. // The drop down box's values are populated from an class called
  16. // a data provider, which returns a list of values based on the
  17. // characters
  18. // that the user has typed into the input box.
  19. //
  20. // Some of the options to the ComboBox are actually arguments to the
  21. // data
  22. // provider.
  23. //
  24. // You can assume that all the form widgets (and thus anything that
  25. // mixes
  26. // in ComboBoxMixin) will inherit from _FormWidget and thus the "this"
  27. // reference will also "be a" _FormWidget.
  28. // item: Object
  29. // This is the item returned by the dojo.data.store implementation that
  30. // provides the data for this cobobox, it's the currently selected item.
  31. item : null,
  32. // pageSize: Integer
  33. // Argument to data provider.
  34. // Specifies number of search results per page (before hitting "next"
  35. // button)
  36. pageSize : Infinity,
  37. // store: Object
  38. // Reference to data provider object used by this ComboBox
  39. store : null,
  40. // query: Object
  41. // A query that can be passed to 'store' to initially filter the items,
  42. // before doing further filtering based on searchAttr and the key.
  43. query : {},
  44. // autoComplete: Boolean
  45. // If you type in a partial string, and then tab out of the <input> box,
  46. // automatically copy the first entry displayed in the drop down list to
  47. // the <input> field
  48. autoComplete : true,
  49. // searchDelay: Integer
  50. // Delay in milliseconds between when user types something and we start
  51. // searching based on that value
  52. searchDelay : 100,
  53. // searchAttr: String
  54. // Searches pattern match against this field
  55. searchAttr : "name",
  56. // ignoreCase: Boolean
  57. // Set true if the ComboBox should ignore case when matching possible
  58. // items
  59. ignoreCase : true,
  60. // hasDownArrow: Boolean
  61. // Set this textbox to have a down arrow button.
  62. // Defaults to true.
  63. hasDownArrow : true,
  64. // _hasFocus: Boolean
  65. // Represents focus state of the textbox
  66. // TODO: get rid of this; it's unnecessary (but currently referenced in
  67. // FilteringSelect)
  68. _hasFocus : false,
  69. templateString : "<table class=\"dijit dijitReset dijitInlineTable dijitLeft\" cellspacing=\"0\" cellpadding=\"0\"\n\tid=\"widget_${id}\" name=\"${name}\" dojoAttachEvent=\"onmouseenter:_onMouse,onmouseleave:_onMouse\" waiRole=\"presentation\"\n\t><tr class=\"dijitReset\"\n\t\t><td class='dijitReset dijitStretch dijitInputField' width=\"100%\"\n\t\t\t><input type=\"text\" autocomplete=\"off\" name=\"${name}\"\n\t\t\tdojoAttachEvent=\"onkeypress, onkeyup, onfocus, compositionend\"\n\t\t\tdojoAttachPoint=\"textbox,focusNode\" waiRole=\"combobox\"\n\t\t/></td\n\t\t><td class=\"dijitReset dijitValidationIconField\" width=\"0%\"\n\t\t\t><div dojoAttachPoint='iconNode' class='dijitValidationIcon'></div\n\t\t\t><div class='dijitValidationIconText'>&Chi;</div\n\t\t></td\n\t\t><td class='dijitReset dijitRight dijitButtonNode dijitDownArrowButton' width=\"0%\"\n\t\t\tdojoAttachPoint=\"downArrowNode\"\n\t\t\tdojoAttachEvent=\"onmousedown:_onArrowMouseDown,onmouseup:_onMouse,onmouseenter:_onMouse,onmouseleave:_onMouse\"\n\t\t\t><div class=\"dijitDownArrowButtonInner\" waiRole=\"presentation\"\n\t\t\t\t><div class=\"dijitDownArrowButtonChar\">&#9660;</div\n\t\t\t></div\n\t\t></td\t\n\t></tr\n></table>\n",
  70. baseClass : "dijitComboBox",
  71. _lastDisplayedValue : "",
  72. getValue : function() {
  73. // don't get the textbox value but rather the previously set hidden
  74. // value
  75. return dijit.form.TextBox.superclass.getValue
  76. .apply(this, arguments);
  77. },
  78. setDisplayedValue : function(/* String */value) {
  79. this._lastDisplayedValue = value;
  80. this.setValue(value, true);
  81. },
  82. _getCaretPos : function(/* DomNode */element) {
  83. // khtml 3.5.2 has selection* methods as does webkit nightlies from
  84. // 2005-06-22
  85. if (typeof(element.selectionStart) == "number") {
  86. // FIXME: this is totally borked on Moz < 1.3. Any recourse?
  87. return element.selectionStart;
  88. } else if (dojo.isIE) {
  89. // in the case of a mouse click in a popup being handled,
  90. // then the document.selection is not the textarea, but the
  91. // popup
  92. // var r = document.selection.createRange();
  93. // hack to get IE 6 to play nice. What a POS browser.
  94. var tr = document.selection.createRange().duplicate();
  95. var ntr = element.createTextRange();
  96. tr.move("character", 0);
  97. ntr.move("character", 0);
  98. try {
  99. // If control doesnt have focus, you get an exception.
  100. // Seems to happen on reverse-tab, but can also happen on
  101. // tab (seems to be a race condition - only happens
  102. // sometimes).
  103. // There appears to be no workaround for this - googled for
  104. // quite a while.
  105. ntr.setEndPoint("EndToEnd", tr);
  106. return String(ntr.text).replace(/\r/g, "").length;
  107. } catch (e) {
  108. return 0; // If focus has shifted, 0 is fine for caret
  109. // pos.
  110. }
  111. }
  112. },
  113. _setCaretPos : function(/* DomNode */element, /* Number */location) {
  114. location = parseInt(location);
  115. this._setSelectedRange(element, location, location);
  116. },
  117. _setSelectedRange : function(/* DomNode */element, /* Number */start, /* Number */
  118. end) {
  119. if (!end) {
  120. end = element.value.length;
  121. } // NOTE: Strange - should be able to put caret at start of text?
  122. // Mozilla
  123. // parts borrowed from
  124. // http://www.faqts.com/knowledge_base/view.phtml/aid/13562/fid/130
  125. if (element.setSelectionRange) {
  126. dijit.focus(element);
  127. element.setSelectionRange(start, end);
  128. } else if (element.createTextRange) { // IE
  129. var range = element.createTextRange();
  130. with (range) {
  131. collapse(true);
  132. moveEnd('character', end);
  133. moveStart('character', start);
  134. select();
  135. }
  136. } else { // otherwise try the event-creation hack (our own
  137. // invention)
  138. // do we need these?
  139. element.value = element.value;
  140. element.blur();
  141. dijit.focus(element);
  142. // figure out how far back to go
  143. var dist = parseInt(element.value.length) - end;
  144. var tchar = String.fromCharCode(37);
  145. var tcc = tchar.charCodeAt(0);
  146. for (var x = 0; x < dist; x++) {
  147. var te = document.createEvent("KeyEvents");
  148. te.initKeyEvent("keypress", true, true, null, false, false,
  149. false, false, tcc, tcc);
  150. element.dispatchEvent(te);
  151. }
  152. }
  153. },
  154. onkeypress : function(/* Event */evt) {
  155. // summary: handles keyboard events
  156. // except for pasting case - ctrl + v(118)
  157. if (evt.altKey || (evt.ctrlKey && evt.charCode != 118)) {
  158. return;
  159. }
  160. var doSearch = false;
  161. this.item = null; // #4872
  162. if (this._isShowingNow) {
  163. this._popupWidget.handleKey(evt);
  164. }
  165. switch (evt.keyCode) {
  166. case dojo.keys.PAGE_DOWN :
  167. case dojo.keys.DOWN_ARROW :
  168. if (!this._isShowingNow || this._prev_key_esc) {
  169. this._arrowPressed();
  170. doSearch = true;
  171. } else {
  172. this._announceOption(this._popupWidget
  173. .getHighlightedOption());
  174. }
  175. dojo.stopEvent(evt);
  176. this._prev_key_backspace = false;
  177. this._prev_key_esc = false;
  178. break;
  179. case dojo.keys.PAGE_UP :
  180. case dojo.keys.UP_ARROW :
  181. if (this._isShowingNow) {
  182. this._announceOption(this._popupWidget
  183. .getHighlightedOption());
  184. }
  185. dojo.stopEvent(evt);
  186. this._prev_key_backspace = false;
  187. this._prev_key_esc = false;
  188. break;
  189. case dojo.keys.ENTER :
  190. // prevent submitting form if user presses enter
  191. // also prevent accepting the value if either Next or
  192. // Previous are selected
  193. var highlighted;
  194. if (this._isShowingNow
  195. && (highlighted = this._popupWidget
  196. .getHighlightedOption())) {
  197. // only stop event on prev/next
  198. if (highlighted == this._popupWidget.nextButton) {
  199. this._nextSearch(1);
  200. dojo.stopEvent(evt);
  201. break;
  202. } else if (highlighted == this._popupWidget.previousButton) {
  203. this._nextSearch(-1);
  204. dojo.stopEvent(evt);
  205. break;
  206. }
  207. } else {
  208. this.setDisplayedValue(this.getDisplayedValue());
  209. }
  210. // default case:
  211. // prevent submit, but allow event to bubble
  212. evt.preventDefault();
  213. // fall through
  214. case dojo.keys.TAB :
  215. var newvalue = this.getDisplayedValue();
  216. // #4617: if the user had More Choices selected fall into
  217. // the _onBlur handler
  218. if (this._popupWidget
  219. && (newvalue == this._popupWidget._messages["previousMessage"] || newvalue == this._popupWidget._messages["nextMessage"])) {
  220. break;
  221. }
  222. if (this._isShowingNow) {
  223. this._prev_key_backspace = false;
  224. this._prev_key_esc = false;
  225. if (this._popupWidget.getHighlightedOption()) {
  226. this._popupWidget.setValue({
  227. target : this._popupWidget
  228. .getHighlightedOption()
  229. }, true);
  230. }
  231. this._hideResultList();
  232. }
  233. break;
  234. case dojo.keys.SPACE :
  235. this._prev_key_backspace = false;
  236. this._prev_key_esc = false;
  237. if (this._isShowingNow
  238. && this._popupWidget.getHighlightedOption()) {
  239. dojo.stopEvent(evt);
  240. this._selectOption();
  241. this._hideResultList();
  242. } else {
  243. doSearch = true;
  244. }
  245. break;
  246. case dojo.keys.ESCAPE :
  247. this._prev_key_backspace = false;
  248. this._prev_key_esc = true;
  249. this._hideResultList();
  250. if (this._lastDisplayedValue != this.getDisplayedValue()) {
  251. this.setDisplayedValue(this._lastDisplayedValue);
  252. dojo.stopEvent(evt);
  253. } else {
  254. this.setValue(this.getValue(), false);
  255. }
  256. break;
  257. case dojo.keys.DELETE :
  258. case dojo.keys.BACKSPACE :
  259. this._prev_key_esc = false;
  260. this._prev_key_backspace = true;
  261. doSearch = true;
  262. break;
  263. case dojo.keys.RIGHT_ARROW : // fall through
  264. case dojo.keys.LEFT_ARROW : // fall through
  265. this._prev_key_backspace = false;
  266. this._prev_key_esc = false;
  267. break;
  268. default :// non char keys (F1-F12 etc..) shouldn't open list
  269. this._prev_key_backspace = false;
  270. this._prev_key_esc = false;
  271. if (dojo.isIE || evt.charCode != 0) {
  272. doSearch = true;
  273. }
  274. }
  275. if (this.searchTimer) {
  276. clearTimeout(this.searchTimer);
  277. }
  278. if (doSearch) {
  279. // need to wait a tad before start search so that the event
  280. // bubbles through DOM and we have value visible
  281. this.searchTimer = setTimeout(dojo.hitch(this,
  282. this._startSearchFromInput), this.searchDelay);
  283. }
  284. },
  285. _autoCompleteText : function(/* String */text) {
  286. // summary:
  287. // Fill in the textbox with the first item from the drop down list,
  288. // and
  289. // highlight the characters that were auto-completed. For example,
  290. // if user
  291. // typed "CA" and the drop down list appeared, the textbox would be
  292. // changed to
  293. // "California" and "ifornia" would be highlighted.
  294. // IE7: clear selection so next highlight works all the time
  295. this._setSelectedRange(this.focusNode, this.focusNode.value.length,
  296. this.focusNode.value.length);
  297. // does text autoComplete the value in the textbox?
  298. // #3744: escape regexp so the user's input isn't treated as a
  299. // regular expression.
  300. // Example: If the user typed "(" then the regexp would throw
  301. // "unterminated parenthetical."
  302. // Also see #2558 for the autocompletion bug this regular expression
  303. // fixes.
  304. if (new RegExp("^" + escape(this.focusNode.value), this.ignoreCase
  305. ? "i"
  306. : "").test(escape(text))) {
  307. var cpos = this._getCaretPos(this.focusNode);
  308. // only try to extend if we added the last character at the end
  309. // of the input
  310. if ((cpos + 1) > this.focusNode.value.length) {
  311. // only add to input node as we would overwrite
  312. // Capitalisation of chars
  313. // actually, that is ok
  314. this.focusNode.value = text;// .substr(cpos);
  315. // visually highlight the autocompleted characters
  316. this._setSelectedRange(this.focusNode, cpos,
  317. this.focusNode.value.length);
  318. dijit.setWaiState(this.focusNode, "valuenow", text);
  319. }
  320. } else {
  321. // text does not autoComplete; replace the whole value and
  322. // highlight
  323. this.focusNode.value = text;
  324. this._setSelectedRange(this.focusNode, 0,
  325. this.focusNode.value.length);
  326. dijit.setWaiState(this.focusNode, "valuenow", text);
  327. }
  328. },
  329. _openResultList : function(/* Object */results, /* Object */dataObject) {
  330. if (this.disabled
  331. || dataObject.query[this.searchAttr] != this._lastQuery) {
  332. return;
  333. }
  334. this._popupWidget.clearResultList();
  335. if (!results.length) {
  336. this._hideResultList();
  337. return;
  338. }
  339. // Fill in the textbox with the first item from the drop down list,
  340. // and
  341. // highlight the characters that were auto-completed. For example,
  342. // if user
  343. // typed "CA" and the drop down list appeared, the textbox would be
  344. // changed to
  345. // "California" and "ifornia" would be highlighted.
  346. var zerothvalue = new String(this.store.getValue(results[0],
  347. this.searchAttr));
  348. if (zerothvalue && this.autoComplete && !this._prev_key_backspace &&
  349. // when the user clicks the arrow button to show the full
  350. // list,
  351. // startSearch looks for "*".
  352. // it does not make sense to autocomplete
  353. // if they are just previewing the options available.
  354. (dataObject.query[this.searchAttr] != "*")) {
  355. this._autoCompleteText(zerothvalue);
  356. // announce the autocompleted value
  357. dijit.setWaiState(this.focusNode || this.domNode, "valuenow",
  358. zerothvalue);
  359. }
  360. this._popupWidget.createOptions(results, dataObject, dojo.hitch(
  361. this, this._getMenuLabelFromItem));
  362. // show our list (only if we have content, else nothing)
  363. this._showResultList();
  364. // #4091: tell the screen reader that the paging callback finished
  365. // by shouting the next choice
  366. if (dataObject.direction) {
  367. if (dataObject.direction == 1) {
  368. this._popupWidget.highlightFirstOption();
  369. } else if (dataObject.direction == -1) {
  370. this._popupWidget.highlightLastOption();
  371. }
  372. this._announceOption(this._popupWidget.getHighlightedOption());
  373. }
  374. },
  375. _showResultList : function() {
  376. this._hideResultList();
  377. var items = this._popupWidget.getItems(), visibleCount = Math.min(
  378. items.length, this.maxListLength);
  379. this._arrowPressed();
  380. // hide the tooltip
  381. this._displayMessage("");
  382. // Position the list and if it's too big to fit on the screen then
  383. // size it to the maximum possible height
  384. // Our dear friend IE doesnt take max-height so we need to calculate
  385. // that on our own every time
  386. // TODO: want to redo this, see
  387. // http://trac.dojotoolkit.org/ticket/3272,
  388. // http://trac.dojotoolkit.org/ticket/4108
  389. with (this._popupWidget.domNode.style) {
  390. // natural size of the list has changed, so erase old
  391. // width/height settings,
  392. // which were hardcoded in a previous call to this function (via
  393. // dojo.marginBox() call)
  394. width = "";
  395. height = "";
  396. }
  397. var best = this.open();
  398. // #3212: only set auto scroll bars if necessary
  399. // prevents issues with scroll bars appearing when they shouldn't
  400. // when node is made wider (fractional pixels cause this)
  401. var popupbox = dojo.marginBox(this._popupWidget.domNode);
  402. this._popupWidget.domNode.style.overflow = ((best.h == popupbox.h) && (best.w == popupbox.w))
  403. ? "hidden"
  404. : "auto";
  405. // #4134: borrow TextArea scrollbar test so content isn't covered by
  406. // scrollbar and horizontal scrollbar doesn't appear
  407. var newwidth = best.w;
  408. if (best.h < this._popupWidget.domNode.scrollHeight) {
  409. newwidth += 16;
  410. }
  411. dojo.marginBox(this._popupWidget.domNode, {
  412. h : best.h,
  413. w : Math.max(newwidth, this.domNode.offsetWidth)
  414. });
  415. },
  416. _hideResultList : function() {
  417. if (this._isShowingNow) {
  418. dijit.popup.close(this._popupWidget);
  419. this._arrowIdle();
  420. this._isShowingNow = false;
  421. }
  422. },
  423. _onBlur : function() {
  424. // summary: called magically when focus has shifted away from this
  425. // widget and it's dropdown
  426. this._hasFocus = false;
  427. this._hasBeenBlurred = true;
  428. this._hideResultList();
  429. this._arrowIdle();
  430. // if the user clicks away from the textbox OR tabs away, set the
  431. // value to the textbox value
  432. // #4617: if value is now more choices or previous choices, revert
  433. // the value
  434. var newvalue = this.getDisplayedValue();
  435. if (this._popupWidget
  436. && (newvalue == this._popupWidget._messages["previousMessage"] || newvalue == this._popupWidget._messages["nextMessage"])) {
  437. this.setValue(this._lastValueReported, true);
  438. } else {
  439. this.setDisplayedValue(newvalue);
  440. }
  441. },
  442. onfocus : function(/* Event */evt) {
  443. this._hasFocus = true;
  444. // update styling to reflect that we are focused
  445. this._onMouse(evt);
  446. },
  447. _announceOption : function(/* Node */node) {
  448. // summary:
  449. // a11y code that puts the highlighted option in the textbox
  450. // This way screen readers will know what is happening in the menu
  451. if (node == null) {
  452. return;
  453. }
  454. // pull the text value from the item attached to the DOM node
  455. var newValue;
  456. if (node == this._popupWidget.nextButton
  457. || node == this._popupWidget.previousButton) {
  458. newValue = node.innerHTML;
  459. } else {
  460. newValue = this.store.getValue(node.item, this.searchAttr);
  461. }
  462. // get the text that the user manually entered (cut off
  463. // autocompleted text)
  464. this.focusNode.value = this.focusNode.value.substring(0, this
  465. ._getCaretPos(this.focusNode));
  466. // autocomplete the rest of the option to announce change
  467. this._autoCompleteText(newValue);
  468. },
  469. _selectOption : function(/* Event */evt) {
  470. var tgt = null;
  471. if (!evt) {
  472. evt = {
  473. target : this._popupWidget.getHighlightedOption()
  474. };
  475. }
  476. // what if nothing is highlighted yet?
  477. if (!evt.target) {
  478. // handle autocompletion where the the user has hit ENTER or TAB
  479. this.setDisplayedValue(this.getDisplayedValue());
  480. return;
  481. // otherwise the user has accepted the autocompleted value
  482. } else {
  483. tgt = evt.target;
  484. }
  485. if (!evt.noHide) {
  486. this._hideResultList();
  487. this._setCaretPos(this.focusNode, this.store.getValue(tgt.item,
  488. this.searchAttr).length);
  489. }
  490. this._doSelect(tgt);
  491. },
  492. _doSelect : function(tgt) {
  493. this.item = tgt.item;
  494. this.setValue(this.store.getValue(tgt.item, this.searchAttr), true);
  495. },
  496. _onArrowMouseDown : function(evt) {
  497. // summary: callback when arrow is clicked
  498. if (this.disabled) {
  499. return;
  500. }
  501. dojo.stopEvent(evt);
  502. this.focus();
  503. if (this._isShowingNow) {
  504. this._hideResultList();
  505. } else {
  506. // forces full population of results, if they click
  507. // on the arrow it means they want to see more options
  508. this._startSearch("");
  509. }
  510. },
  511. _startSearchFromInput : function() {
  512. this._startSearch(this.focusNode.value);
  513. },
  514. _startSearch : function(/* String */key) {
  515. if (!this._popupWidget) {
  516. this._popupWidget = new dijit.form._ComboBoxMenu({
  517. onChange : dojo.hitch(this, this._selectOption)
  518. });
  519. }
  520. // create a new query to prevent accidentally querying for a hidden
  521. // value from FilteringSelect's keyField
  522. var query = this.query;
  523. this._lastQuery = query[this.searchAttr] = key + "*";
  524. var dataObject = this.store.fetch({
  525. queryOptions : {
  526. ignoreCase : this.ignoreCase,
  527. deep : true
  528. },
  529. query : query,
  530. onComplete : dojo.hitch(this, "_openResultList"),
  531. start : 0,
  532. count : this.pageSize
  533. });
  534. function nextSearch(dataObject, direction) {
  535. dataObject.start += dataObject.count * direction;
  536. // #4091: tell callback the direction of the paging so the
  537. // screen reader knows which menu option to shout
  538. dataObject.direction = direction;
  539. dataObject.store.fetch(dataObject);
  540. }
  541. this._nextSearch = this._popupWidget.onPage = dojo.hitch(this,
  542. nextSearch, dataObject);
  543. },
  544. _getValueField : function() {
  545. return this.searchAttr;
  546. },
  547. // ///////////// Event handlers /////////////////////
  548. _arrowPressed : function() {
  549. if (!this.disabled && this.hasDownArrow) {
  550. dojo.addClass(this.downArrowNode, "dijitArrowButtonActive");
  551. }
  552. },
  553. _arrowIdle : function() {
  554. if (!this.disabled && this.hasDownArrow) {
  555. dojo.removeClass(this.downArrowNode, "dojoArrowButtonPushed");
  556. }
  557. },
  558. compositionend : function(/* Event */evt) {
  559. // summary: When inputting characters using an input method, such as
  560. // Asian
  561. // languages, it will generate this event instead of onKeyDown event
  562. // Note: this event is only triggered in FF (not in IE)
  563. this.onkeypress({
  564. charCode : -1
  565. });
  566. },
  567. // ////////// INITIALIZATION METHODS
  568. // ///////////////////////////////////////
  569. constructor : function() {
  570. this.query = {};
  571. },
  572. postMixInProperties : function() {
  573. if (!this.hasDownArrow) {
  574. this.baseClass = "dijitTextBox";
  575. }
  576. if (!this.store) {
  577. // if user didn't specify store, then assume there are option
  578. // tags
  579. var items = this.srcNodeRef ? dojo.query("> option",
  580. this.srcNodeRef).map(function(node) {
  581. node.style.display = "none";
  582. return {
  583. value : node.getAttribute("value"),
  584. name : String(node.innerHTML)
  585. };
  586. }) : {};
  587. this.store = new dojo.data.ItemFileReadStore({
  588. data : {
  589. identifier : this._getValueField(),
  590. items : items
  591. }
  592. });
  593. // if there is no value set and there is an option list,
  594. // set the value to the first value to be consistent with native
  595. // Select
  596. if (items && items.length && !this.value) {
  597. // For <select>, IE does not let you set the value attribute
  598. // of the srcNodeRef (and thus dojo.mixin does not copy it).
  599. // IE does understand selectedIndex though, which is
  600. // automatically set by the selected attribute of an option
  601. // tag
  602. this.value = items[this.srcNodeRef.selectedIndex != -1
  603. ? this.srcNodeRef.selectedIndex
  604. : 0][this._getValueField()];
  605. }
  606. }
  607. },
  608. uninitialize : function() {
  609. if (this._popupWidget) {
  610. this._hideResultList();
  611. this._popupWidget.destroy()
  612. };
  613. },
  614. _getMenuLabelFromItem : function(/* Item */item) {
  615. return {
  616. html : false,
  617. label : this.store.getValue(item, this.searchAttr)
  618. };
  619. },
  620. open : function() {
  621. this._isShowingNow = true;
  622. return dijit.popup.open({
  623. popup : this._popupWidget,
  624. around : this.domNode,
  625. parent : this
  626. });
  627. }
  628. });
  629. dojo.declare("dijit.form._ComboBoxMenu", [dijit._Widget, dijit._Templated],
  630. {
  631. // summary:
  632. // Focus-less div based menu for internal use in ComboBox
  633. templateString : "<div class='dijitMenu' dojoAttachEvent='onmousedown,onmouseup,onmouseover,onmouseout' tabIndex='-1' style='overflow:\"auto\";'>"
  634. + "<div class='dijitMenuItem dijitMenuPreviousButton' dojoAttachPoint='previousButton'></div>"
  635. + "<div class='dijitMenuItem dijitMenuNextButton' dojoAttachPoint='nextButton'></div>"
  636. + "</div>",
  637. _messages : null,
  638. postMixInProperties : function() {
  639. this._messages = dojo.i18n.getLocalization("dijit.form",
  640. "ComboBox", this.lang);
  641. this.inherited("postMixInProperties", arguments);
  642. },
  643. setValue : function(/* Object */value) {
  644. this.value = value;
  645. this.onChange(value);
  646. },
  647. onChange : function(/* Object */value) {
  648. },
  649. onPage : function(/* Number */direction) {
  650. },
  651. postCreate : function() {
  652. // fill in template with i18n messages
  653. this.previousButton.innerHTML = this._messages["previousMessage"];
  654. this.nextButton.innerHTML = this._messages["nextMessage"];
  655. this.inherited("postCreate", arguments);
  656. },
  657. onClose : function() {
  658. this._blurOptionNode();
  659. },
  660. _createOption : function(/* Object */item, labelFunc) {
  661. // summary: creates an option to appear on the popup menu
  662. // subclassed by FilteringSelect
  663. var labelObject = labelFunc(item);
  664. var menuitem = document.createElement("div");
  665. if (labelObject.html) {
  666. menuitem.innerHTML = labelObject.label;
  667. } else {
  668. menuitem
  669. .appendChild(document.createTextNode(labelObject.label));
  670. }
  671. // #3250: in blank options, assign a normal height
  672. if (menuitem.innerHTML == "") {
  673. menuitem.innerHTML = "&nbsp;"
  674. }
  675. menuitem.item = item;
  676. return menuitem;
  677. },
  678. createOptions : function(results, dataObject, labelFunc) {
  679. // this._dataObject=dataObject;
  680. // this._dataObject.onComplete=dojo.hitch(comboBox,
  681. // comboBox._openResultList);
  682. // display "Previous . . ." button
  683. this.previousButton.style.display = dataObject.start == 0
  684. ? "none"
  685. : "";
  686. // create options using _createOption function defined by parent
  687. // ComboBox (or FilteringSelect) class
  688. // #2309: iterate over cache nondestructively
  689. var _this = this;
  690. dojo.forEach(results, function(item) {
  691. var menuitem = _this._createOption(item, labelFunc);
  692. menuitem.className = "dijitMenuItem";
  693. _this.domNode.insertBefore(menuitem, _this.nextButton);
  694. });
  695. // display "Next . . ." button
  696. this.nextButton.style.display = dataObject.count == results.length
  697. ? ""
  698. : "none";
  699. },
  700. clearResultList : function() {
  701. // keep the previous and next buttons of course
  702. while (this.domNode.childNodes.length > 2) {
  703. this.domNode
  704. .removeChild(this.domNode.childNodes[this.domNode.childNodes.length
  705. - 2]);
  706. }
  707. },
  708. // these functions are called in showResultList
  709. getItems : function() {
  710. return this.domNode.childNodes;
  711. },
  712. getListLength : function() {
  713. return this.domNode.childNodes.length - 2;
  714. },
  715. onmousedown : function(/* Event */evt) {
  716. dojo.stopEvent(evt);
  717. },
  718. onmouseup : function(/* Event */evt) {
  719. if (evt.target === this.domNode) {
  720. return;
  721. } else if (evt.target == this.previousButton) {
  722. this.onPage(-1);
  723. } else if (evt.target == this.nextButton) {
  724. this.onPage(1);
  725. } else {
  726. var tgt = evt.target;
  727. // while the clicked node is inside the div
  728. while (!tgt.item) {
  729. // recurse to the top
  730. tgt = tgt.parentNode;
  731. }
  732. this.setValue({
  733. target : tgt
  734. }, true);
  735. }
  736. },
  737. onmouseover : function(/* Event */evt) {
  738. if (evt.target === this.domNode) {
  739. return;
  740. }
  741. var tgt = evt.target;
  742. if (!(tgt == this.previousButton || tgt == this.nextButton)) {
  743. // while the clicked node is inside the div
  744. while (!tgt.item) {
  745. // recurse to the top
  746. tgt = tgt.parentNode;
  747. }
  748. }
  749. this._focusOptionNode(tgt);
  750. },
  751. onmouseout : function(/* Event */evt) {
  752. if (evt.target === this.domNode) {
  753. return;
  754. }
  755. this._blurOptionNode();
  756. },
  757. _focusOptionNode : function(/* DomNode */node) {
  758. // summary:
  759. // does the actual highlight
  760. if (this._highlighted_option != node) {
  761. this._blurOptionNode();
  762. this._highlighted_option = node;
  763. dojo.addClass(this._highlighted_option, "dijitMenuItemHover");
  764. }
  765. },
  766. _blurOptionNode : function() {
  767. // summary:
  768. // removes highlight on highlighted option
  769. if (this._highlighted_option) {
  770. dojo
  771. .removeClass(this._highlighted_option,
  772. "dijitMenuItemHover");
  773. this._highlighted_option = null;
  774. }
  775. },
  776. _highlightNextOption : function() {
  777. // because each press of a button clears the menu,
  778. // the highlighted option sometimes becomes detached from the menu!
  779. // test to see if the option has a parent to see if this is the
  780. // case.
  781. if (!this.getHighlightedOption()) {
  782. this
  783. ._focusOptionNode(this.domNode.firstChild.style.display == "none"
  784. ? this.domNode.firstChild.nextSibling
  785. : this.domNode.firstChild);
  786. } else if (this._highlighted_option.nextSibling
  787. && this._highlighted_option.nextSibling.style.display != "none") {
  788. this._focusOptionNode(this._highlighted_option.nextSibling);
  789. }
  790. // scrollIntoView is called outside of _focusOptionNode because in
  791. // IE putting it inside causes the menu to scroll up on mouseover
  792. dijit.scrollIntoView(this._highlighted_option);
  793. },
  794. highlightFirstOption : function() {
  795. // highlight the non-Previous choices option
  796. this._focusOptionNode(this.domNode.firstChild.nextSibling);
  797. dijit.scrollIntoView(this._highlighted_option);
  798. },
  799. highlightLastOption : function() {
  800. // highlight the noon-More choices option
  801. this._focusOptionNode(this.domNode.lastChild.previousSibling);
  802. dijit.scrollIntoView(this._highlighted_option);
  803. },
  804. _highlightPrevOption : function() {
  805. // if nothing selected, highlight last option
  806. // makes sense if you select Previous and try to keep scrolling up
  807. // the list
  808. if (!this.getHighlightedOption()) {
  809. this
  810. ._focusOptionNode(this.domNode.lastChild.style.display == "none"
  811. ? this.domNode.lastChild.previousSibling
  812. : this.domNode.lastChild);
  813. } else if (this._highlighted_option.previousSibling
  814. && this._highlighted_option.previousSibling.style.display != "none") {
  815. this._focusOptionNode(this._highlighted_option.previousSibling);
  816. }
  817. dijit.scrollIntoView(this._highlighted_option);
  818. },
  819. _page : function(/* Boolean */up) {
  820. var scrollamount = 0;
  821. var oldscroll = this.domNode.scrollTop;
  822. var height = parseInt(dojo.getComputedStyle(this.domNode).height);
  823. // if no item is highlighted, highlight the first option
  824. if (!this.getHighlightedOption()) {
  825. this._highlightNextOption();
  826. }
  827. while (scrollamount < height) {
  828. if (up) {
  829. // stop at option 1
  830. if (!this.getHighlightedOption().previousSibling
  831. || this._highlighted_option.previousSibling.style.display == "none") {
  832. break;
  833. }
  834. this._highlightPrevOption();
  835. } else {
  836. // stop at last option
  837. if (!this.getHighlightedOption().nextSibling
  838. || this._highlighted_option.nextSibling.style.display == "none") {
  839. break;
  840. }
  841. this._highlightNextOption();
  842. }
  843. // going backwards
  844. var newscroll = this.domNode.scrollTop;
  845. scrollamount += (newscroll - oldscroll) * (up ? -1 : 1);
  846. oldscroll = newscroll;
  847. }
  848. },
  849. pageUp : function() {
  850. this._page(true);
  851. },
  852. pageDown : function() {
  853. this._page(false);
  854. },
  855. getHighlightedOption : function() {
  856. // summary:
  857. // Returns the highlighted option.
  858. return this._highlighted_option
  859. && this._highlighted_option.parentNode
  860. ? this._highlighted_option
  861. : null;
  862. },
  863. handleKey : function(evt) {
  864. switch (evt.keyCode) {
  865. case dojo.keys.DOWN_ARROW :
  866. this._highlightNextOption();
  867. break;
  868. case dojo.keys.PAGE_DOWN :
  869. this.pageDown();
  870. break;
  871. case dojo.keys.UP_ARROW :
  872. this._highlightPrevOption();
  873. break;
  874. case dojo.keys.PAGE_UP :
  875. this.pageUp();
  876. break;
  877. }
  878. }
  879. });
  880. dojo.declare("dijit.form.ComboBox", [dijit.form.ValidationTextBox,
  881. dijit.form.ComboBoxMixin], {
  882. postMixInProperties : function() {
  883. dijit.form.ComboBoxMixin.prototype.postMixInProperties
  884. .apply(this, arguments);
  885. dijit.form.ValidationTextBox.prototype.postMixInProperties
  886. .apply(this, arguments);
  887. }
  888. });
  889. }