리치 텍스트 편집기 개발 지침

리치 텍스트 편집기는 GitLab 애플리케이션에서 GitLab Flavored Markdown에 대한 WYSIWYG 편집 경험을 제공하는 UI 컴포넌트입니다. 또한 Markdown에 초점을 맞춘 편집기를 구현하기 위한 기반으로 사용되며, 정적 사이트 생성기와 같은 다른 엔진을 대상으로 합니다.

티프탭 2.0ProseMirror를 사용하여 리치 텍스트 편집기를 구축합니다. 이러한 프레임워크는 기본 contenteditable 웹 기술 위에 추상화 수준을 제공합니다.

사용 가이드

기능에 리치 텍스트 편집기를 포함하려면 다음 지침을 따르세요.

  1. 리치 텍스트 편집기 컴포넌트 포함.
  2. 마크다운 설정 및 가져오기.
  3. 변경 사항 청취.

리치 텍스트 편집기 컴포넌트 포함

ContentEditor Vue 컴포넌트를 가져옵니다. ContentEditor는 큰 의존성이므로 비동기적으로 명명된 가져오기를 권장하며 캐싱을 활용하세요.

<script>
export default {
  components: {
    ContentEditor: () =>
      import(
        /* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
      ),
  },
  // 나머지 컴포넌트 정의
}
</script>

리치 텍스트 편집기에는 두 가지 속성이 필요합니다. - renderMarkdownMarkdown API를 호출하여 응답(String)을 반환하는 비동기 함수입니다. - uploadsPathmultipart/form-data 지원을 통해 GitLab 업로드 서비스를 가리키는 URL입니다.

이 두 속성에 대한 Production 예제는 WikiForm.vue에서 볼 수 있습니다.

마크다운 설정 및 가져오기

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>

구현 가이드

리치 텍스트 편집기는 세 가지 주요 레이어로 구성됩니다.

  • 편집 도구 UI: 툴바 및 테이블 구조 편집기와 같은 편집 도구 UI입니다. 편집기의 상태를 표시하고 명령을 발행하여 상태를 변경합니다.
  • Tiptap Editor 개체: 편집기의 상태를 관리하고 편집 도구 UI에 의해 실행되는 명령으로 비즈니스 로직을 노출합니다.
  • 마크다운 직렬화기: 마크다운 소스 문자열을 ProseMirror 문서로 변환하고 그 반대로 변환합니다.

편집 도구 UI

편집 도구 UI는 Vue 컴포넌트로서 편집기의 상태를 표시하고 명령을 발행하여 상태를 변경합니다. 이러한 컴포넌트는 ~/content_editor/components 디렉터리에 위치합니다. 예를 들어 굵게 툴바 버튼은 사용자가 굵은 텍스트를 선택할 때 활성화되어 편집기의 상태를 표시합니다. 또한 이 버튼은 텍스트를 굵게 서식 지정하는 toggleBold 명령을 발행합니다.

Tiptap object편집 도구 UITiptap object편집 도구 UI상태 쿼리/명령 발행상태 변경 통지

노드 뷰

우리는 테이블 및 이미지와 같은 일부 콘텐츠 유형에 대한 인라인 편집 도구를 제공하기 위해 노드 뷰를 구현합니다. 노드 뷰는 콘텐츠 유형의 표현을 해당 모델에서 분리할 수 있도록 합니다. 표현 레이어에서 Vue 컴포넌트를 사용하여 리치 텍스트 편집기에서 고급 편집 경험을 가능하게 합니다. 노드 뷰는 ~/content_editor/components/wrappers에 위치합니다.

명령 발행

Vue 컴포넌트에 Tiptap Editor 개체를 주입하여 명령을 발행할 수 있습니다.

note
Vue 컴포넌트에서 편집기의 상태를 변경하는 로직은 구현하지 않아야 합니다. 이러한 로직을 명령에 캡슐화하고 컴포넌트의 메서드에서 명령을 발행하세요.
<script>
export default {
  inject: ['tiptapEditor'],
  methods: {
    execute() {
      // 올바르지 않음
      const { state, view } = this.tiptapEditor.state;
      const { tr, schema } = state;
      tr.addMark(state.selection.from, state.selection.to, null, null, schema.mark('bold'));
      
      // 올바름
      this.tiptapEditor.chain().toggleBold().focus().run();
    },
  }
};
</script>
<template>

편집기의 상태 쿼리

EditorStateObserver 렌더리스 컴포넌트를 사용하여 편집기의 상태 변경(예: 문서 또는 선택 변경)에 반응할 수 있습니다. 다음 이벤트를 청취할 수 있습니다. - docUpdate - selectionUpdate - transaction - focus - blur - error. 이러한 이벤트에 대해 자세히 알아보세요: Tiptap 이벤트 가이드.

<script>
// 효율성을 위해 코드 일부는 숨겨졌습니다
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 Editor 클래스는 에디터의 상태를 관리하고 리치 텍스트 편집기를 구동하는 모든 비즈니스 로직을 캡슐화합니다. 리치 텍스트 편집기는 이 클래스의 새 인스턴스를 만들고 GitLab Flavored Markdown을 지원하기 위한 모든 필요한 확장을 제공합니다.

새로운 확장 구현

확장은 리치 텍스트 편집기의 컴포넌트입니다. 새로운 확장을 구현하는 방법은 Tiptap 가이드를 읽어보시기를 권장합니다. 새로운 확장을 구현하기 전에 내장 노드마크 디렉터리을 확인하는 것이 좋습니다.

리치 텍스트 편집기의 확장은 ~/content_editor/extensions 디렉터리에 저장합니다. Tiptap 내장 확장을 사용할 때는 이 디렉터리 안에 ES6 모듈로 래핑하세요:

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

확장의 동작을 사용자 정의하려면 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에 등록하세요. 확장 모듈을 import하고 builtInContentEditorExtensions 배열에 추가합니다:

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

const builtInContentEditorExtensions = [
  Code,
  CodeBlockHighlight,
  Document,
  Dropcursor,
  Emoji,
  // 다른 확장들
]

Markdown 직렬화기

Markdown 직렬화기는 Markdown 문자열을 ProseMirror 문서로 변환하거나 그 반대로 변환합니다.

역직렬화

역직렬화는 Markdown을 ProseMirror 문서로 변환하는 과정입니다. 먼저 Markdown을 HTML로 렌더링한 후 Markdown API 엔드포인트를 이용하여 ProseMirror의 HTML 파싱 및 직렬화 기능을 활용합니다:

ProseMirror parserMarkdown APIMarkdown serializerTiptap objectrich text editorProseMirror parserMarkdown APIMarkdown serializerTiptap objectrich text editordeserialize(markdown)render(markdown)htmlto document(html)documentsetContent(document)

역직렬화기는 확장 모듈에 구현됩니다. parseHTMLaddAttributes를 읽어서 구현하는 방법을 학습하세요. Tiptap API는 ProseMirror의 schema spec API를 감싼 래퍼입니다.

직렬화

직렬화는 ProseMirror 문서를 Markdown으로 변환하는 과정입니다. 콘텐츠 편집기는 prosemirror-markdown 을 사용하여 문서를 직렬화합니다. 직렬화기를 구현하기 전에 MarkdownSerializerMarkdownSerializerState 클래스 문서를 읽는 것을 권장합니다:

ProseMirror MarkdownMarkdown serializerrich text editorProseMirror MarkdownMarkdown serializerrich text editorserialize(document)serialize(document, serializers)Markdown 문자열

prosemirror-markdown은 리치 텍스트 편집기에서 지원하는 각 콘텐츠 유형에 대해 직렬화기 함수를 구현해야 합니다. 직렬화기는 ~/content_editor/services/markdown_serializer.js에 구현합니다.