From c4c886e6cf740882b368b301c5e70c902c55f8eb Mon Sep 17 00:00:00 2001 From: Muhammed Mustafa Date: Thu, 23 Nov 2023 11:20:34 +0000 Subject: [PATCH] fix: Added links support to text area rich --- packages/nc-gui/components/cell/RichText.vue | 5 +- .../components/cell/RichText/LinkOptions.vue | 225 ++++++++++++++++++ .../helpers/dbTiptapExtensions/links.ts | 131 ++++++++++ 3 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 packages/nc-gui/components/cell/RichText/LinkOptions.vue create mode 100644 packages/nc-gui/helpers/dbTiptapExtensions/links.ts diff --git a/packages/nc-gui/components/cell/RichText.vue b/packages/nc-gui/components/cell/RichText.vue index d751c1857e..ef0db2e77f 100644 --- a/packages/nc-gui/components/cell/RichText.vue +++ b/packages/nc-gui/components/cell/RichText.vue @@ -7,6 +7,7 @@ import TurndownService from 'turndown' import { parse } from 'marked' import { generateJSON } from '@tiptap/html' import Underline from '@tiptap/extension-underline' +import { Link } from '@/helpers/dbTiptapExtensions/links' const props = defineProps<{ value?: string | null @@ -26,6 +27,7 @@ const tiptapExtensions = [ nested: true, }), Underline, + Link, ] const editor = useEditor({ @@ -60,8 +62,9 @@ onMounted(() => { diff --git a/packages/nc-gui/helpers/dbTiptapExtensions/links.ts b/packages/nc-gui/helpers/dbTiptapExtensions/links.ts new file mode 100644 index 0000000000..697334bca0 --- /dev/null +++ b/packages/nc-gui/helpers/dbTiptapExtensions/links.ts @@ -0,0 +1,131 @@ +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) + + return ['a', attr, 0] + }, + + 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('.docs-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, +})