feats: 完善选择箭头样式的 ui 样式

This commit is contained in:
chao 2025-04-25 01:07:34 +08:00
parent 5bc27c64f2
commit df48c49812
6 changed files with 191 additions and 77 deletions

View file

@ -1542,46 +1542,92 @@ export const actionChangeArrowhead = register({
};
},
PanelComponent: ({ elements, appState, updateData }) => {
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
const isRTL = getLanguage().rtl;
return (
<fieldset>
<legend>{t("labels.arrowheads")}</legend>
<div className="iconSelectList buttonList">
<IconPicker
label="arrowhead_start"
options={getArrowheadOptions(!isRTL)}
value={getFormValue<Arrowhead | null>(
elements,
appState,
(element) =>
isLinearElement(element) && canHaveArrowheads(element.type)
? element.startArrowhead
: appState.currentItemStartArrowhead,
true,
appState.currentItemStartArrowhead,
)}
onChange={(value) => updateData({ position: "start", type: value })}
numberOfOptionsToAlwaysShow={4}
/>
<IconPicker
label="arrowhead_end"
group="arrowheads"
options={getArrowheadOptions(!!isRTL)}
value={getFormValue<Arrowhead | null>(
elements,
appState,
(element) =>
isLinearElement(element) && canHaveArrowheads(element.type)
? element.endArrowhead
: appState.currentItemEndArrowhead,
true,
appState.currentItemEndArrowhead,
)}
onChange={(value) => updateData({ position: "end", type: value })}
numberOfOptionsToAlwaysShow={4}
/>
</div>
{customOptions?.pickerRenders?.ButtonList && (
<customOptions.pickerRenders.ButtonList className="iconPickerList">
<IconPicker
label="arrowhead_start"
options={getArrowheadOptions(!isRTL)}
value={getFormValue<Arrowhead | null>(
elements,
appState,
(element) =>
isLinearElement(element) && canHaveArrowheads(element.type)
? element.startArrowhead
: appState.currentItemStartArrowhead,
true,
appState.currentItemStartArrowhead,
)}
onChange={(value) =>
updateData({ position: "start", type: value })
}
numberOfOptionsToAlwaysShow={4}
/>
<IconPicker
label="arrowhead_end"
group="arrowheads"
options={getArrowheadOptions(!!isRTL)}
value={getFormValue<Arrowhead | null>(
elements,
appState,
(element) =>
isLinearElement(element) && canHaveArrowheads(element.type)
? element.endArrowhead
: appState.currentItemEndArrowhead,
true,
appState.currentItemEndArrowhead,
)}
onChange={(value) => updateData({ position: "end", type: value })}
numberOfOptionsToAlwaysShow={4}
/>
</customOptions.pickerRenders.ButtonList>
)}
{!customOptions?.pickerRenders?.ButtonList && (
<div className="iconSelectList buttonList">
<IconPicker
label="arrowhead_start"
options={getArrowheadOptions(!isRTL)}
value={getFormValue<Arrowhead | null>(
elements,
appState,
(element) =>
isLinearElement(element) && canHaveArrowheads(element.type)
? element.startArrowhead
: appState.currentItemStartArrowhead,
true,
appState.currentItemStartArrowhead,
)}
onChange={(value) =>
updateData({ position: "start", type: value })
}
numberOfOptionsToAlwaysShow={4}
/>
<IconPicker
label="arrowhead_end"
group="arrowheads"
options={getArrowheadOptions(!!isRTL)}
value={getFormValue<Arrowhead | null>(
elements,
appState,
(element) =>
isLinearElement(element) && canHaveArrowheads(element.type)
? element.endArrowhead
: appState.currentItemEndArrowhead,
true,
appState.currentItemEndArrowhead,
)}
onChange={(value) => updateData({ position: "end", type: value })}
numberOfOptionsToAlwaysShow={4}
/>
</div>
)}
</fieldset>
);
},

View file

