リッチテキストエディタ開発者向けガイドライン

リッチテキストエディタは、GitLabアプリケーションでGitLab Flavored MarkdownのWYSIWYG編集体験を提供するUIコンポーネントです。また、静的サイトジェネレータのような他のエンジンをターゲットにしたMarkdownに特化したエディタを実装するための基盤としても機能します。

リッチテキストエディタを構築するためにTiptap 2.0と ProseMirrorを使っています。これらのフレームワークは、ネイティブのcontenteditable Webテクノロジーの上に抽象化レベルを提供します。

使用ガイド

リッチテキストエディタを機能に含めるには、以下の手順に従ってください。

  1. リッチテキストエディタのコンポーネントをインクルードします。
  2. Markdownの設定と取得
  3. 変更を確認します。

リッチテキストエディタ・コンポーネントを含める

ContentEditor Vueコンポーネントをインポートします。ContentEditorは依存関係が大きいので、キャッシュを利用するために非同期名前付きインポートを使用することをお勧めします。

<script>
export default {
  components: {
    ContentEditor: () =>
      import(
        /* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
      ),
  },
  // rest of the component definition
}
</script>

リッチテキストエディタには2つのプロパティが必要です:

この2つのプロパティの実際の使用例についてはWikiForm.vue コンポーネントを参照してください。

Markdownの設定と取得

ContentEditor Vueコンポーネントは、Vueデータ・バインディング・フロー(v-model)を実装していません。データバインディングは、ユーザーがコンポーネントとやりとりするたびにこれらのオペレーションをトリガします。

その代わりに、initialized イベントをリッスンしてContentEditor クラスのインスタンスを取得する必要があります:

<script>
import { createAlert } from '~/alert';
import { __ } from '~/locale';

export default {
  methods: {
    async loadInitialContent(contentEditor) {
      this.contentEditor = contentEditor;

      try {
        await this.contentEditor.setSerializedContent(this.content);
      } catch (e) {
        createAlert({ message: __('Could not load initial document') });
      }
    },
    submitChanges() {
      const markdown = this.contentEditor.getSerializedContent();
    },
  },
};
</script>
<template>
  <content-editor
    :render-markdown="renderMarkdown"
    :uploads-path="pageInfo.uploadsPath"
    @initialized="loadInitialContent"
  />
</template>

変更をリッスン

リッチテキストエディタでも変更に反応することができます。変更に反応することで、ドキュメントが空かダーティかを知ることができます。この目的のために@change イベントハンドラを使用してください。

<script>
export default {
  data() {
    return {
      empty: false,
    };
  },
  methods: {
    handleContentEditorChange({ empty }) {
      this.empty = empty;
    }
  },
};
</script>
<template>
  <div>
    <content-editor
      :render-markdown="renderMarkdown"
      :uploads-path="pageInfo.uploadsPath"
      @initialized="loadInitialContent"
      @change="handleContentEditorChange"
    />
    <gl-button :disabled="empty" @click="submitChanges">
      {{ __('Submit changes') }}
    </gl-button>
  </div>
</template>

実装ガイド

リッチテキストエディタは主に3つのレイヤーで構成されています:

  • ツールバーやテーブル構造エディタのような編集ツールUI。これらはエディタの状態を表示し、コマンドをディスパッチすることで状態を変化させます。
  • Tiptap Editorオブジェクトはエディタの状態を管理し、編集ツールUIによって実行されるコマンドとしてビジネスロジックを公開します。
  • MarkdownシリアライザーはMarkdownソース文字列をProseMirrorドキュメントに変換し、その逆も同様です。

編集ツールのUI

編集ツールUIは、エディタの状態を表示し、それを変更するコマンドをディスパッチするVueコンポーネントです。これらは~/content_editor/components ディレクトリにあります。例えば、太字ツールバーボタンは、ユーザーが太字テキストを選択するとアクティブティになり、エディタの状態を表示します。このボタンはまた、テキストを太字にフォーマットするtoggleBold コマンドをディスパッチします:

sequenceDiagram participant A as Editing tools UI participant B as Tiptap object A->>B: queries state/dispatches commands B--)A: notifies state changes

ノードビュー

テーブルや画像など、いくつかのコンテンツタイプにインライン編集ツールを提供するために、ノードビューを実装しています。ノードビューは、コンテンツタイプのプレゼンテーションをそのモデルから分離することを可能にします。プレゼンテーションレイヤーに Vue コンポーネントを使用することで、リッチテキストエディタでの洗練された編集体験が可能になります。ノードビューは~/content_editor/components/wrappers にあります。

ディスパッチコマンド

Vue コンポーネントに Tiptap Editor オブジェクトを注入して、コマンドをディスパッチすることができます。

note
エディタの状態を変更するロジックをVueコンポーネントに実装しないでください。このロジックはコマンドにカプセル化し、コンポーネントのメソッドからコマンドをディスパッチしてください。
<script>
export default {
  inject: ['tiptapEditor'],
  methods: {
    execute() {
      //Incorrect
      const { state, view } = this.tiptapEditor.state;
      const { tr, schema } = state;
      tr.addMark(state.selection.from, state.selection.to, null, null, schema.mark('bold'));

      // Correct
      this.tiptapEditor.chain().toggleBold().focus().run();
    },
  }
};
</script>
<template>

エディタの状態のクエリ

EditorStateObserver レンダーレスコンポーネントを使用して、ドキュメントや選択範囲が変更されたときなど、エディタの状態の変化に反応します。以下のイベントをリッスンできます:

  • docUpdate
  • selectionUpdate
  • transaction
  • focus
  • blur
  • error.

これらのイベントの詳細については、Tiptapイベントガイドをご覧ください。

<script>
// Parts of the code has been hidden for efficiency
import EditorStateObserver from './editor_state_observer.vue';

export default {
  components: {
    EditorStateObserver,
  },
  data() {
    return {
      error: null,
    };
  },
  methods: {
    displayError({ message }) {
      this.error = message;
    },
    dismissError() {
      this.error = null;
    },
  },
};
</script>
<template>
  <editor-state-observer @error="displayError">
    <gl-alert v-if="error" class="gl-mb-6" variant="danger" @dismiss="dismissError">
      {{ error }}
    </gl-alert>
  </editor-state-observer>
</template>

Tiptapエディタオブジェクト

Tiptapエディタクラスはエディタの状態を管理し、リッチテキストエディタを動かすすべてのビジネスロジックをカプセル化します。リッチテキストエディタはこのクラスの新しいインスタンスを構築し、GitLab Flavored Markdownをサポートするために必要なすべての拡張機能を提供します。

新しい拡張機能の実装

拡張機能はリッチテキストエディタの構成要素です。Tiptapガイドを読むことで、新しい拡張機能を実装する方法を学ぶことができます。ゼロから新しい拡張機能を実装する前に、組み込みノードと マークのリストを確認することをお勧めします。

リッチテキストエディタの拡張機能を~/content_editor/extensions ディレクトリに保存します。Tiptap組み込みの拡張機能を使用する場合、このディレクトリ内のES6モジュールでラップしてください:

export { Bold as default } from '@tiptap/extension-bold';

Extensionの動作をカスタマイズするには、extend メソッドを使用してください:

import { HardBreak } from '@tiptap/extension-hard-break';

export default HardBreak.extend({
  addKeyboardShortcuts() {
    return {
      'Shift-Enter': () => this.editor.commands.setHardBreak(),
    };
  },
});

エクステンションの登録

新しい拡張モジュールを~/content_editor/services/create_content_editor.js に登録します。 拡張モジュールをインポートし、builtInContentEditorExtensions 配列に追加します:

import Emoji from '../extensions/emoji';

const builtInContentEditorExtensions = [
  Code,
  CodeBlockHighlight,
  Document,
  Dropcursor,
  Emoji,
  // Other extensions
]

Markdownシリアライザー

MarkdownシリアライザはMarkdown文字列をProseMirrorドキュメントに変換します。

デシリアライズ

デシリアライズはMarkdownをProseMirrorドキュメントに変換するプロセスです。最初にMarkdown APIエンドポイントを使用してMarkdownをHTMLとしてレンダリングすることで、ProseMirrorのHTMLパースとシリアライズの機能を利用します:

sequenceDiagram participant A as rich text editor participant E as Tiptap object participant B as Markdown serializer participant C as Markdown API participant D as ProseMirror parser A->>B: deserialize(markdown) B->>C: render(markdown) C-->>B: html B->>D: to document(html) D-->>A: document A->>E: setContent(document)

デシリアライザは拡張モジュールにあります。デシリアライザーは拡張モジュールにあります。parseHTMLaddAttributes についてのTiptapドキュメントを読んで、実装方法を学んでください。Tiptap APIはProseMirrorのスキーマ仕様APIのラッパーです。

シリアライズ

シリアライゼーションとは、ProseMirrorドキュメントをMarkdownに変換するプロセスです。Content Editorは、prosemirror-markdown を使用してドキュメントをシリアライズします。シリアライザーを実装する前に、MarkdownSerializerと MarkdownSerializerStateクラスのドキュメントを読むことをお勧めします:

sequenceDiagram participant A as rich text editor participant B as Markdown serializer participant C as ProseMirror Markdown A->>B: serialize(document) B->>C: serialize(document, serializers) C-->>A: Markdown string

prosemirror-markdown リッチテキストエディタがサポートする各コンテンツタイプに対して、シリアライザ関数を実装する必要があります。私たちは~/content_editor/services/markdown_serializer.js でシリアライザを実装しています。