Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion cypress/e2e/nodes/Links.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('test link marks', function() {
cy.get('.link-view-bubble .widget-default', { timeout: 10000 })
.find('.widget-default--name')
.contains('Nextcloud')
.click({ force: true })
.click()
})

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

it('open button opens a new tab', () => {
const link = 'https://nextcloud.com/'
cy.insertLine(link)
clickLink(link)

cy.get('.link-view-bubble button[title="Open link"]').click()

cy.get('@winOpen').should('have.been.calledOnce')
})

it('closes the link bubble when clicking elsewhere', () => {
const link = 'https://nextcloud.com/'
cy.insertLine(link)
Expand Down
1 change: 1 addition & 0 deletions cypress/e2e/nodes/PreviewOptions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('Preview Options', function() {
})

it('should render previewOptions correctly', function() {
cy.get('.action-button__text').contains('Open in new tab').should('be.visible')
cy.get('.action-button__text').contains('Remove link').should('be.visible')
cy.get('.action-radio__label').each(el => {
cy.wrap(el).invoke('text').should('match', /Text only|Show link preview/)
Expand Down
12 changes: 12 additions & 0 deletions src/components/Editor.provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { openLink } from '../helpers/links.js'
import { logger } from '../helpers/logger.js'

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

export const useEditorMixin = {
inject: {
Expand Down Expand Up @@ -99,3 +101,13 @@ export const useMentionHook = {
},
},
}
export const useOpenLinkHandler = {
inject: {
$openLinkHandler: {
from: OPEN_LINK_HANDLER,
default: {
openLink,
},
},
},
}
19 changes: 18 additions & 1 deletion src/components/Editor/PreviewOptions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,17 @@
{{ t('text', 'Show link preview') }}
</NcActionRadio>
<NcActionSeparator />
<NcActionButton v-if="href" close-after-click @click="openLink">
<template #icon>
<OpenIcon :size="20" />
</template>
{{ t('text', 'Open in new tab') }}
</NcActionButton>
<NcActionButton close-after-click @click="deleteNode">
<template #icon>
<DeleteIcon :size="20" />
</template>
{{ t('text','Remove link') }}
{{ t('text', 'Remove link') }}
</NcActionButton>
</NcActions>
</div>
Expand All @@ -45,6 +51,7 @@ import NcActionCaption from '@nextcloud/vue/components/NcActionCaption'
import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator'
import DotsVerticalIcon from 'vue-material-design-icons/DotsVertical.vue'
import DeleteIcon from 'vue-material-design-icons/Delete.vue'
import OpenIcon from 'vue-material-design-icons/OpenInNew.vue'

export default {
name: 'PreviewOptions',
Expand All @@ -57,13 +64,19 @@ export default {
NcActionRadio,
NcActionSeparator,
DeleteIcon,
OpenIcon,
},

props: {
type: {
type: String,
required: true,
},
href: {
type: String,
required: false,
default: '',
},
},

data() {
Expand All @@ -83,6 +96,10 @@ export default {
deleteNode() {
this.$emit('delete')
},
openLink() {
if (!this.href) return
window.open(this.href, '_blank').focus()
},
},
}
</script>
Expand Down
17 changes: 17 additions & 0 deletions src/components/Link/LinkBubbleView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
<div class="link-view-bubble__title">
{{ title }}
</div>
<!-- open link -->
<NcButton :title="t('text', 'Open link')"
:aria-label="t('text', 'Open link')"
type="tertiary"
@click="openLink(href)">
<template #icon>
<OpenInNewIcon :size="20" />
</template>
</NcButton>
<!-- copy link -->
<NcButton :title="copyLinkTooltip"
:aria-label="copyLinkTooltip"
Expand Down Expand Up @@ -96,9 +105,11 @@ import CheckIcon from 'vue-material-design-icons/Check.vue'
import CloseIcon from 'vue-material-design-icons/Close.vue'
import ContentCopyIcon from 'vue-material-design-icons/ContentCopy.vue'
import LinkOffIcon from 'vue-material-design-icons/LinkOff.vue'
import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue'
import PencilIcon from 'vue-material-design-icons/Pencil.vue'

import CopyToClipboardMixin from '../../mixins/CopyToClipboardMixin.js'
import { useOpenLinkHandler } from '../Editor.provider.js'

const PROTOCOLS_WITH_PREVIEW = ['http:', 'https:']

Expand All @@ -114,11 +125,13 @@ export default {
NcReferenceList,
NcTextField,
LinkOffIcon,
OpenInNewIcon,
PencilIcon,
},

mixins: [
CopyToClipboardMixin,
useOpenLinkHandler,
],

props: {
Expand Down Expand Up @@ -201,6 +214,10 @@ export default {
this.referenceTitle = null
},

openLink(href) {
this.$openLinkHandler.openLink(href)
},

async copyLink() {
await this.copyToClipboard(this.href)
},
Expand Down
6 changes: 1 addition & 5 deletions src/css/prosemirror.scss
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,6 @@ div.ProseMirror {
font-size: var(--default-font-size);
}

img {
cursor: default;
max-width: 100%;
}

hr {
padding: 2px 0;
border: none;
Expand Down Expand Up @@ -394,5 +389,6 @@ div.ProseMirror {
}

.tippy-content div {
box-sizing: border-box;
visibility: visible !important;
}
7 changes: 6 additions & 1 deletion src/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
import Vue from 'vue'
import store from './store/index.js'
import { subscribe } from '@nextcloud/event-bus'
import { EDITOR_UPLOAD, HOOK_MENTION_SEARCH, HOOK_MENTION_INSERT, ATTACHMENT_RESOLVER } from './components/Editor.provider.js'
import { EDITOR_UPLOAD, HOOK_MENTION_SEARCH, HOOK_MENTION_INSERT, ATTACHMENT_RESOLVER, OPEN_LINK_HANDLER } from './components/Editor.provider.js'
import { ACTION_ATTACHMENT_PROMPT } from './components/Editor/MediaHandler.provider.js'
import { openLink } from './helpers/links.js'
// eslint-disable-next-line import/no-unresolved, n/no-missing-import
import 'vite/modulepreload-polyfill'

Expand Down Expand Up @@ -184,6 +185,7 @@ window.OCA.Text.createEditor = async function({
onFileInsert = undefined,
onMentionSearch = undefined,
onMentionInsert = undefined,
openLinkHandler = undefined,
onSearch = undefined,
}) {
const { default: MarkdownContentEditor } = await import(/* webpackChunkName: "editor" */'./components/Editor/MarkdownContentEditor.vue')
Expand All @@ -205,6 +207,9 @@ window.OCA.Text.createEditor = async function({
[EDITOR_UPLOAD]: !!sessionEditor,
[HOOK_MENTION_SEARCH]: sessionEditor ? true : onMentionSearch,
[HOOK_MENTION_INSERT]: sessionEditor ? true : onMentionInsert,
[OPEN_LINK_HANDLER]: {
openLink: openLinkHandler || openLink,
},
[ATTACHMENT_RESOLVER]: {
resolve(src, preferRaw) {
return [{
Expand Down
1 change: 0 additions & 1 deletion src/extensions/LinkBubble.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ const LinkBubble = Extension.create({
return [
linkBubble({
editor: this.editor,
parent: this.editor.contentComponent,
}),
]
},
Expand Down
23 changes: 22 additions & 1 deletion src/helpers/links.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,29 @@ const isLinkToSelfWithHash = function(href) {
return href?.startsWith('#') || href?.startsWith(locationNoHash + '#')
}

/**
* Open links, to be used as custom click handler
*
* @param {string} href the link href
*/
const openLink = function(href) {
const linkUrl = new URL(href, window.location.href)
// Consider rerouting links to Collectives if already inside Collectives app
const collectivesUrlBase = '/apps/collectives'
if (window.OCA.Collectives?.vueRouter
&& linkUrl.pathname.toString().startsWith(generateUrl(collectivesUrlBase))) {
const collectivesUrl = linkUrl.href.substring(
linkUrl.href.indexOf(collectivesUrlBase) + collectivesUrlBase.length,
)
window.OCA.Collectives.vueRouter.push(collectivesUrl)
return
}
window.open(linkUrl, '_blank')
}

export {
domHref,
parseHref,
isLinkToSelfWithHash,
openLink,
parseHref,
}
10 changes: 8 additions & 2 deletions src/plugins/LinkBubblePluginView.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class LinkBubblePluginView {
}

this.#component ||= new VueRenderer(LinkBubbleView, {
parent: this.options.parent,
parent: this.options.editor.contentComponent,
propsData: {
editor: this.options.editor,
href: null,
Expand Down Expand Up @@ -109,7 +109,13 @@ class LinkBubblePluginView {
}

updateTooltip(view, { mark, nodeStart }) {
let referenceEl = view.nodeDOM(nodeStart)
let referenceEl
try {
referenceEl = view.nodeDOM(nodeStart)
} catch (e) {
// Prevent throwing error at rerouting in `openLink()`
return
}
if (Object.prototype.toString.call(referenceEl) === '[object Text]') {
referenceEl = referenceEl.parentElement
}
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/extractLinkParagraphs.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ export default function extractLinkParagraphs(doc) {
pos,
nodeSize: node.nodeSize,
type: 'text-only',
href: extractHref(node.firstChild),
}))
} else if (node.type.name === 'preview') {
paragraphs.push(Object.freeze({
pos,
nodeSize: node.nodeSize,
type: 'link-preview',
href: node.attrs.href,
}))
}
})
Expand Down
5 changes: 3 additions & 2 deletions src/plugins/previewOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,16 @@ function decorationForLinkParagraph(linkParagraph, editor) {
* @param {object} linkParagraph - linkParagraph to generate anchor for
* @param {number} linkParagraph.pos - Position of the node
* @param {string} linkParagraph.type - selected type
* @param {string} linkParagraph.href - href of the link
* @param {number} linkParagraph.nodeSize - size of the node
* @param {object} editor - tiptap editor
*
* @return {Element}
*/
function previewOptionForLinkParagraph({ type, pos, nodeSize }, editor) {
function previewOptionForLinkParagraph({ type, href, pos, nodeSize }, editor) {
const el = document.createElement('div')
const Component = Vue.extend(PreviewOptions)
const propsData = { type }
const propsData = { type, href }
const previewOption = new Component({ propsData }).$mount(el)
previewOption.$on('open', () => {
editor.commands.hideLinkBubble()
Expand Down
19 changes: 10 additions & 9 deletions src/tests/plugins/extractLinkParagraphs.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import Preview from '../../nodes/Preview.js'
import createCustomEditor from '../testHelpers/createCustomEditor.ts'

describe('extractLinkParagraphs', () => {
const link = '<a href="https://nextcloud.com">Link</a>'
const preview = '<a href="https://nextcloud.com" title="preview">Link</a>'
const href = 'https://nextcloud.com'
const link = `<a href="${href}">Link</a>`
const preview = `<a href="${href}" title="preview">Link</a>`

it('returns an empty array for an empty doc', () => {
const doc = prepareDoc('')
Expand All @@ -23,15 +24,15 @@ describe('extractLinkParagraphs', () => {
const doc = prepareDoc(content)
const paragraphs = extractLinkParagraphs(doc)
expect(paragraphs).toEqual([
{ pos: 0, type: 'text-only', nodeSize: 6 },
{ href, pos: 0, type: 'text-only', nodeSize: 6 },
])
})

it('returns paragraphs with a single preview', () => {
const doc = prepareDoc(preview)
const paragraphs = extractLinkParagraphs(doc)
expect(paragraphs).toEqual([
{ pos: 0, type: 'link-preview', nodeSize: 6 },
{ href, pos: 0, type: 'link-preview', nodeSize: 6 },
])
})

Expand All @@ -40,7 +41,7 @@ describe('extractLinkParagraphs', () => {
const doc = prepareDoc(content)
const paragraphs = extractLinkParagraphs(doc)
expect(paragraphs).toEqual([
{ pos: 0, type: 'text-only', nodeSize: 7 },
{ href, pos: 0, type: 'text-only', nodeSize: 7 },
])
})

Expand All @@ -50,8 +51,8 @@ describe('extractLinkParagraphs', () => {
const doc = prepareDoc(content)
const paragraphs = extractLinkParagraphs(doc)
expect(paragraphs).toEqual([
{ pos: 0, type: 'text-only', nodeSize: 6 },
{ pos: 6, type: 'text-only', nodeSize: 6 },
{ href, pos: 0, type: 'text-only', nodeSize: 6 },
{ href, pos: 6, type: 'text-only', nodeSize: 6 },
])
})

Expand All @@ -60,8 +61,8 @@ describe('extractLinkParagraphs', () => {
const doc = prepareDoc(content)
const paragraphs = extractLinkParagraphs(doc)
expect(paragraphs).toEqual([
{ pos: 0, type: 'text-only', nodeSize: 6 },
{ pos: 6, type: 'link-preview', nodeSize: 6 },
{ href, pos: 0, type: 'text-only', nodeSize: 6 },
{ href, pos: 6, type: 'link-preview', nodeSize: 6 },
])
})

Expand Down
Loading