script.aculo.usを読み解く

第3回controls.js(後編) InPlaceEditor

今回は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のコードを見ていきましょう。

去年の夏に書き直されたおかげで、よく整理されていて理解しやすいコードです。

0469: // AJAX in-place editor and collection editor
0470: // Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
0471: 
0472: // Use this if you notice weird scrolling problems on some browsers,
0473: // the DOM might be a bit confused when this gets called so do this
0474: // waits 1 ms (with setTimeout) until it does the activation
0475: Field.scrollFreeActivate = function(field) {
0476:   setTimeout(function() {
0477:     Field.activate(field);
0478:   }, 1);
0479: }
0480:

475~480行目のField.scrollFreeActivateは、DOM操作とField.activate()との間に1msの遅延をいれることで、妙なスクロールが起きてしまうのを防ぐ関数です。後述するbuildOptionListのなかで使われます。

0481: Ajax.InPlaceEditor = Class.create({
0482:   initialize: function(element, url, options) {
0483:     this.url = url;
0484:     this.element = element = $(element);
0485:     this.prepareOptions();
0486:     this._controls = { };
0487:     arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
0488:     Object.extend(this.options, options || { });
0489:     if (!this.options.formId && this.element.id) {
0490:       this.options.formId = this.element.id + '-inplaceeditor';
0491:       if ($(this.options.formId))
0492:         this.options.formId = '';
0493:     }
0494:     if (this.options.externalControl)
0495:       this.options.externalControl = $(this.options.externalControl);
0496:     if (!this.options.externalControl)
0497:       this.options.externalControlOnly = false;
0498:     this._originalBackground = this.element.getStyle('background-color') || 'transparent';
0499:     this.element.title = this.options.clickToEditText;
0500:     this._boundCancelHandler = this.handleFormCancellation.bind(this);
0501:     this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
0502:     this._boundFailureHandler = this.handleAJAXFailure.bind(this);
0503:     this._boundSubmitHandler = this.handleFormSubmission.bind(this);
0504:     this._boundWrapperHandler = this.wrapUp.bind(this);
0505:     this.registerListeners();
0506:   },

481~506行目のinitializeは、オプションを読み込み、イベントリスナを設定するなどの初期化をする関数です。

483行目で、ユーザの更新内容を送信するサーバのURLの設定をします。

485行目で、デフォルトのオプションとイベントリスナを読み込みます。

488行目で、ユーザのオプションを読み込みます。

494行目で、externalControlというのは、ライブラリの利用者が自由に追加する'edit'リンクなどの外部コントロールのことです。

498行目で、Effect.Highlightをかけた後に色を元に戻せるように、'background-color'スタイルプロパティの値を保存しておきます。

500~504行目で、イベントリスナとなる関数をいくつか作ります。

505行目で、後述するregisterListenersを使って、イベントリスナを、要素や外部コントロールに設定します。

0507: checkForEscapeOrReturn: function(e) {
0508:   if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
0509:   if (Event.KEY_ESC == e.keyCode)
0510:     this.handleFormCancellation(e);
0511:   else if (Event.KEY_RETURN == e.keyCode)
0512:     this.handleFormSubmission(e);
0513: },

507~513行目のcheckForEscapeOrReturnは、編集中の特別なキー押下を検知して、適当なイベントリスナを呼び出す関数です。

508行目で、Ctrlキー、Altキー、Shiftキーが押されているときは何もしません。

509行目で、ESCキーで編集中止します。handleFormCancellationを呼びます。

511行目で、リターンキーで編集完了、送信します。handleFormSubmissionを呼びます。

0514: createControl: function(mode, handler, extraClasses) {
0515:   var control = this.options[mode + 'Control'];
0516:   var text = this.options[mode + 'Text'];
0517:   if ('button' == control) {
0518:     var btn = document.createElement('input');
0519:     btn.type = 'submit';
0520:     btn.value = text;
0521:     btn.className = 'editor_' + mode + '_button';
0522:     if ('cancel' == mode)
0523:       btn.onclick = this._boundCancelHandler;
0524:     this._form.appendChild(btn);
0525:     this._controls[mode] = btn;
0526:   } else if ('link' == control) {
0527:     var link = document.createElement('a');
0528:     link.href = '#';
0529:     link.appendChild(document.createTextNode(text));
0530:     link.onclick = 'cancel' == mode ? this._boundCancelHandler: this._boundSubmitHandler;
0531:     link.className = 'editor_' + mode + '_link';
0532:     if (extraClasses)
0533:       link.className += ' ' + extraClasses;
0534:     this._form.appendChild(link);
0535:     this._controls[mode] = link;
0536:   }
0537: },

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という呼び名がついているわけです。

0538: createEditField: function() {
0539:   var text = (this.options.loadTextURL ? this.options.loadingText: this.getText());
0540:   var fld;
0541:   if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
0542:     fld = document.createElement('input');
0543:     fld.type = 'text';
0544:     var size = this.options.size || this.options.cols || 0;
0545:     if (0 < size) fld.size = size;
0546:   } else {
0547:     fld = document.createElement('textarea');
0548:     fld.rows = (1 >= this.options.rows ? this.options.autoRows: this.options.rows);
0549:     fld.cols = this.options.cols || 40;
0550:   }
0551:   fld.name = this.options.paramName;
0552:   fld.value = text; // No HTML breaks conversion anymore
0553:   fld.className = 'editor_field';
0554:   if (this.options.submitOnBlur)
0555:     fld.onblur = this._boundSubmitHandler;
0556:   this._controls.editor = fld;
0557:   if (this.options.loadTextURL)
0558:     this.loadExternalText();
0559:   this._form.appendChild(this._controls.editor);
0560: },

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を呼びます。

0561: createForm: function() {
0562:   var ipe = this;

0563:   function addText(mode, condition) {
0564:     var text = ipe.options['text' + mode + 'Controls'];
0565:     if (!text || condition === false) return;
0566:     ipe._form.appendChild(document.createTextNode(text));
0567:   };
0568:   this._form = $(document.createElement('form'));
0569:   this._form.id = this.options.formId;
0570:   this._form.addClassName(this.options.formClassName);
0571:   this._form.onsubmit = this._boundSubmitHandler;
0572:   this.createEditField();
0573:   if ('textarea' == this._controls.editor.tagName.toLowerCase())
0574:     this._form.appendChild(document.createElement('br'));
0575:   if (this.options.onFormCustomization)
0576:     this.options.onFormCustomization(this, this._form);
0577:   addText('Before', this.options.okControl || this.options.cancelControl);
0578:   this.createControl('ok', this._boundSubmitHandler);
0579:   addText('Between', this.options.okControl && this.options.cancelControl);
0580:   this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
05>0581:   addText('After', this.options.okControl || this.options.cancelControl);
0582: },

561~582行目のcreateFormは、上述のcreateEditField,createControlを使って、入力エリアのフォームを作る関数です。概要としては、次のようなフォームが作られます。

<form id=options.formId className=options.formClassName onsubmit=_boundSubmitHandler>
  createEditField() ここに入力エリアが入る
  入力エリアがtextarea要素なら <br> を挿入
  options.textBeforeControls 文字列が入る
  createControl('ok'); 'ok'ボタンやリンクが入る
  options.textBetweenControls 文字列が入る
  createControl('cancel'); 'cancel'リンクやボタンが入る
  options.textAfterControls 文字列が入る
</form>

563行目のaddTextはオプションのoptions.textBeforeControls、options.textBetweenControls、options.textAfterControlsで指定された文字列を挿入するための関数です。

575行目で、options.onFormCustomizationというフックが用意されています。このフックを使うと、フォームの中身を好きなようにいじることができます。

0583: destroy: function() {
0584:   if (this._oldInnerHTML)
0585:   this.element.innerHTML = this._oldInnerHTML;
0586:   this.leaveEditMode();
0587:   this.unregisterListeners();
0588: },

583~588行目のdestroyは、その場で編集機能を解除します。内部的には使われていません。ライブラリの利用者が使うために用意されています。

584行目で、要素の内容を元に戻します。

586行目で、編集モードを終了します

587行目で、イベントリスナを全て解除します。

0589: enterEditMode: function(e) {

0590:   if (this._saving || this._editing) return;
0591:   this._editing = true;
0592:   this.triggerCallback('onEnterEditMode');
0593:   if (this.options.externalControl)
0594:     this.options.externalControl.hide();
0595:   this.element.hide();
0596:   this.createForm();
0597:   this.element.parentNode.insertBefore(this._form, this.element);
0598:   if (!this.options.loadTextURL)
0599:     this.postProcessEditField();
0600:   if (e) Event.stop(e);
0601: },

589~601行目はenterEditModeです。この関数は、"その場で編集できるHTML"を表現するために、要素を隠すと同時にその場に入力エリアのフォームを作って、入れ替わったように見せます。

592行目で、options.onEnterEditModeフックを呼びます。このフックを使うことで、編集モードに入ったときの動作を変更することができます。

593行目で、外部コントロールを(あれば)隠します。

595行目で、要素を隠します。

596行目で、入力エリアのフォームを作ります。この関数は作ったフォームを、this._formに代入します。

597行目で、insertBeforeで隠した要素の直前に挿入することで、要素が入力エリアのフォームに入れ替わったように見せます。

598行目で、Ajaxでサーバからテキストを読み込むオプションoptions.loadTextURLの設定がなければ、postProcessEditFieldを呼びます。これは入力エリアを'focus'か'active'する関数です。このオプションの設定があるときは、loadExternalTextの中でAjaxの成功時に、postProcessEditFieldが呼ばれます。

0602: enterHover: function(e) {
0603:   if (this.options.hoverClassName)
0604:     this.element.addClassName(this.options.hoverClassName);
0605:   if (this._saving) return;
0606:   this.triggerCallback('onEnterHover');
0607: },

602~607行目のenterHoverは、要素の上にカーソルが入ったときのイベントリスナです。

603行目で、要素のクラス名にoptions.hoverClassNameを追加します。

605行目で、保存中であればonEnterHoverフックを呼びません。

606行目でonEnterHoverフックを呼びます。このフックは、デフォルトでは要素の背景色を黄色にして注目をひきます。

0608: getText: function() {
0609:   return this.element.innerHTML;
0610: },

608~610行目のgetTextは、要素の内容を返す関数です。

0611: handleAJAXFailure: function(transport) {
0612:   this.triggerCallback('onFailure', transport);
0613:   if (this._oldInnerHTML) {
0614:     this.element.innerHTML = this._oldInnerHTML;
0615:     this._oldInnerHTML = null;
0616:   }
0617: },

611~617行目のhandleAJAXFailureは、Ajaxが失敗したときに、内容を復帰する関数です。

612行目で、onFailureフックを呼びます。このフックを使うと、Ajaxが失敗したときの動作を変更することができます。

613行目で、要素の内容を元に戻します。

0618: handleFormCancellation: function(e) {
0619:   this.wrapUp();
0620:   if (e) Event.stop(e);
0621: },

618~621行目のhandleFormCancellationは、編集中止をしたときに、フォームを消して、要素を再び表示する関数です。


0622: handleFormSubmission: function(e) {
0623:   var form = this._form;
0624:   var value = $F(this._controls.editor);
0625:   this.prepareSubmission();
0626:   var params = this.options.callback(form, value) || '';
0627:   if (Object.isString(params))
0628:     params = params.toQueryParams();
0629:   params.editorId = this.element.id;
0630:   if (this.options.htmlResponse) {
0631:     var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
0632:     Object.extend(options, {
0633:       parameters: params,
0634:       onComplete: this._boundWrapperHandler,
0635:       onFailure: this._boundFailureHandler
0636:     });
0637:     new Ajax.Updater({ success: this.element }, this.url, options);
0638:   } else {
0639:     var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
0640:     Object.extend(options, {
0641:       parameters: params,
0642:       onComplete: this._boundWrapperHandler,
0643:       onFailure: this._boundFailureHandler
0644:     });
0645:     new Ajax.Request(this.url, options);
0646:   }
0647:   if (e) Event.stop(e);
0648: },

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)されます。

