fix editor undo

This commit is contained in:
Pete Matsyburka committed 2026-10-05 11:45:16 +03:00
1 parent 25504e8cb1
commit 73339d8cd9
7 files changed
+212 -64

No files matched your search

@@ -272,7 +272,7 @@ export default {
FieldSubmitter, FieldSubmitter,
IconX IconX
}, },
inject: ['t', 'isMobile', 'nativePlatform'], inject: ['t', 'nativePlatform'],
props: { props: {
template: { template: {
type: Object, type: Object,
+88 -29
View File
@@ -1007,6 +1007,9 @@ export default {
return { return {
template: this.template, template: this.template,
save: this.save, save: this.save,
pushUndo: this.pushUndo,
undo: this.undo,
redo: this.redo,
t: this.t, t: this.t,
assignDropAreaSize: this.assignDropAreaSize, assignDropAreaSize: this.assignDropAreaSize,
currencies: this.currencies, currencies: this.currencies,
@@ -1640,9 +1643,11 @@ export default {
this.selectedSubmitter = this.template.submitters[0] this.selectedSubmitter = this.template.submitters[0]
}, },
mounted () { mounted () {
this.undoStack = [JSON.stringify(this.template)] this.undoStack = []
this.redoStack = [] this.redoStack = []
this.undoStack.push(this.buildUndoData())
this.$nextTick(() => { this.$nextTick(() => {
this.resizeObserver = new ResizeObserver(this.onResize) this.resizeObserver = new ResizeObserver(this.onResize)
this.resizeObserver.observe(this.$el.getRootNode().querySelector('div[data-v-app]')) this.resizeObserver.observe(this.$el.getRootNode().querySelector('div[data-v-app]'))
@@ -2287,6 +2292,8 @@ export default {
const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic) const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic)
this.pushUndo()
dynamicDocumentRefs.forEach((ref) => ref.update()) dynamicDocumentRefs.forEach((ref) => ref.update())
this.rebuildVariablesSchema({ disable: false }) this.rebuildVariablesSchema({ disable: false })
@@ -2414,35 +2421,76 @@ export default {
this.drawOption = null this.drawOption = null
}, },
undo () { undo () {
this.selectedAreasRef.value = []
if (this.undoStack.length > 1) { if (this.undoStack.length > 1) {
this.undoStack.pop() this.undoStack.pop()
const stringData = this.undoStack[this.undoStack.length - 1] const undoData = this.undoStack[this.undoStack.length - 1]
const currentStringData = JSON.stringify(this.template) const currentUndoData = this.buildUndoData()
if (stringData && stringData !== currentStringData) { if (!this.isSameUndoData(undoData, currentUndoData)) {
this.redoStack.push(currentStringData) this.redoStack.push(currentUndoData)
Object.assign(this.template, JSON.parse(stringData)) this.applyUndoData(undoData, currentUndoData)
this.save()
} }
} }
}, },
redo () { redo () {
const stringData = this.redoStack.pop() this.selectedAreasRef.value = []
this.lastRedoData = stringData
const currentStringData = JSON.stringify(this.template)
if (stringData && stringData !== currentStringData) { const undoData = this.redoStack.pop()
if (this.undoStack[this.undoStack.length - 1] !== currentStringData) { const currentUndoData = this.buildUndoData()
this.undoStack.push(currentStringData)
if (undoData && !this.isSameUndoData(undoData, currentUndoData)) {
if (!this.isSameUndoData(this.undoStack[this.undoStack.length - 1], currentUndoData)) {
this.undoStack.push(currentUndoData)
} }
Object.assign(this.template, JSON.parse(stringData)) this.undoStack.push(undoData)
this.save() this.applyUndoData(undoData, currentUndoData)
} }
}, },
buildUndoData () {
const template = JSON.stringify(toRaw(this.template))
const lastUndoData = this.undoStack[this.undoStack.length - 1]
return { template: template === lastUndoData?.template ? lastUndoData.template : template, sectionDocs: this.buildSectionDocs() }
},
buildSectionDocs () {
return new Map(this.documentRefs.flatMap((ref) => ref.isDynamic ? ref.getSectionDocs() : []))
},
isSameUndoData (undoData, currentUndoData) {
return undoData.template === currentUndoData.template && this.isSameSectionDocs(undoData, currentUndoData)
},
isSameSectionDocs (undoData, currentUndoData) {
return [...currentUndoData.sectionDocs].every(([sectionKey, doc]) => undoData.sectionDocs.get(sectionKey)?.eq(doc))
},
applyUndoData (undoData, currentUndoData) {
const isTemplateChanged = undoData.template !== currentUndoData.template
if (isTemplateChanged) {
Object.assign(this.template, JSON.parse(undoData.template))
}
this.documentRefs.forEach((ref) => {
if (ref.isDynamic) ref.restoreSectionDocs(undoData.sectionDocs)
})
this.buildSectionDocs().forEach((doc, sectionKey) => undoData.sectionDocs.set(sectionKey, doc))
if (isTemplateChanged) {
this.save()
this.$nextTick(this.syncUndoTemplate)
}
},
syncUndoTemplate () {
const lastUndoData = this.undoStack[this.undoStack.length - 1]
const template = JSON.stringify(toRaw(this.template))
if (template !== lastUndoData.template) lastUndoData.template = template
},
onResize () { onResize () {
const breakpointLg = 1024 const breakpointLg = 1024
const breakpointMd = 768 const breakpointMd = 768
@@ -2609,15 +2657,11 @@ export default {
event.stopImmediatePropagation() event.stopImmediatePropagation()
event.preventDefault() event.preventDefault()
this.selectedAreasRef.value = []
this.redo() this.redo()
} else if ((event.ctrlKey || event.metaKey) && event.key === 'z') { } else if ((event.ctrlKey || event.metaKey) && event.key === 'z') {
event.stopImmediatePropagation() event.stopImmediatePropagation()
event.preventDefault() event.preventDefault()
this.selectedAreasRef.value = []
this.undo() this.undo()
} else if ((event.ctrlKey || event.metaKey) && event.key === 'c' && document.activeElement === document.body) { } else if ((event.ctrlKey || event.metaKey) && event.key === 'c' && document.activeElement === document.body) {
if (this.selectedAreasRef.value.length > 1) { if (this.selectedAreasRef.value.length > 1) {
@@ -2992,16 +3036,29 @@ export default {
return false return false
} }
}, },
pushUndo () { pushUndo ({ amend = false } = {}) {
const stringData = JSON.stringify(this.template) const undoData = this.buildUndoData()
const lastUndoData = this.undoStack[this.undoStack.length - 1]
const isTemplateChanged = undoData.template !== lastUndoData.template
const isSectionDocsChanged = !this.isSameSectionDocs(lastUndoData, undoData)
if (this.undoStack[this.undoStack.length - 1] !== stringData) { if (this.isUndoBatch || (amend && (!isSectionDocsChanged || (!this.redoStack.length && !isTemplateChanged)))) {
this.undoStack.push(stringData) this.undoStack[this.undoStack.length - 1] = undoData
} else if (isTemplateChanged || isSectionDocsChanged) {
this.undoStack.push(undoData)
if (this.lastRedoData !== stringData) { this.redoStack = []
this.redoStack = [] } else {
} return
} }
this.isUndoBatch = true
this.$nextTick(() => {
this.isUndoBatch = false
this.syncUndoTemplate()
})
}, },
setDefaultAreaSize (area, type) { setDefaultAreaSize (area, type) {
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid) const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
@@ -3971,7 +4028,7 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
if (this.$el.closest('template-builder')) { if (this.$el.closest('template-builder')) {
this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template) this.$el.closest('template-builder').dataset.template = JSON.stringify(toRaw(this.template))
} }
}) })
@@ -4004,10 +4061,12 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
if (this.$el.closest('template-builder')) { if (this.$el.closest('template-builder')) {
this.$el.closest('template-builder').dataset.dynamicDocuments = JSON.stringify(this.dynamicDocuments) this.$el.closest('template-builder').dataset.dynamicDocuments = JSON.stringify(toRaw(this.dynamicDocuments))
} }
}) })
this.pushUndo({ amend: true })
this.save() this.save()
}, },
rebuildVariablesSchema ({ disable = true } = {}) { rebuildVariablesSchema ({ disable = true } = {}) {
@@ -191,6 +191,8 @@ export default {
} }
}, },
created () { created () {
this.updatedSectionEditors = new Map()
this.loadListsStyle() this.loadListsStyle()
}, },
mounted () { mounted () {
@@ -242,6 +244,14 @@ export default {
this.sectionRefs.forEach((ref) => ref.reloadContent()) this.sectionRefs.forEach((ref) => ref.reloadContent())
}, },
getSectionDocs () {
return this.sectionRefs.map((ref) => [ref.sectionKey, ref.editor.state.doc])
},
restoreSectionDocs (sectionDocs) {
this.sectionRefs.forEach((ref) => {
if (sectionDocs.has(ref.sectionKey)) ref.restoreDoc(sectionDocs.get(ref.sectionKey))
})
},
onBeforeUnload (event) { onBeforeUnload (event) {
if (this.saveTimer) { if (this.saveTimer) {
event.preventDefault() event.preventDefault()
@@ -267,11 +277,9 @@ export default {
onSectionUpdate (section, { editor }) { onSectionUpdate (section, { editor }) {
clearTimeout(this.saveTimer) clearTimeout(this.saveTimer)
this.saveTimer = setTimeout(async () => { this.updatedSectionEditors.set(section.id, editor)
await this.updateSectionAndSave(section, editor)
delete this.saveTimer this.saveTimer = setTimeout(() => this.updateSectionsAndSave(), 1000)
}, 1000)
}, },
loadListsStyle () { loadListsStyle () {
this.listsStyle = this.bodyDom.querySelector('body > style[data-lists]')?.textContent || null this.listsStyle = this.bodyDom.querySelector('body > style[data-lists]')?.textContent || null
@@ -291,12 +299,16 @@ export default {
updateVariablesSchema () { updateVariablesSchema () {
this.document.variables_schema = buildVariablesSchema(this.bodyDom.body) this.document.variables_schema = buildVariablesSchema(this.bodyDom.body)
}, },
updateSectionAndSave (section, editor) { updateSectionsAndSave () {
const target = this.bodyDom.getElementById(section.id) this.updatedSectionEditors.forEach((editor, sectionId) => {
const target = this.bodyDom.getElementById(sectionId)
if (target) { if (target) {
target.innerHTML = this.getHtmlForSave(editor) target.innerHTML = this.getHtmlForSave(editor)
} }
})
this.updatedSectionEditors.clear()
this.document.body = this.bodyDom.body.innerHTML this.document.body = this.bodyDom.body.innerHTML
@@ -316,6 +328,8 @@ export default {
delete this.saveTimer delete this.saveTimer
this.updatedSectionEditors.clear()
this.sectionRefs.forEach(({ section, editor }) => { this.sectionRefs.forEach(({ section, editor }) => {
const target = this.bodyDom.getElementById(section.id) const target = this.bodyDom.getElementById(section.id)
@@ -5,7 +5,6 @@ import { Decoration, DecorationSet } from '@tiptap/pm/view'
import Document from '@tiptap/extension-document' import Document from '@tiptap/extension-document'
import Text from '@tiptap/extension-text' import Text from '@tiptap/extension-text'
import HardBreak from '@tiptap/extension-hard-break' import HardBreak from '@tiptap/extension-hard-break'
import History from '@tiptap/extension-history'
import Gapcursor from '@tiptap/extension-gapcursor' import Gapcursor from '@tiptap/extension-gapcursor'
import Dropcursor from '@tiptap/extension-dropcursor' import Dropcursor from '@tiptap/extension-dropcursor'
import { createApp, reactive } from 'vue' import { createApp, reactive } from 'vue'
@@ -1475,7 +1474,7 @@ const VariableHighlight = Extension.create({
} }
}) })
export function buildEditor ({ dynamicAreaProps, getAttachmentsIndex, renderHtmlForSaveRef, onFieldDrop, onFieldDestroy, editorOptions }) { export function buildEditor ({ dynamicAreaProps, getAttachmentsIndex, renderHtmlForSaveRef, onFieldDrop, onFieldDestroy, onUndo, onRedo, editorOptions }) {
const FieldNode = Node.create({ const FieldNode = Node.create({
name: 'fieldNode', name: 'fieldNode',
inline: true, inline: true,
@@ -1598,6 +1597,51 @@ export function buildEditor ({ dynamicAreaProps, getAttachmentsIndex, renderHtml
} }
}) })
const undo = () => {
onUndo()
return true
}
const redo = () => {
onRedo()
return true
}
const BuilderHistory = Extension.create({
name: 'builderHistory',
addKeyboardShortcuts () {
return {
'Mod-z': undo,
'Shift-Mod-z': redo,
'Mod-y': redo,
'Mod-я': undo,
'Shift-Mod-я': redo
}
},
addProseMirrorPlugins () {
return [
new Plugin({
key: new PluginKey('builderHistory'),
props: {
handleDOMEvents: {
beforeinput (_view, event) {
const command = { historyUndo: undo, historyRedo: redo }[event.inputType]
if (!command) return false
event.preventDefault()
return command()
}
}
}
})
]
}
})
const RESIZE_HANDLE_PX = 4 const RESIZE_HANDLE_PX = 4
const MIN_SIZE_PX = 16 const MIN_SIZE_PX = 16
@@ -1818,7 +1862,7 @@ export function buildEditor ({ dynamicAreaProps, getAttachmentsIndex, renderHtml
Document, Document,
Text, Text,
HardBreak, HardBreak,
History, BuilderHistory,
Gapcursor, Gapcursor,
Dropcursor, Dropcursor,
CustomParagraph, CustomParagraph,
@@ -41,6 +41,7 @@
:area="selectedArea" :area="selectedArea"
:field="selectedField" :field="selectedField"
:editable="editable" :editable="editable"
:is-mobile="isMobile"
:template="template" :template="template"
:selected-areas-ref="selectedAreasRef" :selected-areas-ref="selectedAreasRef"
:get-field-type-index="getFieldTypeIndex" :get-field-type-index="getFieldTypeIndex"
@@ -116,7 +117,7 @@
<script> <script>
import { shallowRef } from 'vue' import { shallowRef } from 'vue'
import { DOMSerializer, Fragment } from '@tiptap/pm/model' import { DOMSerializer, Fragment } from '@tiptap/pm/model'
import { TextSelection } from '@tiptap/pm/state' import { Selection, TextSelection } from '@tiptap/pm/state'
import { findTable, CellSelection } from '@tiptap/pm/tables' import { findTable, CellSelection } from '@tiptap/pm/tables'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import { IconPlus, IconX } from '@tabler/icons-vue' import { IconPlus, IconX } from '@tabler/icons-vue'
@@ -138,7 +139,7 @@ export default {
IconPlus, IconPlus,
IconX IconX
}, },
inject: ['template', 'save', 'baseFetch', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'selectedAreasRef', 'getFieldTypeIndex', 'fieldTypes', 'withPhone', 'withPayment', 'withVerification', 'withKba', 'backgroundColor'], inject: ['template', 'save', 'pushUndo', 'undo', 'redo', 'baseFetch', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'selectedAreasRef', 'getFieldTypeIndex', 'fieldTypes', 'withPhone', 'withPayment', 'withVerification', 'withKba', 'backgroundColor', 'isMobile'],
props: { props: {
section: { section: {
type: Object, type: Object,
@@ -260,6 +261,9 @@ export default {
editor () { editor () {
return this.editorRef.value return this.editorRef.value
}, },
sectionKey () {
return `${this.attachmentUuid}:${this.section.id}`
},
sectionWidthPx () { sectionWidthPx () {
const pt = parseFloat(this.section.style.width) const pt = parseFloat(this.section.style.width)
@@ -310,6 +314,8 @@ export default {
} }
}, },
mounted () { mounted () {
this.lastUpdateTime = 0
this.editorRef.value = buildEditor({ this.editorRef.value = buildEditor({
dynamicAreaProps: { dynamicAreaProps: {
template: this.template, template: this.template,
@@ -325,6 +331,8 @@ export default {
getAttachmentsIndex: () => this.attachmentsIndex, getAttachmentsIndex: () => this.attachmentsIndex,
onFieldDrop: this.onFieldDrop, onFieldDrop: this.onFieldDrop,
onFieldDestroy: this.onFieldDestroy, onFieldDestroy: this.onFieldDestroy,
onUndo: this.undo,
onRedo: this.redo,
renderHtmlForSaveRef: this.renderHtmlForSaveRef, renderHtmlForSaveRef: this.renderHtmlForSaveRef,
editorOptions: { editorOptions: {
element: this.$refs.editorElement, element: this.$refs.editorElement,
@@ -333,6 +341,12 @@ export default {
onUpdate: (event) => { onUpdate: (event) => {
this.$emit('update', event) this.$emit('update', event)
if (event.transaction.getMeta('addToHistory') !== false) {
this.pushUndo({ amend: event.transaction.time - this.lastUpdateTime < 500 })
this.lastUpdateTime = event.transaction.time
}
if (this.blockMenuCoords) { if (this.blockMenuCoords) {
this.$nextTick(() => this.setBlockMenuCoords(this.editor)) this.$nextTick(() => this.setBlockMenuCoords(this.editor))
} }
@@ -352,6 +366,8 @@ export default {
this.editor.view.dom.addEventListener('mousemove', this.onEditorMouseMove) this.editor.view.dom.addEventListener('mousemove', this.onEditorMouseMove)
this.editor.view.dom.addEventListener('mouseleave', this.onEditorMouseLeave) this.editor.view.dom.addEventListener('mouseleave', this.onEditorMouseLeave)
this.editor.view.dom.addEventListener('keydown', this.onEditorKeyDown) this.editor.view.dom.addEventListener('keydown', this.onEditorKeyDown)
this.$nextTick(() => this.pushUndo({ amend: true }))
}, },
beforeUnmount () { beforeUnmount () {
if (this.editor) { if (this.editor) {
@@ -367,6 +383,19 @@ export default {
reloadContent () { reloadContent () {
this.editor.commands.setContent(this.section.innerHTML, { emitUpdate: false, parseOptions: this.editor.options.parseOptions }) this.editor.commands.setContent(this.section.innerHTML, { emitUpdate: false, parseOptions: this.editor.options.parseOptions })
}, },
restoreDoc (doc) {
const { state, view } = this.editor
const { content } = doc.type.schema === state.schema ? doc : state.schema.nodeFromJSON(doc.toJSON())
const diffEnd = content.findDiffEnd(state.doc.content)
if (diffEnd) {
const tr = state.tr.replaceWith(0, state.doc.content.size, content).setMeta('addToHistory', false)
view.dispatch(tr.setSelection(Selection.near(tr.doc.resolve(diffEnd.a))).scrollIntoView())
this.lastUpdateTime = 0
}
},
findAreaNodePos (areaUuid) { findAreaNodePos (areaUuid) {
const el = this.editor.view.dom.querySelector(`[data-area-uuid="${areaUuid}"]`) const el = this.editor.view.dom.querySelector(`[data-area-uuid="${areaUuid}"]`)
@@ -711,6 +740,8 @@ export default {
if (event.key === 'Escape') { if (event.key === 'Escape') {
this.editor.chain().setNodeSelection(0).blur().run() this.editor.chain().setNodeSelection(0).blur().run()
this.deselectArea() this.deselectArea()
} else if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'z') {
event.stopPropagation()
} }
}, },
onEditorMouseMove (event) { onEditorMouseMove (event) {
@@ -826,25 +857,23 @@ export default {
} }
} }
const newField = { const newField = payload.parentField || {
...field, ...field,
uuid: v4(), uuid: v4(),
submitter_uuid: this.selectedSubmitter.uuid, submitter_uuid: this.selectedSubmitter.uuid,
areas: [area] areas: []
} }
newField.areas.push(area)
if (['radio', 'multiple'].includes(field.type) && field.options?.length) { if (['radio', 'multiple'].includes(field.type) && field.options?.length) {
const oldOptionUuid = area.option_uuid const oldOptionIndex = field.options.findIndex((opt) => opt.uuid === area.option_uuid)
const optionsMap = {}
newField.options = field.options.map((opt) => { if (!payload.parentField) {
const newUuid = v4() newField.options = field.options.map((opt) => ({ ...opt, uuid: v4() }))
optionsMap[opt.uuid] = newUuid }
return { ...opt, uuid: newUuid } area.option_uuid = (newField.options[oldOptionIndex] || newField.options[0]).uuid
})
area.option_uuid = optionsMap[oldOptionUuid] || newField.options[0]?.uuid
} }
return { field: newField, area } return { field: newField, area }
@@ -882,10 +911,13 @@ export default {
let lastArea = null let lastArea = null
const parentFieldsIndex = {}
fieldNodes.forEach((fieldNode) => { fieldNodes.forEach((fieldNode) => {
const fieldValue = fieldNode.dataset.field const fieldValue = fieldNode.dataset.field
const areaValue = fieldNode.dataset.area const areaValue = fieldNode.dataset.area
const templateId = fieldNode.dataset.templateId const templateId = fieldNode.dataset.templateId
const sourceFieldUuid = fieldNode.getAttribute('uuid')
if (!fieldValue || !areaValue) { if (!fieldValue || !areaValue) {
return return
@@ -894,11 +926,16 @@ export default {
const { field, area } = this.buildCopiedField({ const { field, area } = this.buildCopiedField({
field: JSON.parse(fieldValue), field: JSON.parse(fieldValue),
area: JSON.parse(areaValue), area: JSON.parse(areaValue),
templateId: Number(templateId) templateId: Number(templateId),
parentField: parentFieldsIndex[sourceFieldUuid]
}) })
this.insertFieldInTemplate(field, insertIndex) if (!parentFieldsIndex[sourceFieldUuid]) {
insertIndex += 1 parentFieldsIndex[sourceFieldUuid] = field
this.insertFieldInTemplate(field, insertIndex)
insertIndex += 1
}
fieldNode.setAttribute('uuid', field.uuid) fieldNode.setAttribute('uuid', field.uuid)
fieldNode.setAttribute('area-uuid', area.uuid) fieldNode.setAttribute('area-uuid', area.uuid)
-1
View File
@@ -20,7 +20,6 @@
"@tiptap/extension-dropcursor": "^3.19.0", "@tiptap/extension-dropcursor": "^3.19.0",
"@tiptap/extension-gapcursor": "^3.19.0", "@tiptap/extension-gapcursor": "^3.19.0",
"@tiptap/extension-hard-break": "^3.19.0", "@tiptap/extension-hard-break": "^3.19.0",
"@tiptap/extension-history": "^3.19.0",
"@tiptap/extension-italic": "^3.19.0", "@tiptap/extension-italic": "^3.19.0",
"@tiptap/extension-link": "^3.19.0", "@tiptap/extension-link": "^3.19.0",
"@tiptap/extension-paragraph": "^3.19.0", "@tiptap/extension-paragraph": "^3.19.0",
-5
View File
@@ -1796,11 +1796,6 @@
resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-3.19.0.tgz#7120524cec9ed4b957963693cb4c57cbecbaecf8" resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-3.19.0.tgz#7120524cec9ed4b957963693cb4c57cbecbaecf8"
integrity sha512-lAmQraYhPS5hafvCl74xDB5+bLuNwBKIEsVoim35I0sDJj5nTrfhaZgMJ91VamMvT+6FF5f1dvBlxBxAWa8jew== integrity sha512-lAmQraYhPS5hafvCl74xDB5+bLuNwBKIEsVoim35I0sDJj5nTrfhaZgMJ91VamMvT+6FF5f1dvBlxBxAWa8jew==
"@tiptap/extension-history@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-3.19.0.tgz#dc3a52f4eb16b7ac077c28284684b558c03adf00"
integrity sha512-hhN5nL7Pqcd9iomzeUUMKnmSQieKNlJ2FUgf2dHUqqvn4pWvcHA6P6UwdDNhuKFivSJNNMqtajkOvO4bYq1KPw==
"@tiptap/extension-italic@^3.19.0": "@tiptap/extension-italic@^3.19.0":
version "3.19.0" version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-3.19.0.tgz#af2a9c095ec846e379041f3e17e1dd101a5a4bf8" resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-3.19.0.tgz#af2a9c095ec846e379041f3e17e1dd101a5a4bf8"