script.aculo.usを読み解く

第3回 controls.js(後編) InPlaceEditor

この記事を読むのに必要な時間:およそ 20 分

今回は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行目で,イベントリスナを全て解除します。

著者プロフィール

源馬照明(げんまてるあき)

名古屋大学大学院多元数理科学研究科1年。学部生のときにSchemeの素晴らしさを知ったのをきっかけに,関数型言語の世界へ。JavaScriptに,ブラウザからすぐに試せる関数型言語としての魅力と将来性を感じている。

ブログ:Gemmaの日記

コメント

コメントの記入