App mitosis begins (#1047)

This commit is contained in:
Kent Beck 2020-03-22 10:24:50 -07:00 committed by GitHub
parent cdf11809dc
commit ba3cec8d0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -167,6 +167,84 @@ export class App extends React.Component<any, AppState> {
this.actionManager.registerAction(createRedoAction(history)); 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) => { private syncActionResult = withBatchedUpdates((res: ActionResult) => {
if (this.unmounted) { if (this.unmounted) {
return; 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) => { private onCut = withBatchedUpdates((event: ClipboardEvent) => {
if (isWritableElement(event.target)) { if (isWritableElement(event.target)) {
return; return;
@ -212,15 +498,144 @@ export class App extends React.Component<any, AppState> {
copyToAppClipboard(globalSceneState.getAllElements(), this.state); copyToAppClipboard(globalSceneState.getAllElements(), this.state);
event.preventDefault(); event.preventDefault();
}); });
private copyToAppClipboard = () => {
copyToAppClipboard(globalSceneState.getAllElements(), this.state);
};
private onUnload = withBatchedUpdates(() => { private copyToClipboardAsPng = () => {
isHoldingSpace = false; const selectedElements = getSelectedElements(
this.saveDebounced(); globalSceneState.getAllElements(),
this.saveDebounced.flush(); 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,
}); });
private disableEvent: EventHandlerNonNull = event => { globalSceneState.replaceAllElements([
event.preventDefault(); ...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 = () => { private destroySocketClient = () => {
@ -448,132 +863,8 @@ export class App extends React.Component<any, AppState> {
this.setState({}); 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(); public state: AppState = getDefaultAppState();
private onResize = withBatchedUpdates(() => {
globalSceneState
.getAllElements()
.forEach(element => invalidateShapeForElement(element));
this.setState({});
});
private updateCurrentCursorPosition = withBatchedUpdates( private updateCurrentCursorPosition = withBatchedUpdates(
(event: MouseEvent) => { (event: MouseEvent) => {
cursorX = event.x; cursorX = event.x;
@ -581,6 +872,8 @@ export class App extends React.Component<any, AppState> {
}, },
); );
// Input handling
private onKeyDown = withBatchedUpdates((event: KeyboardEvent) => { private onKeyDown = withBatchedUpdates((event: KeyboardEvent) => {
if ( if (
(isWritableElement(event.target) && event.key !== KEYS.ESCAPE) || (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"]) { private selectShapeTool(elementType: AppState["elementType"]) {
if (!isHoldingSpace) { if (!isHoldingSpace) {
setCursorForShape(elementType); setCursorForShape(elementType);
@ -759,119 +983,10 @@ export class App extends React.Component<any, AppState> {
gesture.initialScale = null; 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[]) => { private setElements = (elements: readonly ExcalidrawElement[]) => {
globalSceneState.replaceAllElements(elements); 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 = ( private handleCanvasDoubleClick = (
event: React.MouseEvent<HTMLCanvasElement>, 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) { private getTextWysiwygSnappedToCenterPosition(x: number, y: number) {
const elementClickedInside = getElementContainingPosition( const elementClickedInside = getElementContainingPosition(
globalSceneState.getAllElements(), globalSceneState.getAllElements(),
@ -2378,66 +2444,6 @@ export class App extends React.Component<any, AppState> {
private saveDebounced = debounce(() => { private saveDebounced = debounce(() => {
saveToLocalStorage(globalSceneState.getAllElements(), this.state); saveToLocalStorage(globalSceneState.getAllElements(), this.state);
}, 300); }, 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());
}
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------