|
|
|
|
@ -57,7 +57,6 @@ import {
@@ -57,7 +57,6 @@ import {
|
|
|
|
|
DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT, |
|
|
|
|
DEFAULT_VERTICAL_ALIGN, |
|
|
|
|
DRAGGING_THRESHOLD, |
|
|
|
|
ELEMENT_READY_TO_ERASE_OPACITY, |
|
|
|
|
ELEMENT_SHIFT_TRANSLATE_AMOUNT, |
|
|
|
|
ELEMENT_TRANSLATE_AMOUNT, |
|
|
|
|
ENV, |
|
|
|
|
@ -247,6 +246,7 @@ import {
@@ -247,6 +246,7 @@ import {
|
|
|
|
|
ToolType, |
|
|
|
|
OnUserFollowedPayload, |
|
|
|
|
UnsubscribeCallback, |
|
|
|
|
ElementsPendingErasure, |
|
|
|
|
} from "../types"; |
|
|
|
|
import { |
|
|
|
|
debounce, |
|
|
|
|
@ -402,6 +402,7 @@ import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
@@ -402,6 +402,7 @@ import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
|
|
|
|
|
import { EditorLocalStorage } from "../data/EditorLocalStorage"; |
|
|
|
|
import FollowMode from "./FollowMode/FollowMode"; |
|
|
|
|
import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils"; |
|
|
|
|
import { getRenderOpacity } from "../renderer/renderElement"; |
|
|
|
|
|
|
|
|
|
const AppContext = React.createContext<AppClassProperties>(null!); |
|
|
|
|
const AppPropsContext = React.createContext<AppProps>(null!); |
|
|
|
|
@ -527,6 +528,8 @@ class App extends React.Component<AppProps, AppState> {
@@ -527,6 +528,8 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
private iFrameRefs = new Map<ExcalidrawElement["id"], HTMLIFrameElement>(); |
|
|
|
|
private initializedEmbeds = new Set<ExcalidrawIframeLikeElement["id"]>(); |
|
|
|
|
|
|
|
|
|
private elementsPendingErasure: ElementsPendingErasure = new Set(); |
|
|
|
|
|
|
|
|
|
hitLinkElement?: NonDeletedExcalidrawElement; |
|
|
|
|
lastPointerDownEvent: React.PointerEvent<HTMLElement> | null = null; |
|
|
|
|
lastPointerUpEvent: React.PointerEvent<HTMLElement> | PointerEvent | null = |
|
|
|
|
@ -1075,7 +1078,11 @@ class App extends React.Component<AppProps, AppState> {
@@ -1075,7 +1078,11 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
}px) scale(${scale})` |
|
|
|
|
: "none", |
|
|
|
|
display: isVisible ? "block" : "none", |
|
|
|
|
opacity: el.opacity / 100, |
|
|
|
|
opacity: getRenderOpacity( |
|
|
|
|
el, |
|
|
|
|
getContainingFrame(el), |
|
|
|
|
this.elementsPendingErasure, |
|
|
|
|
), |
|
|
|
|
["--embeddable-radius" as string]: `${getCornerRadius( |
|
|
|
|
Math.min(el.width, el.height), |
|
|
|
|
el, |
|
|
|
|
@ -1583,6 +1590,7 @@ class App extends React.Component<AppProps, AppState> {
@@ -1583,6 +1590,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
renderGrid: true, |
|
|
|
|
canvasBackgroundColor: |
|
|
|
|
this.state.viewBackgroundColor, |
|
|
|
|
elementsPendingErasure: this.elementsPendingErasure, |
|
|
|
|
}} |
|
|
|
|
/> |
|
|
|
|
<InteractiveCanvas |
|
|
|
|
@ -5062,31 +5070,25 @@ class App extends React.Component<AppProps, AppState> {
@@ -5062,31 +5070,25 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
pointerDownState: PointerDownState, |
|
|
|
|
scenePointer: { x: number; y: number }, |
|
|
|
|
) => { |
|
|
|
|
const updateElementIds = (elements: ExcalidrawElement[]) => { |
|
|
|
|
elements.forEach((element) => { |
|
|
|
|
let didChange = false; |
|
|
|
|
|
|
|
|
|
const processElements = (elements: ExcalidrawElement[]) => { |
|
|
|
|
for (const element of elements) { |
|
|
|
|
if (element.locked) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
idsToUpdate.push(element.id); |
|
|
|
|
if (event.altKey) { |
|
|
|
|
if ( |
|
|
|
|
pointerDownState.elementIdsToErase[element.id] && |
|
|
|
|
pointerDownState.elementIdsToErase[element.id].erase |
|
|
|
|
) { |
|
|
|
|
pointerDownState.elementIdsToErase[element.id].erase = false; |
|
|
|
|
if (this.elementsPendingErasure.delete(element.id)) { |
|
|
|
|
didChange = true; |
|
|
|
|
} |
|
|
|
|
} else if (!pointerDownState.elementIdsToErase[element.id]) { |
|
|
|
|
pointerDownState.elementIdsToErase[element.id] = { |
|
|
|
|
erase: true, |
|
|
|
|
opacity: element.opacity, |
|
|
|
|
}; |
|
|
|
|
} else if (!this.elementsPendingErasure.has(element.id)) { |
|
|
|
|
didChange = true; |
|
|
|
|
this.elementsPendingErasure.add(element.id); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const idsToUpdate: Array<string> = []; |
|
|
|
|
|
|
|
|
|
const distance = distance2d( |
|
|
|
|
pointerDownState.lastCoords.x, |
|
|
|
|
pointerDownState.lastCoords.y, |
|
|
|
|
@ -5098,7 +5100,7 @@ class App extends React.Component<AppProps, AppState> {
@@ -5098,7 +5100,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
let samplingInterval = 0; |
|
|
|
|
while (samplingInterval <= distance) { |
|
|
|
|
const hitElements = this.getElementsAtPosition(point.x, point.y); |
|
|
|
|
updateElementIds(hitElements); |
|
|
|
|
processElements(hitElements); |
|
|
|
|
|
|
|
|
|
// Exit since we reached current point
|
|
|
|
|
if (samplingInterval === distance) { |
|
|
|
|
@ -5117,35 +5119,31 @@ class App extends React.Component<AppProps, AppState> {
@@ -5117,35 +5119,31 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
point.y = nextY; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const elements = this.scene.getElementsIncludingDeleted().map((ele) => { |
|
|
|
|
const id = |
|
|
|
|
isBoundToContainer(ele) && idsToUpdate.includes(ele.containerId) |
|
|
|
|
? ele.containerId |
|
|
|
|
: ele.id; |
|
|
|
|
if (idsToUpdate.includes(id)) { |
|
|
|
|
if (event.altKey) { |
|
|
|
|
if ( |
|
|
|
|
pointerDownState.elementIdsToErase[id] && |
|
|
|
|
pointerDownState.elementIdsToErase[id].erase === false |
|
|
|
|
) { |
|
|
|
|
return newElementWith(ele, { |
|
|
|
|
opacity: pointerDownState.elementIdsToErase[id].opacity, |
|
|
|
|
}); |
|
|
|
|
pointerDownState.lastCoords.x = scenePointer.x; |
|
|
|
|
pointerDownState.lastCoords.y = scenePointer.y; |
|
|
|
|
|
|
|
|
|
if (didChange) { |
|
|
|
|
for (const element of this.scene.getNonDeletedElements()) { |
|
|
|
|
if ( |
|
|
|
|
isBoundToContainer(element) && |
|
|
|
|
(this.elementsPendingErasure.has(element.id) || |
|
|
|
|
this.elementsPendingErasure.has(element.containerId)) |
|
|
|
|
) { |
|
|
|
|
if (event.altKey) { |
|
|
|
|
this.elementsPendingErasure.delete(element.id); |
|
|
|
|
this.elementsPendingErasure.delete(element.containerId); |
|
|
|
|
} else { |
|
|
|
|
this.elementsPendingErasure.add(element.id); |
|
|
|
|
this.elementsPendingErasure.add(element.containerId); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
return newElementWith(ele, { |
|
|
|
|
opacity: ELEMENT_READY_TO_ERASE_OPACITY, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return ele; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
this.scene.replaceAllElements(elements); |
|
|
|
|
|
|
|
|
|
pointerDownState.lastCoords.x = scenePointer.x; |
|
|
|
|
pointerDownState.lastCoords.y = scenePointer.y; |
|
|
|
|
this.elementsPendingErasure = new Set(this.elementsPendingErasure); |
|
|
|
|
this.onSceneUpdated(); |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
// set touch moving for mobile context menu
|
|
|
|
|
private handleTouchMove = (event: React.TouchEvent<HTMLCanvasElement>) => { |
|
|
|
|
invalidateContextMenu = true; |
|
|
|
|
@ -5831,7 +5829,6 @@ class App extends React.Component<AppProps, AppState> {
@@ -5831,7 +5829,6 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
boxSelection: { |
|
|
|
|
hasOccurred: false, |
|
|
|
|
}, |
|
|
|
|
elementIdsToErase: {}, |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -7815,18 +7812,14 @@ class App extends React.Component<AppProps, AppState> {
@@ -7815,18 +7812,14 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
scenePointer.x, |
|
|
|
|
scenePointer.y, |
|
|
|
|
); |
|
|
|
|
hitElements.forEach( |
|
|
|
|
(hitElement) => |
|
|
|
|
(pointerDownState.elementIdsToErase[hitElement.id] = { |
|
|
|
|
erase: true, |
|
|
|
|
opacity: hitElement.opacity, |
|
|
|
|
}), |
|
|
|
|
hitElements.forEach((hitElement) => |
|
|
|
|
this.elementsPendingErasure.add(hitElement.id), |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
this.eraseElements(pointerDownState); |
|
|
|
|
this.eraseElements(); |
|
|
|
|
return; |
|
|
|
|
} else if (Object.keys(pointerDownState.elementIdsToErase).length) { |
|
|
|
|
this.restoreReadyToEraseElements(pointerDownState); |
|
|
|
|
} else if (this.elementsPendingErasure.size) { |
|
|
|
|
this.restoreReadyToEraseElements(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if ( |
|
|
|
|
@ -8087,65 +8080,32 @@ class App extends React.Component<AppProps, AppState> {
@@ -8087,65 +8080,32 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private restoreReadyToEraseElements = ( |
|
|
|
|
pointerDownState: PointerDownState, |
|
|
|
|
) => { |
|
|
|
|
const elements = this.scene.getElementsIncludingDeleted().map((ele) => { |
|
|
|
|
if ( |
|
|
|
|
pointerDownState.elementIdsToErase[ele.id] && |
|
|
|
|
pointerDownState.elementIdsToErase[ele.id].erase |
|
|
|
|
) { |
|
|
|
|
return newElementWith(ele, { |
|
|
|
|
opacity: pointerDownState.elementIdsToErase[ele.id].opacity, |
|
|
|
|
}); |
|
|
|
|
} else if ( |
|
|
|
|
isBoundToContainer(ele) && |
|
|
|
|
pointerDownState.elementIdsToErase[ele.containerId] && |
|
|
|
|
pointerDownState.elementIdsToErase[ele.containerId].erase |
|
|
|
|
) { |
|
|
|
|
return newElementWith(ele, { |
|
|
|
|
opacity: pointerDownState.elementIdsToErase[ele.containerId].opacity, |
|
|
|
|
}); |
|
|
|
|
} else if ( |
|
|
|
|
ele.frameId && |
|
|
|
|
pointerDownState.elementIdsToErase[ele.frameId] && |
|
|
|
|
pointerDownState.elementIdsToErase[ele.frameId].erase |
|
|
|
|
) { |
|
|
|
|
return newElementWith(ele, { |
|
|
|
|
opacity: pointerDownState.elementIdsToErase[ele.frameId].opacity, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
return ele; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
this.scene.replaceAllElements(elements); |
|
|
|
|
private restoreReadyToEraseElements = () => { |
|
|
|
|
this.elementsPendingErasure = new Set(); |
|
|
|
|
this.onSceneUpdated(); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
private eraseElements = (pointerDownState: PointerDownState) => { |
|
|
|
|
private eraseElements = () => { |
|
|
|
|
let didChange = false; |
|
|
|
|
const elements = this.scene.getElementsIncludingDeleted().map((ele) => { |
|
|
|
|
if ( |
|
|
|
|
pointerDownState.elementIdsToErase[ele.id] && |
|
|
|
|
pointerDownState.elementIdsToErase[ele.id].erase |
|
|
|
|
) { |
|
|
|
|
return newElementWith(ele, { isDeleted: true }); |
|
|
|
|
} else if ( |
|
|
|
|
isBoundToContainer(ele) && |
|
|
|
|
pointerDownState.elementIdsToErase[ele.containerId] && |
|
|
|
|
pointerDownState.elementIdsToErase[ele.containerId].erase |
|
|
|
|
) { |
|
|
|
|
return newElementWith(ele, { isDeleted: true }); |
|
|
|
|
} else if ( |
|
|
|
|
ele.frameId && |
|
|
|
|
pointerDownState.elementIdsToErase[ele.frameId] && |
|
|
|
|
pointerDownState.elementIdsToErase[ele.frameId].erase |
|
|
|
|
this.elementsPendingErasure.has(ele.id) || |
|
|
|
|
(ele.frameId && this.elementsPendingErasure.has(ele.frameId)) || |
|
|
|
|
(isBoundToContainer(ele) && |
|
|
|
|
this.elementsPendingErasure.has(ele.containerId)) |
|
|
|
|
) { |
|
|
|
|
didChange = true; |
|
|
|
|
return newElementWith(ele, { isDeleted: true }); |
|
|
|
|
} |
|
|
|
|
return ele; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
this.history.resumeRecording(); |
|
|
|
|
this.scene.replaceAllElements(elements); |
|
|
|
|
this.elementsPendingErasure = new Set(); |
|
|
|
|
|
|
|
|
|
if (didChange) { |
|
|
|
|
this.history.resumeRecording(); |
|
|
|
|
this.scene.replaceAllElements(elements); |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
private initializeImage = async ({ |
|
|
|
|
|