ブラウザーのWYSIWYG HTMLエディター。 パート3

この記事では、designModeプロパティとcontentEditableプロパティの使用方法と、単純なテキストエディターの作成例を使用した関連APIについて説明します。
designModeおよびcontentEditableおよび関連するAPIを使用する理論を探るシリーズの最初の記事の翻訳: パート1パート2

はじめに

記事の最初の部分では 、designModeプロパティとcontentEditableプロパティを使用してブラウザーエディターを作成する理論を詳しく調べました。 これらのDOMプロパティはHTML 5で標準化されており、ほとんどのブラウザーで多かれ少なかれサポートされています。 記事の第2部では、単純なクロスブラウザーテキストエディターの作成を考慮して、理論から実践に移ります。
ネットワーク上でエディターの完成バージョンを確認し、 そのコードダウンロードできます 。 リストには、説明を必要とするコードの最も興味深い部分のみが表示されます。残りのコードは退屈なので、考慮されません。 コードは3つのファイルに分かれています。

フレーム

基礎として、IFrame内で空白ページを使用します。
<iframe id="editorFrame" src="blank.html"></iframe> 
about:blankを使用して、本文に要素のない完全に空のページを取得できますが、本文の空の段落で作業を開始できるように、独自の「空の」ページを作成することをお勧めします。
 <title></title> <body><p></p></body> 
Mozillaは、他のすべてのブラウザーと同様に、空のpで入力を開始するため、望ましい方法です。 これが行われない場合、彼女はテキストを本文に直接入力します。 contentEditableプロパティを使用すると、フレームなしで実行できますが、Firefox 2はcontentEditableをサポートしないため、iFrameを使用することをお勧めします。 ( 翻訳者のメモ:FF2は、控えめに言っても、関連性がありません。だから、誰もiframeを必要としないと思います。

編集モードを設定する

関数(editor.jsにあります)を使用して、ページの読み込み時に編集モードを有効にします
 function createEditor() { var editFrame = document.getElementById("editorFrame"); editFrame.contentWindow.document.designMode="on"; } bindEvent(window, "load", createEditor); 
bindEventは、関数をイベント(util.jsで定義)にバインドする役割を果たします。 jQueryのようなフレームワークには、使用する可能性のある適切な機能があります次のステップは、最小限のフォーマット機能を備えたコントロールパネルを作成することです。

制御盤

シンプルなコントロールから始めましょう。「太字」ボタンは、選択したテキストのスタイルを太字に変更します。 ボタンはドキュメントの状態も表示する必要があります-テキスト内のエントリポイントが太字の場合、ボタンが強調表示されますロジックは、ドキュメントと選択ステータス要求に対する実際の操作をカプセル化するコマンドオブジェクトと、イベントを処理するコントローラーオブジェクトボタンの状態をクリックして同期します。 後で説明するように、異なるチームが同じロジックを共有する必要があるため、分離が必要です。 イベントは2つのポイントでトリガーされます。ユーザーがコントロールパネルのボタンを押すと、コントローラーはドキュメントで実行されるコマンドを呼び出し、ユーザーがドキュメント内でカーソルを移動すると、コントロールパネルのボタンの状態を変更します。

コマンドとコントローラーの実装

太字のコマンドは最初はAPIでサポートされているため、コマンドオブジェクトは単なる小さなラッパーです。
 function Command(command, editDoc) { this.execute = function() { editDoc.execCommand(command, false, null); }; this.queryState = function() { return editDoc.queryCommandState(command) }; } 
ラッパーが必要な理由 非標準のチームに標準のチームと同じインターフェースを持たせたいので。
ボタンは単なるスパンです。
 <span id="boldButton">Bold</span> 
スパンは、コントローラを介してコマンドオブジェクトに関連付けられます。
 function TogglCommandController(command, elem) { this.updateUI = function() { var state = command.queryState(); elem.className = state?"active":""; } bindEvent(elem, "click", function(evt) { command.execute(); updateToolbar(); }); } 
コントロールパネルのボタンをクリックしたときに編集ウィンドウによってフォーカスを維持する役割を担っていたコードは、リストから除外されました。 以下では、ToggleCommandController関数を呼び出して、ボタンの状態とテキストのスタイルを、それらの2つの状態を考慮して同期させます。 ボタンが押されると、コマンドが実行されます。 updateUIイベントが発生すると、テキストの状態に応じて、スパンは「アクティブな」クラスを取得するか、クラスを失います。 ボタンの外観を決定するCSSプロパティ:
 .toolbar span { border: outset; } .toolbar span.active { border: inset; } 
コンポーネントは次のように接続されます。
 var command = Command("Bold", editDoc); var elem = document.getElementById(îboldButton); var controller = new TogglCommandController(command, elem); updateListeners.push(controller); 
updateListenersコレクションには、コントロールパネルのコントローラーが含まれています。 updateToolbar関数はリストを反復処理し、すべてのコントロールが正確に最新になるように、各コントローラーのupdateUIメソッドを呼び出します。 ドキュメントを選択するたびにupdateToolbarが呼び出されるように、イベントを添付します。
 bindEvent(editDoc, "keyup", updateToolbar); bindEvent(editDoc, "mouseup", updateToolbar); 
上記のように、コマンドが実行されるとupdateToolbarが呼び出されます。 コマンドに関連付けられているボタンのみを更新するのではなく、各コマンドの実行後にコントロールパネル全体を更新するのはなぜですか? コマンド実行の結果として他のコントロールの状態も変化する可能性があるためです。 たとえば、右揃えコマンドを使用すると、左揃えボタンと中央ボタンの状態も変わります。 考えられるすべての依存関係を追跡する代わりに、コントロールパネル全体を更新する方が簡単です。これで、2つの状態を持つチーム用の基本的なインターフェイスができました。 結果のフレームワークを使用して、太字、斜体、JustifyLeft、JustifyRight、およびJustifyCenterチームが実装されます。

リンク

基本的なテキスト書式設定コマンドを実装した後、ドキュメントにリンクを追加する機能をユーザーに提供することにしました。 createLinkは希望どおりに動作しないため、リンク管理にはより複雑なロジックが必要です。 リンクを作成しますが、選択範囲がリンク内にあるかどうかに関する情報は返しません。 そして、コントロールパネルと選択のステータスを同期するためにこれが必要ですが、選択がリンク内にあるかどうかをどのように確認できますか? これを行うには、getContaining関数を作成します。これは、カーソルが配置されている要素からDOMツリーの上位に移動し、必要な型の親が見つかるまで続きます(この場合はリンク。要素が見つからない場合、関数は何も返しません)。 選択がaタグ内にある場合は、リンク内にあり、リンクのURLをユーザーに尋ねる方法も必要です。 クーラーエディターはこのリクエストに対して非標準のダイアログを作成しますが、タスクを簡素化するために、標準のwindow.prompt関数を使用します。 選択がリンク内にある場合、ユーザーが変更できるように現在のURLを表示します。 それ以外の場合は、http://プレフィックスを表示するだけです。
 function LinkCommand(editDoc) { var tagFilter = function(elem){ return elem.tagName=="A"; }; //(1) this.execute = function() { var a = getContaining(editWindow, tagFilter); //(2) var initialUrl = a ? a.href : "http://"; //(3) var url = window.prompt("Enter an URL:", initialUrl); if (url===null) return; //(4) if (url==="") { editDoc.execCommand("unlink", false, null); //(5) } else { editDoc.execCommand("createLink", false, url); //(6) } }; this.queryState = function() { return !!getContaining(editWindow, tagFilter); //(7) }; } 
関数のロジックは次のとおりです。
  1. この関数は、現在の要素が検索対象かどうかを確認します。 tagNameは、コードの大文字と小文字に関係なく、常に大文字で返されます。
  2. getContainingは、指定された名前を含む要素を検索します。 見つからない場合は、nullを返します。
  3. 親要素の中にリンクが見つかった場合、ダイアログにhref属性を追加します。 それ以外の場合、標準のhttp://になります。
  4. ユーザーが[キャンセル]をクリックすると、プロンプトはnullを返します。 この場合、コマンドの実行は終了します。
  5. ユーザーがURLを削除して[OK]をクリックした場合、ユーザーはリンクを削除したいと考えています。 これを行うには、標準のunlinkコマンドを使用します。
  6. ユーザーがURLを入力して[OK]をクリックすると、createLinkコマンドを使用してリンクが作成されます。 (リンクが既に存在する場合は、URLを新しいものに置き換えます)。
  7. 二重否定はブール型になります-要素が見つかった場合はtrue、そうでない場合はfalse。
  8. コントロールパネルインターフェイスは変更されていないため、LinkCommandと標準のToggleCommandControllerを組み合わせることができます。すべて同じexecuteメソッドとqueryStateメソッドです。

含む

getContaining関数(editlib.jsにあります)を見てみましょう。 この関数は、選択が特定のタイプの要素内にあるかどうかをチェックしますが、IE APIの動作は他のブラウザーのAPIとは少し異なるため、これは少し複雑です。 したがって、関数の2つの独立した実装と、どちらを使用するかを決定するメカニズムを作成する必要があります。これを行うには、getSelectionプロパティの可用性を決定します。 このように:
 var getContaining = (window.getSelection)?w3_getContaining:ie_getContaining; 
IEの関数の実装は、IEの選択APIのいくつかの機能を示しているため、より興味深いものです。
 function ie_getContaining(editWindow, filter) { var selection = editWindow.document.selection; if (selection.type=="Control") { //(1) // control selection var range = selection.createRange(); if (range.length==1) { var elem = range.item(0); //(3) } else { // multiple control selection return null; //(2) } } else { var range = selection.createRange(); //(4) var elem = range.parentElement(); } return getAncestor(elem, filter); } 
次のように機能します。
  1. 選択オブジェクトのタイプは、「Control」または「Text」です。 複数のオブジェクト(コントロール)を選択できます(つまり、ユーザーはctrl +クリックを使用して、隣接していない複数の画像を選択できます)。
  2. 選択した複数のオブジェクトの状況は処理しません。 この場合、コマンドをキャンセルするだけで、何も起こりません。
  3. 選択範囲にオブジェクトが1つある場合は、それを選択します。
  4. 選択がテキストの場合、これを使用してコンテナを取得します。
他のブラウザーで使用されるAPIは比較的単純です。
 function w3_getContaining(editWindow, filter) { var range = editWindow.getSelection().getRangeAt(0); //(1) var container = range.commonAncestorContainer; //(2) return getAncestor(container, filter); } 
次のように機能します。
  1. APIでは複数選択が可能ですが、ユーザーインターフェイスでは1つしか選択できないため、最初の唯一の範囲のみを考慮します。
  2. このメソッドは、現在の選択を含む要素を取得します。
getAncestor関数は単純です-探しているものが見つかるまで、または階層の最上部に到達するまで要素の階層を上に移動します。この場合、nullを返します。
 /* walks up the hierachy until an element with the tagName if found. Returns null if no element is found before BODY */ function getAncestor(elem, filter) { while (elem.tagName!="BODY") { if (filter(elem)) return elem; elem = elem.parentNode; } return null; } 

多くの値を取るコマンド

フォントやサイズの選択などの要素の編集では、ユーザーが値に対していくつかのオプションを選択できるため、若干異なるアプローチが必要です。 これを実装するためのインターフェイスでは、以前のように、ボタンの代わりにドロップダウンリストを使用しました。 さらに、CommandオブジェクトとControllerオブジェクトを書き換えて、バイナリ状態だけでなく多くの値を操作できるようにする必要があります。フォントを選択するためのHTMLコードを次に示します。
 <select id="fontSelector"> <option value="">Default</option> <option value="Courier">Courier</option> <option value="Verdana">Verdana</option> <option value="Georgia">Georgia</option> </select> 
コマンドオブジェクトは、標準のFontNameコマンドのアドオンであるため、依然としてシンプルです。
 function ValueCommand(command, editDoc) { this.execute = function(value) { editDoc.execCommand(command, false, value); }; this.queryValue = function() { return editDoc.queryCommandValue(command) }; } 
ValueCommandと前述のバイナリ状態コマンドの違いは、現在の値を文字列として返すqueryValueメソッドです。 ユーザーがドロップダウンリストから値を選択すると、コントローラーがコマンドを実行します。
 function ValueSelectorController(command, elem) { this.updateUI = function() { var value = command.queryValue(); elem.value = value; } bindEvent(elem, "change", function(evt) { editWindow.focus(); command.execute(elem.value); updateToolbar(); }); } 
ドロップダウンリストの値をコマンドの値として直接使用するため、コントローラーは非常に単純です。フォントサイズのドロップダウンリストは同じように機能します。組み込みのFontSizeコマンドを使用し、使用可能な値として1〜7のサイズを使用します。

カスタムチーム

これまで、標準の組み込みコマンドを使用してHTMLにすべての変更を加えてきました。 ただし、組み込みコマンドでは不可能なように、HTMLを変更する必要がある場合があります。 この場合、DOMとRange APIを使用します例として、入力ポイントにHTMLを追加するコマンドを作成します。 物事をシンプルに保つために、それは単に「Hello World」というテキストのスパンになります。 ただし、他のHTMLを貼り付けて入力する場合のアプローチは変わりません。コマンドは次のようになります。
 function HelloWorldCommand() { this.execute = function() { var elem = editWindow.document.createElement("SPAN"); elem.style.backgroundColor = "red"; elem.innerHTML = "Hello world!"; overwriteWithNode(elem); } this.queryState = function() { return false; } } 
現在の入力ポイントに要素を挿入する、overwriteWithNode関数のトークン。 (メソッドの名前は、空でない選択がある場合、その内容が上書きされることを示します)。 IEとDOM範囲標準をサポートするブラウザとの間のDOMの違いにより、方法の適用方法が異なります。まず、DOM範囲で動作するバージョンを考えてみましょう。
 function w3_overwriteWithNode(node) { var rng = editWindow.getSelection().getRangeAt(0); rng.deleteContents(); if (isTextNode(rng.startContainer)) { var refNode = rightPart(rng.startContainer, rng.startOffset) refNode.parentNode.insertBefore(node, refNode); } else { var refNode = rng.startContainer.childNodes[rng.startOffset]; rng.startContainer.insertBefore(node, refNode); } } 
range.deleteContentsは、その名前に従って、選択内容が縮退していない場合、その内容を削除します。 (選択が縮退している場合は、何もしません)。 DOM Rangeオブジェクトには、DOMでエントリポイントを定義できるプロパティがあります。startContainerはエントリポイントを含むノードで、startOffsetは親ノードのエントリポイントの位置を示す番号です。 たとえば、startContainerが要素でstartOffsetが3の場合、エントリポイントは要素の3番目と4番目の子の間にあります。 startContainerがテキストノードの場合、startOffsetは親の先頭からの文字のオフセットを意味します。 たとえば、startOffsetが3の場合、エントリポイントが3番目と4番目の文字の間にあることを意味します。

同様にendContainerとendOffsetは、選択の終了を示します。 選択が空(縮退)の場合、startContainerおよびstartOffsetと同じ値になります。


エントリポイントがテキストノード内にある場合、2つのノードにデータを挿入できるように2つに分割する必要があります。 rightPartはまさにそれを行う関数です-テキストノードを2つのノードに分割し、その右側の部分を返します。 その後、insertBeforeを使用して、目的のポイントに新しいノードを挿入できますIEのバージョンは少し複雑です。 IEでは、RangeオブジェクトはDOM内の入力ポイントの位置に関する情報へのアクセスを提供しません。 もう1つの問題は、pasteHTMLメソッドを使用してのみデータを挿入できることです。このメソッドは、DOMノードのツリーではなく、文字列としてHTMLを受け取ります。 一般的に、IE Range APIはDOM APIから完全に分離されています! ただし、DOM APIとIE範囲APIを共有できるトリックがあります。pasteHTMLを使用して、マーカー要素を一意のIDで挿入し、DOM内の目的のエントリポイントを見つけます。
 function ie_overwriteWithNode(node) { var range = editWindow.document.selection.createRange(); var marker = writeMarkerNode(range); marker.appendChild(node); marker.removeNode(); // removes node but not children } // writes a marker node on a range and returns the node. function writeMarkerNode(range) { var id = editWindow.document.uniqueID; var html = "<span id='" + id + "'></span>"; range.pasteHTML(html); var node = editWindow.document.getElementById(id); return node; } 
マーカーノードは、終了後に削除されることに注意してください。 これは、HTMLコードが乱雑にならないようにするために必要ですが、選択ポイントに任意のHTMLを挿入するコマンドがあります。 コントロールパネルのボタンとToggleCommandController関数を使用して、このアクションをユーザーインターフェイスに関連付けました。

結論

この記事では、HTMLエディターを作成するための単純なフレームワークに注目しました。 このコードは、より複雑なエディターを開発するためのテンプレートとして使用できます。

Source: https://habr.com/ru/post/J107907/


All Articles