Keep errors, elements and comments consistent (#2340)
Co-authored-by: David Luzar <luzar.david@gmail.com>
This commit is contained in:
parent
2a20c44338
commit
5d295415db
@ -1,7 +1,7 @@
|
|||||||
const { CLIEngine } = require("eslint");
|
const { CLIEngine } = require("eslint");
|
||||||
|
|
||||||
// see https://github.com/okonet/lint-staged#how-can-i-ignore-files-from-eslintignore-
|
// see https://github.com/okonet/lint-staged#how-can-i-ignore-files-from-eslintignore-
|
||||||
// for explanation
|
// for explanation
|
||||||
const cli = new CLIEngine({});
|
const cli = new CLIEngine({});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -540,7 +540,7 @@ export const actionChangeSharpness = register({
|
|||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
const shouldUpdateForNonLinearElements = targetElements.length
|
const shouldUpdateForNonLinearElements = targetElements.length
|
||||||
? targetElements.every((e) => !isLinearElement(e))
|
? targetElements.every((el) => !isLinearElement(el))
|
||||||
: !isLinearElementType(appState.elementType);
|
: !isLinearElementType(appState.elementType);
|
||||||
const shouldUpdateForLinearElements = targetElements.length
|
const shouldUpdateForLinearElements = targetElements.length
|
||||||
? targetElements.every(isLinearElement)
|
? targetElements.every(isLinearElement)
|
||||||
|
@ -57,9 +57,9 @@ export const copyToClipboard = async (
|
|||||||
try {
|
try {
|
||||||
PREFER_APP_CLIPBOARD = false;
|
PREFER_APP_CLIPBOARD = false;
|
||||||
await copyTextToSystemClipboard(json);
|
await copyTextToSystemClipboard(json);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
PREFER_APP_CLIPBOARD = true;
|
PREFER_APP_CLIPBOARD = true;
|
||||||
console.error(err);
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -128,7 +128,7 @@ export const parseClipboard = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if system clipboard contains spreadsheet, use it even though it's
|
// if system clipboard contains spreadsheet, use it even though it's
|
||||||
// technically possible it's staler than in-app clipboard
|
// technically possible it's staler than in-app clipboard
|
||||||
const spreadsheetResult = parsePotentialSpreadsheet(systemClipboard);
|
const spreadsheetResult = parsePotentialSpreadsheet(systemClipboard);
|
||||||
if (spreadsheetResult) {
|
if (spreadsheetResult) {
|
||||||
return spreadsheetResult;
|
return spreadsheetResult;
|
||||||
@ -150,8 +150,8 @@ export const parseClipboard = async (
|
|||||||
return appClipboardData;
|
return appClipboardData;
|
||||||
} catch {
|
} catch {
|
||||||
// system clipboard doesn't contain excalidraw elements → return plaintext
|
// system clipboard doesn't contain excalidraw elements → return plaintext
|
||||||
// unless we set a flag to prefer in-app clipboard because browser didn't
|
// unless we set a flag to prefer in-app clipboard because browser didn't
|
||||||
// support storing to system clipboard on copy
|
// support storing to system clipboard on copy
|
||||||
return PREFER_APP_CLIPBOARD && appClipboardData.elements
|
return PREFER_APP_CLIPBOARD && appClipboardData.elements
|
||||||
? appClipboardData
|
? appClipboardData
|
||||||
: { text: systemClipboard };
|
: { text: systemClipboard };
|
||||||
@ -170,7 +170,7 @@ export const copyTextToSystemClipboard = async (text: string | null) => {
|
|||||||
if (probablySupportsClipboardWriteText) {
|
if (probablySupportsClipboardWriteText) {
|
||||||
try {
|
try {
|
||||||
// NOTE: doesn't work on FF on non-HTTPS domains, or when document
|
// NOTE: doesn't work on FF on non-HTTPS domains, or when document
|
||||||
// not focused
|
// not focused
|
||||||
await navigator.clipboard.writeText(text || "");
|
await navigator.clipboard.writeText(text || "");
|
||||||
copied = true;
|
copied = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -179,7 +179,7 @@ export const copyTextToSystemClipboard = async (text: string | null) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Note that execCommand doesn't allow copying empty strings, so if we're
|
// Note that execCommand doesn't allow copying empty strings, so if we're
|
||||||
// clearing clipboard using this API, we must copy at least an empty char
|
// clearing clipboard using this API, we must copy at least an empty char
|
||||||
if (!copied && !copyTextViaExecCommand(text || " ")) {
|
if (!copied && !copyTextViaExecCommand(text || " ")) {
|
||||||
throw new Error("couldn't copy");
|
throw new Error("couldn't copy");
|
||||||
}
|
}
|
||||||
|
@ -523,7 +523,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
collabForceLoadFlag,
|
collabForceLoadFlag,
|
||||||
);
|
);
|
||||||
// if loading same room as the one previously unloaded within 15sec
|
// if loading same room as the one previously unloaded within 15sec
|
||||||
// force reload without prompting
|
// force reload without prompting
|
||||||
if (previousRoom === roomID && Date.now() - timestamp < 15000) {
|
if (previousRoom === roomID && Date.now() - timestamp < 15000) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -561,8 +561,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
history.clear();
|
history.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Completely resets scene & history.
|
// Completely resets scene & history.
|
||||||
* Do not use for clear scene user action. */
|
// Do not use for clear scene user action.
|
||||||
private resetScene = withBatchedUpdates(() => {
|
private resetScene = withBatchedUpdates(() => {
|
||||||
this.scene.replaceAllElements([]);
|
this.scene.replaceAllElements([]);
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -654,7 +654,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
|
|
||||||
if (isCollaborationScene) {
|
if (isCollaborationScene) {
|
||||||
// when joining a room we don't want user's local scene data to be merged
|
// when joining a room we don't want user's local scene data to be merged
|
||||||
// into the remote scene
|
// into the remote scene
|
||||||
this.resetScene();
|
this.resetScene();
|
||||||
|
|
||||||
this.initializeSocketClient({ showLoadingState: true });
|
this.initializeSocketClient({ showLoadingState: true });
|
||||||
@ -847,7 +847,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
!isSavedToFirebase(this.portal, syncableElements)
|
!isSavedToFirebase(this.portal, syncableElements)
|
||||||
) {
|
) {
|
||||||
// this won't run in time if user decides to leave the site, but
|
// this won't run in time if user decides to leave the site, but
|
||||||
// the purpose is to run in immediately after user decides to stay
|
// the purpose is to run in immediately after user decides to stay
|
||||||
this.saveCollabRoomToFirebase(syncableElements);
|
this.saveCollabRoomToFirebase(syncableElements);
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -943,7 +943,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const { atLeastOneVisibleElement, scrollBars } = renderScene(
|
const { atLeastOneVisibleElement, scrollBars } = renderScene(
|
||||||
elements.filter((element) => {
|
elements.filter((element) => {
|
||||||
// don't render text element that's being currently edited (it's
|
// don't render text element that's being currently edited (it's
|
||||||
// rendered on remote only)
|
// rendered on remote only)
|
||||||
return (
|
return (
|
||||||
!this.state.editingElement ||
|
!this.state.editingElement ||
|
||||||
this.state.editingElement.type !== "text" ||
|
this.state.editingElement.type !== "text" ||
|
||||||
@ -1108,7 +1108,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const elementUnderCursor = document.elementFromPoint(cursorX, cursorY);
|
const elementUnderCursor = document.elementFromPoint(cursorX, cursorY);
|
||||||
if (
|
if (
|
||||||
// if no ClipboardEvent supplied, assume we're pasting via contextMenu
|
// if no ClipboardEvent supplied, assume we're pasting via contextMenu
|
||||||
// thus these checks don't make sense
|
// thus these checks don't make sense
|
||||||
event &&
|
event &&
|
||||||
(!(elementUnderCursor instanceof HTMLCanvasElement) ||
|
(!(elementUnderCursor instanceof HTMLCanvasElement) ||
|
||||||
isWritableElement(target))
|
isWritableElement(target))
|
||||||
@ -1380,7 +1380,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const roomKey = roomMatch[2];
|
const roomKey = roomMatch[2];
|
||||||
|
|
||||||
// fallback in case you're not alone in the room but still don't receive
|
// fallback in case you're not alone in the room but still don't receive
|
||||||
// initial SCENE_UPDATE message
|
// initial SCENE_UPDATE message
|
||||||
this.socketInitializationTimer = setTimeout(
|
this.socketInitializationTimer = setTimeout(
|
||||||
this.initializeSocket,
|
this.initializeSocket,
|
||||||
INITIAL_SCENE_UPDATE_TIMEOUT,
|
INITIAL_SCENE_UPDATE_TIMEOUT,
|
||||||
@ -1432,7 +1432,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
decryptedData.payload.socketID;
|
decryptedData.payload.socketID;
|
||||||
|
|
||||||
// NOTE purposefully mutating collaborators map in case of
|
// NOTE purposefully mutating collaborators map in case of
|
||||||
// pointer updates so as not to trigger LayerUI rerender
|
// pointer updates so as not to trigger LayerUI rerender
|
||||||
this.setState((state) => {
|
this.setState((state) => {
|
||||||
if (!state.collaborators.has(socketId)) {
|
if (!state.collaborators.has(socketId)) {
|
||||||
state.collaborators.set(socketId, {});
|
state.collaborators.set(socketId, {});
|
||||||
@ -1467,9 +1467,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
if (elements) {
|
if (elements) {
|
||||||
this.handleRemoteSceneUpdate(elements, { initFromSnapshot: true });
|
this.handleRemoteSceneUpdate(elements, { initFromSnapshot: true });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
// log the error and move on. other peers will sync us the scene.
|
// log the error and move on. other peers will sync us the scene.
|
||||||
console.error(e);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1814,7 +1814,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// do an initial update to re-initialize element position since we were
|
// do an initial update to re-initialize element position since we were
|
||||||
// modifying element's x/y for sake of editor (case: syncing to remote)
|
// modifying element's x/y for sake of editor (case: syncing to remote)
|
||||||
updateElement(element.text);
|
updateElement(element.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1920,7 +1920,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
|
|
||||||
if (existingTextElement) {
|
if (existingTextElement) {
|
||||||
// if text element is no longer centered to a container, reset
|
// if text element is no longer centered to a container, reset
|
||||||
// verticalAlign to default because it's currently internal-only
|
// verticalAlign to default because it's currently internal-only
|
||||||
if (!parentCenterPosition || element.textAlign !== "center") {
|
if (!parentCenterPosition || element.textAlign !== "center") {
|
||||||
mutateElement(element, { verticalAlign: DEFAULT_VERTICAL_ALIGN });
|
mutateElement(element, { verticalAlign: DEFAULT_VERTICAL_ALIGN });
|
||||||
}
|
}
|
||||||
@ -1931,7 +1931,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// case: creating new text not centered to parent elemenent → offset Y
|
// case: creating new text not centered to parent elemenent → offset Y
|
||||||
// so that the text is centered to cursor position
|
// so that the text is centered to cursor position
|
||||||
if (!parentCenterPosition) {
|
if (!parentCenterPosition) {
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
y: element.y - element.baseline / 2,
|
y: element.y - element.baseline / 2,
|
||||||
@ -1952,7 +1952,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
event: React.MouseEvent<HTMLCanvasElement>,
|
event: React.MouseEvent<HTMLCanvasElement>,
|
||||||
) => {
|
) => {
|
||||||
// case: double-clicking with arrow/line tool selected would both create
|
// case: double-clicking with arrow/line tool selected would both create
|
||||||
// text and enter multiElement mode
|
// text and enter multiElement mode
|
||||||
if (this.state.multiElement) {
|
if (this.state.multiElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -2129,7 +2129,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
|
|
||||||
if (lastPoint === lastCommittedPoint) {
|
if (lastPoint === lastCommittedPoint) {
|
||||||
// if we haven't yet created a temp point and we're beyond commit-zone
|
// if we haven't yet created a temp point and we're beyond commit-zone
|
||||||
// threshold, add a point
|
// threshold, add a point
|
||||||
if (
|
if (
|
||||||
distance2d(
|
distance2d(
|
||||||
scenePointerX - rx,
|
scenePointerX - rx,
|
||||||
@ -2144,11 +2144,11 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
} else {
|
} else {
|
||||||
document.documentElement.style.cursor = CURSOR_TYPE.POINTER;
|
document.documentElement.style.cursor = CURSOR_TYPE.POINTER;
|
||||||
// in this branch, we're inside the commit zone, and no uncommitted
|
// in this branch, we're inside the commit zone, and no uncommitted
|
||||||
// point exists. Thus do nothing (don't add/remove points).
|
// point exists. Thus do nothing (don't add/remove points).
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// cursor moved inside commit zone, and there's uncommitted point,
|
// cursor moved inside commit zone, and there's uncommitted point,
|
||||||
// thus remove it
|
// thus remove it
|
||||||
if (
|
if (
|
||||||
points.length > 2 &&
|
points.length > 2 &&
|
||||||
lastCommittedPoint &&
|
lastCommittedPoint &&
|
||||||
@ -2293,8 +2293,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
// fixes pointermove causing selection of UI texts #32
|
// fixes pointermove causing selection of UI texts #32
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// Preventing the event above disables default behavior
|
// Preventing the event above disables default behavior
|
||||||
// of defocusing potentially focused element, which is what we
|
// of defocusing potentially focused element, which is what we
|
||||||
// want when clicking inside the canvas.
|
// want when clicking inside the canvas.
|
||||||
if (document.activeElement instanceof HTMLElement) {
|
if (document.activeElement instanceof HTMLElement) {
|
||||||
document.activeElement.blur();
|
document.activeElement.blur();
|
||||||
}
|
}
|
||||||
@ -2739,8 +2739,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add hit element to selection. At this point if we're not holding
|
// Add hit element to selection. At this point if we're not holding
|
||||||
// SHIFT the previously selected element(s) were deselected above
|
// SHIFT the previously selected element(s) were deselected above
|
||||||
// (make sure you use setState updater to use latest state)
|
// (make sure you use setState updater to use latest state)
|
||||||
if (
|
if (
|
||||||
!someHitElementIsSelected &&
|
!someHitElementIsSelected &&
|
||||||
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
|
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
|
||||||
@ -2798,8 +2798,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
): void => {
|
): void => {
|
||||||
// if we're currently still editing text, clicking outside
|
// if we're currently still editing text, clicking outside
|
||||||
// should only finalize it, not create another (irrespective
|
// should only finalize it, not create another (irrespective
|
||||||
// of state.elementLocked)
|
// of state.elementLocked)
|
||||||
if (this.state.editingElement?.type === "text") {
|
if (this.state.editingElement?.type === "text") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -2860,7 +2860,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
// clicking outside commit zone → update reference for last committed
|
// clicking outside commit zone → update reference for last committed
|
||||||
// point
|
// point
|
||||||
mutateElement(multiElement, {
|
mutateElement(multiElement, {
|
||||||
lastCommittedPoint: multiElement.points[multiElement.points.length - 1],
|
lastCommittedPoint: multiElement.points[multiElement.points.length - 1],
|
||||||
});
|
});
|
||||||
@ -2986,9 +2986,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// for arrows/lines, don't start dragging until a given threshold
|
// for arrows/lines, don't start dragging until a given threshold
|
||||||
// to ensure we don't create a 2-point arrow by mistake when
|
// to ensure we don't create a 2-point arrow by mistake when
|
||||||
// user clicks mouse in a way that it moves a tiny bit (thus
|
// user clicks mouse in a way that it moves a tiny bit (thus
|
||||||
// triggering pointermove)
|
// triggering pointermove)
|
||||||
if (
|
if (
|
||||||
!pointerDownState.drag.hasOccurred &&
|
!pointerDownState.drag.hasOccurred &&
|
||||||
(this.state.elementType === "arrow" ||
|
(this.state.elementType === "arrow" ||
|
||||||
@ -3127,7 +3127,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
if (
|
if (
|
||||||
this.state.selectedElementIds[element.id] ||
|
this.state.selectedElementIds[element.id] ||
|
||||||
// case: the state.selectedElementIds might not have been
|
// case: the state.selectedElementIds might not have been
|
||||||
// updated yet by the time this mousemove event is fired
|
// updated yet by the time this mousemove event is fired
|
||||||
(element.id === hitElement?.id &&
|
(element.id === hitElement?.id &&
|
||||||
pointerDownState.hit.wasAddedToSelection)
|
pointerDownState.hit.wasAddedToSelection)
|
||||||
) {
|
) {
|
||||||
@ -3331,7 +3331,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
selectionElement: null,
|
selectionElement: null,
|
||||||
cursorButton: "up",
|
cursorButton: "up",
|
||||||
// text elements are reset on finalize, and resetting on pointerup
|
// text elements are reset on finalize, and resetting on pointerup
|
||||||
// may cause issues with double taps
|
// may cause issues with double taps
|
||||||
editingElement:
|
editingElement:
|
||||||
multiElement || isTextElement(this.state.editingElement)
|
multiElement || isTextElement(this.state.editingElement)
|
||||||
? this.state.editingElement
|
? this.state.editingElement
|
||||||
@ -3481,8 +3481,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
if (this.state.selectedElementIds[hitElement.id]) {
|
if (this.state.selectedElementIds[hitElement.id]) {
|
||||||
if (isSelectedViaGroup(this.state, hitElement)) {
|
if (isSelectedViaGroup(this.state, hitElement)) {
|
||||||
// We want to unselect all groups hitElement is part of
|
// We want to unselect all groups hitElement is part of
|
||||||
// as well as all elements that are part of the groups
|
// as well as all elements that are part of the groups
|
||||||
// hitElement is part of
|
// hitElement is part of
|
||||||
const idsOfSelectedElementsThatAreInGroups = hitElement.groupIds
|
const idsOfSelectedElementsThatAreInGroups = hitElement.groupIds
|
||||||
.flatMap((groupId) =>
|
.flatMap((groupId) =>
|
||||||
getElementsInGroup(this.scene.getElements(), groupId),
|
getElementsInGroup(this.scene.getElements(), groupId),
|
||||||
|
@ -167,8 +167,7 @@ class Portal {
|
|||||||
const newElements = sceneElements
|
const newElements = sceneElements
|
||||||
.reduce((elements, element) => {
|
.reduce((elements, element) => {
|
||||||
// if the remote element references one that's currently
|
// if the remote element references one that's currently
|
||||||
// edited on local, skip it (it'll be added in the next
|
// edited on local, skip it (it'll be added in the next step)
|
||||||
// step)
|
|
||||||
if (
|
if (
|
||||||
element.id === this.app.state.editingElement?.id ||
|
element.id === this.app.state.editingElement?.id ||
|
||||||
element.id === this.app.state.resizingElement?.id ||
|
element.id === this.app.state.resizingElement?.id ||
|
||||||
|
@ -108,7 +108,8 @@ export const loadFromBlob = async (
|
|||||||
},
|
},
|
||||||
localAppState,
|
localAppState,
|
||||||
);
|
);
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.error(error.message);
|
||||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -86,7 +86,7 @@ export const isSavedToFirebase = (
|
|||||||
return firebaseSceneVersionCache.get(portal.socket) === sceneVersion;
|
return firebaseSceneVersionCache.get(portal.socket) === sceneVersion;
|
||||||
}
|
}
|
||||||
// if no room exists, consider the room saved so that we don't unnecessarily
|
// if no room exists, consider the room saved so that we don't unnecessarily
|
||||||
// prevent unload (there's nothing we could do at that point anyway)
|
// prevent unload (there's nothing we could do at that point anyway)
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -97,7 +97,7 @@ export async function saveToFirebase(
|
|||||||
const { roomID, roomKey, socket } = portal;
|
const { roomID, roomKey, socket } = portal;
|
||||||
if (
|
if (
|
||||||
// if no room exists, consider the room saved because there's nothing we can
|
// if no room exists, consider the room saved because there's nothing we can
|
||||||
// do at this point
|
// do at this point
|
||||||
!roomID ||
|
!roomID ||
|
||||||
!roomKey ||
|
!roomKey ||
|
||||||
!socket ||
|
!socket ||
|
||||||
|
@ -31,7 +31,7 @@ export class Library {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// detect z-index difference by checking the excalidraw elements
|
// detect z-index difference by checking the excalidraw elements
|
||||||
// are in order
|
// are in order
|
||||||
return libraryItem.every((libItemExcalidrawItem, idx) => {
|
return libraryItem.every((libItemExcalidrawItem, idx) => {
|
||||||
return (
|
return (
|
||||||
libItemExcalidrawItem.id === targetLibraryItem[idx].id &&
|
libItemExcalidrawItem.id === targetLibraryItem[idx].id &&
|
||||||
@ -69,8 +69,8 @@ export class Library {
|
|||||||
Library.libraryCache = JSON.parse(JSON.stringify(items));
|
Library.libraryCache = JSON.parse(JSON.stringify(items));
|
||||||
|
|
||||||
resolve(items);
|
resolve(items);
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
console.error(e);
|
console.error(error);
|
||||||
resolve([]);
|
resolve([]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -81,12 +81,12 @@ export class Library {
|
|||||||
try {
|
try {
|
||||||
const serializedItems = JSON.stringify(items);
|
const serializedItems = JSON.stringify(items);
|
||||||
// cache optimistically so that consumers have access to the latest
|
// cache optimistically so that consumers have access to the latest
|
||||||
// immediately
|
// immediately
|
||||||
Library.libraryCache = JSON.parse(serializedItems);
|
Library.libraryCache = JSON.parse(serializedItems);
|
||||||
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
|
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
Library.libraryCache = prevLibraryItems;
|
Library.libraryCache = prevLibraryItems;
|
||||||
console.error(e);
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ const restoreElementWithProperties = <T extends ExcalidrawElement>(
|
|||||||
const base: Pick<T, keyof ExcalidrawElement> = {
|
const base: Pick<T, keyof ExcalidrawElement> = {
|
||||||
type: element.type,
|
type: element.type,
|
||||||
// all elements must have version > 0 so getSceneVersion() will pick up
|
// all elements must have version > 0 so getSceneVersion() will pick up
|
||||||
// newly added elements
|
// newly added elements
|
||||||
version: element.version || 1,
|
version: element.version || 1,
|
||||||
versionNonce: element.versionNonce ?? 0,
|
versionNonce: element.versionNonce ?? 0,
|
||||||
isDeleted: element.isDeleted ?? false,
|
isDeleted: element.isDeleted ?? false,
|
||||||
@ -112,9 +112,9 @@ const restoreElement = (
|
|||||||
case "diamond":
|
case "diamond":
|
||||||
return restoreElementWithProperties(element, {});
|
return restoreElementWithProperties(element, {});
|
||||||
|
|
||||||
// don't use default case so as to catch a missing an element type case
|
// Don't use default case so as to catch a missing an element type case.
|
||||||
// (we also don't want to throw, but instead return void so we
|
// We also don't want to throw, but instead return void so we filter
|
||||||
// filter out these unsupported elements from the restored array)
|
// out these unsupported elements from the restored array.
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ export const restoreElements = (
|
|||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
return (elements || []).reduce((elements, element) => {
|
return (elements || []).reduce((elements, element) => {
|
||||||
// filtering out selection, which is legacy, no longer kept in elements,
|
// filtering out selection, which is legacy, no longer kept in elements,
|
||||||
// and causing issues if retained
|
// and causing issues if retained
|
||||||
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
|
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
|
||||||
const migratedElement = restoreElement(element);
|
const migratedElement = restoreElement(element);
|
||||||
if (migratedElement) {
|
if (migratedElement) {
|
||||||
@ -161,7 +161,7 @@ const restoreAppState = (
|
|||||||
...nextAppState,
|
...nextAppState,
|
||||||
offsetLeft: appState.offsetLeft || 0,
|
offsetLeft: appState.offsetLeft || 0,
|
||||||
offsetTop: appState.offsetTop || 0,
|
offsetTop: appState.offsetTop || 0,
|
||||||
/* Migrates from previous version where appState.zoom was a number */
|
// Migrates from previous version where appState.zoom was a number
|
||||||
zoom:
|
zoom:
|
||||||
typeof appState.zoom === "number"
|
typeof appState.zoom === "number"
|
||||||
? {
|
? {
|
||||||
|
@ -227,7 +227,7 @@ export class LinearElementEditor {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// if we clicked on a point, set the element as hitElement otherwise
|
// if we clicked on a point, set the element as hitElement otherwise
|
||||||
// it would get deselected if the point is outside the hitbox area
|
// it would get deselected if the point is outside the hitbox area
|
||||||
if (clickedPointIndex > -1) {
|
if (clickedPointIndex > -1) {
|
||||||
ret.hitElement = element;
|
ret.hitElement = element;
|
||||||
} else {
|
} else {
|
||||||
@ -379,8 +379,8 @@ export class LinearElementEditor {
|
|||||||
const pointHandles = this.getPointsGlobalCoordinates(element);
|
const pointHandles = this.getPointsGlobalCoordinates(element);
|
||||||
let idx = pointHandles.length;
|
let idx = pointHandles.length;
|
||||||
// loop from right to left because points on the right are rendered over
|
// loop from right to left because points on the right are rendered over
|
||||||
// points on the left, thus should take precedence when clicking, if they
|
// points on the left, thus should take precedence when clicking, if they
|
||||||
// overlap
|
// overlap
|
||||||
while (--idx > -1) {
|
while (--idx > -1) {
|
||||||
const point = pointHandles[idx];
|
const point = pointHandles[idx];
|
||||||
if (
|
if (
|
||||||
@ -458,10 +458,10 @@ export class LinearElementEditor {
|
|||||||
const { points } = element;
|
const { points } = element;
|
||||||
|
|
||||||
// in case we're moving start point, instead of modifying its position
|
// in case we're moving start point, instead of modifying its position
|
||||||
// which would break the invariant of it being at [0,0], we move
|
// which would break the invariant of it being at [0,0], we move
|
||||||
// all the other points in the opposite direction by delta to
|
// all the other points in the opposite direction by delta to
|
||||||
// offset it. We do the same with actual element.x/y position, so
|
// offset it. We do the same with actual element.x/y position, so
|
||||||
// this hacks are completely transparent to the user.
|
// this hacks are completely transparent to the user.
|
||||||
let offsetX = 0;
|
let offsetX = 0;
|
||||||
let offsetY = 0;
|
let offsetY = 0;
|
||||||
|
|
||||||
@ -475,7 +475,7 @@ export class LinearElementEditor {
|
|||||||
nextPoints.splice(pointIndex, 1);
|
nextPoints.splice(pointIndex, 1);
|
||||||
if (pointIndex === 0) {
|
if (pointIndex === 0) {
|
||||||
// if deleting first point, make the next to be [0,0] and recalculate
|
// if deleting first point, make the next to be [0,0] and recalculate
|
||||||
// positions of the rest with respect to it
|
// positions of the rest with respect to it
|
||||||
offsetX = nextPoints[0][0];
|
offsetX = nextPoints[0][0];
|
||||||
offsetY = nextPoints[0][1];
|
offsetY = nextPoints[0][1];
|
||||||
nextPoints = nextPoints.map((point, idx) => {
|
nextPoints = nextPoints.map((point, idx) => {
|
||||||
|
@ -225,7 +225,7 @@ export const newLinearElement = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Simplified deep clone for the purpose of cloning ExcalidrawElement only
|
// Simplified deep clone for the purpose of cloning ExcalidrawElement only
|
||||||
// (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.)
|
// (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.)
|
||||||
//
|
//
|
||||||
// Adapted from https://github.com/lukeed/klona
|
// Adapted from https://github.com/lukeed/klona
|
||||||
export const deepCopyElement = (val: any, depth: number = 0) => {
|
export const deepCopyElement = (val: any, depth: number = 0) => {
|
||||||
|
@ -25,7 +25,7 @@ const getTransform = (
|
|||||||
const { zoom, offsetTop, offsetLeft } = appState;
|
const { zoom, offsetTop, offsetLeft } = appState;
|
||||||
const degree = (180 * angle) / Math.PI;
|
const degree = (180 * angle) / Math.PI;
|
||||||
// offsets must be multiplied by 2 to account for the division by 2 of
|
// offsets must be multiplied by 2 to account for the division by 2 of
|
||||||
// the whole expression afterwards
|
// the whole expression afterwards
|
||||||
return `translate(${((width - offsetLeft * 2) * (zoom.value - 1)) / 2}px, ${
|
return `translate(${((width - offsetLeft * 2) * (zoom.value - 1)) / 2}px, ${
|
||||||
((height - offsetTop * 2) * (zoom.value - 1)) / 2
|
((height - offsetTop * 2) * (zoom.value - 1)) / 2
|
||||||
}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
||||||
@ -166,7 +166,7 @@ export const textWysiwyg = ({
|
|||||||
const rebindBlur = () => {
|
const rebindBlur = () => {
|
||||||
window.removeEventListener("pointerup", rebindBlur);
|
window.removeEventListener("pointerup", rebindBlur);
|
||||||
// deferred to guard against focus traps on various UIs that steal focus
|
// deferred to guard against focus traps on various UIs that steal focus
|
||||||
// upon pointerUp
|
// upon pointerUp
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
editable.onblur = handleSubmit;
|
editable.onblur = handleSubmit;
|
||||||
// case: clicking on the same property → no change → no update → no focus
|
// case: clicking on the same property → no change → no update → no focus
|
||||||
@ -184,7 +184,7 @@ export const textWysiwyg = ({
|
|||||||
editable.onblur = null;
|
editable.onblur = null;
|
||||||
window.addEventListener("pointerup", rebindBlur);
|
window.addEventListener("pointerup", rebindBlur);
|
||||||
// handle edge-case where pointerup doesn't fire e.g. due to user
|
// handle edge-case where pointerup doesn't fire e.g. due to user
|
||||||
// alt-tabbing away
|
// alt-tabbing away
|
||||||
window.addEventListener("blur", handleSubmit);
|
window.addEventListener("blur", handleSubmit);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -199,7 +199,7 @@ export const textWysiwyg = ({
|
|||||||
|
|
||||||
editable.onblur = handleSubmit;
|
editable.onblur = handleSubmit;
|
||||||
// reposition wysiwyg in case of window resize. Happens on mobile when
|
// reposition wysiwyg in case of window resize. Happens on mobile when
|
||||||
// device keyboard is opened.
|
// device keyboard is opened.
|
||||||
window.addEventListener("resize", updateWysiwygStyle);
|
window.addEventListener("resize", updateWysiwygStyle);
|
||||||
window.addEventListener("pointerdown", onPointerDown);
|
window.addEventListener("pointerdown", onPointerDown);
|
||||||
window.addEventListener("wheel", stopEvent, {
|
window.addEventListener("wheel", stopEvent, {
|
||||||
|
@ -126,7 +126,7 @@ const drawElementOnCanvas = (
|
|||||||
const shouldTemporarilyAttach = rtl && !context.canvas.isConnected;
|
const shouldTemporarilyAttach = rtl && !context.canvas.isConnected;
|
||||||
if (shouldTemporarilyAttach) {
|
if (shouldTemporarilyAttach) {
|
||||||
// to correctly render RTL text mixed with LTR, we have to append it
|
// to correctly render RTL text mixed with LTR, we have to append it
|
||||||
// to the DOM
|
// to the DOM
|
||||||
document.body.appendChild(context.canvas);
|
document.body.appendChild(context.canvas);
|
||||||
}
|
}
|
||||||
context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
|
context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
|
||||||
@ -194,17 +194,17 @@ export const generateRoughOptions = (element: ExcalidrawElement): Options => {
|
|||||||
? DASHARRAY_DOTTED
|
? DASHARRAY_DOTTED
|
||||||
: undefined,
|
: undefined,
|
||||||
// for non-solid strokes, disable multiStroke because it tends to make
|
// for non-solid strokes, disable multiStroke because it tends to make
|
||||||
// dashes/dots overlay each other
|
// dashes/dots overlay each other
|
||||||
disableMultiStroke: element.strokeStyle !== "solid",
|
disableMultiStroke: element.strokeStyle !== "solid",
|
||||||
// for non-solid strokes, increase the width a bit to make it visually
|
// for non-solid strokes, increase the width a bit to make it visually
|
||||||
// similar to solid strokes, because we're also disabling multiStroke
|
// similar to solid strokes, because we're also disabling multiStroke
|
||||||
strokeWidth:
|
strokeWidth:
|
||||||
element.strokeStyle !== "solid"
|
element.strokeStyle !== "solid"
|
||||||
? element.strokeWidth + 0.5
|
? element.strokeWidth + 0.5
|
||||||
: element.strokeWidth,
|
: element.strokeWidth,
|
||||||
// when increasing strokeWidth, we must explicitly set fillWeight and
|
// when increasing strokeWidth, we must explicitly set fillWeight and
|
||||||
// hachureGap because if not specified, roughjs uses strokeWidth to
|
// hachureGap because if not specified, roughjs uses strokeWidth to
|
||||||
// calculate them (and we don't want the fills to be modified)
|
// calculate them (and we don't want the fills to be modified)
|
||||||
fillWeight: element.strokeWidth / 2,
|
fillWeight: element.strokeWidth / 2,
|
||||||
hachureGap: element.strokeWidth * 4,
|
hachureGap: element.strokeWidth * 4,
|
||||||
roughness: element.roughness,
|
roughness: element.roughness,
|
||||||
|
@ -187,7 +187,7 @@ export const renderScene = (
|
|||||||
renderSelection = true,
|
renderSelection = true,
|
||||||
// Whether to employ render optimizations to improve performance.
|
// Whether to employ render optimizations to improve performance.
|
||||||
// Should not be turned on for export operations and similar, because it
|
// Should not be turned on for export operations and similar, because it
|
||||||
// doesn't guarantee pixel-perfect output.
|
// doesn't guarantee pixel-perfect output.
|
||||||
renderOptimizations = false,
|
renderOptimizations = false,
|
||||||
renderGrid = true,
|
renderGrid = true,
|
||||||
}: {
|
}: {
|
||||||
|
@ -129,7 +129,7 @@ class Scene {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
// done not for memory leaks, but to guard against possible late fires
|
// done not for memory leaks, but to guard against possible late fires
|
||||||
// (I guess?)
|
// (I guess?)
|
||||||
this.callbacks.clear();
|
this.callbacks.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,10 +75,9 @@ const registerValidSW = (swUrl: string, config?: Config) => {
|
|||||||
// but the previous service worker will still serve the older
|
// but the previous service worker will still serve the older
|
||||||
// content until all client tabs are closed.
|
// content until all client tabs are closed.
|
||||||
|
|
||||||
// console.log(
|
console.info(
|
||||||
// "New content is available and will be used when all " +
|
"New content is available and will be used when all tabs for this page are closed.",
|
||||||
// "tabs for this page are closed.",
|
);
|
||||||
// );
|
|
||||||
|
|
||||||
// Execute callback
|
// Execute callback
|
||||||
if (config && config.onUpdate) {
|
if (config && config.onUpdate) {
|
||||||
@ -89,7 +88,7 @@ const registerValidSW = (swUrl: string, config?: Config) => {
|
|||||||
// It's the perfect time to display a
|
// It's the perfect time to display a
|
||||||
// "Content is cached for offline use." message.
|
// "Content is cached for offline use." message.
|
||||||
|
|
||||||
// console.log("Content is cached for offline use.");
|
console.info("Content is cached for offline use.");
|
||||||
|
|
||||||
// Execute callback
|
// Execute callback
|
||||||
if (config && config.onSuccess) {
|
if (config && config.onSuccess) {
|
||||||
@ -128,10 +127,11 @@ const checkValidServiceWorker = (swUrl: string, config?: Config) => {
|
|||||||
registerValidSW(swUrl, config);
|
registerValidSW(swUrl, config);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((error) => {
|
||||||
// console.log(
|
console.info(
|
||||||
// "No internet connection found. App is running in offline mode.",
|
"No internet connection found. App is running in offline mode.",
|
||||||
// );
|
error.message,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ describe("element binding", () => {
|
|||||||
expect(API.getSelectedElement().type).toBe("arrow");
|
expect(API.getSelectedElement().type).toBe("arrow");
|
||||||
|
|
||||||
// NOTE this mouse down/up + await needs to be done in order to repro
|
// NOTE this mouse down/up + await needs to be done in order to repro
|
||||||
// the issue, due to https://github.com/excalidraw/excalidraw/blob/46bff3daceb602accf60c40a84610797260fca94/src/components/App.tsx#L740
|
// the issue, due to https://github.com/excalidraw/excalidraw/blob/46bff3daceb602accf60c40a84610797260fca94/src/components/App.tsx#L740
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
expect(h.state.editingLinearElement).not.toBe(null);
|
expect(h.state.editingLinearElement).not.toBe(null);
|
||||||
mouse.down(0, 0);
|
mouse.down(0, 0);
|
||||||
|
@ -19,7 +19,7 @@ const testElements = [
|
|||||||
text: "😀",
|
text: "😀",
|
||||||
}),
|
}),
|
||||||
// can't get jsdom text measurement to work so this is a temp hack
|
// can't get jsdom text measurement to work so this is a temp hack
|
||||||
// to ensure the element isn't stripped as invisible
|
// to ensure the element isn't stripped as invisible
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
},
|
},
|
||||||
|
@ -1569,7 +1569,7 @@ it(
|
|||||||
expect(API.getSelectedElements().length).toBe(3);
|
expect(API.getSelectedElements().length).toBe(3);
|
||||||
|
|
||||||
// clicking on first rectangle that is part of the group should select
|
// clicking on first rectangle that is part of the group should select
|
||||||
// that group (exclusively)
|
// that group (exclusively)
|
||||||
mouse.clickOn(rect1);
|
mouse.clickOn(rect1);
|
||||||
expect(API.getSelectedElements().length).toBe(2);
|
expect(API.getSelectedElements().length).toBe(2);
|
||||||
expect(Object.keys(h.state.selectedGroupIds).length).toBe(1);
|
expect(Object.keys(h.state.selectedGroupIds).length).toBe(1);
|
||||||
@ -1594,8 +1594,7 @@ it(
|
|||||||
mouse.up(100, 100);
|
mouse.up(100, 100);
|
||||||
|
|
||||||
// Select first rectangle while keeping third one selected.
|
// Select first rectangle while keeping third one selected.
|
||||||
// Third rectangle is selected because it was the last element
|
// Third rectangle is selected because it was the last element to be created.
|
||||||
// to be created.
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
mouse.click();
|
mouse.click();
|
||||||
@ -1616,8 +1615,7 @@ it(
|
|||||||
});
|
});
|
||||||
expect(API.getSelectedElements().length).toBe(3);
|
expect(API.getSelectedElements().length).toBe(3);
|
||||||
|
|
||||||
// pointer down o first rectangle that is
|
// Pointer down o first rectangle that is part of the group
|
||||||
// part of the group
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
mouse.down();
|
mouse.down();
|
||||||
|
@ -41,7 +41,7 @@ export type AppState = {
|
|||||||
startBoundElement: NonDeleted<ExcalidrawBindableElement> | null;
|
startBoundElement: NonDeleted<ExcalidrawBindableElement> | null;
|
||||||
suggestedBindings: SuggestedBinding[];
|
suggestedBindings: SuggestedBinding[];
|
||||||
// element being edited, but not necessarily added to elements array yet
|
// element being edited, but not necessarily added to elements array yet
|
||||||
// (e.g. text element when typing into the input)
|
// (e.g. text element when typing into the input)
|
||||||
editingElement: NonDeletedExcalidrawElement | null;
|
editingElement: NonDeletedExcalidrawElement | null;
|
||||||
editingLinearElement: LinearElementEditor | null;
|
editingLinearElement: LinearElementEditor | null;
|
||||||
elementType: typeof SHAPES[number]["value"];
|
elementType: typeof SHAPES[number]["value"];
|
||||||
|
@ -94,7 +94,7 @@ export const measureText = (text: string, font: FontString) => {
|
|||||||
line.innerText = text
|
line.innerText = text
|
||||||
.split("\n")
|
.split("\n")
|
||||||
// replace empty lines with single space because leading/trailing empty
|
// replace empty lines with single space because leading/trailing empty
|
||||||
// lines would be stripped from computation
|
// lines would be stripped from computation
|
||||||
.map((x) => x || " ")
|
.map((x) => x || " ")
|
||||||
.join("\n");
|
.join("\n");
|
||||||
const width = line.offsetWidth;
|
const width = line.offsetWidth;
|
||||||
|
@ -62,7 +62,7 @@ const getTargetIndex = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// if we're editing group, find closest sibling irrespective of whether
|
// if we're editing group, find closest sibling irrespective of whether
|
||||||
// there's a different-group element between them (for legacy reasons)
|
// there's a different-group element between them (for legacy reasons)
|
||||||
if (appState.editingGroupId) {
|
if (appState.editingGroupId) {
|
||||||
return element.groupIds.includes(appState.editingGroupId);
|
return element.groupIds.includes(appState.editingGroupId);
|
||||||
}
|
}
|
||||||
@ -106,7 +106,7 @@ const getTargetIndex = (
|
|||||||
|
|
||||||
if (elementsInSiblingGroup.length) {
|
if (elementsInSiblingGroup.length) {
|
||||||
// assumes getElementsInGroup() returned elements are sorted
|
// assumes getElementsInGroup() returned elements are sorted
|
||||||
// by zIndex (ascending)
|
// by zIndex (ascending)
|
||||||
return direction === "left"
|
return direction === "left"
|
||||||
? elements.indexOf(elementsInSiblingGroup[0])
|
? elements.indexOf(elementsInSiblingGroup[0])
|
||||||
: elements.indexOf(
|
: elements.indexOf(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user