diff --git a/frontend/package.json b/frontend/package.json index 212e290f..014b84f2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "dependencies": { "@tiptap/extension-color": "^2.0.4", "@tiptap/extension-font-family": "^2.0.4", + "@tiptap/extension-link": "^2.1.12", "@tiptap/extension-text-style": "^2.0.4", "@tiptap/pm": "^2.0.4", "@tiptap/starter-kit": "^2.0.4", diff --git a/frontend/src/components/TextBlock.vue b/frontend/src/components/TextBlock.vue index 51f58026..ea1551a3 100644 --- a/frontend/src/components/TextBlock.vue +++ b/frontend/src/components/TextBlock.vue @@ -1,7 +1,89 @@ - + + + { + if (!linkInput) return; + setLink(linkInput?.getInputValue()); + } + " + ref="linkInput"> + + + + + + + + + + + + + + + + + { + if (!editor) return; + if (editor.isActive('link')) { + editor.chain().focus().unsetLink().run(); + } else { + enableLinkInput(); + } + } + " + class="rounded px-2 py-1 hover:bg-gray-100" + :class="{ 'bg-gray-200': editor.isActive('link') }"> + + + + + @@ -14,10 +96,14 @@ import { setFontFromHTML } from "@/utils/fontManager"; import { getDataForKey } from "@/utils/helpers"; import { Color } from "@tiptap/extension-color"; import { FontFamily } from "@tiptap/extension-font-family"; +import { Link } from "@tiptap/extension-link"; import TextStyle from "@tiptap/extension-text-style"; import StarterKit from "@tiptap/starter-kit"; -import { Editor, EditorContent } from "@tiptap/vue-3"; -import { Ref, computed, onBeforeMount, onBeforeUnmount, ref, watch } from "vue"; +import { BubbleMenu, Editor, EditorContent } from "@tiptap/vue-3"; +import { Input } from "frappe-ui"; +import { Ref, computed, inject, nextTick, onBeforeMount, onBeforeUnmount, ref, watch } from "vue"; + +const overlayElement = document.querySelector("#overlay") as HTMLElement; const props = defineProps({ block: { @@ -37,6 +123,25 @@ const props = defineProps({ const store = useStore(); const component = ref(null) as Ref; +const canvasProps = inject("canvasProps") as CanvasProps; +const settingLink = ref(false); +const textLink = ref(""); +const linkInput = ref(null) as Ref; + +const setLink = (value: string | null) => { + if (!value && !textLink.value) { + editor.value?.chain().focus().unsetLink().run(); + } else { + editor.value + ?.chain() + .focus() + .setLink({ href: value || textLink.value }) + .run(); + textLink.value = ""; + } + settingLink.value = false; +}; + const textContent = computed(() => { let innerHTML = props.block.getInnerHTML(); if (props.data) { @@ -96,7 +201,15 @@ if (!props.preview) { if (props.block.isSelected() && !blockController.multipleBlocksSelected()) { editor.value = new Editor({ content: textContent.value, - extensions: [StarterKit, TextStyle, Color, FontFamily], + extensions: [ + StarterKit, + TextStyle, + Color, + FontFamily, + Link.configure({ + openOnClick: false, + }), + ], onUpdate({ editor }) { const innerHTML = editor.isEmpty ? "" : editor.getHTML(); if (props.block.getInnerHTML() === innerHTML) { @@ -104,6 +217,10 @@ if (!props.preview) { } props.block.setInnerHTML(innerHTML); }, + onSelectionUpdate: ({ editor }) => { + settingLink.value = false; + textLink.value = ""; + }, autofocus: false, injectCSS: false, }); @@ -130,4 +247,21 @@ onBeforeMount(() => { defineExpose({ component, }); + +// add cmd + k shortcut to open link menu +const handleKeydown = (e: KeyboardEvent) => { + if (e.key === "k" && e.metaKey) { + enableLinkInput(); + } +}; + +const enableLinkInput = () => { + settingLink.value = true; + nextTick(() => { + if (linkInput.value) { + const input = document.querySelector(".link-input") as HTMLInputElement; + input.focus(); + } + }); +}; diff --git a/frontend/src/index.css b/frontend/src/index.css index aa722c77..2292b773 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -5,4 +5,16 @@ list-style: revert; padding: revert; } +} + +/* to match style of tip tap editor for new lines */ +p:empty::before { + content: ''; + display: inline-block; +} + +.__text_block__ a { + color: var(--link-color); + text-decoration: underline; + background-color: transparent; } \ No newline at end of file diff --git a/frontend/src/utils/block.ts b/frontend/src/utils/block.ts index 30ec9543..18a4843a 100644 --- a/frontend/src/utils/block.ts +++ b/frontend/src/utils/block.ts @@ -91,6 +91,10 @@ class Block implements BlockOptions { delete this.attributes.style; this.classes = options.classes || []; + if (this.isText() && !this.classes.includes("__text_block__")) { + this.classes.push("__text_block__"); + } + if (this.isRoot()) { this.blockId = "root"; this.draggable = false; diff --git a/yarn.lock b/yarn.lock index dcfe4cc9..26d66bab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -579,6 +579,13 @@ dependencies: linkifyjs "^4.1.0" +"@tiptap/extension-link@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.1.12.tgz#a18f83a0b54342e6274ff9e5a5907ef7f15aa723" + integrity sha512-Sti5hhlkCqi5vzdQjU/gbmr8kb578p+u0J4kWS+SSz3BknNThEm/7Id67qdjBTOQbwuN07lHjDaabJL0hSkzGQ== + dependencies: + linkifyjs "^4.1.0" + "@tiptap/extension-list-item@^2.0.4": version "2.0.4" resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.0.4.tgz#8ca7c9959a07bf94602f8957d784d526568f2069"