talkie 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +4 -0
  3. data/README.md +25 -0
  4. data/app/assets/javascripts/talkie/application.js +6 -0
  5. data/app/assets/javascripts/talkie/talkie.mentions.js +25 -0
  6. data/app/assets/stylesheets/talkie/_form.scss +7 -1
  7. data/app/assets/stylesheets/talkie/_variables.scss +6 -1
  8. data/app/assets/stylesheets/talkie/application.scss +2 -0
  9. data/app/blueprints/mentionees_blueprint.rb +15 -0
  10. data/app/controllers/talkie/comments_controller.rb +3 -1
  11. data/app/controllers/talkie/mentions_controller.rb +24 -0
  12. data/app/mailers/talkie/notifications_mailer.rb +15 -0
  13. data/app/models/concerns/talkie/mentionable.rb +55 -0
  14. data/app/models/talkie/comment.rb +6 -0
  15. data/app/models/talkie/subscription.rb +11 -0
  16. data/app/views/talkie/comments/_comment.html.erb +7 -3
  17. data/app/views/talkie/comments/_form.html.erb +6 -2
  18. data/app/views/talkie/notifications_mailer/mentioned.html.erb +15 -0
  19. data/app/views/talkie/notifications_mailer/mentioned.text.erb +5 -0
  20. data/config/locales/en.yml +8 -0
  21. data/config/locales/es.yml +8 -0
  22. data/config/routes.rb +4 -0
  23. data/lib/generators/talkie/templates/create_talkie_comments.rb +14 -0
  24. data/lib/generators/talkie/templates/talkie.rb +28 -1
  25. data/lib/generators/talkie/views_generator.rb +15 -0
  26. data/lib/talkie/acts_as_talker.rb +7 -0
  27. data/lib/talkie/blueprinter.rb +5 -0
  28. data/lib/talkie/nil_mention_tokens.rb +18 -0
  29. data/lib/talkie/permission.rb +3 -3
  30. data/lib/talkie/subscription_error.rb +6 -0
  31. data/lib/talkie/version.rb +1 -1
  32. data/lib/talkie.rb +28 -0
  33. data/talkie.gemspec +2 -0
  34. data/vendor/assets/javascripts/jquery.elastic.js +151 -0
  35. data/vendor/assets/javascripts/jquery.mentionsinput.js +543 -0
  36. data/vendor/assets/javascripts/underscore.js +1590 -0
  37. data/vendor/assets/stylesheets/jquery.mentionsinput.css +112 -0
  38. metadata +46 -2
