From 58ac8175254db7a9db797cf2151b3c3931382357 Mon Sep 17 00:00:00 2001 From: Jonas Date: Tue, 9 Jan 2024 14:09:31 +0100 Subject: [PATCH] feat(editor): Refactor link click handlers New link click behaviour: * Link left clicks without Ctrl/Meta open link bubble (Fixes: #3691) * Link left clicks with Ctrl/Meta open link in new tab * Link middle clicks open link in new tab * Link middle clicks on Linux don't paste content (Fixes: #2198) * No more custom open link handler in editor Implementation details: * Moved link click handler plugins back into Link mark class. * Added 'data-md-href' attribute to a-elements in DOM, to be used in onClick handlers. Signed-off-by: Jonas --- src/components/RichTextReader.vue | 23 ----------- src/extensions/LinkBubblePluginView.js | 4 +- src/helpers/links.js | 4 +- src/marks/Link.js | 55 ++++++++++++++++++++------ src/plugins/link.js | 52 ------------------------ 5 files changed, 46 insertions(+), 92 deletions(-) delete mode 100644 src/plugins/link.js diff --git a/src/components/RichTextReader.vue b/src/components/RichTextReader.vue index d45d410e11c..02f2da37ddc 100644 --- a/src/components/RichTextReader.vue +++ b/src/components/RichTextReader.vue @@ -41,12 +41,6 @@ export default { return [ RichText.configure({ editing: false, - link: { - onClick: (event, attrs) => { - this.$emit('click-link', event, attrs) - return true - }, - }, }), ] }, @@ -58,23 +52,6 @@ export default { required: true, }, }, - - mounted() { - this.$el.addEventListener('click', this.preventOpeningLinks, true) - }, - - unmounted() { - this.$el.removeEventListener('click', this.preventOpeningLinks, true) - }, - - methods: { - preventOpeningLinks(event) { - // We use custom onClick handler only for left clicks - if (event.target.closest('a') && event.button === 0 && !event.ctrlKey) { - event.preventDefault() - } - }, - }, } diff --git a/src/extensions/LinkBubblePluginView.js b/src/extensions/LinkBubblePluginView.js index bddb60f8b0f..18e5dbe5183 100644 --- a/src/extensions/LinkBubblePluginView.js +++ b/src/extensions/LinkBubblePluginView.js @@ -40,8 +40,8 @@ class LinkBubblePluginView { // Required for read-only mode on Firefox. For some reason, editor selection doesn't get // updated when clicking a link in read-only mode on Firefox. clickHandler = (event) => { - // Only regard left clicks without Ctrl - if (event.button !== 0 || event.ctrlKey) { + // Only regard left clicks without Ctrl/Meta + if (event.button !== 0 || event.ctrlKey || event.metaKey) { return false } diff --git a/src/helpers/links.js b/src/helpers/links.js index d31d091690e..61093e4a6f7 100644 --- a/src/helpers/links.js +++ b/src/helpers/links.js @@ -93,7 +93,7 @@ const parseHref = function(dom) { return ref } -const openLink = function(event, _attrs) { +const openLink = function(event, target = '_self') { const linkElement = event.target.closest('a') const htmlHref = linkElement.href const query = OC.parseQueryString(htmlHref) @@ -128,7 +128,7 @@ const openLink = function(event, _attrs) { return } } - window.open(htmlHref) + window.open(htmlHref, target) return true } diff --git a/src/marks/Link.js b/src/marks/Link.js index 0ea39b68eee..0dad50e37a8 100644 --- a/src/marks/Link.js +++ b/src/marks/Link.js @@ -21,15 +21,14 @@ */ import TipTapLink from '@tiptap/extension-link' -import { domHref, parseHref, openLink } from './../helpers/links.js' -import { clickHandler, clickPreventer } from '../plugins/link.js' +import { Plugin, PluginKey } from '@tiptap/pm/state' +import { domHref, parseHref } from './../helpers/links.js' const Link = TipTapLink.extend({ addOptions() { return { ...this.parent?.(), - onClick: openLink, relativePath: null, } }, @@ -63,6 +62,7 @@ const Link = TipTapLink.extend({ return ['a', { ...mark.attrs, href: domHref(mark, this.options.relativePath), + 'data-md-href': mark.attrs.href, rel: 'noopener noreferrer nofollow', }, 0] }, @@ -74,19 +74,48 @@ const Link = TipTapLink.extend({ return !key.startsWith('handleClickLink') }) - if (!this.options.openOnClick) { - return plugins - } - - // add custom click handle plugin + // Custom click handler plugins return [ ...plugins, - clickHandler({ - editor: this.editor, - type: this.type, - onClick: this.options.onClick, + new Plugin({ + key: new PluginKey('textHandleClickLink'), + props: { + handleDOMEvents: { + // Open link in new tab on middle click + pointerup: (view, event) => { + if (event.target.closest('a') && event.button === 1 && !event.ctrlKey && !event.metaKey && !event.shiftKey) { + event.preventDefault() + + const linkElement = event.target.closest('a') + window.open(linkElement.href, '_blank') + } + }, + // Prevent paste into links + // On Linux, middle click pastes, which breaks "open in new tab" on middle click + // Pasting into links will break the link anyway, so just disable it altogether. + paste: (view, event) => { + if (event.target.closest('a')) { + event.stopPropagation() + event.preventDefault() + event.stopImmediatePropagation() + } + }, + // Prevent open link on left click (required for read-only mode) + // Open link in new tab on Ctrl/Cmd + left click + click: (view, event) => { + if (event.target.closest('a')) { + if (event.button === 0) { + event.preventDefault() + if (event.ctrlKey || event.metaKey) { + const linkElement = event.target.closest('a') + window.open(linkElement.href, '_blank') + } + } + } + }, + }, + }, }), - clickPreventer(), ] }, }) diff --git a/src/plugins/link.js b/src/plugins/link.js deleted file mode 100644 index 02d6d5c89d7..00000000000 --- a/src/plugins/link.js +++ /dev/null @@ -1,52 +0,0 @@ -import { Plugin, PluginKey } from '@tiptap/pm/state' - -import { logger } from '../helpers/logger.js' - -const clickHandler = ({ editor, type, onClick }) => { - return new Plugin({ - key: new PluginKey('textHandleClickLink'), - props: { - handleClick: (view, pos, event) => { - // Only regard left clicks without Ctrl - if (event.button !== 0 || event.ctrlKey) { - return false - } - - // Derive link from position of click instead of using `getAttribute()` (like Tiptap handleClick does) - // In Firefox, `getAttribute()` doesn't work in read-only mode - const $clicked = view.state.doc.resolve(pos) - const link = $clicked.marks().find(m => m.type.name === type.name) - if (!link) { - return false - } - - if (!link.attrs.href) { - logger.warn('Could not determine href of link.') - logger.debug('Link', { link }) - return false - } - - event.stopPropagation() - return onClick?.(event, link.attrs) - }, - }, - }) -} - -const clickPreventer = () => { - return new Plugin({ - key: new PluginKey('textAvoidClickLink'), - props: { - handleDOMEvents: { - click: (view, event) => { - if (!view.editable) { - event.preventDefault() - return false - } - }, - }, - }, - }) -} - -export { clickHandler, clickPreventer }