bổ sung chức năng eraser

This commit is contained in:
2026-04-01 09:25:31 +07:00
parent 79eea344e9
commit 89db1c7b5c
+60 -52
View File
@@ -8,7 +8,7 @@ import { api } from "@/lib/api";
// Types // Types
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
type Tool = "select" | "pen" | "highlighter" | "text"; type Tool = "select" | "pen" | "highlighter" | "text" | "eraser";
type ViewMode = "pan" | "draw"; type ViewMode = "pan" | "draw";
type FitMode = "width" | "height"; type FitMode = "width" | "height";
type ScrollMode = "single" | "continuous"; type ScrollMode = "single" | "continuous";
@@ -565,6 +565,18 @@ export default function WorkbookViewer({ pdfId }: Props) {
fc.renderAll(); fc.renderAll();
}); });
break; break;
case "eraser":
fc.isDrawingMode = false;
fc.selection = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fc.on("mouse:down", (options: any) => {
if (!options.target) return;
fc.remove(options.target);
addedObjects.current = addedObjects.current.filter(o => o !== options.target);
fc.renderAll();
});
break;
} }
}, [isReady, mode, tool, penColor, strokeWidth]); }, [isReady, mode, tool, penColor, strokeWidth]);
@@ -602,6 +614,12 @@ export default function WorkbookViewer({ pdfId }: Props) {
const t = new fabric.IText("Text", { left: pointer.x, top: pointer.y, fontSize: 20, fill: penColorRef.current, fontFamily: "Arial, sans-serif", padding: 4 }); const t = new fabric.IText("Text", { left: pointer.x, top: pointer.y, fontSize: 20, fill: penColorRef.current, fontFamily: "Arial, sans-serif", padding: 4 });
fc.add(t); fc.setActiveObject(t); t.enterEditing(); t.selectAll(); fc.renderAll(); fc.add(t); fc.setActiveObject(t); t.enterEditing(); t.selectAll(); fc.renderAll();
}); break; }); break;
case "eraser":
fc.isDrawingMode = false; fc.selection = false;
fc.on("mouse:down", (options: any) => {
if (!options.target) return;
fc.remove(options.target); fc.renderAll();
}); break;
} }
}); });
}, [isReady, scrollMode, mode, tool, penColor, strokeWidth]); }, [isReady, scrollMode, mode, tool, penColor, strokeWidth]);
@@ -700,45 +718,45 @@ export default function WorkbookViewer({ pdfId }: Props) {
{/* ── Sticky Toolbar ──────────────────────────────────────────────────── */} {/* ── Sticky Toolbar ──────────────────────────────────────────────────── */}
<header className="bg-white border-b shadow-sm sticky top-0 z-20"> <header className="bg-white border-b shadow-sm sticky top-0 z-20">
<div className="max-w-5xl mx-auto px-3 py-2 flex flex-wrap items-center gap-2"> <div className="px-3 py-2 flex items-center gap-0">
{/* Scrollable tools strip */}
<div className="flex items-center gap-2 overflow-x-auto flex-1 min-w-0">
{/* Back button + title */} {/* Back button + title */}
<button <button
onClick={() => router.push("/dashboard")} onClick={() => router.push("/dashboard")}
title="Back" title="Back"
className="p-1 text-gray-500 hover:text-blue-600 transition rounded" className="p-1 text-gray-500 hover:text-blue-600 transition rounded flex-shrink-0"
> >
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg> </svg>
</button> </button>
<span className="text-sm font-semibold text-gray-800 truncate max-w-[130px] sm:max-w-xs"> <span className="text-sm font-semibold text-gray-800 truncate max-w-[100px] flex-shrink-0">
{pdfTitle} {pdfTitle}
</span> </span>
<div className="h-5 w-px bg-gray-200 hidden sm:block" /> <div className="h-5 w-px bg-gray-200 flex-shrink-0" />
{/* Thumbnail panel toggle */} {/* Thumbnail toggle */}
<button <button
onClick={() => setShowThumbnails(v => !v)} onClick={() => setShowThumbnails(v => !v)}
title="Thumbnails" title="Thumbnails"
className={`p-1.5 rounded-lg transition flex items-center gap-1 ${ className={`p-1.5 rounded-lg transition flex items-center gap-1 flex-shrink-0 ${
showThumbnails showThumbnails ? "bg-blue-50 text-blue-600" : "text-gray-500 hover:bg-gray-100 hover:text-gray-800"
? "bg-blue-50 text-blue-600"
: "text-gray-500 hover:bg-gray-100 hover:text-gray-800"
}`} }`}
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" /> d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
</svg> </svg>
<span className="text-xs font-medium hidden sm:inline">Thumb</span>
</button> </button>
<div className="h-5 w-px bg-gray-200 hidden sm:block" /> <div className="h-5 w-px bg-gray-200 flex-shrink-0" />
{/* Fit mode buttons */} {/* Fit mode */}
<div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5"> <div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5 flex-shrink-0">
<button <button
onClick={() => setFitMode("width")} onClick={() => setFitMode("width")}
title="Fit Width" title="Fit Width"
@@ -749,7 +767,7 @@ export default function WorkbookViewer({ pdfId }: Props) {
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h8M8 12h8M4 17h16" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h8M8 12h8M4 17h16" />
</svg> </svg>
<span className="hidden sm:inline">Width</span> <span className="hidden lg:inline">Width</span>
</button> </button>
<button <button
onClick={() => setFitMode("height")} onClick={() => setFitMode("height")}
@@ -761,14 +779,14 @@ export default function WorkbookViewer({ pdfId }: Props) {
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8V4m0 0H3m4 0l-4 4M17 8V4m0 0h4m-4 0l4 4M7 16v4m0 0H3m4 0l-4-4M17 16v4m0 0h4m-4 0l4-4" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8V4m0 0H3m4 0l-4 4M17 8V4m0 0h4m-4 0l4 4M7 16v4m0 0H3m4 0l-4-4M17 16v4m0 0h4m-4 0l4-4" />
</svg> </svg>
<span className="hidden sm:inline">Height</span> <span className="hidden lg:inline">Height</span>
</button> </button>
</div> </div>
<div className="h-5 w-px bg-gray-200 hidden sm:block" /> <div className="h-5 w-px bg-gray-200 flex-shrink-0" />
{/* Scroll mode toggle */} {/* Scroll mode */}
<div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5"> <div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5 flex-shrink-0">
<button <button
onClick={() => setScrollMode("single")} onClick={() => setScrollMode("single")}
title="Single page" title="Single page"
@@ -779,7 +797,7 @@ export default function WorkbookViewer({ pdfId }: Props) {
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6M9 8h6M9 16h6M5 4h14a1 1 0 011 1v14a1 1 0 01-1 1H5a1 1 0 01-1-1V5a1 1 0 011-1z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6M9 8h6M9 16h6M5 4h14a1 1 0 011 1v14a1 1 0 01-1 1H5a1 1 0 01-1-1V5a1 1 0 011-1z" />
</svg> </svg>
<span className="hidden sm:inline">Single</span> <span className="hidden lg:inline">Single</span>
</button> </button>
<button <button
onClick={() => setScrollMode("continuous")} onClick={() => setScrollMode("continuous")}
@@ -791,14 +809,14 @@ export default function WorkbookViewer({ pdfId }: Props) {
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
</svg> </svg>
<span className="hidden sm:inline">Continuous</span> <span className="hidden lg:inline">Continuous</span>
</button> </button>
</div> </div>
<div className="h-5 w-px bg-gray-200 hidden sm:block" /> <div className="h-5 w-px bg-gray-200 flex-shrink-0" />
{/* Page navigation */} {/* Page navigation */}
<div className="flex items-center gap-0.5"> <div className="flex items-center gap-0.5 flex-shrink-0">
<button <button
onClick={() => goToPage(currentPage - 1)} onClick={() => goToPage(currentPage - 1)}
disabled={currentPage <= 1 || rendering || !isReady} disabled={currentPage <= 1 || rendering || !isReady}
@@ -840,10 +858,10 @@ export default function WorkbookViewer({ pdfId }: Props) {
</button> </button>
</div> </div>
<div className="h-5 w-px bg-gray-200 hidden sm:block" /> <div className="h-5 w-px bg-gray-200 flex-shrink-0" />
{/* Pan / Draw mode toggle */} {/* Pan / Draw mode toggle */}
<div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5"> <div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5 flex-shrink-0">
<button <button
onClick={() => setViewMode("pan")} onClick={() => setViewMode("pan")}
className={`text-xs px-2.5 py-1 rounded-md font-medium transition ${ className={`text-xs px-2.5 py-1 rounded-md font-medium transition ${
@@ -862,65 +880,57 @@ export default function WorkbookViewer({ pdfId }: Props) {
</button> </button>
</div> </div>
{/* Drawing tools — only shown in Draw mode */} {/* Drawing tools — inline, visible only in draw mode */}
{mode === "draw" && ( {mode === "draw" && (
<> <>
<div className="h-5 w-px bg-gray-200" /> <div className="h-5 w-px bg-gray-200 flex-shrink-0" />
{/* Tool buttons */} <div className="flex items-center gap-0.5 flex-shrink-0">
<div className="flex items-center gap-0.5">
{/* Select */}
<ToolBtn id="select" current={tool} onClick={setTool} title="Select / Move"> <ToolBtn id="select" current={tool} onClick={setTool} title="Select / Move">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l4-4 4 4m0 6l-4 4-4-4" />
d="M8 9l4-4 4 4m0 6l-4 4-4-4" />
</ToolBtn> </ToolBtn>
{/* Pen */}
<ToolBtn id="pen" current={tool} onClick={setTool} title="Pen"> <ToolBtn id="pen" current={tool} onClick={setTool} title="Pen">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" /> d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</ToolBtn> </ToolBtn>
{/* Highlighter */}
<ToolBtn id="highlighter" current={tool} onClick={setTool} title="Highlighter"> <ToolBtn id="highlighter" current={tool} onClick={setTool} title="Highlighter">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" /> d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
</ToolBtn> </ToolBtn>
{/* Text */}
<ToolBtn id="text" current={tool} onClick={setTool} title="Text"> <ToolBtn id="text" current={tool} onClick={setTool} title="Text">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 12h6m-3-3v6M7 20h10a2 2 0 002-2V6a2 2 0 00-2-2H7a2 2 0 00-2 2v12a2 2 0 002 2z" /> d="M9 12h6m-3-3v6M7 20h10a2 2 0 002-2V6a2 2 0 00-2-2H7a2 2 0 00-2 2v12a2 2 0 002 2z" />
</ToolBtn> </ToolBtn>
<ToolBtn id="eraser" current={tool} onClick={setTool} title="Eraser">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M19 7l-1.5 1.5M4.5 19.5l9-9m0 0L9 6l-4.5 4.5 4.5 4.5m4.5-4.5L18 6" />
</ToolBtn>
</div> </div>
{/* Color picker */}
<input <input
type="color" type="color"
value={penColor} value={penColor}
onChange={(e) => setPenColor(e.target.value)} onChange={(e) => setPenColor(e.target.value)}
title="Color" title="Color"
className="w-7 h-7 rounded cursor-pointer border border-gray-300 p-0.5 bg-white" className="w-7 h-7 rounded cursor-pointer border border-gray-300 p-0.5 bg-white flex-shrink-0"
/> />
{/* Stroke width slider */}
<input <input
type="range" type="range"
min={1} min={1}
max={12} max={12}
value={strokeWidth} value={strokeWidth}
onChange={(e) => setStrokeWidth(Number(e.target.value))} onChange={(e) => setStrokeWidth(Number(e.target.value))}
className="w-16 accent-blue-600" className="w-20 accent-blue-600 flex-shrink-0"
title={`Stroke width: ${strokeWidth}`} title={`Stroke width: ${strokeWidth}`}
/> />
<div className="h-5 w-px bg-gray-200" /> <div className="h-5 w-px bg-gray-200 flex-shrink-0" />
{/* Undo */}
<button <button
onClick={handleUndo} onClick={handleUndo}
title="Undo last stroke" title="Undo last stroke"
className="p-1.5 text-gray-500 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition" className="p-1.5 text-gray-500 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition flex-shrink-0"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
@@ -928,11 +938,10 @@ export default function WorkbookViewer({ pdfId }: Props) {
</svg> </svg>
</button> </button>
{/* Clear page */}
<button <button
onClick={handleClear} onClick={handleClear}
title="Clear page annotations" title="Clear page annotations"
className="p-1.5 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition" className="p-1.5 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition flex-shrink-0"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
@@ -941,13 +950,12 @@ export default function WorkbookViewer({ pdfId }: Props) {
</button> </button>
</> </>
)} )}
</div>
{/* Save button — far right */} {/* Save — fixed right, never scrolls away */}
<div className="ml-auto flex items-center gap-2"> <div className="flex items-center gap-2 pl-3 flex-shrink-0">
{saveNotice && ( {saveNotice && (
<span className="text-xs text-green-600 font-medium animate-pulse"> <span className="text-xs text-green-600 font-medium animate-pulse"> Saved</span>
Saved
</span>
)} )}
<button <button
onClick={handleSave} onClick={handleSave}