]> git.mxchange.org Git - friendica.git/blob - view/js/jquery-textcomplete/jquery.textcomplete.js
Merge pull request #8237 from annando/a11y-2
[friendica.git] / view / js / jquery-textcomplete / jquery.textcomplete.js
1 // @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat
2 (function (factory) {
3   if (typeof define === 'function' && define.amd) {
4     // AMD. Register as an anonymous module.
5     define(['jquery'], factory);
6   } else if (typeof module === "object" && module.exports) {
7     var $ = require('jquery');
8     module.exports = factory($);
9   } else {
10     // Browser globals
11     factory(jQuery);
12   }
13 }(function (jQuery) {
14
15 /*!
16  * jQuery.textcomplete
17  *
18  * Repository: https://github.com/yuku-t/jquery-textcomplete
19  * License:    MIT (https://github.com/yuku-t/jquery-textcomplete/blob/master/LICENSE)
20  * Author:     Yuku Takahashi
21  */
22
23 if (typeof jQuery === 'undefined') {
24   throw new Error('jQuery.textcomplete requires jQuery');
25 }
26
27 +function ($) {
28   'use strict';
29
30   var warn = function (message) {
31     if (console.warn) { console.warn(message); }
32   };
33
34   var id = 1;
35
36   $.fn.textcomplete = function (strategies, option) {
37     var args = Array.prototype.slice.call(arguments);
38     return this.each(function () {
39       var self = this;
40       var $this = $(this);
41       var completer = $this.data('textComplete');
42       if (!completer) {
43         option || (option = {});
44         option._oid = id++;  // unique object id
45         completer = new $.fn.textcomplete.Completer(this, option);
46         $this.data('textComplete', completer);
47       }
48       if (typeof strategies === 'string') {
49         if (!completer) return;
50         args.shift()
51         completer[strategies].apply(completer, args);
52         if (strategies === 'destroy') {
53           $this.removeData('textComplete');
54         }
55       } else {
56         // For backward compatibility.
57         // TODO: Remove at v0.4
58         $.each(strategies, function (obj) {
59           $.each(['header', 'footer', 'placement', 'maxCount'], function (name) {
60             if (obj[name]) {
61               completer.option[name] = obj[name];
62               warn(name + 'as a strategy param is deprecated. Use option.');
63               delete obj[name];
64             }
65           });
66         });
67         completer.register($.fn.textcomplete.Strategy.parse(strategies, {
68           el: self,
69           $el: $this
70         }));
71       }
72     });
73   };
74
75 }(jQuery);
76
77 +function ($) {
78   'use strict';
79
80   // Exclusive execution control utility.
81   //
82   // func - The function to be locked. It is executed with a function named
83   //        `free` as the first argument. Once it is called, additional
84   //        execution are ignored until the free is invoked. Then the last
85   //        ignored execution will be replayed immediately.
86   //
87   // Examples
88   //
89   //   var lockedFunc = lock(function (free) {
90   //     setTimeout(function { free(); }, 1000); // It will be free in 1 sec.
91   //     console.log('Hello, world');
92   //   });
93   //   lockedFunc();  // => 'Hello, world'
94   //   lockedFunc();  // none
95   //   lockedFunc();  // none
96   //   // 1 sec past then
97   //   // => 'Hello, world'
98   //   lockedFunc();  // => 'Hello, world'
99   //   lockedFunc();  // none
100   //
101   // Returns a wrapped function.
102   var lock = function (func) {
103     var locked, queuedArgsToReplay;
104
105     return function () {
106       // Convert arguments into a real array.
107       var args = Array.prototype.slice.call(arguments);
108       if (locked) {
109         // Keep a copy of this argument list to replay later.
110         // OK to overwrite a previous value because we only replay
111         // the last one.
112         queuedArgsToReplay = args;
113         return;
114       }
115       locked = true;
116       var self = this;
117       args.unshift(function replayOrFree() {
118         if (queuedArgsToReplay) {
119           // Other request(s) arrived while we were locked.
120           // Now that the lock is becoming available, replay
121           // the latest such request, then call back here to
122           // unlock (or replay another request that arrived
123           // while this one was in flight).
124           var replayArgs = queuedArgsToReplay;
125           queuedArgsToReplay = undefined;
126           replayArgs.unshift(replayOrFree);
127           func.apply(self, replayArgs);
128         } else {
129           locked = false;
130         }
131       });
132       func.apply(this, args);
133     };
134   };
135
136   var isString = function (obj) {
137     return Object.prototype.toString.call(obj) === '[object String]';
138   };
139
140   var isFunction = function (obj) {
141     return Object.prototype.toString.call(obj) === '[object Function]';
142   };
143
144   var uniqueId = 0;
145
146   function Completer(element, option) {
147     this.$el        = $(element);
148     this.id         = 'textcomplete' + uniqueId++;
149     this.strategies = [];
150     this.views      = [];
151     this.option     = $.extend({}, Completer._getDefaults(), option);
152
153     if (!this.$el.is('input[type=text]') && !this.$el.is('input[type=search]') && !this.$el.is('textarea') && !element.isContentEditable && element.contentEditable != 'true') {
154       throw new Error('textcomplete must be called on a Textarea or a ContentEditable.');
155     }
156
157     if (element === document.activeElement) {
158       // element has already been focused. Initialize view objects immediately.
159       this.initialize()
160     } else {
161       // Initialize view objects lazily.
162       var self = this;
163       this.$el.one('focus.' + this.id, function () { self.initialize(); });
164     }
165   }
166
167   Completer._getDefaults = function () {
168     if (!Completer.DEFAULTS) {
169       Completer.DEFAULTS = {
170         appendTo: $('body'),
171         zIndex: '100'
172       };
173     }
174
175     return Completer.DEFAULTS;
176   }
177
178   $.extend(Completer.prototype, {
179     // Public properties
180     // -----------------
181
182     id:         null,
183     option:     null,
184     strategies: null,
185     adapter:    null,
186     dropdown:   null,
187     $el:        null,
188
189     // Public methods
190     // --------------
191
192     initialize: function () {
193       var element = this.$el.get(0);
194       // Initialize view objects.
195       this.dropdown = new $.fn.textcomplete.Dropdown(element, this, this.option);
196       var Adapter, viewName;
197       if (this.option.adapter) {
198         Adapter = this.option.adapter;
199       } else {
200         if (this.$el.is('textarea') || this.$el.is('input[type=text]') || this.$el.is('input[type=search]')) {
201           viewName = typeof element.selectionEnd === 'number' ? 'Textarea' : 'IETextarea';
202         } else {
203           viewName = 'ContentEditable';
204         }
205         Adapter = $.fn.textcomplete[viewName];
206       }
207       this.adapter = new Adapter(element, this, this.option);
208     },
209
210     destroy: function () {
211       this.$el.off('.' + this.id);
212       if (this.adapter) {
213         this.adapter.destroy();
214       }
215       if (this.dropdown) {
216         this.dropdown.destroy();
217       }
218       this.$el = this.adapter = this.dropdown = null;
219     },
220
221     deactivate: function () {
222       if (this.dropdown) {
223         this.dropdown.deactivate();
224       }
225     },
226
227     // Invoke textcomplete.
228     trigger: function (text, skipUnchangedTerm) {
229       if (!this.dropdown) { this.initialize(); }
230       text != null || (text = this.adapter.getTextFromHeadToCaret());
231       var searchQuery = this._extractSearchQuery(text);
232       if (searchQuery.length) {
233         var term = searchQuery[1];
234         // Ignore shift-key, ctrl-key and so on.
235         if (skipUnchangedTerm && this._term === term && term !== "") { return; }
236         this._term = term;
237         this._search.apply(this, searchQuery);
238       } else {
239         this._term = null;
240         this.dropdown.deactivate();
241       }
242     },
243
244     fire: function (eventName) {
245       var args = Array.prototype.slice.call(arguments, 1);
246       this.$el.trigger(eventName, args);
247       return this;
248     },
249
250     register: function (strategies) {
251       Array.prototype.push.apply(this.strategies, strategies);
252     },
253
254     // Insert the value into adapter view. It is called when the dropdown is clicked
255     // or selected.
256     //
257     // value    - The selected element of the array callbacked from search func.
258     // strategy - The Strategy object.
259     // e        - Click or keydown event object.
260     select: function (value, strategy, e) {
261       this._term = null;
262       this.adapter.select(value, strategy, e);
263       this.fire('change').fire('textComplete:select', value, strategy);
264       this.adapter.focus();
265     },
266
267     // Private properties
268     // ------------------
269
270     _clearAtNext: true,
271     _term:        null,
272
273     // Private methods
274     // ---------------
275
276     // Parse the given text and extract the first matching strategy.
277     //
278     // Returns an array including the strategy, the query term and the match
279     // object if the text matches an strategy; otherwise returns an empty array.
280     _extractSearchQuery: function (text) {
281       for (var i = 0; i < this.strategies.length; i++) {
282         var strategy = this.strategies[i];
283         var context = strategy.context(text);
284         if (context || context === '') {
285           var matchRegexp = isFunction(strategy.match) ? strategy.match(text) : strategy.match;
286           if (isString(context)) { text = context; }
287           var match = text.match(matchRegexp);
288           if (match) { return [strategy, match[strategy.index], match]; }
289         }
290       }
291       return []
292     },
293
294     // Call the search method of selected strategy..
295     _search: lock(function (free, strategy, term, match) {
296       var self = this;
297       strategy.search(term, function (data, stillSearching) {
298         if (!self.dropdown.shown) {
299           self.dropdown.activate();
300         }
301         if (self._clearAtNext) {
302           // The first callback in the current lock.
303           self.dropdown.clear();
304           self._clearAtNext = false;
305         }
306         self.dropdown.setPosition(self.adapter.getCaretPosition());
307         self.dropdown.render(self._zip(data, strategy, term));
308         if (!stillSearching) {
309           // The last callback in the current lock.
310           free();
311           self._clearAtNext = true; // Call dropdown.clear at the next time.
312         }
313       }, match);
314     }),
315
316     // Build a parameter for Dropdown#render.
317     //
318     // Examples
319     //
320     //  this._zip(['a', 'b'], 's');
321     //  //=> [{ value: 'a', strategy: 's' }, { value: 'b', strategy: 's' }]
322     _zip: function (data, strategy, term) {
323       return $.map(data, function (value) {
324         return { value: value, strategy: strategy, term: term };
325       });
326     }
327   });
328
329   $.fn.textcomplete.Completer = Completer;
330 }(jQuery);
331
332 +function ($) {
333   'use strict';
334
335   var $window = $(window);
336
337   var include = function (zippedData, datum) {
338     var i, elem;
339     var idProperty = datum.strategy.idProperty
340     for (i = 0; i < zippedData.length; i++) {
341       elem = zippedData[i];
342       if (elem.strategy !== datum.strategy) continue;
343       if (idProperty) {
344         if (elem.value[idProperty] === datum.value[idProperty]) return true;
345       } else {
346         if (elem.value === datum.value) return true;
347       }
348     }
349     return false;
350   };
351
352   var dropdownViews = {};
353   $(document).on('click', function (e) {
354     var id = e.originalEvent && e.originalEvent.keepTextCompleteDropdown;
355     $.each(dropdownViews, function (key, view) {
356       if (key !== id) { view.deactivate(); }
357     });
358   });
359
360   var commands = {
361     SKIP_DEFAULT: 0,
362     KEY_UP: 1,
363     KEY_DOWN: 2,
364     KEY_ENTER: 3,
365     KEY_PAGEUP: 4,
366     KEY_PAGEDOWN: 5,
367     KEY_ESCAPE: 6
368   };
369
370   // Dropdown view
371   // =============
372
373   // Construct Dropdown object.
374   //
375   // element - Textarea or contenteditable element.
376   function Dropdown(element, completer, option) {
377     this.$el       = Dropdown.createElement(option);
378     this.completer = completer;
379     this.id        = completer.id + 'dropdown';
380     this._data     = []; // zipped data.
381     this.$inputEl  = $(element);
382     this.option    = option;
383
384     // Override setPosition method.
385     if (option.listPosition) { this.setPosition = option.listPosition; }
386     if (option.height) { this.$el.height(option.height); }
387     var self = this;
388     $.each(['maxCount', 'placement', 'footer', 'header', 'noResultsMessage', 'className'], function (_i, name) {
389       if (option[name] != null) { self[name] = option[name]; }
390     });
391     this._bindEvents(element);
392     dropdownViews[this.id] = this;
393   }
394
395   $.extend(Dropdown, {
396     // Class methods
397     // -------------
398
399     createElement: function (option) {
400       var $parent = option.appendTo;
401       if (!($parent instanceof $)) { $parent = $($parent); }
402       var $el = $('<ul></ul>')
403         .addClass('dropdown-menu textcomplete-dropdown')
404         .attr('id', 'textcomplete-dropdown-' + option._oid)
405         .css({
406           display: 'none',
407           left: 0,
408           position: 'absolute',
409           zIndex: option.zIndex
410         })
411         .appendTo($parent);
412       return $el;
413     }
414   });
415
416   $.extend(Dropdown.prototype, {
417     // Public properties
418     // -----------------
419
420     $el:       null,  // jQuery object of ul.dropdown-menu element.
421     $inputEl:  null,  // jQuery object of target textarea.
422     completer: null,
423     footer:    null,
424     header:    null,
425     id:        null,
426     maxCount:  10,
427     placement: '',
428     shown:     false,
429     data:      [],     // Shown zipped data.
430     className: '',
431
432     // Public methods
433     // --------------
434
435     destroy: function () {
436       // Don't remove $el because it may be shared by several textcompletes.
437       this.deactivate();
438
439       this.$el.off('.' + this.id);
440       this.$inputEl.off('.' + this.id);
441       this.clear();
442       this.$el.remove();
443       this.$el = this.$inputEl = this.completer = null;
444       delete dropdownViews[this.id]
445     },
446
447     render: function (zippedData) {
448       var contentsHtml = this._buildContents(zippedData);
449       var unzippedData = $.map(this.data, function (d) { return d.value; });
450       if (this.data.length) {
451         var strategy = zippedData[0].strategy;
452         if (strategy.id) {
453           this.$el.attr('data-strategy', strategy.id);
454         } else {
455           this.$el.removeAttr('data-strategy');
456         }
457         this._renderHeader(unzippedData);
458         this._renderFooter(unzippedData);
459         if (contentsHtml) {
460           this._renderContents(contentsHtml);
461           this._fitToBottom();
462           this._fitToRight();
463           this._activateIndexedItem();
464         }
465         this._setScroll();
466       } else if (this.noResultsMessage) {
467         this._renderNoResultsMessage(unzippedData);
468       } else if (this.shown) {
469         this.deactivate();
470       }
471     },
472
473     setPosition: function (pos) {
474       // Make the dropdown fixed if the input is also fixed
475       // This can't be done during init, as textcomplete may be used on multiple elements on the same page
476       // Because the same dropdown is reused behind the scenes, we need to recheck every time the dropdown is showed
477       var position = 'absolute';
478       // Check if input or one of its parents has positioning we need to care about
479       this.$inputEl.add(this.$inputEl.parents()).each(function() {
480         if($(this).css('position') === 'absolute') // The element has absolute positioning, so it's all OK
481           return false;
482         if($(this).css('position') === 'fixed') {
483           pos.top -= $window.scrollTop();
484           pos.left -= $window.scrollLeft();                                     
485           position = 'fixed';
486           return false;
487         }
488       });
489       this.$el.css(this._applyPlacement(pos));
490       this.$el.css({ position: position }); // Update positioning
491
492       return this;
493     },
494
495     clear: function () {
496       this.$el.html('');
497       this.data = [];
498       this._index = 0;
499       this._$header = this._$footer = this._$noResultsMessage = null;
500     },
501
502     activate: function () {
503       if (!this.shown) {
504         this.clear();
505         this.$el.show();
506         if (this.className) { this.$el.addClass(this.className); }
507         this.completer.fire('textComplete:show');
508         this.shown = true;
509       }
510       return this;
511     },
512
513     deactivate: function () {
514       if (this.shown) {
515         this.$el.hide();
516         if (this.className) { this.$el.removeClass(this.className); }
517         this.completer.fire('textComplete:hide');
518         this.shown = false;
519       }
520       return this;
521     },
522
523     isUp: function (e) {
524       return e.keyCode === 38 || (e.ctrlKey && e.keyCode === 80);  // UP, Ctrl-P
525     },
526
527     isDown: function (e) {
528       return e.keyCode === 40 || (e.ctrlKey && e.keyCode === 78);  // DOWN, Ctrl-N
529     },
530
531     isEnter: function (e) {
532       var modifiers = e.ctrlKey || e.altKey || e.metaKey || e.shiftKey;
533       return !modifiers && (e.keyCode === 13 || e.keyCode === 9 || (this.option.completeOnSpace === true && e.keyCode === 32))  // ENTER, TAB
534     },
535
536     isPageup: function (e) {
537       return e.keyCode === 33;  // PAGEUP
538     },
539
540     isPagedown: function (e) {
541       return e.keyCode === 34;  // PAGEDOWN
542     },
543
544     isEscape: function (e) {
545       return e.keyCode === 27;  // ESCAPE
546     },
547
548     // Private properties
549     // ------------------
550
551     _data:    null,  // Currently shown zipped data.
552     _index:   null,
553     _$header: null,
554     _$noResultsMessage: null,
555     _$footer: null,
556
557     // Private methods
558     // ---------------
559
560     _bindEvents: function () {
561       this.$el.on('mousedown.' + this.id, '.textcomplete-item', $.proxy(this._onClick, this));
562       this.$el.on('touchstart.' + this.id, '.textcomplete-item', $.proxy(this._onClick, this));
563       this.$el.on('mouseover.' + this.id, '.textcomplete-item', $.proxy(this._onMouseover, this));
564       this.$inputEl.on('keydown.' + this.id, $.proxy(this._onKeydown, this));
565     },
566
567     _onClick: function (e) {
568       var $el = $(e.target);
569       e.preventDefault();
570       e.originalEvent.keepTextCompleteDropdown = this.id;
571       if (!$el.hasClass('textcomplete-item')) {
572         $el = $el.closest('.textcomplete-item');
573       }
574       var datum = this.data[parseInt($el.data('index'), 10)];
575       this.completer.select(datum.value, datum.strategy, e);
576       var self = this;
577       // Deactive at next tick to allow other event handlers to know whether
578       // the dropdown has been shown or not.
579       setTimeout(function () {
580         self.deactivate();
581         if (e.type === 'touchstart') {
582           self.$inputEl.focus();
583         }
584       }, 0);
585     },
586
587     // Activate hovered item.
588     _onMouseover: function (e) {
589       var $el = $(e.target);
590       e.preventDefault();
591       if (!$el.hasClass('textcomplete-item')) {
592         $el = $el.closest('.textcomplete-item');
593       }
594       this._index = parseInt($el.data('index'), 10);
595       this._activateIndexedItem();
596     },
597
598     _onKeydown: function (e) {
599       if (!this.shown) { return; }
600
601       var command;
602
603       if ($.isFunction(this.option.onKeydown)) {
604         command = this.option.onKeydown(e, commands);
605       }
606
607       if (command == null) {
608         command = this._defaultKeydown(e);
609       }
610
611       switch (command) {
612         case commands.KEY_UP:
613           e.preventDefault();
614           this._up();
615           break;
616         case commands.KEY_DOWN:
617           e.preventDefault();
618           this._down();
619           break;
620         case commands.KEY_ENTER:
621           e.preventDefault();
622           this._enter(e);
623           break;
624         case commands.KEY_PAGEUP:
625           e.preventDefault();
626           this._pageup();
627           break;
628         case commands.KEY_PAGEDOWN:
629           e.preventDefault();
630           this._pagedown();
631           break;
632         case commands.KEY_ESCAPE:
633           e.preventDefault();
634           this.deactivate();
635           break;
636       }
637     },
638
639     _defaultKeydown: function (e) {
640       if (this.isUp(e)) {
641         return commands.KEY_UP;
642       } else if (this.isDown(e)) {
643         return commands.KEY_DOWN;
644       } else if (this.isEnter(e)) {
645         return commands.KEY_ENTER;
646       } else if (this.isPageup(e)) {
647         return commands.KEY_PAGEUP;
648       } else if (this.isPagedown(e)) {
649         return commands.KEY_PAGEDOWN;
650       } else if (this.isEscape(e)) {
651         return commands.KEY_ESCAPE;
652       }
653     },
654
655     _up: function () {
656       if (this._index === 0) {
657         this._index = this.data.length - 1;
658       } else {
659         this._index -= 1;
660       }
661       this._activateIndexedItem();
662       this._setScroll();
663     },
664
665     _down: function () {
666       if (this._index === this.data.length - 1) {
667         this._index = 0;
668       } else {
669         this._index += 1;
670       }
671       this._activateIndexedItem();
672       this._setScroll();
673     },
674
675     _enter: function (e) {
676       var datum = this.data[parseInt(this._getActiveElement().data('index'), 10)];
677       this.completer.select(datum.value, datum.strategy, e);
678       this.deactivate();
679     },
680
681     _pageup: function () {
682       var target = 0;
683       var threshold = this._getActiveElement().position().top - this.$el.innerHeight();
684       this.$el.children().each(function (i) {
685         if ($(this).position().top + $(this).outerHeight() > threshold) {
686           target = i;
687           return false;
688         }
689       });
690       this._index = target;
691       this._activateIndexedItem();
692       this._setScroll();
693     },
694
695     _pagedown: function () {
696       var target = this.data.length - 1;
697       var threshold = this._getActiveElement().position().top + this.$el.innerHeight();
698       this.$el.children().each(function (i) {
699         if ($(this).position().top > threshold) {
700           target = i;
701           return false
702         }
703       });
704       this._index = target;
705       this._activateIndexedItem();
706       this._setScroll();
707     },
708
709     _activateIndexedItem: function () {
710       this.$el.find('.textcomplete-item.active').removeClass('active');
711       this._getActiveElement().addClass('active');
712     },
713
714     _getActiveElement: function () {
715       return this.$el.children('.textcomplete-item:nth(' + this._index + ')');
716     },
717
718     _setScroll: function () {
719       var $activeEl = this._getActiveElement();
720       var itemTop = $activeEl.position().top;
721       var itemHeight = $activeEl.outerHeight();
722       var visibleHeight = this.$el.innerHeight();
723       var visibleTop = this.$el.scrollTop();
724       if (this._index === 0 || this._index == this.data.length - 1 || itemTop < 0) {
725         this.$el.scrollTop(itemTop + visibleTop);
726       } else if (itemTop + itemHeight > visibleHeight) {
727         this.$el.scrollTop(itemTop + itemHeight + visibleTop - visibleHeight);
728       }
729     },
730
731     _buildContents: function (zippedData) {
732       var datum, i, index;
733       var html = '';
734       for (i = 0; i < zippedData.length; i++) {
735         if (this.data.length === this.maxCount) break;
736         datum = zippedData[i];
737         if (include(this.data, datum)) { continue; }
738         index = this.data.length;
739         this.data.push(datum);
740         html += '<li class="textcomplete-item" data-index="' + index + '"><a>';
741         html +=   datum.strategy.template(datum.value, datum.term);
742         html += '</a></li>';
743       }
744       return html;
745     },
746
747     _renderHeader: function (unzippedData) {
748       if (this.header) {
749         if (!this._$header) {
750           this._$header = $('<li class="textcomplete-header"></li>').prependTo(this.$el);
751         }
752         var html = $.isFunction(this.header) ? this.header(unzippedData) : this.header;
753         this._$header.html(html);
754       }
755     },
756
757     _renderFooter: function (unzippedData) {
758       if (this.footer) {
759         if (!this._$footer) {
760           this._$footer = $('<li class="textcomplete-footer"></li>').appendTo(this.$el);
761         }
762         var html = $.isFunction(this.footer) ? this.footer(unzippedData) : this.footer;
763         this._$footer.html(html);
764       }
765     },
766
767     _renderNoResultsMessage: function (unzippedData) {
768       if (this.noResultsMessage) {
769         if (!this._$noResultsMessage) {
770           this._$noResultsMessage = $('<li class="textcomplete-no-results-message"></li>').appendTo(this.$el);
771         }
772         var html = $.isFunction(this.noResultsMessage) ? this.noResultsMessage(unzippedData) : this.noResultsMessage;
773         this._$noResultsMessage.html(html);
774       }
775     },
776
777     _renderContents: function (html) {
778       if (this._$footer) {
779         this._$footer.before(html);
780       } else {
781         this.$el.append(html);
782       }
783     },
784
785     _fitToBottom: function() {
786       var windowScrollBottom = $window.scrollTop() + $window.height();
787       var height = this.$el.height();
788       if ((this.$el.position().top + height) > windowScrollBottom) {
789         this.$el.offset({top: windowScrollBottom - height});
790       }
791     },
792
793     _fitToRight: function() {
794       // We don't know how wide our content is until the browser positions us, and at that point it clips us
795       // to the document width so we don't know if we would have overrun it. As a heuristic to avoid that clipping
796       // (which makes our elements wrap onto the next line and corrupt the next item), if we're close to the right
797       // edge, move left. We don't know how far to move left, so just keep nudging a bit.
798       var tolerance = 30; // pixels. Make wider than vertical scrollbar because we might not be able to use that space.
799       var lastOffset = this.$el.offset().left, offset;
800       var width = this.$el.width();
801       var maxLeft = $window.width() - tolerance;
802       while (lastOffset + width > maxLeft) {
803         this.$el.offset({left: lastOffset - tolerance});
804         offset = this.$el.offset().left;
805         if (offset >= lastOffset) { break; }
806         lastOffset = offset;
807       }
808     },
809
810     _applyPlacement: function (position) {
811       // If the 'placement' option set to 'top', move the position above the element.
812       if (this.placement.indexOf('top') !== -1) {
813         // Overwrite the position object to set the 'bottom' property instead of the top.
814         position = {
815           top: 'auto',
816           bottom: this.$el.parent().height() - position.top + position.lineHeight,
817           left: position.left
818         };
819       } else {
820         position.bottom = 'auto';
821         delete position.lineHeight;
822       }
823       if (this.placement.indexOf('absleft') !== -1) {
824         position.left = 0;
825       } else if (this.placement.indexOf('absright') !== -1) {
826         position.right = 0;
827         position.left = 'auto';
828       }
829       return position;
830     }
831   });
832
833   $.fn.textcomplete.Dropdown = Dropdown;
834   $.extend($.fn.textcomplete, commands);
835 }(jQuery);
836
837 +function ($) {
838   'use strict';
839
840   // Memoize a search function.
841   var memoize = function (func) {
842     var memo = {};
843     return function (term, callback) {
844       if (memo[term]) {
845         callback(memo[term]);
846       } else {
847         func.call(this, term, function (data) {
848           memo[term] = (memo[term] || []).concat(data);
849           callback.apply(null, arguments);
850         });
851       }
852     };
853   };
854
855   function Strategy(options) {
856     $.extend(this, options);
857     if (this.cache) { this.search = memoize(this.search); }
858   }
859
860   Strategy.parse = function (strategiesArray, params) {
861     return $.map(strategiesArray, function (strategy) {
862       var strategyObj = new Strategy(strategy);
863       strategyObj.el = params.el;
864       strategyObj.$el = params.$el;
865       return strategyObj;
866     });
867   };
868
869   $.extend(Strategy.prototype, {
870     // Public properties
871     // -----------------
872
873     // Required
874     match:      null,
875     replace:    null,
876     search:     null,
877
878     // Optional
879     id:         null,
880     cache:      false,
881     context:    function () { return true; },
882     index:      2,
883     template:   function (obj) { return obj; },
884     idProperty: null
885   });
886
887   $.fn.textcomplete.Strategy = Strategy;
888
889 }(jQuery);
890
891 +function ($) {
892   'use strict';
893
894   var now = Date.now || function () { return new Date().getTime(); };
895
896   // Returns a function, that, as long as it continues to be invoked, will not
897   // be triggered. The function will be called after it stops being called for
898   // `wait` msec.
899   //
900   // This utility function was originally implemented at Underscore.js.
901   var debounce = function (func, wait) {
902     var timeout, args, context, timestamp, result;
903     var later = function () {
904       var last = now() - timestamp;
905       if (last < wait) {
906         timeout = setTimeout(later, wait - last);
907       } else {
908         timeout = null;
909         result = func.apply(context, args);
910         context = args = null;
911       }
912     };
913
914     return function () {
915       context = this;
916       args = arguments;
917       timestamp = now();
918       if (!timeout) {
919         timeout = setTimeout(later, wait);
920       }
921       return result;
922     };
923   };
924
925   function Adapter () {}
926
927   $.extend(Adapter.prototype, {
928     // Public properties
929     // -----------------
930
931     id:        null, // Identity.
932     completer: null, // Completer object which creates it.
933     el:        null, // Textarea element.
934     $el:       null, // jQuery object of the textarea.
935     option:    null,
936
937     // Public methods
938     // --------------
939
940     initialize: function (element, completer, option) {
941       this.el        = element;
942       this.$el       = $(element);
943       this.id        = completer.id + this.constructor.name;
944       this.completer = completer;
945       this.option    = option;
946
947       if (this.option.debounce) {
948         this._onKeyup = debounce(this._onKeyup, this.option.debounce);
949       }
950
951       this._bindEvents();
952     },
953
954     destroy: function () {
955       this.$el.off('.' + this.id); // Remove all event handlers.
956       this.$el = this.el = this.completer = null;
957     },
958
959     // Update the element with the given value and strategy.
960     //
961     // value    - The selected object. It is one of the item of the array
962     //            which was callbacked from the search function.
963     // strategy - The Strategy associated with the selected value.
964     select: function (/* value, strategy */) {
965       throw new Error('Not implemented');
966     },
967
968     // Returns the caret's relative coordinates from body's left top corner.
969     getCaretPosition: function () {
970       var position = this._getCaretRelativePosition();
971       var offset = this.$el.offset();
972
973       // Calculate the left top corner of `this.option.appendTo` element.
974       var $parent = this.option.appendTo;
975       if ($parent) {
976          if (!($parent instanceof $)) { $parent = $($parent); }
977          var parentOffset = $parent.offsetParent().offset();
978          offset.top -= parentOffset.top;
979          offset.left -= parentOffset.left;
980       }
981
982       position.top += offset.top;
983       position.left += offset.left;
984       return position;
985     },
986
987     // Focus on the element.
988     focus: function () {
989       this.$el.focus();
990     },
991
992     // Private methods
993     // ---------------
994
995     _bindEvents: function () {
996       this.$el.on('keyup.' + this.id, $.proxy(this._onKeyup, this));
997     },
998
999     _onKeyup: function (e) {
1000       if (this._skipSearch(e)) { return; }
1001       this.completer.trigger(this.getTextFromHeadToCaret(), true);
1002     },
1003
1004     // Suppress searching if it returns true.
1005     _skipSearch: function (clickEvent) {
1006       switch (clickEvent.keyCode) {
1007         case 9:  // TAB
1008         case 13: // ENTER
1009         case 40: // DOWN
1010         case 38: // UP
1011           return true;
1012       }
1013       if (clickEvent.ctrlKey) switch (clickEvent.keyCode) {
1014         case 78: // Ctrl-N
1015         case 80: // Ctrl-P
1016           return true;
1017       }
1018     }
1019   });
1020
1021   $.fn.textcomplete.Adapter = Adapter;
1022 }(jQuery);
1023
1024 +function ($) {
1025   'use strict';
1026
1027   // Textarea adapter
1028   // ================
1029   //
1030   // Managing a textarea. It doesn't know a Dropdown.
1031   function Textarea(element, completer, option) {
1032     this.initialize(element, completer, option);
1033   }
1034
1035   $.extend(Textarea.prototype, $.fn.textcomplete.Adapter.prototype, {
1036     // Public methods
1037     // --------------
1038
1039     // Update the textarea with the given value and strategy.
1040     select: function (value, strategy, e) {
1041       var pre = this.getTextFromHeadToCaret();
1042       var post = this.el.value.substring(this.el.selectionEnd);
1043       var newSubstr = strategy.replace(value, e);
1044       if (typeof newSubstr !== 'undefined') {
1045         if ($.isArray(newSubstr)) {
1046           post = newSubstr[1] + post;
1047           newSubstr = newSubstr[0];
1048         }
1049         pre = pre.replace(strategy.match, newSubstr);
1050         this.$el.val(pre + post);
1051         this.el.selectionStart = this.el.selectionEnd = pre.length;
1052       }
1053     },
1054
1055     getTextFromHeadToCaret: function () {
1056       return this.el.value.substring(0, this.el.selectionEnd);
1057     },
1058
1059     // Private methods
1060     // ---------------
1061
1062     _getCaretRelativePosition: function () {
1063       var p = $.fn.textcomplete.getCaretCoordinates(this.el, this.el.selectionStart);
1064       return {
1065         top: p.top + this._calculateLineHeight() - this.$el.scrollTop(),
1066         left: p.left - this.$el.scrollLeft()
1067       };
1068     },
1069
1070     _calculateLineHeight: function () {
1071       var lineHeight = parseInt(this.$el.css('line-height'), 10);
1072       if (isNaN(lineHeight)) {
1073         // http://stackoverflow.com/a/4515470/1297336
1074         var parentNode = this.el.parentNode;
1075         var temp = document.createElement(this.el.nodeName);
1076         var style = this.el.style;
1077         temp.setAttribute(
1078           'style',
1079           'margin:0px;padding:0px;font-family:' + style.fontFamily + ';font-size:' + style.fontSize
1080         );
1081         temp.innerHTML = 'test';
1082         parentNode.appendChild(temp);
1083         lineHeight = temp.clientHeight;
1084         parentNode.removeChild(temp);
1085       }
1086       return lineHeight;
1087     }
1088   });
1089
1090   $.fn.textcomplete.Textarea = Textarea;
1091 }(jQuery);
1092
1093 +function ($) {
1094   'use strict';
1095
1096   var sentinelChar = '吶';
1097
1098   function IETextarea(element, completer, option) {
1099     this.initialize(element, completer, option);
1100     $('<span>' + sentinelChar + '</span>').css({
1101       position: 'absolute',
1102       top: -9999,
1103       left: -9999
1104     }).insertBefore(element);
1105   }
1106
1107   $.extend(IETextarea.prototype, $.fn.textcomplete.Textarea.prototype, {
1108     // Public methods
1109     // --------------
1110
1111     select: function (value, strategy, e) {
1112       var pre = this.getTextFromHeadToCaret();
1113       var post = this.el.value.substring(pre.length);
1114       var newSubstr = strategy.replace(value, e);
1115       if (typeof newSubstr !== 'undefined') {
1116         if ($.isArray(newSubstr)) {
1117           post = newSubstr[1] + post;
1118           newSubstr = newSubstr[0];
1119         }
1120         pre = pre.replace(strategy.match, newSubstr);
1121         this.$el.val(pre + post);
1122         this.el.focus();
1123         var range = this.el.createTextRange();
1124         range.collapse(true);
1125         range.moveEnd('character', pre.length);
1126         range.moveStart('character', pre.length);
1127         range.select();
1128       }
1129     },
1130
1131     getTextFromHeadToCaret: function () {
1132       this.el.focus();
1133       var range = document.selection.createRange();
1134       range.moveStart('character', -this.el.value.length);
1135       var arr = range.text.split(sentinelChar)
1136       return arr.length === 1 ? arr[0] : arr[1];
1137     }
1138   });
1139
1140   $.fn.textcomplete.IETextarea = IETextarea;
1141 }(jQuery);
1142
1143 // NOTE: TextComplete plugin has contenteditable support but it does not work
1144 //       fine especially on old IEs.
1145 //       Any pull requests are REALLY welcome.
1146
1147 +function ($) {
1148   'use strict';
1149
1150   // ContentEditable adapter
1151   // =======================
1152   //
1153   // Adapter for contenteditable elements.
1154   function ContentEditable (element, completer, option) {
1155     this.initialize(element, completer, option);
1156   }
1157
1158   $.extend(ContentEditable.prototype, $.fn.textcomplete.Adapter.prototype, {
1159     // Public methods
1160     // --------------
1161
1162     // Update the content with the given value and strategy.
1163     // When an dropdown item is selected, it is executed.
1164     select: function (value, strategy, e) {
1165       var pre = this.getTextFromHeadToCaret();
1166       var sel = window.getSelection()
1167       var range = sel.getRangeAt(0);
1168       var selection = range.cloneRange();
1169       selection.selectNodeContents(range.startContainer);
1170       var content = selection.toString();
1171       var post = content.substring(range.startOffset);
1172       var newSubstr = strategy.replace(value, e);
1173       if (typeof newSubstr !== 'undefined') {
1174         if ($.isArray(newSubstr)) {
1175           post = newSubstr[1] + post;
1176           newSubstr = newSubstr[0];
1177         }
1178         pre = pre.replace(strategy.match, newSubstr);
1179         range.selectNodeContents(range.startContainer);
1180         range.deleteContents();
1181         
1182         // create temporary elements
1183         var preWrapper = document.createElement("div");
1184         preWrapper.innerHTML = pre;
1185         var postWrapper = document.createElement("div");
1186         postWrapper.innerHTML = post;
1187         
1188         // create the fragment thats inserted
1189         var fragment = document.createDocumentFragment();
1190         var childNode;
1191         var lastOfPre;
1192         while (childNode = preWrapper.firstChild) {
1193                 lastOfPre = fragment.appendChild(childNode);
1194         }
1195         while (childNode = postWrapper.firstChild) {
1196                 fragment.appendChild(childNode);
1197         }
1198         
1199         // insert the fragment & jump behind the last node in "pre"
1200         range.insertNode(fragment);
1201         range.setStartAfter(lastOfPre);
1202         
1203         range.collapse(true);
1204         sel.removeAllRanges();
1205         sel.addRange(range);
1206       }
1207     },
1208
1209     // Private methods
1210     // ---------------
1211
1212     // Returns the caret's relative position from the contenteditable's
1213     // left top corner.
1214     //
1215     // Examples
1216     //
1217     //   this._getCaretRelativePosition()
1218     //   //=> { top: 18, left: 200, lineHeight: 16 }
1219     //
1220     // Dropdown's position will be decided using the result.
1221     _getCaretRelativePosition: function () {
1222       var range = window.getSelection().getRangeAt(0).cloneRange();
1223       var node = document.createElement('span');
1224       range.insertNode(node);
1225       range.selectNodeContents(node);
1226       range.deleteContents();
1227       var $node = $(node);
1228       var position = $node.offset();
1229       position.left -= this.$el.offset().left;
1230       position.top += $node.height() - this.$el.offset().top;
1231       position.lineHeight = $node.height();
1232       $node.remove();
1233       return position;
1234     },
1235
1236     // Returns the string between the first character and the caret.
1237     // Completer will be triggered with the result for start autocompleting.
1238     //
1239     // Example
1240     //
1241     //   // Suppose the html is '<b>hello</b> wor|ld' and | is the caret.
1242     //   this.getTextFromHeadToCaret()
1243     //   // => ' wor'  // not '<b>hello</b> wor'
1244     getTextFromHeadToCaret: function () {
1245       var range = window.getSelection().getRangeAt(0);
1246       var selection = range.cloneRange();
1247       selection.selectNodeContents(range.startContainer);
1248       return selection.toString().substring(0, range.startOffset);
1249     }
1250   });
1251
1252   $.fn.textcomplete.ContentEditable = ContentEditable;
1253 }(jQuery);
1254
1255 // The MIT License (MIT)
1256 // 
1257 // Copyright (c) 2015 Jonathan Ong me@jongleberry.com
1258 // 
1259 // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
1260 // associated documentation files (the "Software"), to deal in the Software without restriction,
1261 // including without limitation the rights to use, copy, modify, merge, publish, distribute,
1262 // sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
1263 // furnished to do so, subject to the following conditions:
1264 // 
1265 // The above copyright notice and this permission notice shall be included in all copies or
1266 // substantial portions of the Software.
1267 // 
1268 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
1269 // NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
1270 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
1271 // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1272 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1273 //
1274 // https://github.com/component/textarea-caret-position
1275
1276 (function ($) {
1277
1278 // The properties that we copy into a mirrored div.
1279 // Note that some browsers, such as Firefox,
1280 // do not concatenate properties, i.e. padding-top, bottom etc. -> padding,
1281 // so we have to do every single property specifically.
1282 var properties = [
1283   'direction',  // RTL support
1284   'boxSizing',
1285   'width',  // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
1286   'height',
1287   'overflowX',
1288   'overflowY',  // copy the scrollbar for IE
1289
1290   'borderTopWidth',
1291   'borderRightWidth',
1292   'borderBottomWidth',
1293   'borderLeftWidth',
1294   'borderStyle',
1295
1296   'paddingTop',
1297   'paddingRight',
1298   'paddingBottom',
1299   'paddingLeft',
1300
1301   // https://developer.mozilla.org/en-US/docs/Web/CSS/font
1302   'fontStyle',
1303   'fontVariant',
1304   'fontWeight',
1305   'fontStretch',
1306   'fontSize',
1307   'fontSizeAdjust',
1308   'lineHeight',
1309   'fontFamily',
1310
1311   'textAlign',
1312   'textTransform',
1313   'textIndent',
1314   'textDecoration',  // might not make a difference, but better be safe
1315
1316   'letterSpacing',
1317   'wordSpacing',
1318
1319   'tabSize',
1320   'MozTabSize'
1321
1322 ];
1323
1324 var isBrowser = (typeof window !== 'undefined');
1325 var isFirefox = (isBrowser && window.mozInnerScreenX != null);
1326
1327 function getCaretCoordinates(element, position, options) {
1328   if(!isBrowser) {
1329     throw new Error('textarea-caret-position#getCaretCoordinates should only be called in a browser');
1330   }
1331
1332   var debug = options && options.debug || false;
1333   if (debug) {
1334     var el = document.querySelector('#input-textarea-caret-position-mirror-div');
1335     if ( el ) { el.parentNode.removeChild(el); }
1336   }
1337
1338   // mirrored div
1339   var div = document.createElement('div');
1340   div.id = 'input-textarea-caret-position-mirror-div';
1341   document.body.appendChild(div);
1342
1343   var style = div.style;
1344   var computed = window.getComputedStyle? getComputedStyle(element) : element.currentStyle;  // currentStyle for IE < 9
1345
1346   // default textarea styles
1347   style.whiteSpace = 'pre-wrap';
1348   if (element.nodeName !== 'INPUT')
1349     style.wordWrap = 'break-word';  // only for textarea-s
1350
1351   // position off-screen
1352   style.position = 'absolute';  // required to return coordinates properly
1353   if (!debug)
1354     style.visibility = 'hidden';  // not 'display: none' because we want rendering
1355
1356   // transfer the element's properties to the div
1357   properties.forEach(function (prop) {
1358     style[prop] = computed[prop];
1359   });
1360
1361   if (isFirefox) {
1362     // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
1363     if (element.scrollHeight > parseInt(computed.height))
1364       style.overflowY = 'scroll';
1365   } else {
1366     style.overflow = 'hidden';  // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
1367   }
1368
1369   div.textContent = element.value.substring(0, position);
1370   // the second special handling for input type="text" vs textarea: spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
1371   if (element.nodeName === 'INPUT')
1372     div.textContent = div.textContent.replace(/\s/g, '\u00a0');
1373
1374   var span = document.createElement('span');
1375   // Wrapping must be replicated *exactly*, including when a long word gets
1376   // onto the next line, with whitespace at the end of the line before (#7).
1377   // The  *only* reliable way to do that is to copy the *entire* rest of the
1378   // textarea's content into the <span> created at the caret position.
1379   // for inputs, just '.' would be enough, but why bother?
1380   span.textContent = element.value.substring(position) || '.';  // || because a completely empty faux span doesn't render at all
1381   div.appendChild(span);
1382
1383   var coordinates = {
1384     top: span.offsetTop + parseInt(computed['borderTopWidth']),
1385     left: span.offsetLeft + parseInt(computed['borderLeftWidth'])
1386   };
1387
1388   if (debug) {
1389     span.style.backgroundColor = '#aaa';
1390   } else {
1391     document.body.removeChild(div);
1392   }
1393
1394   return coordinates;
1395 }
1396
1397 $.fn.textcomplete.getCaretCoordinates = getCaretCoordinates;
1398
1399 }(jQuery));
1400
1401 return jQuery;
1402 }));
1403 // @license-end