0649: leaveEditMode: function() {
0650:   this.element.removeClassName(this.options.savingClassName);
0651:   this.removeForm();
0652:   this.leaveHover();
0653:   this.element.style.backgroundColor = this._originalBackground;
0654:   this.element.show();
0655:   if (this.options.externalControl)
0656:     this.options.externalControl.show();
0657:   this._saving = false;
0658:   this._editing = false;
0659:   this._oldInnerHTML = null;
0660:   this.triggerCallback('onLeaveEditMode');
0661: },

649~661行目のleaveEditModeは、編集をやめるときに呼ばれる関数で、フォームを消し、要素を再び表示します。

651行目で、入力フォームを消します。

652行目で、後述のleaveHoverを呼びます。

653行目で、onEnterHoverが変更した背景色を元に戻します。

654行目で、もともとの要素を再び表示します。

655行目で、外部コントロールを(もしあれば)再び表示します。

660行目で、onLeaveEditModeフックを呼びます。

0662: leaveHover: function(e) {
0663:   if (this.options.hoverClassName)
0664:     this.element.removeClassName(this.options.hoverClassName);
0665:   if (this._saving) return;
0666:   this.triggerCallback('onLeaveHover');
0667: },

662~667行目のleaveHoverは、要素の上からカーソルが出たときと、編集モードを終えるときに、呼ばれる関数です。

