]>
git.wh0rd.org - tt-rss.git/blob - lib/dijit/form/_AutoCompleterMixin.js.uncompressed.js
edbc137a248cff09b906e7620f52b839d2d1308d
1 define("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
7 "dojo/_base/lang", // lang.clone lang.hitch
9 "dojo/regexp", // regexp.escapeString
10 "dojo/sniff", // has("ie")
11 "dojo/string", // string.substitute
13 "../registry", // registry.byId
14 "./_TextBoxMixin", // defines _TextBoxMixin.selectInputText
16 ], function(filter
, declare
, domAttr
, event
, keys
, lang
, query
, regexp
, has
, string
,
17 DataList
, registry
, _TextBoxMixin
, SearchMixin
){
20 // dijit/form/_AutoCompleterMixin
22 return declare("dijit.form._AutoCompleterMixin", SearchMixin
, {
24 // A mixin that implements the base functionality for `dijit/form/ComboBox`/`dijit/form/FilteringSelect`
26 // All widgets that mix in dijit/form/_AutoCompleterMixin must extend `dijit/form/_FormValueWidget`.
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.
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
41 // highlightMatch: String
42 // One of: "first", "all" or "none".
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}*'
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",
53 // The entries in the drop down list come from this attribute in the
55 // If not specified, the searchAttr attribute is used instead.
59 // Specifies how to interpret the labelAttr in the data store items.
60 // Can be "html" or "text".
63 // Flags to _HasDropDown to limit height of drop down to make it fit in viewport
66 // For backwards compatibility let onClick events propagate, even clicks on the down arrow button
67 _stopClickEvents
: false,
69 _getCaretPos: function(/*DomNode*/ element
){
70 // khtml 3.5.2 has selection* methods as does webkit nightlies from 2005-06-22
72 if(typeof(element
.selectionStart
) == "number"){
73 // FIXME: this is totally borked on Moz < 1.3. Any recourse?
74 pos
= element
.selectionStart
;
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);
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
;
91 // If focus has shifted, 0 is fine for caret pos.
97 _setCaretPos: function(/*DomNode*/ element
, /*Number*/ location
){
98 location
= parseInt(location
);
99 _TextBoxMixin
.selectInputText(element
, location
, location
);
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");
109 _onKey: function(/*Event*/ evt
){
111 // Handles keyboard events
113 if(evt
.charCode
>= 32){ return; } // alphanumeric reserved for searching
115 var key
= evt
.charCode
|| evt
.keyCode
;
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
122 var pw
= this.dropDown
;
123 var highlighted
= null;
126 // _HasDropDown will do some of the work:
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
);
135 if(evt
.altKey
|| evt
.ctrlKey
|| evt
.metaKey
){ return; } // don't process keys with modifiers - but we want shift+TAB
138 highlighted
= pw
.getHighlightedOption();
142 case keys
.DOWN_ARROW
:
145 // Keystroke caused ComboBox_menu to move to a different item.
146 // Copy new item to <input> box.
148 this._announceOption(highlighted
);
154 // prevent submitting form if user presses enter. Also
155 // prevent accepting the value if either Next or Previous
158 // only stop event on prev/next
159 if(highlighted
== pw
.nextButton
){
161 event
.stop(evt
); // prevent submit
163 }else if(highlighted
== pw
.previousButton
){
164 this._nextSearch(-1);
165 event
.stop(evt
); // prevent submit
168 event
.stop(evt
); // prevent submit if ENTER was to choose an item
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
177 var newvalue
= this.get('displayedValue');
178 // if the user had More Choices selected fall into the
181 newvalue
== pw
._messages
["previousMessage"] ||
182 newvalue
== pw
._messages
["nextMessage"])
187 this._selectOption(highlighted
);
193 this._lastQuery
= null; // in case results come back later
194 this.closeDropDown();
200 _autoCompleteText: function(/*String*/ text
){
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.
208 var fn
= this.focusNode
;
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
);
225 // text does not autoComplete; replace the whole value and highlight
227 _TextBoxMixin
.selectInputText(fn
);
231 _openResultList: function(/*Object*/ results
, /*Object*/ query
, /*Object*/ options
){
233 // Callback when a search completes.
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();
244 this._nextSearch
= this.dropDown
.onPage
= lang
.hitch(this, function(direction
){
245 results
.nextPage(direction
!== -1);
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
255 this.dropDown
.createOptions(
258 lang
.hitch(this, "_getMenuLabelFromItem")
261 // show our list (only if we have content, else nothing)
262 this._showResultList();
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();
274 this._announceOption(this.dropDown
.getHighlightedOption());
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
286 _showResultList: function(){
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);
292 this.domNode
.setAttribute("aria-expanded", "true");
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();
302 isLoaded: function(){
303 // signal to _HasDropDown that it needs to call loadDropDown() to load the
304 // drop down asynchronously before displaying it
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.
314 this.inherited(arguments
);
315 this.domNode
.setAttribute("aria-expanded", "false");
316 this.focusNode
.removeAttribute("aria-activedescendant");
320 _setBlurValue: function(){
321 // if the user clicks away from the textbox OR tabs away, set the
322 // value to the textbox value
324 // if value is now more choices or previous choices, revert
326 var newvalue
= this.get('displayedValue');
327 var pw
= this.dropDown
;
329 newvalue
== pw
._messages
["previousMessage"] ||
330 newvalue
== pw
._messages
["nextMessage"]
333 this._setValueAttr(this._lastValueReported
, true);
334 }else if(typeof this.item
== "undefined"){
335 // Update 'value' (ex: KY) according to currently displayed text
337 this.set('displayedValue', newvalue
);
339 if(this.value
!= this._lastValueReported
){
340 this._handleOnChange(this.value
, true);
342 this._refreshState();
346 _setItemAttr: function(/*item*/ item
, /*Boolean?*/ priorityChange
, /*String?*/ displayedValue
){
348 // Set the displayed valued in the input box, and the hidden value
349 // that gets submitted, based on a dojo.data store item.
351 // Users shouldn't call this function; they should be calling
352 // set('item', value)
358 displayedValue
= this.store
._oldAPI
? // remove getValue() for 2.0 (old dojo.data API)
359 this.store
.getValue(item
, this.searchAttr
) : item
[this.searchAttr
];
361 value
= this._getValueField() != this.searchAttr
? this.store
.getIdentity(item
) : displayedValue
;
363 this.set('value', value
, priorityChange
, displayedValue
, item
);
366 _announceOption: function(/*Node*/ node
){
368 // a11y code that puts the highlighted option in the textbox.
369 // This way screen readers will know what is happening in the
375 // pull the text value from the item attached to the DOM node
377 if(node
== this.dropDown
.nextButton
||
378 node
== this.dropDown
.previousButton
){
379 newValue
= node
.innerHTML
;
380 this.item
= undefined;
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
);
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
);
396 _selectOption: function(/*DomNode*/ target
){
398 // Menu callback function, called when an item in the menu is selected.
399 this.closeDropDown();
401 this._announceOption(target
);
403 this._setCaretPos(this.focusNode
, this.focusNode
.value
.length
);
404 this._handleOnChange(this.value
, true);
407 _startSearchAll: function(){
408 this._startSearch('');
411 _startSearchFromInput: function(){
412 this.item
= undefined; // undefined means item needs to be set
413 this.inherited(arguments
);
416 _startSearch: function(/*String*/ key
){
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.
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
),
428 textDir
: this.textDir
430 this.focusNode
.removeAttribute("aria-activedescendant");
431 this.textbox
.setAttribute("aria-owns",popupId
); // associate popup with textbox
433 this._lastInput
= key
; // Store exactly what was entered by the user.
434 this.inherited(arguments
);
437 _getValueField: function(){
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
;
444 //////////// INITIALIZATION METHODS ///////////////////////////////////////
446 postMixInProperties: function(){
447 this.inherited(arguments
);
449 var srcNodeRef
= this.srcNodeRef
;
450 // if user didn't specify store, then assume there are option tags
451 this.store
= new DataList({}, srcNodeRef
);
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());
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
];
470 postCreate: function(){
472 // Subclasses must call this method from their postCreate() methods
476 // find any associated label element and add to ComboBox node.
477 var label
=query('label[for="'+this.id
+'"]');
479 if(!label
[0].id
){ label
[0].id
= this.id
+ "_label"; }
480 this.domNode
.setAttribute("aria-labelledby", label
[0].id
);
483 this.inherited(arguments
);
484 this.connect(this, "onSearch", "_openResultList");
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
);
495 return {html
: labelType
== "html", label
: label
};
498 doHighlight: function(/*String*/ label
, /*String*/ find
){
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.
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 "<"
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)
522 _escapeHtml: function(/*String*/ str
){
523 // TODO Should become dojo.html.entities(), when exists use instead
525 // Adds escape sequences for special characters in XML: `&<>"'`
526 str
= String(str
).replace(/&/gm, "&").replace(/</gm
, "<")
527 .replace(/>/gm, ">").replace(/"/gm, ""
;"); //balance"
528 return str
; // string
532 // Overrides the _FormWidget.reset().
533 // Additionally reset the .item (to clean up).
535 this.inherited(arguments
);
538 labelFunc: function(item
, store
){
540 // Computes the label to display based on the dojo.data store item.
542 // The item from the store
543 // store: dojo/store/api/Store
546 // The label that the ComboBox should display
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
557 _setValueAttr: function(/*String*/ value
, /*Boolean?*/ priorityChange
, /*String?*/ displayedValue
, /*item?*/ item
){
559 // Hook so set('value', value) works.
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
);
566 _setTextDirAttr: function(/*String*/ textDir
){
568 // Setter for textDir, needed for the dropDown's textDir update.
570 // Users shouldn't call this function; they should be calling
571 // set('textDir', value)
574 this.inherited(arguments
);
575 // update the drop down also (_ComboBoxMenuMixin)
577 this.dropDown
._set("textDir", textDir
);