diff --git a/cypress/e2e/Links.spec.js b/cypress/e2e/Links.spec.js index bb27ec2808b..59c587b28d2 100644 --- a/cypress/e2e/Links.spec.js +++ b/cypress/e2e/Links.spec.js @@ -104,7 +104,8 @@ describe('test link marks', function () { cy.insertLine(link) clickLink(link) - cy.get('.link-view-bubble button[title="Remove link"]').click() + cy.get('.link-view-bubble .link-options').click() + cy.get('button').contains('Remove').click() cy.getContent().find(`a[href*="${link}"]`).should('not.exist') }) diff --git a/cypress/e2e/nodes/PreviewOptions.spec.js b/cypress/e2e/nodes/PreviewOptions.spec.js index bc216156aa6..10f85631f12 100644 --- a/cypress/e2e/nodes/PreviewOptions.spec.js +++ b/cypress/e2e/nodes/PreviewOptions.spec.js @@ -21,7 +21,9 @@ describe('Preview Options', function () { 'nextcloud.com', ) cy.get('[data-text-action-entry="insert-link-input"] button').click() - cy.get('.preview-options').click() + + cy.getContent().find(`a[href*="https://nextcloud.com"]`).click() + cy.get('.link-options').click() }) it('should render previewOptions correctly', function () { diff --git a/src/components/Editor/PreviewOptions.vue b/src/components/Editor/PreviewOptions.vue index df4f47880cc..03701dfe86c 100644 --- a/src/components/Editor/PreviewOptions.vue +++ b/src/components/Editor/PreviewOptions.vue @@ -3,47 +3,60 @@ - SPDX-License-Identifier: AGPL-3.0-or-later --> diff --git a/src/plugins/extractLinkParagraphs.js b/src/plugins/extractLinkParagraphs.js deleted file mode 100644 index a11796b741e..00000000000 --- a/src/plugins/extractLinkParagraphs.js +++ /dev/null @@ -1,80 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { isLinkToSelfWithHash } from './../helpers/links.js' - -/** - * Get a list of all paragraphs that can be converted into a preview. - * - * @param {Document} doc - the prosemirror doc - * @return {Array} paragraphs with one link only found in the doc - */ -export default function extractLinkParagraphs(doc) { - const paragraphs = [] - - doc.descendants((node, pos) => { - if (previewPossible(node)) { - paragraphs.push( - Object.freeze({ - pos, - nodeSize: node.nodeSize, - type: 'text-only', - href: extractHref(node.firstChild), - }), - ) - } else if (node.type.name === 'preview') { - paragraphs.push( - Object.freeze({ - pos, - nodeSize: node.nodeSize, - type: 'link-preview', - href: node.attrs.href, - }), - ) - } - }) - return paragraphs -} - -/** - * Is it possible to convert the node into a preview? - * @param {object} node the node in question - * @return {boolean} - */ -function previewPossible(node) { - if (node.type.name !== 'paragraph' || hasOtherContent(node)) { - return false - } - const href = extractHref(node.firstChild) - if (!href || isLinkToSelfWithHash(href)) { - return false - } - return true -} - -/** - * Does the node contain more content than the first child - * @param {object} node node to inspect - * @return {boolean} - */ -function hasOtherContent(node) { - return ( - node.childCount > 2 - || (node.childCount === 2 && node.lastChild.textContent.trim()) - ) -} - -/** - * Get the link href of the given node - * @param {object} node to inspect - * @return {string} The href of the link mark of the node - */ -function extractHref(node) { - if (!node) { - return undefined - } - const link = node.marks.find((mark) => mark.type.name === 'link') - return link?.attrs.href -} diff --git a/src/plugins/previewOptions.js b/src/plugins/previewOptions.js deleted file mode 100644 index 04fcf36bbe4..00000000000 --- a/src/plugins/previewOptions.js +++ /dev/null @@ -1,195 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { Plugin, PluginKey } from '@tiptap/pm/state' -import { Decoration, DecorationSet } from '@tiptap/pm/view' -import Vue from 'vue' -import PreviewOptions from '../components/Editor/PreviewOptions.vue' -import extractLinkParagraphs from './extractLinkParagraphs.js' - -export const previewOptionsPluginKey = new PluginKey('linkParagraphMenu') - -/** - * Preview option decorations ProseMirror plugin - * Add preview options to linkParagraphs. - * - * @param {object} options - options for the plugin - * @param {object} options.editor - the tiptap editor - * - * @return {Plugin} - */ -export default function previewOptions({ editor }) { - return new Plugin({ - key: previewOptionsPluginKey, - - state: { - init(_, { doc }) { - if (!editor.options.editable) { - return { decorations: DecorationSet.create() } - } - const linkParagraphs = extractLinkParagraphs(doc) - return { - linkParagraphs, - decorations: linkParagraphDecorations( - doc, - linkParagraphs, - editor, - ), - } - }, - apply(tr, value, _oldState, newState) { - if (!tr.docChanged) { - return value - } - if (!editor.options.editable) { - return value - } - const linkParagraphs = extractLinkParagraphs(newState.doc) - const decorations = - mapDecorations(value, tr, linkParagraphs) - || linkParagraphDecorations(newState.doc, linkParagraphs, editor) - return { linkParagraphs, decorations } - }, - }, - - props: { - decorations(state) { - return this.getState(state).decorations - }, - }, - }) -} - -/** - * Map the previous deocrations to current document state - * - * Return false if previewParagraphs changes or decorations would get removed. The latter prevents - * lost decorations in case of replacements. - * - * @param {object} value - previous plugin state - * @param {object} tr - current transaction - * @param {Array} linkParagraphs - array of linkParagraphs - * - * @return {false|DecorationSet} - */ -function mapDecorations(value, tr, linkParagraphs) { - if (linkParagraphsChanged(linkParagraphs, value.linkParagraphs)) { - return false - } - let removedDecorations = false - const decorations = value.decorations.map(tr.mapping, tr.doc, { - onRemove: () => { - removedDecorations = true - }, - }) - return removedDecorations ? false : decorations -} - -/** - * Check if the linkParagraphs provided are equivalent. - * - * @param {Array} current - array of linkParagraphs - * @param {Array} prev - linkParagraphs to compare against - * - * @return {boolean} - */ -function linkParagraphsChanged(current, prev) { - return current.length !== prev.length || current.some(isDifferentFrom(prev)) -} - -/** - * Checks if linkParagraphs are different - * - * @param {Array} other - linkParagraphs to compare against - * - * Returns a function to be used to call to Array#some. - * The returned function takes a linkParagraph and an index (as provided by iterators) - */ -const isDifferentFrom = (other) => (linkParagraph, i) => { - return ( - linkParagraph.type !== other[i].type - || linkParagraph.nodeSize !== other[i].nodeSize - ) -} - -/** - * Create anchor decorations for the given linkParagraphs - * - * @param {Document} doc - prosemirror doc - * @param {Array} linkParagraphs - linkParagraphs structure in the doc - * @param {object} editor - tiptap editor - * - * @return {DecorationSet} - */ -function linkParagraphDecorations(doc, linkParagraphs, editor) { - const decorations = linkParagraphs.map((linkParagraph) => - decorationForLinkParagraph(linkParagraph, editor), - ) - return DecorationSet.create(doc, decorations) -} - -/** - * Create a decoration for the given linkParagraph - * - * @param {object} linkParagraph to decorate - * @param {object} editor - tiptap editor - * - * @return {Decoration} - */ -function decorationForLinkParagraph(linkParagraph, editor) { - return Decoration.widget( - linkParagraph.pos + 1, - previewOptionForLinkParagraph(linkParagraph, editor), - { side: -1 }, - ) -} - -/** - * Create a previewOptions element for the given linkParagraph - * - * @param {object} linkParagraph - linkParagraph to generate anchor for - * @param {number} linkParagraph.pos - Position of the node - * @param {string} linkParagraph.type - selected type - * @param {string} linkParagraph.href - href of the link - * @param {number} linkParagraph.nodeSize - size of the node - * @param {object} editor - tiptap editor - * - * @return {Element} - */ -function previewOptionForLinkParagraph({ type, href, pos, nodeSize }, editor) { - const el = document.createElement('div') - const Component = Vue.extend(PreviewOptions) - const propsData = { type, href } - const previewOption = new Component({ propsData }).$mount(el) - previewOption.$on('open', () => { - editor.commands.hideLinkBubble() - }) - previewOption.$on('toggle', (type) => { - setPreview(pos, type, editor) - }) - previewOption.$on('delete', () => { - editor.commands.deleteRange({ from: pos, to: pos + nodeSize }) - }) - return previewOption.$el -} - -/** - * - * - * @param {number} pos - Position of the node - * @param {string} type - selected type - * @param {object} editor - tiptap editor - */ -function setPreview(pos, type, editor) { - const chain = editor - .chain() - .focus() - .setTextSelection(pos + 1) - if (type !== 'text-only') { - chain.setPreview().run() - } else { - chain.unsetPreview().run() - } -} diff --git a/src/tests/plugins/extractLinkParagraphs.spec.js b/src/tests/plugins/extractLinkParagraphs.spec.js deleted file mode 100644 index 63091fac039..00000000000 --- a/src/tests/plugins/extractLinkParagraphs.spec.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import Link from '../../marks/Link.js' -import Preview from '../../nodes/Preview.js' -import extractLinkParagraphs from '../../plugins/extractLinkParagraphs.js' -import createCustomEditor from '../testHelpers/createCustomEditor.ts' - -describe('extractLinkParagraphs', () => { - const href = 'https://nextcloud.com' - const link = `Link` - const preview = `Link` - - it('returns an empty array for an empty doc', () => { - const doc = prepareDoc('') - const paragraphs = extractLinkParagraphs(doc) - expect(paragraphs).toEqual([]) - }) - - it('returns paragraphs with a single link', () => { - const content = `

${link}

` - const doc = prepareDoc(content) - const paragraphs = extractLinkParagraphs(doc) - expect(paragraphs).toEqual([ - { href, pos: 0, type: 'text-only', nodeSize: 6 }, - ]) - }) - - it('returns paragraphs with a single preview', () => { - const doc = prepareDoc(preview) - const paragraphs = extractLinkParagraphs(doc) - expect(paragraphs).toEqual([ - { href, pos: 0, type: 'link-preview', nodeSize: 6 }, - ]) - }) - - it('returns paragraphs with a single link and whitespace', () => { - const content = `

${link}

` - const doc = prepareDoc(content) - const paragraphs = extractLinkParagraphs(doc) - expect(paragraphs).toEqual([ - { href, pos: 0, type: 'text-only', nodeSize: 7 }, - ]) - }) - - it('returns multiple paragraphs with a single link', () => { - const paragraph = `

${link}

` - const content = paragraph + paragraph - const doc = prepareDoc(content) - const paragraphs = extractLinkParagraphs(doc) - expect(paragraphs).toEqual([ - { href, pos: 0, type: 'text-only', nodeSize: 6 }, - { href, pos: 6, type: 'text-only', nodeSize: 6 }, - ]) - }) - - it('returns previews mixed with paragraphs with a single link', () => { - const content = `

${link}

${preview}` - const doc = prepareDoc(content) - const paragraphs = extractLinkParagraphs(doc) - - expect(paragraphs).toEqual([ - { href, pos: 0, type: 'text-only', nodeSize: 6 }, - { href, pos: 6, type: 'link-preview', nodeSize: 6 }, - ]) - }) - - it('ignores an empty paragraph', () => { - const content = '

' - const doc = prepareDoc(content) - const paragraphs = extractLinkParagraphs(doc) - expect(paragraphs).toEqual([]) - }) - - it('ignores paragraphs with text after the link', () => { - const content = `

${link} Hello

` - const doc = prepareDoc(content) - const paragraphs = extractLinkParagraphs(doc) - expect(paragraphs).toEqual([]) - }) - - it('ignores paragraphs with a link to self', () => { - const content = '

test

' - const doc = prepareDoc(content) - const paragraphs = extractLinkParagraphs(doc) - expect(paragraphs).toEqual([]) - }) - - it('ignores paragraphs with text before the link', () => { - const content = `

Hello ${link}

` - const doc = prepareDoc(content) - const paragraphs = extractLinkParagraphs(doc) - expect(paragraphs).toEqual([]) - }) - - it('ignores paragraphs with multiple links', () => { - const content = `

${link} ${link}

` - const doc = prepareDoc(content) - const paragraphs = extractLinkParagraphs(doc) - expect(paragraphs).toEqual([]) - }) -}) - -const prepareDoc = (content) => { - const editor = createCustomEditor(content, [Link, Preview]) - return editor.state.doc -}