リッチテキストエディターの開発:問題と解決策

ソフトウェアの一種としてのテキストエディタは、恐竜よりも少し遅れて登場し、おそらくあなたの人生で出会った最初のソフトウェアである可能性が高く、MS-DOSエディタを見つけた人もいるかもしれません。



ただし、ほとんどのソフトウェアのブラウザーへの移行に伴い、対応するリッチテキストエディターも関連しており、開発には多くの問題のある場所があります。 何らかの理由で独自のエディターを作成することに決めた場合は、もう一度考えてください。これは必要ではないとの意見があります。







より情報に基づいた決定を下せるようにYegor YakovishenSetka Editorの作成プロセスで得たすべての経験を要約し、直面しなければならない問題とそれらを解決するために何ができるかについて話しました。



免責事項: この記事は、2017年6月のFrontend Conf 2017カンファレンスでのYegorのプレゼンテーションに基づいて書かれています。 それ以降、特定のAPIのブラウザサポートが変更された可能性があります。









体験について



私が勤務するSetka社は、コンテンツマーケティング、オンラインメディアの編集、および出版物を立ち上げるブランドのコンテンツチームに関わるチームでプロセスを最適化するツールを開発しています。 当社の製品の1つはSetka Editorです。これは、ユーザーを引き付けるコンテンツを品質を損なうことなく、非常に迅速に作成できるクールなビジュアルエディターです。 エディタの機能は次のとおりです。





このエディターはWordPressで使用でき(このための特別なプラグインをリリースしました)、他のCMSに統合できます。



Setkaは、The Village、Furfur、Wonderzineなどの人気エディションを組み合わせたLook At Media出版社の壁の中で生まれました。 Setka Editorの背景と開発履歴については、Setkaの企業ブログの記事をご覧ください 。 以下は、エディターで作成された投稿のレイアウトの例です。







それは、毎日のニュース、長い読み物、特別なプロジェクトの両方になります。



ネタバレの下には、さらに多くの例があります。




































私たちの出版物のみでSetka Editorで作成された投稿は、月に2,000万を超える月間ビューを生成します。これは、私たちが直面しているタスクの規模を理解するためです。



さあ、行こう!





背景



ソフトウェアの一種であるテキストエディタは、かなり前に登場しました。 おそらく、多くの人にとって、これは一般に彼らが人生で出会った最初のソフトウェアでした。 おそらく、誰かがMS-DOS Editorを見つけました







明らかに、誰もがさまざまなバージョンでメモ帳を見ました。







Microsoft Wordはすでに多くのバージョンよりも長持ちしています。







さらに、非常に複雑な雑誌のレイアウト用に、 Adobe InDesignなどの特殊なプログラムがあります。







しかし、これはデスクトップソフトウェアとしてオペレーティングシステムで動作するすべての古典的なソフトウェアです。



それまでの間、状況はブラウザーによって異なります。 まず、通常のテキストエリアがあります。 これはテキストを書くことができるフィールドであり、どこでも絶対に動作します-優れたクロスブラウザー互換性! しかし、問題があります。このテキストエリアは単なるテキスト入力フィールドであるため、このテキストエリアではスタイリングや、さらに難しいレイアウトを行うことはできません。 テキスト入力よりも少しだけ複雑です。



リッチテキストエディターと呼ばれる多くの製品があります。 おそらく、さまざまなサイトで何度も遭遇しているでしょう。 ここにいくつかの古典的な代表者がいます。



Tinymce





Ckeditor





Froalaエディター





そして、開発レベルの異なる数十のプロジェクトがあり、そのほとんどにいわゆる「出生問題」があります。



一般的なリッチテキストエディターの問題



WYSIWYG



ほとんどすべてのエディターでは、 WYSIWYG (見ているものは得るもの)の重要な原則は尊重されていません。 これは、エディターで表示されるレイアウトが、訪問者がサイトで表示するものと必ずしも一致しないことを意味します。 エディターでは、固定サイズの小さなウィンドウで作業しますが、サイトのページではすべてがまったく異なることが判明しました。 独自のスタイル、コンテンツ領域の異なる幅、追加のブロックなどを備えた特定のサイトテンプレートがあります。 また、ユーザーがモバイルデバイスからサイトにアクセスすると、まったく異なるルールが既にそこに適用されます。



機能が少なすぎるか多すぎます。



このようなエディターの機能は、原則として、非常に少ない(太字、斜体、テキストの配置)、または多すぎる-理解できない5行のボタンです。



既存のサイトデザインで友達を作るのは難しい



デザインはアートディレクターによって既に承認されており、サイトは稼働しています。 そして今、あなたはそのデザインでエディターの友達を作る必要があります。そうすれば、会社のコーポレートアイデンティティの枠組みの中で美しいレイアウトを作ることができます。 これは非常に困難な場合があります。



ブラウザー間の互換性



多くのエディターには、ブラウザー間での互換性(インターフェースと生成されるレイアウトの両方)に問題があります。 これは特に古いIEとSafariに当てはまりますが、Firefoxは時には驚きももたらします。



一般的なツール



ほとんどの場合、リッチテキストエディターは、視覚的なブロックをHTMLおよびCSSコードに変換するための単なるツールであり、さまざまな理由でコードを手動で記述したくない、または記述できないユーザー向けに設計されています。 問題を解決するためのより高度なツールが必要な場合、多くの場合、アシスタントのWYSIWYGエディターが障害になります。



他にも多くの問題があります。例えば:





などなど。 本当に多くの問題と質問があります。



エディターを作ることにしました



それで、Xが来た瞬間に、何らかの理由で独自のエディターを作成することにしました。



良い試みですが、やってはいけません!



インターネットは、これを行うべきではないと言う投稿でいっぱいです。 ほとんどの場合、多くの制限により、本当に優れた製品を作成できなくなります。







特に、これはStackOverflowのスクリーンショットです。CKEditorの開発者の1人(古くて有名な編集者の1人)は、10年もやっていますが、まだ数千の問題があり、残念ながら良い結果が得られないと書いています-彼らが悪い開発者だからではなく、彼らが悪いブラウザだからです。



それにもかかわらず、あなたはまだすべての疑念を拒否し、あなた自身の編集者を作りたいと思っています。 どの重要な質問に対処しなければなりませんか? 以下を理解する必要があります。



  1. Webページのコンテンツを編集する方法
  2. このコンテンツの保存方法。
  3. スタイル(CSS)をどうするか。
  4. エディターの機能を拡張する方法。


今日はこれについてお話します。 順番に始めましょう。



コンテンツを編集するには?



歴史的に、ブラウザはこのために次の機能を提供してきました。



デザインモード



ドキュメントオブジェクトにはこのようなプロパティがあり、非常に長い間存在します。おそらく、90年代後半にInternet Explorerの最初のバージョンで実装されました。



document.designMode = "on"
      
      





このプロパティがオンモードに切り替わると、絶対にページ全体になり、本文のすべてのコンテンツが編集可能になります。 任意の場所にカーソルを置き、テキストを編集できます。



Googleを使用すると、10〜15年前にAntichatなどのフォーラムで、Microsoft、Googleサイトなどのページを編集する方法についての熱狂的なコメントを見ることができます。



この方法は不完全です。 主な問題は、ほとんどの場合、ページ全体を編集する必要はなく、特定のブロックのみを編集する必要があることです。 おそらく、designModeが適用される唯一のケースは、編集可能領域がiframe内にあり、このブロック全体を編集可能にする場合です。 しかし、そのような状況はそれほど多くありません。



コンテンツ編集可能



これは、編集可能なブロックを作成するためのメインのブラウザーAPIです。 この属性をtrueに設定すると、ブロック内のすべてが編集可能になります。 これが、ほとんどのビジュアルエディターの作成方法であり、私たちのエディターも例外ではありません。



 <div contenteditable="true"></div>
      
      





問題は、各ブラウザがこのブロックでさまざまな方法でユーザーアクションを実装することです。 最も単純な例:「Hello、world」というテキストのブロックがあり、contenteditable =「true」をオンにすると、編集可能になります。 次に、単語「Hello」の後のカンマの後ろにカーソルを置き、Enterボタンを押します。



その後、新しい<div>がブロック内のChromeとSafariに表示されます。その中には、複雑なスペースと残りの単語「world」があります。



 <div contenteditable="true"> Hello, <div>nbsp;world</div> </div>
      
      





そして、Firefoxでは、改行<br>。



 <div contenteditable=“true”> Hello, <br> world </div>
      
      





したがって、各ブラウザの動作は異なるため、状況を制御することはできません。



Document.execCommand



これは、ブラウザコマンドを選択したテキストのセクションに適用できる特別な方法です。 そのようなコマンドには、太字、fontSize、formatTextなどの固定数があります。



Js



 document.execCommand('bold'); document.execCommand('fontSize', false, 7);
      
      





HTML



 <b>Hello</b>, world <font size=“7”>Hello</font>, world
      
      





このAPIも非常に長い間存在し、非常に不安定です。 同じ例で、「Hello」という単語を強調表示して、コマンドexecCommand( 'bold')を実行したとします。 テキストは<b>タグでラップされます。 どのような潜在的な問題がありますか? まず、<b>ではなく<strong>(SEO担当者から尋ねられた)が必要な場合があります。 または、この<b>タグにさらにクラスを追加したいので、追加のアクションを実行する必要があります。



さて、<b>まだそれほど怖いわけではありませんが、もし望むなら、フォントを大きな見出しにしますか? fontSize browserコマンドを実行し、値7を渡します。 <font>タグが挿入されます。これは、2000年代の初めから7番目のサイズのサイトでは見られていません。 はい、視覚的には必要なもののように見えますが、コード、セマンティクス、SEO、およびその後のサポートの観点からは、これは悪夢です。



要約:



  • 異なるブラウザは異なるHTMLを生成します。
  • execCommandはどこでも機能するわけではなく、常に機能するわけではありません(また、さまざまな方法で機能します)。
  • 何が起こっているかを制御することはできません。




その結果、標準APIのみを使用すると、エディターは劣っています。 セマンティックタグを使用して正しいスタイルを適用することはできません。後で解析する方法や、SEOにどのように影響するかはわかりません。



教科書の記事「 なぜContentEditableがひどいのか 」があり、開発者はなぜcontenteditableが悪いのか、どうすればよいのかを説明しています。



代替方法



これらの問題を回避するには、 canvasを使用してエディターを作成できますが、そのような例は存在しますが、本質的には、テキスト、カーソル、選択などのレンダリング全体を完全に再発明する必要があります。



実際、これの多くを行う必要があることがわかります。 ただし、Canvasの場合、テキスト領域をまるで写真のように操作します。 これは非常に複雑であり、多くの問題が発生する可能性が高いため、この作業はすべて恩恵がありません。



2番目の方法は、ほとんどすべての現代のエディター(特に私たちと一緒に)で実際に最も頻繁に使用されますが、コンテンツ編集可能領域と 、この領域で行われるアクションに基づくドキュメント状態のストレージの組み合わせです。



コンテンツ編集可能な領域があるとしましょう。 その古典的な使用法では、カーソルを配置し、テキストを記述し、コンテンツを変更します。そして、先ほど述べたように、それは予想外に変更します。 この場合、入力に何かを書き込むと、必要に応じて状態が最初に変化し、次に状態の変化に基づいて入力が再レンダリングされます。 基本的に、これはReactが制御入力を呼び出すアプローチです。 これにより、高速入力でパフォーマンスの問題が発生する可能性があります。 次に、ハイブリッドオプションを使用する必要があります。DOMにテキスト文字をすぐに入力し、より複雑なアクションを最初に自分で処理します。



テキストフィールドにカーソルを置き、Enterキーを押したとします。 エディターは、改行を挿入する必要があることを理解しています。または、その時点でテキストではなくオブジェクトを選択した場合は、他のアクションを実行する必要があります。 したがって、エディターで何が起こっているか:必要なアクションの実行、不要なアクションのキャンセルなど、すべてを完全に制御できます。



一般的なスキーム:





状態を保存できます:





したがって、これはすべて状態に保存でき、この状態を変更するアプリケーションとしてエディターで動作します。 たとえば、Reactで作成されたFacebookのDraft.jsエディターは次のとおりです。







スクリーンショットは、エディターの状態を保存する方法を示しています。 テキストは2行で構成され、右側にJSオブジェクトとして表示されます。 これらにはテキスト属性があり、それぞれに固有のキーがあり、様式化するかどうかなどができます。



しかし、この問題で最も遠いのは、kixエンジンを備えたGoogleドキュメントです。 彼らには完全に非標準的なタスクがありました:エディターを開発するだけでなく、同時マルチユーザー作業を実装することも。 これは、変更、複数のカーソル、および一般にエディター開発の問題の範囲外にある他のすべての複雑なものの同期を意味します。







これを行うために、彼らは特別なIframeのページで発生するすべてのイベントを追跡します。これは通常、画面のはるか遠くにあります。 スクリーンショットは、このiframeの最上部の座標が–10000pxで、高さが1pxのみであることを示しています。 ページで発生するイベントをインターセプトするためにのみ使用されます。 ご覧のとおり、このiframeにはコンテンツさえなく、 contenteditable = "true"の空のコンテナーのみがあります。 つまり、それはもっぱら公式のツールです。



すべてのタスクを解決するために、Google Docsの開発者は実際、エディターでテキストをレンダリングするためにシステム全体を再発明する必要がありました。







上記のスクリーンショットはテキストの選択を示しており、これはオペレーティングシステムの標準選択ではなく、kix-selection-overlayと呼ばれる特別なdivでもあることがわかります。 背景色、不透明度、サイズ、座標があります。 つまり、これはコンテンツの特別なDOMラッパーであり、選択です。







それだけでなく、カーソルも持っています-それは1-2px幅のdivで、高さが1行で、特定のCSSプロパティが適用され、0.5秒ごとに点滅します。 したがって、5人が編集する場合、ページは同時に5つのカーソルを持つことができます。



コンテンツを保存する方法は?



ここで、コンテンツの状態を状態に保存する方法の質問に移ります。 従来、Webページに表示されるコンテンツはHTMLとして保存されます。



これには利点があります:





しかし、欠点もあります:





独自の形式



何らかの方法で、ドキュメントの内容を保存するための独自の便利なフォーマットが必要になる可能性があります。



エンティティの分解



最初に、エディターで操作しているエンティティを分解する必要があります。 ほとんどの場合、テキストの古典的な要素があります。





コードエディタには独自のエンティティがあり、グラフィックエディタには異なるエンティティがあります。 作業しているコンテンツを理解する必要があります。



データ構造



さらに、これらのエンティティは、タイプと属性を備えた構造の形式で表現する必要があり、それを使用して作業することができます。 テキスト要素の構造の抽象的な例を次に示します。



 1: { id: 1, type: "Paragraph", content: "this is a text", style: [ { range: [0,3], format: ["bold"] } ] }
      
      





この例は、Paragraph要素があり、特定のコンテンツがあり、その最初の4文字が太字でフォーマットされていることを示しています。 このような構造は、たとえば次のような任意の表現にレンダリングできます。





重要なのは、このような構造は、エディターで1つの方法で(たとえば、追加の編集要素、ボタンなどを使用して)、外部で-まったく異なる方法で表示できることです。 したがって、あなたは再びあなたのコンテンツで起こっていることの本格的なホストになります。



期待される質問を尋ねることができます。「聞いて、長年HTMLに保存されたコンテンツがあります。自分のCMSがあります。 すべてを忘れて、独自のフォーマットを考え出し、それを使用するためにすべてのコードを書き直しますか? 明らかに、私はこれをやりたくありません。なぜなら、ビジネスの観点からの利点は理解できないからです」



私たちのサイトにエディターを実装したときも、同じ問題がありました。そこでは、レガシーコードの大部分と何千もの投稿が作成されました。



回避策:



  • 既にある形式でデータベースにデータを保存します。 ほとんどの場合、これはHTMLです。
  • エディターにロードするとき、それらを解析し、独自の形式で作業します。 また、データをデータベースに保存するときは、シリアル化してHTMLに戻し、この形式で保存します。
  • これは、確立されたエコシステムで作業する必要がある場合に便利です(たとえば、WordPressなどのプラグインを使用したCMS)。




私たちは今まさにそれをしています。 エディターはさまざまなプラットフォームで動作し、独自のデータストレージ形式を課す権利はありません。 持っているもので作業する必要があります。



ユーザー入力の使用方法



ブラウザでのユーザー入力の種類:





エディターは、これらすべてのインプットメソッドをサポートする必要はありません。 ほとんどの場合、長いテキストを音声で入力する人はほとんどいませんが、それでも可能なオプションを考慮する必要があります。



ブラウザAPI



残念ながら、コンテンツ編集可能領域には、変更が発生したときにサブスクライブして正確に知ることができる単一の変更イベントがないため、ブラウザーはさまざまなAPIの「動物園」全体を提供します。 少なくとも何らかの形で作業できるいくつかの重要なAPIがあります。 ビジュアルエディターの作成に関するほぼすべての投稿で、私の話も例外ではありませんが、ブラウザーAPIの状況が悪いことを強調しています。



選択APIを使用すると、ページ上のテキストの選択を操作できます。



selectstartselectionchangeの 2つの便利なイベントが含まれています。 Selectstartは、ページ上のテキスト領域の選択を開始するとトリガーされ、selectionchangeは、選択領域が変更されるたびにトリガーされます。 さらに、これはカーソルで選択が変更されたときだけでなく、Shift +矢印またはCtrl-Aを押してページ上のすべてのコンテンツを選択したときにも機能します。



Window.getSelection() -このメソッドを使用すると、ページ上のテキストの現在の選択に関する情報を持つオブジェクトを取得できます。 このオブジェクトには、特にanchorNodeとfocusNodeの有用なプロパティが含まれています。 AnchorNodeは選択が開始されたノードであり、focusNodeは選択が終了したノードです。 これらのノードはそれぞれ独自のオフセットを持つことができます。 このノードで選択された文字の数。



例として、FrontendConfサイトページのスクリーンショットを検討してください。 「デザイン」という単語で始まり、「モバイルサイト」というフレーズで終わるテキストを選択し、現在の選択を考慮して、強調表示されているものを確認しました。 anchorNodeとfocusNodeは両方ともテキストノードであることがわかります。anchorOffset= 11(「デザイン」という単語は11番目の位置から始まります)、およびfocusOffset = 15です。 これにより、ページ上のどのテキストが現在強調表示されているかを理解できます。







Selection APIは十分にサポートされており、かなり安定しています。







次のAPIは、 クリップボードClipboard APIを使用しています 。 彼にはもっと多くの問題があり、どこにでも星やコメントがあります。







Clipboard APIには、サブスクライブできるイベントがいくつかあります:コンテンツのコピー、切り取り、貼り付け、およびその直前の処理:beforecopy、beforecut、beforepaste。



それらのいくつかはexecCommandを使用して独立して呼び出すことができますが、セキュリティ上の理由から、クリップボードでの多くのアクションはブロックされます。



たとえば、ユーザー自身がコピーを開始し、<Ctrl-C>または<Edit-Copy>を押すと、コピーイベントが発生し、この時点で開発者介入して特定のアクションを実行できます。





しかし同時に、ユーザー自身が望んでいない場合、クリップボードで操作を開始することはできません。



この理由は明らかです。たとえば、クリップボード上の人はパスワードまたはクレジットカード番号を持っています。 ブラウザアプリケーションがこのデータを独立して読み取り、どこかに送信できる場合、セキュリティはありません。 したがって、ユーザーがイベントを開始しない場合、自分でイベントをトリガーすることはできません。



クリップボードAPIのサポートは良好ですが、特定の制限があります。 テキストだけでなく、エディター内の複雑なオブジェクトのコピーを実装するとします。 ただし、ページ上にテキストが選択されていない場合、 コピーイベントは発生しません。 これを行うには、Trelloアルゴリズムを使用できます。Trelloアルゴリズムは、 Stack Overflowについて公然と記述しています。 ユーザーにとって、これは次のように機能します。ボード内のカードの上にカーソルを置き、Ctrl-Cを押してからカーソルを別の場所に移動し、Ctrl-Vを押すと、このカードが正しい場所に挿入されます。







次のように実装されます。





次に、ユーザーがカーソルを別の場所に移動して「貼り付け」をクリックすると、挿入イベントがインターセプトされ、JSを使用して、カーソルが置かれているリストにカードが挿入されます。



自宅では、同様の方法で複雑なオブジェクトのコピーを実装しました。



作曲イベント



APIイベントには別のタイプがあり、あまり頻繁に発生することはありません。これらのイベントは、主にいくつかの複合アクションによって挿入される発音区別記号と象形文字に関連しているためです。 また、これらのイベントは、音声などの代替入力方法に関連付けられています。



動作の簡単な説明:à-発音区別記号を挿入する場合は、macOSで<Alt + '>を押してから<a>を押すと、単一のà文字になります。



これにより、いくつかのイベントが同時に発生します。





音声入力も同じように機能します。テキストをポップし、ブラウザがそれをデコードし、文字列に変換して、compositionupdateが発生します。 , compositionend, .



Undo / Redo



— undo redo, .. . , , , state, , DOM, .. state .



<Command+Z> / <Ctrl+Z> Undo/Redo, , , state. : - , , . . redux-undo .



CSS?



, . , :





, :





WordPress CSS



WordPress. — , UI- ( , , ..). — UI-. ( ). DOM- , iframe, , .







CSS WordPress: H2, id=”poststuff”, . , , id, , .



 #poststuff h2 { font-size: 14px; padding: 8px 12px; margin: 0; line-height: 1.4; }
      
      





, :





, .



3 CSS:



  1. CSS reset + – . , .
  2. iframe – «» , .
  3. Shadow DOM Custom Elements , .


CSS reset +



, CSS- WordPress, . , — .



WordPress CSS



 #poststuff h2 { font-size: 14px; }
      
      





CSS



 #my-editor h2 { font-size: 28px; }
      
      





- !important , ( , ).



, 100% – , .



iframe



, iframe , , . . . , , iframe, . , — , .



Shadow DOM + Custom Elements



, , , – Shadow DOM Custom Elements. -, shadow-root .



 <my-editor> #shadow-root </my-editor>
      
      





, , Shadow DOM .







Custom Elements .









, — , . , , , , . , , . – , .



(preview), , , mobile , ( ). 2 iframe – , mobile. CSS-, . WYSIWYG. - , iframe , media queries. , , . Preview HTML iframe, . iframe SRC, .



?



, , . , : . , , API . , , .



API :







immutable state, , , , Redux, – actions reducers. :





, . , :



  • oembed HTML;
  • ;
  • ;
  • HTML CSS-;
  • - ();
  • ;
  • -;
  • API.




rich text editing



-, Web Components , , . API , W3C Input Events , input beforeinput , , , :





:





, API, -. , , , .



結論



?





?





CSS?







:





:





:



» E-mail: yakovishen@setka.io

» Telegram: t.me/yaplusplus

» Facebook: facebook.com/yaplusplus

» Twitter: twitter.com/yaplusplus

» Setka: https://setka.io



, , , , . – , . , ++ .



All Articles