Ajax.InPlaceEditor でさまざまなフォームを表示する

趣旨

Rails の in_place_editor_field メソッドの本体は、prototype 系ライブラリ script.aculo.us の controls.js にある Ajax.InPlaceEditor である。InPlaceEditor は、フォーム表示に関して、実はあまり柔軟性がない。任意のフォームは使えないだろうかと考えて、InPlaceEditor を改造してみた。

どう使うかというと・・・。

<%= javascript_include_tag :defaults %>
<%= javascript_include_tag "controls_ext" %>

<form class="inplaceeditor-form" id="editor_form" style="display:none">
	<input type="text" name="value" id="editor_field" class="editor_field"/><br />
	<input type="button" value="保存" id="editor_ok_button" class="editor_ok_button"/>
	<input type="button" value="キャンセル" id="editor_cancel_button" class="editor_cancel_button"/>
</form>

name: <%= in_place_editor_field :book, :name, {},  :external_form_id => 'editor_form' %>

あらかじめ、編集モード時に表示すべきフォームを用意しておいて、その ID を in_place_editor_field に渡す。こうやって、さまざまなフォームが表示できるようになる。フォームの構成要素は3つあり、

  1. テキストフィールド(あるいはテキストエリア)
  2. 保存ボタン
  3. キャンセルボタン

である。それらの要素の id はデフォルトでそれぞれ editor_field, editor_ok_button, editor_cancel_button であるが、

name: <%= in_place_editor_field :book, :name, {},  :external_form_id => 'editor_form', :editor_field_id => 'my_editor_field', :ok_button_id => 'my_ok_button', :cancel_button_id => 'my_cancel_button'  %>

というふうに変更することもできる。

ソースコード

Rails 側(environment.rb)
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['cancelText'] = %('#{options[:cancel_text]}') if options[:cancel_text]
    js_options['okText'] = %('#{options[:save_text]}') if options[:save_text]
    js_options['loadingText'] = %('#{options[:loading_text]}') if options[:loading_text]
    js_options['savingText'] = %('#{options[:saving_text]}') if options[:saving_text]
    js_options['rows'] = options[:rows] if options[:rows]
    js_options['cols'] = options[:cols] if options[:cols]
    js_options['size'] = options[:size] if options[:size]
    js_options['externalControl'] = "'#{options[:external_control]}'" if options[:external_control]
    js_options['loadTextURL'] = "'#{url_for(options[:load_text_url])}'" if options[:load_text_url]        
    js_options['ajaxOptions'] = options[:options] if options[:options]
    js_options['evalScripts'] = options[:script] if options[:script]
    callback = if options[:with]
       "function(form) { var params = #{options[:with]};"
    else
       "function(form) { var params = Form.serialize(form); "
    end
     callback += "params = (params ? params + '&' : '') + '#{request_forgery_protection_token}=' + encodeURIComponent('#{escape_javascript form_authenticity_token}'); return params;}" 
    js_options['callback']   = callback
    js_options['clickToEditText'] = %('#{options[:click_to_edit_text]}') if options[:click_to_edit_text]

    js_options['externalFormId'] = options[:external_form_id] if options[:external_form_id]
    js_options['editorFieldId'] = options[:editor_field_id] if options[:editor_field_id]
    js_options['okButtonId'] = options[:ok_button_id] if options[:ok_button_id]
    js_options['cancelButtonId'] = options[:cancel_button_id] if options[:cancel_button_id]
    
    function << (', ' + options_for_javascript(js_options)) unless js_options.empty?
    
    function << ')'

    javascript_tag(function)
  end
end
Javascript 側(controls_ext.js)
Object.extend(Ajax.InPlaceEditor.prototype, {
  createForm : function() {
    this._form = $(this.options.externalFormId);
    this._form.style.display = "inline";
    this._form.remove();
    this.element.parentNode.insertBefore(this._form, this.element);

    var editorFieldId  = this.options.editorFieldId || 'editor_field';
    var okButtonId     = this.options.okButtonId    || 'editor_ok_button';
    var cancelButtonId = this.options.cancelButtonId || 'editor_cancel_button';

    this._controls.editor = $(editorFieldId);	
    this._controls.editor.value = this.getText();

    var okButton = $(okButtonId);
    okButton.onclick = this._boundSubmitHandler;

    var cancelButton = $(cancelButtonId);
    cancelButton.onclick = this._boundCancelHandler;
  },
	
  enterEditMode: function(e) {
    if (this._saving || this._editing) return;
    this._editing = true;
    this.triggerCallback('onEnterEditMode');
    if (this.options.externalControl)
      this.options.externalControl.hide();
    this.element.hide();
    this.createForm();
    if (!this.options.loadTextURL)
      this.postProcessEditField();
    if (e) Event.stop(e);
  },
	
  removeForm: function() {
    if (!this._form) return;
    this._form.style.display = "none";		
    this._controls = { };
  }
});

つぶやき

微妙に投げやり感がただようコードとなっている。というのも prototype.js 系のライブラリはつくづく垢抜けないので、いじっていて楽しくないのだ。おそらくは jQuery 系に乗り換えるべき潮時なのだろうな・・・