4 // Exclusive execution control utility.
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.
13 // var lockedFunc = lock(function (free) {
14 // setTimeout(function { free(); }, 1000); // It will be free in 1 sec.
15 // console.log('Hello, world');
17 // lockedFunc(); // => 'Hello, world'
18 // lockedFunc(); // none
19 // lockedFunc(); // none
21 // // => 'Hello, world'
22 // lockedFunc(); // => 'Hello, world'
23 // lockedFunc(); // none
25 // Returns a wrapped function.
26 var lock = function (func
) {
27 var locked
, queuedArgsToReplay
;
30 // Convert arguments into a real array.
31 var args
= Array
.prototype.slice
.call(arguments
);
33 // Keep a copy of this argument list to replay later.
34 // OK to overwrite a previous value because we only replay
36 queuedArgsToReplay
= args
;
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
);
56 func
.apply(this, args
);
60 var isString = function (obj
) {
61 return Object
.prototype.toString
.call(obj
) === '[object String]';
66 function Completer(element
, option
) {
67 this.$el
= $(element
);
68 this.id
= 'textcomplete' + uniqueId
++;
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.
82 // Initialize view objects lazily.
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'];
102 Completer
.defaults
= {
104 className
: '', // deprecated option
105 dropdownClassName
: 'dropdown-menu textcomplete-dropdown',
111 $.extend(Completer
.prototype, {
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
);
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
;
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';
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
);
160 this.adapter
.destroy();
163 this.dropdown
.destroy();
165 this.$el
= this.adapter
= this.dropdown
= null;
168 deactivate: function () {
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; }
184 this._search
.apply(this, searchQuery
);
187 this.dropdown
.deactivate();
191 fire: function (eventName
) {
192 var args
= Array
.prototype.slice
.call(arguments
, 1);
193 this.$el
.trigger(eventName
, args
);
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
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
) {
209 this.adapter
.select(value
, strategy
, e
);
210 this.fire('change').fire('textComplete:select', value
, strategy
);
211 this.adapter
.focus();
214 // Private properties
215 // ------------------
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
]; }
241 // Call the search method of selected strategy..
242 _search
: lock(function (free
, strategy
, term
, match
) {
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.
258 self
._clearAtNext
= true; // Call dropdown.clear at the next time.
263 // Build a parameter for Dropdown#render.
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
;