Add NonDeleted<ExcalidrawElement> (#1068)
* add NonDeleted * make test:all script run tests without prompt * rename helper * replace with helper * make element contructors return nonDeleted elements * cache filtered elements where appliacable for better perf * rename manager element getter * remove unnecessary assertion * fix test * make element types in resizeElement into nonDeleted Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
c714c778ab
commit
df0613d8ac
@ -110,6 +110,7 @@
|
||||
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
||||
"start": "react-scripts start",
|
||||
"test": "npm run test:app",
|
||||
"test:all": "npm run test:typecheck && npm run test:code && npm run test:other && npm run test:app -- --watchAll=false",
|
||||
"test:update": "npm run test:app -- --updateSnapshot --watchAll=false",
|
||||
"test:app": "react-scripts test --env=jsdom --passWithNoTests",
|
||||
"test:code": "eslint --max-warnings=0 --ignore-path .gitignore --ext .js,.ts,.tsx .",
|
||||
|
@ -5,6 +5,7 @@ import React from "react";
|
||||
import { trash } from "../components/icons";
|
||||
import { t } from "../i18n";
|
||||
import { register } from "./register";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
|
||||
export const actionDeleteSelected = register({
|
||||
name: "deleteSelectedElements",
|
||||
@ -20,7 +21,10 @@ export const actionDeleteSelected = register({
|
||||
elementType: "selection",
|
||||
multiElement: null,
|
||||
},
|
||||
commitToHistory: isSomeElementSelected(elements, appState),
|
||||
commitToHistory: isSomeElementSelected(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
),
|
||||
};
|
||||
},
|
||||
contextItemLabel: "labels.delete",
|
||||
@ -33,7 +37,7 @@ export const actionDeleteSelected = register({
|
||||
title={t("labels.delete")}
|
||||
aria-label={t("labels.delete")}
|
||||
onClick={() => updateData(null)}
|
||||
visible={isSomeElementSelected(elements, appState)}
|
||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
@ -2,7 +2,7 @@ import React from "react";
|
||||
import { KEYS } from "../keys";
|
||||
import { register } from "./register";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { duplicateElement } from "../element";
|
||||
import { duplicateElement, getNonDeletedElements } from "../element";
|
||||
import { isSomeElementSelected } from "../scene";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { clone } from "../components/icons";
|
||||
@ -43,7 +43,7 @@ export const actionDuplicateSelection = register({
|
||||
)}`}
|
||||
aria-label={t("labels.duplicateSelection")}
|
||||
onClick={() => updateData(null)}
|
||||
visible={isSomeElementSelected(elements, appState)}
|
||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
@ -2,7 +2,7 @@ import React from "react";
|
||||
import { menu, palette } from "../components/icons";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { t } from "../i18n";
|
||||
import { showSelectedShapeActions } from "../element";
|
||||
import { showSelectedShapeActions, getNonDeletedElements } from "../element";
|
||||
import { register } from "./register";
|
||||
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
|
||||
import { KEYS } from "../keys";
|
||||
@ -39,7 +39,10 @@ export const actionToggleEditMenu = register({
|
||||
}),
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<ToolButton
|
||||
visible={showSelectedShapeActions(appState, elements)}
|
||||
visible={showSelectedShapeActions(
|
||||
appState,
|
||||
getNonDeletedElements(elements),
|
||||
)}
|
||||
type="button"
|
||||
icon={palette}
|
||||
aria-label={t("buttons.edit")}
|
||||
|
@ -5,7 +5,11 @@ import {
|
||||
isSomeElementSelected,
|
||||
} from "../scene";
|
||||
import { ButtonSelect } from "../components/ButtonSelect";
|
||||
import { isTextElement, redrawTextBoundingBox } from "../element";
|
||||
import {
|
||||
isTextElement,
|
||||
redrawTextBoundingBox,
|
||||
getNonDeletedElements,
|
||||
} from "../element";
|
||||
import { ColorPicker } from "../components/ColorPicker";
|
||||
import { AppState } from "../../src/types";
|
||||
import { t } from "../i18n";
|
||||
@ -33,10 +37,15 @@ const getFormValue = function <T>(
|
||||
defaultValue?: T,
|
||||
): T | null {
|
||||
const editingElement = appState.editingElement;
|
||||
const nonDeletedElements = getNonDeletedElements(elements);
|
||||
return (
|
||||
(editingElement && getAttribute(editingElement)) ??
|
||||
(isSomeElementSelected(elements, appState)
|
||||
? getCommonAttributeOfSelectedElements(elements, appState, getAttribute)
|
||||
(isSomeElementSelected(nonDeletedElements, appState)
|
||||
? getCommonAttributeOfSelectedElements(
|
||||
nonDeletedElements,
|
||||
appState,
|
||||
getAttribute,
|
||||
)
|
||||
: defaultValue) ??
|
||||
null
|
||||
);
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppState } from "../types";
|
||||
import { t } from "../i18n";
|
||||
import { globalSceneState } from "../scene";
|
||||
|
||||
export class ActionManager implements ActionsManagerInterface {
|
||||
actions = {} as ActionsManagerInterface["actions"];
|
||||
@ -17,16 +18,18 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
|
||||
getAppState: () => AppState;
|
||||
|
||||
getElements: () => readonly ExcalidrawElement[];
|
||||
getElementsIncludingDeleted: () => readonly ExcalidrawElement[];
|
||||
|
||||
constructor(
|
||||
updater: UpdaterFn,
|
||||
getAppState: () => AppState,
|
||||
getElements: () => readonly ExcalidrawElement[],
|
||||
getElementsIncludingDeleted: () => ReturnType<
|
||||
typeof globalSceneState["getElementsIncludingDeleted"]
|
||||
>,
|
||||
) {
|
||||
this.updater = updater;
|
||||
this.getAppState = getAppState;
|
||||
this.getElements = getElements;
|
||||
this.getElementsIncludingDeleted = getElementsIncludingDeleted;
|
||||
}
|
||||
|
||||
registerAction(action: Action) {
|
||||
@ -43,7 +46,11 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
.filter(
|
||||
(action) =>
|
||||
action.keyTest &&
|
||||
action.keyTest(event, this.getAppState(), this.getElements()),
|
||||
action.keyTest(
|
||||
event,
|
||||
this.getAppState(),
|
||||
this.getElementsIncludingDeleted(),
|
||||
),
|
||||
);
|
||||
|
||||
if (data.length === 0) {
|
||||
@ -51,12 +58,24 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
this.updater(data[0].perform(this.getElements(), this.getAppState(), null));
|
||||
this.updater(
|
||||
data[0].perform(
|
||||
this.getElementsIncludingDeleted(),
|
||||
this.getAppState(),
|
||||
null,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
executeAction(action: Action) {
|
||||
this.updater(action.perform(this.getElements(), this.getAppState(), null));
|
||||
this.updater(
|
||||
action.perform(
|
||||
this.getElementsIncludingDeleted(),
|
||||
this.getAppState(),
|
||||
null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getContextMenuItems(actionFilter: ActionFilterFn = (action) => action) {
|
||||
@ -72,7 +91,11 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
label: action.contextItemLabel ? t(action.contextItemLabel) : "",
|
||||
action: () => {
|
||||
this.updater(
|
||||
action.perform(this.getElements(), this.getAppState(), null),
|
||||
action.perform(
|
||||
this.getElementsIncludingDeleted(),
|
||||
this.getAppState(),
|
||||
null,
|
||||
),
|
||||
);
|
||||
},
|
||||
}));
|
||||
@ -84,13 +107,17 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
const PanelComponent = action.PanelComponent!;
|
||||
const updateData = (formState?: any) => {
|
||||
this.updater(
|
||||
action.perform(this.getElements(), this.getAppState(), formState),
|
||||
action.perform(
|
||||
this.getElementsIncludingDeleted(),
|
||||
this.getAppState(),
|
||||
formState,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PanelComponent
|
||||
elements={this.getElements()}
|
||||
elements={this.getElementsIncludingDeleted()}
|
||||
appState={this.getAppState()}
|
||||
updateData={updateData}
|
||||
/>
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { ExcalidrawElement } from "./element/types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "./element/types";
|
||||
import { getSelectedElements } from "./scene";
|
||||
import { AppState } from "./types";
|
||||
import { SVG_EXPORT_TAG } from "./scene/export";
|
||||
@ -19,7 +22,7 @@ export const probablySupportsClipboardBlob =
|
||||
"toBlob" in HTMLCanvasElement.prototype;
|
||||
|
||||
export async function copyToAppClipboard(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) {
|
||||
CLIPBOARD = JSON.stringify(getSelectedElements(elements, appState));
|
||||
|
@ -9,6 +9,7 @@ import { ToolButton } from "./ToolButton";
|
||||
import { capitalizeString, setCursorForShape } from "../utils";
|
||||
import Stack from "./Stack";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
|
||||
export function SelectedShapeActions({
|
||||
appState,
|
||||
@ -21,7 +22,10 @@ export function SelectedShapeActions({
|
||||
renderAction: ActionManager["renderAction"];
|
||||
elementType: ExcalidrawElement["type"];
|
||||
}) {
|
||||
const targetElements = getTargetElement(elements, appState);
|
||||
const targetElements = getTargetElement(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
const isEditing = Boolean(appState.editingElement);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
@ -82,13 +86,9 @@ export function SelectedShapeActions({
|
||||
export function ShapesSwitcher({
|
||||
elementType,
|
||||
setAppState,
|
||||
setElements,
|
||||
elements,
|
||||
}: {
|
||||
elementType: ExcalidrawElement["type"];
|
||||
setAppState: any;
|
||||
setElements: any;
|
||||
elements: readonly ExcalidrawElement[];
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
|
@ -20,7 +20,6 @@ import {
|
||||
getElementMap,
|
||||
getDrawingVersion,
|
||||
getSyncableElements,
|
||||
hasNonDeletedElements,
|
||||
newLinearElement,
|
||||
ResizeArrowFnType,
|
||||
resizeElements,
|
||||
@ -185,7 +184,7 @@ export class App extends React.Component<any, AppState> {
|
||||
this.actionManager = new ActionManager(
|
||||
this.syncActionResult,
|
||||
() => this.state,
|
||||
() => globalSceneState.getAllElements(),
|
||||
() => globalSceneState.getElementsIncludingDeleted(),
|
||||
);
|
||||
this.actionManager.registerAll(actions);
|
||||
|
||||
@ -209,10 +208,7 @@ export class App extends React.Component<any, AppState> {
|
||||
appState={this.state}
|
||||
setAppState={this.setAppState}
|
||||
actionManager={this.actionManager}
|
||||
elements={globalSceneState.getAllElements().filter((element) => {
|
||||
return !element.isDeleted;
|
||||
})}
|
||||
setElements={this.setElements}
|
||||
elements={globalSceneState.getElements()}
|
||||
onRoomCreate={this.openPortal}
|
||||
onRoomDestroy={this.closePortal}
|
||||
onLockToggle={this.toggleLock}
|
||||
@ -310,7 +306,7 @@ export class App extends React.Component<any, AppState> {
|
||||
try {
|
||||
await Promise.race([
|
||||
document.fonts?.ready?.then(() => {
|
||||
globalSceneState.getAllElements().forEach((element) => {
|
||||
globalSceneState.getElementsIncludingDeleted().forEach((element) => {
|
||||
if (isTextElement(element)) {
|
||||
invalidateShapeForElement(element);
|
||||
}
|
||||
@ -431,7 +427,7 @@ export class App extends React.Component<any, AppState> {
|
||||
}
|
||||
private onResize = withBatchedUpdates(() => {
|
||||
globalSceneState
|
||||
.getAllElements()
|
||||
.getElementsIncludingDeleted()
|
||||
.forEach((element) => invalidateShapeForElement(element));
|
||||
this.setState({});
|
||||
});
|
||||
@ -439,7 +435,7 @@ export class App extends React.Component<any, AppState> {
|
||||
private beforeUnload = withBatchedUpdates((event: BeforeUnloadEvent) => {
|
||||
if (
|
||||
this.state.isCollaborating &&
|
||||
hasNonDeletedElements(globalSceneState.getAllElements())
|
||||
globalSceneState.getElements().length > 0
|
||||
) {
|
||||
event.preventDefault();
|
||||
// NOTE: modern browsers no longer allow showing a custom message here
|
||||
@ -484,8 +480,9 @@ export class App extends React.Component<any, AppState> {
|
||||
);
|
||||
cursorButton[socketID] = user.button;
|
||||
});
|
||||
const elements = globalSceneState.getElements();
|
||||
const { atLeastOneVisibleElement, scrollBars } = renderScene(
|
||||
globalSceneState.getAllElements().filter((element) => {
|
||||
elements.filter((element) => {
|
||||
// don't render text element that's being currently edited (it's
|
||||
// rendered on remote only)
|
||||
return (
|
||||
@ -517,22 +514,20 @@ export class App extends React.Component<any, AppState> {
|
||||
if (scrollBars) {
|
||||
currentScrollBars = scrollBars;
|
||||
}
|
||||
const scrolledOutside =
|
||||
!atLeastOneVisibleElement &&
|
||||
hasNonDeletedElements(globalSceneState.getAllElements());
|
||||
const scrolledOutside = !atLeastOneVisibleElement && elements.length > 0;
|
||||
if (this.state.scrolledOutside !== scrolledOutside) {
|
||||
this.setState({ scrolledOutside: scrolledOutside });
|
||||
}
|
||||
this.saveDebounced();
|
||||
|
||||
if (
|
||||
getDrawingVersion(globalSceneState.getAllElements()) >
|
||||
getDrawingVersion(globalSceneState.getElementsIncludingDeleted()) >
|
||||
this.lastBroadcastedOrReceivedSceneVersion
|
||||
) {
|
||||
this.broadcastScene("SCENE_UPDATE");
|
||||
}
|
||||
|
||||
history.record(this.state, globalSceneState.getAllElements());
|
||||
history.record(this.state, globalSceneState.getElementsIncludingDeleted());
|
||||
}
|
||||
|
||||
// Copy/paste
|
||||
@ -543,7 +538,7 @@ export class App extends React.Component<any, AppState> {
|
||||
}
|
||||
this.copyAll();
|
||||
const { elements: nextElements, appState } = deleteSelectedElements(
|
||||
globalSceneState.getAllElements(),
|
||||
globalSceneState.getElementsIncludingDeleted(),
|
||||
this.state,
|
||||
);
|
||||
globalSceneState.replaceAllElements(nextElements);
|
||||
@ -561,19 +556,16 @@ export class App extends React.Component<any, AppState> {
|
||||
});
|
||||
|
||||
private copyAll = () => {
|
||||
copyToAppClipboard(globalSceneState.getAllElements(), this.state);
|
||||
copyToAppClipboard(globalSceneState.getElements(), this.state);
|
||||
};
|
||||
|
||||
private copyToClipboardAsPng = () => {
|
||||
const selectedElements = getSelectedElements(
|
||||
globalSceneState.getAllElements(),
|
||||
this.state,
|
||||
);
|
||||
const elements = globalSceneState.getElements();
|
||||
|
||||
const selectedElements = getSelectedElements(elements, this.state);
|
||||
exportCanvas(
|
||||
"clipboard",
|
||||
selectedElements.length
|
||||
? selectedElements
|
||||
: globalSceneState.getAllElements(),
|
||||
selectedElements.length ? selectedElements : elements,
|
||||
this.state,
|
||||
this.canvas!,
|
||||
this.state,
|
||||
@ -582,14 +574,14 @@ export class App extends React.Component<any, AppState> {
|
||||
|
||||
private copyToClipboardAsSvg = () => {
|
||||
const selectedElements = getSelectedElements(
|
||||
globalSceneState.getAllElements(),
|
||||
globalSceneState.getElements(),
|
||||
this.state,
|
||||
);
|
||||
exportCanvas(
|
||||
"clipboard-svg",
|
||||
selectedElements.length
|
||||
? selectedElements
|
||||
: globalSceneState.getAllElements(),
|
||||
: globalSceneState.getElements(),
|
||||
this.state,
|
||||
this.canvas!,
|
||||
this.state,
|
||||
@ -669,7 +661,7 @@ export class App extends React.Component<any, AppState> {
|
||||
);
|
||||
|
||||
globalSceneState.replaceAllElements([
|
||||
...globalSceneState.getAllElements(),
|
||||
...globalSceneState.getElementsIncludingDeleted(),
|
||||
...newElements,
|
||||
]);
|
||||
history.resumeRecording();
|
||||
@ -703,7 +695,7 @@ export class App extends React.Component<any, AppState> {
|
||||
});
|
||||
|
||||
globalSceneState.replaceAllElements([
|
||||
...globalSceneState.getAllElements(),
|
||||
...globalSceneState.getElementsIncludingDeleted(),
|
||||
element,
|
||||
]);
|
||||
this.setState({ selectedElementIds: { [element.id]: true } });
|
||||
@ -789,15 +781,15 @@ export class App extends React.Component<any, AppState> {
|
||||
// elements with more staler versions than ours, ignore them
|
||||
// and keep ours.
|
||||
if (
|
||||
globalSceneState.getAllElements() == null ||
|
||||
globalSceneState.getAllElements().length === 0
|
||||
globalSceneState.getElementsIncludingDeleted() == null ||
|
||||
globalSceneState.getElementsIncludingDeleted().length === 0
|
||||
) {
|
||||
globalSceneState.replaceAllElements(remoteElements);
|
||||
} else {
|
||||
// create a map of ids so we don't have to iterate
|
||||
// over the array more than once.
|
||||
const localElementMap = getElementMap(
|
||||
globalSceneState.getAllElements(),
|
||||
globalSceneState.getElementsIncludingDeleted(),
|
||||
);
|
||||
|
||||
// Reconcile
|
||||
@ -982,12 +974,14 @@ export class App extends React.Component<any, AppState> {
|
||||
const data: SocketUpdateDataSource[typeof sceneType] = {
|
||||
type: sceneType,
|
||||
payload: {
|
||||
elements: getSyncableElements(globalSceneState.getAllElements()),
|
||||
elements: getSyncableElements(
|
||||
globalSceneState.getElementsIncludingDeleted(),
|
||||
),
|
||||
},
|
||||
};
|
||||
this.lastBroadcastedOrReceivedSceneVersion = Math.max(
|
||||
this.lastBroadcastedOrReceivedSceneVersion,
|
||||
getDrawingVersion(globalSceneState.getAllElements()),
|
||||
getDrawingVersion(globalSceneState.getElementsIncludingDeleted()),
|
||||
);
|
||||
return this._broadcastSocketData(
|
||||
data as typeof data & { _brand: "socketUpdateData" },
|
||||
@ -1063,7 +1057,7 @@ export class App extends React.Component<any, AppState> {
|
||||
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
||||
: ELEMENT_TRANSLATE_AMOUNT;
|
||||
globalSceneState.replaceAllElements(
|
||||
globalSceneState.getAllElements().map((el) => {
|
||||
globalSceneState.getElementsIncludingDeleted().map((el) => {
|
||||
if (this.state.selectedElementIds[el.id]) {
|
||||
const update: { x?: number; y?: number } = {};
|
||||
if (event.key === KEYS.ARROW_LEFT) {
|
||||
@ -1083,7 +1077,7 @@ export class App extends React.Component<any, AppState> {
|
||||
event.preventDefault();
|
||||
} else if (event.key === KEYS.ENTER) {
|
||||
const selectedElements = getSelectedElements(
|
||||
globalSceneState.getAllElements(),
|
||||
globalSceneState.getElements(),
|
||||
this.state,
|
||||
);
|
||||
|
||||
@ -1188,7 +1182,7 @@ export class App extends React.Component<any, AppState> {
|
||||
|
||||
const deleteElement = () => {
|
||||
globalSceneState.replaceAllElements([
|
||||
...globalSceneState.getAllElements().map((_element) => {
|
||||
...globalSceneState.getElementsIncludingDeleted().map((_element) => {
|
||||
if (_element.id === element.id) {
|
||||
return newElementWith(_element, { isDeleted: true });
|
||||
}
|
||||
@ -1199,7 +1193,7 @@ export class App extends React.Component<any, AppState> {
|
||||
|
||||
const updateElement = (text: string) => {
|
||||
globalSceneState.replaceAllElements([
|
||||
...globalSceneState.getAllElements().map((_element) => {
|
||||
...globalSceneState.getElementsIncludingDeleted().map((_element) => {
|
||||
if (_element.id === element.id) {
|
||||
return newTextElement({
|
||||
...(_element as ExcalidrawTextElement),
|
||||
@ -1271,7 +1265,7 @@ export class App extends React.Component<any, AppState> {
|
||||
centerIfPossible?: boolean;
|
||||
}) => {
|
||||
const elementAtPosition = getElementAtPosition(
|
||||
globalSceneState.getAllElements(),
|
||||
globalSceneState.getElements(),
|
||||
this.state,
|
||||
x,
|
||||
y,
|
||||
@ -1326,7 +1320,7 @@ export class App extends React.Component<any, AppState> {
|
||||
});
|
||||
} else {
|
||||
globalSceneState.replaceAllElements([
|
||||
...globalSceneState.getAllElements(),
|
||||
...globalSceneState.getElementsIncludingDeleted(),
|
||||
element,
|
||||
]);
|
||||
|
||||
@ -1503,13 +1497,12 @@ export class App extends React.Component<any, AppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedElements = getSelectedElements(
|
||||
globalSceneState.getAllElements(),
|
||||
this.state,
|
||||
);
|
||||
const elements = globalSceneState.getElements();
|
||||
|
||||
const selectedElements = getSelectedElements(elements, this.state);
|
||||
if (selectedElements.length === 1 && !isOverScrollBar) {
|
||||
const elementWithResizeHandler = getElementWithResizeHandler(
|
||||
globalSceneState.getAllElements(),
|
||||
elements,
|
||||
this.state,
|
||||
{ x, y },
|
||||
this.state.zoom,
|
||||
@ -1538,7 +1531,7 @@ export class App extends React.Component<any, AppState> {
|
||||
}
|
||||
}
|
||||
const hitElement = getElementAtPosition(
|
||||
globalSceneState.getAllElements(),
|
||||
elements,
|
||||
this.state,
|
||||
x,
|
||||
y,
|
||||
@ -1737,13 +1730,11 @@ export class App extends React.Component<any, AppState> {
|
||||
let hitElement: ExcalidrawElement | null = null;
|
||||
let hitElementWasAddedToSelection = false;
|
||||
if (this.state.elementType === "selection") {
|
||||
const selectedElements = getSelectedElements(
|
||||
globalSceneState.getAllElements(),
|
||||
this.state,
|
||||
);
|
||||
const elements = globalSceneState.getElements();
|
||||
const selectedElements = getSelectedElements(elements, this.state);
|
||||
if (selectedElements.length === 1) {
|
||||
const elementWithResizeHandler = getElementWithResizeHandler(
|
||||
globalSceneState.getAllElements(),
|
||||
elements,
|
||||
this.state,
|
||||
{ x, y },
|
||||
this.state.zoom,
|
||||
@ -1781,7 +1772,7 @@ export class App extends React.Component<any, AppState> {
|
||||
}
|
||||
if (!isResizingElements) {
|
||||
hitElement = getElementAtPosition(
|
||||
globalSceneState.getAllElements(),
|
||||
elements,
|
||||
this.state,
|
||||
x,
|
||||
y,
|
||||
@ -1809,7 +1800,7 @@ export class App extends React.Component<any, AppState> {
|
||||
},
|
||||
}));
|
||||
globalSceneState.replaceAllElements(
|
||||
globalSceneState.getAllElements(),
|
||||
globalSceneState.getElementsIncludingDeleted(),
|
||||
);
|
||||
hitElementWasAddedToSelection = true;
|
||||
}
|
||||
@ -1820,7 +1811,7 @@ export class App extends React.Component<any, AppState> {
|
||||
// put the duplicates where the selected elements used to be.
|
||||
const nextElements = [];
|
||||
const elementsToAppend = [];
|
||||
for (const element of globalSceneState.getAllElements()) {
|
||||
for (const element of globalSceneState.getElementsIncludingDeleted()) {
|
||||
if (
|
||||
this.state.selectedElementIds[element.id] ||
|
||||
(element.id === hitElement.id && hitElementWasAddedToSelection)
|
||||
@ -1930,7 +1921,7 @@ export class App extends React.Component<any, AppState> {
|
||||
points: [...element.points, [0, 0]],
|
||||
});
|
||||
globalSceneState.replaceAllElements([
|
||||
...globalSceneState.getAllElements(),
|
||||
...globalSceneState.getElementsIncludingDeleted(),
|
||||
element,
|
||||
]);
|
||||
this.setState({
|
||||
@ -1958,7 +1949,7 @@ export class App extends React.Component<any, AppState> {
|
||||
});
|
||||
} else {
|
||||
globalSceneState.replaceAllElements([
|
||||
...globalSceneState.getAllElements(),
|
||||
...globalSceneState.getElementsIncludingDeleted(),
|
||||
element,
|
||||
]);
|
||||
this.setState({
|
||||
@ -2047,7 +2038,7 @@ export class App extends React.Component<any, AppState> {
|
||||
// if elements should be deselected on pointerup
|
||||
draggingOccurred = true;
|
||||
const selectedElements = getSelectedElements(
|
||||
globalSceneState.getAllElements(),
|
||||
globalSceneState.getElements(),
|
||||
this.state,
|
||||
);
|
||||
if (selectedElements.length > 0) {
|
||||
@ -2123,14 +2114,12 @@ export class App extends React.Component<any, AppState> {
|
||||
}
|
||||
|
||||
if (this.state.elementType === "selection") {
|
||||
if (
|
||||
!event.shiftKey &&
|
||||
isSomeElementSelected(globalSceneState.getAllElements(), this.state)
|
||||
) {
|
||||
const elements = globalSceneState.getElements();
|
||||
if (!event.shiftKey && isSomeElementSelected(elements, this.state)) {
|
||||
this.setState({ selectedElementIds: {} });
|
||||
}
|
||||
const elementsWithinSelection = getElementsWithinSelection(
|
||||
globalSceneState.getAllElements(),
|
||||
elements,
|
||||
draggingElement,
|
||||
);
|
||||
this.setState((prevState) => ({
|
||||
@ -2223,7 +2212,7 @@ export class App extends React.Component<any, AppState> {
|
||||
) {
|
||||
// remove invisible element which was added in onPointerDown
|
||||
globalSceneState.replaceAllElements(
|
||||
globalSceneState.getAllElements().slice(0, -1),
|
||||
globalSceneState.getElementsIncludingDeleted().slice(0, -1),
|
||||
);
|
||||
this.setState({
|
||||
draggingElement: null,
|
||||
@ -2240,7 +2229,7 @@ export class App extends React.Component<any, AppState> {
|
||||
if (resizingElement && isInvisiblySmallElement(resizingElement)) {
|
||||
globalSceneState.replaceAllElements(
|
||||
globalSceneState
|
||||
.getAllElements()
|
||||
.getElementsIncludingDeleted()
|
||||
.filter((el) => el.id !== resizingElement.id),
|
||||
);
|
||||
}
|
||||
@ -2285,7 +2274,7 @@ export class App extends React.Component<any, AppState> {
|
||||
|
||||
if (
|
||||
elementType !== "selection" ||
|
||||
isSomeElementSelected(globalSceneState.getAllElements(), this.state)
|
||||
isSomeElementSelected(globalSceneState.getElements(), this.state)
|
||||
) {
|
||||
history.resumeRecording();
|
||||
}
|
||||
@ -2366,8 +2355,9 @@ export class App extends React.Component<any, AppState> {
|
||||
window.devicePixelRatio,
|
||||
);
|
||||
|
||||
const elements = globalSceneState.getElements();
|
||||
const element = getElementAtPosition(
|
||||
globalSceneState.getAllElements(),
|
||||
elements,
|
||||
this.state,
|
||||
x,
|
||||
y,
|
||||
@ -2381,12 +2371,12 @@ export class App extends React.Component<any, AppState> {
|
||||
action: () => this.pasteFromClipboard(null),
|
||||
},
|
||||
probablySupportsClipboardBlob &&
|
||||
hasNonDeletedElements(globalSceneState.getAllElements()) && {
|
||||
elements.length > 0 && {
|
||||
label: t("labels.copyAsPng"),
|
||||
action: this.copyToClipboardAsPng,
|
||||
},
|
||||
probablySupportsClipboardWriteText &&
|
||||
hasNonDeletedElements(globalSceneState.getAllElements()) && {
|
||||
elements.length > 0 && {
|
||||
label: t("labels.copyAsSvg"),
|
||||
action: this.copyToClipboardAsSvg,
|
||||
},
|
||||
@ -2468,7 +2458,7 @@ export class App extends React.Component<any, AppState> {
|
||||
scale: number,
|
||||
) {
|
||||
const elementClickedInside = getElementContainingPosition(
|
||||
globalSceneState.getAllElements(),
|
||||
globalSceneState.getElementsIncludingDeleted(),
|
||||
x,
|
||||
y,
|
||||
);
|
||||
@ -2522,7 +2512,10 @@ export class App extends React.Component<any, AppState> {
|
||||
}, 300);
|
||||
|
||||
private saveDebounced = debounce(() => {
|
||||
saveToLocalStorage(globalSceneState.getAllElements(), this.state);
|
||||
saveToLocalStorage(
|
||||
globalSceneState.getElementsIncludingDeleted(),
|
||||
this.state,
|
||||
);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
@ -2548,7 +2541,7 @@ if (process.env.NODE_ENV === "test" || process.env.NODE_ENV === "development") {
|
||||
Object.defineProperties(window.h, {
|
||||
elements: {
|
||||
get() {
|
||||
return globalSceneState.getAllElements();
|
||||
return globalSceneState.getElementsIncludingDeleted();
|
||||
},
|
||||
set(elements: ExcalidrawElement[]) {
|
||||
return globalSceneState.replaceAllElements(elements);
|
||||
|
@ -4,7 +4,7 @@ import React, { useState, useEffect, useRef } from "react";
|
||||
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { clipboard, exportFile, link } from "./icons";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { AppState } from "../types";
|
||||
import { exportToCanvas } from "../scene/export";
|
||||
import { ActionsManagerInterface } from "../actions/types";
|
||||
@ -20,7 +20,7 @@ const scales = [1, 2, 3];
|
||||
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
|
||||
|
||||
export type ExportCB = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scale?: number,
|
||||
) => void;
|
||||
|
||||
@ -35,7 +35,7 @@ function ExportModal({
|
||||
onExportToBackend,
|
||||
}: {
|
||||
appState: AppState;
|
||||
elements: readonly ExcalidrawElement[];
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
exportPadding?: number;
|
||||
actionManager: ActionsManagerInterface;
|
||||
onExportToPng: ExportCB;
|
||||
@ -166,7 +166,7 @@ export function ExportDialog({
|
||||
onExportToBackend,
|
||||
}: {
|
||||
appState: AppState;
|
||||
elements: readonly ExcalidrawElement[];
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
exportPadding?: number;
|
||||
actionManager: ActionsManagerInterface;
|
||||
onExportToPng: ExportCB;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { t } from "../i18n";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { getSelectedElements } from "../scene";
|
||||
|
||||
import "./HintViewer.scss";
|
||||
@ -9,7 +9,7 @@ import { isLinearElement } from "../element/typeChecks";
|
||||
|
||||
interface Hint {
|
||||
appState: AppState;
|
||||
elements: readonly ExcalidrawElement[];
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
}
|
||||
|
||||
const getHints = ({ appState, elements }: Hint) => {
|
||||
|
@ -4,7 +4,7 @@ import { calculateScrollCenter } from "../scene";
|
||||
import { exportCanvas } from "../data";
|
||||
|
||||
import { AppState } from "../types";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { Island } from "./Island";
|
||||
@ -31,8 +31,7 @@ interface LayerUIProps {
|
||||
appState: AppState;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
setAppState: any;
|
||||
elements: readonly ExcalidrawElement[];
|
||||
setElements: (elements: readonly ExcalidrawElement[]) => void;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
onRoomCreate: () => void;
|
||||
onRoomDestroy: () => void;
|
||||
onLockToggle: () => void;
|
||||
@ -45,7 +44,6 @@ export const LayerUI = React.memo(
|
||||
setAppState,
|
||||
canvas,
|
||||
elements,
|
||||
setElements,
|
||||
onRoomCreate,
|
||||
onRoomDestroy,
|
||||
onLockToggle,
|
||||
@ -96,7 +94,6 @@ export const LayerUI = React.memo(
|
||||
<MobileMenu
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
setElements={setElements}
|
||||
actionManager={actionManager}
|
||||
exportButton={renderExportDialog()}
|
||||
setAppState={setAppState}
|
||||
@ -170,8 +167,6 @@ export const LayerUI = React.memo(
|
||||
<ShapesSwitcher
|
||||
elementType={appState.elementType}
|
||||
setAppState={setAppState}
|
||||
setElements={setElements}
|
||||
elements={elements}
|
||||
/>
|
||||
</Stack.Row>
|
||||
</Island>
|
||||
|
@ -5,7 +5,7 @@ import { t, setLanguage } from "../i18n";
|
||||
import Stack from "./Stack";
|
||||
import { LanguageList } from "./LanguageList";
|
||||
import { showSelectedShapeActions } from "../element";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { FixedSideContainer } from "./FixedSideContainer";
|
||||
import { Island } from "./Island";
|
||||
import { HintViewer } from "./HintViewer";
|
||||
@ -22,8 +22,7 @@ type MobileMenuProps = {
|
||||
actionManager: ActionManager;
|
||||
exportButton: React.ReactNode;
|
||||
setAppState: any;
|
||||
elements: readonly ExcalidrawElement[];
|
||||
setElements: any;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
onRoomCreate: () => void;
|
||||
onRoomDestroy: () => void;
|
||||
onLockToggle: () => void;
|
||||
@ -32,7 +31,6 @@ type MobileMenuProps = {
|
||||
export function MobileMenu({
|
||||
appState,
|
||||
elements,
|
||||
setElements,
|
||||
actionManager,
|
||||
exportButton,
|
||||
setAppState,
|
||||
@ -54,8 +52,6 @@ export function MobileMenu({
|
||||
<ShapesSwitcher
|
||||
elementType={appState.elementType}
|
||||
setAppState={setAppState}
|
||||
setElements={setElements}
|
||||
elements={elements}
|
||||
/>
|
||||
</Stack.Row>
|
||||
</Island>
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
|
||||
import { getDefaultAppState } from "../appState";
|
||||
|
||||
@ -16,7 +19,6 @@ import { serializeAsJSON } from "./json";
|
||||
import { ExportType } from "../scene/types";
|
||||
import { restore } from "./restore";
|
||||
import { restoreFromLocalStorage } from "./localStorage";
|
||||
import { hasNonDeletedElements } from "../element";
|
||||
|
||||
export { loadFromBlob } from "./blob";
|
||||
export { saveAsJSON, loadFromJSON } from "./json";
|
||||
@ -283,7 +285,7 @@ export async function importFromBackend(
|
||||
|
||||
export async function exportCanvas(
|
||||
type: ExportType,
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
canvas: HTMLCanvasElement,
|
||||
{
|
||||
@ -300,7 +302,7 @@ export async function exportCanvas(
|
||||
scale?: number;
|
||||
},
|
||||
) {
|
||||
if (!hasNonDeletedElements(elements)) {
|
||||
if (elements.length === 0) {
|
||||
return window.alert(t("alerts.cannotExportEmptyCanvas"));
|
||||
}
|
||||
if (type === "svg" || type === "clipboard-svg") {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { distanceBetweenPointAndSegment } from "../math";
|
||||
|
||||
import { ExcalidrawElement } from "./types";
|
||||
import { NonDeletedExcalidrawElement } from "./types";
|
||||
|
||||
import { getDiamondPoints, getElementAbsoluteCoords } from "./bounds";
|
||||
import { Point } from "../types";
|
||||
@ -11,7 +11,7 @@ import { isLinearElement } from "./typeChecks";
|
||||
import { rotate } from "../math";
|
||||
|
||||
function isElementDraggableFromInside(
|
||||
element: ExcalidrawElement,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
appState: AppState,
|
||||
): boolean {
|
||||
return (
|
||||
@ -21,7 +21,7 @@ function isElementDraggableFromInside(
|
||||
}
|
||||
|
||||
export function hitTest(
|
||||
element: ExcalidrawElement,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
appState: AppState,
|
||||
x: number,
|
||||
y: number,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ExcalidrawElement } from "./types";
|
||||
import { ExcalidrawElement, NonDeletedExcalidrawElement } from "./types";
|
||||
import { isInvisiblySmallElement } from "./sizeHelpers";
|
||||
|
||||
export {
|
||||
@ -63,6 +63,9 @@ export function getDrawingVersion(elements: readonly ExcalidrawElement[]) {
|
||||
return elements.reduce((acc, el) => acc + el.version, 0);
|
||||
}
|
||||
|
||||
export function hasNonDeletedElements(elements: readonly ExcalidrawElement[]) {
|
||||
return elements.some((element) => !element.isDeleted);
|
||||
export function getNonDeletedElements(elements: readonly ExcalidrawElement[]) {
|
||||
return (
|
||||
elements.filter((element) => !element.isDeleted) as
|
||||
readonly NonDeletedExcalidrawElement[]
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawGenericElement,
|
||||
NonDeleted,
|
||||
} from "../element/types";
|
||||
import { measureText } from "../utils";
|
||||
import { randomInteger, randomId } from "../random";
|
||||
@ -56,7 +57,7 @@ function _newElementBase<T extends ExcalidrawElement>(
|
||||
seed: rest.seed ?? randomInteger(),
|
||||
version: rest.version || 1,
|
||||
versionNonce: rest.versionNonce ?? 0,
|
||||
isDeleted: rest.isDeleted ?? false,
|
||||
isDeleted: false as false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -64,7 +65,7 @@ export function newElement(
|
||||
opts: {
|
||||
type: ExcalidrawGenericElement["type"];
|
||||
} & ElementConstructorOpts,
|
||||
): ExcalidrawGenericElement {
|
||||
): NonDeleted<ExcalidrawGenericElement> {
|
||||
return _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
||||
}
|
||||
|
||||
@ -73,13 +74,12 @@ export function newTextElement(
|
||||
text: string;
|
||||
font: string;
|
||||
} & ElementConstructorOpts,
|
||||
): ExcalidrawTextElement {
|
||||
): NonDeleted<ExcalidrawTextElement> {
|
||||
const { text, font } = opts;
|
||||
const metrics = measureText(text, font);
|
||||
const textElement = newElementWith(
|
||||
{
|
||||
..._newElementBase<ExcalidrawTextElement>("text", opts),
|
||||
isDeleted: false,
|
||||
text: text,
|
||||
font: font,
|
||||
// Center the text
|
||||
@ -100,7 +100,7 @@ export function newLinearElement(
|
||||
type: ExcalidrawLinearElement["type"];
|
||||
lastCommittedPoint?: ExcalidrawLinearElement["lastCommittedPoint"];
|
||||
} & ElementConstructorOpts,
|
||||
): ExcalidrawLinearElement {
|
||||
): NonDeleted<ExcalidrawLinearElement> {
|
||||
return {
|
||||
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
||||
points: [],
|
||||
|
@ -3,7 +3,11 @@ import { SHIFT_LOCKING_ANGLE } from "../constants";
|
||||
import { getSelectedElements, globalSceneState } from "../scene";
|
||||
import { rescalePoints } from "../points";
|
||||
import { rotate, adjustXYWithRotation } from "../math";
|
||||
import { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
|
||||
import {
|
||||
ExcalidrawLinearElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeleted,
|
||||
} from "./types";
|
||||
import { getElementAbsoluteCoords, getCommonBounds } from "./bounds";
|
||||
import { isLinearElement } from "./typeChecks";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
@ -17,7 +21,7 @@ import {
|
||||
type ResizeTestType = ReturnType<typeof resizeTest>;
|
||||
|
||||
export type ResizeArrowFnType = (
|
||||
element: ExcalidrawLinearElement,
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
pointIndex: number,
|
||||
deltaX: number,
|
||||
deltaY: number,
|
||||
@ -27,13 +31,13 @@ export type ResizeArrowFnType = (
|
||||
) => void;
|
||||
|
||||
const arrowResizeOrigin: ResizeArrowFnType = (
|
||||
element: ExcalidrawLinearElement,
|
||||
pointIndex: number,
|
||||
deltaX: number,
|
||||
deltaY: number,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
perfect: boolean,
|
||||
element,
|
||||
pointIndex,
|
||||
deltaX,
|
||||
deltaY,
|
||||
pointerX,
|
||||
pointerY,
|
||||
perfect,
|
||||
) => {
|
||||
const [px, py] = element.points[pointIndex];
|
||||
let x = element.x + deltaX;
|
||||
@ -63,13 +67,13 @@ const arrowResizeOrigin: ResizeArrowFnType = (
|
||||
};
|
||||
|
||||
const arrowResizeEnd: ResizeArrowFnType = (
|
||||
element: ExcalidrawLinearElement,
|
||||
pointIndex: number,
|
||||
deltaX: number,
|
||||
deltaY: number,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
perfect: boolean,
|
||||
element,
|
||||
pointIndex,
|
||||
deltaX,
|
||||
deltaY,
|
||||
pointerX,
|
||||
pointerY,
|
||||
perfect,
|
||||
) => {
|
||||
const [px, py] = element.points[pointIndex];
|
||||
if (perfect) {
|
||||
@ -110,7 +114,7 @@ export function resizeElements(
|
||||
isRotating: resizeHandle === "rotation",
|
||||
});
|
||||
const selectedElements = getSelectedElements(
|
||||
globalSceneState.getAllElements(),
|
||||
globalSceneState.getElements(),
|
||||
appState,
|
||||
);
|
||||
if (selectedElements.length === 1) {
|
||||
@ -451,7 +455,7 @@ export function resizeElements(
|
||||
}
|
||||
|
||||
export function canResizeMutlipleElements(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
) {
|
||||
return elements.every((element) =>
|
||||
["rectangle", "diamond", "ellipse"].includes(element.type),
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { ExcalidrawElement, PointerType } from "./types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
PointerType,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "./types";
|
||||
|
||||
import {
|
||||
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
||||
@ -24,7 +28,7 @@ function isInHandlerRect(
|
||||
}
|
||||
|
||||
export function resizeTest(
|
||||
element: ExcalidrawElement,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
appState: AppState,
|
||||
x: number,
|
||||
y: number,
|
||||
@ -66,7 +70,7 @@ export function resizeTest(
|
||||
}
|
||||
|
||||
export function getElementWithResizeHandler(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
{ x, y }: { x: number; y: number },
|
||||
zoom: number,
|
||||
@ -78,7 +82,7 @@ export function getElementWithResizeHandler(
|
||||
}
|
||||
const resizeHandle = resizeTest(element, appState, x, y, zoom, pointerType);
|
||||
return resizeHandle ? { element, resizeHandle } : null;
|
||||
}, null as { element: ExcalidrawElement; resizeHandle: ReturnType<typeof resizeTest> } | null);
|
||||
}, null as { element: NonDeletedExcalidrawElement; resizeHandle: ReturnType<typeof resizeTest> } | null);
|
||||
}
|
||||
|
||||
export function getResizeHandlerFromCoords(
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { AppState } from "../types";
|
||||
import { ExcalidrawElement } from "./types";
|
||||
import { NonDeletedExcalidrawElement } from "./types";
|
||||
import { getSelectedElements } from "../scene";
|
||||
|
||||
export const showSelectedShapeActions = (
|
||||
appState: AppState,
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
) =>
|
||||
Boolean(
|
||||
appState.editingElement ||
|
||||
|
@ -33,6 +33,12 @@ export type ExcalidrawElement =
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawLinearElement;
|
||||
|
||||
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
|
||||
isDeleted: false;
|
||||
};
|
||||
|
||||
export type NonDeletedExcalidrawElement = NonDeleted<ExcalidrawElement>;
|
||||
|
||||
export type ExcalidrawTextElement = _ExcalidrawElementBase &
|
||||
Readonly<{
|
||||
type: "text";
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawTextElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import { isTextElement } from "../element/typeChecks";
|
||||
import {
|
||||
getDiamondPoints,
|
||||
@ -24,7 +28,7 @@ export interface ExcalidrawElementWithCanvas {
|
||||
}
|
||||
|
||||
function generateElementCanvas(
|
||||
element: ExcalidrawElement,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
zoom: number,
|
||||
): ExcalidrawElementWithCanvas {
|
||||
const canvas = document.createElement("canvas");
|
||||
@ -72,7 +76,7 @@ function generateElementCanvas(
|
||||
}
|
||||
|
||||
function drawElementOnCanvas(
|
||||
element: ExcalidrawElement,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
rc: RoughCanvas,
|
||||
context: CanvasRenderingContext2D,
|
||||
) {
|
||||
@ -133,7 +137,7 @@ export function invalidateShapeForElement(element: ExcalidrawElement) {
|
||||
}
|
||||
|
||||
function generateElement(
|
||||
element: ExcalidrawElement,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
generator: RoughGenerator,
|
||||
sceneState?: SceneState,
|
||||
) {
|
||||
@ -285,7 +289,7 @@ function drawElementFromCanvas(
|
||||
}
|
||||
|
||||
export function renderElement(
|
||||
element: ExcalidrawElement,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
rc: RoughCanvas,
|
||||
context: CanvasRenderingContext2D,
|
||||
renderOptimizations: boolean,
|
||||
@ -342,7 +346,7 @@ export function renderElement(
|
||||
}
|
||||
|
||||
export function renderElementToSvg(
|
||||
element: ExcalidrawElement,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
rsvg: RoughSVG,
|
||||
svgRoot: SVGElement,
|
||||
offsetX?: number,
|
||||
|
@ -2,7 +2,10 @@ import { RoughCanvas } from "roughjs/bin/canvas";
|
||||
import { RoughSVG } from "roughjs/bin/svg";
|
||||
|
||||
import { FlooredNumber, AppState } from "../types";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import {
|
||||
getElementAbsoluteCoords,
|
||||
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
||||
@ -74,9 +77,9 @@ function strokeCircle(
|
||||
}
|
||||
|
||||
export function renderScene(
|
||||
allElements: readonly ExcalidrawElement[],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
selectionElement: ExcalidrawElement | null,
|
||||
selectionElement: NonDeletedExcalidrawElement | null,
|
||||
scale: number,
|
||||
rc: RoughCanvas,
|
||||
canvas: HTMLCanvasElement,
|
||||
@ -99,8 +102,6 @@ export function renderScene(
|
||||
return { atLeastOneVisibleElement: false };
|
||||
}
|
||||
|
||||
const elements = allElements.filter((element) => !element.isDeleted);
|
||||
|
||||
const context = canvas.getContext("2d")!;
|
||||
context.scale(scale, scale);
|
||||
|
||||
@ -493,7 +494,7 @@ function isVisibleElement(
|
||||
|
||||
// This should be only called for exporting purposes
|
||||
export function renderSceneToSvg(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
rsvg: RoughSVG,
|
||||
svgRoot: SVGElement,
|
||||
{
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
|
||||
import { getElementAbsoluteCoords, hitTest } from "../element";
|
||||
import { AppState } from "../types";
|
||||
@ -16,7 +19,7 @@ export const hasStroke = (type: string) =>
|
||||
export const hasText = (type: string) => type === "text";
|
||||
|
||||
export function getElementAtPosition(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
x: number,
|
||||
y: number,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import rough from "roughjs/bin/rough";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { getCommonBounds } from "../element/bounds";
|
||||
import { renderScene, renderSceneToSvg } from "../renderer/renderScene";
|
||||
import { distance, SVG_NS } from "../utils";
|
||||
@ -9,7 +9,7 @@ import { AppState } from "../types";
|
||||
export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||
|
||||
export function exportToCanvas(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
{
|
||||
exportBackground,
|
||||
@ -66,7 +66,7 @@ export function exportToCanvas(
|
||||
}
|
||||
|
||||
export function exportToSvg(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
{
|
||||
exportBackground,
|
||||
exportPadding = 10,
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
|
||||
export interface SceneStateCallback {
|
||||
(): void;
|
||||
@ -8,15 +12,19 @@ export interface SceneStateCallbackRemover {
|
||||
(): void;
|
||||
}
|
||||
|
||||
class SceneState {
|
||||
class GlobalScene {
|
||||
private callbacks: Set<SceneStateCallback> = new Set();
|
||||
|
||||
constructor(private _elements: readonly ExcalidrawElement[] = []) {}
|
||||
|
||||
getAllElements() {
|
||||
getElementsIncludingDeleted() {
|
||||
return this._elements;
|
||||
}
|
||||
|
||||
getElements(): readonly NonDeletedExcalidrawElement[] {
|
||||
return getNonDeletedElements(this._elements);
|
||||
}
|
||||
|
||||
replaceAllElements(nextElements: readonly ExcalidrawElement[]) {
|
||||
this._elements = nextElements;
|
||||
this.informMutation();
|
||||
@ -44,4 +52,4 @@ class SceneState {
|
||||
}
|
||||
}
|
||||
|
||||
export const globalSceneState = new SceneState();
|
||||
export const globalSceneState = new GlobalScene();
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import { getElementAbsoluteCoords, getElementBounds } from "../element";
|
||||
import { AppState } from "../types";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
|
||||
export function getElementsWithinSelection(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
selection: ExcalidrawElement,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
selection: NonDeletedExcalidrawElement,
|
||||
) {
|
||||
const [
|
||||
selectionX1,
|
||||
@ -47,7 +50,7 @@ export function deleteSelectedElements(
|
||||
}
|
||||
|
||||
export function isSomeElementSelected(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
): boolean {
|
||||
return elements.some((element) => appState.selectedElementIds[element.id]);
|
||||
@ -58,7 +61,7 @@ export function isSomeElementSelected(
|
||||
* elements. If elements don't share the same value, returns `null`.
|
||||
*/
|
||||
export function getCommonAttributeOfSelectedElements<T>(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
getAttribute: (element: ExcalidrawElement) => T,
|
||||
): T | null {
|
||||
@ -73,14 +76,14 @@ export function getCommonAttributeOfSelectedElements<T>(
|
||||
}
|
||||
|
||||
export function getSelectedElements(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
): readonly ExcalidrawElement[] {
|
||||
) {
|
||||
return elements.filter((element) => appState.selectedElementIds[element.id]);
|
||||
}
|
||||
|
||||
export function getTargetElement(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) {
|
||||
return appState.editingElement
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
actionBringToFront,
|
||||
actionSendToBack,
|
||||
} from "../actions";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
|
||||
// Unmount ReactDOM from root
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
@ -27,7 +28,7 @@ function populateElements(
|
||||
const selectedElementIds: any = {};
|
||||
|
||||
h.elements = elements.map(({ id, isDeleted = false, isSelected = false }) => {
|
||||
const element: Mutable<ReturnType<typeof newElement>> = newElement({
|
||||
const element: Mutable<ExcalidrawElement> = newElement({
|
||||
type: "rectangle",
|
||||
x: 100,
|
||||
y: 100,
|
||||
|
13
src/types.ts
13
src/types.ts
@ -1,7 +1,8 @@
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
PointerType,
|
||||
ExcalidrawLinearElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeleted,
|
||||
} from "./element/types";
|
||||
import { SHAPES } from "./shapes";
|
||||
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
@ -12,13 +13,13 @@ export type Point = Readonly<RoughPoint>;
|
||||
export type AppState = {
|
||||
isLoading: boolean;
|
||||
errorMessage: string | null;
|
||||
draggingElement: ExcalidrawElement | null;
|
||||
resizingElement: ExcalidrawElement | null;
|
||||
multiElement: ExcalidrawLinearElement | null;
|
||||
selectionElement: ExcalidrawElement | null;
|
||||
draggingElement: NonDeletedExcalidrawElement | null;
|
||||
resizingElement: NonDeletedExcalidrawElement | null;
|
||||
multiElement: NonDeleted<ExcalidrawLinearElement> | null;
|
||||
selectionElement: NonDeletedExcalidrawElement | null;
|
||||
// element being edited, but not necessarily added to elements array yet
|
||||
// (e.g. text element when typing into the input)
|
||||
editingElement: ExcalidrawElement | null;
|
||||
editingElement: NonDeletedExcalidrawElement | null;
|
||||
elementType: typeof SHAPES[number]["value"];
|
||||
elementLocked: boolean;
|
||||
exportBackground: boolean;
|
||||
|
Loading…
x
Reference in New Issue
Block a user