import React, {
Ref,
useMemo,
useContext,
MutableRefObject,
useRef,
useState,
useEffect,
forwardRef,
useCallback,
} from 'react';
import { theme, Button } from 'antd';
import { OnDrag, OnResize, OnClick, OnRotate } from 'react-moveable';
import { ZOOM, SchemaForUI, Size, ChangeSchemas, BasePdf, isBlankPdf } from '@pdfme-tables/common';
import { PluginsRegistry } from '../../../contexts';
import { CloseOutlined } from '@ant-design/icons';
import { RULER_HEIGHT, RIGHT_SIDEBAR_WIDTH } from '../../../constants';
import { usePrevious } from '../../../hooks';
import { uuid, round, flatten } from '../../../helper';
import Paper from '../../Paper';
import Renderer from '../../Renderer';
import Selecto from './Selecto';
import Moveable from './Moveable';
import Guides from './Guides';
import Mask from './Mask';
import Padding from './Padding';
const mm2px = (mm: number) => mm * 3.7795275591;
const DELETE_BTN_ID = uuid();
const fmt4Num = (prop: string) => Number(prop.replace('px', ''));
const fmt = (prop: string) => round(fmt4Num(prop) / ZOOM, 2);
const isTopLeftResize = (d: string) => d === '-1,-1' || d === '-1,0' || d === '0,-1';
const normalizeRotate = (angle: number) => ((angle % 360) + 360) % 360;
const DeleteButton = ({ activeElements: aes }: { activeElements: HTMLElement[] }) => {
const { token } = theme.useToken();
const size = 26;
const top = Math.min(...aes.map(({ style }) => fmt4Num(style.top)));
const left = Math.max(...aes.map(({ style }) => fmt4Num(style.left) + fmt4Num(style.width))) + 10;
return (
);
};
interface GuidesInterface {
getGuides(): number[];
scroll(pos: number): void;
scrollGuides(pos: number): void;
loadGuides(guides: number[]): void;
resize(): void;
}
interface Props {
basePdf: BasePdf;
height: number;
hoveringSchemaId: string | null;
onChangeHoveringSchemaId: (id: string | null) => void;
pageCursor: number;
schemasList: SchemaForUI[][];
scale: number;
backgrounds: string[];
pageSizes: Size[];
size: Size;
activeElements: HTMLElement[];
onEdit: (targets: HTMLElement[]) => void;
changeSchemas: ChangeSchemas;
removeSchemas: (ids: string[]) => void;
paperRefs: MutableRefObject;
sidebarOpen: boolean;
}
const Canvas = (props: Props, ref: Ref) => {
const {
basePdf,
pageCursor,
scale,
backgrounds,
pageSizes,
size,
activeElements,
schemasList,
hoveringSchemaId,
onEdit,
changeSchemas,
removeSchemas,
onChangeHoveringSchemaId,
paperRefs,
sidebarOpen,
} = props;
const { token } = theme.useToken();
const pluginsRegistry = useContext(PluginsRegistry);
const verticalGuides = useRef([]);
const horizontalGuides = useRef([]);
const moveable = useRef(null);
const [isPressShiftKey, setIsPressShiftKey] = useState(false);
const [editing, setEditing] = useState(false);
const prevSchemas = usePrevious(schemasList[pageCursor]);
const onKeydown = (e: KeyboardEvent) => {
if (e.shiftKey) setIsPressShiftKey(true);
};
const onKeyup = (e: KeyboardEvent) => {
if (e.key === 'Shift' || !e.shiftKey) setIsPressShiftKey(false);
if (e.key === 'Escape' || e.key === 'Esc') setEditing(false);
};
const initEvents = useCallback(() => {
window.addEventListener('keydown', onKeydown);
window.addEventListener('keyup', onKeyup);
}, []);
const destroyEvents = useCallback(() => {
window.removeEventListener('keydown', onKeydown);
window.removeEventListener('keyup', onKeyup);
}, []);
useEffect(() => {
initEvents();
return destroyEvents;
}, [initEvents, destroyEvents]);
useEffect(() => {
moveable.current?.updateRect();
if (!prevSchemas) {
return;
}
const prevSchemaKeys = JSON.stringify(prevSchemas[pageCursor] || {});
const schemaKeys = JSON.stringify(schemasList[pageCursor] || {});
if (prevSchemaKeys === schemaKeys) {
moveable.current?.updateRect();
}
}, [pageCursor, schemasList, prevSchemas]);
const onDrag = ({ target, top, left }: OnDrag) => {
const { width: _width, height: _height } = target.style;
const targetWidth = fmt(_width);
const targetHeight = fmt(_height);
const actualTop = top / ZOOM;
const actualLeft = left / ZOOM;
const { width: pageWidth, height: pageHeight } = pageSizes[pageCursor];
let topPadding = 0;
let rightPadding = 0;
let bottomPadding = 0;
let leftPadding = 0;
if (isBlankPdf(basePdf)) {
const [t, r, b, l] = basePdf.padding;
topPadding = t * ZOOM;
rightPadding = r;
bottomPadding = b;
leftPadding = l * ZOOM;
}
if (actualTop + targetHeight > pageHeight - bottomPadding) {
target.style.top = `${(pageHeight - targetHeight - bottomPadding) * ZOOM}px`;
} else {
target.style.top = `${top < topPadding ? topPadding : top}px`;
}
if (actualLeft + targetWidth > pageWidth - rightPadding) {
target.style.left = `${(pageWidth - targetWidth - rightPadding) * ZOOM}px`;
} else {
target.style.left = `${left < leftPadding ? leftPadding : left}px`;
}
};
const onDragEnd = ({ target }: { target: HTMLElement | SVGElement }) => {
const { top, left } = target.style;
changeSchemas([
{ key: 'position.y', value: fmt(top), schemaId: target.id },
{ key: 'position.x', value: fmt(left), schemaId: target.id },
]);
};
const onDragEnds = ({ targets }: { targets: (HTMLElement | SVGElement)[] }) => {
const arg = targets.map(({ style: { top, left }, id }) => [
{ key: 'position.y', value: fmt(top), schemaId: id },
{ key: 'position.x', value: fmt(left), schemaId: id },
]);
changeSchemas(flatten(arg));
};
const onRotate = ({ target, rotate }: OnRotate) => {
target.style.transform = `rotate(${rotate}deg)`;
};
const onRotateEnd = ({ target }: { target: HTMLElement | SVGElement }) => {
const { transform } = target.style;
const rotate = Number(transform.replace('rotate(', '').replace('deg)', ''));
const normalizedRotate = normalizeRotate(rotate);
changeSchemas([{ key: 'rotate', value: normalizedRotate, schemaId: target.id }]);
};
const onRotateEnds = ({ targets }: { targets: (HTMLElement | SVGElement)[] }) => {
const arg = targets.map(({ style: { transform }, id }) => {
const rotate = Number(transform.replace('rotate(', '').replace('deg)', ''));
const normalizedRotate = normalizeRotate(rotate);
return [{ key: 'rotate', value: normalizedRotate, schemaId: id }];
});
changeSchemas(flatten(arg));
};
const onResizeEnd = ({ target }: { target: HTMLElement | SVGElement }) => {
const { id, style } = target;
const { width, height, top, left } = style;
changeSchemas([
{ key: 'position.x', value: fmt(left), schemaId: id },
{ key: 'position.y', value: fmt(top), schemaId: id },
{ key: 'width', value: fmt(width), schemaId: id },
{ key: 'height', value: fmt(height), schemaId: id },
]);
const targetSchema = schemasList[pageCursor].find((schema) => schema.id === id);
if (!targetSchema) return;
targetSchema.position.x = fmt(left);
targetSchema.position.y = fmt(top);
targetSchema.width = fmt(width);
targetSchema.height = fmt(height);
};
const onResizeEnds = ({ targets }: { targets: (HTMLElement | SVGElement)[] }) => {
const arg = targets.map(({ style: { width, height, top, left }, id }) => [
{ key: 'width', value: fmt(width), schemaId: id },
{ key: 'height', value: fmt(height), schemaId: id },
{ key: 'position.y', value: fmt(top), schemaId: id },
{ key: 'position.x', value: fmt(left), schemaId: id },
]);
changeSchemas(flatten(arg));
};
const onResize = ({ target, width, height, direction }: OnResize) => {
if (!target) return;
let topPadding = 0;
let rightPadding = 0;
let bottomPadding = 0;
let leftPadding = 0;
if (isBlankPdf(basePdf)) {
const [t, r, b, l] = basePdf.padding;
topPadding = t * ZOOM;
rightPadding = mm2px(r);
bottomPadding = mm2px(b);
leftPadding = l * ZOOM;
}
const pageWidth = mm2px(pageSizes[pageCursor].width);
const pageHeight = mm2px(pageSizes[pageCursor].height);
const obj: { top?: string; left?: string; width: string; height: string } = {
width: `${width}px`,
height: `${height}px`,
};
const s = target.style;
let newLeft = fmt4Num(s.left) + (fmt4Num(s.width) - width);
let newTop = fmt4Num(s.top) + (fmt4Num(s.height) - height);
if (newLeft < leftPadding) {
newLeft = leftPadding;
}
if (newTop < topPadding) {
newTop = topPadding;
}
if (newLeft + width > pageWidth - rightPadding) {
obj.width = `${pageWidth - rightPadding - newLeft}px`;
}
if (newTop + height > pageHeight - bottomPadding) {
obj.height = `${pageHeight - bottomPadding - newTop}px`;
}
const d = direction.toString();
if (isTopLeftResize(d)) {
obj.top = `${newTop}px`;
obj.left = `${newLeft}px`;
} else if (d === '1,-1') {
obj.top = `${newTop}px`;
} else if (d === '-1,1') {
obj.left = `${newLeft}px`;
}
Object.assign(s, obj);
};
const getGuideLines = (guides: GuidesInterface[], index: number) =>
guides[index] && guides[index].getGuides().map((g) => g * ZOOM);
const onClickMoveable = (e: OnClick) => {
e.inputEvent.stopPropagation();
setEditing(true);
};
const rotatable = useMemo(() => {
const selectedSchemas = (schemasList[pageCursor] || []).filter((s) =>
activeElements.map((ae) => ae.id).includes(s.id)
);
const schemaTypes = selectedSchemas.map((s) => s.type);
const uniqueSchemaTypes = [...new Set(schemaTypes)];
const defaultSchemas = Object.values(pluginsRegistry).map(
(plugin) => plugin?.propPanel.defaultSchema
);
return uniqueSchemaTypes.every(
(type) => defaultSchemas.find((ds) => ds.type === type)?.rotate !== undefined
);
}, [activeElements, pageCursor, schemasList, pluginsRegistry]);
return (
{
const { inputEvent } = e;
const isMoveableElement = moveable.current?.isMoveableElement(inputEvent.target);
if ((inputEvent.type === 'touchstart' && e.isTrusted) || isMoveableElement) {
e.stop();
}
if (paperRefs.current[pageCursor] === inputEvent.target) {
onEdit([]);
}
if (inputEvent.target?.id === DELETE_BTN_ID) {
removeSchemas(activeElements.map((ae) => ae.id));
}
}}
onSelect={({ added, removed, selected, inputEvent }) => {
const isClick = inputEvent.type === 'mousedown';
let newActiveElements: HTMLElement[] = isClick ? (selected as HTMLElement[]) : [];
if (!isClick && added.length > 0) {
newActiveElements = activeElements.concat(added as HTMLElement[]);
}
if (!isClick && removed.length > 0) {
newActiveElements = activeElements.filter((ae) => !removed.includes(ae));
}
onEdit(newActiveElements);
if (newActiveElements != activeElements) {
setEditing(false);
}
// For MacOS CMD+SHIFT+3/4 screenshots where the keydown event is never received, check mouse too
if (!inputEvent.shiftKey) {
setIsPressShiftKey(false);
}
}}
/>
(
<>
{!editing && activeElements.length > 0 && pageCursor === index && (
)}
{
if (e) horizontalGuides.current[index] = e;
}}
verticalRef={(e) => {
if (e) verticalGuides.current[index] = e;
}}
/>
{pageCursor !== index ? (
) : (
!editing && (
)
)}
>
)}
renderSchema={({ schema }) => (
ae.id).includes(schema.id)
? 'designer'
: 'viewer'
}
onChange={(arg) => {
const args = Array.isArray(arg) ? arg : [arg];
changeSchemas(args.map(({ key, value }) => ({ key, value, schemaId: schema.id })));
}}
stopEditing={() => setEditing(false)}
outline={`1px ${hoveringSchemaId === schema.id ? 'solid' : 'dashed'} ${
schema.readOnly && hoveringSchemaId !== schema.id ? 'transparent' : token.colorPrimary
}`}
scale={scale}
/>
)}
/>
);
};
export default forwardRef(Canvas);