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.
- checksums.yaml +4 -4
- data/Gemfile.lock +4 -0
- data/README.md +25 -0
- data/app/assets/javascripts/talkie/application.js +6 -0
- data/app/assets/javascripts/talkie/talkie.mentions.js +25 -0
- data/app/assets/stylesheets/talkie/_form.scss +7 -1
- data/app/assets/stylesheets/talkie/_variables.scss +6 -1
- data/app/assets/stylesheets/talkie/application.scss +2 -0
- data/app/blueprints/mentionees_blueprint.rb +15 -0
- data/app/controllers/talkie/comments_controller.rb +3 -1
- data/app/controllers/talkie/mentions_controller.rb +24 -0
- data/app/mailers/talkie/notifications_mailer.rb +15 -0
- data/app/models/concerns/talkie/mentionable.rb +55 -0
- data/app/models/talkie/comment.rb +6 -0
- data/app/models/talkie/subscription.rb +11 -0
- data/app/views/talkie/comments/_comment.html.erb +7 -3
- data/app/views/talkie/comments/_form.html.erb +6 -2
- data/app/views/talkie/notifications_mailer/mentioned.html.erb +15 -0
- data/app/views/talkie/notifications_mailer/mentioned.text.erb +5 -0
- data/config/locales/en.yml +8 -0
- data/config/locales/es.yml +8 -0
- data/config/routes.rb +4 -0
- data/lib/generators/talkie/templates/create_talkie_comments.rb +14 -0
- data/lib/generators/talkie/templates/talkie.rb +28 -1
- data/lib/generators/talkie/views_generator.rb +15 -0
- data/lib/talkie/acts_as_talker.rb +7 -0
- data/lib/talkie/blueprinter.rb +5 -0
- data/lib/talkie/nil_mention_tokens.rb +18 -0
- data/lib/talkie/permission.rb +3 -3
- data/lib/talkie/subscription_error.rb +6 -0
- data/lib/talkie/version.rb +1 -1
- data/lib/talkie.rb +28 -0
- data/talkie.gemspec +2 -0
- data/vendor/assets/javascripts/jquery.elastic.js +151 -0
- data/vendor/assets/javascripts/jquery.mentionsinput.js +543 -0
- data/vendor/assets/javascripts/underscore.js +1590 -0
- data/vendor/assets/stylesheets/jquery.mentionsinput.css +112 -0
- 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, '&').replace(/ {2}/g, ' ').replace(/<|>/g, '>').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 + ' ' !== 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 + ' ');
|
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, ' '); //Replace the 2 preceding token to
|
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 = " ", 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, _);
|