-
Notifications
You must be signed in to change notification settings - Fork 35
/
Copy pathjquery-ui.triggeredAutocomplete.js
266 lines (223 loc) · 8.26 KB
/
jquery-ui.triggeredAutocomplete.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
/********************************************************************************
/*
* triggeredAutocomplete (jQuery UI autocomplete widget)
* 2012 by Hawkee.com ([email protected])
*
* Version 1.4.5
*
* Requires jQuery 1.7 and jQuery UI 1.8
*
* Dual licensed under MIT or GPLv2 licenses
* http://en.wikipedia.org/wiki/MIT_License
* http://en.wikipedia.org/wiki/GNU_General_Public_License
*
*/
;(function ( $, window, document, undefined ) {
$.widget("ui.triggeredAutocomplete", $.extend(true, {}, $.ui.autocomplete.prototype, {
options: {
trigger: "@",
allowDuplicates: true,
maxLength: 0
},
_create:function() {
var self = this;
this.id_map = new Object();
this.stopIndex = -1;
this.stopLength = -1;
this.contents = '';
this.cursorPos = 0;
/** Fixes some events improperly handled by ui.autocomplete */
this.element.bind('keydown.autocomplete.fix', function (e) {
switch (e.keyCode) {
case $.ui.keyCode.ESCAPE:
self.close(e);
e.stopImmediatePropagation();
break;
case $.ui.keyCode.UP:
case $.ui.keyCode.DOWN:
if (!self.menu.element.is(":visible")) {
e.stopImmediatePropagation();
}
}
});
// Check for the id_map as an attribute. This is for editing.
var id_map_string = this.element.attr('id_map');
if(id_map_string) this.id_map = jQuery.parseJSON(id_map_string);
this.ac = $.ui.autocomplete.prototype;
this.ac._create.apply(this, arguments);
this.updateHidden();
// Select function defined via options.
this.options.select = function(event, ui) {
var contents = self.contents;
var cursorPos = self.cursorPos;
// Save everything following the cursor (in case they went back to add a mention)
// Separate everything before the cursor
// Remove the trigger and search
// Rebuild: start + result + end
var end = contents.substring(cursorPos, contents.length);
var start = contents.substring(0, cursorPos);
start = start.substring(0, start.lastIndexOf(self.options.trigger));
var top = self.element.scrollTop();
this.value = start + self.options.trigger+ui.item.label+' ' + end;
self.element.scrollTop(top);
// Create an id map so we can create a hidden version of this string with id's instead of labels.
self.id_map[ui.item.label] = ui.item.value;
self.updateHidden();
/** Places the caret right after the inserted item. */
var index = start.length + self.options.trigger.length + ui.item.label.length + 2;
if (this.createTextRange) {
var range = this.createTextRange();
range.move('character', index);
range.select();
} else if (this.setSelectionRange) {
this.setSelectionRange(index, index);
}
return false;
};
// Don't change the input as you browse the results.
this.options.focus = function(event, ui) { return false; }
this.menu.options.blur = function(event, ui) { return false; }
// Any changes made need to update the hidden field.
this.element.focus(function() { self.updateHidden(); });
this.element.change(function() { self.updateHidden(); });
},
// If there is an 'img' then show it beside the label.
_renderItem: function( ul, item ) {
if(item.img != undefined) {
return $( "<li></li>" )
.data( "item.autocomplete", item )
.append( "<a>" + "<img src='" + item.img + "' /><span>"+item.label+"</span></a>" )
.appendTo( ul );
}
else {
return $( "<li></li>" )
.data( "item.autocomplete", item )
.append( $( "<a></a>" ).text( item.label ) )
.appendTo( ul );
}
},
// This stops the input box from being cleared when traversing the menu.
_move: function( direction, event ) {
if ( !this.menu.element.is(":visible") ) {
this.search( null, event );
return;
}
if ( this.menu.first() && /^previous/.test(direction) ||
this.menu.last() && /^next/.test(direction) ) {
this.menu.deactivate();
return;
}
this.menu[ direction ]( event );
},
search: function(value, event) {
var contents = this.element.val();
var cursorPos = this.getCursor();
this.contents = contents;
this.cursorPos = cursorPos;
// Include the character before the trigger and check that the trigger is not in the middle of a word
// This avoids trying to match in the middle of email addresses when '@' is used as the trigger
var check_contents = contents.substring(contents.lastIndexOf(this.options.trigger) - 1, cursorPos);
var regex = new RegExp('\\B\\'+this.options.trigger+'([\\w\\-]+)');
if (contents.indexOf(this.options.trigger) >= 0 && check_contents.match(regex)) {
// Get the characters following the trigger and before the cursor position.
// Get the contents up to the cursortPos first then get the lastIndexOf the trigger to find the search term.
contents = contents.substring(0, cursorPos);
var term = contents.substring(contents.lastIndexOf(this.options.trigger) + 1, contents.length);
// Only query the server if we have a term and we haven't received a null response.
// First check the current query to see if it already returned a null response.
if(this.stopIndex == contents.lastIndexOf(this.options.trigger) && term.length > this.stopLength) { term = ''; }
if (term.length > 0 && (!this.options.maxLength || term.length <= this.options.maxLength)) {
// Updates the hidden field to check if a name was removed so that we can put them back in the list.
this.updateHidden();
return this._search(term);
}
else this.close();
}
},
// Slightly altered the default ajax call to stop querying after the search produced no results.
// This is to prevent unnecessary querying.
_initSource: function() {
var self = this, array, url;
if ( $.isArray(this.options.source) ) {
array = this.options.source;
this.source = function( request, response ) {
response( $.ui.autocomplete.filter(array, request.term) );
};
} else if ( typeof this.options.source === "string" ) {
url = this.options.source;
this.source = function( request, response ) {
if ( self.xhr ) {
self.xhr.abort();
}
self.xhr = $.ajax({
url: url,
data: request,
dataType: 'json',
success: function(data) {
if(data != null) {
response($.map(data, function(item) {
if (typeof item === "string") {
label = item;
}
else {
label = item.label;
}
// If the item has already been selected don't re-include it.
if(!self.id_map[label] || self.options.allowDuplicates) {
return item
}
}));
self.stopLength = -1;
self.stopIndex = -1;
}
else {
// No results, record length of string and stop querying unless the length decreases
self.stopLength = request.term.length;
self.stopIndex = self.contents.lastIndexOf(self.options.trigger);
self.close();
}
}
});
};
} else {
this.source = this.options.source;
}
},
destroy: function() {
$.Widget.prototype.destroy.call(this);
},
// Gets the position of the cursor in the input box.
getCursor: function() {
var i = this.element[0];
if(i.selectionStart) {
return i.selectionStart;
}
else if(i.ownerDocument.selection) {
var range = i.ownerDocument.selection.createRange();
if(!range) return 0;
var textrange = i.createTextRange();
var textrange2 = textrange.duplicate();
textrange.moveToBookmark(range.getBookmark());
textrange2.setEndPoint('EndToStart', textrange);
return textrange2.text.length;
}
},
// Populates the hidden field with the contents of the entry box but with
// ID's instead of usernames. Better for storage.
updateHidden: function() {
var trigger = this.options.trigger;
var top = this.element.scrollTop();
var contents = this.element.val();
for(var key in this.id_map) {
var find = trigger+key;
find = find.replace(/[^a-zA-Z 0-9@]+/g,'\\$&');
var regex = new RegExp(find, "g");
var old_contents = contents;
contents = contents.replace(regex, trigger+'['+this.id_map[key]+']');
if(old_contents == contents) delete this.id_map[key];
}
$(this.options.hidden).val(contents);
this.element.scrollTop(top);
}
}));
})( jQuery, window , document );