Skip to content

Commit 5bf15b4

Browse files
committed
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>
1 parent 0b7b0d6 commit 5bf15b4

5 files changed

Lines changed: 93 additions & 57 deletions

File tree

src/components/ReadOnlyEditor.vue

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,17 @@ export default {
8080
this.editor.destroy()
8181
},
8282
methods: {
83+
8384
createRichEditor() {
8485
return new Editor({
8586
content: this.htmlContent,
8687
extensions: [
87-
RichText.configure(this.richTextOptions),
88+
RichText.configure({
89+
...this.richTextOptions,
90+
link: {
91+
onClick: (event, attrs) => this.$emit('click-link', event, attrs),
92+
},
93+
}),
8894
...this.extensions,
8995
],
9096
})
@@ -97,6 +103,17 @@ export default {
97103
})
98104
},
99105
106+
/* Stop the browser from opening links.
107+
* Clicks are handled inside the Link mark just like in edit mode.
108+
*/
109+
preventOpeningLinks() {
110+
this.$el.addEventListener('click', event => {
111+
if (event.target.closest('a')) {
112+
event.preventDefault()
113+
}
114+
})
115+
},
116+
100117
updateContent() {
101118
this.editor.commands.setContent(this.htmlContent)
102119
},

src/extensions/RichText.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export default Extension.create({
5858
addOptions() {
5959
return {
6060
currentDirectory: undefined,
61+
link: {},
6162
}
6263
},
6364

@@ -94,7 +95,10 @@ export default Extension.create({
9495
Dropcursor,
9596
]
9697
if (this.options.link !== false) {
97-
extensions.push(Link.configure({ openOnClick: true }))
98+
extensions.push(Link.configure({
99+
...this.options.link,
100+
openOnClick: true,
101+
}))
98102
}
99103
return extensions
100104
},

src/helpers/links.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
*/
2222

2323
import { generateUrl } from '@nextcloud/router'
24+
import markdownit from './../markdownit/index.js'
2425

2526
const absolutePath = function(base, rel) {
2627
if (!rel) {
@@ -77,7 +78,41 @@ const parseHref = function(dom) {
7778
return ref
7879
}
7980

81+
const openLink = function(event, _attrs) {
82+
const linkElement = event.target.closest('a')
83+
event.stopPropagation()
84+
const htmlHref = linkElement.href
85+
if (event.button === 0 && !event.ctrlKey && htmlHref.startsWith(window.location.origin)) {
86+
const query = OC.parseQueryString(htmlHref)
87+
const fragment = OC.parseQueryString(htmlHref.split('#').pop())
88+
if (query.dir && fragment.relPath) {
89+
const filename = fragment.relPath.split('/').pop()
90+
const path = `${query.dir}/${filename}`
91+
document.title = `${filename} - ${OC.theme.title}`
92+
if (window.location.pathname.match(/apps\/files\/$/)) {
93+
// The files app still lacks a popState handler
94+
// to allow for using the back button
95+
// OC.Util.History.pushState('', htmlHref)
96+
}
97+
OCA.Viewer.open({ path })
98+
return
99+
}
100+
if (query.fileId) {
101+
// open the direct file link
102+
window.open(generateUrl(`/f/${query.fileId}`))
103+
return
104+
}
105+
}
106+
if (!markdownit.validateLink(htmlHref)) {
107+
console.error('Invalid link', htmlHref)
108+
return false
109+
}
110+
window.open(htmlHref)
111+
return true
112+
}
113+
80114
export {
81115
domHref,
82116
parseHref,
117+
openLink,
83118
}

src/marks/Link.js

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,29 @@
2121
*/
2222

2323
import TipTapLink from '@tiptap/extension-link'
24-
import { domHref, parseHref } from './../helpers/links.js'
24+
import { domHref, parseHref, openLink } from './../helpers/links.js'
2525
import { clickHandler } from '../plugins/link.js'
2626

2727
const Link = TipTapLink.extend({
2828

29-
attrs: {
30-
href: {
31-
default: null,
32-
},
29+
addOptions() {
30+
return {
31+
...this.parent?.(),
32+
onClick: openLink,
33+
}
34+
},
35+
36+
addAttributes() {
37+
return {
38+
href: {
39+
default: null,
40+
},
41+
}
3342
},
3443

3544
inclusive: false,
3645

37-
parseDOM: [
46+
parseHTML: [
3847
{
3948
tag: 'a[href]',
4049
getAttrs: dom => ({
@@ -43,10 +52,10 @@ const Link = TipTapLink.extend({
4352
},
4453
],
4554

46-
toDOM: node => ['a', {
47-
...node.attrs,
48-
href: domHref(node),
49-
title: node.attrs.href,
55+
renderHTML: ({ mark, HTMLAttributes }) => ['a', {
56+
...mark.attrs,
57+
href: domHref(mark),
58+
title: mark.attrs.href,
5059
rel: 'noopener noreferrer nofollow',
5160
}, 0],
5261

@@ -62,7 +71,14 @@ const Link = TipTapLink.extend({
6271
}
6372

6473
// add custom click handler
65-
return [...plugins, clickHandler({ editor: this.editor, type: this.type })]
74+
return [
75+
...plugins,
76+
clickHandler({
77+
editor: this.editor,
78+
type: this.type,
79+
onClick: this.options.onClick,
80+
}),
81+
]
6682
},
6783
})
6884

src/plugins/link.js

Lines changed: 8 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,16 @@
1-
import { generateUrl } from '@nextcloud/router'
2-
import { Plugin } from 'prosemirror-state'
3-
import markdownit from './../markdownit/index.js'
1+
import { Plugin, PluginKey } from 'prosemirror-state'
42

5-
const clickHandler = ({ editor }) => {
3+
const clickHandler = ({ editor, type, onClick }) => {
64
return new Plugin({
75
props: {
6+
key: new PluginKey('textLink'),
87
handleClick: (view, pos, event) => {
9-
const linkElement = event.target.parentElement instanceof HTMLAnchorElement
10-
? event.target.parentElement
11-
: event.target
12-
13-
const isLink = linkElement && linkElement instanceof HTMLAnchorElement
14-
15-
const htmlHref = linkElement?.href
16-
17-
// is handleable link
18-
if (htmlHref && isLink) {
19-
event.stopPropagation()
20-
21-
if (event.button === 0 && !event.ctrlKey && htmlHref.startsWith(window.location.origin)) {
22-
const query = OC.parseQueryString(htmlHref)
23-
const fragment = OC.parseQueryString(htmlHref.split('#').pop())
24-
if (query.dir && fragment.relPath) {
25-
const filename = fragment.relPath.split('/').pop()
26-
const path = `${query.dir}/${filename}`
27-
document.title = `${filename} - ${OC.theme.title}`
28-
if (window.location.pathname.match(/apps\/files\/$/)) {
29-
// The files app still lacks a popState handler
30-
// to allow for using the back button
31-
// OC.Util.History.pushState('', htmlHref)
32-
}
33-
OCA.Viewer.open({ path })
34-
return
35-
}
36-
if (query.fileId) {
37-
// open the direct file link
38-
window.open(generateUrl(`/f/${query.fileId}`))
39-
return
40-
}
41-
}
42-
43-
if (!markdownit.validateLink(htmlHref)) {
44-
console.error('Invalid link', htmlHref)
45-
return
46-
}
47-
48-
window.open(htmlHref)
8+
const attrs = editor.getAttributes(type)
9+
const link = event.target.closest('a')
10+
if (link && attrs.href && onClick) {
11+
return onClick(event, attrs)
4912
}
13+
return false
5014
},
5115
},
5216
})

0 commit comments

Comments
 (0)