diff --git a/src/actions/actionAlign.tsx b/src/actions/actionAlign.tsx
index eb51e80b..f00ae298 100644
--- a/src/actions/actionAlign.tsx
+++ b/src/actions/actionAlign.tsx
@@ -8,13 +8,13 @@ import {
CenterVerticallyIcon,
} from "../components/icons";
import { ToolButton } from "../components/ToolButton";
-import { getElementMap, getNonDeletedElements } from "../element";
+import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { KEYS } from "../keys";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { AppState } from "../types";
-import { getShortcutKey } from "../utils";
+import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
const enableActionGroup = (
@@ -34,9 +34,11 @@ const alignSelectedElements = (
const updatedElements = alignElements(selectedElements, alignment);
- const updatedElementsMap = getElementMap(updatedElements);
+ const updatedElementsMap = arrayToMap(updatedElements);
- return elements.map((element) => updatedElementsMap[element.id] || element);
+ return elements.map(
+ (element) => updatedElementsMap.get(element.id) || element,
+ );
};
export const actionAlignTop = register({
diff --git a/src/actions/actionDistribute.tsx b/src/actions/actionDistribute.tsx
index 8744fac6..578a4a57 100644
--- a/src/actions/actionDistribute.tsx
+++ b/src/actions/actionDistribute.tsx
@@ -4,13 +4,13 @@ import {
} from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { distributeElements, Distribution } from "../disitrubte";
-import { getElementMap, getNonDeletedElements } from "../element";
+import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { CODES } from "../keys";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { AppState } from "../types";
-import { getShortcutKey } from "../utils";
+import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
const enableActionGroup = (
@@ -30,9 +30,11 @@ const distributeSelectedElements = (
const updatedElements = distributeElements(selectedElements, distribution);
- const updatedElementsMap = getElementMap(updatedElements);
+ const updatedElementsMap = arrayToMap(updatedElements);
- return elements.map((element) => updatedElementsMap[element.id] || element);
+ return elements.map(
+ (element) => updatedElementsMap.get(element.id) || element,
+ );
};
export const distributeHorizontally = register({
diff --git a/src/actions/actionFlip.ts b/src/actions/actionFlip.ts
index 4a4e4bd1..b3045103 100644
--- a/src/actions/actionFlip.ts
+++ b/src/actions/actionFlip.ts
@@ -1,6 +1,6 @@
import { register } from "./register";
import { getSelectedElements } from "../scene";
-import { getElementMap, getNonDeletedElements } from "../element";
+import { getNonDeletedElements } from "../element";
import { mutateElement } from "../element/mutateElement";
import { ExcalidrawElement, NonDeleted } from "../element/types";
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
@@ -9,6 +9,7 @@ import { getTransformHandles } from "../element/transformHandles";
import { isFreeDrawElement, isLinearElement } from "../element/typeChecks";
import { updateBoundElements } from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor";
+import { arrayToMap } from "../utils";
const enableActionFlipHorizontal = (
elements: readonly ExcalidrawElement[],
@@ -83,9 +84,11 @@ const flipSelectedElements = (
flipDirection,
);
- const updatedElementsMap = getElementMap(updatedElements);
+ const updatedElementsMap = arrayToMap(updatedElements);
- return elements.map((element) => updatedElementsMap[element.id] || element);
+ return elements.map(
+ (element) => updatedElementsMap.get(element.id) || element,
+ );
};
const flipElements = (
diff --git a/src/actions/actionHistory.tsx b/src/actions/actionHistory.tsx
index 6b3cf713..3aa4ca8a 100644
--- a/src/actions/actionHistory.tsx
+++ b/src/actions/actionHistory.tsx
@@ -6,9 +6,9 @@ import History, { HistoryEntry } from "../history";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { isWindows, KEYS } from "../keys";
-import { getElementMap } from "../element";
import { newElementWith } from "../element/mutateElement";
import { fixBindingsAfterDeletion } from "../element/binding";
+import { arrayToMap } from "../utils";
const writeData = (
prevElements: readonly ExcalidrawElement[],
@@ -27,17 +27,17 @@ const writeData = (
return { commitToHistory };
}
- const prevElementMap = getElementMap(prevElements);
+ const prevElementMap = arrayToMap(prevElements);
const nextElements = data.elements;
- const nextElementMap = getElementMap(nextElements);
+ const nextElementMap = arrayToMap(nextElements);
const deletedElements = prevElements.filter(
- (prevElement) => !nextElementMap.hasOwnProperty(prevElement.id),
+ (prevElement) => !nextElementMap.has(prevElement.id),
);
const elements = nextElements
.map((nextElement) =>
newElementWith(
- prevElementMap[nextElement.id] || nextElement,
+ prevElementMap.get(nextElement.id) || nextElement,
nextElement,
),
)
diff --git a/src/components/CheckboxItem.tsx b/src/components/CheckboxItem.tsx
index a1fe246e..c7685294 100644
--- a/src/components/CheckboxItem.tsx
+++ b/src/components/CheckboxItem.tsx
@@ -6,14 +6,14 @@ import "./CheckboxItem.scss";
export const CheckboxItem: React.FC<{
checked: boolean;
- onChange: (checked: boolean) => void;
+ onChange: (checked: boolean, event: React.MouseEvent) => void;
className?: string;
}> = ({ children, checked, onChange, className }) => {
return (
{
- onChange(!checked);
+ onChange(!checked, event);
(
(event.currentTarget as HTMLDivElement).querySelector(
".Checkbox-box",
diff --git a/src/components/LibraryMenu.tsx b/src/components/LibraryMenu.tsx
index 26bec7ab..763dfb88 100644
--- a/src/components/LibraryMenu.tsx
+++ b/src/components/LibraryMenu.tsx
@@ -18,6 +18,7 @@ import "./LibraryMenu.scss";
import LibraryMenuItems from "./LibraryMenuItems";
import { EVENT } from "../constants";
import { KEYS } from "../keys";
+import { arrayToMap } from "../utils";
const useOnClickOutside = (
ref: RefObject
,
@@ -236,6 +237,10 @@ export const LibraryMenu = ({
],
);
+ const [lastSelectedItem, setLastSelectedItem] = useState<
+ LibraryItem["id"] | null
+ >(null);
+
return loadingState === "preloading" ? null : (
{showPublishLibraryDialog && (
@@ -271,10 +276,44 @@ export const LibraryMenu = ({
files={files}
id={id}
selectedItems={selectedItems}
- onToggle={(id) => {
- if (!selectedItems.includes(id)) {
- setSelectedItems([...selectedItems, id]);
+ onToggle={(id, event) => {
+ const shouldSelect = !selectedItems.includes(id);
+
+ if (shouldSelect) {
+ if (event.shiftKey && lastSelectedItem) {
+ const rangeStart = libraryItems.findIndex(
+ (item) => item.id === lastSelectedItem,
+ );
+ const rangeEnd = libraryItems.findIndex(
+ (item) => item.id === id,
+ );
+
+ if (rangeStart === -1 || rangeEnd === -1) {
+ setSelectedItems([...selectedItems, id]);
+ return;
+ }
+
+ const selectedItemsMap = arrayToMap(selectedItems);
+ const nextSelectedIds = libraryItems.reduce(
+ (acc: LibraryItem["id"][], item, idx) => {
+ if (
+ (idx >= rangeStart && idx <= rangeEnd) ||
+ selectedItemsMap.has(item.id)
+ ) {
+ acc.push(item.id);
+ }
+ return acc;
+ },
+ [],
+ );
+
+ setSelectedItems(nextSelectedIds);
+ } else {
+ setSelectedItems([...selectedItems, id]);
+ }
+ setLastSelectedItem(id);
} else {
+ setLastSelectedItem(null);
setSelectedItems(selectedItems.filter((_id) => _id !== id));
}
}}
diff --git a/src/components/LibraryMenuItems.tsx b/src/components/LibraryMenuItems.tsx
index 93f8bd17..b2dd630d 100644
--- a/src/components/LibraryMenuItems.tsx
+++ b/src/components/LibraryMenuItems.tsx
@@ -52,7 +52,7 @@ const LibraryMenuItems = ({
library: Library;
id: string;
selectedItems: LibraryItem["id"][];
- onToggle: (id: LibraryItem["id"]) => void;
+ onToggle: (id: LibraryItem["id"], event: React.MouseEvent) => void;
onPublish: () => void;
resetLibrary: () => void;
}) => {
@@ -213,10 +213,8 @@ const LibraryMenuItems = ({
onClick={params.onClick || (() => {})}
id={params.item?.id || null}
selected={!!params.item?.id && selectedItems.includes(params.item.id)}
- onToggle={() => {
- if (params.item?.id) {
- onToggle(params.item.id);
- }
+ onToggle={(id, event) => {
+ onToggle(id, event);
}}
/>
diff --git a/src/components/LibraryUnit.scss b/src/components/LibraryUnit.scss
index f4e81848..6181da9c 100644
--- a/src/components/LibraryUnit.scss
+++ b/src/components/LibraryUnit.scss
@@ -99,8 +99,13 @@
margin-top: -10px;
pointer-events: none;
}
- .library-unit--hover .library-unit__adder {
- color: $oc-blue-7;
+ .library-unit:hover .library-unit__adder {
+ fill: $oc-blue-7;
+ }
+ .library-unit:active .library-unit__adder {
+ animation: none;
+ transform: scale(0.8);
+ fill: $oc-black;
}
.library-unit__active {
diff --git a/src/components/LibraryUnit.tsx b/src/components/LibraryUnit.tsx
index f12c5caf..46b0e8d4 100644
--- a/src/components/LibraryUnit.tsx
+++ b/src/components/LibraryUnit.tsx
@@ -8,12 +8,15 @@ import { BinaryFiles, LibraryItem } from "../types";
import "./LibraryUnit.scss";
import { CheckboxItem } from "./CheckboxItem";
-// fa-plus
const PLUS_ICON = (
);
@@ -33,7 +36,7 @@ export const LibraryUnit = ({
isPending?: boolean;
onClick: () => void;
selected: boolean;
- onToggle: (id: string) => void;
+ onToggle: (id: string, event: React.MouseEvent) => void;
}) => {
const ref = useRef(null);
useEffect(() => {
@@ -84,7 +87,17 @@ export const LibraryUnit = ({
})}
ref={ref}
draggable={!!elements}
- onClick={!!elements || !!isPending ? onClick : undefined}
+ onClick={
+ !!elements || !!isPending
+ ? (event) => {
+ if (id && event.shiftKey) {
+ onToggle(id, event);
+ } else {
+ onClick();
+ }
+ }
+ : undefined
+ }
onDragStart={(event) => {
setIsHovered(false);
event.dataTransfer.setData(
@@ -97,7 +110,7 @@ export const LibraryUnit = ({
{id && elements && (isHovered || isMobile || selected) && (
onToggle(id)}
+ onChange={(checked, event) => onToggle(id, event)}
className="library-unit__checkbox"
/>
)}
diff --git a/src/data/restore.ts b/src/data/restore.ts
index 32c7acb7..cb316b4c 100644
--- a/src/data/restore.ts
+++ b/src/data/restore.ts
@@ -10,11 +10,7 @@ import {
NormalizedZoomValue,
} from "../types";
import { ImportedDataState } from "./types";
-import {
- getElementMap,
- getNormalizedDimensions,
- isInvisiblySmallElement,
-} from "../element";
+import { getNormalizedDimensions, isInvisiblySmallElement } from "../element";
import { isLinearElementType } from "../element/typeChecks";
import { randomId } from "../random";
import {
@@ -27,6 +23,7 @@ import { getDefaultAppState } from "../appState";
import { LinearElementEditor } from "../element/linearElementEditor";
import { bumpVersion } from "../element/mutateElement";
import { getUpdatedTimestamp } from "../utils";
+import { arrayToMap } from "../utils";
type RestoredAppState = Omit<
AppState,
@@ -206,14 +203,14 @@ export const restoreElements = (
/** NOTE doesn't serve for reconciliation */
localElements: readonly ExcalidrawElement[] | null | undefined,
): ExcalidrawElement[] => {
- const localElementsMap = localElements ? getElementMap(localElements) : null;
+ const localElementsMap = localElements ? arrayToMap(localElements) : null;
return (elements || []).reduce((elements, element) => {
// filtering out selection, which is legacy, no longer kept in elements,
// and causing issues if retained
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
let migratedElement: ExcalidrawElement | null = restoreElement(element);
if (migratedElement) {
- const localElement = localElementsMap?.[element.id];
+ const localElement = localElementsMap?.get(element.id);
if (localElement && localElement.version > migratedElement.version) {
migratedElement = bumpVersion(migratedElement, localElement.version);
}
diff --git a/src/element/index.ts b/src/element/index.ts
index 78eaa669..ef4059c9 100644
--- a/src/element/index.ts
+++ b/src/element/index.ts
@@ -59,15 +59,6 @@ export {
} from "./sizeHelpers";
export { showSelectedShapeActions } from "./showSelectedShapeActions";
-export const getElementMap = (elements: readonly ExcalidrawElement[]) =>
- elements.reduce(
- (acc: { [key: string]: ExcalidrawElement }, element: ExcalidrawElement) => {
- acc[element.id] = element;
- return acc;
- },
- {},
- );
-
export const getSceneVersion = (elements: readonly ExcalidrawElement[]) =>
elements.reduce((acc, el) => acc + el.version, 0);
diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md
index 8db3d150..afbb7b1e 100644
--- a/src/packages/excalidraw/CHANGELOG.md
+++ b/src/packages/excalidraw/CHANGELOG.md
@@ -15,6 +15,10 @@ Please add the latest change on the top under the correct section.
### Features
+- #### BREAKING CHANGE
+
+ Removed `getElementMap` util method.
+
- Changes to [`exportToCanvas`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportToCanvas) util function:
- Add `maxWidthOrHeight?: number` attribute.
diff --git a/src/packages/excalidraw/README_NEXT.md b/src/packages/excalidraw/README_NEXT.md
index e4c1a561..6b38f9a5 100644
--- a/src/packages/excalidraw/README_NEXT.md
+++ b/src/packages/excalidraw/README_NEXT.md
@@ -865,22 +865,6 @@ import { isInvisiblySmallElement } from "@excalidraw/excalidraw-next";
Returns `true` if element is invisibly small (e.g. width & height are zero).
-#### `getElementMap`
-
-**_Signature_**
-
-
-getElementsMap(elements: ExcalidrawElement[]): {[id: string]: ExcalidrawElement}
-
-
-**How to use**
-
-```js
-import { getElementsMap } from "@excalidraw/excalidraw-next";
-```
-
-This function returns an object where each element is mapped to its id.
-
#### `loadLibraryFromBlob`
```js
diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx
index aa0e3495..060908a5 100644
--- a/src/packages/excalidraw/index.tsx
+++ b/src/packages/excalidraw/index.tsx
@@ -171,7 +171,6 @@ const forwardedRefComp = forwardRef<
export default React.memo(forwardedRefComp, areEqual);
export {
getSceneVersion,
- getElementMap,
isInvisiblySmallElement,
getNonDeletedElements,
} from "../../element";
diff --git a/src/utils.ts b/src/utils.ts
index a3b9fe0f..dd6e6573 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -476,3 +476,16 @@ export const bytesToHexString = (bytes: Uint8Array) => {
export const getUpdatedTimestamp = () =>
process.env.NODE_ENV === "test" ? 1 : Date.now();
+
+/**
+ * Transforms array of objects containing `id` attribute,
+ * or array of ids (strings), into a Map, keyd by `id`.
+ */
+export const arrayToMap = (
+ items: readonly T[],
+) => {
+ return items.reduce((acc: Map, element) => {
+ acc.set(typeof element === "string" ? element : element.id, element);
+ return acc;
+ }, new Map());
+};