actionProperties.tsx 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. import React from "react";
  2. import { Action } from "./types";
  3. import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
  4. import { getCommonAttributeOfSelectedElements } from "../scene";
  5. import { ButtonSelect } from "../components/ButtonSelect";
  6. import { isTextElement, redrawTextBoundingBox } from "../element";
  7. import { ColorPicker } from "../components/ColorPicker";
  8. import { AppState } from "../../src/types";
  9. import { t } from "../i18n";
  10. const changeProperty = (
  11. elements: readonly ExcalidrawElement[],
  12. callback: (element: ExcalidrawElement) => ExcalidrawElement,
  13. ) => {
  14. return elements.map(element => {
  15. if (element.isSelected) {
  16. return callback(element);
  17. }
  18. return element;
  19. });
  20. };
  21. const getFormValue = function<T>(
  22. editingElement: AppState["editingElement"],
  23. elements: readonly ExcalidrawElement[],
  24. getAttribute: (element: ExcalidrawElement) => T,
  25. defaultValue?: T,
  26. ): T | null {
  27. return (
  28. (editingElement && getAttribute(editingElement)) ??
  29. (elements.some(element => element.isSelected)
  30. ? getCommonAttributeOfSelectedElements(elements, getAttribute)
  31. : defaultValue) ??
  32. null
  33. );
  34. };
  35. export const actionChangeStrokeColor: Action = {
  36. name: "changeStrokeColor",
  37. perform: (elements, appState, value) => {
  38. return {
  39. elements: changeProperty(elements, el => ({
  40. ...el,
  41. shape: null,
  42. strokeColor: value,
  43. })),
  44. appState: { ...appState, currentItemStrokeColor: value },
  45. };
  46. },
  47. commitToHistory: () => true,
  48. PanelComponent: ({ elements, appState, updateData }) => (
  49. <>
  50. <h3 aria-hidden="true">{t("labels.stroke")}</h3>
  51. <ColorPicker
  52. type="elementStroke"
  53. label={t("labels.stroke")}
  54. color={getFormValue(
  55. appState.editingElement,
  56. elements,
  57. element => element.strokeColor,
  58. appState.currentItemStrokeColor,
  59. )}
  60. onChange={updateData}
  61. />
  62. </>
  63. ),
  64. };
  65. export const actionChangeBackgroundColor: Action = {
  66. name: "changeBackgroundColor",
  67. perform: (elements, appState, value) => {
  68. return {
  69. elements: changeProperty(elements, el => ({
  70. ...el,
  71. shape: null,
  72. backgroundColor: value,
  73. })),
  74. appState: { ...appState, currentItemBackgroundColor: value },
  75. };
  76. },
  77. commitToHistory: () => true,
  78. PanelComponent: ({ elements, appState, updateData }) => (
  79. <>
  80. <h3 aria-hidden="true">{t("labels.background")}</h3>
  81. <ColorPicker
  82. type="elementBackground"
  83. label={t("labels.background")}
  84. color={getFormValue(
  85. appState.editingElement,
  86. elements,
  87. element => element.backgroundColor,
  88. appState.currentItemBackgroundColor,
  89. )}
  90. onChange={updateData}
  91. />
  92. </>
  93. ),
  94. };
  95. export const actionChangeFillStyle: Action = {
  96. name: "changeFillStyle",
  97. perform: (elements, appState, value) => {
  98. return {
  99. elements: changeProperty(elements, el => ({
  100. ...el,
  101. shape: null,
  102. fillStyle: value,
  103. })),
  104. appState: { ...appState, currentItemFillStyle: value },
  105. };
  106. },
  107. commitToHistory: () => true,
  108. PanelComponent: ({ elements, appState, updateData }) => (
  109. <fieldset>
  110. <legend>{t("labels.fill")}</legend>
  111. <ButtonSelect
  112. options={[
  113. { value: "solid", text: t("labels.solid") },
  114. { value: "hachure", text: t("labels.hachure") },
  115. { value: "cross-hatch", text: t("labels.crossHatch") },
  116. ]}
  117. group="fill"
  118. value={getFormValue(
  119. appState.editingElement,
  120. elements,
  121. element => element.fillStyle,
  122. appState.currentItemFillStyle,
  123. )}
  124. onChange={value => {
  125. updateData(value);
  126. }}
  127. />
  128. </fieldset>
  129. ),
  130. };
  131. export const actionChangeStrokeWidth: Action = {
  132. name: "changeStrokeWidth",
  133. perform: (elements, appState, value) => {
  134. return {
  135. elements: changeProperty(elements, el => ({
  136. ...el,
  137. shape: null,
  138. strokeWidth: value,
  139. })),
  140. appState: { ...appState, currentItemStrokeWidth: value },
  141. };
  142. },
  143. commitToHistory: () => true,
  144. PanelComponent: ({ elements, appState, updateData }) => (
  145. <fieldset>
  146. <legend>{t("labels.strokeWidth")}</legend>
  147. <ButtonSelect
  148. group="stroke-width"
  149. options={[
  150. { value: 1, text: t("labels.thin") },
  151. { value: 2, text: t("labels.bold") },
  152. { value: 4, text: t("labels.extraBold") },
  153. ]}
  154. value={getFormValue(
  155. appState.editingElement,
  156. elements,
  157. element => element.strokeWidth,
  158. appState.currentItemStrokeWidth,
  159. )}
  160. onChange={value => updateData(value)}
  161. />
  162. </fieldset>
  163. ),
  164. };
  165. export const actionChangeSloppiness: Action = {
  166. name: "changeSloppiness",
  167. perform: (elements, appState, value) => {
  168. return {
  169. elements: changeProperty(elements, el => ({
  170. ...el,
  171. shape: null,
  172. roughness: value,
  173. })),
  174. appState: { ...appState, currentItemRoughness: value },
  175. };
  176. },
  177. commitToHistory: () => true,
  178. PanelComponent: ({ elements, appState, updateData }) => (
  179. <fieldset>
  180. <legend>{t("labels.sloppiness")}</legend>
  181. <ButtonSelect
  182. group="sloppiness"
  183. options={[
  184. { value: 0, text: t("labels.architect") },
  185. { value: 1, text: t("labels.artist") },
  186. { value: 2, text: t("labels.cartoonist") },
  187. ]}
  188. value={getFormValue(
  189. appState.editingElement,
  190. elements,
  191. element => element.roughness,
  192. appState.currentItemRoughness,
  193. )}
  194. onChange={value => updateData(value)}
  195. />
  196. </fieldset>
  197. ),
  198. };
  199. export const actionChangeOpacity: Action = {
  200. name: "changeOpacity",
  201. perform: (elements, appState, value) => {
  202. return {
  203. elements: changeProperty(elements, el => ({
  204. ...el,
  205. shape: null,
  206. opacity: value,
  207. })),
  208. appState: { ...appState, currentItemOpacity: value },
  209. };
  210. },
  211. commitToHistory: () => true,
  212. PanelComponent: ({ elements, appState, updateData }) => (
  213. <label className="control-label">
  214. {t("labels.opacity")}
  215. <input
  216. type="range"
  217. min="0"
  218. max="100"
  219. step="10"
  220. onChange={e => updateData(+e.target.value)}
  221. onWheel={e => {
  222. e.stopPropagation();
  223. const target = e.target as HTMLInputElement;
  224. const STEP = 10;
  225. const MAX = 100;
  226. const MIN = 0;
  227. const value = +target.value;
  228. if (e.deltaY < 0 && value < MAX) {
  229. updateData(value + STEP);
  230. } else if (e.deltaY > 0 && value > MIN) {
  231. updateData(value - STEP);
  232. }
  233. }}
  234. value={
  235. getFormValue(
  236. appState.editingElement,
  237. elements,
  238. element => element.opacity,
  239. appState.currentItemOpacity,
  240. ) ?? undefined
  241. }
  242. />
  243. </label>
  244. ),
  245. };
  246. export const actionChangeFontSize: Action = {
  247. name: "changeFontSize",
  248. perform: (elements, appState, value) => {
  249. return {
  250. elements: changeProperty(elements, el => {
  251. if (isTextElement(el)) {
  252. const element: ExcalidrawTextElement = {
  253. ...el,
  254. shape: null,
  255. font: `${value}px ${el.font.split("px ")[1]}`,
  256. };
  257. redrawTextBoundingBox(element);
  258. return element;
  259. }
  260. return el;
  261. }),
  262. appState: {
  263. ...appState,
  264. currentItemFont: `${value}px ${
  265. appState.currentItemFont.split("px ")[1]
  266. }`,
  267. },
  268. };
  269. },
  270. commitToHistory: () => true,
  271. PanelComponent: ({ elements, appState, updateData }) => (
  272. <fieldset>
  273. <legend>{t("labels.fontSize")}</legend>
  274. <ButtonSelect
  275. group="font-size"
  276. options={[
  277. { value: 16, text: t("labels.small") },
  278. { value: 20, text: t("labels.medium") },
  279. { value: 28, text: t("labels.large") },
  280. { value: 36, text: t("labels.veryLarge") },
  281. ]}
  282. value={getFormValue(
  283. appState.editingElement,
  284. elements,
  285. element => isTextElement(element) && +element.font.split("px ")[0],
  286. +(appState.currentItemFont || "20px Virgil").split("px ")[0],
  287. )}
  288. onChange={value => updateData(value)}
  289. />
  290. </fieldset>
  291. ),
  292. };
  293. export const actionChangeFontFamily: Action = {
  294. name: "changeFontFamily",
  295. perform: (elements, appState, value) => {
  296. return {
  297. elements: changeProperty(elements, el => {
  298. if (isTextElement(el)) {
  299. const element: ExcalidrawTextElement = {
  300. ...el,
  301. shape: null,
  302. font: `${el.font.split("px ")[0]}px ${value}`,
  303. };
  304. redrawTextBoundingBox(element);
  305. return element;
  306. }
  307. return el;
  308. }),
  309. appState: {
  310. ...appState,
  311. currentItemFont: `${
  312. appState.currentItemFont.split("px ")[0]
  313. }px ${value}`,
  314. },
  315. };
  316. },
  317. commitToHistory: () => true,
  318. PanelComponent: ({ elements, appState, updateData }) => (
  319. <fieldset>
  320. <legend>{t("labels.fontFamily")}</legend>
  321. <ButtonSelect
  322. group="font-family"
  323. options={[
  324. { value: "Virgil", text: t("labels.handDrawn") },
  325. { value: "Helvetica", text: t("labels.normal") },
  326. { value: "Cascadia", text: t("labels.code") },
  327. ]}
  328. value={getFormValue(
  329. appState.editingElement,
  330. elements,
  331. element => isTextElement(element) && element.font.split("px ")[1],
  332. (appState.currentItemFont || "20px Virgil").split("px ")[1],
  333. )}
  334. onChange={value => updateData(value)}
  335. />
  336. </fieldset>
  337. ),
  338. };