jQuery を用いた in_place_editor_field

趣旨

インプレース編集を可能とする in_place_editor_field。prototype.js 系はアレなので、jQuery 版を作ってみた。といっても、完全互換ではない。mouse hover がらみの処理は省略。みなさんなりの実装をするときの参考にしてもらえれば幸いです。

使い方

  1. 下のソースコードを見て config/environment.rb を修正し、public/javascripts/ajax_in_place_editor.js を新たに作る。
  2. コントローラ・ビューを下のように設定する。
コントローラ (app/controllers/blog_controller.rb)
  class BlogController < ApplicationController
    in_place_edit_for :post, :title
    
    def update_in_place
      @post = Post.find(1)
    end
  end
ビュー (app/views/blog/update_in_place.html.erb)
  <%= javascript_include_tag 'jquery' %>
  <%= javascript_include_tag 'ajax_in_place_editor' %>

  # 基本
  <%= in_place_editor_field :post, 'title' %>
  
  # 画像をクリックして編集開始(独自拡張)
  <%= in_place_editor_field :post, 'title', {}, click_to_edit_image => 'rails.png' %>
  
  # 編集フォームのカスタマイズ(独自拡張)
  <%= in_place_editor_field :post, 'title', {}, :form_inner_html => '<input type="text" name="value" value="" class="editor_text"/><input type="button" value="保存しちゃうぞ" class="editor_ok_button"/><input type="button" value="やっぱりやめる" class="editor_cancel_button"/>' %>

ソースコード

Ruby側 (config/environment.rb)
# Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license  
# Copyright (c) 2008 Eiji Sakai, released under the MIT license

module InPlaceEditing
  def self.included(base)
    base.extend(ClassMethods)
  end

  # Example:
  #
  #   # Controller
  #   class BlogController < ApplicationController
  #     in_place_edit_for :post, :title
  #   end
  #
  #   # View
  #   <%= in_place_editor_field :post, 'title' %>
  #
  module ClassMethods
    def in_place_edit_for(object, attribute, options = {})
      define_method("set_#{object}_#{attribute}") do
        @item = object.to_s.camelize.constantize.find(params[:id])
        @item.update_attribute(attribute, params[:value])
        render :text => @item.send(attribute).to_s
      end
    end
  end
end

module InPlaceMacrosHelper
  def in_place_editor(field_id, options = {})
    function =  "new Ajax.InPlaceEditor("
    function << "'#{field_id}', "
    function << "'#{url_for(options[:url])}'"

    js_options = {}
    js_options['authParam'] = %('#{request_forgery_protection_token}=#{URI.encode(escape_javascript(form_authenticity_token))}')
    js_options['clickToEditImage'] = %('#{options[:click_to_edit_image].gsub(/\//, '_')}').gsub(/\./, "_") if options[:click_to_edit_image]
    js_options['formInnerHTML'] = %('#{options[:form_inner_html]}') if options[:form_inner_html]
    
    function << (', ' + options_for_javascript(js_options)) unless js_options.empty?
    
    function << ')'

    javascript_tag(function)
  end
  
  # Renders the value of the specified object and method with in-place editing capabilities.
  def in_place_editor_field(object, method, tag_options = {}, in_place_editor_options = {})
    tag = ::ActionView::Helpers::InstanceTag.new(object, method, self)
    tag_options = {:tag => "span", :id => "#{object}_#{method}_#{tag.object.id}_in_place_editor", :class => "in_place_editor_field"}.merge!(tag_options)
    in_place_editor_options[:url] = in_place_editor_options[:url] || url_for({ :action => "set_#{object}_#{method}", :id => tag.object.id })
    image_html = in_place_editor_options[:click_to_edit_image] ? image_tag(in_place_editor_options[:click_to_edit_image], :id => in_place_editor_options[:click_to_edit_image].gsub(/\//, '_').gsub(/\./, "_")) : ""
    
    image_html +
    tag.to_content_tag(tag_options.delete(:tag), tag_options) +
    in_place_editor(tag_options[:id], in_place_editor_options)
  end
end

ActionController::Base.send :include, InPlaceEditing
ActionController::Base.helper InPlaceMacrosHelper
Javascript 側(public/javascripts/ajax_in_place_editor.js)
# Copyright (c) 2008 Eiji Sakai, released under the MIT license

var Ajax = {
	InPlaceEditor: function(element, url, options) {
		var e = $("#" + element);
		options = options || {};

		var formHtml = options["formInnerHTML"] || '<input type="text" name="value" value="" class="editor_text"/><input type="button" value="ok" class="editor_ok_button"/><input type="button" value="cancel" class="editor_cancel_button"/>';
		var clickToEditSelector = "#" + (options["clickToEditImage"] || element)
		var clickToEditElement = $(clickToEditSelector); 

		clickToEditElement.click(function() {
			var clickHandler = arguments.callee;
			clickToEditElement.unbind('click');
			var oldValue = e.html();
			e.html(formHtml);
			$("#" + element + " .editor_text").val(oldValue);
			$("#" + element + " .editor_ok_button").click(function() {
				var newValue = $("#" + element + " .editor_text").val();
				$.ajax({
					"url" : url,
					"type" : "post",
					"data" : "value=" + encodeURIComponent(newValue) + "&" + options["authParam"],
					"success" : function(data, status){
						e.html(newValue);
						clickToEditElement.click(clickHandler);
					},
					"failure" : function(data, status) {
						alert("failure");
					}
				});
			});
			$("#" + element + " .editor_cancel_button").click(function() {
				e.html(oldValue);
				clickToEditElement.click(clickHandler);
			});
		});
	}
};