From 205c4e7ed424981ce3d5942c3fef6e1c060d3d1c Mon Sep 17 00:00:00 2001 From: Eric Allen Date: Fri, 2 Apr 2010 09:14:25 -0400 Subject: [PATCH 1/8] Much smarter project note truncation Fixes #780 --- app/helpers/notes_helper.rb | 4 ++ app/views/layouts/standard.html.erb | 3 +- app/views/notes/_notes_summary.rhtml | 2 +- public/javascripts/application.js | 2 + public/javascripts/jquery.truncator.js | 81 ++++++++++++++++++++++++++ 5 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 public/javascripts/jquery.truncator.js diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 170c6aca..1c3d51c0 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -2,4 +2,8 @@ module NotesHelper def truncated_note(note, characters = 50) sanitize(textilize_without_paragraph(truncate(note.body, :length => characters, :omission => "..."))) end + + def rendered_note(note) + sanitize(textilize_without_paragraph(note.body)) + end end diff --git a/app/views/layouts/standard.html.erb b/app/views/layouts/standard.html.erb index f5713db2..6bdccf61 100644 --- a/app/views/layouts/standard.html.erb +++ b/app/views/layouts/standard.html.erb @@ -6,7 +6,8 @@ 'jquery.autocomplete', :cache => true %> <%= stylesheet_link_tag "print", :media => "print" %> <%= javascript_include_tag 'jquery','jquery-ui','jquery.cookie', - 'jquery.blockUI','jquery.jeditable','jquery.autocomplete', :cache => 'jquery-all' %> + 'jquery.blockUI','jquery.jeditable','jquery.autocomplete', + 'jquery.truncator', :cache => 'jquery-all' %> <%= javascript_include_tag 'hoverIntent','superfish','application', 'accesskey-hints','niftycube','flashobject', :cache => 'tracks' %> <%= javascript_tag "var AUTH_TOKEN = #{form_authenticity_token.inspect};" if protect_against_forgery? %> diff --git a/app/views/notes/_notes_summary.rhtml b/app/views/notes/_notes_summary.rhtml index 9eb70db0..a6e8af7c 100644 --- a/app/views/notes/_notes_summary.rhtml +++ b/app/views/notes/_notes_summary.rhtml @@ -1,6 +1,6 @@ <% note = notes_summary -%>
<%= link_to( image_tag("blank.png", :border => 0), note_path(note), :title => "Show note", :class => "link_to_notes icon") %>  -<%= truncated_note(note) %> +<%= rendered_note(note) %>
<% note = nil -%> diff --git a/public/javascripts/application.js b/public/javascripts/application.js index 6fa769ef..f7c6c90c 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -289,6 +289,8 @@ $(document).ready(function() { $(this).next().toggle("fast"); return false; }); + $('.note_wrapper').truncate({max_length: 90, more: '', less: ''}); + $(".show_successors").live('click', function () { $(this).next().toggle("fast"); return false; }); diff --git a/public/javascripts/jquery.truncator.js b/public/javascripts/jquery.truncator.js new file mode 100644 index 00000000..0274d3df --- /dev/null +++ b/public/javascripts/jquery.truncator.js @@ -0,0 +1,81 @@ +// HTML Truncator for jQuery +// by Henrik Nyh 2008-02-28. +// modified by Eric Allen 2010-04-02 +// Free to modify and redistribute with credit. + +(function($) { + + var trailing_whitespace = true; + + $.fn.truncate = function(options) { + + var opts = $.extend({}, $.fn.truncate.defaults, options); + + $(this).each(function() { + + var content_length = $.trim(squeeze($(this).text())).length; + if (content_length <= opts.max_length) + return; // bail early if not overlong + + var actual_max_length = opts.max_length - opts.more.length - 3; // 3 for " ()" + var truncated_node = recursivelyTruncate(this, actual_max_length); + var full_node = $(this).hide(); + + truncated_node.insertAfter(full_node); + + findNodeForMore(truncated_node).append('...'); + }); + } + + // Note that the " (…more)" bit counts towards the max length – so a max + // length of 10 would truncate "1234567890" to "12 (…more)". + $.fn.truncate.defaults = { + max_length: 100, + more: '…more', + less: 'less' + }; + + function recursivelyTruncate(node, max_length) { + return (node.nodeType == 3) ? truncateText(node, max_length) : truncateNode(node, max_length); + } + + function truncateNode(node, max_length) { + var node = $(node); + var new_node = node.clone().empty(); + var truncatedChild; + node.contents().each(function() { + var remaining_length = max_length - new_node.text().length; + if (remaining_length == 0) return; // breaks the loop + truncatedChild = recursivelyTruncate(this, remaining_length); + if (truncatedChild) new_node.append(truncatedChild); + }); + return new_node; + } + + function truncateText(node, max_length) { + var text = squeeze(node.data); + if (trailing_whitespace) // remove initial whitespace if last text + text = text.replace(/^ /, ''); // node had trailing whitespace. + trailing_whitespace = !!text.match(/ $/); + var text = text.slice(0, max_length); + // Ensure HTML entities are encoded + // http://debuggable.com/posts/encode-html-entities-with-jquery:480f4dd6-13cc-4ce9-8071-4710cbdd56cb + text = $('
').text(text).html(); + return text; + } + + // Collapses a sequence of whitespace into a single space. + function squeeze(string) { + return string.replace(/\s+/g, ' '); + } + + // Finds the last, innermost block-level element + function findNodeForMore(node) { + var $node = $(node); + var last_child = $node.children(":last"); + if (!last_child) return node; + var display = last_child.css('display'); + if (!display || display=='inline') return $node; + return findNodeForMore(last_child); + }; +})(jQuery); From d682d30e6d1235a33963af2041bb1d98f6414522 Mon Sep 17 00:00:00 2001 From: Eric Allen Date: Fri, 2 Apr 2010 09:17:13 -0400 Subject: [PATCH 2/8] No reason to sanitize if we're already h()'ing Fixes #824 --- app/views/todos/_todo.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/todos/_todo.html.erb b/app/views/todos/_todo.html.erb index 4e325c8b..dae70ed6 100644 --- a/app/views/todos/_todo.html.erb +++ b/app/views/todos/_todo.html.erb @@ -29,7 +29,7 @@ parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag'
<%= grip_span %> <%= date_span -%> - <%= h sanitize(todo.description) %> + <%= h todo.description %> <% #= successors_span %> <%= image_tag_for_recurring_todo(todo) if @todo.from_recurring_todo? %> <%= tag_list %> From e4394761c5afb401ad6e2981a48abb29a6eb4133 Mon Sep 17 00:00:00 2001 From: Eric Allen Date: Fri, 2 Apr 2010 09:45:29 -0400 Subject: [PATCH 3/8] Blank DB with pre-defined contexts for easier start Fixes #968 --- ...ks-17-blank.db => tracks-20-blank.sqlite3} | Bin 47104 -> 52224 bytes 1 file changed, 0 insertions(+), 0 deletions(-) rename db/{tracks-17-blank.db => tracks-20-blank.sqlite3} (61%) diff --git a/db/tracks-17-blank.db b/db/tracks-20-blank.sqlite3 similarity index 61% rename from db/tracks-17-blank.db rename to db/tracks-20-blank.sqlite3 index befc0396e187670d5c2bbc2ca81c0a1ca2d538c4..b5bd761ff2d008784fc94f838da5162e2de8c60f 100644 GIT binary patch delta 4280 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 Date: Fri, 2 Apr 2010 12:53:29 -0400 Subject: [PATCH 4/8] Kill HTML tags in todo notes We still want to preserve quotes for textile, but tags of any sort shouldn't be able to mess up the output. Fixes #765 --- app/helpers/application_helper.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 645be1c4..76622c50 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -202,9 +202,11 @@ module ApplicationHelper end def format_note(note) + note.gsub!(//, '>') + note = markdown(note) note = auto_link_message(note) note = auto_link(note) - note = markdown(note) - note = sanitize(note) + return note end end From 9ab69adb3882233b65a3b79b85998789f83226de Mon Sep 17 00:00:00 2001 From: Eric Allen Date: Fri, 2 Apr 2010 13:23:24 -0400 Subject: [PATCH 5/8] Dynamically load autocompletes This cuts something like 100ms off of page load times! Closes #1011 --- app/controllers/application_controller.rb | 5 +++++ app/controllers/contexts_controller.rb | 1 + app/controllers/projects_controller.rb | 1 + app/controllers/todos_controller.rb | 7 +++++++ app/helpers/todos_helper.rb | 14 -------------- app/views/layouts/standard.html.erb | 3 --- config/initializers/mime_types.rb | 3 ++- config/routes.rb | 3 +++ public/javascripts/application.js | 15 ++++++++++----- public/javascripts/jquery.autocomplete.js | 2 +- 10 files changed, 30 insertions(+), 24 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 56dc69f6..d451215d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -123,6 +123,11 @@ class ApplicationController < ActionController::Base formatted_date end + + def for_autocomplete(coll) + coll.map {|item| "#{item.name}|#{item.id}"}.join("\n") + end + # Uses RedCloth to transform text using either Textile or Markdown Need to # require redcloth above RedCloth 3.0 or greater is needed to use Markdown, # otherwise it only handles Textile diff --git a/app/controllers/contexts_controller.rb b/app/controllers/contexts_controller.rb index 75f3d1c5..bd6b900f 100644 --- a/app/controllers/contexts_controller.rb +++ b/app/controllers/contexts_controller.rb @@ -22,6 +22,7 @@ class ContextsController < ApplicationController format.rss &render_contexts_rss_feed format.atom &render_contexts_atom_feed format.text { render :action => 'index', :layout => false, :content_type => Mime::TEXT } + format.autocomplete { render :text => for_autocomplete(@active_contexts + @hidden_contexts)} end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index a6604cb3..454a7e6e 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -26,6 +26,7 @@ class ProjectsController < ApplicationController format.rss &render_rss_feed format.atom &render_atom_feed format.text &render_text_feed + format.autocomplete { render :text => for_autocomplete(@projects) } end end end diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index 67b983f2..e55298b9 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -526,6 +526,13 @@ class TodosController < ApplicationController } end end + + def tags + @tags = Tag.all + respond_to do |format| + format.autocomplete { render :text => for_autocomplete(@tags) } + end + end def defer @source_view = params['_source_view'] || 'todo' diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 8eafc567..6c8703c6 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -276,20 +276,6 @@ module TodosHelper return "c#{todo.context_id}empty-nd" end - def project_names_for_autocomplete - array_or_string_for_javascript( ['None'] + current_user.projects.active.collect{|p| escape_javascript(p.name) } ) - end - - def context_names_for_autocomplete - # #return array_or_string_for_javascript(['Create a new context']) if - # @contexts.empty? - array_or_string_for_javascript( current_user.contexts.collect{|c| escape_javascript(c.name) } ) - end - - def tag_names_for_autocomplete - array_or_string_for_javascript( Tag.all.collect{|c| escape_javascript(c.name) } ) - end - def default_contexts_for_autocomplete projects = current_user.projects.find(:all, :conditions => ['default_context_id is not null']) Hash[*projects.map{ |p| [p.name, p.default_context.name] }.flatten].to_json diff --git a/app/views/layouts/standard.html.erb b/app/views/layouts/standard.html.erb index 6bdccf61..cf555a73 100644 --- a/app/views/layouts/standard.html.erb +++ b/app/views/layouts/standard.html.erb @@ -14,11 +14,8 @@ <%= javascript_tag "var SOURCE_VIEW = '#{@source_view}';" %> <%= javascript_tag "var TAG_NAME = '#{@tag_name}';" if @tag_name %>