Files
TasGrid/src/components/CustomTaskItem.ts
T
2026-02-28 15:47:12 -06:00

165 lines
5.6 KiB
TypeScript

import { Node, mergeAttributes } from '@tiptap/core'
export const CustomTaskItem = Node.create({
name: 'taskItem',
addOptions() {
return {
nested: false,
HTMLAttributes: {},
}
},
content: 'paragraph block*',
defining: true,
addAttributes() {
return {
checked: {
default: false,
keepOnSplit: false,
parseHTML: element => element.getAttribute('data-checked') === 'true',
renderHTML: attributes => ({
'data-checked': attributes.checked,
}),
},
}
},
parseHTML() {
return [
{
tag: 'li[data-type="taskItem"]',
priority: 51,
},
]
},
renderHTML({ node, HTMLAttributes }) {
return [
'li',
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
'data-type': 'taskItem',
}),
[
'label',
[
'input',
{
type: 'checkbox',
checked: node.attrs.checked ? 'checked' : null,
},
],
['span', ''],
],
['div', 0],
]
},
addKeyboardShortcuts() {
return {
Enter: () => this.editor.commands.splitListItem(this.name),
'Shift-Tab': () => this.editor.commands.liftListItem(this.name),
Tab: () => this.editor.commands.sinkListItem(this.name),
}
},
addNodeView() {
return ({ node, getPos, editor }) => {
let currentNode = node;
const dom = document.createElement('li')
dom.classList.add('flex', 'items-center', 'group', 'relative', 'my-1')
dom.dataset.type = 'taskItem'
if (currentNode.attrs.checked) {
dom.dataset.checked = 'true'
}
// Wrapper for the checkbox
const handle = document.createElement('label')
handle.contentEditable = 'false'
handle.classList.add(
'flex', 'items-center', 'justify-center',
'mr-3', 'select-none', 'relative'
)
const checkbox = document.createElement('div')
checkbox.classList.add(
'w-5', 'h-5',
'flex', 'items-center', 'justify-center',
'rounded-md', 'border-[1.5px]',
'transition-colors', 'duration-300', 'cursor-pointer',
'shrink-0' // removed mt-0.5
)
// The SVG checkmark
const checkIcon = document.createElement('div')
checkIcon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>'
checkIcon.classList.add('transition-opacity', 'duration-300', 'ease-in-out')
checkbox.appendChild(checkIcon)
const updateCheckboxVisuals = (isChecked: boolean) => {
if (isChecked) {
checkbox.classList.add('bg-primary', 'border-primary', 'text-primary-foreground')
checkbox.classList.remove('border-muted-foreground/30', 'bg-transparent', 'text-transparent')
checkIcon.style.opacity = '1'
} else {
checkbox.classList.remove('bg-primary', 'border-primary', 'text-primary-foreground')
checkbox.classList.add('border-muted-foreground/30', 'bg-transparent', 'text-transparent')
checkIcon.style.opacity = '0'
}
}
// Initialize visually
updateCheckboxVisuals(currentNode.attrs.checked)
handle.addEventListener('click', (e) => {
// Prevent label default toggling since we handle it manually
e.preventDefault();
if (typeof getPos === 'function') {
editor.commands.command(({ tr }) => {
const position = getPos()
if (typeof position === 'number') {
tr.setNodeMarkup(position, undefined, {
checked: !currentNode.attrs.checked,
})
return true
}
return false
})
}
})
handle.appendChild(checkbox)
const content = document.createElement('div')
content.classList.add('flex-1', 'min-w-0', 'min-h-[1.5rem]')
// Add a class that can be targeted for the zero-width-space hack
content.classList.add('tiptap-task-content')
dom.appendChild(handle)
dom.appendChild(content)
return {
dom,
contentDOM: content,
update: (updatedNode) => {
if (updatedNode.type.name !== this.name) {
return false
}
currentNode = updatedNode;
updateCheckboxVisuals(updatedNode.attrs.checked)
if (updatedNode.attrs.checked) {
dom.dataset.checked = 'true'
} else {
delete dom.dataset.checked
}
return true
},
}
}
},
})