171 lines
4.9 KiB
TypeScript
171 lines
4.9 KiB
TypeScript
import React from 'react'
|
|
import MDEditor from '@uiw/react-md-editor'
|
|
import { BaseBoxShapeUtil, TLBaseShape } from '@tldraw/tldraw'
|
|
|
|
export type IMarkdownShape = TLBaseShape<
|
|
'Markdown',
|
|
{
|
|
w: number
|
|
h: number
|
|
text: string
|
|
}
|
|
>
|
|
|
|
export class MarkdownShape extends BaseBoxShapeUtil<IMarkdownShape> {
|
|
static type = 'Markdown' as const
|
|
|
|
getDefaultProps(): IMarkdownShape['props'] {
|
|
return {
|
|
w: 300,
|
|
h: 200,
|
|
text: '',
|
|
}
|
|
}
|
|
|
|
component(shape: IMarkdownShape) {
|
|
// Hooks must be at the top level
|
|
const isSelected = this.editor.getSelectedShapeIds().includes(shape.id)
|
|
const markdownRef = React.useRef<HTMLDivElement>(null)
|
|
|
|
// Single useEffect hook that handles checkbox interactivity
|
|
React.useEffect(() => {
|
|
if (!isSelected && markdownRef.current) {
|
|
const checkboxes = markdownRef.current.querySelectorAll('input[type="checkbox"]')
|
|
checkboxes.forEach((checkbox) => {
|
|
checkbox.removeAttribute('disabled')
|
|
checkbox.addEventListener('click', handleCheckboxClick)
|
|
})
|
|
|
|
// Cleanup function
|
|
return () => {
|
|
if (markdownRef.current) {
|
|
const checkboxes = markdownRef.current.querySelectorAll('input[type="checkbox"]')
|
|
checkboxes.forEach((checkbox) => {
|
|
checkbox.removeEventListener('click', handleCheckboxClick)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}, [isSelected, shape.props.text])
|
|
|
|
// Handler function defined outside useEffect
|
|
const handleCheckboxClick = (event: Event) => {
|
|
event.stopPropagation()
|
|
const target = event.target as HTMLInputElement
|
|
const checked = target.checked
|
|
|
|
const text = shape.props.text
|
|
const lines = text.split('\n')
|
|
const checkboxRegex = /^\s*[-*+]\s+\[([ x])\]/
|
|
|
|
const newText = lines.map(line => {
|
|
if (line.includes(target.parentElement?.textContent || '')) {
|
|
return line.replace(checkboxRegex, `- [${checked ? 'x' : ' '}]`)
|
|
}
|
|
return line
|
|
}).join('\n')
|
|
|
|
this.editor.updateShape<IMarkdownShape>({
|
|
id: shape.id,
|
|
type: 'Markdown',
|
|
props: {
|
|
...shape.props,
|
|
text: newText,
|
|
},
|
|
})
|
|
}
|
|
|
|
const wrapperStyle: React.CSSProperties = {
|
|
width: '100%',
|
|
height: '100%',
|
|
backgroundColor: 'white',
|
|
border: '1px solid #ddd',
|
|
borderRadius: '4px',
|
|
overflow: 'hidden',
|
|
}
|
|
|
|
// Simplified contentStyle - removed padding and center alignment
|
|
const contentStyle: React.CSSProperties = {
|
|
width: '100%',
|
|
height: '100%',
|
|
backgroundColor: '#FFFFFF',
|
|
cursor: isSelected ? 'text' : 'default',
|
|
pointerEvents: 'all',
|
|
}
|
|
|
|
// Show MDEditor when selected
|
|
if (isSelected) {
|
|
return (
|
|
<div style={wrapperStyle}>
|
|
<div style={contentStyle}>
|
|
<MDEditor
|
|
value={shape.props.text}
|
|
onChange={(value = '') => {
|
|
this.editor.updateShape<IMarkdownShape>({
|
|
id: shape.id,
|
|
type: 'Markdown',
|
|
props: {
|
|
...shape.props,
|
|
text: value,
|
|
},
|
|
})
|
|
}}
|
|
preview='edit'
|
|
hideToolbar={true}
|
|
style={{
|
|
height: '100%',
|
|
border: 'none',
|
|
}}
|
|
textareaProps={{
|
|
placeholder: "Enter markdown text...",
|
|
style: {
|
|
backgroundColor: 'transparent',
|
|
height: '100%',
|
|
padding: '12px',
|
|
lineHeight: '1.5',
|
|
fontSize: '14px',
|
|
}
|
|
}}
|
|
onPointerDown={(e) => {
|
|
e.stopPropagation()
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Show rendered markdown when not selected
|
|
return (
|
|
<div style={wrapperStyle}>
|
|
<div style={contentStyle}>
|
|
<div ref={markdownRef} style={{ width: '100%', height: '100%', padding: '12px' }}>
|
|
{shape.props.text ? (
|
|
<MDEditor.Markdown source={shape.props.text} />
|
|
) : (
|
|
<span style={{ opacity: 0.5 }}>Click to edit markdown...</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
indicator(shape: IMarkdownShape) {
|
|
return <rect width={shape.props.w} height={shape.props.h} />
|
|
}
|
|
|
|
// Add handlers for better interaction
|
|
override onDoubleClick = (shape: IMarkdownShape) => {
|
|
const textarea = document.querySelector(`[data-shape-id="${shape.id}"] textarea`) as HTMLTextAreaElement
|
|
textarea?.focus()
|
|
}
|
|
|
|
onPointerDown = (shape: IMarkdownShape) => {
|
|
if (!shape.props.text) {
|
|
const textarea = document.querySelector(`[data-shape-id="${shape.id}"] textarea`) as HTMLTextAreaElement
|
|
textarea?.focus()
|
|
}
|
|
}
|
|
}
|