PageRenderTime 30ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/vendor/assets/javascripts/jquery.mentionsInput.js

https://bitbucket.org/cfield/diaspora
JavaScript | 423 lines | 384 code | 27 blank | 12 comment | 11 complexity | 32ce1fc777b84239d096f2ff6490516e MD5 | raw file
  1. /*
  2. * Mentions Input
  3. * Version 1.0.2
  4. * Written by: Kenneth Auchenberg (Podio)
  5. *
  6. * Using underscore.js
  7. *
  8. * License: MIT License - http://www.opensource.org/licenses/mit-license.php
  9. */
  10. (function ($, _, undefined) {
  11. // Settings
  12. var KEY = { BACKSPACE : 8, TAB : 9, RETURN : 13, ESC : 27, LEFT : 37, UP : 38, RIGHT : 39, DOWN : 40, COMMA : 188, SPACE : 32, HOME : 36, END : 35 }; // Keys "enum"
  13. var defaultSettings = {
  14. triggerChar : '@',
  15. onDataRequest : $.noop,
  16. minChars : 2,
  17. showAvatars : true,
  18. elastic : true,
  19. classes : {
  20. autoCompleteItemActive : "active"
  21. },
  22. templates : {
  23. wrapper : _.template('<div class="mentions-input-box"></div>'),
  24. autocompleteList : _.template('<div class="mentions-autocomplete-list"></div>'),
  25. autocompleteListItem : _.template('<li data-ref-id="<%= id %>" data-ref-type="<%= type %>" data-display="<%= display %>"><%= content %></li>'),
  26. autocompleteListItemAvatar : _.template('<img src="<%= avatar %>" />'),
  27. autocompleteListItemIcon : _.template('<div class="icon <%= icon %>"></div>'),
  28. mentionsOverlay : _.template('<div class="mentions"><div></div></div>'),
  29. mentionItemSyntax : _.template('@[<%= value %>](<%= type %>:<%= id %>)'),
  30. mentionItemHighlight : _.template('<strong><span><%= value %></span></strong>')
  31. }
  32. };
  33. var utils = {
  34. htmlEncode : function (str) {
  35. return _.escape(str);
  36. },
  37. highlightTerm : function (value, term) {
  38. if (!term && !term.length) {
  39. return value;
  40. }
  41. return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
  42. },
  43. setCaratPosition : function (domNode, caretPos) {
  44. if (domNode.createTextRange) {
  45. var range = domNode.createTextRange();
  46. range.move('character', caretPos);
  47. range.select();
  48. } else {
  49. if (domNode.selectionStart) {
  50. domNode.focus();
  51. domNode.setSelectionRange(caretPos, caretPos);
  52. } else {
  53. domNode.focus();
  54. }
  55. }
  56. },
  57. rtrim: function(string) {
  58. return string.replace(/\s+$/,"");
  59. }
  60. };
  61. var MentionsInput = function (settings) {
  62. var domInput, elmInputBox, elmInputWrapper, elmAutocompleteList, elmWrapperBox, elmMentionsOverlay, elmActiveAutoCompleteItem;
  63. var mentionsCollection = [];
  64. var autocompleteItemCollection = {};
  65. var inputBuffer = [];
  66. var currentDataQuery = '';
  67. settings = $.extend(true, {}, defaultSettings, settings );
  68. function initTextarea() {
  69. elmInputBox = $(domInput);
  70. if (elmInputBox.attr('data-mentions-input') == 'true') {
  71. return;
  72. }
  73. elmInputWrapper = elmInputBox.parent();
  74. elmWrapperBox = $(settings.templates.wrapper());
  75. elmInputBox.wrapAll(elmWrapperBox);
  76. elmWrapperBox = elmInputWrapper.find('> div');
  77. elmInputBox.attr('data-mentions-input', 'true');
  78. elmInputBox.bind('keydown', onInputBoxKeyDown);
  79. elmInputBox.bind('keypress', onInputBoxKeyPress);
  80. elmInputBox.bind('input', onInputBoxInput);
  81. elmInputBox.bind('click', onInputBoxClick);
  82. elmInputBox.bind('blur', onInputBoxBlur);
  83. // Elastic textareas, internal setting for the Dispora guys
  84. if( settings.elastic ) {
  85. elmInputBox.elastic();
  86. }
  87. }
  88. function initAutocomplete() {
  89. elmAutocompleteList = $(settings.templates.autocompleteList());
  90. elmAutocompleteList.appendTo(elmWrapperBox);
  91. elmAutocompleteList.delegate('li', 'mousedown', onAutoCompleteItemClick);
  92. }
  93. function initMentionsOverlay() {
  94. elmMentionsOverlay = $(settings.templates.mentionsOverlay());
  95. elmMentionsOverlay.prependTo(elmWrapperBox);
  96. }
  97. function updateValues() {
  98. var syntaxMessage = getInputBoxValue();
  99. _.each(mentionsCollection, function (mention) {
  100. var textSyntax = settings.templates.mentionItemSyntax(mention);
  101. syntaxMessage = syntaxMessage.replace(mention.value, textSyntax);
  102. });
  103. var mentionText = utils.htmlEncode(syntaxMessage);
  104. _.each(mentionsCollection, function (mention) {
  105. var formattedMention = _.extend({}, mention, {value: utils.htmlEncode(mention.value)});
  106. var textSyntax = settings.templates.mentionItemSyntax(formattedMention);
  107. var textHighlight = settings.templates.mentionItemHighlight(formattedMention);
  108. mentionText = mentionText.replace(textSyntax, textHighlight);
  109. });
  110. mentionText = mentionText.replace(/\n/g, '<br />');
  111. mentionText = mentionText.replace(/ {2}/g, '&nbsp; ');
  112. elmInputBox.data('messageText', syntaxMessage);
  113. elmMentionsOverlay.find('div').html(mentionText);
  114. }
  115. function resetBuffer() {
  116. inputBuffer = [];
  117. }
  118. function updateMentionsCollection() {
  119. var inputText = getInputBoxValue();
  120. mentionsCollection = _.reject(mentionsCollection, function (mention, index) {
  121. return !mention.value || inputText.indexOf(mention.value) == -1;
  122. });
  123. mentionsCollection = _.compact(mentionsCollection);
  124. }
  125. function addMention(mention) {
  126. var currentMessage = getInputBoxValue();
  127. // Using a regex to figure out positions
  128. var regex = new RegExp("\\" + settings.triggerChar + currentDataQuery, "gi");
  129. regex.exec(currentMessage);
  130. var startCaretPosition = regex.lastIndex - currentDataQuery.length - 1;
  131. var currentCaretPosition = regex.lastIndex;
  132. var start = currentMessage.substr(0, startCaretPosition);
  133. var end = currentMessage.substr(currentCaretPosition, currentMessage.length);
  134. var startEndIndex = (start + mention.value).length + 1;
  135. mentionsCollection.push(mention);
  136. // Cleaning before inserting the value, otherwise auto-complete would be triggered with "old" inputbuffer
  137. resetBuffer();
  138. currentDataQuery = '';
  139. hideAutoComplete();
  140. // Mentions & syntax message
  141. var updatedMessageText = start + mention.value + ' ' + end;
  142. elmInputBox.val(updatedMessageText);
  143. updateValues();
  144. // Set correct focus and selection
  145. elmInputBox.focus();
  146. utils.setCaratPosition(elmInputBox[0], startEndIndex);
  147. }
  148. function getInputBoxValue() {
  149. return $.trim(elmInputBox.val());
  150. }
  151. function onAutoCompleteItemClick(e) {
  152. var elmTarget = $(this);
  153. var mention = autocompleteItemCollection[elmTarget.attr('data-uid')];
  154. addMention(mention);
  155. return false;
  156. }
  157. function onInputBoxClick(e) {
  158. resetBuffer();
  159. }
  160. function onInputBoxBlur(e) {
  161. hideAutoComplete();
  162. }
  163. function onInputBoxInput(e) {
  164. updateValues();
  165. updateMentionsCollection();
  166. hideAutoComplete();
  167. var triggerCharIndex = _.lastIndexOf(inputBuffer, settings.triggerChar);
  168. if (triggerCharIndex > -1) {
  169. currentDataQuery = inputBuffer.slice(triggerCharIndex + 1).join('');
  170. currentDataQuery = utils.rtrim(currentDataQuery);
  171. _.defer(_.bind(doSearch, this, currentDataQuery));
  172. }
  173. }
  174. function onInputBoxKeyPress(e) {
  175. if(e.keyCode !== KEY.BACKSPACE) {
  176. var typedValue = String.fromCharCode(e.which || e.keyCode);
  177. inputBuffer.push(typedValue);
  178. }
  179. }
  180. function onInputBoxKeyDown(e) {
  181. // This also matches HOME/END on OSX which is CMD+LEFT, CMD+RIGHT
  182. if (e.keyCode == KEY.LEFT || e.keyCode == KEY.RIGHT || e.keyCode == KEY.HOME || e.keyCode == KEY.END) {
  183. // Defer execution to ensure carat pos has changed after HOME/END keys
  184. _.defer(resetBuffer);
  185. // IE9 doesn't fire the oninput event when backspace or delete is pressed. This causes the highlighting
  186. // to stay on the screen whenever backspace is pressed after a highlighed word. This is simply a hack
  187. // to force updateValues() to fire when backspace/delete is pressed in IE9.
  188. if (navigator.userAgent.indexOf("MSIE 9") > -1) {
  189. _.defer(updateValues);
  190. }
  191. return;
  192. }
  193. if (e.keyCode == KEY.BACKSPACE) {
  194. inputBuffer = inputBuffer.slice(0, -1 + inputBuffer.length); // Can't use splice, not available in IE
  195. return;
  196. }
  197. if (!elmAutocompleteList.is(':visible')) {
  198. return true;
  199. }
  200. switch (e.keyCode) {
  201. case KEY.UP:
  202. case KEY.DOWN:
  203. var elmCurrentAutoCompleteItem = null;
  204. if (e.keyCode == KEY.DOWN) {
  205. if (elmActiveAutoCompleteItem && elmActiveAutoCompleteItem.length) {
  206. elmCurrentAutoCompleteItem = elmActiveAutoCompleteItem.next();
  207. } else {
  208. elmCurrentAutoCompleteItem = elmAutocompleteList.find('li').first();
  209. }
  210. } else {
  211. elmCurrentAutoCompleteItem = $(elmActiveAutoCompleteItem).prev();
  212. }
  213. if (elmCurrentAutoCompleteItem.length) {
  214. selectAutoCompleteItem(elmCurrentAutoCompleteItem);
  215. }
  216. return false;
  217. case KEY.RETURN:
  218. case KEY.TAB:
  219. if (elmActiveAutoCompleteItem && elmActiveAutoCompleteItem.length) {
  220. elmActiveAutoCompleteItem.trigger('mousedown');
  221. return false;
  222. }
  223. break;
  224. }
  225. return true;
  226. }
  227. function hideAutoComplete() {
  228. elmActiveAutoCompleteItem = null;
  229. elmAutocompleteList.empty().hide();
  230. }
  231. function selectAutoCompleteItem(elmItem) {
  232. elmItem.addClass(settings.classes.autoCompleteItemActive);
  233. elmItem.siblings().removeClass(settings.classes.autoCompleteItemActive);
  234. elmActiveAutoCompleteItem = elmItem;
  235. }
  236. function populateDropdown(query, results) {
  237. elmAutocompleteList.show();
  238. // Filter items that has already been mentioned
  239. var mentionValues = _.pluck(mentionsCollection, 'value');
  240. results = _.reject(results, function (item) {
  241. return _.include(mentionValues, item.name);
  242. });
  243. if (!results.length) {
  244. hideAutoComplete();
  245. return;
  246. }
  247. elmAutocompleteList.empty();
  248. var elmDropDownList = $("<ul>").appendTo(elmAutocompleteList).hide();
  249. _.each(results, function (item, index) {
  250. var itemUid = _.uniqueId('mention_');
  251. autocompleteItemCollection[itemUid] = _.extend({}, item, {value: item.name});
  252. var elmListItem = $(settings.templates.autocompleteListItem({
  253. 'id' : utils.htmlEncode(item.id),
  254. 'display' : utils.htmlEncode(item.name),
  255. 'type' : utils.htmlEncode(item.type),
  256. 'content' : utils.highlightTerm(utils.htmlEncode((item.name)), query)
  257. })).attr('data-uid', itemUid);
  258. if (index === 0) {
  259. selectAutoCompleteItem(elmListItem);
  260. }
  261. if (settings.showAvatars) {
  262. var elmIcon;
  263. if (item.avatar) {
  264. elmIcon = $(settings.templates.autocompleteListItemAvatar({ avatar : item.avatar }));
  265. } else {
  266. elmIcon = $(settings.templates.autocompleteListItemIcon({ icon : item.icon }));
  267. }
  268. elmIcon.prependTo(elmListItem);
  269. }
  270. elmListItem = elmListItem.appendTo(elmDropDownList);
  271. });
  272. elmAutocompleteList.show();
  273. elmDropDownList.show();
  274. }
  275. function doSearch(query) {
  276. if (query && query.length && query.length >= settings.minChars) {
  277. settings.onDataRequest.call(this, 'search', query, function (responseData) {
  278. populateDropdown(query, responseData);
  279. });
  280. }
  281. }
  282. function resetInput() {
  283. elmInputBox.val('');
  284. mentionsCollection = [];
  285. updateValues();
  286. }
  287. // Public methods
  288. return {
  289. init : function (domTarget) {
  290. domInput = domTarget;
  291. initTextarea();
  292. initAutocomplete();
  293. initMentionsOverlay();
  294. resetInput();
  295. if( settings.prefillMention ) {
  296. addMention( settings.prefillMention );
  297. }
  298. },
  299. val : function (callback) {
  300. if (!_.isFunction(callback)) {
  301. return;
  302. }
  303. var value = mentionsCollection.length ? elmInputBox.data('messageText') : getInputBoxValue();
  304. callback.call(this, value);
  305. },
  306. reset : function () {
  307. resetInput();
  308. },
  309. getMentions : function (callback) {
  310. if (!_.isFunction(callback)) {
  311. return;
  312. }
  313. callback.call(this, mentionsCollection);
  314. }
  315. };
  316. };
  317. $.fn.mentionsInput = function (method, settings) {
  318. var outerArguments = arguments;
  319. if (typeof method === 'object' || !method) {
  320. settings = method;
  321. }
  322. return this.each(function () {
  323. var instance = $.data(this, 'mentionsInput') || $.data(this, 'mentionsInput', new MentionsInput(settings));
  324. if (_.isFunction(instance[method])) {
  325. return instance[method].apply(this, Array.prototype.slice.call(outerArguments, 1));
  326. } else if (typeof method === 'object' || !method) {
  327. return instance.init.call(this, this);
  328. } else {
  329. $.error('Method ' + method + ' does not exist');
  330. }
  331. });
  332. };
  333. })(jQuery, _);