refactor: remove dependency on the (static) Scene (#9389)
This commit is contained in:
@@ -122,10 +122,7 @@ import {
|
||||
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
|
||||
import {
|
||||
mutateElement,
|
||||
newElementWith,
|
||||
} from "@excalidraw/element/mutateElement";
|
||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import {
|
||||
newFrameElement,
|
||||
@@ -303,6 +300,10 @@ import {
|
||||
|
||||
import { isNonDeletedElement } from "@excalidraw/element";
|
||||
|
||||
import Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import type { ElementUpdate } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
@@ -328,9 +329,10 @@ import type {
|
||||
MagicGenerationData,
|
||||
ExcalidrawNonSelectionElement,
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawElbowArrowElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type { ValueOf } from "@excalidraw/common/utility-types";
|
||||
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
|
||||
|
||||
import {
|
||||
actionAddToLibrary,
|
||||
@@ -403,7 +405,6 @@ import {
|
||||
hasBackground,
|
||||
isSomeElementSelected,
|
||||
} from "../scene";
|
||||
import Scene from "../scene/Scene";
|
||||
import { getStateForZoom } from "../scene/zoom";
|
||||
import {
|
||||
dataURLToFile,
|
||||
@@ -760,6 +761,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (excalidrawAPI) {
|
||||
const api: ExcalidrawImperativeAPI = {
|
||||
updateScene: this.updateScene,
|
||||
mutateElement: this.mutateElement,
|
||||
updateLibrary: this.library.updateLibrary,
|
||||
addFiles: this.addFiles,
|
||||
resetScene: this.resetScene,
|
||||
@@ -1387,7 +1389,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
private resetEditingFrame = (frame: ExcalidrawFrameLikeElement | null) => {
|
||||
if (frame) {
|
||||
mutateElement(frame, { name: frame.name?.trim() || null });
|
||||
this.scene.mutateElement(frame, { name: frame.name?.trim() || null });
|
||||
}
|
||||
this.setState({ editingFrame: null });
|
||||
};
|
||||
@@ -1444,7 +1446,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
autoFocus
|
||||
value={frameNameInEdit}
|
||||
onChange={(e) => {
|
||||
mutateElement(f, {
|
||||
this.scene.mutateElement(f, {
|
||||
name: e.target.value,
|
||||
});
|
||||
}}
|
||||
@@ -1670,7 +1672,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
<Hyperlink
|
||||
key={firstSelectedElement.id}
|
||||
element={firstSelectedElement}
|
||||
elementsMap={allElementsMap}
|
||||
scene={this.scene}
|
||||
setAppState={this.setAppState}
|
||||
onLinkOpen={this.props.onLinkOpen}
|
||||
setToast={this.setToast}
|
||||
@@ -1938,16 +1940,20 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// state only.
|
||||
// Thus reset so that we prefer local cache (if there was some
|
||||
// generationData set previously)
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
frameElement,
|
||||
{ customData: { generationData: undefined } },
|
||||
false,
|
||||
{
|
||||
customData: { generationData: undefined },
|
||||
},
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
} else {
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
frameElement,
|
||||
{ customData: { generationData: data } },
|
||||
false,
|
||||
{
|
||||
customData: { generationData: data },
|
||||
},
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
}
|
||||
this.magicGenerations.set(frameElement.id, data);
|
||||
@@ -2119,7 +2125,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.insertElement(frame);
|
||||
|
||||
for (const child of selectedElements) {
|
||||
mutateElement(child, { frameId: frame.id });
|
||||
this.scene.mutateElement(child, { frameId: frame.id });
|
||||
}
|
||||
|
||||
this.setState({
|
||||
@@ -2918,8 +2924,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
nonDeletedElementsMap,
|
||||
),
|
||||
),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3317,11 +3322,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
newElement,
|
||||
this.scene.getElementsMapIncludingDeleted(),
|
||||
);
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
container,
|
||||
this.scene.getElementsMapIncludingDeleted(),
|
||||
);
|
||||
redrawTextBoundingBox(newElement, container, this.scene);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3444,7 +3445,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
// hack to reset the `y` coord because we vertically center during
|
||||
// insertImageElement
|
||||
mutateElement(initializedImageElement, { y }, false);
|
||||
this.scene.mutateElement(
|
||||
initializedImageElement,
|
||||
{ y },
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
|
||||
y = imageElement.y + imageElement.height + 25;
|
||||
|
||||
@@ -3998,6 +4003,17 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
);
|
||||
|
||||
public mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
element: TElement,
|
||||
updates: ElementUpdate<TElement>,
|
||||
informMutation = true,
|
||||
) => {
|
||||
return this.scene.mutateElement(element, updates, {
|
||||
informMutation,
|
||||
isDragging: false,
|
||||
});
|
||||
};
|
||||
|
||||
private triggerRender = (
|
||||
/** force always re-renders canvas even if no change */
|
||||
force?: boolean,
|
||||
@@ -4166,9 +4182,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
) {
|
||||
this.flowChartCreator.createNodes(
|
||||
selectedElements[0],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state,
|
||||
getLinkDirectionFromKey(event.key),
|
||||
this.scene,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4410,16 +4426,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
selectedElements.forEach((element) => {
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
element,
|
||||
{
|
||||
x: element.x + offsetX,
|
||||
y: element.y + offsetY,
|
||||
},
|
||||
false,
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
|
||||
updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
|
||||
updateBoundElements(element, this.scene, {
|
||||
simultaneouslyUpdated: selectedElements,
|
||||
});
|
||||
});
|
||||
@@ -4453,6 +4469,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.setState({
|
||||
editingLinearElement: new LinearElementEditor(
|
||||
selectedElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -4646,11 +4663,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (isArrowKey(event.key)) {
|
||||
bindOrUnbindLinearElements(
|
||||
this.scene.getSelectedElements(this.state).filter(isLinearElement),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene,
|
||||
isBindingEnabled(this.state),
|
||||
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
||||
this.scene,
|
||||
this.state.zoom,
|
||||
);
|
||||
this.setState({ suggestedBindings: [] });
|
||||
@@ -4957,7 +4972,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
onChange: withBatchedUpdates((nextOriginalText) => {
|
||||
updateElement(nextOriginalText, false);
|
||||
if (isNonDeletedElement(element)) {
|
||||
updateBoundElements(element, this.scene.getNonDeletedElementsMap());
|
||||
updateBoundElements(element, this.scene);
|
||||
}
|
||||
}),
|
||||
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
|
||||
@@ -5320,7 +5335,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
|
||||
const newHeight = Math.max(container.height, minHeight);
|
||||
const newWidth = Math.max(container.width, minWidth);
|
||||
mutateElement(container, { height: newHeight, width: newWidth });
|
||||
this.scene.mutateElement(container, {
|
||||
height: newHeight,
|
||||
width: newWidth,
|
||||
});
|
||||
sceneX = container.x + newWidth / 2;
|
||||
sceneY = container.y + newHeight / 2;
|
||||
if (parentCenterPosition) {
|
||||
@@ -5371,7 +5389,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
|
||||
if (!existingTextElement && shouldBindToContainer && container) {
|
||||
mutateElement(container, {
|
||||
this.scene.mutateElement(container, {
|
||||
boundElements: (container.boundElements || []).concat({
|
||||
type: "text",
|
||||
id: element.id,
|
||||
@@ -5447,7 +5465,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
) {
|
||||
this.store.shouldCaptureIncrement();
|
||||
this.setState({
|
||||
editingLinearElement: new LinearElementEditor(selectedElements[0]),
|
||||
editingLinearElement: new LinearElementEditor(
|
||||
selectedElements[0],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
});
|
||||
return;
|
||||
} else if (
|
||||
@@ -5471,7 +5492,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
if (midPoint && midPoint > -1) {
|
||||
this.store.shouldCaptureIncrement();
|
||||
LinearElementEditor.deleteFixedSegment(selectedElements[0], midPoint);
|
||||
LinearElementEditor.deleteFixedSegment(
|
||||
selectedElements[0],
|
||||
this.scene,
|
||||
midPoint,
|
||||
);
|
||||
|
||||
const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords(
|
||||
{
|
||||
@@ -5854,7 +5879,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
this,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
if (
|
||||
@@ -5916,7 +5940,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
lastPoint,
|
||||
) >= LINE_CONFIRM_THRESHOLD
|
||||
) {
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
multiElement,
|
||||
{
|
||||
points: [
|
||||
@@ -5924,7 +5948,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
pointFrom<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
|
||||
],
|
||||
},
|
||||
false,
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
} else {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||
@@ -5940,12 +5964,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
) < LINE_CONFIRM_THRESHOLD
|
||||
) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
multiElement,
|
||||
{
|
||||
points: points.slice(0, -1),
|
||||
},
|
||||
false,
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
} else {
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
@@ -5977,8 +6001,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (isPathALoop(points, this.state.zoom.value)) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||
}
|
||||
|
||||
// update last uncommitted point
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
multiElement,
|
||||
{
|
||||
points: [
|
||||
@@ -5989,9 +6014,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
),
|
||||
],
|
||||
},
|
||||
false,
|
||||
{
|
||||
isDragging: true,
|
||||
informMutation: false,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6578,7 +6603,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
const frame = this.getTopLayerFrameAtSceneCoords({ x, y });
|
||||
|
||||
mutateElement(pendingImageElement, {
|
||||
this.scene.mutateElement(pendingImageElement, {
|
||||
x,
|
||||
y,
|
||||
frameId: frame ? frame.id : null,
|
||||
@@ -7633,7 +7658,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
multiElement.type === "line" &&
|
||||
isPathALoop(multiElement.points, this.state.zoom.value)
|
||||
) {
|
||||
mutateElement(multiElement, {
|
||||
this.scene.mutateElement(multiElement, {
|
||||
lastCommittedPoint:
|
||||
multiElement.points[multiElement.points.length - 1],
|
||||
});
|
||||
@@ -7644,7 +7669,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// Elbow arrows cannot be created by putting down points
|
||||
// only the start and end points can be defined
|
||||
if (isElbowArrow(multiElement) && multiElement.points.length > 1) {
|
||||
mutateElement(multiElement, {
|
||||
this.scene.mutateElement(multiElement, {
|
||||
lastCommittedPoint:
|
||||
multiElement.points[multiElement.points.length - 1],
|
||||
});
|
||||
@@ -7681,7 +7706,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}));
|
||||
// clicking outside commit zone → update reference for last committed
|
||||
// point
|
||||
mutateElement(multiElement, {
|
||||
this.scene.mutateElement(multiElement, {
|
||||
lastCommittedPoint: multiElement.points[multiElement.points.length - 1],
|
||||
});
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||
@@ -7767,7 +7792,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
),
|
||||
};
|
||||
});
|
||||
mutateElement(element, {
|
||||
this.scene.mutateElement(element, {
|
||||
points: [...element.points, pointFrom<LocalPoint>(0, 0)],
|
||||
});
|
||||
const boundElement = getHoveredElementForBinding(
|
||||
@@ -8017,7 +8042,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
index,
|
||||
gridX,
|
||||
gridY,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.scene,
|
||||
);
|
||||
|
||||
flushSync(() => {
|
||||
@@ -8122,7 +8147,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
pointerCoords,
|
||||
this,
|
||||
!event[KEYS.CTRL_OR_CMD],
|
||||
elementsMap,
|
||||
this.scene,
|
||||
);
|
||||
if (!ret) {
|
||||
return;
|
||||
@@ -8349,7 +8374,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
),
|
||||
};
|
||||
|
||||
mutateElement(croppingElement, {
|
||||
this.scene.mutateElement(croppingElement, {
|
||||
crop: nextCrop,
|
||||
});
|
||||
|
||||
@@ -8606,13 +8631,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||
? newElement.pressures
|
||||
: [...newElement.pressures, event.pressure];
|
||||
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
newElement,
|
||||
{
|
||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||
pressures,
|
||||
},
|
||||
false,
|
||||
{
|
||||
informMutation: false,
|
||||
isDragging: false,
|
||||
},
|
||||
);
|
||||
|
||||
this.setState({
|
||||
@@ -8635,24 +8663,23 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
if (points.length === 1) {
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
newElement,
|
||||
{
|
||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||
},
|
||||
false,
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
} else if (
|
||||
points.length === 2 ||
|
||||
(points.length > 1 && isElbowArrow(newElement))
|
||||
) {
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
newElement,
|
||||
{
|
||||
points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
||||
},
|
||||
false,
|
||||
{ isDragging: true },
|
||||
{ isDragging: true, informMutation: false },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8763,7 +8790,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
selectedLinearElement:
|
||||
elementsWithinSelection.length === 1 &&
|
||||
isLinearElement(elementsWithinSelection[0])
|
||||
? new LinearElementEditor(elementsWithinSelection[0])
|
||||
? new LinearElementEditor(
|
||||
elementsWithinSelection[0],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
: null,
|
||||
showHyperlinkPopup:
|
||||
elementsWithinSelection.length === 1 &&
|
||||
@@ -8869,7 +8899,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
.map((e) => elementsMap.get(e.id))
|
||||
.filter((e) => isElbowArrow(e))
|
||||
.forEach((e) => {
|
||||
!!e && mutateElement(e, {}, true);
|
||||
!!e && this.scene.mutateElement(e, {});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8905,7 +8935,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (element) {
|
||||
mutateElement(element, {}, true);
|
||||
this.scene.mutateElement(
|
||||
element as ExcalidrawElbowArrowElement,
|
||||
{},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8934,7 +8967,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
element,
|
||||
startBindingElement,
|
||||
endBindingElement,
|
||||
elementsMap,
|
||||
this.scene,
|
||||
);
|
||||
}
|
||||
@@ -9001,7 +9033,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
? []
|
||||
: [...newElement.pressures, childEvent.pressure];
|
||||
|
||||
mutateElement(newElement, {
|
||||
this.scene.mutateElement(newElement, {
|
||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||
pressures,
|
||||
lastCommittedPoint: pointFrom<LocalPoint>(dx, dy),
|
||||
@@ -9048,15 +9080,20 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
|
||||
if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) {
|
||||
mutateElement(newElement, {
|
||||
points: [
|
||||
...newElement.points,
|
||||
pointFrom<LocalPoint>(
|
||||
pointerCoords.x - newElement.x,
|
||||
pointerCoords.y - newElement.y,
|
||||
),
|
||||
],
|
||||
});
|
||||
this.scene.mutateElement(
|
||||
newElement,
|
||||
{
|
||||
points: [
|
||||
...newElement.points,
|
||||
pointFrom<LocalPoint>(
|
||||
pointerCoords.x - newElement.x,
|
||||
pointerCoords.y - newElement.y,
|
||||
),
|
||||
],
|
||||
},
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
|
||||
this.setState({
|
||||
multiElement: newElement,
|
||||
newElement,
|
||||
@@ -9070,8 +9107,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
newElement,
|
||||
this.state,
|
||||
pointerCoords,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene,
|
||||
);
|
||||
}
|
||||
this.setState({ suggestedBindings: [], startBoundElement: null });
|
||||
@@ -9089,7 +9125,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
prevState,
|
||||
),
|
||||
selectedLinearElement: new LinearElementEditor(newElement),
|
||||
selectedLinearElement: new LinearElementEditor(
|
||||
newElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
}));
|
||||
} else {
|
||||
this.setState((prevState) => ({
|
||||
@@ -9112,7 +9151,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
|
||||
if (newElement.width < minWidth) {
|
||||
mutateElement(newElement, {
|
||||
this.scene.mutateElement(newElement, {
|
||||
autoResize: true,
|
||||
});
|
||||
}
|
||||
@@ -9162,7 +9201,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
if (newElement) {
|
||||
mutateElement(newElement, getNormalizedDimensions(newElement));
|
||||
this.scene.mutateElement(
|
||||
newElement,
|
||||
getNormalizedDimensions(newElement),
|
||||
{
|
||||
informMutation: false,
|
||||
isDragging: false,
|
||||
},
|
||||
);
|
||||
// the above does not guarantee the scene to be rendered again, hence the trigger below
|
||||
this.scene.triggerUpdate();
|
||||
}
|
||||
@@ -9194,7 +9240,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
) {
|
||||
// remove the linear element from all groups
|
||||
// before removing it from the frame as well
|
||||
mutateElement(linearElement, {
|
||||
this.scene.mutateElement(linearElement, {
|
||||
groupIds: [],
|
||||
});
|
||||
|
||||
@@ -9223,12 +9269,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.state.editingGroupId!,
|
||||
);
|
||||
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
element,
|
||||
{
|
||||
groupIds: element.groupIds.slice(0, index),
|
||||
},
|
||||
false,
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9240,12 +9286,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
element.groupIds[element.groupIds.length - 1],
|
||||
).length < 2
|
||||
) {
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
element,
|
||||
{
|
||||
groupIds: [],
|
||||
},
|
||||
false,
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -9355,7 +9401,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// the one we've hit
|
||||
if (selectedElements.length === 1) {
|
||||
this.setState({
|
||||
selectedLinearElement: new LinearElementEditor(hitElement),
|
||||
selectedLinearElement: new LinearElementEditor(
|
||||
hitElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -9480,7 +9529,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
selectedLinearElement:
|
||||
newSelectedElements.length === 1 &&
|
||||
isLinearElement(newSelectedElements[0])
|
||||
? new LinearElementEditor(newSelectedElements[0])
|
||||
? new LinearElementEditor(
|
||||
newSelectedElements[0],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
: prevState.selectedLinearElement,
|
||||
};
|
||||
});
|
||||
@@ -9554,7 +9606,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1.
|
||||
// Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized
|
||||
prevState.selectedLinearElement?.elementId !== hitElement.id
|
||||
? new LinearElementEditor(hitElement)
|
||||
? new LinearElementEditor(
|
||||
hitElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
: prevState.selectedLinearElement,
|
||||
}));
|
||||
}
|
||||
@@ -9647,11 +9702,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
bindOrUnbindLinearElements(
|
||||
linearElements,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene,
|
||||
isBindingEnabled(this.state),
|
||||
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
||||
this.scene,
|
||||
this.state.zoom,
|
||||
);
|
||||
}
|
||||
@@ -9808,12 +9861,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const dataURL =
|
||||
this.files[fileId]?.dataURL || (await getDataURL(imageFile));
|
||||
|
||||
const imageElement = mutateElement(
|
||||
const imageElement = this.scene.mutateElement(
|
||||
_imageElement,
|
||||
{
|
||||
fileId,
|
||||
},
|
||||
false,
|
||||
{ informMutation: false, isDragging: false },
|
||||
) as NonDeleted<InitializedExcalidrawImageElement>;
|
||||
|
||||
return new Promise<NonDeleted<InitializedExcalidrawImageElement>>(
|
||||
@@ -9879,7 +9932,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
showCursorImagePreview,
|
||||
});
|
||||
} catch (error: any) {
|
||||
mutateElement(imageElement, {
|
||||
this.scene.mutateElement(imageElement, {
|
||||
isDeleted: true,
|
||||
});
|
||||
this.actionManager.executeAction(actionFinalize);
|
||||
@@ -10025,7 +10078,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
imageElement.height < DRAGGING_THRESHOLD / this.state.zoom.value
|
||||
) {
|
||||
const placeholderSize = 100 / this.state.zoom.value;
|
||||
mutateElement(imageElement, {
|
||||
this.scene.mutateElement(imageElement, {
|
||||
x: imageElement.x - placeholderSize / 2,
|
||||
y: imageElement.y - placeholderSize / 2,
|
||||
width: placeholderSize,
|
||||
@@ -10059,7 +10112,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const x = imageElement.x + imageElement.width / 2 - width / 2;
|
||||
const y = imageElement.y + imageElement.height / 2 - height / 2;
|
||||
|
||||
mutateElement(imageElement, {
|
||||
this.scene.mutateElement(imageElement, {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
@@ -10490,7 +10543,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this,
|
||||
),
|
||||
selectedLinearElement: isLinearElement(element)
|
||||
? new LinearElementEditor(element)
|
||||
? new LinearElementEditor(
|
||||
element,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
: null,
|
||||
}
|
||||
: this.state),
|
||||
@@ -10523,8 +10579,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
height: distance(pointerDownState.origin.y, pointerCoords.y),
|
||||
shouldMaintainAspectRatio: shouldMaintainAspectRatio(event),
|
||||
shouldResizeFromCenter: false,
|
||||
scene: this.scene,
|
||||
zoom: this.state.zoom.value,
|
||||
informMutation,
|
||||
informMutation: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -10588,6 +10645,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
: shouldMaintainAspectRatio(event),
|
||||
shouldResizeFromCenter: shouldResizeFromCenter(event),
|
||||
zoom: this.state.zoom.value,
|
||||
scene: this.scene,
|
||||
widthAspectRatio: aspectRatio,
|
||||
originOffset: this.state.originSnapOffset,
|
||||
informMutation,
|
||||
@@ -10675,7 +10733,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
transformHandleType,
|
||||
);
|
||||
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
croppingElement,
|
||||
cropElement(
|
||||
croppingElement,
|
||||
@@ -10690,16 +10748,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
),
|
||||
);
|
||||
|
||||
updateBoundElements(
|
||||
croppingElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
{
|
||||
newSize: {
|
||||
width: croppingElement.width,
|
||||
height: croppingElement.height,
|
||||
},
|
||||
updateBoundElements(croppingElement, this.scene, {
|
||||
newSize: {
|
||||
width: croppingElement.width,
|
||||
height: croppingElement.height,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
isCropping: transformHandleType && transformHandleType !== "rotation",
|
||||
@@ -10813,7 +10867,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
pointerDownState.originalElements,
|
||||
transformHandleType,
|
||||
selectedElements,
|
||||
this.scene.getElementsMapIncludingDeleted(),
|
||||
this.scene,
|
||||
shouldRotateWithDiscreteAngle(event),
|
||||
shouldResizeFromCenter(event),
|
||||
|
||||
@@ -6,9 +6,10 @@ import {
|
||||
defaultGetElementLinkFromSelection,
|
||||
getLinkIdAndTypeFromSelection,
|
||||
} from "@excalidraw/element/elementLink";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { t } from "../i18n";
|
||||
import { getSelectedElements } from "../scene";
|
||||
@@ -21,20 +22,20 @@ import { TrashIcon } from "./icons";
|
||||
import "./ElementLinkDialog.scss";
|
||||
|
||||
import type { AppProps, AppState, UIAppState } from "../types";
|
||||
|
||||
const ElementLinkDialog = ({
|
||||
sourceElementId,
|
||||
onClose,
|
||||
elementsMap,
|
||||
appState,
|
||||
scene,
|
||||
generateLinkForSelection = defaultGetElementLinkFromSelection,
|
||||
}: {
|
||||
sourceElementId: ExcalidrawElement["id"];
|
||||
elementsMap: ElementsMap;
|
||||
appState: UIAppState;
|
||||
scene: Scene;
|
||||
onClose?: () => void;
|
||||
generateLinkForSelection: AppProps["generateLinkForSelection"];
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const originalLink = elementsMap.get(sourceElementId)?.link ?? null;
|
||||
|
||||
const [nextLink, setNextLink] = useState<string | null>(originalLink);
|
||||
@@ -70,7 +71,7 @@ const ElementLinkDialog = ({
|
||||
if (nextLink && nextLink !== elementsMap.get(sourceElementId)?.link) {
|
||||
const elementToLink = elementsMap.get(sourceElementId);
|
||||
elementToLink &&
|
||||
mutateElement(elementToLink, {
|
||||
scene.mutateElement(elementToLink, {
|
||||
link: nextLink,
|
||||
});
|
||||
}
|
||||
@@ -78,13 +79,13 @@ const ElementLinkDialog = ({
|
||||
if (!nextLink && linkEdited && sourceElementId) {
|
||||
const elementToLink = elementsMap.get(sourceElementId);
|
||||
elementToLink &&
|
||||
mutateElement(elementToLink, {
|
||||
scene.mutateElement(elementToLink, {
|
||||
link: null,
|
||||
});
|
||||
}
|
||||
|
||||
onClose?.();
|
||||
}, [sourceElementId, nextLink, elementsMap, linkEdited, onClose]);
|
||||
}, [sourceElementId, nextLink, elementsMap, linkEdited, scene, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
CLASSES,
|
||||
DEFAULT_SIDEBAR,
|
||||
TOOL_TYPE,
|
||||
arrayToMap,
|
||||
capitalizeString,
|
||||
isShallowEqual,
|
||||
} from "@excalidraw/common";
|
||||
@@ -17,7 +18,6 @@ import { ShapeCache } from "@excalidraw/element/ShapeCache";
|
||||
|
||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import Scene from "../scene/Scene";
|
||||
import { actionToggleStats } from "../actions";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { isHandToolActive } from "../appState";
|
||||
@@ -446,22 +446,18 @@ const LayerUI = ({
|
||||
|
||||
if (selectedElements.length) {
|
||||
for (const element of selectedElements) {
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
[altKey && eyeDropperState.swapPreviewOnAlt
|
||||
? colorPickerType === "elementBackground"
|
||||
? "strokeColor"
|
||||
: "backgroundColor"
|
||||
: colorPickerType === "elementBackground"
|
||||
? "backgroundColor"
|
||||
: "strokeColor"]: color,
|
||||
},
|
||||
false,
|
||||
);
|
||||
mutateElement(element, arrayToMap(elements), {
|
||||
[altKey && eyeDropperState.swapPreviewOnAlt
|
||||
? colorPickerType === "elementBackground"
|
||||
? "strokeColor"
|
||||
: "backgroundColor"
|
||||
: colorPickerType === "elementBackground"
|
||||
? "backgroundColor"
|
||||
: "strokeColor"]: color,
|
||||
});
|
||||
ShapeCache.delete(element);
|
||||
}
|
||||
Scene.getScene(selectedElements[0])?.triggerUpdate();
|
||||
app.scene.triggerUpdate();
|
||||
} else if (colorPickerType === "elementBackground") {
|
||||
setAppState({
|
||||
currentItemBackgroundColor: color,
|
||||
@@ -494,7 +490,7 @@ const LayerUI = ({
|
||||
openDialog: null,
|
||||
});
|
||||
}}
|
||||
elementsMap={app.scene.getNonDeletedElementsMap()}
|
||||
scene={app.scene}
|
||||
appState={appState}
|
||||
generateLinkForSelection={generateLinkForSelection}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { degreesToRadians, radiansToDegrees } from "@excalidraw/math";
|
||||
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||
import { isArrowElement, isElbowArrow } from "@excalidraw/element/typeChecks";
|
||||
|
||||
@@ -9,13 +7,14 @@ import type { Degrees } from "@excalidraw/math";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { angleIcon } from "../icons";
|
||||
|
||||
import DragInput from "./DragInput";
|
||||
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface AngleProps {
|
||||
@@ -35,7 +34,6 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||
scene,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const origElement = originalElements[0];
|
||||
if (origElement && !isElbowArrow(origElement)) {
|
||||
const latestElement = elementsMap.get(origElement.id);
|
||||
@@ -45,14 +43,14 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||
|
||||
if (nextValue !== undefined) {
|
||||
const nextAngle = degreesToRadians(nextValue as Degrees);
|
||||
mutateElement(latestElement, {
|
||||
scene.mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
updateBindings(latestElement, elementsMap, elements, scene);
|
||||
updateBindings(latestElement, scene);
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
mutateElement(boundTextElement, { angle: nextAngle });
|
||||
scene.mutateElement(boundTextElement, { angle: nextAngle });
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -71,14 +69,14 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||
|
||||
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
|
||||
|
||||
mutateElement(latestElement, {
|
||||
scene.mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
updateBindings(latestElement, elementsMap, elements, scene);
|
||||
updateBindings(latestElement, scene);
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
mutateElement(boundTextElement, { angle: nextAngle });
|
||||
scene.mutateElement(boundTextElement, { angle: nextAngle });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { getNormalizedGridStep } from "../../scene";
|
||||
|
||||
import StatsDragInput from "./DragInput";
|
||||
import { getStepSizedValue } from "./utils";
|
||||
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface PositionProps {
|
||||
|
||||
@@ -5,17 +5,17 @@ import {
|
||||
MINIMAL_CROP_SIZE,
|
||||
getUncroppedWidthAndHeight,
|
||||
} from "@excalidraw/element/cropElement";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import { resizeSingleElement } from "@excalidraw/element/resizeElements";
|
||||
import { isImageElement } from "@excalidraw/element/typeChecks";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import DragInput from "./DragInput";
|
||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface DimensionDragInputProps {
|
||||
@@ -113,7 +113,7 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
};
|
||||
}
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
crop: nextCrop,
|
||||
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
||||
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
||||
@@ -144,7 +144,7 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
height: nextCropHeight,
|
||||
};
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
crop: nextCrop,
|
||||
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
||||
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
||||
@@ -176,8 +176,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
nextHeight,
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
property === "width" ? "e" : "s",
|
||||
{
|
||||
shouldMaintainAspectRatio: keepAspectRatio,
|
||||
@@ -223,8 +223,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
nextHeight,
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
property === "width" ? "e" : "s",
|
||||
{
|
||||
shouldMaintainAspectRatio: keepAspectRatio,
|
||||
|
||||
@@ -7,6 +7,8 @@ import { deepCopyElement } from "@excalidraw/element/duplicate";
|
||||
|
||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { CaptureUpdateAction } from "../../store";
|
||||
import { useApp } from "../App";
|
||||
import { InlineIcon } from "../InlineIcon";
|
||||
@@ -16,7 +18,6 @@ import { SMALLEST_DELTA } from "./utils";
|
||||
import "./DragInput.scss";
|
||||
|
||||
import type { StatsInputProperty } from "./utils";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
export type DragInputCallbackType<
|
||||
@@ -216,13 +217,12 @@ const StatsDragInput = <
|
||||
y: number;
|
||||
} | null = null;
|
||||
|
||||
let originalElementsMap: Map<string, ExcalidrawElement> | null =
|
||||
app.scene
|
||||
.getNonDeletedElements()
|
||||
.reduce((acc: ElementsMap, element) => {
|
||||
acc.set(element.id, deepCopyElement(element));
|
||||
return acc;
|
||||
}, new Map());
|
||||
let originalElementsMap: ElementsMap | null = app.scene
|
||||
.getNonDeletedElements()
|
||||
.reduce((acc: ElementsMap, element) => {
|
||||
acc.set(element.id, deepCopyElement(element));
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
let originalElements: readonly E[] | null = elements.map(
|
||||
(element) => originalElementsMap!.get(element.id) as E,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
redrawTextBoundingBox,
|
||||
@@ -13,13 +12,14 @@ import type {
|
||||
ExcalidrawTextElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { fontSizeIcon } from "../icons";
|
||||
|
||||
import StatsDragInput from "./DragInput";
|
||||
import { getStepSizedValue } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface FontSizeProps {
|
||||
@@ -68,13 +68,13 @@ const handleFontSizeChange: DragInputCallbackType<
|
||||
}
|
||||
|
||||
if (nextFontSize) {
|
||||
mutateElement(latestElement, {
|
||||
scene.mutateElement(latestElement, {
|
||||
fontSize: nextFontSize,
|
||||
});
|
||||
redrawTextBoundingBox(
|
||||
latestElement,
|
||||
scene.getContainerElement(latestElement),
|
||||
scene.getNonDeletedElementsMap(),
|
||||
scene,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { degreesToRadians, radiansToDegrees } from "@excalidraw/math";
|
||||
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||
import { isArrowElement } from "@excalidraw/element/typeChecks";
|
||||
|
||||
@@ -11,13 +9,14 @@ import type { Degrees } from "@excalidraw/math";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { angleIcon } from "../icons";
|
||||
|
||||
import DragInput from "./DragInput";
|
||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface MultiAngleProps {
|
||||
@@ -54,17 +53,13 @@ const handleDegreeChange: DragInputCallbackType<
|
||||
if (!element) {
|
||||
continue;
|
||||
}
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
angle: nextAngle,
|
||||
},
|
||||
false,
|
||||
);
|
||||
scene.mutateElement(element, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(element)) {
|
||||
mutateElement(boundTextElement, { angle: nextAngle }, false);
|
||||
scene.mutateElement(boundTextElement, { angle: nextAngle });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,17 +87,13 @@ const handleDegreeChange: DragInputCallbackType<
|
||||
|
||||
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
|
||||
|
||||
mutateElement(
|
||||
latestElement,
|
||||
{
|
||||
angle: nextAngle,
|
||||
},
|
||||
false,
|
||||
);
|
||||
scene.mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
mutateElement(boundTextElement, { angle: nextAngle }, false);
|
||||
scene.mutateElement(boundTextElement, { angle: nextAngle });
|
||||
}
|
||||
}
|
||||
scene.triggerUpdate();
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useMemo } from "react";
|
||||
|
||||
import { MIN_WIDTH_OR_HEIGHT } from "@excalidraw/common";
|
||||
import { updateBoundElements } from "@excalidraw/element/binding";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import {
|
||||
rescalePointsInElement,
|
||||
resizeSingleElement,
|
||||
@@ -23,13 +22,14 @@ import type {
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import DragInput from "./DragInput";
|
||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
import { getElementsInAtomicUnit } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type { AtomicUnit } from "./utils";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface MultiDimensionProps {
|
||||
@@ -75,33 +75,31 @@ const resizeElementInGroup = (
|
||||
scale: number,
|
||||
latestElement: ExcalidrawElement,
|
||||
origElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
|
||||
|
||||
mutateElement(latestElement, updates, false);
|
||||
scene.mutateElement(latestElement, updates);
|
||||
|
||||
const boundTextElement = getBoundTextElement(
|
||||
origElement,
|
||||
originalElementsMap,
|
||||
);
|
||||
if (boundTextElement) {
|
||||
const newFontSize = boundTextElement.fontSize * scale;
|
||||
updateBoundElements(latestElement, elementsMap, {
|
||||
updateBoundElements(latestElement, scene, {
|
||||
newSize: { width: updates.width, height: updates.height },
|
||||
});
|
||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
|
||||
mutateElement(
|
||||
latestBoundTextElement,
|
||||
{
|
||||
fontSize: newFontSize,
|
||||
},
|
||||
false,
|
||||
);
|
||||
scene.mutateElement(latestBoundTextElement, {
|
||||
fontSize: newFontSize,
|
||||
});
|
||||
handleBindTextResize(
|
||||
latestElement,
|
||||
elementsMap,
|
||||
scene,
|
||||
property === "width" ? "e" : "s",
|
||||
true,
|
||||
);
|
||||
@@ -118,8 +116,8 @@ const resizeGroup = (
|
||||
property: MultiDimensionProps["property"],
|
||||
latestElements: ExcalidrawElement[],
|
||||
originalElements: ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
// keep aspect ratio for groups
|
||||
if (property === "width") {
|
||||
@@ -141,8 +139,8 @@ const resizeGroup = (
|
||||
scale,
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -194,8 +192,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
property,
|
||||
latestElements,
|
||||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
} else {
|
||||
const [el] = elementsInUnit;
|
||||
@@ -237,8 +235,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
nextHeight,
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
property === "width" ? "e" : "s",
|
||||
{
|
||||
shouldInformMutation: false,
|
||||
@@ -301,8 +299,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
property,
|
||||
latestElements,
|
||||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
} else {
|
||||
const [el] = elementsInUnit;
|
||||
@@ -340,8 +338,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
nextHeight,
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
property === "width" ? "e" : "s",
|
||||
{
|
||||
shouldInformMutation: false,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
redrawTextBoundingBox,
|
||||
@@ -16,13 +15,14 @@ import type {
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { fontSizeIcon } from "../icons";
|
||||
|
||||
import StatsDragInput from "./DragInput";
|
||||
import { getStepSizedValue } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface MultiFontSizeProps {
|
||||
@@ -84,19 +84,14 @@ const handleFontSizeChange: DragInputCallbackType<
|
||||
nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
|
||||
|
||||
for (const textElement of latestTextElements) {
|
||||
mutateElement(
|
||||
textElement,
|
||||
{
|
||||
fontSize: nextFontSize,
|
||||
},
|
||||
false,
|
||||
);
|
||||
scene.mutateElement(textElement, {
|
||||
fontSize: nextFontSize,
|
||||
});
|
||||
|
||||
redrawTextBoundingBox(
|
||||
textElement,
|
||||
scene.getContainerElement(textElement),
|
||||
elementsMap,
|
||||
false,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,19 +112,14 @@ const handleFontSizeChange: DragInputCallbackType<
|
||||
if (shouldChangeByStepSize) {
|
||||
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
|
||||
}
|
||||
mutateElement(
|
||||
latestElement,
|
||||
{
|
||||
fontSize: nextFontSize,
|
||||
},
|
||||
false,
|
||||
);
|
||||
scene.mutateElement(latestElement, {
|
||||
fontSize: nextFontSize,
|
||||
});
|
||||
|
||||
redrawTextBoundingBox(
|
||||
latestElement,
|
||||
scene.getContainerElement(latestElement),
|
||||
elementsMap,
|
||||
false,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,9 @@ import { isTextElement } from "@excalidraw/element/typeChecks";
|
||||
|
||||
import { getCommonBounds } from "@excalidraw/element/bounds";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import StatsDragInput from "./DragInput";
|
||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
@@ -18,7 +15,6 @@ import { getElementsInAtomicUnit, moveElement } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type { AtomicUnit } from "./utils";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface MultiPositionProps {
|
||||
@@ -36,13 +32,11 @@ const moveElements = (
|
||||
property: MultiPositionProps["property"],
|
||||
changeInTopX: number,
|
||||
changeInTopY: number,
|
||||
elements: readonly ExcalidrawElement[],
|
||||
originalElements: readonly ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
for (let i = 0; i < originalElements.length; i++) {
|
||||
const origElement = originalElements[i];
|
||||
|
||||
const [cx, cy] = [
|
||||
@@ -65,8 +59,6 @@ const moveElements = (
|
||||
newTopLeftX,
|
||||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
@@ -78,11 +70,10 @@ const moveGroupTo = (
|
||||
nextX: number,
|
||||
nextY: number,
|
||||
originalElements: ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const [x1, y1, ,] = getCommonBounds(originalElements);
|
||||
const offsetX = nextX - x1;
|
||||
const offsetY = nextY - y1;
|
||||
@@ -112,8 +103,6 @@ const moveGroupTo = (
|
||||
topLeftX + offsetX,
|
||||
topLeftY + offsetY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
@@ -135,7 +124,6 @@ const handlePositionChange: DragInputCallbackType<
|
||||
originalAppState,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
|
||||
if (nextValue !== undefined) {
|
||||
for (const atomicUnit of getAtomicUnits(
|
||||
@@ -159,8 +147,6 @@ const handlePositionChange: DragInputCallbackType<
|
||||
newTopLeftX,
|
||||
newTopLeftY,
|
||||
elementsInUnit.map((el) => el.original),
|
||||
elementsMap,
|
||||
elements,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
@@ -188,8 +174,6 @@ const handlePositionChange: DragInputCallbackType<
|
||||
newTopLeftX,
|
||||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
@@ -214,8 +198,6 @@ const handlePositionChange: DragInputCallbackType<
|
||||
changeInTopX,
|
||||
changeInTopY,
|
||||
originalElements,
|
||||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
|
||||
@@ -4,16 +4,16 @@ import {
|
||||
getFlipAdjustedCropPosition,
|
||||
getUncroppedWidthAndHeight,
|
||||
} from "@excalidraw/element/cropElement";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import { isImageElement } from "@excalidraw/element/typeChecks";
|
||||
|
||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import StatsDragInput from "./DragInput";
|
||||
import { getStepSizedValue, moveElement } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface PositionProps {
|
||||
@@ -38,7 +38,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||
originalAppState,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const origElement = originalElements[0];
|
||||
const [cx, cy] = [
|
||||
origElement.x + origElement.width / 2,
|
||||
@@ -101,7 +100,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||
};
|
||||
}
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
crop: nextCrop,
|
||||
});
|
||||
|
||||
@@ -119,7 +118,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||
y: clamp(crop.y + changeInY, 0, crop.naturalHeight - crop.height),
|
||||
};
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
crop: nextCrop,
|
||||
});
|
||||
|
||||
@@ -133,8 +132,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||
newTopLeftX,
|
||||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
);
|
||||
@@ -166,8 +163,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||
newTopLeftX,
|
||||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import type {
|
||||
ExcalidrawTextElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { Excalidraw, getCommonBounds, mutateElement } from "../..";
|
||||
import { Excalidraw, getCommonBounds } from "../..";
|
||||
import { actionGroup } from "../../actions";
|
||||
import { t } from "../../i18n";
|
||||
import * as StaticScene from "../../renderer/staticScene";
|
||||
@@ -478,7 +478,7 @@ describe("stats for a non-generic element", () => {
|
||||
containerId: container.id,
|
||||
fontSize: 20,
|
||||
});
|
||||
mutateElement(container, {
|
||||
h.app.scene.mutateElement(container, {
|
||||
boundElements: [{ type: "text", id: text.id }],
|
||||
});
|
||||
API.setElements([container, text]);
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
bindOrUnbindLinearElements,
|
||||
updateBoundElements,
|
||||
} from "@excalidraw/element/binding";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||
import {
|
||||
isFrameLikeElement,
|
||||
@@ -24,10 +23,10 @@ import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
export type StatsInputProperty =
|
||||
@@ -119,12 +118,11 @@ export const moveElement = (
|
||||
newTopLeftX: number,
|
||||
newTopLeftY: number,
|
||||
originalElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scene: Scene,
|
||||
originalElementsMap: ElementsMap,
|
||||
shouldInformMutation = true,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const latestElement = elementsMap.get(originalElement.id);
|
||||
if (!latestElement) {
|
||||
return;
|
||||
@@ -148,15 +146,15 @@ export const moveElement = (
|
||||
-originalElement.angle as Radians,
|
||||
);
|
||||
|
||||
mutateElement(
|
||||
scene.mutateElement(
|
||||
latestElement,
|
||||
{
|
||||
x,
|
||||
y,
|
||||
},
|
||||
shouldInformMutation,
|
||||
{ informMutation: shouldInformMutation, isDragging: false },
|
||||
);
|
||||
updateBindings(latestElement, elementsMap, elements, scene);
|
||||
updateBindings(latestElement, scene);
|
||||
|
||||
const boundTextElement = getBoundTextElement(
|
||||
originalElement,
|
||||
@@ -165,13 +163,13 @@ export const moveElement = (
|
||||
if (boundTextElement) {
|
||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||
latestBoundTextElement &&
|
||||
mutateElement(
|
||||
scene.mutateElement(
|
||||
latestBoundTextElement,
|
||||
{
|
||||
x: boundTextElement.x + changeInX,
|
||||
y: boundTextElement.y + changeInY,
|
||||
},
|
||||
shouldInformMutation,
|
||||
{ informMutation: shouldInformMutation, isDragging: false },
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -199,8 +197,6 @@ export const getAtomicUnits = (
|
||||
|
||||
export const updateBindings = (
|
||||
latestElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scene: Scene,
|
||||
options?: {
|
||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||
@@ -209,16 +205,8 @@ export const updateBindings = (
|
||||
},
|
||||
) => {
|
||||
if (isLinearElement(latestElement)) {
|
||||
bindOrUnbindLinearElements(
|
||||
[latestElement],
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
true,
|
||||
[],
|
||||
options?.zoom,
|
||||
);
|
||||
bindOrUnbindLinearElements([latestElement], true, [], scene, options?.zoom);
|
||||
} else {
|
||||
updateBoundElements(latestElement, elementsMap, options);
|
||||
updateBoundElements(latestElement, scene, options);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,8 +21,6 @@ import {
|
||||
embeddableURLValidator,
|
||||
} from "@excalidraw/element/embeddable";
|
||||
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import {
|
||||
sceneCoordsToViewportCoords,
|
||||
viewportCoordsToSceneCoords,
|
||||
@@ -33,6 +31,8 @@ import {
|
||||
|
||||
import { isEmbeddableElement } from "@excalidraw/element/typeChecks";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawEmbeddableElement,
|
||||
@@ -70,14 +70,14 @@ const embeddableLinkCache = new Map<
|
||||
|
||||
export const Hyperlink = ({
|
||||
element,
|
||||
elementsMap,
|
||||
scene,
|
||||
setAppState,
|
||||
onLinkOpen,
|
||||
setToast,
|
||||
updateEmbedValidationStatus,
|
||||
}: {
|
||||
element: NonDeletedExcalidrawElement;
|
||||
elementsMap: ElementsMap;
|
||||
scene: Scene;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
onLinkOpen: ExcalidrawProps["onLinkOpen"];
|
||||
setToast: (
|
||||
@@ -88,6 +88,7 @@ export const Hyperlink = ({
|
||||
status: boolean,
|
||||
) => void;
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const appState = useExcalidrawAppState();
|
||||
const appProps = useAppProps();
|
||||
const device = useDevice();
|
||||
@@ -114,7 +115,7 @@ export const Hyperlink = ({
|
||||
setAppState({ activeEmbeddable: null });
|
||||
}
|
||||
if (!link) {
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
link: null,
|
||||
});
|
||||
updateEmbedValidationStatus(element, false);
|
||||
@@ -126,7 +127,7 @@ export const Hyperlink = ({
|
||||
setToast({ message: t("toast.unableToEmbed"), closable: true });
|
||||
}
|
||||
element.link && embeddableLinkCache.set(element.id, element.link);
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
link,
|
||||
});
|
||||
updateEmbedValidationStatus(element, false);
|
||||
@@ -144,7 +145,7 @@ export const Hyperlink = ({
|
||||
: 1;
|
||||
const hasLinkChanged =
|
||||
embeddableLinkCache.get(element.id) !== element.link;
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
...(hasLinkChanged
|
||||
? {
|
||||
width:
|
||||
@@ -169,10 +170,11 @@ export const Hyperlink = ({
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mutateElement(element, { link });
|
||||
scene.mutateElement(element, { link });
|
||||
}
|
||||
}, [
|
||||
element,
|
||||
scene,
|
||||
setToast,
|
||||
appProps.validateEmbeddable,
|
||||
appState.activeEmbeddable,
|
||||
@@ -229,9 +231,9 @@ export const Hyperlink = ({
|
||||
|
||||
const handleRemove = useCallback(() => {
|
||||
trackEvent("hyperlink", "delete");
|
||||
mutateElement(element, { link: null });
|
||||
scene.mutateElement(element, { link: null });
|
||||
setAppState({ showHyperlinkPopup: false });
|
||||
}, [setAppState, element]);
|
||||
}, [setAppState, element, scene]);
|
||||
|
||||
const onEdit = () => {
|
||||
trackEvent("hyperlink", "edit", "popup-ui");
|
||||
|
||||
Reference in New Issue
Block a user