|
|
|
import TiptapLink from '@tiptap/extension-link'
|
|
|
|
import { mergeAttributes } from '@tiptap/core'
|
|
|
|
import { Plugin, TextSelection } from 'prosemirror-state'
|
|
|
|
import type { AddMarkStep, Step } from 'prosemirror-transform'
|
|
|
|
|
|
|
|
export const Link = TiptapLink.extend({
|
|
|
|
addOptions() {
|
|
|
|
return {
|
|
|
|
openOnClick: true,
|
|
|
|
linkOnPaste: true,
|
|
|
|
autolink: true,
|
|
|
|
protocols: [],
|
|
|
|
HTMLAttributes: {
|
|
|
|
target: '_blank',
|
|
|
|
rel: 'noopener noreferrer nofollow',
|
|
|
|
class: null,
|
|
|
|
},
|
|
|
|
validate: undefined,
|
|
|
|
internal: false,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
addAttributes() {
|
|
|
|
return {
|
|
|
|
href: {
|
|
|
|
default: null,
|
|
|
|
},
|
|
|
|
target: {
|
|
|
|
default: this.options.HTMLAttributes.target,
|
|
|
|
},
|
|
|
|
class: {
|
|
|
|
default: this.options.HTMLAttributes.class,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
renderHTML({ HTMLAttributes }) {
|
|
|
|
const attr = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)
|
|
|
|
|
|
|
|
// We use this as a workaround to show a tooltip on the content
|
|
|
|
// We use the href to store the tooltip content
|
|
|
|
if (isValidURL(attr.href) || !attr.href.includes('~~~###~~~')) {
|
|
|
|
return ['a', attr, 0]
|
|
|
|
}
|
|
|
|
|
|
|
|
// The class is used to identify the text that needs to show the tooltip
|
|
|
|
// The data-tooltip is the content of the tooltip
|
|
|
|
attr.class = 'nc-rich-link-tooltip'
|
|
|
|
attr['data-tooltip'] = attr.href?.split('~~~###~~~')[1]?.replace(/_/g, ' ')
|
|
|
|
return ['span', attr]
|
|
|
|
},
|
|
|
|
|
|
|
|
addKeyboardShortcuts() {
|
|
|
|
return {
|
|
|
|
'Mod-j': () => {
|
|
|
|
const selection = this.editor.view.state.selection
|
|
|
|
this.editor
|
|
|
|
.chain()
|
|
|
|
.toggleLink({
|
|
|
|
href: '',
|
|
|
|
})
|
|
|
|
.setTextSelection(selection.to)
|
|
|
|
.run()
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
const linkInput = document.querySelector('.nc-text-area-rich-link-option-input')
|
|
|
|
if (linkInput) {
|
|
|
|
;(linkInput as any).focus()
|
|
|
|
}
|
|
|
|
}, 100)
|
|
|
|
},
|
|
|
|
'Space': () => {
|
|
|
|
// If we press space twice we stop the link mark and have normal text
|
|
|
|
const editor = this.editor
|
|
|
|
const selection = editor.view.state.selection
|
|
|
|
const nodeBefore = selection.$to.nodeBefore
|
|
|
|
const nodeAfter = selection.$to.nodeAfter
|
|
|
|
|
|
|
|
if (!nodeBefore) return false
|
|
|
|
|
|
|
|
const nodeBeforeText = nodeBefore.text!
|
|
|
|
|
|
|
|
// If we are not inside a link, we don't do anything
|
|
|
|
if (
|
|
|
|
!nodeBefore?.marks.some((mark) => mark.type.name === 'link') ||
|
|
|
|
nodeAfter?.marks.some((mark) => mark.type.name === 'link')
|
|
|
|
) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Last text character should be a space
|
|
|
|
if (nodeBeforeText[nodeBeforeText.length - 1] !== ' ') {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
editor.view.dispatch(
|
|
|
|
editor.view.state.tr.removeMark(selection.$to.pos - 1, selection.$to.pos, editor.view.state.schema.marks.link),
|
|
|
|
)
|
|
|
|
|
|
|
|
return true
|
|
|
|
},
|
|
|
|
} as any
|
|
|
|
},
|
|
|
|
addProseMirrorPlugins() {
|
|
|
|
return [
|
|
|
|
// To have proseMirror plugins from the parent extension
|
|
|
|
...(this.parent?.() ?? []),
|
|
|
|
new Plugin({
|
|
|
|
//
|
|
|
|
// Put cursor at the end of the link when we add a link
|
|
|
|
//
|
|
|
|
appendTransaction: (transactions, _, newState) => {
|
|
|
|
try {
|
|
|
|
if (transactions.length !== 1) return null
|
|
|
|
const steps = transactions[0].steps
|
|
|
|
if (steps.length !== 1) return null
|
|
|
|
|
|
|
|
const step: Step = steps[0] as Step
|
|
|
|
const stepJson = step.toJSON()
|
|
|
|
// Ignore we are not adding a mark(i.e link, bold, etc)
|
|
|
|
if (stepJson.stepType !== 'addMark') return null
|
|
|
|
|
|
|
|
const addMarkStep: AddMarkStep = step as AddMarkStep
|
|
|
|
if (!addMarkStep) return null
|
|
|
|
|
|
|
|
if (addMarkStep.from === addMarkStep.to) return null
|
|
|
|
|
|
|
|
if (addMarkStep.mark.type.name !== 'link') return null
|
|
|
|
|
|
|
|
const { tr } = newState
|
|
|
|
return tr.setSelection(new TextSelection(tr.doc.resolve(addMarkStep.to)))
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
]
|
|
|
|
},
|
|
|
|
}).configure({
|
|
|
|
openOnClick: false,
|
|
|
|
})
|