InfoGrab DocsInfoGrab Docs

리치 텍스트 편집기 개발 가이드라인

요약

리치 텍스트 편집기는 GitLab 애플리케이션에서 GitLab Flavored Markdown을 위한 WYSIWYG 편집 환경을 제공하는 UI 컴포넌트입니다. 리치 텍스트 편집기를 구축하기 위해 Tiptap 2.0과 ProseMirror를 사용합니다.

리치 텍스트 편집기는 GitLab 애플리케이션에서 GitLab Flavored Markdown을 위한 WYSIWYG 편집 환경을 제공하는 UI 컴포넌트입니다. 또한 정적 사이트 생성기와 같이 다른 엔진을 대상으로 하는 Markdown 중심 편집기를 구현하기 위한 기반으로도 사용됩니다.

리치 텍스트 편집기를 구축하기 위해 Tiptap 2.0ProseMirror를 사용합니다. 이 프레임워크들은 네이티브 contenteditable 웹 기술 위에 추상화 계층을 제공합니다.

사용 가이드#

기능에 리치 텍스트 편집기를 포함시키려면 아래 지침을 따르세요.

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

ContentEditor Vue 컴포넌트를 임포트하세요. ContentEditor는 큰 의존성이므로 캐싱을 활용하기 위해 비동기 이름 있는 임포트(asynchronous named imports)를 사용하는 것을 권장합니다.

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

리치 텍스트 편집기에는 두 가지 속성이 필요합니다:

  • renderMarkdownMarkdown API 호출의 응답(문자열)을 반환하는 비동기 함수입니다.

  • uploadsPathmultipart/form-data를 지원하는 GitLab 업로드 서비스를 가리키는 URL입니다.

이 두 속성의 프로덕션 예시는 WikiForm.vue 컴포넌트를 참고하세요.

Markdown 설정 및 가져오기#

ContentEditor Vue 컴포넌트는 Markdown 설정과 가져오기가 비용이 큰 작업이기 때문에 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 {
      disabled: false,
    };
  },
  methods: {
    handleContentEditorChange({ markdown }) {
      this.disabled = !!/XXX/.exec(markdown);
    }
  },
};
</script>
<template>
  <div>
    <content-editor
      :render-markdown="renderMarkdown"
      :uploads-path="pageInfo.uploadsPath"
      @initialized="loadInitialContent"
      @change="handleContentEditorChange"
    />
    <gl-button :disabled="disabled" @click="submitChanges">
      {{ __('Submit changes') }}
    </gl-button>
  </div>

</template>

구현 가이드#

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

  • 편집 도구 UI: 툴바와 테이블 구조 편집기와 같은 도구입니다. 편집기의 상태를 표시하고 커맨드를 디스패치하여 상태를 변경합니다.

  • Tiptap Editor 객체: 편집기의 상태를 관리하고, 편집 도구 UI가 실행하는 커맨드로서 비즈니스 로직을 노출합니다.

  • Markdown 직렬화기: Markdown 소스 문자열을 ProseMirror 문서로, 또는 그 반대로 변환합니다.

편집 도구 UI#

편집 도구 UI는 편집기의 상태를 표시하고 상태를 변경하기 위해 커맨드를 디스패치하는 Vue 컴포넌트입니다. ~/content_editor/components 디렉터리에 위치합니다. 예를 들어, Bold 툴바 버튼은 사용자가 굵은 텍스트를 선택하면 활성화되어 편집기의 상태를 표시합니다. 이 버튼은 또한 텍스트를 굵게 포맷하기 위해 toggleBold 커맨드를 디스패치합니다:

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

노드 뷰#

테이블, 이미지와 같은 일부 콘텐츠 유형에 대한 인라인 편집 도구를 제공하기 위해 node views를 구현합니다. 노드 뷰는 콘텐츠 유형의 표현을 해당 모델과 분리할 수 있게 해줍니다. 프레젠테이션 레이어에서 Vue 컴포넌트를 사용하면 리치 텍스트 편집기에서 정교한 편집 환경을 구현할 수 있습니다. 노드 뷰는 ~/content_editor/components/wrappers에 위치합니다.

커맨드 디스패치#

Vue 컴포넌트에 Tiptap Editor 객체를 주입하여 커맨드를 디스패치할 수 있습니다.

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 Editor 클래스는 편집기의 상태를 관리하고 리치 텍스트 편집기를 구동하는 모든 비즈니스 로직을 캡슐화합니다. 리치 텍스트 편집기는 이 클래스의 새 인스턴스를 생성하고 GitLab Flavored Markdown을 지원하는 데 필요한 모든 확장을 제공합니다.

새 확장 구현#

확장은 리치 텍스트 편집기의 기본 구성 요소입니다. Tiptap 가이드를 읽어 새로운 확장을 구현하는 방법을 배울 수 있습니다. 새 확장을 처음부터 구현하기 전에 내장된 nodesmarks 목록을 먼저 확인하는 것을 권장합니다.

리치 텍스트 편집기 확장은 ~/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에 새 확장을 등록하세요. 확장 모듈을 임포트하고 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)

역직렬화기는 확장 모듈에 있습니다. Tiptap 문서의 parseHTMLaddAttributes를 읽어 구현 방법을 알아보세요. Tiptap API는 ProseMirror의 schema spec API 래퍼입니다.

직렬화#

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

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에 구현합니다.

