Import source for “1.7.1” from upstream tarball.
[debian_jquery-textcomplete.git] / src / completer.js
blob848a6477b9eef8bef04230e5e84fdb39334b18d6
1 +function ($) {
2 'use strict';
4 // Exclusive execution control utility.
5 //
6 // func - The function to be locked. It is executed with a function named
7 // `free` as the first argument. Once it is called, additional
8 // execution are ignored until the free is invoked. Then the last
9 // ignored execution will be replayed immediately.
11 // Examples
13 // var lockedFunc = lock(function (free) {
14 // setTimeout(function { free(); }, 1000); // It will be free in 1 sec.
15 // console.log('Hello, world');
16 // });
17 // lockedFunc(); // => 'Hello, world'
18 // lockedFunc(); // none
19 // lockedFunc(); // none
20 // // 1 sec past then
21 // // => 'Hello, world'
22 // lockedFunc(); // => 'Hello, world'
23 // lockedFunc(); // none
25 // Returns a wrapped function.
26 var lock = function (func) {
27 var locked, queuedArgsToReplay;
29 return function () {
30 // Convert arguments into a real array.
31 var args = Array.prototype.slice.call(arguments);
32 if (locked) {
33 // Keep a copy of this argument list to replay later.
34 // OK to overwrite a previous value because we only replay
35 // the last one.
36 queuedArgsToReplay = args;
37 return;
39 locked = true;
40 var self = this;
41 args.unshift(function replayOrFree() {
42 if (queuedArgsToReplay) {
43 // Other request(s) arrived while we were locked.
44 // Now that the lock is becoming available, replay
45 // the latest such request, then call back here to
46 // unlock (or replay another request that arrived
47 // while this one was in flight).
48 var replayArgs = queuedArgsToReplay;
49 queuedArgsToReplay = undefined;
50 replayArgs.unshift(replayOrFree);
51 func.apply(self, replayArgs);
52 } else {
53 locked = false;
55 });
56 func.apply(this, args);
60 var isString = function (obj) {
61 return Object.prototype.toString.call(obj) === '[object String]';
64 var uniqueId = 0;
66 function Completer(element, option) {
67 this.$el = $(element);
68 this.id = 'textcomplete' + uniqueId++;
69 this.strategies = [];
70 this.views = [];
71 this.option = $.extend({}, Completer.defaults, option);
73 if (!this.$el.is('input[type=text]') && !this.$el.is('input[type=search]') && !this.$el.is('textarea') && !element.isContentEditable && element.contentEditable != 'true') {
74 throw new Error('textcomplete must be called on a Textarea or a ContentEditable.');
77 // use ownerDocument to fix iframe / IE issues
78 if (element === element.ownerDocument.activeElement) {
79 // element has already been focused. Initialize view objects immediately.
80 this.initialize()
81 } else {
82 // Initialize view objects lazily.
83 var self = this;
84 this.$el.one('focus.' + this.id, function () { self.initialize(); });
86 // Special handling for CKEditor: lazy init on instance load
87 if ((!this.option.adapter || this.option.adapter == 'CKEditor') && typeof CKEDITOR != 'undefined' && (this.$el.is('textarea'))) {
88 CKEDITOR.on("instanceReady", function(event) {
89 event.editor.once("focus", function(event2) {
90 // replace the element with the Iframe element and flag it as CKEditor
91 self.$el = $(event.editor.editable().$);
92 if (!self.option.adapter) {
93 self.option.adapter = $.fn.textcomplete['CKEditor'];
95 self.initialize();
96 });
97 });
102 Completer.defaults = {
103 appendTo: 'body',
104 className: '', // deprecated option
105 dropdownClassName: 'dropdown-menu textcomplete-dropdown',
106 maxCount: 10,
107 zIndex: '100',
108 rightEdgeOffset: 30
111 $.extend(Completer.prototype, {
112 // Public properties
113 // -----------------
115 id: null,
116 option: null,
117 strategies: null,
118 adapter: null,
119 dropdown: null,
120 $el: null,
121 $iframe: null,
123 // Public methods
124 // --------------
126 initialize: function () {
127 var element = this.$el.get(0);
129 // check if we are in an iframe
130 // we need to alter positioning logic if using an iframe
131 if (this.$el.prop('ownerDocument') !== document && window.frames.length) {
132 for (var iframeIndex = 0; iframeIndex < window.frames.length; iframeIndex++) {
133 if (this.$el.prop('ownerDocument') === window.frames[iframeIndex].document) {
134 this.$iframe = $(window.frames[iframeIndex].frameElement);
135 break;
141 // Initialize view objects.
142 this.dropdown = new $.fn.textcomplete.Dropdown(element, this, this.option);
143 var Adapter, viewName;
144 if (this.option.adapter) {
145 Adapter = this.option.adapter;
146 } else {
147 if (this.$el.is('textarea') || this.$el.is('input[type=text]') || this.$el.is('input[type=search]')) {
148 viewName = typeof element.selectionEnd === 'number' ? 'Textarea' : 'IETextarea';
149 } else {
150 viewName = 'ContentEditable';
152 Adapter = $.fn.textcomplete[viewName];
154 this.adapter = new Adapter(element, this, this.option);
157 destroy: function () {
158 this.$el.off('.' + this.id);
159 if (this.adapter) {
160 this.adapter.destroy();
162 if (this.dropdown) {
163 this.dropdown.destroy();
165 this.$el = this.adapter = this.dropdown = null;
168 deactivate: function () {
169 if (this.dropdown) {
170 this.dropdown.deactivate();
174 // Invoke textcomplete.
175 trigger: function (text, skipUnchangedTerm) {
176 if (!this.dropdown) { this.initialize(); }
177 text != null || (text = this.adapter.getTextFromHeadToCaret());
178 var searchQuery = this._extractSearchQuery(text);
179 if (searchQuery.length) {
180 var term = searchQuery[1];
181 // Ignore shift-key, ctrl-key and so on.
182 if (skipUnchangedTerm && this._term === term && term !== "") { return; }
183 this._term = term;
184 this._search.apply(this, searchQuery);
185 } else {
186 this._term = null;
187 this.dropdown.deactivate();
191 fire: function (eventName) {
192 var args = Array.prototype.slice.call(arguments, 1);
193 this.$el.trigger(eventName, args);
194 return this;
197 register: function (strategies) {
198 Array.prototype.push.apply(this.strategies, strategies);
201 // Insert the value into adapter view. It is called when the dropdown is clicked
202 // or selected.
204 // value - The selected element of the array callbacked from search func.
205 // strategy - The Strategy object.
206 // e - Click or keydown event object.
207 select: function (value, strategy, e) {
208 this._term = null;
209 this.adapter.select(value, strategy, e);
210 this.fire('change').fire('textComplete:select', value, strategy);
211 this.adapter.focus();
214 // Private properties
215 // ------------------
217 _clearAtNext: true,
218 _term: null,
220 // Private methods
221 // ---------------
223 // Parse the given text and extract the first matching strategy.
225 // Returns an array including the strategy, the query term and the match
226 // object if the text matches an strategy; otherwise returns an empty array.
227 _extractSearchQuery: function (text) {
228 for (var i = 0; i < this.strategies.length; i++) {
229 var strategy = this.strategies[i];
230 var context = strategy.context(text);
231 if (context || context === '') {
232 var matchRegexp = $.isFunction(strategy.match) ? strategy.match(text) : strategy.match;
233 if (isString(context)) { text = context; }
234 var match = text.match(matchRegexp);
235 if (match) { return [strategy, match[strategy.index], match]; }
238 return []
241 // Call the search method of selected strategy..
242 _search: lock(function (free, strategy, term, match) {
243 var self = this;
244 strategy.search(term, function (data, stillSearching) {
245 if (!self.dropdown.shown) {
246 self.dropdown.activate();
248 if (self._clearAtNext) {
249 // The first callback in the current lock.
250 self.dropdown.clear();
251 self._clearAtNext = false;
253 self.dropdown.setPosition(self.adapter.getCaretPosition());
254 self.dropdown.render(self._zip(data, strategy, term));
255 if (!stillSearching) {
256 // The last callback in the current lock.
257 free();
258 self._clearAtNext = true; // Call dropdown.clear at the next time.
260 }, match);
263 // Build a parameter for Dropdown#render.
265 // Examples
267 // this._zip(['a', 'b'], 's');
268 // //=> [{ value: 'a', strategy: 's' }, { value: 'b', strategy: 's' }]
269 _zip: function (data, strategy, term) {
270 return $.map(data, function (value) {
271 return { value: value, strategy: strategy, term: term };
276 $.fn.textcomplete.Completer = Completer;
277 }(jQuery);