|
|
|
@ -1,71 +1,35 @@
|
|
|
|
|
<script lang="ts" setup> |
|
|
|
|
import type { Editor } from '@tiptap/vue-3' |
|
|
|
|
import { BubbleMenu } from '@tiptap/vue-3' |
|
|
|
|
import MdiFormatBulletList from '~icons/mdi/format-list-bulleted' |
|
|
|
|
import MdiFormatStrikeThrough from '~icons/mdi/format-strikethrough' |
|
|
|
|
import MdiFormatListNumber from '~icons/mdi/format-list-numbered' |
|
|
|
|
import MdiFormatListCheckbox from '~icons/mdi/format-list-checkbox' |
|
|
|
|
|
|
|
|
|
const { editor } = defineProps<Props>() |
|
|
|
|
|
|
|
|
|
interface Props { |
|
|
|
|
editor: Editor |
|
|
|
|
embedMode?: boolean |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Debounce show menu to prevent flickering |
|
|
|
|
const showMenu = computed(() => { |
|
|
|
|
if (!editor) return false |
|
|
|
|
|
|
|
|
|
return !editor.state.selection.empty |
|
|
|
|
}) |
|
|
|
|
const showMenuDebounced = ref(false) |
|
|
|
|
|
|
|
|
|
watchDebounced( |
|
|
|
|
() => showMenu.value, |
|
|
|
|
(value) => { |
|
|
|
|
showMenuDebounced.value = value |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
debounce: 200, |
|
|
|
|
maxWait: 800, |
|
|
|
|
immediate: true, |
|
|
|
|
}, |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
const handleEditorMouseDown = (e: MouseEvent) => { |
|
|
|
|
const domsInEvent = document.elementsFromPoint(e.clientX, e.clientY) as HTMLElement[] |
|
|
|
|
const isBubble = domsInEvent.some((dom) => dom?.classList?.contains('bubble-menu')) |
|
|
|
|
if (isBubble) return |
|
|
|
|
|
|
|
|
|
const pageContent = document.querySelector('.nc-docs-page-wrapper') |
|
|
|
|
pageContent?.classList.add('bubble-menu-hidden') |
|
|
|
|
} |
|
|
|
|
const props = defineProps<Props>() |
|
|
|
|
|
|
|
|
|
const handleEditorMouseUp = (e: MouseEvent) => { |
|
|
|
|
const domsInEvent = document.elementsFromPoint(e.clientX, e.clientY) as HTMLElement[] |
|
|
|
|
const isBubble = domsInEvent.some((dom) => dom?.classList?.contains('bubble-menu')) |
|
|
|
|
if (isBubble) return |
|
|
|
|
const editor = computed(() => props.editor) |
|
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
const pageContent = document.querySelector('.nc-docs-page-wrapper') |
|
|
|
|
pageContent?.classList.remove('bubble-menu-hidden') |
|
|
|
|
}, 100) |
|
|
|
|
} |
|
|
|
|
const embedMode = computed(() => props.embedMode) |
|
|
|
|
|
|
|
|
|
const onToggleLink = () => { |
|
|
|
|
const activeNode = editor?.state?.selection?.$from?.nodeBefore || editor?.state?.selection?.$from?.nodeAfter |
|
|
|
|
const activeNode = editor.value?.state?.selection?.$from?.nodeBefore || editor.value?.state?.selection?.$from?.nodeAfter |
|
|
|
|
|
|
|
|
|
const isLinkMarkedStoredInEditor = editor?.state?.storedMarks?.some((mark: Mark) => mark.type.name === 'link') |
|
|
|
|
const isLinkMarkedStoredInEditor = editor.value?.state?.storedMarks?.some((mark: any) => mark.type.name === 'link') |
|
|
|
|
|
|
|
|
|
const isActiveNodeMarkActive = activeNode?.marks?.some((mark: Mark) => mark.type.name === 'link') || isLinkMarkedStoredInEditor |
|
|
|
|
const isActiveNodeMarkActive = activeNode?.marks?.some((mark: any) => mark.type.name === 'link') || isLinkMarkedStoredInEditor |
|
|
|
|
|
|
|
|
|
if (isActiveNodeMarkActive) { |
|
|
|
|
editor!.chain().focus().unsetLink().run() |
|
|
|
|
editor.value!.chain().focus().unsetLink().run() |
|
|
|
|
} else { |
|
|
|
|
editor! |
|
|
|
|
.chain() |
|
|
|
|
editor |
|
|
|
|
.value!.chain() |
|
|
|
|
.focus() |
|
|
|
|
.toggleLink({ |
|
|
|
|
.setLink({ |
|
|
|
|
href: '', |
|
|
|
|
}) |
|
|
|
|
.selectTextblockEnd() |
|
|
|
@ -79,110 +43,104 @@ const onToggleLink = () => {
|
|
|
|
|
}, 100) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
onMounted(() => { |
|
|
|
|
document.addEventListener('mouseup', handleEditorMouseUp) |
|
|
|
|
document.addEventListener('mousedown', handleEditorMouseDown) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
onUnmounted(() => { |
|
|
|
|
document.removeEventListener('mouseup', handleEditorMouseUp) |
|
|
|
|
document.removeEventListener('mousedown', handleEditorMouseDown) |
|
|
|
|
}) |
|
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
<template> |
|
|
|
|
<BubbleMenu :editor="editor" :update-delay="300" :tippy-options="{ duration: 100, maxWidth: 600 }"> |
|
|
|
|
<div v-if="showMenuDebounced" class="bubble-menu flex flex-row gap-x-1 bg-gray-100 py-1 rounded-lg px-1"> |
|
|
|
|
<a-button |
|
|
|
|
type="text" |
|
|
|
|
:class="{ 'is-active': editor.isActive('bold') }" |
|
|
|
|
:aria-active="editor.isActive('bold')" |
|
|
|
|
class="menu-button" |
|
|
|
|
data-testid="nc-docs-editor-bold-button" |
|
|
|
|
@click="editor!.chain().focus().toggleBold().run()" |
|
|
|
|
> |
|
|
|
|
<MdiFormatBold /> |
|
|
|
|
</a-button> |
|
|
|
|
<a-button |
|
|
|
|
type="text" |
|
|
|
|
:class="{ 'is-active': editor.isActive('italic') }" |
|
|
|
|
:aria-active="editor.isActive('italic')" |
|
|
|
|
class="menu-button" |
|
|
|
|
data-testid="nc-docs-editor-italic-button" |
|
|
|
|
@click=";(editor!.chain().focus() as any).toggleItalic().run()" |
|
|
|
|
> |
|
|
|
|
<MdiFormatItalic /> |
|
|
|
|
</a-button> |
|
|
|
|
<a-button |
|
|
|
|
type="text" |
|
|
|
|
:class="{ 'is-active': editor.isActive('underline') }" |
|
|
|
|
:aria-active="editor.isActive('underline')" |
|
|
|
|
class="menu-button" |
|
|
|
|
data-testid="nc-docs-editor-underline-button" |
|
|
|
|
@click="editor!.chain().focus().toggleUnderline().run()" |
|
|
|
|
> |
|
|
|
|
<MdiFormatUnderline /> |
|
|
|
|
</a-button> |
|
|
|
|
<a-button |
|
|
|
|
type="text" |
|
|
|
|
:class="{ 'is-active': editor.isActive('strike') }" |
|
|
|
|
:aria-active="editor.isActive('strike')" |
|
|
|
|
class="menu-button" |
|
|
|
|
data-testid="nc-docs-editor-strike-button" |
|
|
|
|
@click="editor!.chain().focus().toggleStrike().run()" |
|
|
|
|
> |
|
|
|
|
<MdiFormatStrikeThrough /> |
|
|
|
|
</a-button> |
|
|
|
|
<div class="divider"></div> |
|
|
|
|
|
|
|
|
|
<a-button |
|
|
|
|
type="text" |
|
|
|
|
:class="{ 'is-active': editor.isActive('taskList') }" |
|
|
|
|
:aria-active="editor.isActive('taskList')" |
|
|
|
|
class="menu-button" |
|
|
|
|
data-testid="nc-docs-editor-task-button" |
|
|
|
|
@click="editor!.chain().focus().toggleTaskList().run()" |
|
|
|
|
> |
|
|
|
|
<MdiFormatListCheckbox /> |
|
|
|
|
</a-button> |
|
|
|
|
<a-button |
|
|
|
|
type="text" |
|
|
|
|
:class="{ 'is-active': editor.isActive('bulletList') }" |
|
|
|
|
:aria-active="editor.isActive('bulletList')" |
|
|
|
|
class="menu-button" |
|
|
|
|
data-testid="nc-docs-editor-bullet-button" |
|
|
|
|
@click="editor!.chain().focus().toggleBulletList().run()" |
|
|
|
|
> |
|
|
|
|
<MdiFormatBulletList /> |
|
|
|
|
</a-button> |
|
|
|
|
<a-button |
|
|
|
|
type="text" |
|
|
|
|
:class="{ 'is-active': editor.isActive('orderedList') }" |
|
|
|
|
:aria-active="editor.isActive('orderedList')" |
|
|
|
|
class="menu-button" |
|
|
|
|
data-testid="nc-docs-editor-ordered-button" |
|
|
|
|
@click="editor!.chain().focus().toggleOrderedList().run()" |
|
|
|
|
> |
|
|
|
|
<MdiFormatListNumber /> |
|
|
|
|
</a-button> |
|
|
|
|
<div class="divider"></div> |
|
|
|
|
|
|
|
|
|
<a-button |
|
|
|
|
type="text" |
|
|
|
|
:class="{ 'is-active': editor.isActive('link') }" |
|
|
|
|
:aria-active="editor.isActive('link')" |
|
|
|
|
class="menu-button" |
|
|
|
|
data-testid="nc-docs-editor-link-button" |
|
|
|
|
@click="onToggleLink" |
|
|
|
|
> |
|
|
|
|
<div class="flex flex-row items-center px-0.5"> |
|
|
|
|
<MdiLink /> |
|
|
|
|
<div class="!text-xs !ml-1">Link</div> |
|
|
|
|
</div> |
|
|
|
|
</a-button> |
|
|
|
|
</div> |
|
|
|
|
</BubbleMenu> |
|
|
|
|
<div |
|
|
|
|
class="bubble-menu flex flex-row gap-x-1 bg-gray-100 py-1 rounded-lg px-1" |
|
|
|
|
:class="{ |
|
|
|
|
'embed-mode': embedMode, |
|
|
|
|
'full-mode': !embedMode, |
|
|
|
|
}" |
|
|
|
|
> |
|
|
|
|
<a-button |
|
|
|
|
type="text" |
|
|
|
|
:class="{ 'is-active': editor.isActive('bold') }" |
|
|
|
|
:aria-active="editor.isActive('bold')" |
|
|
|
|
class="menu-button" |
|
|
|
|
data-testid="nc-docs-editor-bold-button" |
|
|
|
|
@click="editor!.chain().focus().toggleBold().run()" |
|
|
|
|
> |
|
|
|
|
<MdiFormatBold /> |
|
|
|
|
</a-button> |
|
|
|
|
<a-button |
|
|
|
|
type="text" |
|
|
|
|
:class="{ 'is-active': editor.isActive('italic') }" |
|
|
|
|
:aria-active="editor.isActive('italic')" |
|
|
|
|
class="menu-button" |
|
|
|
|
data-testid="nc-docs-editor-italic-button" |
|
|
|
|
@click=";(editor!.chain().focus() as any).toggleItalic().run()" |
|
|
|
|
> |
|
|
|
|
<MdiFormatItalic /> |
|
|
|
|
</a-button> |
|
|
|
|
<a-button |
|
|
|
|
type="text" |
|
|
|
|
:class="{ 'is-active': editor.isActive('underline') }" |
|
|
|
|
:aria-active="editor.isActive('underline')" |
|
|
|
|
class="menu-button" |
|
|
|
|
data-testid="nc-docs-editor-underline-button" |
|
|
|
|
@click="editor!.chain().focus().toggleUnderline().run()" |
|
|
|
|
> |
|
|
|
|
<MdiFormatUnderline /> |
|
|
|
|
</a-button> |
|
|
|
|
<a-button |
|
|
|
|
type="text" |
|
|
|
|
:class="{ 'is-active': editor.isActive('strike') }" |
|
|
|
|
:aria-active="editor.isActive('strike')" |
|
|
|
|
class="menu-button" |
|
|
|
|
data-testid="nc-docs-editor-strike-button" |
|
|
|
|
@click="editor!.chain().focus().toggleStrike().run()" |
|
|
|
|
> |
|
|
|
|
<MdiFormatStrikeThrough /> |
|
|
|
|
</a-button> |
|
|
|
|
<div class="divider"></div> |
|
|
|
|
|
|
|
|
|
<a-button |
|
|
|
|
type="text" |
|
|
|
|
:class="{ 'is-active': editor.isActive('taskList') }" |
|
|
|
|
:aria-active="editor.isActive('taskList')" |
|
|
|
|
class="menu-button" |
|
|
|
|
data-testid="nc-docs-editor-task-button" |
|
|
|
|
@click="editor!.chain().focus().toggleTaskList().run()" |
|
|
|
|
> |
|
|
|
|
<MdiFormatListCheckbox /> |
|
|
|
|
</a-button> |
|
|
|
|
<a-button |
|
|
|
|
type="text" |
|
|
|
|
:class="{ 'is-active': editor.isActive('bulletList') }" |
|
|
|
|
:aria-active="editor.isActive('bulletList')" |
|
|
|
|
class="menu-button" |
|
|
|
|
data-testid="nc-docs-editor-bullet-button" |
|
|
|
|
@click="editor!.chain().focus().toggleBulletList().run()" |
|
|
|
|
> |
|
|
|
|
<MdiFormatBulletList /> |
|
|
|
|
</a-button> |
|
|
|
|
<a-button |
|
|
|
|
type="text" |
|
|
|
|
:class="{ 'is-active': editor.isActive('orderedList') }" |
|
|
|
|
:aria-active="editor.isActive('orderedList')" |
|
|
|
|
class="menu-button" |
|
|
|
|
data-testid="nc-docs-editor-ordered-button" |
|
|
|
|
@click="editor!.chain().focus().toggleOrderedList().run()" |
|
|
|
|
> |
|
|
|
|
<MdiFormatListNumber /> |
|
|
|
|
</a-button> |
|
|
|
|
<div class="divider"></div> |
|
|
|
|
|
|
|
|
|
<a-button |
|
|
|
|
type="text" |
|
|
|
|
:class="{ 'is-active': editor.isActive('link') }" |
|
|
|
|
:aria-active="editor.isActive('link')" |
|
|
|
|
class="menu-button" |
|
|
|
|
data-testid="nc-docs-editor-link-button" |
|
|
|
|
@click="onToggleLink" |
|
|
|
|
> |
|
|
|
|
<div class="flex flex-row items-center px-0.5"> |
|
|
|
|
<MdiLink /> |
|
|
|
|
<div class="!text-xs !ml-1">Link</div> |
|
|
|
|
</div> |
|
|
|
|
</a-button> |
|
|
|
|
</div> |
|
|
|
|
</template> |
|
|
|
|
|
|
|
|
|
<style lang="scss"> |
|
|
|
@ -205,11 +163,19 @@ onUnmounted(() => {
|
|
|
|
|
@apply rounded-md py-1 my-0 pl-2.5 pr-3 cursor-pointer items-center gap-x-2.5 hover:bg-gray-100; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.bubble-menu.full-mode { |
|
|
|
|
@apply border-gray-100 |
|
|
|
|
box-shadow: 0px 0px 1.2rem 0 rgb(230, 230, 230) !important; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.bubble-menu.embed-mode { |
|
|
|
|
@apply border-transparent !shadow-none; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.bubble-menu { |
|
|
|
|
// shadow |
|
|
|
|
@apply border-gray-100 bg-white; |
|
|
|
|
@apply bg-white; |
|
|
|
|
border-width: 1px; |
|
|
|
|
box-shadow: 0px 0px 1.2rem 0 rgb(230, 230, 230) !important; |
|
|
|
|
|
|
|
|
|
.is-active { |
|
|
|
|
@apply border-1 !hover:bg-gray-200 border-1 border-gray-200 bg-gray-100; |
|
|
|
|