App mitosis begins (#1047)
This commit is contained in:
parent
cdf11809dc
commit
ba3cec8d0d
@ -167,6 +167,84 @@ export class App extends React.Component<any, AppState> {
|
||||
this.actionManager.registerAction(createRedoAction(history));
|
||||
}
|
||||
|
||||
public render() {
|
||||
const canvasDOMWidth = window.innerWidth;
|
||||
const canvasDOMHeight = window.innerHeight;
|
||||
|
||||
const canvasScale = window.devicePixelRatio;
|
||||
|
||||
const canvasWidth = canvasDOMWidth * canvasScale;
|
||||
const canvasHeight = canvasDOMHeight * canvasScale;
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<LayerUI
|
||||
canvas={this.canvas}
|
||||
appState={this.state}
|
||||
setAppState={this.setAppState}
|
||||
actionManager={this.actionManager}
|
||||
elements={globalSceneState.getAllElements().filter(element => {
|
||||
return !element.isDeleted;
|
||||
})}
|
||||
setElements={this.setElements}
|
||||
language={getLanguage()}
|
||||
onRoomCreate={this.createRoom}
|
||||
onRoomDestroy={this.destroyRoom}
|
||||
onToggleLock={this.toggleLock}
|
||||
/>
|
||||
<main>
|
||||
<canvas
|
||||
id="canvas"
|
||||
style={{
|
||||
width: canvasDOMWidth,
|
||||
height: canvasDOMHeight,
|
||||
}}
|
||||
width={canvasWidth}
|
||||
height={canvasHeight}
|
||||
ref={canvas => {
|
||||
// canvas is null when unmounting
|
||||
if (canvas !== null) {
|
||||
this.canvas = canvas;
|
||||
this.rc = rough.canvas(this.canvas);
|
||||
|
||||
this.canvas.addEventListener("wheel", this.handleWheel, {
|
||||
passive: false,
|
||||
});
|
||||
} else {
|
||||
this.canvas?.removeEventListener("wheel", this.handleWheel);
|
||||
}
|
||||
}}
|
||||
onContextMenu={this.handleCanvasContextMenu}
|
||||
onPointerDown={this.handleCanvasPointerDown}
|
||||
onDoubleClick={this.handleCanvasDoubleClick}
|
||||
onPointerMove={this.handleCanvasPointerMove}
|
||||
onPointerUp={this.removePointer}
|
||||
onPointerCancel={this.removePointer}
|
||||
onDrop={event => {
|
||||
const file = event.dataTransfer.files[0];
|
||||
if (
|
||||
file?.type === "application/json" ||
|
||||
file?.name.endsWith(".excalidraw")
|
||||
) {
|
||||
loadFromBlob(file)
|
||||
.then(({ elements, appState }) =>
|
||||
this.syncActionResult({
|
||||
elements,
|
||||
appState,
|
||||
commitToHistory: false,
|
||||
}),
|
||||
)
|
||||
.catch(error => console.error(error));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("labels.drawingCanvas")}
|
||||
</canvas>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private syncActionResult = withBatchedUpdates((res: ActionResult) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
@ -190,6 +268,214 @@ export class App extends React.Component<any, AppState> {
|
||||
}
|
||||
});
|
||||
|
||||
// Lifecycle
|
||||
|
||||
private onUnload = withBatchedUpdates(() => {
|
||||
isHoldingSpace = false;
|
||||
this.saveDebounced();
|
||||
this.saveDebounced.flush();
|
||||
});
|
||||
|
||||
private disableEvent: EventHandlerNonNull = event => {
|
||||
event.preventDefault();
|
||||
};
|
||||
private unmounted = false;
|
||||
|
||||
public async componentDidMount() {
|
||||
if (
|
||||
process.env.NODE_ENV === "test" ||
|
||||
process.env.NODE_ENV === "development"
|
||||
) {
|
||||
const setState = this.setState.bind(this);
|
||||
Object.defineProperties(window.h, {
|
||||
state: {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
return this.state;
|
||||
},
|
||||
},
|
||||
setState: {
|
||||
configurable: true,
|
||||
value: (...args: Parameters<typeof setState>) => {
|
||||
return this.setState(...args);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.removeSceneCallback = globalSceneState.addCallback(
|
||||
this.onSceneUpdated,
|
||||
);
|
||||
|
||||
document.addEventListener("copy", this.onCopy);
|
||||
document.addEventListener("paste", this.pasteFromClipboard);
|
||||
document.addEventListener("cut", this.onCut);
|
||||
|
||||
document.addEventListener("keydown", this.onKeyDown, false);
|
||||
document.addEventListener("keyup", this.onKeyUp, { passive: true });
|
||||
document.addEventListener("mousemove", this.updateCurrentCursorPosition);
|
||||
window.addEventListener("resize", this.onResize, false);
|
||||
window.addEventListener("unload", this.onUnload, false);
|
||||
window.addEventListener("blur", this.onUnload, false);
|
||||
window.addEventListener("dragover", this.disableEvent, false);
|
||||
window.addEventListener("drop", this.disableEvent, false);
|
||||
|
||||
// Safari-only desktop pinch zoom
|
||||
document.addEventListener(
|
||||
"gesturestart",
|
||||
this.onGestureStart as any,
|
||||
false,
|
||||
);
|
||||
document.addEventListener(
|
||||
"gesturechange",
|
||||
this.onGestureChange as any,
|
||||
false,
|
||||
);
|
||||
document.addEventListener("gestureend", this.onGestureEnd as any, false);
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const id = searchParams.get("id");
|
||||
|
||||
if (id) {
|
||||
// Backwards compatibility with legacy url format
|
||||
const scene = await loadScene(id);
|
||||
this.syncActionResult(scene);
|
||||
}
|
||||
|
||||
const jsonMatch = window.location.hash.match(
|
||||
/^#json=([0-9]+),([a-zA-Z0-9_-]+)$/,
|
||||
);
|
||||
if (jsonMatch) {
|
||||
const scene = await loadScene(jsonMatch[1], jsonMatch[2]);
|
||||
this.syncActionResult(scene);
|
||||
return;
|
||||
}
|
||||
|
||||
const roomMatch = getCollaborationLinkData(window.location.href);
|
||||
if (roomMatch) {
|
||||
this.initializeSocketClient();
|
||||
return;
|
||||
}
|
||||
const scene = await loadScene(null);
|
||||
this.syncActionResult(scene);
|
||||
|
||||
window.addEventListener("beforeunload", this.beforeUnload);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.removeSceneCallback!();
|
||||
|
||||
document.removeEventListener("copy", this.onCopy);
|
||||
document.removeEventListener("paste", this.pasteFromClipboard);
|
||||
document.removeEventListener("cut", this.onCut);
|
||||
|
||||
document.removeEventListener("keydown", this.onKeyDown, false);
|
||||
document.removeEventListener(
|
||||
"mousemove",
|
||||
this.updateCurrentCursorPosition,
|
||||
false,
|
||||
);
|
||||
document.removeEventListener("keyup", this.onKeyUp);
|
||||
window.removeEventListener("resize", this.onResize, false);
|
||||
window.removeEventListener("unload", this.onUnload, false);
|
||||
window.removeEventListener("blur", this.onUnload, false);
|
||||
window.removeEventListener("dragover", this.disableEvent, false);
|
||||
window.removeEventListener("drop", this.disableEvent, false);
|
||||
|
||||
document.removeEventListener(
|
||||
"gesturestart",
|
||||
this.onGestureStart as any,
|
||||
false,
|
||||
);
|
||||
document.removeEventListener(
|
||||
"gesturechange",
|
||||
this.onGestureChange as any,
|
||||
false,
|
||||
);
|
||||
document.removeEventListener("gestureend", this.onGestureEnd as any, false);
|
||||
window.removeEventListener("beforeunload", this.beforeUnload);
|
||||
}
|
||||
private onResize = withBatchedUpdates(() => {
|
||||
globalSceneState
|
||||
.getAllElements()
|
||||
.forEach(element => invalidateShapeForElement(element));
|
||||
this.setState({});
|
||||
});
|
||||
|
||||
private beforeUnload = withBatchedUpdates((event: BeforeUnloadEvent) => {
|
||||
if (
|
||||
this.state.isCollaborating &&
|
||||
hasNonDeletedElements(globalSceneState.getAllElements())
|
||||
) {
|
||||
event.preventDefault();
|
||||
// NOTE: modern browsers no longer allow showing a custom message here
|
||||
event.returnValue = "";
|
||||
}
|
||||
});
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.state.isCollaborating && !this.socket) {
|
||||
this.initializeSocketClient();
|
||||
}
|
||||
const pointerViewportCoords: {
|
||||
[id: string]: { x: number; y: number };
|
||||
} = {};
|
||||
this.state.collaborators.forEach((user, socketID) => {
|
||||
if (!user.pointer) {
|
||||
return;
|
||||
}
|
||||
pointerViewportCoords[socketID] = sceneCoordsToViewportCoords(
|
||||
{
|
||||
sceneX: user.pointer.x,
|
||||
sceneY: user.pointer.y,
|
||||
},
|
||||
this.state,
|
||||
this.canvas,
|
||||
window.devicePixelRatio,
|
||||
);
|
||||
});
|
||||
const { atLeastOneVisibleElement, scrollBars } = renderScene(
|
||||
globalSceneState.getAllElements(),
|
||||
this.state,
|
||||
this.state.selectionElement,
|
||||
window.devicePixelRatio,
|
||||
this.rc!,
|
||||
this.canvas!,
|
||||
{
|
||||
scrollX: this.state.scrollX,
|
||||
scrollY: this.state.scrollY,
|
||||
viewBackgroundColor: this.state.viewBackgroundColor,
|
||||
zoom: this.state.zoom,
|
||||
remotePointerViewportCoords: pointerViewportCoords,
|
||||
},
|
||||
{
|
||||
renderOptimizations: true,
|
||||
},
|
||||
);
|
||||
if (scrollBars) {
|
||||
currentScrollBars = scrollBars;
|
||||
}
|
||||
const scrolledOutside =
|
||||
!atLeastOneVisibleElement &&
|
||||
hasNonDeletedElements(globalSceneState.getAllElements());
|
||||
if (this.state.scrolledOutside !== scrolledOutside) {
|
||||
this.setState({ scrolledOutside: scrolledOutside });
|
||||
}
|
||||
this.saveDebounced();
|
||||
|
||||
if (
|
||||
getDrawingVersion(globalSceneState.getAllElements()) >
|
||||
this.lastBroadcastedOrReceivedSceneVersion
|
||||
) {
|
||||
this.broadcastSceneUpdate();
|
||||
}
|
||||
|
||||
history.record(this.state, globalSceneState.getAllElements());
|
||||
}
|
||||
|
||||
// Copy/paste
|
||||
|
||||
private onCut = withBatchedUpdates((event: ClipboardEvent) => {
|
||||
if (isWritableElement(event.target)) {
|
||||
return;
|
||||
@ -212,15 +498,144 @@ export class App extends React.Component<any, AppState> {
|
||||
copyToAppClipboard(globalSceneState.getAllElements(), this.state);
|
||||
event.preventDefault();
|
||||
});
|
||||
private copyToAppClipboard = () => {
|
||||
copyToAppClipboard(globalSceneState.getAllElements(), this.state);
|
||||
};
|
||||
|
||||
private onUnload = withBatchedUpdates(() => {
|
||||
isHoldingSpace = false;
|
||||
this.saveDebounced();
|
||||
this.saveDebounced.flush();
|
||||
});
|
||||
private copyToClipboardAsPng = () => {
|
||||
const selectedElements = getSelectedElements(
|
||||
globalSceneState.getAllElements(),
|
||||
this.state,
|
||||
);
|
||||
exportCanvas(
|
||||
"clipboard",
|
||||
selectedElements.length
|
||||
? selectedElements
|
||||
: globalSceneState.getAllElements(),
|
||||
this.state,
|
||||
this.canvas!,
|
||||
this.state,
|
||||
);
|
||||
};
|
||||
|
||||
private disableEvent: EventHandlerNonNull = event => {
|
||||
event.preventDefault();
|
||||
private pasteFromClipboard = withBatchedUpdates(
|
||||
async (event: ClipboardEvent | null) => {
|
||||
// #686
|
||||
const target = document.activeElement;
|
||||
const elementUnderCursor = document.elementFromPoint(cursorX, cursorY);
|
||||
if (
|
||||
// if no ClipboardEvent supplied, assume we're pasting via contextMenu
|
||||
// thus these checks don't make sense
|
||||
!event ||
|
||||
(elementUnderCursor instanceof HTMLCanvasElement &&
|
||||
!isWritableElement(target))
|
||||
) {
|
||||
const data = await getClipboardContent(event);
|
||||
if (data.elements) {
|
||||
this.addElementsFromPaste(data.elements);
|
||||
} else if (data.text) {
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
{ clientX: cursorX, clientY: cursorY },
|
||||
this.state,
|
||||
this.canvas,
|
||||
window.devicePixelRatio,
|
||||
);
|
||||
|
||||
const element = newTextElement({
|
||||
x: x,
|
||||
y: y,
|
||||
strokeColor: this.state.currentItemStrokeColor,
|
||||
backgroundColor: this.state.currentItemBackgroundColor,
|
||||
fillStyle: this.state.currentItemFillStyle,
|
||||
strokeWidth: this.state.currentItemStrokeWidth,
|
||||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
text: data.text,
|
||||
font: this.state.currentItemFont,
|
||||
});
|
||||
|
||||
globalSceneState.replaceAllElements([
|
||||
...globalSceneState.getAllElements(),
|
||||
element,
|
||||
]);
|
||||
this.setState({ selectedElementIds: { [element.id]: true } });
|
||||
history.resumeRecording();
|
||||
}
|
||||
this.selectShapeTool("selection");
|
||||
event?.preventDefault();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
private addElementsFromPaste = (
|
||||
clipboardElements: readonly ExcalidrawElement[],
|
||||
) => {
|
||||
const [minX, minY, maxX, maxY] = getCommonBounds(clipboardElements);
|
||||
|
||||
const elementsCenterX = distance(minX, maxX) / 2;
|
||||
const elementsCenterY = distance(minY, maxY) / 2;
|
||||
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
{ clientX: cursorX, clientY: cursorY },
|
||||
this.state,
|
||||
this.canvas,
|
||||
window.devicePixelRatio,
|
||||
);
|
||||
|
||||
const dx = x - elementsCenterX;
|
||||
const dy = y - elementsCenterY;
|
||||
|
||||
const newElements = clipboardElements.map(element =>
|
||||
duplicateElement(element, {
|
||||
x: element.x + dx - minX,
|
||||
y: element.y + dy - minY,
|
||||
}),
|
||||
);
|
||||
|
||||
globalSceneState.replaceAllElements([
|
||||
...globalSceneState.getAllElements(),
|
||||
...newElements,
|
||||
]);
|
||||
history.resumeRecording();
|
||||
this.setState({
|
||||
selectedElementIds: newElements.reduce((map, element) => {
|
||||
map[element.id] = true;
|
||||
return map;
|
||||
}, {} as any),
|
||||
});
|
||||
};
|
||||
|
||||
// Collaboration
|
||||
|
||||
setAppState = (obj: any) => {
|
||||
this.setState(obj);
|
||||
};
|
||||
|
||||
removePointer = (event: React.PointerEvent<HTMLElement>) => {
|
||||
gesture.pointers.delete(event.pointerId);
|
||||
};
|
||||
|
||||
createRoom = async () => {
|
||||
window.history.pushState(
|
||||
{},
|
||||
"Excalidraw",
|
||||
await generateCollaborationLink(),
|
||||
);
|
||||
this.initializeSocketClient();
|
||||
};
|
||||
|
||||
destroyRoom = () => {
|
||||
window.history.pushState({}, "Excalidraw", window.location.origin);
|
||||
this.destroySocketClient();
|
||||
};
|
||||
|
||||
toggleLock = () => {
|
||||
this.setState(prevState => ({
|
||||
elementLocked: !prevState.elementLocked,
|
||||
elementType: prevState.elementLocked
|
||||
? "selection"
|
||||
: prevState.elementType,
|
||||
}));
|
||||
};
|
||||
|
||||
private destroySocketClient = () => {
|
||||
@ -448,132 +863,8 @@ export class App extends React.Component<any, AppState> {
|
||||
this.setState({});
|
||||
};
|
||||
|
||||
private unmounted = false;
|
||||
public async componentDidMount() {
|
||||
if (
|
||||
process.env.NODE_ENV === "test" ||
|
||||
process.env.NODE_ENV === "development"
|
||||
) {
|
||||
const setState = this.setState.bind(this);
|
||||
Object.defineProperties(window.h, {
|
||||
state: {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
return this.state;
|
||||
},
|
||||
},
|
||||
setState: {
|
||||
configurable: true,
|
||||
value: (...args: Parameters<typeof setState>) => {
|
||||
return this.setState(...args);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.removeSceneCallback = globalSceneState.addCallback(
|
||||
this.onSceneUpdated,
|
||||
);
|
||||
|
||||
document.addEventListener("copy", this.onCopy);
|
||||
document.addEventListener("paste", this.pasteFromClipboard);
|
||||
document.addEventListener("cut", this.onCut);
|
||||
|
||||
document.addEventListener("keydown", this.onKeyDown, false);
|
||||
document.addEventListener("keyup", this.onKeyUp, { passive: true });
|
||||
document.addEventListener("mousemove", this.updateCurrentCursorPosition);
|
||||
window.addEventListener("resize", this.onResize, false);
|
||||
window.addEventListener("unload", this.onUnload, false);
|
||||
window.addEventListener("blur", this.onUnload, false);
|
||||
window.addEventListener("dragover", this.disableEvent, false);
|
||||
window.addEventListener("drop", this.disableEvent, false);
|
||||
|
||||
// Safari-only desktop pinch zoom
|
||||
document.addEventListener(
|
||||
"gesturestart",
|
||||
this.onGestureStart as any,
|
||||
false,
|
||||
);
|
||||
document.addEventListener(
|
||||
"gesturechange",
|
||||
this.onGestureChange as any,
|
||||
false,
|
||||
);
|
||||
document.addEventListener("gestureend", this.onGestureEnd as any, false);
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const id = searchParams.get("id");
|
||||
|
||||
if (id) {
|
||||
// Backwards compatibility with legacy url format
|
||||
const scene = await loadScene(id);
|
||||
this.syncActionResult(scene);
|
||||
}
|
||||
|
||||
const jsonMatch = window.location.hash.match(
|
||||
/^#json=([0-9]+),([a-zA-Z0-9_-]+)$/,
|
||||
);
|
||||
if (jsonMatch) {
|
||||
const scene = await loadScene(jsonMatch[1], jsonMatch[2]);
|
||||
this.syncActionResult(scene);
|
||||
return;
|
||||
}
|
||||
|
||||
const roomMatch = getCollaborationLinkData(window.location.href);
|
||||
if (roomMatch) {
|
||||
this.initializeSocketClient();
|
||||
return;
|
||||
}
|
||||
const scene = await loadScene(null);
|
||||
this.syncActionResult(scene);
|
||||
|
||||
window.addEventListener("beforeunload", this.beforeUnload);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.removeSceneCallback!();
|
||||
|
||||
document.removeEventListener("copy", this.onCopy);
|
||||
document.removeEventListener("paste", this.pasteFromClipboard);
|
||||
document.removeEventListener("cut", this.onCut);
|
||||
|
||||
document.removeEventListener("keydown", this.onKeyDown, false);
|
||||
document.removeEventListener(
|
||||
"mousemove",
|
||||
this.updateCurrentCursorPosition,
|
||||
false,
|
||||
);
|
||||
document.removeEventListener("keyup", this.onKeyUp);
|
||||
window.removeEventListener("resize", this.onResize, false);
|
||||
window.removeEventListener("unload", this.onUnload, false);
|
||||
window.removeEventListener("blur", this.onUnload, false);
|
||||
window.removeEventListener("dragover", this.disableEvent, false);
|
||||
window.removeEventListener("drop", this.disableEvent, false);
|
||||
|
||||
document.removeEventListener(
|
||||
"gesturestart",
|
||||
this.onGestureStart as any,
|
||||
false,
|
||||
);
|
||||
document.removeEventListener(
|
||||
"gesturechange",
|
||||
this.onGestureChange as any,
|
||||
false,
|
||||
);
|
||||
document.removeEventListener("gestureend", this.onGestureEnd as any, false);
|
||||
window.removeEventListener("beforeunload", this.beforeUnload);
|
||||
}
|
||||
|
||||
public state: AppState = getDefaultAppState();
|
||||
|
||||
private onResize = withBatchedUpdates(() => {
|
||||
globalSceneState
|
||||
.getAllElements()
|
||||
.forEach(element => invalidateShapeForElement(element));
|
||||
this.setState({});
|
||||
});
|
||||
|
||||
private updateCurrentCursorPosition = withBatchedUpdates(
|
||||
(event: MouseEvent) => {
|
||||
cursorX = event.x;
|
||||
@ -581,6 +872,8 @@ export class App extends React.Component<any, AppState> {
|
||||
},
|
||||
);
|
||||
|
||||
// Input handling
|
||||
|
||||
private onKeyDown = withBatchedUpdates((event: KeyboardEvent) => {
|
||||
if (
|
||||
(isWritableElement(event.target) && event.key !== KEYS.ESCAPE) ||
|
||||
@ -658,75 +951,6 @@ export class App extends React.Component<any, AppState> {
|
||||
}
|
||||
});
|
||||
|
||||
private copyToAppClipboard = () => {
|
||||
copyToAppClipboard(globalSceneState.getAllElements(), this.state);
|
||||
};
|
||||
|
||||
private copyToClipboardAsPng = () => {
|
||||
const selectedElements = getSelectedElements(
|
||||
globalSceneState.getAllElements(),
|
||||
this.state,
|
||||
);
|
||||
exportCanvas(
|
||||
"clipboard",
|
||||
selectedElements.length
|
||||
? selectedElements
|
||||
: globalSceneState.getAllElements(),
|
||||
this.state,
|
||||
this.canvas!,
|
||||
this.state,
|
||||
);
|
||||
};
|
||||
|
||||
private pasteFromClipboard = withBatchedUpdates(
|
||||
async (event: ClipboardEvent | null) => {
|
||||
// #686
|
||||
const target = document.activeElement;
|
||||
const elementUnderCursor = document.elementFromPoint(cursorX, cursorY);
|
||||
if (
|
||||
// if no ClipboardEvent supplied, assume we're pasting via contextMenu
|
||||
// thus these checks don't make sense
|
||||
!event ||
|
||||
(elementUnderCursor instanceof HTMLCanvasElement &&
|
||||
!isWritableElement(target))
|
||||
) {
|
||||
const data = await getClipboardContent(event);
|
||||
if (data.elements) {
|
||||
this.addElementsFromPaste(data.elements);
|
||||
} else if (data.text) {
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
{ clientX: cursorX, clientY: cursorY },
|
||||
this.state,
|
||||
this.canvas,
|
||||
window.devicePixelRatio,
|
||||
);
|
||||
|
||||
const element = newTextElement({
|
||||
x: x,
|
||||
y: y,
|
||||
strokeColor: this.state.currentItemStrokeColor,
|
||||
backgroundColor: this.state.currentItemBackgroundColor,
|
||||
fillStyle: this.state.currentItemFillStyle,
|
||||
strokeWidth: this.state.currentItemStrokeWidth,
|
||||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
text: data.text,
|
||||
font: this.state.currentItemFont,
|
||||
});
|
||||
|
||||
globalSceneState.replaceAllElements([
|
||||
...globalSceneState.getAllElements(),
|
||||
element,
|
||||
]);
|
||||
this.setState({ selectedElementIds: { [element.id]: true } });
|
||||
history.resumeRecording();
|
||||
}
|
||||
this.selectShapeTool("selection");
|
||||
event?.preventDefault();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
private selectShapeTool(elementType: AppState["elementType"]) {
|
||||
if (!isHoldingSpace) {
|
||||
setCursorForShape(elementType);
|
||||
@ -759,119 +983,10 @@ export class App extends React.Component<any, AppState> {
|
||||
gesture.initialScale = null;
|
||||
});
|
||||
|
||||
setAppState = (obj: any) => {
|
||||
this.setState(obj);
|
||||
};
|
||||
|
||||
removePointer = (event: React.PointerEvent<HTMLElement>) => {
|
||||
gesture.pointers.delete(event.pointerId);
|
||||
};
|
||||
|
||||
createRoom = async () => {
|
||||
window.history.pushState(
|
||||
{},
|
||||
"Excalidraw",
|
||||
await generateCollaborationLink(),
|
||||
);
|
||||
this.initializeSocketClient();
|
||||
};
|
||||
|
||||
destroyRoom = () => {
|
||||
window.history.pushState({}, "Excalidraw", window.location.origin);
|
||||
this.destroySocketClient();
|
||||
};
|
||||
|
||||
toggleLock = () => {
|
||||
this.setState(prevState => ({
|
||||
elementLocked: !prevState.elementLocked,
|
||||
elementType: prevState.elementLocked
|
||||
? "selection"
|
||||
: prevState.elementType,
|
||||
}));
|
||||
};
|
||||
|
||||
private setElements = (elements: readonly ExcalidrawElement[]) => {
|
||||
globalSceneState.replaceAllElements(elements);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const canvasDOMWidth = window.innerWidth;
|
||||
const canvasDOMHeight = window.innerHeight;
|
||||
|
||||
const canvasScale = window.devicePixelRatio;
|
||||
|
||||
const canvasWidth = canvasDOMWidth * canvasScale;
|
||||
const canvasHeight = canvasDOMHeight * canvasScale;
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<LayerUI
|
||||
canvas={this.canvas}
|
||||
appState={this.state}
|
||||
setAppState={this.setAppState}
|
||||
actionManager={this.actionManager}
|
||||
elements={globalSceneState.getAllElements().filter(element => {
|
||||
return !element.isDeleted;
|
||||
})}
|
||||
setElements={this.setElements}
|
||||
language={getLanguage()}
|
||||
onRoomCreate={this.createRoom}
|
||||
onRoomDestroy={this.destroyRoom}
|
||||
onToggleLock={this.toggleLock}
|
||||
/>
|
||||
<main>
|
||||
<canvas
|
||||
id="canvas"
|
||||
style={{
|
||||
width: canvasDOMWidth,
|
||||
height: canvasDOMHeight,
|
||||
}}
|
||||
width={canvasWidth}
|
||||
height={canvasHeight}
|
||||
ref={canvas => {
|
||||
// canvas is null when unmounting
|
||||
if (canvas !== null) {
|
||||
this.canvas = canvas;
|
||||
this.rc = rough.canvas(this.canvas);
|
||||
|
||||
this.canvas.addEventListener("wheel", this.handleWheel, {
|
||||
passive: false,
|
||||
});
|
||||
} else {
|
||||
this.canvas?.removeEventListener("wheel", this.handleWheel);
|
||||
}
|
||||
}}
|
||||
onContextMenu={this.handleCanvasContextMenu}
|
||||
onPointerDown={this.handleCanvasPointerDown}
|
||||
onDoubleClick={this.handleCanvasDoubleClick}
|
||||
onPointerMove={this.handleCanvasPointerMove}
|
||||
onPointerUp={this.removePointer}
|
||||
onPointerCancel={this.removePointer}
|
||||
onDrop={event => {
|
||||
const file = event.dataTransfer.files[0];
|
||||
if (
|
||||
file?.type === "application/json" ||
|
||||
file?.name.endsWith(".excalidraw")
|
||||
) {
|
||||
loadFromBlob(file)
|
||||
.then(({ elements, appState }) =>
|
||||
this.syncActionResult({
|
||||
elements,
|
||||
appState,
|
||||
commitToHistory: false,
|
||||
}),
|
||||
)
|
||||
.catch(error => console.error(error));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("labels.drawingCanvas")}
|
||||
</canvas>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private handleCanvasDoubleClick = (
|
||||
event: React.MouseEvent<HTMLCanvasElement>,
|
||||
) => {
|
||||
@ -2287,55 +2402,6 @@ export class App extends React.Component<any, AppState> {
|
||||
}));
|
||||
});
|
||||
|
||||
private beforeUnload = withBatchedUpdates((event: BeforeUnloadEvent) => {
|
||||
if (
|
||||
this.state.isCollaborating &&
|
||||
hasNonDeletedElements(globalSceneState.getAllElements())
|
||||
) {
|
||||
event.preventDefault();
|
||||
// NOTE: modern browsers no longer allow showing a custom message here
|
||||
event.returnValue = "";
|
||||
}
|
||||
});
|
||||
|
||||
private addElementsFromPaste = (
|
||||
clipboardElements: readonly ExcalidrawElement[],
|
||||
) => {
|
||||
const [minX, minY, maxX, maxY] = getCommonBounds(clipboardElements);
|
||||
|
||||
const elementsCenterX = distance(minX, maxX) / 2;
|
||||
const elementsCenterY = distance(minY, maxY) / 2;
|
||||
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
{ clientX: cursorX, clientY: cursorY },
|
||||
this.state,
|
||||
this.canvas,
|
||||
window.devicePixelRatio,
|
||||
);
|
||||
|
||||
const dx = x - elementsCenterX;
|
||||
const dy = y - elementsCenterY;
|
||||
|
||||
const newElements = clipboardElements.map(element =>
|
||||
duplicateElement(element, {
|
||||
x: element.x + dx - minX,
|
||||
y: element.y + dy - minY,
|
||||
}),
|
||||
);
|
||||
|
||||
globalSceneState.replaceAllElements([
|
||||
...globalSceneState.getAllElements(),
|
||||
...newElements,
|
||||
]);
|
||||
history.resumeRecording();
|
||||
this.setState({
|
||||
selectedElementIds: newElements.reduce((map, element) => {
|
||||
map[element.id] = true;
|
||||
return map;
|
||||
}, {} as any),
|
||||
});
|
||||
};
|
||||
|
||||
private getTextWysiwygSnappedToCenterPosition(x: number, y: number) {
|
||||
const elementClickedInside = getElementContainingPosition(
|
||||
globalSceneState.getAllElements(),
|
||||
@ -2378,66 +2444,6 @@ export class App extends React.Component<any, AppState> {
|
||||
private saveDebounced = debounce(() => {
|
||||
saveToLocalStorage(globalSceneState.getAllElements(), this.state);
|
||||
}, 300);
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.state.isCollaborating && !this.socket) {
|
||||
this.initializeSocketClient();
|
||||
}
|
||||
const pointerViewportCoords: {
|
||||
[id: string]: { x: number; y: number };
|
||||
} = {};
|
||||
this.state.collaborators.forEach((user, socketID) => {
|
||||
if (!user.pointer) {
|
||||
return;
|
||||
}
|
||||
pointerViewportCoords[socketID] = sceneCoordsToViewportCoords(
|
||||
{
|
||||
sceneX: user.pointer.x,
|
||||
sceneY: user.pointer.y,
|
||||
},
|
||||
this.state,
|
||||
this.canvas,
|
||||
window.devicePixelRatio,
|
||||
);
|
||||
});
|
||||
const { atLeastOneVisibleElement, scrollBars } = renderScene(
|
||||
globalSceneState.getAllElements(),
|
||||
this.state,
|
||||
this.state.selectionElement,
|
||||
window.devicePixelRatio,
|
||||
this.rc!,
|
||||
this.canvas!,
|
||||
{
|
||||
scrollX: this.state.scrollX,
|
||||
scrollY: this.state.scrollY,
|
||||
viewBackgroundColor: this.state.viewBackgroundColor,
|
||||
zoom: this.state.zoom,
|
||||
remotePointerViewportCoords: pointerViewportCoords,
|
||||
},
|
||||
{
|
||||
renderOptimizations: true,
|
||||
},
|
||||
);
|
||||
if (scrollBars) {
|
||||
currentScrollBars = scrollBars;
|
||||
}
|
||||
const scrolledOutside =
|
||||
!atLeastOneVisibleElement &&
|
||||
hasNonDeletedElements(globalSceneState.getAllElements());
|
||||
if (this.state.scrolledOutside !== scrolledOutside) {
|
||||
this.setState({ scrolledOutside: scrolledOutside });
|
||||
}
|
||||
this.saveDebounced();
|
||||
|
||||
if (
|
||||
getDrawingVersion(globalSceneState.getAllElements()) >
|
||||
this.lastBroadcastedOrReceivedSceneVersion
|
||||
) {
|
||||
this.broadcastSceneUpdate();
|
||||
}
|
||||
|
||||
history.record(this.state, globalSceneState.getAllElements());
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
Loading…
x
Reference in New Issue
Block a user