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 | ... | ... |