@@ -0,0 +1,151 @@
1
+ /**
2
+ * @name Elastic
3
+ * @descripton Elastic is jQuery plugin that grow and shrink your textareas automatically
4
+ * @version 1.6.10
5
+ * @requires jQuery 1.2.6+
6
+ *
7
+ * @author Jan Jarfalk
8
+ * @author-email jan.jarfalk@unwrongest.com
9
+ * @author-website http://www.unwrongest.com
10
+ *
11
+ * @licence MIT License - http://www.opensource.org/licenses/mit-license.php
12
+ */
13
+
14
+ (function(jQuery) {
15
+ jQuery.fn.extend({
16
+ elastic: function() {
17
+
18
+ // We will create a div clone of the textarea
19
+ // by copying these attributes from the textarea to the div.
20
+ var mimics = [
21
+ 'paddingTop',
22
+ 'paddingRight',
23
+ 'paddingBottom',
24
+ 'paddingLeft',
25
+ 'marginTop',
26
+ 'marginRight',
27
+ 'marginBottom',
28
+ 'marginLeft',
29
+ 'fontSize',
30
+ 'lineHeight',
31
+ 'fontFamily',
32
+ 'width',
33
+ 'fontWeight',
34
+ 'border-top-width',
35
+ 'border-right-width',
36
+ 'border-bottom-width',
37
+ 'border-left-width',
38
+ 'borderTopStyle',
39
+ 'borderTopColor',
40
+ 'borderRightStyle',
41
+ 'borderRightColor',
42
+ 'borderBottomStyle',
43
+ 'borderBottomColor',
44
+ 'borderLeftStyle',
45
+ 'borderLeftColor',
46
+ 'box-sizing',
47
+ '-moz-box-sizing',
48
+ '-webkit-box-sizing'
49
+ ];
50
+
51
+ return this.each(function() {
52
+
53
+ // Elastic only works on textareas
54
+ if (this.type !== 'textarea') {
55
+ return false;
56
+ }
57
+
58
+ var $textarea = jQuery(this),
59
+ $twin = jQuery('<div />').css({'position': 'absolute','display':'none','word-wrap':'break-word'}),
60
+ lineHeight = parseInt($textarea.css('line-height'), 10) || parseInt($textarea.css('font-size'), '10'),
61
+ minheight = parseInt($textarea.css('height'), 10) || lineHeight * 3,
62
+ maxheight = parseInt($textarea.css('max-height'), 10) || Number.MAX_VALUE,
63
+ goalheight = 0;
64
+
65
+ // Opera returns max-height of -1 if not set
66
+ if (maxheight < 0) {
67
+ maxheight = Number.MAX_VALUE;
68
+ }
69
+
70
+ // Append the twin to the DOM
71
+ // We are going to meassure the height of this, not the textarea.
72
+ $twin.appendTo($textarea.parent());
73
+
74
+ // Copy the essential styles (mimics) from the textarea to the twin
75
+ var i = mimics.length;
76
+ while (i--) {
77
+
78
+ if (mimics[i].toString() === 'width' && $textarea.css(mimics[i].toString()) === '0px') {
79
+ setTwinWidth();
80
+ } else {
81
+ $twin.css(mimics[i].toString(), $textarea.css(mimics[i].toString()));
82
+ }
83
+ }
84
+
85
+ update(true);
86
+
87
+ // Updates the width of the twin. (solution for textareas with widths in percent)
88
+ function setTwinWidth() {
89
+ curatedWidth = Math.floor(parseInt($textarea.width(), 10));
90
+ if ($twin.width() !== curatedWidth) {
91
+ $twin.css({'width': curatedWidth + 'px'});
92
+
93
+ // Update height of textarea
94
+ update(true);
95
+ }
96
+ }
97
+
98
+ // Sets a given height and overflow state on the textarea
99
+ function setHeightAndOverflow(height, overflow) {
100
+
101
+ var curratedHeight = Math.floor(parseInt(height, 10));
102
+ if ($textarea.height() !== curratedHeight) {
103
+ $textarea.css({'height': curratedHeight + 'px','overflow':overflow});
104
+
105
+ // Fire the custom event resize
106
+ $textarea.triggerHandler('resize');
107
+
108
+ }
109
+ }
110
+
111
+ // This function will update the height of the textarea if necessary
112
+ function update(forced) {
113
+
114
+ // Get curated content from the textarea.
115
+ var textareaContent = $textarea.val().replace(/&/g, '&amp;').replace(/ {2}/g, '&nbsp;').replace(/<|>/g, '&gt;').replace(/\n/g, '<br />');
116
+
117
+ // Compare curated content with curated twin.
118
+ var twinContent = $twin.html().replace(/<br>/ig, '<br />');
119
+
120
+ if (forced || textareaContent + '&nbsp;' !== twinContent) {
121
+
122
+ // Add an extra white space so new rows are added when you are at the end of a row.
123
+ $twin.html(textareaContent + '&nbsp;');
124
+
125
+ // Change textarea height if twin plus the height of one line differs more than 3 pixel from textarea height
126
+ if (Math.abs($twin.outerHeight() + lineHeight - $textarea.outerHeight()) > 3) {
127
+
128
+ var goalheight = $twin.outerHeight();
129
+ if (goalheight >= maxheight) {
130
+ setHeightAndOverflow(maxheight, 'auto');
131
+ } else if (goalheight <= minheight) {
132
+ setHeightAndOverflow(minheight, 'hidden');
133
+ } else {
134
+ setHeightAndOverflow(goalheight, 'hidden');
135
+ }
136
+
137
+ }
138
+
139
+ }
140
+
141
+ }
142
+
143
+ // Update textarea size on keyup, change, cut and paste
144
+ $textarea.bind('input', update);
145
+ $textarea.bind('change', update);
146
+ $(window).bind('resize', setTwinWidth);
147
+ });
148
+
149
+ }
150
+ });
151
+ })(jQuery);
@@ -0,0 +1,543 @@
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
+
11
+ (function ($, _, undefined) {
12
+
13
+ // Settings
14
+ 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"
15
+
16
+ //Default settings
17
+ var defaultSettings = {
18
+ triggerChar : '@', //Char that respond to event
19
+ onDataRequest : $.noop, //Function where we can search the data
20
+ minChars : 2, //Minimum chars to fire the event
21
+ allowRepeat : false, //Allow repeat mentions
22
+ showAvatars : true, //Show the avatars
23
+ elastic : true, //Grow the textarea automatically
24
+ defaultValue : '',
25
+ onCaret : false,
26
+ conserveTriggerChar: true,
27
+ classes : {
28
+ autoCompleteItemActive : "active" //Classes to apply in each item
29
+ },
30
+ templates : {
31
+ wrapper : _.template('<div class="mentions-input-box"></div>'),
32
+ autocompleteList : _.template('<div class="mentions-autocomplete-list"></div>'),
33
+ autocompleteListItem : _.template('<li data-ref-id="<%= id %>" data-ref-type="<%= type %>" data-display="<%= display %>"><%= content %></li>'),
34
+ autocompleteListItemAvatar : _.template('<img src="<%= avatar %>" />'),
35
+ autocompleteListItemIcon : _.template('<div class="icon <%= icon %>"></div>'),
36
+ mentionsOverlay : _.template('<div class="mentions"><div></div></div>'),
37
+ mentionItemSyntax : _.template('@[<%= value %>](<%= type %>:<%= id %>)'),
38
+ mentionItemHighlight : _.template('<strong><span><%= value %></span></strong>')
39
+ }
40
+ };
41
+
42
+ //Class util
43
+ var utils = {
44
+ //Encodes the character with _.escape function (undersocre)
45
+ htmlEncode : function (str) {
46
+ return _.escape(str);
47
+ },
48
+ //Encodes the character to be used with RegExp
49
+ regexpEncode : function (str) {
50
+ return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
51
+ },
52
+ highlightTerm : function (value, term) {
53
+ if (!term && !term.length) {
54
+ return value;
55
+ }
56
+ return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
57
+ },
58
+ //Sets the caret in a valid position
59
+ setCaratPosition : function (domNode, caretPos) {
60
+ if (domNode.createTextRange) {
61
+ var range = domNode.createTextRange();
62
+ range.move('character', caretPos);
63
+ range.select();
64
+ } else {
65
+ if (domNode.selectionStart) {
66
+ domNode.focus();
67
+ domNode.setSelectionRange(caretPos, caretPos);
68
+ } else {
69
+ domNode.focus();
70
+ }
71
+ }
72
+ },
73
+ //Deletes the white spaces
74
+ rtrim: function(string) {
75
+ return string.replace(/\s+$/,"");
76
+ }
77
+ };
78
+
79
+ //Main class of MentionsInput plugin
80
+ var MentionsInput = function (settings) {
81
+
82
+ var domInput,
83
+ elmInputBox,
84
+ elmInputWrapper,
85
+ elmAutocompleteList,
86
+ elmWrapperBox,
87
+ elmMentionsOverlay,
88
+ elmActiveAutoCompleteItem,
89
+ mentionsCollection = [],
90
+ autocompleteItemCollection = {},
91
+ inputBuffer = [],
92
+ currentDataQuery = '';
93
+
94
+ //Mix the default setting with the users settings
95
+ settings = $.extend(true, {}, defaultSettings, settings );
96
+
97
+ //Initializes the text area target
98
+ function initTextarea() {
99
+ elmInputBox = $(domInput); //Get the text area target
100
+
101
+ //If the text area is already configured, return
102
+ if (elmInputBox.attr('data-mentions-input') === 'true') {
103
+ return;
104
+ }
105
+
106
+ elmInputWrapper = elmInputBox.parent(); //Get the DOM element parent
107
+ elmWrapperBox = $(settings.templates.wrapper());
108
+ elmInputBox.wrapAll(elmWrapperBox); //Wrap all the text area into the div elmWrapperBox
109
+ elmWrapperBox = elmInputWrapper.find('> div.mentions-input-box'); //Obtains the div elmWrapperBox that now contains the text area
110
+
111
+ elmInputBox.attr('data-mentions-input', 'true'); //Sets the attribute data-mentions-input to true -> Defines if the text area is already configured
112
+ elmInputBox.bind('keydown', onInputBoxKeyDown); //Bind the keydown event to the text area
113
+ elmInputBox.bind('keypress', onInputBoxKeyPress); //Bind the keypress event to the text area
114
+ elmInputBox.bind('click', onInputBoxClick); //Bind the click event to the text area
115
+ elmInputBox.bind('blur', onInputBoxBlur); //Bind the blur event to the text area
116
+
117
+ if (navigator.userAgent.indexOf("MSIE 8") > -1) {
118
+ elmInputBox.bind('propertychange', onInputBoxInput); //IE8 won't fire the input event, so let's bind to the propertychange
119
+ } else {
120
+ elmInputBox.bind('input', onInputBoxInput); //Bind the input event to the text area
121
+ }
122
+
123
+ // Elastic textareas, grow automatically
124
+ if( settings.elastic ) {
125
+ elmInputBox.elastic();
126
+ }
127
+ }
128
+
129
+ //Initializes the autocomplete list, append to elmWrapperBox and delegate the mousedown event to li elements
130
+ function initAutocomplete() {
131
+ elmAutocompleteList = $(settings.templates.autocompleteList()); //Get the HTML code for the list
132
+ elmAutocompleteList.appendTo(elmWrapperBox); //Append to elmWrapperBox element
133
+ elmAutocompleteList.delegate('li', 'mousedown', onAutoCompleteItemClick); //Delegate the event
134
+ }
135
+
136
+ //Initializes the mentions' overlay
137
+ function initMentionsOverlay() {
138
+ elmMentionsOverlay = $(settings.templates.mentionsOverlay()); //Get the HTML code of the mentions' overlay
139
+ elmMentionsOverlay.prependTo(elmWrapperBox); //Insert into elmWrapperBox the mentions overlay
140
+ }
141
+
142
+ //Updates the values of the main variables
143
+ function updateValues() {
144
+ var syntaxMessage = getInputBoxValue(); //Get the actual value of the text area
145
+
146
+ _.each(mentionsCollection, function (mention) {
147
+ var textSyntax = settings.templates.mentionItemSyntax(mention);
148
+ syntaxMessage = syntaxMessage.replace(new RegExp(utils.regexpEncode(mention.value), 'g'), textSyntax);
149
+ });
150
+
151
+ var mentionText = utils.htmlEncode(syntaxMessage); //Encode the syntaxMessage
152
+
153
+ _.each(mentionsCollection, function (mention) {
154
+ var formattedMention = _.extend({}, mention, {value: utils.htmlEncode(mention.value)});
155
+ var textSyntax = settings.templates.mentionItemSyntax(formattedMention);
156
+ var textHighlight = settings.templates.mentionItemHighlight(formattedMention);
157
+
158
+ mentionText = mentionText.replace(new RegExp(utils.regexpEncode(textSyntax), 'g'), textHighlight);
159
+ });
160
+
161
+ mentionText = mentionText.replace(/\n/g, '<br />'); //Replace the escape character for <br />
162
+ mentionText = mentionText.replace(/ {2}/g, '&nbsp; '); //Replace the 2 preceding token to &nbsp;
163
+
164
+ elmInputBox.data('messageText', syntaxMessage); //Save the messageText to elmInputBox
165
+ elmInputBox.trigger('updated');
166
+ elmMentionsOverlay.find('div').html(mentionText); //Insert into a div of the elmMentionsOverlay the mention text
167
+ }
168
+
169
+ //Cleans the buffer
170
+ function resetBuffer() {
171
+ inputBuffer = [];
172
+ }
173
+
174
+ //Updates the mentions collection
175
+ function updateMentionsCollection() {
176
+ var inputText = getInputBoxValue(); //Get the actual value of text area
177
+
178
+ //Returns the values that doesn't match the condition
179
+ mentionsCollection = _.reject(mentionsCollection, function (mention, index) {
180
+ return !mention.value || inputText.indexOf(mention.value) == -1;
181
+ });
182
+ mentionsCollection = _.compact(mentionsCollection); //Delete all the falsy values of the array and return the new array
183
+ }
184
+
185
+ //Adds mention to mentions collections
186
+ function addMention(mention) {
187
+
188
+ var currentMessage = getInputBoxValue(); //Get the actual value of the text area
189
+
190
+ // Using a regex to figure out positions
191
+ var regex = new RegExp("\\" + settings.triggerChar + currentDataQuery, "gi");
192
+ regex.exec(currentMessage); //Executes a search for a match in a specified string. Returns a result array, or null
193
+
194
+ var startCaretPosition = regex.lastIndex - currentDataQuery.length - 1; //Set the star caret position
195
+ var currentCaretPosition = regex.lastIndex; //Set the current caret position
196
+
197
+ var start = currentMessage.substr(0, startCaretPosition);
198
+ var end = currentMessage.substr(currentCaretPosition, currentMessage.length);
199
+ var startEndIndex = (start + mention.value).length + 1;
200
+
201
+ if (settings.conserveTriggerChar) {
202
+ mention.value = settings.triggerChar + mention.value;
203
+ }
204
+
205
+ // See if there's the same mention in the list
206
+ if( !_.find(mentionsCollection, function (object) { return object.id == mention.id; }) ) {
207
+ mentionsCollection.push(mention);//Add the mention to mentionsColletions
208
+ }
209
+
210
+ // Cleaning before inserting the value, otherwise auto-complete would be triggered with "old" inputbuffer
211
+ resetBuffer();
212
+ currentDataQuery = '';
213
+ hideAutoComplete();
214
+
215
+ // Mentions and syntax message
216
+ var updatedMessageText = start + mention.value + ' ' + end;
217
+ elmInputBox.val(updatedMessageText); //Set the value to the txt area
218
+ elmInputBox.trigger('mention');
219
+ updateValues();
220
+
221
+ // Set correct focus and selection
222
+ elmInputBox.focus();
223
+ utils.setCaratPosition(elmInputBox[0], startEndIndex);
224
+ }
225
+
226
+ //Gets the actual value of the text area without white spaces from the beginning and end of the value
227
+ function getInputBoxValue() {
228
+ return $.trim(elmInputBox.val());
229
+ }
230
+
231
+ // This is taken straight from live (as of Sep 2012) GitHub code. The
232
+ // technique is known around the web. Just google it. Github's is quite
233
+ // succint though. NOTE: relies on selectionEnd, which as far as IE is concerned,
234
+ // it'll only work on 9+. Good news is nothing will happen if the browser
235
+ // doesn't support it.
236
+ function textareaSelectionPosition($el) {
237
+ var a, b, c, d, e, f, g, h, i, j, k;
238
+ if (!(i = $el[0])) return;
239
+ if (!$(i).is("textarea")) return;
240
+ if (i.selectionEnd == null) return;
241
+ g = {
242
+ position: "absolute",
243
+ overflow: "auto",
244
+ whiteSpace: "pre-wrap",
245
+ wordWrap: "break-word",
246
+ boxSizing: "content-box",
247
+ top: 0,
248
+ left: -9999
249
+ }, h = ["boxSizing", "fontFamily", "fontSize", "fontStyle", "fontVariant", "fontWeight", "height", "letterSpacing", "lineHeight", "paddingBottom", "paddingLeft", "paddingRight", "paddingTop", "textDecoration", "textIndent", "textTransform", "width", "word-spacing"];
250
+ for (j = 0, k = h.length; j < k; j++) e = h[j], g[e] = $(i).css(e);
251
+ 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
252
+ }
253
+
254
+ //Scrolls back to the input after autocomplete if the window has scrolled past the input
255
+ function scrollToInput() {
256
+ var elmDistanceFromTop = $(elmInputBox).offset().top; //input offset
257
+ var bodyDistanceFromTop = $('body').offset().top; //body offset
258
+ var distanceScrolled = $(window).scrollTop(); //distance scrolled
259
+
260
+ if (distanceScrolled > elmDistanceFromTop) {
261
+ //subtracts body distance to handle fixed headers
262
+ $(window).scrollTop(elmDistanceFromTop - bodyDistanceFromTop);
263
+ }
264
+ }
265
+
266
+ //Takes the click event when the user select a item of the dropdown
267
+ function onAutoCompleteItemClick(e) {
268
+ var elmTarget = $(this); //Get the item selected
269
+ var mention = autocompleteItemCollection[elmTarget.attr('data-uid')]; //Obtains the mention
270
+
271
+ addMention(mention);
272
+ scrollToInput();
273
+ return false;
274
+ }
275
+
276
+ //Takes the click event on text area
277
+ function onInputBoxClick(e) {
278
+ resetBuffer();
279
+ }
280
+
281
+ //Takes the blur event on text area
282
+ function onInputBoxBlur(e) {
283
+ hideAutoComplete();
284
+ }
285
+
286
+ //Takes the input event when users write or delete something
287
+ function onInputBoxInput(e) {
288
+ updateValues();
289
+ updateMentionsCollection();
290
+
291
+ var triggerCharIndex = _.lastIndexOf(inputBuffer, settings.triggerChar); //Returns the last match of the triggerChar in the inputBuffer
292
+ if (triggerCharIndex > -1) { //If the triggerChar is present in the inputBuffer array
293
+ currentDataQuery = inputBuffer.slice(triggerCharIndex + 1).join(''); //Gets the currentDataQuery
294
+ currentDataQuery = utils.rtrim(currentDataQuery); //Deletes the whitespaces
295
+ _.defer(_.bind(doSearch, this, currentDataQuery)); //Invoking the function doSearch ( Bind the function to this)
296
+ }
297
+ }
298
+
299
+ //Takes the keypress event
300
+ function onInputBoxKeyPress(e) {
301
+ if(e.keyCode !== KEY.BACKSPACE) { //If the key pressed is not the backspace
302
+ var typedValue = String.fromCharCode(e.which || e.keyCode); //Takes the string that represent this CharCode
303
+ inputBuffer.push(typedValue); //Push the value pressed into inputBuffer
304
+ }
305
+ }
306
+
307
+ //Takes the keydown event
308
+ function onInputBoxKeyDown(e) {
309
+
310
+ // This also matches HOME/END on OSX which is CMD+LEFT, CMD+RIGHT
311
+ if (e.keyCode === KEY.LEFT || e.keyCode === KEY.RIGHT || e.keyCode === KEY.HOME || e.keyCode === KEY.END) {
312
+ // Defer execution to ensure carat pos has changed after HOME/END keys then call the resetBuffer function
313
+ _.defer(resetBuffer);
314
+
315
+ // IE9 doesn't fire the oninput event when backspace or delete is pressed. This causes the highlighting
316
+ // to stay on the screen whenever backspace is pressed after a highlighed word. This is simply a hack
317
+ // to force updateValues() to fire when backspace/delete is pressed in IE9.
318
+ if (navigator.userAgent.indexOf("MSIE 9") > -1) {
319
+ _.defer(updateValues); //Call the updateValues function
320
+ }
321
+
322
+ return;
323
+ }
324
+
325
+ //If the key pressed was the backspace
326
+ if (e.keyCode === KEY.BACKSPACE) {
327
+ inputBuffer = inputBuffer.slice(0, -1 + inputBuffer.length); // Can't use splice, not available in IE
328
+ return;
329
+ }
330
+
331
+ //If the elmAutocompleteList is hidden
332
+ if (!elmAutocompleteList.is(':visible')) {
333
+ return true;
334
+ }
335
+
336
+ switch (e.keyCode) {
337
+ case KEY.UP: //If the key pressed was UP or DOWN
338
+ case KEY.DOWN:
339
+ var elmCurrentAutoCompleteItem = null;
340
+ if (e.keyCode === KEY.DOWN) { //If the key pressed was DOWN
341
+ if (elmActiveAutoCompleteItem && elmActiveAutoCompleteItem.length) { //If elmActiveAutoCompleteItem exits
342
+ elmCurrentAutoCompleteItem = elmActiveAutoCompleteItem.next(); //Gets the next li element in the list
343
+ } else {
344
+ elmCurrentAutoCompleteItem = elmAutocompleteList.find('li').first(); //Gets the first li element found
345
+ }
346
+ } else {
347
+ elmCurrentAutoCompleteItem = $(elmActiveAutoCompleteItem).prev(); //The key pressed was UP and gets the previous li element
348
+ }
349
+ if (elmCurrentAutoCompleteItem.length) {
350
+ selectAutoCompleteItem(elmCurrentAutoCompleteItem);
351
+ }
352
+ return false;
353
+ case KEY.RETURN: //If the key pressed was RETURN or TAB
354
+ case KEY.TAB:
355
+ if (elmActiveAutoCompleteItem && elmActiveAutoCompleteItem.length) { //If the elmActiveAutoCompleteItem exists
356
+ elmActiveAutoCompleteItem.trigger('mousedown'); //Calls the mousedown event
357
+ return false;
358
+ }
359
+ break;
360
+ }
361
+
362
+ return true;
363
+ }
364
+
365
+ //Hides the autoomplete
366
+ function hideAutoComplete() {
367
+ elmActiveAutoCompleteItem = null;
368
+ elmAutocompleteList.empty().hide();
369
+ }
370
+
371
+ //Selects the item in the autocomplete list
372
+ function selectAutoCompleteItem(elmItem) {
373
+ elmItem.addClass(settings.classes.autoCompleteItemActive); //Add the class active to item
374
+ elmItem.siblings().removeClass(settings.classes.autoCompleteItemActive); //Gets all li elements in autocomplete list and remove the class active
375
+
376
+ elmActiveAutoCompleteItem = elmItem; //Sets the item to elmActiveAutoCompleteItem
377
+ }
378
+
379
+ //Populates dropdown
380
+ function populateDropdown(query, results) {
381
+ elmAutocompleteList.show(); //Shows the autocomplete list
382
+
383
+ if(!settings.allowRepeat) {
384
+ // Filter items that has already been mentioned
385
+ var mentionValues = _.pluck(mentionsCollection, 'value');
386
+ results = _.reject(results, function (item) {
387
+ return _.include(mentionValues, item.name);
388
+ });
389
+ }
390
+
391
+ if (!results.length) { //If there are not elements hide the autocomplete list
392
+ hideAutoComplete();
393
+ return;
394
+ }
395
+
396
+ elmAutocompleteList.empty(); //Remove all li elements in autocomplete list
397
+ var elmDropDownList = $("<ul>").appendTo(elmAutocompleteList).hide(); //Inserts a ul element to autocomplete div and hide it
398
+
399
+ _.each(results, function (item, index) {
400
+ var itemUid = _.uniqueId('mention_'); //Gets the item with unique id
401
+
402
+ autocompleteItemCollection[itemUid] = _.extend({}, item, {value: item.name}); //Inserts the new item to autocompleteItemCollection
403
+
404
+ var elmListItem = $(settings.templates.autocompleteListItem({
405
+ 'id' : utils.htmlEncode(item.id),
406
+ 'display' : utils.htmlEncode(item.name),
407
+ 'type' : utils.htmlEncode(item.type),
408
+ 'content' : utils.highlightTerm(utils.htmlEncode((item.display ? item.display : item.name)), query)
409
+ })).attr('data-uid', itemUid); //Inserts the new item to list
410
+
411
+ //If the index is 0
412
+ if (index === 0) {
413
+ selectAutoCompleteItem(elmListItem);
414
+ }
415
+
416
+ //If show avatars is true
417
+ if (settings.showAvatars) {
418
+ var elmIcon;
419
+
420
+ //If the item has an avatar
421
+ if (item.avatar) {
422
+ elmIcon = $(settings.templates.autocompleteListItemAvatar({ avatar : item.avatar }));
423
+ } else { //If not then we set an default icon
424
+ elmIcon = $(settings.templates.autocompleteListItemIcon({ icon : item.icon }));
425
+ }
426
+ elmIcon.prependTo(elmListItem); //Inserts the elmIcon to elmListItem
427
+ }
428
+ elmListItem = elmListItem.appendTo(elmDropDownList); //Insets the elmListItem to elmDropDownList
429
+ });
430
+
431
+ elmAutocompleteList.show(); //Shows the elmAutocompleteList div
432
+ if (settings.onCaret) {
433
+ positionAutocomplete(elmAutocompleteList, elmInputBox);
434
+ }
435
+ elmDropDownList.show(); //Shows the elmDropDownList
436
+ }
437
+
438
+ //Search into data list passed as parameter
439
+ function doSearch(query) {
440
+ //If the query is not null, undefined, empty and has the minimum chars
441
+ if (query && query.length && query.length >= settings.minChars) {
442
+ //Call the onDataRequest function and then call the populateDropDrown
443
+ settings.onDataRequest.call(this, 'search', query, function (responseData) {
444
+ populateDropdown(query, responseData);
445
+ });
446
+ } else { //If the query is null, undefined, empty or has not the minimun chars
447
+ hideAutoComplete(); //Hide the autocompletelist
448
+ }
449
+ }
450
+
451
+ function positionAutocomplete(elmAutocompleteList, elmInputBox) {
452
+ var position = textareaSelectionPosition(elmInputBox),
453
+ lineHeight = parseInt(elmInputBox.css('line-height'), 10) || 18;
454
+ elmAutocompleteList.css('width', '15em'); // Sort of a guess
455
+ elmAutocompleteList.css('left', position.left);
456
+ elmAutocompleteList.css('top', lineHeight + position.top);
457
+
458
+ //check if the right position of auto complete is larger than the right position of the input
459
+ //if yes, reset the left of auto complete list to make it fit the input
460
+ var elmInputBoxRight = elmInputBox.offset().left + elmInputBox.width(),
461
+ elmAutocompleteListRight = elmAutocompleteList.offset().left + elmAutocompleteList.width();
462
+ if (elmInputBoxRight <= elmAutocompleteListRight) {
463
+ elmAutocompleteList.css('left', Math.abs(elmAutocompleteList.position().left - (elmAutocompleteListRight - elmInputBoxRight)));
464
+ }
465
+ }
466
+
467
+ //Resets the text area
468
+ function resetInput(currentVal) {
469
+ mentionsCollection = [];
470
+ var mentionText = utils.htmlEncode(currentVal);
471
+ var regex = new RegExp("(" + settings.triggerChar + ")\\[(.*?)\\]\\((.*?):(.*?)\\)", "gi");
472
+ var match, newMentionText = mentionText;
473
+ while ((match = regex.exec(mentionText)) != null) {
474
+ newMentionText = newMentionText.replace(match[0], match[1] + match[2]);
475
+ mentionsCollection.push({ 'id': match[4], 'type': match[3], 'value': match[2], 'trigger': match[1] });
476
+ }
477
+ elmInputBox.val(newMentionText);
478
+ updateValues();
479
+ }
480
+ // Public methods
481
+ return {
482
+ //Initializes the mentionsInput component on a specific element.
483
+ init : function (domTarget) {
484
+
485
+ domInput = domTarget;
486
+
487
+ initTextarea();
488
+ initAutocomplete();
489
+ initMentionsOverlay();
490
+ resetInput(settings.defaultValue);
491
+
492
+ //If the autocomplete list has prefill mentions
493
+ if( settings.prefillMention ) {
494
+ addMention( settings.prefillMention );
495
+ }
496
+ },
497
+
498
+ //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.
499
+ val : function (callback) {
500
+ if (!_.isFunction(callback)) {
501
+ return;
502
+ }
503
+ callback.call(this, mentionsCollection.length ? elmInputBox.data('messageText') : getInputBoxValue());
504
+ },
505
+
506
+ //Resets the text area value and clears all mentions
507
+ reset : function () {
508
+ resetInput();
509
+ },
510
+
511
+ //An async method which accepts a callback function and returns a collection of mentions as hash objects as a first parameter.
512
+ getMentions : function (callback) {
513
+ if (!_.isFunction(callback)) {
514
+ return;
515
+ }
516
+ callback.call(this, mentionsCollection);
517
+ }
518
+ };
519
+ };
520
+
521
+ //Main function to include into jQuery and initialize the plugin
522
+ $.fn.mentionsInput = function (method, settings) {
523
+
524
+ var outerArguments = arguments; //Gets the arguments
525
+ //If method is not a function
526
+ if (typeof method === 'object' || !method) {
527
+ settings = method;
528
+ }
529
+
530
+ return this.each(function () {
531
+ var instance = $.data(this, 'mentionsInput') || $.data(this, 'mentionsInput', new MentionsInput(settings));
532
+
533
+ if (_.isFunction(instance[method])) {
534
+ return instance[method].apply(this, Array.prototype.slice.call(outerArguments, 1));
535
+ } else if (typeof method === 'object' || !method) {
536
+ return instance.init.call(this, this);
537
+ } else {
538
+ $.error('Method ' + method + ' does not exist');
539
+ }
540
+ });
541
+ };
542
+
543
+ })(jQuery, _);