Skip to content

Commit 32536e4

Browse files
authored
Merge pull request #7475 from nextcloud/backport/7474/stable30
[stable30] Several link bubble/preview fixes
2 parents 6d378c2 + 2a6cf30 commit 32536e4

12 files changed

Lines changed: 109 additions & 22 deletions

File tree

cypress/e2e/nodes/Links.spec.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe('test link marks', function() {
4747
cy.get('.link-view-bubble .widget-default', { timeout: 10000 })
4848
.find('.widget-default--name')
4949
.contains('Nextcloud')
50-
.click({ force: true })
50+
.click()
5151
})
5252

5353
it('shows a link preview in the bubble after browsing to link', () => {
@@ -64,6 +64,16 @@ describe('test link marks', function() {
6464
.contains('Nextcloud')
6565
})
6666

67+
it('open button opens a new tab', () => {
68+
const link = 'https://nextcloud.com/'
69+
cy.insertLine(link)
70+
clickLink(link)
71+
72+
cy.get('.link-view-bubble button[title="Open link"]').click()
73+
74+
cy.get('@winOpen').should('have.been.calledOnce')
75+
})
76+
6777
it('closes the link bubble when clicking elsewhere', () => {
6878
const link = 'https://nextcloud.com/'
6979
cy.insertLine(link)

cypress/e2e/nodes/PreviewOptions.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe('Preview Options', function() {
2323
})
2424

2525
it('should render previewOptions correctly', function() {
26+
cy.get('.action-button__text').contains('Open in new tab').should('be.visible')
2627
cy.get('.action-button__text').contains('Remove link').should('be.visible')
2728
cy.get('.action-radio__label').each(el => {
2829
cy.wrap(el).invoke('text').should('match', /Text only|Show link preview/)

src/components/Editor.provider.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6+
import { openLink } from '../helpers/links.js'
67
import { logger } from '../helpers/logger.js'
78

89
export const EDITOR = Symbol('tiptap:editor')
@@ -16,6 +17,7 @@ export const SYNC_SERVICE = Symbol('sync:service')
1617
export const EDITOR_UPLOAD = Symbol('editor:upload')
1718
export const HOOK_MENTION_SEARCH = Symbol('hook:mention-search')
1819
export const HOOK_MENTION_INSERT = Symbol('hook:mention-insert')
20+
export const OPEN_LINK_HANDLER = Symbol('editor:open-link-handler')
1921

2022
export const useEditorMixin = {
2123
inject: {
@@ -99,3 +101,13 @@ export const useMentionHook = {
99101
},
100102
},
101103
}
104+
export const useOpenLinkHandler = {
105+
inject: {
106+
$openLinkHandler: {
107+
from: OPEN_LINK_HANDLER,
108+
default: {
109+
openLink,
110+
},
111+
},
112+
},
113+
}

src/components/Editor/PreviewOptions.vue

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,17 @@
2727
{{ t('text', 'Show link preview') }}
2828
</NcActionRadio>
2929
<NcActionSeparator />
30-
<NcActionButton close-after-click="true" @click="deleteNode">
30+
<NcActionButton v-if="href" close-after-click @click="openLink">
31+
<template #icon>
32+
<OpenIcon :size="20" />
33+
</template>
34+
{{ t('text', 'Open in new tab') }}
35+
</NcActionButton>
36+
<NcActionButton close-after-click @click="deleteNode">
3137
<template #icon>
3238
<DeleteIcon :size="20" />
3339
</template>
34-
{{ t('text','Remove link') }}
40+
{{ t('text', 'Remove link') }}
3541
</NcActionButton>
3642
</NcActions>
3743
</div>
@@ -45,6 +51,7 @@ import NcActionCaption from '@nextcloud/vue/components/NcActionCaption'
4551
import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator'
4652
import DotsVerticalIcon from 'vue-material-design-icons/DotsVertical.vue'
4753
import DeleteIcon from 'vue-material-design-icons/Delete.vue'
54+
import OpenIcon from 'vue-material-design-icons/OpenInNew.vue'
4855
4956
export default {
5057
name: 'PreviewOptions',
@@ -57,13 +64,19 @@ export default {
5764
NcActionRadio,
5865
NcActionSeparator,
5966
DeleteIcon,
67+
OpenIcon,
6068
},
6169
6270
props: {
6371
type: {
6472
type: String,
6573
required: true,
6674
},
75+
href: {
76+
type: String,
77+
required: false,
78+
default: '',
79+
},
6780
offset: {
6881
type: Number,
6982
required: true,
@@ -104,6 +117,10 @@ export default {
104117
to: this.offset + this.nodeSize,
105118
})
106119
},
120+
openLink() {
121+
if (!this.href) return
122+
window.open(this.href, '_blank').focus()
123+
},
107124
},
108125
}
109126
</script>

src/components/Link/LinkBubbleView.vue

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@
1010
<div class="link-view-bubble__title">
1111
{{ title }}
1212
</div>
13+
<!-- open link -->
14+
<NcButton :title="t('text', 'Open link')"
15+
:aria-label="t('text', 'Open link')"
16+
type="tertiary"
17+
@click="openLink(href)">
18+
<template #icon>
19+
<OpenInNewIcon :size="20" />
20+
</template>
21+
</NcButton>
1322
<!-- copy link -->
1423
<NcButton :title="copyLinkTooltip"
1524
:aria-label="copyLinkTooltip"
@@ -95,9 +104,11 @@ import CheckIcon from 'vue-material-design-icons/Check.vue'
95104
import CloseIcon from 'vue-material-design-icons/Close.vue'
96105
import ContentCopyIcon from 'vue-material-design-icons/ContentCopy.vue'
97106
import LinkOffIcon from 'vue-material-design-icons/LinkOff.vue'
107+
import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue'
98108
import PencilIcon from 'vue-material-design-icons/Pencil.vue'
99109
100110
import CopyToClipboardMixin from '../../mixins/CopyToClipboardMixin.js'
111+
import { useOpenLinkHandler } from '../Editor.provider.js'
101112
102113
const PROTOCOLS_WITH_PREVIEW = ['http:', 'https:']
103114
@@ -113,11 +124,13 @@ export default {
113124
NcReferenceList,
114125
NcTextField,
115126
LinkOffIcon,
127+
OpenInNewIcon,
116128
PencilIcon,
117129
},
118130
119131
mixins: [
120132
CopyToClipboardMixin,
133+
useOpenLinkHandler,
121134
],
122135
123136
props: {
@@ -195,6 +208,10 @@ export default {
195208
this.referenceTitle = null
196209
},
197210
211+
openLink(href) {
212+
this.$openLinkHandler.openLink(href)
213+
},
214+
198215
async copyLink() {
199216
await this.copyToClipboard(this.href)
200217
},

src/css/prosemirror.scss

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,6 @@ div.ProseMirror {
177177
font-size: var(--default-font-size);
178178
}
179179

180-
img {
181-
cursor: default;
182-
max-width: 100%;
183-
}
184-
185180
hr {
186181
padding: 2px 0;
187182
border: none;
@@ -402,5 +397,6 @@ div.ProseMirror {
402397
}
403398

404399
.tippy-content div {
400+
box-sizing: border-box;
405401
visibility: visible !important;
406402
}

src/editor.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
import Vue from 'vue'
77
import store from './store/index.js'
88
import { subscribe } from '@nextcloud/event-bus'
9-
import { EDITOR_UPLOAD, HOOK_MENTION_SEARCH, HOOK_MENTION_INSERT, ATTACHMENT_RESOLVER } from './components/Editor.provider.js'
9+
import { EDITOR_UPLOAD, HOOK_MENTION_SEARCH, HOOK_MENTION_INSERT, ATTACHMENT_RESOLVER, OPEN_LINK_HANDLER } from './components/Editor.provider.js'
1010
import { ACTION_ATTACHMENT_PROMPT } from './components/Editor/MediaHandler.provider.js'
11+
import { openLink } from './helpers/links.js'
1112
// eslint-disable-next-line import/no-unresolved, n/no-missing-import
1213
import 'vite/modulepreload-polyfill'
1314

@@ -184,6 +185,7 @@ window.OCA.Text.createEditor = async function({
184185
onFileInsert = undefined,
185186
onMentionSearch = undefined,
186187
onMentionInsert = undefined,
188+
openLinkHandler = undefined,
187189
onSearch = undefined,
188190
}) {
189191
const { default: MarkdownContentEditor } = await import(/* webpackChunkName: "editor" */'./components/Editor/MarkdownContentEditor.vue')
@@ -205,6 +207,9 @@ window.OCA.Text.createEditor = async function({
205207
[EDITOR_UPLOAD]: !!sessionEditor,
206208
[HOOK_MENTION_SEARCH]: sessionEditor ? true : onMentionSearch,
207209
[HOOK_MENTION_INSERT]: sessionEditor ? true : onMentionInsert,
210+
[OPEN_LINK_HANDLER]: {
211+
openLink: openLinkHandler || openLink,
212+
},
208213
[ATTACHMENT_RESOLVER]: {
209214
resolve(src, preferRaw) {
210215
return [{

src/extensions/LinkBubble.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ const LinkBubble = Extension.create({
2121
return [
2222
linkBubble({
2323
editor: this.editor,
24-
parent: this.editor.contentComponent,
2524
}),
2625
]
2726
},

src/helpers/links.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,29 @@ const isLinkToSelfWithHash = function(href) {
5656
return href?.startsWith('#') || href?.startsWith(locationNoHash + '#')
5757
}
5858

59+
/**
60+
* Open links, to be used as custom click handler
61+
*
62+
* @param {string} href the link href
63+
*/
64+
const openLink = function(href) {
65+
const linkUrl = new URL(href, window.location.href)
66+
// Consider rerouting links to Collectives if already inside Collectives app
67+
const collectivesUrlBase = '/apps/collectives'
68+
if (window.OCA.Collectives?.vueRouter
69+
&& linkUrl.pathname.toString().startsWith(generateUrl(collectivesUrlBase))) {
70+
const collectivesUrl = linkUrl.href.substring(
71+
linkUrl.href.indexOf(collectivesUrlBase) + collectivesUrlBase.length,
72+
)
73+
window.OCA.Collectives.vueRouter.push(collectivesUrl)
74+
return
75+
}
76+
window.open(linkUrl, '_blank')
77+
}
78+
5979
export {
6080
domHref,
61-
parseHref,
6281
isLinkToSelfWithHash,
82+
openLink,
83+
parseHref,
6384
}

src/plugins/LinkBubblePluginView.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class LinkBubblePluginView {
6969
}
7070

7171
this.#component ||= new VueRenderer(LinkBubbleView, {
72-
parent: this.options.parent,
72+
parent: this.options.editor.contentComponent,
7373
propsData: {
7474
editor: this.options.editor,
7575
href: null,
@@ -109,7 +109,13 @@ class LinkBubblePluginView {
109109
}
110110

111111
updateTooltip(view, { mark, nodeStart }) {
112-
let referenceEl = view.nodeDOM(nodeStart)
112+
let referenceEl
113+
try {
114+
referenceEl = view.nodeDOM(nodeStart)
115+
} catch (e) {
116+
// Prevent throwing error at rerouting in `openLink()`
117+
return
118+
}
113119
if (Object.prototype.toString.call(referenceEl) === '[object Text]') {
114120
referenceEl = referenceEl.parentElement
115121
}

0 commit comments

Comments
 (0)