fix: TipTap editor reactive destructuring

This commit is contained in:
Dominik Pschenitschni 2024-11-01 18:09:23 +01:00
parent fc91cf6013
commit 7d3d426663
1 changed files with 44 additions and 67 deletions

View File

@ -137,7 +137,7 @@
</template>
<script setup lang="ts">
import {computed, nextTick, onBeforeUnmount, onMounted, ref, watch} from 'vue'
import {computed, nextTick, onBeforeUnmount, onMounted, ref, watch, watchEffect} from 'vue'
import {useI18n} from 'vue-i18n'
import {eventToHotkeyString} from '@github/hotkey'
@ -162,8 +162,6 @@ import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'
import HardBreak from '@tiptap/extension-hard-break'
import {Node} from '@tiptap/pm/model'
import Commands from './commands'
import suggestionSetup from './suggestion'
@ -181,17 +179,7 @@ import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
import inputPrompt from '@/helpers/inputPrompt'
import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor'
const {
modelValue,
uploadCallback,
isEditEnabled,
bottomActions,
showSave,
placeholder,
editShortcut,
enableDiscardShortcut,
} = withDefaults(defineProps<{
modelValue: string,
const props = withDefaults(defineProps<{
uploadCallback?: UploadCallback,
isEditEnabled?: boolean,
bottomActions?: BottomAction[],
@ -200,6 +188,7 @@ const {
editShortcut?: string,
enableDiscardShortcut?: boolean,
}>(), {
uploadCallback: undefined,
isEditEnabled: true,
bottomActions: () => [],
showSave: false,
@ -208,7 +197,9 @@ const {
enableDiscardShortcut: false,
})
const emit = defineEmits(['update:modelValue', 'save'])
const emit = defineEmits(['save'])
const modelValue = defineModel<string>({ default: '' })
const tiptapInstanceRef = ref<HTMLInputElement | null>(null)
@ -273,7 +264,7 @@ const CustomImage = Image.extend({
nextTick(async () => {
const img = document.getElementById(id)
const img = document.getElementById(id) as HTMLImageElement | null
if (!img) return
@ -304,13 +295,13 @@ const CustomImage = Image.extend({
type Mode = 'edit' | 'preview'
const internalMode = ref<Mode>('preview')
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
const isEditing = computed(() => internalMode.value === 'edit' && props.isEditEnabled)
const contentHasChanged = ref<boolean>(false)
let lastSavedState = ''
watch(
() => modelValue,
modelValue,
(newValue) => {
if (!contentHasChanged.value) {
lastSavedState = newValue
@ -320,7 +311,7 @@ watch(
)
watch(
() => internalMode.value,
internalMode,
mode => {
if (mode === 'preview') {
contentHasChanged.value = false
@ -360,25 +351,19 @@ const extensions : Extensions = [
}),
Placeholder.configure({
placeholder: ({editor}) => {
if (!isEditing.value) {
placeholder({editor}) {
if (!isEditing.value || editor.getText() !== '' && !editor.isFocused) {
return ''
}
if (editor.getText() !== '' && !editor.isFocused) {
return ''
}
return placeholder !== ''
? placeholder
: t('input.editor.placeholder')
return props.placeholder || t('input.editor.placeholder')
},
}),
Typography,
Underline,
Link.configure({
openOnClick: false,
validate: (href: string) => (new RegExp(
validate: (href) => (new RegExp(
`^(https?|${additionalLinkProtocols.join('|')}):\\/\\/`,
'i',
)).test(href),
@ -397,8 +382,8 @@ const extensions : Extensions = [
TaskList,
TaskItem.configure({
nested: true,
onReadOnlyChecked: (node: Node, checked: boolean): boolean => {
if (!isEditEnabled) {
onReadOnlyChecked(node, checked) {
if (!props.isEditEnabled) {
return false
}
@ -429,7 +414,7 @@ const extensions : Extensions = [
]
// Add a custom extension for the Escape key
if (enableDiscardShortcut) {
if (props.enableDiscardShortcut) {
extensions.push(Extension.create({
name: 'escapeKey',
@ -448,21 +433,13 @@ const editor = useEditor({
// eslint-disable-next-line vue/no-ref-object-reactivity-loss
editable: isEditing.value,
extensions: extensions,
onUpdate: () => {
bubbleNow()
},
onUpdate: bubbleNow,
})
watch(
() => isEditing.value,
() => {
editor.value?.setEditable(isEditing.value)
},
{immediate: true},
)
watchEffect(() => editor.value?.setEditable(isEditing.value))
watch(
() => modelValue,
modelValue,
value => {
if (!editor?.value) return
@ -476,13 +453,14 @@ watch(
)
function bubbleNow() {
if (editor.value?.getHTML() === modelValue ||
(editor.value?.getHTML() === '<p></p>') && modelValue === '') {
const editorVal = editor.value!.getHTML()
if (editorVal === modelValue.value ||
(editorVal === '<p></p>') && modelValue.value === '') {
return
}
contentHasChanged.value = true
emit('update:modelValue', editor.value?.getHTML())
modelValue.value = editorVal
}
function bubbleSave() {
@ -502,7 +480,7 @@ function exitEditMode() {
}
function setEditIfApplicable() {
if (!isEditEnabled) return
if (!props.isEditEnabled) return
if (isEditing.value) return
setEdit()
@ -519,21 +497,20 @@ onBeforeUnmount(() => editor.value?.destroy())
const uploadInputRef = ref<HTMLInputElement | null>(null)
function uploadAndInsertFiles(files: File[] | FileList) {
uploadCallback(files).then(urls => {
urls?.forEach(url => {
editor.value
?.chain()
.focus()
.setImage({src: url})
.run()
})
bubbleSave()
async function uploadAndInsertFiles(files: File[] | FileList) {
const urls = await props.uploadCallback?.(files)
urls?.forEach(url => {
editor.value
?.chain()
.focus()
.setImage({src: url})
.run()
})
bubbleSave()
}
function triggerImageInput(event) {
if (typeof uploadCallback !== 'undefined') {
if (typeof props.uploadCallback !== 'undefined') {
uploadInputRef.value?.click()
return
}
@ -543,7 +520,7 @@ function triggerImageInput(event) {
async function addImage(event) {
if (typeof uploadCallback !== 'undefined') {
if (typeof props.uploadCallback !== 'undefined') {
const files = uploadInputRef.value?.files
if (!files || files.length === 0) {
@ -568,28 +545,28 @@ function setLink(event) {
}
onMounted(async () => {
if (editShortcut !== '') {
if (props.editShortcut !== '') {
document.addEventListener('keydown', setFocusToEditor)
}
await nextTick()
if (typeof uploadCallback !== 'undefined') {
if (typeof props.uploadCallback !== 'undefined') {
const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0]
input?.addEventListener('paste', handleImagePaste)
}
setModeAndValue(modelValue)
setModeAndValue(modelValue.value)
})
onBeforeUnmount(() => {
nextTick(() => {
if (typeof uploadCallback !== 'undefined') {
if (typeof props.uploadCallback !== 'undefined') {
const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0]
input?.removeEventListener('paste', handleImagePaste)
}
})
if (editShortcut !== '') {
if (props.editShortcut !== '') {
document.removeEventListener('keydown', setFocusToEditor)
}
})
@ -620,7 +597,7 @@ function setFocusToEditor(event) {
const hotkeyString = eventToHotkeyString(event)
if (!hotkeyString) return
if (hotkeyString !== editShortcut ||
if (hotkeyString !== props.editShortcut ||
event.target.tagName.toLowerCase() === 'input' ||
event.target.tagName.toLowerCase() === 'textarea' ||
event.target.contentEditable === 'true') {
@ -629,7 +606,7 @@ function setFocusToEditor(event) {
event.preventDefault()
if (!isEditing.value && isEditEnabled) {
if (!isEditing.value && props.isEditEnabled) {
internalMode.value = 'edit'
}
@ -653,7 +630,7 @@ function clickTasklistCheckbox(event) {
}
watch(
() => isEditing.value,
isEditing,
async editing => {
await nextTick()