From 489054630591b12b4c9e930719e05a6b06c94991 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/helpers/platform.js | 9 ++++ src/marks/Link.js | 68 +++++++++++++++++++++----- src/plugins/link.js | 52 -------------------- 6 files changed, 68 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/helpers/platform.js b/src/helpers/platform.js index 433b0d31c46..fadcf41f26c 100644 --- a/src/helpers/platform.js +++ b/src/helpers/platform.js @@ -22,3 +22,12 @@ export function isMobilePlatform() { return mobileDevices.some(regex => navigator.userAgent.match(regex)) } + +/** + * Check if current platform is Linux + * + * @return {boolean} whether the platform is Linux + */ +export function isLinux() { + return navigator.userAgent.match(/linux/i) +} diff --git a/src/marks/Link.js b/src/marks/Link.js index 0ea39b68eee..769bde9ae89 100644 --- a/src/marks/Link.js +++ b/src/marks/Link.js @@ -21,16 +21,23 @@ */ 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' +import { isLinux } from '../helpers/platform.js' const Link = TipTapLink.extend({ addOptions() { return { ...this.parent?.(), - onClick: openLink, relativePath: null, + isLinuxCached: isLinux(), + } + }, + + addStorage() { + return { + nopaste: false, } }, @@ -63,6 +70,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 +82,53 @@ 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: { + // Prevent paste on middle click in links (Linux only) + // 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) { + if (this.options.isLinuxCached) { + this.storage.nopaste = true + } + event.preventDefault() + + const linkElement = event.target.closest('a') + window.open(linkElement.href, '_blank') + } + }, + // Prevent paste on middle click in links (Linux only) + paste: (view, event) => { + if (event.target.closest('a') && this.storage.nopaste) { + if (this.options.isLinuxCached) { + event.stopPropagation() + event.preventDefault() + event.stopImmediatePropagation() + this.storage.nopaste = false + } + } + }, + // 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 }