refactor: make element type conversion more generic (#9504)

* feat: add `reduceToCommonValue()` & improve opacity slider

* feat: generalize and simplify type conversion cache

* refactor: change cache from atoms to Map

* feat: always attempt to reuse original fontSize when converting generic types
This commit is contained in:
David Luzar
2025-05-10 20:06:16 +02:00
committed by GitHub
parent 7e41026812
commit 51dbd4831b
10 changed files with 346 additions and 344 deletions

View File

@@ -20,6 +20,7 @@ import {
getShortcutKey,
tupleToCoors,
getLineHeight,
reduceToCommonValue,
} from "@excalidraw/common";
import { getNonDeletedElements } from "@excalidraw/element";
@@ -130,7 +131,6 @@ import { Fonts } from "../fonts";
import { getLanguage, t } from "../i18n";
import {
canHaveArrowheads,
getCommonAttributeOfSelectedElements,
getSelectedElements,
getTargetElements,
isSomeElementSelected,
@@ -167,12 +167,12 @@ export const changeProperty = (
export const getFormValue = function <T extends Primitive>(
elements: readonly ExcalidrawElement[],
appState: AppState,
app: AppClassProperties,
getAttribute: (element: ExcalidrawElement) => T,
isRelevantElement: true | ((element: ExcalidrawElement) => boolean),
defaultValue: T | ((isSomeElementSelected: boolean) => T),
): T {
const editingTextElement = appState.editingTextElement;
const editingTextElement = app.state.editingTextElement;
const nonDeletedElements = getNonDeletedElements(elements);
let ret: T | null = null;
@@ -182,17 +182,17 @@ export const getFormValue = function <T extends Primitive>(
}
if (!ret) {
const hasSelection = isSomeElementSelected(nonDeletedElements, appState);
const hasSelection = isSomeElementSelected(nonDeletedElements, app.state);
if (hasSelection) {
const selectedElements = app.scene.getSelectedElements(app.state);
const targetElements =
isRelevantElement === true
? selectedElements
: selectedElements.filter((el) => isRelevantElement(el));
ret =
getCommonAttributeOfSelectedElements(
isRelevantElement === true
? nonDeletedElements
: nonDeletedElements.filter((el) => isRelevantElement(el)),
appState,
getAttribute,
) ??
reduceToCommonValue(targetElements, getAttribute) ??
(typeof defaultValue === "function"
? defaultValue(true)
: defaultValue);
@@ -320,7 +320,7 @@ export const actionChangeStrokeColor = register({
: CaptureUpdateAction.EVENTUALLY,
};
},
PanelComponent: ({ elements, appState, updateData, appProps }) => (
PanelComponent: ({ elements, appState, updateData, app }) => (
<>
<h3 aria-hidden="true">{t("labels.stroke")}</h3>
<ColorPicker
@@ -330,7 +330,7 @@ export const actionChangeStrokeColor = register({
label={t("labels.stroke")}
color={getFormValue(
elements,
appState,
app,
(element) => element.strokeColor,
true,
appState.currentItemStrokeColor,
@@ -366,7 +366,7 @@ export const actionChangeBackgroundColor = register({
: CaptureUpdateAction.EVENTUALLY,
};
},
PanelComponent: ({ elements, appState, updateData, appProps }) => (
PanelComponent: ({ elements, appState, updateData, app }) => (
<>
<h3 aria-hidden="true">{t("labels.background")}</h3>
<ColorPicker
@@ -376,7 +376,7 @@ export const actionChangeBackgroundColor = register({
label={t("labels.background")}
color={getFormValue(
elements,
appState,
app,
(element) => element.backgroundColor,
true,
appState.currentItemBackgroundColor,
@@ -410,7 +410,7 @@ export const actionChangeFillStyle = register({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData }) => {
PanelComponent: ({ elements, appState, updateData, app }) => {
const selectedElements = getSelectedElements(elements, appState);
const allElementsZigZag =
selectedElements.length > 0 &&
@@ -446,7 +446,7 @@ export const actionChangeFillStyle = register({
]}
value={getFormValue(
elements,
appState,
app,
(element) => element.fillStyle,
(element) => element.hasOwnProperty("fillStyle"),
(hasSelection) =>
@@ -483,7 +483,7 @@ export const actionChangeStrokeWidth = register({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData }) => (
PanelComponent: ({ elements, appState, updateData, app }) => (
<fieldset>
<legend>{t("labels.strokeWidth")}</legend>
<ButtonIconSelect
@@ -510,7 +510,7 @@ export const actionChangeStrokeWidth = register({
]}
value={getFormValue(
elements,
appState,
app,
(element) => element.strokeWidth,
(element) => element.hasOwnProperty("strokeWidth"),
(hasSelection) =>
@@ -538,7 +538,7 @@ export const actionChangeSloppiness = register({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData }) => (
PanelComponent: ({ elements, appState, updateData, app }) => (
<fieldset>
<legend>{t("labels.sloppiness")}</legend>
<ButtonIconSelect
@@ -562,7 +562,7 @@ export const actionChangeSloppiness = register({
]}
value={getFormValue(
elements,
appState,
app,
(element) => element.roughness,
(element) => element.hasOwnProperty("roughness"),
(hasSelection) =>
@@ -589,7 +589,7 @@ export const actionChangeStrokeStyle = register({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData }) => (
PanelComponent: ({ elements, appState, updateData, app }) => (
<fieldset>
<legend>{t("labels.strokeStyle")}</legend>
<ButtonIconSelect
@@ -613,7 +613,7 @@ export const actionChangeStrokeStyle = register({
]}
value={getFormValue(
elements,
appState,
app,
(element) => element.strokeStyle,
(element) => element.hasOwnProperty("strokeStyle"),
(hasSelection) =>
@@ -644,13 +644,8 @@ export const actionChangeOpacity = register({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData }) => (
<Range
updateData={updateData}
elements={elements}
appState={appState}
testId="opacity"
/>
PanelComponent: ({ app, updateData }) => (
<Range updateData={updateData} app={app} testId="opacity" />
),
});
@@ -694,7 +689,7 @@ export const actionChangeFontSize = register({
]}
value={getFormValue(
elements,
appState,
app,
(element) => {
if (isTextElement(element)) {
return element.fontSize;
@@ -992,7 +987,7 @@ export const actionChangeFontFamily = register({
) =>
getFormValue(
elementsArray,
appState,
app,
(element) => {
if (isTextElement(element)) {
return element.fontFamily;
@@ -1030,7 +1025,7 @@ export const actionChangeFontFamily = register({
// popup props are not in sync, hence we are in the middle of an update, so keeping the previous value we've had
return prevSelectedFontFamilyRef.current;
}, [batchedData.openPopup, appState, elements, app.scene]);
}, [batchedData.openPopup, appState, elements, app]);
useEffect(() => {
prevSelectedFontFamilyRef.current = selectedFontFamily;
@@ -1216,7 +1211,7 @@ export const actionChangeTextAlign = register({
]}
value={getFormValue(
elements,
appState,
app,
(element) => {
if (isTextElement(element)) {
return element.textAlign;
@@ -1304,7 +1299,7 @@ export const actionChangeVerticalAlign = register({
]}
value={getFormValue(
elements,
appState,
app,
(element) => {
if (isTextElement(element) && element.containerId) {
return element.verticalAlign;
@@ -1362,7 +1357,7 @@ export const actionChangeRoundness = register({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData }) => {
PanelComponent: ({ elements, appState, updateData, app }) => {
const targetElements = getTargetElements(
getNonDeletedElements(elements),
appState,
@@ -1391,7 +1386,7 @@ export const actionChangeRoundness = register({
]}
value={getFormValue(
elements,
appState,
app,
(element) =>
hasLegacyRoundness ? null : element.roundness ? "round" : "sharp",
(element) =>
@@ -1521,7 +1516,7 @@ export const actionChangeArrowhead = register({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData }) => {
PanelComponent: ({ elements, appState, updateData, app }) => {
const isRTL = getLanguage().rtl;
return (
@@ -1533,7 +1528,7 @@ export const actionChangeArrowhead = register({
options={getArrowheadOptions(!isRTL)}
value={getFormValue<Arrowhead | null>(
elements,
appState,
app,
(element) =>
isLinearElement(element) && canHaveArrowheads(element.type)
? element.startArrowhead
@@ -1550,7 +1545,7 @@ export const actionChangeArrowhead = register({
options={getArrowheadOptions(!!isRTL)}
value={getFormValue<Arrowhead | null>(
elements,
appState,
app,
(element) =>
isLinearElement(element) && canHaveArrowheads(element.type)
? element.endArrowhead
@@ -1759,7 +1754,7 @@ export const actionChangeArrowType = register({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData }) => {
PanelComponent: ({ elements, appState, updateData, app }) => {
return (
<fieldset>
<legend>{t("labels.arrowtypes")}</legend>
@@ -1787,7 +1782,7 @@ export const actionChangeArrowType = register({
]}
value={getFormValue(
elements,
appState,
app,
(element) => {
if (isArrowElement(element)) {
return element.elbowed