fix(tasklist): refresh editor after checkbox toggle

This commit is contained in:
kolaente
2025-06-10 21:06:39 +02:00
parent 433b8b9115
commit 4d5bac320e
2 changed files with 84 additions and 54 deletions

View File

@@ -911,8 +911,8 @@ describe('Task', () => {
.should('exist')
})
it('Can check items off a checklist', () => {
const tasks = TaskFactory.create(1, {
it('Can check items off a checklist', () => {
const tasks = TaskFactory.create(1, {
id: 1,
description: `
<ul data-type="taskList">
@@ -950,8 +950,35 @@ describe('Task', () => {
cy.get('.tiptap__editor input[type=checkbox]')
.should('have.length', 5)
cy.get('.task-view .checklist-summary')
.should('contain.text', '2 of 5 tasks')
})
.should('contain.text', '2 of 5 tasks')
})
it('Checklist item stays checked after reload', () => {
const tasks = TaskFactory.create(1, {
id: 1,
description: `
<ul data-type="taskList">
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>First Item</p></div>
</li>
</ul>`,
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.tiptap__editor ul > li input[type=checkbox]')
.click()
cy.get('.task-view .details.content.description h3 span.is-small.has-text-success')
.contains('Saved!')
.should('exist')
cy.reload()
cy.get('.tiptap__editor ul > li input[type=checkbox]')
.should('be.checked')
cy.get('.task-view .checklist-summary')
.should('contain.text', '1 of 1 tasks')
})
it('Should use the editor to render description', () => {
const tasks = TaskFactory.create(1, {

View File

@@ -454,10 +454,11 @@ const extensions : Extensions = [
...node.attrs,
checked,
})
editor.value!.view.dispatch(tr)
bubbleSave()
}
})
editor.value!.view.dispatch(tr)
bubbleSave()
refreshTasklistCheckboxes()
}
})
return true
@@ -679,57 +680,59 @@ function focusIfEditing() {
}
function clickTasklistCheckbox(event) {
event.stopImmediatePropagation()
event.stopImmediatePropagation()
if (event.target.localName !== 'p') {
return
}
if (event.target.localName !== 'p') {
return
}
event.target.parentNode.parentNode.firstChild.click()
event.target.parentNode.parentNode.firstChild.click()
}
async function refreshTasklistCheckboxes() {
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 (isEditing.value) {
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
}
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)
})
}
watch(
() => isEditing.value,
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
}
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},
() => isEditing.value,
() => refreshTasklistCheckboxes(),
{immediate: true},
)
</script>