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 = '' 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 }, } } }, })