Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-03-18 23:02:30 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-03-18 23:02:30 +0300
commit41fe97390ceddf945f3d967b8fdb3de4c66b7dea (patch)
tree9c8d89a8624828992f06d892cd2f43818ff5dcc8 /app/assets/javascripts/content_editor
parent0804d2dc31052fb45a1efecedc8e06ce9bc32862 (diff)
Add latest changes from gitlab-org/gitlab@14-9-stable-eev14.9.0-rc42
Diffstat (limited to 'app/assets/javascripts/content_editor')
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue29
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor_provider.vue1
-rw-r--r--app/assets/javascripts/content_editor/components/editor_state_observer.vue34
-rw-r--r--app/assets/javascripts/content_editor/components/loading_indicator.vue39
-rw-r--r--app/assets/javascripts/content_editor/constants.js4
-rw-r--r--app/assets/javascripts/content_editor/extensions/attachment.js17
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_block_highlight.js2
-rw-r--r--app/assets/javascripts/content_editor/extensions/paste_markdown.js86
-rw-r--r--app/assets/javascripts/content_editor/extensions/table.js3
-rw-r--r--app/assets/javascripts/content_editor/services/content_editor.js48
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js13
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_deserializer.js33
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js27
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_sourcemap.js6
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js9
-rw-r--r--app/assets/javascripts/content_editor/services/upload_helpers.js19
16 files changed, 269 insertions, 101 deletions
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index a8405fe37c7..a942c9f1149 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -1,17 +1,16 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2';
-import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } from '../constants';
import { createContentEditor } from '../services/create_content_editor';
import ContentEditorAlert from './content_editor_alert.vue';
import ContentEditorProvider from './content_editor_provider.vue';
import EditorStateObserver from './editor_state_observer.vue';
import FormattingBubbleMenu from './formatting_bubble_menu.vue';
import TopToolbar from './top_toolbar.vue';
+import LoadingIndicator from './loading_indicator.vue';
export default {
components: {
- GlLoadingIcon,
+ LoadingIndicator,
ContentEditorAlert,
ContentEditorProvider,
TiptapEditorContent,
@@ -41,7 +40,6 @@ export default {
},
data() {
return {
- isLoadingContent: false,
focused: false,
};
},
@@ -55,25 +53,14 @@ export default {
extensions,
serializerConfig,
});
-
- this.contentEditor.on(LOADING_CONTENT_EVENT, this.displayLoadingIndicator);
- this.contentEditor.on(LOADING_SUCCESS_EVENT, this.hideLoadingIndicator);
- this.contentEditor.on(LOADING_ERROR_EVENT, this.hideLoadingIndicator);
+ },
+ mounted() {
this.$emit('initialized', this.contentEditor);
},
beforeDestroy() {
this.contentEditor.dispose();
- this.contentEditor.off(LOADING_CONTENT_EVENT, this.displayLoadingIndicator);
- this.contentEditor.off(LOADING_SUCCESS_EVENT, this.hideLoadingIndicator);
- this.contentEditor.off(LOADING_ERROR_EVENT, this.hideLoadingIndicator);
},
methods: {
- displayLoadingIndicator() {
- this.isLoadingContent = true;
- },
- hideLoadingIndicator() {
- this.isLoadingContent = false;
- },
focus() {
this.focused = true;
},
@@ -100,13 +87,11 @@ export default {
:class="{ 'is-focused': focused }"
>
<top-toolbar ref="toolbar" class="gl-mb-4" />
- <div v-if="isLoadingContent" class="gl-w-full gl-display-flex gl-justify-content-center">
- <gl-loading-icon size="sm" />
- </div>
- <template v-else>
+ <div class="gl-relative">
<formatting-bubble-menu />
<tiptap-editor-content class="md" :editor="contentEditor.tiptapEditor" />
- </template>
+ <loading-indicator />
+ </div>
</div>
</div>
</content-editor-provider>
diff --git a/app/assets/javascripts/content_editor/components/content_editor_provider.vue b/app/assets/javascripts/content_editor/components/content_editor_provider.vue
index 630aff9858f..cba3b627390 100644
--- a/app/assets/javascripts/content_editor/components/content_editor_provider.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor_provider.vue
@@ -8,6 +8,7 @@ export default {
return {
contentEditor,
+ eventHub: contentEditor.eventHub,
tiptapEditor: contentEditor.tiptapEditor,
};
},
diff --git a/app/assets/javascripts/content_editor/components/editor_state_observer.vue b/app/assets/javascripts/content_editor/components/editor_state_observer.vue
index 0604047a953..02de6470cf2 100644
--- a/app/assets/javascripts/content_editor/components/editor_state_observer.vue
+++ b/app/assets/javascripts/content_editor/components/editor_state_observer.vue
@@ -1,5 +1,11 @@
<script>
import { debounce } from 'lodash';
+import {
+ LOADING_CONTENT_EVENT,
+ LOADING_SUCCESS_EVENT,
+ LOADING_ERROR_EVENT,
+ ALERT_EVENT,
+} from '../constants';
export const tiptapToComponentMap = {
update: 'docUpdate',
@@ -7,30 +13,48 @@ export const tiptapToComponentMap = {
transaction: 'transaction',
focus: 'focus',
blur: 'blur',
- alert: 'alert',
};
+export const eventHubEvents = [
+ ALERT_EVENT,
+ LOADING_CONTENT_EVENT,
+ LOADING_SUCCESS_EVENT,
+ LOADING_ERROR_EVENT,
+];
+
const getComponentEventName = (tiptapEventName) => tiptapToComponentMap[tiptapEventName];
export default {
- inject: ['tiptapEditor'],
+ inject: ['tiptapEditor', 'eventHub'],
created() {
this.disposables = [];
Object.keys(tiptapToComponentMap).forEach((tiptapEvent) => {
- const eventHandler = debounce((params) => this.handleTipTapEvent(tiptapEvent, params), 100);
+ const eventHandler = debounce(
+ (params) => this.bubbleEvent(getComponentEventName(tiptapEvent), params),
+ 100,
+ );
this.tiptapEditor?.on(tiptapEvent, eventHandler);
this.disposables.push(() => this.tiptapEditor?.off(tiptapEvent, eventHandler));
});
+
+ eventHubEvents.forEach((event) => {
+ const handler = (...params) => {
+ this.bubbleEvent(event, ...params);
+ };
+
+ this.eventHub.$on(event, handler);
+ this.disposables.push(() => this.eventHub?.$off(event, handler));
+ });
},
beforeDestroy() {
this.disposables.forEach((dispose) => dispose());
},
methods: {
- handleTipTapEvent(tiptapEvent, params) {
- this.$emit(getComponentEventName(tiptapEvent), params);
+ bubbleEvent(eventHubEvent, params) {
+ this.$emit(eventHubEvent, params);
},
},
render() {
diff --git a/app/assets/javascripts/content_editor/components/loading_indicator.vue b/app/assets/javascripts/content_editor/components/loading_indicator.vue
new file mode 100644
index 00000000000..5b9383d6e11
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/loading_indicator.vue
@@ -0,0 +1,39 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import EditorStateObserver from './editor_state_observer.vue';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ EditorStateObserver,
+ },
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+ methods: {
+ displayLoadingIndicator() {
+ this.isLoading = true;
+ },
+ hideLoadingIndicator() {
+ this.isLoading = false;
+ },
+ },
+};
+</script>
+<template>
+ <editor-state-observer
+ @loading="displayLoadingIndicator"
+ @loadingSuccess="hideLoadingIndicator"
+ @loadingError="hideLoadingIndicator"
+ >
+ <div
+ v-if="isLoading"
+ class="gl-w-full gl-display-flex gl-justify-content-center gl-align-items-center gl-absolute gl-top-0 gl-bottom-0"
+ >
+ <div class="gl-bg-white gl-absolute gl-w-full gl-h-full gl-opacity-3"></div>
+ <gl-loading-icon size="md" />
+ </div>
+ </editor-state-observer>
+</template>
diff --git a/app/assets/javascripts/content_editor/constants.js b/app/assets/javascripts/content_editor/constants.js
index 5e56078df01..a39a243ec6b 100644
--- a/app/assets/javascripts/content_editor/constants.js
+++ b/app/assets/javascripts/content_editor/constants.js
@@ -42,9 +42,10 @@ export const TEXT_STYLE_DROPDOWN_ITEMS = [
},
];
-export const LOADING_CONTENT_EVENT = 'loadingContent';
+export const LOADING_CONTENT_EVENT = 'loading';
export const LOADING_SUCCESS_EVENT = 'loadingSuccess';
export const LOADING_ERROR_EVENT = 'loadingError';
+export const ALERT_EVENT = 'alert';
export const PARSE_HTML_PRIORITY_LOWEST = 1;
export const PARSE_HTML_PRIORITY_DEFAULT = 50;
@@ -56,3 +57,4 @@ export const EXTENSION_PRIORITY_LOWER = 75;
* https://tiptap.dev/guide/custom-extensions/#priority
*/
export const EXTENSION_PRIORITY_DEFAULT = 100;
+export const EXTENSION_PRIORITY_HIGHEST = 200;
diff --git a/app/assets/javascripts/content_editor/extensions/attachment.js b/app/assets/javascripts/content_editor/extensions/attachment.js
index 72df1d071d1..9634730f637 100644
--- a/app/assets/javascripts/content_editor/extensions/attachment.js
+++ b/app/assets/javascripts/content_editor/extensions/attachment.js
@@ -9,15 +9,22 @@ export default Extension.create({
return {
uploadsPath: null,
renderMarkdown: null,
+ eventHub: null,
};
},
addCommands() {
return {
uploadAttachment: ({ file }) => () => {
- const { uploadsPath, renderMarkdown } = this.options;
+ const { uploadsPath, renderMarkdown, eventHub } = this.options;
- return handleFileEvent({ file, uploadsPath, renderMarkdown, editor: this.editor });
+ return handleFileEvent({
+ file,
+ uploadsPath,
+ renderMarkdown,
+ editor: this.editor,
+ eventHub,
+ });
},
};
},
@@ -29,23 +36,25 @@ export default Extension.create({
key: new PluginKey('attachment'),
props: {
handlePaste: (_, event) => {
- const { uploadsPath, renderMarkdown } = this.options;
+ const { uploadsPath, renderMarkdown, eventHub } = this.options;
return handleFileEvent({
editor,
file: event.clipboardData.files[0],
uploadsPath,
renderMarkdown,
+ eventHub,
});
},
handleDrop: (_, event) => {
- const { uploadsPath, renderMarkdown } = this.options;
+ const { uploadsPath, renderMarkdown, eventHub } = this.options;
return handleFileEvent({
editor,
file: event.dataTransfer.files[0],
uploadsPath,
renderMarkdown,
+ eventHub,
});
},
},
diff --git a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
index 9dc17fcd570..204ac07d401 100644
--- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
+++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@@ -1,5 +1,5 @@
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
-import * as lowlight from 'lowlight';
+import { lowlight } from 'lowlight/lib/all';
const extractLanguage = (element) => element.getAttribute('lang');
diff --git a/app/assets/javascripts/content_editor/extensions/paste_markdown.js b/app/assets/javascripts/content_editor/extensions/paste_markdown.js
new file mode 100644
index 00000000000..c349aa42a62
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/paste_markdown.js
@@ -0,0 +1,86 @@
+import { Extension } from '@tiptap/core';
+import { Plugin, PluginKey } from 'prosemirror-state';
+import { __ } from '~/locale';
+import { VARIANT_DANGER } from '~/flash';
+import createMarkdownDeserializer from '../services/markdown_deserializer';
+import {
+ ALERT_EVENT,
+ LOADING_CONTENT_EVENT,
+ LOADING_SUCCESS_EVENT,
+ LOADING_ERROR_EVENT,
+ EXTENSION_PRIORITY_HIGHEST,
+} from '../constants';
+
+const TEXT_FORMAT = 'text/plain';
+const HTML_FORMAT = 'text/html';
+const VS_CODE_FORMAT = 'vscode-editor-data';
+
+export default Extension.create({
+ name: 'pasteMarkdown',
+ priority: EXTENSION_PRIORITY_HIGHEST,
+ addOptions() {
+ return {
+ renderMarkdown: null,
+ };
+ },
+ addCommands() {
+ return {
+ pasteMarkdown: (markdown) => () => {
+ const { editor, options } = this;
+ const { renderMarkdown, eventHub } = options;
+ const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
+
+ eventHub.$emit(LOADING_CONTENT_EVENT);
+
+ deserializer
+ .deserialize({ schema: editor.schema, content: markdown })
+ .then(({ document }) => {
+ if (!document) {
+ return;
+ }
+
+ const { state, view } = editor;
+ const { tr, selection } = state;
+
+ tr.replaceWith(selection.from - 1, selection.to, document.content);
+ view.dispatch(tr);
+ eventHub.$emit(LOADING_SUCCESS_EVENT);
+ })
+ .catch(() => {
+ eventHub.$emit(ALERT_EVENT, {
+ message: __('An error occurred while pasting text in the editor. Please try again.'),
+ variant: VARIANT_DANGER,
+ });
+ eventHub.$emit(LOADING_ERROR_EVENT);
+ });
+
+ return true;
+ },
+ };
+ },
+ addProseMirrorPlugins() {
+ return [
+ new Plugin({
+ key: new PluginKey('pasteMarkdown'),
+ props: {
+ handlePaste: (_, event) => {
+ const { clipboardData } = event;
+ const content = clipboardData.getData(TEXT_FORMAT);
+ const hasHTML = clipboardData.types.some((type) => type === HTML_FORMAT);
+ const hasVsCode = clipboardData.types.some((type) => type === VS_CODE_FORMAT);
+ const vsCodeMeta = hasVsCode ? JSON.parse(clipboardData.getData(VS_CODE_FORMAT)) : {};
+ const language = vsCodeMeta.mode;
+
+ if (!content || (hasHTML && !hasVsCode) || (hasVsCode && language !== 'markdown')) {
+ return false;
+ }
+
+ this.editor.commands.pasteMarkdown(content);
+
+ return true;
+ },
+ },
+ }),
+ ];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/table.js b/app/assets/javascripts/content_editor/extensions/table.js
index 004bb8b815c..d7456ab4094 100644
--- a/app/assets/javascripts/content_editor/extensions/table.js
+++ b/app/assets/javascripts/content_editor/extensions/table.js
@@ -1,5 +1,6 @@
import { Table } from '@tiptap/extension-table';
import { debounce } from 'lodash';
+import { VARIANT_WARNING } from '~/flash';
import { __ } from '~/locale';
import { getMarkdownSource } from '../services/markdown_sourcemap';
import { shouldRenderHTMLTable } from '../services/serialization_helpers';
@@ -14,7 +15,7 @@ const onUpdate = debounce((editor) => {
message: __(
'The content editor may change the markdown formatting style of the document, which may not match your original markdown style.',
),
- variant: 'warning',
+ variant: VARIANT_WARNING,
});
alertShown = true;
diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js
index a387322bff7..c5638da2daf 100644
--- a/app/assets/javascripts/content_editor/services/content_editor.js
+++ b/app/assets/javascripts/content_editor/services/content_editor.js
@@ -1,17 +1,23 @@
-import eventHubFactory from '~/helpers/event_hub_factory';
+import { TextSelection } from 'prosemirror-state';
import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } from '../constants';
+
/* eslint-disable no-underscore-dangle */
export class ContentEditor {
- constructor({ tiptapEditor, serializer }) {
+ constructor({ tiptapEditor, serializer, deserializer, eventHub }) {
this._tiptapEditor = tiptapEditor;
this._serializer = serializer;
- this._eventHub = eventHubFactory();
+ this._deserializer = deserializer;
+ this._eventHub = eventHub;
}
get tiptapEditor() {
return this._tiptapEditor;
}
+ get eventHub() {
+ return this._eventHub;
+ }
+
get empty() {
const doc = this.tiptapEditor?.state.doc;
@@ -23,39 +29,31 @@ export class ContentEditor {
this.tiptapEditor.destroy();
}
- once(type, handler) {
- this._eventHub.$once(type, handler);
- }
-
- on(type, handler) {
- this._eventHub.$on(type, handler);
- }
-
- emit(type, params = {}) {
- this._eventHub.$emit(type, params);
- }
-
- off(type, handler) {
- this._eventHub.$off(type, handler);
- }
-
disposeAllEvents() {
this._eventHub.dispose();
}
async setSerializedContent(serializedContent) {
- const { _tiptapEditor: editor, _serializer: serializer } = this;
+ const { _tiptapEditor: editor, _deserializer: deserializer, _eventHub: eventHub } = this;
+ const { doc, tr } = editor.state;
+ const selection = TextSelection.create(doc, 0, doc.content.size);
try {
- this._eventHub.$emit(LOADING_CONTENT_EVENT);
- const document = await serializer.deserialize({
+ eventHub.$emit(LOADING_CONTENT_EVENT);
+ const { document } = await deserializer.deserialize({
schema: editor.schema,
content: serializedContent,
});
- editor.commands.setContent(document);
- this._eventHub.$emit(LOADING_SUCCESS_EVENT);
+
+ if (document) {
+ tr.setSelection(selection)
+ .replaceSelectionWith(document, false)
+ .setMeta('preventUpdate', true);
+ editor.view.dispatch(tr);
+ }
+ eventHub.$emit(LOADING_SUCCESS_EVENT);
} catch (e) {
- this._eventHub.$emit(LOADING_ERROR_EVENT, e);
+ eventHub.$emit(LOADING_ERROR_EVENT, e);
throw e;
}
}
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
index f451357e211..d9d39a387d0 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -1,5 +1,6 @@
import { Editor } from '@tiptap/vue-2';
import { isFunction } from 'lodash';
+import eventHubFactory from '~/helpers/event_hub_factory';
import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants';
import Attachment from '../extensions/attachment';
import Audio from '../extensions/audio';
@@ -38,6 +39,7 @@ import Loading from '../extensions/loading';
import MathInline from '../extensions/math_inline';
import OrderedList from '../extensions/ordered_list';
import Paragraph from '../extensions/paragraph';
+import PasteMarkdown from '../extensions/paste_markdown';
import Reference from '../extensions/reference';
import Strike from '../extensions/strike';
import Subscript from '../extensions/subscript';
@@ -54,6 +56,7 @@ import Video from '../extensions/video';
import WordBreak from '../extensions/word_break';
import { ContentEditor } from './content_editor';
import createMarkdownSerializer from './markdown_serializer';
+import createMarkdownDeserializer from './markdown_deserializer';
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
const createTiptapEditor = ({ extensions = [], ...options } = {}) =>
@@ -78,8 +81,10 @@ export const createContentEditor = ({
throw new Error(PROVIDE_SERIALIZER_OR_RENDERER_ERROR);
}
+ const eventHub = eventHubFactory();
+
const builtInContentEditorExtensions = [
- Attachment.configure({ uploadsPath, renderMarkdown }),
+ Attachment.configure({ uploadsPath, renderMarkdown, eventHub }),
Audio,
Blockquote,
Bold,
@@ -116,6 +121,7 @@ export const createContentEditor = ({
MathInline,
OrderedList,
Paragraph,
+ PasteMarkdown.configure({ renderMarkdown, eventHub }),
Reference,
Strike,
Subscript,
@@ -135,7 +141,8 @@ export const createContentEditor = ({
const allExtensions = [...builtInContentEditorExtensions, ...extensions];
const trackedExtensions = allExtensions.map(trackInputRulesAndShortcuts);
const tiptapEditor = createTiptapEditor({ extensions: trackedExtensions, ...tiptapOptions });
- const serializer = createMarkdownSerializer({ render: renderMarkdown, serializerConfig });
+ const serializer = createMarkdownSerializer({ serializerConfig });
+ const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
- return new ContentEditor({ tiptapEditor, serializer });
+ return new ContentEditor({ tiptapEditor, serializer, eventHub, deserializer });
};
diff --git a/app/assets/javascripts/content_editor/services/markdown_deserializer.js b/app/assets/javascripts/content_editor/services/markdown_deserializer.js
new file mode 100644
index 00000000000..cd4863d8eac
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/markdown_deserializer.js
@@ -0,0 +1,33 @@
+import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
+
+export default ({ render }) => {
+ /**
+ * Converts a Markdown string into a ProseMirror JSONDocument based
+ * on a ProseMirror schema.
+ *
+ * @param {Object} options — The schema and content for deserialization
+ * @param {ProseMirror.Schema} params.schema A ProseMirror schema that defines
+ * the types of content supported in the document
+ * @param {String} params.content An arbitrary markdown string
+ *
+ * @returns An object with the following properties:
+ * - document: A ProseMirror document object generated from the deserialized Markdown
+ * - dom: The Markdown Deserializer renders Markdown as HTML to generate the ProseMirror
+ * document. The dom property contains the HTML generated from the Markdown Source.
+ */
+ return {
+ deserialize: async ({ schema, content }) => {
+ const html = await render(content);
+
+ if (!html) return {};
+
+ const parser = new DOMParser();
+ const { body } = parser.parseFromString(html, 'text/html');
+
+ // append original source as a comment that nodes can access
+ body.append(document.createComment(content));
+
+ return { document: ProseMirrorDOMParser.fromSchema(schema).parse(body), dom: body };
+ },
+ };
+};
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index 925b411e51c..eaaf69c3068 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -1,4 +1,3 @@
-import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
import {
MarkdownSerializer as ProseMirrorMarkdownSerializer,
defaultMarkdownSerializer,
@@ -237,31 +236,7 @@ const defaultSerializerConfig = {
* that parses the Markdown and converts it into HTML.
* @returns a markdown serializer
*/
-export default ({ render = () => null, serializerConfig = {} } = {}) => ({
- /**
- * Converts a Markdown string into a ProseMirror JSONDocument based
- * on a ProseMirror schema.
- * @param {ProseMirror.Schema} params.schema A ProseMirror schema that defines
- * the types of content supported in the document
- * @param {String} params.content An arbitrary markdown string
- * @returns A ProseMirror JSONDocument
- */
- deserialize: async ({ schema, content }) => {
- const html = await render(content);
-
- if (!html) return null;
-
- const parser = new DOMParser();
- const { body } = parser.parseFromString(html, 'text/html');
-
- // append original source as a comment that nodes can access
- body.append(document.createComment(content));
-
- const state = ProseMirrorDOMParser.fromSchema(schema).parse(body);
-
- return state.toJSON();
- },
-
+export default ({ serializerConfig = {} } = {}) => ({
/**
* Converts a ProseMirror JSONDocument based
* on a ProseMirror schema into Markdown
diff --git a/app/assets/javascripts/content_editor/services/markdown_sourcemap.js b/app/assets/javascripts/content_editor/services/markdown_sourcemap.js
index a1199589c9b..4285e04bbab 100644
--- a/app/assets/javascripts/content_editor/services/markdown_sourcemap.js
+++ b/app/assets/javascripts/content_editor/services/markdown_sourcemap.js
@@ -1,7 +1,9 @@
-const getFullSource = (element) => {
+import { isString } from 'lodash';
+
+export const getFullSource = (element) => {
const commentNode = element.ownerDocument.body.lastChild;
- if (commentNode.nodeName === '#comment') {
+ if (commentNode?.nodeName === '#comment' && isString(commentNode.textContent)) {
return commentNode.textContent.split('\n');
}
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index 4d5a54c0347..5fdd294aa96 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -259,11 +259,16 @@ export function renderContent(state, node, forceRenderInline) {
}
}
-export function renderHTMLNode(tagName, forceRenderInline = false) {
+export function renderHTMLNode(tagName, forceRenderContentInline = false) {
return (state, node) => {
renderTagOpen(state, tagName, node.attrs);
- renderContent(state, node, forceRenderInline);
+ renderContent(state, node, forceRenderContentInline);
renderTagClose(state, tagName, false);
+
+ if (forceRenderContentInline) {
+ state.closeBlock(node);
+ state.flushClose();
+ }
};
}
diff --git a/app/assets/javascripts/content_editor/services/upload_helpers.js b/app/assets/javascripts/content_editor/services/upload_helpers.js
index f5bf2742748..1abecb8f414 100644
--- a/app/assets/javascripts/content_editor/services/upload_helpers.js
+++ b/app/assets/javascripts/content_editor/services/upload_helpers.js
@@ -1,3 +1,4 @@
+import { VARIANT_DANGER } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import { extractFilename, readFileAsDataURL } from './utils';
@@ -49,7 +50,7 @@ export const uploadFile = async ({ uploadsPath, renderMarkdown, file }) => {
return extractAttachmentLinkUrl(rendered);
};
-const uploadImage = async ({ editor, file, uploadsPath, renderMarkdown }) => {
+const uploadImage = async ({ editor, file, uploadsPath, renderMarkdown, eventHub }) => {
const encodedSrc = await readFileAsDataURL(file);
const { view } = editor;
@@ -72,14 +73,14 @@ const uploadImage = async ({ editor, file, uploadsPath, renderMarkdown }) => {
);
} catch (e) {
editor.commands.deleteRange({ from: position, to: position + 1 });
- editor.emit('alert', {
+ eventHub.$emit('alert', {
message: __('An error occurred while uploading the image. Please try again.'),
- variant: 'danger',
+ variant: VARIANT_DANGER,
});
}
};
-const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown }) => {
+const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown, eventHub }) => {
await Promise.resolve();
const { view } = editor;
@@ -103,23 +104,23 @@ const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown }) =
);
} catch (e) {
editor.commands.deleteRange({ from, to: from + 1 });
- editor.emit('alert', {
+ eventHub.$emit('alert', {
message: __('An error occurred while uploading the file. Please try again.'),
- variant: 'danger',
+ variant: VARIANT_DANGER,
});
}
};
-export const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown }) => {
+export const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown, eventHub }) => {
if (!file) return false;
if (acceptedMimes.image.includes(file?.type)) {
- uploadImage({ editor, file, uploadsPath, renderMarkdown });
+ uploadImage({ editor, file, uploadsPath, renderMarkdown, eventHub });
return true;
}
- uploadAttachment({ editor, file, uploadsPath, renderMarkdown });
+ uploadAttachment({ editor, file, uploadsPath, renderMarkdown, eventHub });
return true;
};