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 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.innerHe
682ight(); 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;
1078font-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 [email protected] 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. 1281var 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 1323var isBrowser = (typeof window !== 'undefined'); 1324var isFirefox = (isBrowser && window.mozInnerScreenX != null); 1325 1326function 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 1400return jQuery; 1401}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.