Commit ba4fc5021ca4825b32c91b4bf4d1d30d6d0a1125

Authored by Fabio Teixeira
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>
app/controllers/public/profile_controller.rb
... ... @@ -434,6 +434,9 @@ class ProfileController &lt; PublicController
434 434 end
435 435 end
436 436  
  437 + def show_tracked_action
  438 + @activity = ProfileActivity.find_by(:activity_id => params[:activity_id]).activity
  439 + end
437 440  
438 441 protected
439 442  
... ...
app/controllers/public/search_controller.rb
... ... @@ -154,6 +154,33 @@ class SearchController &lt; 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
... ...
app/helpers/mention_helper.rb 0 → 100644
... ... @@ -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
... ...
app/jobs/notify_mentioned_users_job.rb 0 → 100644
... ... @@ -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 &lt; 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 &lt; 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 &lt; 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 &lt; 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'%>
... ...
app/views/profile/_notify_mentioned_users.html.erb 0 → 100644
... ... @@ -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'%>
... ...
app/views/profile/show_tracked_action.html.erb 0 → 100644
... ... @@ -0,0 +1,15 @@
  1 +<div id='profile-network'>
  2 + <ul id='network-activities' class='profile-activities'>
  3 + <% if @activity.kind_of?(ActionTracker::Record) %>
  4 + <%= render :partial => 'profile_activity', :locals => { :activity => @activity, :tab_action => 'wall' } if (@activity.visible? && @activity.show_on_wall) %>
  5 + <% else %>
  6 + <%= render :partial => 'profile_scraps', :locals => { :activity => @activity, :scrap => @activity } %>
  7 + <% end %>
  8 + </ul>
  9 +</div>
  10 +
  11 +<script>
  12 + $(document).ready(function() {
  13 + $(".profile-wall-activities-comments a").trigger("click");
  14 + });
  15 +</script>
... ...
config/initializers/action_tracker.rb
... ... @@ -39,6 +39,8 @@ ActionTrackerConfig.verbs = {
39 39 favorite_enterprise: {
40 40 },
41 41  
  42 + notify_mentioned_users: {
  43 + },
42 44 }
43 45  
44 46 ActionTrackerConfig.current_user = proc do
... ...
db/migrate/20160714175829_add_show_on_wall_to_action_tracker.rb 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +class AddShowOnWallToActionTracker < ActiveRecord::Migration
  2 + def change
  3 + add_column :action_tracker, :show_on_wall, :boolean, :default => true
  4 + end
  5 +end
... ...
public/javascripts/user-mention.js 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 +
... ...
public/javascripts/vendor/jquery.elastic.js 0 → 100644
... ... @@ -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,'&amp;').replace(/ {2}/g, '&nbsp;').replace(/<|>/g, '&gt;').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+'&nbsp;' !== 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+'&nbsp;');
  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
