jquery_textcomplete 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1489 @@
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
+ if (typeof jQuery === 'undefined') {
15
+ throw new Error('jQuery.textcomplete requires jQuery');
16
+ }
17
+
18
+ +function ($) {
19
+ 'use strict';
20
+
21
+ var warn = function (message) {
22
+ if (console.warn) { console.warn(message); }
23
+ };
24
+
25
+ var id = 1;
26
+
27
+ $.fn.textcomplete = function (strategies, option) {
28
+ var args = Array.prototype.slice.call(arguments);
29
+ return this.each(function () {
30
+ var self = this;
31
+ var $this = $(this);
32
+ var completer = $this.data('textComplete');
33
+ if (!completer) {
34
+ option || (option = {});
35
+ option._oid = id++; // unique object id
36
+ completer = new $.fn.textcomplete.Completer(this, option);
37
+ $this.data('textComplete', completer);
38
+ }
39
+ if (typeof strategies === 'string') {
40
+ if (!completer) return;
41
+ args.shift()
42
+ completer[strategies].apply(completer, args);
43
+ if (strategies === 'destroy') {
44
+ $this.removeData('textComplete');
45
+ }
46
+ } else {
47
+ // For backward compatibility.
48
+ // TODO: Remove at v0.4
49
+ $.each(strategies, function (obj) {
50
+ $.each(['header', 'footer', 'placement', 'maxCount'], function (name) {
51
+ if (obj[name]) {
52
+ completer.option[name] = obj[name];
53
+ warn(name + 'as a strategy param is deprecated. Use option.');
54
+ delete obj[name];
55
+ }
56
+ });
57
+ });
58
+ completer.register($.fn.textcomplete.Strategy.parse(strategies, {
59
+ el: self,
60
+ $el: $this
61
+ }));
62
+ }
63
+ });
64
+ };
65
+
66
+ }(jQuery);
67
+
68
+ +function ($) {
69
+ 'use strict';
70
+
71
+ // Exclusive execution control utility.
72
+ //
73
+ // func - The function to be locked. It is executed with a function named
74
+ // `free` as the first argument. Once it is called, additional
75
+ // execution are ignored until the free is invoked. Then the last
76
+ // ignored execution will be replayed immediately.
77
+ //
78
+ // Examples
79
+ //
80
+ // var lockedFunc = lock(function (free) {
81
+ // setTimeout(function { free(); }, 1000); // It will be free in 1 sec.
82
+ // console.log('Hello, world');
83
+ // });
84
+ // lockedFunc(); // => 'Hello, world'
85
+ // lockedFunc(); // none
86
+ // lockedFunc(); // none
87
+ // // 1 sec past then
88
+ // // => 'Hello, world'
89
+ // lockedFunc(); // => 'Hello, world'
90
+ // lockedFunc(); // none
91
+ //
92
+ // Returns a wrapped function.
93
+ var lock = function (func) {
94
+ var locked, queuedArgsToReplay;
95
+
96
+ return function () {
97
+ // Convert arguments into a real array.
98
+ var args = Array.prototype.slice.call(arguments);
99
+ if (locked) {
100
+ // Keep a copy of this argument list to replay later.
101
+ // OK to overwrite a previous value because we only replay
102
+ // the last one.
103
+ queuedArgsToReplay = args;
104
+ return;
105
+ }
106
+ locked = true;
107
+ var self = this;
108
+ args.unshift(function replayOrFree() {
109
+ if (queuedArgsToReplay) {
110
+ // Other request(s) arrived while we were locked.
111
+ // Now that the lock is becoming available, replay
112
+ // the latest such request, then call back here to
113
+ // unlock (or replay another request that arrived
114
+ // while this one was in flight).
115
+ var replayArgs = queuedArgsToReplay;
116
+ queuedArgsToReplay = undefined;
117
+ replayArgs.unshift(replayOrFree);
118
+ func.apply(self, replayArgs);
119
+ } else {
120
+ locked = false;
121
+ }
122
+ });
123
+ func.apply(this, args);
124
+ };
125
+ };
126
+
127
+ var isString = function (obj) {
128
+ return Object.prototype.toString.call(obj) === '[object String]';
129
+ };
130
+
131
+ var uniqueId = 0;
132
+ var initializedEditors = [];
133
+
134
+ function Completer(element, option) {
135
+ this.$el = $(element);
136
+ this.id = 'textcomplete' + uniqueId++;
137
+ this.strategies = [];
138
+ this.views = [];
139
+ this.option = $.extend({}, Completer.defaults, option);
140
+
141
+ if (!this.$el.is('input[type=text]') && !this.$el.is('input[type=search]') && !this.$el.is('textarea') && !element.isContentEditable && element.contentEditable != 'true') {
142
+ throw new Error('textcomplete must be called on a Textarea or a ContentEditable.');
143
+ }
144
+
145
+ // use ownerDocument to fix iframe / IE issues
146
+ if (element === element.ownerDocument.activeElement) {
147
+ // element has already been focused. Initialize view objects immediately.
148
+ this.initialize()
149
+ } else {
150
+ // Initialize view objects lazily.
151
+ var self = this;
152
+ this.$el.one('focus.' + this.id, function () { self.initialize(); });
153
+
154
+ // Special handling for CKEditor: lazy init on instance load
155
+ if ((!this.option.adapter || this.option.adapter == 'CKEditor') && typeof CKEDITOR != 'undefined' && (this.$el.is('textarea'))) {
156
+ CKEDITOR.on("instanceReady", function(event) { //For multiple ckeditors on one page: this needs to be executed each time a ckeditor-instance is ready.
157
+
158
+ if($.inArray(event.editor.id, initializedEditors) == -1) { //For multiple ckeditors on one page: focus-eventhandler should only be added once for every editor.
159
+ initializedEditors.push(event.editor.id);
160
+
161
+ event.editor.on("focus", function(event2) {
162
+ //replace the element with the Iframe element and flag it as CKEditor
163
+ self.$el = $(event.editor.editable().$);
164
+ if (!self.option.adapter) {
165
+ self.option.adapter = $.fn.textcomplete['CKEditor'];
166
+ }
167
+ self.option.ckeditor_instance = event.editor; //For multiple ckeditors on one page: in the old code this was not executed when adapter was alread set. So we were ALWAYS working with the FIRST instance.
168
+ self.initialize();
169
+ });
170
+ }
171
+ });
172
+ }
173
+ }
174
+ }
175
+
176
+ Completer.defaults = {
177
+ appendTo: 'body',
178
+ className: '', // deprecated option
179
+ dropdownClassName: 'dropdown-menu textcomplete-dropdown',
180
+ maxCount: 10,
181
+ zIndex: '100',
182
+ rightEdgeOffset: 30
183
+ };
184
+
185
+ $.extend(Completer.prototype, {
186
+ // Public properties
187
+ // -----------------
188
+
189
+ id: null,
190
+ option: null,
191
+ strategies: null,
192
+ adapter: null,
193
+ dropdown: null,
194
+ $el: null,
195
+ $iframe: null,
196
+
197
+ // Public methods
198
+ // --------------
199
+
200
+ initialize: function () {
201
+ var element = this.$el.get(0);
202
+
203
+ // check if we are in an iframe
204
+ // we need to alter positioning logic if using an iframe
205
+ if (this.$el.prop('ownerDocument') !== document && window.frames.length) {
206
+ for (var iframeIndex = 0; iframeIndex < window.frames.length; iframeIndex++) {
207
+ if (this.$el.prop('ownerDocument') === window.frames[iframeIndex].document) {
208
+ this.$iframe = $(window.frames[iframeIndex].frameElement);
209
+ break;
210
+ }
211
+ }
212
+ }
213
+
214
+
215
+ // Initialize view objects.
216
+ this.dropdown = new $.fn.textcomplete.Dropdown(element, this, this.option);
217
+ var Adapter, viewName;
218
+ if (this.option.adapter) {
219
+ Adapter = this.option.adapter;
220
+ } else {
221
+ if (this.$el.is('textarea') || this.$el.is('input[type=text]') || this.$el.is('input[type=search]')) {
222
+ viewName = typeof element.selectionEnd === 'number' ? 'Textarea' : 'IETextarea';
223
+ } else {
224
+ viewName = 'ContentEditable';
225
+ }
226
+ Adapter = $.fn.textcomplete[viewName];
227
+ }
228
+ this.adapter = new Adapter(element, this, this.option);
229
+ },
230
+
231
+ destroy: function () {
232
+ this.$el.off('.' + this.id);
233
+ if (this.adapter) {
234
+ this.adapter.destroy();
235
+ }
236
+ if (this.dropdown) {
237
+ this.dropdown.destroy();
238
+ }
239
+ this.$el = this.adapter = this.dropdown = null;
240
+ },
241
+
242
+ deactivate: function () {
243
+ if (this.dropdown) {
244
+ this.dropdown.deactivate();
245
+ }
246
+ },
247
+
248
+ // Invoke textcomplete.
249
+ trigger: function (text, skipUnchangedTerm) {
250
+ if (!this.dropdown) { this.initialize(); }
251
+ text != null || (text = this.adapter.getTextFromHeadToCaret());
252
+ var searchQuery = this._extractSearchQuery(text);
253
+ if (searchQuery.length) {
254
+ var term = searchQuery[1];
255
+ // Ignore shift-key, ctrl-key and so on.
256
+ if (skipUnchangedTerm && this._term === term && term !== "") { return; }
257
+ this._term = term;
258
+ this._search.apply(this, searchQuery);
259
+ } else {
260
+ this._term = null;
261
+ this.dropdown.deactivate();
262
+ }
263
+ },
264
+
265
+ fire: function (eventName) {
266
+ var args = Array.prototype.slice.call(arguments, 1);
267
+ this.$el.trigger(eventName, args);
268
+ return this;
269
+ },
270
+
271
+ register: function (strategies) {
272
+ Array.prototype.push.apply(this.strategies, strategies);
273
+ },
274
+
275
+ // Insert the value into adapter view. It is called when the dropdown is clicked
276
+ // or selected.
277
+ //
278
+ // value - The selected element of the array callbacked from search func.
279
+ // strategy - The Strategy object.
280
+ // e - Click or keydown event object.
281
+ select: function (value, strategy, e) {
282
+ this._term = null;
283
+ this.adapter.select(value, strategy, e);
284
+ this.fire('change').fire('textComplete:select', value, strategy);
285
+ this.adapter.focus();
286
+ },
287
+
288
+ // Private properties
289
+ // ------------------
290
+
291
+ _clearAtNext: false,
292
+ _term: null,
293
+
294
+ // Private methods
295
+ // ---------------
296
+
297
+ // Parse the given text and extract the first matching strategy.
298
+ //
299
+ // Returns an array including the strategy, the query term and the match
300
+ // object if the text matches an strategy; otherwise returns an empty array.
301
+ _extractSearchQuery: function (text) {
302
+ for (var i = 0; i < this.strategies.length; i++) {
303
+ var strategy = this.strategies[i];
304
+ var context = strategy.context(text);
305
+ if (context || context === '') {
306
+ var matchRegexp = $.isFunction(strategy.match) ? strategy.match(text) : strategy.match;
307
+ if (isString(context)) { text = context; }
308
+ var match = text.match(matchRegexp);
309
+ if (match) { return [strategy, match[strategy.index], match]; }
310
+ }
311
+ }
312
+ return []
313
+ },
314
+
315
+ // Call the search method of selected strategy..
316
+ _search: lock(function (free, strategy, term, match) {
317
+ var self = this;
318
+ strategy.search(term, function (data, stillSearching) {
319
+ if (!self.dropdown.shown) {
320
+ self.dropdown.activate();
321
+ }
322
+ if (self._clearAtNext) {
323
+ // The first callback in the current lock.
324
+ self.dropdown.clear();
325
+ self._clearAtNext = false;
326
+ }
327
+ self.dropdown.setPosition(self.adapter.getCaretPosition());
328
+ self.dropdown.render(self._zip(data, strategy, term));
329
+ if (!stillSearching) {
330
+ // The last callback in the current lock.
331
+ free();
332
+ self._clearAtNext = true; // Call dropdown.clear at the next time.
333
+ }
334
+ }, match);
335
+ }),
336
+
337
+ // Build a parameter for Dropdown#render.
338
+ //
339
+ // Examples
340
+ //
341
+ // this._zip(['a', 'b'], 's');
342
+ // //=> [{ value: 'a', strategy: 's' }, { value: 'b', strategy: 's' }]
343
+ _zip: function (data, strategy, term) {
344
+ return $.map(data, function (value) {
345
+ return { value: value, strategy: strategy, term: term };
346
+ });
347
+ }
348
+ });
349
+
350
+ $.fn.textcomplete.Completer = Completer;
351
+ }(jQuery);
352
+
353
+ +function ($) {
354
+ 'use strict';
355
+
356
+ var $window = $(window);
357
+
358
+ var include = function (zippedData, datum) {
359
+ var i, elem;
360
+ var idProperty = datum.strategy.idProperty
361
+ for (i = 0; i < zippedData.length; i++) {
362
+ elem = zippedData[i];
363
+ if (elem.strategy !== datum.strategy) continue;
364
+ if (idProperty) {
365
+ if (elem.value[idProperty] === datum.value[idProperty]) return true;
366
+ } else {
367
+ if (elem.value === datum.value) return true;
368
+ }
369
+ }
370
+ return false;
371
+ };
372
+
373
+ var dropdownViews = {};
374
+ $(document).on('click', function (e) {
375
+ var id = e.originalEvent && e.originalEvent.keepTextCompleteDropdown;
376
+ $.each(dropdownViews, function (key, view) {
377
+ if (key !== id) { view.deactivate(); }
378
+ });
379
+ });
380
+
381
+ var commands = {
382
+ SKIP_DEFAULT: 0,
383
+ KEY_UP: 1,
384
+ KEY_DOWN: 2,
385
+ KEY_ENTER: 3,
386
+ KEY_PAGEUP: 4,
387
+ KEY_PAGEDOWN: 5,
388
+ KEY_ESCAPE: 6
389
+ };
390
+
391
+ // Dropdown view
392
+ // =============
393
+
394
+ // Construct Dropdown object.
395
+ //
396
+ // element - Textarea or contenteditable element.
397
+ function Dropdown(element, completer, option) {
398
+ this.$el = Dropdown.createElement(option);
399
+ this.completer = completer;
400
+ this.id = completer.id + 'dropdown';
401
+ this._data = []; // zipped data.
402
+ this.$inputEl = $(element);
403
+ this.option = option;
404
+
405
+ // Override setPosition method.
406
+ if (option.listPosition) { this.setPosition = option.listPosition; }
407
+ if (option.height) { this.$el.height(option.height); }
408
+ var self = this;
409
+ $.each(['maxCount', 'placement', 'footer', 'header', 'noResultsMessage', 'className'], function (_i, name) {
410
+ if (option[name] != null) { self[name] = option[name]; }
411
+ });
412
+ this._bindEvents(element);
413
+ dropdownViews[this.id] = this;
414
+ }
415
+
416
+ $.extend(Dropdown, {
417
+ // Class methods
418
+ // -------------
419
+
420
+ createElement: function (option) {
421
+ var $parent = option.appendTo;
422
+ if (!($parent instanceof $)) { $parent = $($parent); }
423
+ var $el = $('<ul></ul>')
424
+ .addClass(option.dropdownClassName)
425
+ .attr('id', 'textcomplete-dropdown-' + option._oid)
426
+ .css({
427
+ display: 'none',
428
+ left: 0,
429
+ position: 'absolute',
430
+ zIndex: option.zIndex
431
+ })
432
+ .appendTo($parent);
433
+ return $el;
434
+ }
435
+ });
436
+
437
+ $.extend(Dropdown.prototype, {
438
+ // Public properties
439
+ // -----------------
440
+
441
+ $el: null, // jQuery object of ul.dropdown-menu element.
442
+ $inputEl: null, // jQuery object of target textarea.
443
+ completer: null,
444
+ footer: null,
445
+ header: null,
446
+ id: null,
447
+ maxCount: null,
448
+ placement: '',
449
+ shown: false,
450
+ data: [], // Shown zipped data.
451
+ className: '',
452
+
453
+ // Public methods
454
+ // --------------
455
+
456
+ destroy: function () {
457
+ // Don't remove $el because it may be shared by several textcompletes.
458
+ this.deactivate();
459
+
460
+ this.$el.off('.' + this.id);
461
+ this.$inputEl.off('.' + this.id);
462
+ this.clear();
463
+ this.$el.remove();
464
+ this.$el = this.$inputEl = this.completer = null;
465
+ delete dropdownViews[this.id]
466
+ },
467
+
468
+ render: function (zippedData) {
469
+ var contentsHtml = this._buildContents(zippedData);
470
+ var unzippedData = $.map(zippedData, function (d) { return d.value; });
471
+ if (zippedData.length) {
472
+ var strategy = zippedData[0].strategy;
473
+ if (strategy.id) {
474
+ this.$el.attr('data-strategy', strategy.id);
475
+ } else {
476
+ this.$el.removeAttr('data-strategy');
477
+ }
478
+ this._renderHeader(unzippedData);
479
+ this._renderFooter(unzippedData);
480
+ if (contentsHtml) {
481
+ this._renderContents(contentsHtml);
482
+ this._fitToBottom();
483
+ this._fitToRight();
484
+ this._activateIndexedItem();
485
+ }
486
+ this._setScroll();
487
+ } else if (this.noResultsMessage) {
488
+ this._renderNoResultsMessage(unzippedData);
489
+ } else if (this.shown) {
490
+ this.deactivate();
491
+ }
492
+ },
493
+
494
+ setPosition: function (pos) {
495
+ // Make the dropdown fixed if the input is also fixed
496
+ // This can't be done during init, as textcomplete may be used on multiple elements on the same page
497
+ // Because the same dropdown is reused behind the scenes, we need to recheck every time the dropdown is showed
498
+ var position = 'absolute';
499
+ // Check if input or one of its parents has positioning we need to care about
500
+ this.$inputEl.add(this.$inputEl.parents()).each(function() {
501
+ if($(this).css('position') === 'absolute') // The element has absolute positioning, so it's all OK
502
+ return false;
503
+ if($(this).css('position') === 'fixed') {
504
+ pos.top -= $window.scrollTop();
505
+ pos.left -= $window.scrollLeft();
506
+ position = 'fixed';
507
+ return false;
508
+ }
509
+ });
510
+ this.$el.css(this._applyPlacement(pos));
511
+ this.$el.css({ position: position }); // Update positioning
512
+
513
+ return this;
514
+ },
515
+
516
+ clear: function () {
517
+ this.$el.html('');
518
+ this.data = [];
519
+ this._index = 0;
520
+ this._$header = this._$footer = this._$noResultsMessage = null;
521
+ },
522
+
523
+ activate: function () {
524
+ if (!this.shown) {
525
+ this.clear();
526
+ this.$el.show();
527
+ if (this.className) { this.$el.addClass(this.className); }
528
+ this.completer.fire('textComplete:show');
529
+ this.shown = true;
530
+ }
531
+ return this;
532
+ },
533
+
534
+ deactivate: function () {
535
+ if (this.shown) {
536
+ this.$el.hide();
537
+ if (this.className) { this.$el.removeClass(this.className); }
538
+ this.completer.fire('textComplete:hide');
539
+ this.shown = false;
540
+ }
541
+ return this;
542
+ },
543
+
544
+ isUp: function (e) {
545
+ return e.keyCode === 38 || (e.ctrlKey && e.keyCode === 80); // UP, Ctrl-P
546
+ },
547
+
548
+ isDown: function (e) {
549
+ return e.keyCode === 40 || (e.ctrlKey && e.keyCode === 78); // DOWN, Ctrl-N
550
+ },
551
+
552
+ isEnter: function (e) {
553
+ var modifiers = e.ctrlKey || e.altKey || e.metaKey || e.shiftKey;
554
+ return !modifiers && (e.keyCode === 13 || e.keyCode === 9 || (this.option.completeOnSpace === true && e.keyCode === 32)) // ENTER, TAB
555
+ },
556
+
557
+ isPageup: function (e) {
558
+ return e.keyCode === 33; // PAGEUP
559
+ },
560
+
561
+ isPagedown: function (e) {
562
+ return e.keyCode === 34; // PAGEDOWN
563
+ },
564
+
565
+ isEscape: function (e) {
566
+ return e.keyCode === 27; // ESCAPE
567
+ },
568
+
569
+ // Private properties
570
+ // ------------------
571
+
572
+ _data: null, // Currently shown zipped data.
573
+ _index: null,
574
+ _$header: null,
575
+ _$noResultsMessage: null,
576
+ _$footer: null,
577
+
578
+ // Private methods
579
+ // ---------------
580
+
581
+ _bindEvents: function () {
582
+ this.$el.on('mousedown.' + this.id, '.textcomplete-item', $.proxy(this._onClick, this));
583
+ this.$el.on('touchstart.' + this.id, '.textcomplete-item', $.proxy(this._onClick, this));
584
+ this.$el.on('mouseover.' + this.id, '.textcomplete-item', $.proxy(this._onMouseover, this));
585
+ this.$inputEl.on('keydown.' + this.id, $.proxy(this._onKeydown, this));
586
+ },
587
+
588
+ _onClick: function (e) {
589
+ var $el = $(e.target);
590
+ e.preventDefault();
591
+ e.originalEvent.keepTextCompleteDropdown = this.id;
592
+ if (!$el.hasClass('textcomplete-item')) {
593
+ $el = $el.closest('.textcomplete-item');
594
+ }
595
+ var datum = this.data[parseInt($el.data('index'), 10)];
596
+ this.completer.select(datum.value, datum.strategy, e);
597
+ var self = this;
598
+ // Deactive at next tick to allow other event handlers to know whether
599
+ // the dropdown has been shown or not.
600
+ setTimeout(function () {
601
+ self.deactivate();
602
+ if (e.type === 'touchstart') {
603
+ self.$inputEl.focus();
604
+ }
605
+ }, 0);
606
+ },
607
+
608
+ // Activate hovered item.
609
+ _onMouseover: function (e) {
610
+ var $el = $(e.target);
611
+ e.preventDefault();
612
+ if (!$el.hasClass('textcomplete-item')) {
613
+ $el = $el.closest('.textcomplete-item');
614
+ }
615
+ this._index = parseInt($el.data('index'), 10);
616
+ this._activateIndexedItem();
617
+ },
618
+
619
+ _onKeydown: function (e) {
620
+ if (!this.shown) { return; }
621
+
622
+ var command;
623
+
624
+ if ($.isFunction(this.option.onKeydown)) {
625
+ command = this.option.onKeydown(e, commands);
626
+ }
627
+
628
+ if (command == null) {
629
+ command = this._defaultKeydown(e);
630
+ }
631
+
632
+ switch (command) {
633
+ case commands.KEY_UP:
634
+ e.preventDefault();
635
+ this._up();
636
+ break;
637
+ case commands.KEY_DOWN:
638
+ e.preventDefault();
639
+ this._down();
640
+ break;
641
+ case commands.KEY_ENTER:
642
+ e.preventDefault();
643
+ this._enter(e);
644
+ break;
645
+ case commands.KEY_PAGEUP:
646
+ e.preventDefault();
647
+ this._pageup();
648
+ break;
649
+ case commands.KEY_PAGEDOWN:
650
+ e.preventDefault();
651
+ this._pagedown();
652
+ break;
653
+ case commands.KEY_ESCAPE:
654
+ e.preventDefault();
655
+ this.deactivate();
656
+ break;
657
+ }
658
+ },
659
+
660
+ _defaultKeydown: function (e) {
661
+ if (this.isUp(e)) {
662
+ return commands.KEY_UP;
663
+ } else if (this.isDown(e)) {
664
+ return commands.KEY_DOWN;
665
+ } else if (this.isEnter(e)) {
666
+ return commands.KEY_ENTER;
667
+ } else if (this.isPageup(e)) {
668
+ return commands.KEY_PAGEUP;
669
+ } else if (this.isPagedown(e)) {
670
+ return commands.KEY_PAGEDOWN;
671
+ } else if (this.isEscape(e)) {
672
+ return commands.KEY_ESCAPE;
673
+ }
674
+ },
675
+
676
+ _up: function () {
677
+ if (this._index === 0) {
678
+ this._index = this.data.length - 1;
679
+ } else {
680
+ this._index -= 1;
681
+ }
682
+ this._activateIndexedItem();
683
+ this._setScroll();
684
+ },
685
+
686
+ _down: function () {
687
+ if (this._index === this.data.length - 1) {
688
+ this._index = 0;
689
+ } else {
690
+ this._index += 1;
691
+ }
692
+ this._activateIndexedItem();
693
+ this._setScroll();
694
+ },
695
+
696
+ _enter: function (e) {
697
+ var datum = this.data[parseInt(this._getActiveElement().data('index'), 10)];
698
+ this.completer.select(datum.value, datum.strategy, e);
699
+ this.deactivate();
700
+ },
701
+
702
+ _pageup: function () {
703
+ var target = 0;
704
+ var threshold = this._getActiveElement().position().top - this.$el.innerHeight();
705
+ this.$el.children().each(function (i) {
706
+ if ($(this).position().top + $(this).outerHeight() > threshold) {
707
+ target = i;
708
+ return false;
709
+ }
710
+ });
711
+ this._index = target;
712
+ this._activateIndexedItem();
713
+ this._setScroll();
714
+ },
715
+
716
+ _pagedown: function () {
717
+ var target = this.data.length - 1;
718
+ var threshold = this._getActiveElement().position().top + this.$el.innerHeight();
719
+ this.$el.children().each(function (i) {
720
+ if ($(this).position().top > threshold) {
721
+ target = i;
722
+ return false
723
+ }
724
+ });
725
+ this._index = target;
726
+ this._activateIndexedItem();
727
+ this._setScroll();
728
+ },
729
+
730
+ _activateIndexedItem: function () {
731
+ this.$el.find('.textcomplete-item.active').removeClass('active');
732
+ this._getActiveElement().addClass('active');
733
+ },
734
+
735
+ _getActiveElement: function () {
736
+ return this.$el.children('.textcomplete-item:nth(' + this._index + ')');
737
+ },
738
+
739
+ _setScroll: function () {
740
+ var $activeEl = this._getActiveElement();
741
+ var itemTop = $activeEl.position().top;
742
+ var itemHeight = $activeEl.outerHeight();
743
+ var visibleHeight = this.$el.innerHeight();
744
+ var visibleTop = this.$el.scrollTop();
745
+ if (this._index === 0 || this._index == this.data.length - 1 || itemTop < 0) {
746
+ this.$el.scrollTop(itemTop + visibleTop);
747
+ } else if (itemTop + itemHeight > visibleHeight) {
748
+ this.$el.scrollTop(itemTop + itemHeight + visibleTop - visibleHeight);
749
+ }
750
+ },
751
+
752
+ _buildContents: function (zippedData) {
753
+ var datum, i, index;
754
+ var html = '';
755
+ for (i = 0; i < zippedData.length; i++) {
756
+ if (this.data.length === this.maxCount) break;
757
+ datum = zippedData[i];
758
+ if (include(this.data, datum)) { continue; }
759
+ index = this.data.length;
760
+ this.data.push(datum);
761
+ html += '<li class="textcomplete-item" data-index="' + index + '"><a>';
762
+ html += datum.strategy.template(datum.value, datum.term);
763
+ html += '</a></li>';
764
+ }
765
+ return html;
766
+ },
767
+
768
+ _renderHeader: function (unzippedData) {
769
+ if (this.header) {
770
+ if (!this._$header) {
771
+ this._$header = $('<li class="textcomplete-header"></li>').prependTo(this.$el);
772
+ }
773
+ var html = $.isFunction(this.header) ? this.header(unzippedData) : this.header;
774
+ this._$header.html(html);
775
+ }
776
+ },
777
+
778
+ _renderFooter: function (unzippedData) {
779
+ if (this.footer) {
780
+ if (!this._$footer) {
781
+ this._$footer = $('<li class="textcomplete-footer"></li>').appendTo(this.$el);
782
+ }
783
+ var html = $.isFunction(this.footer) ? this.footer(unzippedData) : this.footer;
784
+ this._$footer.html(html);
785
+ }
786
+ },
787
+
788
+ _renderNoResultsMessage: function (unzippedData) {
789
+ if (this.noResultsMessage) {
790
+ if (!this._$noResultsMessage) {
791
+ this._$noResultsMessage = $('<li class="textcomplete-no-results-message"></li>').appendTo(this.$el);
792
+ }
793
+ var html = $.isFunction(this.noResultsMessage) ? this.noResultsMessage(unzippedData) : this.noResultsMessage;
794
+ this._$noResultsMessage.html(html);
795
+ }
796
+ },
797
+
798
+ _renderContents: function (html) {
799
+ if (this._$footer) {
800
+ this._$footer.before(html);
801
+ } else {
802
+ this.$el.append(html);
803
+ }
804
+ },
805
+
806
+ _fitToBottom: function() {
807
+ var windowScrollBottom = $window.scrollTop() + $window.height();
808
+ var height = this.$el.height();
809
+ if ((this.$el.position().top + height) > windowScrollBottom) {
810
+ // only do this if we are not in an iframe
811
+ if (!this.completer.$iframe) {
812
+ this.$el.offset({top: windowScrollBottom - height});
813
+ }
814
+ }
815
+ },
816
+
817
+ _fitToRight: function() {
818
+ // We don't know how wide our content is until the browser positions us, and at that point it clips us
819
+ // to the document width so we don't know if we would have overrun it. As a heuristic to avoid that clipping
820
+ // (which makes our elements wrap onto the next line and corrupt the next item), if we're close to the right
821
+ // edge, move left. We don't know how far to move left, so just keep nudging a bit.
822
+ var tolerance = this.option.rightEdgeOffset; // pixels. Make wider than vertical scrollbar because we might not be able to use that space.
823
+ var lastOffset = this.$el.offset().left, offset;
824
+ var width = this.$el.width();
825
+ var maxLeft = $window.width() - tolerance;
826
+ while (lastOffset + width > maxLeft) {
827
+ this.$el.offset({left: lastOffset - tolerance});
828
+ offset = this.$el.offset().left;
829
+ if (offset >= lastOffset) { break; }
830
+ lastOffset = offset;
831
+ }
832
+ },
833
+
834
+ _applyPlacement: function (position) {
835
+ // If the 'placement' option set to 'top', move the position above the element.
836
+ if (this.placement.indexOf('top') !== -1) {
837
+ // Overwrite the position object to set the 'bottom' property instead of the top.
838
+ position = {
839
+ top: 'auto',
840
+ bottom: this.$el.parent().height() - position.top + position.lineHeight,
841
+ left: position.left
842
+ };
843
+ } else {
844
+ position.bottom = 'auto';
845
+ delete position.lineHeight;
846
+ }
847
+ if (this.placement.indexOf('absleft') !== -1) {
848
+ position.left = 0;
849
+ } else if (this.placement.indexOf('absright') !== -1) {
850
+ position.right = 0;
851
+ position.left = 'auto';
852
+ }
853
+ return position;
854
+ }
855
+ });
856
+
857
+ $.fn.textcomplete.Dropdown = Dropdown;
858
+ $.extend($.fn.textcomplete, commands);
859
+ }(jQuery);
860
+
861
+ +function ($) {
862
+ 'use strict';
863
+
864
+ // Memoize a search function.
865
+ var memoize = function (func) {
866
+ var memo = {};
867
+ return function (term, callback) {
868
+ if (memo[term]) {
869
+ callback(memo[term]);
870
+ } else {
871
+ func.call(this, term, function (data) {
872
+ memo[term] = (memo[term] || []).concat(data);
873
+ callback.apply(null, arguments);
874
+ });
875
+ }
876
+ };
877
+ };
878
+
879
+ function Strategy(options) {
880
+ $.extend(this, options);
881
+ if (this.cache) { this.search = memoize(this.search); }
882
+ }
883
+
884
+ Strategy.parse = function (strategiesArray, params) {
885
+ return $.map(strategiesArray, function (strategy) {
886
+ var strategyObj = new Strategy(strategy);
887
+ strategyObj.el = params.el;
888
+ strategyObj.$el = params.$el;
889
+ return strategyObj;
890
+ });
891
+ };
892
+
893
+ $.extend(Strategy.prototype, {
894
+ // Public properties
895
+ // -----------------
896
+
897
+ // Required
898
+ match: null,
899
+ replace: null,
900
+ search: null,
901
+
902
+ // Optional
903
+ id: null,
904
+ cache: false,
905
+ context: function () { return true; },
906
+ index: 2,
907
+ template: function (obj) { return obj; },
908
+ idProperty: null
909
+ });
910
+
911
+ $.fn.textcomplete.Strategy = Strategy;
912
+
913
+ }(jQuery);
914
+
915
+ +function ($) {
916
+ 'use strict';
917
+
918
+ var now = Date.now || function () { return new Date().getTime(); };
919
+
920
+ // Returns a function, that, as long as it continues to be invoked, will not
921
+ // be triggered. The function will be called after it stops being called for
922
+ // `wait` msec.
923
+ //
924
+ // This utility function was originally implemented at Underscore.js.
925
+ var debounce = function (func, wait) {
926
+ var timeout, args, context, timestamp, result;
927
+ var later = function () {
928
+ var last = now() - timestamp;
929
+ if (last < wait) {
930
+ timeout = setTimeout(later, wait - last);
931
+ } else {
932
+ timeout = null;
933
+ result = func.apply(context, args);
934
+ context = args = null;
935
+ }
936
+ };
937
+
938
+ return function () {
939
+ context = this;
940
+ args = arguments;
941
+ timestamp = now();
942
+ if (!timeout) {
943
+ timeout = setTimeout(later, wait);
944
+ }
945
+ return result;
946
+ };
947
+ };
948
+
949
+ function Adapter () {}
950
+
951
+ $.extend(Adapter.prototype, {
952
+ // Public properties
953
+ // -----------------
954
+
955
+ id: null, // Identity.
956
+ completer: null, // Completer object which creates it.
957
+ el: null, // Textarea element.
958
+ $el: null, // jQuery object of the textarea.
959
+ option: null,
960
+
961
+ // Public methods
962
+ // --------------
963
+
964
+ initialize: function (element, completer, option) {
965
+ this.el = element;
966
+ this.$el = $(element);
967
+ this.id = completer.id + this.constructor.name;
968
+ this.completer = completer;
969
+ this.option = option;
970
+
971
+ if (this.option.debounce) {
972
+ this._onKeyup = debounce(this._onKeyup, this.option.debounce);
973
+ }
974
+
975
+ this._bindEvents();
976
+ },
977
+
978
+ destroy: function () {
979
+ this.$el.off('.' + this.id); // Remove all event handlers.
980
+ this.$el = this.el = this.completer = null;
981
+ },
982
+
983
+ // Update the element with the given value and strategy.
984
+ //
985
+ // value - The selected object. It is one of the item of the array
986
+ // which was callbacked from the search function.
987
+ // strategy - The Strategy associated with the selected value.
988
+ select: function (/* value, strategy */) {
989
+ throw new Error('Not implemented');
990
+ },
991
+
992
+ // Returns the caret's relative coordinates from body's left top corner.
993
+ getCaretPosition: function () {
994
+ var position = this._getCaretRelativePosition();
995
+ var offset = this.$el.offset();
996
+
997
+ // Calculate the left top corner of `this.option.appendTo` element.
998
+ var $parent = this.option.appendTo;
999
+ if ($parent) {
1000
+ if (!($parent instanceof $)) { $parent = $($parent); }
1001
+ var parentOffset = $parent.offsetParent().offset();
1002
+ offset.top -= parentOffset.top;
1003
+ offset.left -= parentOffset.left;
1004
+ }
1005
+
1006
+ position.top += offset.top;
1007
+ position.left += offset.left;
1008
+ return position;
1009
+ },
1010
+
1011
+ // Focus on the element.
1012
+ focus: function () {
1013
+ this.$el.focus();
1014
+ },
1015
+
1016
+ // Private methods
1017
+ // ---------------
1018
+
1019
+ _bindEvents: function () {
1020
+ this.$el.on('keyup.' + this.id, $.proxy(this._onKeyup, this));
1021
+ },
1022
+
1023
+ _onKeyup: function (e) {
1024
+ if (this._skipSearch(e)) { return; }
1025
+ this.completer.trigger(this.getTextFromHeadToCaret(), true);
1026
+ },
1027
+
1028
+ // Suppress searching if it returns true.
1029
+ _skipSearch: function (clickEvent) {
1030
+ switch (clickEvent.keyCode) {
1031
+ case 9: // TAB
1032
+ case 13: // ENTER
1033
+ case 16: // SHIFT
1034
+ case 17: // CTRL
1035
+ case 18: // ALT
1036
+ case 33: // PAGEUP
1037
+ case 34: // PAGEDOWN
1038
+ case 40: // DOWN
1039
+ case 38: // UP
1040
+ case 27: // ESC
1041
+ return true;
1042
+ }
1043
+ if (clickEvent.ctrlKey) switch (clickEvent.keyCode) {
1044
+ case 78: // Ctrl-N
1045
+ case 80: // Ctrl-P
1046
+ return true;
1047
+ }
1048
+ }
1049
+ });
1050
+
1051
+ $.fn.textcomplete.Adapter = Adapter;
1052
+ }(jQuery);
1053
+
1054
+ +function ($) {
1055
+ 'use strict';
1056
+
1057
+ // Textarea adapter
1058
+ // ================
1059
+ //
1060
+ // Managing a textarea. It doesn't know a Dropdown.
1061
+ function Textarea(element, completer, option) {
1062
+ this.initialize(element, completer, option);
1063
+ }
1064
+
1065
+ $.extend(Textarea.prototype, $.fn.textcomplete.Adapter.prototype, {
1066
+ // Public methods
1067
+ // --------------
1068
+
1069
+ // Update the textarea with the given value and strategy.
1070
+ select: function (value, strategy, e) {
1071
+ var pre = this.getTextFromHeadToCaret();
1072
+ var post = this.el.value.substring(this.el.selectionEnd);
1073
+ var newSubstr = strategy.replace(value, e);
1074
+ var regExp;
1075
+ if (typeof newSubstr !== 'undefined') {
1076
+ if ($.isArray(newSubstr)) {
1077
+ post = newSubstr[1] + post;
1078
+ newSubstr = newSubstr[0];
1079
+ }
1080
+ regExp = $.isFunction(strategy.match) ? strategy.match(pre) : strategy.match;
1081
+ pre = pre.replace(regExp, newSubstr);
1082
+ this.$el.val(pre + post);
1083
+ this.el.selectionStart = this.el.selectionEnd = pre.length;
1084
+ }
1085
+ },
1086
+
1087
+ getTextFromHeadToCaret: function () {
1088
+ return this.el.value.substring(0, this.el.selectionEnd);
1089
+ },
1090
+
1091
+ // Private methods
1092
+ // ---------------
1093
+
1094
+ _getCaretRelativePosition: function () {
1095
+ var p = $.fn.textcomplete.getCaretCoordinates(this.el, this.el.selectionStart);
1096
+ return {
1097
+ top: p.top + this._calculateLineHeight() - this.$el.scrollTop(),
1098
+ left: p.left - this.$el.scrollLeft(),
1099
+ lineHeight: this._calculateLineHeight()
1100
+ };
1101
+ },
1102
+
1103
+ _calculateLineHeight: function () {
1104
+ var lineHeight = parseInt(this.$el.css('line-height'), 10);
1105
+ if (isNaN(lineHeight)) {
1106
+ // http://stackoverflow.com/a/4515470/1297336
1107
+ var parentNode = this.el.parentNode;
1108
+ var temp = document.createElement(this.el.nodeName);
1109
+ var style = this.el.style;
1110
+ temp.setAttribute(
1111
+ 'style',
1112
+ 'margin:0px;padding:0px;font-family:' + style.fontFamily + ';font-size:' + style.fontSize
1113
+ );
1114
+ temp.innerHTML = 'test';
1115
+ parentNode.appendChild(temp);
1116
+ lineHeight = temp.clientHeight;
1117
+ parentNode.removeChild(temp);
1118
+ }
1119
+ return lineHeight;
1120
+ }
1121
+ });
1122
+
1123
+ $.fn.textcomplete.Textarea = Textarea;
1124
+ }(jQuery);
1125
+
1126
+ +function ($) {
1127
+ 'use strict';
1128
+
1129
+ var sentinelChar = '吶';
1130
+
1131
+ function IETextarea(element, completer, option) {
1132
+ this.initialize(element, completer, option);
1133
+ $('<span>' + sentinelChar + '</span>').css({
1134
+ position: 'absolute',
1135
+ top: -9999,
1136
+ left: -9999
1137
+ }).insertBefore(element);
1138
+ }
1139
+
1140
+ $.extend(IETextarea.prototype, $.fn.textcomplete.Textarea.prototype, {
1141
+ // Public methods
1142
+ // --------------
1143
+
1144
+ select: function (value, strategy, e) {
1145
+ var pre = this.getTextFromHeadToCaret();
1146
+ var post = this.el.value.substring(pre.length);
1147
+ var newSubstr = strategy.replace(value, e);
1148
+ var regExp;
1149
+ if (typeof newSubstr !== 'undefined') {
1150
+ if ($.isArray(newSubstr)) {
1151
+ post = newSubstr[1] + post;
1152
+ newSubstr = newSubstr[0];
1153
+ }
1154
+ regExp = $.isFunction(strategy.match) ? strategy.match(pre) : strategy.match;
1155
+ pre = pre.replace(regExp, newSubstr);
1156
+ this.$el.val(pre + post);
1157
+ this.el.focus();
1158
+ var range = this.el.createTextRange();
1159
+ range.collapse(true);
1160
+ range.moveEnd('character', pre.length);
1161
+ range.moveStart('character', pre.length);
1162
+ range.select();
1163
+ }
1164
+ },
1165
+
1166
+ getTextFromHeadToCaret: function () {
1167
+ this.el.focus();
1168
+ var range = document.selection.createRange();
1169
+ range.moveStart('character', -this.el.value.length);
1170
+ var arr = range.text.split(sentinelChar)
1171
+ return arr.length === 1 ? arr[0] : arr[1];
1172
+ }
1173
+ });
1174
+
1175
+ $.fn.textcomplete.IETextarea = IETextarea;
1176
+ }(jQuery);
1177
+
1178
+ // NOTE: TextComplete plugin has contenteditable support but it does not work
1179
+ // fine especially on old IEs.
1180
+ // Any pull requests are REALLY welcome.
1181
+
1182
+ +function ($) {
1183
+ 'use strict';
1184
+
1185
+ // ContentEditable adapter
1186
+ // =======================
1187
+ //
1188
+ // Adapter for contenteditable elements.
1189
+ function ContentEditable (element, completer, option) {
1190
+ this.initialize(element, completer, option);
1191
+ }
1192
+
1193
+ $.extend(ContentEditable.prototype, $.fn.textcomplete.Adapter.prototype, {
1194
+ // Public methods
1195
+ // --------------
1196
+
1197
+ // Update the content with the given value and strategy.
1198
+ // When an dropdown item is selected, it is executed.
1199
+ select: function (value, strategy, e) {
1200
+ var pre = this.getTextFromHeadToCaret();
1201
+ // use ownerDocument instead of window to support iframes
1202
+ var sel = this.el.ownerDocument.getSelection();
1203
+
1204
+ var range = sel.getRangeAt(0);
1205
+ var selection = range.cloneRange();
1206
+ selection.selectNodeContents(range.startContainer);
1207
+ var content = selection.toString();
1208
+ var post = content.substring(range.startOffset);
1209
+ var newSubstr = strategy.replace(value, e);
1210
+ var regExp;
1211
+ if (typeof newSubstr !== 'undefined') {
1212
+ if ($.isArray(newSubstr)) {
1213
+ post = newSubstr[1] + post;
1214
+ newSubstr = newSubstr[0];
1215
+ }
1216
+ regExp = $.isFunction(strategy.match) ? strategy.match(pre) : strategy.match;
1217
+ pre = pre.replace(regExp, newSubstr)
1218
+ .replace(/ $/, "&nbsp"); // &nbsp necessary at least for CKeditor to not eat spaces
1219
+ range.selectNodeContents(range.startContainer);
1220
+ range.deleteContents();
1221
+
1222
+ // create temporary elements
1223
+ var preWrapper = this.el.ownerDocument.createElement("div");
1224
+ preWrapper.innerHTML = pre;
1225
+ var postWrapper = this.el.ownerDocument.createElement("div");
1226
+ postWrapper.innerHTML = post;
1227
+
1228
+ // create the fragment thats inserted
1229
+ var fragment = this.el.ownerDocument.createDocumentFragment();
1230
+ var childNode;
1231
+ var lastOfPre;
1232
+ while (childNode = preWrapper.firstChild) {
1233
+ lastOfPre = fragment.appendChild(childNode);
1234
+ }
1235
+ while (childNode = postWrapper.firstChild) {
1236
+ fragment.appendChild(childNode);
1237
+ }
1238
+
1239
+ // insert the fragment & jump behind the last node in "pre"
1240
+ range.insertNode(fragment);
1241
+ range.setStartAfter(lastOfPre);
1242
+
1243
+ range.collapse(true);
1244
+ sel.removeAllRanges();
1245
+ sel.addRange(range);
1246
+ }
1247
+ },
1248
+
1249
+ // Private methods
1250
+ // ---------------
1251
+
1252
+ // Returns the caret's relative position from the contenteditable's
1253
+ // left top corner.
1254
+ //
1255
+ // Examples
1256
+ //
1257
+ // this._getCaretRelativePosition()
1258
+ // //=> { top: 18, left: 200, lineHeight: 16 }
1259
+ //
1260
+ // Dropdown's position will be decided using the result.
1261
+ _getCaretRelativePosition: function () {
1262
+ var range = this.el.ownerDocument.getSelection().getRangeAt(0).cloneRange();
1263
+ var wrapperNode = range.endContainer.parentNode;
1264
+ var node = this.el.ownerDocument.createElement('span');
1265
+ range.insertNode(node);
1266
+ range.selectNodeContents(node);
1267
+ range.deleteContents();
1268
+ setTimeout(function() { wrapperNode.normalize(); }, 0);
1269
+ var $node = $(node);
1270
+ var position = $node.offset();
1271
+ position.left -= this.$el.offset().left;
1272
+ position.top += $node.height() - this.$el.offset().top;
1273
+ position.lineHeight = $node.height();
1274
+
1275
+ // special positioning logic for iframes
1276
+ // this is typically used for contenteditables such as tinymce or ckeditor
1277
+ if (this.completer.$iframe) {
1278
+ var iframePosition = this.completer.$iframe.offset();
1279
+ position.top += iframePosition.top;
1280
+ position.left += iframePosition.left;
1281
+ // We need to get the scrollTop of the html-element inside the iframe and not of the body-element,
1282
+ // because on IE the scrollTop of the body-element (this.$el) is always zero.
1283
+ position.top -= $(this.completer.$iframe[0].contentWindow.document).scrollTop();
1284
+ }
1285
+
1286
+ $node.remove();
1287
+ return position;
1288
+ },
1289
+
1290
+ // Returns the string between the first character and the caret.
1291
+ // Completer will be triggered with the result for start autocompleting.
1292
+ //
1293
+ // Example
1294
+ //
1295
+ // // Suppose the html is '<b>hello</b> wor|ld' and | is the caret.
1296
+ // this.getTextFromHeadToCaret()
1297
+ // // => ' wor' // not '<b>hello</b> wor'
1298
+ getTextFromHeadToCaret: function () {
1299
+ var range = this.el.ownerDocument.getSelection().getRangeAt(0);
1300
+ var selection = range.cloneRange();
1301
+ selection.selectNodeContents(range.startContainer);
1302
+ return selection.toString().substring(0, range.startOffset);
1303
+ }
1304
+ });
1305
+
1306
+ $.fn.textcomplete.ContentEditable = ContentEditable;
1307
+ }(jQuery);
1308
+
1309
+ // NOTE: TextComplete plugin has contenteditable support but it does not work
1310
+ // fine especially on old IEs.
1311
+ // Any pull requests are REALLY welcome.
1312
+
1313
+ +function ($) {
1314
+ 'use strict';
1315
+
1316
+ // CKEditor adapter
1317
+ // =======================
1318
+ //
1319
+ // Adapter for CKEditor, based on contenteditable elements.
1320
+ function CKEditor (element, completer, option) {
1321
+ this.initialize(element, completer, option);
1322
+ }
1323
+
1324
+ $.extend(CKEditor.prototype, $.fn.textcomplete.ContentEditable.prototype, {
1325
+ _bindEvents: function () {
1326
+ var $this = this;
1327
+ this.option.ckeditor_instance.on('key', function(event) {
1328
+ var domEvent = event.data;
1329
+ $this._onKeyup(domEvent);
1330
+ if ($this.completer.dropdown.shown && $this._skipSearch(domEvent)) {
1331
+ return false;
1332
+ }
1333
+ }, null, null, 1); // 1 = Priority = Important!
1334
+ // we actually also need the native event, as the CKEditor one is happening to late
1335
+ this.$el.on('keyup.' + this.id, $.proxy(this._onKeyup, this));
1336
+ },
1337
+ });
1338
+
1339
+ $.fn.textcomplete.CKEditor = CKEditor;
1340
+ }(jQuery);
1341
+
1342
+ // The MIT License (MIT)
1343
+ //
1344
+ // Copyright (c) 2015 Jonathan Ong me@jongleberry.com
1345
+ //
1346
+ // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
1347
+ // associated documentation files (the "Software"), to deal in the Software without restriction,
1348
+ // including without limitation the rights to use, copy, modify, merge, publish, distribute,
1349
+ // sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
1350
+ // furnished to do so, subject to the following conditions:
1351
+ //
1352
+ // The above copyright notice and this permission notice shall be included in all copies or
1353
+ // substantial portions of the Software.
1354
+ //
1355
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
1356
+ // NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
1357
+ // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
1358
+ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1359
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1360
+ //
1361
+ // https://github.com/component/textarea-caret-position
1362
+
1363
+ (function ($) {
1364
+
1365
+ // The properties that we copy into a mirrored div.
1366
+ // Note that some browsers, such as Firefox,
1367
+ // do not concatenate properties, i.e. padding-top, bottom etc. -> padding,
1368
+ // so we have to do every single property specifically.
1369
+ var properties = [
1370
+ 'direction', // RTL support
1371
+ 'boxSizing',
1372
+ 'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
1373
+ 'height',
1374
+ 'overflowX',
1375
+ 'overflowY', // copy the scrollbar for IE
1376
+
1377
+ 'borderTopWidth',
1378
+ 'borderRightWidth',
1379
+ 'borderBottomWidth',
1380
+ 'borderLeftWidth',
1381
+ 'borderStyle',
1382
+
1383
+ 'paddingTop',
1384
+ 'paddingRight',
1385
+ 'paddingBottom',
1386
+ 'paddingLeft',
1387
+
1388
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/font
1389
+ 'fontStyle',
1390
+ 'fontVariant',
1391
+ 'fontWeight',
1392
+ 'fontStretch',
1393
+ 'fontSize',
1394
+ 'fontSizeAdjust',
1395
+ 'lineHeight',
1396
+ 'fontFamily',
1397
+
1398
+ 'textAlign',
1399
+ 'textTransform',
1400
+ 'textIndent',
1401
+ 'textDecoration', // might not make a difference, but better be safe
1402
+
1403
+ 'letterSpacing',
1404
+ 'wordSpacing',
1405
+
1406
+ 'tabSize',
1407
+ 'MozTabSize'
1408
+
1409
+ ];
1410
+
1411
+ var isBrowser = (typeof window !== 'undefined');
1412
+ var isFirefox = (isBrowser && window.mozInnerScreenX != null);
1413
+
1414
+ function getCaretCoordinates(element, position, options) {
1415
+ if(!isBrowser) {
1416
+ throw new Error('textarea-caret-position#getCaretCoordinates should only be called in a browser');
1417
+ }
1418
+
1419
+ var debug = options && options.debug || false;
1420
+ if (debug) {
1421
+ var el = document.querySelector('#input-textarea-caret-position-mirror-div');
1422
+ if ( el ) { el.parentNode.removeChild(el); }
1423
+ }
1424
+
1425
+ // mirrored div
1426
+ var div = document.createElement('div');
1427
+ div.id = 'input-textarea-caret-position-mirror-div';
1428
+ document.body.appendChild(div);
1429
+
1430
+ var style = div.style;
1431
+ var computed = window.getComputedStyle? getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9
1432
+
1433
+ // default textarea styles
1434
+ style.whiteSpace = 'pre-wrap';
1435
+ if (element.nodeName !== 'INPUT')
1436
+ style.wordWrap = 'break-word'; // only for textarea-s
1437
+
1438
+ // position off-screen
1439
+ style.position = 'absolute'; // required to return coordinates properly
1440
+ if (!debug)
1441
+ style.visibility = 'hidden'; // not 'display: none' because we want rendering
1442
+
1443
+ // transfer the element's properties to the div
1444
+ properties.forEach(function (prop) {
1445
+ style[prop] = computed[prop];
1446
+ });
1447
+
1448
+ if (isFirefox) {
1449
+ // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
1450
+ if (element.scrollHeight > parseInt(computed.height))
1451
+ style.overflowY = 'scroll';
1452
+ } else {
1453
+ style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
1454
+ }
1455
+
1456
+ div.textContent = element.value.substring(0, position);
1457
+ // 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
1458
+ if (element.nodeName === 'INPUT')
1459
+ div.textContent = div.textContent.replace(/\s/g, '\u00a0');
1460
+
1461
+ var span = document.createElement('span');
1462
+ // Wrapping must be replicated *exactly*, including when a long word gets
1463
+ // onto the next line, with whitespace at the end of the line before (#7).
1464
+ // The *only* reliable way to do that is to copy the *entire* rest of the
1465
+ // textarea's content into the <span> created at the caret position.
1466
+ // for inputs, just '.' would be enough, but why bother?
1467
+ span.textContent = element.value.substring(position) || '.'; // || because a completely empty faux span doesn't render at all
1468
+ div.appendChild(span);
1469
+
1470
+ var coordinates = {
1471
+ top: span.offsetTop + parseInt(computed['borderTopWidth']),
1472
+ left: span.offsetLeft + parseInt(computed['borderLeftWidth'])
1473
+ };
1474
+
1475
+ if (debug) {
1476
+ span.style.backgroundColor = '#aaa';
1477
+ } else {
1478
+ document.body.removeChild(div);
1479
+ }
1480
+
1481
+ return coordinates;
1482
+ }
1483
+
1484
+ $.fn.textcomplete.getCaretCoordinates = getCaretCoordinates;
1485
+
1486
+ }(jQuery));
1487
+
1488
+ return jQuery;
1489
+ }));