snapshot of https://github.com/excalidraw/excalidraw.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Tests / test (push) Failing after 15s
Some checks failed
Tests / test (push) Failing after 15s
This commit is contained in:
191
packages/excalidraw/components/shapeActionPredicates.ts
Normal file
191
packages/excalidraw/components/shapeActionPredicates.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { isTransparent } from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
shouldAllowVerticalAlign,
|
||||
suppportsHorizontalAlign,
|
||||
getColorTargetElement,
|
||||
hasBoundTextElement,
|
||||
isElbowArrow,
|
||||
isImageElement,
|
||||
isLinearElement,
|
||||
isTextElement,
|
||||
hasStrokeColor,
|
||||
toolIsArrow,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawElementType,
|
||||
NonDeletedElementsMap,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { alignActionsPredicate } from "../actions/actionAlign";
|
||||
import {
|
||||
canChangeRoundness,
|
||||
canHaveArrowheads,
|
||||
getSelectedElements,
|
||||
hasBackground,
|
||||
hasFillStyle,
|
||||
hasFreedrawMode,
|
||||
hasRoughness,
|
||||
hasStrokeStyle,
|
||||
hasStrokeWidth,
|
||||
} from "../scene";
|
||||
|
||||
import type { ElementOrToolType } from "../types";
|
||||
|
||||
import type { AppClassProperties, UIAppState } from "../types";
|
||||
|
||||
export const canChangeStrokeColor = (
|
||||
appState: UIAppState,
|
||||
targetElements: ExcalidrawElement[],
|
||||
) => {
|
||||
let commonSelectedType: ExcalidrawElementType | null =
|
||||
targetElements[0]?.type || null;
|
||||
|
||||
for (const element of targetElements) {
|
||||
if (element.type !== commonSelectedType) {
|
||||
commonSelectedType = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
(hasStrokeColor(appState.activeTool.type) &&
|
||||
commonSelectedType !== "image" &&
|
||||
commonSelectedType !== "frame" &&
|
||||
commonSelectedType !== "magicframe") ||
|
||||
targetElements.some((element) => hasStrokeColor(element.type))
|
||||
);
|
||||
};
|
||||
|
||||
export const canChangeBackgroundColor = (
|
||||
appState: UIAppState,
|
||||
targetElements: ExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
return (
|
||||
hasBackground(appState.activeTool.type) ||
|
||||
// a note's label (the target while editing it) has no fill, but a
|
||||
// background pick on it colors the note — so the picker stays available
|
||||
targetElements.some((element) =>
|
||||
hasBackground(
|
||||
getColorTargetElement(element, "backgroundColor", elementsMap).type,
|
||||
),
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Single source of truth for "which shape-action controls are relevant right
|
||||
* now". Each flag answers whether a given control should be shown, given the
|
||||
* active tool and the current selection. All three styles-panel layouts
|
||||
* (full / compact / mobile) and the compact popovers consume these flags so
|
||||
* that visibility logic lives in one place and layout stays pure.
|
||||
*/
|
||||
export const getShapeActionPredicates = (
|
||||
appState: UIAppState,
|
||||
targetElements: ExcalidrawElement[],
|
||||
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap,
|
||||
app: AppClassProperties,
|
||||
) => {
|
||||
const activeToolType = appState.activeTool.type;
|
||||
|
||||
// A property is relevant when it applies to the active tool (so it can be
|
||||
// preconfigured before drawing) or to any currently selected element.
|
||||
const forToolOrSelection = (
|
||||
predicate: (type: ElementOrToolType) => boolean,
|
||||
) =>
|
||||
predicate(activeToolType) ||
|
||||
targetElements.some((element) => predicate(element.type));
|
||||
|
||||
const singleSelected = targetElements.length === 1;
|
||||
|
||||
// a container + its bound text read as a single logical element (2 selected)
|
||||
const isSingleElementBoundContainer =
|
||||
targetElements.length === 2 &&
|
||||
(hasBoundTextElement(targetElements[0]) ||
|
||||
hasBoundTextElement(targetElements[1]));
|
||||
|
||||
const isEditingTextOrNewElement = Boolean(
|
||||
appState.editingTextElement || appState.newElement,
|
||||
);
|
||||
const hasSelection = targetElements.length > 0;
|
||||
|
||||
return {
|
||||
/** some element(s) selected */
|
||||
hasSelection,
|
||||
/** actions on selected elements (delete/duplicate/...) */
|
||||
showExtraActions: hasSelection && !isEditingTextOrNewElement,
|
||||
|
||||
// color
|
||||
strokeColor: canChangeStrokeColor(appState, targetElements),
|
||||
backgroundColor: canChangeBackgroundColor(
|
||||
appState,
|
||||
targetElements,
|
||||
elementsMap,
|
||||
),
|
||||
fill:
|
||||
// bucket fill never renders transparent (it falls back to a real
|
||||
// color), so its fill style stays relevant either way
|
||||
activeToolType === "bucketfill" ||
|
||||
(hasFillStyle(activeToolType) &&
|
||||
!isTransparent(appState.currentItemBackgroundColor)) ||
|
||||
targetElements.some(
|
||||
(element) =>
|
||||
hasFillStyle(element.type) && !isTransparent(element.backgroundColor),
|
||||
),
|
||||
|
||||
// stroke / shape properties
|
||||
strokeWidth: forToolOrSelection(hasStrokeWidth),
|
||||
freedrawMode: forToolOrSelection(hasFreedrawMode),
|
||||
strokeStyle: forToolOrSelection(hasStrokeStyle),
|
||||
sloppiness: forToolOrSelection(hasRoughness),
|
||||
roundness: forToolOrSelection(canChangeRoundness),
|
||||
arrowType: forToolOrSelection(toolIsArrow),
|
||||
arrowheads: forToolOrSelection(canHaveArrowheads),
|
||||
|
||||
// text
|
||||
text: activeToolType === "text" || targetElements.some(isTextElement),
|
||||
textAlign:
|
||||
activeToolType === "text" ||
|
||||
suppportsHorizontalAlign(targetElements, elementsMap),
|
||||
verticalAlign: shouldAllowVerticalAlign(targetElements, elementsMap),
|
||||
|
||||
opacity: activeToolType !== "autoshape" || hasSelection,
|
||||
|
||||
// arrangement
|
||||
// z-order controls are hidden while the freedraw or drawShape tool is
|
||||
// active without an actual selection
|
||||
layers:
|
||||
(activeToolType !== "freedraw" &&
|
||||
activeToolType !== "autoshape" &&
|
||||
!targetElements.some((element) => element.type === "freedraw")) ||
|
||||
getSelectedElements(elementsMap, appState).some(
|
||||
(element) => element.type === "freedraw",
|
||||
),
|
||||
align:
|
||||
!isSingleElementBoundContainer && alignActionsPredicate(appState, app),
|
||||
distribute: targetElements.length > 2,
|
||||
|
||||
// per-element actions
|
||||
// NOTE: the full panel treats a bound container as linkable; the compact /
|
||||
// mobile layouts only link a truly single selection. Both are preserved via
|
||||
// the two flags below.
|
||||
link: singleSelected || isSingleElementBoundContainer,
|
||||
linkSingleOnly: singleSelected,
|
||||
cropEditor:
|
||||
!appState.croppingElementId &&
|
||||
singleSelected &&
|
||||
isImageElement(targetElements[0]),
|
||||
lineEditor:
|
||||
!appState.selectedLinearElement?.isEditing &&
|
||||
singleSelected &&
|
||||
isLinearElement(targetElements[0]) &&
|
||||
!isElbowArrow(targetElements[0]),
|
||||
};
|
||||
};
|
||||
|
||||
export type ShapeActionPredicates = ReturnType<typeof getShapeActionPredicates>;
|
||||
Reference in New Issue
Block a user