UNPKG

jquery-textcomplete

Version:

[![npm version](https://badge.fury.io/js/jquery-textcomplete.svg)](http://badge.fury.io/js/jquery-textcomplete) [![Bower version](https://badge.fury.io/bo/jquery-textcomplete.svg)](http://badge.fury.io/bo/jquery-textcomplete) [![Analytics](https://ga-beac

283 lines (252 loc) 9.32 kB
+function ($) { 'use strict'; // Exclusive execution control utility. // // func - The function to be locked. It is executed with a function named // `free` as the first argument. Once it is called, additional // execution are ignored until the free is invoked. Then the last // ignored execution will be replayed immediately. // // Examples // // var lockedFunc = lock(function (free) { // setTimeout(function { free(); }, 1000); // It will be free in 1 sec. // console.log('Hello, world'); // }); // lockedFunc(); // => 'Hello, world' // lockedFunc(); // none // lockedFunc(); // none // // 1 sec past then // // => 'Hello, world' // lockedFunc(); // => 'Hello, world' // lockedFunc(); // none // // Returns a wrapped function. var lock = function (func) { var locked, queuedArgsToReplay; return function () { // Convert arguments into a real array. var args = Array.prototype.slice.call(arguments); if (locked) { // Keep a copy of this argument list to replay later. // OK to overwrite a previous value because we only replay // the last one. queuedArgsToReplay = args; return; } locked = true; var self = this; args.unshift(function replayOrFree() { if (queuedArgsToReplay) { // Other request(s) arrived while we were locked. // Now that the lock is becoming available, replay // the latest such request, then call back here to // unlock (or replay another request that arrived // while this one was in flight). var replayArgs = queuedArgsToReplay; queuedArgsToReplay = undefined; replayArgs.unshift(replayOrFree); func.apply(self, replayArgs); } else { locked = false; } }); func.apply(this, args); }; }; var isString = function (obj) { return Object.prototype.toString.call(obj) === '[object String]'; }; var uniqueId = 0; function Completer(element, option) { this.$el = $(element); this.id = 'textcomplete' + uniqueId++; this.strategies = []; this.views = []; this.option = $.extend({}, Completer._getDefaults(), option); if (!this.$el.is('input[type=text]') && !this.$el.is('input[type=search]') && !this.$el.is('textarea') && !element.isContentEditable && element.contentEditable != 'true') { throw new Error('textcomplete must be called on a Textarea or a ContentEditable.'); } // use ownerDocument to fix iframe / IE issues if (element === element.ownerDocument.activeElement) { // element has already been focused. Initialize view objects immediately. this.initialize() } else { // Initialize view objects lazily. var self = this; this.$el.one('focus.' + this.id, function () { self.initialize(); }); // Special handling for CKEditor: lazy init on instance load if ((!this.option.adapter || this.option.adapter == 'CKEditor') && typeof CKEDITOR != 'undefined' && (this.$el.is('textarea'))) { CKEDITOR.on("instanceReady", function(event) { event.editor.once("focus", function(event2) { // replace the element with the Iframe element and flag it as CKEditor self.$el = $(event.editor.editable().$); if (!self.option.adapter) { self.option.adapter = $.fn.textcomplete['CKEditor']; } self.initialize(); }); }); } } } Completer._getDefaults = function () { if (!Completer.DEFAULTS) { Completer.DEFAULTS = { appendTo: $('body'), className: '', // deprecated option dropdownClassName: 'dropdown-menu textcomplete-dropdown', maxCount: 10, zIndex: '100' }; } return Completer.DEFAULTS; } $.extend(Completer.prototype, { // Public properties // ----------------- id: null, option: null, strategies: null, adapter: null, dropdown: null, $el: null, $iframe: null, // Public methods // -------------- initialize: function () { var element = this.$el.get(0); // check if we are in an iframe // we need to alter positioning logic if using an iframe if (this.$el.prop('ownerDocument') !== document && window.frames.length) { for (var iframeIndex = 0; iframeIndex < window.frames.length; iframeIndex++) { if (this.$el.prop('ownerDocument') === window.frames[iframeIndex].document) { this.$iframe = $(window.frames[iframeIndex].frameElement); break; } } } // Initialize view objects. this.dropdown = new $.fn.textcomplete.Dropdown(element, this, this.option); var Adapter, viewName; if (this.option.adapter) { Adapter = this.option.adapter; } else { if (this.$el.is('textarea') || this.$el.is('input[type=text]') || this.$el.is('input[type=search]')) { viewName = typeof element.selectionEnd === 'number' ? 'Textarea' : 'IETextarea'; } else { viewName = 'ContentEditable'; } Adapter = $.fn.textcomplete[viewName]; } this.adapter = new Adapter(element, this, this.option); }, destroy: function () { this.$el.off('.' + this.id); if (this.adapter) { this.adapter.destroy(); } if (this.dropdown) { this.dropdown.destroy(); } this.$el = this.adapter = this.dropdown = null; }, deactivate: function () { if (this.dropdown) { this.dropdown.deactivate(); } }, // Invoke textcomplete. trigger: function (text, skipUnchangedTerm) { if (!this.dropdown) { this.initialize(); } text != null || (text = this.adapter.getTextFromHeadToCaret()); var searchQuery = this._extractSearchQuery(text); if (searchQuery.length) { var term = searchQuery[1]; // Ignore shift-key, ctrl-key and so on. if (skipUnchangedTerm && this._term === term && term !== "") { return; } this._term = term; this._search.apply(this, searchQuery); } else { this._term = null; this.dropdown.deactivate(); } }, fire: function (eventName) { var args = Array.prototype.slice.call(arguments, 1); this.$el.trigger(eventName, args); return this; }, register: function (strategies) { Array.prototype.push.apply(this.strategies, strategies); }, // Insert the value into adapter view. It is called when the dropdown is clicked // or selected. // // value - The selected element of the array callbacked from search func. // strategy - The Strategy object. // e - Click or keydown event object. select: function (value, strategy, e) { this._term = null; this.adapter.select(value, strategy, e); this.fire('change').fire('textComplete:select', value, strategy); this.adapter.focus(); }, // Private properties // ------------------ _clearAtNext: true, _term: null, // Private methods // --------------- // Parse the given text and extract the first matching strategy. // // Returns an array including the strategy, the query term and the match // object if the text matches an strategy; otherwise returns an empty array. _extractSearchQuery: function (text) { for (var i = 0; i < this.strategies.length; i++) { var strategy = this.strategies[i]; var context = strategy.context(text); if (context || context === '') { var matchRegexp = $.isFunction(strategy.match) ? strategy.match(text) : strategy.match; if (isString(context)) { text = context; } var match = text.match(matchRegexp); if (match) { return [strategy, match[strategy.index], match]; } } } return [] }, // Call the search method of selected strategy.. _search: lock(function (free, strategy, term, match) { var self = this; strategy.search(term, function (data, stillSearching) { if (!self.dropdown.shown) { self.dropdown.activate(); } if (self._clearAtNext) { // The first callback in the current lock. self.dropdown.clear(); self._clearAtNext = false; } self.dropdown.setPosition(self.adapter.getCaretPosition()); self.dropdown.render(self._zip(data, strategy, term)); if (!stillSearching) { // The last callback in the current lock. free(); self._clearAtNext = true; // Call dropdown.clear at the next time. } }, match); }), // Build a parameter for Dropdown#render. // // Examples // // this._zip(['a', 'b'], 's'); // //=> [{ value: 'a', strategy: 's' }, { value: 'b', strategy: 's' }] _zip: function (data, strategy, term) { return $.map(data, function (value) { return { value: value, strategy: strategy, term: term }; }); } }); $.fn.textcomplete.Completer = Completer; }(jQuery);