diff --git a/cypress/e2e/nodes/Links.spec.js b/cypress/e2e/Links.spec.js similarity index 99% rename from cypress/e2e/nodes/Links.spec.js rename to cypress/e2e/Links.spec.js index 13d6db365e6..36cfc17a57c 100644 --- a/cypress/e2e/nodes/Links.spec.js +++ b/cypress/e2e/Links.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { randUser } from '../../utils/index.js' +import { randUser } from '../utils/index.js' const user = randUser() const fileName = 'empty.md' diff --git a/cypress/e2e/marks/Link.spec.js b/cypress/e2e/marks/Link.spec.js new file mode 100644 index 00000000000..59bb3402013 --- /dev/null +++ b/cypress/e2e/marks/Link.spec.js @@ -0,0 +1,45 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import Markdown from './../../../src/extensions/Markdown.js' +import { Italic, Link } from './../../../src/marks/index.js' +import { createCustomEditor } from './../../support/components.js' +import { loadMarkdown, expectMarkdown } from '../nodes/helpers.js' + +describe('Link marks', { retries: 0 }, () => { + const editor = createCustomEditor({ + content: '', + extensions: [Markdown, Link, Italic], + }) + + describe('insertOrSetLink command', { retries: 0 }, () => { + it('is available in commands', () => { + expect(editor.commands).to.have.property('insertOrSetLink') + }) + + it('can run on normal paragraph', () => { + prepareEditor('hello\n', 3) + expect(editor.can().insertOrSetLink()).toBe(true) + }) + + it('will insert a link in a normal paragraph', () => { + prepareEditor('hello\n', 3) + editor.commands.insertOrSetLink('https://nextcloud.com', { + href: 'https://nextcloud.com', + }) + expectMarkdown(editor, 'he\n\n\n\nllo') + }) + }) + + /** + * + * @param {*} input markdown content + * @param {*} position cursor pos + */ + function prepareEditor(input, position = 1) { + loadMarkdown(editor, input) + editor.commands.setTextSelection(position) + } +}) diff --git a/src/components/Menu/ActionInsertLink.vue b/src/components/Menu/ActionInsertLink.vue index 8a918c09dcb..8c50d815076 100644 --- a/src/components/Menu/ActionInsertLink.vue +++ b/src/components/Menu/ActionInsertLink.vue @@ -189,24 +189,7 @@ export default { // Avoid issues when parsing urls later on in markdown that might be entered in an invalid format (e.g. "mailto: example@example.com") const href = url.replaceAll(' ', '%20') const chain = this.$editor.chain() - // Check if any text is selected, if not insert the link using the given text property - if (this.$editor.view.state?.selection.empty) { - chain.insertContent({ - type: 'paragraph', - content: [{ - type: 'text', - marks: [{ - type: 'link', - attrs: { - href, - }, - }], - text, - }], - }) - } else { - chain.setLink({ href }) - } + chain.insertOrSetLink(text, { href }) chain.focus().run() }, /** diff --git a/src/marks/Link.js b/src/marks/Link.js index 1878f43958b..5b460b5fb5a 100644 --- a/src/marks/Link.js +++ b/src/marks/Link.js @@ -3,10 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { markInputRule } from '@tiptap/core' import TipTapLink from '@tiptap/extension-link' import { domHref, parseHref } from './../helpers/links.js' import { linkClicking } from '../plugins/links.js' +import { markInputRule, getMarkRange, isMarkActive } from '@tiptap/core' const PROTOCOLS_TO_LINK_TO = ['http:', 'https:', 'mailto:', 'tel:'] @@ -87,6 +87,37 @@ const Link = TipTapLink.extend({ }), ] }, + addCommands() { + return { + insertOrSetLink: (text, attrs) => ({ state, chain, commands }) => { + // Check if any text is selected, + // if not insert the link using the given text property + if (state.selection.empty) { + if (isMarkActive(state, this.name)) { + + // get current href to check what to replace, assumes there's only one link mark on the anchor + let href = '' + state.selection.$anchor.marks().forEach(item => { + if (item.attrs.href && item.type.name === 'link') { + href = item.attrs.href + } + }) + commands.deleteRange(getMarkRange(state.selection.$anchor, state.schema.marks.link, { href })) + } + return chain().insertContent({ + type: 'text', + marks: [{ + type: 'link', + attrs, + }], + text, + }) + } else { + return commands.setLink(attrs) + } + }, + } + }, addProseMirrorPlugins() { const plugins = this.parent() diff --git a/src/tests/marks/Link.spec.js b/src/tests/marks/Link.spec.js new file mode 100644 index 00000000000..547f22a7a91 --- /dev/null +++ b/src/tests/marks/Link.spec.js @@ -0,0 +1,106 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import Link from './../../marks/Link.js' +import Underline from '../../marks/Underline.js' +import createCustomEditor from '../testHelpers/createCustomEditor.ts' + +describe('Link extension integrated in the editor', () => { + it('should have link available in commands', () => { + const editor = createCustomEditor('

Test HELLO WORLD

', [Link]) + expect(editor.commands).toHaveProperty('insertOrSetLink') + }) + + it('should update link if anchor has mark', () => { + const editor = createCustomEditor( + '

Test HELLO WORLD

', + [Link, Underline], + ) + editor.commands.setTextSelection(3) + editor.commands.insertOrSetLink('updated.de', { href: 'updated.de' }) + expect(editor.getJSON()).toEqual({ + content: [ + { + content: [ + { + marks: [ + { attrs: { href: 'updated.de', title: null }, type: 'link' }, + ], + text: 'updated.de', + type: 'text', + }, + { text: ' HELLO WORLD', type: 'text' }, + ], + type: 'paragraph', + }, + ], + type: 'doc', + }) + }) + + it('Should only update link the anchor is on', () => { + const editor = createCustomEditor( + '

Testsecond link

', + [Link], + ) + editor.commands.setTextSelection(3) + editor.commands.insertOrSetLink('updated.de', { href: 'updated.de' }) + expect(editor.getJSON()).toEqual({ + content: [ + { + content: [ + { + marks: [ + { attrs: { href: 'updated.de', title: null }, type: 'link' }, + ], + text: 'updated.de', + type: 'text', + }, + { + marks: [ + { + attrs: { + href: 'not-nextcloud.com', + title: null, + }, + type: 'link', + }, + ], + text: 'second link', + type: 'text', + }, + ], + type: 'paragraph', + }, + ], + type: 'doc', + }) + }) + + it('should insert new link if none at anchor', () => { + const editor = createCustomEditor( + '

Test HELLO WORLD

', + [Link], + ) + editor.commands.setTextSelection(10) + expect(editor.getJSON()).toEqual({ + content: [ + { + content: [ + { + marks: [ + { attrs: { href: 'nextcloud.com', title: null }, type: 'link' }, + ], + text: 'Test', + type: 'text', + }, + { text: ' HELLO WORLD', type: 'text' }, + ], + type: 'paragraph', + }, + ], + type: 'doc', + }) + }) +})