Resize handler detection should not be active when moving multip… (#767)

* Fix bug.

* Implement `getSelectedElements`.

* Explicit condition.

* Respect variable naming.

* Keep state consistent.

* Use `isSomeElementSelected` abstraction.

* Missing ones.
This commit is contained in:
Enzo Ferey 2020-02-16 22:54:50 +01:00 committed by GitHub
parent ad72946131
commit 6ebd41734f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 53 additions and 37 deletions

View File

@ -1,5 +1,5 @@
import { Action } from "./types"; import { Action } from "./types";
import { deleteSelectedElements } from "../scene"; import { deleteSelectedElements, isSomeElementSelected } from "../scene";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
export const actionDeleteSelected: Action = { export const actionDeleteSelected: Action = {
@ -12,6 +12,6 @@ export const actionDeleteSelected: Action = {
}, },
contextItemLabel: "labels.delete", contextItemLabel: "labels.delete",
contextMenuOrder: 3, contextMenuOrder: 3,
commitToHistory: (_, elements) => elements.some(el => el.isSelected), commitToHistory: (_, elements) => isSomeElementSelected(elements),
keyTest: event => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE, keyTest: event => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
}; };

View File

@ -1,7 +1,10 @@
import React from "react"; import React from "react";
import { Action } from "./types"; import { Action } from "./types";
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types"; import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
import { getCommonAttributeOfSelectedElements } from "../scene"; import {
getCommonAttributeOfSelectedElements,
isSomeElementSelected,
} from "../scene";
import { ButtonSelect } from "../components/ButtonSelect"; import { ButtonSelect } from "../components/ButtonSelect";
import { isTextElement, redrawTextBoundingBox } from "../element"; import { isTextElement, redrawTextBoundingBox } from "../element";
import { ColorPicker } from "../components/ColorPicker"; import { ColorPicker } from "../components/ColorPicker";
@ -28,7 +31,7 @@ const getFormValue = function<T>(
): T | null { ): T | null {
return ( return (
(editingElement && getAttribute(editingElement)) ?? (editingElement && getAttribute(editingElement)) ??
(elements.some(element => element.isSelected) (isSomeElementSelected(elements)
? getCommonAttributeOfSelectedElements(elements, getAttribute) ? getCommonAttributeOfSelectedElements(elements, getAttribute)
: defaultValue) ?? : defaultValue) ??
null null

View File

@ -1,4 +1,5 @@
import { ExcalidrawElement } from "./element/types"; import { ExcalidrawElement } from "./element/types";
import { getSelectedElements } from "./scene";
let CLIPBOARD = ""; let CLIPBOARD = "";
let PREFER_APP_CLIPBOARD = false; let PREFER_APP_CLIPBOARD = false;
@ -19,9 +20,7 @@ export async function copyToAppClipboard(
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
) { ) {
CLIPBOARD = JSON.stringify( CLIPBOARD = JSON.stringify(
elements getSelectedElements(elements).map(({ shape, ...el }) => el),
.filter(element => element.isSelected)
.map(({ shape, ...el }) => el),
); );
try { try {
// when copying to in-app clipboard, clear system clipboard so that if // when copying to in-app clipboard, clear system clipboard so that if

View File

@ -16,6 +16,7 @@ import { t } from "../i18n";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { probablySupportsClipboardBlob } from "../clipboard"; import { probablySupportsClipboardBlob } from "../clipboard";
import { getSelectedElements, isSomeElementSelected } from "../scene";
const scales = [1, 2, 3]; const scales = [1, 2, 3];
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1; const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
@ -46,7 +47,7 @@ function ExportModal({
onExportToBackend: ExportCB; onExportToBackend: ExportCB;
onCloseRequest: () => void; onCloseRequest: () => void;
}) { }) {
const someElementIsSelected = elements.some(element => element.isSelected); const someElementIsSelected = isSomeElementSelected(elements);
const [scale, setScale] = useState(defaultScale); const [scale, setScale] = useState(defaultScale);
const [exportSelected, setExportSelected] = useState(someElementIsSelected); const [exportSelected, setExportSelected] = useState(someElementIsSelected);
const previewRef = useRef<HTMLDivElement>(null); const previewRef = useRef<HTMLDivElement>(null);
@ -56,7 +57,7 @@ function ExportModal({
const onlySelectedInput = useRef<HTMLInputElement>(null); const onlySelectedInput = useRef<HTMLInputElement>(null);
const exportedElements = exportSelected const exportedElements = exportSelected
? elements.filter(element => element.isSelected) ? getSelectedElements(elements)
: elements; : elements;
useEffect(() => { useEffect(() => {

View File

@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { t } from "../i18n"; import { t } from "../i18n";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { getSelectedElements } from "../scene";
import "./HintViewer.css"; import "./HintViewer.css";
@ -20,7 +21,7 @@ const getHints = ({ elementType, multiMode, isResizing, elements }: Hint) => {
} }
if (isResizing) { if (isResizing) {
const selectedElements = elements.filter(el => el.isSelected); const selectedElements = getSelectedElements(elements);
if ( if (
selectedElements.length === 1 && selectedElements.length === 1 &&
(selectedElements[0].type === "arrow" || (selectedElements[0].type === "arrow" ||

View File

@ -36,6 +36,8 @@ import {
loadFromBlob, loadFromBlob,
getZoomOrigin, getZoomOrigin,
getNormalizedZoom, getNormalizedZoom,
getSelectedElements,
isSomeElementSelected,
} from "./scene"; } from "./scene";
import { renderScene } from "./renderer"; import { renderScene } from "./renderer";
@ -275,7 +277,7 @@ const LayerUI = React.memo(
const { elementType, editingElement } = appState; const { elementType, editingElement } = appState;
const targetElements = editingElement const targetElements = editingElement
? [editingElement] ? [editingElement]
: elements.filter(el => el.isSelected); : getSelectedElements(elements);
if (!targetElements.length && elementType === "selection") { if (!targetElements.length && elementType === "selection") {
return null; return null;
} }
@ -1046,11 +1048,15 @@ export class App extends React.Component<any, AppState> {
{ x, y }, { x, y },
this.state.zoom, this.state.zoom,
); );
this.setState({
resizingElement: resizeElement ? resizeElement.element : null,
});
if (resizeElement) { const selectedElements = getSelectedElements(elements);
if (selectedElements.length === 1 && resizeElement) {
this.setState({
resizingElement: resizeElement
? resizeElement.element
: null,
});
resizeHandle = resizeElement.resizeHandle; resizeHandle = resizeElement.resizeHandle;
document.documentElement.style.cursor = getCursorForResizingElement( document.documentElement.style.cursor = getCursorForResizingElement(
resizeElement, resizeElement,
@ -1087,13 +1093,11 @@ export class App extends React.Component<any, AppState> {
...element, ...element,
isSelected: false, isSelected: false,
})), })),
...elements ...getSelectedElements(elements).map(element => {
.filter(element => element.isSelected) const newElement = duplicateElement(element);
.map(element => { newElement.isSelected = true;
const newElement = duplicateElement(element); return newElement;
newElement.isSelected = true; }),
return newElement;
}),
]; ];
} }
} }
@ -1328,7 +1332,7 @@ export class App extends React.Component<any, AppState> {
if (isResizingElements && this.state.resizingElement) { if (isResizingElements && this.state.resizingElement) {
this.setState({ isResizing: true }); this.setState({ isResizing: true });
const el = this.state.resizingElement; const el = this.state.resizingElement;
const selectedElements = elements.filter(el => el.isSelected); const selectedElements = getSelectedElements(elements);
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
const { x, y } = viewportCoordsToSceneCoords( const { x, y } = viewportCoordsToSceneCoords(
e, e,
@ -1555,8 +1559,8 @@ export class App extends React.Component<any, AppState> {
// Marking that click was used for dragging to check // Marking that click was used for dragging to check
// if elements should be deselected on mouseup // if elements should be deselected on mouseup
draggingOccurred = true; draggingOccurred = true;
const selectedElements = elements.filter(el => el.isSelected); const selectedElements = getSelectedElements(elements);
if (selectedElements.length) { if (selectedElements.length > 0) {
const { x, y } = viewportCoordsToSceneCoords( const { x, y } = viewportCoordsToSceneCoords(
e, e,
this.state, this.state,
@ -1638,7 +1642,7 @@ export class App extends React.Component<any, AppState> {
draggingElement.shape = null; draggingElement.shape = null;
if (this.state.elementType === "selection") { if (this.state.elementType === "selection") {
if (!e.shiftKey && elements.some(el => el.isSelected)) { if (!e.shiftKey && isSomeElementSelected(elements)) {
elements = clearSelection(elements); elements = clearSelection(elements);
} }
const elementsWithinSelection = getElementsWithinSelection( const elementsWithinSelection = getElementsWithinSelection(
@ -1772,7 +1776,7 @@ export class App extends React.Component<any, AppState> {
if ( if (
elementType !== "selection" || elementType !== "selection" ||
elements.some(el => el.isSelected) isSomeElementSelected(elements)
) { ) {
history.resumeRecording(); history.resumeRecording();
} }
@ -1941,9 +1945,8 @@ export class App extends React.Component<any, AppState> {
return; return;
} }
const selectedElements = elements.filter(e => e.isSelected) const selectedElements = getSelectedElements(elements);
.length; if (selectedElements.length === 1) {
if (selectedElements === 1) {
const resizeElement = getElementWithResizeHandler( const resizeElement = getElementWithResizeHandler(
elements, elements,
{ x, y }, { x, y },

View File

@ -12,6 +12,7 @@ import {
SCROLLBAR_WIDTH, SCROLLBAR_WIDTH,
} from "../scene/scrollbars"; } from "../scene/scrollbars";
import { getZoomTranslation } from "../scene/zoom"; import { getZoomTranslation } from "../scene/zoom";
import { getSelectedElements } from "../scene/selection";
import { renderElement, renderElementToSvg } from "./renderElement"; import { renderElement, renderElementToSvg } from "./renderElement";
@ -135,7 +136,7 @@ export function renderScene(
// Pain selected elements // Pain selected elements
if (renderSelection) { if (renderSelection) {
const selectedElements = elements.filter(element => element.isSelected); const selectedElements = getSelectedElements(elements);
const dashledLinePadding = 4 / sceneState.zoom; const dashledLinePadding = 4 / sceneState.zoom;
context.save(); context.save();

View File

@ -3,9 +3,10 @@ export {
clearSelection, clearSelection,
getSelectedIndices, getSelectedIndices,
deleteSelectedElements, deleteSelectedElements,
someElementIsSelected, isSomeElementSelected,
getElementsWithinSelection, getElementsWithinSelection,
getCommonAttributeOfSelectedElements, getCommonAttributeOfSelectedElements,
getSelectedElements,
} from "./selection"; } from "./selection";
export { export {
exportCanvas, exportCanvas,

View File

@ -55,8 +55,11 @@ export function getSelectedIndices(elements: readonly ExcalidrawElement[]) {
return selectedIndices; return selectedIndices;
} }
export const someElementIsSelected = (elements: readonly ExcalidrawElement[]) => export function isSomeElementSelected(
elements.some(element => element.isSelected); elements: readonly ExcalidrawElement[],
): boolean {
return elements.some(element => element.isSelected);
}
/** /**
* Returns common attribute (picked by `getAttribute` callback) of selected * Returns common attribute (picked by `getAttribute` callback) of selected
@ -68,10 +71,14 @@ export function getCommonAttributeOfSelectedElements<T>(
): T | null { ): T | null {
const attributes = Array.from( const attributes = Array.from(
new Set( new Set(
elements getSelectedElements(elements).map(element => getAttribute(element)),
.filter(element => element.isSelected)
.map(element => getAttribute(element)),
), ),
); );
return attributes.length === 1 ? attributes[0] : null; return attributes.length === 1 ? attributes[0] : null;
} }
export function getSelectedElements(
elements: readonly ExcalidrawElement[],
): readonly ExcalidrawElement[] {
return elements.filter(element => element.isSelected);
}