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:
Diffstat (limited to 'app/assets/javascripts/content_editor/extensions')
-rw-r--r--app/assets/javascripts/content_editor/extensions/code.js10
-rw-r--r--app/assets/javascripts/content_editor/extensions/description_item.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/details_content.js12
-rw-r--r--app/assets/javascripts/content_editor/extensions/drawio_diagram.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/link.js1
-rw-r--r--app/assets/javascripts/content_editor/extensions/paste_markdown.js102
-rw-r--r--app/assets/javascripts/content_editor/extensions/reference.js84
-rw-r--r--app/assets/javascripts/content_editor/extensions/reference_label.js14
-rw-r--r--app/assets/javascripts/content_editor/extensions/suggestions.js2
9 files changed, 197 insertions, 39 deletions
diff --git a/app/assets/javascripts/content_editor/extensions/code.js b/app/assets/javascripts/content_editor/extensions/code.js
index 53f6d9b995c..8477c8dbd28 100644
--- a/app/assets/javascripts/content_editor/extensions/code.js
+++ b/app/assets/javascripts/content_editor/extensions/code.js
@@ -1,12 +1,22 @@
+import { Mark } from '@tiptap/core';
import Code from '@tiptap/extension-code';
import { EXTENSION_PRIORITY_LOWER } from '../constants';
export default Code.extend({
excludes: null,
+
/**
* Reduce the rendering priority of the code mark to
* ensure the bold, italic, and strikethrough marks
* are rendered first.
*/
priority: EXTENSION_PRIORITY_LOWER,
+
+ addKeyboardShortcuts() {
+ return {
+ ArrowRight: () => {
+ return Mark.handleExit({ editor: this.editor, mark: this });
+ },
+ };
+ },
});
diff --git a/app/assets/javascripts/content_editor/extensions/description_item.js b/app/assets/javascripts/content_editor/extensions/description_item.js
index 06fecf8196d..d3fa4bb84bd 100644
--- a/app/assets/javascripts/content_editor/extensions/description_item.js
+++ b/app/assets/javascripts/content_editor/extensions/description_item.js
@@ -39,9 +39,13 @@ export default Node.create({
addKeyboardShortcuts() {
return {
Enter: () => {
+ if (!this.editor.isActive('descriptionItem')) return false;
+
return this.editor.commands.splitListItem('descriptionItem');
},
Tab: () => {
+ if (!this.editor.isActive('descriptionItem')) return false;
+
const { isTerm } = this.editor.getAttributes('descriptionItem');
if (isTerm)
return this.editor.commands.updateAttributes('descriptionItem', { isTerm: !isTerm });
@@ -49,6 +53,8 @@ export default Node.create({
return false;
},
'Shift-Tab': () => {
+ if (!this.editor.isActive('descriptionItem')) return false;
+
const { isTerm } = this.editor.getAttributes('descriptionItem');
if (isTerm) return this.editor.commands.liftListItem('descriptionItem');
diff --git a/app/assets/javascripts/content_editor/extensions/details_content.js b/app/assets/javascripts/content_editor/extensions/details_content.js
index fbe58664a10..61bef0729db 100644
--- a/app/assets/javascripts/content_editor/extensions/details_content.js
+++ b/app/assets/javascripts/content_editor/extensions/details_content.js
@@ -26,8 +26,16 @@ export default Node.create({
addKeyboardShortcuts() {
return {
- Enter: () => this.editor.commands.splitListItem('detailsContent'),
- 'Shift-Tab': () => this.editor.commands.liftListItem('detailsContent'),
+ Enter: () => {
+ if (!this.editor.isActive('detailsContent')) return false;
+
+ return this.editor.commands.splitListItem('detailsContent');
+ },
+ 'Shift-Tab': () => {
+ if (!this.editor.isActive('detailsContent')) return false;
+
+ return this.editor.commands.liftListItem('detailsContent');
+ },
};
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/drawio_diagram.js b/app/assets/javascripts/content_editor/extensions/drawio_diagram.js
index 8c3012ecf59..0d453919571 100644
--- a/app/assets/javascripts/content_editor/extensions/drawio_diagram.js
+++ b/app/assets/javascripts/content_editor/extensions/drawio_diagram.js
@@ -1,7 +1,6 @@
import { create } from '~/drawio/content_editor_facade';
import { launchDrawioEditor } from '~/drawio/drawio_editor';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
-import createAssetResolver from '../services/asset_resolver';
import Image from './image';
export default Image.extend({
@@ -10,7 +9,7 @@ export default Image.extend({
return {
...this.parent?.(),
uploadsPath: null,
- renderMarkdown: null,
+ assetResolver: null,
};
},
parseHTML() {
@@ -32,7 +31,7 @@ export default Image.extend({
tiptapEditor: this.editor,
drawioNodeName: this.name,
uploadsPath: this.options.uploadsPath,
- assetResolver: createAssetResolver({ renderMarkdown: this.options.renderMarkdown }),
+ assetResolver: this.options.assetResolver,
}),
});
},
diff --git a/app/assets/javascripts/content_editor/extensions/link.js b/app/assets/javascripts/content_editor/extensions/link.js
index b83814103d1..584e7b9e4f7 100644
--- a/app/assets/javascripts/content_editor/extensions/link.js
+++ b/app/assets/javascripts/content_editor/extensions/link.js
@@ -40,7 +40,6 @@ export default Link.extend({
},
addAttributes() {
return {
- ...this.parent?.(),
uploading: {
default: false,
renderHTML: ({ uploading }) => (uploading ? { class: 'with-attachment-icon' } : {}),
diff --git a/app/assets/javascripts/content_editor/extensions/paste_markdown.js b/app/assets/javascripts/content_editor/extensions/paste_markdown.js
index 82fa5ce6c1d..db13438de5e 100644
--- a/app/assets/javascripts/content_editor/extensions/paste_markdown.js
+++ b/app/assets/javascripts/content_editor/extensions/paste_markdown.js
@@ -1,5 +1,7 @@
+import OrderedMap from 'orderedmap';
import { Extension } from '@tiptap/core';
import { Plugin, PluginKey } from '@tiptap/pm/state';
+import { Schema, DOMParser as ProseMirrorDOMParser, DOMSerializer } from '@tiptap/pm/model';
import { __ } from '~/locale';
import { VARIANT_DANGER } from '~/alert';
import createMarkdownDeserializer from '../services/gl_api_markdown_deserializer';
@@ -9,47 +11,55 @@ import Diagram from './diagram';
import Frontmatter from './frontmatter';
const TEXT_FORMAT = 'text/plain';
+const GFM_FORMAT = 'text/x-gfm';
const HTML_FORMAT = 'text/html';
const VS_CODE_FORMAT = 'vscode-editor-data';
const CODE_BLOCK_NODE_TYPES = [CodeBlockHighlight.name, Diagram.name, Frontmatter.name];
+function parseHTML(schema, html) {
+ const parser = new DOMParser();
+ const startTag = '<body>';
+ const endTag = '</body>';
+ const { body } = parser.parseFromString(startTag + html + endTag, 'text/html');
+ return { document: ProseMirrorDOMParser.fromSchema(schema).parse(body) };
+}
+
export default Extension.create({
name: 'pasteMarkdown',
priority: EXTENSION_PRIORITY_HIGHEST,
addOptions() {
return {
renderMarkdown: null,
+ serializer: null,
};
},
addCommands() {
return {
- pasteMarkdown: (markdown) => () => {
+ pasteContent: (content = '', processMarkdown = true) => async () => {
const { editor, options } = this;
const { renderMarkdown, eventHub } = options;
const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
- deserializer
- .deserialize({ schema: editor.schema, markdown })
+ const pasteSchemaSpec = { ...editor.schema.spec };
+ pasteSchemaSpec.marks = OrderedMap.from(pasteSchemaSpec.marks).remove('span');
+ pasteSchemaSpec.nodes = OrderedMap.from(pasteSchemaSpec.nodes).remove('div').remove('pre');
+ const schema = new Schema(pasteSchemaSpec);
+
+ const promise = processMarkdown
+ ? deserializer.deserialize({ schema, markdown: content })
+ : Promise.resolve(parseHTML(schema, content));
+
+ promise
.then(({ document }) => {
- if (!document) {
- return;
- }
+ if (!document) return;
- const { state, view } = editor;
- const { tr, selection } = state;
const { firstChild } = document.content;
- const content =
+ const toPaste =
document.content.childCount === 1 && firstChild.type.name === 'paragraph'
? firstChild.content
: document.content;
- if (selection.to - selection.from > 0) {
- tr.replaceWith(selection.from, selection.to, content);
- } else {
- tr.insert(selection.from, content);
- }
-
- view.dispatch(tr);
+ editor.commands.insertContent(toPaste.toJSON());
})
.catch(() => {
eventHub.$emit(ALERT_EVENT, {
@@ -65,24 +75,57 @@ export default Extension.create({
addProseMirrorPlugins() {
let pasteRaw = false;
+ const handleCutAndCopy = (view, event) => {
+ const slice = view.state.selection.content();
+ const gfmContent = this.options.serializer.serialize({ doc: slice.content });
+ const documentFragment = DOMSerializer.fromSchema(view.state.schema).serializeFragment(
+ slice.content,
+ );
+ const div = document.createElement('div');
+ div.appendChild(documentFragment);
+
+ event.clipboardData.setData(TEXT_FORMAT, div.innerText);
+ event.clipboardData.setData(HTML_FORMAT, div.innerHTML);
+ event.clipboardData.setData(GFM_FORMAT, gfmContent);
+
+ event.preventDefault();
+ event.stopPropagation();
+ };
+
return [
new Plugin({
key: new PluginKey('pasteMarkdown'),
props: {
+ handleDOMEvents: {
+ copy: handleCutAndCopy,
+ cut: (view, event) => {
+ handleCutAndCopy(view, event);
+ this.editor.commands.deleteSelection();
+ },
+ },
handleKeyDown: (_, event) => {
pasteRaw = event.key === 'v' && (event.metaKey || event.ctrlKey) && event.shiftKey;
},
handlePaste: (view, event) => {
const { clipboardData } = event;
- const content = clipboardData.getData(TEXT_FORMAT);
- const { state } = view;
- const { tr, selection } = state;
- const { from, to } = selection;
+
+ const gfmContent = clipboardData.getData(GFM_FORMAT);
+
+ if (gfmContent) {
+ return this.editor.commands.pasteContent(gfmContent, true);
+ }
+
+ const textContent = clipboardData.getData(TEXT_FORMAT);
+ const htmlContent = clipboardData.getData(HTML_FORMAT);
+
+ const { from, to } = view.state.selection;
if (pasteRaw) {
- tr.insertText(content.replace(/^\s+|\s+$/gm, ''), from, to);
- view.dispatch(tr);
+ this.editor.commands.insertContentAt(
+ { from, to },
+ textContent.replace(/^\s+|\s+$/gm, ''),
+ );
return true;
}
@@ -91,18 +134,19 @@ export default Extension.create({
const vsCodeMeta = hasVsCode ? JSON.parse(clipboardData.getData(VS_CODE_FORMAT)) : {};
const language = vsCodeMeta.mode;
- if (!content || (hasHTML && !hasVsCode) || (hasVsCode && language !== 'markdown')) {
- return false;
- }
-
// if a code block is active, paste as plain text
- if (CODE_BLOCK_NODE_TYPES.some((type) => this.editor.isActive(type))) {
+ if (!textContent || CODE_BLOCK_NODE_TYPES.some((type) => this.editor.isActive(type))) {
return false;
}
- this.editor.commands.pasteMarkdown(content);
+ if (hasVsCode) {
+ return this.editor.commands.pasteContent(
+ language === 'markdown' ? textContent : `\`\`\`${language}\n${textContent}\n\`\`\``,
+ true,
+ );
+ }
- return true;
+ return this.editor.commands.pasteContent(hasHTML ? htmlContent : textContent, !hasHTML);
},
},
}),
diff --git a/app/assets/javascripts/content_editor/extensions/reference.js b/app/assets/javascripts/content_editor/extensions/reference.js
index b56aa8596a0..ef69b9bbda6 100644
--- a/app/assets/javascripts/content_editor/extensions/reference.js
+++ b/app/assets/javascripts/content_editor/extensions/reference.js
@@ -1,4 +1,4 @@
-import { Node } from '@tiptap/core';
+import { Node, InputRule } from '@tiptap/core';
import { VueNodeViewRenderer } from '@tiptap/vue-2';
import ReferenceWrapper from '../components/wrappers/reference.vue';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
@@ -8,6 +8,21 @@ const getAnchor = (element) => {
return element.querySelector('a');
};
+const findReference = (editor, reference) => {
+ let position;
+
+ editor.view.state.doc.descendants((descendant, pos) => {
+ if (descendant.isText && descendant.text.includes(reference)) {
+ position = pos + descendant.text.indexOf(reference);
+ return false;
+ }
+
+ return true;
+ });
+
+ return position;
+};
+
export default Node.create({
name: 'reference',
@@ -17,6 +32,12 @@ export default Node.create({
atom: true,
+ addOptions() {
+ return {
+ assetResolver: null,
+ };
+ },
+
addAttributes() {
return {
className: {
@@ -42,6 +63,54 @@ export default Node.create({
};
},
+ addInputRules() {
+ const { editor } = this;
+ const { assetResolver } = this.options;
+ const referenceInputRegex = /(?:^|\s)([\w/]*([!&#])\d+(\+?s?))(?:\s|\n)/m;
+ const referenceTypes = {
+ '#': 'issue',
+ '!': 'merge_request',
+ '&': 'epic',
+ };
+
+ return [
+ new InputRule({
+ find: referenceInputRegex,
+ handler: async ({ match }) => {
+ const [, referenceId, referenceSymbol, expansionType] = match;
+ const referenceType = referenceTypes[referenceSymbol];
+
+ const {
+ href,
+ text,
+ expandedText,
+ fullyExpandedText,
+ } = await assetResolver.resolveReference(referenceId);
+
+ if (!text) return;
+
+ let referenceText = text;
+ if (expansionType === '+') referenceText = expandedText;
+ if (expansionType === '+s') referenceText = fullyExpandedText;
+
+ const position = findReference(editor, referenceId);
+ if (!position) return;
+
+ editor.view.dispatch(
+ editor.state.tr.replaceWith(position, position + referenceId.length, [
+ this.type.create({
+ referenceType,
+ originalText: referenceId,
+ href,
+ text: referenceText,
+ }),
+ ]),
+ );
+ },
+ }),
+ ];
+ },
+
parseHTML() {
return [
{
@@ -51,6 +120,19 @@ export default Node.create({
];
},
+ renderHTML({ node }) {
+ return [
+ 'gl-reference',
+ {
+ 'data-reference-type': node.attrs.referenceType,
+ 'data-original-text': node.attrs.originalText,
+ href: node.attrs.href,
+ text: node.attrs.text,
+ },
+ node.attrs.text,
+ ];
+ },
+
addNodeView() {
return new VueNodeViewRenderer(ReferenceWrapper);
},
diff --git a/app/assets/javascripts/content_editor/extensions/reference_label.js b/app/assets/javascripts/content_editor/extensions/reference_label.js
index 0441f8ef8d2..9cd55a0f87c 100644
--- a/app/assets/javascripts/content_editor/extensions/reference_label.js
+++ b/app/assets/javascripts/content_editor/extensions/reference_label.js
@@ -4,7 +4,7 @@ import LabelWrapper from '../components/wrappers/reference_label.vue';
import Reference from './reference';
export default Reference.extend({
- name: 'reference_label',
+ name: 'referenceLabel',
addAttributes() {
return {
@@ -20,11 +20,21 @@ export default Reference.extend({
},
color: {
default: null,
- parseHTML: (element) => element.querySelector('.gl-label-text').style.backgroundColor,
+ parseHTML: (element) => {
+ let color = element.querySelector('.gl-label-text').style.backgroundColor;
+ if (!color || color.startsWith('var'))
+ color = element.style.getPropertyValue('--label-background-color');
+
+ return color;
+ },
},
};
},
+ addInputRules() {
+ return [];
+ },
+
parseHTML() {
return [{ tag: 'span.gl-label' }];
},
diff --git a/app/assets/javascripts/content_editor/extensions/suggestions.js b/app/assets/javascripts/content_editor/extensions/suggestions.js
index e72b5c7365c..f29222a5289 100644
--- a/app/assets/javascripts/content_editor/extensions/suggestions.js
+++ b/app/assets/javascripts/content_editor/extensions/suggestions.js
@@ -162,7 +162,7 @@ export default Node.create({
editor: this.editor,
char: '~',
dataSource: this.options.autocompleteDataSources.labels,
- nodeType: 'reference_label',
+ nodeType: 'referenceLabel',
nodeProps: {
referenceType: 'label',
},