리치 텍스트 편집기 개발 가이드라인

GitLab v19.1
원문 보기
요약

리치 텍스트 편집기는 GitLab 애플리케이션에서 GitLab Flavored Markdown을 위한 WYSIWYG 편집 환경을 제공하는 UI 컴포넌트입니다. 리치 텍스트 편집기를 구축하기 위해 Tiptap 2.0과 ProseMirror를 사용합니다.

리치 텍스트 편집기는 GitLab 애플리케이션에서 GitLab Flavored Markdown을 위한 WYSIWYG 편집 환경을 제공하는 UI 컴포넌트입니다. 또한 정적 사이트 생성기와 같이 다른 엔진을 대상으로 하는 Markdown 중심 편집기를 구현하기 위한 기반으로도 사용됩니다.

리치 텍스트 편집기를 구축하기 위해 Tiptap 2.0ProseMirror를 사용합니다. 이 프레임워크들은 네이티브 contenteditable 웹 기술 위에 추상화 계층을 제공합니다.

사용 가이드#

기능에 리치 텍스트 편집기를 포함시키려면 아래 지침을 따르세요.

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

ContentEditor Vue 컴포넌트를 임포트하세요. ContentEditor는 큰 의존성이므로 캐싱을 활용하기 위해 비동기 이름 있는 임포트(asynchronous named imports)를 사용하는 것을 권장합니다.

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

리치 텍스트 편집기에는 두 가지 속성이 필요합니다:

  • renderMarkdownMarkdown API 호출의 응답(문자열)을 반환하는 비동기 함수입니다.

  • uploadsPathmultipart/form-data를 지원하는 GitLab 업로드 서비스를 가리키는 URL입니다.

이 두 속성의 프로덕션 예시는 WikiForm.vue 컴포넌트를 참고하세요.

Markdown 설정 및 가져오기#

ContentEditor Vue 컴포넌트는 Markdown 설정과 가져오기가 비용이 큰 작업이기 때문에 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 {
      disabled: false,
    };
  },
  methods: {
    handleContentEditorChange({ markdown }) {
      this.disabled = !!/XXX/.exec(markdown);
    }
  },
};
</script>
<template>
  <div>
    <content-editor
      :render-markdown="renderMarkdown"
      :uploads-path="pageInfo.uploadsPath"
      @initialized="loadInitialContent"
      @change="handleContentEditorChange"
    />
    <gl-button :disabled="disabled" @click="submitChanges">
      {{ __('Submit changes') }}
    </gl-button>
  </div>

</template>

구현 가이드#

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

  • 편집 도구 UI: 툴바와 테이블 구조 편집기와 같은 도구입니다. 편집기의 상태를 표시하고 커맨드를 디스패치하여 상태를 변경합니다.

  • Tiptap Editor 객체: 편집기의 상태를 관리하고, 편집 도구 UI가 실행하는 커맨드로서 비즈니스 로직을 노출합니다.

  • Markdown 직렬화기: Markdown 소스 문자열을 ProseMirror 문서로, 또는 그 반대로 변환합니다.

편집 도구 UI#

편집 도구 UI는 편집기의 상태를 표시하고 상태를 변경하기 위해 커맨드를 디스패치하는 Vue 컴포넌트입니다. ~/content_editor/components 디렉터리에 위치합니다. 예를 들어, Bold 툴바 버튼은 사용자가 굵은 텍스트를 선택하면 활성화되어 편집기의 상태를 표시합니다. 이 버튼은 또한 텍스트를 굵게 포맷하기 위해 toggleBold 커맨드를 디스패치합니다:

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

노드 뷰#

테이블, 이미지와 같은 일부 콘텐츠 유형에 대한 인라인 편집 도구를 제공하기 위해 node views를 구현합니다. 노드 뷰는 콘텐츠 유형의 표현을 해당 모델과 분리할 수 있게 해줍니다. 프레젠테이션 레이어에서 Vue 컴포넌트를 사용하면 리치 텍스트 편집기에서 정교한 편집 환경을 구현할 수 있습니다. 노드 뷰는 ~/content_editor/components/wrappers에 위치합니다.

커맨드 디스패치#

Vue 컴포넌트에 Tiptap Editor 객체를 주입하여 커맨드를 디스패치할 수 있습니다.

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 Editor 클래스는 편집기의 상태를 관리하고 리치 텍스트 편집기를 구동하는 모든 비즈니스 로직을 캡슐화합니다. 리치 텍스트 편집기는 이 클래스의 새 인스턴스를 생성하고 GitLab Flavored Markdown을 지원하는 데 필요한 모든 확장을 제공합니다.

새 확장 구현#

확장은 리치 텍스트 편집기의 기본 구성 요소입니다. Tiptap 가이드를 읽어 새로운 확장을 구현하는 방법을 배울 수 있습니다. 새 확장을 처음부터 구현하기 전에 내장된 nodesmarks 목록을 먼저 확인하는 것을 권장합니다.

리치 텍스트 편집기 확장은 ~/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에 새 확장을 등록하세요. 확장 모듈을 임포트하고 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)

역직렬화기는 확장 모듈에 있습니다. Tiptap 문서의 parseHTMLaddAttributes를 읽어 구현 방법을 알아보세요. Tiptap API는 ProseMirror의 schema spec API 래퍼입니다.

직렬화#

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

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에 구현합니다.