今回はcontrols.jsの解説の後編として、Webページがその場で編集できるようになるInPlaceEditorを解説します。controls.jsの続きとはいえ、前回のAutocompleterとの依存性は全くありませんので、それぞれ別々に読んでいただいて問題ありません。
InPlaceEditorとは?
1991年にティム・バーナーズ=リーが作った、世界で一番最初のブラウザであるWorldWideWebは、ブラウザであると同時に、タグ打ちせずにHTMLを編集できるHTMLエディターでもあったといいます。時代が巡り、ブラウザからそのような機能は失われましたが、現在のWikiやBlogは、それを再発見しようととしているのかもしれません。それらの多くは、編集と更新の間にページ遷移をはさみますが、このInPlaceEditorを使うことで、Webページをその場で編集することができるようになります。
Ajax.InPlaceEditorは、要素がその場でinput要素やtextarea要素に早変わりし、内容を入力エリアで編集できるようになる機能です。
Ajax.InPlaceCollectionEditorは、同様に、その場でselect要素に早変わりし、新しい内容をプルダウンメニューから選べる機能です。
Ajax.InPlaceEditor
それでは、実際にcontrols.jsの後半部分から、Ajax.InPlaceEditorのコードを見ていきましょう。
去年の夏に書き直されたおかげで、よく整理されていて理解しやすいコードです。
475~480行目のField.scrollFreeActivateは、DOM操作とField.activate()との間に1msの遅延をいれることで、妙なスクロールが起きてしまうのを防ぐ関数です。後述するbuildOptionListのなかで使われます。
481~506行目のinitializeは、オプションを読み込み、イベントリスナを設定するなどの初期化をする関数です。
483行目で、ユーザの更新内容を送信するサーバのURLの設定をします。
485行目で、デフォルトのオプションとイベントリスナを読み込みます。
488行目で、ユーザのオプションを読み込みます。
494行目で、externalControlというのは、ライブラリの利用者が自由に追加する'edit'リンクなどの外部コントロールのことです。
498行目で、Effect.Highlightをかけた後に色を元に戻せるように、'background-color'スタイルプロパティの値を保存しておきます。
500~504行目で、イベントリスナとなる関数をいくつか作ります。
505行目で、後述するregisterListenersを使って、イベントリスナを、要素や外部コントロールに設定します。
507~513行目のcheckForEscapeOrReturnは、編集中の特別なキー押下を検知して、適当なイベントリスナを呼び出す関数です。
508行目で、Ctrlキー、Altキー、Shiftキーが押されているときは何もしません。
509行目で、ESCキーで編集中止します。handleFormCancellationを呼びます。
511行目で、リターンキーで編集完了、送信します。handleFormSubmissionを呼びます。
514~537行目のcreateControlは、okボタンやcancelリンクのDOM要素を作る関数です。デフォルトでは、
okボタンとして<input type='submit' value='ok' className='editor_ok_button'>
cancelリンクとして<a href='#' onclick=_boundCancelHandler className='editor_cancel_link'>cancel</a>
が作られます。
これらは設定しだいで、okリンクでもcancelボタンでもよいので、ひっくるめてcreateControlという呼び名がついているわけです。
538~560行目のcreateEditFieldは、入力エリアを作る関数です。入力エリアの最初の内容を、Ajaxでサーバから読み込むこともできます。
539行目で、options.loadTextURLが設定されている場合はAjaxが使われ、Ajaxの間、入力エリアの内容がoptions.loadingText(デフォルトで"Loading...")になります。その後、Ajaxの完了時に書き換えられます。URLの設定がなければ、要素の内容をそのまま受け継ぎます。
542行目で、options.rowsの指定が1以下、かつ、要素の内容が改行を含まないとき、1行分の入力エリアとしてinput要素が作られます。
547行目で、そうでないとき、複数行分の入力エリアとしてtextarea要素が作られます。
551行目で、入力エリアのnameプロパティをoptions.paramNameにします。
552行目で、入力エリアの内容を539行目のtext変数の値にします。
553行目で、入力エリアのclassNameプロパティを'editor_field'とします。
554行目で、フォーカスを失ったときに送信するオプションoptions.submitOnBlurが設定されているときは、onblurイベントリスナに_boundSubmitHandlerを設定します。
557行目で、入力エリアの最初の内容をAjaxで問い合わせるloadExternalTextを呼びます。
561~582行目のcreateFormは、上述のcreateEditField,createControlを使って、入力エリアのフォームを作る関数です。概要としては、次のようなフォームが作られます。
563行目のaddTextはオプションのoptions.textBeforeControls、options.textBetweenControls、options.textAfterControlsで指定された文字列を挿入するための関数です。
575行目で、options.onFormCustomizationというフックが用意されています。このフックを使うと、フォームの中身を好きなようにいじることができます。
583~588行目のdestroyは、その場で編集機能を解除します。内部的には使われていません。ライブラリの利用者が使うために用意されています。
584行目で、要素の内容を元に戻します。
586行目で、編集モードを終了します
587行目で、イベントリスナを全て解除します。
589~601行目はenterEditModeです。この関数は、"その場で編集できるHTML"を表現するために、要素を隠すと同時にその場に入力エリアのフォームを作って、入れ替わったように見せます。
592行目で、options.onEnterEditModeフックを呼びます。このフックを使うことで、編集モードに入ったときの動作を変更することができます。
593行目で、外部コントロールを(あれば)隠します。
595行目で、要素を隠します。
596行目で、入力エリアのフォームを作ります。この関数は作ったフォームを、this._formに代入します。
597行目で、insertBeforeで隠した要素の直前に挿入することで、要素が入力エリアのフォームに入れ替わったように見せます。
598行目で、Ajaxでサーバからテキストを読み込むオプションoptions.loadTextURLの設定がなければ、postProcessEditFieldを呼びます。これは入力エリアを'focus'か'active'する関数です。このオプションの設定があるときは、loadExternalTextの中でAjaxの成功時に、postProcessEditFieldが呼ばれます。
602~607行目のenterHoverは、要素の上にカーソルが入ったときのイベントリスナです。
603行目で、要素のクラス名にoptions.hoverClassNameを追加します。
605行目で、保存中であればonEnterHoverフックを呼びません。
606行目でonEnterHoverフックを呼びます。このフックは、デフォルトでは要素の背景色を黄色にして注目をひきます。
608~610行目のgetTextは、要素の内容を返す関数です。
611~617行目のhandleAJAXFailureは、Ajaxが失敗したときに、内容を復帰する関数です。
612行目で、onFailureフックを呼びます。このフックを使うと、Ajaxが失敗したときの動作を変更することができます。
613行目で、要素の内容を元に戻します。
618~621行目のhandleFormCancellationは、編集中止をしたときに、フォームを消して、要素を再び表示する関数です。
622~648行目のhandleFormSubmissionは、編集完了したときにフォームの内容をAjaxでサーバに送る関数です。
625行目で、まずは後述するprepareSubmissionで、入力フォームを閉じ、"Saving..."を表示します。
626行目以降で、Ajaxのクエリパラメータを構築します。ここにoptions.callbackというフックが用意されていて、自分の好きなようにクエリパラメータをいじれます。
Ajaxの完了時は、フォームを消して要素を再び表示する_boundWrapperHandler(実体はthis.wrapUp)が呼ばれます。
失敗時は、要素の内容を復帰する_boundFailureHandler(実体はthis.handleAJAXFailure)が呼ばれます。
options.htmlResponseがtrueのとき、Ajax.Updaterを使って、Ajaxの結果を要素に反映します。このとき、evalScriptsオプションがtrueで与えられるので、Ajax.Updaterの機能で、AjaxレスポンスのHTMLコンテンツに含まれる<script>タグの内容は評価(eval)されます。
options.htmlResponseがfalseのとき、Ajax.Requestが使われます。Ajax.Requestの機能で、AjaxレスポンスのContent-typeヘッダの値がtext/javascriptであった場合、レスポンス本体は評価(eval)されます。
649~661行目のleaveEditModeは、編集をやめるときに呼ばれる関数で、フォームを消し、要素を再び表示します。
651行目で、入力フォームを消します。
652行目で、後述のleaveHoverを呼びます。
653行目で、onEnterHoverが変更した背景色を元に戻します。
654行目で、もともとの要素を再び表示します。
655行目で、外部コントロールを(もしあれば)再び表示します。
660行目で、onLeaveEditModeフックを呼びます。
662~667行目のleaveHoverは、要素の上からカーソルが出たときと、編集モードを終えるときに、呼ばれる関数です。
663行目で、enterHoverでつけたクラス名を取り除きます。
665行目で、この関数が、カーソルが出たときに呼ばれたのか、編集モードを終えたときに呼ばれたのかを判断します。this._savingがfalseなら前者、trueなら後者です。
666行目で、カーソルが出たときに呼ばれた場合とわかったので、onLeaveHoverフックを呼びます。
668~687行目のloadExternalTextは、編集モードに入ったときに、入力エリアの最初の内容をAjaxで読み込む関数です。
669行目で、入力フォームのクラス名にoptions.loadingClassNameを追加します。
670行目で、入力エリアを入力不可にします。これはAjaxが成功するまでの間です。
672~685行目で、Ajaxのオプションを構築します。
673行目で、クエリパラメータのeditorIdに要素のidを設定します。
674行目で、onCompleteフックに何もしない関数を設定します。
675行目で、Ajaxの成功時に呼ばれるonSuccessフックに以下のような動作をする関数を設定します。
676行目で、入力フォームのクラス名からoptions.loadingClassNameを除きます。
677行目で、Ajaxレスポンスを取り出します。
678行目で、options.stripLoadedTextTagsが設定されているならば、レスポンスからタグを除きます。
680行目で、取り出したレスポンスを入力エリアの最初の内容にします。
681行目で、入力エリアを入力可能にします。
682行目で、Ajaxの成功時は、後述するpostProcessEditFieldで入力エリアにフォーカスが移るようになっています。
684行目で、Ajaxの失敗時は、_boundFailureHandler(実体はhandleAJAXFailure)で、編集モードを取りやめることになっています。
686行目で、Ajaxでoptions.loadTextURLにアクセスします。
688~692行目のpostProcessEditFieldは、fieldPostCreationオプション次第で、入力エリアをfocus()あるいはactivate()します。
693~699行目の prepareOptionsはデフォルトのオプションをthis.optionsに取り込みます。取り込まれるのは、DefaultOptions, DefaultCallbacks, this._extraDefaultOptions の内容です。
700~705行目のprepareSubmissionは、フォームを消して、leaveHoverを呼び、Ajaxが成功するまでの間"Saving..."を表示します。handleFormSubmissionで、編集完了時にAjaxで入力エリアの内容を送信する前に呼ばれます。
706~717行目のregisterListenersは、要素と外部コントロールにイベントリスナを登録します。
709行目で、Ajax.InPlaceEditor.Listenersにあげられている、click,keydown,mouseover,mouseoutイベントにイベントリスナを登録します。
デフォルトではこれらを要素と外部コントロールの両方に登録しますが、712行目のとおり、externalControlOnlyオプションが設定されている場合は、外部コントロールだけに登録します。
718~723行目のremoveFormは、DOMから入力エリアのフォームを抹消する関数です。
724~730行目の showSaving は、Ajaxの結果が帰ってくるまでの間、要素に options.savingText(デフォルトでは "..Saving")を表示します。
725行目で、現在の要素の内容を保存しておきます。Ajaxの失敗時などに内容を復帰するのに使われます。
726行目で、要素に options.savingText(デフォルトでは "..Saving")を表示します。
727行目で、要素のクラス名にoptions.savingClassNameを追加します。
728行目で、要素の背景色を元に戻します(色を変えたのはonEnterHoverです)。
729行目で、enterEditModeで隠されたこの要素を再び表示します。
731~735行目のtriggerCallbackはオプションであたえられたフックを呼ぶ関数です。
736~743行目のunregisterListenersは、要素と外部コントロールのイベントリスナを全て解除します。
744~751行目のwrapUpは、編集が完了してAjaxが成功したときと、編集を中止したときに呼ばれます。
745行目で、leaveEditModeを呼んで、編集モードを終了します。
748行目の_boundCompleteの実体はoptions.onCompleteで、要素をハイライトします。
InPlaceEditorクラスにdisposeメソッドを追加しています。このメソッドはdestoryメソッド同様、内部的には使われません。
以上でAjax.InPlaceEditorの解説は終わりです。
InPlaceCollectionEditorとは
InPlaceCollectionEditorでは、要素をクリックすると(入力エリアではなく)select要素のプルダウンメニューに入れ替わります。メニューで選択した結果がAjaxでサーバに送られ、サーバのレスポンスが要素の新しい内容になります。
オプションで、選択肢のリストと、デフォルトの選択肢を与えることができます。ローカルで与えるほかに、これらはAjaxでサーバから読み込めるようにもなっています。選択肢のリストを読み込む場合は、サーバは、["one","two","three"]などと、JavaScriptの配列を表現したレスポンスを返してやります。デフォルトの選択肢を読み込む場合は、サーバは"two"などと返します。
Ajax.InPlaceCollectionEditor
それでは、コードを見ていきましょう。 Ajax.InPlaceCollectionEditor は、Ajax.InPlaceEditorを継承します。このような継承の機能はprototype.jsの1.6.0で導入されたものです。
処理の流れは、createEditFieldで選択肢のリストを読み込み、checkForExternalTextに進んでメニューのデフォルトの選択肢を設定し、buildOptionListに進んでDOMを構築する、となっています。
その中で、Ajaxで選択肢のリストを読み込むloadCollectionと、デフォルトの選択肢を読み込むloadExternalTextが呼ばれます。
757~761行目のinitializeは、InPlaceCollectionEditor特有のオプションを追加してから、$superで、親クラスのメソッドであるAjax.InPlaceEditorのinitializeを呼びます。この$superは、prototype.jsの1.6.0で導入された機能です。
762~774行目のcreateEditFieldは、入力フォームの中のプルダウンメニューの部分を作ります。
763行目で、select要素を作ります。
764行目で、要素のnameプロパティにoptions.paramNameオプションの値を設定します。
767行目で、選択肢のリストとして、ローカルで指定されたoptions.collectionを読み込みます。
769行目で、options.loadCollectionURLが設定されているときは、loadCollectionで選択肢のリストをAjaxで読み込みます。
771行目で、そのような設定がなく、Ajaxの必要がないときは、次の段階として、デフォルトの選択肢の設定をするcheckForExternalTextに進みます。
775~793行目のloadCollectionは、メニューの選択肢の内容をAjaxで問い合わせます。Ajaxに対してサーバは、['one','two','three']といったJavaScriptの配列を表現したレスポンスを返すようにします。
776行目で、入力フォームのクラス名にoptions.loadingClassNameを追加します。
777行目で、Ajaxが成功するまでの間、要素にoptions.loadingCollectionText(デフォルトでは'Loading options...')を表示します。
778~791行目で、Ajaxのオプションを作ります。
780行目で、クエリパラメータのeditorIdに要素のidを設定します。
781行目で、onCompleteフックに何もしない関数を設定します。
782行目で、Ajaxの成功時に呼ばれるonSuccessフックに以下のような動作をする関数を設定します。
783行目で、Ajaxのレスポンスを取り出し、先頭と末尾の空白を取り除きます。
784行目で、レスポンスがJavaScriptの配列の表現である[...]の形になっているかチェックし、なっていなければエラーを発生させて処理を中止します。
786行目で、レスポンスを評価して、実際の配列にしてから、_collectionプロパティに代入します。
787行目で、次の段階として、デフォルトの選択肢の設定をするcheckForExternalTextに進みます。
791行目で、Ajaxでoptions.loadCollectionURLにアクセスします。
793~805行目のshowLoadingTextは、Ajaxが成功するまでの間、select要素のプルダウンメニューに、ロード中を表すテキストをoption要素で表示します。
795行目で、buildOptionListが呼ばれるまでの間、メニューを入力不可にします。
796行目で、select要素の先頭の子要素を取りだします。子要素はまだ作られていないかもしれません
797~802行目で、まだ作られていなかったときは、createElement('option')を使って作り、valueプロパティを空白にし、メニューのselect要素にappendChildしてから、selectedプロパティをtrueにして、選択肢の一番上に表示してやります。
803行目で、その表示をtextの値(デフォルトでは"Loading...")にします。textは外部から読み込まれるので注意して扱うように、<script /> ブロックとHTML、XMLタグを取り除いてから使います。
806~813行目のcheckForExternalTextは、メニューのデフォルトの選択肢を設定する関数です。
807行目で、まずはもともとの要素の値をgetTextで取り出してデフォルトの選択肢とします。
808行目で、loadTextURLが設定されているときはデフォルトの選択肢をAjaxで読み込むためにloadExternalTextを呼びます。
Ajaxを使う必要がなければ、次の段階であるbuildOptionListに進んで、DOMの構築に入ります。
814~828行目のloadExternalTextは、デフォルトの選択肢をAjaxで読み込む関数です。
815行目で、メニューにロード中をあらわすoptions.loadingText(デフォルトでは"Loading...")を表示します。
816~825行目で、Ajaxのオプションを構築します。
818行目で、クエリパラメータのeditorIdに要素のidを設定します。
819行目で、onCompleteフックに何もしない関数を設定します。
820行目で、Ajaxの成功時に呼ばれるonSuccessフックに以下のような動作をする関数を設定します。
821行目で、Ajaxレスポンスを取り出します。先頭と末尾の空白を除きます
822行目で、次の段階であるbuildOptionListに進んで、DOMの構築に入ります。
824行目で、Ajaxの失敗時に呼ばれるonFailureフックに、onFailureを設定します。onFailureの中身はデフォルトでは、alertで通信エラーのメッセージを表示するだけです。
826行目で、Ajaxでoptions.loadTextURLにアクセスします。
829~851行目のbuildOptionListは、ここまでで得た、this._collection(選択肢の配列)と、this._text(デフォルトの選択肢)の情報から、DOMを構築します。ただしデフォルトの選択肢については、options.valueの設定が優先されます。
this._collectionの内容は、
["one","two","three"]の形か、
[["one","はい"],["two","いいえ"],["three","無回答"]]の形の配列です。
831行目で、前者の形は、[["one","one"],["two","two"],["three","three"]]となって、後者の形に統一されます。
834行目のmarkerは、デフォルトの選択肢を表します。まずoptions.valueを見て、その設定がなければthis._textになります。
835行目のtextFoundには、選択肢の配列の中で、デフォルトの選択肢のmarkerと一致する値が入ります。
838行目で、メニューの要素の内容を空白にします。
840~846行目で、option要素のDOMを作ります。例えばデフォルトの選択肢が"two"ならば、結果は次のようになります。
- <option value="one" selected=false>はい</option>
- <option value="two" selected=true>いいえ</option>
- <option value="three" selected=false>無回答</option>
847行目で、メニューの入力を可能にします。これはshowLoadingTextで不可にしていました。
848行目で、メニューをアクティブにします。
852~870行目のdealWithDeprecatedOptionsは古いオプションを扱うための関数です。いずれ削除されるので、はやめにコードを新しいAPIに変更せよとのコメントがあります。
cancelLink,cancelButton,okLink,okButton,highlightcolor,highlightendcolorという古いオプションを、今のオプションに対応づけています。
871~901行目は、オプションのデフォルト設定です。
902~929行目は、デフォルトのコールバックです。
callbackは入力フォームの内容がサーバに送信される直前に呼ばれます。この時点で、入力フォームはDOMから除かれ、'Saving...'が表示されています。
このフックは引数に、入力フォームと入力フォームの内容(デフォルトでは入力フォームがForm.serializeでシリアライズされたもの)をとります。これを使うことで、入力フォームの内容を簡単にカスタマイズすることができます。
onEnterEditModeは、編集が始まる直前に呼ばれます、原則として"その場で編集"要素をクリックしてすぐです。外部コントロールは、(もしあれば)まだ表示されたままです。要素もそのままです。入力フォームはまだ生成されていません。
このフックがとる引数はひとつで、InPlaceEditorのインスタンスです。
onLeaveEditModeは対称で、すべての編集作業が終わって入力フォームがページのDOMから取り払われたあとに呼ばれます。
フックの引数はonEnterEditModeと同じです。
onEnterHoverは、カーソルが"その場で編集"要素の上にはいったときに呼ばれます。デフォルトでは、背景の色をhighlightColorオプションの色にします。
このフックがとる引数はひとつで、InPlaceEditorのインスタンスです。
onLeaveHoverは対称で、カーソルが要素の上からでたときに呼ばれます。Effect.Highlightで背景色をハイライトしてから、元の色に戻します(背景イメージもハイライトします)。
onCompleteは編集をキャンセルしたときと、編集を完了してサーバーに送信が成功したときに呼ばれます。ハイライトして反応を示します(背景イメージもハイライトします)。
このフックがとる引数はふたつです。XMLHttpRequestオブジェクト(キャンセルのときはundefinedです)と"その場で編集"要素です。
onFailureは、内部でAjaxリクエストが失敗したとき(あるいはHTTPレスポンスが2xxコードでなかったとき)に呼ばれます。デフォルトではレスポンスの内容の詳細が書かれたメッセージボックスを表示します。
このフックは引数にXMLHttpRequestとInPlaceEditorのインスタンスをとります。
onFormCustomizationは最近になって新設されたフックで、フォームをカスタマイズしやすくするために用意されました。フォームが作成されてすぐに呼ばれます。この時点では、他のコントロール(OKボタン/Cancelリンクなど)はまだありません。
このフックは引数にInPlaceEditorのインスタンスとフォームをとります。自分でコントロールをいくつか追加したいときはここでappendChildを使えばいいので、ずいぶん便利になりました。
930~937行目は、イベントリスナの設定です。
938~941行目は、InPlaceCollectionEditorのオプションにloadingCollectionTextを追加します。
以上でAjax.InPlaceCollectionEditorの解説は終わりです。
Form.Element.DelayedObserver
942~965行目のForm.Element.DelayedObserverは、前回のAutocompleterで解説したような、フォームの入力エリアでのキー入力の小休止を見計らって、指定のフック(callback)を呼ぶ仕組みのクラスです。内部的には使われていません。動作について興味のあるかたは前回のAutocompleterの解説をご覧ください。
以上で、controls.jsの解説は終わりです。