Fix zoom resizing, add multi page support, fix text box bugs, fix broken upload button
This commit is contained in:
parent
5b2140187f
commit
7ff5c8130d
3 changed files with 113 additions and 30 deletions
|
|
@ -21,6 +21,11 @@ export function AnnotationLayer({ pageNumber, width, height, viewportParams }: A
|
|||
|
||||
const annotations = useEditorStore(state => state.annotations);
|
||||
|
||||
const latestViewportParams = useRef(viewportParams);
|
||||
useEffect(() => {
|
||||
latestViewportParams.current = viewportParams;
|
||||
}, [viewportParams]);
|
||||
|
||||
// Initial setup and event binding
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current) return;
|
||||
|
|
@ -40,14 +45,15 @@ export function AnnotationLayer({ pageNumber, width, height, viewportParams }: A
|
|||
// Find existing annotation
|
||||
const ann = useEditorStore.getState().annotations.find(a => a.id === obj.id);
|
||||
if (ann) {
|
||||
const pt = screenToPdf({ x: obj.left, y: obj.top }, viewportParams);
|
||||
const currentVp = latestViewportParams.current;
|
||||
const pt = screenToPdf({ x: obj.left, y: obj.top }, currentVp);
|
||||
|
||||
const scaleX = obj.scaleX || 1;
|
||||
const scaleY = obj.scaleY || 1;
|
||||
|
||||
// Box width/height in PDF space
|
||||
const newWidth = (obj.width * scaleX) / (viewportParams.scale * (viewportParams.dpr || 1));
|
||||
const newHeight = (obj.height * scaleY) / (viewportParams.scale * (viewportParams.dpr || 1));
|
||||
const newWidth = (obj.width * scaleX) / (currentVp.scale * (currentVp.dpr || 1));
|
||||
const newHeight = (obj.height * scaleY) / (currentVp.scale * (currentVp.dpr || 1));
|
||||
|
||||
useEditorStore.getState().updateAnnotation(obj.id, {
|
||||
rect: { ...ann.rect, x: pt.x, y: pt.y, width: newWidth, height: newHeight }
|
||||
|
|
@ -106,25 +112,30 @@ export function AnnotationLayer({ pageNumber, width, height, viewportParams }: A
|
|||
};
|
||||
}, []); // Run once on mount
|
||||
|
||||
const prevScaleRef = useRef(viewportParams.scale);
|
||||
|
||||
// Handle dimensions and re-rendering annotations
|
||||
useEffect(() => {
|
||||
if (!fabricRef.current) return;
|
||||
const canvas = fabricRef.current;
|
||||
canvas.setDimensions({ width, height });
|
||||
|
||||
// Simple naive sync: clear and re-render everything
|
||||
const activeObj = canvas.getActiveObject() as any;
|
||||
const activeId = activeObj?.id;
|
||||
|
||||
const pageAnns = annotations.filter(a => a.page === pageNumber);
|
||||
const annIds = new Set(pageAnns.map(a => a.id));
|
||||
|
||||
// Remove objects that are no longer in annotations
|
||||
canvas.getObjects().forEach((obj: any) => {
|
||||
// Don't remove the active object unless it was deleted from the store
|
||||
if (obj.id === activeId && annIds.has(activeId)) return;
|
||||
const scaleChanged = prevScaleRef.current !== viewportParams.scale;
|
||||
prevScaleRef.current = viewportParams.scale;
|
||||
|
||||
if (!annIds.has(obj.id)) {
|
||||
// Remove objects that are no longer in annotations, OR if the zoom scale changed
|
||||
canvas.getObjects().forEach((obj: any) => {
|
||||
// Don't remove the active object unless it was deleted from the store,
|
||||
// UNLESS the scale changed (we must recreate everything on zoom)
|
||||
if (!scaleChanged && obj.id === activeId && annIds.has(activeId)) return;
|
||||
|
||||
if (scaleChanged || !annIds.has(obj.id)) {
|
||||
canvas.remove(obj);
|
||||
}
|
||||
});
|
||||
|
|
@ -132,7 +143,7 @@ export function AnnotationLayer({ pageNumber, width, height, viewportParams }: A
|
|||
// Add missing annotations
|
||||
const canvasObjIds = new Set(canvas.getObjects().map((o: any) => o.id));
|
||||
pageAnns.forEach(ann => {
|
||||
if (ann.id === activeId) return; // Skip rendering the active object
|
||||
if (!scaleChanged && ann.id === activeId) return; // Skip rendering the active object
|
||||
|
||||
if (!canvasObjIds.has(ann.id)) {
|
||||
const tool = getTool(ann.type);
|
||||
|
|
@ -141,7 +152,14 @@ export function AnnotationLayer({ pageNumber, width, height, viewportParams }: A
|
|||
}
|
||||
}
|
||||
});
|
||||
}, [width, height, annotations, pageNumber]); // We ignore viewportParams identity for now to avoid loops
|
||||
|
||||
if (scaleChanged && activeId && annIds.has(activeId)) {
|
||||
const newlyRenderedActiveObj = canvas.getObjects().find((o: any) => o.id === activeId);
|
||||
if (newlyRenderedActiveObj) {
|
||||
canvas.setActiveObject(newlyRenderedActiveObj);
|
||||
}
|
||||
}
|
||||
}, [width, height, annotations, pageNumber, viewportParams.scale]);
|
||||
|
||||
// Handle tool activation/deactivation and event binding
|
||||
useEffect(() => {
|
||||
|
|
@ -162,16 +180,16 @@ export function AnnotationLayer({ pageNumber, width, height, viewportParams }: A
|
|||
if (tool.onActivate) tool.onActivate(canvas);
|
||||
|
||||
if (tool.onPointerDown) {
|
||||
canvas.on('mouse:down', (e) => tool.onPointerDown!(e, canvas, viewportParams, pageNumber));
|
||||
canvas.on('mouse:down', (e) => tool.onPointerDown!(e, canvas, latestViewportParams.current, pageNumber));
|
||||
}
|
||||
if (tool.onPointerMove) {
|
||||
canvas.on('mouse:move', (e) => tool.onPointerMove!(e, canvas, viewportParams, pageNumber));
|
||||
canvas.on('mouse:move', (e) => tool.onPointerMove!(e, canvas, latestViewportParams.current, pageNumber));
|
||||
}
|
||||
if (tool.onPointerUp) {
|
||||
canvas.on('mouse:up', (e) => tool.onPointerUp!(e, canvas, viewportParams, pageNumber));
|
||||
canvas.on('mouse:up', (e) => tool.onPointerUp!(e, canvas, latestViewportParams.current, pageNumber));
|
||||
}
|
||||
if (tool.onPathCreated) {
|
||||
canvas.on('path:created', (e) => tool.onPathCreated!(e, canvas, viewportParams, pageNumber));
|
||||
canvas.on('path:created', (e) => tool.onPathCreated!(e, canvas, latestViewportParams.current, pageNumber));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,21 +53,71 @@ export function TextFormatToolbar({ annotationId, viewportParams }: TextFormatTo
|
|||
|
||||
const applyStyle = (styleName: string, value: any, globalPropName: keyof TextProps, globalValue?: any) => {
|
||||
const canvas = (window as any).__fabricCanvas as any;
|
||||
let didInline = false;
|
||||
|
||||
if (canvas) {
|
||||
const activeObj = canvas.getActiveObject();
|
||||
if (activeObj && activeObj.id === annotationId) {
|
||||
const isStructural = styleName === 'fontSize' || styleName === 'fontFamily';
|
||||
|
||||
if (isStructural) {
|
||||
// Structural properties MUST be applied to the base object. Fabric 7's bounding box calculations
|
||||
// frequently fail when inline styles are used for size/font.
|
||||
activeObj.set(styleName, value);
|
||||
|
||||
// Obliterate any inline styles for this property so the base property strictly applies to all text
|
||||
if (activeObj.styles) {
|
||||
for (const line in activeObj.styles) {
|
||||
for (const char in activeObj.styles[line]) {
|
||||
if (activeObj.styles[line][char]) {
|
||||
delete activeObj.styles[line][char][styleName];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the box is empty and we are currently editing it, Fabric's invisible cursor
|
||||
// cache will still stubbornly hold the old size unless we violently wipe it.
|
||||
if (activeObj.isEditing && !activeObj.text) {
|
||||
activeObj.styles = {};
|
||||
if (activeObj.hiddenTextarea) {
|
||||
if (styleName === 'fontSize') activeObj.hiddenTextarea.style.fontSize = `${value}px`;
|
||||
if (styleName === 'fontFamily') activeObj.hiddenTextarea.style.fontFamily = value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Cosmetic properties (bold, italic, color) work fine with inline styles
|
||||
if (activeObj.isEditing) {
|
||||
if (activeObj.selectionStart !== activeObj.selectionEnd) {
|
||||
activeObj.setSelectionStyles({ [styleName]: value });
|
||||
didInline = true;
|
||||
} else {
|
||||
if (!activeObj.text) {
|
||||
activeObj.set(styleName, value);
|
||||
}
|
||||
} else {
|
||||
// If NOT editing, they selected the whole box. Update the base property.
|
||||
activeObj.set(styleName, value);
|
||||
|
||||
// Clear any inline styles for this property so the base property actually takes effect!
|
||||
if (activeObj.styles) {
|
||||
for (const line in activeObj.styles) {
|
||||
for (const char in activeObj.styles[line]) {
|
||||
if (activeObj.styles[line][char]) {
|
||||
delete activeObj.styles[line][char][styleName];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Critical: Fabric 7 heavily caches text. We MUST mark it dirty to force a redraw!
|
||||
activeObj.dirty = true;
|
||||
if ((activeObj as any)._forceClearCache !== undefined) {
|
||||
(activeObj as any)._forceClearCache = true;
|
||||
}
|
||||
|
||||
// Remove manual height constraint so the box can grow with the new font size
|
||||
delete activeObj.customHeight;
|
||||
if (activeObj.initDimensions) activeObj.initDimensions();
|
||||
activeObj.setCoords();
|
||||
canvas.requestRenderAll();
|
||||
}
|
||||
}
|
||||
|
|
@ -76,9 +126,10 @@ export function TextFormatToolbar({ annotationId, viewportParams }: TextFormatTo
|
|||
const newProps = { [globalPropName]: finalGlobalValue } as any;
|
||||
setDefaultTextProps(newProps);
|
||||
|
||||
if (!didInline && !isDraft) {
|
||||
// Always update store so the toolbar displays the new value
|
||||
if (!isDraft) {
|
||||
updateAnnotation(annotationId, { props: { ...props, ...newProps } });
|
||||
} else if (!didInline && isDraft) {
|
||||
} else {
|
||||
useEditorStore.getState().setDraftAnnotation({
|
||||
...textAnn,
|
||||
props: { ...props, ...newProps }
|
||||
|
|
@ -120,6 +171,12 @@ export function TextFormatToolbar({ annotationId, viewportParams }: TextFormatTo
|
|||
ref={toolbarRef}
|
||||
className="absolute z-50 bg-white rounded-md shadow-lg border border-neutral-200 flex items-center p-1 gap-1 h-10"
|
||||
style={{ top: `${Math.max(0, top)}px`, left: `${left}px` }}
|
||||
onMouseDown={(e) => {
|
||||
// Prevent clicking the toolbar from stealing focus and deselecting the canvas object
|
||||
if ((e.target as HTMLElement).tagName !== 'INPUT') {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Font Family Dropdown */}
|
||||
<div className="relative">
|
||||
|
|
|
|||
|
|
@ -138,12 +138,20 @@ export const LibraryHeader = ({ currentTab, onTabChange }: { currentTab: 'librar
|
|||
accept="application/pdf"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const uploader = document.querySelector('input[type="file"][multiple]') as HTMLInputElement
|
||||
if (uploader && uploader !== e.target) {
|
||||
uploader.files = e.target.files
|
||||
const event = new Event('change', { bubbles: true })
|
||||
uploader.dispatchEvent(event)
|
||||
onChange={async (e) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
const files = e.target.files;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (file.type === 'application/pdf') {
|
||||
try {
|
||||
await useLibrary.getState().uploadDocument(file);
|
||||
} catch (err) {
|
||||
console.error('Failed to upload', file.name, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
e.target.value = '';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue