More Arrowheads: dot, bar (#2486)
Co-authored-by: Jed Fox <git@jedfox.com> Co-authored-by: Lipis <lipiridis@gmail.com>
This commit is contained in:
parent
7c7fb4903b
commit
c742225f43
@ -1,4 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { getLanguage } from "../i18n";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
@ -16,7 +17,7 @@ import {
|
|||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { ButtonSelect } from "../components/ButtonSelect";
|
import { ButtonSelect } from "../components/ButtonSelect";
|
||||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
||||||
import { ButtonIconCycle } from "../components/ButtonIconCycle";
|
import { IconPicker } from "../components/IconPicker";
|
||||||
import {
|
import {
|
||||||
isTextElement,
|
isTextElement,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
@ -43,7 +44,10 @@ import {
|
|||||||
SloppinessArchitectIcon,
|
SloppinessArchitectIcon,
|
||||||
SloppinessArtistIcon,
|
SloppinessArtistIcon,
|
||||||
SloppinessCartoonistIcon,
|
SloppinessCartoonistIcon,
|
||||||
ArrowArrowheadIcon,
|
ArrowheadArrowIcon,
|
||||||
|
ArrowheadBarIcon,
|
||||||
|
ArrowheadDotIcon,
|
||||||
|
ArrowheadNoneIcon,
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
import { EVENT_CHANGE, trackEvent } from "../analytics";
|
import { EVENT_CHANGE, trackEvent } from "../analytics";
|
||||||
import colors from "../colors";
|
import colors from "../colors";
|
||||||
@ -671,66 +675,124 @@ export const actionChangeArrowhead = register({
|
|||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
<fieldset>
|
const isRTL = getLanguage().rtl;
|
||||||
<legend>{t("labels.arrowheads")}</legend>
|
|
||||||
<div className="buttonList buttonListIcon">
|
return (
|
||||||
<ButtonIconCycle
|
<fieldset>
|
||||||
group="arrowhead_start"
|
<legend>{t("labels.arrowheads")}</legend>
|
||||||
options={[
|
<div className="iconSelectList">
|
||||||
{
|
<IconPicker
|
||||||
value: null,
|
label="arrowhead_start"
|
||||||
text: t("labels.arrowhead_none"),
|
options={[
|
||||||
icon: <StrokeStyleSolidIcon appearance={appState.appearance} />,
|
{
|
||||||
},
|
value: null,
|
||||||
{
|
text: t("labels.arrowhead_none"),
|
||||||
value: "arrow",
|
icon: <ArrowheadNoneIcon appearance={appState.appearance} />,
|
||||||
text: t("labels.arrowhead_arrow"),
|
keyBinding: "q",
|
||||||
icon: (
|
},
|
||||||
<ArrowArrowheadIcon
|
{
|
||||||
appearance={appState.appearance}
|
value: "arrow",
|
||||||
flip={true}
|
text: t("labels.arrowhead_arrow"),
|
||||||
/>
|
icon: (
|
||||||
),
|
<ArrowheadArrowIcon
|
||||||
},
|
appearance={appState.appearance}
|
||||||
]}
|
flip={!isRTL}
|
||||||
value={getFormValue<Arrowhead | null>(
|
/>
|
||||||
elements,
|
),
|
||||||
appState,
|
keyBinding: "w",
|
||||||
(element) =>
|
},
|
||||||
isLinearElement(element) && canHaveArrowheads(element.type)
|
{
|
||||||
? element.startArrowhead
|
value: "bar",
|
||||||
: appState.currentItemArrowheads.start,
|
text: t("labels.arrowhead_bar"),
|
||||||
appState.currentItemArrowheads.start,
|
icon: (
|
||||||
)}
|
<ArrowheadBarIcon
|
||||||
onChange={(value) => updateData({ position: "start", type: value })}
|
appearance={appState.appearance}
|
||||||
/>
|
flip={!isRTL}
|
||||||
<ButtonIconCycle
|
/>
|
||||||
group="arrowhead_end"
|
),
|
||||||
options={[
|
keyBinding: "e",
|
||||||
{
|
},
|
||||||
value: null,
|
{
|
||||||
text: t("labels.arrowhead_none"),
|
value: "dot",
|
||||||
icon: <StrokeStyleSolidIcon appearance={appState.appearance} />,
|
text: t("labels.arrowhead_dot"),
|
||||||
},
|
icon: (
|
||||||
{
|
<ArrowheadDotIcon
|
||||||
value: "arrow",
|
appearance={appState.appearance}
|
||||||
text: t("labels.arrowhead_arrow"),
|
flip={!isRTL}
|
||||||
icon: <ArrowArrowheadIcon appearance={appState.appearance} />,
|
/>
|
||||||
},
|
),
|
||||||
]}
|
keyBinding: "r",
|
||||||
value={getFormValue<Arrowhead | null>(
|
},
|
||||||
elements,
|
]}
|
||||||
appState,
|
value={getFormValue<Arrowhead | null>(
|
||||||
(element) =>
|
elements,
|
||||||
isLinearElement(element) && canHaveArrowheads(element.type)
|
appState,
|
||||||
? element.endArrowhead
|
(element) =>
|
||||||
: appState.currentItemArrowheads.end,
|
isLinearElement(element) && canHaveArrowheads(element.type)
|
||||||
appState.currentItemArrowheads.end,
|
? element.startArrowhead
|
||||||
)}
|
: appState.currentItemArrowheads.start,
|
||||||
onChange={(value) => updateData({ position: "end", type: value })}
|
appState.currentItemArrowheads.start,
|
||||||
/>
|
)}
|
||||||
</div>
|
onChange={(value) => updateData({ position: "start", type: value })}
|
||||||
</fieldset>
|
/>
|
||||||
),
|
<IconPicker
|
||||||
|
label="arrowhead_end"
|
||||||
|
group="arrowheads"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: null,
|
||||||
|
text: t("labels.arrowhead_none"),
|
||||||
|
keyBinding: "q",
|
||||||
|
icon: <ArrowheadNoneIcon appearance={appState.appearance} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "arrow",
|
||||||
|
text: t("labels.arrowhead_arrow"),
|
||||||
|
keyBinding: "w",
|
||||||
|
icon: (
|
||||||
|
<ArrowheadArrowIcon
|
||||||
|
appearance={appState.appearance}
|
||||||
|
flip={isRTL}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "bar",
|
||||||
|
text: t("labels.arrowhead_bar"),
|
||||||
|
keyBinding: "e",
|
||||||
|
icon: (
|
||||||
|
<ArrowheadBarIcon
|
||||||
|
appearance={appState.appearance}
|
||||||
|
flip={isRTL}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "dot",
|
||||||
|
text: t("labels.arrowhead_dot"),
|
||||||
|
keyBinding: "r",
|
||||||
|
icon: (
|
||||||
|
<ArrowheadDotIcon
|
||||||
|
appearance={appState.appearance}
|
||||||
|
flip={isRTL}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
value={getFormValue<Arrowhead | null>(
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
(element) =>
|
||||||
|
isLinearElement(element) && canHaveArrowheads(element.type)
|
||||||
|
? element.endArrowhead
|
||||||
|
: appState.currentItemArrowheads.end,
|
||||||
|
appState.currentItemArrowheads.end,
|
||||||
|
)}
|
||||||
|
onChange={(value) => updateData({ position: "end", type: value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
137
src/components/IconPicker.scss
Normal file
137
src/components/IconPicker.scss
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
@import "open-color/open-color.scss";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.picker-container {
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker {
|
||||||
|
background: var(--popup-background-color);
|
||||||
|
border: 0px solid transparentize($oc-white, 0.75);
|
||||||
|
box-shadow: transparentize($oc-black, 0.75) 0px 1px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-container button,
|
||||||
|
.picker button {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: transparent;
|
||||||
|
background-color: var(--button-gray-2);
|
||||||
|
& svg {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--button-gray-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--button-gray-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin: 0;
|
||||||
|
width: 36px;
|
||||||
|
height: 18px;
|
||||||
|
opacity: 0.6;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker button {
|
||||||
|
padding: 0.25rem 0.28rem 0.35rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-triangle {
|
||||||
|
width: 0px;
|
||||||
|
height: 0px;
|
||||||
|
position: relative;
|
||||||
|
top: -10px;
|
||||||
|
:root[dir="ltr"] & {
|
||||||
|
left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[dir="rtl"] & {
|
||||||
|
right: 12px;
|
||||||
|
}
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0px 9px 10px;
|
||||||
|
border-color: transparent transparent transparentize($oc-black, 0.9);
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0px 9px 10px;
|
||||||
|
border-color: transparent transparent var(--popup-background-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-content {
|
||||||
|
padding: 0.5rem;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-gap: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-keybinding {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2px;
|
||||||
|
|
||||||
|
:root[dir="ltr"] & {
|
||||||
|
right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[dir="rtl"] & {
|
||||||
|
left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-type-canvasBackground .picker-keybinding {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-type-elementBackground .picker-keybinding {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-swatch[aria-label="transparent"] .picker-keybinding {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-type-elementStroke .picker-keybinding {
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.Appearance_dark {
|
||||||
|
.picker-type-elementBackground .picker-keybinding {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.picker-swatch[aria-label="transparent"] .picker-keybinding {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
188
src/components/IconPicker.tsx
Normal file
188
src/components/IconPicker.tsx
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Popover } from "./Popover";
|
||||||
|
|
||||||
|
import "./IconPicker.scss";
|
||||||
|
import { isArrowKey, KEYS } from "../keys";
|
||||||
|
import { getLanguage } from "../i18n";
|
||||||
|
|
||||||
|
function Picker<T>({
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
onChange,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: T;
|
||||||
|
options: { value: T; text: string; icon: JSX.Element; keyBinding: string }[];
|
||||||
|
onChange: (value: T) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const rFirstItem = React.useRef<HTMLButtonElement>();
|
||||||
|
const rActiveItem = React.useRef<HTMLButtonElement>();
|
||||||
|
const rGallery = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// After the component is first mounted focus on first input
|
||||||
|
if (rActiveItem.current) {
|
||||||
|
rActiveItem.current.focus();
|
||||||
|
} else if (rGallery.current) {
|
||||||
|
rGallery.current.focus();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||||
|
const pressedOption = options.find(
|
||||||
|
(option) => option.keyBinding === event.key.toLowerCase(),
|
||||||
|
)!;
|
||||||
|
|
||||||
|
if (!(event.metaKey || event.altKey || event.ctrlKey) && pressedOption) {
|
||||||
|
// Keybinding navigation
|
||||||
|
const index = options.indexOf(pressedOption);
|
||||||
|
(rGallery!.current!.children![index] as any).focus();
|
||||||
|
event.preventDefault();
|
||||||
|
} else if (event.key === KEYS.TAB) {
|
||||||
|
// Tab navigation cycle through options. If the user tabs
|
||||||
|
// away from the picker, close the picker. We need to use
|
||||||
|
// a timeout here to let the stack clear before checking.
|
||||||
|
setTimeout(() => {
|
||||||
|
const active = rActiveItem.current;
|
||||||
|
const docActive = document.activeElement;
|
||||||
|
if (active !== docActive) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
} else if (isArrowKey(event.key)) {
|
||||||
|
// Arrow navigation
|
||||||
|
const { activeElement } = document;
|
||||||
|
const isRTL = getLanguage().rtl;
|
||||||
|
const index = Array.prototype.indexOf.call(
|
||||||
|
rGallery!.current!.children,
|
||||||
|
activeElement,
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
const length = options.length;
|
||||||
|
let nextIndex = index;
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
// Select the next option
|
||||||
|
case isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT:
|
||||||
|
case KEYS.ARROW_DOWN: {
|
||||||
|
nextIndex = (index + 1) % length;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Select the previous option
|
||||||
|
case isRTL ? KEYS.ARROW_RIGHT : KEYS.ARROW_LEFT:
|
||||||
|
case KEYS.ARROW_UP: {
|
||||||
|
nextIndex = (length + index - 1) % length;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(rGallery.current!.children![nextIndex] as any).focus();
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
} else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
|
||||||
|
// Close on escape or enter
|
||||||
|
event.preventDefault();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`picker`}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={label}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
>
|
||||||
|
<div className="picker-content" ref={rGallery}>
|
||||||
|
{options.map((option, i) => (
|
||||||
|
<button
|
||||||
|
className="picker-option"
|
||||||
|
onClick={(event) => {
|
||||||
|
(event.currentTarget as HTMLButtonElement).focus();
|
||||||
|
onChange(option.value);
|
||||||
|
}}
|
||||||
|
title={`${option.text} — ${option.keyBinding.toUpperCase()}`}
|
||||||
|
aria-label={option.text || "none"}
|
||||||
|
aria-keyshortcuts={option.keyBinding}
|
||||||
|
key={option.text}
|
||||||
|
ref={(el) => {
|
||||||
|
if (el && i === 0) {
|
||||||
|
rFirstItem.current = el;
|
||||||
|
}
|
||||||
|
if (el && option.value === value) {
|
||||||
|
rActiveItem.current = el;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
onChange(option.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.icon}
|
||||||
|
<span className="picker-keybinding">{option.keyBinding}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconPicker<T>({
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
group = "",
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: T;
|
||||||
|
options: { value: T; text: string; icon: JSX.Element; keyBinding: string }[];
|
||||||
|
onChange: (value: T) => void;
|
||||||
|
group?: string;
|
||||||
|
}) {
|
||||||
|
const [isActive, setActive] = React.useState(false);
|
||||||
|
const rPickerButton = React.useRef<any>(null);
|
||||||
|
const isRTL = getLanguage().rtl;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label className={"picker-container"}>
|
||||||
|
<button
|
||||||
|
name={group}
|
||||||
|
className={isActive ? "active" : ""}
|
||||||
|
aria-label={label}
|
||||||
|
onClick={() => setActive(!isActive)}
|
||||||
|
ref={rPickerButton}
|
||||||
|
>
|
||||||
|
{options.find((option) => option.value === value)?.icon}
|
||||||
|
</button>
|
||||||
|
<React.Suspense fallback="">
|
||||||
|
{isActive ? (
|
||||||
|
<>
|
||||||
|
<Popover
|
||||||
|
onCloseRequest={(event) =>
|
||||||
|
event.target !== rPickerButton.current && setActive(false)
|
||||||
|
}
|
||||||
|
{...(isRTL ? { right: 5.5 } : { left: -5.5 })}
|
||||||
|
>
|
||||||
|
<Picker
|
||||||
|
options={options}
|
||||||
|
value={value}
|
||||||
|
label={label}
|
||||||
|
onChange={onChange}
|
||||||
|
onClose={() => {
|
||||||
|
setActive(false);
|
||||||
|
rPickerButton.current?.focus();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
<div className="picker-triangle" />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</React.Suspense>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
@ -82,7 +82,7 @@ export const MobileMenu = ({
|
|||||||
marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Island padding={3}>
|
<Island padding={0}>
|
||||||
{appState.openMenu === "canvas" ? (
|
{appState.openMenu === "canvas" ? (
|
||||||
<Section className="App-mobile-menu" heading="canvasActions">
|
<Section className="App-mobile-menu" heading="canvasActions">
|
||||||
<div className="panelColumn">
|
<div className="panelColumn">
|
||||||
|
@ -725,7 +725,23 @@ export const EdgeRoundIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ArrowArrowheadIcon = React.memo(
|
export const ArrowheadNoneIcon = React.memo(
|
||||||
|
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
d="M6 10H34"
|
||||||
|
stroke={iconFillColor(appearance)}
|
||||||
|
strokeWidth={2}
|
||||||
|
fill="none"
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
width: 40,
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ArrowheadArrowIcon = React.memo(
|
||||||
({
|
({
|
||||||
appearance,
|
appearance,
|
||||||
flip = false,
|
flip = false,
|
||||||
@ -743,6 +759,48 @@ export const ArrowArrowheadIcon = React.memo(
|
|||||||
<path d="M34 10H6M34 10L27 5M34 10L27 15" />
|
<path d="M34 10H6M34 10L27 5M34 10L27 15" />
|
||||||
<path d="M27.5 5L34.5 10L27.5 15" />
|
<path d="M27.5 5L34.5 10L27.5 15" />
|
||||||
</g>,
|
</g>,
|
||||||
{ width: 40, height: 20, mirror: true },
|
{ width: 40, height: 20 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ArrowheadDotIcon = React.memo(
|
||||||
|
({
|
||||||
|
appearance,
|
||||||
|
flip = false,
|
||||||
|
}: {
|
||||||
|
appearance: "light" | "dark";
|
||||||
|
flip?: boolean;
|
||||||
|
}) =>
|
||||||
|
createIcon(
|
||||||
|
<g
|
||||||
|
stroke={iconFillColor(appearance)}
|
||||||
|
fill={iconFillColor(appearance)}
|
||||||
|
transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}
|
||||||
|
>
|
||||||
|
<path d="M32 10L6 10" strokeWidth={2} />
|
||||||
|
<circle r="4" transform="matrix(-1 0 0 1 30 10)" />
|
||||||
|
</g>,
|
||||||
|
{ width: 40, height: 20 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ArrowheadBarIcon = React.memo(
|
||||||
|
({
|
||||||
|
appearance,
|
||||||
|
flip = false,
|
||||||
|
}: {
|
||||||
|
appearance: "light" | "dark";
|
||||||
|
flip?: boolean;
|
||||||
|
}) =>
|
||||||
|
createIcon(
|
||||||
|
<g transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}>
|
||||||
|
<path
|
||||||
|
d="M34 10H5.99996M34 10L34 5M34 10L34 15"
|
||||||
|
stroke={iconFillColor(appearance)}
|
||||||
|
strokeWidth={2}
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</g>,
|
||||||
|
{ width: 40, height: 20 },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -84,6 +84,11 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.iconSelectList {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.buttonList {
|
.buttonList {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
@ -236,6 +241,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
pointer-events: initial;
|
pointer-events: initial;
|
||||||
|
|
||||||
|
.panelColumn {
|
||||||
|
padding: 8px 8px 0px 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,6 +258,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-mobile-menu {
|
.App-mobile-menu {
|
||||||
|
@ -212,7 +212,11 @@ export const getArrowheadPoints = (
|
|||||||
const nx = (x2 - x1) / distance;
|
const nx = (x2 - x1) / distance;
|
||||||
const ny = (y2 - y1) / distance;
|
const ny = (y2 - y1) / distance;
|
||||||
|
|
||||||
const size = 30; // pixels (will differ for each arrowhead)
|
const size = {
|
||||||
|
arrow: 30,
|
||||||
|
bar: 15,
|
||||||
|
dot: 15,
|
||||||
|
}[arrowhead]; // pixels (will differ for each arrowhead)
|
||||||
|
|
||||||
const length = element.points.reduce((total, [cx, cy], idx, points) => {
|
const length = element.points.reduce((total, [cx, cy], idx, points) => {
|
||||||
const [px, py] = idx > 0 ? points[idx - 1] : [0, 0];
|
const [px, py] = idx > 0 ? points[idx - 1] : [0, 0];
|
||||||
@ -226,7 +230,15 @@ export const getArrowheadPoints = (
|
|||||||
const xs = x2 - nx * minSize;
|
const xs = x2 - nx * minSize;
|
||||||
const ys = y2 - ny * minSize;
|
const ys = y2 - ny * minSize;
|
||||||
|
|
||||||
const angle = 20; // degrees
|
if (arrowhead === "dot") {
|
||||||
|
const r = Math.hypot(ys - y2, xs - x2);
|
||||||
|
return [x2, y2, r];
|
||||||
|
}
|
||||||
|
|
||||||
|
const angle = {
|
||||||
|
arrow: 20,
|
||||||
|
bar: 90,
|
||||||
|
}[arrowhead]; // degrees
|
||||||
|
|
||||||
// Return points
|
// Return points
|
||||||
const [x3, y3] = rotate(xs, ys, x2, y2, (-angle * Math.PI) / 180);
|
const [x3, y3] = rotate(xs, ys, x2, y2, (-angle * Math.PI) / 180);
|
||||||
|
@ -98,7 +98,7 @@ export type PointBinding = {
|
|||||||
gap: number;
|
gap: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Arrowhead = "arrow";
|
export type Arrowhead = "arrow" | "bar" | "dot";
|
||||||
|
|
||||||
export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
||||||
Readonly<{
|
Readonly<{
|
||||||
|
@ -31,6 +31,8 @@
|
|||||||
"arrowheads": "Arrowheads",
|
"arrowheads": "Arrowheads",
|
||||||
"arrowhead_none": "None",
|
"arrowhead_none": "None",
|
||||||
"arrowhead_arrow": "Arrow",
|
"arrowhead_arrow": "Arrow",
|
||||||
|
"arrowhead_bar": "Bar",
|
||||||
|
"arrowhead_dot": "Dot",
|
||||||
"fontSize": "Font size",
|
"fontSize": "Font size",
|
||||||
"fontFamily": "Font family",
|
"fontFamily": "Font family",
|
||||||
"onlySelected": "Only selected",
|
"onlySelected": "Only selected",
|
||||||
|
@ -356,6 +356,17 @@ const generateElementShape = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Other arrowheads here...
|
// Other arrowheads here...
|
||||||
|
if (arrowhead === "dot") {
|
||||||
|
const [x, y, r] = arrowheadPoints;
|
||||||
|
|
||||||
|
return [
|
||||||
|
generator.circle(x, y, r, {
|
||||||
|
...options,
|
||||||
|
fill: element.strokeColor,
|
||||||
|
fillStyle: "solid",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// Arrow arrowheads
|
// Arrow arrowheads
|
||||||
const [x2, y2, x3, y3, x4, y4] = arrowheadPoints;
|
const [x2, y2, x3, y3, x4, y4] = arrowheadPoints;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user