Merge branch 'master' into mtolmacs/feat/fixed-point-simple-arrow-binding

This commit is contained in:
Mark Tolmacs
2025-09-13 11:17:02 +02:00
42 changed files with 1969 additions and 406 deletions

View File

@@ -139,6 +139,11 @@ import {
isSomeElementSelected,
} from "../scene";
import {
withCaretPositionPreservation,
restoreCaretPosition,
} from "../hooks/useTextEditorFocus";
import { register } from "./register";
import type { AppClassProperties, AppState, Primitive } from "../types";
@@ -325,9 +330,11 @@ export const actionChangeStrokeColor = register<
: CaptureUpdateAction.EVENTUALLY,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData, app, data }) => (
<>
<h3 aria-hidden="true">{t("labels.stroke")}</h3>
{appState.stylesPanelMode === "full" && (
<h3 aria-hidden="true">{t("labels.stroke")}</h3>
)}
<ColorPicker
topPicks={DEFAULT_ELEMENT_STROKE_PICKS}
palette={DEFAULT_ELEMENT_STROKE_COLOR_PALETTE}
@@ -345,6 +352,7 @@ export const actionChangeStrokeColor = register<
elements={elements}
appState={appState}
updateData={updateData}
compactMode={appState.stylesPanelMode === "compact"}
/>
</>
),
@@ -404,9 +412,11 @@ export const actionChangeBackgroundColor = register<
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData, app, data }) => (
<>
<h3 aria-hidden="true">{t("labels.background")}</h3>
{appState.stylesPanelMode === "full" && (
<h3 aria-hidden="true">{t("labels.background")}</h3>
)}
<ColorPicker
topPicks={DEFAULT_ELEMENT_BACKGROUND_PICKS}
palette={DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE}
@@ -424,6 +434,7 @@ export const actionChangeBackgroundColor = register<
elements={elements}
appState={appState}
updateData={updateData}
compactMode={appState.stylesPanelMode === "compact"}
/>
</>
),
@@ -526,9 +537,11 @@ export const actionChangeStrokeWidth = register<
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData, app, data }) => (
<fieldset>
<legend>{t("labels.strokeWidth")}</legend>
{appState.stylesPanelMode === "full" && (
<legend>{t("labels.strokeWidth")}</legend>
)}
<div className="buttonList">
<RadioSelection
group="stroke-width"
@@ -583,9 +596,11 @@ export const actionChangeSloppiness = register<ExcalidrawElement["roughness"]>({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData, app, data }) => (
<fieldset>
<legend>{t("labels.sloppiness")}</legend>
{appState.stylesPanelMode === "full" && (
<legend>{t("labels.sloppiness")}</legend>
)}
<div className="buttonList">
<RadioSelection
group="sloppiness"
@@ -638,9 +653,11 @@ export const actionChangeStrokeStyle = register<
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData, app, data }) => (
<fieldset>
<legend>{t("labels.strokeStyle")}</legend>
{appState.stylesPanelMode === "full" && (
<legend>{t("labels.strokeStyle")}</legend>
)}
<div className="buttonList">
<RadioSelection
group="strokeStyle"
@@ -1042,7 +1059,7 @@ export const actionChangeFontFamily = register<{
return result;
},
PanelComponent: ({ elements, appState, app, updateData }) => {
PanelComponent: ({ elements, appState, app, updateData, data }) => {
const cachedElementsRef = useRef<ElementsMap>(new Map());
const prevSelectedFontFamilyRef = useRef<number | null>(null);
// relying on state batching as multiple `FontPicker` handlers could be called in rapid succession and we want to combine them
@@ -1120,20 +1137,28 @@ export const actionChangeFontFamily = register<{
return (
<fieldset>
<legend>{t("labels.fontFamily")}</legend>
{appState.stylesPanelMode === "full" && (
<legend>{t("labels.fontFamily")}</legend>
)}
<FontPicker
isOpened={appState.openPopup === "fontFamily"}
selectedFontFamily={selectedFontFamily}
hoveredFontFamily={appState.currentHoveredFontFamily}
compactMode={appState.stylesPanelMode === "compact"}
onSelect={(fontFamily) => {
setBatchedData({
openPopup: null,
currentHoveredFontFamily: null,
currentItemFontFamily: fontFamily,
});
// defensive clear so immediate close won't abuse the cached elements
cachedElementsRef.current.clear();
withCaretPositionPreservation(
() => {
setBatchedData({
openPopup: null,
currentHoveredFontFamily: null,
currentItemFontFamily: fontFamily,
});
// defensive clear so immediate close won't abuse the cached elements
cachedElementsRef.current.clear();
},
appState.stylesPanelMode === "compact",
!!appState.editingTextElement,
);
}}
onHover={(fontFamily) => {
setBatchedData({
@@ -1190,25 +1215,28 @@ export const actionChangeFontFamily = register<{
}
setBatchedData({
...batchedData,
openPopup: "fontFamily",
});
} else {
// close, use the cache and clear it afterwards
const data = {
openPopup: null,
const fontFamilyData = {
currentHoveredFontFamily: null,
cachedElements: new Map(cachedElementsRef.current),
resetAll: true,
} as ChangeFontFamilyData;
if (isUnmounted.current) {
// in case the component was unmounted by the parent, trigger the update directly
updateData({ ...batchedData, ...data });
} else {
setBatchedData(data);
}
setBatchedData({
...fontFamilyData,
});
cachedElementsRef.current.clear();
// Refocus text editor when font picker closes if we were editing text
if (
appState.stylesPanelMode === "compact" &&
appState.editingTextElement
) {
restoreCaretPosition(null); // Just refocus without saved position
}
}
}}
/>
@@ -1251,8 +1279,9 @@ export const actionChangeTextAlign = register<TextAlign>({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => {
PanelComponent: ({ elements, appState, updateData, app, data }) => {
const elementsMap = app.scene.getNonDeletedElementsMap();
return (
<fieldset>
<legend>{t("labels.textAlign")}</legend>
@@ -1301,7 +1330,14 @@ export const actionChangeTextAlign = register<TextAlign>({
(hasSelection) =>
hasSelection ? null : appState.currentItemTextAlign,
)}
onChange={(value) => updateData(value)}
onChange={(value) => {
withCaretPositionPreservation(
() => updateData(value),
appState.stylesPanelMode === "compact",
!!appState.editingTextElement,
data?.onPreventClose,
);
}}
/>
</div>
</fieldset>
@@ -1343,7 +1379,7 @@ export const actionChangeVerticalAlign = register<VerticalAlign>({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => {
PanelComponent: ({ elements, appState, updateData, app, data }) => {
return (
<fieldset>
<div className="buttonList">
@@ -1393,7 +1429,14 @@ export const actionChangeVerticalAlign = register<VerticalAlign>({
) !== null,
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
)}
onChange={(value) => updateData(value)}
onChange={(value) => {
withCaretPositionPreservation(
() => updateData(value),
appState.stylesPanelMode === "compact",
!!appState.editingTextElement,
data?.onPreventClose,
);
}}
/>
</div>
</fieldset>
@@ -1643,6 +1686,25 @@ export const actionChangeArrowhead = register<{
},
});
export const actionChangeArrowProperties = register({
name: "changeArrowProperties",
label: "Change arrow properties",
trackEvent: false,
perform: (elements, appState, value, app) => {
// This action doesn't perform any changes directly
// It's just a container for the arrow type and arrowhead actions
return false;
},
PanelComponent: ({ elements, appState, updateData, app, renderAction }) => {
return (
<div className="selected-shape-actions">
{renderAction("changeArrowType")}
{renderAction("changeArrowhead")}
</div>
);
},
});
export const actionChangeArrowType = register<keyof typeof ARROW_TYPE>({
name: "changeArrowType",
label: "Change arrow types",