├── init.rb ├── test ├── test_helper.rb └── in_place_editing_test.rb ├── README ├── Rakefile └── lib ├── in_place_editing.rb └── in_place_macros_helper.rb /init.rb: -------------------------------------------------------------------------------- 1 | ActionController::Base.send :include, InPlaceEditing 2 | ActionController::Base.helper InPlaceMacrosHelper -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__) + '/../lib') 2 | 3 | require 'test/unit' 4 | require 'rubygems' 5 | require 'action_controller' 6 | require 'action_controller/assertions' 7 | require 'in_place_editing' 8 | require 'in_place_macros_helper' -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | InPlaceEditing 2 | ============== 3 | 4 | Example: 5 | 6 | # Controller 7 | class BlogController < ApplicationController 8 | in_place_edit_for :post, :title 9 | end 10 | 11 | # View 12 | <%= in_place_editor_field :post, 'title' %> 13 | 14 | Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | 5 | desc 'Default: run unit tests.' 6 | task :default => :test 7 | 8 | desc 'Test in_place_editing plugin.' 9 | Rake::TestTask.new(:test) do |t| 10 | t.libs << 'lib' 11 | t.pattern = 'test/**/*_test.rb' 12 | t.verbose = true 13 | end 14 | 15 | desc 'Generate documentation for in_place_editing plugin.' 16 | Rake::RDocTask.new(:rdoc) do |rdoc| 17 | rdoc.rdoc_dir = 'rdoc' 18 | rdoc.title = 'InPlaceEditing' 19 | rdoc.options << '--line-numbers' << '--inline-source' 20 | rdoc.rdoc_files.include('README') 21 | rdoc.rdoc_files.include('lib/**/*.rb') 22 | end 23 | -------------------------------------------------------------------------------- /lib/in_place_editing.rb: -------------------------------------------------------------------------------- 1 | module InPlaceEditing 2 | def self.included(base) 3 | base.extend(ClassMethods) 4 | end 5 | 6 | # Example: 7 | # 8 | # # Controller 9 | # class BlogController < ApplicationController 10 | # in_place_edit_for :post, :title 11 | # end 12 | # 13 | # # View 14 | # <%= in_place_editor_field :post, 'title' %> 15 | # 16 | module ClassMethods 17 | def in_place_edit_for(object, attribute, options = {}) 18 | define_method("set_#{object}_#{attribute}") do 19 | unless [:post, :put].include?(request.method) then 20 | return render(:text => 'Method not allowed', :status => 405) 21 | end 22 | @item = object.to_s.camelize.constantize.find(params[:id]) 23 | @item.update_attribute(attribute, params[:value]) 24 | render :text => CGI::escapeHTML(@item.send(attribute).to_s) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/in_place_editing_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/test_helper") 2 | 3 | class InPlaceEditingTest < Test::Unit::TestCase 4 | include InPlaceEditing 5 | include InPlaceMacrosHelper 6 | 7 | include ActionView::Helpers::UrlHelper 8 | include ActionView::Helpers::TagHelper 9 | include ActionView::Helpers::TextHelper 10 | include ActionView::Helpers::FormHelper 11 | include ActionView::Helpers::CaptureHelper 12 | 13 | def setup 14 | @controller = Class.new do 15 | def url_for(options) 16 | url = "http://www.example.com/" 17 | url << options[:action].to_s if options and options[:action] 18 | url 19 | end 20 | end 21 | @controller = @controller.new 22 | @protect_against_forgery = false 23 | end 24 | 25 | def protect_against_forgery? 26 | @protect_against_forgery 27 | end 28 | 29 | def test_in_place_editor_external_control 30 | assert_dom_equal %(), 31 | in_place_editor('some_input', {:url => {:action => 'inplace_edit'}, :external_control => 'blah'}) 32 | end 33 | 34 | def test_in_place_editor_size 35 | assert_dom_equal %(), 36 | in_place_editor('some_input', {:url => {:action => 'inplace_edit'}, :size => 4}) 37 | end 38 | 39 | def test_in_place_editor_cols_no_rows 40 | assert_dom_equal %(), 41 | in_place_editor('some_input', {:url => {:action => 'inplace_edit'}, :cols => 4}) 42 | end 43 | 44 | def test_in_place_editor_cols_with_rows 45 | assert_dom_equal %(), 46 | in_place_editor('some_input', {:url => {:action => 'inplace_edit'}, :rows => 5, :cols => 40}) 47 | end 48 | 49 | def test_inplace_editor_loading_text 50 | assert_dom_equal %(), 51 | in_place_editor('some_input', {:url => {:action => 'inplace_edit'}, :loading_text => 'Why are we waiting?'}) 52 | end 53 | 54 | def test_in_place_editor_url 55 | assert_match "Ajax.InPlaceEditor('id-goes-here', 'http://www.example.com/action_to_set_value')", 56 | in_place_editor( 'id-goes-here', :url => { :action => "action_to_set_value" }) 57 | end 58 | 59 | def test_in_place_editor_load_text_url 60 | assert_match "Ajax.InPlaceEditor('id-goes-here', 'http://www.example.com/action_to_set_value', {loadTextURL:'http://www.example.com/action_to_get_value'})", 61 | in_place_editor( 'id-goes-here', 62 | :url => { :action => "action_to_set_value" }, 63 | :load_text_url => { :action => "action_to_get_value" }) 64 | end 65 | 66 | def test_in_place_editor_html_response 67 | assert_match "Ajax.InPlaceEditor('id-goes-here', 'http://www.example.com/action_to_set_value', {htmlResponse:false})", 68 | in_place_editor( 'id-goes-here', 69 | :url => { :action => "action_to_set_value" }, 70 | :script => true ) 71 | end 72 | 73 | def form_authenticity_token 74 | "authenticity token" 75 | end 76 | 77 | def test_in_place_editor_with_forgery_protection 78 | @protect_against_forgery = true 79 | assert_match "Ajax.InPlaceEditor('id-goes-here', 'http://www.example.com/action_to_set_value', {callback:function(form) { return Form.serialize(form) + '&authenticity_token=' + encodeURIComponent('authenticity token') }})", 80 | in_place_editor( 'id-goes-here', :url => { :action => "action_to_set_value" }) 81 | end 82 | 83 | def test_in_place_editor_text_between_controls 84 | assert_match "Ajax.InPlaceEditor('id-goes-here', 'http://www.example.com/action_to_set_value', {textBetweenControls:'or'})", 85 | in_place_editor( 'id-goes-here', 86 | :url => { :action => "action_to_set_value" }, 87 | :text_between_controls => "or" ) 88 | end 89 | end -------------------------------------------------------------------------------- /lib/in_place_macros_helper.rb: -------------------------------------------------------------------------------- 1 | module InPlaceMacrosHelper 2 | # Makes an HTML element specified by the DOM ID +field_id+ become an in-place 3 | # editor of a property. 4 | # 5 | # A form is automatically created and displayed when the user clicks the element, 6 | # something like this: 7 | #
8 | # 9 | # 10 | # cancel 11 | #
12 | # 13 | # The form is serialized and sent to the server using an AJAX call, the action on 14 | # the server should process the value and return the updated value in the body of 15 | # the reponse. The element will automatically be updated with the changed value 16 | # (as returned from the server). 17 | # 18 | # Required +options+ are: 19 | # :url:: Specifies the url where the updated value should 20 | # be sent after the user presses "ok". 21 | # 22 | # Addtional +options+ are: 23 | # :rows:: Number of rows (more than 1 will use a TEXTAREA) 24 | # :cols:: Number of characters the text input should span (works for both INPUT and TEXTAREA) 25 | # :size:: Synonym for :cols when using a single line text input. 26 | # :cancel_text:: The text on the cancel link. (default: "cancel") 27 | # :save_text:: The text on the save link. (default: "ok") 28 | # :loading_text:: The text to display while the data is being loaded from the server (default: "Loading...") 29 | # :saving_text:: The text to display when submitting to the server (default: "Saving...") 30 | # :external_control:: The id of an external control used to enter edit mode. 31 | # :load_text_url:: URL where initial value of editor (content) is retrieved. 32 | # :options:: Pass through options to the AJAX call (see prototype's Ajax.Updater) 33 | # :with:: JavaScript snippet that should return what is to be sent 34 | # in the AJAX call, +form+ is an implicit parameter 35 | # :script:: Instructs the in-place editor to evaluate the remote JavaScript response (default: false) 36 | # :click_to_edit_text::The text shown during mouseover the editable text (default: "Click to edit") 37 | def in_place_editor(field_id, options = {}) 38 | function = "new Ajax.InPlaceEditor(" 39 | function << "'#{field_id}', " 40 | function << "'#{url_for(options[:url])}'" 41 | 42 | js_options = {} 43 | 44 | if protect_against_forgery? 45 | options[:with] ||= "Form.serialize(form)" 46 | options[:with] += " + '&authenticity_token=' + encodeURIComponent('#{form_authenticity_token}')" 47 | end 48 | 49 | js_options['cancelText'] = %('#{options[:cancel_text]}') if options[:cancel_text] 50 | js_options['okText'] = %('#{options[:save_text]}') if options[:save_text] 51 | js_options['loadingText'] = %('#{options[:loading_text]}') if options[:loading_text] 52 | js_options['savingText'] = %('#{options[:saving_text]}') if options[:saving_text] 53 | js_options['rows'] = options[:rows] if options[:rows] 54 | js_options['cols'] = options[:cols] if options[:cols] 55 | js_options['size'] = options[:size] if options[:size] 56 | js_options['externalControl'] = "'#{options[:external_control]}'" if options[:external_control] 57 | js_options['loadTextURL'] = "'#{url_for(options[:load_text_url])}'" if options[:load_text_url] 58 | js_options['ajaxOptions'] = options[:options] if options[:options] 59 | js_options['htmlResponse'] = !options[:script] if options[:script] 60 | js_options['callback'] = "function(form) { return #{options[:with]} }" if options[:with] 61 | js_options['clickToEditText'] = %('#{options[:click_to_edit_text]}') if options[:click_to_edit_text] 62 | js_options['textBetweenControls'] = %('#{options[:text_between_controls]}') if options[:text_between_controls] 63 | function << (', ' + options_for_javascript(js_options)) unless js_options.empty? 64 | 65 | function << ')' 66 | 67 | javascript_tag(function) 68 | end 69 | 70 | # Renders the value of the specified object and method with in-place editing capabilities. 71 | def in_place_editor_field(object, method, tag_options = {}, in_place_editor_options = {}) 72 | instance_tag = ::ActionView::Helpers::InstanceTag.new(object, method, self) 73 | tag_options = {:tag => "span", 74 | :id => "#{object}_#{method}_#{instance_tag.object.id}_in_place_editor", 75 | :class => "in_place_editor_field"}.merge!(tag_options) 76 | in_place_editor_options[:url] = in_place_editor_options[:url] || url_for({ :action => "set_#{object}_#{method}", :id => instance_tag.object.id }) 77 | tag = content_tag(tag_options.delete(:tag), h(instance_tag.value(instance_tag.object)),tag_options) 78 | return tag + in_place_editor(tag_options[:id], in_place_editor_options) 79 | end 80 | end 81 | --------------------------------------------------------------------------------