'use client' import { Checkbox } from '@admin/components/ui/checkbox' import { getTaskItemCheckboxLabel } from '@admin/utils/editor/get-task-item-checkbox-label' import { hasOwnAttribute } from '@admin/utils/editor/has-own-attribute' import { isTaskItemChecked } from '@admin/utils/editor/is-task-item-checked' import { setElementAttribute } from '@admin/utils/editor/set-element-attribute' import { getRenderedAttributes } from '@tiptap/core' import { TaskItem } from '@tiptap/extension-list' import { createRoot } from 'react-dom/client' export const ContentEditorTaskItem = TaskItem.extend({ addNodeView() { return ({ node, HTMLAttributes, getPos, editor }) => { let currentNode = node let renderedChecked = isTaskItemChecked(node) const listItem = document.createElement('li') const checkboxContainer = document.createElement('span') const checkboxMount = document.createElement('span') const content = document.createElement('div') const checkboxRoot = createRoot(checkboxMount) const staticAttributes = this.options.HTMLAttributes as Record checkboxContainer.contentEditable = 'false' checkboxContainer.className = 'content-editor-task-item-checkbox' checkboxContainer.append(checkboxMount) listItem.append(checkboxContainer, content) const renderCheckbox = () => { const hasReadOnlyHandler = Boolean(this.options.onReadOnlyChecked) checkboxRoot.render( event.preventDefault()} onCheckedChange={(checkedState) => { const checked = checkedState === true if (!editor.isEditable && !this.options.onReadOnlyChecked) { renderCheckbox() return } if (editor.isEditable && typeof getPos === 'function') { editor .chain() .focus(undefined, { scrollIntoView: false }) .command(({ tr }) => { const position = getPos() if (typeof position !== 'number') return false const taskItem = tr.doc.nodeAt(position) tr.setNodeMarkup(position, undefined, { ...taskItem?.attrs, checked }) return true }) .run() return } if (!editor.isEditable && this.options.onReadOnlyChecked?.(currentNode, checked)) { renderedChecked = checked } renderCheckbox() }} /> ) } function applyAttributes(nextHTMLAttributes: Record) { for (const [key, value] of Object.entries(staticAttributes)) { setElementAttribute(listItem, key, value) } for (const [key, value] of Object.entries(nextHTMLAttributes)) { setElementAttribute(listItem, key, value) } listItem.dataset.checked = String(renderedChecked) } applyAttributes(HTMLAttributes as Record) renderCheckbox() let prevRenderedAttributeKeys = new Set(Object.keys(HTMLAttributes)) return { dom: listItem, contentDOM: content, update: (updatedNode) => { if (updatedNode.type !== this.type) return false currentNode = updatedNode renderedChecked = isTaskItemChecked(updatedNode) const extensionAttributes = editor.extensionManager.attributes const newHTMLAttributes = getRenderedAttributes( updatedNode, extensionAttributes ) as Record const newKeys = new Set(Object.keys(newHTMLAttributes)) prevRenderedAttributeKeys.forEach((key) => { if (newKeys.has(key)) return if (hasOwnAttribute(staticAttributes, key)) { setElementAttribute(listItem, key, staticAttributes[key]) return } listItem.removeAttribute(key) }) applyAttributes(newHTMLAttributes) prevRenderedAttributeKeys = newKeys renderCheckbox() return true }, stopEvent: (event) => { return checkboxContainer.contains(event.target as Node | null) }, ignoreMutation: (mutation) => { return checkboxContainer.contains(mutation.target) }, destroy: () => { queueMicrotask(() => checkboxRoot.unmount()) } } } } }) export default ContentEditorTaskItem