| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056 |
- import { AppState } from "../../src/types";
- import { ButtonIconSelect } from "../components/ButtonIconSelect";
- import { ColorPicker } from "../components/ColorPicker";
- import { IconPicker } from "../components/IconPicker";
- import {
- ArrowheadArrowIcon,
- ArrowheadBarIcon,
- ArrowheadDotIcon,
- ArrowheadTriangleIcon,
- ArrowheadNoneIcon,
- EdgeRoundIcon,
- EdgeSharpIcon,
- FillCrossHatchIcon,
- FillHachureIcon,
- FillSolidIcon,
- FontFamilyCodeIcon,
- FontFamilyHandDrawnIcon,
- FontFamilyNormalIcon,
- FontSizeExtraLargeIcon,
- FontSizeLargeIcon,
- FontSizeMediumIcon,
- FontSizeSmallIcon,
- SloppinessArchitectIcon,
- SloppinessArtistIcon,
- SloppinessCartoonistIcon,
- StrokeStyleDashedIcon,
- StrokeStyleDottedIcon,
- StrokeStyleSolidIcon,
- StrokeWidthIcon,
- TextAlignCenterIcon,
- TextAlignLeftIcon,
- TextAlignRightIcon,
- TextAlignTopIcon,
- TextAlignBottomIcon,
- TextAlignMiddleIcon,
- } from "../components/icons";
- import {
- DEFAULT_FONT_FAMILY,
- DEFAULT_FONT_SIZE,
- FONT_FAMILY,
- VERTICAL_ALIGN,
- } from "../constants";
- import {
- getNonDeletedElements,
- isTextElement,
- redrawTextBoundingBox,
- } from "../element";
- import { mutateElement, newElementWith } from "../element/mutateElement";
- import {
- getBoundTextElement,
- getContainerElement,
- } from "../element/textElement";
- import {
- isBoundToContainer,
- isLinearElement,
- isLinearElementType,
- } from "../element/typeChecks";
- import {
- Arrowhead,
- ExcalidrawElement,
- ExcalidrawLinearElement,
- ExcalidrawTextElement,
- FontFamilyValues,
- TextAlign,
- VerticalAlign,
- } from "../element/types";
- import { getLanguage, t } from "../i18n";
- import { KEYS } from "../keys";
- import { randomInteger } from "../random";
- import {
- canChangeSharpness,
- canHaveArrowheads,
- getCommonAttributeOfSelectedElements,
- getSelectedElements,
- getTargetElements,
- isSomeElementSelected,
- } from "../scene";
- import { hasStrokeColor } from "../scene/comparisons";
- import { arrayToMap } from "../utils";
- import { register } from "./register";
- const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
- const changeProperty = (
- elements: readonly ExcalidrawElement[],
- appState: AppState,
- callback: (element: ExcalidrawElement) => ExcalidrawElement,
- includeBoundText = false,
- ) => {
- const selectedElementIds = arrayToMap(
- getSelectedElements(elements, appState, includeBoundText),
- );
- return elements.map((element) => {
- if (
- selectedElementIds.get(element.id) ||
- element.id === appState.editingElement?.id
- ) {
- return callback(element);
- }
- return element;
- });
- };
- const getFormValue = function <T>(
- elements: readonly ExcalidrawElement[],
- appState: AppState,
- getAttribute: (element: ExcalidrawElement) => T,
- defaultValue?: T,
- ): T | null {
- const editingElement = appState.editingElement;
- const nonDeletedElements = getNonDeletedElements(elements);
- return (
- (editingElement && getAttribute(editingElement)) ??
- (isSomeElementSelected(nonDeletedElements, appState)
- ? getCommonAttributeOfSelectedElements(
- nonDeletedElements,
- appState,
- getAttribute,
- )
- : defaultValue) ??
- null
- );
- };
- const offsetElementAfterFontResize = (
- prevElement: ExcalidrawTextElement,
- nextElement: ExcalidrawTextElement,
- ) => {
- if (isBoundToContainer(nextElement)) {
- return nextElement;
- }
- return mutateElement(
- nextElement,
- {
- x:
- prevElement.textAlign === "left"
- ? prevElement.x
- : prevElement.x +
- (prevElement.width - nextElement.width) /
- (prevElement.textAlign === "center" ? 2 : 1),
- // centering vertically is non-standard, but for Excalidraw I think
- // it makes sense
- y: prevElement.y + (prevElement.height - nextElement.height) / 2,
- },
- false,
- );
- };
- const changeFontSize = (
- elements: readonly ExcalidrawElement[],
- appState: AppState,
- getNewFontSize: (element: ExcalidrawTextElement) => number,
- fallbackValue?: ExcalidrawTextElement["fontSize"],
- ) => {
- const newFontSizes = new Set<number>();
- return {
- elements: changeProperty(
- elements,
- appState,
- (oldElement) => {
- if (isTextElement(oldElement)) {
- const newFontSize = getNewFontSize(oldElement);
- newFontSizes.add(newFontSize);
- let newElement: ExcalidrawTextElement = newElementWith(oldElement, {
- fontSize: newFontSize,
- });
- redrawTextBoundingBox(newElement, getContainerElement(oldElement));
- newElement = offsetElementAfterFontResize(oldElement, newElement);
- return newElement;
- }
- return oldElement;
- },
- true,
- ),
- appState: {
- ...appState,
- // update state only if we've set all select text elements to
- // the same font size
- currentItemFontSize:
- newFontSizes.size === 1
- ? [...newFontSizes][0]
- : fallbackValue ?? appState.currentItemFontSize,
- },
- commitToHistory: true,
- };
- };
- // -----------------------------------------------------------------------------
- export const actionChangeStrokeColor = register({
- name: "changeStrokeColor",
- trackEvent: false,
- perform: (elements, appState, value) => {
- return {
- ...(value.currentItemStrokeColor && {
- elements: changeProperty(
- elements,
- appState,
- (el) => {
- return hasStrokeColor(el.type)
- ? newElementWith(el, {
- strokeColor: value.currentItemStrokeColor,
- })
- : el;
- },
- true,
- ),
- }),
- appState: {
- ...appState,
- ...value,
- },
- commitToHistory: !!value.currentItemStrokeColor,
- };
- },
- PanelComponent: ({ elements, appState, updateData }) => (
- <>
- <h3 aria-hidden="true">{t("labels.stroke")}</h3>
- <ColorPicker
- type="elementStroke"
- label={t("labels.stroke")}
- color={getFormValue(
- elements,
- appState,
- (element) => element.strokeColor,
- appState.currentItemStrokeColor,
- )}
- onChange={(color) => updateData({ currentItemStrokeColor: color })}
- isActive={appState.openPopup === "strokeColorPicker"}
- setActive={(active) =>
- updateData({ openPopup: active ? "strokeColorPicker" : null })
- }
- elements={elements}
- appState={appState}
- />
- </>
- ),
- });
- export const actionChangeBackgroundColor = register({
- name: "changeBackgroundColor",
- trackEvent: false,
- perform: (elements, appState, value) => {
- return {
- ...(value.currentItemBackgroundColor && {
- elements: changeProperty(elements, appState, (el) =>
- newElementWith(el, {
- backgroundColor: value.currentItemBackgroundColor,
- }),
- ),
- }),
- appState: {
- ...appState,
- ...value,
- },
- commitToHistory: !!value.currentItemBackgroundColor,
- };
- },
- PanelComponent: ({ elements, appState, updateData }) => (
- <>
- <h3 aria-hidden="true">{t("labels.background")}</h3>
- <ColorPicker
- type="elementBackground"
- label={t("labels.background")}
- color={getFormValue(
- elements,
- appState,
- (element) => element.backgroundColor,
- appState.currentItemBackgroundColor,
- )}
- onChange={(color) => updateData({ currentItemBackgroundColor: color })}
- isActive={appState.openPopup === "backgroundColorPicker"}
- setActive={(active) =>
- updateData({ openPopup: active ? "backgroundColorPicker" : null })
- }
- elements={elements}
- appState={appState}
- />
- </>
- ),
- });
- export const actionChangeFillStyle = register({
- name: "changeFillStyle",
- trackEvent: false,
- perform: (elements, appState, value) => {
- return {
- elements: changeProperty(elements, appState, (el) =>
- newElementWith(el, {
- fillStyle: value,
- }),
- ),
- appState: { ...appState, currentItemFillStyle: value },
- commitToHistory: true,
- };
- },
- PanelComponent: ({ elements, appState, updateData }) => (
- <fieldset>
- <legend>{t("labels.fill")}</legend>
- <ButtonIconSelect
- options={[
- {
- value: "hachure",
- text: t("labels.hachure"),
- icon: <FillHachureIcon theme={appState.theme} />,
- },
- {
- value: "cross-hatch",
- text: t("labels.crossHatch"),
- icon: <FillCrossHatchIcon theme={appState.theme} />,
- },
- {
- value: "solid",
- text: t("labels.solid"),
- icon: <FillSolidIcon theme={appState.theme} />,
- },
- ]}
- group="fill"
- value={getFormValue(
- elements,
- appState,
- (element) => element.fillStyle,
- appState.currentItemFillStyle,
- )}
- onChange={(value) => {
- updateData(value);
- }}
- />
- </fieldset>
- ),
- });
- export const actionChangeStrokeWidth = register({
- name: "changeStrokeWidth",
- trackEvent: false,
- perform: (elements, appState, value) => {
- return {
- elements: changeProperty(elements, appState, (el) =>
- newElementWith(el, {
- strokeWidth: value,
- }),
- ),
- appState: { ...appState, currentItemStrokeWidth: value },
- commitToHistory: true,
- };
- },
- PanelComponent: ({ elements, appState, updateData }) => (
- <fieldset>
- <legend>{t("labels.strokeWidth")}</legend>
- <ButtonIconSelect
- group="stroke-width"
- options={[
- {
- value: 1,
- text: t("labels.thin"),
- icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={2} />,
- },
- {
- value: 2,
- text: t("labels.bold"),
- icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={6} />,
- },
- {
- value: 4,
- text: t("labels.extraBold"),
- icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={10} />,
- },
- ]}
- value={getFormValue(
- elements,
- appState,
- (element) => element.strokeWidth,
- appState.currentItemStrokeWidth,
- )}
- onChange={(value) => updateData(value)}
- />
- </fieldset>
- ),
- });
- export const actionChangeSloppiness = register({
- name: "changeSloppiness",
- trackEvent: false,
- perform: (elements, appState, value) => {
- return {
- elements: changeProperty(elements, appState, (el) =>
- newElementWith(el, {
- seed: randomInteger(),
- roughness: value,
- }),
- ),
- appState: { ...appState, currentItemRoughness: value },
- commitToHistory: true,
- };
- },
- PanelComponent: ({ elements, appState, updateData }) => (
- <fieldset>
- <legend>{t("labels.sloppiness")}</legend>
- <ButtonIconSelect
- group="sloppiness"
- options={[
- {
- value: 0,
- text: t("labels.architect"),
- icon: <SloppinessArchitectIcon theme={appState.theme} />,
- },
- {
- value: 1,
- text: t("labels.artist"),
- icon: <SloppinessArtistIcon theme={appState.theme} />,
- },
- {
- value: 2,
- text: t("labels.cartoonist"),
- icon: <SloppinessCartoonistIcon theme={appState.theme} />,
- },
- ]}
- value={getFormValue(
- elements,
- appState,
- (element) => element.roughness,
- appState.currentItemRoughness,
- )}
- onChange={(value) => updateData(value)}
- />
- </fieldset>
- ),
- });
- export const actionChangeStrokeStyle = register({
- name: "changeStrokeStyle",
- trackEvent: false,
- perform: (elements, appState, value) => {
- return {
- elements: changeProperty(elements, appState, (el) =>
- newElementWith(el, {
- strokeStyle: value,
- }),
- ),
- appState: { ...appState, currentItemStrokeStyle: value },
- commitToHistory: true,
- };
- },
- PanelComponent: ({ elements, appState, updateData }) => (
- <fieldset>
- <legend>{t("labels.strokeStyle")}</legend>
- <ButtonIconSelect
- group="strokeStyle"
- options={[
- {
- value: "solid",
- text: t("labels.strokeStyle_solid"),
- icon: <StrokeStyleSolidIcon theme={appState.theme} />,
- },
- {
- value: "dashed",
- text: t("labels.strokeStyle_dashed"),
- icon: <StrokeStyleDashedIcon theme={appState.theme} />,
- },
- {
- value: "dotted",
- text: t("labels.strokeStyle_dotted"),
- icon: <StrokeStyleDottedIcon theme={appState.theme} />,
- },
- ]}
- value={getFormValue(
- elements,
- appState,
- (element) => element.strokeStyle,
- appState.currentItemStrokeStyle,
- )}
- onChange={(value) => updateData(value)}
- />
- </fieldset>
- ),
- });
- export const actionChangeOpacity = register({
- name: "changeOpacity",
- trackEvent: false,
- perform: (elements, appState, value) => {
- return {
- elements: changeProperty(
- elements,
- appState,
- (el) =>
- newElementWith(el, {
- opacity: value,
- }),
- true,
- ),
- appState: { ...appState, currentItemOpacity: value },
- commitToHistory: true,
- };
- },
- PanelComponent: ({ elements, appState, updateData }) => (
- <label className="control-label">
- {t("labels.opacity")}
- <input
- type="range"
- min="0"
- max="100"
- step="10"
- onChange={(event) => updateData(+event.target.value)}
- value={
- getFormValue(
- elements,
- appState,
- (element) => element.opacity,
- appState.currentItemOpacity,
- ) ?? undefined
- }
- />
- </label>
- ),
- });
- export const actionChangeFontSize = register({
- name: "changeFontSize",
- trackEvent: false,
- perform: (elements, appState, value) => {
- return changeFontSize(elements, appState, () => value, value);
- },
- PanelComponent: ({ elements, appState, updateData }) => (
- <fieldset>
- <legend>{t("labels.fontSize")}</legend>
- <ButtonIconSelect
- group="font-size"
- options={[
- {
- value: 16,
- text: t("labels.small"),
- icon: <FontSizeSmallIcon theme={appState.theme} />,
- testId: "fontSize-small",
- },
- {
- value: 20,
- text: t("labels.medium"),
- icon: <FontSizeMediumIcon theme={appState.theme} />,
- testId: "fontSize-medium",
- },
- {
- value: 28,
- text: t("labels.large"),
- icon: <FontSizeLargeIcon theme={appState.theme} />,
- testId: "fontSize-large",
- },
- {
- value: 36,
- text: t("labels.veryLarge"),
- icon: <FontSizeExtraLargeIcon theme={appState.theme} />,
- testId: "fontSize-veryLarge",
- },
- ]}
- value={getFormValue(
- elements,
- appState,
- (element) => {
- if (isTextElement(element)) {
- return element.fontSize;
- }
- const boundTextElement = getBoundTextElement(element);
- if (boundTextElement) {
- return boundTextElement.fontSize;
- }
- return null;
- },
- appState.currentItemFontSize || DEFAULT_FONT_SIZE,
- )}
- onChange={(value) => updateData(value)}
- />
- </fieldset>
- ),
- });
- export const actionDecreaseFontSize = register({
- name: "decreaseFontSize",
- trackEvent: false,
- perform: (elements, appState, value) => {
- return changeFontSize(elements, appState, (element) =>
- Math.round(
- // get previous value before relative increase (doesn't work fully
- // due to rounding and float precision issues)
- (1 / (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)) * element.fontSize,
- ),
- );
- },
- keyTest: (event) => {
- return (
- event[KEYS.CTRL_OR_CMD] &&
- event.shiftKey &&
- // KEYS.COMMA needed for MacOS
- (event.key === KEYS.CHEVRON_LEFT || event.key === KEYS.COMMA)
- );
- },
- });
- export const actionIncreaseFontSize = register({
- name: "increaseFontSize",
- trackEvent: false,
- perform: (elements, appState, value) => {
- return changeFontSize(elements, appState, (element) =>
- Math.round(element.fontSize * (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)),
- );
- },
- keyTest: (event) => {
- return (
- event[KEYS.CTRL_OR_CMD] &&
- event.shiftKey &&
- // KEYS.PERIOD needed for MacOS
- (event.key === KEYS.CHEVRON_RIGHT || event.key === KEYS.PERIOD)
- );
- },
- });
- export const actionChangeFontFamily = register({
- name: "changeFontFamily",
- trackEvent: false,
- perform: (elements, appState, value) => {
- return {
- elements: changeProperty(
- elements,
- appState,
- (oldElement) => {
- if (isTextElement(oldElement)) {
- const newElement: ExcalidrawTextElement = newElementWith(
- oldElement,
- {
- fontFamily: value,
- },
- );
- redrawTextBoundingBox(newElement, getContainerElement(oldElement));
- return newElement;
- }
- return oldElement;
- },
- true,
- ),
- appState: {
- ...appState,
- currentItemFontFamily: value,
- },
- commitToHistory: true,
- };
- },
- PanelComponent: ({ elements, appState, updateData }) => {
- const options: {
- value: FontFamilyValues;
- text: string;
- icon: JSX.Element;
- }[] = [
- {
- value: FONT_FAMILY.Virgil,
- text: t("labels.handDrawn"),
- icon: <FontFamilyHandDrawnIcon theme={appState.theme} />,
- },
- {
- value: FONT_FAMILY.Helvetica,
- text: t("labels.normal"),
- icon: <FontFamilyNormalIcon theme={appState.theme} />,
- },
- {
- value: FONT_FAMILY.Cascadia,
- text: t("labels.code"),
- icon: <FontFamilyCodeIcon theme={appState.theme} />,
- },
- ];
- return (
- <fieldset>
- <legend>{t("labels.fontFamily")}</legend>
- <ButtonIconSelect<FontFamilyValues | false>
- group="font-family"
- options={options}
- value={getFormValue(
- elements,
- appState,
- (element) => {
- if (isTextElement(element)) {
- return element.fontFamily;
- }
- const boundTextElement = getBoundTextElement(element);
- if (boundTextElement) {
- return boundTextElement.fontFamily;
- }
- return null;
- },
- appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
- )}
- onChange={(value) => updateData(value)}
- />
- </fieldset>
- );
- },
- });
- export const actionChangeTextAlign = register({
- name: "changeTextAlign",
- trackEvent: false,
- perform: (elements, appState, value) => {
- return {
- elements: changeProperty(
- elements,
- appState,
- (oldElement) => {
- if (isTextElement(oldElement)) {
- const newElement: ExcalidrawTextElement = newElementWith(
- oldElement,
- { textAlign: value },
- );
- redrawTextBoundingBox(newElement, getContainerElement(oldElement));
- return newElement;
- }
- return oldElement;
- },
- true,
- ),
- appState: {
- ...appState,
- currentItemTextAlign: value,
- },
- commitToHistory: true,
- };
- },
- PanelComponent: ({ elements, appState, updateData }) => {
- return (
- <fieldset>
- <legend>{t("labels.textAlign")}</legend>
- <ButtonIconSelect<TextAlign | false>
- group="text-align"
- options={[
- {
- value: "left",
- text: t("labels.left"),
- icon: <TextAlignLeftIcon theme={appState.theme} />,
- },
- {
- value: "center",
- text: t("labels.center"),
- icon: <TextAlignCenterIcon theme={appState.theme} />,
- },
- {
- value: "right",
- text: t("labels.right"),
- icon: <TextAlignRightIcon theme={appState.theme} />,
- },
- ]}
- value={getFormValue(
- elements,
- appState,
- (element) => {
- if (isTextElement(element)) {
- return element.textAlign;
- }
- const boundTextElement = getBoundTextElement(element);
- if (boundTextElement) {
- return boundTextElement.textAlign;
- }
- return null;
- },
- appState.currentItemTextAlign,
- )}
- onChange={(value) => updateData(value)}
- />
- </fieldset>
- );
- },
- });
- export const actionChangeVerticalAlign = register({
- name: "changeVerticalAlign",
- trackEvent: { category: "element" },
- perform: (elements, appState, value) => {
- return {
- elements: changeProperty(
- elements,
- appState,
- (oldElement) => {
- if (isTextElement(oldElement)) {
- const newElement: ExcalidrawTextElement = newElementWith(
- oldElement,
- { verticalAlign: value },
- );
- redrawTextBoundingBox(newElement, getContainerElement(oldElement));
- return newElement;
- }
- return oldElement;
- },
- true,
- ),
- appState: {
- ...appState,
- },
- commitToHistory: true,
- };
- },
- PanelComponent: ({ elements, appState, updateData }) => {
- return (
- <fieldset>
- <ButtonIconSelect<VerticalAlign | false>
- group="text-align"
- options={[
- {
- value: VERTICAL_ALIGN.TOP,
- text: t("labels.alignTop"),
- icon: <TextAlignTopIcon theme={appState.theme} />,
- },
- {
- value: VERTICAL_ALIGN.MIDDLE,
- text: t("labels.centerVertically"),
- icon: <TextAlignMiddleIcon theme={appState.theme} />,
- },
- {
- value: VERTICAL_ALIGN.BOTTOM,
- text: t("labels.alignBottom"),
- icon: <TextAlignBottomIcon theme={appState.theme} />,
- },
- ]}
- value={getFormValue(elements, appState, (element) => {
- if (isTextElement(element) && element.containerId) {
- return element.verticalAlign;
- }
- const boundTextElement = getBoundTextElement(element);
- if (boundTextElement) {
- return boundTextElement.verticalAlign;
- }
- return null;
- })}
- onChange={(value) => updateData(value)}
- />
- </fieldset>
- );
- },
- });
- export const actionChangeSharpness = register({
- name: "changeSharpness",
- trackEvent: false,
- perform: (elements, appState, value) => {
- const targetElements = getTargetElements(
- getNonDeletedElements(elements),
- appState,
- );
- const shouldUpdateForNonLinearElements = targetElements.length
- ? targetElements.every((el) => !isLinearElement(el))
- : !isLinearElementType(appState.activeTool.type);
- const shouldUpdateForLinearElements = targetElements.length
- ? targetElements.every(isLinearElement)
- : isLinearElementType(appState.activeTool.type);
- return {
- elements: changeProperty(elements, appState, (el) =>
- newElementWith(el, {
- strokeSharpness: value,
- }),
- ),
- appState: {
- ...appState,
- currentItemStrokeSharpness: shouldUpdateForNonLinearElements
- ? value
- : appState.currentItemStrokeSharpness,
- currentItemLinearStrokeSharpness: shouldUpdateForLinearElements
- ? value
- : appState.currentItemLinearStrokeSharpness,
- },
- commitToHistory: true,
- };
- },
- PanelComponent: ({ elements, appState, updateData }) => (
- <fieldset>
- <legend>{t("labels.edges")}</legend>
- <ButtonIconSelect
- group="edges"
- options={[
- {
- value: "sharp",
- text: t("labels.sharp"),
- icon: <EdgeSharpIcon theme={appState.theme} />,
- },
- {
- value: "round",
- text: t("labels.round"),
- icon: <EdgeRoundIcon theme={appState.theme} />,
- },
- ]}
- value={getFormValue(
- elements,
- appState,
- (element) => element.strokeSharpness,
- (canChangeSharpness(appState.activeTool.type) &&
- (isLinearElementType(appState.activeTool.type)
- ? appState.currentItemLinearStrokeSharpness
- : appState.currentItemStrokeSharpness)) ||
- null,
- )}
- onChange={(value) => updateData(value)}
- />
- </fieldset>
- ),
- });
- export const actionChangeArrowhead = register({
- name: "changeArrowhead",
- trackEvent: false,
- perform: (
- elements,
- appState,
- value: { position: "start" | "end"; type: Arrowhead },
- ) => {
- return {
- elements: changeProperty(elements, appState, (el) => {
- if (isLinearElement(el)) {
- const { position, type } = value;
- if (position === "start") {
- const element: ExcalidrawLinearElement = newElementWith(el, {
- startArrowhead: type,
- });
- return element;
- } else if (position === "end") {
- const element: ExcalidrawLinearElement = newElementWith(el, {
- endArrowhead: type,
- });
- return element;
- }
- }
- return el;
- }),
- appState: {
- ...appState,
- [value.position === "start"
- ? "currentItemStartArrowhead"
- : "currentItemEndArrowhead"]: value.type,
- },
- commitToHistory: true,
- };
- },
- PanelComponent: ({ elements, appState, updateData }) => {
- const isRTL = getLanguage().rtl;
- return (
- <fieldset>
- <legend>{t("labels.arrowheads")}</legend>
- <div className="iconSelectList">
- <IconPicker
- label="arrowhead_start"
- options={[
- {
- value: null,
- text: t("labels.arrowhead_none"),
- icon: <ArrowheadNoneIcon theme={appState.theme} />,
- keyBinding: "q",
- },
- {
- value: "arrow",
- text: t("labels.arrowhead_arrow"),
- icon: (
- <ArrowheadArrowIcon theme={appState.theme} flip={!isRTL} />
- ),
- keyBinding: "w",
- },
- {
- value: "bar",
- text: t("labels.arrowhead_bar"),
- icon: <ArrowheadBarIcon theme={appState.theme} flip={!isRTL} />,
- keyBinding: "e",
- },
- {
- value: "dot",
- text: t("labels.arrowhead_dot"),
- icon: <ArrowheadDotIcon theme={appState.theme} flip={!isRTL} />,
- keyBinding: "r",
- },
- {
- value: "triangle",
- text: t("labels.arrowhead_triangle"),
- icon: (
- <ArrowheadTriangleIcon theme={appState.theme} flip={!isRTL} />
- ),
- keyBinding: "t",
- },
- ]}
- value={getFormValue<Arrowhead | null>(
- elements,
- appState,
- (element) =>
- isLinearElement(element) && canHaveArrowheads(element.type)
- ? element.startArrowhead
- : appState.currentItemStartArrowhead,
- appState.currentItemStartArrowhead,
- )}
- onChange={(value) => updateData({ position: "start", type: value })}
- />
- <IconPicker
- label="arrowhead_end"
- group="arrowheads"
- options={[
- {
- value: null,
- text: t("labels.arrowhead_none"),
- keyBinding: "q",
- icon: <ArrowheadNoneIcon theme={appState.theme} />,
- },
- {
- value: "arrow",
- text: t("labels.arrowhead_arrow"),
- keyBinding: "w",
- icon: (
- <ArrowheadArrowIcon theme={appState.theme} flip={isRTL} />
- ),
- },
- {
- value: "bar",
- text: t("labels.arrowhead_bar"),
- keyBinding: "e",
- icon: <ArrowheadBarIcon theme={appState.theme} flip={isRTL} />,
- },
- {
- value: "dot",
- text: t("labels.arrowhead_dot"),
- keyBinding: "r",
- icon: <ArrowheadDotIcon theme={appState.theme} flip={isRTL} />,
- },
- {
- value: "triangle",
- text: t("labels.arrowhead_triangle"),
- icon: (
- <ArrowheadTriangleIcon theme={appState.theme} flip={isRTL} />
- ),
- keyBinding: "t",
- },
- ]}
- value={getFormValue<Arrowhead | null>(
- elements,
- appState,
- (element) =>
- isLinearElement(element) && canHaveArrowheads(element.type)
- ? element.endArrowhead
- : appState.currentItemEndArrowhead,
- appState.currentItemEndArrowhead,
- )}
- onChange={(value) => updateData({ position: "end", type: value })}
- />
- </div>
- </fieldset>
- );
- },
- });
|