diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-13 21:09:34 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-13 21:09:34 +0300 |
commit | 53d77359a0e6bf78bfc8ef8c72995eebe1f9e63b (patch) | |
tree | 444eb2851a18271c20ea6fd23fab0b02b7e4d9b2 /app/assets/javascripts/content_editor | |
parent | dd7da8bd31926a1210011856f7d66ee43fa65156 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/content_editor')
7 files changed, 147 insertions, 177 deletions
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue index 06b80a65528..cef446c4cf8 100644 --- a/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue +++ b/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue @@ -35,9 +35,6 @@ export default { ); }, }, - toggleLinkCommandParams: { - href: '', - }, }; </script> <template> @@ -122,8 +119,7 @@ export default { data-testid="link" content-type="link" icon-name="link" - editor-command="toggleLink" - :editor-command-params="$options.toggleLinkCommandParams" + editor-command="editLink" category="tertiary" size="medium" :label="__('Insert link')" diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/link_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/link_bubble_menu.vue index a4713eb3275..a3065be3772 100644 --- a/app/assets/javascripts/content_editor/components/bubble_menus/link_bubble_menu.vue +++ b/app/assets/javascripts/content_editor/components/bubble_menus/link_bubble_menu.vue @@ -8,6 +8,7 @@ import { GlButtonGroup, GlTooltipDirective as GlTooltip, } from '@gitlab/ui'; +import { getMarkType, getMarkRange } from '@tiptap/core'; import Link from '../../extensions/link'; import EditorStateObserver from '../editor_state_observer.vue'; import BubbleMenu from './bubble_menu.vue'; @@ -31,12 +32,36 @@ export default { return { linkHref: undefined, linkCanonicalSrc: undefined, - linkTitle: undefined, + linkText: undefined, isEditing: false, }; }, methods: { + linkIsEmpty() { + return ( + !this.linkCanonicalSrc && + !this.linkHref && + (!this.linkText || this.linkText === this.linkTextInDoc()) + ); + }, + + linkTextInDoc() { + const { state } = this.tiptapEditor; + const type = getMarkType(Link.name, state.schema); + let { selection: range } = state; + if (range.from === range.to) { + range = + getMarkRange(state.selection.$from, type) || + getMarkRange(state.selection.$to, type) || + {}; + } + + if (!range.from || !range.to) return ''; + + return state.doc.textBetween(range.from, range.to, ' '); + }, + shouldShow() { return this.tiptapEditor.isActive(Link.name); }, @@ -52,31 +77,51 @@ export default { this.isEditing = false; this.linkHref = await this.contentEditor.resolveUrl(this.linkCanonicalSrc); - - if (!this.linkCanonicalSrc && !this.linkHref) { - this.removeLink(); - } }, cancelEditingLink() { this.endEditingLink(); - this.updateLinkToState(); + + if (this.linkIsEmpty()) { + this.removeLink(); + } else { + this.updateLinkToState(); + } }, async saveEditedLink() { - if (!this.linkCanonicalSrc) { + const chain = this.tiptapEditor.chain().focus(); + + const attrs = { + href: this.linkCanonicalSrc, + canonicalSrc: this.linkCanonicalSrc, + }; + + // if nothing was entered by the user and the link is empty, remove it + // since we don't want to insert an empty link + if (this.linkIsEmpty()) { this.removeLink(); - } else { - this.tiptapEditor - .chain() - .focus() + return; + } + + if (!this.linkText) { + this.linkText = this.linkCanonicalSrc; + } + + // if link text was updated, insert a new link in the doc with the new text + if (this.linkTextInDoc() !== this.linkText) { + chain .extendMarkRange(Link.name) - .updateAttributes(Link.name, { - href: this.linkCanonicalSrc, - canonicalSrc: this.linkCanonicalSrc, - title: this.linkTitle, + .setMeta('preventAutolink', true) + .insertContent({ + marks: [{ type: Link.name, attrs }], + type: 'text', + text: this.linkText, }) .run(); + } else { + // if link text was not updated, just update the attributes + chain.updateAttributes(Link.name, attrs).run(); } this.endEditingLink(); @@ -84,22 +129,27 @@ export default { updateLinkToState() { const editor = this.tiptapEditor; - - const { href, title, canonicalSrc } = editor.getAttributes(Link.name); + const { href, canonicalSrc } = editor.getAttributes(Link.name); + const text = this.linkTextInDoc(); if ( canonicalSrc === this.linkCanonicalSrc && href === this.linkHref && - title === this.linkTitle + text === this.linkText ) { return; } - this.linkTitle = title; + this.linkText = text; this.linkHref = href; this.linkCanonicalSrc = canonicalSrc || href; + }, - this.isEditing = !this.linkCanonicalSrc; + onTransaction({ transaction }) { + this.linkText = this.linkTextInDoc(); + if (transaction.getMeta('creatingLink')) { + this.isEditing = true; + } }, copyLinkHref() { @@ -107,31 +157,49 @@ export default { }, removeLink() { - this.tiptapEditor.chain().focus().extendMarkRange(Link.name).unsetLink().run(); + const chain = this.tiptapEditor.chain().focus(); + if (this.linkTextInDoc()) { + chain.unsetLink().run(); + } else { + chain + .insertContent({ + type: 'text', + text: ' ', + }) + .extendMarkRange(Link.name) + .unsetLink() + .deleteSelection() + .run(); + } }, resetBubbleMenuState() { - this.linkTitle = undefined; + this.linkText = undefined; this.linkHref = undefined; this.linkCanonicalSrc = undefined; }, }, tippyOptions: { placement: 'bottom', + appendTo: () => document.body, }, }; </script> <template> - <bubble-menu - data-testid="link-bubble-menu" - class="gl-shadow gl-rounded-base gl-bg-white" - plugin-key="bubbleMenuLink" - :should-show="shouldShow" - :tippy-options="$options.tippyOptions" - @show="updateLinkToState" - @hidden="resetBubbleMenuState" + <editor-state-observer + :debounce="0" + @transaction="onTransaction" + @selectionUpdate="updateLinkToState" > - <editor-state-observer @selectionUpdate="updateLinkToState"> + <bubble-menu + data-testid="link-bubble-menu" + class="gl-shadow gl-rounded-base gl-bg-white" + plugin-key="bubbleMenuLink" + :should-show="shouldShow" + :tippy-options="$options.tippyOptions" + @show="updateLinkToState" + @hidden="resetBubbleMenuState" + > <gl-button-group v-if="!isEditing" class="gl-display-flex gl-align-items-center"> <gl-link v-gl-tooltip @@ -178,12 +246,12 @@ export default { /> </gl-button-group> <gl-form v-else class="bubble-menu-form gl-p-4 gl-w-100" @submit.prevent="saveEditedLink"> + <gl-form-group :label="__('Text')" label-for="link-text"> + <gl-form-input id="link-text" v-model="linkText" data-testid="link-text" /> + </gl-form-group> <gl-form-group :label="__('URL')" label-for="link-href"> <gl-form-input id="link-href" v-model="linkCanonicalSrc" data-testid="link-href" /> </gl-form-group> - <gl-form-group :label="__('Title')" label-for="link-title"> - <gl-form-input id="link-title" v-model="linkTitle" data-testid="link-title" /> - </gl-form-group> <div class="gl-display-flex gl-justify-content-end"> <gl-button class="gl-mr-3" data-testid="cancel-link" @click="cancelEditingLink"> {{ __('Cancel') }} @@ -193,6 +261,6 @@ export default { </gl-button> </div> </gl-form> - </editor-state-observer> - </bubble-menu> + </bubble-menu> + </editor-state-observer> </template> 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 ccb46e3b593..62f2113a8f4 100644 --- a/app/assets/javascripts/content_editor/components/editor_state_observer.vue +++ b/app/assets/javascripts/content_editor/components/editor_state_observer.vue @@ -16,14 +16,21 @@ const getComponentEventName = (tiptapEventName) => tiptapToComponentMap[tiptapEv export default { inject: ['tiptapEditor', 'eventHub'], + props: { + debounce: { + type: Number, + required: false, + default: 100, + }, + }, created() { this.disposables = []; Object.keys(tiptapToComponentMap).forEach((tiptapEvent) => { - const eventHandler = debounce( - (params) => this.bubbleEvent(getComponentEventName(tiptapEvent), params), - 100, - ); + let eventHandler = (params) => this.bubbleEvent(getComponentEventName(tiptapEvent), params); + if (this.debounce) { + eventHandler = debounce(eventHandler, this.debounce); + } this.tiptapEditor?.on(tiptapEvent, eventHandler); diff --git a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue index 11b3a5a3069..230141fe1c1 100644 --- a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue +++ b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue @@ -2,7 +2,6 @@ import trackUIControl from '../services/track_ui_control'; import ToolbarButton from './toolbar_button.vue'; import ToolbarAttachmentButton from './toolbar_attachment_button.vue'; -import ToolbarLinkButton from './toolbar_link_button.vue'; import ToolbarTableButton from './toolbar_table_button.vue'; import ToolbarTextStyleDropdown from './toolbar_text_style_dropdown.vue'; import ToolbarMoreDropdown from './toolbar_more_dropdown.vue'; @@ -11,7 +10,6 @@ export default { components: { ToolbarButton, ToolbarTextStyleDropdown, - ToolbarLinkButton, ToolbarTableButton, ToolbarAttachmentButton, ToolbarMoreDropdown, @@ -24,7 +22,10 @@ export default { }; </script> <template> - <div class="gl-w-full gl-border-b gl-display-flex gl-justify-content-end"> + <div + class="gl-w-full gl-border-b gl-display-flex gl-justify-content-end" + data-testid="formatting-toolbar" + > <div class="gl-py-2 gl-display-flex gl-flex-wrap-wrap gl-align-items-end"> <toolbar-text-style-dropdown data-testid="text-styles" @@ -62,7 +63,14 @@ export default { :label="__('Code')" @execute="trackToolbarControlExecution" /> - <toolbar-link-button data-testid="link" @execute="trackToolbarControlExecution" /> + <toolbar-button + data-testid="link" + content-type="link" + icon-name="link" + editor-command="editLink" + :label="__('Insert link')" + @execute="trackToolbarControlExecution" + /> <toolbar-button data-testid="bullet-list" content-type="bulletList" diff --git a/app/assets/javascripts/content_editor/components/toolbar_link_button.vue b/app/assets/javascripts/content_editor/components/toolbar_link_button.vue deleted file mode 100644 index ff1a52264f0..00000000000 --- a/app/assets/javascripts/content_editor/components/toolbar_link_button.vue +++ /dev/null @@ -1,125 +0,0 @@ -<script> -import { - GlDropdown, - GlDropdownForm, - GlButton, - GlFormInputGroup, - GlDropdownDivider, - GlDropdownItem, - GlTooltipDirective as GlTooltip, -} from '@gitlab/ui'; -import Link from '../extensions/link'; -import { hasSelection } from '../services/utils'; -import EditorStateObserver from './editor_state_observer.vue'; - -export default { - components: { - GlDropdown, - GlDropdownForm, - GlFormInputGroup, - GlDropdownDivider, - GlDropdownItem, - GlButton, - EditorStateObserver, - }, - directives: { - GlTooltip, - }, - inject: ['tiptapEditor'], - data() { - return { - linkHref: '', - isActive: false, - }; - }, - methods: { - resetFields() { - this.imgSrc = ''; - this.$refs.fileSelector.value = ''; - }, - updateLinkState({ editor }) { - const { canonicalSrc, href } = editor.getAttributes(Link.name); - - this.isActive = editor.isActive(Link.name); - this.linkHref = canonicalSrc || href; - }, - updateLink() { - this.tiptapEditor - .chain() - .focus() - .unsetLink() - .setLink({ - href: this.linkHref, - canonicalSrc: this.linkHref, - }) - .run(); - - this.$emit('execute', { contentType: Link.name }); - }, - selectLink() { - const { tiptapEditor } = this; - - // a selection has already been made by the user, so do nothing - if (!hasSelection(tiptapEditor)) { - tiptapEditor.chain().focus().extendMarkRange(Link.name).run(); - } - }, - removeLink() { - this.tiptapEditor.chain().focus().unsetLink().run(); - - this.$emit('execute', { contentType: Link.name }); - }, - onFileSelect(e) { - this.tiptapEditor - .chain() - .focus() - .uploadAttachment({ - file: e.target.files[0], - }) - .run(); - - this.resetFields(); - this.$emit('execute', { contentType: Link.name }); - }, - }, -}; -</script> -<template> - <editor-state-observer @transaction="updateLinkState"> - <span class="gl-display-inline-flex"> - <gl-dropdown - v-gl-tooltip - :title="__('Insert link')" - :text="__('Insert link')" - :toggle-class="{ active: isActive }" - size="small" - category="tertiary" - icon="link" - text-sr-only - lazy - @show="selectLink()" - > - <gl-dropdown-form class="gl-px-3!" :class="{ 'gl-pb-2!': isActive }"> - <gl-form-input-group v-model="linkHref" :placeholder="__('Link URL')"> - <template #append> - <gl-button variant="confirm" @click="updateLink">{{ __('Apply') }}</gl-button> - </template> - </gl-form-input-group> - </gl-dropdown-form> - <div v-if="isActive"> - <gl-dropdown-divider /> - <gl-dropdown-item @click="removeLink"> - {{ __('Remove link') }} - </gl-dropdown-item> - </div> - </gl-dropdown> - <input - ref="fileSelector" - type="file" - name="content_editor_attachment" - class="gl-display-none" - @change="onFileSelect" - /> - </span> - </editor-state-observer> -</template> diff --git a/app/assets/javascripts/content_editor/extensions/link.js b/app/assets/javascripts/content_editor/extensions/link.js index e985e561fda..314d5230b01 100644 --- a/app/assets/javascripts/content_editor/extensions/link.js +++ b/app/assets/javascripts/content_editor/extensions/link.js @@ -18,6 +18,8 @@ export const extractHrefFromMarkdownLink = (match) => { }; export default Link.extend({ + inclusive: false, + addOptions() { return { ...this.parent?.(), @@ -64,4 +66,18 @@ export default Link.extend({ }, }; }, + addCommands() { + return { + ...this.parent?.(), + editLink: (attrs) => ({ chain }) => { + chain().setMeta('creatingLink', true).setLink(attrs).run(); + }, + }; + }, + + addKeyboardShortcuts() { + return { + 'Mod-k': () => this.editor.commands.editLink(), + }; + }, }); diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js index ab8cf7014d9..664473fccfe 100644 --- a/app/assets/javascripts/content_editor/services/serialization_helpers.js +++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js @@ -607,7 +607,7 @@ export const link = { return '['; } - const attrs = { href: state.esc(href || canonicalSrc) }; + const attrs = { href: state.esc(href || canonicalSrc || '') }; if (title) { attrs.title = title; @@ -623,14 +623,14 @@ export const link = { const { canonicalSrc, href, title, sourceMarkdown, isReference } = mark.attrs; if (isReference) { - return `][${state.esc(canonicalSrc || href)}]`; + return `][${state.esc(canonicalSrc || href || '')}]`; } if (linkType(sourceMarkdown) === LINK_HTML) { return closeTag('a'); } - return `](${state.esc(canonicalSrc || href)}${title ? ` ${state.quote(title)}` : ''})`; + return `](${state.esc(canonicalSrc || href || '')}${title ? ` ${state.quote(title)}` : ''})`; }, }; |