리치 텍스트 편집기 개발 가이드라인
GitLab v19.4요약
리치 텍스트 편집기는 GitLab 애플리케이션에서 GitLab Flavored Markdown을 WYSIWYG 방식으로 편집할 수 있게 해 주는 UI 컴포넌트입니다. 리치 텍스트 편집기는 Tiptap 2.0과 ProseMirror로 만듭니다.
리치 텍스트 편집기는 GitLab 애플리케이션에서 GitLab Flavored Markdown을 WYSIWYG 방식으로 편집할 수 있게 해 주는 UI 컴포넌트입니다. 또한 정적 사이트 생성기 같은 다른 엔진을 대상으로 하는 Markdown 중심 편집기를 구현하는 기반 역할도 합니다.
리치 텍스트 편집기는 Tiptap 2.0과 ProseMirror로
만듭니다. 이 프레임워크들은 네이티브
contenteditable
웹 기술 위에 한 단계의 추상화를 제공합니다.
사용 가이드#
기능에 리치 텍스트 편집기를 포함하려면 다음 절차를 따릅니다.
리치 텍스트 편집기 컴포넌트 포함하기#
ContentEditor Vue 컴포넌트를 임포트합니다. ContentEditor는 크기가 큰 의존성이므로 캐시를
활용할 수 있도록 비동기 명명 임포트를 사용하기를 권장합니다.
<script>
export default {
components: {
ContentEditor: () =>
import(
/* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
),
},
// rest of the component definition
}
</script>
리치 텍스트 편집기에는 두 가지 속성이 필요합니다.
renderMarkdown은 Markdown API 호출 결과(String)를 반환하는 비동기 함수입니다.uploadsPath는multipart/form-data를 지원하는 GitLab 업로드 서비스를 가리키는 URL 입니다.
이 두 속성의 실제 사용 예는
WikiForm.vue 컴포넌트를 참고합니다.
Markdown 설정 및 가져오기#
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 {
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노드 뷰#
표나 이미지 같은 일부 콘텐츠 유형에 인라인 편집 도구를 제공하기 위해
노드 뷰를 구현합니다. 노드 뷰를 사용하면
콘텐츠 유형의 표현을 그 모델에서
분리할 수 있습니다. 표현 계층에 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를 사용합니다. 다음 이벤트를
수신할 수 있습니다.
doc-updateselection-updatetransactionfocusblurerror.
이 이벤트에 대한 자세한 내용은 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 가이드에서 확인할 수 있습니다. 새 확장을 처음부터 구현하기 전에 내장 노드와 마크 목록을 먼저 확인하기를 권장합니다.
리치 텍스트 편집기 확장은 ~/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 문서의
parseHTML과
addAttributes를
참고합니다. 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 stringprosemirror-markdown을 사용하려면 리치 텍스트 편집기가 지원하는 콘텐츠 유형마다 직렬화기
함수를 구현해야 합니다. 직렬화기는 ~/content_editor/services/markdown_serializer.js에 구현합니다.