mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-19 15:31:04 +02:00
mobile actions
This commit is contained in:
@@ -110,8 +110,8 @@
|
|||||||
--default-button-size: 2rem;
|
--default-button-size: 2rem;
|
||||||
|
|
||||||
.compact-action-button {
|
.compact-action-button {
|
||||||
width: 2rem;
|
width: 1.625rem;
|
||||||
height: 2rem;
|
height: 1.625rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: var(--border-radius-lg);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -167,6 +167,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ToolIcon__icon {
|
||||||
|
width: 1.625rem;
|
||||||
|
height: 1.625rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-shape-actions-island {
|
.compact-shape-actions-island {
|
||||||
@@ -199,6 +204,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-shape-actions {
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
.shape-actions-theme-scope {
|
.shape-actions-theme-scope {
|
||||||
--button-border: transparent;
|
--button-border: transparent;
|
||||||
--button-bg: var(--color-surface-mid);
|
--button-bg: var(--color-surface-mid);
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -86,6 +86,7 @@ import type {
|
|||||||
AppState,
|
AppState,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import type { ActionManager } from "../actions/manager";
|
import type { ActionManager } from "../actions/manager";
|
||||||
|
import { Island } from "./Island";
|
||||||
|
|
||||||
// Common CSS class combinations
|
// Common CSS class combinations
|
||||||
const PROPERTIES_CLASSES = clsx([
|
const PROPERTIES_CLASSES = clsx([
|
||||||
@@ -305,27 +306,19 @@ export const SelectedShapeActions = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CompactShapeActions = ({
|
const CombinedShapeProperties = ({
|
||||||
appState,
|
appState,
|
||||||
elementsMap,
|
|
||||||
renderAction,
|
renderAction,
|
||||||
app,
|
|
||||||
setAppState,
|
setAppState,
|
||||||
|
targetElements,
|
||||||
|
container,
|
||||||
}: {
|
}: {
|
||||||
|
targetElements: ExcalidrawElement[];
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap;
|
|
||||||
renderAction: ActionManager["renderAction"];
|
renderAction: ActionManager["renderAction"];
|
||||||
app: AppClassProperties;
|
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
container: HTMLDivElement | null;
|
||||||
}) => {
|
}) => {
|
||||||
const targetElements = getTargetElements(elementsMap, appState);
|
|
||||||
const { saveCaretPosition, restoreCaretPosition } = useTextEditorFocus();
|
|
||||||
const { container } = useExcalidrawContainer();
|
|
||||||
|
|
||||||
const isEditingTextOrNewElement = Boolean(
|
|
||||||
appState.editingTextElement || appState.newElement,
|
|
||||||
);
|
|
||||||
|
|
||||||
const showFillIcons =
|
const showFillIcons =
|
||||||
(hasBackground(appState.activeTool.type) &&
|
(hasBackground(appState.activeTool.type) &&
|
||||||
!isTransparent(appState.currentItemBackgroundColor)) ||
|
!isTransparent(appState.currentItemBackgroundColor)) ||
|
||||||
@@ -334,56 +327,20 @@ export const CompactShapeActions = ({
|
|||||||
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
||||||
);
|
);
|
||||||
|
|
||||||
const showLinkIcon = targetElements.length === 1;
|
const showShowCombinedProperties =
|
||||||
|
showFillIcons ||
|
||||||
const showLineEditorAction =
|
|
||||||
!appState.selectedLinearElement?.isEditing &&
|
|
||||||
targetElements.length === 1 &&
|
|
||||||
isLinearElement(targetElements[0]) &&
|
|
||||||
!isElbowArrow(targetElements[0]);
|
|
||||||
|
|
||||||
const showCropEditorAction =
|
|
||||||
!appState.croppingElementId &&
|
|
||||||
targetElements.length === 1 &&
|
|
||||||
isImageElement(targetElements[0]);
|
|
||||||
|
|
||||||
const showAlignActions = alignActionsPredicate(appState, app);
|
|
||||||
|
|
||||||
let isSingleElementBoundContainer = false;
|
|
||||||
if (
|
|
||||||
targetElements.length === 2 &&
|
|
||||||
(hasBoundTextElement(targetElements[0]) ||
|
|
||||||
hasBoundTextElement(targetElements[1]))
|
|
||||||
) {
|
|
||||||
isSingleElementBoundContainer = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="compact-shape-actions">
|
|
||||||
{/* Stroke Color */}
|
|
||||||
{canChangeStrokeColor(appState, targetElements) && (
|
|
||||||
<div className={clsx("compact-action-item")}>
|
|
||||||
{renderAction("changeStrokeColor")}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Background Color */}
|
|
||||||
{canChangeBackgroundColor(appState, targetElements) && (
|
|
||||||
<div className="compact-action-item">
|
|
||||||
{renderAction("changeBackgroundColor")}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Combined Properties (Fill, Stroke, Opacity) */}
|
|
||||||
{(showFillIcons ||
|
|
||||||
hasStrokeWidth(appState.activeTool.type) ||
|
hasStrokeWidth(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => hasStrokeWidth(element.type)) ||
|
targetElements.some((element) => hasStrokeWidth(element.type)) ||
|
||||||
hasStrokeStyle(appState.activeTool.type) ||
|
hasStrokeStyle(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => hasStrokeStyle(element.type)) ||
|
targetElements.some((element) => hasStrokeStyle(element.type)) ||
|
||||||
canChangeRoundness(appState.activeTool.type) ||
|
canChangeRoundness(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => canChangeRoundness(element.type))) && (
|
targetElements.some((element) => canChangeRoundness(element.type));
|
||||||
|
|
||||||
|
if (!showShowCombinedProperties) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="compact-action-item">
|
<div className="compact-action-item">
|
||||||
<Popover.Root
|
<Popover.Root
|
||||||
open={appState.openPopup === "compactStrokeStyles"}
|
open={appState.openPopup === "compactStrokeStyles"}
|
||||||
@@ -449,11 +406,33 @@ export const CompactShapeActions = ({
|
|||||||
)}
|
)}
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
{/* Combined Arrow Properties */}
|
const CombinedArrowProperties = ({
|
||||||
{(toolIsArrow(appState.activeTool.type) ||
|
appState,
|
||||||
targetElements.some((element) => toolIsArrow(element.type))) && (
|
renderAction,
|
||||||
|
setAppState,
|
||||||
|
targetElements,
|
||||||
|
container,
|
||||||
|
app,
|
||||||
|
}: {
|
||||||
|
targetElements: ExcalidrawElement[];
|
||||||
|
appState: UIAppState;
|
||||||
|
renderAction: ActionManager["renderAction"];
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
container: HTMLDivElement | null;
|
||||||
|
app: AppClassProperties;
|
||||||
|
}) => {
|
||||||
|
const showShowArrowProperties =
|
||||||
|
toolIsArrow(appState.activeTool.type) ||
|
||||||
|
targetElements.some((element) => toolIsArrow(element.type));
|
||||||
|
|
||||||
|
if (!showShowArrowProperties) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="compact-action-item">
|
<div className="compact-action-item">
|
||||||
<Popover.Root
|
<Popover.Root
|
||||||
open={appState.openPopup === "compactArrowProperties"}
|
open={appState.openPopup === "compactArrowProperties"}
|
||||||
@@ -524,22 +503,27 @@ export const CompactShapeActions = ({
|
|||||||
)}
|
)}
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
{/* Linear Editor */}
|
const CombinedTextProperties = ({
|
||||||
{showLineEditorAction && (
|
appState,
|
||||||
<div className="compact-action-item">
|
renderAction,
|
||||||
{renderAction("toggleLinearEditor")}
|
setAppState,
|
||||||
</div>
|
targetElements,
|
||||||
)}
|
container,
|
||||||
|
elementsMap,
|
||||||
|
}: {
|
||||||
|
appState: UIAppState;
|
||||||
|
renderAction: ActionManager["renderAction"];
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
targetElements: ExcalidrawElement[];
|
||||||
|
container: HTMLDivElement | null;
|
||||||
|
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap;
|
||||||
|
}) => {
|
||||||
|
const { saveCaretPosition, restoreCaretPosition } = useTextEditorFocus();
|
||||||
|
|
||||||
{/* Text Properties */}
|
return (
|
||||||
{(appState.activeTool.type === "text" ||
|
|
||||||
targetElements.some(isTextElement)) && (
|
|
||||||
<>
|
|
||||||
<div className="compact-action-item">
|
|
||||||
{renderAction("changeFontFamily")}
|
|
||||||
</div>
|
|
||||||
<div className="compact-action-item">
|
<div className="compact-action-item">
|
||||||
<Popover.Root
|
<Popover.Root
|
||||||
open={appState.openPopup === "compactTextProperties"}
|
open={appState.openPopup === "compactTextProperties"}
|
||||||
@@ -607,25 +591,49 @@ export const CompactShapeActions = ({
|
|||||||
)}
|
)}
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
</div>
|
</div>
|
||||||
</>
|
);
|
||||||
)}
|
};
|
||||||
|
|
||||||
{/* Dedicated Copy Button */}
|
const CombinedExtraActions = ({
|
||||||
{!isEditingTextOrNewElement && targetElements.length > 0 && (
|
appState,
|
||||||
<div className="compact-action-item">
|
renderAction,
|
||||||
{renderAction("duplicateSelection")}
|
targetElements,
|
||||||
</div>
|
setAppState,
|
||||||
)}
|
container,
|
||||||
|
app,
|
||||||
|
}: {
|
||||||
|
appState: UIAppState;
|
||||||
|
targetElements: ExcalidrawElement[];
|
||||||
|
renderAction: ActionManager["renderAction"];
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
container: HTMLDivElement | null;
|
||||||
|
app: AppClassProperties;
|
||||||
|
}) => {
|
||||||
|
const isEditingTextOrNewElement = Boolean(
|
||||||
|
appState.editingTextElement || appState.newElement,
|
||||||
|
);
|
||||||
|
const showCropEditorAction =
|
||||||
|
!appState.croppingElementId &&
|
||||||
|
targetElements.length === 1 &&
|
||||||
|
isImageElement(targetElements[0]);
|
||||||
|
const showLinkIcon = targetElements.length === 1;
|
||||||
|
const showAlignActions = alignActionsPredicate(appState, app);
|
||||||
|
let isSingleElementBoundContainer = false;
|
||||||
|
if (
|
||||||
|
targetElements.length === 2 &&
|
||||||
|
(hasBoundTextElement(targetElements[0]) ||
|
||||||
|
hasBoundTextElement(targetElements[1]))
|
||||||
|
) {
|
||||||
|
isSingleElementBoundContainer = true;
|
||||||
|
}
|
||||||
|
|
||||||
{/* Dedicated Delete Button */}
|
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||||
{!isEditingTextOrNewElement && targetElements.length > 0 && (
|
|
||||||
<div className="compact-action-item">
|
|
||||||
{renderAction("deleteSelectedElements")}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Combined Other Actions */}
|
if (isEditingTextOrNewElement || targetElements.length === 0) {
|
||||||
{!isEditingTextOrNewElement && targetElements.length > 0 && (
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="compact-action-item">
|
<div className="compact-action-item">
|
||||||
<Popover.Root
|
<Popover.Root
|
||||||
open={appState.openPopup === "compactOtherProperties"}
|
open={appState.openPopup === "compactOtherProperties"}
|
||||||
@@ -662,7 +670,6 @@ export const CompactShapeActions = ({
|
|||||||
container={container}
|
container={container}
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "12rem",
|
maxWidth: "12rem",
|
||||||
// center the popover content
|
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
@@ -731,11 +738,281 @@ export const CompactShapeActions = ({
|
|||||||
)}
|
)}
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LinearEditorAction = ({
|
||||||
|
appState,
|
||||||
|
renderAction,
|
||||||
|
targetElements,
|
||||||
|
}: {
|
||||||
|
appState: UIAppState;
|
||||||
|
targetElements: ExcalidrawElement[];
|
||||||
|
renderAction: ActionManager["renderAction"];
|
||||||
|
}) => {
|
||||||
|
const showLineEditorAction =
|
||||||
|
!appState.selectedLinearElement?.isEditing &&
|
||||||
|
targetElements.length === 1 &&
|
||||||
|
isLinearElement(targetElements[0]) &&
|
||||||
|
!isElbowArrow(targetElements[0]);
|
||||||
|
|
||||||
|
if (!showLineEditorAction) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="compact-action-item">
|
||||||
|
{renderAction("toggleLinearEditor")}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CompactShapeActions = ({
|
||||||
|
appState,
|
||||||
|
elementsMap,
|
||||||
|
renderAction,
|
||||||
|
app,
|
||||||
|
setAppState,
|
||||||
|
}: {
|
||||||
|
appState: UIAppState;
|
||||||
|
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap;
|
||||||
|
renderAction: ActionManager["renderAction"];
|
||||||
|
app: AppClassProperties;
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
}) => {
|
||||||
|
const targetElements = getTargetElements(elementsMap, appState);
|
||||||
|
const { container } = useExcalidrawContainer();
|
||||||
|
|
||||||
|
const isEditingTextOrNewElement = Boolean(
|
||||||
|
appState.editingTextElement || appState.newElement,
|
||||||
|
);
|
||||||
|
|
||||||
|
const showLineEditorAction =
|
||||||
|
!appState.selectedLinearElement?.isEditing &&
|
||||||
|
targetElements.length === 1 &&
|
||||||
|
isLinearElement(targetElements[0]) &&
|
||||||
|
!isElbowArrow(targetElements[0]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="compact-shape-actions">
|
||||||
|
{/* Stroke Color */}
|
||||||
|
{canChangeStrokeColor(appState, targetElements) && (
|
||||||
|
<div
|
||||||
|
className={clsx("compact-action-item")}
|
||||||
|
style={{
|
||||||
|
marginRight: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderAction("changeStrokeColor")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Background Color */}
|
||||||
|
{canChangeBackgroundColor(appState, targetElements) && (
|
||||||
|
<div className="compact-action-item">
|
||||||
|
{renderAction("changeBackgroundColor")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CombinedShapeProperties
|
||||||
|
appState={appState}
|
||||||
|
renderAction={renderAction}
|
||||||
|
setAppState={setAppState}
|
||||||
|
targetElements={targetElements}
|
||||||
|
container={container}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CombinedArrowProperties
|
||||||
|
appState={appState}
|
||||||
|
renderAction={renderAction}
|
||||||
|
setAppState={setAppState}
|
||||||
|
targetElements={targetElements}
|
||||||
|
container={container}
|
||||||
|
app={app}
|
||||||
|
/>
|
||||||
|
{/* Linear Editor */}
|
||||||
|
{showLineEditorAction && (
|
||||||
|
<div className="compact-action-item">
|
||||||
|
{renderAction("toggleLinearEditor")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Text Properties */}
|
||||||
|
{(appState.activeTool.type === "text" ||
|
||||||
|
targetElements.some(isTextElement)) && (
|
||||||
|
<>
|
||||||
|
<div className="compact-action-item">
|
||||||
|
{renderAction("changeFontFamily")}
|
||||||
|
</div>
|
||||||
|
<CombinedTextProperties
|
||||||
|
appState={appState}
|
||||||
|
renderAction={renderAction}
|
||||||
|
setAppState={setAppState}
|
||||||
|
targetElements={targetElements}
|
||||||
|
container={container}
|
||||||
|
elementsMap={elementsMap}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dedicated Copy Button */}
|
||||||
|
{!isEditingTextOrNewElement && targetElements.length > 0 && (
|
||||||
|
<div className="compact-action-item">
|
||||||
|
{renderAction("duplicateSelection")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dedicated Delete Button */}
|
||||||
|
{!isEditingTextOrNewElement && targetElements.length > 0 && (
|
||||||
|
<div className="compact-action-item">
|
||||||
|
{renderAction("deleteSelectedElements")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CombinedExtraActions
|
||||||
|
appState={appState}
|
||||||
|
renderAction={renderAction}
|
||||||
|
targetElements={targetElements}
|
||||||
|
setAppState={setAppState}
|
||||||
|
container={container}
|
||||||
|
app={app}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MobileShapeActions = ({
|
||||||
|
appState,
|
||||||
|
elementsMap,
|
||||||
|
renderAction,
|
||||||
|
app,
|
||||||
|
setAppState,
|
||||||
|
}: {
|
||||||
|
appState: UIAppState;
|
||||||
|
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap;
|
||||||
|
renderAction: ActionManager["renderAction"];
|
||||||
|
app: AppClassProperties;
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
}) => {
|
||||||
|
const targetElements = getTargetElements(elementsMap, appState);
|
||||||
|
const { container } = useExcalidrawContainer();
|
||||||
|
const mobileActionsRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const width = mobileActionsRef.current?.getBoundingClientRect()?.width ?? 0;
|
||||||
|
|
||||||
|
const WIDTH = 26;
|
||||||
|
const GAP = 8;
|
||||||
|
|
||||||
|
// max 6 actions + undo
|
||||||
|
const MIN_WIDTH = 7 * WIDTH + 6 * GAP;
|
||||||
|
|
||||||
|
const ADDITIONAL_WIDTH = WIDTH + GAP;
|
||||||
|
|
||||||
|
const showDelete = width >= MIN_WIDTH;
|
||||||
|
const showDuplicate = width >= MIN_WIDTH + 2 * ADDITIONAL_WIDTH;
|
||||||
|
const showRedo = width >= MIN_WIDTH + 3 * ADDITIONAL_WIDTH;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Island
|
||||||
|
className="compact-shape-actions mobile-shape-actions"
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
boxShadow: "none",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
padding: 0,
|
||||||
|
margin: "0 0.25rem",
|
||||||
|
zIndex: 2,
|
||||||
|
height: WIDTH * 1.75,
|
||||||
|
alignItems: "center",
|
||||||
|
gap: GAP,
|
||||||
|
}}
|
||||||
|
ref={mobileActionsRef}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: GAP,
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{canChangeStrokeColor(appState, targetElements) && (
|
||||||
|
<div className={clsx("compact-action-item")}>
|
||||||
|
{renderAction("changeStrokeColor")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{canChangeBackgroundColor(appState, targetElements) && (
|
||||||
|
<div className="compact-action-item">
|
||||||
|
{renderAction("changeBackgroundColor")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<CombinedShapeProperties
|
||||||
|
appState={appState}
|
||||||
|
renderAction={renderAction}
|
||||||
|
setAppState={setAppState}
|
||||||
|
targetElements={targetElements}
|
||||||
|
container={container}
|
||||||
|
/>
|
||||||
|
{/* Combined Arrow Properties */}
|
||||||
|
<CombinedArrowProperties
|
||||||
|
appState={appState}
|
||||||
|
renderAction={renderAction}
|
||||||
|
setAppState={setAppState}
|
||||||
|
targetElements={targetElements}
|
||||||
|
container={container}
|
||||||
|
app={app}
|
||||||
|
/>
|
||||||
|
{/* Linear Editor */}
|
||||||
|
<LinearEditorAction
|
||||||
|
appState={appState}
|
||||||
|
renderAction={renderAction}
|
||||||
|
targetElements={targetElements}
|
||||||
|
/>
|
||||||
|
{/* Text Properties */}
|
||||||
|
{(appState.activeTool.type === "text" ||
|
||||||
|
targetElements.some(isTextElement)) && (
|
||||||
|
<>
|
||||||
|
<div className="compact-action-item">
|
||||||
|
{renderAction("changeFontFamily")}
|
||||||
|
</div>
|
||||||
|
<CombinedTextProperties
|
||||||
|
appState={appState}
|
||||||
|
renderAction={renderAction}
|
||||||
|
setAppState={setAppState}
|
||||||
|
targetElements={targetElements}
|
||||||
|
container={container}
|
||||||
|
elementsMap={elementsMap}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDuplicate && renderAction("duplicateSelection")}
|
||||||
|
{showDelete && renderAction("deleteSelectedElements")}
|
||||||
|
|
||||||
|
{/* Combined Other Actions */}
|
||||||
|
<CombinedExtraActions
|
||||||
|
appState={appState}
|
||||||
|
renderAction={renderAction}
|
||||||
|
targetElements={targetElements}
|
||||||
|
setAppState={setAppState}
|
||||||
|
container={container}
|
||||||
|
app={app}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: GAP,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderAction("undo")}
|
||||||
|
{showRedo && renderAction("redo")}
|
||||||
|
</div>
|
||||||
|
</Island>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ShapesSwitcher = ({
|
export const ShapesSwitcher = ({
|
||||||
activeTool,
|
activeTool,
|
||||||
appState,
|
appState,
|
||||||
|
@@ -106,6 +106,7 @@ export const FontPicker = React.memo(
|
|||||||
<FontPickerTrigger
|
<FontPickerTrigger
|
||||||
selectedFontFamily={selectedFontFamily}
|
selectedFontFamily={selectedFontFamily}
|
||||||
isOpened={isOpened}
|
isOpened={isOpened}
|
||||||
|
compactMode={compactMode}
|
||||||
/>
|
/>
|
||||||
{isOpened && (
|
{isOpened && (
|
||||||
<FontPickerList
|
<FontPickerList
|
||||||
|
@@ -338,11 +338,13 @@ export const FontPickerList = React.memo(
|
|||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
preventAutoFocusOnTouch={!!app.state.editingTextElement}
|
preventAutoFocusOnTouch={!!app.state.editingTextElement}
|
||||||
>
|
>
|
||||||
|
{app.state.stylesPanelMode === "full" && (
|
||||||
<QuickSearch
|
<QuickSearch
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
placeholder={t("quickSearch.placeholder")}
|
placeholder={t("quickSearch.placeholder")}
|
||||||
onChange={debounce(setSearchTerm, 20)}
|
onChange={debounce(setSearchTerm, 20)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
className="dropdown-menu fonts manual-hover"
|
className="dropdown-menu fonts manual-hover"
|
||||||
placeholder={t("fontList.empty")}
|
placeholder={t("fontList.empty")}
|
||||||
|
@@ -11,11 +11,13 @@ import { useExcalidrawSetAppState } from "../App";
|
|||||||
interface FontPickerTriggerProps {
|
interface FontPickerTriggerProps {
|
||||||
selectedFontFamily: FontFamilyValues | null;
|
selectedFontFamily: FontFamilyValues | null;
|
||||||
isOpened?: boolean;
|
isOpened?: boolean;
|
||||||
|
compactMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FontPickerTrigger = ({
|
export const FontPickerTrigger = ({
|
||||||
selectedFontFamily,
|
selectedFontFamily,
|
||||||
isOpened = false,
|
isOpened = false,
|
||||||
|
compactMode = false,
|
||||||
}: FontPickerTriggerProps) => {
|
}: FontPickerTriggerProps) => {
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
|
|
||||||
@@ -37,6 +39,8 @@ export const FontPickerTrigger = ({
|
|||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
border: "none",
|
border: "none",
|
||||||
|
width: compactMode ? "1.625rem" : undefined,
|
||||||
|
height: compactMode ? "1.625rem" : undefined,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user