@ -1,12 +1,14 @@
import * as Popover from "@radix-ui/react-popover";
import clsx from "clsx";
import React, { useEffect } from "react";
import React, { useContext, useEffect } from "react";
import { isArrowKey, KEYS } from "@excalidraw/common";
import { atom, useAtom } from "../editor-jotai";
import { getLanguage, t } from "../i18n";
import { ExcalidrawPropsCustomOptionsContext } from "../types";
import Collapsible from "./Stats/Collapsible";
import { useDevice } from "./App";
@ -115,39 +117,63 @@ function Picker<T>({
}
}, [value, alwaysVisibleOptions, setShowMoreOptions]);
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
const renderOptions = (options: Option<T>[]) => {
return (
<div className="picker-content">
{options.map((option, i) => (
<button
type="button"
className={clsx("picker-option", {
{options.map((option, i) => {
if (customOptions?.pickerRenders?.layerButtonRender) {
return customOptions.pickerRenders.layerButtonRender({
active: value === option.value,
})}
onClick={(event) => {
onChange(option.value);
}}
title={`${option.text} ${
option.keyBinding && `${option.keyBinding.toUpperCase()}`
}`}
aria-label={option.text || "none"}
aria-keyshortcuts={option.keyBinding || undefined}
key={option.text}
ref={(ref) => {
if (value === option.value) {
// Use a timeout here to render focus properly
setTimeout(() => {
ref?.focus();
}, 0);
}
}}
>
{option.icon}
{option.keyBinding && (
<span className="picker-keybinding">{option.keyBinding}</span>
)}
</button>
))}
title: option.text,
children: (
<>
{option.icon}
{/* {option.keyBinding && (
<span className="picker-keybinding">
{option.keyBinding}
</span>
)} */}
</>
),
key: option.text,
onClick: () => onChange(option.value),
name: option.text,
});
}
return (
<button
type="button"
className={clsx("picker-option", {
active: value === option.value,
})}
onClick={(event) => {
onChange(option.value);
}}
title={`${option.text} ${
option.keyBinding && `${option.keyBinding.toUpperCase()}`
}`}
aria-label={option.text || "none"}
aria-keyshortcuts={option.keyBinding || undefined}
key={option.text}
ref={(ref) => {
if (value === option.value) {
// Use a timeout here to render focus properly
setTimeout(() => {
ref?.focus();
}, 0);
}
}}
>
{option.icon}
{option.keyBinding && (
<span className="picker-keybinding">{option.keyBinding}</span>
)}
</button>
);
})}
</div>
);
};
@ -162,7 +188,7 @@ function Picker<T>({
align="start"
sideOffset={12}
style={{ zIndex: "var(--zIndex-popup)" }}
onKeyDown={handleKeyDown}
onKeyDown={customOptions?.disableKeyEvents ? undefined : handleKeyDown}
>
<div
className={`picker`}
@ -209,22 +235,46 @@ export function IconPicker<T>({
numberOfOptionsToAlwaysShow?: number;
group?: string;
}) {
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
const [isActive, setActive] = React.useState(false);
const rPickerButton = React.useRef<any>(null);
const renderTrigger = () => {
if (customOptions?.pickerRenders?.layerButtonRender) {
return (
<>
<Popover.Trigger
name={group}
type="button"
aria-label={label}
onClick={() => setActive(!isActive)}
ref={rPickerButton}
className={isActive ? "active" : ""}
style={{
padding: 0,
border: "unset",
width: 0,
}}
>
{options.find((option) => option.value === value)?.icon}
</Popover.Trigger>
{customOptions.pickerRenders.layerButtonRender({
name: group,
title: "",
onClick: () => setActive(!isActive),
children: options.find((option) => option.value === value)?.icon,
active: isActive,
})}
</>
);
}
};
return (
<div>
<Popover.Root open={isActive} onOpenChange={(open) => setActive(open)}>
<Popover.Trigger
name={group}
type="button"
aria-label={label}
onClick={() => setActive(!isActive)}
ref={rPickerButton}
className={isActive ? "active" : ""}
>
{options.find((option) => option.value === value)?.icon}
</Popover.Trigger>
{renderTrigger()}
{isActive && (
<Picker
options={options}

View file

@ -47,6 +47,7 @@ const StaticCanvas = (props: StaticCanvasProps) => {
wrapper.replaceChildren(canvas);
canvas.classList.add("excalidraw__canvas", "static");
canvas.id = "excalidraw__content-canvas";
}
const widthString = `${props.appState.width}px`;

View file

@ -44,12 +44,19 @@
"arrowhead_triangle_outline": "三角箭头(空心)",
"arrowhead_diamond": "菱形",
"arrowhead_diamond_outline": "菱形(空心)",
"arrowhead_crowfoot_many": "交叉箭头(多个)",
"arrowhead_crowfoot_one": "交叉箭头(一个)",
"arrowhead_crowfoot_one_or_many": "交叉箭头(一个或多个)",
"arrowtypes": "箭头类型",
"arrowtype_sharp": "尖锐箭头",
"arrowtype_round": "圆润箭头",
"arrowtype_elbowed": "弯曲箭头",
"fontSize": "字体大小",
"fontFamily": "字体",
"addWatermark": "添加 “使用 Excalidraw 创建” 水印",
"handDrawn": "手写",
"normal": "普通",
"more_options": "更多选项",
"code": "代码",
"small": "小",
"medium": "中",

View file

@ -44,11 +44,19 @@
"arrowhead_triangle_outline": "三角形(外框)",
"arrowhead_diamond": "菱形",
"arrowhead_diamond_outline": "菱形(外框)",
"arrowhead_crowfoot_many": "交叉箭頭(多個)",
"arrowhead_crowfoot_one": "交叉箭頭(一個)",
"arrowhead_crowfoot_one_or_many": "交叉箭頭(一個或多個)",
"arrowtypes": "箭頭類型",
"arrowtype_sharp": "尖銳箭頭",
"arrowtype_round": "圓潤箭頭",
"arrowtype_elbowed": "彎曲箭頭",
"fontSize": "字型大小",
"fontFamily": "字體集",
"addWatermark": "加上 \"Made with Excalidraw\" 浮水印",
"handDrawn": "手寫",
"normal": "一般",
"more_options": "更多選項",
"code": "代碼",
"small": "小",
"medium": "中",

View file

@ -544,7 +544,7 @@ export interface ExcalidrawPropsCustomOptions {
menuRender?: (props: { children: React.ReactNode }) => React.ReactNode;
};
pickerRenders?: {
ButtonList?: React.ComponentType<{ children: React.ReactNode }>;
ButtonList?: React.ComponentType<{ children: React.ReactNode; className?: string }>;
elementStrokeColors?: ColorTuple;
elementBackgroundColors?: ColorTuple;
buttonIconSelectRender?: <T extends Object>(
@ -569,6 +569,8 @@ export interface ExcalidrawPropsCustomOptions {
name: string;
visible?: boolean;
hidden?: boolean;
key?: string;
active?: boolean;
}) => JSX.Element;
rangeRender?: (props: {
value: number;