jquery.textcomplete.js 46 KB

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