663行目で、enterHoverでつけたクラス名を取り除きます。

665行目で、この関数が、カーソルが出たときに呼ばれたのか、編集モードを終えたときに呼ばれたのかを判断します。this._savingがfalseなら前者、trueなら後者です。

666行目で、カーソルが出たときに呼ばれた場合とわかったので、onLeaveHoverフックを呼びます。

0668: loadExternalText: function() {
0669:   this._form.addClassName(this.options.loadingClassName);
0670:   this._controls.editor.disabled = true;
0671:   var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
0672:   Object.extend(options, {
0673:     parameters: 'editorId=' + encodeURIComponent(this.element.id),
0674:     onComplete: Prototype.emptyFunction,
0675:     onSuccess: function(transport) {
0676:       this._form.removeClassName(this.options.loadingClassName);
0677:       var text = transport.responseText;
0678:       if (this.options.stripLoadedTextTags)
0679:         text = text.stripTags();
0680:       this._controls.editor.value = text;
0681:       this._controls.editor.disabled = false;
0682:       this.postProcessEditField();
0683:     }.bind(this),
0684:     onFailure: this._boundFailureHandler
0685:   });
0686:   new Ajax.Request(this.options.loadTextURL, options);
0687: },

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にアクセスします。

0688: postProcessEditField: function() {
0689:   var fpc = this.options.fieldPostCreation;
0690:   if (fpc)
0691:     $(this._controls.editor)['focus' == fpc ? 'focus': 'activate']();
0692: },

