talkie 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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, _);