fix(editor): ensure task list clicks are only fired once

Before this fix, clicking on a task list item with the same name as another one, both would get marked as done. This was due to the mechanism which walks the dom tree to look for the node to update used its content for comparison. To prevent this, this fix first added unique ids to all task list items and then compared the nodes based on their id instead of the content.

Resolves vikunja/vikunja#2091
This commit is contained in:
kolaente 2024-02-14 14:12:28 +01:00
parent 5ab720d709
commit 3969f6ae66
Signed by untrusted user: konrad
GPG Key ID: F40E70337AB24C9B
1 changed files with 44 additions and 21 deletions

View File

@ -193,6 +193,7 @@ import {mergeAttributes} from '@tiptap/core'
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
import inputPrompt from '@/helpers/inputPrompt'
import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor'
import {createRandomID} from '@/helpers/randomId'
const {
modelValue,
@ -388,7 +389,20 @@ const editor = useEditor({
CustomImage,
TaskList,
TaskItem.configure({
TaskItem.extend({
addAttributes() {
return {
...this.parent?.(),
id: {
default: createRandomID,
parseHTML: element => element.getAttribute('data-id'),
renderHTML: attributes => ({
'data-id': attributes.id,
}),
},
}
},
}).configure({
nested: true,
onReadOnlyChecked: (node: Node, checked: boolean): boolean => {
if (!isEditEnabled) {
@ -400,7 +414,7 @@ const editor = useEditor({
// https://github.com/ueberdosis/tiptap/issues/3676
editor.value!.state.doc.descendants((subnode, pos) => {
if (node.eq(subnode)) {
if (node.attrs.id === subnode.attrs.id) {
const {tr} = editor.value!.state
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
@ -408,10 +422,10 @@ const editor = useEditor({
})
editor.value!.view.dispatch(tr)
bubbleSave()
return true
}
})
return true
},
}),
@ -594,27 +608,21 @@ function clickTasklistCheckbox(event) {
watch(
() => isEditing.value,
editing => {
nextTick(() => {
const checkboxes = tiptapInstanceRef.value?.querySelectorAll('[data-checked]')
async editing => {
await nextTick()
let checkboxes = tiptapInstanceRef.value?.querySelectorAll('[data-checked]')
if (typeof checkboxes === 'undefined' || checkboxes.length === 0) {
// For some reason, this works when we check a second time.
await nextTick()
checkboxes = tiptapInstanceRef.value?.querySelectorAll('[data-checked]')
if (typeof checkboxes === 'undefined' || checkboxes.length === 0) {
return
}
}
if (editing) {
checkboxes.forEach(check => {
if (check.children.length < 2) {
return
}
// We assume the first child contains the label element with the checkbox and the second child the actual label
// When the actual label is clicked, we forward that click to the checkbox.
check.children[1].removeEventListener('click', clickTasklistCheckbox)
})
return
}
if (editing) {
checkboxes.forEach(check => {
if (check.children.length < 2) {
return
@ -622,8 +630,21 @@ watch(
// We assume the first child contains the label element with the checkbox and the second child the actual label
// When the actual label is clicked, we forward that click to the checkbox.
check.children[1].addEventListener('click', clickTasklistCheckbox)
check.children[1].removeEventListener('click', clickTasklistCheckbox)
})
return
}
checkboxes.forEach(check => {
if (check.children.length < 2) {
return
}
// We assume the first child contains the label element with the checkbox and the second child the actual label
// When the actual label is clicked, we forward that click to the checkbox.
check.children[1].removeEventListener('click', clickTasklistCheckbox)
check.children[1].addEventListener('click', clickTasklistCheckbox)
})
},
{immediate: true},
@ -781,6 +802,7 @@ watch(
.ProseMirror {
/* Table-specific styling */
table {
border-collapse: collapse;
table-layout: fixed;
@ -836,6 +858,7 @@ watch(
}
// Lists
ul {
margin-left: .5rem;
margin-top: 0 !important;