mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-10 11:00:41 +02:00
change properties for a new or editing text
This commit is contained in:
@@ -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";
|
||||||
@@ -701,7 +706,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">
|
||||||
@@ -760,7 +765,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),
|
||||||
|
!!data?.compactMode,
|
||||||
|
!!appState.editingTextElement,
|
||||||
|
data?.onPreventClose,
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -1105,14 +1117,19 @@ export const actionChangeFontFamily = register({
|
|||||||
hoveredFontFamily={appState.currentHoveredFontFamily}
|
hoveredFontFamily={appState.currentHoveredFontFamily}
|
||||||
compactMode={data?.compactMode}
|
compactMode={data?.compactMode}
|
||||||
onSelect={(fontFamily) => {
|
onSelect={(fontFamily) => {
|
||||||
setBatchedData({
|
withCaretPositionPreservation(
|
||||||
openPopup: null,
|
() => {
|
||||||
currentHoveredFontFamily: null,
|
setBatchedData({
|
||||||
currentItemFontFamily: fontFamily,
|
openPopup: null,
|
||||||
});
|
currentHoveredFontFamily: null,
|
||||||
|
currentItemFontFamily: fontFamily,
|
||||||
// defensive clear so immediate close won't abuse the cached elements
|
});
|
||||||
cachedElementsRef.current.clear();
|
// defensive clear so immediate close won't abuse the cached elements
|
||||||
|
cachedElementsRef.current.clear();
|
||||||
|
},
|
||||||
|
!!data?.compactMode,
|
||||||
|
!!appState.editingTextElement,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
onHover={(fontFamily) => {
|
onHover={(fontFamily) => {
|
||||||
setBatchedData({
|
setBatchedData({
|
||||||
@@ -1172,7 +1189,7 @@ export const actionChangeFontFamily = register({
|
|||||||
setBatchedData({});
|
setBatchedData({});
|
||||||
} else {
|
} else {
|
||||||
// close: clear openPopup if we're still the active popup
|
// close: clear openPopup if we're still the active popup
|
||||||
const data = {
|
const fontFamilyData = {
|
||||||
openPopup:
|
openPopup:
|
||||||
appState.openPopup === "fontFamily"
|
appState.openPopup === "fontFamily"
|
||||||
? null
|
? null
|
||||||
@@ -1183,9 +1200,14 @@ export const actionChangeFontFamily = register({
|
|||||||
} as ChangeFontFamilyData;
|
} as ChangeFontFamilyData;
|
||||||
|
|
||||||
// apply immediately to avoid racing with other popovers opening
|
// apply immediately to avoid racing with other popovers opening
|
||||||
updateData({ ...batchedData, ...data });
|
updateData({ ...batchedData, ...fontFamilyData });
|
||||||
setBatchedData({});
|
setBatchedData({});
|
||||||
cachedElementsRef.current.clear();
|
cachedElementsRef.current.clear();
|
||||||
|
|
||||||
|
// Refocus text editor when font picker closes if we were editing text
|
||||||
|
if (data?.compactMode && appState.editingTextElement) {
|
||||||
|
restoreCaretPosition(null); // Just refocus without saved position
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -1228,8 +1250,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>
|
||||||
@@ -1278,7 +1301,14 @@ export const actionChangeTextAlign = register({
|
|||||||
(hasSelection) =>
|
(hasSelection) =>
|
||||||
hasSelection ? null : appState.currentItemTextAlign,
|
hasSelection ? null : appState.currentItemTextAlign,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => {
|
||||||
|
withCaretPositionPreservation(
|
||||||
|
() => updateData(value),
|
||||||
|
!!data?.compactMode,
|
||||||
|
!!appState.editingTextElement,
|
||||||
|
data?.onPreventClose,
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -1320,7 +1350,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">
|
||||||
@@ -1370,7 +1400,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),
|
||||||
|
!!data?.compactMode,
|
||||||
|
!!appState.editingTextElement,
|
||||||
|
data?.onPreventClose,
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
@@ -50,6 +50,8 @@ import {
|
|||||||
|
|
||||||
import { getFormValue } from "../actions/actionProperties";
|
import { getFormValue } from "../actions/actionProperties";
|
||||||
|
|
||||||
|
import { useTextEditorFocus } from "../hooks/useTextEditorFocus";
|
||||||
|
|
||||||
import { getToolbarTools } from "./shapes";
|
import { getToolbarTools } from "./shapes";
|
||||||
|
|
||||||
import "./Actions.scss";
|
import "./Actions.scss";
|
||||||
@@ -85,6 +87,12 @@ import type {
|
|||||||
} from "../types";
|
} from "../types";
|
||||||
import type { ActionManager } from "../actions/manager";
|
import type { ActionManager } from "../actions/manager";
|
||||||
|
|
||||||
|
// Common CSS class combinations
|
||||||
|
const PROPERTIES_CLASSES = clsx([
|
||||||
|
CLASSES.SHAPE_ACTIONS_THEME_SCOPE,
|
||||||
|
"properties-content",
|
||||||
|
]);
|
||||||
|
|
||||||
export const canChangeStrokeColor = (
|
export const canChangeStrokeColor = (
|
||||||
appState: UIAppState,
|
appState: UIAppState,
|
||||||
targetElements: ExcalidrawElement[],
|
targetElements: ExcalidrawElement[],
|
||||||
@@ -313,6 +321,8 @@ export const CompactShapeActions = ({
|
|||||||
const targetElements = getTargetElements(elementsMap, appState);
|
const targetElements = getTargetElements(elementsMap, appState);
|
||||||
const [strokePopoverOpen, setStrokePopoverOpen] = useState(false);
|
const [strokePopoverOpen, setStrokePopoverOpen] = useState(false);
|
||||||
const [otherActionsPopoverOpen, setOtherActionsPopoverOpen] = useState(false);
|
const [otherActionsPopoverOpen, setOtherActionsPopoverOpen] = useState(false);
|
||||||
|
const [preventTextAlignClose, setPreventTextAlignClose] = useState(false);
|
||||||
|
const { saveCaretPosition, restoreCaretPosition } = useTextEditorFocus();
|
||||||
const { container } = useExcalidrawContainer();
|
const { container } = useExcalidrawContainer();
|
||||||
|
|
||||||
const isEditingTextOrNewElement = Boolean(
|
const isEditingTextOrNewElement = Boolean(
|
||||||
@@ -364,12 +374,11 @@ export const CompactShapeActions = ({
|
|||||||
return (
|
return (
|
||||||
<div className="compact-shape-actions">
|
<div className="compact-shape-actions">
|
||||||
{/* Stroke Color */}
|
{/* Stroke Color */}
|
||||||
{canChangeStrokeColor(appState, targetElements) &&
|
{canChangeStrokeColor(appState, targetElements) && (
|
||||||
!appState.editingTextElement && (
|
<div className={clsx("compact-action-item")}>
|
||||||
<div className={clsx("compact-action-item")}>
|
{renderAction("changeStrokeColor", { compactMode: true })}
|
||||||
{renderAction("changeStrokeColor", { compactMode: true })}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Background Color */}
|
{/* Background Color */}
|
||||||
{canChangeBackgroundColor(appState, targetElements) && (
|
{canChangeBackgroundColor(appState, targetElements) && (
|
||||||
@@ -400,7 +409,7 @@ export const CompactShapeActions = ({
|
|||||||
<Popover.Trigger asChild>
|
<Popover.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="compact-action-button"
|
className="compact-action-button properties-trigger"
|
||||||
title={t("labels.stroke")}
|
title={t("labels.stroke")}
|
||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -423,7 +432,7 @@ export const CompactShapeActions = ({
|
|||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
{strokePopoverOpen && (
|
{strokePopoverOpen && (
|
||||||
<PropertiesPopover
|
<PropertiesPopover
|
||||||
className={CLASSES.SHAPE_ACTIONS_THEME_SCOPE}
|
className={PROPERTIES_CLASSES}
|
||||||
container={container}
|
container={container}
|
||||||
style={{ maxWidth: "13rem" }}
|
style={{ maxWidth: "13rem" }}
|
||||||
onClose={() => setStrokePopoverOpen(false)}
|
onClose={() => setStrokePopoverOpen(false)}
|
||||||
@@ -474,7 +483,7 @@ export const CompactShapeActions = ({
|
|||||||
<Popover.Trigger asChild>
|
<Popover.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="compact-action-button"
|
className="compact-action-button properties-trigger"
|
||||||
title={t("labels.arrowtypes")}
|
title={t("labels.arrowtypes")}
|
||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -524,6 +533,7 @@ export const CompactShapeActions = ({
|
|||||||
{appState.openPopup === "arrowProperties" && (
|
{appState.openPopup === "arrowProperties" && (
|
||||||
<PropertiesPopover
|
<PropertiesPopover
|
||||||
container={container}
|
container={container}
|
||||||
|
className="properties-content"
|
||||||
style={{ maxWidth: "13rem" }}
|
style={{ maxWidth: "13rem" }}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setAppState((prev: AppState) =>
|
setAppState((prev: AppState) =>
|
||||||
@@ -549,80 +559,99 @@ export const CompactShapeActions = ({
|
|||||||
|
|
||||||
{/* Text Properties */}
|
{/* Text Properties */}
|
||||||
{(appState.activeTool.type === "text" ||
|
{(appState.activeTool.type === "text" ||
|
||||||
targetElements.some(isTextElement)) &&
|
targetElements.some(isTextElement)) && (
|
||||||
!appState.editingTextElement && (
|
<>
|
||||||
<>
|
<div className="compact-action-item">
|
||||||
<div className="compact-action-item">
|
{renderAction("changeFontFamily", {
|
||||||
{renderAction("changeFontFamily", {
|
compactMode: true,
|
||||||
compactMode: true,
|
onPreventClose: () => setPreventTextAlignClose(true),
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="compact-action-item">
|
<div className="compact-action-item">
|
||||||
<Popover.Root
|
<Popover.Root
|
||||||
open={appState.openPopup === "textAlign"}
|
open={appState.openPopup === "textAlign"}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (open) {
|
if (!open) {
|
||||||
setAppState({ openPopup: "textAlign" });
|
// If we're preventing close due to selection, ignore this close event
|
||||||
setStrokePopoverOpen(false);
|
if (preventTextAlignClose) {
|
||||||
setOtherActionsPopoverOpen(false);
|
setPreventTextAlignClose(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}}
|
|
||||||
>
|
setAppState({ openPopup: null });
|
||||||
<Popover.Trigger asChild>
|
|
||||||
<button
|
// Refocus text editor if it was being edited and restore caret position
|
||||||
type="button"
|
if (appState.editingTextElement) {
|
||||||
className="compact-action-button"
|
restoreCaretPosition();
|
||||||
title={t("labels.textAlign")}
|
}
|
||||||
onPointerDown={(e) => {
|
} else {
|
||||||
|
// Save current caret position before opening popover
|
||||||
|
if (appState.editingTextElement) {
|
||||||
|
saveCaretPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
setAppState({ openPopup: "textAlign" });
|
||||||
|
setStrokePopoverOpen(false);
|
||||||
|
setOtherActionsPopoverOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Popover.Trigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="compact-action-button properties-trigger"
|
||||||
|
title={t("labels.textAlign")}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
// Prevent default behavior that might dismiss keyboard on mobile
|
||||||
|
if (appState.editingTextElement) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (appState.openPopup === "textAlign") {
|
}
|
||||||
setAppState({ openPopup: null });
|
}}
|
||||||
} else {
|
onClick={() => {}}
|
||||||
setStrokePopoverOpen(false);
|
>
|
||||||
setOtherActionsPopoverOpen(false);
|
{TextSizeIcon}
|
||||||
setAppState({ openPopup: "textAlign" });
|
</button>
|
||||||
}
|
</Popover.Trigger>
|
||||||
}}
|
{appState.openPopup === "textAlign" && (
|
||||||
onClick={(e) => {
|
<PropertiesPopover
|
||||||
e.preventDefault();
|
className={PROPERTIES_CLASSES}
|
||||||
e.stopPropagation();
|
container={container}
|
||||||
}}
|
style={{ maxWidth: "13rem" }}
|
||||||
>
|
// Improve focus handling for text editing scenarios
|
||||||
{TextSizeIcon}
|
preventAutoFocusOnTouch={!!appState.editingTextElement}
|
||||||
</button>
|
onClose={() => {
|
||||||
</Popover.Trigger>
|
setAppState({ openPopup: null });
|
||||||
{appState.openPopup === "textAlign" && (
|
// Refocus text editor when popover closes with caret restoration
|
||||||
<PropertiesPopover
|
if (appState.editingTextElement) {
|
||||||
className={CLASSES.SHAPE_ACTIONS_THEME_SCOPE}
|
restoreCaretPosition();
|
||||||
container={container}
|
}
|
||||||
style={{ maxWidth: "13rem" }}
|
}}
|
||||||
onClose={() => {
|
>
|
||||||
setAppState((prev: AppState) =>
|
<div className="selected-shape-actions">
|
||||||
prev.openPopup === "textAlign"
|
{(appState.activeTool.type === "text" ||
|
||||||
? { openPopup: null }
|
suppportsHorizontalAlign(targetElements, elementsMap)) &&
|
||||||
: null,
|
renderAction("changeTextAlign", {
|
||||||
);
|
compactMode: true,
|
||||||
}}
|
onPreventClose: () => setPreventTextAlignClose(true),
|
||||||
>
|
})}
|
||||||
<div className="selected-shape-actions">
|
{shouldAllowVerticalAlign(targetElements, elementsMap) &&
|
||||||
{(appState.activeTool.type === "text" ||
|
renderAction("changeVerticalAlign", {
|
||||||
suppportsHorizontalAlign(
|
compactMode: true,
|
||||||
targetElements,
|
onPreventClose: () => setPreventTextAlignClose(true),
|
||||||
elementsMap,
|
})}
|
||||||
)) &&
|
{(appState.activeTool.type === "text" ||
|
||||||
renderAction("changeTextAlign")}
|
targetElements.some(isTextElement)) &&
|
||||||
{shouldAllowVerticalAlign(targetElements, elementsMap) &&
|
renderAction("changeFontSize", {
|
||||||
renderAction("changeVerticalAlign")}
|
compactMode: true,
|
||||||
{(appState.activeTool.type === "text" ||
|
onPreventClose: () => setPreventTextAlignClose(true),
|
||||||
targetElements.some(isTextElement)) &&
|
})}
|
||||||
renderAction("changeFontSize")}
|
</div>
|
||||||
</div>
|
</PropertiesPopover>
|
||||||
</PropertiesPopover>
|
)}
|
||||||
)}
|
</Popover.Root>
|
||||||
</Popover.Root>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Dedicated Copy Button */}
|
{/* Dedicated Copy Button */}
|
||||||
{!isEditingTextOrNewElement && targetElements.length > 0 && (
|
{!isEditingTextOrNewElement && targetElements.length > 0 && (
|
||||||
@@ -654,7 +683,7 @@ export const CompactShapeActions = ({
|
|||||||
<Popover.Trigger asChild>
|
<Popover.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="compact-action-button"
|
className="compact-action-button properties-trigger"
|
||||||
title={t("labels.actions")}
|
title={t("labels.actions")}
|
||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -677,7 +706,7 @@ export const CompactShapeActions = ({
|
|||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
{otherActionsPopoverOpen && (
|
{otherActionsPopoverOpen && (
|
||||||
<PropertiesPopover
|
<PropertiesPopover
|
||||||
className={CLASSES.SHAPE_ACTIONS_THEME_SCOPE}
|
className={PROPERTIES_CLASSES}
|
||||||
container={container}
|
container={container}
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "12rem",
|
maxWidth: "12rem",
|
||||||
|
@@ -19,6 +19,11 @@ import { ButtonSeparator } from "../ButtonSeparator";
|
|||||||
import { activeEyeDropperAtom } from "../EyeDropper";
|
import { activeEyeDropperAtom } from "../EyeDropper";
|
||||||
import { PropertiesPopover } from "../PropertiesPopover";
|
import { PropertiesPopover } from "../PropertiesPopover";
|
||||||
import { backgroundIcon, slashIcon, strokeIcon } from "../icons";
|
import { backgroundIcon, slashIcon, strokeIcon } from "../icons";
|
||||||
|
import {
|
||||||
|
saveCaretPosition,
|
||||||
|
restoreCaretPosition,
|
||||||
|
temporarilyDisableTextEditorBlur,
|
||||||
|
} from "../../hooks/useTextEditorFocus";
|
||||||
|
|
||||||
import { ColorInput } from "./ColorInput";
|
import { ColorInput } from "./ColorInput";
|
||||||
import { Picker } from "./Picker";
|
import { Picker } from "./Picker";
|
||||||
@@ -79,6 +84,7 @@ const ColorPickerPopupContent = ({
|
|||||||
palette = COLOR_PALETTE,
|
palette = COLOR_PALETTE,
|
||||||
updateData,
|
updateData,
|
||||||
getOpenPopup,
|
getOpenPopup,
|
||||||
|
appState,
|
||||||
}: Pick<
|
}: Pick<
|
||||||
ColorPickerProps,
|
ColorPickerProps,
|
||||||
| "type"
|
| "type"
|
||||||
@@ -88,6 +94,7 @@ const ColorPickerPopupContent = ({
|
|||||||
| "elements"
|
| "elements"
|
||||||
| "palette"
|
| "palette"
|
||||||
| "updateData"
|
| "updateData"
|
||||||
|
| "appState"
|
||||||
> & {
|
> & {
|
||||||
getOpenPopup: () => AppState["openPopup"];
|
getOpenPopup: () => AppState["openPopup"];
|
||||||
}) => {
|
}) => {
|
||||||
@@ -121,6 +128,8 @@ const ColorPickerPopupContent = ({
|
|||||||
<PropertiesPopover
|
<PropertiesPopover
|
||||||
container={container}
|
container={container}
|
||||||
style={{ maxWidth: "13rem" }}
|
style={{ maxWidth: "13rem" }}
|
||||||
|
// Improve focus handling for text editing scenarios
|
||||||
|
preventAutoFocusOnTouch={!!appState.editingTextElement}
|
||||||
onFocusOutside={(event) => {
|
onFocusOutside={(event) => {
|
||||||
// refocus due to eye dropper
|
// refocus due to eye dropper
|
||||||
focusPickerContent();
|
focusPickerContent();
|
||||||
@@ -140,6 +149,18 @@ const ColorPickerPopupContent = ({
|
|||||||
updateData({ openPopup: null });
|
updateData({ openPopup: null });
|
||||||
}
|
}
|
||||||
setActiveColorPickerSection(null);
|
setActiveColorPickerSection(null);
|
||||||
|
|
||||||
|
// Refocus text editor when popover closes if we were editing text
|
||||||
|
if (appState.editingTextElement) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const textEditor = document.querySelector(
|
||||||
|
".excalidraw-wysiwyg",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
if (textEditor) {
|
||||||
|
textEditor.focus();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{palette ? (
|
{palette ? (
|
||||||
@@ -148,7 +169,17 @@ const ColorPickerPopupContent = ({
|
|||||||
palette={palette}
|
palette={palette}
|
||||||
color={color}
|
color={color}
|
||||||
onChange={(changedColor) => {
|
onChange={(changedColor) => {
|
||||||
|
// Save caret position before color change if editing text
|
||||||
|
const savedSelection = appState.editingTextElement
|
||||||
|
? saveCaretPosition()
|
||||||
|
: null;
|
||||||
|
|
||||||
onChange(changedColor);
|
onChange(changedColor);
|
||||||
|
|
||||||
|
// Restore caret position after color change if editing text
|
||||||
|
if (appState.editingTextElement && savedSelection) {
|
||||||
|
restoreCaretPosition(savedSelection);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onEyeDropperToggle={(force) => {
|
onEyeDropperToggle={(force) => {
|
||||||
setEyeDropperState((state) => {
|
setEyeDropperState((state) => {
|
||||||
@@ -199,6 +230,7 @@ const ColorPickerTrigger = ({
|
|||||||
compactMode = false,
|
compactMode = false,
|
||||||
mode = "background",
|
mode = "background",
|
||||||
onToggle,
|
onToggle,
|
||||||
|
editingTextElement,
|
||||||
}: {
|
}: {
|
||||||
color: string | null;
|
color: string | null;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -206,7 +238,21 @@ const ColorPickerTrigger = ({
|
|||||||
compactMode?: boolean;
|
compactMode?: boolean;
|
||||||
mode?: "background" | "stroke";
|
mode?: "background" | "stroke";
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
|
editingTextElement?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
|
const handlePointerDown = (e: React.PointerEvent) => {
|
||||||
|
// use pointerdown so we run before outside-close logic
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// If editing text, temporarily disable the wysiwyg blur event
|
||||||
|
if (editingTextElement) {
|
||||||
|
temporarilyDisableTextEditorBlur();
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggle();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Trigger
|
<Popover.Trigger
|
||||||
type="button"
|
type="button"
|
||||||
@@ -223,12 +269,7 @@ const ColorPickerTrigger = ({
|
|||||||
: t("labels.showBackground")
|
: t("labels.showBackground")
|
||||||
}
|
}
|
||||||
data-openpopup={type}
|
data-openpopup={type}
|
||||||
onPointerDown={(e) => {
|
onPointerDown={handlePointerDown}
|
||||||
// use pointerdown so we run before outside-close logic
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
onToggle();
|
|
||||||
}}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
// suppress Radix default toggle to avoid double-toggle flicker
|
// suppress Radix default toggle to avoid double-toggle flicker
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -316,6 +357,7 @@ export const ColorPicker = ({
|
|||||||
type={type}
|
type={type}
|
||||||
compactMode={compactMode}
|
compactMode={compactMode}
|
||||||
mode={type === "elementStroke" ? "stroke" : "background"}
|
mode={type === "elementStroke" ? "stroke" : "background"}
|
||||||
|
editingTextElement={!!appState.editingTextElement}
|
||||||
onToggle={() => {
|
onToggle={() => {
|
||||||
// atomic switch: if another popup is open, close it first, then open this one next tick
|
// atomic switch: if another popup is open, close it first, then open this one next tick
|
||||||
if (appState.openPopup === type) {
|
if (appState.openPopup === type) {
|
||||||
@@ -342,6 +384,7 @@ export const ColorPicker = ({
|
|||||||
palette={palette}
|
palette={palette}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
getOpenPopup={() => openRef.current}
|
getOpenPopup={() => openRef.current}
|
||||||
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
|
@@ -102,21 +102,11 @@ export const FontPicker = React.memo(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!compactMode && <ButtonSeparator />}
|
{!compactMode && <ButtonSeparator />}
|
||||||
<Popover.Root open={isOpened} onOpenChange={() => {}}>
|
<Popover.Root open={isOpened} onOpenChange={onPopupChange}>
|
||||||
<FontPickerTrigger
|
<FontPickerTrigger
|
||||||
selectedFontFamily={selectedFontFamily}
|
selectedFontFamily={selectedFontFamily}
|
||||||
onTrigger={(e) => {
|
onToggle={() => {}} // No longer needed, Radix handles it
|
||||||
// suppress default to avoid double toggle
|
isOpened={isOpened}
|
||||||
if (e && e.preventDefault) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOpened) {
|
|
||||||
onPopupChange(false);
|
|
||||||
} else {
|
|
||||||
onPopupChange(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{isOpened && (
|
{isOpened && (
|
||||||
<FontPickerList
|
<FontPickerList
|
||||||
|
@@ -90,7 +90,8 @@ export const FontPickerList = React.memo(
|
|||||||
onClose,
|
onClose,
|
||||||
}: FontPickerListProps) => {
|
}: FontPickerListProps) => {
|
||||||
const { container } = useExcalidrawContainer();
|
const { container } = useExcalidrawContainer();
|
||||||
const { fonts } = useApp();
|
const app = useApp();
|
||||||
|
const { fonts } = app;
|
||||||
const { showDeprecatedFonts } = useAppProps();
|
const { showDeprecatedFonts } = useAppProps();
|
||||||
|
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
@@ -187,6 +188,42 @@ export const FontPickerList = React.memo(
|
|||||||
onLeave,
|
onLeave,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Create a wrapped onSelect function that preserves caret position
|
||||||
|
const wrappedOnSelect = useCallback(
|
||||||
|
(fontFamily: FontFamilyValues) => {
|
||||||
|
// Save caret position before font selection if editing text
|
||||||
|
let savedSelection: { start: number; end: number } | null = null;
|
||||||
|
if (app.state.editingTextElement) {
|
||||||
|
const textEditor = document.querySelector(
|
||||||
|
".excalidraw-wysiwyg",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
if (textEditor) {
|
||||||
|
savedSelection = {
|
||||||
|
start: textEditor.selectionStart,
|
||||||
|
end: textEditor.selectionEnd,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect(fontFamily);
|
||||||
|
|
||||||
|
// Restore caret position after font selection if editing text
|
||||||
|
if (app.state.editingTextElement && savedSelection) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const textEditor = document.querySelector(
|
||||||
|
".excalidraw-wysiwyg",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
if (textEditor && savedSelection) {
|
||||||
|
textEditor.focus();
|
||||||
|
textEditor.selectionStart = savedSelection.start;
|
||||||
|
textEditor.selectionEnd = savedSelection.end;
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onSelect, app.state.editingTextElement],
|
||||||
|
);
|
||||||
|
|
||||||
const onKeyDown = useCallback<KeyboardEventHandler<HTMLDivElement>>(
|
const onKeyDown = useCallback<KeyboardEventHandler<HTMLDivElement>>(
|
||||||
(event) => {
|
(event) => {
|
||||||
const handled = fontPickerKeyHandler({
|
const handled = fontPickerKeyHandler({
|
||||||
@@ -194,7 +231,7 @@ export const FontPickerList = React.memo(
|
|||||||
inputRef,
|
inputRef,
|
||||||
hoveredFont,
|
hoveredFont,
|
||||||
filteredFonts,
|
filteredFonts,
|
||||||
onSelect,
|
onSelect: wrappedOnSelect,
|
||||||
onHover,
|
onHover,
|
||||||
onClose,
|
onClose,
|
||||||
});
|
});
|
||||||
@@ -204,7 +241,7 @@ export const FontPickerList = React.memo(
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[hoveredFont, filteredFonts, onSelect, onHover, onClose],
|
[hoveredFont, filteredFonts, wrappedOnSelect, onHover, onClose],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -240,7 +277,7 @@ export const FontPickerList = React.memo(
|
|||||||
// allow to tab between search and selected font
|
// allow to tab between search and selected font
|
||||||
tabIndex={font.value === selectedFontFamily ? 0 : -1}
|
tabIndex={font.value === selectedFontFamily ? 0 : -1}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
onSelect(Number(e.currentTarget.value));
|
wrappedOnSelect(Number(e.currentTarget.value));
|
||||||
}}
|
}}
|
||||||
onMouseMove={() => {
|
onMouseMove={() => {
|
||||||
if (hoveredFont?.value !== font.value) {
|
if (hoveredFont?.value !== font.value) {
|
||||||
@@ -282,10 +319,24 @@ export const FontPickerList = React.memo(
|
|||||||
className="properties-content"
|
className="properties-content"
|
||||||
container={container}
|
container={container}
|
||||||
style={{ width: "15rem" }}
|
style={{ width: "15rem" }}
|
||||||
onClose={onClose}
|
onClose={() => {
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
// Refocus text editor when font picker closes if we were editing text
|
||||||
|
if (app.state.editingTextElement) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const textEditor = document.querySelector(
|
||||||
|
".excalidraw-wysiwyg",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
if (textEditor) {
|
||||||
|
textEditor.focus();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onPointerLeave={onLeave}
|
onPointerLeave={onLeave}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
preventAutoFocusOnTouch={true}
|
preventAutoFocusOnTouch={!!app.state.editingTextElement}
|
||||||
>
|
>
|
||||||
<QuickSearch
|
<QuickSearch
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
@@ -6,18 +6,22 @@ import type { FontFamilyValues } from "@excalidraw/element/types";
|
|||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import { ButtonIcon } from "../ButtonIcon";
|
import { ButtonIcon } from "../ButtonIcon";
|
||||||
import { TextIcon } from "../icons";
|
import { TextIcon } from "../icons";
|
||||||
|
import { useExcalidrawAppState } from "../App";
|
||||||
|
|
||||||
import { isDefaultFont } from "./FontPicker";
|
import { isDefaultFont } from "./FontPicker";
|
||||||
|
|
||||||
interface FontPickerTriggerProps {
|
interface FontPickerTriggerProps {
|
||||||
selectedFontFamily: FontFamilyValues | null;
|
selectedFontFamily: FontFamilyValues | null;
|
||||||
onTrigger?: (event: React.SyntheticEvent) => void;
|
onToggle: () => void;
|
||||||
|
isOpened?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FontPickerTrigger = ({
|
export const FontPickerTrigger = ({
|
||||||
selectedFontFamily,
|
selectedFontFamily,
|
||||||
onTrigger,
|
onToggle,
|
||||||
|
isOpened = false,
|
||||||
}: FontPickerTriggerProps) => {
|
}: FontPickerTriggerProps) => {
|
||||||
|
const appState = useExcalidrawAppState();
|
||||||
const isTriggerActive = useMemo(
|
const isTriggerActive = useMemo(
|
||||||
() => Boolean(selectedFontFamily && !isDefaultFont(selectedFontFamily)),
|
() => Boolean(selectedFontFamily && !isDefaultFont(selectedFontFamily)),
|
||||||
[selectedFontFamily],
|
[selectedFontFamily],
|
||||||
@@ -25,16 +29,14 @@ export const FontPickerTrigger = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Trigger asChild>
|
<Popover.Trigger asChild>
|
||||||
{/* Empty div as trigger so it's stretched 100% due to different button sizes */}
|
|
||||||
<div
|
<div
|
||||||
data-openpopup="fontFamily"
|
data-openpopup="fontFamily"
|
||||||
|
className="properties-trigger"
|
||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
e.preventDefault();
|
// Prevent default behavior that might dismiss keyboard on mobile when editing text
|
||||||
onTrigger?.(e);
|
if (appState.editingTextElement) {
|
||||||
}}
|
e.preventDefault();
|
||||||
onClick={(e) => {
|
}
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ButtonIcon
|
<ButtonIcon
|
||||||
@@ -43,11 +45,8 @@ export const FontPickerTrigger = ({
|
|||||||
title={t("labels.showFonts")}
|
title={t("labels.showFonts")}
|
||||||
className="properties-trigger"
|
className="properties-trigger"
|
||||||
testId={"font-family-show-fonts"}
|
testId={"font-family-show-fonts"}
|
||||||
active={isTriggerActive}
|
active={isTriggerActive || isOpened}
|
||||||
onClick={(e) => {
|
onClick={() => {}} // Let Radix handle the toggle
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
style={{
|
style={{
|
||||||
border: "none",
|
border: "none",
|
||||||
}}
|
}}
|
||||||
|
112
packages/excalidraw/hooks/useTextEditorFocus.ts
Normal file
112
packages/excalidraw/hooks/useTextEditorFocus.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { useState, useCallback } from "react";
|
||||||
|
|
||||||
|
// Utility type for caret position
|
||||||
|
export type CaretPosition = {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Utility function to get text editor element
|
||||||
|
const getTextEditor = (): HTMLTextAreaElement | null => {
|
||||||
|
return document.querySelector(".excalidraw-wysiwyg") as HTMLTextAreaElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Utility functions for caret position management
|
||||||
|
export const saveCaretPosition = (): CaretPosition | null => {
|
||||||
|
const textEditor = getTextEditor();
|
||||||
|
if (textEditor) {
|
||||||
|
return {
|
||||||
|
start: textEditor.selectionStart,
|
||||||
|
end: textEditor.selectionEnd,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const restoreCaretPosition = (position: CaretPosition | null): void => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const textEditor = getTextEditor();
|
||||||
|
if (textEditor) {
|
||||||
|
textEditor.focus();
|
||||||
|
if (position) {
|
||||||
|
textEditor.selectionStart = position.start;
|
||||||
|
textEditor.selectionEnd = position.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const withCaretPositionPreservation = (
|
||||||
|
callback: () => void,
|
||||||
|
isCompactMode: boolean,
|
||||||
|
isEditingText: boolean,
|
||||||
|
onPreventClose?: () => void,
|
||||||
|
): void => {
|
||||||
|
// Prevent popover from closing in compact mode
|
||||||
|
if (isCompactMode && onPreventClose) {
|
||||||
|
onPreventClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save caret position if editing text
|
||||||
|
const savedPosition =
|
||||||
|
isCompactMode && isEditingText ? saveCaretPosition() : null;
|
||||||
|
|
||||||
|
// Execute the callback
|
||||||
|
callback();
|
||||||
|
|
||||||
|
// Restore caret position if needed
|
||||||
|
if (isCompactMode && isEditingText) {
|
||||||
|
restoreCaretPosition(savedPosition);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook for managing text editor caret position with state
|
||||||
|
export const useTextEditorFocus = () => {
|
||||||
|
const [savedCaretPosition, setSavedCaretPosition] =
|
||||||
|
useState<CaretPosition | null>(null);
|
||||||
|
|
||||||
|
const saveCaretPositionToState = useCallback(() => {
|
||||||
|
const position = saveCaretPosition();
|
||||||
|
setSavedCaretPosition(position);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const restoreCaretPositionFromState = useCallback(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const textEditor = getTextEditor();
|
||||||
|
if (textEditor) {
|
||||||
|
textEditor.focus();
|
||||||
|
if (savedCaretPosition) {
|
||||||
|
textEditor.selectionStart = savedCaretPosition.start;
|
||||||
|
textEditor.selectionEnd = savedCaretPosition.end;
|
||||||
|
setSavedCaretPosition(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}, [savedCaretPosition]);
|
||||||
|
|
||||||
|
const clearSavedPosition = useCallback(() => {
|
||||||
|
setSavedCaretPosition(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
saveCaretPosition: saveCaretPositionToState,
|
||||||
|
restoreCaretPosition: restoreCaretPositionFromState,
|
||||||
|
clearSavedPosition,
|
||||||
|
hasSavedPosition: !!savedCaretPosition,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Utility function to temporarily disable text editor blur
|
||||||
|
export const temporarilyDisableTextEditorBlur = (
|
||||||
|
duration: number = 100,
|
||||||
|
): void => {
|
||||||
|
const textEditor = getTextEditor();
|
||||||
|
if (textEditor) {
|
||||||
|
const originalOnBlur = textEditor.onblur;
|
||||||
|
textEditor.onblur = null;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
textEditor.onblur = originalOnBlur;
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
};
|
@@ -538,6 +538,7 @@ export const textWysiwyg = ({
|
|||||||
if (isDestroyed) {
|
if (isDestroyed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isDestroyed = true;
|
isDestroyed = true;
|
||||||
// cleanup must be run before onSubmit otherwise when app blurs the wysiwyg
|
// cleanup must be run before onSubmit otherwise when app blurs the wysiwyg
|
||||||
// it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the
|
// it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the
|
||||||
@@ -621,14 +622,24 @@ export const textWysiwyg = ({
|
|||||||
const isPropertiesTrigger =
|
const isPropertiesTrigger =
|
||||||
target instanceof HTMLElement &&
|
target instanceof HTMLElement &&
|
||||||
target.classList.contains("properties-trigger");
|
target.classList.contains("properties-trigger");
|
||||||
|
const isPropertiesContent =
|
||||||
|
(target instanceof HTMLElement || target instanceof SVGElement) &&
|
||||||
|
!!(target as Element).closest(".properties-content");
|
||||||
|
const inShapeActionsMenu =
|
||||||
|
(target instanceof HTMLElement || target instanceof SVGElement) &&
|
||||||
|
(!!(target as Element).closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) ||
|
||||||
|
!!(target as Element).closest(".compact-shape-actions-island"));
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
editable.onblur = handleSubmit;
|
// If we interacted within shape actions menu or its popovers/triggers,
|
||||||
|
// keep submit disabled and don't steal focus back to textarea.
|
||||||
// case: clicking on the same property → no change → no update → no focus
|
if (inShapeActionsMenu || isPropertiesTrigger || isPropertiesContent) {
|
||||||
if (!isPropertiesTrigger) {
|
return;
|
||||||
editable.focus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Otherwise, re-enable submit on blur and refocus the editor.
|
||||||
|
editable.onblur = handleSubmit;
|
||||||
|
editable.focus();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -651,6 +662,7 @@ export const textWysiwyg = ({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
app.handleCanvasPanUsingWheelOrSpaceDrag(event);
|
app.handleCanvasPanUsingWheelOrSpaceDrag(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
temporarilyDisableSubmit();
|
temporarilyDisableSubmit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -658,15 +670,20 @@ export const textWysiwyg = ({
|
|||||||
const isPropertiesTrigger =
|
const isPropertiesTrigger =
|
||||||
target instanceof HTMLElement &&
|
target instanceof HTMLElement &&
|
||||||
target.classList.contains("properties-trigger");
|
target.classList.contains("properties-trigger");
|
||||||
|
const isPropertiesContent =
|
||||||
|
(target instanceof HTMLElement || target instanceof SVGElement) &&
|
||||||
|
!!(target as Element).closest(".properties-content");
|
||||||
|
|
||||||
if (
|
if (
|
||||||
((event.target instanceof HTMLElement ||
|
((event.target instanceof HTMLElement ||
|
||||||
event.target instanceof SVGElement) &&
|
event.target instanceof SVGElement) &&
|
||||||
event.target.closest(
|
(event.target.closest(
|
||||||
`.${CLASSES.SHAPE_ACTIONS_MENU}, .${CLASSES.ZOOM_ACTIONS}`,
|
`.${CLASSES.SHAPE_ACTIONS_MENU}, .${CLASSES.ZOOM_ACTIONS}`,
|
||||||
) &&
|
) ||
|
||||||
|
event.target.closest(".compact-shape-actions-island")) &&
|
||||||
!isWritableElement(event.target)) ||
|
!isWritableElement(event.target)) ||
|
||||||
isPropertiesTrigger
|
isPropertiesTrigger ||
|
||||||
|
isPropertiesContent
|
||||||
) {
|
) {
|
||||||
temporarilyDisableSubmit();
|
temporarilyDisableSubmit();
|
||||||
} else if (
|
} else if (
|
||||||
|
Reference in New Issue
Block a user