688~692行目のpostProcessEditFieldは、fieldPostCreationオプション次第で、入力エリアをfocus()あるいはactivate()します。

0693: prepareOptions: function() {
0694:   this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
0695:   Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
0696:   [this._extraDefaultOptions].flatten().compact().each(function(defs) {
0697:     Object.extend(this.options, defs);
0698:   }.bind(this));
0699: },

693~699行目の prepareOptionsはデフォルトのオプションをthis.optionsに取り込みます。取り込まれるのは、DefaultOptions, DefaultCallbacks, this._extraDefaultOptions の内容です。

0700: prepareSubmission: function() {
0701:   this._saving = true;
0702:   this.removeForm();
0703:   this.leaveHover();
0704:   this.showSaving();
0705: },

700~705行目のprepareSubmissionは、フォームを消して、leaveHoverを呼び、Ajaxが成功するまでの間"Saving..."を表示します。handleFormSubmissionで、編集完了時にAjaxで入力エリアの内容を送信する前に呼ばれます。


0706: registerListeners: function() {
0707:   this._listeners = { };
0708:   var listener;
0709:   $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
0710:     listener = this[pair.value].bind(this);
0711:     this._listeners[pair.key] = listener;
0712:     if (!this.options.externalControlOnly)
0713:       this.element.observe(pair.key, listener);
0714:     if (this.options.externalControl)
0715:       this.options.externalControl.observe(pair.key, listener);
0716:   }.bind(this));
0717: },

706~717行目のregisterListenersは、要素と外部コントロールにイベントリスナを登録します。

709行目で、Ajax.InPlaceEditor.Listenersにあげられている、click,keydown,mouseover,mouseoutイベントにイベントリスナを登録します。

デフォルトではこれらを要素と外部コントロールの両方に登録しますが、712行目のとおり、externalControlOnlyオプションが設定されている場合は、外部コントロールだけに登録します。

0718: removeForm: function() {
0719:   if (!this._form) return;
0720:   this._form.remove();
0721:   this._form = null;
0722:   this._controls = { };
0723: },

718~723行目のremoveFormは、DOMから入力エリアのフォームを抹消する関数です。

0724: showSaving: function() {
0725:   this._oldInnerHTML = this.element.innerHTML;
0726:   this.element.innerHTML = this.options.savingText;
0727:   this.element.addClassName(this.options.savingClassName);
0728:   this.element.style.backgroundColor = this._originalBackground;
0729:   this.element.show();
0730: },

