diff options
author | Max <max@nextcloud.com> | 2022-04-11 17:11:37 +0300 |
---|---|---|
committer | Max <max@nextcloud.com> | 2022-06-07 20:41:57 +0300 |
commit | 5bf15b4dc0e1e683f08db3aff57f258443715ef5 (patch) | |
tree | 4e351ae05d015b444e41e1055813d22d38947d9a | |
parent | 0b7b0d61a0bbf7ef25bf8fe36af2f8d8718f507f (diff) |
feature: emit clickLink from `ReadOnlyEditor`
The Prosemirror plugin with the `handleClick` handler
only customizes the prosemirror handling of the click event.
In read only mode we are not in a content-editable section.
So clicking a link will cause the browser to open the url with a page reload.
Allow overwriting this behavior by handling all link clicks via prosemirror.
Set `onClick` option on the `Link` mark to customize the behavior.
Emit a `click-link` event from `ReadOnlyEditor` with info about the event
and the attributes of the link mark.
Find the link that was clicked based on the clicked marks
rather than the element in the event.
This way we can get access to the attributes of the mark
without relying on the selection or even changing it.
Also add plugin key to link click handler
Signed-off-by: Max <max@nextcloud.com>
-rw-r--r-- | src/components/ReadOnlyEditor.vue | 19 | ||||
-rw-r--r-- | src/extensions/RichText.js | 6 | ||||
-rw-r--r-- | src/helpers/links.js | 35 | ||||
-rw-r--r-- | src/marks/Link.js | 38 | ||||
-rw-r--r-- | src/plugins/link.js | 52 |
5 files changed, 93 insertions, 57 deletions
diff --git a/src/components/ReadOnlyEditor.vue b/src/components/ReadOnlyEditor.vue index 02d2b8c1c..e35aa9896 100644 --- a/src/components/ReadOnlyEditor.vue +++ b/src/components/ReadOnlyEditor.vue @@ -80,11 +80,17 @@ export default { this.editor.destroy() }, methods: { + createRichEditor() { return new Editor({ content: this.htmlContent, extensions: [ - RichText.configure(this.richTextOptions), + RichText.configure({ + ...this.richTextOptions, + link: { + onClick: (event, attrs) => this.$emit('click-link', event, attrs), + }, + }), ...this.extensions, ], }) @@ -97,6 +103,17 @@ export default { }) }, + /* Stop the browser from opening links. + * Clicks are handled inside the Link mark just like in edit mode. + */ + preventOpeningLinks() { + this.$el.addEventListener('click', event => { + if (event.target.closest('a')) { + event.preventDefault() + } + }) + }, + updateContent() { this.editor.commands.setContent(this.htmlContent) }, diff --git a/src/extensions/RichText.js b/src/extensions/RichText.js index d9a9ea0e7..ce8931371 100644 --- a/src/extensions/RichText.js +++ b/src/extensions/RichText.js @@ -58,6 +58,7 @@ export default Extension.create({ addOptions() { return { currentDirectory: undefined, + link: {}, } }, @@ -94,7 +95,10 @@ export default Extension.create({ Dropcursor, ] if (this.options.link !== false) { - extensions.push(Link.configure({ openOnClick: true })) + extensions.push(Link.configure({ + ...this.options.link, + openOnClick: true, + })) } return extensions }, diff --git a/src/helpers/links.js b/src/helpers/links.js index 18f556b5c..81df51444 100644 --- a/src/helpers/links.js +++ b/src/helpers/links.js @@ -21,6 +21,7 @@ */ import { generateUrl } from '@nextcloud/router' +import markdownit from './../markdownit/index.js' const absolutePath = function(base, rel) { if (!rel) { @@ -77,7 +78,41 @@ const parseHref = function(dom) { return ref } +const openLink = function(event, _attrs) { + const linkElement = event.target.closest('a') + event.stopPropagation() + const htmlHref = linkElement.href + if (event.button === 0 && !event.ctrlKey && htmlHref.startsWith(window.location.origin)) { + const query = OC.parseQueryString(htmlHref) + const fragment = OC.parseQueryString(htmlHref.split('#').pop()) + if (query.dir && fragment.relPath) { + const filename = fragment.relPath.split('/').pop() + const path = `${query.dir}/${filename}` + document.title = `${filename} - ${OC.theme.title}` + if (window.location.pathname.match(/apps\/files\/$/)) { + // The files app still lacks a popState handler + // to allow for using the back button + // OC.Util.History.pushState('', htmlHref) + } + OCA.Viewer.open({ path }) + return + } + if (query.fileId) { + // open the direct file link + window.open(generateUrl(`/f/${query.fileId}`)) + return + } + } + if (!markdownit.validateLink(htmlHref)) { + console.error('Invalid link', htmlHref) + return false + } + window.open(htmlHref) + return true +} + export { domHref, parseHref, + openLink, } diff --git a/src/marks/Link.js b/src/marks/Link.js index 20cd22b9f..f6e1d7c4a 100644 --- a/src/marks/Link.js +++ b/src/marks/Link.js @@ -21,20 +21,29 @@ */ import TipTapLink from '@tiptap/extension-link' -import { domHref, parseHref } from './../helpers/links.js' +import { domHref, parseHref, openLink } from './../helpers/links.js' import { clickHandler } from '../plugins/link.js' const Link = TipTapLink.extend({ - attrs: { - href: { - default: null, - }, + addOptions() { + return { + ...this.parent?.(), + onClick: openLink, + } + }, + + addAttributes() { + return { + href: { + default: null, + }, + } }, inclusive: false, - parseDOM: [ + parseHTML: [ { tag: 'a[href]', getAttrs: dom => ({ @@ -43,10 +52,10 @@ const Link = TipTapLink.extend({ }, ], - toDOM: node => ['a', { - ...node.attrs, - href: domHref(node), - title: node.attrs.href, + renderHTML: ({ mark, HTMLAttributes }) => ['a', { + ...mark.attrs, + href: domHref(mark), + title: mark.attrs.href, rel: 'noopener noreferrer nofollow', }, 0], @@ -62,7 +71,14 @@ const Link = TipTapLink.extend({ } // add custom click handler - return [...plugins, clickHandler({ editor: this.editor, type: this.type })] + return [ + ...plugins, + clickHandler({ + editor: this.editor, + type: this.type, + onClick: this.options.onClick, + }), + ] }, }) diff --git a/src/plugins/link.js b/src/plugins/link.js index 5f352851e..1a656362e 100644 --- a/src/plugins/link.js +++ b/src/plugins/link.js @@ -1,52 +1,16 @@ -import { generateUrl } from '@nextcloud/router' -import { Plugin } from 'prosemirror-state' -import markdownit from './../markdownit/index.js' +import { Plugin, PluginKey } from 'prosemirror-state' -const clickHandler = ({ editor }) => { +const clickHandler = ({ editor, type, onClick }) => { return new Plugin({ props: { + key: new PluginKey('textLink'), handleClick: (view, pos, event) => { - const linkElement = event.target.parentElement instanceof HTMLAnchorElement - ? event.target.parentElement - : event.target - - const isLink = linkElement && linkElement instanceof HTMLAnchorElement - - const htmlHref = linkElement?.href - - // is handleable link - if (htmlHref && isLink) { - event.stopPropagation() - - if (event.button === 0 && !event.ctrlKey && htmlHref.startsWith(window.location.origin)) { - const query = OC.parseQueryString(htmlHref) - const fragment = OC.parseQueryString(htmlHref.split('#').pop()) - if (query.dir && fragment.relPath) { - const filename = fragment.relPath.split('/').pop() - const path = `${query.dir}/${filename}` - document.title = `${filename} - ${OC.theme.title}` - if (window.location.pathname.match(/apps\/files\/$/)) { - // The files app still lacks a popState handler - // to allow for using the back button - // OC.Util.History.pushState('', htmlHref) - } - OCA.Viewer.open({ path }) - return - } - if (query.fileId) { - // open the direct file link - window.open(generateUrl(`/f/${query.fileId}`)) - return - } - } - - if (!markdownit.validateLink(htmlHref)) { - console.error('Invalid link', htmlHref) - return - } - - window.open(htmlHref) + const attrs = editor.getAttributes(type) + const link = event.target.closest('a') + if (link && attrs.href && onClick) { + return onClick(event, attrs) } + return false }, }, }) |