Commit ba4fc5021ca4825b32c91b4bf4d1d30d6d0a1125
Committed by
Marcos Pereira
1 parent
f2b2ff07
Add mentions system to comments and scraps
Signed-off-by: Fabio Teixeira <fabio1079@gmail.com> Signed-off-by: Gustavo Jaruga <darksshades@gmail.com> Signed-off-by: Marcos Ronaldo <marcos.rpj2@gmail.com> Signed-off-by: Matheus Miranda <matheusmirandalacerda@gmail.com> Signed-off-by: Sabryna Sousa <sabryna.sousa1323@gmail.com>
Showing
28 changed files
with
1271 additions
and
13 deletions
Show diff stats
app/controllers/public/profile_controller.rb
@@ -434,6 +434,9 @@ class ProfileController < PublicController | @@ -434,6 +434,9 @@ class ProfileController < PublicController | ||
434 | end | 434 | end |
435 | end | 435 | end |
436 | 436 | ||
437 | + def show_tracked_action | ||
438 | + @activity = ProfileActivity.find_by(:activity_id => params[:activity_id]).activity | ||
439 | + end | ||
437 | 440 | ||
438 | protected | 441 | protected |
439 | 442 |
app/controllers/public/search_controller.rb
@@ -154,6 +154,33 @@ class SearchController < PublicController | @@ -154,6 +154,33 @@ class SearchController < PublicController | ||
154 | render :text => find_suggestions(normalize_term(params[:term]), environment, params[:asset]).to_json | 154 | render :text => find_suggestions(normalize_term(params[:term]), environment, params[:asset]).to_json |
155 | end | 155 | end |
156 | 156 | ||
157 | + def search_for_users | ||
158 | + # If it isn't a ajax call give to the user an access denied response | ||
159 | + return render_access_denied unless request.xhr? | ||
160 | + | ||
161 | + scope = user.friends.select(:id, :name, :identifier) | ||
162 | + | ||
163 | + results = find_by_contents( | ||
164 | + :people, environment, scope, | ||
165 | + params['q'], {:page => 1} | ||
166 | + )[:results] | ||
167 | + | ||
168 | + if params[:community].present? and (community = Community.find_by_identifier params[:community]) | ||
169 | + scope = community.members # .select(:id, :name, :identifier) is not working here | ||
170 | + results += find_by_contents( | ||
171 | + :people, environment, scope, | ||
172 | + params['q'], {:page => 1} | ||
173 | + )[:results] | ||
174 | + | ||
175 | + # because .select does not work on members | ||
176 | + results.map! {|r| | ||
177 | + {id: r.id, name: r.name, identifier: r.identifier} | ||
178 | + } | ||
179 | + end | ||
180 | + | ||
181 | + render :json => results.to_json | ||
182 | + end | ||
183 | + | ||
157 | ####################################################### | 184 | ####################################################### |
158 | protected | 185 | protected |
159 | 186 |
app/helpers/action_tracker_helper.rb
@@ -24,6 +24,11 @@ module ActionTrackerHelper | @@ -24,6 +24,11 @@ module ActionTrackerHelper | ||
24 | } | 24 | } |
25 | end | 25 | end |
26 | 26 | ||
27 | + def notify_mentioned_users_description ta | ||
28 | + source_link = link_to _("here"), ta.get_url | ||
29 | + (_("mentioned you %{link}") % {link: source_link}).html_safe | ||
30 | + end | ||
31 | + | ||
27 | def join_community_description ta | 32 | def join_community_description ta |
28 | n_('has joined 1 community:<br />%{name}', 'has joined %{num} communities:<br />%{name}', ta.get_resource_name.size).html_safe % { | 33 | n_('has joined 1 community:<br />%{name}', 'has joined %{num} communities:<br />%{name}', ta.get_resource_name.size).html_safe % { |
29 | num: ta.get_resource_name.size, | 34 | num: ta.get_resource_name.size, |
@@ -62,19 +67,19 @@ module ActionTrackerHelper | @@ -62,19 +67,19 @@ module ActionTrackerHelper | ||
62 | end | 67 | end |
63 | 68 | ||
64 | def reply_scrap_description ta | 69 | def reply_scrap_description ta |
65 | - _('sent a message to %{receiver}: <br /> "%{message}"') % { | 70 | + (_('sent a message to %{receiver}: <br /> "%{message}"') % { |
66 | receiver: link_to(ta.get_receiver_name, ta.get_receiver_url), | 71 | receiver: link_to(ta.get_receiver_name, ta.get_receiver_url), |
67 | message: auto_link_urls(ta.get_content) | 72 | message: auto_link_urls(ta.get_content) |
68 | - } | 73 | + }).html_safe |
69 | end | 74 | end |
70 | 75 | ||
71 | alias :leave_scrap_description :reply_scrap_description | 76 | alias :leave_scrap_description :reply_scrap_description |
72 | alias :reply_scrap_on_self_description :reply_scrap_description | 77 | alias :reply_scrap_on_self_description :reply_scrap_description |
73 | 78 | ||
74 | def leave_scrap_to_self_description ta | 79 | def leave_scrap_to_self_description ta |
75 | - _('wrote: <br /> "%{text}"') % { | 80 | + (_('wrote: <br /> "%{text}"') % { |
76 | text: auto_link_urls(ta.get_content) | 81 | text: auto_link_urls(ta.get_content) |
77 | - } | 82 | + }).html_safe |
78 | end | 83 | end |
79 | 84 | ||
80 | def favorite_enterprise_description ta | 85 | def favorite_enterprise_description ta |
@@ -0,0 +1,51 @@ | @@ -0,0 +1,51 @@ | ||
1 | +module MentionHelper | ||
2 | + def mention_search_regex | ||
3 | + /(?:^\s?|\s)@([^\s]+)/m | ||
4 | + end | ||
5 | + | ||
6 | + def has_mentions? text="" | ||
7 | + text.scan(mention_search_regex).present? | ||
8 | + end | ||
9 | + | ||
10 | + def get_mentions text="" | ||
11 | + text.scan(mention_search_regex).flatten.uniq | ||
12 | + end | ||
13 | + | ||
14 | + def get_invalid_mentions text, person, profile=nil | ||
15 | + mentions = get_mentions text | ||
16 | + # remove the friends of the user | ||
17 | + mentions -= person.friends.select(:identifier).where(:identifier => mentions).map(&:identifier) | ||
18 | + | ||
19 | + if profile.present? and profile.kind_of?(Community) | ||
20 | + # remove the members of the same community | ||
21 | + mentions -= profile.members.where(:identifier => mentions).map(&:identifier) | ||
22 | + end | ||
23 | + | ||
24 | + # any remaining mention is invalid | ||
25 | + mentions | ||
26 | + end | ||
27 | + | ||
28 | + def remove_invalid_mentions text, invalid_mentions=[] | ||
29 | + if not invalid_mentions.empty? | ||
30 | + # remove the "@" from the invalid mentions, so that the notify job dont send notifications to them | ||
31 | + invalid_mentions.each do |mention| | ||
32 | + text.gsub! "@#{mention}", mention | ||
33 | + end | ||
34 | + end | ||
35 | + | ||
36 | + text | ||
37 | + end | ||
38 | + | ||
39 | + def has_valid_mentions? model, attribute, person, profile=nil | ||
40 | + text = model.send attribute | ||
41 | + | ||
42 | + if has_mentions? text | ||
43 | + invalid_mentions = get_invalid_mentions text, person, profile | ||
44 | + text = remove_invalid_mentions text, invalid_mentions | ||
45 | + # to prevent invalid notifications, remove the invalid mentions | ||
46 | + model.update_attribute attribute, text | ||
47 | + end | ||
48 | + | ||
49 | + has_mentions? text | ||
50 | + end | ||
51 | +end |
@@ -0,0 +1,36 @@ | @@ -0,0 +1,36 @@ | ||
1 | +class NotifyMentionedUsersJob < Struct.new(:tracked_action_id) | ||
2 | + include MentionHelper | ||
3 | + | ||
4 | + def perform | ||
5 | + return unless ActionTracker::Record.exists?(tracked_action_id) | ||
6 | + tracked_action = ActionTracker::Record.find(tracked_action_id) | ||
7 | + | ||
8 | + mention_creator = Person.find(tracked_action.user_id) | ||
9 | + | ||
10 | + remove_followers = false | ||
11 | + mention_text = if tracked_action.target_type == "Comment" | ||
12 | + tracked_action.params["body"] | ||
13 | + else #scrap | ||
14 | + remove_followers = true #scraps already notify followers. | ||
15 | + tracked_action.params["content"] | ||
16 | + end | ||
17 | + | ||
18 | + people_identifiers = get_mentions mention_text | ||
19 | + people_identifiers -= [mention_creator.identifier] | ||
20 | + | ||
21 | + followers = mention_creator.followers | ||
22 | + people_idenfifiers -= followers.map(&:identifier) if (followers.present? && remove_followers) | ||
23 | + | ||
24 | + return if people_identifiers.empty? | ||
25 | + | ||
26 | + people_identifiers = "'" + people_identifiers.join("','") + "'" | ||
27 | + | ||
28 | + notification_sql = "INSERT INTO action_tracker_notifications(profile_id,action_tracker_id) " + | ||
29 | + "SELECT p.id, #{tracked_action.id} FROM profiles AS p " + | ||
30 | + "WHERE p.identifier IN (#{people_identifiers}) AND " + | ||
31 | + "p.id NOT IN (SELECT atn.profile_id FROM action_tracker_notifications AS atn " + | ||
32 | + "WHERE atn.action_tracker_id = #{tracked_action.id})" | ||
33 | + | ||
34 | + ActionTrackerNotification.connection.execute(notification_sql) | ||
35 | + end | ||
36 | +end |
app/models/comment.rb
1 | class Comment < ApplicationRecord | 1 | class Comment < ApplicationRecord |
2 | 2 | ||
3 | + include MentionHelper | ||
4 | + | ||
5 | + track_actions :notify_mentioned_users, | ||
6 | + :after_create, | ||
7 | + :keep_params => ["body", "url"], | ||
8 | + :show_on_wall => false, | ||
9 | + :if => Proc.new {|c| has_valid_mentions?(c, :body, c.author, profile) } # if there is a mention | ||
10 | + | ||
3 | SEARCHABLE_FIELDS = { | 11 | SEARCHABLE_FIELDS = { |
4 | :title => {:label => _('Title'), :weight => 10}, | 12 | :title => {:label => _('Title'), :weight => 10}, |
5 | :name => {:label => _('Name'), :weight => 4}, | 13 | :name => {:label => _('Name'), :weight => 4}, |
@@ -79,7 +87,11 @@ class Comment < ApplicationRecord | @@ -79,7 +87,11 @@ class Comment < ApplicationRecord | ||
79 | end | 87 | end |
80 | 88 | ||
81 | def url | 89 | def url |
82 | - article.view_url.merge(:anchor => anchor) | 90 | + if source.kind_of? ActionTracker::Record |
91 | + source.url.merge(:anchor => anchor) | ||
92 | + else | ||
93 | + article.view_url.merge(:anchor => anchor) | ||
94 | + end | ||
83 | end | 95 | end |
84 | 96 | ||
85 | def message | 97 | def message |
app/models/person.rb
@@ -504,7 +504,11 @@ class Person < Profile | @@ -504,7 +504,11 @@ class Person < Profile | ||
504 | end | 504 | end |
505 | 505 | ||
506 | def self.notify_activity(tracked_action) | 506 | def self.notify_activity(tracked_action) |
507 | - Delayed::Job.enqueue NotifyActivityToProfilesJob.new(tracked_action.id) | 507 | + if tracked_action.verb != "notify_mentioned_users" |
508 | + Delayed::Job.enqueue NotifyActivityToProfilesJob.new(tracked_action.id) | ||
509 | + else | ||
510 | + Delayed::Job.enqueue NotifyMentionedUsersJob.new(tracked_action.id) | ||
511 | + end | ||
508 | end | 512 | end |
509 | 513 | ||
510 | def is_member_of?(profile) | 514 | def is_member_of?(profile) |
app/models/scrap.rb
1 | class Scrap < ApplicationRecord | 1 | class Scrap < ApplicationRecord |
2 | 2 | ||
3 | + include MentionHelper | ||
3 | include SanitizeHelper | 4 | include SanitizeHelper |
4 | 5 | ||
5 | attr_accessible :content, :sender_id, :receiver_id, :scrap_id | 6 | attr_accessible :content, :sender_id, :receiver_id, :scrap_id |
@@ -32,6 +33,12 @@ class Scrap < ApplicationRecord | @@ -32,6 +33,12 @@ class Scrap < ApplicationRecord | ||
32 | 33 | ||
33 | track_actions :reply_scrap_on_self, :after_create, :keep_params => ['sender.name', 'content'], :if => Proc.new{|s| s.sender != s.receiver && s.sender == s.top_root.receiver}, :custom_user => :sender | 34 | track_actions :reply_scrap_on_self, :after_create, :keep_params => ['sender.name', 'content'], :if => Proc.new{|s| s.sender != s.receiver && s.sender == s.top_root.receiver}, :custom_user => :sender |
34 | 35 | ||
36 | + track_actions :notify_mentioned_users, | ||
37 | + :after_create, | ||
38 | + :keep_params => ["content","url"], | ||
39 | + :show_on_wall => false, | ||
40 | + :if => Proc.new {|s| has_valid_mentions?(s, :content, s.sender, s.receiver)} | ||
41 | + | ||
35 | after_create :send_notification | 42 | after_create :send_notification |
36 | 43 | ||
37 | before_validation :strip_all_html_tags | 44 | before_validation :strip_all_html_tags |
@@ -57,6 +64,7 @@ class Scrap < ApplicationRecord | @@ -57,6 +64,7 @@ class Scrap < ApplicationRecord | ||
57 | def scrap_wall_url | 64 | def scrap_wall_url |
58 | is_root? ? root.receiver.wall_url : receiver.wall_url | 65 | is_root? ? root.receiver.wall_url : receiver.wall_url |
59 | end | 66 | end |
67 | + alias url scrap_wall_url | ||
60 | 68 | ||
61 | def send_notification? | 69 | def send_notification? |
62 | sender != receiver && (is_root? ? root.receiver.receives_scrap_notification? : receiver.receives_scrap_notification?) | 70 | sender != receiver && (is_root? ? root.receiver.receives_scrap_notification? : receiver.receives_scrap_notification?) |
app/views/comment/_comment_form.html.erb
@@ -102,3 +102,9 @@ function check_captcha(button, confirm_action) { | @@ -102,3 +102,9 @@ function check_captcha(button, confirm_action) { | ||
102 | </div><!-- end class="page-comment-form" --> | 102 | </div><!-- end class="page-comment-form" --> |
103 | 103 | ||
104 | <%= javascript_include_tag 'comment_form'%> | 104 | <%= javascript_include_tag 'comment_form'%> |
105 | + | ||
106 | +<!-- Files needed for mention system --> | ||
107 | +<%= javascript_include_tag 'vendor/jquery.elastic.min'%> | ||
108 | +<%= javascript_include_tag 'vendor/jquery.events.input.min'%> | ||
109 | +<%= javascript_include_tag 'vendor/jquery.mentionsInput.mod.min'%> | ||
110 | +<%= javascript_include_tag 'user-mention'%> |
@@ -0,0 +1 @@ | @@ -0,0 +1 @@ | ||
1 | +<%= render :partial => 'default_activity', :locals => { :activity => activity, :tab_action => tab_action } %> |
app/views/profile/_profile_activities_list.html.erb
@@ -2,7 +2,7 @@ | @@ -2,7 +2,7 @@ | ||
2 | <% activities.each do |profile_activity| %> | 2 | <% activities.each do |profile_activity| %> |
3 | <% activity = profile_activity.activity %> | 3 | <% activity = profile_activity.activity %> |
4 | <% if activity.kind_of?(ActionTracker::Record) %> | 4 | <% if activity.kind_of?(ActionTracker::Record) %> |
5 | - <%= render :partial => 'profile_activity', :locals => { :activity => activity, :tab_action => 'wall' } if activity.visible? %> | 5 | + <%= render :partial => 'profile_activity', :locals => { :activity => activity, :tab_action => 'wall' } if (activity.visible? && activity.show_on_wall) %> |
6 | <% else %> | 6 | <% else %> |
7 | <%= render :partial => 'profile_scraps', :locals => { :activity => activity, :scrap => activity } %> | 7 | <%= render :partial => 'profile_scraps', :locals => { :activity => activity, :scrap => activity } %> |
8 | <% end %> | 8 | <% end %> |
app/views/profile/_profile_wall.html.erb
@@ -10,3 +10,9 @@ | @@ -10,3 +10,9 @@ | ||
10 | <ul id='profile_activities' class='profile-activities'> | 10 | <ul id='profile_activities' class='profile-activities'> |
11 | <%= render :partial => 'profile_activities_list', :locals => {:activities => @activities} %> | 11 | <%= render :partial => 'profile_activities_list', :locals => {:activities => @activities} %> |
12 | </ul> | 12 | </ul> |
13 | + | ||
14 | +<!-- Files needed for mention system --> | ||
15 | +<%= javascript_include_tag 'vendor/jquery.elastic.min'%> | ||
16 | +<%= javascript_include_tag 'vendor/jquery.events.input.min'%> | ||
17 | +<%= javascript_include_tag 'vendor/jquery.mentionsInput.mod.min'%> | ||
18 | +<%= javascript_include_tag 'user-mention'%> |
@@ -0,0 +1,15 @@ | @@ -0,0 +1,15 @@ | ||
1 | +<div id='profile-network'> | ||
2 | + <ul id='network-activities' class='profile-activities'> | ||
3 | + <% if @activity.kind_of?(ActionTracker::Record) %> | ||
4 | + <%= render :partial => 'profile_activity', :locals => { :activity => @activity, :tab_action => 'wall' } if (@activity.visible? && @activity.show_on_wall) %> | ||
5 | + <% else %> | ||
6 | + <%= render :partial => 'profile_scraps', :locals => { :activity => @activity, :scrap => @activity } %> | ||
7 | + <% end %> | ||
8 | + </ul> | ||
9 | +</div> | ||
10 | + | ||
11 | +<script> | ||
12 | + $(document).ready(function() { | ||
13 | + $(".profile-wall-activities-comments a").trigger("click"); | ||
14 | + }); | ||
15 | +</script> |
config/initializers/action_tracker.rb
@@ -39,6 +39,8 @@ ActionTrackerConfig.verbs = { | @@ -39,6 +39,8 @@ ActionTrackerConfig.verbs = { | ||
39 | favorite_enterprise: { | 39 | favorite_enterprise: { |
40 | }, | 40 | }, |
41 | 41 | ||
42 | + notify_mentioned_users: { | ||
43 | + }, | ||
42 | } | 44 | } |
43 | 45 | ||
44 | ActionTrackerConfig.current_user = proc do | 46 | ActionTrackerConfig.current_user = proc do |
db/migrate/20160714175829_add_show_on_wall_to_action_tracker.rb
0 → 100644
@@ -0,0 +1,46 @@ | @@ -0,0 +1,46 @@ | ||
1 | +(function($, undefined) { | ||
2 | + $(document).ready(function() { | ||
3 | + var is_community = _.isEmpty($('html.profile-type-is-community')) !== true; | ||
4 | + var community = null; | ||
5 | + | ||
6 | + if (is_community) { | ||
7 | + community = noosfero.profile; | ||
8 | + } | ||
9 | + | ||
10 | + $('#comment_body, #leave_scrap_content, form.profile-wall-reply-form textarea').mentionsInput({ | ||
11 | + onDataRequest: function (mode, keyword, onDataRequestCompleteCallback) { | ||
12 | + var search_url = "/search/search_for_users?q="+keyword; | ||
13 | + | ||
14 | + $.ajax({ | ||
15 | + method: "GET", | ||
16 | + url: search_url, | ||
17 | + dataType: "json", | ||
18 | + data: {'community': community}, | ||
19 | + success: function (response) { | ||
20 | + var data = response.map(function(item) { | ||
21 | + return { | ||
22 | + name: item.identifier, | ||
23 | + fullName: item.name, | ||
24 | + id: item.id, | ||
25 | + type: 'contact' | ||
26 | + }; | ||
27 | + }); | ||
28 | + | ||
29 | + // Call this to populate mention. | ||
30 | + onDataRequestCompleteCallback.call(this, data); | ||
31 | + } | ||
32 | + }); // $.ajax end | ||
33 | + }, | ||
34 | + | ||
35 | + triggerChar: '@', | ||
36 | + | ||
37 | + allowRepeat: true, | ||
38 | + | ||
39 | + minChars: 3, | ||
40 | + | ||
41 | + keepTriggerCharacter: true | ||
42 | + }); // mentionsInput end | ||
43 | + }); // ready end | ||
44 | + | ||
45 | +}) (jQuery); | ||
46 | + |
@@ -0,0 +1,162 @@ | @@ -0,0 +1,162 @@ | ||
1 | +/** | ||
2 | +* @name Elastic | ||
3 | +* @descripton Elastic is jQuery plugin that grow and shrink your textareas automatically | ||
4 | +* @version 1.6.11 | ||
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($){ | ||
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 | + 'fontSize', | ||
26 | + 'lineHeight', | ||
27 | + 'fontFamily', | ||
28 | + 'width', | ||
29 | + 'fontWeight', | ||
30 | + 'border-top-width', | ||
31 | + 'border-right-width', | ||
32 | + 'border-bottom-width', | ||
33 | + 'border-left-width', | ||
34 | + 'borderTopStyle', | ||
35 | + 'borderTopColor', | ||
36 | + 'borderRightStyle', | ||
37 | + 'borderRightColor', | ||
38 | + 'borderBottomStyle', | ||
39 | + 'borderBottomColor', | ||
40 | + 'borderLeftStyle', | ||
41 | + 'borderLeftColor' | ||
42 | + ]; | ||
43 | + | ||
44 | + return this.each( function() { | ||
45 | + | ||
46 | + // Elastic only works on textareas | ||
47 | + if ( this.type !== 'textarea' ) { | ||
48 | + return false; | ||
49 | + } | ||
50 | + | ||
51 | + var $textarea = jQuery(this), | ||
52 | + $twin = jQuery('<div />').css({ | ||
53 | + 'position' : 'absolute', | ||
54 | + 'display' : 'none', | ||
55 | + 'word-wrap' : 'break-word', | ||
56 | + 'white-space' :'pre-wrap' | ||
57 | + }), | ||
58 | + lineHeight = parseInt($textarea.css('line-height'),10) || parseInt($textarea.css('font-size'),'10'), | ||
59 | + minheight = parseInt($textarea.css('height'),10) || lineHeight*3, | ||
60 | + maxheight = parseInt($textarea.css('max-height'),10) || Number.MAX_VALUE, | ||
61 | + goalheight = 0; | ||
62 | + | ||
63 | + // Opera returns max-height of -1 if not set | ||
64 | + if (maxheight < 0) { maxheight = Number.MAX_VALUE; } | ||
65 | + | ||
66 | + // Append the twin to the DOM | ||
67 | + // We are going to meassure the height of this, not the textarea. | ||
68 | + $twin.appendTo($textarea.parent()); | ||
69 | + | ||
70 | + // Copy the essential styles (mimics) from the textarea to the twin | ||
71 | + var i = mimics.length; | ||
72 | + while(i--){ | ||
73 | + $twin.css(mimics[i].toString(),$textarea.css(mimics[i].toString())); | ||
74 | + } | ||
75 | + | ||
76 | + // Updates the width of the twin. (solution for textareas with widths in percent) | ||
77 | + function setTwinWidth(){ | ||
78 | + var curatedWidth = Math.floor(parseInt($textarea.width(),10)); | ||
79 | + if($twin.width() !== curatedWidth){ | ||
80 | + $twin.css({'width': curatedWidth + 'px'}); | ||
81 | + | ||
82 | + // Update height of textarea | ||
83 | + update(true); | ||
84 | + } | ||
85 | + } | ||
86 | + | ||
87 | + // Sets a given height and overflow state on the textarea | ||
88 | + function setHeightAndOverflow(height, overflow){ | ||
89 | + | ||
90 | + var curratedHeight = Math.floor(parseInt(height,10)); | ||
91 | + if($textarea.height() !== curratedHeight){ | ||
92 | + $textarea.css({'height': curratedHeight + 'px','overflow':overflow}); | ||
93 | + } | ||
94 | + } | ||
95 | + | ||
96 | + // This function will update the height of the textarea if necessary | ||
97 | + function update(forced) { | ||
98 | + | ||
99 | + // Get curated content from the textarea. | ||
100 | + var textareaContent = $textarea.val().replace(/&/g,'&').replace(/ {2}/g, ' ').replace(/<|>/g, '>').replace(/\n/g, '<br />'); | ||
101 | + | ||
102 | + // Compare curated content with curated twin. | ||
103 | + var twinContent = $twin.html().replace(/<br>/ig,'<br />'); | ||
104 | + | ||
105 | + if(forced || textareaContent+' ' !== twinContent){ | ||
106 | + | ||
107 | + // Add an extra white space so new rows are added when you are at the end of a row. | ||
108 | + $twin.html(textareaContent+' '); | ||
109 | + | ||
110 | + // Change textarea height if twin plus the height of one line differs more than 3 pixel from textarea height | ||
111 | + if(Math.abs($twin.height() + lineHeight - $textarea.height()) > 3){ | ||
112 | + | ||
113 | + var goalheight = $twin.height()+lineHeight; | ||
114 | + if(goalheight >= maxheight) { | ||
115 | + setHeightAndOverflow(maxheight,'auto'); | ||
116 | + } else if(goalheight <= minheight) { | ||
117 | + setHeightAndOverflow(minheight,'hidden'); | ||
118 | + } else { | ||
119 | + setHeightAndOverflow(goalheight,'hidden'); | ||
120 | + } | ||
121 | + | ||
122 | + } | ||
123 | + | ||
124 | + } | ||
125 | + | ||
126 | + } | ||
127 | + | ||
128 | + // Hide scrollbars | ||
129 | + $textarea.css({'overflow':'hidden'}); | ||
130 | + | ||
131 | + // Update textarea size on keyup, change, cut and paste | ||
132 | + $textarea.bind('keyup change cut paste', function(){ | ||
133 | + update(); | ||
134 | + }); | ||
135 | + | ||
136 | + // Update width of twin if browser or textarea is resized (solution for textareas with widths in percent) | ||
137 | + $(window).bind('resize', setTwinWidth); | ||
138 | + $textarea.bind('resize', setTwinWidth); | ||
139 | + $textarea.bind('update', update); | ||
140 | + | ||
141 | + // Compact textarea on blur | ||
142 | + $textarea.bind('blur',function(){ | ||
143 | + if($twin.height() < maxheight){ | ||
144 | + if($twin.height() > minheight) { | ||
145 | + $textarea.height($twin.height()); | ||
146 | + } else { | ||
147 | + $textarea.height(minheight); | ||
148 | + } | ||
149 | + } | ||
150 | + }); | ||
151 | + | ||
152 | + // And this line is to catch the browser paste event | ||
153 | + $textarea.bind('input paste',function(e){ setTimeout( update, 250); }); | ||
154 | + | ||
155 | + // Run update once when elastic is initialized | ||
156 | + update(); | ||
157 | + | ||
158 | + }); | ||
159 | + | ||
160 | + } | ||
161 | + }); | ||
162 | +})(jQuery); | ||
0 | \ No newline at end of file | 163 | \ No newline at end of file |
@@ -0,0 +1 @@ | @@ -0,0 +1 @@ | ||
1 | +!function(t){jQuery.fn.extend({elastic:function(){var e=["paddingTop","paddingRight","paddingBottom","paddingLeft","fontSize","lineHeight","fontFamily","width","fontWeight","border-top-width","border-right-width","border-bottom-width","border-left-width","borderTopStyle","borderTopColor","borderRightStyle","borderRightColor","borderBottomStyle","borderBottomColor","borderLeftStyle","borderLeftColor"];return this.each(function(){function r(){var t=Math.floor(parseInt(n.width(),10));h.width()!==t&&(h.css({width:t+"px"}),o(!0))}function i(t,e){var r=Math.floor(parseInt(t,10));n.height()!==r&&n.css({height:r+"px",overflow:e})}function o(t){var e=n.val().replace(/&/g,"&").replace(/ {2}/g," ").replace(/<|>/g,">").replace(/\n/g,"<br />"),r=h.html().replace(/<br>/gi,"<br />");if((t||e+" "!==r)&&(h.html(e+" "),Math.abs(h.height()+d-n.height())>3)){var o=h.height()+d;o>=s?i(s,"auto"):o<=a?i(a,"hidden"):i(o,"hidden")}}if("textarea"!==this.type)return!1;var n=jQuery(this),h=jQuery("<div />").css({position:"absolute",display:"none","word-wrap":"break-word","white-space":"pre-wrap"}),d=parseInt(n.css("line-height"),10)||parseInt(n.css("font-size"),"10"),a=parseInt(n.css("height"),10)||3*d,s=parseInt(n.css("max-height"),10)||Number.MAX_VALUE;s<0&&(s=Number.MAX_VALUE),h.appendTo(n.parent());for(var p=e.length;p--;)h.css(e[p].toString(),n.css(e[p].toString()));n.css({overflow:"hidden"}),n.bind("keyup change cut paste",function(){o()}),t(window).bind("resize",r),n.bind("resize",r),n.bind("update",o),n.bind("blur",function(){h.height()<s&&(h.height()>a?n.height(h.height()):n.height(a))}),n.bind("input paste",function(t){setTimeout(o,250)}),o()})}})}(jQuery); |
@@ -0,0 +1,132 @@ | @@ -0,0 +1,132 @@ | ||
1 | +/* | ||
2 | + jQuery `input` special event v1.0 | ||
3 | + | ||
4 | + http://whattheheadsaid.com/projects/input-special-event | ||
5 | + | ||
6 | + (c) 2010-2011 Andy Earnshaw | ||
7 | + MIT license | ||
8 | + www.opensource.org/licenses/mit-license.php | ||
9 | + | ||
10 | + Modified by Kenneth Auchenberg | ||
11 | + * Disabled usage of onPropertyChange event in IE, since its a bit delayed, if you type really fast. | ||
12 | +*/ | ||
13 | + | ||
14 | +(function($) { | ||
15 | + // Handler for propertychange events only | ||
16 | + function propHandler() { | ||
17 | + var $this = $(this); | ||
18 | + if (window.event.propertyName == "value" && !$this.data("triggering.inputEvent")) { | ||
19 | + $this.data("triggering.inputEvent", true).trigger("input"); | ||
20 | + window.setTimeout(function () { | ||
21 | + $this.data("triggering.inputEvent", false); | ||
22 | + }, 0); | ||
23 | + } | ||
24 | + } | ||
25 | + | ||
26 | + $.event.special.input = { | ||
27 | + setup: function(data, namespaces) { | ||
28 | + var timer, | ||
29 | + // Get a reference to the element | ||
30 | + elem = this, | ||
31 | + // Store the current state of the element | ||
32 | + state = elem.value, | ||
33 | + // Create a dummy element that we can use for testing event support | ||
34 | + tester = document.createElement(this.tagName), | ||
35 | + // Check for native oninput | ||
36 | + oninput = "oninput" in tester || checkEvent(tester), | ||
37 | + // Check for onpropertychange | ||
38 | + onprop = "onpropertychange" in tester, | ||
39 | + // Generate a random namespace for event bindings | ||
40 | + ns = "inputEventNS" + ~~(Math.random() * 10000000), | ||
41 | + // Last resort event names | ||
42 | + evts = ["focus", "blur", "paste", "cut", "keydown", "drop", ""].join("." + ns + " "); | ||
43 | + | ||
44 | + function checkState() { | ||
45 | + var $this = $(elem); | ||
46 | + if (elem.value != state && !$this.data("triggering.inputEvent")) { | ||
47 | + state = elem.value; | ||
48 | + | ||
49 | + $this.data("triggering.inputEvent", true).trigger("input"); | ||
50 | + window.setTimeout(function () { | ||
51 | + $this.data("triggering.inputEvent", false); | ||
52 | + }, 0); | ||
53 | + } | ||
54 | + } | ||
55 | + | ||
56 | + // Set up a function to handle the different events that may fire | ||
57 | + function handler(e) { | ||
58 | + // When focusing, set a timer that polls for changes to the value | ||
59 | + if (e.type == "focus") { | ||
60 | + checkState(); | ||
61 | + clearInterval(timer); | ||
62 | + timer = window.setInterval(checkState, 250); | ||
63 | + } else if (e.type == "blur") { | ||
64 | + // When blurring, cancel the aforeset timer | ||
65 | + window.clearInterval(timer); | ||
66 | + } else { | ||
67 | + // For all other events, queue a timer to check state ASAP | ||
68 | + window.setTimeout(checkState, 0); | ||
69 | + } | ||
70 | + } | ||
71 | + | ||
72 | + // Bind to native event if available | ||
73 | + if (oninput) { | ||
74 | + return false; | ||
75 | +// } else if (onprop) { | ||
76 | +// // Else fall back to propertychange if available | ||
77 | +// $(this).find("input, textarea").andSelf().filter("input, textarea").bind("propertychange." + ns, propHandler); | ||
78 | + } else { | ||
79 | + // Else clutch at straws! | ||
80 | + $(this).find("input, textarea").andSelf().filter("input, textarea").bind(evts, handler); | ||
81 | + } | ||
82 | + $(this).data("inputEventHandlerNS", ns); | ||
83 | + }, | ||
84 | + teardown: function () { | ||
85 | + var elem = $(this); | ||
86 | + elem.find("input, textarea").unbind(elem.data("inputEventHandlerNS")); | ||
87 | + elem.data("inputEventHandlerNS", ""); | ||
88 | + } | ||
89 | + }; | ||
90 | + | ||
91 | + // Setup our jQuery shorthand method | ||
92 | + $.fn.input = function (handler) { | ||
93 | + return handler ? this.bind("input", handler) : this.trigger("input"); | ||
94 | + }; | ||
95 | + | ||
96 | + /* | ||
97 | + The following function tests the element for oninput support in Firefox. Many thanks to | ||
98 | + http://blog.danielfriesen.name/2010/02/16/html5-browser-maze-oninput-support/ | ||
99 | + */ | ||
100 | + function checkEvent(el) { | ||
101 | + // First check, for if Firefox fixes its issue with el.oninput = function | ||
102 | + el.setAttribute("oninput", "return"); | ||
103 | + if (typeof el.oninput == "function") { | ||
104 | + return true; | ||
105 | + } | ||
106 | + // Second check, because Firefox doesn't map oninput attribute to oninput property | ||
107 | + try { | ||
108 | + | ||
109 | + // "* Note * : Disabled focus and dispatch of keypress event due to conflict with DOMready, which resulted in scrolling down to the bottom of the page, possibly because layout wasn't finished rendering. | ||
110 | + var e = document.createEvent("KeyboardEvent"), | ||
111 | + ok = false, | ||
112 | + tester = function(e) { | ||
113 | + ok = true; | ||
114 | + e.preventDefault(); | ||
115 | + e.stopPropagation(); | ||
116 | + }; | ||
117 | + | ||
118 | + // e.initKeyEvent("keypress", true, true, window, false, false, false, false, 0, "e".charCodeAt(0)); | ||
119 | + | ||
120 | + document.body.appendChild(el); | ||
121 | + el.addEventListener("input", tester, false); | ||
122 | + // el.focus(); | ||
123 | + // el.dispatchEvent(e); | ||
124 | + el.removeEventListener("input", tester, false); | ||
125 | + document.body.removeChild(el); | ||
126 | + return ok; | ||
127 | + | ||
128 | + } catch(error) { | ||
129 | + | ||
130 | + } | ||
131 | + } | ||
132 | +})(jQuery); | ||
0 | \ No newline at end of file | 133 | \ No newline at end of file |
@@ -0,0 +1 @@ | @@ -0,0 +1 @@ | ||
1 | +!function(t){function n(t){if(t.setAttribute("oninput","return"),"function"==typeof t.oninput)return!0;try{var n=(document.createEvent("KeyboardEvent"),!1),e=function(t){n=!0,t.preventDefault(),t.stopPropagation()};return document.body.appendChild(t),t.addEventListener("input",e,!1),t.removeEventListener("input",e,!1),document.body.removeChild(t),n}catch(t){}}t.event.special.input={setup:function(e,i){function u(){var n=t(o);o.value==d||n.data("triggering.inputEvent")||(d=o.value,n.data("triggering.inputEvent",!0).trigger("input"),window.setTimeout(function(){n.data("triggering.inputEvent",!1)},0))}function r(t){"focus"==t.type?(u(),clearInterval(a),a=window.setInterval(u,250)):"blur"==t.type?window.clearInterval(a):window.setTimeout(u,0)}var a,o=this,d=o.value,p=document.createElement(this.tagName),v="oninput"in p||n(p),c="inputEventNS"+~~(1e7*Math.random()),f=["focus","blur","paste","cut","keydown","drop",""].join("."+c+" ");return!v&&(t(this).find("input, textarea").andSelf().filter("input, textarea").bind(f,r),void t(this).data("inputEventHandlerNS",c))},teardown:function(){var n=t(this);n.find("input, textarea").unbind(n.data("inputEventHandlerNS")),n.data("inputEventHandlerNS","")}},t.fn.input=function(t){return t?this.bind("input",t):this.trigger("input")}}(jQuery); |
@@ -0,0 +1,596 @@ | @@ -0,0 +1,596 @@ | ||
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 | + // Fix for Noosfero rewrite on interpolate setting | ||
14 | + _.templateSettings = { | ||
15 | + interpolate: /<%=([\s\S]+?)%>/g | ||
16 | + }; | ||
17 | + | ||
18 | + // Settings | ||
19 | + var KEY = { BACKSPACE : 8, TAB : 9, RETURN : 13, ESC : 27, LEFT : 37, UP : 38, RIGHT : 39, DOWN : 40, COMMA : 188, SPACE : 32, HOME : 36, END : 35 }; // Keys "enum" | ||
20 | + | ||
21 | + //Default settings | ||
22 | + var defaultSettings = { | ||
23 | + triggerChar : '@', //Char that respond to event | ||
24 | + onDataRequest : $.noop, //Function where we can search the data | ||
25 | + minChars : 2, //Minimum chars to fire the event | ||
26 | + allowRepeat : false, //Allow repeat mentions | ||
27 | + showAvatars : true, //Show the avatars | ||
28 | + elastic : true, //Grow the textarea automatically | ||
29 | + defaultValue : '', | ||
30 | + onCaret : false, | ||
31 | + keepTriggerCharacter: false, | ||
32 | + classes : { | ||
33 | + autoCompleteItemActive : "active" //Classes to apply in each item | ||
34 | + }, | ||
35 | + templates : { | ||
36 | + wrapper : _.template('<div class="mentions-input-box"></div>'), | ||
37 | + autocompleteList : _.template('<div class="mentions-autocomplete-list"></div>'), | ||
38 | + autocompleteListItem : _.template('<li data-ref-id="<%= id %>" data-ref-type="<%= type %>" data-display="<%= display %>"><%= content %> <small><%= fullName %></small></li>'), | ||
39 | + autocompleteListItemAvatar : _.template('<img src="<%= avatar %>" />'), | ||
40 | + autocompleteListItemIcon : _.template('<div class="icon <%= icon %>"></div>'), | ||
41 | + mentionsOverlay : _.template('<div class="mentions"><div></div></div>'), | ||
42 | + mentionItemSyntax : _.template('@[<%= value %>](<%= type %>:<%= id %>)'), | ||
43 | + mentionItemHighlight : _.template('<strong><span><%= value %></span></strong>') | ||
44 | + } | ||
45 | + }; | ||
46 | + | ||
47 | + //Class util | ||
48 | + var utils = { | ||
49 | + //Encodes the character with _.escape function (undersocre) | ||
50 | + htmlEncode : function (str) { | ||
51 | + return _.escape(str); | ||
52 | + }, | ||
53 | + //Encodes the character to be used with RegExp | ||
54 | + regexpEncode : function (str) { | ||
55 | + return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); | ||
56 | + }, | ||
57 | + highlightTerm : function (value, term) { | ||
58 | + if (!term && !term.length) { | ||
59 | + return value; | ||
60 | + } | ||
61 | + return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>"); | ||
62 | + }, | ||
63 | + //Sets the caret in a valid position | ||
64 | + setCaratPosition : function (domNode, caretPos) { | ||
65 | + if (domNode.createTextRange) { | ||
66 | + var range = domNode.createTextRange(); | ||
67 | + range.move('character', caretPos); | ||
68 | + range.select(); | ||
69 | + } else { | ||
70 | + if (domNode.selectionStart) { | ||
71 | + domNode.focus(); | ||
72 | + domNode.setSelectionRange(caretPos, caretPos); | ||
73 | + } else { | ||
74 | + domNode.focus(); | ||
75 | + } | ||
76 | + } | ||
77 | + }, | ||
78 | + //Deletes the white spaces | ||
79 | + rtrim: function(string) { | ||
80 | + return string.replace(/\s+$/,""); | ||
81 | + } | ||
82 | + }; | ||
83 | + | ||
84 | + //Main class of MentionsInput plugin | ||
85 | + var MentionsInput = function (settings) { | ||
86 | + | ||
87 | + var domInput, | ||
88 | + elmInputBox, | ||
89 | + elmInputWrapper, | ||
90 | + elmAutocompleteList, | ||
91 | + elmWrapperBox, | ||
92 | + elmMentionsOverlay, | ||
93 | + elmActiveAutoCompleteItem, | ||
94 | + mentionsCollection = [], | ||
95 | + autocompleteItemCollection = {}, | ||
96 | + inputBuffer = [], | ||
97 | + currentDataQuery = ''; | ||
98 | + | ||
99 | + //Mix the default setting with the users settings | ||
100 | + settings = $.extend(true, {}, defaultSettings, settings ); | ||
101 | + | ||
102 | + //Initializes the text area target | ||
103 | + function initTextarea() { | ||
104 | + elmInputBox = $(domInput); //Get the text area target | ||
105 | + | ||
106 | + //If the text area is already configured, return | ||
107 | + if (elmInputBox.attr('data-mentions-input') === 'true') { | ||
108 | + return; | ||
109 | + } | ||
110 | + | ||
111 | + elmInputWrapper = elmInputBox.parent(); //Get the DOM element parent | ||
112 | + elmWrapperBox = $(settings.templates.wrapper()); | ||
113 | + elmInputBox.wrapAll(elmWrapperBox); //Wrap all the text area into the div elmWrapperBox | ||
114 | + elmWrapperBox = elmInputWrapper.find('> div.mentions-input-box'); //Obtains the div elmWrapperBox that now contains the text area | ||
115 | + | ||
116 | + elmInputBox.attr('data-mentions-input', 'true'); //Sets the attribute data-mentions-input to true -> Defines if the text area is already configured | ||
117 | + elmInputBox.bind('keydown', onInputBoxKeyDown); //Bind the keydown event to the text area | ||
118 | + elmInputBox.bind('keypress', onInputBoxKeyPress); //Bind the keypress event to the text area | ||
119 | + elmInputBox.bind('click', onInputBoxClick); //Bind the click event to the text area | ||
120 | + elmInputBox.bind('blur', onInputBoxBlur); //Bind the blur event to the text area | ||
121 | + | ||
122 | + if (navigator.userAgent.indexOf("MSIE 8") > -1) { | ||
123 | + elmInputBox.bind('propertychange', onInputBoxInput); //IE8 won't fire the input event, so let's bind to the propertychange | ||
124 | + } else { | ||
125 | + elmInputBox.bind('input', onInputBoxInput); //Bind the input event to the text area | ||
126 | + } | ||
127 | + | ||
128 | + // Elastic textareas, grow automatically | ||
129 | + if( settings.elastic ) { | ||
130 | + elmInputBox.elastic(); | ||
131 | + } | ||
132 | + } | ||
133 | + | ||
134 | + //Initializes the autocomplete list, append to elmWrapperBox and delegate the mousedown event to li elements | ||
135 | + function initAutocomplete() { | ||
136 | + elmAutocompleteList = $(settings.templates.autocompleteList()); //Get the HTML code for the list | ||
137 | + elmAutocompleteList.appendTo(elmWrapperBox); //Append to elmWrapperBox element | ||
138 | + elmAutocompleteList.delegate('li', 'mousedown', onAutoCompleteItemClick); //Delegate the event | ||
139 | + } | ||
140 | + | ||
141 | + //Initializes the mentions' overlay | ||
142 | + function initMentionsOverlay() { | ||
143 | + elmMentionsOverlay = $(settings.templates.mentionsOverlay()); //Get the HTML code of the mentions' overlay | ||
144 | + elmMentionsOverlay.prependTo(elmWrapperBox); //Insert into elmWrapperBox the mentions overlay | ||
145 | + } | ||
146 | + | ||
147 | + //Updates the values of the main variables | ||
148 | + function updateValues() { | ||
149 | + var syntaxMessage = getInputBoxValue(); //Get the actual value of the text area | ||
150 | + | ||
151 | + _.each(mentionsCollection, function (mention) { | ||
152 | + var textSyntax = settings.templates.mentionItemSyntax(mention); | ||
153 | + syntaxMessage = syntaxMessage.replace(new RegExp(utils.regexpEncode(mention.value), 'g'), textSyntax); | ||
154 | + }); | ||
155 | + | ||
156 | + var mentionText = utils.htmlEncode(syntaxMessage); //Encode the syntaxMessage | ||
157 | + | ||
158 | + _.each(mentionsCollection, function (mention) { | ||
159 | + var formattedMention = _.extend({}, mention, {value: utils.htmlEncode(mention.value)}); | ||
160 | + var textSyntax = settings.templates.mentionItemSyntax(formattedMention); | ||
161 | + var textHighlight = settings.templates.mentionItemHighlight(formattedMention); | ||
162 | + | ||
163 | + mentionText = mentionText.replace(new RegExp(utils.regexpEncode(textSyntax), 'g'), textHighlight); | ||
164 | + }); | ||
165 | + | ||
166 | + mentionText = mentionText.replace(/\n/g, '<br />'); //Replace the escape character for <br /> | ||
167 | + mentionText = mentionText.replace(/ {2}/g, ' '); //Replace the 2 preceding token to | ||
168 | + | ||
169 | + elmInputBox.data('messageText', syntaxMessage); //Save the messageText to elmInputBox | ||
170 | + elmInputBox.trigger('updated'); | ||
171 | + elmMentionsOverlay.find('div').html(mentionText); //Insert into a div of the elmMentionsOverlay the mention text | ||
172 | + } | ||
173 | + | ||
174 | + //Cleans the buffer | ||
175 | + function resetBuffer() { | ||
176 | + inputBuffer = []; | ||
177 | + } | ||
178 | + | ||
179 | + //Updates the mentions collection | ||
180 | + function updateMentionsCollection() { | ||
181 | + var inputText = getInputBoxValue(); //Get the actual value of text area | ||
182 | + | ||
183 | + //Returns the values that doesn't match the condition | ||
184 | + mentionsCollection = _.reject(mentionsCollection, function (mention, index) { | ||
185 | + return !mention.value || inputText.indexOf(mention.value) == -1; | ||
186 | + }); | ||
187 | + mentionsCollection = _.compact(mentionsCollection); //Delete all the falsy values of the array and return the new array | ||
188 | + } | ||
189 | + | ||
190 | + //Adds mention to mentions collections | ||
191 | + function addMention(mention) { | ||
192 | + | ||
193 | + var currentMessage = getInputBoxValue(), | ||
194 | + caretStart = elmInputBox[0].selectionStart, | ||
195 | + shortestDistance = false, | ||
196 | + bestLastIndex = false; | ||
197 | + | ||
198 | + // Using a regex to figure out positions | ||
199 | + var regex = new RegExp("\\" + settings.triggerChar + currentDataQuery, "gi"), | ||
200 | + regexMatch; | ||
201 | + | ||
202 | + while(regexMatch = regex.exec(currentMessage)) { | ||
203 | + if (shortestDistance === false || Math.abs(regex.lastIndex - caretStart) < shortestDistance) { | ||
204 | + shortestDistance = Math.abs(regex.lastIndex - caretStart); | ||
205 | + bestLastIndex = regex.lastIndex; | ||
206 | + } | ||
207 | + } | ||
208 | + | ||
209 | + var startCaretPosition = bestLastIndex - currentDataQuery.length - 1; //Set the start caret position (right before the @) | ||
210 | + var currentCaretPosition = bestLastIndex; //Set the current caret position (right after the end of the "mention") | ||
211 | + | ||
212 | + | ||
213 | + var start = currentMessage.substr(0, startCaretPosition); | ||
214 | + var end = currentMessage.substr(currentCaretPosition, currentMessage.length); | ||
215 | + var startEndIndex = (start + mention.value).length + 1; | ||
216 | + | ||
217 | + // See if there's the same mention in the list | ||
218 | + if( !_.find(mentionsCollection, function (object) { return object.id == mention.id; }) ) { | ||
219 | + mentionsCollection.push(mention);//Add the mention to mentionsColletions | ||
220 | + } | ||
221 | + | ||
222 | + // Cleaning before inserting the value, otherwise auto-complete would be triggered with "old" inputbuffer | ||
223 | + resetBuffer(); | ||
224 | + currentDataQuery = ''; | ||
225 | + hideAutoComplete(); | ||
226 | + | ||
227 | + // Mentions and syntax message | ||
228 | + var updatedMessageText = start; | ||
229 | + | ||
230 | + if (settings.keepTriggerCharacter) { | ||
231 | + updatedMessageText += settings.triggerChar; | ||
232 | + } | ||
233 | + | ||
234 | + updatedMessageText += mention.value + ' ' + end; | ||
235 | + elmInputBox.val(updatedMessageText); //Set the value to the txt area | ||
236 | + elmInputBox.trigger('mention'); | ||
237 | + updateValues(); | ||
238 | + | ||
239 | + // Set correct focus and selection | ||
240 | + elmInputBox.focus(); | ||
241 | + utils.setCaratPosition(elmInputBox[0], startEndIndex); | ||
242 | + } | ||
243 | + | ||
244 | + //Gets the actual value of the text area without white spaces from the beginning and end of the value | ||
245 | + function getInputBoxValue() { | ||
246 | + return $.trim(elmInputBox.val()); | ||
247 | + } | ||
248 | + | ||
249 | + // This is taken straight from live (as of Sep 2012) GitHub code. The | ||
250 | + // technique is known around the web. Just google it. Github's is quite | ||
251 | + // succint though. NOTE: relies on selectionEnd, which as far as IE is concerned, | ||
252 | + // it'll only work on 9+. Good news is nothing will happen if the browser | ||
253 | + // doesn't support it. | ||
254 | + function textareaSelectionPosition($el) { | ||
255 | + var a, b, c, d, e, f, g, h, i, j, k; | ||
256 | + if (!(i = $el[0])) return; | ||
257 | + if (!$(i).is("textarea")) return; | ||
258 | + if (i.selectionEnd == null) return; | ||
259 | + g = { | ||
260 | + position: "absolute", | ||
261 | + overflow: "auto", | ||
262 | + whiteSpace: "pre-wrap", | ||
263 | + wordWrap: "break-word", | ||
264 | + boxSizing: "content-box", | ||
265 | + top: 0, | ||
266 | + left: -9999 | ||
267 | + }, h = ["boxSizing", "fontFamily", "fontSize", "fontStyle", "fontVariant", "fontWeight", "height", "letterSpacing", "lineHeight", "paddingBottom", "paddingLeft", "paddingRight", "paddingTop", "textDecoration", "textIndent", "textTransform", "width", "word-spacing"]; | ||
268 | + for (j = 0, k = h.length; j < k; j++) e = h[j], g[e] = $(i).css(e); | ||
269 | + 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 | ||
270 | + } | ||
271 | + | ||
272 | + //same as above function but return offset instead of position | ||
273 | + function textareaSelectionOffset($el) { | ||
274 | + var a, b, c, d, e, f, g, h, i, j, k; | ||
275 | + if (!(i = $el[0])) return; | ||
276 | + if (!$(i).is("textarea")) return; | ||
277 | + if (i.selectionEnd == null) return; | ||
278 | + g = { | ||
279 | + position: "absolute", | ||
280 | + overflow: "auto", | ||
281 | + whiteSpace: "pre-wrap", | ||
282 | + wordWrap: "break-word", | ||
283 | + boxSizing: "content-box", | ||
284 | + top: 0, | ||
285 | + left: -9999 | ||
286 | + }, h = ["boxSizing", "fontFamily", "fontSize", "fontStyle", "fontVariant", "fontWeight", "height", "letterSpacing", "lineHeight", "paddingBottom", "paddingLeft", "paddingRight", "paddingTop", "textDecoration", "textIndent", "textTransform", "width", "word-spacing"]; | ||
287 | + for (j = 0, k = h.length; j < k; j++) e = h[j], g[e] = $(i).css(e); | ||
288 | + 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).offset(), $(c).remove(), f | ||
289 | + } | ||
290 | + | ||
291 | + //Scrolls back to the input after autocomplete if the window has scrolled past the input | ||
292 | + function scrollToInput() { | ||
293 | + var elmDistanceFromTop = $(elmInputBox).offset().top; //input offset | ||
294 | + var bodyDistanceFromTop = $('body').offset().top; //body offset | ||
295 | + var distanceScrolled = $(window).scrollTop(); //distance scrolled | ||
296 | + | ||
297 | + if (distanceScrolled > elmDistanceFromTop) { | ||
298 | + //subtracts body distance to handle fixed headers | ||
299 | + $(window).scrollTop(elmDistanceFromTop - bodyDistanceFromTop); | ||
300 | + } | ||
301 | + } | ||
302 | + | ||
303 | + //Takes the click event when the user select a item of the dropdown | ||
304 | + function onAutoCompleteItemClick(e) { | ||
305 | + var elmTarget = $(this); //Get the item selected | ||
306 | + var mention = autocompleteItemCollection[elmTarget.attr('data-uid')]; //Obtains the mention | ||
307 | + | ||
308 | + addMention(mention); | ||
309 | + scrollToInput(); | ||
310 | + return false; | ||
311 | + } | ||
312 | + | ||
313 | + //Takes the click event on text area | ||
314 | + function onInputBoxClick(e) { | ||
315 | + resetBuffer(); | ||
316 | + } | ||
317 | + | ||
318 | + //Takes the blur event on text area | ||
319 | + function onInputBoxBlur(e) { | ||
320 | + hideAutoComplete(); | ||
321 | + } | ||
322 | + | ||
323 | + //Takes the input event when users write or delete something | ||
324 | + function onInputBoxInput(e) { | ||
325 | + updateValues(); | ||
326 | + updateMentionsCollection(); | ||
327 | + | ||
328 | + var triggerCharIndex = _.lastIndexOf(inputBuffer, settings.triggerChar); //Returns the last match of the triggerChar in the inputBuffer | ||
329 | + if (triggerCharIndex > -1) { //If the triggerChar is present in the inputBuffer array | ||
330 | + currentDataQuery = inputBuffer.slice(triggerCharIndex + 1).join(''); //Gets the currentDataQuery | ||
331 | + currentDataQuery = utils.rtrim(currentDataQuery); //Deletes the whitespaces | ||
332 | + _.defer(_.bind(doSearch, this, currentDataQuery)); //Invoking the function doSearch ( Bind the function to this) | ||
333 | + } | ||
334 | + } | ||
335 | + | ||
336 | + //Takes the keypress event | ||
337 | + function onInputBoxKeyPress(e) { | ||
338 | + if(e.keyCode !== KEY.BACKSPACE) { //If the key pressed is not the backspace | ||
339 | + var typedValue = String.fromCharCode(e.which || e.keyCode); //Takes the string that represent this CharCode | ||
340 | + inputBuffer.push(typedValue); //Push the value pressed into inputBuffer | ||
341 | + } | ||
342 | + } | ||
343 | + | ||
344 | + //Takes the keydown event | ||
345 | + function onInputBoxKeyDown(e) { | ||
346 | + | ||
347 | + // This also matches HOME/END on OSX which is CMD+LEFT, CMD+RIGHT | ||
348 | + if (e.keyCode === KEY.LEFT || e.keyCode === KEY.RIGHT || e.keyCode === KEY.HOME || e.keyCode === KEY.END) { | ||
349 | + // Defer execution to ensure carat pos has changed after HOME/END keys then call the resetBuffer function | ||
350 | + _.defer(resetBuffer); | ||
351 | + | ||
352 | + // IE9 doesn't fire the oninput event when backspace or delete is pressed. This causes the highlighting | ||
353 | + // to stay on the screen whenever backspace is pressed after a highlighed word. This is simply a hack | ||
354 | + // to force updateValues() to fire when backspace/delete is pressed in IE9. | ||
355 | + if (navigator.userAgent.indexOf("MSIE 9") > -1) { | ||
356 | + _.defer(updateValues); //Call the updateValues function | ||
357 | + } | ||
358 | + | ||
359 | + return; | ||
360 | + } | ||
361 | + | ||
362 | + //If the key pressed was the backspace | ||
363 | + if (e.keyCode === KEY.BACKSPACE) { | ||
364 | + inputBuffer = inputBuffer.slice(0, -1 + inputBuffer.length); // Can't use splice, not available in IE | ||
365 | + return; | ||
366 | + } | ||
367 | + | ||
368 | + //If the elmAutocompleteList is hidden | ||
369 | + if (!elmAutocompleteList.is(':visible')) { | ||
370 | + return true; | ||
371 | + } | ||
372 | + | ||
373 | + switch (e.keyCode) { | ||
374 | + case KEY.UP: //If the key pressed was UP or DOWN | ||
375 | + case KEY.DOWN: | ||
376 | + var elmCurrentAutoCompleteItem = null; | ||
377 | + if (e.keyCode === KEY.DOWN) { //If the key pressed was DOWN | ||
378 | + if (elmActiveAutoCompleteItem && elmActiveAutoCompleteItem.length) { //If elmActiveAutoCompleteItem exits | ||
379 | + elmCurrentAutoCompleteItem = elmActiveAutoCompleteItem.next(); //Gets the next li element in the list | ||
380 | + } else { | ||
381 | + elmCurrentAutoCompleteItem = elmAutocompleteList.find('li').first(); //Gets the first li element found | ||
382 | + } | ||
383 | + } else { | ||
384 | + elmCurrentAutoCompleteItem = $(elmActiveAutoCompleteItem).prev(); //The key pressed was UP and gets the previous li element | ||
385 | + } | ||
386 | + if (elmCurrentAutoCompleteItem.length) { | ||
387 | + selectAutoCompleteItem(elmCurrentAutoCompleteItem); | ||
388 | + } | ||
389 | + return false; | ||
390 | + case KEY.RETURN: //If the key pressed was RETURN or TAB | ||
391 | + case KEY.TAB: | ||
392 | + if (elmActiveAutoCompleteItem && elmActiveAutoCompleteItem.length) { //If the elmActiveAutoCompleteItem exists | ||
393 | + elmActiveAutoCompleteItem.trigger('mousedown'); //Calls the mousedown event | ||
394 | + return false; | ||
395 | + } | ||
396 | + break; | ||
397 | + } | ||
398 | + | ||
399 | + return true; | ||
400 | + } | ||
401 | + | ||
402 | + //Hides the autoomplete | ||
403 | + function hideAutoComplete() { | ||
404 | + elmActiveAutoCompleteItem = null; | ||
405 | + elmAutocompleteList.empty().hide(); | ||
406 | + } | ||
407 | + | ||
408 | + //Selects the item in the autocomplete list | ||
409 | + function selectAutoCompleteItem(elmItem) { | ||
410 | + elmItem.addClass(settings.classes.autoCompleteItemActive); //Add the class active to item | ||
411 | + elmItem.siblings().removeClass(settings.classes.autoCompleteItemActive); //Gets all li elements in autocomplete list and remove the class active | ||
412 | + | ||
413 | + elmActiveAutoCompleteItem = elmItem; //Sets the item to elmActiveAutoCompleteItem | ||
414 | + } | ||
415 | + | ||
416 | + //Populates dropdown | ||
417 | + function populateDropdown(query, results) { | ||
418 | + elmAutocompleteList.show(); //Shows the autocomplete list | ||
419 | + | ||
420 | + if(!settings.allowRepeat) { | ||
421 | + // Filter items that has already been mentioned | ||
422 | + var mentionValues = _.pluck(mentionsCollection, 'value'); | ||
423 | + results = _.reject(results, function (item) { | ||
424 | + return _.include(mentionValues, item.name); | ||
425 | + }); | ||
426 | + } | ||
427 | + | ||
428 | + if (!results.length) { //If there are not elements hide the autocomplete list | ||
429 | + hideAutoComplete(); | ||
430 | + return; | ||
431 | + } | ||
432 | + | ||
433 | + elmAutocompleteList.empty(); //Remove all li elements in autocomplete list | ||
434 | + var elmDropDownList = $("<ul>").appendTo(elmAutocompleteList).hide(); //Inserts a ul element to autocomplete div and hide it | ||
435 | + | ||
436 | + _.each(results, function (item, index) { | ||
437 | + var itemUid = _.uniqueId('mention_'); //Gets the item with unique id | ||
438 | + | ||
439 | + autocompleteItemCollection[itemUid] = _.extend({}, item, {value: item.name}); //Inserts the new item to autocompleteItemCollection | ||
440 | + | ||
441 | + var elmListItem = $(settings.templates.autocompleteListItem({ | ||
442 | + 'id' : utils.htmlEncode(item.id), | ||
443 | + 'display' : utils.htmlEncode(item.name), | ||
444 | + 'type' : utils.htmlEncode(item.type), | ||
445 | + 'fullName': utils.htmlEncode(item.fullName), | ||
446 | + 'content' : utils.highlightTerm(utils.htmlEncode((item.display ? item.display : item.name)), query) | ||
447 | + })).attr('data-uid', itemUid); //Inserts the new item to list | ||
448 | + | ||
449 | + //If the index is 0 | ||
450 | + if (index === 0) { | ||
451 | + selectAutoCompleteItem(elmListItem); | ||
452 | + } | ||
453 | + | ||
454 | + //If show avatars is true | ||
455 | + if (settings.showAvatars) { | ||
456 | + var elmIcon; | ||
457 | + | ||
458 | + //If the item has an avatar | ||
459 | + if (item.avatar) { | ||
460 | + elmIcon = $(settings.templates.autocompleteListItemAvatar({ avatar : item.avatar })); | ||
461 | + } else { //If not then we set an default icon | ||
462 | + elmIcon = $(settings.templates.autocompleteListItemIcon({ icon : item.icon })); | ||
463 | + } | ||
464 | + elmIcon.prependTo(elmListItem); //Inserts the elmIcon to elmListItem | ||
465 | + } | ||
466 | + elmListItem = elmListItem.appendTo(elmDropDownList); //Insets the elmListItem to elmDropDownList | ||
467 | + }); | ||
468 | + | ||
469 | + elmAutocompleteList.show(); //Shows the elmAutocompleteList div | ||
470 | + if (settings.onCaret) { | ||
471 | + positionAutocomplete(elmAutocompleteList, elmInputBox); | ||
472 | + } | ||
473 | + elmDropDownList.show(); //Shows the elmDropDownList | ||
474 | + } | ||
475 | + | ||
476 | + //Search into data list passed as parameter | ||
477 | + function doSearch(query) { | ||
478 | + //If the query is not null, undefined, empty and has the minimum chars | ||
479 | + if (query && query.length && query.length >= settings.minChars) { | ||
480 | + //Call the onDataRequest function and then call the populateDropDrown | ||
481 | + settings.onDataRequest.call(this, 'search', query, function (responseData) { | ||
482 | + populateDropdown(query, responseData); | ||
483 | + }); | ||
484 | + } else { //If the query is null, undefined, empty or has not the minimun chars | ||
485 | + hideAutoComplete(); //Hide the autocompletelist | ||
486 | + } | ||
487 | + } | ||
488 | + | ||
489 | + function positionAutocomplete(elmAutocompleteList, elmInputBox) { | ||
490 | + var elmAutocompleteListPosition = elmAutocompleteList.css('position'); | ||
491 | + if (elmAutocompleteListPosition == 'absolute') { | ||
492 | + var position = textareaSelectionPosition(elmInputBox), | ||
493 | + lineHeight = parseInt(elmInputBox.css('line-height'), 10) || 18; | ||
494 | + elmAutocompleteList.css('width', '15em'); // Sort of a guess | ||
495 | + elmAutocompleteList.css('left', position.left); | ||
496 | + elmAutocompleteList.css('top', lineHeight + position.top); | ||
497 | + | ||
498 | + //check if the right position of auto complete is larger than the right position of the input | ||
499 | + //if yes, reset the left of auto complete list to make it fit the input | ||
500 | + var elmInputBoxRight = elmInputBox.offset().left + elmInputBox.width(), | ||
501 | + elmAutocompleteListRight = elmAutocompleteList.offset().left + elmAutocompleteList.width(); | ||
502 | + if (elmInputBoxRight <= elmAutocompleteListRight) { | ||
503 | + elmAutocompleteList.css('left', Math.abs(elmAutocompleteList.position().left - (elmAutocompleteListRight - elmInputBoxRight))); | ||
504 | + } | ||
505 | + } | ||
506 | + else if (elmAutocompleteListPosition == 'fixed') { | ||
507 | + var offset = textareaSelectionOffset(elmInputBox), | ||
508 | + lineHeight = parseInt(elmInputBox.css('line-height'), 10) || 18; | ||
509 | + elmAutocompleteList.css('width', '15em'); // Sort of a guess | ||
510 | + elmAutocompleteList.css('left', offset.left + 10000); | ||
511 | + elmAutocompleteList.css('top', lineHeight + offset.top); | ||
512 | + } | ||
513 | + } | ||
514 | + | ||
515 | + //Resets the text area | ||
516 | + function resetInput(currentVal) { | ||
517 | + mentionsCollection = []; | ||
518 | + var mentionText = utils.htmlEncode(currentVal); | ||
519 | + var regex = new RegExp("(" + settings.triggerChar + ")\\[(.*?)\\]\\((.*?):(.*?)\\)", "gi"); | ||
520 | + var match, newMentionText = mentionText; | ||
521 | + while ((match = regex.exec(mentionText)) != null) { | ||
522 | + newMentionText = newMentionText.replace(match[0], match[1] + match[2]); | ||
523 | + mentionsCollection.push({ 'id': match[4], 'type': match[3], 'value': match[2], 'trigger': match[1] }); | ||
524 | + } | ||
525 | + elmInputBox.val(newMentionText); | ||
526 | + updateValues(); | ||
527 | + } | ||
528 | + // Public methods | ||
529 | + return { | ||
530 | + //Initializes the mentionsInput component on a specific element. | ||
531 | + init : function (domTarget) { | ||
532 | + | ||
533 | + domInput = domTarget; | ||
534 | + | ||
535 | + initTextarea(); | ||
536 | + initAutocomplete(); | ||
537 | + initMentionsOverlay(); | ||
538 | + resetInput(settings.defaultValue); | ||
539 | + | ||
540 | + //If the autocomplete list has prefill mentions | ||
541 | + if( settings.prefillMention ) { | ||
542 | + addMention( settings.prefillMention ); | ||
543 | + } | ||
544 | + }, | ||
545 | + | ||
546 | + //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. | ||
547 | + val : function (callback) { | ||
548 | + if (!_.isFunction(callback)) { | ||
549 | + return; | ||
550 | + } | ||
551 | + callback.call(this, mentionsCollection.length ? elmInputBox.data('messageText') : getInputBoxValue()); | ||
552 | + }, | ||
553 | + | ||
554 | + //Resets the text area value and clears all mentions | ||
555 | + reset : function () { | ||
556 | + resetInput(); | ||
557 | + }, | ||
558 | + | ||
559 | + //Reinit with the text area value if it was changed programmatically | ||
560 | + reinit : function () { | ||
561 | + resetInput(false); | ||
562 | + }, | ||
563 | + | ||
564 | + //An async method which accepts a callback function and returns a collection of mentions as hash objects as a first parameter. | ||
565 | + getMentions : function (callback) { | ||
566 | + if (!_.isFunction(callback)) { | ||
567 | + return; | ||
568 | + } | ||
569 | + callback.call(this, mentionsCollection); | ||
570 | + } | ||
571 | + }; | ||
572 | + }; | ||
573 | + | ||
574 | + //Main function to include into jQuery and initialize the plugin | ||
575 | + $.fn.mentionsInput = function (method, settings) { | ||
576 | + | ||
577 | + var outerArguments = arguments; //Gets the arguments | ||
578 | + //If method is not a function | ||
579 | + if (typeof method === 'object' || !method) { | ||
580 | + settings = method; | ||
581 | + } | ||
582 | + | ||
583 | + return this.each(function () { | ||
584 | + var instance = $.data(this, 'mentionsInput') || $.data(this, 'mentionsInput', new MentionsInput(settings)); | ||
585 | + | ||
586 | + if (_.isFunction(instance[method])) { | ||
587 | + return instance[method].apply(this, Array.prototype.slice.call(outerArguments, 1)); | ||
588 | + } else if (typeof method === 'object' || !method) { | ||
589 | + return instance.init.call(this, this); | ||
590 | + } else { | ||
591 | + $.error('Method ' + method + ' does not exist'); | ||
592 | + } | ||
593 | + }); | ||
594 | + }; | ||
595 | + | ||
596 | +})(jQuery, _.runInContext()); |
public/javascripts/vendor/jquery.mentionsInput.mod.min.js
0 → 100644
@@ -0,0 +1 @@ | @@ -0,0 +1 @@ | ||
1 | +!function(e,t,n){t.templateSettings={interpolate:/<%=([\s\S]+?)%>/g};var i={BACKSPACE:8,TAB:9,RETURN:13,ESC:27,LEFT:37,UP:38,RIGHT:39,DOWN:40,COMMA:188,SPACE:32,HOME:36,END:35},a={triggerChar:"@",onDataRequest:e.noop,minChars:2,allowRepeat:!1,showAvatars:!0,elastic:!0,defaultValue:"",onCaret:!1,keepTriggerCharacter:!1,classes:{autoCompleteItemActive:"active"},templates:{wrapper:t.template('<div class="mentions-input-box"></div>'),autocompleteList:t.template('<div class="mentions-autocomplete-list"></div>'),autocompleteListItem:t.template('<li data-ref-id="<%= id %>" data-ref-type="<%= type %>" data-display="<%= display %>"><%= content %> <small><%= fullName %></small></li>'),autocompleteListItemAvatar:t.template('<img src="<%= avatar %>" />'),autocompleteListItemIcon:t.template('<div class="icon <%= icon %>"></div>'),mentionsOverlay:t.template('<div class="mentions"><div></div></div>'),mentionItemSyntax:t.template("@[<%= value %>](<%= type %>:<%= id %>)"),mentionItemHighlight:t.template("<strong><span><%= value %></span></strong>")}},o={htmlEncode:function(e){return t.escape(e)},regexpEncode:function(e){return e.replace(/([.*+?^=!:${}()|\[\]\/\\])/g,"\\$1")},highlightTerm:function(e,t){return t||t.length?e.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)("+t+")(?![^<>]*>)(?![^&;]+;)","gi"),"<b>$1</b>"):e},setCaratPosition:function(e,t){if(e.createTextRange){var n=e.createTextRange();n.move("character",t),n.select()}else e.selectionStart?(e.focus(),e.setSelectionRange(t,t)):e.focus()},rtrim:function(e){return e.replace(/\s+$/,"")}},r=function(n){function r(){M=e(k),"true"!==M.attr("data-mentions-input")&&(L=M.parent(),O=e(n.templates.wrapper()),M.wrapAll(O),O=L.find("> div.mentions-input-box"),M.attr("data-mentions-input","true"),M.bind("keydown",b),M.bind("keypress",E),M.bind("click",x),M.bind("blur",C),navigator.userAgent.indexOf("MSIE 8")>-1?M.bind("propertychange",w):M.bind("input",w),n.elastic&&M.elastic())}function l(){N=e(n.templates.autocompleteList()),N.appendTo(O),N.delegate("li","mousedown",v)}function s(){H=e(n.templates.mentionsOverlay()),H.prependTo(O)}function c(){var e=f();t.each(P,function(t){var i=n.templates.mentionItemSyntax(t);e=e.replace(new RegExp(o.regexpEncode(t.value),"g"),i)});var i=o.htmlEncode(e);t.each(P,function(e){var a=t.extend({},e,{value:o.htmlEncode(e.value)}),r=n.templates.mentionItemSyntax(a),l=n.templates.mentionItemHighlight(a);i=i.replace(new RegExp(o.regexpEncode(r),"g"),l)}),i=i.replace(/\n/g,"<br />"),i=i.replace(/ {2}/g," "),M.data("messageText",e),M.trigger("updated"),H.find("div").html(i)}function d(){F=[]}function p(){var e=f();P=t.reject(P,function(t,n){return!t.value||e.indexOf(t.value)==-1}),P=t.compact(P)}function u(e){for(var i,a=f(),r=M[0].selectionStart,l=!1,s=!1,p=new RegExp("\\"+n.triggerChar+W,"gi");i=p.exec(a);)(l===!1||Math.abs(p.lastIndex-r)<l)&&(l=Math.abs(p.lastIndex-r),s=p.lastIndex);var u=s-W.length-1,m=s,g=a.substr(0,u),h=a.substr(m,a.length),v=(g+e.value).length+1;t.find(P,function(t){return t.id==e.id})||P.push(e),d(),W="",y();var x=g;n.keepTriggerCharacter&&(x+=n.triggerChar),x+=e.value+" "+h,M.val(x),M.trigger("mention"),c(),M.focus(),o.setCaratPosition(M[0],v)}function f(){return e.trim(M.val())}function m(t){var n,i,a,o,r,l,s,c,d,p,u;if((d=t[0])&&e(d).is("textarea")&&null!=d.selectionEnd){for(s={position:"absolute",overflow:"auto",whiteSpace:"pre-wrap",wordWrap:"break-word",boxSizing:"content-box",top:0,left:-9999},c=["boxSizing","fontFamily","fontSize","fontStyle","fontVariant","fontWeight","height","letterSpacing","lineHeight","paddingBottom","paddingLeft","paddingRight","paddingTop","textDecoration","textIndent","textTransform","width","word-spacing"],p=0,u=c.length;p<u;p++)r=c[p],s[r]=e(d).css(r);return a=document.createElement("div"),e(a).css(s),e(d).after(a),i=document.createTextNode(d.value.substring(0,d.selectionEnd)),n=document.createTextNode(d.value.substring(d.selectionEnd)),o=document.createElement("span"),o.innerHTML=" ",a.appendChild(i),a.appendChild(o),a.appendChild(n),a.scrollTop=d.scrollTop,l=e(o).position(),e(a).remove(),l}}function g(t){var n,i,a,o,r,l,s,c,d,p,u;if((d=t[0])&&e(d).is("textarea")&&null!=d.selectionEnd){for(s={position:"absolute",overflow:"auto",whiteSpace:"pre-wrap",wordWrap:"break-word",boxSizing:"content-box",top:0,left:-9999},c=["boxSizing","fontFamily","fontSize","fontStyle","fontVariant","fontWeight","height","letterSpacing","lineHeight","paddingBottom","paddingLeft","paddingRight","paddingTop","textDecoration","textIndent","textTransform","width","word-spacing"],p=0,u=c.length;p<u;p++)r=c[p],s[r]=e(d).css(r);return a=document.createElement("div"),e(a).css(s),e(d).after(a),i=document.createTextNode(d.value.substring(0,d.selectionEnd)),n=document.createTextNode(d.value.substring(d.selectionEnd)),o=document.createElement("span"),o.innerHTML=" ",a.appendChild(i),a.appendChild(o),a.appendChild(n),a.scrollTop=d.scrollTop,l=e(o).offset(),e(a).remove(),l}}function h(){var t=e(M).offset().top,n=e("body").offset().top,i=e(window).scrollTop();i>t&&e(window).scrollTop(t-n)}function v(t){var n=e(this),i=B[n.attr("data-uid")];return u(i),h(),!1}function x(e){d()}function C(e){y()}function w(e){c(),p();var i=t.lastIndexOf(F,n.triggerChar);i>-1&&(W=F.slice(i+1).join(""),W=o.rtrim(W),t.defer(t.bind(S,this,W)))}function E(e){if(e.keyCode!==i.BACKSPACE){var t=String.fromCharCode(e.which||e.keyCode);F.push(t)}}function b(n){if(n.keyCode===i.LEFT||n.keyCode===i.RIGHT||n.keyCode===i.HOME||n.keyCode===i.END)return t.defer(d),void(navigator.userAgent.indexOf("MSIE 9")>-1&&t.defer(c));if(n.keyCode===i.BACKSPACE)return void(F=F.slice(0,-1+F.length));if(!N.is(":visible"))return!0;switch(n.keyCode){case i.UP:case i.DOWN:var a=null;return a=n.keyCode===i.DOWN?D&&D.length?D.next():N.find("li").first():e(D).prev(),a.length&&T(a),!1;case i.RETURN:case i.TAB:if(D&&D.length)return D.trigger("mousedown"),!1}return!0}function y(){D=null,N.empty().hide()}function T(e){e.addClass(n.classes.autoCompleteItemActive),e.siblings().removeClass(n.classes.autoCompleteItemActive),D=e}function I(i,a){if(N.show(),!n.allowRepeat){var r=t.pluck(P,"value");a=t.reject(a,function(e){return t.include(r,e.name)})}if(!a.length)return void y();N.empty();var l=e("<ul>").appendTo(N).hide();t.each(a,function(a,r){var s=t.uniqueId("mention_");B[s]=t.extend({},a,{value:a.name});var c=e(n.templates.autocompleteListItem({id:o.htmlEncode(a.id),display:o.htmlEncode(a.name),type:o.htmlEncode(a.type),fullName:o.htmlEncode(a.fullName),content:o.highlightTerm(o.htmlEncode(a.display?a.display:a.name),i)})).attr("data-uid",s);if(0===r&&T(c),n.showAvatars){var d;d=e(a.avatar?n.templates.autocompleteListItemAvatar({avatar:a.avatar}):n.templates.autocompleteListItemIcon({icon:a.icon})),d.prependTo(c)}c=c.appendTo(l)}),N.show(),n.onCaret&&A(N,M),l.show()}function S(e){e&&e.length&&e.length>=n.minChars?n.onDataRequest.call(this,"search",e,function(t){I(e,t)}):y()}function A(e,t){var n=e.css("position");if("absolute"==n){var i=m(t),a=parseInt(t.css("line-height"),10)||18;e.css("width","15em"),e.css("left",i.left),e.css("top",a+i.top);var o=t.offset().left+t.width(),r=e.offset().left+e.width();o<=r&&e.css("left",Math.abs(e.position().left-(r-o)))}else if("fixed"==n){var l=g(t),a=parseInt(t.css("line-height"),10)||18;e.css("width","15em"),e.css("left",l.left+1e4),e.css("top",a+l.top)}}function R(e){P=[];for(var t,i=o.htmlEncode(e),a=new RegExp("("+n.triggerChar+")\\[(.*?)\\]\\((.*?):(.*?)\\)","gi"),r=i;null!=(t=a.exec(i));)r=r.replace(t[0],t[1]+t[2]),P.push({id:t[4],type:t[3],value:t[2],trigger:t[1]});M.val(r),c()}var k,M,L,N,O,H,D,P=[],B={},F=[],W="";return n=e.extend(!0,{},a,n),{init:function(e){k=e,r(),l(),s(),R(n.defaultValue),n.prefillMention&&u(n.prefillMention)},val:function(e){t.isFunction(e)&&e.call(this,P.length?M.data("messageText"):f())},reset:function(){R()},reinit:function(){R(!1)},getMentions:function(e){t.isFunction(e)&&e.call(this,P)}}};e.fn.mentionsInput=function(n,i){var a=arguments;return"object"!=typeof n&&n||(i=n),this.each(function(){var o=e.data(this,"mentionsInput")||e.data(this,"mentionsInput",new r(i));return t.isFunction(o[n])?o[n].apply(this,Array.prototype.slice.call(a,1)):"object"!=typeof n&&n?void e.error("Method "+n+" does not exist"):o.init.call(this,this)})}}(jQuery,_.runInContext()); |
public/stylesheets/application.scss
@@ -48,6 +48,7 @@ | @@ -48,6 +48,7 @@ | ||
48 | @import 'profile-members'; | 48 | @import 'profile-members'; |
49 | @import 'profile-search'; | 49 | @import 'profile-search'; |
50 | @import 'profile-activity'; | 50 | @import 'profile-activity'; |
51 | +@import 'jquery.mentionsInput.min'; | ||
51 | // admin | 52 | // admin |
52 | @import 'admin-panel'; | 53 | @import 'admin-panel'; |
53 | @import 'manage-fields'; | 54 | @import 'manage-fields'; |
public/stylesheets/comments.scss
@@ -366,3 +366,11 @@ a.comment-picture { | @@ -366,3 +366,11 @@ a.comment-picture { | ||
366 | margin-bottom: 40px; | 366 | margin-bottom: 40px; |
367 | } | 367 | } |
368 | 368 | ||
369 | +textarea[data-mentions-input='true'] { | ||
370 | + min-height: 70px; | ||
371 | +} | ||
372 | + | ||
373 | +.mentions-input-box .mentions > div > strong { | ||
374 | + background: none; | ||
375 | +} | ||
376 | + |
@@ -0,0 +1,121 @@ | @@ -0,0 +1,121 @@ | ||
1 | + | ||
2 | +.mentions-input-box { | ||
3 | + position: relative; | ||
4 | + background: #fff; | ||
5 | +} | ||
6 | + | ||
7 | +.mentions-input-box textarea { | ||
8 | + width: 100%; | ||
9 | + display: block; | ||
10 | + height: 18px; | ||
11 | + padding: 9px; | ||
12 | + border: 1px solid #dcdcdc; | ||
13 | + border-radius:3px; | ||
14 | + overflow: hidden; | ||
15 | + background: transparent; | ||
16 | + position: relative; | ||
17 | + outline: 0; | ||
18 | + resize: none; | ||
19 | + | ||
20 | + -webkit-box-sizing: border-box; | ||
21 | + -moz-box-sizing: border-box; | ||
22 | + box-sizing: border-box; | ||
23 | +} | ||
24 | + | ||
25 | +.mentions-input-box .mentions-autocomplete-list { | ||
26 | + display: none; | ||
27 | + background: #fff; | ||
28 | + border: 1px solid #b2b2b2; | ||
29 | + position: absolute; | ||
30 | + left: 0; | ||
31 | + right: 0; | ||
32 | + z-index: 10000; | ||
33 | + margin-top: -2px; | ||
34 | + | ||
35 | + border-radius:5px; | ||
36 | + border-top-right-radius:0; | ||
37 | + border-top-left-radius:0; | ||
38 | + | ||
39 | + -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); | ||
40 | + -moz-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); | ||
41 | + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); | ||
42 | +} | ||
43 | + | ||
44 | +.mentions-input-box .mentions-autocomplete-list ul { | ||
45 | + margin: 0; | ||
46 | + padding: 0; | ||
47 | +} | ||
48 | + | ||
49 | +.mentions-input-box .mentions-autocomplete-list li { | ||
50 | + background-color: #fff; | ||
51 | + padding: 0 5px; | ||
52 | + margin: 0; | ||
53 | + width: auto; | ||
54 | + border-bottom: 1px solid #eee; | ||
55 | + height: 26px; | ||
56 | + line-height: 26px; | ||
57 | + overflow: hidden; | ||
58 | + cursor: pointer; | ||
59 | + list-style: none; | ||
60 | + white-space: nowrap; | ||
61 | +} | ||
62 | + | ||
63 | +.mentions-input-box .mentions-autocomplete-list li:last-child { | ||
64 | + border-radius:5px; | ||
65 | +} | ||
66 | + | ||
67 | +.mentions-input-box .mentions-autocomplete-list li > img, | ||
68 | +.mentions-input-box .mentions-autocomplete-list li > div.icon { | ||
69 | + width: 16px; | ||
70 | + height: 16px; | ||
71 | + float: left; | ||
72 | + margin-top:5px; | ||
73 | + margin-right: 5px; | ||
74 | + -moz-background-origin:3px; | ||
75 | + | ||
76 | + border-radius:3px; | ||
77 | +} | ||
78 | + | ||
79 | +.mentions-input-box .mentions-autocomplete-list li em { | ||
80 | + font-weight: bold; | ||
81 | + font-style: none; | ||
82 | +} | ||
83 | + | ||
84 | +.mentions-input-box .mentions-autocomplete-list li:hover, | ||
85 | +.mentions-input-box .mentions-autocomplete-list li.active { | ||
86 | + background-color: #f2f2f2; | ||
87 | +} | ||
88 | + | ||
89 | +.mentions-input-box .mentions-autocomplete-list li b { | ||
90 | + background: #ffff99; | ||
91 | + font-weight: normal; | ||
92 | +} | ||
93 | + | ||
94 | +.mentions-input-box .mentions { | ||
95 | + position: absolute; | ||
96 | + left: 1px; | ||
97 | + right: 0; | ||
98 | + top: 1px; | ||
99 | + bottom: 0; | ||
100 | + padding: 9px; | ||
101 | + color: #fff; | ||
102 | + overflow: hidden; | ||
103 | + | ||
104 | + white-space: pre-wrap; | ||
105 | + word-wrap: break-word; | ||
106 | +} | ||
107 | + | ||
108 | +.mentions-input-box .mentions > div { | ||
109 | + color: #fff; | ||
110 | + white-space: pre-wrap; | ||
111 | + width: 100%; | ||
112 | +} | ||
113 | + | ||
114 | +.mentions-input-box .mentions > div > strong { | ||
115 | + font-weight:normal; | ||
116 | + background: #d8dfea; | ||
117 | +} | ||
118 | + | ||
119 | +.mentions-input-box .mentions > div > strong > span { | ||
120 | + filter: progid:DXImageTransform.Microsoft.Alpha(opacity=0); | ||
121 | +} |
@@ -0,0 +1 @@ | @@ -0,0 +1 @@ | ||
1 | +.mentions-input-box{position:relative;background:#fff}.mentions-input-box textarea{width:100%;display:block;height:18px;padding:9px;border:1px solid #dcdcdc;border-radius:3px;overflow:hidden;background:transparent;position:relative;outline:0;resize:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.mentions-input-box .mentions-autocomplete-list{display:none;background:#fff;border:1px solid #b2b2b2;position:absolute;left:0;right:0;z-index:10000;margin-top:-2px;border-radius:5px;border-top-right-radius:0;border-top-left-radius:0;-webkit-box-shadow:0 2px 5px rgba(0,0,0,0.148438);-moz-box-shadow:0 2px 5px rgba(0,0,0,0.148438);box-shadow:0 2px 5px rgba(0,0,0,0.148438)}.mentions-input-box .mentions-autocomplete-list ul{margin:0;padding:0}.mentions-input-box .mentions-autocomplete-list li{background-color:#fff;padding:0 5px;margin:0;width:auto;border-bottom:1px solid #eee;height:26px;line-height:26px;overflow:hidden;cursor:pointer;list-style:none;white-space:nowrap}.mentions-input-box .mentions-autocomplete-list li:last-child{border-radius:5px}.mentions-input-box .mentions-autocomplete-list li>img,.mentions-input-box .mentions-autocomplete-list li>div.icon{width:16px;height:16px;float:left;margin-top:5px;margin-right:5px;-moz-background-origin:3px;border-radius:3px}.mentions-input-box .mentions-autocomplete-list li em{font-weight:bold;font-style:none}.mentions-input-box .mentions-autocomplete-list li:hover,.mentions-input-box .mentions-autocomplete-list li.active{background-color:#f2f2f2}.mentions-input-box .mentions-autocomplete-list li b{background:#ff9;font-weight:normal}.mentions-input-box .mentions{position:absolute;left:1px;right:0;top:1px;bottom:0;padding:9px;color:#fff;overflow:hidden;white-space:pre-wrap;word-wrap:break-word}.mentions-input-box .mentions>div{color:#fff;white-space:pre-wrap;width:100%}.mentions-input-box .mentions>div>strong{font-weight:normal;background:#d8dfea}.mentions-input-box .mentions>div>strong>span{filter:alpha(opacity=0)} |
vendor/plugins/action_tracker/lib/action_tracker.rb
@@ -68,7 +68,9 @@ module ActionTracker | @@ -68,7 +68,9 @@ module ActionTracker | ||
68 | post_proc = options.delete(:post_processing) || options.delete('post_processing') || Proc.new{} | 68 | post_proc = options.delete(:post_processing) || options.delete('post_processing') || Proc.new{} |
69 | custom_user = options.delete(:custom_user) || options.delete('custom_user') || nil | 69 | custom_user = options.delete(:custom_user) || options.delete('custom_user') || nil |
70 | custom_target = options.delete(:custom_target) || options.delete('custom_target') || nil | 70 | custom_target = options.delete(:custom_target) || options.delete('custom_target') || nil |
71 | - send(callback, Proc.new { |tracked| tracked.save_action_for_verb(verb.to_s, keep_params, post_proc, custom_user, custom_target) }, options) | 71 | + show_on_wall = options.symbolize_keys.delete(:show_on_wall) |
72 | + show_on_wall = true if show_on_wall.nil? | ||
73 | + send(callback, Proc.new { |tracked| tracked.save_action_for_verb(verb.to_s, keep_params, post_proc, custom_user, custom_target, show_on_wall) }, options) | ||
72 | send :include, InstanceMethods | 74 | send :include, InstanceMethods |
73 | end | 75 | end |
74 | 76 | ||
@@ -88,7 +90,7 @@ module ActionTracker | @@ -88,7 +90,7 @@ module ActionTracker | ||
88 | time.to_f | 90 | time.to_f |
89 | end | 91 | end |
90 | 92 | ||
91 | - def save_action_for_verb(verb, keep_params = :all, post_proc = Proc.new{}, custom_user = nil, custom_target = nil) | 93 | + def save_action_for_verb(verb, keep_params = :all, post_proc = Proc.new{}, custom_user = nil, custom_target = nil, show_on_wall = true) |
92 | user = self.send(custom_user) unless custom_user.blank? | 94 | user = self.send(custom_user) unless custom_user.blank? |
93 | user ||= ActionTracker::Record.current_user | 95 | user ||= ActionTracker::Record.current_user |
94 | target = self.send(custom_target) unless custom_target.blank? | 96 | target = self.send(custom_target) unless custom_target.blank? |
@@ -107,12 +109,13 @@ module ActionTracker | @@ -107,12 +109,13 @@ module ActionTracker | ||
107 | end | 109 | end |
108 | tracked_action = case ActionTrackerConfig.verb_type(verb) | 110 | tracked_action = case ActionTrackerConfig.verb_type(verb) |
109 | when :groupable | 111 | when :groupable |
110 | - Record.add_or_create :verb => verb, :params => stored_params, :user => user, :target => target | 112 | + Record.add_or_create :verb => verb, :params => stored_params, :user => user, :target => target, :show_on_wall => show_on_wall |
111 | when :updatable | 113 | when :updatable |
112 | - Record.update_or_create :verb => verb, :params => stored_params, :user => user, :target => target | 114 | + Record.update_or_create :verb => verb, :params => stored_params, :user => user, :target => target, :show_on_wall => show_on_wall |
113 | when :single | 115 | when :single |
114 | - Record.new :verb => verb, :params => stored_params, :user => user | 116 | + Record.new :verb => verb, :params => stored_params, :user => user, :show_on_wall => show_on_wall |
115 | end | 117 | end |
118 | + | ||
116 | tracked_action.target = target || self | 119 | tracked_action.target = target || self |
117 | user.tracked_actions << tracked_action | 120 | user.tracked_actions << tracked_action |
118 | post_proc.call tracked_action | 121 | post_proc.call tracked_action |
vendor/plugins/action_tracker/lib/action_tracker_model.rb
1 | module ActionTracker | 1 | module ActionTracker |
2 | class Record < ActiveRecord::Base | 2 | class Record < ActiveRecord::Base |
3 | - attr_accessible :verb, :params, :user, :target | 3 | + attr_accessible :verb, :params, :user, :target, :show_on_wall |
4 | 4 | ||
5 | self.table_name = 'action_tracker' | 5 | self.table_name = 'action_tracker' |
6 | 6 | ||
@@ -58,6 +58,10 @@ module ActionTracker | @@ -58,6 +58,10 @@ module ActionTracker | ||
58 | l | 58 | l |
59 | end | 59 | end |
60 | 60 | ||
61 | + def url | ||
62 | + {:controller => "profile", :action => "show_tracked_action", :activity_id => self.id} | ||
63 | + end | ||
64 | + | ||
61 | def self.time_spent(conditions = {}) # In seconds | 65 | def self.time_spent(conditions = {}) # In seconds |
62 | #FIXME Better if it could be completely done in the database, but SQLite does not support difference between two timestamps | 66 | #FIXME Better if it could be completely done in the database, but SQLite does not support difference between two timestamps |
63 | time = 0 | 67 | time = 0 |