1 // @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat
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($);
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
23 if (typeof jQuery === 'undefined') {
24 throw new Error('jQuery.textcomplete requires jQuery');
30 var warn = function (message) {
31 if (console.warn) { console.warn(message); }
36 $.fn.textcomplete = function (strategies, option) {
37 var args = Array.prototype.slice.call(arguments);
38 return this.each(function () {
41 var completer = $this.data('textComplete');
43 option || (option = {});
44 option._oid = id++; // unique object id
45 completer = new $.fn.textcomplete.Completer(this, option);
46 $this.data('textComplete', completer);
48 if (typeof strategies === 'string') {
49 if (!completer) return;
51 completer[strategies].apply(completer, args);
52 if (strategies === 'destroy') {
53 $this.removeData('textComplete');
56 // For backward compatibility.
57 // TODO: Remove at v0.4
58 $.each(strategies, function (obj) {
59 $.each(['header', 'footer', 'placement', 'maxCount'], function (name) {
61 completer.option[name] = obj[name];
62 warn(name + 'as a strategy param is deprecated. Use option.');
67 completer.register($.fn.textcomplete.Strategy.parse(strategies, {
80 // Exclusive execution control utility.
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.
89 // var lockedFunc = lock(function (free) {
90 // setTimeout(function { free(); }, 1000); // It will be free in 1 sec.
91 // console.log('Hello, world');
93 // lockedFunc(); // => 'Hello, world'
94 // lockedFunc(); // none
95 // lockedFunc(); // none
97 // // => 'Hello, world'
98 // lockedFunc(); // => 'Hello, world'
99 // lockedFunc(); // none
101 // Returns a wrapped function.
102 var lock = function (func) {
103 var locked, queuedArgsToReplay;
106 // Convert arguments into a real array.
107 var args = Array.prototype.slice.call(arguments);
109 // Keep a copy of this argument list to replay later.
110 // OK to overwrite a previous value because we only replay
112 queuedArgsToReplay = args;
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);
132 func.apply(this, args);
136 var isString = function (obj) {
137 return Object.prototype.toString.call(obj) === '[object String]';
140 var isFunction = function (obj) {
141 return Object.prototype.toString.call(obj) === '[object Function]';
146 function Completer(element, option) {
147 this.$el = $(element);
148 this.id = 'textcomplete' + uniqueId++;
149 this.strategies = [];
151 this.option = $.extend({}, Completer._getDefaults(), option);
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.');
157 if (element === document.activeElement) {
158 // element has already been focused. Initialize view objects immediately.
161 // Initialize view objects lazily.
163 this.$el.one('focus.' + this.id, function () { self.initialize(); });
167 Completer._getDefaults = function () {
168 if (!Completer.DEFAULTS) {
169 Completer.DEFAULTS = {
175 return Completer.DEFAULTS;
178 $.extend(Completer.prototype, {
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;
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';
203 viewName = 'ContentEditable';
205 Adapter = $.fn.textcomplete[viewName];
207 this.adapter = new Adapter(element, this, this.option);
210 destroy: function () {
211 this.$el.off('.' + this.id);
213 this.adapter.destroy();
216 this.dropdown.destroy();
218 this.$el = this.adapter = this.dropdown = null;
221 deactivate: function () {
223 this.dropdown.deactivate();
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; }
237 this._search.apply(this, searchQuery);
240 this.dropdown.deactivate();
244 fire: function (eventName) {
245 var args = Array.prototype.slice.call(arguments, 1);
246 this.$el.trigger(eventName, args);
250 register: function (strategies) {
251 Array.prototype.push.apply(this.strategies, strategies);
254 // Insert the value into adapter view. It is called when the dropdown is clicked
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) {
262 this.adapter.select(value, strategy, e);
263 this.fire('change').fire('textComplete:select', value, strategy);
264 this.adapter.focus();
267 // Private properties
268 // ------------------
276 // Parse the given text and extract the first matching strategy.
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]; }
294 // Call the search method of selected strategy..
295 _search: lock(function (free, strategy, term, match) {
297 strategy.search(term, function (data, stillSearching) {
298 if (!self.dropdown.shown) {
299 self.dropdown.activate();
301 if (self._clearAtNext) {
302 // The first callback in the current lock.
303 self.dropdown.clear();
304 self._clearAtNext = false;
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.
311 self._clearAtNext = true; // Call dropdown.clear at the next time.
316 // Build a parameter for Dropdown#render.
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 };
329 $.fn.textcomplete.Completer = Completer;
335 var $window = $(window);
337 var include = function (zippedData, datum) {
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;
344 if (elem.value[idProperty] === datum.value[idProperty]) return true;
346 if (elem.value === datum.value) return true;
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(); }
373 // Construct Dropdown object.
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;
384 // Override setPosition method.
385 if (option.listPosition) { this.setPosition = option.listPosition; }
386 if (option.height) { this.$el.height(option.height); }
388 $.each(['maxCount', 'placement', 'footer', 'header', 'noResultsMessage', 'className'], function (_i, name) {
389 if (option[name] != null) { self[name] = option[name]; }
391 this._bindEvents(element);
392 dropdownViews[this.id] = this;
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)
408 position: 'absolute',
409 zIndex: option.zIndex
416 $.extend(Dropdown.prototype, {
420 $el: null, // jQuery object of ul.dropdown-menu element.
421 $inputEl: null, // jQuery object of target textarea.
429 data: [], // Shown zipped data.
435 destroy: function () {
436 // Don't remove $el because it may be shared by several textcompletes.
439 this.$el.off('.' + this.id);
440 this.$inputEl.off('.' + this.id);
443 this.$el = this.$inputEl = this.completer = null;
444 delete dropdownViews[this.id]
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;
453 this.$el.attr('data-strategy', strategy.id);
455 this.$el.removeAttr('data-strategy');
457 this._renderHeader(unzippedData);
458 this._renderFooter(unzippedData);
460 this._renderContents(contentsHtml);
463 this._activateIndexedItem();
466 } else if (this.noResultsMessage) {
467 this._renderNoResultsMessage(unzippedData);
468 } else if (this.shown) {
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
482 if($(this).css('position') === 'fixed') {
483 pos.top -= $window.scrollTop();
484 pos.left -= $window.scrollLeft();
489 this.$el.css(this._applyPlacement(pos));
490 this.$el.css({ position: position }); // Update positioning
499 this._$header = this._$footer = this._$noResultsMessage = null;
502 activate: function () {
506 if (this.className) { this.$el.addClass(this.className); }
507 this.completer.fire('textComplete:show');
513 deactivate: function () {
516 if (this.className) { this.$el.removeClass(this.className); }
517 this.completer.fire('textComplete:hide');
524 return e.keyCode === 38 || (e.ctrlKey && e.keyCode === 80); // UP, Ctrl-P
527 isDown: function (e) {
528 return e.keyCode === 40 || (e.ctrlKey && e.keyCode === 78); // DOWN, Ctrl-N
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
536 isPageup: function (e) {
537 return e.keyCode === 33; // PAGEUP
540 isPagedown: function (e) {
541 return e.keyCode === 34; // PAGEDOWN
544 isEscape: function (e) {
545 return e.keyCode === 27; // ESCAPE
548 // Private properties
549 // ------------------
551 _data: null, // Currently shown zipped data.
554 _$noResultsMessage: null,
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));
567 _onClick: function (e) {
568 var $el = $(e.target);
570 e.originalEvent.keepTextCompleteDropdown = this.id;
571 if (!$el.hasClass('textcomplete-item')) {
572 $el = $el.closest('.textcomplete-item');
574 var datum = this.data[parseInt($el.data('index'), 10)];
575 this.completer.select(datum.value, datum.strategy, e);
577 // Deactive at next tick to allow other event handlers to know whether
578 // the dropdown has been shown or not.
579 setTimeout(function () {
581 if (e.type === 'touchstart') {
582 self.$inputEl.focus();
587 // Activate hovered item.
588 _onMouseover: function (e) {
589 var $el = $(e.target);
591 if (!$el.hasClass('textcomplete-item')) {
592 $el = $el.closest('.textcomplete-item');
594 this._index = parseInt($el.data('index'), 10);
595 this._activateIndexedItem();
598 _onKeydown: function (e) {
599 if (!this.shown) { return; }
603 if ($.isFunction(this.option.onKeydown)) {
604 command = this.option.onKeydown(e, commands);
607 if (command == null) {
608 command = this._defaultKeydown(e);
612 case commands.KEY_UP:
616 case commands.KEY_DOWN:
620 case commands.KEY_ENTER:
624 case commands.KEY_PAGEUP:
628 case commands.KEY_PAGEDOWN:
632 case commands.KEY_ESCAPE:
639 _defaultKeydown: function (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;
656 if (this._index === 0) {
657 this._index = this.data.length - 1;
661 this._activateIndexedItem();
666 if (this._index === this.data.length - 1) {
671 this._activateIndexedItem();
675 _enter: function (e) {
676 var datum = this.data[parseInt(this._getActiveElement().data('index'), 10)];
677 this.completer.select(datum.value, datum.strategy, e);
681 _pageup: function () {
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) {
690 this._index = target;
691 this._activateIndexedItem();
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) {
704 this._index = target;
705 this._activateIndexedItem();
709 _activateIndexedItem: function () {
710 this.$el.find('.textcomplete-item.active').removeClass('active');
711 this._getActiveElement().addClass('active');
714 _getActiveElement: function () {
715 return this.$el.children('.textcomplete-item:nth(' + this._index + ')');
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);
731 _buildContents: function (zippedData) {
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);
747 _renderHeader: function (unzippedData) {
749 if (!this._$header) {
750 this._$header = $('<li class="textcomplete-header"></li>').prependTo(this.$el);
752 var html = $.isFunction(this.header) ? this.header(unzippedData) : this.header;
753 this._$header.html(html);
757 _renderFooter: function (unzippedData) {
759 if (!this._$footer) {
760 this._$footer = $('<li class="textcomplete-footer"></li>').appendTo(this.$el);
762 var html = $.isFunction(this.footer) ? this.footer(unzippedData) : this.footer;
763 this._$footer.html(html);
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);
772 var html = $.isFunction(this.noResultsMessage) ? this.noResultsMessage(unzippedData) : this.noResultsMessage;
773 this._$noResultsMessage.html(html);
777 _renderContents: function (html) {
779 this._$footer.before(html);
781 this.$el.append(html);
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});
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; }
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.
816 bottom: this.$el.parent().height() - position.top + position.lineHeight,
820 position.bottom = 'auto';
821 delete position.lineHeight;
823 if (this.placement.indexOf('absleft') !== -1) {
825 } else if (this.placement.indexOf('absright') !== -1) {
827 position.left = 'auto';
833 $.fn.textcomplete.Dropdown = Dropdown;
834 $.extend($.fn.textcomplete, commands);
840 // Memoize a search function.
841 var memoize = function (func) {
843 return function (term, callback) {
845 callback(memo[term]);
847 func.call(this, term, function (data) {
848 memo[term] = (memo[term] || []).concat(data);
849 callback.apply(null, arguments);
855 function Strategy(options) {
856 $.extend(this, options);
857 if (this.cache) { this.search = memoize(this.search); }
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;
869 $.extend(Strategy.prototype, {
881 context: function () { return true; },
883 template: function (obj) { return obj; },
887 $.fn.textcomplete.Strategy = Strategy;
894 var now = Date.now || function () { return new Date().getTime(); };
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
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;
906 timeout = setTimeout(later, wait - last);
909 result = func.apply(context, args);
910 context = args = null;
919 timeout = setTimeout(later, wait);
925 function Adapter () {}
927 $.extend(Adapter.prototype, {
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.
940 initialize: function (element, completer, option) {
942 this.$el = $(element);
943 this.id = completer.id + this.constructor.name;
944 this.completer = completer;
945 this.option = option;
947 if (this.option.debounce) {
948 this._onKeyup = debounce(this._onKeyup, this.option.debounce);
954 destroy: function () {
955 this.$el.off('.' + this.id); // Remove all event handlers.
956 this.$el = this.el = this.completer = null;
959 // Update the element with the given value and strategy.
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');
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();
973 // Calculate the left top corner of `this.option.appendTo` element.
974 var $parent = this.option.appendTo;
976 if (!($parent instanceof $)) { $parent = $($parent); }
977 var parentOffset = $parent.offsetParent().offset();
978 offset.top -= parentOffset.top;
979 offset.left -= parentOffset.left;
982 position.top += offset.top;
983 position.left += offset.left;
987 // Focus on the element.
995 _bindEvents: function () {
996 this.$el.on('keyup.' + this.id, $.proxy(this._onKeyup, this));
999 _onKeyup: function (e) {
1000 if (this._skipSearch(e)) { return; }
1001 this.completer.trigger(this.getTextFromHeadToCaret(), true);
1004 // Suppress searching if it returns true.
1005 _skipSearch: function (clickEvent) {
1006 switch (clickEvent.keyCode) {
1013 if (clickEvent.ctrlKey) switch (clickEvent.keyCode) {
1021 $.fn.textcomplete.Adapter = Adapter;
1030 // Managing a textarea. It doesn't know a Dropdown.
1031 function Textarea(element, completer, option) {
1032 this.initialize(element, completer, option);
1035 $.extend(Textarea.prototype, $.fn.textcomplete.Adapter.prototype, {
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];
1049 pre = pre.replace(strategy.match, newSubstr);
1050 this.$el.val(pre + post);
1051 this.el.selectionStart = this.el.selectionEnd = pre.length;
1055 getTextFromHeadToCaret: function () {
1056 return this.el.value.substring(0, this.el.selectionEnd);
1062 _getCaretRelativePosition: function () {
1063 var p = $.fn.textcomplete.getCaretCoordinates(this.el, this.el.selectionStart);
1065 top: p.top + this._calculateLineHeight() - this.$el.scrollTop(),
1066 left: p.left - this.$el.scrollLeft()
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;
1079 'margin:0px;padding:0px;font-family:' + style.fontFamily + ';font-size:' + style.fontSize
1081 temp.innerHTML = 'test';
1082 parentNode.appendChild(temp);
1083 lineHeight = temp.clientHeight;
1084 parentNode.removeChild(temp);
1090 $.fn.textcomplete.Textarea = Textarea;
1096 var sentinelChar = '吶';
1098 function IETextarea(element, completer, option) {
1099 this.initialize(element, completer, option);
1100 $('<span>' + sentinelChar + '</span>').css({
1101 position: 'absolute',
1104 }).insertBefore(element);
1107 $.extend(IETextarea.prototype, $.fn.textcomplete.Textarea.prototype, {
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];
1120 pre = pre.replace(strategy.match, newSubstr);
1121 this.$el.val(pre + post);
1123 var range = this.el.createTextRange();
1124 range.collapse(true);
1125 range.moveEnd('character', pre.length);
1126 range.moveStart('character', pre.length);
1131 getTextFromHeadToCaret: function () {
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];
1140 $.fn.textcomplete.IETextarea = IETextarea;
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.
1150 // ContentEditable adapter
1151 // =======================
1153 // Adapter for contenteditable elements.
1154 function ContentEditable (element, completer, option) {
1155 this.initialize(element, completer, option);
1158 $.extend(ContentEditable.prototype, $.fn.textcomplete.Adapter.prototype, {
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];
1178 pre = pre.replace(strategy.match, newSubstr);
1179 range.selectNodeContents(range.startContainer);
1180 range.deleteContents();
1182 // create temporary elements
1183 var preWrapper = document.createElement("div");
1184 preWrapper.innerHTML = pre;
1185 var postWrapper = document.createElement("div");
1186 postWrapper.innerHTML = post;
1188 // create the fragment thats inserted
1189 var fragment = document.createDocumentFragment();
1192 while (childNode = preWrapper.firstChild) {
1193 lastOfPre = fragment.appendChild(childNode);
1195 while (childNode = postWrapper.firstChild) {
1196 fragment.appendChild(childNode);
1199 // insert the fragment & jump behind the last node in "pre"
1200 range.insertNode(fragment);
1201 range.setStartAfter(lastOfPre);
1203 range.collapse(true);
1204 sel.removeAllRanges();
1205 sel.addRange(range);
1212 // Returns the caret's relative position from the contenteditable's
1217 // this._getCaretRelativePosition()
1218 // //=> { top: 18, left: 200, lineHeight: 16 }
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();
1236 // Returns the string between the first character and the caret.
1237 // Completer will be triggered with the result for start autocompleting.
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);
1252 $.fn.textcomplete.ContentEditable = ContentEditable;
1255 // The MIT License (MIT)
1257 // Copyright (c) 2015 Jonathan Ong me@jongleberry.com
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:
1265 // The above copyright notice and this permission notice shall be included in all copies or
1266 // substantial portions of the Software.
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.
1274 // https://github.com/component/textarea-caret-position
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.
1283 'direction', // RTL support
1285 'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
1288 'overflowY', // copy the scrollbar for IE
1292 'borderBottomWidth',
1301 // https://developer.mozilla.org/en-US/docs/Web/CSS/font
1314 'textDecoration', // might not make a difference, but better be safe
1324 var isBrowser = (typeof window !== 'undefined');
1325 var isFirefox = (isBrowser && window.mozInnerScreenX != null);
1327 function getCaretCoordinates(element, position, options) {
1329 throw new Error('textarea-caret-position#getCaretCoordinates should only be called in a browser');
1332 var debug = options && options.debug || false;
1334 var el = document.querySelector('#input-textarea-caret-position-mirror-div');
1335 if ( el ) { el.parentNode.removeChild(el); }
1339 var div = document.createElement('div');
1340 div.id = 'input-textarea-caret-position-mirror-div';
1341 document.body.appendChild(div);
1343 var style = div.style;
1344 var computed = window.getComputedStyle? getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9
1346 // default textarea styles
1347 style.whiteSpace = 'pre-wrap';
1348 if (element.nodeName !== 'INPUT')
1349 style.wordWrap = 'break-word'; // only for textarea-s
1351 // position off-screen
1352 style.position = 'absolute'; // required to return coordinates properly
1354 style.visibility = 'hidden'; // not 'display: none' because we want rendering
1356 // transfer the element's properties to the div
1357 properties.forEach(function (prop) {
1358 style[prop] = computed[prop];
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';
1366 style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
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');
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);
1384 top: span.offsetTop + parseInt(computed['borderTopWidth']),
1385 left: span.offsetLeft + parseInt(computed['borderLeftWidth'])
1389 span.style.backgroundColor = '#aaa';
1391 document.body.removeChild(div);
1397 $.fn.textcomplete.getCaretCoordinates = getCaretCoordinates;