mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feats: 完善选择箭头样式的 ui 样式
This commit is contained in:
parent
5bc27c64f2
commit
df48c49812
6 changed files with 191 additions and 77 deletions
|
@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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`;
|
||||||
|
|
|
@ -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": "中",
|
||||||
|
|
|
@ -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": "中",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue