リッチテキストエディタ開発者向けガイドライン
リッチテキストエディタは、GitLabアプリケーションでGitLab Flavored MarkdownのWYSIWYG編集体験を提供するUIコンポーネントです。また、静的サイトジェネレータのような他のエンジンをターゲットにしたMarkdownに特化したエディタを実装するための基盤としても機能します。
リッチテキストエディタを構築するためにTiptap 2.0と ProseMirrorを使っています。これらのフレームワークは、ネイティブのcontenteditable
Webテクノロジーの上に抽象化レベルを提供します。
使用ガイド
リッチテキストエディタを機能に含めるには、以下の手順に従ってください。
- リッチテキストエディタのコンポーネントをインクルードします。
- Markdownの設定と取得。
- 変更を確認します。
リッチテキストエディタ・コンポーネントを含める
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つのプロパティが必要です:
-
renderMarkdown
はMarkdown APIを呼び出したレスポンス (文字列) を返す非同期関数です。 -
uploadsPath
は、multipart/form-data
をサポートするGitLab アップロードサービスを指す URL です。
この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
コマンドをディスパッチします:
ノードビュー
テーブルや画像など、いくつかのコンテンツタイプにインライン編集ツールを提供するために、ノードビューを実装しています。ノードビューは、コンテンツタイプのプレゼンテーションをそのモデルから分離することを可能にします。プレゼンテーションレイヤーに Vue コンポーネントを使用することで、リッチテキストエディタでの洗練された編集体験が可能になります。ノードビューは~/content_editor/components/wrappers
にあります。
ディスパッチコマンド
Vue コンポーネントに Tiptap Editor オブジェクトを注入して、コマンドをディスパッチすることができます。
<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パースとシリアライズの機能を利用します:
デシリアライザは拡張モジュールにあります。デシリアライザーは拡張モジュールにあります。parseHTML
とaddAttributes
についてのTiptapドキュメントを読んで、実装方法を学んでください。Tiptap APIはProseMirrorのスキーマ仕様APIのラッパーです。
シリアライズ
シリアライゼーションとは、ProseMirrorドキュメントをMarkdownに変換するプロセスです。Content Editorは、prosemirror-markdown
を使用してドキュメントをシリアライズします。シリアライザーを実装する前に、MarkdownSerializerと MarkdownSerializerStateクラスのドキュメントを読むことをお勧めします:
prosemirror-markdown
リッチテキストエディタがサポートする各コンテンツタイプに対して、シリアライザ関数を実装する必要があります。私たちは~/content_editor/services/markdown_serializer.js
でシリアライザを実装しています。