* switch to biome * writer format * migrate format * packages format * hudozka format * writer fix * biome config updated * migrate fix * packages fix * hudozka fix * claude updated
114 lines
4.2 KiB
TypeScript
114 lines
4.2 KiB
TypeScript
import { mergeAttributes, Node } from "@tiptap/core"
|
|
import type { NodeViewProps } from "@tiptap/react"
|
|
import { NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react"
|
|
import { useEffect, useState } from "react"
|
|
import { BlockActions } from "../components/BlockActions"
|
|
import { BlockInsert } from "../components/BlockInsert"
|
|
import { ImagePicker } from "../components/ImagePicker"
|
|
import { pb } from "../pb"
|
|
import type { PbImage } from "../types"
|
|
|
|
function ImageBlockView({ node, updateAttributes, editor, getPos }: NodeViewProps) {
|
|
const { imageId, wide, caption } = node.attrs
|
|
const [thumbnail, setThumbnail] = useState<string | null>(null)
|
|
const [showPicker, setShowPicker] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (!imageId) return
|
|
pb.collection("images")
|
|
.getOne<PbImage>(imageId)
|
|
.then(img => {
|
|
const url = pb.files.getURL(img, img.file, { thumb: "300x200" })
|
|
setThumbnail(url)
|
|
})
|
|
.catch(() => {
|
|
setThumbnail(null)
|
|
})
|
|
}, [imageId])
|
|
|
|
function handleSelect(newImageId: string) {
|
|
updateAttributes({ imageId: newImageId })
|
|
setShowPicker(false)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<NodeViewWrapper className="node-view-block node-image-block">
|
|
<BlockActions editor={editor} getPos={getPos} />
|
|
<div className="node-view-content">
|
|
<div className="node-block-label">Image</div>
|
|
{thumbnail ? (
|
|
<button
|
|
type="button"
|
|
className="node-image-thumb-button"
|
|
onClick={() => setShowPicker(true)}
|
|
style={{ cursor: "pointer", padding: 0, border: 0, background: "none" }}
|
|
>
|
|
<img src={thumbnail} alt={caption || "image"} className="node-image-thumb" />
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
className="node-image-placeholder"
|
|
onClick={() => setShowPicker(true)}
|
|
style={{ cursor: "pointer" }}
|
|
>
|
|
{imageId ? "Loading..." : "Click to select image"}
|
|
</button>
|
|
)}
|
|
<div className="node-image-controls">
|
|
<label>
|
|
<input
|
|
type="checkbox"
|
|
checked={wide}
|
|
onChange={e => updateAttributes({ wide: e.target.checked })}
|
|
/>
|
|
Wide
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="Caption"
|
|
value={caption || ""}
|
|
onChange={e => updateAttributes({ caption: e.target.value })}
|
|
className="node-image-caption"
|
|
/>
|
|
<button type="button" className="node-image-pick-btn" onClick={() => setShowPicker(true)}>
|
|
Pick
|
|
</button>
|
|
</div>
|
|
{showPicker && <ImagePicker onSelect={handleSelect} onClose={() => setShowPicker(false)} />}
|
|
</div>
|
|
</NodeViewWrapper>
|
|
<BlockInsert editor={editor} getPos={getPos} />
|
|
</>
|
|
)
|
|
}
|
|
|
|
export const ImageBlock = Node.create({
|
|
name: "imageBlock",
|
|
group: "block",
|
|
atom: true,
|
|
draggable: true,
|
|
|
|
addAttributes() {
|
|
return {
|
|
id: { default: null },
|
|
imageId: { default: "" },
|
|
wide: { default: false },
|
|
caption: { default: "" },
|
|
}
|
|
},
|
|
|
|
parseHTML() {
|
|
return [{ tag: "div[data-image-block]" }]
|
|
},
|
|
|
|
renderHTML({ HTMLAttributes }) {
|
|
return ["div", mergeAttributes(HTMLAttributes, { "data-image-block": "" })]
|
|
},
|
|
|
|
addNodeView() {
|
|
return ReactNodeViewRenderer(ImageBlockView)
|
|
},
|
|
})
|