feat: support selecting multiple library items via shift
(#4306)
This commit is contained in:
parent
b53d1f6f3e
commit
06db702b5d
@ -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({
|
||||
|
@ -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({
|
||||
|
@ -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 = (
|
||||
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
@ -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 (
|
||||
<div
|
||||
className={clsx("Checkbox", className, { "is-checked": checked })}
|
||||
onClick={(event) => {
|
||||
onChange(!checked);
|
||||
onChange(!checked, event);
|
||||
(
|
||||
(event.currentTarget as HTMLDivElement).querySelector(
|
||||
".Checkbox-box",
|
||||
|
@ -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<HTMLElement>,
|
||||
@ -236,6 +237,10 @@ export const LibraryMenu = ({
|
||||
],
|
||||
);
|
||||
|
||||
const [lastSelectedItem, setLastSelectedItem] = useState<
|
||||
LibraryItem["id"] | null
|
||||
>(null);
|
||||
|
||||
return loadingState === "preloading" ? null : (
|
||||
<Island padding={1} ref={ref} className="layer-ui__library">
|
||||
{showPublishLibraryDialog && (
|
||||
@ -271,10 +276,44 @@ export const LibraryMenu = ({
|
||||
files={files}
|
||||
id={id}
|
||||
selectedItems={selectedItems}
|
||||
onToggle={(id) => {
|
||||
if (!selectedItems.includes(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));
|
||||
}
|
||||
}}
|
||||
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
</Stack.Col>
|
||||
|
@ -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 {
|
||||
|
@ -8,12 +8,15 @@ import { BinaryFiles, LibraryItem } from "../types";
|
||||
import "./LibraryUnit.scss";
|
||||
import { CheckboxItem } from "./CheckboxItem";
|
||||
|
||||
// fa-plus
|
||||
const PLUS_ICON = (
|
||||
<svg viewBox="0 0 1792 1792">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M1600 736v192q0 40-28 68t-68 28h-416v416q0 40-28 68t-68 28h-192q-40 0-68-28t-28-68v-416h-416q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h416v-416q0-40 28-68t68-28h192q40 0 68 28t28 68v416h416q40 0 68 28t28 68z"
|
||||
d="M1600 736v192c0 26.667-9.33 49.333-28 68-18.67 18.67-41.33 28-68 28h-416v416c0 26.67-9.33 49.33-28 68s-41.33 28-68 28H800c-26.667 0-49.333-9.33-68-28s-28-41.33-28-68v-416H288c-26.667 0-49.333-9.33-68-28-18.667-18.667-28-41.333-28-68V736c0-26.667 9.333-49.333 28-68s41.333-28 68-28h416V224c0-26.667 9.333-49.333 28-68s41.333-28 68-28h192c26.67 0 49.33 9.333 68 28s28 41.333 28 68v416h416c26.67 0 49.33 9.333 68 28s28 41.333 28 68Z"
|
||||
style={{
|
||||
stroke: "#fff",
|
||||
strokeWidth: 140,
|
||||
}}
|
||||
transform="translate(0 64)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@ -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<HTMLDivElement | null>(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) && (
|
||||
<CheckboxItem
|
||||
checked={selected}
|
||||
onChange={() => onToggle(id)}
|
||||
onChange={(checked, event) => onToggle(id, event)}
|
||||
className="library-unit__checkbox"
|
||||
/>
|
||||
)}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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_**
|
||||
|
||||
<pre>
|
||||
getElementsMap(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>): {[id: string]: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement</a>}
|
||||
</pre>
|
||||
|
||||
**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
|
||||
|
@ -171,7 +171,6 @@ const forwardedRefComp = forwardRef<
|
||||
export default React.memo(forwardedRefComp, areEqual);
|
||||
export {
|
||||
getSceneVersion,
|
||||
getElementMap,
|
||||
isInvisiblySmallElement,
|
||||
getNonDeletedElements,
|
||||
} from "../../element";
|
||||
|
13
src/utils.ts
13
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 = <T extends { id: string } | string>(
|
||||
items: readonly T[],
|
||||
) => {
|
||||
return items.reduce((acc: Map<string, T>, element) => {
|
||||
acc.set(typeof element === "string" ? element : element.id, element);
|
||||
return acc;
|
||||
}, new Map());
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user