724~730行目の showSaving は、Ajaxの結果が帰ってくるまでの間、要素に options.savingText(デフォルトでは "..Saving")を表示します。

725行目で、現在の要素の内容を保存しておきます。Ajaxの失敗時などに内容を復帰するのに使われます。

726行目で、要素に options.savingText(デフォルトでは "..Saving")を表示します。

727行目で、要素のクラス名にoptions.savingClassNameを追加します。

728行目で、要素の背景色を元に戻します(色を変えたのはonEnterHoverです⁠⁠。

729行目で、enterEditModeで隠されたこの要素を再び表示します。

0731: triggerCallback: function(cbName, arg) {
0732:   if ('function' == typeof this.options[cbName]) {
0733:     this.options[cbName](this, arg);
0734:   }
0735: },

731~735行目のtriggerCallbackはオプションであたえられたフックを呼ぶ関数です。

0736: unregisterListeners: function() {
0737:   $H(this._listeners).each(function(pair) {
0738:     if (!this.options.externalControlOnly)
0739:       this.element.stopObserving(pair.key, pair.value);
0740:     if (this.options.externalControl)
0741:       this.options.externalControl.stopObserving(pair.key, pair.value);
0742:   }.bind(this));
0743: },

736~743行目のunregisterListenersは、要素と外部コントロールのイベントリスナを全て解除します。

0744: wrapUp: function(transport) {
0745:   this.leaveEditMode();
0746:   // Can't use triggerCallback due to backward compatibility: requires
0747:   // binding + direct element
0748:   this._boundComplete(transport, this.element);
0749: }
0750: });
0751: 

744~751行目のwrapUpは、編集が完了してAjaxが成功したときと、編集を中止したときに呼ばれます。

745行目で、leaveEditModeを呼んで、編集モードを終了します。

748行目の_boundCompleteの実体はoptions.onCompleteで、要素をハイライトします。

0752: Object.extend(Ajax.InPlaceEditor.prototype, {
0753: dispose: Ajax.InPlaceEditor.prototype.destroy
0754: });
0755: 

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が呼ばれます。

0756: Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
0757: initialize: function($super, element, url, options) {
0758:   this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
0759:   $super(element, url, options);
0760: },
0761:

757~761行目のinitializeは、InPlaceCollectionEditor特有のオプションを追加してから、$superで、親クラスのメソッドであるAjax.InPlaceEditorのinitializeを呼びます。この$superは、prototype.jsの1.6.0で導入された機能です。

0762: createEditField: function() {
0763:   var list = document.createElement('select');
0764:   list.name = this.options.paramName;
0765:   list.size = 1;
0766:   this._controls.editor = list;
0767:   this._collection = this.options.collection || [];
0768:   if (this.options.loadCollectionURL)
0769:     this.loadCollection();
0770:   else
0771:     this.checkForExternalText();
0772:   this._form.appendChild(this._controls.editor);
0773: },
0774: 

762~774行目のcreateEditFieldは、入力フォームの中のプルダウンメニューの部分を作ります。

763行目で、select要素を作ります。

764行目で、要素のnameプロパティにoptions.paramNameオプションの値を設定します。

767行目で、選択肢のリストとして、ローカルで指定されたoptions.collectionを読み込みます。

769行目で、options.loadCollectionURLが設定されているときは、loadCollectionで選択肢のリストをAjaxで読み込みます。

771行目で、そのような設定がなく、Ajaxの必要がないときは、次の段階として、デフォルトの選択肢の設定をするcheckForExternalTextに進みます。

0775: loadCollection: function() {
0776:   this._form.addClassName(this.options.loadingClassName);
0777:   this.showLoadingText(this.options.loadingCollectionText);
0778:   var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
0779:   Object.extend(options, {
0780:     parameters: 'editorId=' + encodeURIComponent(this.element.id),
0781:     onComplete: Prototype.emptyFunction,
0782:     onSuccess: function(transport) {
0783:       var js = transport.responseText.strip();
0784:       if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
0785:         throw 'Server returned an invalid collection representation.';
0786:       this._collection = eval(js);
0787:       this.checkForExternalText();
0788:     }.bind(this),
0789:     onFailure: this.onFailure
0790:   });
0791:   new Ajax.Request(this.options.loadCollectionURL, options);
0792: },
0793: 

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にアクセスします。

