]>
git.wh0rd.org - tt-rss.git/blob - lib/dijit/form/_AutoCompleterMixin.js.uncompressed.js
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
9 "dojo/_base/lang", // lang.clone lang.hitch
10 "dojo/query", // query
11 "dojo/regexp", // regexp.escapeString
12 "dojo/_base/sniff", // has("ie")
13 "dojo/string", // string.substitute
14 "dojo/_base/window", // win.doc.selection.createRange
16 "../registry", // registry.byId
17 "./_TextBoxMixin" // defines _TextBoxMixin.selectInputText
18 ], function(connect
, filter
, declare
, Deferred
, domAttr
, event
, keys
, lang
, query
, regexp
, has
, string
, win
,
19 DataList
, registry
, _TextBoxMixin
){
22 // dijit/form/_AutoCompleterMixin
24 // A mixin that implements the base functionality for `dijit.form.ComboBox`/`dijit.form.FilteringSelect`
27 return declare("dijit.form._AutoCompleterMixin", null, {
29 // A mixin that implements the base functionality for `dijit.form.ComboBox`/`dijit.form.FilteringSelect`
31 // All widgets that mix in dijit.form._AutoCompleterMixin must extend `dijit.form._FormValueWidget`.
36 // This is the item returned by the dojo.data.store implementation that
37 // provides the data for this ComboBox, it's the currently selected item.
41 // Argument to data provider.
42 // Specifies number of search results per page (before hitting "next" button)
45 // store: [const] dojo.store.api.Store
46 // Reference to data provider object used by this ComboBox
49 // fetchProperties: Object
50 // Mixin to the store's fetch.
51 // For example, to set the sort order of the ComboBox menu, pass:
52 // | { sort: [{attribute:"name",descending: true}] }
53 // To override the default queryOptions so that deep=false, do:
54 // | { queryOptions: {ignoreCase: true, deep: false} }
58 // A query that can be passed to 'store' to initially filter the items,
59 // before doing further filtering based on `searchAttr` and the key.
60 // Any reference to the `searchAttr` is ignored.
63 // autoComplete: Boolean
64 // If user types in a partial string, and then tab out of the `<input>` box,
65 // automatically copy the first entry displayed in the drop down list to
66 // the `<input>` field
69 // highlightMatch: String
70 // One of: "first", "all" or "none".
72 // If the ComboBox/FilteringSelect opens with the search results and the searched
73 // string can be found, it will be highlighted. If set to "all"
74 // then will probably want to change `queryExpr` parameter to '*${0}*'
76 // Highlighting is only performed when `labelType` is "text", so as to not
77 // interfere with any HTML markup an HTML label might contain.
78 highlightMatch
: "first",
80 // searchDelay: Integer
81 // Delay in milliseconds between when user types something and we start
82 // searching based on that value
86 // Search for items in the data store where this attribute (in the item)
87 // matches what the user typed
91 // The entries in the drop down list come from this attribute in the
93 // If not specified, the searchAttr attribute is used instead.
97 // Specifies how to interpret the labelAttr in the data store items.
98 // Can be "html" or "text".
102 // This specifies what query ComboBox/FilteringSelect sends to the data store,
103 // based on what the user has typed. Changing this expression will modify
104 // whether the drop down shows only exact matches, a "starting with" match,
105 // etc. Use it in conjunction with highlightMatch.
106 // dojo.data query expression pattern.
107 // `${0}` will be substituted for the user text.
108 // `*` is used for wildcards.
109 // `${0}*` means "starts with", `*${0}*` means "contains", `${0}` means "is"
112 // ignoreCase: Boolean
113 // Set true if the ComboBox/FilteringSelect should ignore case when matching possible items
116 // Flags to _HasDropDown to limit height of drop down to make it fit in viewport
119 // For backwards compatibility let onClick events propagate, even clicks on the down arrow button
120 _stopClickEvents
: false,
122 _getCaretPos: function(/*DomNode*/ element
){
123 // khtml 3.5.2 has selection* methods as does webkit nightlies from 2005-06-22
125 if(typeof(element
.selectionStart
) == "number"){
126 // FIXME: this is totally borked on Moz < 1.3. Any recourse?
127 pos
= element
.selectionStart
;
129 // in the case of a mouse click in a popup being handled,
130 // then the win.doc.selection is not the textarea, but the popup
131 // var r = win.doc.selection.createRange();
132 // hack to get IE 6 to play nice. What a POS browser.
133 var tr
= win
.doc
.selection
.createRange().duplicate();
134 var ntr
= element
.createTextRange();
135 tr
.move("character",0);
136 ntr
.move("character",0);
138 // If control doesn't have focus, you get an exception.
139 // Seems to happen on reverse-tab, but can also happen on tab (seems to be a race condition - only happens sometimes).
140 // There appears to be no workaround for this - googled for quite a while.
141 ntr
.setEndPoint("EndToEnd", tr
);
142 pos
= String(ntr
.text
).replace(/\r/g,"").length
;
144 // If focus has shifted, 0 is fine for caret pos.
150 _setCaretPos: function(/*DomNode*/ element
, /*Number*/ location
){
151 location
= parseInt(location
);
152 _TextBoxMixin
.selectInputText(element
, location
, location
);
155 _setDisabledAttr: function(/*Boolean*/ value
){
156 // Additional code to set disabled state of ComboBox node.
157 // Overrides _FormValueWidget._setDisabledAttr() or ValidationTextBox._setDisabledAttr().
158 this.inherited(arguments
);
159 this.domNode
.setAttribute("aria-disabled", value
);
162 _abortQuery: function(){
163 // stop in-progress query
164 if(this.searchTimer
){
165 clearTimeout(this.searchTimer
);
166 this.searchTimer
= null;
168 if(this._fetchHandle
){
169 if(this._fetchHandle
.cancel
){
170 this._cancelingQuery
= true;
171 this._fetchHandle
.cancel();
172 this._cancelingQuery
= false;
174 this._fetchHandle
= null;
178 _onInput: function(/*Event*/ evt
){
180 // Handles paste events
181 this.inherited(arguments
);
182 if(evt
.charOrCode
== 229){ // IME or cut/paste event
187 _onKey: function(/*Event*/ evt
){
189 // Handles keyboard events
191 if(this.disabled
|| this.readOnly
){ return; }
192 var key
= evt
.charOrCode
;
194 // except for cutting/pasting case - ctrl + x/v
195 if(evt
.altKey
|| ((evt
.ctrlKey
|| evt
.metaKey
) && (key
!= 'x' && key
!= 'v')) || key
== keys
.SHIFT
){
196 return; // throw out weird key combinations and spurious events
199 var doSearch
= false;
200 var pw
= this.dropDown
;
201 var highlighted
= null;
202 this._prev_key_backspace
= false;
205 // _HasDropDown will do some of the work:
206 // 1. when drop down is not yet shown:
207 // - if user presses the down arrow key, call loadDropDown()
208 // 2. when drop down is already displayed:
209 // - on ESC key, call closeDropDown()
210 // - otherwise, call dropDown.handleKey() to process the keystroke
211 this.inherited(arguments
);
214 highlighted
= pw
.getHighlightedOption();
218 case keys
.DOWN_ARROW
:
221 // Keystroke caused ComboBox_menu to move to a different item.
222 // Copy new item to <input> box.
224 this._announceOption(highlighted
);
230 // prevent submitting form if user presses enter. Also
231 // prevent accepting the value if either Next or Previous
234 // only stop event on prev/next
235 if(highlighted
== pw
.nextButton
){
239 }else if(highlighted
== pw
.previousButton
){
240 this._nextSearch(-1);
245 // Update 'value' (ex: KY) according to currently displayed text
246 this._setBlurValue(); // set value if needed
247 this._setCaretPos(this.focusNode
, this.focusNode
.value
.length
); // move cursor to end and cancel highlighting
250 // if enter pressed while drop down is open, or for FilteringSelect,
251 // if we are in the middle of a query to convert a directly typed in value to an item,
253 if(this._opened
|| this._fetchHandle
){
259 var newvalue
= this.get('displayedValue');
260 // if the user had More Choices selected fall into the
263 newvalue
== pw
._messages
["previousMessage"] ||
264 newvalue
== pw
._messages
["nextMessage"])
269 this._selectOption(highlighted
);
275 this._lastQuery
= null; // in case results come back later
276 this.closeDropDown();
282 // user is effectively clicking a choice in the drop down menu
284 this._selectOption(highlighted
);
285 this.closeDropDown();
287 // user typed a space into the input box, treat as normal character
294 this._prev_key_backspace
= true;
299 // Non char keys (F1-F12 etc..) shouldn't open list.
300 // Ascii characters and IME input (Chinese, Japanese etc.) should.
301 //IME input produces keycode == 229.
302 doSearch
= typeof key
== 'string' || key
== 229;
305 // need to wait a tad before start search so that the event
306 // bubbles through DOM and we have value visible
307 this.item
= undefined; // undefined means item needs to be set
308 this.searchTimer
= setTimeout(lang
.hitch(this, "_startSearchFromInput"),1);
312 _autoCompleteText: function(/*String*/ text
){
314 // Fill in the textbox with the first item from the drop down
315 // list, and highlight the characters that were
316 // auto-completed. For example, if user typed "CA" and the
317 // drop down list appeared, the textbox would be changed to
318 // "California" and "ifornia" would be highlighted.
320 var fn
= this.focusNode
;
322 // IE7: clear selection so next highlight works all the time
323 _TextBoxMixin
.selectInputText(fn
, fn
.value
.length
);
324 // does text autoComplete the value in the textbox?
325 var caseFilter
= this.ignoreCase
? 'toLowerCase' : 'substr';
326 if(text
[caseFilter
](0).indexOf(this.focusNode
.value
[caseFilter
](0)) == 0){
327 var cpos
= this.autoComplete
? this._getCaretPos(fn
) : fn
.value
.length
;
328 // only try to extend if we added the last character at the end of the input
329 if((cpos
+1) > fn
.value
.length
){
330 // only add to input node as we would overwrite Capitalisation of chars
331 // actually, that is ok
332 fn
.value
= text
;//.substr(cpos);
333 // visually highlight the autocompleted characters
334 _TextBoxMixin
.selectInputText(fn
, cpos
);
337 // text does not autoComplete; replace the whole value and highlight
339 _TextBoxMixin
.selectInputText(fn
);
343 _openResultList: function(/*Object*/ results
, /*Object*/ query
, /*Object*/ options
){
345 // Callback when a search completes.
347 // 1. generates drop-down list and calls _showResultList() to display it
348 // 2. if this result list is from user pressing "more choices"/"previous choices"
349 // then tell screen reader to announce new option
350 this._fetchHandle
= null;
353 (query
[this.searchAttr
] !== this._lastQuery
) // TODO: better way to avoid getting unwanted notify
357 var wasSelected
= this.dropDown
.getHighlightedOption();
358 this.dropDown
.clearResultList();
359 if(!results
.length
&& options
.start
== 0){ // if no results and not just the previous choices button
360 this.closeDropDown();
364 // Fill in the textbox with the first item from the drop down list,
365 // and highlight the characters that were auto-completed. For
366 // example, if user typed "CA" and the drop down list appeared, the
367 // textbox would be changed to "California" and "ifornia" would be
370 var nodes
= this.dropDown
.createOptions(
373 lang
.hitch(this, "_getMenuLabelFromItem")
376 // show our list (only if we have content, else nothing)
377 this._showResultList();
380 // tell the screen reader that the paging callback finished by
381 // shouting the next choice
382 if(options
.direction
){
383 if(1 == options
.direction
){
384 this.dropDown
.highlightFirstOption();
385 }else if(-1 == options
.direction
){
386 this.dropDown
.highlightLastOption();
389 this._announceOption(this.dropDown
.getHighlightedOption());
391 }else if(this.autoComplete
&& !this._prev_key_backspace
392 // when the user clicks the arrow button to show the full list,
393 // startSearch looks for "*".
394 // it does not make sense to autocomplete
395 // if they are just previewing the options available.
396 && !/^[*]+$/.test(query
[this.searchAttr
].toString())){
397 this._announceOption(nodes
[1]); // 1st real item
401 _showResultList: function(){
403 // Display the drop down if not already displayed, or if it is displayed, then
404 // reposition it if necessary (reposition may be necessary if drop down's height changed).
405 this.closeDropDown(true);
407 this.domNode
.setAttribute("aria-expanded", "true");
410 loadDropDown: function(/*Function*/ /*===== callback =====*/){
411 // Overrides _HasDropDown.loadDropDown().
412 // This is called when user has pressed button icon or pressed the down arrow key
413 // to open the drop down.
415 this._startSearchAll();
418 isLoaded: function(){
419 // signal to _HasDropDown that it needs to call loadDropDown() to load the
420 // drop down asynchronously before displaying it
424 closeDropDown: function(){
425 // Overrides _HasDropDown.closeDropDown(). Closes the drop down (assuming that it's open).
426 // This method is the callback when the user types ESC or clicking
427 // the button icon while the drop down is open. It's also called by other code.
430 this.inherited(arguments
);
431 this.domNode
.setAttribute("aria-expanded", "false");
432 this.focusNode
.removeAttribute("aria-activedescendant");
436 _setBlurValue: function(){
437 // if the user clicks away from the textbox OR tabs away, set the
438 // value to the textbox value
440 // if value is now more choices or previous choices, revert
442 var newvalue
= this.get('displayedValue');
443 var pw
= this.dropDown
;
445 newvalue
== pw
._messages
["previousMessage"] ||
446 newvalue
== pw
._messages
["nextMessage"]
449 this._setValueAttr(this._lastValueReported
, true);
450 }else if(typeof this.item
== "undefined"){
451 // Update 'value' (ex: KY) according to currently displayed text
453 this.set('displayedValue', newvalue
);
455 if(this.value
!= this._lastValueReported
){
456 this._handleOnChange(this.value
, true);
458 this._refreshState();
462 _setItemAttr: function(/*item*/ item
, /*Boolean?*/ priorityChange
, /*String?*/ displayedValue
){
464 // Set the displayed valued in the input box, and the hidden value
465 // that gets submitted, based on a dojo.data store item.
467 // Users shouldn't call this function; they should be calling
468 // set('item', value)
474 displayedValue
= this.store
._oldAPI
? // remove getValue() for 2.0 (old dojo.data API)
475 this.store
.getValue(item
, this.searchAttr
) : item
[this.searchAttr
];
477 value
= this._getValueField() != this.searchAttr
? this.store
.getIdentity(item
) : displayedValue
;
479 this.set('value', value
, priorityChange
, displayedValue
, item
);
482 _announceOption: function(/*Node*/ node
){
484 // a11y code that puts the highlighted option in the textbox.
485 // This way screen readers will know what is happening in the
491 // pull the text value from the item attached to the DOM node
493 if(node
== this.dropDown
.nextButton
||
494 node
== this.dropDown
.previousButton
){
495 newValue
= node
.innerHTML
;
496 this.item
= undefined;
499 newValue
= (this.store
._oldAPI
? // remove getValue() for 2.0 (old dojo.data API)
500 this.store
.getValue(node
.item
, this.searchAttr
) : node
.item
[this.searchAttr
]).toString();
501 this.set('item', node
.item
, false, newValue
);
503 // get the text that the user manually entered (cut off autocompleted text)
504 this.focusNode
.value
= this.focusNode
.value
.substring(0, this._lastInput
.length
);
505 // set up ARIA activedescendant
506 this.focusNode
.setAttribute("aria-activedescendant", domAttr
.get(node
, "id"));
507 // autocomplete the rest of the option to announce change
508 this._autoCompleteText(newValue
);
511 _selectOption: function(/*DomNode*/ target
){
513 // Menu callback function, called when an item in the menu is selected.
514 this.closeDropDown();
516 this._announceOption(target
);
518 this._setCaretPos(this.focusNode
, this.focusNode
.value
.length
);
519 this._handleOnChange(this.value
, true);
522 _startSearchAll: function(){
523 this._startSearch('');
526 _startSearchFromInput: function(){
527 this._startSearch(this.focusNode
.value
.replace(/([\\\*\?])/g, "\\$1"));
530 _getQueryString: function(/*String*/ text
){
531 return string
.substitute(this.queryExpr
, [text
]);
534 _startSearch: function(/*String*/ key
){
536 // Starts a search for elements matching key (key=="" means to return all items),
537 // and calls _openResultList() when the search completes, to display the results.
539 var popupId
= this.id
+ "_popup",
540 dropDownConstructor
= lang
.isString(this.dropDownClass
) ?
541 lang
.getObject(this.dropDownClass
, false) : this.dropDownClass
;
542 this.dropDown
= new dropDownConstructor({
543 onChange
: lang
.hitch(this, this._selectOption
),
546 textDir
: this.textDir
548 this.focusNode
.removeAttribute("aria-activedescendant");
549 this.textbox
.setAttribute("aria-owns",popupId
); // associate popup with textbox
551 this._lastInput
= key
; // Store exactly what was entered by the user.
553 // Setup parameters to be passed to store.query().
554 // Create a new query to prevent accidentally querying for a hidden
555 // value from FilteringSelect's keyField
556 var query
= lang
.clone(this.query
); // #5970
559 count
: this.pageSize
,
560 queryOptions
: { // remove for 2.0
561 ignoreCase
: this.ignoreCase
,
565 lang
.mixin(options
, this.fetchProperties
);
568 var qs
= this._getQueryString(key
), q
;
569 if(this.store
._oldAPI
){
570 // remove this branch for 2.0
573 // Query on searchAttr is a regex for benefit of dojo.store.Memory,
574 // but with a toString() method to help dojo.store.JsonRest.
575 // Search string like "Co*" converted to regex like /^Co.*$/i.
576 q
= filter
.patternToRegExp(qs
, this.ignoreCase
);
577 q
.toString = function(){ return qs
; };
579 this._lastQuery
= query
[this.searchAttr
] = q
;
581 // Function to run the query, wait for the results, and then call _openResultList()
583 startQuery = function(){
584 var resPromise
= _this
._fetchHandle
= _this
.store
.query(query
, options
);
585 Deferred
.when(resPromise
, function(res
){
586 _this
._fetchHandle
= null;
587 res
.total
= resPromise
.total
;
588 _this
._openResultList(res
, query
, options
);
590 _this
._fetchHandle
= null;
591 if(!_this
._cancelingQuery
){ // don't treat canceled query as an error
592 console
.error(_this
.declaredClass
+ ' ' + err
.toString());
593 _this
.closeDropDown();
598 // #5970: set _lastQuery, *then* start the timeout
599 // otherwise, if the user types and the last query returns before the timeout,
600 // _lastQuery won't be set and their input gets rewritten
602 this.searchTimer
= setTimeout(lang
.hitch(this, function(query
, _this
){
603 this.searchTimer
= null;
607 // Setup method to handle clicking next/previous buttons to page through results
608 this._nextSearch
= this.dropDown
.onPage = function(direction
){
609 options
.start
+= options
.count
* direction
;
610 // tell callback the direction of the paging so the screen
611 // reader knows which menu option to shout
612 options
.direction
= direction
;
616 }, query
, this), this.searchDelay
);
619 _getValueField: function(){
621 // Helper for postMixInProperties() to set this.value based on data inlined into the markup.
622 // Returns the attribute name in the item (in dijit.form._ComboBoxDataStore) to use as the value.
623 return this.searchAttr
;
626 //////////// INITIALIZATION METHODS ///////////////////////////////////////
628 constructor: function(){
630 this.fetchProperties
={};
633 postMixInProperties: function(){
635 var srcNodeRef
= this.srcNodeRef
;
636 var list
= this.list
;
638 this.store
= registry
.byId(list
);
640 // if user didn't specify store, then assume there are option tags
641 this.store
= new DataList({}, srcNodeRef
);
644 // if there is no value set and there is an option list, set
645 // the value to the first value to be consistent with native Select
646 // Firefox and Safari set value
647 // IE6 and Opera set selectedIndex, which is automatically set
648 // by the selected attribute of an option tag
649 // IE6 does not set value, Opera sets value = selectedIndex
650 if(!("value" in this.params
)){
651 var item
= (this.item
= this.store
.fetchSelectedItem());
653 var valueField
= this._getValueField();
654 // remove getValue() for 2.0 (old dojo.data API)
655 this.value
= this.store
._oldAPI
? this.store
.getValue(item
, valueField
) : item
[valueField
];
660 this.inherited(arguments
);
663 postCreate: function(){
665 // Subclasses must call this method from their postCreate() methods
669 // find any associated label element and add to ComboBox node.
670 var label
=query('label[for="'+this.id
+'"]');
672 label
[0].id
= (this.id
+"_label");
673 this.domNode
.setAttribute("aria-labelledby", label
[0].id
);
676 this.inherited(arguments
);
679 _getMenuLabelFromItem: function(/*Item*/ item
){
680 var label
= this.labelFunc(item
, this.store
),
681 labelType
= this.labelType
;
682 // If labelType is not "text" we don't want to screw any markup ot whatever.
683 if(this.highlightMatch
!= "none" && this.labelType
== "text" && this._lastInput
){
684 label
= this.doHighlight(label
, this._escapeHtml(this._lastInput
));
687 return {html
: labelType
== "html", label
: label
};
690 doHighlight: function(/*String*/ label
, /*String*/ find
){
692 // Highlights the string entered by the user in the menu. By default this
693 // highlights the first occurrence found. Override this method
694 // to implement your custom highlighting.
699 // Add (g)lobal modifier when this.highlightMatch == "all" and (i)gnorecase when this.ignoreCase == true
700 modifiers
= (this.ignoreCase
? "i" : "") + (this.highlightMatch
== "all" ? "g" : ""),
701 i
= this.queryExpr
.indexOf("${0}");
702 find
= regexp
.escapeString(find
); // escape regexp special chars
703 return this._escapeHtml(label
).replace(
704 // prepend ^ when this.queryExpr == "${0}*" and append $ when this.queryExpr == "*${0}"
705 new RegExp((i
== 0 ? "^" : "") + "("+ find
+")" + (i
== (this.queryExpr
.length
- 4) ? "$" : ""), modifiers
),
706 '<span class="dijitComboBoxHighlightMatch">$1</span>'
707 ); // returns String, (almost) valid HTML (entities encoded)
710 _escapeHtml: function(/*String*/ str
){
711 // TODO Should become dojo.html.entities(), when exists use instead
713 // Adds escape sequences for special characters in XML: &<>"'
714 str
= String(str
).replace(/&/gm, "&").replace(/</gm
, "<")
715 .replace(/>/gm, ">").replace(/"/gm, ""
;"); //balance"
716 return str
; // string
720 // Overrides the _FormWidget.reset().
721 // Additionally reset the .item (to clean up).
723 this.inherited(arguments
);
726 labelFunc: function(/*item*/ item
, /*dojo.store.api.Store*/ store
){
728 // Computes the label to display based on the dojo.data store item.
730 // The label that the ComboBox should display
734 // Use toString() because XMLStore returns an XMLItem whereas this
735 // method is expected to return a String (#9354).
736 // Remove getValue() for 2.0 (old dojo.data API)
737 return (store
._oldAPI
? store
.getValue(item
, this.labelAttr
|| this.searchAttr
) :
738 item
[this.labelAttr
|| this.searchAttr
]).toString(); // String
741 _setValueAttr: function(/*String*/ value
, /*Boolean?*/ priorityChange
, /*String?*/ displayedValue
, /*item?*/ item
){
743 // Hook so set('value', value) works.
745 // Sets the value of the select.
746 this._set("item", item
||null); // value not looked up in store
747 if(!value
){ value
= ''; } // null translates to blank
748 this.inherited(arguments
);
750 _setTextDirAttr: function(/*String*/ textDir
){
752 // Setter for textDir, needed for the dropDown's textDir update.
754 // Users shouldn't call this function; they should be calling
755 // set('textDir', value)
758 this.inherited(arguments
);
759 // update the drop down also (_ComboBoxMenuMixin)
761 this.dropDown
._set("textDir", textDir
);