gitlab-org--gitlab-foss/app/assets/javascripts/content_editor/components/content_editor.vue

183 lines
4.8 KiB
Vue

<script>
import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2';
import { __ } from '~/locale';
import { VARIANT_DANGER } from '~/flash';
import { createContentEditor } from '../services/create_content_editor';
import { ALERT_EVENT, TIPTAP_AUTOFOCUS_OPTIONS } from '../constants';
import ContentEditorAlert from './content_editor_alert.vue';
import ContentEditorProvider from './content_editor_provider.vue';
import EditorStateObserver from './editor_state_observer.vue';
import FormattingBubbleMenu from './bubble_menus/formatting_bubble_menu.vue';
import CodeBlockBubbleMenu from './bubble_menus/code_block_bubble_menu.vue';
import LinkBubbleMenu from './bubble_menus/link_bubble_menu.vue';
import MediaBubbleMenu from './bubble_menus/media_bubble_menu.vue';
import TopToolbar from './top_toolbar.vue';
import LoadingIndicator from './loading_indicator.vue';
export default {
components: {
LoadingIndicator,
ContentEditorAlert,
ContentEditorProvider,
TiptapEditorContent,
TopToolbar,
FormattingBubbleMenu,
CodeBlockBubbleMenu,
LinkBubbleMenu,
MediaBubbleMenu,
EditorStateObserver,
},
props: {
renderMarkdown: {
type: Function,
required: true,
},
uploadsPath: {
type: String,
required: true,
},
extensions: {
type: Array,
required: false,
default: () => [],
},
serializerConfig: {
type: Object,
required: false,
default: () => {},
},
markdown: {
type: String,
required: false,
default: '',
},
autofocus: {
type: [String, Boolean],
required: false,
default: false,
validator: (autofocus) => TIPTAP_AUTOFOCUS_OPTIONS.includes(autofocus),
},
},
data() {
return {
focused: false,
isLoading: false,
latestMarkdown: null,
};
},
watch: {
markdown(markdown) {
if (markdown !== this.latestMarkdown) {
this.setSerializedContent(markdown);
}
},
},
created() {
const { renderMarkdown, uploadsPath, extensions, serializerConfig, autofocus } = this;
// This is a non-reactive attribute intentionally since this is a complex object.
this.contentEditor = createContentEditor({
renderMarkdown,
uploadsPath,
extensions,
serializerConfig,
tiptapOptions: {
autofocus,
},
});
},
mounted() {
this.$emit('initialized');
this.setSerializedContent(this.markdown);
},
beforeDestroy() {
this.contentEditor.dispose();
},
methods: {
async setSerializedContent(markdown) {
this.notifyLoading();
try {
await this.contentEditor.setSerializedContent(markdown);
this.contentEditor.setEditable(true);
this.notifyLoadingSuccess();
this.latestMarkdown = markdown;
} catch {
this.contentEditor.eventHub.$emit(ALERT_EVENT, {
message: __(
'An error occurred while trying to render the content editor. Please try again.',
),
variant: VARIANT_DANGER,
actionLabel: __('Retry'),
action: () => {
this.setSerializedContent(markdown);
},
});
this.contentEditor.setEditable(false);
this.notifyLoadingError();
}
},
focus() {
this.focused = true;
},
blur() {
this.focused = false;
},
notifyLoading() {
this.isLoading = true;
this.$emit('loading');
},
notifyLoadingSuccess() {
this.isLoading = false;
this.$emit('loadingSuccess');
},
notifyLoadingError(error) {
this.isLoading = false;
this.$emit('loadingError', error);
},
notifyChange() {
this.latestMarkdown = this.contentEditor.getSerializedContent();
this.$emit('change', {
empty: this.contentEditor.empty,
changed: this.contentEditor.changed,
markdown: this.latestMarkdown,
});
},
},
};
</script>
<template>
<content-editor-provider :content-editor="contentEditor">
<div>
<editor-state-observer
@docUpdate="notifyChange"
@focus="focus"
@blur="blur"
@keydown="$emit('keydown', $event)"
/>
<content-editor-alert />
<div
data-testid="content-editor"
data-qa-selector="content_editor_container"
class="md-area"
:class="{ 'is-focused': focused }"
>
<top-toolbar ref="toolbar" class="gl-mb-4" />
<div class="gl-relative">
<formatting-bubble-menu />
<code-block-bubble-menu />
<link-bubble-menu />
<media-bubble-menu />
<tiptap-editor-content
class="md"
data-testid="content_editor_editablebox"
:editor="contentEditor.tiptapEditor"
/>
<loading-indicator v-if="isLoading" />
</div>
</div>
</div>
</content-editor-provider>
</template>