checklist rebuild
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -3,7 +3,7 @@ import { createTiptapEditor } from "solid-tiptap";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import TaskList from "@tiptap/extension-task-list";
|
||||
import TaskItem from "@tiptap/extension-task-item";
|
||||
import { CustomTaskItem } from "./CustomTaskItem";
|
||||
import Image from "@tiptap/extension-image";
|
||||
import Underline from "@tiptap/extension-underline";
|
||||
import { X } from "lucide-solid";
|
||||
@@ -57,7 +57,7 @@ export const TaskEditor: Component<TaskEditorProps> = (props) => {
|
||||
class: "not-prose pl-2",
|
||||
},
|
||||
}),
|
||||
TaskItem.configure({
|
||||
CustomTaskItem.configure({
|
||||
nested: true,
|
||||
HTMLAttributes: {
|
||||
class: "flex gap-2 items-start my-1",
|
||||
@@ -91,12 +91,10 @@ export const TaskEditor: Component<TaskEditorProps> = (props) => {
|
||||
// Custom Task List Styling
|
||||
"[&_ul[data-type='taskList']]:list-none [&_ul[data-type='taskList']]:p-0",
|
||||
"[&_li[data-type='taskItem']]:flex [&_li[data-type='taskItem']]:items-start",
|
||||
"[&_li[data-type='taskItem']>label]:mr-3 [&_li[data-type='taskItem']>label]:mt-1 [&_li[data-type='taskItem']>label]:select-none",
|
||||
"[&_li[data-type='taskItem']>label>input]:w-5 [&_li[data-type='taskItem']>label>input]:h-5 [&_li[data-type='taskItem']>label>input]:cursor-pointer",
|
||||
"[&_li[data-type='taskItem']>div]:flex-1 [&_li[data-type='taskItem']>div]:min-w-0 [&_li[data-type='taskItem']>div]:min-h-[1.5rem]",
|
||||
"[&_li[data-type='taskItem']>div>p]:min-h-[1.5rem] [&_li[data-type='taskItem']>div>p]:m-0 [&_li[data-type='taskItem']>div>p]:w-full",
|
||||
"[&_li[data-type='taskItem']>div>p:empty]:after:content-['\\200B'] [&_li[data-type='taskItem']>div>p:empty]:after:inline-block",
|
||||
"[&_li[data-type='taskItem']>div>p>br]:after:content-['\\200B'] [&_li[data-type='taskItem']>div>p>br]:after:inline-block", // if tiptap adds br
|
||||
"[&_.tiptap-task-content]:flex-1 [&_.tiptap-task-content]:min-w-0 [&_.tiptap-task-content]:min-h-[1.5rem]",
|
||||
"[&_.tiptap-task-content>p]:min-h-[1.5rem] [&_.tiptap-task-content>p]:m-0 [&_.tiptap-task-content>p]:w-full",
|
||||
"[&_.tiptap-task-content>p:empty]:after:content-['\\200B'] [&_.tiptap-task-content>p:empty]:after:inline-block",
|
||||
"[&_.tiptap-task-content>p>br]:after:content-['\\200B'] [&_.tiptap-task-content>p>br]:after:inline-block", // if tiptap adds br
|
||||
props.class
|
||||
),
|
||||
},
|
||||
@@ -123,7 +121,7 @@ export const TaskEditor: Component<TaskEditorProps> = (props) => {
|
||||
return (
|
||||
<div class="relative w-full h-full flex flex-col">
|
||||
<div
|
||||
class="absolute -top-4 -left-4 -right-4 h-8 cursor-pointer group flex items-center justify-center z-10"
|
||||
class="absolute -top-6 -left-4 -right-4 h-8 cursor-pointer group flex items-center justify-center z-10"
|
||||
onClick={() => {
|
||||
const e = editor();
|
||||
if (!e) return;
|
||||
@@ -131,7 +129,7 @@ export const TaskEditor: Component<TaskEditorProps> = (props) => {
|
||||
}}
|
||||
title="Add empty line at the top"
|
||||
>
|
||||
<span class="opacity-0 group-hover:opacity-100 text-[10px] uppercase font-bold text-muted-foreground tracking-widest transition-opacity mt-2">
|
||||
<span class="opacity-0 group-hover:opacity-100 text-[10px] uppercase font-bold text-muted-foreground tracking-widest transition-opacity mt-1">
|
||||
Click to insert newline above
|
||||
</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user