jquery-textcomplete-rails 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 90c1c9ba599310c8c6d011213a76b99085fe0b4d
4
- data.tar.gz: 439b33e3ae46b0f8280904009fe5db5278078ed7
3
+ metadata.gz: ef7d394462222644001ff74b69c283c785535540
4
+ data.tar.gz: 592fc3192ecfb007a33f0a1c207ed8dc2b6dc4ab
5
5
  SHA512:
6
- metadata.gz: b95b9e7a0131f0166d37ea7100fa99c23f76602c5e9d835f70abbed0a8ed5d4fca4f5dcb23639fc3c9535e777c7296b668dde77dd6882df13d66c8aa33cc5878
7
- data.tar.gz: d6b23c9d0c2d8aff2ff8e2a81407611de8e7e33f911a70d134ab6e199ac266d30e6b426601f16353d5a2f04b2898f712cd54fafef8af1fef07aa84e5ca951956
6
+ metadata.gz: 5f8909adb632b917131fc98e416fc00d314f02a12d8de3aa00fdcaa209b5521329a5975d2f7b1bf439cc8348250b4da6c48d5fb8a4dfc60f81f524ed5eb430a5
7
+ data.tar.gz: ceae6b8c03cae6ac5a66a9aa82aa86eef984f49575db3aae7f160bcc0aa98b44bb1eb848c728ec4d1acbc67ca7882445174395d6dca461e180d8c05d93969c7c
data/README.md CHANGED
@@ -21,14 +21,14 @@ Or install it yourself as:
21
21
  Include required javascripts in you `application.js` file:
22
22
  ```javascript
23
23
  ...
24
- //= require jquery-textcomplete-rails
24
+ //= require jquery-textcomplete
25
25
  ...
26
26
  ```
27
27
 
28
28
  Include required stylesheets into your `application.css` file:
29
- ```
29
+ ```sass
30
30
  ...
31
- //= require jquery-textcomplete-rails
31
+ //= require jquery-textcomplete
32
32
  ...