... ...
public/javascripts/vendor/jquery.elastic.min.js 0 → 100644
... ... @@ -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,"&amp;").replace(/ {2}/g,"&nbsp;").replace(/<|>/g,"&gt;").replace(/\n/g,"<br />"),r=h.html().replace(/<br>/gi,"<br />");if((t||e+"&nbsp;"!==r)&&(h.html(e+"&nbsp;"),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);
... ...
public/javascripts/vendor/jquery.events.input.js 0 → 100644
... ... @@ -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
... ...
public/javascripts/vendor/jquery.events.input.min.js 0 → 100644
... ... @@ -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);
... ...
public/javascripts/vendor/jquery.mentionsInput.mod.js 0 → 100644
... ... @@ -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, '&nbsp; '); //Replace the 2 preceding token to &nbsp;
  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 = "&nbsp;", 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 = "&nbsp;", 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,"&nbsp; "),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="&nbsp;",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="&nbsp;",a.appendChild(i),a.appendChild(o),a.appendChild(n),a.scrollTop=d.scrollTop,l=e(o).offset(),e(a).remove(),l}}function h(){var t=e(M).offset().top,n=e("body").offset().top,i=e(window).scrollTop();i>t&&e(window).scrollTop(t-n)}function v(t){var n=e(this),i=B[n.attr("data-uid")];return u(i),h(),!1}function x(e){d()}function C(e){y()}function w(e){c(),p();var i=t.lastIndexOf(F,n.triggerChar);i>-1&&(W=F.slice(i+1).join(""),W=o.rtrim(W),t.defer(t.bind(S,this,W)))}function E(e){if(e.keyCode!==i.BACKSPACE){var t=String.fromCharCode(e.which||e.keyCode);F.push(t)}}function b(n){if(n.keyCode===i.LEFT||n.keyCode===i.RIGHT||n.keyCode===i.HOME||n.keyCode===i.END)return t.defer(d),void(navigator.userAgent.indexOf("MSIE 9")>-1&&t.defer(c));if(n.keyCode===i.BACKSPACE)return void(F=F.slice(0,-1+F.length));if(!N.is(":visible"))return!0;switch(n.keyCode){case i.UP:case i.DOWN:var a=null;return a=n.keyCode===i.DOWN?D&&D.length?D.next():N.find("li").first():e(D).prev(),a.length&&T(a),!1;case i.RETURN:case i.TAB:if(D&&D.length)return D.trigger("mousedown"),!1}return!0}function y(){D=null,N.empty().hide()}function T(e){e.addClass(n.classes.autoCompleteItemActive),e.siblings().removeClass(n.classes.autoCompleteItemActive),D=e}function I(i,a){if(N.show(),!n.allowRepeat){var r=t.pluck(P,"value");a=t.reject(a,function(e){return t.include(r,e.name)})}if(!a.length)return void y();N.empty();var l=e("<ul>").appendTo(N).hide();t.each(a,function(a,r){var s=t.uniqueId("mention_");B[s]=t.extend({},a,{value:a.name});var c=e(n.templates.autocompleteListItem({id:o.htmlEncode(a.id),display:o.htmlEncode(a.name),type:o.htmlEncode(a.type),fullName:o.htmlEncode(a.fullName),content:o.highlightTerm(o.htmlEncode(a.display?a.display:a.name),i)})).attr("data-uid",s);if(0===r&&T(c),n.showAvatars){var d;d=e(a.avatar?n.templates.autocompleteListItemAvatar({avatar:a.avatar}):n.templates.autocompleteListItemIcon({icon:a.icon})),d.prependTo(c)}c=c.appendTo(l)}),N.show(),n.onCaret&&A(N,M),l.show()}function S(e){e&&e.length&&e.length>=n.minChars?n.onDataRequest.call(this,"search",e,function(t){I(e,t)}):y()}function A(e,t){var n=e.css("position");if("absolute"==n){var i=m(t),a=parseInt(t.css("line-height"),10)||18;e.css("width","15em"),e.css("left",i.left),e.css("top",a+i.top);var o=t.offset().left+t.width(),r=e.offset().left+e.width();o<=r&&e.css("left",Math.abs(e.position().left-(r-o)))}else if("fixed"==n){var l=g(t),a=parseInt(t.css("line-height"),10)||18;e.css("width","15em"),e.css("left",l.left+1e4),e.css("top",a+l.top)}}function R(e){P=[];for(var t,i=o.htmlEncode(e),a=new RegExp("("+n.triggerChar+")\\[(.*?)\\]\\((.*?):(.*?)\\)","gi"),r=i;null!=(t=a.exec(i));)r=r.replace(t[0],t[1]+t[2]),P.push({id:t[4],type:t[3],value:t[2],trigger:t[1]});M.val(r),c()}var k,M,L,N,O,H,D,P=[],B={},F=[],W="";return n=e.extend(!0,{},a,n),{init:function(e){k=e,r(),l(),s(),R(n.defaultValue),n.prefillMention&&u(n.prefillMention)},val:function(e){t.isFunction(e)&&e.call(this,P.length?M.data("messageText"):f())},reset:function(){R()},reinit:function(){R(!1)},getMentions:function(e){t.isFunction(e)&&e.call(this,P)}}};e.fn.mentionsInput=function(n,i){var a=arguments;return"object"!=typeof n&&n||(i=n),this.each(function(){var o=e.data(this,"mentionsInput")||e.data(this,"mentionsInput",new r(i));return t.isFunction(o[n])?o[n].apply(this,Array.prototype.slice.call(a,1)):"object"!=typeof n&&n?void e.error("Method "+n+" does not exist"):o.init.call(this,this)})}}(jQuery,_.runInContext());
... ...
public/stylesheets/application.scss
... ... @@ -48,6 +48,7 @@
48 48 @import 'profile-members';
49 49 @import 'profile-search';
50 50 @import 'profile-activity';
  51 +@import 'jquery.mentionsInput.min';
51 52 // admin
52 53 @import 'admin-panel';
53 54 @import 'manage-fields';
... ...
public/stylesheets/comments.scss
... ... @@ -366,3 +366,11 @@ a.comment-picture {
366 366 margin-bottom: 40px;
367 367 }
368 368  
  369 +textarea[data-mentions-input='true'] {
  370 + min-height: 70px;
  371 +}
  372 +
  373 +.mentions-input-box .mentions > div > strong {
  374 + background: none;
  375 +}
  376 +
... ...
public/stylesheets/jquery.mentionsInput.css 0 → 100644
... ... @@ -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 +}
... ...
public/stylesheets/jquery.mentionsInput.min.css 0 → 100644
... ... @@ -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
... ...