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