Skip to content

Commit

Permalink
feat: add support for more UML arrowheads (excalidraw#7391)
Browse files Browse the repository at this point in the history
  • Loading branch information
dwelle committed Dec 6, 2023
1 parent a04cc70 commit b9cfbc2
Show file tree
Hide file tree
Showing 15 changed files with 449 additions and 216 deletions.
143 changes: 78 additions & 65 deletions src/actions/actionProperties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { IconPicker } from "../components/IconPicker";
import {
ArrowheadArrowIcon,
ArrowheadBarIcon,
ArrowheadDotIcon,
ArrowheadCircleIcon,
ArrowheadTriangleIcon,
ArrowheadNoneIcon,
StrokeStyleDashedIcon,
Expand Down Expand Up @@ -45,6 +45,10 @@ import {
TextAlignCenterIcon,
TextAlignRightIcon,
FillZigZagIcon,
ArrowheadTriangleOutlineIcon,
ArrowheadCircleOutlineIcon,
ArrowheadDiamondIcon,
ArrowheadDiamondOutlineIcon,
} from "../components/icons";
import {
DEFAULT_FONT_FAMILY,
Expand Down Expand Up @@ -1013,6 +1017,77 @@ export const actionChangeRoundness = register({
},
});

const getArrowheadOptions = (flip: boolean) => {
return [
{
value: null,
text: t("labels.arrowhead_none"),
keyBinding: "q",
icon: ArrowheadNoneIcon,
},
{
value: "arrow",
text: t("labels.arrowhead_arrow"),
keyBinding: "w",
icon: <ArrowheadArrowIcon flip={flip} />,
},
{
value: "bar",
text: t("labels.arrowhead_bar"),
keyBinding: "e",
icon: <ArrowheadBarIcon flip={flip} />,
},
{
value: "dot",
text: t("labels.arrowhead_circle"),
keyBinding: null,
icon: <ArrowheadCircleIcon flip={flip} />,
showInPicker: false,
},
{
value: "circle",
text: t("labels.arrowhead_circle"),
keyBinding: "r",
icon: <ArrowheadCircleIcon flip={flip} />,
showInPicker: false,
},
{
value: "circle_outline",
text: t("labels.arrowhead_circle_outline"),
keyBinding: null,
icon: <ArrowheadCircleOutlineIcon flip={flip} />,
showInPicker: false,
},
{
value: "triangle",
text: t("labels.arrowhead_triangle"),
icon: <ArrowheadTriangleIcon flip={flip} />,
keyBinding: "t",
},
{
value: "triangle_outline",
text: t("labels.arrowhead_triangle_outline"),
icon: <ArrowheadTriangleOutlineIcon flip={flip} />,
keyBinding: null,
showInPicker: false,
},
{
value: "diamond",
text: t("labels.arrowhead_diamond"),
icon: <ArrowheadDiamondIcon flip={flip} />,
keyBinding: null,
showInPicker: false,
},
{
value: "diamond_outline",
text: t("labels.arrowhead_diamond_outline"),
icon: <ArrowheadDiamondOutlineIcon flip={flip} />,
keyBinding: null,
showInPicker: false,
},
] as const;
};

export const actionChangeArrowhead = register({
name: "changeArrowhead",
trackEvent: false,
Expand Down Expand Up @@ -1059,38 +1134,7 @@ export const actionChangeArrowhead = register({
<div className="iconSelectList buttonList">
<IconPicker
label="arrowhead_start"
options={[
{
value: null,
text: t("labels.arrowhead_none"),
icon: ArrowheadNoneIcon,
keyBinding: "q",
},
{
value: "arrow",
text: t("labels.arrowhead_arrow"),
icon: <ArrowheadArrowIcon flip={!isRTL} />,
keyBinding: "w",
},
{
value: "bar",
text: t("labels.arrowhead_bar"),
icon: <ArrowheadBarIcon flip={!isRTL} />,
keyBinding: "e",
},
{
value: "dot",
text: t("labels.arrowhead_dot"),
icon: <ArrowheadDotIcon flip={!isRTL} />,
keyBinding: "r",
},
{
value: "triangle",
text: t("labels.arrowhead_triangle"),
icon: <ArrowheadTriangleIcon flip={!isRTL} />,
keyBinding: "t",
},
]}
options={getArrowheadOptions(!isRTL)}
value={getFormValue<Arrowhead | null>(
elements,
appState,
Expand All @@ -1106,38 +1150,7 @@ export const actionChangeArrowhead = register({
<IconPicker
label="arrowhead_end"
group="arrowheads"
options={[
{
value: null,
text: t("labels.arrowhead_none"),
keyBinding: "q",
icon: ArrowheadNoneIcon,
},
{
value: "arrow",
text: t("labels.arrowhead_arrow"),
keyBinding: "w",
icon: <ArrowheadArrowIcon flip={isRTL} />,
},
{
value: "bar",
text: t("labels.arrowhead_bar"),
keyBinding: "e",
icon: <ArrowheadBarIcon flip={isRTL} />,
},
{
value: "dot",
text: t("labels.arrowhead_dot"),
keyBinding: "r",
icon: <ArrowheadDotIcon flip={isRTL} />,
},
{
value: "triangle",
text: t("labels.arrowhead_triangle"),
icon: <ArrowheadTriangleIcon flip={isRTL} />,
keyBinding: "t",
},
]}
options={getArrowheadOptions(!!isRTL)}
value={getFormValue<Arrowhead | null>(
elements,
appState,
Expand Down
2 changes: 2 additions & 0 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1556,6 +1556,8 @@ class App extends React.Component<AppProps, AppState> {
imageCache: this.imageCache,
isExporting: false,
renderGrid: true,
canvasBackgroundColor:
this.state.viewBackgroundColor,
}}
/>
<InteractiveCanvas
Expand Down
27 changes: 21 additions & 6 deletions src/components/IconPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ function Picker<T>({
}: {
label: string;
value: T;
options: { value: T; text: string; icon: JSX.Element; keyBinding: string }[];
options: {
value: T;
text: string;
icon: JSX.Element;
keyBinding: string | null;
}[];
onChange: (value: T) => void;
onClose: () => void;
}) {
Expand Down Expand Up @@ -110,9 +115,11 @@ function Picker<T>({
(event.currentTarget as HTMLButtonElement).focus();
onChange(option.value);
}}
title={`${option.text}${option.keyBinding.toUpperCase()}`}
title={`${option.text} ${
option.keyBinding && `— ${option.keyBinding.toUpperCase()}`
}`}
aria-label={option.text || "none"}
aria-keyshortcuts={option.keyBinding}
aria-keyshortcuts={option.keyBinding || undefined}
key={option.text}
ref={(el) => {
if (el && i === 0) {
Expand All @@ -127,7 +134,9 @@ function Picker<T>({
}}
>
{option.icon}
<span className="picker-keybinding">{option.keyBinding}</span>
{option.keyBinding && (
<span className="picker-keybinding">{option.keyBinding}</span>
)}
</button>
))}
</div>
Expand All @@ -144,7 +153,13 @@ export function IconPicker<T>({
}: {
label: string;
value: T;
options: { value: T; text: string; icon: JSX.Element; keyBinding: string }[];
options: readonly {
value: T;
text: string;
icon: JSX.Element;
keyBinding: string | null;
showInPicker?: boolean;
}[];
onChange: (value: T) => void;
group?: string;
}) {
Expand Down Expand Up @@ -173,7 +188,7 @@ export function IconPicker<T>({
{...(isRTL ? { right: 5.5 } : { left: -5.5 })}
>
<Picker
options={options}
options={options.filter((opt) => opt.showInPicker !== false)}
value={value}
label={label}
onChange={onChange}
Expand Down
70 changes: 69 additions & 1 deletion src/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1281,7 +1281,7 @@ export const ArrowheadArrowIcon = React.memo(
),
);

export const ArrowheadDotIcon = React.memo(
export const ArrowheadCircleIcon = React.memo(
({ flip = false }: { flip?: boolean }) =>
createIcon(
<g
Expand All @@ -1296,6 +1296,22 @@ export const ArrowheadDotIcon = React.memo(
),
);

export const ArrowheadCircleOutlineIcon = React.memo(
({ flip = false }: { flip?: boolean }) =>
createIcon(
<g
stroke="currentColor"
fill="none"
transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}
strokeWidth={2}
>
<path d="M26 10L6 10" />
<circle r="4" transform="matrix(-1 0 0 1 30 10)" />
</g>,
{ width: 40, height: 20 },
),
);

export const ArrowheadBarIcon = React.memo(
({ flip = false }: { flip?: boolean }) =>
createIcon(
Expand Down Expand Up @@ -1326,6 +1342,58 @@ export const ArrowheadTriangleIcon = React.memo(
),
);

export const ArrowheadTriangleOutlineIcon = React.memo(
({ flip = false }: { flip?: boolean }) =>
createIcon(
<g
stroke="currentColor"
fill="none"
transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}
strokeWidth={2}
strokeLinejoin="round"
>
<path d="M6,9.5H27" />
<path d="M27,5L34,10L27,14Z" fill="none" />
</g>,

{ width: 40, height: 20 },
),
);

export const ArrowheadDiamondIcon = React.memo(
({ flip = false }: { flip?: boolean }) =>
createIcon(
<g
stroke="currentColor"
fill="currentColor"
transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}
strokeLinejoin="round"
strokeWidth={2}
>
<path d="M6,9.5H20" />
<path d="M27,5L34,10L27,14L20,9.5Z" />
</g>,
{ width: 40, height: 20 },
),
);

export const ArrowheadDiamondOutlineIcon = React.memo(
({ flip = false }: { flip?: boolean }) =>
createIcon(
<g
stroke="currentColor"
fill="none"
transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}
strokeLinejoin="round"
strokeWidth={2}
>
<path d="M6,9.5H20" />
<path d="M27,5L34,10L27,14L20,9.5Z" />
</g>,
{ width: 40, height: 20 },
),
);

export const FontSizeSmallIcon = createIcon(
<>
<g clipPath="url(#a)">
Expand Down
Loading

0 comments on commit b9cfbc2

Please sign in to comment.