|
@@ -137,6 +137,11 @@ import {
|
|
isSomeElementSelected,
|
|
isSomeElementSelected,
|
|
} from "../scene";
|
|
} from "../scene";
|
|
|
|
|
|
|
|
+import {
|
|
|
|
+ withCaretPositionPreservation,
|
|
|
|
+ restoreCaretPosition,
|
|
|
|
+} from "../hooks/useTextEditorFocus";
|
|
|
|
+
|
|
import { register } from "./register";
|
|
import { register } from "./register";
|
|
|
|
|
|
import type { AppClassProperties, AppState, Primitive } from "../types";
|
|
import type { AppClassProperties, AppState, Primitive } from "../types";
|
|
@@ -321,9 +326,11 @@ export const actionChangeStrokeColor = register({
|
|
: CaptureUpdateAction.EVENTUALLY,
|
|
: 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
|
|
<ColorPicker
|
|
topPicks={DEFAULT_ELEMENT_STROKE_PICKS}
|
|
topPicks={DEFAULT_ELEMENT_STROKE_PICKS}
|
|
palette={DEFAULT_ELEMENT_STROKE_COLOR_PALETTE}
|
|
palette={DEFAULT_ELEMENT_STROKE_COLOR_PALETTE}
|
|
@@ -341,6 +348,7 @@ export const actionChangeStrokeColor = register({
|
|
elements={elements}
|
|
elements={elements}
|
|
appState={appState}
|
|
appState={appState}
|
|
updateData={updateData}
|
|
updateData={updateData}
|
|
|
|
+ compactMode={appState.stylesPanelMode === "compact"}
|
|
/>
|
|
/>
|
|
</>
|
|
</>
|
|
),
|
|
),
|
|
@@ -398,9 +406,11 @@ export const actionChangeBackgroundColor = register({
|
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
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
|
|
<ColorPicker
|
|
topPicks={DEFAULT_ELEMENT_BACKGROUND_PICKS}
|
|
topPicks={DEFAULT_ELEMENT_BACKGROUND_PICKS}
|
|
palette={DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE}
|
|
palette={DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE}
|
|
@@ -418,6 +428,7 @@ export const actionChangeBackgroundColor = register({
|
|
elements={elements}
|
|
elements={elements}
|
|
appState={appState}
|
|
appState={appState}
|
|
updateData={updateData}
|
|
updateData={updateData}
|
|
|
|
+ compactMode={appState.stylesPanelMode === "compact"}
|
|
/>
|
|
/>
|
|
</>
|
|
</>
|
|
),
|
|
),
|
|
@@ -518,9 +529,11 @@ export const actionChangeStrokeWidth = register({
|
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
};
|
|
};
|
|
},
|
|
},
|
|
- PanelComponent: ({ elements, appState, updateData, app }) => (
|
|
|
|
|
|
+ PanelComponent: ({ elements, appState, updateData, app, data }) => (
|
|
<fieldset>
|
|
<fieldset>
|
|
- <legend>{t("labels.strokeWidth")}</legend>
|
|
|
|
|
|
+ {appState.stylesPanelMode === "full" && (
|
|
|
|
+ <legend>{t("labels.strokeWidth")}</legend>
|
|
|
|
+ )}
|
|
<div className="buttonList">
|
|
<div className="buttonList">
|
|
<RadioSelection
|
|
<RadioSelection
|
|
group="stroke-width"
|
|
group="stroke-width"
|
|
@@ -575,9 +588,11 @@ export const actionChangeSloppiness = register({
|
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
};
|
|
};
|
|
},
|
|
},
|
|
- PanelComponent: ({ elements, appState, updateData, app }) => (
|
|
|
|
|
|
+ PanelComponent: ({ elements, appState, updateData, app, data }) => (
|
|
<fieldset>
|
|
<fieldset>
|
|
- <legend>{t("labels.sloppiness")}</legend>
|
|
|
|
|
|
+ {appState.stylesPanelMode === "full" && (
|
|
|
|
+ <legend>{t("labels.sloppiness")}</legend>
|
|
|
|
+ )}
|
|
<div className="buttonList">
|
|
<div className="buttonList">
|
|
<RadioSelection
|
|
<RadioSelection
|
|
group="sloppiness"
|
|
group="sloppiness"
|
|
@@ -628,9 +643,11 @@ export const actionChangeStrokeStyle = register({
|
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
};
|
|
};
|
|
},
|
|
},
|
|
- PanelComponent: ({ elements, appState, updateData, app }) => (
|
|
|
|
|
|
+ PanelComponent: ({ elements, appState, updateData, app, data }) => (
|
|
<fieldset>
|
|
<fieldset>
|
|
- <legend>{t("labels.strokeStyle")}</legend>
|
|
|
|
|
|
+ {appState.stylesPanelMode === "full" && (
|
|
|
|
+ <legend>{t("labels.strokeStyle")}</legend>
|
|
|
|
+ )}
|
|
<div className="buttonList">
|
|
<div className="buttonList">
|
|
<RadioSelection
|
|
<RadioSelection
|
|
group="strokeStyle"
|
|
group="strokeStyle"
|
|
@@ -697,7 +714,7 @@ export const actionChangeFontSize = register({
|
|
perform: (elements, appState, value, app) => {
|
|
perform: (elements, appState, value, app) => {
|
|
return changeFontSize(elements, appState, app, () => value, value);
|
|
return changeFontSize(elements, appState, app, () => value, value);
|
|
},
|
|
},
|
|
- PanelComponent: ({ elements, appState, updateData, app }) => (
|
|
|
|
|
|
+ PanelComponent: ({ elements, appState, updateData, app, data }) => (
|
|
<fieldset>
|
|
<fieldset>
|
|
<legend>{t("labels.fontSize")}</legend>
|
|
<legend>{t("labels.fontSize")}</legend>
|
|
<div className="buttonList">
|
|
<div className="buttonList">
|
|
@@ -756,7 +773,14 @@ export const actionChangeFontSize = register({
|
|
? null
|
|
? null
|
|
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
|
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
|
)}
|
|
)}
|
|
- onChange={(value) => updateData(value)}
|
|
|
|
|
|
+ onChange={(value) => {
|
|
|
|
+ withCaretPositionPreservation(
|
|
|
|
+ () => updateData(value),
|
|
|
|
+ appState.stylesPanelMode === "compact",
|
|
|
|
+ !!appState.editingTextElement,
|
|
|
|
+ data?.onPreventClose,
|
|
|
|
+ );
|
|
|
|
+ }}
|
|
/>
|
|
/>
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
</fieldset>
|
|
@@ -1016,7 +1040,7 @@ export const actionChangeFontFamily = register({
|
|
|
|
|
|
return result;
|
|
return result;
|
|
},
|
|
},
|
|
- PanelComponent: ({ elements, appState, app, updateData }) => {
|
|
|
|
|
|
+ PanelComponent: ({ elements, appState, app, updateData, data }) => {
|
|
const cachedElementsRef = useRef<ElementsMap>(new Map());
|
|
const cachedElementsRef = useRef<ElementsMap>(new Map());
|
|
const prevSelectedFontFamilyRef = useRef<number | null>(null);
|
|
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
|
|
// relying on state batching as multiple `FontPicker` handlers could be called in rapid succession and we want to combine them
|
|
@@ -1094,20 +1118,28 @@ export const actionChangeFontFamily = register({
|
|
|
|
|
|
return (
|
|
return (
|
|
<fieldset>
|
|
<fieldset>
|
|
- <legend>{t("labels.fontFamily")}</legend>
|
|
|
|
|
|
+ {appState.stylesPanelMode === "full" && (
|
|
|
|
+ <legend>{t("labels.fontFamily")}</legend>
|
|
|
|
+ )}
|
|
<FontPicker
|
|
<FontPicker
|
|
isOpened={appState.openPopup === "fontFamily"}
|
|
isOpened={appState.openPopup === "fontFamily"}
|
|
selectedFontFamily={selectedFontFamily}
|
|
selectedFontFamily={selectedFontFamily}
|
|
hoveredFontFamily={appState.currentHoveredFontFamily}
|
|
hoveredFontFamily={appState.currentHoveredFontFamily}
|
|
|
|
+ compactMode={appState.stylesPanelMode === "compact"}
|
|
onSelect={(fontFamily) => {
|
|
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) => {
|
|
onHover={(fontFamily) => {
|
|
setBatchedData({
|
|
setBatchedData({
|
|
@@ -1164,25 +1196,28 @@ export const actionChangeFontFamily = register({
|
|
}
|
|
}
|
|
|
|
|
|
setBatchedData({
|
|
setBatchedData({
|
|
|
|
+ ...batchedData,
|
|
openPopup: "fontFamily",
|
|
openPopup: "fontFamily",
|
|
});
|
|
});
|
|
} else {
|
|
} else {
|
|
- // close, use the cache and clear it afterwards
|
|
|
|
- const data = {
|
|
|
|
- openPopup: null,
|
|
|
|
|
|
+ const fontFamilyData = {
|
|
currentHoveredFontFamily: null,
|
|
currentHoveredFontFamily: null,
|
|
cachedElements: new Map(cachedElementsRef.current),
|
|
cachedElements: new Map(cachedElementsRef.current),
|
|
resetAll: true,
|
|
resetAll: true,
|
|
} as ChangeFontFamilyData;
|
|
} 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();
|
|
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
|
|
|
|
+ }
|
|
}
|
|
}
|
|
}}
|
|
}}
|
|
/>
|
|
/>
|
|
@@ -1225,8 +1260,9 @@ export const actionChangeTextAlign = register({
|
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
};
|
|
};
|
|
},
|
|
},
|
|
- PanelComponent: ({ elements, appState, updateData, app }) => {
|
|
|
|
|
|
+ PanelComponent: ({ elements, appState, updateData, app, data }) => {
|
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
|
|
|
+
|
|
return (
|
|
return (
|
|
<fieldset>
|
|
<fieldset>
|
|
<legend>{t("labels.textAlign")}</legend>
|
|
<legend>{t("labels.textAlign")}</legend>
|
|
@@ -1275,7 +1311,14 @@ export const actionChangeTextAlign = register({
|
|
(hasSelection) =>
|
|
(hasSelection) =>
|
|
hasSelection ? null : appState.currentItemTextAlign,
|
|
hasSelection ? null : appState.currentItemTextAlign,
|
|
)}
|
|
)}
|
|
- onChange={(value) => updateData(value)}
|
|
|
|
|
|
+ onChange={(value) => {
|
|
|
|
+ withCaretPositionPreservation(
|
|
|
|
+ () => updateData(value),
|
|
|
|
+ appState.stylesPanelMode === "compact",
|
|
|
|
+ !!appState.editingTextElement,
|
|
|
|
+ data?.onPreventClose,
|
|
|
|
+ );
|
|
|
|
+ }}
|
|
/>
|
|
/>
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
</fieldset>
|
|
@@ -1317,7 +1360,7 @@ export const actionChangeVerticalAlign = register({
|
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
};
|
|
};
|
|
},
|
|
},
|
|
- PanelComponent: ({ elements, appState, updateData, app }) => {
|
|
|
|
|
|
+ PanelComponent: ({ elements, appState, updateData, app, data }) => {
|
|
return (
|
|
return (
|
|
<fieldset>
|
|
<fieldset>
|
|
<div className="buttonList">
|
|
<div className="buttonList">
|
|
@@ -1367,7 +1410,14 @@ export const actionChangeVerticalAlign = register({
|
|
) !== null,
|
|
) !== null,
|
|
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
|
|
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
|
|
)}
|
|
)}
|
|
- onChange={(value) => updateData(value)}
|
|
|
|
|
|
+ onChange={(value) => {
|
|
|
|
+ withCaretPositionPreservation(
|
|
|
|
+ () => updateData(value),
|
|
|
|
+ appState.stylesPanelMode === "compact",
|
|
|
|
+ !!appState.editingTextElement,
|
|
|
|
+ data?.onPreventClose,
|
|
|
|
+ );
|
|
|
|
+ }}
|
|
/>
|
|
/>
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
</fieldset>
|
|
@@ -1616,6 +1666,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({
|
|
export const actionChangeArrowType = register({
|
|
name: "changeArrowType",
|
|
name: "changeArrowType",
|
|
label: "Change arrow types",
|
|
label: "Change arrow types",
|