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

View file

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

View file

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

View file

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

View file

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

View file

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