33
33
  ```
34
34
 
@@ -1,7 +1,7 @@
1
1
  module Jquery
2
2
  module Textcomplete
3
3
  module Rails
4
- VERSION = '0.1.3'
4
+ VERSION = '0.1.4'
5
5
  end
6
6
  end
7
7
  end
@@ -13,5 +13,5 @@
13
13
  #= require jquery
14
14
  #= require jquery_ujs
15
15
  #= require turbolinks
16
- #= require jquery-textcomplete-rails
16
+ #= require jquery-textcomplete
17
17
  #= require_tree .
@@ -9,6 +9,6 @@
9
9
  * compiled file, but it's generally better to create a new file per style scope.
10
10
  *
11
11
  *= require_self
12
- *= require jquery-textcomplete-rails
12
+ *= require jquery-textcomplete
13
13
  *= require_tree .
14
14
  */
@@ -1,70 +1,699 @@
1
1
  /*!
2
- * jQuery.textcomplete.js
2
+ * jQuery.textcomplete
3
3
  *
4
- * Repositiory: https://github.com/yuku-t/jquery-textcomplete
5
- * License: MIT
6
- * Author: Yuku Takahashi
4
+ * Repository: https://github.com/yuku-t/jquery-textcomplete
5
+ * License: MIT (https://github.com/yuku-t/jquery-textcomplete/blob/master/LICENSE)
6
+ * Author: Yuku Takahashi
7
7
  */
8
8
 
9
- ;(function ($) {
9
+ if (typeof jQuery === 'undefined') {
10
+ throw new Error('jQuery.textcomplete requires jQuery');
11
+ }
10
12
 
13
+ +function ($) {
11
14
  'use strict';
12
15
 
13
- /**
14
- * Exclusive execution control utility.
15
- */
16
+ var warn = function (message) {
17
+ if (console.warn) { console.warn(message); }
18
+ };
19
+
20
+ $.fn.textcomplete = function (strategies, option) {
21
+ var args = Array.prototype.slice.call(arguments);
22
+ return this.each(function () {
23
+ var $this = $(this);
24
+ var completer = $this.data('textComplete');
25
+ if (!completer) {
26
+ completer = new $.fn.textcomplete.Completer(this, option || {});
27
+ $this.data('textComplete', completer);
28
+ }
29
+ if (typeof strategies === 'string') {
30
+ if (!completer) return;
31
+ args.shift()
32
+ completer[strategies].apply(completer, args);
33
+ if (strategies === 'destroy') {
34
+ $this.removeData('textComplete');
35
+ }
36
+ } else {
37
+ // For backward compatibility.
38
+ // TODO: Remove at v0.4
39
+ $.each(strategies, function (obj) {
40
+ $.each(['header', 'footer', 'placement', 'maxCount'], function (name) {
41
+ if (obj[name]) {
42
+ completer.option[name] = obj[name];
43
+ warn(name + 'as a strategy param is deprecated. Use option.');
44
+ delete obj[name];
45
+ }
46
+ });
47
+ });
48
+ completer.register($.fn.textcomplete.Strategy.parse(strategies));
49
+ }
50
+ });
51
+ };
52
+
53
+ }(jQuery);
54
+
55
+ +function ($) {
56
+ 'use strict';
57
+
58
+ // Exclusive execution control utility.
59
+ //
60
+ // func - The function to be locked. It is executed with a function named
61
+ // `free` as the first argument. Once it is called, additional
62
+ // execution are ignored until the free is invoked. Then the last
63
+ // ignored execution will be replayed immediately.
64
+ //
65
+ // Examples
66
+ //
67
+ // var lockedFunc = lock(function (free) {
68
+ // setTimeout(function { free(); }, 1000); // It will be free in 1 sec.
69
+ // console.log('Hello, world');
70
+ // });
71
+ // lockedFunc(); // => 'Hello, world'
72
+ // lockedFunc(); // none
73
+ // lockedFunc(); // none
74
+ // // 1 sec past then
75
+ // // => 'Hello, world'
76
+ // lockedFunc(); // => 'Hello, world'
77
+ // lockedFunc(); // none
78
+ //
79
+ // Returns a wrapped function.
16
80
  var lock = function (func) {
17
- var free, locked;
18
- free = function () { locked = false; };
81
+ var locked, queuedArgsToReplay;
82
+
19
83
  return function () {
20
- var args;
21
- if (locked) return;
84
+ // Convert arguments into a real array.
85
+ var args = Array.prototype.slice.call(arguments);
86
+ if (locked) {
87
+ // Keep a copy of this argument list to replay later.
88
+ // OK to overwrite a previous value because we only replay
89
+ // the last one.
90
+ queuedArgsToReplay = args;
91
+ return;
92
+ }
22
93
  locked = true;
23
- args = toArray(arguments);
24
- args.unshift(free);
94
+ var self = this;
95
+ args.unshift(function replayOrFree() {
96
+ if (queuedArgsToReplay) {
97
+ // Other request(s) arrived while we were locked.
98
+ // Now that the lock is becoming available, replay
99
+ // the latest such request, then call back here to
100
+ // unlock (or replay another request that arrived
101
+ // while this one was in flight).
102
+ var replayArgs = queuedArgsToReplay;
103
+ queuedArgsToReplay = undefined;
104
+ replayArgs.unshift(replayOrFree);
105
+ func.apply(self, replayArgs);
106
+ } else {
107
+ locked = false;
108
+ }
109
+ });
25
110
  func.apply(this, args);
26
111
  };
27
112
  };
28
113
 
29
- /**
30
- * Convert arguments into a real array.
31
- */
32
- var toArray = function (args) {
33
- var result;
34
- result = Array.prototype.slice.call(args);
35
- return result;
114
+ var isString = function (obj) {
115
+ return Object.prototype.toString.call(obj) === '[object String]';
36
116
  };
37
117
 
38
- /**
39
- * Get the styles of any element from property names.
40
- */
41
- var getStyles = (function () {
42
- var color;
43
- color = $('<div></div>').css(['color']).color;
44
- if (typeof color !== 'undefined') {
45
- return function ($el, properties) {
46
- return $el.css(properties);
47
- };
48
- } else { // for jQuery 1.8 or below
49
- return function ($el, properties) {
50
- var styles;
51
- styles = {};
52
- $.each(properties, function (i, property) {
53
- styles[property] = $el.css(property);
54
- });
55
- return styles;
118
+ var uniqueId = 0;
119
+
120
+ function Completer(element, option) {
121
+ this.$el = $(element);
122
+ this.id = 'textcomplete' + uniqueId++;
123
+ this.strategies = [];
124
+ this.views = [];
125
+ this.option = $.extend({}, Completer._getDefaults(), option);
126
+
127
+ if (!this.$el.is('input[type=text]') && !this.$el.is('textarea') && !element.isContentEditable && element.contentEditable != 'true') {
128
+ throw new Error('textcomplete must be called on a Textarea or a ContentEditable.');
129
+ }
130
+
131
+ if (element === document.activeElement) {
132
+ // element has already been focused. Initialize view objects immediately.
133
+ this.initialize()
134
+ } else {
135
+ // Initialize view objects lazily.
136
+ var self = this;
137
+ this.$el.one('focus.' + this.id, function () { self.initialize(); });
138
+ }
139
+ }
140
+
141
+ Completer._getDefaults = function () {
142
+ if (!Completer.DEFAULTS) {
143
+ Completer.DEFAULTS = {
144
+ appendTo: $('body'),
145
+ zIndex: '100'
56
146
  };
57
147
  }
58
- })();
59
148
 
60
- /**
61
- * Default template function.
62
- */
63
- var identity = function (obj) { return obj; };
149
+ return Completer.DEFAULTS;
150
+ }
151
+
152
+ $.extend(Completer.prototype, {
153
+ // Public properties
154
+ // -----------------
155
+
156
+ id: null,
157
+ option: null,
158
+ strategies: null,
159
+ adapter: null,
160
+ dropdown: null,
161
+ $el: null,
162
+
163
+ // Public methods
164
+ // --------------
165
+
166
+ initialize: function () {
167
+ var element = this.$el.get(0);
168
+ // Initialize view objects.
169
+ this.dropdown = new $.fn.textcomplete.Dropdown(element, this, this.option);
170
+ var Adapter, viewName;
171
+ if (this.option.adapter) {
172
+ Adapter = this.option.adapter;
173
+ } else {
174
+ if (this.$el.is('textarea') || this.$el.is('input[type=text]')) {
175
+ viewName = typeof element.selectionEnd === 'number' ? 'Textarea' : 'IETextarea';
176
+ } else {
177
+ viewName = 'ContentEditable';
178
+ }
179
+ Adapter = $.fn.textcomplete[viewName];
180
+ }
181
+ this.adapter = new Adapter(element, this, this.option);
182
+ },
183
+
184
+ destroy: function () {
185
+ this.$el.off('.' + this.id);
186
+ if (this.adapter) {
187
+ this.adapter.destroy();
188
+ }
189
+ if (this.dropdown) {
190
+ this.dropdown.destroy();
191
+ }
192
+ this.$el = this.adapter = this.dropdown = null;
193
+ },
194
+
195
+ // Invoke textcomplete.
196
+ trigger: function (text, skipUnchangedTerm) {
197
+ if (!this.dropdown) { this.initialize(); }
198
+ text != null || (text = this.adapter.getTextFromHeadToCaret());
199
+ var searchQuery = this._extractSearchQuery(text);
200
+ if (searchQuery.length) {
201
+ var term = searchQuery[1];
202
+ // Ignore shift-key, ctrl-key and so on.
203
+ if (skipUnchangedTerm && this._term === term) { return; }
204
+ this._term = term;
205
+ this._search.apply(this, searchQuery);
206
+ } else {
207
+ this._term = null;
208
+ this.dropdown.deactivate();
209
+ }
210
+ },
211
+
212
+ fire: function (eventName) {
213
+ var args = Array.prototype.slice.call(arguments, 1);
214
+ this.$el.trigger(eventName, args);
215
+ return this;
216
+ },
217
+
218
+ register: function (strategies) {
219
+ Array.prototype.push.apply(this.strategies, strategies);
220
+ },
221
+
222
+ // Insert the value into adapter view. It is called when the dropdown is clicked
223
+ // or selected.
224
+ //
225
+ // value - The selected element of the array callbacked from search func.
226
+ // strategy - The Strategy object.
227
+ select: function (value, strategy) {
228
+ this.adapter.select(value, strategy);
229
+ this.fire('change').fire('textComplete:select', value, strategy);
230
+ this.adapter.focus();
231
+ },
232
+
233
+ // Private properties
234
+ // ------------------
235
+
236
+ _clearAtNext: true,
237
+ _term: null,
238
+
239
+ // Private methods
240
+ // ---------------
241
+
242
+ // Parse the given text and extract the first matching strategy.
243
+ //
244
+ // Returns an array including the strategy, the query term and the match
245
+ // object if the text matches an strategy; otherwise returns an empty array.
246
+ _extractSearchQuery: function (text) {
247
+ for (var i = 0; i < this.strategies.length; i++) {
248
+ var strategy = this.strategies[i];
249
+ var context = strategy.context(text);
250
+ if (context || context === '') {
251
+ if (isString(context)) { text = context; }
252
+ var match = text.match(strategy.match);
253
+ if (match) { return [strategy, match[strategy.index], match]; }
254
+ }
255
+ }
256
+ return []
257
+ },
258
+
259
+ // Call the search method of selected strategy..
260
+ _search: lock(function (free, strategy, term, match) {
261
+ var self = this;
262
+ strategy.search(term, function (data, stillSearching) {
263
+ if (!self.dropdown.shown) {
264
+ self.dropdown.activate();
265
+ self.dropdown.setPosition(self.adapter.getCaretPosition());
266
+ }
267
+ if (self._clearAtNext) {
268
+ // The first callback in the current lock.
269
+ self.dropdown.clear();
270
+ self._clearAtNext = false;
271
+ }
272
+ self.dropdown.render(self._zip(data, strategy));
273
+ if (!stillSearching) {
274
+ // The last callback in the current lock.
275
+ free();
276
+ self._clearAtNext = true; // Call dropdown.clear at the next time.
277
+ }
278
+ }, match);
279
+ }),
280
+
281
+ // Build a parameter for Dropdown#render.
282
+ //
283
+ // Examples
284
+ //
285
+ // this._zip(['a', 'b'], 's');
286
+ // //=> [{ value: 'a', strategy: 's' }, { value: 'b', strategy: 's' }]
287
+ _zip: function (data, strategy) {
288
+ return $.map(data, function (value) {
289
+ return { value: value, strategy: strategy };
290
+ });
291
+ }
292
+ });
293
+
294
+ $.fn.textcomplete.Completer = Completer;
295
+ }(jQuery);
296
+
297
+ +function ($) {
298
+ 'use strict';
64
299
 
65
- /**
66
- * Memoize a search function.
67
- */
300
+ var include = function (zippedData, datum) {
301
+ var i, elem;
302
+ var idProperty = datum.strategy.idProperty
303
+ for (i = 0; i < zippedData.length; i++) {
304
+ elem = zippedData[i];
305
+ if (elem.strategy !== datum.strategy) continue;
306
+ if (idProperty) {
307
+ if (elem.value[idProperty] === datum.value[idProperty]) return true;
308
+ } else {
309
+ if (elem.value === datum.value) return true;
310
+ }
311
+ }
312
+ return false;
313
+ };
314
+
315
+ var dropdownViews = {};
316
+ $(document).on('click', function (e) {
317
+ var id = e.originalEvent && e.originalEvent.keepTextCompleteDropdown;
318
+ $.each(dropdownViews, function (key, view) {
319
+ if (key !== id) { view.deactivate(); }
320
+ });
321
+ });
322
+
323
+ // Dropdown view
324
+ // =============
325
+
326
+ // Construct Dropdown object.
327
+ //
328
+ // element - Textarea or contenteditable element.
329
+ function Dropdown(element, completer, option) {
330
+ this.$el = Dropdown.findOrCreateElement(option);
331
+ this.completer = completer;
332
+ this.id = completer.id + 'dropdown';
333
+ this._data = []; // zipped data.
334
+ this.$inputEl = $(element);
335
+ this.option = option;
336
+
337
+ // Override setPosition method.
338
+ if (option.listPosition) { this.setPosition = option.listPosition; }
339
+ if (option.height) { this.$el.height(option.height); }
340
+ var self = this;
341
+ $.each(['maxCount', 'placement', 'footer', 'header', 'className'], function (_i, name) {
342
+ if (option[name] != null) { self[name] = option[name]; }
343
+ });
344
+ this._bindEvents(element);
345
+ dropdownViews[this.id] = this;
346
+ }
347
+
348
+ $.extend(Dropdown, {
349
+ // Class methods
350
+ // -------------
351
+
352
+ findOrCreateElement: function (option) {
353
+ var $parent = option.appendTo;
354
+ if (!($parent instanceof $)) { $parent = $($parent); }
355
+ var $el = $parent.children('.dropdown-menu')
356
+ if (!$el.length) {
357
+ $el = $('<ul class="dropdown-menu"></ul>').css({
358
+ display: 'none',
359
+ left: 0,
360
+ position: 'absolute',
361
+ zIndex: option.zIndex
362
+ }).appendTo($parent);
363
+ }
364
+ return $el;
365
+ }
366
+ });
367
+
368
+ $.extend(Dropdown.prototype, {
369
+ // Public properties
370
+ // -----------------
371
+
372
+ $el: null, // jQuery object of ul.dropdown-menu element.
373
+ $inputEl: null, // jQuery object of target textarea.
374
+ completer: null,
375
+ footer: null,
376
+ header: null,
377
+ id: null,
378
+ maxCount: 10,
379
+ placement: '',
380
+ shown: false,
381
+ data: [], // Shown zipped data.
382
+ className: '',
383
+
384
+ // Public methods
385
+ // --------------
386
+
387
+ destroy: function () {
388
+ // Don't remove $el because it may be shared by several textcompletes.
389
+ this.deactivate();
390
+
391
+ this.$el.off('.' + this.id);
392
+ this.$inputEl.off('.' + this.id);
393
+ this.clear();
394
+ this.$el = this.$inputEl = this.completer = null;
395
+ delete dropdownViews[this.id]
396
+ },
397
+
398
+ render: function (zippedData) {
399
+ var contentsHtml = this._buildContents(zippedData);
400
+ var unzippedData = $.map(this.data, function (d) { return d.value; });
401
+ if (this.data.length) {
402
+ this._renderHeader(unzippedData);
403
+ this._renderFooter(unzippedData);
404
+ if (contentsHtml) {
405
+ this._renderContents(contentsHtml);
406
+ this._activateIndexedItem();
407
+ }
408
+ this._setScroll();
409
+ } else if (this.shown) {
410
+ this.deactivate();
411
+ }
412
+ },
413
+
414
+ setPosition: function (position) {
415
+ this.$el.css(this._applyPlacement(position));
416
+
417
+ // Make the dropdown fixed if the input is also fixed
418
+ // This can't be done during init, as textcomplete may be used on multiple elements on the same page
419
+ // Because the same dropdown is reused behind the scenes, we need to recheck every time the dropdown is showed
420
+ var position = 'absolute';
421
+ // Check if input or one of its parents has positioning we need to care about
422
+ this.$inputEl.add(this.$inputEl.parents()).each(function() {
423
+ if($(this).css('position') === 'absolute') // The element has absolute positioning, so it's all OK
424
+ return false;
425
+ if($(this).css('position') === 'fixed') {
426
+ position = 'fixed';
427
+ return false;
428
+ }
429
+ });
430
+ this.$el.css({ position: position }); // Update positioning
431
+
432
+ return this;
433
+ },
434
+
435
+ clear: function () {
436
+ this.$el.html('');
437
+ this.data = [];
438
+ this._index = 0;
439
+ this._$header = this._$footer = null;
440
+ },
441
+
442
+ activate: function () {
443
+ if (!this.shown) {
444
+ this.clear();
445
+ this.$el.show();
446
+ if (this.className) { this.$el.addClass(this.className); }
447
+ this.completer.fire('textComplete:show');
448
+ this.shown = true;
449
+ }
450
+ return this;
451
+ },
452
+
453
+ deactivate: function () {
454
+ if (this.shown) {
455
+ this.$el.hide();
456
+ if (this.className) { this.$el.removeClass(this.className); }
457
+ this.completer.fire('textComplete:hide');
458
+ this.shown = false;
459
+ }
460
+ return this;
461
+ },
462
+
463
+ isUp: function (e) {
464
+ return e.keyCode === 38 || (e.ctrlKey && e.keyCode === 80); // UP, Ctrl-P
465
+ },
466
+
467
+ isDown: function (e) {
468
+ return e.keyCode === 40 || (e.ctrlKey && e.keyCode === 78); // DOWN, Ctrl-N
469
+ },
470
+
471
+ isEnter: function (e) {
472
+ var modifiers = e.ctrlKey || e.altKey || e.metaKey || e.shiftKey;
473
+ return !modifiers && (e.keyCode === 13 || e.keyCode === 9 || (this.option.completeOnSpace === true && e.keyCode === 32)) // ENTER, TAB
474
+ },
475
+
476
+ isPageup: function (e) {
477
+ return e.keyCode === 33; // PAGEUP
478
+ },
479
+
480
+ isPagedown: function (e) {
481
+ return e.keyCode === 34; // PAGEDOWN
482
+ },
483
+
484
+ // Private properties
485
+ // ------------------
486
+
487
+ _data: null, // Currently shown zipped data.
488
+ _index: null,
489
+ _$header: null,
490
+ _$footer: null,
491
+
492
+ // Private methods
493
+ // ---------------
494
+
495
+ _bindEvents: function () {
496
+ this.$el.on('mousedown.' + this.id, '.textcomplete-item', $.proxy(this._onClick, this))
497
+ this.$el.on('mouseover.' + this.id, '.textcomplete-item', $.proxy(this._onMouseover, this));
498
+ this.$inputEl.on('keydown.' + this.id, $.proxy(this._onKeydown, this));
499
+ },
500
+
501
+ _onClick: function (e) {
502
+ var $el = $(e.target);
503
+ e.preventDefault();
504
+ e.originalEvent.keepTextCompleteDropdown = this.id;
505
+ if (!$el.hasClass('textcomplete-item')) {
506
+ $el = $el.closest('.textcomplete-item');
507
+ }
508
+ var datum = this.data[parseInt($el.data('index'), 10)];
509
+ this.completer.select(datum.value, datum.strategy);
510
+ var self = this;
511
+ // Deactive at next tick to allow other event handlers to know whether
512
+ // the dropdown has been shown or not.
513
+ setTimeout(function () { self.deactivate(); }, 0);
514
+ },
515
+
516
+ // Activate hovered item.
517
+ _onMouseover: function (e) {
518
+ var $el = $(e.target);
519
+ e.preventDefault();
520
+ if (!$el.hasClass('textcomplete-item')) {
521
+ $el = $el.closest('.textcomplete-item');
522
+ }
523
+ this._index = parseInt($el.data('index'), 10);
524
+ this._activateIndexedItem();
525
+ },
526
+
527
+ _onKeydown: function (e) {
528
+ if (!this.shown) { return; }
529
+ if (this.isUp(e)) {
530
+ e.preventDefault();
531
+ this._up();
532
+ } else if (this.isDown(e)) {
533
+ e.preventDefault();
534
+ this._down();
535
+ } else if (this.isEnter(e)) {
536
+ e.preventDefault();
537
+ this._enter();
538
+ } else if (this.isPageup(e)) {
539
+ e.preventDefault();
540
+ this._pageup();
541
+ } else if (this.isPagedown(e)) {
542
+ e.preventDefault();
543
+ this._pagedown();
544
+ }
545
+ },
546
+
547
+ _up: function () {
548
+ if (this._index === 0) {
549
+ this._index = this.data.length - 1;
550
+ } else {
551
+ this._index -= 1;
552
+ }
553
+ this._activateIndexedItem();
554
+ this._setScroll();
555
+ },
556
+
557
+ _down: function () {
558
+ if (this._index === this.data.length - 1) {
559
+ this._index = 0;
560
+ } else {
561
+ this._index += 1;
562
+ }
563
+ this._activateIndexedItem();
564
+ this._setScroll();
565
+ },
566
+
567
+ _enter: function () {
568
+ var datum = this.data[parseInt(this._getActiveElement().data('index'), 10)];
569
+ this.completer.select(datum.value, datum.strategy);
570
+ this._setScroll();
571
+ },
572
+
573
+ _pageup: function () {
574
+ var target = 0;
575
+ var threshold = this._getActiveElement().position().top - this.$el.innerHeight();
576
+ this.$el.children().each(function (i) {
577
+ if ($(this).position().top + $(this).outerHeight() > threshold) {
578
+ target = i;
579
+ return false;
580
+ }
581
+ });
582
+ this._index = target;
583
+ this._activateIndexedItem();
584
+ this._setScroll();
585
+ },
586
+
587
+ _pagedown: function () {
588
+ var target = this.data.length - 1;
589
+ var threshold = this._getActiveElement().position().top + this.$el.innerHeight();
590
+ this.$el.children().each(function (i) {
591
+ if ($(this).position().top > threshold) {
592
+ target = i;
593
+ return false
594
+ }
595
+ });
596
+ this._index = target;
597
+ this._activateIndexedItem();
598
+ this._setScroll();
599
+ },
600
+
601
+ _activateIndexedItem: function () {
602
+ this.$el.find('.textcomplete-item.active').removeClass('active');
603
+ this._getActiveElement().addClass('active');
604
+ },
605
+
606
+ _getActiveElement: function () {
607
+ return this.$el.children('.textcomplete-item:nth(' + this._index + ')');
608
+ },
609
+
610
+ _setScroll: function () {
611
+ var $activeEl = this._getActiveElement();
612
+ var itemTop = $activeEl.position().top;
613
+ var itemHeight = $activeEl.outerHeight();
614
+ var visibleHeight = this.$el.innerHeight();
615
+ var visibleTop = this.$el.scrollTop();
616
+ if (this._index === 0 || this._index == this.data.length - 1 || itemTop < 0) {
617
+ this.$el.scrollTop(itemTop + visibleTop);
618
+ } else if (itemTop + itemHeight > visibleHeight) {
619
+ this.$el.scrollTop(itemTop + itemHeight + visibleTop - visibleHeight);
620
+ }
621
+ },
622
+
623
+ _buildContents: function (zippedData) {
624
+ var datum, i, index;
625
+ var html = '';
626
+ for (i = 0; i < zippedData.length; i++) {
627
+ if (this.data.length === this.maxCount) break;
628
+ datum = zippedData[i];
629
+ if (include(this.data, datum)) { continue; }
630
+ index = this.data.length;
631
+ this.data.push(datum);
632
+ html += '<li class="textcomplete-item" data-index="' + index + '"><a>';
633
+ html += datum.strategy.template(datum.value);
634
+ html += '</a></li>';
635
+ }
636
+ return html;
637
+ },
638
+
639
+ _renderHeader: function (unzippedData) {
640
+ if (this.header) {
641
+ if (!this._$header) {
642
+ this._$header = $('<li class="textcomplete-header"></li>').prependTo(this.$el);
643
+ }
644
+ var html = $.isFunction(this.header) ? this.header(unzippedData) : this.header;
645
+ this._$header.html(html);
646
+ }
647
+ },
648
+
649
+ _renderFooter: function (unzippedData) {
650
+ if (this.footer) {
651
+ if (!this._$footer) {
652
+ this._$footer = $('<li class="textcomplete-footer"></li>').appendTo(this.$el);
653
+ }
654
+ var html = $.isFunction(this.footer) ? this.footer(unzippedData) : this.footer;
655
+ this._$footer.html(html);
656
+ }
657
+ },
658
+
659
+ _renderContents: function (html) {
660
+ if (this._$footer) {
661
+ this._$footer.before(html);
662
+ } else {
663
+ this.$el.append(html);
664
+ }
665
+ },
666
+
667
+ _applyPlacement: function (position) {
668
+ // If the 'placement' option set to 'top', move the position above the element.
669
+ if (this.placement.indexOf('top') !== -1) {
670
+ // Overwrite the position object to set the 'bottom' property instead of the top.
671
+ position = {
672
+ top: 'auto',
673
+ bottom: this.$el.parent().height() - position.top + position.lineHeight,
674
+ left: position.left
675
+ };
676
+ } else {
677
+ position.bottom = 'auto';
678
+ delete position.lineHeight;
679
+ }
680
+ if (this.placement.indexOf('absleft') !== -1) {
681
+ position.left = 0;
682
+ } else if (this.placement.indexOf('absright') !== -1) {
683
+ position.right = 0;
684
+ position.left = 'auto';
685
+ }
686
+ return position;
687
+ }
688
+ });
689
+
690
+ $.fn.textcomplete.Dropdown = Dropdown;
691
+ }(jQuery);
692
+
693
+ +function ($) {
694
+ 'use strict';
695
+
696
+ // Memoize a search function.
68
697
  var memoize = function (func) {
69
698
  var memo = {};
70
699
  return function (term, callback) {
@@ -79,475 +708,397 @@
79
708
  };
80
709
  };
81
710
 
82
- /**
83
- * Determine if the array contains a given value.
84
- */
85
- var include = function (array, value) {
86
- var i, l;
87
- if (array.indexOf) return array.indexOf(value) != -1;
88
- for (i = 0, l = array.length; i < l; i++) {
89
- if (array[i] === value) return true;
90
- }
91
- return false;
711
+ function Strategy(options) {
712
+ $.extend(this, options);
713
+ if (this.cache) { this.search = memoize(this.search); }
714
+ }
715
+
716
+ Strategy.parse = function (optionsArray) {
717
+ return $.map(optionsArray, function (options) {
718
+ return new Strategy(options);
719
+ });
92
720
  };
93
721
 
94
- /**
95
- * Textarea manager class.
96
- */
97
- var Completer = (function () {
98
- var html, css, $baseWrapper, $baseList, _id;
722
+ $.extend(Strategy.prototype, {
723
+ // Public properties
724
+ // -----------------
725
+
726
+ // Required
727
+ match: null,
728
+ replace: null,
729
+ search: null,
730
+
731
+ // Optional
732
+ cache: false,
733
+ context: function () { return true; },
734
+ index: 2,
735
+ template: function (obj) { return obj; },
736
+ idProperty: null
737
+ });
738
+
739
+ $.fn.textcomplete.Strategy = Strategy;
740
+
741
+ }(jQuery);
742
+
743
+ +function ($) {
744
+ 'use strict';
99
745
 
100
- html = {
101
- wrapper: '<div class="textcomplete-wrapper"></div>',
102
- list: '<ul class="dropdown-menu"></ul>'
746
+ var now = Date.now || function () { return new Date().getTime(); };
747
+
748
+ // Returns a function, that, as long as it continues to be invoked, will not
749
+ // be triggered. The function will be called after it stops being called for
750
+ // `wait` msec.
751
+ //
752
+ // This utility function was originally implemented at Underscore.js.
753
+ var debounce = function (func, wait) {
754
+ var timeout, args, context, timestamp, result;
755
+ var later = function () {
756
+ var last = now() - timestamp;
757
+ if (last < wait) {
758
+ timeout = setTimeout(later, wait - last);
759
+ } else {
760
+ timeout = null;
761
+ result = func.apply(context, args);
762
+ context = args = null;
763
+ }
103
764
  };
104
- css = {
105
- wrapper: {
106
- position: 'relative'
107
- },
108
- list: {
109
- position: 'absolute',
110
- top: 0,
111
- left: 0,
112
- zIndex: '100',
113
- display: 'none'
765
+
766
+ return function () {
767
+ context = this;
768
+ args = arguments;
769
+ timestamp = now();
770
+ if (!timeout) {
771
+ timeout = setTimeout(later, wait);
114
772
  }
773
+ return result;
115
774
  };
116
- $baseWrapper = $(html.wrapper).css(css.wrapper);
117
- $baseList = $(html.list).css(css.list);
118
- _id = 0;
119
-
120
- function Completer($el) {
121
- var focus;
122
- this.el = $el.get(0); // textarea element
123
- focus = this.el === document.activeElement;
124
- // Cannot wrap $el at initialize method lazily due to Firefox's behavior.
125
- this.$el = wrapElement($el); // Focus is lost
126
- this.id = 'textComplete' + _id++;
127
- this.strategies = [];
128
- if (focus) {
129
- this.initialize();
130
- this.$el.focus();
131
- } else {
132
- this.$el.one('focus.textComplete', $.proxy(this.initialize, this));
775
+ };
776
+
777
+ function Adapter () {}
778
+
779
+ $.extend(Adapter.prototype, {
780
+ // Public properties
781
+ // -----------------
782
+
783
+ id: null, // Identity.
784
+ completer: null, // Completer object which creates it.
785
+ el: null, // Textarea element.
786
+ $el: null, // jQuery object of the textarea.
787
+ option: null,
788
+
789
+ // Public methods
790
+ // --------------
791
+
792
+ initialize: function (element, completer, option) {
793
+ this.el = element;
794
+ this.$el = $(element);
795
+ this.id = completer.id + this.constructor.name;
796
+ this.completer = completer;
797
+ this.option = option;
798
+
799
+ if (this.option.debounce) {
800
+ this._onKeyup = debounce(this._onKeyup, this.option.debounce);
133
801
  }
134
- }
135
802
 
136
- /**
137
- * Completer's public methods
138
- */
139
- $.extend(Completer.prototype, {
140
-
141
- /**
142
- * Prepare ListView and bind events.
143
- */
144
- initialize: function () {
145
- var $list, globalEvents;
146
- $list = $baseList.clone();
147
- this.listView = new ListView($list, this);
148
- this.$el
149
- .before($list)
150
- .on({
151
- 'keyup.textComplete': $.proxy(this.onKeyup, this),
152
- 'keydown.textComplete': $.proxy(this.listView.onKeydown,
153
- this.listView)
154
- });
155
- globalEvents = {};
156
- globalEvents['click.' + this.id] = $.proxy(this.onClickDocument, this);
157
- globalEvents['keyup.' + this.id] = $.proxy(this.onKeyupDocument, this);
158
- $(document).on(globalEvents);
159
- },
160
-
161
- /**
162
- * Register strategies to the completer.
163
- */
164
- register: function (strategies) {
165
- this.strategies = this.strategies.concat(strategies);
166
- },
167
-
168
- /**
169
- * Show autocomplete list next to the caret.
170
- */
171
- renderList: function (data) {
172
- if (this.clearAtNext) {
173
- this.listView.clear();
174
- this.clearAtNext = false;
175
- }
176
- if (data.length) {
177
- if (!this.listView.shown) {
178
- this.listView
179
- .setPosition(this.getCaretPosition())
180
- .clear()
181
- .activate();
182
- this.listView.strategy = this.strategy;
183
- }
184
- data = data.slice(0, this.strategy.maxCount);
185
- this.listView.render(data);
186
- }
803
+ this._bindEvents();
804
+ },
187
805
 
188
- if (!this.listView.data.length && this.listView.shown) {
189
- this.listView.deactivate();
190
- }
191
- },
192
-
193
- searchCallbackFactory: function (free) {
194
- var self = this;
195
- return function (data, keep) {
196
- self.renderList(data);
197
- if (!keep) {
198
- // This is the last callback for this search.
199
- free();
200
- self.clearAtNext = true;
201
- }
202
- };
203
- },
204
-
205
- /**
206
- * Keyup event handler.
207
- */
208
- onKeyup: function (e) {
209
- var searchQuery, term;
210
- if (this.skipSearch(e)) { return; }
211
-
212
- searchQuery = this.extractSearchQuery(this.getTextFromHeadToCaret());
213
- if (searchQuery.length) {
214
- term = searchQuery[1];
215
- if (this.term === term) return; // Ignore shift-key or something.
216
- this.term = term;
217
- this.search(searchQuery);
218
- } else {
219
- this.term = null;
220
- this.listView.deactivate();
221
- }
222
- },
223
-
224
- /**
225
- * Suppress searching if it returns true.
226
- */
227
- skipSearch: function (e) {
228
- if (this.skipNextKeyup) {
229
- this.skipNextKeyup = false;
230
- return true;
231
- }
232
- switch (e.keyCode) {
233
- case 40:
234
- case 38:
235
- return true;
236
- }
237
- },
806
+ destroy: function () {
807
+ this.$el.off('.' + this.id); // Remove all event handlers.
808
+ this.$el = this.el = this.completer = null;
809
+ },
238
810
 
239
- onSelect: function (value) {
240
- var pre, post, newSubStr;
241
- pre = this.getTextFromHeadToCaret();
242
- post = this.el.value.substring(this.el.selectionEnd);
811
+ // Update the element with the given value and strategy.
812
+ //
813
+ // value - The selected object. It is one of the item of the array
814
+ // which was callbacked from the search function.
815
+ // strategy - The Strategy associated with the selected value.
816
+ select: function (/* value, strategy */) {
817
+ throw new Error('Not implemented');
818
+ },
243
819
 
244
- newSubStr = this.strategy.replace(value);
245
- if ($.isArray(newSubStr)) {
246
- post = newSubStr[1] + post;
247
- newSubStr = newSubStr[0];
248
- }
249
- pre = pre.replace(this.strategy.match, newSubStr);
250
- this.$el.val(pre + post)
251
- .trigger('change')
252
- .trigger('textComplete:select', value);
253
- this.el.focus();
254
- this.el.selectionStart = this.el.selectionEnd = pre.length;
255
- this.skipNextKeyup = true;
256
- },
257
-
258
- /**
259
- * Global click event handler.
260
- */
261
- onClickDocument: function (e) {
262
- if (e.originalEvent && !e.originalEvent.keepTextCompleteDropdown) {
263
- this.listView.deactivate();
264
- }
265
- },
266
-
267
- /**
268
- * Global keyup event handler.
269
- */
270
- onKeyupDocument: function (e) {
271
- if (this.listView.shown && e.keyCode === 27) { // ESC
272
- this.listView.deactivate();
273
- this.$el.focus();
274
- }
275
- },
276
-
277
- /**
278
- * Remove all event handlers and the wrapper element.
279
- */
280
- destroy: function () {
281
- var $wrapper;
282
- this.$el.off('.textComplete');
283
- $(document).off('.' + this.id);
284
- if (this.listView) { this.listView.destroy(); }
285
- $wrapper = this.$el.parent();
286
- $wrapper.after(this.$el).remove();
287
- this.$el.data('textComplete', void 0);
288
- this.$el = null;
289
- },
290
-
291
- // Helper methods
292
- // ==============
293
-
294
- /**
295
- * Returns caret's relative coordinates from textarea's left top corner.
296
- */
297
- getCaretPosition: function () {
298
- // Browser native API does not provide the way to know the position of
299
- // caret in pixels, so that here we use a kind of hack to accomplish
300
- // the aim. First of all it puts a div element and completely copies
301
- // the textarea's style to the element, then it inserts the text and a
302
- // span element into the textarea.
303
- // Consequently, the span element's position is the thing what we want.
304
-
305
- if (this.el.selectionEnd === 0) return;
306
- var properties, css, $div, $span, position, dir;
307
-
308
- dir = this.$el.attr('dir') || this.$el.css('direction');
309
- properties = ['border-width', 'font-family', 'font-size', 'font-style',
310
- 'font-variant', 'font-weight', 'height', 'letter-spacing',
311
- 'word-spacing', 'line-height', 'text-decoration', 'text-align',
312
- 'width', 'padding-top', 'padding-right', 'padding-bottom',
313
- 'padding-left', 'margin-top', 'margin-right', 'margin-bottom',
314
- 'margin-left'
315
- ];
316
- css = $.extend({
317
- position: 'absolute',
318
- overflow: 'auto',
319
- 'white-space': 'pre-wrap',
320
- top: 0,
321
- left: -9999,
322
- direction: dir
323
- }, getStyles(this.$el, properties));
324
-
325
- $div = $('<div></div>').css(css).text(this.getTextFromHeadToCaret());
326
- $span = $('<span></span>').text('.').appendTo($div);
327
- this.$el.before($div);
328
- position = $span.position();
329
- position.top += $span.height() - this.$el.scrollTop();
330
- if (dir === 'rtl') { position.left -= this.listView.$el.width(); }
331
- $div.remove();
332
- return position;
333
- },
334
-
335
- getTextFromHeadToCaret: function () {
336
- var text, selectionEnd, range;
337
- selectionEnd = this.el.selectionEnd;
338
- if (typeof selectionEnd === 'number') {
339
- text = this.el.value.substring(0, selectionEnd);
340
- } else if (document.selection) {
341
- range = this.el.createTextRange();
342
- range.moveStart('character', 0);
343
- range.moveEnd('textedit');
344
- text = range.text;
345
- }
346
- return text;
347
- },
348
-
349
- /**
350
- * Parse the value of textarea and extract search query.
351
- */
352
- extractSearchQuery: function (text) {
353
- // If a search query found, it returns used strategy and the query
354
- // term. If the caret is currently in a code block or search query does
355
- // not found, it returns an empty array.
356
-
357
- var i, l, strategy, match;
358
- for (i = 0, l = this.strategies.length; i < l; i++) {
359
- strategy = this.strategies[i];
360
- match = text.match(strategy.match);
361
- if (match) { return [strategy, match[strategy.index]]; }
362
- }
363
- return [];
364
- },
365
-
366
- search: lock(function (free, searchQuery) {
367
- var term;
368
- this.strategy = searchQuery[0];
369
- term = searchQuery[1];
370
- this.strategy.search(term, this.searchCallbackFactory(free));
371
- })
372
- });
820
+ // Returns the caret's relative coordinates from body's left top corner.
821
+ //
822
+ // FIXME: Calculate the left top corner of `this.option.appendTo` element.
823
+ getCaretPosition: function () {
824
+ var position = this._getCaretRelativePosition();
825
+ var offset = this.$el.offset();
826
+ position.top += offset.top;
827
+ position.left += offset.left;
828
+ return position;
829
+ },
373
830
 
374
- /**
375
- * Completer's private functions
376
- */
377
- var wrapElement = function ($el) {
378
- return $el.wrap($baseWrapper.clone().css('display', $el.css('display')));
379
- };
831
+ // Focus on the element.
832
+ focus: function () {
833
+ this.$el.focus();
834
+ },
380
835
 
381
- return Completer;
382
- })();
836
+ // Private methods
837
+ // ---------------
383
838
 
384
- /**
385
- * Dropdown menu manager class.
386
- */
387
- var ListView = (function () {
839
+ _bindEvents: function () {
840
+ this.$el.on('keyup.' + this.id, $.proxy(this._onKeyup, this));
841
+ },
388
842
 
389
- function ListView($el, completer) {
390
- this.data = [];
391
- this.$el = $el;
392
- this.index = 0;
393
- this.completer = completer;
843
+ _onKeyup: function (e) {
844
+ if (this._skipSearch(e)) { return; }
845
+ this.completer.trigger(this.getTextFromHeadToCaret(), true);
846
+ },
394
847
 
395
- this.$el.on('click.textComplete', 'li.textcomplete-item',
396
- $.proxy(this.onClick, this));
848
+ // Suppress searching if it returns true.
849
+ _skipSearch: function (clickEvent) {
850
+ switch (clickEvent.keyCode) {
851
+ case 40: // DOWN
852
+ case 38: // UP
853
+ return true;
854
+ }
855
+ if (clickEvent.ctrlKey) switch (clickEvent.keyCode) {
856
+ case 78: // Ctrl-N
857
+ case 80: // Ctrl-P
858
+ return true;
859
+ }
397
860
  }
861
+ });
398
862
 
399
- $.extend(ListView.prototype, {
400
- shown: false,
401
-
402
- render: function (data) {
403
- var html, i, l, index, val;
404
-
405
- html = '';
406
- for (i = 0, l = data.length; i < l; i++) {
407
- val = data[i];
408
- if (include(this.data, val)) continue;
409
- index = this.data.length;
410
- this.data.push(val);
411
- html += '<li class="textcomplete-item" data-index="' + index + '"><a>';
412
- html += this.strategy.template(val);
413
- html += '</a></li>';
414
- if (this.data.length === this.strategy.maxCount) break;
415
- }
416
- this.$el.append(html);
417
- if (!this.data.length) {
418
- this.deactivate();
419
- } else {
420
- this.activateIndexedItem();
421
- }
422
- },
423
-
424
- clear: function () {
425
- this.data = [];
426
- this.$el.html('');
427
- this.index = 0;
428
- return this;
429
- },
430
-
431
- activateIndexedItem: function () {
432
- this.$el.find('.active').removeClass('active');
433
- this.getActiveItem().addClass('active');
434
- },
435
-
436
- getActiveItem: function () {
437
- return $(this.$el.children().get(this.index));
438
- },
439
-
440
- activate: function () {
441
- if (!this.shown) {
442
- this.$el.show();
443
- this.completer.$el.trigger('textComplete:show');
444
- this.shown = true;
445
- }
446
- return this;
447
- },
448
-
449
- deactivate: function () {
450
- if (this.shown) {
451
- this.$el.hide();
452
- this.completer.$el.trigger('textComplete:hide');
453
- this.shown = false;
454
- this.data = this.index = null;
455
- }
456
- return this;
457
- },
458
-
459
- setPosition: function (position) {
460
- this.$el.css(position);
461
- return this;
462
- },
463
-
464
- select: function (index) {
465
- var self = this;
466
- this.completer.onSelect(this.data[index]);
467
- // Deactive at next tick to allow other event handlers to know whether
468
- // the dropdown has been shown or not.
469
- setTimeout(function () { self.deactivate(); }, 0);
470
- },
471
-
472
- onKeydown: function (e) {
473
- if (!this.shown) return;
474
- if (e.keyCode === 38) { // UP
475
- e.preventDefault();
476
- if (this.index === 0) {
477
- this.index = this.data.length-1;
478
- } else {
479
- this.index -= 1;
480
- }
481
- this.activateIndexedItem();
482
- } else if (e.keyCode === 40) { // DOWN
483
- e.preventDefault();
484
- if (this.index === this.data.length - 1) {
485
- this.index = 0;
486
- } else {
487
- this.index += 1;
488
- }
489
- this.activateIndexedItem();
490
- } else if (e.keyCode === 13 || e.keyCode === 9) { // ENTER or TAB
491
- e.preventDefault();
492
- this.select(parseInt(this.getActiveItem().data('index'), 10));
493
- }
494
- },
863
+ $.fn.textcomplete.Adapter = Adapter;
864
+ }(jQuery);
495
865
 
496
- onClick: function (e) {
497
- var $e = $(e.target);
498
- e.originalEvent.keepTextCompleteDropdown = true;
499
- if (!$e.hasClass('textcomplete-item')) {
500
- $e = $e.parents('li.textcomplete-item');
501
- }
502
- this.select(parseInt($e.data('index'), 10));
503
- },
866
+ +function ($) {
867
+ 'use strict';
504
868
 
505
- destroy: function () {
506
- this.deactivate();
507
- this.$el.off('click.textComplete').remove();
508
- this.$el = null;
869
+ // Textarea adapter
870
+ // ================
871
+ //
872
+ // Managing a textarea. It doesn't know a Dropdown.
873
+ function Textarea(element, completer, option) {
874
+ this.initialize(element, completer, option);
875
+ }
876
+
877
+ Textarea.DIV_PROPERTIES = {
878
+ left: -9999,
879
+ position: 'absolute',
880
+ top: 0,
881
+ whiteSpace: 'pre-wrap'
882
+ }
883
+
884
+ Textarea.COPY_PROPERTIES = [
885
+ 'border-width', 'font-family', 'font-size', 'font-style', 'font-variant',
886
+ 'font-weight', 'height', 'letter-spacing', 'word-spacing', 'line-height',
887
+ 'text-decoration', 'text-align', 'width', 'padding-top', 'padding-right',
888
+ 'padding-bottom', 'padding-left', 'margin-top', 'margin-right',
889
+ 'margin-bottom', 'margin-left', 'border-style', 'box-sizing', 'tab-size'
890
+ ];
891
+
892
+ $.extend(Textarea.prototype, $.fn.textcomplete.Adapter.prototype, {
893
+ // Public methods
894
+ // --------------
895
+
896
+ // Update the textarea with the given value and strategy.
897
+ select: function (value, strategy) {
898
+ var pre = this.getTextFromHeadToCaret();
899
+ var post = this.el.value.substring(this.el.selectionEnd);
900
+ var newSubstr = strategy.replace(value);
901
+ if ($.isArray(newSubstr)) {
902
+ post = newSubstr[1] + post;
903
+ newSubstr = newSubstr[0];
509
904
  }
510
- });
905
+ pre = pre.replace(strategy.match, newSubstr);
906
+ this.$el.val(pre + post);
907
+ this.el.selectionStart = this.el.selectionEnd = pre.length;
908
+ },
511
909
 
512
- return ListView;
513
- })();
910
+ // Private methods
911
+ // ---------------
514
912
 
515
- $.fn.textcomplete = function (strategies) {
516
- var i, l, strategy, dataKey;
913
+ // Returns the caret's relative coordinates from textarea's left top corner.
914
+ //
915
+ // Browser native API does not provide the way to know the position of
916
+ // caret in pixels, so that here we use a kind of hack to accomplish
917
+ // the aim. First of all it puts a dummy div element and completely copies
918
+ // the textarea's style to the element, then it inserts the text and a
919
+ // span element into the textarea.
920
+ // Consequently, the span element's position is the thing what we want.
921
+ _getCaretRelativePosition: function () {
922
+ var dummyDiv = $('<div></div>').css(this._copyCss())
923
+ .text(this.getTextFromHeadToCaret());
924
+ var span = $('<span></span>').text('.').appendTo(dummyDiv);
925
+ this.$el.before(dummyDiv);
926
+ var position = span.position();
927
+ position.top += span.height() - this.$el.scrollTop();
928
+ position.lineHeight = span.height();
929
+ dummyDiv.remove();
930
+ return position;
931
+ },
517
932
 
518
- dataKey = 'textComplete';
933
+ _copyCss: function () {
934
+ return $.extend({
935
+ // Set 'scroll' if a scrollbar is being shown; otherwise 'auto'.
936
+ overflow: this.el.scrollHeight > this.el.offsetHeight ? 'scroll' : 'auto'
937
+ }, Textarea.DIV_PROPERTIES, this._getStyles());
938
+ },
519
939
 
520
- if (strategies === 'destroy') {
521
- return this.each(function () {
522
- var completer = $(this).data(dataKey);
523
- if (completer) { completer.destroy(); }
524
- });
940
+ _getStyles: (function ($) {
941
+ var color = $('<div></div>').css(['color']).color;
942
+ if (typeof color !== 'undefined') {
943
+ return function () {
944
+ return this.$el.css(Textarea.COPY_PROPERTIES);
945
+ };
946
+ } else { // jQuery < 1.8
947
+ return function () {
948
+ var $el = this.$el;
949
+ var styles = {};
950
+ $.each(Textarea.COPY_PROPERTIES, function (i, property) {
951
+ styles[property] = $el.css(property);
952
+ });
953
+ return styles;
954
+ };
955
+ }
956
+ })($),
957
+
958
+ getTextFromHeadToCaret: function () {
959
+ return this.el.value.substring(0, this.el.selectionEnd);
525
960
  }
961
+ });
526
962
 
527
- for (i = 0, l = strategies.length; i < l; i++) {
528
- strategy = strategies[i];
529
- if (!strategy.template) {
530
- strategy.template = identity;
531
- }
532
- if (strategy.index == null) {
533
- strategy.index = 2;
534
- }
535
- if (strategy.cache) {
536
- strategy.search = memoize(strategy.search);
963
+ $.fn.textcomplete.Textarea = Textarea;
964
+ }(jQuery);
965
+
966
+ +function ($) {
967
+ 'use strict';
968
+
969
+ var sentinelChar = '吶';
970
+
971
+ function IETextarea(element, completer, option) {
972
+ this.initialize(element, completer, option);
973
+ $('<span>' + sentinelChar + '</span>').css({
974
+ position: 'absolute',
975
+ top: -9999,
976
+ left: -9999
977
+ }).insertBefore(element);
978
+ }
979
+
980
+ $.extend(IETextarea.prototype, $.fn.textcomplete.Textarea.prototype, {
981
+ // Public methods
982
+ // --------------
983
+
984
+ select: function (value, strategy) {
985
+ var pre = this.getTextFromHeadToCaret();
986
+ var post = this.el.value.substring(pre.length);
987
+ var newSubstr = strategy.replace(value);
988
+ if ($.isArray(newSubstr)) {
989
+ post = newSubstr[1] + post;
990
+ newSubstr = newSubstr[0];
537
991
  }
538
- strategy.maxCount || (strategy.maxCount = 10);
992
+ pre = pre.replace(strategy.match, newSubstr);
993
+ this.$el.val(pre + post);
994
+ this.el.focus();
995
+ var range = this.el.createTextRange();
996
+ range.collapse(true);
997
+ range.moveEnd('character', pre.length);
998
+ range.moveStart('character', pre.length);
999
+ range.select();
1000
+ },
1001
+
1002
+ getTextFromHeadToCaret: function () {
1003
+ this.el.focus();
1004
+ var range = document.selection.createRange();
1005
+ range.moveStart('character', -this.el.value.length);
1006
+ var arr = range.text.split(sentinelChar)
1007
+ return arr.length === 1 ? arr[0] : arr[1];
539
1008
  }
1009
+ });
540
1010
 
541
- return this.each(function () {
542
- var $this, completer;
543
- $this = $(this);
544
- completer = $this.data(dataKey);
545
- if (!completer) {
546
- completer = new Completer($this);
547
- $this.data(dataKey, completer);
1011
+ $.fn.textcomplete.IETextarea = IETextarea;
1012
+ }(jQuery);
1013
+
1014
+ // NOTE: TextComplete plugin has contenteditable support but it does not work
1015
+ // fine especially on old IEs.
1016
+ // Any pull requests are REALLY welcome.
1017
+
1018
+ +function ($) {
1019
+ 'use strict';
1020
+
1021
+ // ContentEditable adapter
1022
+ // =======================
1023
+ //
1024
+ // Adapter for contenteditable elements.
1025
+ function ContentEditable (element, completer, option) {
1026
+ this.initialize(element, completer, option);
1027
+ }
1028
+
1029
+ $.extend(ContentEditable.prototype, $.fn.textcomplete.Adapter.prototype, {
1030
+ // Public methods
1031
+ // --------------
1032
+
1033
+ // Update the content with the given value and strategy.
1034
+ // When an dropdown item is selected, it is executed.
1035
+ select: function (value, strategy) {
1036
+ var pre = this.getTextFromHeadToCaret();
1037
+ var sel = window.getSelection()
1038
+ var range = sel.getRangeAt(0);
1039
+ var selection = range.cloneRange();
1040
+ selection.selectNodeContents(range.startContainer);
1041
+ var content = selection.toString();
1042
+ var post = content.substring(range.startOffset);
1043
+ var newSubstr = strategy.replace(value);
1044
+ if ($.isArray(newSubstr)) {
1045
+ post = newSubstr[1] + post;
1046
+ newSubstr = newSubstr[0];
548
1047
  }
549
- completer.register(strategies);
550
- });
551
- };
1048
+ pre = pre.replace(strategy.match, newSubstr);
1049
+ range.selectNodeContents(range.startContainer);
1050
+ range.deleteContents();
1051
+ var node = document.createTextNode(pre + post);
1052
+ range.insertNode(node);
1053
+ range.setStart(node, pre.length);
1054
+ range.collapse(true);
1055
+ sel.removeAllRanges();
1056
+ sel.addRange(range);
1057
+ },
1058
+
1059
+ // Private methods
1060
+ // ---------------
1061
+
1062
+ // Returns the caret's relative position from the contenteditable's
1063
+ // left top corner.
1064
+ //
1065
+ // Examples
1066
+ //
1067
+ // this._getCaretRelativePosition()
1068
+ // //=> { top: 18, left: 200, lineHeight: 16 }
1069
+ //
1070
+ // Dropdown's position will be decided using the result.
1071
+ _getCaretRelativePosition: function () {
1072
+ var range = window.getSelection().getRangeAt(0).cloneRange();
1073
+ var node = document.createElement('span');
1074
+ range.insertNode(node);
1075
+ range.selectNodeContents(node);
1076
+ range.deleteContents();
1077
+ var $node = $(node);
1078
+ var position = $node.offset();
1079
+ position.left -= this.$el.offset().left;
1080
+ position.top += $node.height() - this.$el.offset().top;
1081
+ position.lineHeight = $node.height();
1082
+ var dir = this.$el.attr('dir') || this.$el.css('direction');
1083
+ if (dir === 'rtl') { position.left -= this.listView.$el.width(); }
1084
+ return position;
1085
+ },
1086
+
1087
+ // Returns the string between the first character and the caret.
1088
+ // Completer will be triggered with the result for start autocompleting.
1089
+ //
1090
+ // Example
1091
+ //
1092
+ // // Suppose the html is '<b>hello</b> wor|ld' and | is the caret.
1093
+ // this.getTextFromHeadToCaret()
1094
+ // // => ' wor' // not '<b>hello</b> wor'
1095
+ getTextFromHeadToCaret: function () {
1096
+ var range = window.getSelection().getRangeAt(0);
1097
+ var selection = range.cloneRange();
1098
+ selection.selectNodeContents(range.startContainer);
1099
+ return selection.toString().substring(0, range.startOffset);
1100
+ }
1101
+ });
552
1102
 
553
- })(window.jQuery || window.Zepto);
1103
+ $.fn.textcomplete.ContentEditable = ContentEditable;
1104
+ }(jQuery);