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