diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-19 17:16:28 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-19 17:16:28 +0300 |
commit | e4384360a16dd9a19d4d2d25d0ef1f2b862ed2a6 (patch) | |
tree | 2fcdfa7dcdb9db8f5208b2562f4b4e803d671243 /app/assets/javascripts/content_editor/extensions | |
parent | ffda4e7bcac36987f936b4ba515995a6698698f0 (diff) |
Add latest changes from gitlab-org/gitlab@16-2-stable-eev16.2.0-rc42
Diffstat (limited to 'app/assets/javascripts/content_editor/extensions')
-rw-r--r-- | app/assets/javascripts/content_editor/extensions/code_suggestion.js | 81 | ||||
-rw-r--r-- | app/assets/javascripts/content_editor/extensions/comment.js | 49 | ||||
-rw-r--r-- | app/assets/javascripts/content_editor/extensions/copy_paste.js (renamed from app/assets/javascripts/content_editor/extensions/paste_markdown.js) | 62 | ||||
-rw-r--r-- | app/assets/javascripts/content_editor/extensions/hard_break.js | 4 | ||||
-rw-r--r-- | app/assets/javascripts/content_editor/extensions/image.js | 5 | ||||
-rw-r--r-- | app/assets/javascripts/content_editor/extensions/loading.js | 23 | ||||
-rw-r--r-- | app/assets/javascripts/content_editor/extensions/paragraph.js | 10 | ||||
-rw-r--r-- | app/assets/javascripts/content_editor/extensions/reference.js | 6 |
8 files changed, 175 insertions, 65 deletions
diff --git a/app/assets/javascripts/content_editor/extensions/code_suggestion.js b/app/assets/javascripts/content_editor/extensions/code_suggestion.js new file mode 100644 index 00000000000..c70a96769fb --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/code_suggestion.js @@ -0,0 +1,81 @@ +import { lowlight } from 'lowlight/lib/core'; +import { textblockTypeInputRule } from '@tiptap/core'; +import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants'; +import { memoizedGet } from '../services/utils'; +import CodeBlockHighlight from './code_block_highlight'; + +const backtickInputRegex = /^```suggestion[\s\n]$/; + +export default CodeBlockHighlight.extend({ + name: 'codeSuggestion', + + isolating: true, + + addOptions() { + return { + lowlight, + config: {}, + }; + }, + + addAttributes() { + return { + ...this.parent?.(), + language: { + default: 'suggestion', + }, + isCodeSuggestion: { + default: true, + }, + }; + }, + + addCommands() { + const ext = this; + + return { + insertCodeSuggestion: (attributes) => async ({ editor }) => { + // do not insert a new suggestion if already inside a suggestion + if (editor.isActive('codeSuggestion')) return false; + + const rawPath = ext.options.config.diffFile.view_path.replace('/blob/', '/raw/'); + const allLines = (await memoizedGet(rawPath)).split('\n'); + const { line } = ext.options.config; + let { lines } = ext.options.config; + + if (!lines.length) lines = [line]; + + const content = lines.map((l) => allLines[l.new_line - 1]).join('\n'); + const lineNumbers = `-${lines.length - 1}+0`; + + editor.commands.insertContent({ + type: 'codeSuggestion', + attrs: { langParams: lineNumbers, ...attributes }, + // empty strings are not allowed in text nodes + content: [{ type: 'text', text: content || ' ' }], + }); + + return true; + }, + }; + }, + + parseHTML() { + return [ + { + priority: PARSE_HTML_PRIORITY_HIGHEST, + tag: 'pre[lang="suggestion"]', + }, + ]; + }, + + addInputRules() { + return [ + textblockTypeInputRule({ + find: backtickInputRegex, + type: this.type, + getAttributes: () => ({ language: 'suggestion', langParams: '-0+0' }), + }), + ]; + }, +}); diff --git a/app/assets/javascripts/content_editor/extensions/comment.js b/app/assets/javascripts/content_editor/extensions/comment.js deleted file mode 100644 index 8e247e552a3..00000000000 --- a/app/assets/javascripts/content_editor/extensions/comment.js +++ /dev/null @@ -1,49 +0,0 @@ -import { Node, textblockTypeInputRule } from '@tiptap/core'; - -export const commentInputRegex = /^<!--[\s\n]$/; - -export default Node.create({ - name: 'comment', - content: 'text*', - marks: '', - group: 'block', - code: true, - isolating: true, - defining: true, - - parseHTML() { - return [ - { - tag: 'comment', - preserveWhitespace: 'full', - getContent(element, schema) { - const node = schema.node('paragraph', {}, [ - schema.text( - element.textContent.replace(/&#x([0-9A-F]{2,4});/gi, (_, code) => - String.fromCharCode(parseInt(code, 16)), - ) || ' ', - ), - ]); - return node.content; - }, - }, - ]; - }, - - renderHTML() { - return [ - 'pre', - { class: 'gl-p-0 gl-border-0 gl-bg-transparent gl-text-gray-300' }, - ['span', { class: 'content-editor-comment' }, 0], - ]; - }, - - addInputRules() { - return [ - textblockTypeInputRule({ - find: commentInputRegex, - type: this.type, - }), - ]; - }, -}); diff --git a/app/assets/javascripts/content_editor/extensions/paste_markdown.js b/app/assets/javascripts/content_editor/extensions/copy_paste.js index db13438de5e..f484ce98e90 100644 --- a/app/assets/javascripts/content_editor/extensions/paste_markdown.js +++ b/app/assets/javascripts/content_editor/extensions/copy_paste.js @@ -2,11 +2,13 @@ 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 { uniqueId } from 'lodash'; import { __ } from '~/locale'; import { VARIANT_DANGER } from '~/alert'; import createMarkdownDeserializer from '../services/gl_api_markdown_deserializer'; import { ALERT_EVENT, EXTENSION_PRIORITY_HIGHEST } from '../constants'; import CodeBlockHighlight from './code_block_highlight'; +import CodeSuggestion from './code_suggestion'; import Diagram from './diagram'; import Frontmatter from './frontmatter'; @@ -14,7 +16,12 @@ 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]; +const CODE_BLOCK_NODE_TYPES = [ + CodeBlockHighlight.name, + CodeSuggestion.name, + Diagram.name, + Frontmatter.name, +]; function parseHTML(schema, html) { const parser = new DOMParser(); @@ -24,8 +31,23 @@ function parseHTML(schema, html) { return { document: ProseMirrorDOMParser.fromSchema(schema).parse(body) }; } +const findLoader = (editor, loaderId) => { + let position; + + editor.view.state.doc.descendants((descendant, pos) => { + if (descendant.type.name === 'loading' && descendant.attrs.id === loaderId) { + position = pos; + return false; + } + + return true; + }); + + return position; +}; + export default Extension.create({ - name: 'pasteMarkdown', + name: 'copyPaste', priority: EXTENSION_PRIORITY_HIGHEST, addOptions() { return { @@ -35,7 +57,7 @@ export default Extension.create({ }, addCommands() { return { - pasteContent: (content = '', processMarkdown = true) => async () => { + pasteContent: (content = '', processMarkdown = true) => () => { const { editor, options } = this; const { renderMarkdown, eventHub } = options; const deserializer = createMarkdownDeserializer({ render: renderMarkdown }); @@ -43,23 +65,37 @@ export default Extension.create({ 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 pasteSchema = new Schema(pasteSchemaSpec); const promise = processMarkdown - ? deserializer.deserialize({ schema, markdown: content }) - : Promise.resolve(parseHTML(schema, content)); - - promise - .then(({ document }) => { + ? deserializer.deserialize({ schema: pasteSchema, markdown: content }) + : Promise.resolve(parseHTML(pasteSchema, content)); + const loaderId = uniqueId('loading'); + + Promise.resolve() + .then(() => { + editor.commands.insertContent({ type: 'loading', attrs: { id: loaderId } }); + return promise; + }) + .then(async ({ document }) => { if (!document) return; - const { firstChild } = document.content; + const pos = findLoader(editor, loaderId); + if (!pos) return; + + const { firstChild, childCount } = document.content; const toPaste = - document.content.childCount === 1 && firstChild.type.name === 'paragraph' + childCount === 1 && firstChild.type.name === 'paragraph' ? firstChild.content : document.content; - editor.commands.insertContent(toPaste.toJSON()); + editor + .chain() + .deleteRange({ from: pos, to: pos + 1 }) + .insertContentAt(pos, toPaste.toJSON(), { + updateSelection: false, + }) + .run(); }) .catch(() => { eventHub.$emit(ALERT_EVENT, { @@ -94,7 +130,7 @@ export default Extension.create({ return [ new Plugin({ - key: new PluginKey('pasteMarkdown'), + key: new PluginKey('copyPaste'), props: { handleDOMEvents: { copy: handleCutAndCopy, diff --git a/app/assets/javascripts/content_editor/extensions/hard_break.js b/app/assets/javascripts/content_editor/extensions/hard_break.js index fb81c6b79b6..6d7ff92e64b 100644 --- a/app/assets/javascripts/content_editor/extensions/hard_break.js +++ b/app/assets/javascripts/content_editor/extensions/hard_break.js @@ -2,8 +2,6 @@ import { HardBreak } from '@tiptap/extension-hard-break'; export default HardBreak.extend({ addKeyboardShortcuts() { - return { - 'Shift-Enter': () => this.editor.commands.setHardBreak(), - }; + return {}; }, }); diff --git a/app/assets/javascripts/content_editor/extensions/image.js b/app/assets/javascripts/content_editor/extensions/image.js index 58c16297886..d245b86543f 100644 --- a/app/assets/javascripts/content_editor/extensions/image.js +++ b/app/assets/javascripts/content_editor/extensions/image.js @@ -1,5 +1,7 @@ import { Image } from '@tiptap/extension-image'; +import { VueNodeViewRenderer } from '@tiptap/vue-2'; import { PARSE_HTML_PRIORITY_HIGH } from '../constants'; +import ImageWrapper from '../components/wrappers/image.vue'; const resolveImageEl = (element) => element.nodeName === 'IMG' ? element : element.querySelector('img'); @@ -97,4 +99,7 @@ export default Image.extend({ }, ]; }, + addNodeView() { + return VueNodeViewRenderer(ImageWrapper); + }, }); diff --git a/app/assets/javascripts/content_editor/extensions/loading.js b/app/assets/javascripts/content_editor/extensions/loading.js new file mode 100644 index 00000000000..0115fb10d5d --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/loading.js @@ -0,0 +1,23 @@ +import { Node } from '@tiptap/core'; + +export default Node.create({ + name: 'loading', + inline: true, + group: 'inline', + + addAttributes() { + return { + id: { + default: null, + }, + }; + }, + + renderHTML() { + return [ + 'span', + { class: 'gl-display-inline-flex gl-align-items-center' }, + ['span', { class: 'gl-dots-loader gl-mx-2' }, ['span']], + ]; + }, +}); diff --git a/app/assets/javascripts/content_editor/extensions/paragraph.js b/app/assets/javascripts/content_editor/extensions/paragraph.js index c63b64fd784..bddd8b38b06 100644 --- a/app/assets/javascripts/content_editor/extensions/paragraph.js +++ b/app/assets/javascripts/content_editor/extensions/paragraph.js @@ -9,4 +9,14 @@ export default Paragraph.extend({ }, }; }, + + addKeyboardShortcuts() { + return { + 'Shift-Enter': async () => { + // can only delegate one shortcut to another async + await Promise.resolve(); + this.editor.commands.enter(); + }, + }; + }, }); diff --git a/app/assets/javascripts/content_editor/extensions/reference.js b/app/assets/javascripts/content_editor/extensions/reference.js index ef69b9bbda6..fd248709b5a 100644 --- a/app/assets/javascripts/content_editor/extensions/reference.js +++ b/app/assets/javascripts/content_editor/extensions/reference.js @@ -63,6 +63,12 @@ export default Node.create({ }; }, + addCommands() { + return { + insertQuickAction: () => ({ commands }) => commands.insertContent('<p>/</p>'), + }; + }, + addInputRules() { const { editor } = this; const { assetResolver } = this.options; |