리치 텍스트 편집기 개발 지침
리치 텍스트 편집기는 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'
),
},
// 나머지 컴포넌트 정의
}
</script>
리치 텍스트 편집기는 다음 두 속성이 필요합니다.
-
renderMarkdown
은 Markdown API를 호출한 응답(String)을 반환하는 비동기 함수입니다. -
uploadsPath
는multipart/form-data
를 지원하는 GitLab 업로드 서비스를 가리키는 URL입니다.
이 두 속성의 제작 예는 WikiForm.vue
컴포넌트에서 확인할 수 있습니다.
Markdown 설정 및 가져오기
ContentEditor
Vue 컴포넌트는 Vue 데이터 바인딩 흐름 (v-model
)을 구현하지 않습니다. Markdown 설정 및 가져오기가 비용이 많이 드는 작업이기 때문입니다. 데이터 바인딩은 사용자가 컴포넌트와 상호 작용할 때마다 이러한 작업을 트리거할 것입니다.
대신 ContentEditor
클래스의 인스턴스를 initialized
이벤트를 수신해서 얻어야 합니다.
<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에서 실행된 명령으로 비즈니스 로직을 노출합니다.
- Markdown 직렬화기: Markdown 소스 문자열을 ProseMirror 문서로, 그 반대로 변환합니다.
편집 도구 UI
편집 도구 UI는 편집기의 상태를 표시하고 commands를 발송하여 상태를 변경하는 Vue 컴포넌트입니다. 이들은 ~/content_editor/components
디렉토리에 위치합니다. 예를 들어 볼드 툴바 버튼은 사용자가 볼드로 지정된 텍스트를 선택할 때 활성화되어 편집기의 상태를 표시합니다. 이 버튼은 또한 텍스트를 볼드로 서식 지정하기 위해 toggleBold
명령을 발송합니다:
노드 뷰
우리는 노드 뷰를 구현하여 테이블 및 이미지와 같은 일부 콘텐츠 유형에 대한 인라인 편집 도구를 제공합니다. 노드 뷰를 사용하면 콘텐츠 유형의 표현과 모델을 분리할 수 있습니다. 표현 레이어에 Vue 컴포넌트를 사용하면 리치 텍스트 편집기에서 정교한 편집 경험을 가능하게 합니다. 노드 뷰는 ~/content_editor/components/wrappers
에 위치합니다.
명령 발송
Vue 컴포넌트에 Tiptap Editor 객체를 주입하여 명령을 발송할 수 있습니다.
참고: 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
.
이벤트 가이드에서 이러한 이벤트에 대해 자세히 알아보세요.
<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 가이드를 읽어 새로운 확장을 어떻게 구현하는지 알아볼 수 있습니다. 새로운 확장을 구현하기 전에 내장된 nodes 및 marks 목록을 확인하는 것이 좋습니다.
리치 텍스트 편집기 확장을 ~/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을 Markdown API 엔드포인트를 사용하여 HTML로 렌더링한 다음 ProseMirror의 HTML 파싱 및 직렬화 기능을 활용합니다:
역직렬화기는 확장 모듈에 있습니다. 어떻게 구현하는지 알아보려면 Tiptap 문서를 참조하세요.
직렬화
직렬화는 ProseMirror 문서를 Markdown으로 변환하는 과정입니다. 컨텐츠 편집기는 prosemirror-markdown
를 사용하여 문서를 직렬화합니다. 직렬화기를 구현하기 전에 MarkdownSerializer 및 MarkdownSerializerState 클래스 문서를 참고하는 것이 좋습니다:
prosemirror-markdown
은 리치 텍스트 편집기에서 지원하는 각 콘텐츠 유형에 대한 직렬화 함수를 구현해야 합니다. 직렬화기는 ~/content_editor/services/markdown_serializer.js
에 구현해야 합니다.