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
app/controllers/public/search_controller.rb
... | ... | @@ -154,6 +154,33 @@ class SearchController < PublicController |
154 | 154 | render :text => find_suggestions(normalize_term(params[:term]), environment, params[:asset]).to_json |
155 | 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 | 185 | protected |
159 | 186 | ... | ... |
app/helpers/action_tracker_helper.rb
... | ... | @@ -24,6 +24,11 @@ module ActionTrackerHelper |
24 | 24 | } |
25 | 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 | 32 | def join_community_description ta |
28 | 33 | n_('has joined 1 community:<br />%{name}', 'has joined %{num} communities:<br />%{name}', ta.get_resource_name.size).html_safe % { |
29 | 34 | num: ta.get_resource_name.size, |
... | ... | @@ -62,19 +67,19 @@ module ActionTrackerHelper |
62 | 67 | end |
63 | 68 | |
64 | 69 | def reply_scrap_description ta |
65 | - _('sent a message to %{receiver}: <br /> "%{message}"') % { | |
70 | + (_('sent a message to %{receiver}: <br /> "%{message}"') % { | |
66 | 71 | receiver: link_to(ta.get_receiver_name, ta.get_receiver_url), |
67 | 72 | message: auto_link_urls(ta.get_content) |
68 | - } | |
73 | + }).html_safe | |
69 | 74 | end |
70 | 75 | |
71 | 76 | alias :leave_scrap_description :reply_scrap_description |
72 | 77 | alias :reply_scrap_on_self_description :reply_scrap_description |
73 | 78 | |
74 | 79 | def leave_scrap_to_self_description ta |
75 | - _('wrote: <br /> "%{text}"') % { | |
80 | + (_('wrote: <br /> "%{text}"') % { | |
76 | 81 | text: auto_link_urls(ta.get_content) |
77 | - } | |
82 | + }).html_safe | |
78 | 83 | end |
79 | 84 | |
80 | 85 | def favorite_enterprise_description ta | ... | ... |
... | ... | @@ -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 @@ |
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 | 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 | 11 | SEARCHABLE_FIELDS = { |
4 | 12 | :title => {:label => _('Title'), :weight => 10}, |
5 | 13 | :name => {:label => _('Name'), :weight => 4}, |
... | ... | @@ -79,7 +87,11 @@ class Comment < ApplicationRecord |
79 | 87 | end |
80 | 88 | |
81 | 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 | 95 | end |
84 | 96 | |
85 | 97 | def message | ... | ... |
app/models/person.rb
... | ... | @@ -504,7 +504,11 @@ class Person < Profile |
504 | 504 | end |
505 | 505 | |
506 | 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 | 512 | end |
509 | 513 | |
510 | 514 | def is_member_of?(profile) | ... | ... |
app/models/scrap.rb
1 | 1 | class Scrap < ApplicationRecord |
2 | 2 | |
3 | + include MentionHelper | |
3 | 4 | include SanitizeHelper |
4 | 5 | |
5 | 6 | attr_accessible :content, :sender_id, :receiver_id, :scrap_id |
... | ... | @@ -32,6 +33,12 @@ class Scrap < ApplicationRecord |
32 | 33 | |
33 | 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 | 42 | after_create :send_notification |
36 | 43 | |
37 | 44 | before_validation :strip_all_html_tags |
... | ... | @@ -57,6 +64,7 @@ class Scrap < ApplicationRecord |
57 | 64 | def scrap_wall_url |
58 | 65 | is_root? ? root.receiver.wall_url : receiver.wall_url |
59 | 66 | end |
67 | + alias url scrap_wall_url | |
60 | 68 | |
61 | 69 | def send_notification? |
62 | 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 | 102 | </div><!-- end class="page-comment-form" --> |
103 | 103 | |
104 | 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 @@ |
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 | 2 | <% activities.each do |profile_activity| %> |
3 | 3 | <% activity = profile_activity.activity %> |
4 | 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 | 6 | <% else %> |
7 | 7 | <%= render :partial => 'profile_scraps', :locals => { :activity => activity, :scrap => activity } %> |
8 | 8 | <% end %> | ... | ... |
app/views/profile/_profile_wall.html.erb
... | ... | @@ -10,3 +10,9 @@ |
10 | 10 | <ul id='profile_activities' class='profile-activities'> |
11 | 11 | <%= render :partial => 'profile_activities_list', :locals => {:activities => @activities} %> |
12 | 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 @@ |
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
db/migrate/20160714175829_add_show_on_wall_to_action_tracker.rb
0 → 100644
... | ... | @@ -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 @@ |
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 | 163 | \ No newline at end of file | ... | ... |
... | ... | @@ -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 @@ |
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 | 133 | \ No newline at end of file | ... | ... |
... | ... | @@ -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 @@ |
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 @@ |
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
public/stylesheets/comments.scss
... | ... | @@ -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 @@ |
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 | 68 | post_proc = options.delete(:post_processing) || options.delete('post_processing') || Proc.new{} |
69 | 69 | custom_user = options.delete(:custom_user) || options.delete('custom_user') || nil |
70 | 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 | 74 | send :include, InstanceMethods |
73 | 75 | end |
74 | 76 | |
... | ... | @@ -88,7 +90,7 @@ module ActionTracker |
88 | 90 | time.to_f |
89 | 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 | 94 | user = self.send(custom_user) unless custom_user.blank? |
93 | 95 | user ||= ActionTracker::Record.current_user |
94 | 96 | target = self.send(custom_target) unless custom_target.blank? |
... | ... | @@ -107,12 +109,13 @@ module ActionTracker |
107 | 109 | end |
108 | 110 | tracked_action = case ActionTrackerConfig.verb_type(verb) |
109 | 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 | 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 | 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 | 117 | end |
118 | + | |
116 | 119 | tracked_action.target = target || self |
117 | 120 | user.tracked_actions << tracked_action |
118 | 121 | post_proc.call tracked_action | ... | ... |
vendor/plugins/action_tracker/lib/action_tracker_model.rb
1 | 1 | module ActionTracker |
2 | 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 | 5 | self.table_name = 'action_tracker' |
6 | 6 | |
... | ... | @@ -58,6 +58,10 @@ module ActionTracker |
58 | 58 | l |
59 | 59 | end |
60 | 60 | |
61 | + def url | |
62 | + {:controller => "profile", :action => "show_tracked_action", :activity_id => self.id} | |
63 | + end | |
64 | + | |
61 | 65 | def self.time_spent(conditions = {}) # In seconds |
62 | 66 | #FIXME Better if it could be completely done in the database, but SQLite does not support difference between two timestamps |
63 | 67 | time = 0 | ... | ... |