Skip to content

Commit f34d060

Browse files
authored
Merge pull request #5422 from nextcloud/fix/link_handling2
Several link handling fixes
2 parents 1ce51ea + 0dd249c commit f34d060

File tree

5 files changed

+50
-70
lines changed

5 files changed

+50
-70
lines changed

src/components/Menu/ActionInsertLink.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
</NcActionButton>
5151
<NcActionInput v-if="isInputMode"
5252
type="text"
53-
:value="href"
53+
:value.sync="href"
5454
:data-text-action-entry="`${actionEntry.key}-input`"
5555
@submit="linkWebsite">
5656
<template #icon>
@@ -144,8 +144,8 @@ export default {
144144
.then((file) => {
145145
const client = OC.Files.getClient()
146146
client.getFileInfo(file).then((_status, fileInfo) => {
147-
const href = generateUrl(`/f/${fileInfo.id}`)
148-
this.setLink(href, fileInfo.name)
147+
const url = new URL(generateUrl(`/f/${fileInfo.id}`), window.origin)
148+
this.setLink(url.href, fileInfo.name)
149149
this.startPath = fileInfo.path + (fileInfo.type === 'dir' ? `/${fileInfo.name}/` : '')
150150
})
151151
this.menuOpen = false

src/extensions/LinkBubblePluginView.js

Lines changed: 12 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
import { VueRenderer } from '@tiptap/vue-2'
22
import tippy from 'tippy.js'
3+
import debounce from 'debounce'
34
import { domHref } from '../helpers/links.js'
45
import LinkBubbleView from '../components/Link/LinkBubbleView.vue'
56

67
import { getViewerVue } from '../ViewerVue.js'
78

8-
const updateDelay = 250
9-
109
class LinkBubblePluginView {
1110

1211
#component = null
13-
#preventHide = false
1412
#hadUpdateFromClick = false
15-
#updateDebounceTimer = undefined
1613

1714
constructor({ editor, view }) {
1815
this.editor = editor
@@ -33,18 +30,16 @@ class LinkBubblePluginView {
3330
this.view.dom.addEventListener('dragstart', this.dragOrScrollHandler)
3431
this.view.dom.addEventListener('click', this.clickHandler)
3532
document.addEventListener('scroll', this.dragOrScrollHandler, { capture: true })
36-
this.editor.on('focus', this.focusHandler)
37-
this.editor.on('blur', this.blurHandler)
3833
}
3934

40-
dragOrScrollHandler = () => {
35+
dragOrScrollHandler = (event) => {
36+
// Cypress fires unexpected scroll events, which breaks testing the link bubble
37+
if (window.Cypress) {
38+
return
39+
}
4140
this.hide()
4241
}
4342

44-
pointerdownHandler = () => {
45-
this.#preventHide = true
46-
}
47-
4843
// Required for read-only mode on Firefox. For some reason, editor selection doesn't get
4944
// updated when clicking a link in read-only mode on Firefox.
5045
clickHandler = (event) => {
@@ -67,28 +62,6 @@ class LinkBubblePluginView {
6762
setTimeout(() => this.updateFromClick(this.editor.view, clickedPos))
6863
}
6964

70-
focusHandler = () => {
71-
// we use `setTimeout` to make sure `selection` is already updated
72-
setTimeout(() => this.update(this.editor.view))
73-
}
74-
75-
blurHandler = ({ event }) => {
76-
if (this.#preventHide) {
77-
this.#preventHide = false
78-
return
79-
}
80-
81-
if (event?.relatedTarget && this.tippy?.popper.firstChild.contains(event.relatedTarget)) {
82-
return
83-
}
84-
85-
this.hide()
86-
}
87-
88-
tippyBlurHandler = () => {
89-
this.hide()
90-
}
91-
9265
keydownHandler = (event) => {
9366
if (event.key === 'Escape') {
9467
event.preventDefault()
@@ -116,10 +89,6 @@ class LinkBubblePluginView {
11689
strategy: 'fixed',
11790
},
11891
})
119-
120-
this.tippy.popper.firstChild?.addEventListener('pointerdown', this.pointerdownHandler, { capture: true })
121-
// Hide tippy on its own blur event as well
122-
this.tippy.popper.firstChild?.addEventListener('blur', this.tippyBlurHandler)
12392
}
12493

12594
update(view, oldState) {
@@ -132,16 +101,10 @@ class LinkBubblePluginView {
132101
return
133102
}
134103

135-
if (this.#updateDebounceTimer) {
136-
clearTimeout(this.#updateDebounceTimer)
137-
}
138-
139-
this.#updateDebounceTimer = window.setTimeout(() => {
140-
this.updateFromSelection(view)
141-
}, updateDelay)
104+
this.updateFromSelection(view)
142105
}
143106

144-
updateFromSelection(view) {
107+
updateFromSelection = debounce((view) => {
145108
// Don't update directly after updateFromClick. Prevents race condition in read-only documents in Chrome.
146109
if (this.#hadUpdateFromClick) {
147110
return
@@ -164,7 +127,7 @@ class LinkBubblePluginView {
164127
const shouldShow = !!linkNode && hasEditorFocus
165128

166129
this.updateTooltip(view, shouldShow, linkNode, nodeStart)
167-
}
130+
}, 250)
168131

169132
updateFromClick(view, clickedLinkPos) {
170133
const nodeStart = clickedLinkPos.pos - clickedLinkPos.textOffset
@@ -174,11 +137,11 @@ class LinkBubblePluginView {
174137
this.#hadUpdateFromClick = true
175138
setTimeout(() => {
176139
this.#hadUpdateFromClick = false
177-
}, 200)
140+
}, 500)
178141
this.updateTooltip(this.editor.view, shouldShow, clickedNode, nodeStart)
179142
}
180143

181-
updateTooltip = (view, shouldShow, linkNode, nodeStart) => {
144+
updateTooltip(view, shouldShow, linkNode, nodeStart) {
182145
this.createTooltip()
183146

184147
if (!shouldShow || !linkNode) {
@@ -216,17 +179,13 @@ class LinkBubblePluginView {
216179
}
217180

218181
destroy() {
219-
this.tippy?.popper.firstChild?.removeEventListener('blur', this.tippyBlurHandler)
220-
this.tippy?.popper.firstChild?.removeEventListener('pointerdown', this.pointerdownHandler, { capture: true })
221182
this.tippy?.destroy()
222183
this.view.dom.removeEventListener('dragstart', this.dragOrScrollHandler)
223184
this.view.dom.removeEventListener('click', this.clickHandler)
224185
document.removeEventListener('scroll', this.dragOrScrollHandler, { capture: true })
225-
this.editor.off('focus', this.focusHandler)
226-
this.editor.off('blur', this.blurHandler)
227186
}
228187

229-
linkNodeFromSelection = (view) => {
188+
linkNodeFromSelection(view) {
230189
const { state } = view
231190
const { selection } = state
232191

src/helpers/links.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
*
2121
*/
2222

23+
import { loadState } from '@nextcloud/initial-state'
2324
import { generateUrl } from '@nextcloud/router'
2425

2526
const domHref = function(node, relativePath) {
@@ -36,6 +37,10 @@ const domHref = function(node, relativePath) {
3637
if (ref.startsWith('#')) {
3738
return ref
3839
}
40+
// Don't rewrite links in collectives app context
41+
if (loadState('core', 'active-app') === 'collectives') {
42+
return ref
43+
}
3944
// Don't rewrite links to the collectives app
4045
if (ref.includes('/apps/collectives/')) {
4146
return ref
@@ -45,7 +50,7 @@ const domHref = function(node, relativePath) {
4550
const match = ref.match(/^([^?]*)\?fileId=(\d+)/)
4651
if (match) {
4752
const [, , id] = match
48-
return generateUrl(`/f/${id}`)
53+
return (new URL(generateUrl(`/f/${id}`), window.origin)).href
4954
}
5055
return ref
5156
}
@@ -58,7 +63,7 @@ const parseHref = function(dom) {
5863
const match = ref.match(/\?dir=([^&]*)&openfile=([^&]*)#relPath=([^&]*)/)
5964
if (match) {
6065
const [, , id] = match
61-
return generateUrl(`/f/${id}`)
66+
return (new URL(generateUrl(`/f/${id}`), window.origin)).href
6267
}
6368
return ref
6469
}

src/marks/Link.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,10 @@ const Link = TipTapLink.extend({
8282
props: {
8383
handleDOMEvents: {
8484
// Open link in new tab on middle click
85-
pointerup: (view, event) => {
85+
auxclick: (view, event) => {
8686
if (event.target.closest('a') && event.button === 1 && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
8787
event.preventDefault()
88+
event.stopImmediatePropagation()
8889

8990
const linkElement = event.target.closest('a')
9091
window.open(linkElement.href, '_blank')

src/tests/helpers/links.spec.js

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { domHref, parseHref } from '../../helpers/links'
2+
import { loadState } from '@nextcloud/initial-state'
23

34
global.OCA = {
45
Viewer: {
@@ -12,6 +13,9 @@ global.OC = {
1213

1314
global._oc_webroot = ''
1415

16+
jest.mock('@nextcloud/initial-state')
17+
loadState.mockImplementation((app, key) => 'files')
18+
1519
describe('Preparing href attributes for the DOM', () => {
1620

1721
test('leave empty hrefs alone', () => {
@@ -34,22 +38,22 @@ describe('Preparing href attributes for the DOM', () => {
3438

3539
test('relative link with fileid (old format from file picker)', () => {
3640
expect(domHref({attrs: {href: 'otherfile?fileId=123'}}))
37-
.toBe('/f/123')
41+
.toBe('http://localhost/f/123')
3842
})
3943

4044
test('relative path with ../ (old format from file picker)', () => {
4145
expect(domHref({attrs: {href: '../other/otherfile?fileId=123'}}))
42-
.toBe('/f/123')
46+
.toBe('http://localhost/f/123')
4347
})
4448

4549
test('absolute path (old format from file picker)', () => {
4650
expect(domHref({attrs: {href: '/other/otherfile?fileId=123'}}))
47-
.toBe('/f/123')
51+
.toBe('http://localhost/f/123')
4852
})
4953

5054
test('absolute path (old format from file picker)', () => {
5155
expect(domHref({attrs: {href: '/otherfile?fileId=123'}}))
52-
.toBe('/f/123')
56+
.toBe('http://localhost/f/123')
5357
})
5458

5559
})
@@ -72,7 +76,7 @@ describe('Extracting short urls from the DOM', () => {
7276

7377
test('relative link with fileid (old format from file picker)', () => {
7478
expect(parseHref(domStub('?dir=/other&openfile=123#relPath=../other/otherfile')))
75-
.toBe('/f/123')
79+
.toBe('http://localhost/f/123')
7680
})
7781

7882
})
@@ -99,22 +103,22 @@ describe('Inserting hrefs into the dom and extracting them again', () => {
99103

100104
test('old relative link format (from file picker) is rewritten', () => {
101105
expect(insertAndExtract({href: 'otherfile?fileId=123'}))
102-
.toBe('/f/123')
106+
.toBe('http://localhost/f/123')
103107
})
104108

105109
test('old relative link format with ../ (from file picker) is rewritten', () => {
106110
expect(insertAndExtract({href: '../otherfile?fileId=123'}))
107-
.toBe('/f/123')
111+
.toBe('http://localhost/f/123')
108112
})
109113

110114
test('old absolute link format (from file picker) is rewritten', () => {
111115
expect(insertAndExtract({href: '/otherfile?fileId=123'}))
112-
.toBe('/f/123')
116+
.toBe('http://localhost/f/123')
113117
})
114118

115-
test('default absolute link format is unchanged', () => {
116-
expect(insertAndExtract({href: '/f/123'}))
117-
.toBe('/f/123')
119+
test('default full URL link format is unchanged', () => {
120+
expect(insertAndExtract({href: 'http://localhost/f/123'}))
121+
.toBe('http://localhost/f/123')
118122
})
119123

120124
test('absolute link to collectives page is unchanged', () => {
@@ -123,3 +127,14 @@ describe('Inserting hrefs into the dom and extracting them again', () => {
123127
})
124128

125129
})
130+
131+
describe('Preparing href attributes for the DOM in Collectives app', () => {
132+
beforeAll(() => {
133+
loadState.mockImplementation((app, key) => 'collectives')
134+
})
135+
136+
test('relative link with fileid in Collectives', () => {
137+
expect(domHref({attrs: {href: 'otherfile?fileId=123'}}))
138+
.toBe('otherfile?fileId=123')
139+
})
140+
})

0 commit comments

Comments
 (0)