0794: showLoadingText: function(text) {
0795:   this._controls.editor.disabled = true;
0796:   var tempOption = this._controls.editor.firstChild;
0797:   if (!tempOption) {
0798:     tempOption = document.createElement('option');
0799:     tempOption.value = '';
0800:     this._controls.editor.appendChild(tempOption);
0801:     tempOption.selected = true;
0802:   }
0803:   tempOption.update((text || '').stripScripts().stripTags());
0804: },
0805: 

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タグを取り除いてから使います。

0806: checkForExternalText: function() {
0807:   this._text = this.getText();
0808:   if (this.options.loadTextURL)
0809:     this.loadExternalText();
0810:   else
0811:     this.buildOptionList();
0812: },
0813:

806~813行目のcheckForExternalTextは、メニューのデフォルトの選択肢を設定する関数です。

807行目で、まずはもともとの要素の値をgetTextで取り出してデフォルトの選択肢とします。

808行目で、loadTextURLが設定されているときはデフォルトの選択肢をAjaxで読み込むためにloadExternalTextを呼びます。

Ajaxを使う必要がなければ、次の段階であるbuildOptionListに進んで、DOMの構築に入ります。

0814: loadExternalText: function() {
0815:   this.showLoadingText(this.options.loadingText);
0816:   var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
0817:   Object.extend(options, {
0818:     parameters: 'editorId=' + encodeURIComponent(this.element.id),
0819:     onComplete: Prototype.emptyFunction,
0820:     onSuccess: function(transport) {
0821:       this._text = transport.responseText.strip();
0822:       this.buildOptionList();
0823:     }.bind(this),
0824:     onFailure: this.onFailure
0825:   });
0826:   new Ajax.Request(this.options.loadTextURL, options);
0827: },
0828:

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にアクセスします。

0829: buildOptionList: function() {
0830:   this._form.removeClassName(this.options.loadingClassName);
0831:   this._collection = this._collection.map(function(entry) {
0832:     return 2 === entry.length ? entry: [entry, entry].flatten();
0833:   });
0834:   var marker = ('value' in this.options) ? this.options.value: this._text;
0835:   var textFound = this._collection.any(function(entry) {
0836:     return entry[0] == marker;
0837:   }.bind(this));
0838:   this._controls.editor.update('');
0839:   var option;
0840:   this._collection.each(function(entry, index) {
0841:     option = document.createElement('option');
0842:     option.value = entry[0];
0843:     option.selected = textFound ? entry[0] == marker: 0 == index;
0844:     option.appendChild(document.createTextNode(entry[1]));
0845:     this._controls.editor.appendChild(option);
0846:   }.bind(this));
0847:   this._controls.editor.disabled = false;
0848:   Field.scrollFreeActivate(this._controls.editor);
0849: }
0850: });
0851: 

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行目で、メニューをアクティブにします。

0852: //**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
0853: //**** This only  exists for a while,  in order to  let ****
0854: //**** users adapt to  the new API.  Read up on the new ****
0855: //**** API and convert your code to it ASAP!            ****
0856: 
0857: Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
0858: if (!options) return;
0859: function fallback(name, expr) {
0860:   if (name in options || expr === undefined) return;
0861:   options[name] = expr;
0862: };
0863: fallback('cancelControl', (options.cancelLink ? 'link': (options.cancelButton ? 'button':
0864:   options.cancelLink == options.cancelButton == false ? false: undefined)));
0865: fallback('okControl', (options.okLink ? 'link': (options.okButton ? 'button':
0866:   options.okLink == options.okButton == false ? false: undefined)));
0867: fallback('highlightColor', options.highlightcolor);
0868: fallback('highlightEndColor', options.highlightendcolor);
0869: };
0870:

852~870行目のdealWithDeprecatedOptionsは古いオプションを扱うための関数です。いずれ削除されるので、はやめにコードを新しいAPIに変更せよとのコメントがあります。

cancelLink,cancelButton,okLink,okButton,highlightcolor,highlightendcolorという古いオプションを、今のオプションに対応づけています。

