diff options
Diffstat (limited to 'app/assets/javascripts/content_editor')
13 files changed, 122 insertions, 17 deletions
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue index a9668ebdb69..98b7203778f 100644 --- a/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue +++ b/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue @@ -166,9 +166,7 @@ export default { icon="arrow-left" @click.prevent.stop="showCustomLanguageInput = false" /> - <p - class="gl-text-center gl-new-dropdown-header-top gl-mb-0! gl-border-none! gl-pb-1!" - > + <p class="gl-text-center gl-dropdown-header-top gl-mb-0! gl-border-none! gl-pb-1!"> {{ __('Create custom type') }} </p> </div> diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue index 22381377389..53a37fc0c51 100644 --- a/app/assets/javascripts/content_editor/components/content_editor.vue +++ b/app/assets/javascripts/content_editor/components/content_editor.vue @@ -11,7 +11,7 @@ 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 FormattingToolbar from './formatting_toolbar.vue'; import LoadingIndicator from './loading_indicator.vue'; export default { @@ -20,7 +20,7 @@ export default { ContentEditorAlert, ContentEditorProvider, TiptapEditorContent, - TopToolbar, + FormattingToolbar, FormattingBubbleMenu, CodeBlockBubbleMenu, LinkBubbleMenu, @@ -57,6 +57,11 @@ export default { default: false, validator: (autofocus) => TIPTAP_AUTOFOCUS_OPTIONS.includes(autofocus), }, + useBottomToolbar: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -163,8 +168,8 @@ export default { class="md-area" :class="{ 'is-focused': focused }" > - <top-toolbar ref="toolbar" class="gl-mb-4" /> - <div class="gl-relative"> + <formatting-toolbar v-if="!useBottomToolbar" ref="toolbar" class="gl-border-b" /> + <div class="gl-relative gl-mt-4"> <formatting-bubble-menu /> <code-block-bubble-menu /> <link-bubble-menu /> @@ -176,6 +181,7 @@ export default { /> <loading-indicator v-if="isLoading" /> </div> + <formatting-toolbar v-if="useBottomToolbar" ref="toolbar" class="gl-border-t" /> </div> </div> </content-editor-provider> diff --git a/app/assets/javascripts/content_editor/components/top_toolbar.vue b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue index 460368b6a11..8a25ad3fd96 100644 --- a/app/assets/javascripts/content_editor/components/top_toolbar.vue +++ b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue @@ -24,9 +24,7 @@ export default { }; </script> <template> - <div - class="gl-display-flex gl-flex-wrap gl-pb-3 gl-pt-3 gl-border-b-solid gl-border-b-1 gl-border-b-gray-200" - > + <div class="gl-display-flex gl-flex-wrap gl-pb-3 gl-pt-3"> <toolbar-text-style-dropdown data-testid="text-styles" class="gl-mr-3" diff --git a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue index 001b34a00fa..37e6ef61d50 100644 --- a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue +++ b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue @@ -210,10 +210,10 @@ export default { <template> <ul :class="{ show: items.length > 0 }" - class="gl-new-dropdown dropdown-menu gl-relative" + class="gl-dropdown dropdown-menu gl-relative" data-testid="content-editor-suggestions-dropdown" > - <div class="gl-new-dropdown-inner gl-overflow-y-auto"> + <div class="gl-dropdown-inner gl-overflow-y-auto"> <gl-dropdown-item v-for="(item, index) in items" ref="dropdownItems" diff --git a/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue b/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue index 6bb122153ef..93b31ea7d20 100644 --- a/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue +++ b/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue @@ -58,6 +58,9 @@ export default { right lazy > + <gl-dropdown-item @click="insert('comment')"> + {{ __('Comment') }} + </gl-dropdown-item> <gl-dropdown-item @click="insert('codeBlock')"> {{ __('Code block') }} </gl-dropdown-item> 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 27432b1e18b..1d85bfcc965 100644 --- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js +++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js @@ -23,6 +23,10 @@ export default CodeBlockLowlight.extend({ // eslint-disable-next-line @gitlab/require-i18n-strings default: 'code highlight', }, + langParams: { + default: null, + parseHTML: (element) => element.dataset.langParams, + }, }; }, addInputRules() { diff --git a/app/assets/javascripts/content_editor/extensions/comment.js b/app/assets/javascripts/content_editor/extensions/comment.js new file mode 100644 index 00000000000..8e247e552a3 --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/comment.js @@ -0,0 +1,49 @@ +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/image.js b/app/assets/javascripts/content_editor/extensions/image.js index 65849ec4d0d..fc4c108b773 100644 --- a/app/assets/javascripts/content_editor/extensions/image.js +++ b/app/assets/javascripts/content_editor/extensions/image.js @@ -52,6 +52,22 @@ export default Image.extend({ return img.getAttribute('title'); }, }, + width: { + default: null, + parseHTML: (element) => { + const img = resolveImageEl(element); + + return img.getAttribute('width'); + }, + }, + height: { + default: null, + parseHTML: (element) => { + const img = resolveImageEl(element); + + return img.getAttribute('height'); + }, + }, isReference: { default: false, renderHTML: () => '', @@ -76,6 +92,8 @@ export default Image.extend({ src: HTMLAttributes.src, alt: HTMLAttributes.alt, title: HTMLAttributes.title, + width: HTMLAttributes.width, + height: HTMLAttributes.height, }, ]; }, diff --git a/app/assets/javascripts/content_editor/extensions/reference_label.js b/app/assets/javascripts/content_editor/extensions/reference_label.js index 716e191c3d5..9dff0b7a689 100644 --- a/app/assets/javascripts/content_editor/extensions/reference_label.js +++ b/app/assets/javascripts/content_editor/extensions/reference_label.js @@ -1,5 +1,5 @@ import { VueNodeViewRenderer } from '@tiptap/vue-2'; -import { SCOPED_LABEL_DELIMITER } from '~/vue_shared/components/sidebar/labels_select_widget/constants'; +import { SCOPED_LABEL_DELIMITER } from '~/sidebar/components/labels/labels_select_widget/constants'; import LabelWrapper from '../components/wrappers/label.vue'; import Reference from './reference'; 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 ba9ce705c62..61c6be574d0 100644 --- a/app/assets/javascripts/content_editor/services/create_content_editor.js +++ b/app/assets/javascripts/content_editor/services/create_content_editor.js @@ -10,6 +10,7 @@ import BulletList from '../extensions/bullet_list'; import Code from '../extensions/code'; import CodeBlockHighlight from '../extensions/code_block_highlight'; import ColorChip from '../extensions/color_chip'; +import Comment from '../extensions/comment'; import DescriptionItem from '../extensions/description_item'; import DescriptionList from '../extensions/description_list'; import Details from '../extensions/details'; @@ -100,6 +101,7 @@ export const createContentEditor = ({ BulletList, Code, ColorChip, + Comment, CodeBlockHighlight, DescriptionItem, DescriptionList, diff --git a/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js b/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js index fa46bd9ff81..796dc06ad93 100644 --- a/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js +++ b/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js @@ -1,4 +1,5 @@ import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model'; +import { replaceCommentsWith } from '~/lib/utils/dom_utils'; export default ({ render }) => { /** @@ -22,7 +23,9 @@ export default ({ render }) => { if (!html) return {}; const parser = new DOMParser(); - const { body } = parser.parseFromString(html, 'text/html'); + const { body } = parser.parseFromString(`<body>${html}</body>`, 'text/html'); + + replaceCommentsWith(body, 'comment'); // append original source as a comment that nodes can access body.append(document.createComment(markdown)); diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js index 958c27c281a..4e29f85004b 100644 --- a/app/assets/javascripts/content_editor/services/markdown_serializer.js +++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js @@ -12,6 +12,7 @@ import DescriptionItem from '../extensions/description_item'; import DescriptionList from '../extensions/description_list'; import Details from '../extensions/details'; import DetailsContent from '../extensions/details_content'; +import Comment from '../extensions/comment'; import Diagram from '../extensions/diagram'; import Emoji from '../extensions/emoji'; import Figure from '../extensions/figure'; @@ -50,6 +51,7 @@ import Text from '../extensions/text'; import Video from '../extensions/video'; import WordBreak from '../extensions/word_break'; import { + renderComment, renderCodeBlock, renderHardBreak, renderTable, @@ -130,6 +132,7 @@ const defaultSerializerConfig = { }), [BulletList.name]: preserveUnchanged(renderBulletList), [CodeBlockHighlight.name]: preserveUnchanged(renderCodeBlock), + [Comment.name]: renderComment, [Diagram.name]: preserveUnchanged(renderCodeBlock), [DescriptionList.name]: renderHTMLNode('dl', true), [DescriptionItem.name]: (state, node, parent, index) => { diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js index 5c0cb21075a..131c79357bf 100644 --- a/app/assets/javascripts/content_editor/services/serialization_helpers.js +++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js @@ -308,7 +308,7 @@ export function renderHardBreak(state, node, parent, index) { } export function renderImage(state, node) { - const { alt, canonicalSrc, src, title, isReference } = node.attrs; + const { alt, canonicalSrc, src, title, width, height, isReference } = node.attrs; if (isString(src) || isString(canonicalSrc)) { const quotedTitle = title ? ` ${state.quote(title)}` : ''; @@ -316,7 +316,17 @@ export function renderImage(state, node) { ? `[${canonicalSrc}]` : `(${state.esc(canonicalSrc || src)}${quotedTitle})`; - state.write(`![${state.esc(alt || '')}]${sourceExpression}`); + const sizeAttributes = []; + if (width) { + sizeAttributes.push(`width=${JSON.stringify(width)}`); + } + if (height) { + sizeAttributes.push(`height=${JSON.stringify(height)}`); + } + + const attributes = sizeAttributes.length ? `{${sizeAttributes.join(' ')}}` : ''; + + state.write(`![${state.esc(alt || '')}]${sourceExpression}${attributes}`); } } @@ -324,8 +334,19 @@ export function renderPlayable(state, node) { renderImage(state, node); } +export function renderComment(state, node) { + state.text('<!--'); + state.text(node.textContent); + state.text('-->'); + state.closeBlock(node); +} + export function renderCodeBlock(state, node) { - state.write(`\`\`\`${node.attrs.language || ''}\n`); + state.write( + `\`\`\`${ + (node.attrs.language || '') + (node.attrs.langParams ? `:${node.attrs.langParams}` : '') + }\n`, + ); state.text(node.textContent, false); state.ensureNewLine(); state.write('```'); |