HelpDialog.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. import React from "react";
  2. import { t } from "../i18n";
  3. import { KEYS } from "../keys";
  4. import { Dialog } from "./Dialog";
  5. import { getShortcutKey } from "../utils";
  6. import "./HelpDialog.scss";
  7. import { ExternalLinkIcon } from "./icons";
  8. import { probablySupportsClipboardBlob } from "../clipboard";
  9. import { isDarwin, isFirefox, isWindows } from "../constants";
  10. const Header = () => (
  11. <div className="HelpDialog__header">
  12. <a
  13. className="HelpDialog__btn"
  14. href="https://github.com/excalidraw/excalidraw#documentation"
  15. target="_blank"
  16. rel="noopener noreferrer"
  17. >
  18. {t("helpDialog.documentation")}
  19. <div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
  20. </a>
  21. <a
  22. className="HelpDialog__btn"
  23. href="https://blog.excalidraw.com"
  24. target="_blank"
  25. rel="noopener noreferrer"
  26. >
  27. {t("helpDialog.blog")}
  28. <div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
  29. </a>
  30. <a
  31. className="HelpDialog__btn"
  32. href="https://github.com/excalidraw/excalidraw/issues"
  33. target="_blank"
  34. rel="noopener noreferrer"
  35. >
  36. {t("helpDialog.github")}
  37. <div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
  38. </a>
  39. </div>
  40. );
  41. const Section = (props: { title: string; children: React.ReactNode }) => (
  42. <>
  43. <h3>{props.title}</h3>
  44. <div className="HelpDialog__islands-container">{props.children}</div>
  45. </>
  46. );
  47. const ShortcutIsland = (props: {
  48. caption: string;
  49. children: React.ReactNode;
  50. className?: string;
  51. }) => (
  52. <div className={`HelpDialog__island ${props.className}`}>
  53. <h4 className="HelpDialog__island-title">{props.caption}</h4>
  54. <div className="HelpDialog__island-content">{props.children}</div>
  55. </div>
  56. );
  57. function* intersperse(as: JSX.Element[][], delim: string | null) {
  58. let first = true;
  59. for (const x of as) {
  60. if (!first) {
  61. yield delim;
  62. }
  63. first = false;
  64. yield x;
  65. }
  66. }
  67. const upperCaseSingleChars = (str: string) => {
  68. return str.replace(/\b[a-z]\b/, (c) => c.toUpperCase());
  69. };
  70. const Shortcut = ({
  71. label,
  72. shortcuts,
  73. isOr = true,
  74. }: {
  75. label: string;
  76. shortcuts: string[];
  77. isOr?: boolean;
  78. }) => {
  79. const splitShortcutKeys = shortcuts.map((shortcut) => {
  80. const keys = shortcut.endsWith("++")
  81. ? [...shortcut.slice(0, -2).split("+"), "+"]
  82. : shortcut.split("+");
  83. return keys.map((key) => (
  84. <ShortcutKey key={key}>{upperCaseSingleChars(key)}</ShortcutKey>
  85. ));
  86. });
  87. return (
  88. <div className="HelpDialog__shortcut">
  89. <div>{label}</div>
  90. <div className="HelpDialog__key-container">
  91. {[...intersperse(splitShortcutKeys, isOr ? t("helpDialog.or") : null)]}
  92. </div>
  93. </div>
  94. );
  95. };
  96. const ShortcutKey = (props: { children: React.ReactNode }) => (
  97. <kbd className="HelpDialog__key" {...props} />
  98. );
  99. export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
  100. const handleClose = React.useCallback(() => {
  101. if (onClose) {
  102. onClose();
  103. }
  104. }, [onClose]);
  105. return (
  106. <>
  107. <Dialog
  108. onCloseRequest={handleClose}
  109. title={t("helpDialog.title")}
  110. className={"HelpDialog"}
  111. >
  112. <Header />
  113. <Section title={t("helpDialog.shortcuts")}>
  114. <ShortcutIsland
  115. className="HelpDialog__island--tools"
  116. caption={t("helpDialog.tools")}
  117. >
  118. <Shortcut label={t("toolBar.hand")} shortcuts={[KEYS.H]} />
  119. <Shortcut
  120. label={t("toolBar.selection")}
  121. shortcuts={[KEYS.V, KEYS["1"]]}
  122. />
  123. <Shortcut
  124. label={t("toolBar.rectangle")}
  125. shortcuts={[KEYS.R, KEYS["2"]]}
  126. />
  127. <Shortcut
  128. label={t("toolBar.diamond")}
  129. shortcuts={[KEYS.D, KEYS["3"]]}
  130. />
  131. <Shortcut
  132. label={t("toolBar.ellipse")}
  133. shortcuts={[KEYS.O, KEYS["4"]]}
  134. />
  135. <Shortcut
  136. label={t("toolBar.arrow")}
  137. shortcuts={[KEYS.A, KEYS["5"]]}
  138. />
  139. <Shortcut
  140. label={t("toolBar.line")}
  141. shortcuts={[KEYS.L, KEYS["6"]]}
  142. />
  143. <Shortcut
  144. label={t("toolBar.freedraw")}
  145. shortcuts={[KEYS.P, KEYS["7"]]}
  146. />
  147. <Shortcut
  148. label={t("toolBar.text")}
  149. shortcuts={[KEYS.T, KEYS["8"]]}
  150. />
  151. <Shortcut label={t("toolBar.image")} shortcuts={[KEYS["9"]]} />
  152. <Shortcut
  153. label={t("toolBar.eraser")}
  154. shortcuts={[KEYS.E, KEYS["0"]]}
  155. />
  156. <Shortcut
  157. label={t("labels.eyeDropper")}
  158. shortcuts={[KEYS.I, "Shift+S", "Shift+G"]}
  159. />
  160. <Shortcut
  161. label={t("helpDialog.editLineArrowPoints")}
  162. shortcuts={[getShortcutKey("CtrlOrCmd+Enter")]}
  163. />
  164. <Shortcut
  165. label={t("helpDialog.editText")}
  166. shortcuts={[getShortcutKey("Enter")]}
  167. />
  168. <Shortcut
  169. label={t("helpDialog.textNewLine")}
  170. shortcuts={[
  171. getShortcutKey("Enter"),
  172. getShortcutKey("Shift+Enter"),
  173. ]}
  174. />
  175. <Shortcut
  176. label={t("helpDialog.textFinish")}
  177. shortcuts={[
  178. getShortcutKey("Esc"),
  179. getShortcutKey("CtrlOrCmd+Enter"),
  180. ]}
  181. />
  182. <Shortcut
  183. label={t("helpDialog.curvedArrow")}
  184. shortcuts={[
  185. "A",
  186. t("helpDialog.click"),
  187. t("helpDialog.click"),
  188. t("helpDialog.click"),
  189. ]}
  190. isOr={false}
  191. />
  192. <Shortcut
  193. label={t("helpDialog.curvedLine")}
  194. shortcuts={[
  195. "L",
  196. t("helpDialog.click"),
  197. t("helpDialog.click"),
  198. t("helpDialog.click"),
  199. ]}
  200. isOr={false}
  201. />
  202. <Shortcut label={t("toolBar.lock")} shortcuts={[KEYS.Q]} />
  203. <Shortcut
  204. label={t("helpDialog.preventBinding")}
  205. shortcuts={[getShortcutKey("CtrlOrCmd")]}
  206. />
  207. <Shortcut
  208. label={t("toolBar.link")}
  209. shortcuts={[getShortcutKey("CtrlOrCmd+K")]}
  210. />
  211. </ShortcutIsland>
  212. <ShortcutIsland
  213. className="HelpDialog__island--view"
  214. caption={t("helpDialog.view")}
  215. >
  216. <Shortcut
  217. label={t("buttons.zoomIn")}
  218. shortcuts={[getShortcutKey("CtrlOrCmd++")]}
  219. />
  220. <Shortcut
  221. label={t("buttons.zoomOut")}
  222. shortcuts={[getShortcutKey("CtrlOrCmd+-")]}
  223. />
  224. <Shortcut
  225. label={t("buttons.resetZoom")}
  226. shortcuts={[getShortcutKey("CtrlOrCmd+0")]}
  227. />
  228. <Shortcut
  229. label={t("helpDialog.zoomToFit")}
  230. shortcuts={["Shift+1"]}
  231. />
  232. <Shortcut
  233. label={t("helpDialog.zoomToSelection")}
  234. shortcuts={["Shift+2"]}
  235. />
  236. <Shortcut
  237. label={t("helpDialog.movePageUpDown")}
  238. shortcuts={["PgUp/PgDn"]}
  239. />
  240. <Shortcut
  241. label={t("helpDialog.movePageLeftRight")}
  242. shortcuts={["Shift+PgUp/PgDn"]}
  243. />
  244. <Shortcut label={t("buttons.fullScreen")} shortcuts={["F"]} />
  245. <Shortcut
  246. label={t("buttons.zenMode")}
  247. shortcuts={[getShortcutKey("Alt+Z")]}
  248. />
  249. <Shortcut
  250. label={t("labels.showGrid")}
  251. shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
  252. />
  253. <Shortcut
  254. label={t("labels.viewMode")}
  255. shortcuts={[getShortcutKey("Alt+R")]}
  256. />
  257. <Shortcut
  258. label={t("labels.toggleTheme")}
  259. shortcuts={[getShortcutKey("Alt+Shift+D")]}
  260. />
  261. <Shortcut
  262. label={t("stats.title")}
  263. shortcuts={[getShortcutKey("Alt+/")]}
  264. />
  265. </ShortcutIsland>
  266. <ShortcutIsland
  267. className="HelpDialog__island--editor"
  268. caption={t("helpDialog.editor")}
  269. >
  270. <Shortcut
  271. label={t("labels.moveCanvas")}
  272. shortcuts={[
  273. getShortcutKey(`Space+${t("helpDialog.drag")}`),
  274. getShortcutKey(`Wheel+${t("helpDialog.drag")}`),
  275. ]}
  276. isOr={true}
  277. />
  278. <Shortcut
  279. label={t("buttons.clearReset")}
  280. shortcuts={[getShortcutKey("CtrlOrCmd+Delete")]}
  281. />
  282. <Shortcut
  283. label={t("labels.delete")}
  284. shortcuts={[getShortcutKey("Delete")]}
  285. />
  286. <Shortcut
  287. label={t("labels.cut")}
  288. shortcuts={[getShortcutKey("CtrlOrCmd+X")]}
  289. />
  290. <Shortcut
  291. label={t("labels.copy")}
  292. shortcuts={[getShortcutKey("CtrlOrCmd+C")]}
  293. />
  294. <Shortcut
  295. label={t("labels.paste")}
  296. shortcuts={[getShortcutKey("CtrlOrCmd+V")]}
  297. />
  298. <Shortcut
  299. label={t("labels.pasteAsPlaintext")}
  300. shortcuts={[getShortcutKey("CtrlOrCmd+Shift+V")]}
  301. />
  302. <Shortcut
  303. label={t("labels.selectAll")}
  304. shortcuts={[getShortcutKey("CtrlOrCmd+A")]}
  305. />
  306. <Shortcut
  307. label={t("labels.multiSelect")}
  308. shortcuts={[getShortcutKey(`Shift+${t("helpDialog.click")}`)]}
  309. />
  310. <Shortcut
  311. label={t("helpDialog.deepSelect")}
  312. shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`)]}
  313. />
  314. <Shortcut
  315. label={t("helpDialog.deepBoxSelect")}
  316. shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`)]}
  317. />
  318. {/* firefox supports clipboard API under a flag, so we'll
  319. show users what they can do in the error message */}
  320. {(probablySupportsClipboardBlob || isFirefox) && (
  321. <Shortcut
  322. label={t("labels.copyAsPng")}
  323. shortcuts={[getShortcutKey("Shift+Alt+C")]}
  324. />
  325. )}
  326. <Shortcut
  327. label={t("labels.copyStyles")}
  328. shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]}
  329. />
  330. <Shortcut
  331. label={t("labels.pasteStyles")}
  332. shortcuts={[getShortcutKey("CtrlOrCmd+Alt+V")]}
  333. />
  334. <Shortcut
  335. label={t("labels.sendToBack")}
  336. shortcuts={[
  337. isDarwin
  338. ? getShortcutKey("CtrlOrCmd+Alt+[")
  339. : getShortcutKey("CtrlOrCmd+Shift+["),
  340. ]}
  341. />
  342. <Shortcut
  343. label={t("labels.bringToFront")}
  344. shortcuts={[
  345. isDarwin
  346. ? getShortcutKey("CtrlOrCmd+Alt+]")
  347. : getShortcutKey("CtrlOrCmd+Shift+]"),
  348. ]}
  349. />
  350. <Shortcut
  351. label={t("labels.sendBackward")}
  352. shortcuts={[getShortcutKey("CtrlOrCmd+[")]}
  353. />
  354. <Shortcut
  355. label={t("labels.bringForward")}
  356. shortcuts={[getShortcutKey("CtrlOrCmd+]")]}
  357. />
  358. <Shortcut
  359. label={t("labels.alignTop")}
  360. shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Up")]}
  361. />
  362. <Shortcut
  363. label={t("labels.alignBottom")}
  364. shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Down")]}
  365. />
  366. <Shortcut
  367. label={t("labels.alignLeft")}
  368. shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Left")]}
  369. />
  370. <Shortcut
  371. label={t("labels.alignRight")}
  372. shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Right")]}
  373. />
  374. <Shortcut
  375. label={t("labels.duplicateSelection")}
  376. shortcuts={[
  377. getShortcutKey("CtrlOrCmd+D"),
  378. getShortcutKey(`Alt+${t("helpDialog.drag")}`),
  379. ]}
  380. />
  381. <Shortcut
  382. label={t("helpDialog.toggleElementLock")}
  383. shortcuts={[getShortcutKey("CtrlOrCmd+Shift+L")]}
  384. />
  385. <Shortcut
  386. label={t("buttons.undo")}
  387. shortcuts={[getShortcutKey("CtrlOrCmd+Z")]}
  388. />
  389. <Shortcut
  390. label={t("buttons.redo")}
  391. shortcuts={
  392. isWindows
  393. ? [
  394. getShortcutKey("CtrlOrCmd+Y"),
  395. getShortcutKey("CtrlOrCmd+Shift+Z"),
  396. ]
  397. : [getShortcutKey("CtrlOrCmd+Shift+Z")]
  398. }
  399. />
  400. <Shortcut
  401. label={t("labels.group")}
  402. shortcuts={[getShortcutKey("CtrlOrCmd+G")]}
  403. />
  404. <Shortcut
  405. label={t("labels.ungroup")}
  406. shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]}
  407. />
  408. <Shortcut
  409. label={t("labels.flipHorizontal")}
  410. shortcuts={[getShortcutKey("Shift+H")]}
  411. />
  412. <Shortcut
  413. label={t("labels.flipVertical")}
  414. shortcuts={[getShortcutKey("Shift+V")]}
  415. />
  416. <Shortcut
  417. label={t("labels.showStroke")}
  418. shortcuts={[getShortcutKey("S")]}
  419. />
  420. <Shortcut
  421. label={t("labels.showBackground")}
  422. shortcuts={[getShortcutKey("G")]}
  423. />
  424. <Shortcut
  425. label={t("labels.decreaseFontSize")}
  426. shortcuts={[getShortcutKey("CtrlOrCmd+Shift+<")]}
  427. />
  428. <Shortcut
  429. label={t("labels.increaseFontSize")}
  430. shortcuts={[getShortcutKey("CtrlOrCmd+Shift+>")]}
  431. />
  432. </ShortcutIsland>
  433. </Section>
  434. </Dialog>
  435. </>
  436. );
  437. };