mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Merge remote-tracking branch 'origin/master' into aakansha-custom-elements
This commit is contained in:
commit
4953828d86
126 changed files with 4379 additions and 1606 deletions
|
@ -11,6 +11,7 @@ import {
|
|||
actionCopy,
|
||||
actionCopyAsPng,
|
||||
actionCopyAsSvg,
|
||||
copyText,
|
||||
actionCopyStyles,
|
||||
actionCut,
|
||||
actionDeleteSelected,
|
||||
|
@ -30,6 +31,7 @@ import {
|
|||
actionBindText,
|
||||
actionUngroup,
|
||||
actionLink,
|
||||
actionToggleLock,
|
||||
} from "../actions";
|
||||
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
|
@ -74,7 +76,6 @@ import {
|
|||
ZOOM_STEP,
|
||||
} from "../constants";
|
||||
import { loadFromBlob } from "../data";
|
||||
import { isValidLibrary } from "../data/json";
|
||||
import Library from "../data/library";
|
||||
import { restore, restoreElements, restoreLibraryItems } from "../data/restore";
|
||||
import {
|
||||
|
@ -234,6 +235,7 @@ import {
|
|||
generateIdFromFile,
|
||||
getDataURL,
|
||||
isSupportedImageFile,
|
||||
loadLibraryFromBlob,
|
||||
resizeImageFile,
|
||||
SVGStringToFile,
|
||||
} from "../data/blob";
|
||||
|
@ -451,6 +453,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
strokeSharpness: this.state.currentItemLinearStrokeSharpness,
|
||||
width,
|
||||
height,
|
||||
locked: false,
|
||||
});
|
||||
|
||||
const unbind = this.scene.addCallback(() => {
|
||||
|
@ -768,28 +771,21 @@ class App extends React.Component<AppProps, AppState> {
|
|||
try {
|
||||
const request = await fetch(decodeURIComponent(url));
|
||||
const blob = await request.blob();
|
||||
const json = JSON.parse(await blob.text());
|
||||
if (!isValidLibrary(json)) {
|
||||
throw new Error();
|
||||
}
|
||||
const defaultStatus = "published";
|
||||
const libraryItems = await loadLibraryFromBlob(blob, defaultStatus);
|
||||
if (
|
||||
token === this.id ||
|
||||
window.confirm(
|
||||
t("alerts.confirmAddLibrary", {
|
||||
numShapes: (json.libraryItems || json.library || []).length,
|
||||
numShapes: libraryItems.length,
|
||||
}),
|
||||
)
|
||||
) {
|
||||
await this.library.importLibrary(blob, "published");
|
||||
// hack to rerender the library items after import
|
||||
if (this.state.isLibraryOpen) {
|
||||
this.setState({ isLibraryOpen: false });
|
||||
}
|
||||
this.setState({ isLibraryOpen: true });
|
||||
await this.library.importLibrary(libraryItems, defaultStatus);
|
||||
}
|
||||
} catch (error: any) {
|
||||
window.alert(t("alerts.errorLoadingLibrary"));
|
||||
console.error(error);
|
||||
this.setState({ errorMessage: t("errors.importLibraryError") });
|
||||
} finally {
|
||||
this.focusContainer();
|
||||
}
|
||||
|
@ -854,10 +850,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
try {
|
||||
initialData = (await this.props.initialData) || null;
|
||||
if (initialData?.libraryItems) {
|
||||
this.libraryItemsFromStorage = restoreLibraryItems(
|
||||
initialData.libraryItems,
|
||||
"unpublished",
|
||||
) as LibraryItems;
|
||||
this.library.importLibrary(initialData.libraryItems, "unpublished");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
|
@ -1134,7 +1127,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
activeTool: { ...this.state.activeTool, type: "selection" },
|
||||
});
|
||||
}
|
||||
if (prevState.theme !== this.state.theme) {
|
||||
if (
|
||||
this.state.activeTool.type === "eraser" &&
|
||||
prevState.theme !== this.state.theme
|
||||
) {
|
||||
setEraserCursor(this.canvas, this.state.theme);
|
||||
}
|
||||
// Hide hyperlink popup if shown when element type is not selection
|
||||
|
@ -1197,7 +1193,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
prevState.activeTool !== this.state.activeTool &&
|
||||
multiElement != null &&
|
||||
isBindingEnabled(this.state) &&
|
||||
isBindingElement(multiElement)
|
||||
isBindingElement(multiElement, false)
|
||||
) {
|
||||
maybeBindLinearElement(
|
||||
multiElement,
|
||||
|
@ -1610,6 +1606,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
fontFamily: this.state.currentItemFontFamily,
|
||||
textAlign: this.state.currentItemTextAlign,
|
||||
verticalAlign: DEFAULT_VERTICAL_ALIGN,
|
||||
locked: false,
|
||||
});
|
||||
|
||||
this.scene.replaceAllElements([
|
||||
|
@ -1744,7 +1741,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||
appState?: Pick<AppState, K> | null;
|
||||
collaborators?: SceneData["collaborators"];
|
||||
commitToHistory?: SceneData["commitToHistory"];
|
||||
libraryItems?: SceneData["libraryItems"];
|
||||
libraryItems?:
|
||||
| Required<SceneData>["libraryItems"]
|
||||
| Promise<Required<SceneData>["libraryItems"]>;
|
||||
}) => {
|
||||
if (sceneData.commitToHistory) {
|
||||
this.history.resumeRecording();
|
||||
|
@ -1764,7 +1763,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
if (sceneData.libraryItems) {
|
||||
this.library.saveLibrary(
|
||||
restoreLibraryItems(sceneData.libraryItems, "unpublished"),
|
||||
new Promise<LibraryItems>(async (resolve, reject) => {
|
||||
try {
|
||||
resolve(
|
||||
restoreLibraryItems(
|
||||
await sceneData.libraryItems,
|
||||
"unpublished",
|
||||
),
|
||||
);
|
||||
} catch {
|
||||
reject(new Error(t("errors.importLibraryError")));
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -2194,12 +2204,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||
of all hit elements */
|
||||
preferSelected?: boolean;
|
||||
includeBoundTextElement?: boolean;
|
||||
includeLockedElements?: boolean;
|
||||
},
|
||||
): NonDeleted<ExcalidrawElement> | null {
|
||||
const allHitElements = this.getElementsAtPosition(
|
||||
x,
|
||||
y,
|
||||
opts?.includeBoundTextElement,
|
||||
opts?.includeLockedElements,
|
||||
);
|
||||
if (allHitElements.length > 1) {
|
||||
if (opts?.preferSelected) {
|
||||
|
@ -2232,14 +2244,19 @@ class App extends React.Component<AppProps, AppState> {
|
|||
x: number,
|
||||
y: number,
|
||||
includeBoundTextElement: boolean = false,
|
||||
includeLockedElements: boolean = false,
|
||||
): NonDeleted<ExcalidrawElement>[] {
|
||||
const elements = includeBoundTextElement
|
||||
? this.scene.getElements()
|
||||
: this.scene
|
||||
.getElements()
|
||||
.filter(
|
||||
(element) => !(isTextElement(element) && element.containerId),
|
||||
);
|
||||
const elements =
|
||||
includeBoundTextElement && includeLockedElements
|
||||
? this.scene.getElements()
|
||||
: this.scene
|
||||
.getElements()
|
||||
.filter(
|
||||
(element) =>
|
||||
(includeLockedElements || !element.locked) &&
|
||||
(includeBoundTextElement ||
|
||||
!(isTextElement(element) && element.containerId)),
|
||||
);
|
||||
|
||||
return getElementsAtPosition(elements, (element) =>
|
||||
hitTest(element, this.state, x, y),
|
||||
|
@ -2281,7 +2298,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
if (selectedElements.length === 1) {
|
||||
if (isTextElement(selectedElements[0])) {
|
||||
existingTextElement = selectedElements[0];
|
||||
} else if (isTextBindableContainer(selectedElements[0])) {
|
||||
} else if (isTextBindableContainer(selectedElements[0], false)) {
|
||||
container = selectedElements[0];
|
||||
existingTextElement = getBoundTextElement(container);
|
||||
}
|
||||
|
@ -2301,7 +2318,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.scene
|
||||
.getElements()
|
||||
.filter(
|
||||
(ele) => isTextBindableContainer(ele) && !getBoundTextElement(ele),
|
||||
(ele) =>
|
||||
isTextBindableContainer(ele, false) && !getBoundTextElement(ele),
|
||||
),
|
||||
sceneX,
|
||||
sceneY,
|
||||
|
@ -2359,6 +2377,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
: DEFAULT_VERTICAL_ALIGN,
|
||||
containerId: container?.id ?? undefined,
|
||||
groupIds: container?.groupIds ?? [],
|
||||
locked: false,
|
||||
});
|
||||
|
||||
this.setState({ editingElement: element });
|
||||
|
@ -2665,7 +2684,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
// Hovering with a selected tool or creating new linear element via click
|
||||
// and point
|
||||
const { draggingElement } = this.state;
|
||||
if (isBindingElement(draggingElement)) {
|
||||
if (isBindingElement(draggingElement, false)) {
|
||||
this.maybeSuggestBindingsForLinearElementAtCoords(
|
||||
draggingElement,
|
||||
[scenePointer],
|
||||
|
@ -2848,7 +2867,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.isHittingCommonBoundingBoxOfSelectedElements(
|
||||
scenePointer,
|
||||
selectedElements,
|
||||
))
|
||||
)) &&
|
||||
!hitElement?.locked
|
||||
) {
|
||||
setCursor(this.canvas, CURSOR_TYPE.MOVE);
|
||||
} else {
|
||||
|
@ -2864,6 +2884,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
) => {
|
||||
const updateElementIds = (elements: ExcalidrawElement[]) => {
|
||||
elements.forEach((element) => {
|
||||
if (element.locked) {
|
||||
return;
|
||||
}
|
||||
|
||||
idsToUpdate.push(element.id);
|
||||
if (event.altKey) {
|
||||
if (
|
||||
|
@ -2989,6 +3013,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
});
|
||||
this.savePointer(event.clientX, event.clientY, "down");
|
||||
|
||||
this.updateGestureOnPointerDown(event);
|
||||
|
||||
if (this.handleCanvasPanUsingWheelOrSpaceDrag(event)) {
|
||||
return;
|
||||
}
|
||||
|
@ -3001,8 +3027,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return;
|
||||
}
|
||||
|
||||
this.updateGestureOnPointerDown(event);
|
||||
|
||||
// don't select while panning
|
||||
if (gesture.pointers.size > 1) {
|
||||
return;
|
||||
|
@ -3201,7 +3225,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
): boolean => {
|
||||
if (
|
||||
!(
|
||||
gesture.pointers.size === 0 &&
|
||||
gesture.pointers.size <= 1 &&
|
||||
(event.button === POINTER_BUTTON.WHEEL ||
|
||||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
|
||||
this.state.viewModeEnabled)
|
||||
|
@ -3715,6 +3739,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
opacity: this.state.currentItemOpacity,
|
||||
strokeSharpness: this.state.currentItemLinearStrokeSharpness,
|
||||
simulatePressure: event.pressure === 0.5,
|
||||
locked: false,
|
||||
});
|
||||
|
||||
this.setState((prevState) => ({
|
||||
|
@ -3770,6 +3795,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
strokeSharpness: this.state.currentItemLinearStrokeSharpness,
|
||||
locked: false,
|
||||
});
|
||||
|
||||
return element;
|
||||
|
@ -3857,6 +3883,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
strokeSharpness: this.state.currentItemLinearStrokeSharpness,
|
||||
startArrowhead,
|
||||
endArrowhead,
|
||||
locked: false,
|
||||
});
|
||||
this.setState((prevState) => ({
|
||||
selectedElementIds: {
|
||||
|
@ -3905,6 +3932,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
strokeSharpness: this.state.currentItemStrokeSharpness,
|
||||
locked: false,
|
||||
});
|
||||
|
||||
if (element.type === "selection") {
|
||||
|
@ -4054,13 +4082,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||
pointerDownState.hit.element?.id ||
|
||||
pointerDownState.hit.hasHitElementInside)
|
||||
) {
|
||||
// Marking that click was used for dragging to check
|
||||
// if elements should be deselected on pointerup
|
||||
pointerDownState.drag.hasOccurred = true;
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getElements(),
|
||||
this.state,
|
||||
);
|
||||
if (selectedElements.every((element) => element.locked)) {
|
||||
return;
|
||||
}
|
||||
// Marking that click was used for dragging to check
|
||||
// if elements should be deselected on pointerup
|
||||
pointerDownState.drag.hasOccurred = true;
|
||||
// prevent dragging even if we're no longer holding cmd/ctrl otherwise
|
||||
// it would have weird results (stuff jumping all over the screen)
|
||||
if (selectedElements.length > 0 && !pointerDownState.withCmdOrCtrl) {
|
||||
|
@ -4204,7 +4235,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
});
|
||||
}
|
||||
|
||||
if (isBindingElement(draggingElement)) {
|
||||
if (isBindingElement(draggingElement, false)) {
|
||||
// When creating a linear element by dragging
|
||||
this.maybeSuggestBindingsForLinearElementAtCoords(
|
||||
draggingElement,
|
||||
|
@ -4484,7 +4515,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
} else if (pointerDownState.drag.hasOccurred && !multiElement) {
|
||||
if (
|
||||
isBindingEnabled(this.state) &&
|
||||
isBindingElement(draggingElement)
|
||||
isBindingElement(draggingElement, false)
|
||||
) {
|
||||
maybeBindLinearElement(
|
||||
draggingElement,
|
||||
|
@ -5343,11 +5374,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||
) {
|
||||
this.library
|
||||
.importLibrary(file)
|
||||
.then(() => {
|
||||
// Close and then open to get the libraries updated
|
||||
this.setState({ isLibraryOpen: false });
|
||||
this.setState({ isLibraryOpen: true });
|
||||
})
|
||||
.catch((error) =>
|
||||
this.setState({ isLoading: false, errorMessage: error.message }),
|
||||
);
|
||||
|
@ -5402,7 +5428,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
|
||||
const { x, y } = viewportCoordsToSceneCoords(event, this.state);
|
||||
const element = this.getElementAtPosition(x, y, { preferSelected: true });
|
||||
const element = this.getElementAtPosition(x, y, {
|
||||
preferSelected: true,
|
||||
includeLockedElements: true,
|
||||
});
|
||||
|
||||
const type = element ? "element" : "canvas";
|
||||
|
||||
|
@ -5413,9 +5442,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||
const top = event.clientY - offsetTop;
|
||||
|
||||
if (element && !this.state.selectedElementIds[element.id]) {
|
||||
this.setState({ selectedElementIds: { [element.id]: true } }, () => {
|
||||
this._openContextMenu({ top, left }, type);
|
||||
});
|
||||
this.setState(
|
||||
selectGroupsForSelectedElements(
|
||||
{
|
||||
...this.state,
|
||||
selectedElementIds: { [element.id]: true },
|
||||
},
|
||||
this.scene.getElements(),
|
||||
),
|
||||
() => {
|
||||
this._openContextMenu({ top, left }, type);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
this._openContextMenu({ top, left }, type);
|
||||
}
|
||||
|
@ -5575,6 +5613,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
const elements = this.scene.getElements();
|
||||
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getElements(),
|
||||
this.state,
|
||||
);
|
||||
|
||||
const options: ContextMenuOption[] = [];
|
||||
if (probablySupportsClipboardBlob && elements.length > 0) {
|
||||
options.push(actionCopyAsPng);
|
||||
|
@ -5583,6 +5626,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||
if (probablySupportsClipboardWriteText && elements.length > 0) {
|
||||
options.push(actionCopyAsSvg);
|
||||
}
|
||||
|
||||
if (
|
||||
type === "element" &&
|
||||
copyText.contextItemPredicate(elements, this.state) &&
|
||||
probablySupportsClipboardWriteText
|
||||
) {
|
||||
options.push(copyText);
|
||||
}
|
||||
if (type === "canvas") {
|
||||
const viewModeOptions = [
|
||||
...options,
|
||||
|
@ -5626,6 +5677,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||
probablySupportsClipboardWriteText &&
|
||||
elements.length > 0 &&
|
||||
actionCopyAsSvg,
|
||||
probablySupportsClipboardWriteText &&
|
||||
selectedElements.length > 0 &&
|
||||
copyText,
|
||||
((probablySupportsClipboardBlob && elements.length > 0) ||
|
||||
(probablySupportsClipboardWriteText && elements.length > 0)) &&
|
||||
separator,
|
||||
|
@ -5698,6 +5752,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
(maybeFlipHorizontal || maybeFlipVertical) && separator,
|
||||
actionLink.contextItemPredicate(elements, this.state) && actionLink,
|
||||
actionDuplicateSelection,
|
||||
actionToggleLock,
|
||||
separator,
|
||||
actionDeleteSelected,
|
||||
],
|
||||
top,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue