in_place_editorの機能拡張

今まで、InPlaceEditorを導入しているにもかかわらず、通常のEditリンクを残していたのは、InPlaceEditorの挙動を完全にコントロールできないで悩んでいたからである。解決したい問題は以下の2点。

  1. htmlエスケープの処理方法
    • 現状では、<h1>abc</h1>という文字列を素直に編集することが出来ない。以下のような状況。
      • loadTextURLオプションで、htmlタグを含む文字列を渡すと、編集モードでは、タグ部分は削除されて表示される。(→ abc
      • かといって、文字列を渡す前にhtmlエスケープ処理して渡すと、エスケープされた見慣れない文字列を見ることになる。(→ <h1>abc</h1>
  2. ブランク項目に対する入力方法
    • 現状では、externalControlオプションでid属性を指定したマークをクリックすることで実現している。しかし、以下のような問題がある。
      • ブランクかどうかに関係なく、コメント項目の全てに表示されてしまう。
      • 初期表示については、Ruby側でブランクかどうか判定して、マークの表示を制御することは出来る。しかし、InPlaceEditorで編集した後の表示を制御することが出来ない。(編集した結果ブランクになってしまっても、編集モードに移行するマークが表示できないので困る。)
      • Safariで、InPlaceEditorからコメント項目をブランクにすると、Saving...状態が永遠と続いてしまう。
      • 目に見えない半角スペース等の文字を誤って入力してしまうと、見かけ上はブランクであるのに、「空白である」フィルター条件にヒットしなくて悩むことになる。

長らく放置していた問題は、Ajax.In Place Editor in scriptaculous wikiをちゃんと読むことで、 結果、スマートに解決できたのであった...。(過程は試行錯誤)

htmlタグをそのまま表示する機能拡張

public/javascripts/extensions.js
in_place_editorのjavaスクリプト機能拡張ファイル
Object.extend(Ajax.InPlaceEditor.prototype, {
    onLoadedExternalText: function(transport) {
        Element.removeClassName(this.form, this.options.loadingClassName);
        this.editField.disabled = false;
        this.editField.value = transport.responseText;
        Field.scrollFreeActivate(this.editField);
    }
});

Object.extend(Ajax.InPlaceEditor.prototype, {
    getText: function() {
        return this.element.childNodes[0] ? this.element.childNodes[0].nodeValue : '';
    }
});
Ajax.In Place Editor in scriptaculous wiki - Using HTML within the In Place Editor
app/views/layouts/csvs.rhtml
レイアウトファイル
<%= javascript_include_tag 'extensions' %>


これで、InPlaceEditorがhtmlタグを勝手に削除してしまうことは無くなった。エスケープされた文字も元通りに復元して表示してくれる。以下は、ビューとコントローラーの内容。

app/views/csvs/_listd.rhtml
ビュー(リストデータを表示する部分)
  • 以前と変更なし。
...(途中省略)...
  <td <%= arranged_class("file_comment") %>>
      <% listd.file_comment = h(listd.file_comment) %>
      <% if management_section?(listd.management_section) %>
        <span class="edit_mark" id="file_comment<%= listd.id %>"><%= "&#187;" %></span>
        <span class="in_place_edit">
        <%= in_place_editor_field :csv, :file_comment, {}, :rows => 2, 
                                  :save_text=>"保存", :cancel_text=>"キャンセル", 
                                  :external_control=>"file_comment#{listd.id}" %>
        </span>
      <% else %>
        <%=h listd.file_comment %>
      <% end %></td>
...(途中省略)...
app/controllers/csvs_controller.rb
コントローラー
  • in_place_edit_for :csv, :file_commentを変更して、修正後もrender :inlineでhtmlエスケープした結果を返すように変更した。
#  in_place_edit_for :csv, :file_comment
  def set_csv_file_comment
    @csv = Csv.find(params[:id])
    @csv.update_attribute('file_comment', params[:value])
    render :inline => "<%=h @csv.file_comment %>"
  end

これで、htmlエスケープの問題は解決。

ブランクの時、'click to edit...'を表示する機能拡張

インストールすることで、ブランクの時だけ、編集モードに移行するための文字列を表示してくれる。

public/javascripts/extensions.js
in_place_editorのjavaスクリプト機能拡張ファイル

scriptaculous 1.8.0では、右記コードをsetOptions:に追記する必要あり。if (this.options.externalControl){this.options.externalControl = $(this.options.externalControl);}

setOptions:に、if (this.options.externalControl){this.options.externalControl = $(this.options.externalControl);}を追記済み。

/*
 * InPlaceEditor extension that adds a 'click to edit' text when the field is 
 * empty.
 */
Ajax.InPlaceEditor.prototype.__initialize = Ajax.InPlaceEditor.prototype.initialize;
Ajax.InPlaceEditor.prototype.__getText = Ajax.InPlaceEditor.prototype.getText;
Ajax.InPlaceEditor.prototype.__onComplete = Ajax.InPlaceEditor.prototype.onComplete;
Ajax.InPlaceEditor.prototype = Object.extend(Ajax.InPlaceEditor.prototype, {

    initialize: function(element, url, options){
        this.__initialize(element,url,options)
        this.setOptions(options);
        this._checkEmpty();
    },

    setOptions: function(options){
        this.options = Object.extend(Object.extend(this.options,{
            emptyText: 'click to edit...',
            emptyClassName: 'inplaceeditor-empty'
        }),options||{});
        if (this.options.externalControl)
          this.options.externalControl = $(this.options.externalControl);
    },

    _checkEmpty: function(){
        if( this.element.innerHTML.length == 0 ){
            this.element.appendChild(
                Builder.node('span',{className:this.options.emptyClassName},this.options.emptyText));
        }
    },

    getText: function(){
        document.getElementsByClassName(this.options.emptyClassName,this.element).each(function(child){
            this.element.removeChild(child);
        }.bind(this));
        return this.__getText();
    },

    onComplete: function(transport){
        this._checkEmpty();
        this.__onComplete(transport);
    }
});
Ajax.In Place Editor in scriptaculous wiki - Small extension to editor to add a text in case field is empty.
  • これで、ブランクの時「click to edit...」が表示されると期待したが、自分の環境では動かなかった...。
  • コードを確認してみると、_checkEmpty: function()で、Builderクラスを呼び出している。これが怪しいと予想して...
  • script.aculo.us - downloadsscript.aculo.us 1.7.0をダウンロードしてみると、builder.jsが同胞されている!
  • バージョンの違いで悩みたくないので、以下のファイル全てを上書きインストールした。
    • builder.js
    • controls.js
    • dragdrop.js
    • effects.js
    • prototype.js(1.5.0が同胞されていたので、control.modalとの互換性も問題なさそうだ。)
app/views/layouts/csvs.rhtml
レイアウトファイル
  <%= javascript_include_tag :defaults %>
  <%= javascript_include_tag 'builder' %>
  <%= javascript_include_tag 'extensions' %>


これで、ブランクの時に'click to edit...'が表示されるようになった。

「click to edit...」を「編集>>」に変更したい。

InPlaceEditorのオプションで、emptyText: '編集>>'と設定してあげれば良いのだが、拡張した機能なのでRailsのヘルパメソッドが対応していない。拡張したin_place_editorメソッドを定義してみた。

  • Railsのソースからin_place_editorの部分をコピーする。目的のソースのパスは深い。(/Applications/Locomotive2/Bundles/standardRailsSept2006.locobundle/i386/lib/ruby/gems/1.8/gems/actionpack-1.12.5/lib/action_view/helpers/java_script_macros_helper.rb)
  • application_helper.rbにコピーして、オレンジ色の部分を追記した。
app/helpers/application_helper.rb
ヘルパー
...(途中省略)...
  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['emptyText'] = %('#{options[:empty_text]}') if options[:empty_text]
        js_options['loadingText'] = %('#{options[:loading_text]}') if options[:loading_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]
        js_options['callback']   = "function(form) { return #{options[:with]} }" if options[:with]
        function << (', ' + options_for_javascript(js_options)) unless js_options.empty?

        function << ')'

        javascript_tag(function)
  end
...(途中省略)...
  • これで、Railsのヘルパメソッドから:empty_textオプションが指定できるようになる。
      • InPlaceEditorのオプションには、他にもヘルパメソッドから利用できないものがあるので、手軽なこの方法で同じように機能拡張が期待できる。
app/views/csvs/_listd.rhtml
ビュー(リストデータを表示する部分)
  • 不要になった:external_controlオプションは削除して以下のように変更した。
...(途中省略)...
  <td <%= arranged_class("file_comment") %>>
      <%  listd.file_comment = h(listd.file_comment) %>
      <% if management_section?(listd.management_section) %>
        <%= in_place_editor_field :csv, :file_comment, {}, :rows => 2, 
                                  :save_text=>"保存", :cancel_text=>"戻る", 
                                  :empty_text=>"編集>>" %>
      <% else %>
        <%=h listd.file_comment %>
      <% end %></td>
...(途中省略)...
ブランクの保存が出来ないSafariに対応する。
  • Safariでは、ブランクの状態で保存ボタンを押すと、いつまでも処理中になってしまうため、以下のように変更してみた。何らかの文字列を返してあげれば、処理が正常終了するようだ。(Firefoxだとifなんか使わずに、ちゃんと動作するのだが...。)
  • ついでに、空白文字しか入力がない場合は、保存するとブランクになるように変更した。
...(途中省略)...
  def set_csv_file_comment
    params[:value] = '' unless /\S/ =~ params[:value]
    @csv = Csv.find(params[:id])
    @csv.update_attribute('file_comment', params[:value])
    if @csv.file_comment.blank?
      render :inline => "編集>>"
    else
      render :inline => "<%=h @csv.file_comment %>"
    end
  end
...(途中省略)...
スタイルシートで見た目を調整
  • InPlaceEditorの要素は、以下のように指定して、調整することが出来るようだ。
form.inplaceeditor-form { /* The form */
}
form.inplaceeditor-form input[type="text"] { /* Input box */
  width: 100%;
  font-size: 100%;
  font-family: verdana, arial, helvetica, sans-serif;
}
form.inplaceeditor-form textarea { /* Textarea, if multiple columns */
  width: 100%;
  font-size: 100%;
  font-family: verdana, arial, helvetica, sans-serif;
}
form.inplaceeditor-form input[type="submit"] { /* The submit button */
  font-size: 75%;
  margin-left: 0px;
  padding: 0px;
}
form.inplaceeditor-form a { /* The cancel link */
  font-size: 75%;
  margin-left: 0px;
  padding: 0px 2px;
}
.inplaceeditor-empty{ /* a text in case field is empty */
  font-size: 75%; 
  font-style: italic;
  color: #aaa;
}


これで、完全にInPlaceEditorに依存した環境に移行できそう。Editページは不要になった。

in_place_editorのオプション一覧(自分の理解)

以下、試行錯誤の中で自分が理解したオプション一覧。

オプション 対応V 初期値 説明 自分の理解
okButton V1.6 “true” If a submit button is shown in edit mode (true,false) 送信(ok)ボタンを表示するかどうか。
okText V1.5 “ok” The text of the submit button that submits the changed value to the server 送信ボタンに表示する文字列。
cancelLink V1.6 “true” If a cancel link is shown in edit mode (true,false) キャンセル(cancel)リンクを表示するかどうか。
cancelText V1.5 “cancel” The text of the link that cancels editing キャンセルリンクに表示する文字列。
savingText V1.5 “Saving…” The text shown while the text is sent to the server 送信ボタンを押してから、その処理中に表示する文字列。
clickToEditText V1.6 “Click to edit” The text shown during mouseover the editable text 編集可能なテキストにマウスを乗せている間表示する文字列。
formId V1.5 id of the element to edit plus ‘InPlaceForm’ The id given to the element InPlaceEditorのフォームのid属性。
externalControl V1.5 null ID of an element that acts as an external control used to enter edit mode. The external control will be hidden when entering edit mode and shown again when leaving edit mode. ここで指定したid属性の表示をクリックすると編集モードに移行する。
rows V1.5 1 The row height of the input field (anything greater than 1 uses a multiline textarea for input) フォームの行数。1行ならtextfield、2行以上はtextarea。
onComplete V1.6 “function(transport, element) {new Effect.Highlight(element, {startcolor: this.options.highlightcolor});}” Code run if update successful with server 更新が成功した時に処理される内容。
onFailure V1.6 “function(transport) {alert(“Error communicating with the server: ” + transport.responseText.stripTags());}” Code run if update failed with server 更新が失敗した時に処理される内容。
cols V1.5 none The number of columns the text area should span (works for both single line or multi line) フォームの横幅。
size V1.5 none Synonym for ‘cols’ when using single-line (rows=1) input rowsの別名。
highlightcolor ? Ajax.InPlaceEditor.defaultHighlightColor The highlight color ハイライトカラー。マウスを乗せたときの強調表示の色。
highlightendcolor ? ”#FFFFFF” The color which the highlight fades to ハイライトカラーはここで指定した色まで段階的に変化する。
savingClassName V1.5 “inplaceeditor-saving” CSS class added to the element while displaying “Saving…” (removed when server responds) 更新処理中に追加されるクラス名。
formClass Name? V1.5 “inplaceeditor-form” CSS class used for the in place edit form InPlaceEditorフォームのクラス名
hoverClass Name? ? ?
loadTextURL V1.5 null Will cause the text to be loaded from the server (useful if your text is actually textile and formatted on the server) 編集モードに移行した時に、データを取得するURL。Railsでは”/controller/action/id”のように指定すれば、そのアクションを処理した結果がInPlaceEditorフォームにセットされる。指定がなければ、既にページ上で表示されている値がセットされる。
loadingText V1.5 “Loading…” If the loadText URL option is specified then this text is displayed while the text is being loaded from the server 上記データ取得中に表示する文字列。
callback V1.5 function(form) {Form.serialize(form)} A function that will get executed just before the request is sent to the server, should return the parameters to be sent in the URL. Will get two parameters, the entire form and the value of the text control.
submitOnBlur V1.6 “false” This option if true will submit the in_place_edit form when the input tag loses focus. フォームからフォーカスが外れると常に更新処理が実行される。表計算ソフトのような操作性が期待できるかも。
ajaxOptions V1.5 {} Options specified to all AJAX calls (loading and saving text), these options are passed through to the prototype AJAX classes.
emptyText 拡張 'click to edit...' 内容がブランクの時表示される文字列。これをクリックすることで編集モードへ移行可能になる。
emptyClassName 拡張 'inplaceeditor-empty' 上記文字列のクラス名。スタイルシートで色をグレーにするなどの指定が可能。