From a0f499f2eb64e4b346da7889ba938b1eb70961d0 Mon Sep 17 00:00:00 2001 From: Jonas Date: Tue, 22 Jul 2025 10:11:51 +0200 Subject: [PATCH 1/7] fix(PreviewOptions): Pass href, so "open in new tab" option is displayed Signed-off-by: Jonas --- cypress/e2e/nodes/PreviewOptions.spec.js | 3 +++ src/plugins/previewOptions.js | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/nodes/PreviewOptions.spec.js b/cypress/e2e/nodes/PreviewOptions.spec.js index b3fbd83a427..bc216156aa6 100644 --- a/cypress/e2e/nodes/PreviewOptions.spec.js +++ b/cypress/e2e/nodes/PreviewOptions.spec.js @@ -25,6 +25,9 @@ describe('Preview Options', function () { }) it('should render previewOptions correctly', function () { + cy.get('.action-button__text') + .contains('Open in new tab') + .should('be.visible') cy.get('.action-button__text').contains('Remove link').should('be.visible') cy.get('.action-radio__label').each((el) => { cy.wrap(el) diff --git a/src/plugins/previewOptions.js b/src/plugins/previewOptions.js index 2100a026c1d..04fcf36bbe4 100644 --- a/src/plugins/previewOptions.js +++ b/src/plugins/previewOptions.js @@ -152,15 +152,16 @@ function decorationForLinkParagraph(linkParagraph, editor) { * @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, pos, nodeSize }, editor) { +function previewOptionForLinkParagraph({ type, href, pos, nodeSize }, editor) { const el = document.createElement('div') const Component = Vue.extend(PreviewOptions) - const propsData = { type } + const propsData = { type, href } const previewOption = new Component({ propsData }).$mount(el) previewOption.$on('open', () => { editor.commands.hideLinkBubble() From a3a76764acab892102699ce0285df90c3de6f8de Mon Sep 17 00:00:00 2001 From: Jonas Date: Tue, 22 Jul 2025 10:29:03 +0200 Subject: [PATCH 2/7] fix(img): Remove global CSS rules for images in prosemirror We have a custom node for displaying images anyway. This fixes the cursor being a pointer in the preview images of preview links. Signed-off-by: Jonas --- src/css/prosemirror.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/css/prosemirror.scss b/src/css/prosemirror.scss index 1992c5b7c01..2105b3eb9b9 100644 --- a/src/css/prosemirror.scss +++ b/src/css/prosemirror.scss @@ -183,11 +183,6 @@ div.ProseMirror { font-size: var(--default-font-size); } - img { - cursor: default; - max-width: 100%; - } - hr { padding: 2px 0; border: none; From 4bcb03e4f2f2f2b85722e7d8c3457bd0b28d86f1 Mon Sep 17 00:00:00 2001 From: Jonas Date: Tue, 22 Jul 2025 10:32:29 +0200 Subject: [PATCH 3/7] fix(links): Add open button to link bubble Fixes: #1226 Signed-off-by: Jonas --- src/components/Link/LinkBubbleView.vue | 15 +++++++++++++++ src/helpers/links.js | 20 +++++++++++++++++++- src/plugins/LinkBubblePluginView.js | 8 +++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/components/Link/LinkBubbleView.vue b/src/components/Link/LinkBubbleView.vue index 97ac00dd61b..fe10210aa94 100644 --- a/src/components/Link/LinkBubbleView.vue +++ b/src/components/Link/LinkBubbleView.vue @@ -10,6 +10,16 @@ + + + + import { t } from '@nextcloud/l10n' +import { generateUrl } from '@nextcloud/router' import NcButton from '@nextcloud/vue/components/NcButton' import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' import NcTextField from '@nextcloud/vue/components/NcTextField' @@ -103,9 +114,11 @@ import CheckIcon from 'vue-material-design-icons/Check.vue' import CloseIcon from 'vue-material-design-icons/Close.vue' import ContentCopyIcon from 'vue-material-design-icons/ContentCopy.vue' import LinkOffIcon from 'vue-material-design-icons/LinkOff.vue' +import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue' import PencilOutlineIcon from 'vue-material-design-icons/PencilOutline.vue' import CopyToClipboardMixin from '../../mixins/CopyToClipboardMixin.js' +import { openLink } from '../../helpers/links.js' const PROTOCOLS_WITH_PREVIEW = ['http:', 'https:'] @@ -121,6 +134,7 @@ export default { NcReferenceList, NcTextField, LinkOffIcon, + OpenInNewIcon, PencilOutlineIcon, }, @@ -198,6 +212,7 @@ export default { }, methods: { + openLink, resetBubble() { this.edit = false this.newHref = null diff --git a/src/helpers/links.js b/src/helpers/links.js index 237808def5a..1f2c12600d9 100644 --- a/src/helpers/links.js +++ b/src/helpers/links.js @@ -57,4 +57,22 @@ const isLinkToSelfWithHash = function (href) { return href?.startsWith('#') || href?.startsWith(locationNoHash + '#') } -export { domHref, isLinkToSelfWithHash, parseHref } +/** + * Open links, to be used as custom click handler + * + * @param {string} href the link href + */ +const openLink = function (href) { + const linkUrl = new URL(href, window.location.href) + // Consider rerouting links to Collectives if already inside Collectives app + const collectivesUrlBase = '/apps/collectives' + if (window.OCA.Collectives?.vueRouter + && linkUrl.pathname.toString().startsWith(generateUrl(collectivesUrlBase))) { + const collectivesUrl = linkUrl.href.substring(linkUrl.href.indexOf(collectivesUrlBase) + collectivesUrlBase.length) + window.OCA.Collectives.vueRouter.push(collectivesUrl) + return + } + window.open(linkUrl, '_blank') +} + +export { domHref, isLinkToSelfWithHash, parseHref, openLink } diff --git a/src/plugins/LinkBubblePluginView.js b/src/plugins/LinkBubblePluginView.js index 18436f55faf..bfaf2a9adb1 100644 --- a/src/plugins/LinkBubblePluginView.js +++ b/src/plugins/LinkBubblePluginView.js @@ -98,7 +98,13 @@ class LinkBubblePluginView { } updateTooltip(view, { mark, nodeStart }) { - let referenceEl = view.nodeDOM(nodeStart) + let referenceEl + try { + referenceEl = view.nodeDOM(nodeStart) + } catch (e) { + // Prevent throwing error at rerouting in `openLink()` + return + } if (Object.prototype.toString.call(referenceEl) === '[object Text]') { referenceEl = referenceEl.parentElement } From 80a773c0c57975ef131d518ffd624130043df7b0 Mon Sep 17 00:00:00 2001 From: Jonas Date: Wed, 23 Jul 2025 16:19:41 +0200 Subject: [PATCH 4/7] fix(LinkBubble): Read `editor.contentComponent` when it's ready When initiating the editor extensions (i.e. in `addProseMirrorPlugins()`), `editor.contentComponent` is not yet available. So let's read it when we actually use it, when creating the tooltip in `LinkBubblePluginView`. Before, a new Vue instance was initiated for each `LinkBubbleView.vue` as the `parent` given to `VueRenderer()` was `undefined`. With this fix, `LinkBubbleView.vue` becomes a child component of the root editor Vue instance. Signed-off-by: Jonas --- src/extensions/LinkBubble.js | 1 - src/plugins/LinkBubblePluginView.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/extensions/LinkBubble.js b/src/extensions/LinkBubble.js index fd189604269..8ae10e27ab8 100644 --- a/src/extensions/LinkBubble.js +++ b/src/extensions/LinkBubble.js @@ -23,7 +23,6 @@ const LinkBubble = Extension.create({ return [ linkBubble({ editor: this.editor, - parent: this.editor.contentComponent, }), ] }, diff --git a/src/plugins/LinkBubblePluginView.js b/src/plugins/LinkBubblePluginView.js index bfaf2a9adb1..df0a398df5d 100644 --- a/src/plugins/LinkBubblePluginView.js +++ b/src/plugins/LinkBubblePluginView.js @@ -58,7 +58,7 @@ class LinkBubblePluginView { } this.#component ||= new VueRenderer(LinkBubbleView, { - parent: this.options.parent, + parent: this.options.editor.contentComponent, propsData: { editor: this.options.editor, href: null, From bca4e51b671f95b04b79ebbb47e5e10fda7252e7 Mon Sep 17 00:00:00 2001 From: Jonas Date: Wed, 23 Jul 2025 14:35:34 +0200 Subject: [PATCH 5/7] enh(editorAPI): Allow to pass an openLinkHandler to editor Signed-off-by: Jonas --- src/components/Editor.provider.ts | 12 ++++++++++++ src/components/Link/LinkBubbleView.vue | 10 ++++++---- src/editor.js | 6 ++++++ src/helpers/links.js | 12 ++++++++---- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/components/Editor.provider.ts b/src/components/Editor.provider.ts index fa49465dfd6..9e866078fa1 100644 --- a/src/components/Editor.provider.ts +++ b/src/components/Editor.provider.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +import { openLink } from '../helpers/links.js' import { logger } from '../helpers/logger.js' export const FILE = Symbol('editor:file') @@ -11,6 +12,7 @@ export const IS_MOBILE = Symbol('editor:is-mobile') export const EDITOR_UPLOAD = Symbol('editor:upload') export const HOOK_MENTION_SEARCH = Symbol('hook:mention-search') export const HOOK_MENTION_INSERT = Symbol('hook:mention-insert') +export const OPEN_LINK_HANDLER = Symbol('editor:open-link-handler') export const useIsMobileMixin = { inject: { @@ -66,3 +68,13 @@ export const useMentionHook = { }, }, } +export const useOpenLinkHandler = { + inject: { + $openLinkHandler: { + from: OPEN_LINK_HANDLER, + default: { + openLink, + }, + }, + }, +} diff --git a/src/components/Link/LinkBubbleView.vue b/src/components/Link/LinkBubbleView.vue index fe10210aa94..5ed97243907 100644 --- a/src/components/Link/LinkBubbleView.vue +++ b/src/components/Link/LinkBubbleView.vue @@ -105,7 +105,6 @@