]> git.mxchange.org Git - friendica.git/blob - library/jquery-textcomplete/jquery.textcomplete.js
rework autocomplete: update query-textcomplete
[friendica.git] / library / jquery-textcomplete / jquery.textcomplete.js
1 (function (factory) {
2   if (typeof define === 'function' && define.amd) {
3     // AMD. Register as an anonymous module.
4     define(['jquery'], factory);
5   } else if (typeof module === "object" && module.exports) {
6     var $ = require('jquery');
7     module.exports = factory($);
8   } else {
9     // Browser globals
10     factory(jQuery);
11   }
12 }(function (jQuery) {
13
14 /*!
15  * jQuery.textcomplete
16  *
17  * Repository: https://github.com/yuku-t/jquery-textcomplete
18  * License:    MIT (https://github.com/yuku-t/jquery-textcomplete/blob/master/LICENSE)
19  * Author:     Yuku Takahashi
20  */
21
22 if (typeof jQuery === 'undefined') {
23   throw new Error('jQuery.textcomplete requires jQuery');
24 }
25
26 +function ($) {
27   'use strict';
28
29   var warn = function (message) {
30     if (console.warn) { console.warn(message); }
31   };
32
33   var id = 1;
34
35   $.fn.textcomplete = function (strategies, option) {
36     var args = Array.prototype.slice.call(arguments);
37     return this.each(function () {
38       var self = this;
39       var $this = $(this);
40       var completer = $this.data('textComplete');
41       if (!completer) {
42         option || (option = {});
43         option._oid = id++;  // unique object id
44         completer = new $.fn.textcomplete.Completer(this, option);
45         $this.data('textComplete', completer);
46       }
47       if (typeof strategies === 'string') {
48         if (!completer) return;
49         args.shift()
50         completer[strategies].apply(completer, args);
51         if (strategies === 'destroy') {
52           $this.removeData('textComplete');
53         }
54       } else {
55         // For backward compatibility.
56         // TODO: Remove at v0.4
57         $.each(strategies, function (obj) {
58           $.each(['header', 'footer', 'placement', 'maxCount'], function (name) {
59             if (obj[name]) {
60               completer.option[name] = obj[name];
61               warn(name + 'as a strategy param is deprecated. Use option.');
62               delete obj[name];
63             }
64           });
65         });
66         completer.register($.fn.textcomplete.Strategy.parse(strategies, {
67           el: self,
68           $el: $this
69         }));
70       }
71     });
72   };
73
74 }(jQuery);
75
76 +function ($) {
77   'use strict';
78
79   // Exclusive execution control utility.
80   //
81   // func - The function to be locked. It is executed with a function named
82   //        `free` as the first argument. Once it is called, additional
83   //        execution are ignored until the free is invoked. Then the last
84   //        ignored execution will be replayed immediately.
85   //
86   // Examples
87   //
88   //   var lockedFunc = lock(function (free) {
89   //     setTimeout(function { free(); }, 1000); // It will be free in 1 sec.
90   //     console.log('Hello, world');
91   //   });
92   //   lockedFunc();  // => 'Hello, world'
93   //   lockedFunc();  // none
94   //   lockedFunc();  // none
95   //   // 1 sec past then
96   //   // => 'Hello, world'
97   //   lockedFunc();  // => 'Hello, world'
98   //   lockedFunc();  // none
99   //
100   // Returns a wrapped function.
101   var lock = function (func) {
102     var locked, queuedArgsToReplay;
103
104     return function () {
105       // Convert arguments into a real array.
106       var args = Array.prototype.slice.call(arguments);
107       if (locked) {
108         // Keep a copy of this argument list to replay later.
109         // OK to overwrite a previous value because we only replay
110         // the last one.
111         queuedArgsToReplay = args;
112         return;
113       }
114       locked = true;
115       var self = this;
116       args.unshift(function replayOrFree() {
117         if (queuedArgsToReplay) {
118           // Other request(s) arrived while we were locked.
119           // Now that the lock is becoming available, replay
120           // the latest such request, then call back here to
121           // unlock (or replay another request that arrived
122           // while this one was in flight).
123           var replayArgs = queuedArgsToReplay;
124           queuedArgsToReplay = undefined;
125           replayArgs.unshift(replayOrFree);
126           func.apply(self, replayArgs);
127         } else {
128           locked = false;
129         }
130       });
131       func.apply(this, args);
132     };
133   };
134
135   var isString = function (obj) {
136     return Object.prototype.toString.call(obj) === '[object String]';
137   };
138
139   var isFunction = function (obj) {
140     return Object.prototype.toString.call(obj) === '[object Function]';
141   };
142
143   var uniqueId = 0;
144
145   function Completer(element, option) {
146     this.$el        = $(element);
147     this.id         = 'textcomplete' + uniqueId++;
148     this.strategies = [];
149     this.views      = [];
150     this.option     = $.extend({}, Completer._getDefaults(), option);
151
152     if (!this.$el.is('input[type=text]') && !this.$el.is('textarea') && !element.isContentEditable && element.contentEditable != 'true') {
153       throw new Error('textcomplete must be called on a Textarea or a ContentEditable.');
154     }
155
156     if (element === document.activeElement) {
157       // element has already been focused. Initialize view objects immediately.
158       this.initialize()
159     } else {
160       // Initialize view objects lazily.
161       var self = this;
162       this.$el.one('focus.' + this.id, function () { self.initialize(); });
163     }
164   }
165
166   Completer._getDefaults = function () {
167     if (!Completer.DEFAULTS) {
168       Completer.DEFAULTS = {
169         appendTo: $('body'),
170         zIndex: '100'
171       };
172     }
173
174     return Completer.DEFAULTS;
175   }
176
177   $.extend(Completer.prototype, {
178     // Public properties
179     // -----------------
180
181     id:         null,
182     option:     null,
183     strategies: null,
184     adapter:    null,
185     dropdown:   null,
186     $el:        null,
187
188     // Public methods
189     // --------------
190
191     initialize: function () {
192       var element = this.$el.get(0);
193       // Initialize view objects.
194       this.dropdown = new $.fn.textcomplete.Dropdown(element, this, this.option);
195       var Adapter, viewName;
196       if (this.option.adapter) {
197         Adapter = this.option.adapter;
198       } else {
199         if (this.$el.is('textarea') || this.$el.is('input[type=text]')) {
200           viewName = typeof element.selectionEnd === 'number' ? 'Textarea' : 'IETextarea';
201         } else {
202           viewName = 'ContentEditable';
203         }
204         Adapter = $.fn.textcomplete[viewName];
205       }
206       this.adapter = new Adapter(element, this, this.option);
207     },
208
209     destroy: function () {
210       this.$el.off('.' + this.id);
211       if (this.adapter) {
212         this.adapter.destroy();
213       }
214       if (this.dropdown) {
215         this.dropdown.destroy();
216       }
217       this.$el = this.adapter = this.dropdown = null;
218     },
219
220     // Invoke textcomplete.
221     trigger: function (text, skipUnchangedTerm) {
222       if (!this.dropdown) { this.initialize(); }
223       text != null || (text = this.adapter.getTextFromHeadToCaret());
224       var searchQuery = this._extractSearchQuery(text);
225       if (searchQuery.length) {
226         var term = searchQuery[1];
227         // Ignore shift-key, ctrl-key and so on.
228         if (skipUnchangedTerm && this._term === term) { return; }
229         this._term = term;
230         this._search.apply(this, searchQuery);
231       } else {
232         this._term = null;
233         this.dropdown.deactivate();
234       }
235     },
236
237     fire: function (eventName) {
238       var args = Array.prototype.slice.call(arguments, 1);
239       this.$el.trigger(eventName, args);
240       return this;
241     },
242
243     register: function (strategies) {
244       Array.prototype.push.apply(this.strategies, strategies);
245     },
246
247     // Insert the value into adapter view. It is called when the dropdown is clicked
248     // or selected.
249     //
250     // value    - The selected element of the array callbacked from search func.
251     // strategy - The Strategy object.
252     // e        - Click or keydown event object.
253     select: function (value, strategy, e) {
254       this._term = null;
255       this.adapter.select(value, strategy, e);
256       this.fire('change').fire('textComplete:select', value, strategy);
257       this.adapter.focus();
258     },
259
260     // Private properties
261     // ------------------
262
263     _clearAtNext: true,
264     _term:        null,
265
266     // Private methods
267     // ---------------
268
269     // Parse the given text and extract the first matching strategy.
270     //
271     // Returns an array including the strategy, the query term and the match
272     // object if the text matches an strategy; otherwise returns an empty array.
273     _extractSearchQuery: function (text) {
274       for (var i = 0; i < this.strategies.length; i++) {
275         var strategy = this.strategies[i];
276         var context = strategy.context(text);
277         if (context || context === '') {
278           var matchRegexp = isFunction(strategy.match) ? strategy.match(text) : strategy.match;
279           if (isString(context)) { text = context; }
280           var match = text.match(matchRegexp);
281           if (match) { return [strategy, match[strategy.index], match]; }
282         }
283       }
284       return []
285     },
286
287     // Call the search method of selected strategy..
288     _search: lock(function (free, strategy, term, match) {
289       var self = this;
290       strategy.search(term, function (data, stillSearching) {
291         if (!self.dropdown.shown) {
292           self.dropdown.activate();
293         }
294         if (self._clearAtNext) {
295           // The first callback in the current lock.
296           self.dropdown.clear();
297           self._clearAtNext = false;
298         }
299         self.dropdown.setPosition(self.adapter.getCaretPosition());
300         self.dropdown.render(self._zip(data, strategy, term));
301         if (!stillSearching) {
302           // The last callback in the current lock.
303           free();
304           self._clearAtNext = true; // Call dropdown.clear at the next time.
305         }
306       }, match);
307     }),
308
309     // Build a parameter for Dropdown#render.
310     //
311     // Examples
312     //
313     //  this._zip(['a', 'b'], 's');
314     //  //=> [{ value: 'a', strategy: 's' }, { value: 'b', strategy: 's' }]
315     _zip: function (data, strategy, term) {
316       return $.map(data, function (value) {
317         return { value: value, strategy: strategy, term: term };
318       });
319     }
320   });
321
322   $.fn.textcomplete.Completer = Completer;
323 }(jQuery);
324
325 +function ($) {
326   'use strict';
327
328   var $window = $(window);
329
330   var include = function (zippedData, datum) {
331     var i, elem;
332     var idProperty = datum.strategy.idProperty
333     for (i = 0; i < zippedData.length; i++) {
334       elem = zippedData[i];
335       if (elem.strategy !== datum.strategy) continue;
336       if (idProperty) {
337         if (elem.value[idProperty] === datum.value[idProperty]) return true;
338       } else {
339         if (elem.value === datum.value) return true;
340       }
341     }
342     return false;
343   };
344
345   var dropdownViews = {};
346   $(document).on('click', function (e) {
347     var id = e.originalEvent && e.originalEvent.keepTextCompleteDropdown;
348     $.each(dropdownViews, function (key, view) {
349       if (key !== id) { view.deactivate(); }
350     });
351   });
352
353   var commands = {
354     SKIP_DEFAULT: 0,
355     KEY_UP: 1,
356     KEY_DOWN: 2,
357     KEY_ENTER: 3,
358     KEY_PAGEUP: 4,
359     KEY_PAGEDOWN: 5,
360     KEY_ESCAPE: 6
361   };
362
363   // Dropdown view
364   // =============
365
366   // Construct Dropdown object.
367   //
368   // element - Textarea or contenteditable element.
369   function Dropdown(element, completer, option) {
370     this.$el       = Dropdown.createElement(option);
371     this.completer = completer;
372     this.id        = completer.id + 'dropdown';
373     this._data     = []; // zipped data.
374     this.$inputEl  = $(element);
375     this.option    = option;
376
377     // Override setPosition method.
378     if (option.listPosition) { this.setPosition = option.listPosition; }
379     if (option.height) { this.$el.height(option.height); }
380     var self = this;
381     $.each(['maxCount', 'placement', 'footer', 'header', 'noResultsMessage', 'className'], function (_i, name) {
382       if (option[name] != null) { self[name] = option[name]; }
383     });
384     this._bindEvents(element);
385     dropdownViews[this.id] = this;
386   }
387
388   $.extend(Dropdown, {
389     // Class methods
390     // -------------
391
392     createElement: function (option) {
393       var $parent = option.appendTo;
394       if (!($parent instanceof $)) { $parent = $($parent); }
395       var $el = $('<ul></ul>')
396         .addClass('dropdown-menu textcomplete-dropdown')
397         .attr('id', 'textcomplete-dropdown-' + option._oid)
398         .css({
399           display: 'none',
400           left: 0,
401           position: 'absolute',
402           zIndex: option.zIndex
403         })
404         .appendTo($parent);
405       return $el;
406     }
407   });
408
409   $.extend(Dropdown.prototype, {
410     // Public properties
411     // -----------------
412
413     $el:       null,  // jQuery object of ul.dropdown-menu element.
414     $inputEl:  null,  // jQuery object of target textarea.
415     completer: null,
416     footer:    null,
417     header:    null,
418     id:        null,
419     maxCount:  10,
420     placement: '',
421     shown:     false,
422     data:      [],     // Shown zipped data.
423     className: '',
424
425     // Public methods
426     // --------------
427
428     destroy: function () {
429       // Don't remove $el because it may be shared by several textcompletes.
430       this.deactivate();
431
432       this.$el.off('.' + this.id);
433       this.$inputEl.off('.' + this.id);
434       this.clear();
435       this.$el = this.$inputEl = this.completer = null;
436       delete dropdownViews[this.id]
437     },
438
439     render: function (zippedData) {
440       var contentsHtml = this._buildContents(zippedData);
441       var unzippedData = $.map(this.data, function (d) { return d.value; });
442       if (this.data.length) {
443         this._renderHeader(unzippedData);
444         this._renderFooter(unzippedData);
445         if (contentsHtml) {
446           this._renderContents(contentsHtml);
447           this._fitToBottom();
448           this._activateIndexedItem();
449         }
450         this._setScroll();
451       } else if (this.noResultsMessage) {
452         this._renderNoResultsMessage(unzippedData);
453       } else if (this.shown) {
454         this.deactivate();
455       }
456     },
457
458     setPosition: function (pos) {
459       this.$el.css(this._applyPlacement(pos));
460
461       // Make the dropdown fixed if the input is also fixed
462       // This can't be done during init, as textcomplete may be used on multiple elements on the same page
463       // Because the same dropdown is reused behind the scenes, we need to recheck every time the dropdown is showed
464       var position = 'absolute';
465       // Check if input or one of its parents has positioning we need to care about
466       this.$inputEl.add(this.$inputEl.parents()).each(function() {
467         if($(this).css('position') === 'absolute') // The element has absolute positioning, so it's all OK
468           return false;
469         if($(this).css('position') === 'fixed') {
470           position = 'fixed';
471           return false;
472         }
473       });
474       this.$el.css({ position: position }); // Update positioning
475
476       return this;
477     },
478
479     clear: function () {
480       this.$el.html('');
481       this.data = [];
482       this._index = 0;
483       this._$header = this._$footer = this._$noResultsMessage = null;
484     },
485
486     activate: function () {
487       if (!this.shown) {
488         this.clear();
489         this.$el.show();
490         if (this.className) { this.$el.addClass(this.className); }
491         this.completer.fire('textComplete:show');
492         this.shown = true;
493       }
494       return this;
495     },
496
497     deactivate: function () {
498       if (this.shown) {
499         this.$el.hide();
500         if (this.className) { this.$el.removeClass(this.className); }
501         this.completer.fire('textComplete:hide');
502         this.shown = false;
503       }
504       return this;
505     },
506
507     isUp: function (e) {
508       return e.keyCode === 38 || (e.ctrlKey && e.keyCode === 80);  // UP, Ctrl-P
509     },
510
511     isDown: function (e) {
512       return e.keyCode === 40 || (e.ctrlKey && e.keyCode === 78);  // DOWN, Ctrl-N
513     },
514
515     isEnter: function (e) {
516       var modifiers = e.ctrlKey || e.altKey || e.metaKey || e.shiftKey;
517       return !modifiers && (e.keyCode === 13 || e.keyCode === 9 || (this.option.completeOnSpace === true && e.keyCode === 32))  // ENTER, TAB
518     },
519
520     isPageup: function (e) {
521       return e.keyCode === 33;  // PAGEUP
522     },
523
524     isPagedown: function (e) {
525       return e.keyCode === 34;  // PAGEDOWN
526     },
527
528     isEscape: function (e) {
529       return e.keyCode === 27;  // ESCAPE
530     },
531
532     // Private properties
533     // ------------------
534
535     _data:    null,  // Currently shown zipped data.
536     _index:   null,
537     _$header: null,
538     _$noResultsMessage: null,
539     _$footer: null,
540
541     // Private methods
542     // ---------------
543
544     _bindEvents: function () {
545       this.$el.on('mousedown.' + this.id, '.textcomplete-item', $.proxy(this._onClick, this));
546       this.$el.on('touchstart.' + this.id, '.textcomplete-item', $.proxy(this._onClick, this));
547       this.$el.on('mouseover.' + this.id, '.textcomplete-item', $.proxy(this._onMouseover, this));
548       this.$inputEl.on('keydown.' + this.id, $.proxy(this._onKeydown, this));
549     },
550
551     _onClick: function (e) {
552       var $el = $(e.target);
553       e.preventDefault();
554       e.originalEvent.keepTextCompleteDropdown = this.id;
555       if (!$el.hasClass('textcomplete-item')) {
556         $el = $el.closest('.textcomplete-item');
557       }
558       var datum = this.data[parseInt($el.data('index'), 10)];
559       this.completer.select(datum.value, datum.strategy, e);
560       var self = this;
561       // Deactive at next tick to allow other event handlers to know whether
562       // the dropdown has been shown or not.
563       setTimeout(function () {
564         self.deactivate();
565         if (e.type === 'touchstart') {
566           self.$inputEl.focus();
567         }
568       }, 0);
569     },
570
571     // Activate hovered item.
572     _onMouseover: function (e) {
573       var $el = $(e.target);
574       e.preventDefault();
575       if (!$el.hasClass('textcomplete-item')) {
576         $el = $el.closest('.textcomplete-item');
577       }
578       this._index = parseInt($el.data('index'), 10);
579       this._activateIndexedItem();
580     },
581
582     _onKeydown: function (e) {
583       if (!this.shown) { return; }
584
585       var command;
586
587       if ($.isFunction(this.option.onKeydown)) {
588         command = this.option.onKeydown(e, commands);
589       }
590
591       if (command == null) {
592         command = this._defaultKeydown(e);
593       }
594
595       switch (command) {
596         case commands.KEY_UP:
597           e.preventDefault();
598           this._up();
599           break;
600         case commands.KEY_DOWN:
601           e.preventDefault();
602           this._down();
603           break;
604         case commands.KEY_ENTER:
605           e.preventDefault();
606           this._enter(e);
607           break;
608         case commands.KEY_PAGEUP:
609           e.preventDefault();
610           this._pageup();
611           break;
612         case commands.KEY_PAGEDOWN:
613           e.preventDefault();
614           this._pagedown();
615           break;
616         case commands.KEY_ESCAPE:
617           e.preventDefault();
618           this.deactivate();
619           break;
620       }
621     },
622
623     _defaultKeydown: function (e) {
624       if (this.isUp(e)) {
625         return commands.KEY_UP;
626       } else if (this.isDown(e)) {
627         return commands.KEY_DOWN;
628       } else if (this.isEnter(e)) {
629         return commands.KEY_ENTER;
630       } else if (this.isPageup(e)) {
631         return commands.KEY_PAGEUP;
632       } else if (this.isPagedown(e)) {
633         return commands.KEY_PAGEDOWN;
634       } else if (this.isEscape(e)) {
635         return commands.KEY_ESCAPE;
636       }
637     },
638
639     _up: function () {
640       if (this._index === 0) {
641         this._index = this.data.length - 1;
642       } else {
643         this._index -= 1;
644       }
645       this._activateIndexedItem();
646       this._setScroll();
647     },
648
649     _down: function () {
650       if (this._index === this.data.length - 1) {
651         this._index = 0;
652       } else {
653         this._index += 1;
654       }
655       this._activateIndexedItem();
656       this._setScroll();
657     },
658
659     _enter: function (e) {
660       var datum = this.data[parseInt(this._getActiveElement().data('index'), 10)];
661       this.completer.select(datum.value, datum.strategy, e);
662       this.deactivate();
663     },
664
665     _pageup: function () {
666       var target = 0;
667       var threshold = this._getActiveElement().position().top - this.$el.innerHeight();
668       this.$el.children().each(function (i) {
669         if ($(this).position().top + $(this).outerHeight() > threshold) {
670           target = i;
671           return false;
672         }
673       });
674       this._index = target;
675       this._activateIndexedItem();
676       this._setScroll();
677     },
678
679     _pagedown: function () {
680       var target = this.data.length - 1;
681       var threshold = this._getActiveElement().position().top + this.$el.innerHeight();
682       this.$el.children().each(function (i) {
683         if ($(this).position().top > threshold) {
684           target = i;
685           return false
686         }
687       });
688       this._index = target;
689       this._activateIndexedItem();
690       this._setScroll();
691     },
692
693     _activateIndexedItem: function () {
694       this.$el.find('.textcomplete-item.active').removeClass('active');
695       this._getActiveElement().addClass('active');
696     },
697
698     _getActiveElement: function () {
699       return this.$el.children('.textcomplete-item:nth(' + this._index + ')');
700     },
701
702     _setScroll: function () {
703       var $activeEl = this._getActiveElement();
704       var itemTop = $activeEl.position().top;
705       var itemHeight = $activeEl.outerHeight();
706       var visibleHeight = this.$el.innerHeight();
707       var visibleTop = this.$el.scrollTop();
708       if (this._index === 0 || this._index == this.data.length - 1 || itemTop < 0) {
709         this.$el.scrollTop(itemTop + visibleTop);
710       } else if (itemTop + itemHeight > visibleHeight) {
711         this.$el.scrollTop(itemTop + itemHeight + visibleTop - visibleHeight);
712       }
713     },
714
715     _buildContents: function (zippedData) {
716       var datum, i, index;
717       var html = '';
718       for (i = 0; i < zippedData.length; i++) {
719         if (this.data.length === this.maxCount) break;
720         datum = zippedData[i];
721         if (include(this.data, datum)) { continue; }
722         index = this.data.length;
723         this.data.push(datum);
724         html += '<li class="textcomplete-item" data-index="' + index + '"><a>';
725         html +=   datum.strategy.template(datum.value, datum.term);
726         html += '</a></li>';
727       }
728       return html;
729     },
730
731     _renderHeader: function (unzippedData) {
732       if (this.header) {
733         if (!this._$header) {
734           this._$header = $('<li class="textcomplete-header"></li>').prependTo(this.$el);
735         }
736         var html = $.isFunction(this.header) ? this.header(unzippedData) : this.header;
737         this._$header.html(html);
738       }
739     },
740
741     _renderFooter: function (unzippedData) {
742       if (this.footer) {
743         if (!this._$footer) {
744           this._$footer = $('<li class="textcomplete-footer"></li>').appendTo(this.$el);
745         }
746         var html = $.isFunction(this.footer) ? this.footer(unzippedData) : this.footer;
747         this._$footer.html(html);
748       }
749     },
750
751     _renderNoResultsMessage: function (unzippedData) {
752       if (this.noResultsMessage) {
753         if (!this._$noResultsMessage) {
754           this._$noResultsMessage = $('<li class="textcomplete-no-results-message"></li>').appendTo(this.$el);
755         }
756         var html = $.isFunction(this.noResultsMessage) ? this.noResultsMessage(unzippedData) : this.noResultsMessage;
757         this._$noResultsMessage.html(html);
758       }
759     },
760
761     _renderContents: function (html) {
762       if (this._$footer) {
763         this._$footer.before(html);
764       } else {
765         this.$el.append(html);
766       }
767     },
768
769     _fitToBottom: function() {
770       var windowScrollBottom = $window.scrollTop() + $window.height();
771       var height = this.$el.height();
772       if ((this.$el.position().top + height) > windowScrollBottom) {
773         this.$el.offset({top: windowScrollBottom - height});
774       }
775     },
776
777     _applyPlacement: function (position) {
778       // If the 'placement' option set to 'top', move the position above the element.
779       if (this.placement.indexOf('top') !== -1) {
780         // Overwrite the position object to set the 'bottom' property instead of the top.
781         position = {
782           top: 'auto',
783           bottom: this.$el.parent().height() - position.top + position.lineHeight,
784           left: position.left
785         };
786       } else {
787         position.bottom = 'auto';
788         delete position.lineHeight;
789       }
790       if (this.placement.indexOf('absleft') !== -1) {
791         position.left = 0;
792       } else if (this.placement.indexOf('absright') !== -1) {
793         position.right = 0;
794         position.left = 'auto';
795       }
796       return position;
797     }
798   });
799
800   $.fn.textcomplete.Dropdown = Dropdown;
801   $.extend($.fn.textcomplete, commands);
802 }(jQuery);
803
804 +function ($) {
805   'use strict';
806
807   // Memoize a search function.
808   var memoize = function (func) {
809     var memo = {};
810     return function (term, callback) {
811       if (memo[term]) {
812         callback(memo[term]);
813       } else {
814         func.call(this, term, function (data) {
815           memo[term] = (memo[term] || []).concat(data);
816           callback.apply(null, arguments);
817         });
818       }
819     };
820   };
821
822   function Strategy(options) {
823     $.extend(this, options);
824     if (this.cache) { this.search = memoize(this.search); }
825   }
826
827   Strategy.parse = function (strategiesArray, params) {
828     return $.map(strategiesArray, function (strategy) {
829       var strategyObj = new Strategy(strategy);
830       strategyObj.el = params.el;
831       strategyObj.$el = params.$el;
832       return strategyObj;
833     });
834   };
835
836   $.extend(Strategy.prototype, {
837     // Public properties
838     // -----------------
839
840     // Required
841     match:      null,
842     replace:    null,
843     search:     null,
844
845     // Optional
846     cache:      false,
847     context:    function () { return true; },
848     index:      2,
849     template:   function (obj) { return obj; },
850     idProperty: null
851   });
852
853   $.fn.textcomplete.Strategy = Strategy;
854
855 }(jQuery);
856
857 +function ($) {
858   'use strict';
859
860   var now = Date.now || function () { return new Date().getTime(); };
861
862   // Returns a function, that, as long as it continues to be invoked, will not
863   // be triggered. The function will be called after it stops being called for
864   // `wait` msec.
865   //
866   // This utility function was originally implemented at Underscore.js.
867   var debounce = function (func, wait) {
868     var timeout, args, context, timestamp, result;
869     var later = function () {
870       var last = now() - timestamp;
871       if (last < wait) {
872         timeout = setTimeout(later, wait - last);
873       } else {
874         timeout = null;
875         result = func.apply(context, args);
876         context = args = null;
877       }
878     };
879
880     return function () {
881       context = this;
882       args = arguments;
883       timestamp = now();
884       if (!timeout) {
885         timeout = setTimeout(later, wait);
886       }
887       return result;
888     };
889   };
890
891   function Adapter () {}
892
893   $.extend(Adapter.prototype, {
894     // Public properties
895     // -----------------
896
897     id:        null, // Identity.
898     completer: null, // Completer object which creates it.
899     el:        null, // Textarea element.
900     $el:       null, // jQuery object of the textarea.
901     option:    null,
902
903     // Public methods
904     // --------------
905
906     initialize: function (element, completer, option) {
907       this.el        = element;
908       this.$el       = $(element);
909       this.id        = completer.id + this.constructor.name;
910       this.completer = completer;
911       this.option    = option;
912
913       if (this.option.debounce) {
914         this._onKeyup = debounce(this._onKeyup, this.option.debounce);
915       }
916
917       this._bindEvents();
918     },
919
920     destroy: function () {
921       this.$el.off('.' + this.id); // Remove all event handlers.
922       this.$el = this.el = this.completer = null;
923     },
924
925     // Update the element with the given value and strategy.
926     //
927     // value    - The selected object. It is one of the item of the array
928     //            which was callbacked from the search function.
929     // strategy - The Strategy associated with the selected value.
930     select: function (/* value, strategy */) {
931       throw new Error('Not implemented');
932     },
933
934     // Returns the caret's relative coordinates from body's left top corner.
935     //
936     // FIXME: Calculate the left top corner of `this.option.appendTo` element.
937     getCaretPosition: function () {
938       var position = this._getCaretRelativePosition();
939       var offset = this.$el.offset();
940       position.top += offset.top;
941       position.left += offset.left;
942       return position;
943     },
944
945     // Focus on the element.
946     focus: function () {
947       this.$el.focus();
948     },
949
950     // Private methods
951     // ---------------
952
953     _bindEvents: function () {
954       this.$el.on('keyup.' + this.id, $.proxy(this._onKeyup, this));
955     },
956
957     _onKeyup: function (e) {
958       if (this._skipSearch(e)) { return; }
959       this.completer.trigger(this.getTextFromHeadToCaret(), true);
960     },
961
962     // Suppress searching if it returns true.
963     _skipSearch: function (clickEvent) {
964       switch (clickEvent.keyCode) {
965         case 13: // ENTER
966         case 40: // DOWN
967         case 38: // UP
968           return true;
969       }
970       if (clickEvent.ctrlKey) switch (clickEvent.keyCode) {
971         case 78: // Ctrl-N
972         case 80: // Ctrl-P
973           return true;
974       }
975     }
976   });
977
978   $.fn.textcomplete.Adapter = Adapter;
979 }(jQuery);
980
981 +function ($) {
982   'use strict';
983
984   // Textarea adapter
985   // ================
986   //
987   // Managing a textarea. It doesn't know a Dropdown.
988   function Textarea(element, completer, option) {
989     this.initialize(element, completer, option);
990   }
991
992   Textarea.DIV_PROPERTIES = {
993     left: -9999,
994     position: 'absolute',
995     top: 0,
996     whiteSpace: 'pre-wrap'
997   }
998
999   Textarea.COPY_PROPERTIES = [
1000     'border-width', 'font-family', 'font-size', 'font-style', 'font-variant',
1001     'font-weight', 'height', 'letter-spacing', 'word-spacing', 'line-height',
1002     'text-decoration', 'text-align', 'width', 'padding-top', 'padding-right',
1003     'padding-bottom', 'padding-left', 'margin-top', 'margin-right',
1004     'margin-bottom', 'margin-left', 'border-style', 'box-sizing', 'tab-size'
1005   ];
1006
1007   $.extend(Textarea.prototype, $.fn.textcomplete.Adapter.prototype, {
1008     // Public methods
1009     // --------------
1010
1011     // Update the textarea with the given value and strategy.
1012     select: function (value, strategy, e) {
1013       var pre = this.getTextFromHeadToCaret();
1014       var post = this.el.value.substring(this.el.selectionEnd);
1015       var newSubstr = strategy.replace(value, e);
1016       if (typeof newSubstr !== 'undefined') {
1017         if ($.isArray(newSubstr)) {
1018           post = newSubstr[1] + post;
1019           newSubstr = newSubstr[0];
1020         }
1021         pre = pre.replace(strategy.match, newSubstr);
1022         this.$el.val(pre + post);
1023         this.el.selectionStart = this.el.selectionEnd = pre.length;
1024       }
1025     },
1026
1027     // Private methods
1028     // ---------------
1029
1030     // Returns the caret's relative coordinates from textarea's left top corner.
1031     //
1032     // Browser native API does not provide the way to know the position of
1033     // caret in pixels, so that here we use a kind of hack to accomplish
1034     // the aim. First of all it puts a dummy div element and completely copies
1035     // the textarea's style to the element, then it inserts the text and a
1036     // span element into the textarea.
1037     // Consequently, the span element's position is the thing what we want.
1038     _getCaretRelativePosition: function () {
1039       var dummyDiv = $('<div></div>').css(this._copyCss())
1040         .text(this.getTextFromHeadToCaret());
1041       var span = $('<span></span>').text('.').appendTo(dummyDiv);
1042       this.$el.before(dummyDiv);
1043       var position = span.position();
1044       position.top += span.height() - this.$el.scrollTop();
1045       position.lineHeight = span.height();
1046       dummyDiv.remove();
1047       return position;
1048     },
1049
1050     _copyCss: function () {
1051       return $.extend({
1052         // Set 'scroll' if a scrollbar is being shown; otherwise 'auto'.
1053         overflow: this.el.scrollHeight > this.el.offsetHeight ? 'scroll' : 'auto'
1054       }, Textarea.DIV_PROPERTIES, this._getStyles());
1055     },
1056
1057     _getStyles: (function ($) {
1058       var color = $('<div></div>').css(['color']).color;
1059       if (typeof color !== 'undefined') {
1060         return function () {
1061           return this.$el.css(Textarea.COPY_PROPERTIES);
1062         };
1063       } else { // jQuery < 1.8
1064         return function () {
1065           var $el = this.$el;
1066           var styles = {};
1067           $.each(Textarea.COPY_PROPERTIES, function (i, property) {
1068             styles[property] = $el.css(property);
1069           });
1070           return styles;
1071         };
1072       }
1073     })($),
1074
1075     getTextFromHeadToCaret: function () {
1076       return this.el.value.substring(0, this.el.selectionEnd);
1077     }
1078   });
1079
1080   $.fn.textcomplete.Textarea = Textarea;
1081 }(jQuery);
1082
1083 +function ($) {
1084   'use strict';
1085
1086   var sentinelChar = '吶';
1087
1088   function IETextarea(element, completer, option) {
1089     this.initialize(element, completer, option);
1090     $('<span>' + sentinelChar + '</span>').css({
1091       position: 'absolute',
1092       top: -9999,
1093       left: -9999
1094     }).insertBefore(element);
1095   }
1096
1097   $.extend(IETextarea.prototype, $.fn.textcomplete.Textarea.prototype, {
1098     // Public methods
1099     // --------------
1100
1101     select: function (value, strategy, e) {
1102       var pre = this.getTextFromHeadToCaret();
1103       var post = this.el.value.substring(pre.length);
1104       var newSubstr = strategy.replace(value, e);
1105       if (typeof newSubstr !== 'undefined') {
1106         if ($.isArray(newSubstr)) {
1107           post = newSubstr[1] + post;
1108           newSubstr = newSubstr[0];
1109         }
1110         pre = pre.replace(strategy.match, newSubstr);
1111         this.$el.val(pre + post);
1112         this.el.focus();
1113         var range = this.el.createTextRange();
1114         range.collapse(true);
1115         range.moveEnd('character', pre.length);
1116         range.moveStart('character', pre.length);
1117         range.select();
1118       }
1119     },
1120
1121     getTextFromHeadToCaret: function () {
1122       this.el.focus();
1123       var range = document.selection.createRange();
1124       range.moveStart('character', -this.el.value.length);
1125       var arr = range.text.split(sentinelChar)
1126       return arr.length === 1 ? arr[0] : arr[1];
1127     }
1128   });
1129
1130   $.fn.textcomplete.IETextarea = IETextarea;
1131 }(jQuery);
1132
1133 // NOTE: TextComplete plugin has contenteditable support but it does not work
1134 //       fine especially on old IEs.
1135 //       Any pull requests are REALLY welcome.
1136
1137 +function ($) {
1138   'use strict';
1139
1140   // ContentEditable adapter
1141   // =======================
1142   //
1143   // Adapter for contenteditable elements.
1144   function ContentEditable (element, completer, option) {
1145     this.initialize(element, completer, option);
1146   }
1147
1148   $.extend(ContentEditable.prototype, $.fn.textcomplete.Adapter.prototype, {
1149     // Public methods
1150     // --------------
1151
1152     // Update the content with the given value and strategy.
1153     // When an dropdown item is selected, it is executed.
1154     select: function (value, strategy, e) {
1155       var pre = this.getTextFromHeadToCaret();
1156       var sel = window.getSelection()
1157       var range = sel.getRangeAt(0);
1158       var selection = range.cloneRange();
1159       selection.selectNodeContents(range.startContainer);
1160       var content = selection.toString();
1161       var post = content.substring(range.startOffset);
1162       var newSubstr = strategy.replace(value, e);
1163       if (typeof newSubstr !== 'undefined') {
1164         if ($.isArray(newSubstr)) {
1165           post = newSubstr[1] + post;
1166           newSubstr = newSubstr[0];
1167         }
1168         pre = pre.replace(strategy.match, newSubstr);
1169         range.selectNodeContents(range.startContainer);
1170         range.deleteContents();
1171         var node = document.createTextNode(pre + post);
1172         range.insertNode(node);
1173         range.setStart(node, pre.length);
1174         range.collapse(true);
1175         sel.removeAllRanges();
1176         sel.addRange(range);
1177       }
1178     },
1179
1180     // Private methods
1181     // ---------------
1182
1183     // Returns the caret's relative position from the contenteditable's
1184     // left top corner.
1185     //
1186     // Examples
1187     //
1188     //   this._getCaretRelativePosition()
1189     //   //=> { top: 18, left: 200, lineHeight: 16 }
1190     //
1191     // Dropdown's position will be decided using the result.
1192     _getCaretRelativePosition: function () {
1193       var range = window.getSelection().getRangeAt(0).cloneRange();
1194       var node = document.createElement('span');
1195       range.insertNode(node);
1196       range.selectNodeContents(node);
1197       range.deleteContents();
1198       var $node = $(node);
1199       var position = $node.offset();
1200       position.left -= this.$el.offset().left;
1201       position.top += $node.height() - this.$el.offset().top;
1202       position.lineHeight = $node.height();
1203       $node.remove();
1204       return position;
1205     },
1206
1207     // Returns the string between the first character and the caret.
1208     // Completer will be triggered with the result for start autocompleting.
1209     //
1210     // Example
1211     //
1212     //   // Suppose the html is '<b>hello</b> wor|ld' and | is the caret.
1213     //   this.getTextFromHeadToCaret()
1214     //   // => ' wor'  // not '<b>hello</b> wor'
1215     getTextFromHeadToCaret: function () {
1216       var range = window.getSelection().getRangeAt(0);
1217       var selection = range.cloneRange();
1218       selection.selectNodeContents(range.startContainer);
1219       return selection.toString().substring(0, range.startOffset);
1220     }
1221   });
1222
1223   $.fn.textcomplete.ContentEditable = ContentEditable;
1224 }(jQuery);
1225
1226 return jQuery;
1227 }));