1 var $ = require('jquery');
4 * jQuery.textcomplete.js
6 * Repositiory: https://github.com/yuku-t/jquery-textcomplete
8 * Author: Yuku Takahashi
15 * Convert arguments into a real array.
17 var toArray = function(args
) {
19 result
= Array
.prototype.slice
.call(args
);
24 * Bind the func to the context.
26 var bind = function(func
, context
) {
28 // Use native Function#bind if it's available.
29 return func
.bind(context
);
32 func
.apply(context
, arguments
);
38 * Get the styles of any element from property names.
40 var getStyles
= (function() {
42 color
= $('<div></div>').css(['color']).color
;
43 if (typeof color
!== 'undefined') {
44 return function($el
, properties
) {
45 return $el
.css(properties
);
48 // for jQuery 1.8 or below
49 return function($el
, properties
) {
52 $.each(properties
, function(i
, property
) {
53 styles
[property
] = $el
.css(property
);
61 * Default template function.
63 var identity = function(obj
) {
68 * Memoize a search function.
70 var memoize = function(func
) {
72 return function(term
, callback
) {
76 func
.call(this, term
, function(data
) {
77 memo
[term
] = (memo
[term
] || []).concat(data
);
78 callback
.apply(null, arguments
);
85 * Determine if the array contains a given value.
87 var include = function(array
, value
) {
89 if (array
.indexOf
) return array
.indexOf(value
) != -1;
90 for (i
= 0, l
= array
.length
; i
< l
; i
++) {
91 if (array
[i
] === value
) return true;
97 * Textarea manager class.
99 var Completer
= (function() {
100 var html
, css
, $baseWrapper
, $baseList
;
103 wrapper
: '<div class="textcomplete-wrapper"></div>',
104 list
: '<ul class="dropdown-menu"></ul>'
111 position
: 'absolute',
118 $baseWrapper
= $(html
.wrapper
).css(css
.wrapper
);
119 $baseList
= $(html
.list
).css(css
.list
);
121 function Completer($el
, strategies
) {
122 var $wrapper
, $list
, focused
;
123 $list
= $baseList
.clone();
124 this.el
= $el
.get(0); // textarea element
126 $wrapper
= prepareWrapper(this.$el
);
128 // Refocus the textarea if it is being focused
129 focused
= this.el
=== document
.activeElement
;
130 this.$el
.wrap($wrapper
).before($list
);
135 this.listView
= new ListView($list
, this);
136 this.strategies
= strategies
;
137 this.$el
.on('keyup', bind(this.onKeyup
, this));
138 this.$el
.on('keydown', bind(this.listView
.onKeydown
, this.listView
));
140 // Global click event handler
144 if (e
.originalEvent
&& !e
.originalEvent
.keepTextCompleteDropdown
) {
145 this.listView
.deactivate();
152 * Completer's public methods
154 $.extend(Completer
.prototype, {
156 * Show autocomplete list next to the caret.
158 renderList: function(data
) {
159 if (this.clearAtNext
) {
160 this.listView
.clear();
161 this.clearAtNext
= false;
164 if (!this.listView
.shown
) {
166 .setPosition(this.getCaretPosition())
169 this.listView
.strategy
= this.strategy
;
171 data
= data
.slice(0, this.strategy
.maxCount
);
172 this.listView
.render(data
);
175 if ((!this.listView
.data
|| !this.listView
.data
.length
) && this.listView
.shown
) {
176 this.listView
.deactivate();
180 searchCallbackFactory: function(term
) {
182 return function(data
, keep
) {
183 // ignore old calbacks
184 if (term
!= self
.term
) return;
186 self
.renderList(data
);
188 // This is the last callback for this search.
189 self
.clearAtNext
= true;
195 * Keyup event handler.
197 onKeyup: function(e
) {
198 var searchQuery
, term
;
200 searchQuery
= this.extractSearchQuery(this.getTextFromHeadToCaret());
201 if (searchQuery
.length
) {
202 term
= searchQuery
[1];
203 if (this.term
=== term
) return; // Ignore shift-key or something.
205 this.search(searchQuery
);
208 this.listView
.deactivate();
212 onSelect: function(value
) {
213 var pre
, post
, newSubStr
;
214 pre
= this.getTextFromHeadToCaret();
215 post
= this.el
.value
.substring(this.el
.selectionEnd
);
217 newSubStr
= this.strategy
.replace(value
);
218 if ($.isArray(newSubStr
)) {
219 post
= newSubStr
[1] + post
;
220 newSubStr
= newSubStr
[0];
222 pre
= pre
.replace(this.strategy
.match
, newSubStr
);
223 this.$el
.val(pre
+ post
);
225 this.el
.selectionStart
= this.el
.selectionEnd
= pre
.length
;
232 * Returns caret's relative coordinates from textarea's left top corner.
234 getCaretPosition: function() {
235 // Browser native API does not provide the way to know the position of
236 // caret in pixels, so that here we use a kind of hack to accomplish
237 // the aim. First of all it puts a div element and completely copies
238 // the textarea's style to the element, then it inserts the text and a
239 // span element into the textarea.
240 // Consequently, the span element's position is the thing what we want.
242 if (this.el
.selectionEnd
=== 0) return;
243 var properties
, css
, $div
, $span
, position
;
270 position
: 'absolute',
272 'white-space': 'pre-wrap',
276 getStyles(this.$el
, properties
)
279 $div
= $('<div></div>')
281 .text(this.getTextFromHeadToCaret());
282 $span
= $('<span></span>')
285 this.$el
.before($div
);
286 position
= $span
.position();
287 position
.bottom
= $div
.height() - position
.top
;
292 getTextFromHeadToCaret: function() {
293 var text
, selectionEnd
, range
;
294 selectionEnd
= this.el
.selectionEnd
;
295 if (typeof selectionEnd
=== 'number') {
296 text
= this.el
.value
.substring(0, selectionEnd
);
297 } else if (document
.selection
) {
298 range
= this.el
.createTextRange();
299 range
.moveStart('character', 0);
300 range
.moveEnd('textedit');
307 * Parse the value of textarea and extract search query.
309 extractSearchQuery: function(text
) {
310 // If a search query found, it returns used strategy and the query
311 // term. If the caret is currently in a code block or search query does
312 // not found, it returns an empty array.
314 var i
, l
, strategy
, match
;
315 for (i
= 0, l
= this.strategies
.length
; i
< l
; i
++) {
316 strategy
= this.strategies
[i
];
317 match
= text
.match(strategy
.match
);
319 return [strategy
, match
[strategy
.index
]];
325 search: function(searchQuery
) {
327 this.strategy
= searchQuery
[0];
328 term
= searchQuery
[1];
329 this.strategy
.search(term
, this.searchCallbackFactory(term
));
334 * Completer's private functions
336 var prepareWrapper = function($el
) {
337 return $baseWrapper
.clone().css('display', $el
.css('display'));
344 * Dropdown menu manager class.
346 var ListView
= (function() {
347 function ListView($el
, completer
) {
351 this.completer
= completer
;
353 this.$el
.on('click', 'li.textcomplete-item', bind(this.onClick
, this));
356 $.extend(ListView
.prototype, {
359 render: function(data
) {
360 var html
, i
, l
, index
, val
;
363 for (i
= 0, l
= data
.length
; i
< l
; i
++) {
365 if (include(this.data
, val
)) continue;
366 index
= this.data
.length
;
368 html
+= '<li class="textcomplete-item" data-index="' + index
+ '"><a>';
369 html
+= this.strategy
.template(val
);
371 if (this.data
.length
=== this.strategy
.maxCount
) break;
373 this.$el
.append(html
);
374 if (!this.data
.length
) {
377 this.activateIndexedItem();
388 activateIndexedItem: function() {
390 this.$el
.find('.active').removeClass('active');
391 this.getActiveItem().addClass('active');
394 getActiveItem: function() {
395 return $(this.$el
.children().get(this.index
));
398 activate: function() {
401 this.completer
.$el
.trigger('textComplete:show');
407 deactivate: function() {
410 this.completer
.$el
.trigger('textComplete:hide');
412 this.data
= this.index
= null;
417 setPosition: function(position
) {
421 bottom
: position
.bottom
426 select: function(index
) {
427 this.completer
.onSelect(this.data
[index
]);
431 onKeydown: function(e
) {
433 if (!this.shown
) return;
434 if (e
.keyCode
=== 27) {
437 } else if (e
.keyCode
=== 38) {
440 if (this.index
=== 0) {
441 this.index
= this.data
.length
- 1;
445 this.activateIndexedItem();
446 } else if (e
.keyCode
=== 40) {
449 if (this.index
=== this.data
.length
- 1) {
454 this.activateIndexedItem();
455 } else if (e
.keyCode
=== 13 || e
.keyCode
=== 9) {
458 this.select(parseInt(this.getActiveItem().data('index')));
462 onClick: function(e
) {
463 var $e
= $(e
.target
);
464 e
.originalEvent
.keepTextCompleteDropdown
= true;
465 if (!$e
.hasClass('textcomplete-item')) {
466 $e
= $e
.parents('li.textcomplete-item');
468 this.select(parseInt($e
.data('index')));
475 $.fn
.textcomplete = function(strategies
) {
477 for (i
= 0, l
= strategies
.length
; i
< l
; i
++) {
478 strategy
= strategies
[i
];
479 if (!strategy
.template
) {
480 strategy
.template
= identity
;
482 if (strategy
.index
== null) {
485 if (strategy
.cache
) {
486 strategy
.search
= memoize(strategy
.search
);
488 strategy
.maxCount
|| (strategy
.maxCount
= 10);
490 new Completer(this, strategies
);