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