thredded 0.12.4 → 0.13.0

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