0871: Object.extend(Ajax.InPlaceEditor, {
0872: DefaultOptions: {
0873:   ajaxOptions: { },
0874:   autoRows: 3,                                // Use when multi-line w/ rows == 1
0875:   cancelControl: 'link',                      // 'link'|'button'|false
0876:   cancelText: 'cancel',
0877:   clickToEditText: 'Click to edit',
0878:   externalControl: null,                      // id|elt
0879:   externalControlOnly: false,
0880:   fieldPostCreation: 'activate',              // 'activate'|'focus'|false
0881:   formClassName: 'inplaceeditor-form',
0882:   formId: null,                               // id|elt
0883:   highlightColor: '#ffff99',
0884:   highlightEndColor: '#ffffff',
0885:   hoverClassName: '',
0886:   htmlResponse: true,
0887:   loadingClassName: 'inplaceeditor-loading',
0888:   loadingText: 'Loading...',
0889:   okControl: 'button',                        // 'link'|'button'|false
0890:   okText: 'ok',
0891:   paramName: 'value',
0892:   rows: 1,                                    // If 1 and multi-line, uses autoRows
0893:   savingClassName: 'inplaceeditor-saving',
0894:   savingText: 'Saving...',
0895:   size: 0,
0896:   stripLoadedTextTags: false,
0897:   submitOnBlur: false,
0898:   textAfterControls: '',
0899:   textBeforeControls: '',
0900:   textBetweenControls: ''
0901: },

871~901行目は、オプションのデフォルト設定です。

0902: DefaultCallbacks: {
0903:   callback: function(form) {
0904:     return Form.serialize(form);
0905:   },
0906:   onComplete: function(transport, element) {
0907:     // For backward compatibility, this one is bound to the IPE, and passes
0908:     // the element directly.  It was too often customized, so we don't break it.
0909:     new Effect.Highlight(element, {
0910:       startcolor: this.options.highlightColor, keepBackgroundImage: true });
0911:   },
0912:   onEnterEditMode: null,
0913:   onEnterHover: function(ipe) {
0914:     ipe.element.style.backgroundColor = ipe.options.highlightColor;
0915:     if (ipe._effect)
0916:       ipe._effect.cancel();
0917:   },
0918:   onFailure: function(transport, ipe) {
0919:     alert('Error communication with the server: ' + transport.responseText.stripTags());
0920:   },
0921:   onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
0922:   onLeaveEditMode: null,
0923:   onLeaveHover: function(ipe) {
0924:     ipe._effect = new Effect.Highlight(ipe.element, {
0925:       startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
0926:       restorecolor: ipe._originalBackground, keepBackgroundImage: true
0927:     });
0928:   }
0929: },

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を使えばいいので、ずいぶん便利になりました。

0930: Listeners: {
0931:   click: 'enterEditMode',
0932:   keydown: 'checkForEscapeOrReturn',
0933:   mouseover: 'enterHover',
0934:   mouseout: 'leaveHover'
0935: }
0936: });
0937: 

930~937行目は、イベントリスナの設定です。

0938: Ajax.InPlaceCollectionEditor.DefaultOptions = {
0939:   loadingCollectionText: 'Loading options...'
0940: };
0941:

938~941行目は、InPlaceCollectionEditorのオプションにloadingCollectionTextを追加します。

以上でAjax.InPlaceCollectionEditorの解説は終わりです。

Form.Element.DelayedObserver

0942: // Delayed observer, like Form.Element.Observer, 
0943: // but waits for delay after last key input
0944: // Ideal for live-search fields
0945: 
0946: Form.Element.DelayedObserver = Class.create({
0947:   initialize: function(element, delay, callback) {
0948:     this.delay     = delay || 0.5;
0949:     this.element   = $(element);
0950:     this.callback  = callback;
0951:     this.timer     = null;
0952:     this.lastValue = $F(this.element); 
0953:     Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
0954:   },
0955:   delayedListener: function(event) {
0956:     if(this.lastValue == $F(this.element)) return;
0957:     if(this.timer) clearTimeout(this.timer);
0958:     this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
0959:     this.lastValue = $F(this.element);
0960:   },
0961:   onTimerEvent: function() {
0962:     this.timer = null;
0963:     this.callback(this.element, $F(this.element));
0964:   }
0965: });

942~965行目のForm.Element.DelayedObserverは、前回のAutocompleterで解説したような、フォームの入力エリアでのキー入力の小休止を見計らって、指定のフック(callback)を呼ぶ仕組みのクラスです。内部的には使われていません。動作について興味のあるかたは前回のAutocompleterの解説をご覧ください。

以上で、controls.jsの解説は終わりです。

おすすめ記事

記事・ニュース一覧