PageRenderTime 50ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/app/assets/javascripts/jquery-mentions-input/jquery.mentionsInput.js

https://gitlab.com/vectorci/samson
JavaScript | 546 lines | 482 code | 28 blank | 36 comment | 12 complexity | 3df9bca1ee3ba173557dbe54b2ff2a1d MD5 | raw file
  1. /*
  2. * TODO use gem 'rails-assets-jquery-mentions-input' once
  3. * https://github.com/podio/jquery-mentions-input/pull/131 is released
  4. * or rails-assets-jquery-mentions gem
  5. * jquery.mentionsInput is super old and unmaintained and it's rails-assets version does not work
  6. * jquery-mentions needs coffe-script due to a bug in sprockets (preferring coffee over js)
  7. * and then it does not work either ... no other gem on rubygems that does it either
  8. *
  9. * Mentions Input
  10. * Version 1.0.2
  11. * Written by: Kenneth Auchenberg (Podio)
  12. *
  13. * Using underscore.js
  14. *
  15. * License: MIT License - http://www.opensource.org/licenses/mit-license.php
  16. */
  17. (function ($, _, undefined) {
  18. // Settings
  19. 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"
  20. //Default settings
  21. var defaultSettings = {
  22. triggerChar : '@', //Char that respond to event
  23. onDataRequest : $.noop, //Function where we can search the data
  24. minChars : 2, //Minimum chars to fire the event
  25. allowRepeat : false, //Allow repeat mentions
  26. showAvatars : true, //Show the avatars
  27. elastic : true, //Grow the textarea automatically
  28. defaultValue : '',
  29. onCaret : false,
  30. classes : {
  31. autoCompleteItemActive : "active" //Classes to apply in each item
  32. },
  33. templates : {
  34. wrapper : _.template('<div class="mentions-input-box"></div>'),
  35. autocompleteList : _.template('<div class="mentions-autocomplete-list"></div>'),
  36. autocompleteListItem : _.template('<li data-ref-id="<%= id %>" data-ref-type="<%= type %>" data-display="<%= display %>"><%= content %></li>'),
  37. autocompleteListItemAvatar : _.template('<img src="<%= avatar %>" />'),
  38. autocompleteListItemIcon : _.template('<div class="icon <%= icon %>"></div>'),
  39. mentionsOverlay : _.template('<div class="mentions"><div></div></div>'),
  40. mentionItemSyntax : _.template('@[<%= value %>](<%= type %>:<%= id %>)'),
  41. mentionItemHighlight : _.template('<strong><span><%= value %></span></strong>')
  42. }
  43. };
  44. //Class util
  45. var utils = {
  46. //Encodes the character with _.escape function (undersocre)
  47. htmlEncode : function (str) {
  48. return _.escape(str);
  49. },
  50. //Encodes the character to be used with RegExp
  51. regexpEncode : function (str) {
  52. return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
  53. },
  54. highlightTerm : function (value, term) {
  55. if (!term && !term.length) {
  56. return value;
  57. }
  58. return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
  59. },
  60. //Sets the caret in a valid position
  61. setCaratPosition : function (domNode, caretPos) {
  62. if (domNode.createTextRange) {
  63. var range = domNode.createTextRange();
  64. range.move('character', caretPos);
  65. range.select();
  66. } else {
  67. if (domNode.selectionStart) {
  68. domNode.focus();
  69. domNode.setSelectionRange(caretPos, caretPos);
  70. } else {
  71. domNode.focus();
  72. }
  73. }
  74. },
  75. //Deletes the white spaces
  76. rtrim: function(string) {
  77. return string.replace(/\s+$/,"");
  78. }
  79. };
  80. //Main class of MentionsInput plugin
  81. var MentionsInput = function (settings) {
  82. var domInput,
  83. elmInputBox,
  84. elmInputWrapper,
  85. elmAutocompleteList,
  86. elmWrapperBox,
  87. elmMentionsOverlay,
  88. elmActiveAutoCompleteItem,
  89. mentionsCollection = [],
  90. autocompleteItemCollection = {},
  91. inputBuffer = [],
  92. currentDataQuery = '';
  93. //Mix the default setting with the users settings
  94. settings = $.extend(true, {}, defaultSettings, settings );
  95. //Initializes the text area target
  96. function initTextarea() {
  97. elmInputBox = $(domInput); //Get the text area target
  98. //If the text area is already configured, return
  99. if (elmInputBox.attr('data-mentions-input') === 'true') {
  100. return;
  101. }
  102. elmInputWrapper = elmInputBox.parent(); //Get the DOM element parent
  103. elmWrapperBox = $(settings.templates.wrapper());
  104. elmInputBox.wrapAll(elmWrapperBox); //Wrap all the text area into the div elmWrapperBox
  105. elmWrapperBox = elmInputWrapper.find('> div.mentions-input-box'); //Obtains the div elmWrapperBox that now contains the text area
  106. elmInputBox.attr('data-mentions-input', 'true'); //Sets the attribute data-mentions-input to true -> Defines if the text area is already configured
  107. elmInputBox.bind('keydown', onInputBoxKeyDown); //Bind the keydown event to the text area
  108. elmInputBox.bind('keypress', onInputBoxKeyPress); //Bind the keypress event to the text area
  109. elmInputBox.bind('click', onInputBoxClick); //Bind the click event to the text area
  110. elmInputBox.bind('blur', onInputBoxBlur); //Bind the blur event to the text area
  111. if (navigator.userAgent.indexOf("MSIE 8") > -1) {
  112. elmInputBox.bind('propertychange', onInputBoxInput); //IE8 won't fire the input event, so let's bind to the propertychange
  113. } else {
  114. elmInputBox.bind('input', onInputBoxInput); //Bind the input event to the text area
  115. }
  116. // Elastic textareas, grow automatically
  117. if( settings.elastic ) {
  118. elmInputBox.elastic();
  119. }
  120. }
  121. //Initializes the autocomplete list, append to elmWrapperBox and delegate the mousedown event to li elements
  122. function initAutocomplete() {
  123. elmAutocompleteList = $(settings.templates.autocompleteList()); //Get the HTML code for the list
  124. elmAutocompleteList.appendTo(elmWrapperBox); //Append to elmWrapperBox element
  125. elmAutocompleteList.delegate('li', 'mousedown', onAutoCompleteItemClick); //Delegate the event
  126. }
  127. //Initializes the mentions' overlay
  128. function initMentionsOverlay() {
  129. elmMentionsOverlay = $(settings.templates.mentionsOverlay()); //Get the HTML code of the mentions' overlay
  130. elmMentionsOverlay.prependTo(elmWrapperBox); //Insert into elmWrapperBox the mentions overlay
  131. }
  132. //Updates the values of the main variables
  133. function updateValues() {
  134. var syntaxMessage = getInputBoxValue(); //Get the actual value of the text area
  135. _.each(mentionsCollection, function (mention) {
  136. var textSyntax = settings.templates.mentionItemSyntax(mention);
  137. syntaxMessage = syntaxMessage.replace(new RegExp(utils.regexpEncode(mention.value), 'g'), textSyntax);
  138. });
  139. var mentionText = utils.htmlEncode(syntaxMessage); //Encode the syntaxMessage
  140. _.each(mentionsCollection, function (mention) {
  141. var formattedMention = _.extend({}, mention, {value: utils.htmlEncode(mention.value)});
  142. var textSyntax = settings.templates.mentionItemSyntax(formattedMention);
  143. var textHighlight = settings.templates.mentionItemHighlight(formattedMention);
  144. mentionText = mentionText.replace(new RegExp(utils.regexpEncode(textSyntax), 'g'), textHighlight);
  145. });
  146. mentionText = mentionText.replace(/\n/g, '<br />'); //Replace the escape character for <br />
  147. mentionText = mentionText.replace(/ {2}/g, '&nbsp; '); //Replace the 2 preceding token to &nbsp;
  148. elmInputBox.data('messageText', syntaxMessage); //Save the messageText to elmInputBox
  149. elmInputBox.trigger('updated');
  150. elmMentionsOverlay.find('div').html(mentionText); //Insert into a div of the elmMentionsOverlay the mention text
  151. }
  152. //Cleans the buffer
  153. function resetBuffer() {
  154. inputBuffer = [];
  155. }
  156. //Updates the mentions collection
  157. function updateMentionsCollection() {
  158. var inputText = getInputBoxValue(); //Get the actual value of text area
  159. //Returns the values that doesn't match the condition
  160. mentionsCollection = _.reject(mentionsCollection, function (mention, index) {
  161. return !mention.value || inputText.indexOf(mention.value) == -1;
  162. });
  163. mentionsCollection = _.compact(mentionsCollection); //Delete all the falsy values of the array and return the new array
  164. }
  165. //Adds mention to mentions collections
  166. function addMention(mention) {
  167. var currentMessage = getInputBoxValue(); //Get the actual value of the text area
  168. // Using a regex to figure out positions
  169. var regex = new RegExp("\\" + settings.triggerChar + currentDataQuery, "gi");
  170. regex.exec(currentMessage); //Executes a search for a match in a specified string. Returns a result array, or null
  171. var startCaretPosition = regex.lastIndex - currentDataQuery.length - 1; //Set the star caret position
  172. var currentCaretPosition = regex.lastIndex; //Set the current caret position
  173. var start = currentMessage.substr(0, startCaretPosition);
  174. var end = currentMessage.substr(currentCaretPosition, currentMessage.length);
  175. var startEndIndex = (start + mention.value).length + 1;
  176. // See if there's the same mention in the list
  177. if( !_.find(mentionsCollection, function (object) { return object.id == mention.id; }) ) {
  178. mentionsCollection.push(mention);//Add the mention to mentionsColletions
  179. }
  180. // Cleaning before inserting the value, otherwise auto-complete would be triggered with "old" inputbuffer
  181. resetBuffer();
  182. currentDataQuery = '';
  183. hideAutoComplete();
  184. // Mentions and syntax message
  185. var updatedMessageText = start + mention.value + ' ' + end;
  186. elmInputBox.val(updatedMessageText); //Set the value to the txt area
  187. elmInputBox.trigger('mention');
  188. updateValues();
  189. // Set correct focus and selection
  190. elmInputBox.focus();
  191. utils.setCaratPosition(elmInputBox[0], startEndIndex);
  192. }
  193. //Gets the actual value of the text area without white spaces from the beginning and end of the value
  194. function getInputBoxValue() {
  195. return $.trim(elmInputBox.val());
  196. }
  197. // This is taken straight from live (as of Sep 2012) GitHub code. The
  198. // technique is known around the web. Just google it. Github's is quite
  199. // succint though. NOTE: relies on selectionEnd, which as far as IE is concerned,
  200. // it'll only work on 9+. Good news is nothing will happen if the browser
  201. // doesn't support it.
  202. function textareaSelectionPosition($el) {
  203. var a, b, c, d, e, f, g, h, i, j, k;
  204. if (!(i = $el[0])) return;
  205. if (!$(i).is("textarea")) return;
  206. if (i.selectionEnd === null) return;
  207. g = {
  208. position: "absolute",
  209. overflow: "auto",
  210. whiteSpace: "pre-wrap",
  211. wordWrap: "break-word",
  212. boxSizing: "content-box",
  213. top: 0,
  214. left: -9999
  215. };
  216. h = ["boxSizing", "fontFamily", "fontSize", "fontStyle", "fontVariant", "fontWeight", "height", "letterSpacing", "lineHeight", "paddingBottom", "paddingLeft", "paddingRight", "paddingTop", "textDecoration", "textIndent", "textTransform", "width", "word-spacing"];
  217. for (j = 0, k = h.length; j < k; j++) { e = h[j]; g[e] = $(i).css(e); }
  218. return c = document.createElement("div"), $(c).css(g), $(i).after(c), b = document.createTextNode(i.value.substring(0, i.selectionEnd)), a = document.createTextNode(i.value.substring(i.selectionEnd)), d = document.createElement("span"), d.innerHTML = "&nbsp;", c.appendChild(b), c.appendChild(d), c.appendChild(a), c.scrollTop = i.scrollTop, f = $(d).position(), $(c).remove(), f;
  219. }
  220. //Scrolls back to the input after autocomplete if the window has scrolled past the input
  221. function scrollToInput() {
  222. var elmDistanceFromTop = $(elmInputBox).offset().top; //input offset
  223. var bodyDistanceFromTop = $('body').offset().top; //body offset
  224. var distanceScrolled = $(window).scrollTop(); //distance scrolled
  225. if (distanceScrolled > elmDistanceFromTop) {
  226. //subtracts body distance to handle fixed headers
  227. $(window).scrollTop(elmDistanceFromTop - bodyDistanceFromTop);
  228. }
  229. }
  230. //Takes the click event when the user select a item of the dropdown
  231. function onAutoCompleteItemClick(e) {
  232. var elmTarget = $(this); //Get the item selected
  233. var mention = autocompleteItemCollection[elmTarget.attr('data-uid')]; //Obtains the mention
  234. addMention(mention);
  235. scrollToInput();
  236. return false;
  237. }
  238. //Takes the click event on text area
  239. function onInputBoxClick(e) {
  240. resetBuffer();
  241. }
  242. //Takes the blur event on text area
  243. function onInputBoxBlur(e) {
  244. hideAutoComplete();
  245. }
  246. //Takes the input event when users write or delete something
  247. function onInputBoxInput(e) {
  248. updateValues();
  249. updateMentionsCollection();
  250. var triggerCharIndex = _.lastIndexOf(inputBuffer, settings.triggerChar); //Returns the last match of the triggerChar in the inputBuffer
  251. if (triggerCharIndex > -1) { //If the triggerChar is present in the inputBuffer array
  252. currentDataQuery = inputBuffer.slice(triggerCharIndex + 1).join(''); //Gets the currentDataQuery
  253. currentDataQuery = utils.rtrim(currentDataQuery); //Deletes the whitespaces
  254. _.defer(_.bind(doSearch, this, currentDataQuery)); //Invoking the function doSearch ( Bind the function to this)
  255. }
  256. }
  257. //Takes the keypress event
  258. function onInputBoxKeyPress(e) {
  259. if(e.keyCode !== KEY.BACKSPACE) { //If the key pressed is not the backspace
  260. var typedValue = String.fromCharCode(e.which || e.keyCode); //Takes the string that represent this CharCode
  261. inputBuffer.push(typedValue); //Push the value pressed into inputBuffer
  262. }
  263. }
  264. //Takes the keydown event
  265. function onInputBoxKeyDown(e) {
  266. // This also matches HOME/END on OSX which is CMD+LEFT, CMD+RIGHT
  267. if (e.keyCode === KEY.LEFT || e.keyCode === KEY.RIGHT || e.keyCode === KEY.HOME || e.keyCode === KEY.END) {
  268. // Defer execution to ensure carat pos has changed after HOME/END keys then call the resetBuffer function
  269. _.defer(resetBuffer);
  270. // IE9 doesn't fire the oninput event when backspace or delete is pressed. This causes the highlighting
  271. // to stay on the screen whenever backspace is pressed after a highlighed word. This is simply a hack
  272. // to force updateValues() to fire when backspace/delete is pressed in IE9.
  273. if (navigator.userAgent.indexOf("MSIE 9") > -1) {
  274. _.defer(updateValues); //Call the updateValues function
  275. }
  276. return;
  277. }
  278. //If the key pressed was the backspace
  279. if (e.keyCode === KEY.BACKSPACE) {
  280. inputBuffer = inputBuffer.slice(0, -1 + inputBuffer.length); // Can't use splice, not available in IE
  281. return;
  282. }
  283. //If the elmAutocompleteList is hidden
  284. if (!elmAutocompleteList.is(':visible')) {
  285. return true;
  286. }
  287. switch (e.keyCode) {
  288. case KEY.UP: //If the key pressed was UP or DOWN
  289. case KEY.DOWN:
  290. var elmCurrentAutoCompleteItem = null;
  291. if (e.keyCode === KEY.DOWN) { //If the key pressed was DOWN
  292. if (elmActiveAutoCompleteItem && elmActiveAutoCompleteItem.length) { //If elmActiveAutoCompleteItem exits
  293. elmCurrentAutoCompleteItem = elmActiveAutoCompleteItem.next(); //Gets the next li element in the list
  294. } else {
  295. elmCurrentAutoCompleteItem = elmAutocompleteList.find('li').first(); //Gets the first li element found
  296. }
  297. } else {
  298. elmCurrentAutoCompleteItem = $(elmActiveAutoCompleteItem).prev(); //The key pressed was UP and gets the previous li element
  299. }
  300. if (elmCurrentAutoCompleteItem.length) {
  301. selectAutoCompleteItem(elmCurrentAutoCompleteItem);
  302. }
  303. return false;
  304. case KEY.RETURN: //If the key pressed was RETURN or TAB
  305. case KEY.TAB:
  306. if (elmActiveAutoCompleteItem && elmActiveAutoCompleteItem.length) { //If the elmActiveAutoCompleteItem exists
  307. elmActiveAutoCompleteItem.trigger('mousedown'); //Calls the mousedown event
  308. return false;
  309. }
  310. break;
  311. }
  312. return true;
  313. }
  314. //Hides the autoomplete
  315. function hideAutoComplete() {
  316. elmActiveAutoCompleteItem = null;
  317. elmAutocompleteList.empty().hide();
  318. }
  319. //Selects the item in the autocomplete list
  320. function selectAutoCompleteItem(elmItem) {
  321. elmItem.addClass(settings.classes.autoCompleteItemActive); //Add the class active to item
  322. elmItem.siblings().removeClass(settings.classes.autoCompleteItemActive); //Gets all li elements in autocomplete list and remove the class active
  323. elmActiveAutoCompleteItem = elmItem; //Sets the item to elmActiveAutoCompleteItem
  324. }
  325. //Populates dropdown
  326. function populateDropdown(query, results) {
  327. elmAutocompleteList.show(); //Shows the autocomplete list
  328. if(!settings.allowRepeat) {
  329. // Filter items that has already been mentioned
  330. var mentionValues = _.pluck(mentionsCollection, 'value');
  331. results = _.reject(results, function (item) {
  332. return _.include(mentionValues, item.name);
  333. });
  334. }
  335. if (!results.length) { //If there are not elements hide the autocomplete list
  336. hideAutoComplete();
  337. return;
  338. }
  339. elmAutocompleteList.empty(); //Remove all li elements in autocomplete list
  340. var elmDropDownList = $("<ul>").appendTo(elmAutocompleteList).hide(); //Inserts a ul element to autocomplete div and hide it
  341. _.each(results, function (item, index) {
  342. var itemUid = _.uniqueId('mention_'); //Gets the item with unique id
  343. autocompleteItemCollection[itemUid] = _.extend({}, item, {value: item.name}); //Inserts the new item to autocompleteItemCollection
  344. var elmListItem = $(settings.templates.autocompleteListItem({
  345. 'id' : utils.htmlEncode(item.id),
  346. 'display' : utils.htmlEncode(item.name),
  347. 'type' : utils.htmlEncode(item.type),
  348. 'content' : utils.highlightTerm(utils.htmlEncode((item.display ? item.display : item.name)), query)
  349. })).attr('data-uid', itemUid); //Inserts the new item to list
  350. //If the index is 0
  351. if (index === 0) {
  352. selectAutoCompleteItem(elmListItem);
  353. }
  354. //If show avatars is true
  355. if (settings.showAvatars) {
  356. var elmIcon;
  357. //If the item has an avatar
  358. if (item.avatar) {
  359. elmIcon = $(settings.templates.autocompleteListItemAvatar({ avatar : item.avatar }));
  360. } else { //If not then we set an default icon
  361. elmIcon = $(settings.templates.autocompleteListItemIcon({ icon : item.icon }));
  362. }
  363. elmIcon.prependTo(elmListItem); //Inserts the elmIcon to elmListItem
  364. }
  365. elmListItem = elmListItem.appendTo(elmDropDownList); //Insets the elmListItem to elmDropDownList
  366. });
  367. elmAutocompleteList.show(); //Shows the elmAutocompleteList div
  368. if (settings.onCaret) {
  369. positionAutocomplete(elmAutocompleteList, elmInputBox);
  370. }
  371. elmDropDownList.show(); //Shows the elmDropDownList
  372. }
  373. //Search into data list passed as parameter
  374. function doSearch(query) {
  375. //If the query is not null, undefined, empty and has the minimum chars
  376. if (query && query.length && query.length >= settings.minChars) {
  377. //Call the onDataRequest function and then call the populateDropDrown
  378. settings.onDataRequest.call(this, 'search', query, function (responseData) {
  379. populateDropdown(query, responseData);
  380. });
  381. } else { //If the query is null, undefined, empty or has not the minimun chars
  382. hideAutoComplete(); //Hide the autocompletelist
  383. }
  384. }
  385. function positionAutocomplete(elmAutocompleteList, elmInputBox) {
  386. var position = textareaSelectionPosition(elmInputBox),
  387. lineHeight = parseInt(elmInputBox.css('line-height'), 10) || 18;
  388. elmAutocompleteList.css('width', '15em'); // Sort of a guess
  389. elmAutocompleteList.css('left', position.left);
  390. elmAutocompleteList.css('top', lineHeight + position.top);
  391. //check if the right position of auto complete is larger than the right position of the input
  392. //if yes, reset the left of auto complete list to make it fit the input
  393. var elmInputBoxRight = elmInputBox.offset().left + elmInputBox.width(),
  394. elmAutocompleteListRight = elmAutocompleteList.offset().left + elmAutocompleteList.width();
  395. if (elmInputBoxRight <= elmAutocompleteListRight) {
  396. elmAutocompleteList.css('left', Math.abs(elmAutocompleteList.position().left - (elmAutocompleteListRight - elmInputBoxRight)));
  397. }
  398. }
  399. //Resets the text area
  400. function resetInput(currentVal) {
  401. mentionsCollection = [];
  402. var mentionText = utils.htmlEncode(currentVal);
  403. var regex = new RegExp("(" + settings.triggerChar + ")\\[(.*?)\\]\\((.*?):(.*?)\\)", "gi");
  404. var match, newMentionText = mentionText;
  405. while ((match = regex.exec(mentionText)) !== null) {
  406. newMentionText = newMentionText.replace(match[0], match[1] + match[2]);
  407. mentionsCollection.push({ 'id': match[4], 'type': match[3], 'value': match[2], 'trigger': match[1] });
  408. }
  409. elmInputBox.val(newMentionText);
  410. updateValues();
  411. }
  412. // Public methods
  413. return {
  414. //Initializes the mentionsInput component on a specific element.
  415. init : function (domTarget) {
  416. domInput = domTarget;
  417. initTextarea();
  418. initAutocomplete();
  419. initMentionsOverlay();
  420. resetInput(settings.defaultValue);
  421. //If the autocomplete list has prefill mentions
  422. if( settings.prefillMention ) {
  423. addMention( settings.prefillMention );
  424. }
  425. },
  426. //An async method which accepts a callback function and returns a value of the input field (including markup) as a first parameter of this function. This is the value you want to send to your server.
  427. val : function (callback) {
  428. if (!_.isFunction(callback)) {
  429. return;
  430. }
  431. callback.call(this, mentionsCollection.length ? elmInputBox.data('messageText') : getInputBoxValue());
  432. },
  433. //Resets the text area value and clears all mentions
  434. reset : function () {
  435. resetInput();
  436. },
  437. //An async method which accepts a callback function and returns a collection of mentions as hash objects as a first parameter.
  438. getMentions : function (callback) {
  439. if (!_.isFunction(callback)) {
  440. return;
  441. }
  442. callback.call(this, mentionsCollection);
  443. }
  444. };
  445. };
  446. //Main function to include into jQuery and initialize the plugin
  447. $.fn.mentionsInput = function (method, settings) {
  448. var outerArguments = arguments; //Gets the arguments
  449. //If method is not a function
  450. if (typeof method === 'object' || !method) {
  451. settings = method;
  452. }
  453. return this.each(function () {
  454. var instance = $.data(this, 'mentionsInput') || $.data(this, 'mentionsInput', new MentionsInput(settings));
  455. if (_.isFunction(instance[method])) {
  456. return instance[method].apply(this, Array.prototype.slice.call(outerArguments, 1));
  457. } else if (typeof method === 'object' || !method) {
  458. return instance.init.call(this, this);
  459. } else {
  460. $.error('Method ' + method + ' does not exist');
  461. }
  462. });
  463. };
  464. })(jQuery, _);