From 6ec259a0e71174651bae95d4628138bf6fd68742 Mon Sep 17 00:00:00 2001 From: kj_sh604 Date: Sun, 15 Mar 2026 16:19:35 -0400 Subject: refactor: packages/ --- packages/excalidraw/components/Actions.scss | 93 + packages/excalidraw/components/Actions.tsx | 478 + .../excalidraw/components/ActiveConfirmDialog.tsx | 35 + packages/excalidraw/components/App.tsx | 11180 +++++++++++++++++++ packages/excalidraw/components/Avatar.scss | 7 + packages/excalidraw/components/Avatar.tsx | 41 + .../components/BraveMeasureTextError.tsx | 43 + packages/excalidraw/components/Button.scss | 7 + packages/excalidraw/components/Button.tsx | 44 + packages/excalidraw/components/ButtonIcon.scss | 12 + packages/excalidraw/components/ButtonIcon.tsx | 37 + packages/excalidraw/components/ButtonIconCycle.tsx | 29 + .../excalidraw/components/ButtonIconSelect.tsx | 58 + packages/excalidraw/components/ButtonSelect.tsx | 30 + packages/excalidraw/components/ButtonSeparator.tsx | 10 + packages/excalidraw/components/Card.scss | 57 + packages/excalidraw/components/Card.tsx | 28 + packages/excalidraw/components/CheckboxItem.scss | 91 + packages/excalidraw/components/CheckboxItem.tsx | 36 + .../components/ColorPicker/ColorInput.tsx | 130 + .../components/ColorPicker/ColorPicker.scss | 441 + .../components/ColorPicker/ColorPicker.tsx | 246 + .../components/ColorPicker/CustomColorList.tsx | 63 + .../components/ColorPicker/HotkeyLabel.tsx | 29 + .../excalidraw/components/ColorPicker/Picker.tsx | 178 + .../components/ColorPicker/PickerColorList.tsx | 91 + .../components/ColorPicker/PickerHeading.tsx | 7 + .../components/ColorPicker/ShadeList.tsx | 105 + .../excalidraw/components/ColorPicker/TopPicks.tsx | 65 + .../components/ColorPicker/colorPickerUtils.ts | 133 + .../components/ColorPicker/keyboardNavHandlers.ts | 286 + .../components/CommandPalette/CommandPalette.scss | 137 + .../components/CommandPalette/CommandPalette.tsx | 956 ++ .../CommandPalette/defaultCommandPaletteItems.ts | 11 + .../excalidraw/components/CommandPalette/types.ts | 26 + packages/excalidraw/components/ConfirmDialog.scss | 11 + packages/excalidraw/components/ConfirmDialog.tsx | 78 + packages/excalidraw/components/ContextMenu.scss | 98 + packages/excalidraw/components/ContextMenu.tsx | 128 + packages/excalidraw/components/DarkModeToggle.tsx | 52 + .../excalidraw/components/DefaultSidebar.test.tsx | 144 + packages/excalidraw/components/DefaultSidebar.tsx | 121 + .../DiagramToCodePlugin/DiagramToCodePlugin.tsx | 17 + packages/excalidraw/components/Dialog.scss | 54 + packages/excalidraw/components/Dialog.tsx | 134 + .../excalidraw/components/DialogActionButton.scss | 47 + .../excalidraw/components/DialogActionButton.tsx | 46 + .../excalidraw/components/ElementLinkDialog.scss | 87 + .../excalidraw/components/ElementLinkDialog.tsx | 174 + packages/excalidraw/components/ErrorDialog.tsx | 40 + packages/excalidraw/components/ExcalidrawLogo.scss | 73 + packages/excalidraw/components/ExcalidrawLogo.tsx | 69 + packages/excalidraw/components/ExportDialog.scss | 129 + packages/excalidraw/components/EyeDropper.scss | 48 + packages/excalidraw/components/EyeDropper.tsx | 235 + packages/excalidraw/components/FilledButton.scss | 317 + packages/excalidraw/components/FilledButton.tsx | 114 + .../excalidraw/components/FixedSideContainer.scss | 39 + .../excalidraw/components/FixedSideContainer.tsx | 26 + .../components/FollowMode/FollowMode.scss | 59 + .../components/FollowMode/FollowMode.tsx | 42 + .../components/FontPicker/FontPicker.scss | 15 + .../components/FontPicker/FontPicker.tsx | 110 + .../components/FontPicker/FontPickerList.tsx | 272 + .../components/FontPicker/FontPickerTrigger.tsx | 38 + .../components/FontPicker/keyboardNavHandlers.ts | 66 + packages/excalidraw/components/HandButton.tsx | 32 + packages/excalidraw/components/HelpButton.tsx | 20 + packages/excalidraw/components/HelpDialog.scss | 130 + packages/excalidraw/components/HelpDialog.tsx | 503 + packages/excalidraw/components/HintViewer.scss | 38 + packages/excalidraw/components/HintViewer.tsx | 194 + packages/excalidraw/components/IconPicker.scss | 109 + packages/excalidraw/components/IconPicker.tsx | 239 + .../excalidraw/components/ImageExportDialog.scss | 175 + .../excalidraw/components/ImageExportDialog.tsx | 407 + packages/excalidraw/components/InitializeApp.tsx | 28 + packages/excalidraw/components/InlineIcon.tsx | 15 + packages/excalidraw/components/Island.scss | 16 + packages/excalidraw/components/Island.tsx | 23 + .../excalidraw/components/JSONExportDialog.tsx | 136 + .../excalidraw/components/LaserPointerButton.tsx | 41 + packages/excalidraw/components/LayerUI.scss | 119 + packages/excalidraw/components/LayerUI.tsx | 607 + packages/excalidraw/components/LibraryMenu.scss | 150 + packages/excalidraw/components/LibraryMenu.tsx | 290 + .../components/LibraryMenuBrowseButton.tsx | 31 + .../components/LibraryMenuControlButtons.tsx | 33 + .../components/LibraryMenuHeaderContent.tsx | 321 + .../excalidraw/components/LibraryMenuItems.scss | 99 + .../excalidraw/components/LibraryMenuItems.tsx | 342 + .../excalidraw/components/LibraryMenuSection.tsx | 78 + packages/excalidraw/components/LibraryUnit.scss | 185 + packages/excalidraw/components/LibraryUnit.tsx | 108 + packages/excalidraw/components/LoadingMessage.tsx | 40 + packages/excalidraw/components/LockButton.tsx | 48 + packages/excalidraw/components/MagicButton.tsx | 39 + packages/excalidraw/components/MobileMenu.tsx | 211 + packages/excalidraw/components/Modal.scss | 136 + packages/excalidraw/components/Modal.tsx | 65 + .../OverwriteConfirm/OverwriteConfirm.scss | 126 + .../OverwriteConfirm/OverwriteConfirm.tsx | 74 + .../OverwriteConfirm/OverwriteConfirmActions.tsx | 85 + .../OverwriteConfirm/OverwriteConfirmState.ts | 45 + packages/excalidraw/components/Paragraph.tsx | 10 + .../excalidraw/components/PasteChartDialog.scss | 46 + .../excalidraw/components/PasteChartDialog.tsx | 136 + packages/excalidraw/components/PenModeButton.tsx | 46 + packages/excalidraw/components/Popover.scss | 8 + packages/excalidraw/components/Popover.tsx | 152 + packages/excalidraw/components/ProjectName.scss | 25 + packages/excalidraw/components/ProjectName.tsx | 57 + .../excalidraw/components/PropertiesPopover.tsx | 96 + packages/excalidraw/components/PublishLibrary.scss | 172 + packages/excalidraw/components/PublishLibrary.tsx | 540 + packages/excalidraw/components/QuickSearch.scss | 48 + packages/excalidraw/components/QuickSearch.tsx | 28 + packages/excalidraw/components/RadioGroup.scss | 91 + packages/excalidraw/components/RadioGroup.tsx | 45 + packages/excalidraw/components/Range.scss | 56 + packages/excalidraw/components/Range.tsx | 65 + packages/excalidraw/components/SVGLayer.scss | 24 + packages/excalidraw/components/SVGLayer.tsx | 33 + packages/excalidraw/components/ScrollableList.scss | 21 + packages/excalidraw/components/ScrollableList.tsx | 24 + packages/excalidraw/components/SearchMenu.scss | 110 + packages/excalidraw/components/SearchMenu.tsx | 713 ++ packages/excalidraw/components/Section.tsx | 28 + .../excalidraw/components/ShareableLinkDialog.scss | 91 + .../excalidraw/components/ShareableLinkDialog.tsx | 80 + .../excalidraw/components/Sidebar/Sidebar.scss | 176 + .../excalidraw/components/Sidebar/Sidebar.test.tsx | 393 + packages/excalidraw/components/Sidebar/Sidebar.tsx | 213 + .../components/Sidebar/SidebarHeader.tsx | 57 + .../excalidraw/components/Sidebar/SidebarTab.tsx | 18 + .../components/Sidebar/SidebarTabTrigger.tsx | 26 + .../components/Sidebar/SidebarTabTriggers.tsx | 16 + .../excalidraw/components/Sidebar/SidebarTabs.tsx | 36 + .../components/Sidebar/SidebarTrigger.scss | 38 + .../components/Sidebar/SidebarTrigger.tsx | 45 + packages/excalidraw/components/Sidebar/common.ts | 42 + .../components/Sidebar/siderbar.test.helpers.tsx | 42 + packages/excalidraw/components/Spinner.scss | 49 + packages/excalidraw/components/Spinner.tsx | 43 + packages/excalidraw/components/Stack.scss | 19 + packages/excalidraw/components/Stack.tsx | 62 + packages/excalidraw/components/Stats/Angle.tsx | 95 + .../excalidraw/components/Stats/CanvasGrid.tsx | 67 + .../excalidraw/components/Stats/Collapsible.tsx | 46 + packages/excalidraw/components/Stats/Dimension.tsx | 272 + .../excalidraw/components/Stats/DragInput.scss | 76 + packages/excalidraw/components/Stats/DragInput.tsx | 355 + packages/excalidraw/components/Stats/FontSize.tsx | 99 + .../excalidraw/components/Stats/MultiAngle.tsx | 136 + .../excalidraw/components/Stats/MultiDimension.tsx | 401 + .../excalidraw/components/Stats/MultiFontSize.tsx | 164 + .../excalidraw/components/Stats/MultiPosition.tsx | 270 + packages/excalidraw/components/Stats/Position.tsx | 214 + packages/excalidraw/components/Stats/Stats.scss | 72 + packages/excalidraw/components/Stats/index.tsx | 434 + .../excalidraw/components/Stats/stats.test.tsx | 724 ++ packages/excalidraw/components/Stats/utils.ts | 219 + packages/excalidraw/components/Switch.scss | 118 + packages/excalidraw/components/Switch.tsx | 38 + .../components/TTDDialog/MermaidToExcalidraw.scss | 10 + .../components/TTDDialog/MermaidToExcalidraw.tsx | 132 + .../excalidraw/components/TTDDialog/TTDDialog.scss | 315 + .../excalidraw/components/TTDDialog/TTDDialog.tsx | 394 + .../components/TTDDialog/TTDDialogInput.tsx | 53 + .../components/TTDDialog/TTDDialogOutput.tsx | 39 + .../components/TTDDialog/TTDDialogPanel.tsx | 63 + .../components/TTDDialog/TTDDialogPanels.tsx | 5 + .../TTDDialog/TTDDialogSubmitShortcut.tsx | 14 + .../components/TTDDialog/TTDDialogTab.tsx | 17 + .../components/TTDDialog/TTDDialogTabTrigger.tsx | 21 + .../components/TTDDialog/TTDDialogTabTriggers.tsx | 13 + .../components/TTDDialog/TTDDialogTabs.tsx | 55 + .../components/TTDDialog/TTDDialogTrigger.tsx | 35 + packages/excalidraw/components/TTDDialog/common.ts | 161 + packages/excalidraw/components/TextField.scss | 123 + packages/excalidraw/components/TextField.tsx | 112 + packages/excalidraw/components/TextInput.scss | 7 + packages/excalidraw/components/Toast.scss | 49 + packages/excalidraw/components/Toast.tsx | 63 + packages/excalidraw/components/ToolButton.tsx | 206 + packages/excalidraw/components/ToolIcon.scss | 199 + packages/excalidraw/components/Toolbar.scss | 50 + packages/excalidraw/components/Tooltip.scss | 47 + packages/excalidraw/components/Tooltip.tsx | 119 + packages/excalidraw/components/Trans.test.tsx | 72 + packages/excalidraw/components/Trans.tsx | 170 + packages/excalidraw/components/UserList.scss | 160 + packages/excalidraw/components/UserList.tsx | 293 + .../components/__snapshots__/App.test.tsx.snap | 50 + .../components/canvases/InteractiveCanvas.tsx | 240 + .../components/canvases/NewElementCanvas.tsx | 56 + .../components/canvases/StaticCanvas.tsx | 141 + packages/excalidraw/components/canvases/index.tsx | 4 + .../components/dropdownMenu/DropdownMenu.scss | 218 + .../components/dropdownMenu/DropdownMenu.test.tsx | 26 + .../components/dropdownMenu/DropdownMenu.tsx | 43 + .../dropdownMenu/DropdownMenuContent.tsx | 88 + .../components/dropdownMenu/DropdownMenuGroup.tsx | 23 + .../components/dropdownMenu/DropdownMenuItem.tsx | 123 + .../dropdownMenu/DropdownMenuItemContent.tsx | 28 + .../dropdownMenu/DropdownMenuItemContentRadio.tsx | 51 + .../dropdownMenu/DropdownMenuItemCustom.tsx | 25 + .../dropdownMenu/DropdownMenuItemLink.tsx | 49 + .../dropdownMenu/DropdownMenuSeparator.tsx | 14 + .../dropdownMenu/DropdownMenuTrigger.tsx | 40 + .../excalidraw/components/dropdownMenu/common.ts | 38 + .../components/dropdownMenu/dropdownMenuUtils.ts | 35 + packages/excalidraw/components/footer/Footer.tsx | 95 + .../excalidraw/components/footer/FooterCenter.scss | 11 + .../excalidraw/components/footer/FooterCenter.tsx | 24 + .../components/hoc/withInternalFallback.test.tsx | 101 + .../components/hoc/withInternalFallback.tsx | 75 + .../excalidraw/components/hyperlink/Hyperlink.scss | 70 + .../excalidraw/components/hyperlink/Hyperlink.tsx | 480 + .../excalidraw/components/hyperlink/helpers.ts | 99 + packages/excalidraw/components/icons.tsx | 2222 ++++ .../LiveCollaborationTrigger.scss | 66 + .../LiveCollaborationTrigger.tsx | 42 + .../components/main-menu/DefaultItems.scss | 21 + .../components/main-menu/DefaultItems.tsx | 391 + .../excalidraw/components/main-menu/MainMenu.tsx | 84 + .../welcome-screen/WelcomeScreen.Center.tsx | 195 + .../welcome-screen/WelcomeScreen.Hints.tsx | 52 + .../components/welcome-screen/WelcomeScreen.scss | 272 + .../components/welcome-screen/WelcomeScreen.tsx | 26 + 230 files changed, 39876 insertions(+) create mode 100644 packages/excalidraw/components/Actions.scss create mode 100644 packages/excalidraw/components/Actions.tsx create mode 100644 packages/excalidraw/components/ActiveConfirmDialog.tsx create mode 100644 packages/excalidraw/components/App.tsx create mode 100644 packages/excalidraw/components/Avatar.scss create mode 100644 packages/excalidraw/components/Avatar.tsx create mode 100644 packages/excalidraw/components/BraveMeasureTextError.tsx create mode 100644 packages/excalidraw/components/Button.scss create mode 100644 packages/excalidraw/components/Button.tsx create mode 100644 packages/excalidraw/components/ButtonIcon.scss create mode 100644 packages/excalidraw/components/ButtonIcon.tsx create mode 100644 packages/excalidraw/components/ButtonIconCycle.tsx create mode 100644 packages/excalidraw/components/ButtonIconSelect.tsx create mode 100644 packages/excalidraw/components/ButtonSelect.tsx create mode 100644 packages/excalidraw/components/ButtonSeparator.tsx create mode 100644 packages/excalidraw/components/Card.scss create mode 100644 packages/excalidraw/components/Card.tsx create mode 100644 packages/excalidraw/components/CheckboxItem.scss create mode 100644 packages/excalidraw/components/CheckboxItem.tsx create mode 100644 packages/excalidraw/components/ColorPicker/ColorInput.tsx create mode 100644 packages/excalidraw/components/ColorPicker/ColorPicker.scss create mode 100644 packages/excalidraw/components/ColorPicker/ColorPicker.tsx create mode 100644 packages/excalidraw/components/ColorPicker/CustomColorList.tsx create mode 100644 packages/excalidraw/components/ColorPicker/HotkeyLabel.tsx create mode 100644 packages/excalidraw/components/ColorPicker/Picker.tsx create mode 100644 packages/excalidraw/components/ColorPicker/PickerColorList.tsx create mode 100644 packages/excalidraw/components/ColorPicker/PickerHeading.tsx create mode 100644 packages/excalidraw/components/ColorPicker/ShadeList.tsx create mode 100644 packages/excalidraw/components/ColorPicker/TopPicks.tsx create mode 100644 packages/excalidraw/components/ColorPicker/colorPickerUtils.ts create mode 100644 packages/excalidraw/components/ColorPicker/keyboardNavHandlers.ts create mode 100644 packages/excalidraw/components/CommandPalette/CommandPalette.scss create mode 100644 packages/excalidraw/components/CommandPalette/CommandPalette.tsx create mode 100644 packages/excalidraw/components/CommandPalette/defaultCommandPaletteItems.ts create mode 100644 packages/excalidraw/components/CommandPalette/types.ts create mode 100644 packages/excalidraw/components/ConfirmDialog.scss create mode 100644 packages/excalidraw/components/ConfirmDialog.tsx create mode 100644 packages/excalidraw/components/ContextMenu.scss create mode 100644 packages/excalidraw/components/ContextMenu.tsx create mode 100644 packages/excalidraw/components/DarkModeToggle.tsx create mode 100644 packages/excalidraw/components/DefaultSidebar.test.tsx create mode 100644 packages/excalidraw/components/DefaultSidebar.tsx create mode 100644 packages/excalidraw/components/DiagramToCodePlugin/DiagramToCodePlugin.tsx create mode 100644 packages/excalidraw/components/Dialog.scss create mode 100644 packages/excalidraw/components/Dialog.tsx create mode 100644 packages/excalidraw/components/DialogActionButton.scss create mode 100644 packages/excalidraw/components/DialogActionButton.tsx create mode 100644 packages/excalidraw/components/ElementLinkDialog.scss create mode 100644 packages/excalidraw/components/ElementLinkDialog.tsx create mode 100644 packages/excalidraw/components/ErrorDialog.tsx create mode 100644 packages/excalidraw/components/ExcalidrawLogo.scss create mode 100644 packages/excalidraw/components/ExcalidrawLogo.tsx create mode 100644 packages/excalidraw/components/ExportDialog.scss create mode 100644 packages/excalidraw/components/EyeDropper.scss create mode 100644 packages/excalidraw/components/EyeDropper.tsx create mode 100644 packages/excalidraw/components/FilledButton.scss create mode 100644 packages/excalidraw/components/FilledButton.tsx create mode 100644 packages/excalidraw/components/FixedSideContainer.scss create mode 100644 packages/excalidraw/components/FixedSideContainer.tsx create mode 100644 packages/excalidraw/components/FollowMode/FollowMode.scss create mode 100644 packages/excalidraw/components/FollowMode/FollowMode.tsx create mode 100644 packages/excalidraw/components/FontPicker/FontPicker.scss create mode 100644 packages/excalidraw/components/FontPicker/FontPicker.tsx create mode 100644 packages/excalidraw/components/FontPicker/FontPickerList.tsx create mode 100644 packages/excalidraw/components/FontPicker/FontPickerTrigger.tsx create mode 100644 packages/excalidraw/components/FontPicker/keyboardNavHandlers.ts create mode 100644 packages/excalidraw/components/HandButton.tsx create mode 100644 packages/excalidraw/components/HelpButton.tsx create mode 100644 packages/excalidraw/components/HelpDialog.scss create mode 100644 packages/excalidraw/components/HelpDialog.tsx create mode 100644 packages/excalidraw/components/HintViewer.scss create mode 100644 packages/excalidraw/components/HintViewer.tsx create mode 100644 packages/excalidraw/components/IconPicker.scss create mode 100644 packages/excalidraw/components/IconPicker.tsx create mode 100644 packages/excalidraw/components/ImageExportDialog.scss create mode 100644 packages/excalidraw/components/ImageExportDialog.tsx create mode 100644 packages/excalidraw/components/InitializeApp.tsx create mode 100644 packages/excalidraw/components/InlineIcon.tsx create mode 100644 packages/excalidraw/components/Island.scss create mode 100644 packages/excalidraw/components/Island.tsx create mode 100644 packages/excalidraw/components/JSONExportDialog.tsx create mode 100644 packages/excalidraw/components/LaserPointerButton.tsx create mode 100644 packages/excalidraw/components/LayerUI.scss create mode 100644 packages/excalidraw/components/LayerUI.tsx create mode 100644 packages/excalidraw/components/LibraryMenu.scss create mode 100644 packages/excalidraw/components/LibraryMenu.tsx create mode 100644 packages/excalidraw/components/LibraryMenuBrowseButton.tsx create mode 100644 packages/excalidraw/components/LibraryMenuControlButtons.tsx create mode 100644 packages/excalidraw/components/LibraryMenuHeaderContent.tsx create mode 100644 packages/excalidraw/components/LibraryMenuItems.scss create mode 100644 packages/excalidraw/components/LibraryMenuItems.tsx create mode 100644 packages/excalidraw/components/LibraryMenuSection.tsx create mode 100644 packages/excalidraw/components/LibraryUnit.scss create mode 100644 packages/excalidraw/components/LibraryUnit.tsx create mode 100644 packages/excalidraw/components/LoadingMessage.tsx create mode 100644 packages/excalidraw/components/LockButton.tsx create mode 100644 packages/excalidraw/components/MagicButton.tsx create mode 100644 packages/excalidraw/components/MobileMenu.tsx create mode 100644 packages/excalidraw/components/Modal.scss create mode 100644 packages/excalidraw/components/Modal.tsx create mode 100644 packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm.scss create mode 100644 packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm.tsx create mode 100644 packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmActions.tsx create mode 100644 packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState.ts create mode 100644 packages/excalidraw/components/Paragraph.tsx create mode 100644 packages/excalidraw/components/PasteChartDialog.scss create mode 100644 packages/excalidraw/components/PasteChartDialog.tsx create mode 100644 packages/excalidraw/components/PenModeButton.tsx create mode 100644 packages/excalidraw/components/Popover.scss create mode 100644 packages/excalidraw/components/Popover.tsx create mode 100644 packages/excalidraw/components/ProjectName.scss create mode 100644 packages/excalidraw/components/ProjectName.tsx create mode 100644 packages/excalidraw/components/PropertiesPopover.tsx create mode 100644 packages/excalidraw/components/PublishLibrary.scss create mode 100644 packages/excalidraw/components/PublishLibrary.tsx create mode 100644 packages/excalidraw/components/QuickSearch.scss create mode 100644 packages/excalidraw/components/QuickSearch.tsx create mode 100644 packages/excalidraw/components/RadioGroup.scss create mode 100644 packages/excalidraw/components/RadioGroup.tsx create mode 100644 packages/excalidraw/components/Range.scss create mode 100644 packages/excalidraw/components/Range.tsx create mode 100644 packages/excalidraw/components/SVGLayer.scss create mode 100644 packages/excalidraw/components/SVGLayer.tsx create mode 100644 packages/excalidraw/components/ScrollableList.scss create mode 100644 packages/excalidraw/components/ScrollableList.tsx create mode 100644 packages/excalidraw/components/SearchMenu.scss create mode 100644 packages/excalidraw/components/SearchMenu.tsx create mode 100644 packages/excalidraw/components/Section.tsx create mode 100644 packages/excalidraw/components/ShareableLinkDialog.scss create mode 100644 packages/excalidraw/components/ShareableLinkDialog.tsx create mode 100644 packages/excalidraw/components/Sidebar/Sidebar.scss create mode 100644 packages/excalidraw/components/Sidebar/Sidebar.test.tsx create mode 100644 packages/excalidraw/components/Sidebar/Sidebar.tsx create mode 100644 packages/excalidraw/components/Sidebar/SidebarHeader.tsx create mode 100644 packages/excalidraw/components/Sidebar/SidebarTab.tsx create mode 100644 packages/excalidraw/components/Sidebar/SidebarTabTrigger.tsx create mode 100644 packages/excalidraw/components/Sidebar/SidebarTabTriggers.tsx create mode 100644 packages/excalidraw/components/Sidebar/SidebarTabs.tsx create mode 100644 packages/excalidraw/components/Sidebar/SidebarTrigger.scss create mode 100644 packages/excalidraw/components/Sidebar/SidebarTrigger.tsx create mode 100644 packages/excalidraw/components/Sidebar/common.ts create mode 100644 packages/excalidraw/components/Sidebar/siderbar.test.helpers.tsx create mode 100644 packages/excalidraw/components/Spinner.scss create mode 100644 packages/excalidraw/components/Spinner.tsx create mode 100644 packages/excalidraw/components/Stack.scss create mode 100644 packages/excalidraw/components/Stack.tsx create mode 100644 packages/excalidraw/components/Stats/Angle.tsx create mode 100644 packages/excalidraw/components/Stats/CanvasGrid.tsx create mode 100644 packages/excalidraw/components/Stats/Collapsible.tsx create mode 100644 packages/excalidraw/components/Stats/Dimension.tsx create mode 100644 packages/excalidraw/components/Stats/DragInput.scss create mode 100644 packages/excalidraw/components/Stats/DragInput.tsx create mode 100644 packages/excalidraw/components/Stats/FontSize.tsx create mode 100644 packages/excalidraw/components/Stats/MultiAngle.tsx create mode 100644 packages/excalidraw/components/Stats/MultiDimension.tsx create mode 100644 packages/excalidraw/components/Stats/MultiFontSize.tsx create mode 100644 packages/excalidraw/components/Stats/MultiPosition.tsx create mode 100644 packages/excalidraw/components/Stats/Position.tsx create mode 100644 packages/excalidraw/components/Stats/Stats.scss create mode 100644 packages/excalidraw/components/Stats/index.tsx create mode 100644 packages/excalidraw/components/Stats/stats.test.tsx create mode 100644 packages/excalidraw/components/Stats/utils.ts create mode 100644 packages/excalidraw/components/Switch.scss create mode 100644 packages/excalidraw/components/Switch.tsx create mode 100644 packages/excalidraw/components/TTDDialog/MermaidToExcalidraw.scss create mode 100644 packages/excalidraw/components/TTDDialog/MermaidToExcalidraw.tsx create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialog.scss create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialog.tsx create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogInput.tsx create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogOutput.tsx create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogPanel.tsx create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogPanels.tsx create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogSubmitShortcut.tsx create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogTab.tsx create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogTabTrigger.tsx create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogTabTriggers.tsx create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogTabs.tsx create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogTrigger.tsx create mode 100644 packages/excalidraw/components/TTDDialog/common.ts create mode 100644 packages/excalidraw/components/TextField.scss create mode 100644 packages/excalidraw/components/TextField.tsx create mode 100644 packages/excalidraw/components/TextInput.scss create mode 100644 packages/excalidraw/components/Toast.scss create mode 100644 packages/excalidraw/components/Toast.tsx create mode 100644 packages/excalidraw/components/ToolButton.tsx create mode 100644 packages/excalidraw/components/ToolIcon.scss create mode 100644 packages/excalidraw/components/Toolbar.scss create mode 100644 packages/excalidraw/components/Tooltip.scss create mode 100644 packages/excalidraw/components/Tooltip.tsx create mode 100644 packages/excalidraw/components/Trans.test.tsx create mode 100644 packages/excalidraw/components/Trans.tsx create mode 100644 packages/excalidraw/components/UserList.scss create mode 100644 packages/excalidraw/components/UserList.tsx create mode 100644 packages/excalidraw/components/__snapshots__/App.test.tsx.snap create mode 100644 packages/excalidraw/components/canvases/InteractiveCanvas.tsx create mode 100644 packages/excalidraw/components/canvases/NewElementCanvas.tsx create mode 100644 packages/excalidraw/components/canvases/StaticCanvas.tsx create mode 100644 packages/excalidraw/components/canvases/index.tsx create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenu.scss create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenu.test.tsx create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenu.tsx create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuGroup.tsx create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuItem.tsx create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuItemContent.tsx create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.tsx create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuItemCustom.tsx create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuItemLink.tsx create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuSeparator.tsx create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuTrigger.tsx create mode 100644 packages/excalidraw/components/dropdownMenu/common.ts create mode 100644 packages/excalidraw/components/dropdownMenu/dropdownMenuUtils.ts create mode 100644 packages/excalidraw/components/footer/Footer.tsx create mode 100644 packages/excalidraw/components/footer/FooterCenter.scss create mode 100644 packages/excalidraw/components/footer/FooterCenter.tsx create mode 100644 packages/excalidraw/components/hoc/withInternalFallback.test.tsx create mode 100644 packages/excalidraw/components/hoc/withInternalFallback.tsx create mode 100644 packages/excalidraw/components/hyperlink/Hyperlink.scss create mode 100644 packages/excalidraw/components/hyperlink/Hyperlink.tsx create mode 100644 packages/excalidraw/components/hyperlink/helpers.ts create mode 100644 packages/excalidraw/components/icons.tsx create mode 100644 packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.scss create mode 100644 packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.tsx create mode 100644 packages/excalidraw/components/main-menu/DefaultItems.scss create mode 100644 packages/excalidraw/components/main-menu/DefaultItems.tsx create mode 100644 packages/excalidraw/components/main-menu/MainMenu.tsx create mode 100644 packages/excalidraw/components/welcome-screen/WelcomeScreen.Center.tsx create mode 100644 packages/excalidraw/components/welcome-screen/WelcomeScreen.Hints.tsx create mode 100644 packages/excalidraw/components/welcome-screen/WelcomeScreen.scss create mode 100644 packages/excalidraw/components/welcome-screen/WelcomeScreen.tsx (limited to 'packages/excalidraw/components') diff --git a/packages/excalidraw/components/Actions.scss b/packages/excalidraw/components/Actions.scss new file mode 100644 index 0000000..5826628 --- /dev/null +++ b/packages/excalidraw/components/Actions.scss @@ -0,0 +1,93 @@ +.zoom-actions, +.undo-redo-buttons { + background-color: var(--island-bg-color); + border-radius: var(--border-radius-lg); + box-shadow: 0 0 0 1px var(--color-surface-lowest); +} + +.zoom-button, +.undo-redo-buttons button { + border-radius: 0 !important; + background-color: var(--color-surface-low) !important; + font-size: 0.875rem !important; + width: var(--lg-button-size); + height: var(--lg-button-size); + + svg { + width: var(--lg-icon-size) !important; + height: var(--lg-icon-size) !important; + } + + .ToolIcon__icon { + width: 100%; + height: 100%; + } +} + +.reset-zoom-button { + border-left: 0 !important; + border-right: 0 !important; + padding: 0 0.625rem !important; + width: 3.75rem !important; + justify-content: center; + color: var(--text-primary-color); +} + +.zoom-out-button { + border-top-left-radius: var(--border-radius-lg) !important; + border-bottom-left-radius: var(--border-radius-lg) !important; + + :root[dir="rtl"] & { + transform: scaleX(-1); + } + + .ToolIcon__icon { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + } +} + +.zoom-in-button { + border-top-right-radius: var(--border-radius-lg) !important; + border-bottom-right-radius: var(--border-radius-lg) !important; + + :root[dir="rtl"] & { + transform: scaleX(-1); + } + + .ToolIcon__icon { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; + } +} + +.undo-redo-buttons { + .undo-button-container button { + border-top-left-radius: var(--border-radius-lg) !important; + border-bottom-left-radius: var(--border-radius-lg) !important; + border-right: 0 !important; + + :root[dir="rtl"] & { + transform: scaleX(-1); + } + + .ToolIcon__icon { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + } + } + + .redo-button-container button { + border-top-right-radius: var(--border-radius-lg) !important; + border-bottom-right-radius: var(--border-radius-lg) !important; + + :root[dir="rtl"] & { + transform: scaleX(-1); + } + + .ToolIcon__icon { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; + } + } +} diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx new file mode 100644 index 0000000..cd9120f --- /dev/null +++ b/packages/excalidraw/components/Actions.tsx @@ -0,0 +1,478 @@ +import { useState } from "react"; +import type { ActionManager } from "../actions/manager"; +import type { + ExcalidrawElement, + ExcalidrawElementType, + NonDeletedElementsMap, + NonDeletedSceneElementsMap, +} from "../element/types"; +import { t } from "../i18n"; +import { useDevice } from "./App"; +import { + canChangeRoundness, + canHaveArrowheads, + getTargetElements, + hasBackground, + hasStrokeStyle, + hasStrokeWidth, +} from "../scene"; +import { SHAPES } from "../shapes"; +import type { AppClassProperties, AppProps, UIAppState, Zoom } from "../types"; +import { capitalizeString, isTransparent } from "../utils"; +import Stack from "./Stack"; +import { ToolButton } from "./ToolButton"; +import { hasStrokeColor, toolIsArrow } from "../scene/comparisons"; +import { trackEvent } from "../analytics"; +import { + hasBoundTextElement, + isElbowArrow, + isImageElement, + isLinearElement, + isTextElement, +} from "../element/typeChecks"; +import clsx from "clsx"; +import { actionToggleZenMode } from "../actions"; +import { Tooltip } from "./Tooltip"; +import { + shouldAllowVerticalAlign, + suppportsHorizontalAlign, +} from "../element/textElement"; + +import "./Actions.scss"; +import DropdownMenu from "./dropdownMenu/DropdownMenu"; +import { + EmbedIcon, + extraToolsIcon, + frameToolIcon, + mermaidLogoIcon, + laserPointerToolIcon, + MagicIcon, +} from "./icons"; +import { KEYS } from "../keys"; +import { useTunnels } from "../context/tunnels"; +import { CLASSES } from "../constants"; +import { alignActionsPredicate } from "../actions/actionAlign"; + +export const canChangeStrokeColor = ( + appState: UIAppState, + targetElements: ExcalidrawElement[], +) => { + let commonSelectedType: ExcalidrawElementType | null = + targetElements[0]?.type || null; + + for (const element of targetElements) { + if (element.type !== commonSelectedType) { + commonSelectedType = null; + break; + } + } + + return ( + (hasStrokeColor(appState.activeTool.type) && + appState.activeTool.type !== "image" && + commonSelectedType !== "image" && + commonSelectedType !== "frame" && + commonSelectedType !== "magicframe") || + targetElements.some((element) => hasStrokeColor(element.type)) + ); +}; + +export const canChangeBackgroundColor = ( + appState: UIAppState, + targetElements: ExcalidrawElement[], +) => { + return ( + hasBackground(appState.activeTool.type) || + targetElements.some((element) => hasBackground(element.type)) + ); +}; + +export const SelectedShapeActions = ({ + appState, + elementsMap, + renderAction, + app, +}: { + appState: UIAppState; + elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap; + renderAction: ActionManager["renderAction"]; + app: AppClassProperties; +}) => { + const targetElements = getTargetElements(elementsMap, appState); + + let isSingleElementBoundContainer = false; + if ( + targetElements.length === 2 && + (hasBoundTextElement(targetElements[0]) || + hasBoundTextElement(targetElements[1])) + ) { + isSingleElementBoundContainer = true; + } + const isEditingTextOrNewElement = Boolean( + appState.editingTextElement || appState.newElement, + ); + const device = useDevice(); + const isRTL = document.documentElement.getAttribute("dir") === "rtl"; + + const showFillIcons = + (hasBackground(appState.activeTool.type) && + !isTransparent(appState.currentItemBackgroundColor)) || + targetElements.some( + (element) => + hasBackground(element.type) && !isTransparent(element.backgroundColor), + ); + + const showLinkIcon = + targetElements.length === 1 || isSingleElementBoundContainer; + + const showLineEditorAction = + !appState.editingLinearElement && + targetElements.length === 1 && + isLinearElement(targetElements[0]) && + !isElbowArrow(targetElements[0]); + + const showCropEditorAction = + !appState.croppingElementId && + targetElements.length === 1 && + isImageElement(targetElements[0]); + + const showAlignActions = + !isSingleElementBoundContainer && alignActionsPredicate(appState, app); + + return ( +
+
+ {canChangeStrokeColor(appState, targetElements) && + renderAction("changeStrokeColor")} +
+ {canChangeBackgroundColor(appState, targetElements) && ( +
{renderAction("changeBackgroundColor")}
+ )} + {showFillIcons && renderAction("changeFillStyle")} + + {(hasStrokeWidth(appState.activeTool.type) || + targetElements.some((element) => hasStrokeWidth(element.type))) && + renderAction("changeStrokeWidth")} + + {(appState.activeTool.type === "freedraw" || + targetElements.some((element) => element.type === "freedraw")) && + renderAction("changeStrokeShape")} + + {(hasStrokeStyle(appState.activeTool.type) || + targetElements.some((element) => hasStrokeStyle(element.type))) && ( + <> + {renderAction("changeStrokeStyle")} + {renderAction("changeSloppiness")} + + )} + + {(canChangeRoundness(appState.activeTool.type) || + targetElements.some((element) => canChangeRoundness(element.type))) && ( + <>{renderAction("changeRoundness")} + )} + + {(toolIsArrow(appState.activeTool.type) || + targetElements.some((element) => toolIsArrow(element.type))) && ( + <>{renderAction("changeArrowType")} + )} + + {(appState.activeTool.type === "text" || + targetElements.some(isTextElement)) && ( + <> + {renderAction("changeFontFamily")} + {renderAction("changeFontSize")} + {(appState.activeTool.type === "text" || + suppportsHorizontalAlign(targetElements, elementsMap)) && + renderAction("changeTextAlign")} + + )} + + {shouldAllowVerticalAlign(targetElements, elementsMap) && + renderAction("changeVerticalAlign")} + {(canHaveArrowheads(appState.activeTool.type) || + targetElements.some((element) => canHaveArrowheads(element.type))) && ( + <>{renderAction("changeArrowhead")} + )} + + {renderAction("changeOpacity")} + +
+ {t("labels.layers")} +
+ {renderAction("sendToBack")} + {renderAction("sendBackward")} + {renderAction("bringForward")} + {renderAction("bringToFront")} +
+
+ + {showAlignActions && !isSingleElementBoundContainer && ( +
+ {t("labels.align")} +
+ { + // swap this order for RTL so the button positions always match their action + // (i.e. the leftmost button aligns left) + } + {isRTL ? ( + <> + {renderAction("alignRight")} + {renderAction("alignHorizontallyCentered")} + {renderAction("alignLeft")} + + ) : ( + <> + {renderAction("alignLeft")} + {renderAction("alignHorizontallyCentered")} + {renderAction("alignRight")} + + )} + {targetElements.length > 2 && + renderAction("distributeHorizontally")} + {/* breaks the row ˇˇ */} +
+
+ {renderAction("alignTop")} + {renderAction("alignVerticallyCentered")} + {renderAction("alignBottom")} + {targetElements.length > 2 && + renderAction("distributeVertically")} +
+
+
+ )} + {!isEditingTextOrNewElement && targetElements.length > 0 && ( +
+ {t("labels.actions")} +
+ {!device.editor.isMobile && renderAction("duplicateSelection")} + {!device.editor.isMobile && renderAction("deleteSelectedElements")} + {renderAction("group")} + {renderAction("ungroup")} + {showLinkIcon && renderAction("hyperlink")} + {showCropEditorAction && renderAction("cropEditor")} + {showLineEditorAction && renderAction("toggleLinearEditor")} +
+
+ )} +
+ ); +}; + +export const ShapesSwitcher = ({ + activeTool, + appState, + app, + UIOptions, +}: { + activeTool: UIAppState["activeTool"]; + appState: UIAppState; + app: AppClassProperties; + UIOptions: AppProps["UIOptions"]; +}) => { + const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false); + + const frameToolSelected = activeTool.type === "frame"; + const laserToolSelected = activeTool.type === "laser"; + const embeddableToolSelected = activeTool.type === "embeddable"; + + const { TTDDialogTriggerTunnel } = useTunnels(); + + return ( + <> + {SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => { + if ( + UIOptions.tools?.[ + value as Extract + ] === false + ) { + return null; + } + + const label = t(`toolBar.${value}`); + const letter = + key && capitalizeString(typeof key === "string" ? key : key[0]); + const shortcut = letter + ? `${letter} ${t("helpDialog.or")} ${numericKey}` + : `${numericKey}`; + return ( + { + if (!appState.penDetected && pointerType === "pen") { + app.togglePenMode(true); + } + }} + onChange={({ pointerType }) => { + if (appState.activeTool.type !== value) { + trackEvent("toolbar", value, "ui"); + } + if (value === "image") { + app.setActiveTool({ + type: value, + insertOnCanvasDirectly: pointerType !== "mouse", + }); + } else { + app.setActiveTool({ type: value }); + } + }} + /> + ); + })} +
+ + + setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)} + title={t("toolBar.extraTools")} + > + {extraToolsIcon} + + setIsExtraToolsMenuOpen(false)} + onSelect={() => setIsExtraToolsMenuOpen(false)} + className="App-toolbar__extra-tools-dropdown" + > + app.setActiveTool({ type: "frame" })} + icon={frameToolIcon} + shortcut={KEYS.F.toLocaleUpperCase()} + data-testid="toolbar-frame" + selected={frameToolSelected} + > + {t("toolBar.frame")} + + app.setActiveTool({ type: "embeddable" })} + icon={EmbedIcon} + data-testid="toolbar-embeddable" + selected={embeddableToolSelected} + > + {t("toolBar.embeddable")} + + app.setActiveTool({ type: "laser" })} + icon={laserPointerToolIcon} + data-testid="toolbar-laser" + selected={laserToolSelected} + shortcut={KEYS.K.toLocaleUpperCase()} + > + {t("toolBar.laser")} + +
+ Generate +
+ {app.props.aiEnabled !== false && } + app.setOpenDialog({ name: "ttd", tab: "mermaid" })} + icon={mermaidLogoIcon} + data-testid="toolbar-embeddable" + > + {t("toolBar.mermaidToExcalidraw")} + + {app.props.aiEnabled !== false && app.plugins.diagramToCode && ( + <> + app.onMagicframeToolSelect()} + icon={MagicIcon} + data-testid="toolbar-magicframe" + > + {t("toolBar.magicframe")} + AI + + + )} +
+
+ + ); +}; + +export const ZoomActions = ({ + renderAction, + zoom, +}: { + renderAction: ActionManager["renderAction"]; + zoom: Zoom; +}) => ( + + + {renderAction("zoomOut")} + {renderAction("resetZoom")} + {renderAction("zoomIn")} + + +); + +export const UndoRedoActions = ({ + renderAction, + className, +}: { + renderAction: ActionManager["renderAction"]; + className?: string; +}) => ( +
+
+ {renderAction("undo")} +
+
+ {renderAction("redo")} +
+
+); + +export const ExitZenModeAction = ({ + actionManager, + showExitZenModeBtn, +}: { + actionManager: ActionManager; + showExitZenModeBtn: boolean; +}) => ( + +); + +export const FinalizeAction = ({ + renderAction, + className, +}: { + renderAction: ActionManager["renderAction"]; + className?: string; +}) => ( +
+ {renderAction("finalize", { size: "small" })} +
+); diff --git a/packages/excalidraw/components/ActiveConfirmDialog.tsx b/packages/excalidraw/components/ActiveConfirmDialog.tsx new file mode 100644 index 0000000..699fbc6 --- /dev/null +++ b/packages/excalidraw/components/ActiveConfirmDialog.tsx @@ -0,0 +1,35 @@ +import { actionClearCanvas } from "../actions"; +import { t } from "../i18n"; +import { atom, useAtom } from "../editor-jotai"; +import { useExcalidrawActionManager } from "./App"; +import ConfirmDialog from "./ConfirmDialog"; + +export const activeConfirmDialogAtom = atom<"clearCanvas" | null>(null); + +export const ActiveConfirmDialog = () => { + const [activeConfirmDialog, setActiveConfirmDialog] = useAtom( + activeConfirmDialogAtom, + ); + const actionManager = useExcalidrawActionManager(); + + if (!activeConfirmDialog) { + return null; + } + + if (activeConfirmDialog === "clearCanvas") { + return ( + { + actionManager.executeAction(actionClearCanvas); + setActiveConfirmDialog(null); + }} + onCancel={() => setActiveConfirmDialog(null)} + title={t("clearCanvasDialog.title")} + > +

{t("alerts.clearReset")}

+
+ ); + } + + return null; +}; diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx new file mode 100644 index 0000000..dc6d287 --- /dev/null +++ b/packages/excalidraw/components/App.tsx @@ -0,0 +1,11180 @@ +import React, { useContext } from "react"; +import { flushSync } from "react-dom"; + +import type { RoughCanvas } from "roughjs/bin/canvas"; +import rough from "roughjs/bin/rough"; +import clsx from "clsx"; +import { nanoid } from "nanoid"; +import { + actionAddToLibrary, + actionBringForward, + actionBringToFront, + actionCopy, + actionCopyAsPng, + actionCopyAsSvg, + copyText, + actionCopyStyles, + actionCut, + actionDeleteSelected, + actionDuplicateSelection, + actionFinalize, + actionFlipHorizontal, + actionFlipVertical, + actionGroup, + actionPasteStyles, + actionSelectAll, + actionSendBackward, + actionSendToBack, + actionToggleGridMode, + actionToggleStats, + actionToggleZenMode, + actionUnbindText, + actionBindText, + actionUngroup, + actionLink, + actionToggleElementLock, + actionToggleLinearEditor, + actionToggleObjectsSnapMode, + actionToggleCropEditor, +} from "../actions"; +import { createRedoAction, createUndoAction } from "../actions/actionHistory"; +import { ActionManager } from "../actions/manager"; +import { actions } from "../actions/register"; +import type { Action, ActionResult } from "../actions/types"; +import { trackEvent } from "../analytics"; +import { + getDefaultAppState, + isEraserActive, + isHandToolActive, +} from "../appState"; +import type { PastedMixedContent } from "../clipboard"; +import { copyTextToSystemClipboard, parseClipboard } from "../clipboard"; +import { + APP_NAME, + CURSOR_TYPE, + DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT, + DEFAULT_VERTICAL_ALIGN, + DRAGGING_THRESHOLD, + ELEMENT_SHIFT_TRANSLATE_AMOUNT, + ELEMENT_TRANSLATE_AMOUNT, + ENV, + EVENT, + FRAME_STYLE, + IMAGE_MIME_TYPES, + IMAGE_RENDER_TIMEOUT, + isBrave, + LINE_CONFIRM_THRESHOLD, + MAX_ALLOWED_FILE_BYTES, + MIME_TYPES, + MQ_MAX_HEIGHT_LANDSCAPE, + MQ_MAX_WIDTH_LANDSCAPE, + MQ_MAX_WIDTH_PORTRAIT, + MQ_RIGHT_SIDEBAR_MIN_WIDTH, + POINTER_BUTTON, + ROUNDNESS, + SCROLL_TIMEOUT, + TAP_TWICE_TIMEOUT, + TEXT_TO_CENTER_SNAP_THRESHOLD, + THEME, + THEME_FILTER, + TOUCH_CTX_MENU_TIMEOUT, + VERTICAL_ALIGN, + YOUTUBE_STATES, + ZOOM_STEP, + POINTER_EVENTS, + TOOL_TYPE, + isIOS, + supportsResizeObserver, + DEFAULT_COLLISION_THRESHOLD, + DEFAULT_TEXT_ALIGN, + ARROW_TYPE, + DEFAULT_REDUCED_GLOBAL_ALPHA, + isSafari, + type EXPORT_IMAGE_TYPES, +} from "../constants"; +import type { ExportedElements } from "../data"; +import { exportCanvas, loadFromBlob } from "../data"; +import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library"; +import { restore, restoreElements } from "../data/restore"; +import { + dragNewElement, + dragSelectedElements, + duplicateElement, + getCommonBounds, + getCursorForResizingElement, + getDragOffsetXY, + getElementWithTransformHandleType, + getNormalizedDimensions, + getResizeArrowDirection, + getResizeOffsetXY, + getLockedLinearCursorAlignSize, + getTransformHandleTypeFromCoords, + isInvisiblySmallElement, + isNonDeletedElement, + isTextElement, + newElement, + newLinearElement, + newTextElement, + newImageElement, + transformElements, + refreshTextDimensions, + redrawTextBoundingBox, + getElementAbsoluteCoords, +} from "../element"; +import { + bindOrUnbindLinearElement, + bindOrUnbindLinearElements, + fixBindingsAfterDeletion, + fixBindingsAfterDuplication, + getHoveredElementForBinding, + isBindingEnabled, + isLinearElementSimpleAndAlreadyBound, + maybeBindLinearElement, + shouldEnableBindingForPointerEvent, + updateBoundElements, + getSuggestedBindingsForArrows, +} from "../element/binding"; +import { LinearElementEditor } from "../element/linearElementEditor"; +import { mutateElement, newElementWith } from "../element/mutateElement"; +import { + deepCopyElement, + duplicateElements, + newFrameElement, + newFreeDrawElement, + newEmbeddableElement, + newMagicFrameElement, + newIframeElement, + newArrowElement, +} from "../element/newElement"; +import { + hasBoundTextElement, + isArrowElement, + isBindingElement, + isBindingElementType, + isBoundToContainer, + isFrameLikeElement, + isImageElement, + isEmbeddableElement, + isInitializedImageElement, + isLinearElement, + isLinearElementType, + isUsingAdaptiveRadius, + isIframeElement, + isIframeLikeElement, + isMagicFrameElement, + isTextBindableContainer, + isElbowArrow, + isFlowchartNodeElement, + isBindableElement, +} from "../element/typeChecks"; +import type { + ExcalidrawBindableElement, + ExcalidrawElement, + ExcalidrawFreeDrawElement, + ExcalidrawGenericElement, + ExcalidrawLinearElement, + ExcalidrawTextElement, + NonDeleted, + InitializedExcalidrawImageElement, + ExcalidrawImageElement, + FileId, + NonDeletedExcalidrawElement, + ExcalidrawTextContainer, + ExcalidrawFrameLikeElement, + ExcalidrawMagicFrameElement, + ExcalidrawIframeLikeElement, + IframeData, + ExcalidrawIframeElement, + ExcalidrawEmbeddableElement, + Ordered, + MagicGenerationData, + ExcalidrawNonSelectionElement, + ExcalidrawArrowElement, +} from "../element/types"; +import { getCenter, getDistance } from "../gesture"; +import { + editGroupForSelectedElement, + getElementsInGroup, + getSelectedGroupIdForElement, + getSelectedGroupIds, + isElementInGroup, + isSelectedViaGroup, + selectGroupsForSelectedElements, +} from "../groups"; +import { History } from "../history"; +import { defaultLang, getLanguage, languages, setLanguage, t } from "../i18n"; +import { + CODES, + shouldResizeFromCenter, + shouldMaintainAspectRatio, + shouldRotateWithDiscreteAngle, + isArrowKey, + KEYS, +} from "../keys"; +import { + isElementCompletelyInViewport, + isElementInViewport, +} from "../element/sizeHelpers"; +import { + calculateScrollCenter, + getElementsWithinSelection, + getNormalizedZoom, + getSelectedElements, + hasBackground, + isSomeElementSelected, +} from "../scene"; +import Scene from "../scene/Scene"; +import type { + RenderInteractiveSceneCallback, + ScrollBars, +} from "../scene/types"; +import { getStateForZoom } from "../scene/zoom"; +import { + findShapeByKey, + getBoundTextShape, + getCornerRadius, + getElementShape, + isPathALoop, +} from "../shapes"; +import { getSelectionBoxShape } from "@excalidraw/utils/geometry/shape"; +import { isPointInShape } from "@excalidraw/utils/collision"; +import type { + AppClassProperties, + AppProps, + AppState, + BinaryFileData, + DataURL, + ExcalidrawImperativeAPI, + BinaryFiles, + Gesture, + GestureEvent, + LibraryItems, + PointerDownState, + SceneData, + Device, + FrameNameBoundsCache, + SidebarName, + SidebarTabName, + KeyboardModifiersObject, + CollaboratorPointer, + ToolType, + OnUserFollowedPayload, + UnsubscribeCallback, + EmbedsValidationStatus, + ElementsPendingErasure, + GenerateDiagramToCode, + NullableGridSize, + Offsets, +} from "../types"; +import { + debounce, + distance, + getFontString, + getNearestScrollableContainer, + isInputLike, + isToolIcon, + isWritableElement, + sceneCoordsToViewportCoords, + tupleToCoors, + viewportCoordsToSceneCoords, + wrapEvent, + updateObject, + updateActiveTool, + getShortcutKey, + isTransparent, + easeToValuesRAF, + muteFSAbortError, + isTestEnv, + easeOut, + updateStable, + addEventListener, + normalizeEOL, + getDateTime, + isShallowEqual, + arrayToMap, +} from "../utils"; +import { + createSrcDoc, + embeddableURLValidator, + maybeParseEmbedSrc, + getEmbedLink, +} from "../element/embeddable"; +import type { ContextMenuItems } from "./ContextMenu"; +import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu"; +import LayerUI from "./LayerUI"; +import { Toast } from "./Toast"; +import { actionToggleViewMode } from "../actions/actionToggleViewMode"; +import { + dataURLToFile, + dataURLToString, + generateIdFromFile, + getDataURL, + getDataURL_sync, + getFileFromEvent, + ImageURLToFile, + isImageFileHandle, + isSupportedImageFile, + loadSceneOrLibraryFromBlob, + normalizeFile, + parseLibraryJSON, + resizeImageFile, + SVGStringToFile, +} from "../data/blob"; +import { + getInitializedImageElements, + loadHTMLImageElement, + normalizeSVG, + updateImageCache as _updateImageCache, +} from "../element/image"; +import throttle from "lodash.throttle"; +import type { FileSystemHandle } from "../data/filesystem"; +import { fileOpen } from "../data/filesystem"; +import { + bindTextToShapeAfterDuplication, + getBoundTextElement, + getContainerCenter, + getContainerElement, + isValidTextContainer, +} from "../element/textElement"; +import { + showHyperlinkTooltip, + hideHyperlinkToolip, + Hyperlink, +} from "../components/hyperlink/Hyperlink"; +import { isLocalLink, normalizeLink, toValidURL } from "../data/url"; +import { shouldShowBoundingBox } from "../element/transformHandles"; +import { actionUnlockAllElements } from "../actions/actionElementLock"; +import { Fonts, getLineHeight } from "../fonts"; +import { + getFrameChildren, + isCursorInFrame, + bindElementsToFramesAfterDuplication, + addElementsToFrame, + replaceAllElementsInFrame, + removeElementsFromFrame, + getElementsInResizingFrame, + getElementsInNewFrame, + getContainingFrame, + elementOverlapsWithFrame, + updateFrameMembershipOfSelectedElements, + isElementInFrame, + getFrameLikeTitle, + getElementsOverlappingFrame, + filterElementsEligibleAsFrameChildren, +} from "../frame"; +import { + excludeElementsInFramesFromSelection, + makeNextSelectedElementIds, +} from "../scene/selection"; +import { actionPaste } from "../actions/actionClipboard"; +import { + actionRemoveAllElementsFromFrame, + actionSelectAllElementsInFrame, + actionWrapSelectionInFrame, +} from "../actions/actionFrame"; +import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas"; +import { editorJotaiStore } from "../editor-jotai"; +import { activeConfirmDialogAtom } from "./ActiveConfirmDialog"; +import { ImageSceneDataError } from "../errors"; +import { + getSnapLinesAtPointer, + snapDraggedElements, + isActiveToolNonLinearSnappable, + snapNewElement, + snapResizingElements, + isSnappingEnabled, + getVisibleGaps, + getReferenceSnapPoints, + SnapCache, + isGridModeEnabled, + getGridPoint, +} from "../snapping"; +import { actionWrapTextInContainer } from "../actions/actionBoundText"; +import BraveMeasureTextError from "./BraveMeasureTextError"; +import { activeEyeDropperAtom } from "./EyeDropper"; +import type { ExcalidrawElementSkeleton } from "../data/transform"; +import { convertToExcalidrawElements } from "../data/transform"; +import type { ValueOf } from "../utility-types"; +import { isSidebarDockedAtom } from "./Sidebar/Sidebar"; +import { StaticCanvas, InteractiveCanvas } from "./canvases"; +import { Renderer } from "../scene/Renderer"; +import { ShapeCache } from "../scene/ShapeCache"; +import { SVGLayer } from "./SVGLayer"; +import { + setEraserCursor, + setCursor, + resetCursor, + setCursorForShape, +} from "../cursor"; +import { Emitter } from "../emitter"; +import { ElementCanvasButtons } from "../element/ElementCanvasButtons"; +import { COLOR_PALETTE } from "../colors"; +import { ElementCanvasButton } from "./MagicButton"; +import { MagicIcon, copyIcon, fullscreenIcon } from "./icons"; +import FollowMode from "./FollowMode/FollowMode"; +import { Store, CaptureUpdateAction } from "../store"; +import { AnimationFrameHandler } from "../animation-frame-handler"; +import { AnimatedTrail } from "../animated-trail"; +import { LaserTrails } from "../laser-trails"; +import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils"; +import { getRenderOpacity } from "../renderer/renderElement"; +import { + hitElementBoundText, + hitElementBoundingBoxOnly, + hitElementItself, +} from "../element/collision"; +import { textWysiwyg } from "../element/textWysiwyg"; +import { isOverScrollBars } from "../scene/scrollbars"; +import { syncInvalidIndices, syncMovedIndices } from "../fractionalIndex"; +import { + isPointHittingLink, + isPointHittingLinkIcon, +} from "./hyperlink/helpers"; +import { getShortcutFromShortcutName } from "../actions/shortcuts"; +import { actionTextAutoResize } from "../actions/actionTextAutoResize"; +import { getVisibleSceneBounds } from "../element/bounds"; +import { isMaybeMermaidDefinition } from "../mermaid"; +import NewElementCanvas from "./canvases/NewElementCanvas"; +import { + FlowChartCreator, + FlowChartNavigator, + getLinkDirectionFromKey, +} from "../element/flowchart"; +import { searchItemInFocusAtom } from "./SearchMenu"; +import type { LocalPoint, Radians } from "@excalidraw/math"; +import { + clamp, + pointFrom, + pointDistance, + vector, + pointRotateRads, + vectorScale, + vectorFromPoint, + vectorSubtract, + vectorDot, + vectorNormalize, +} from "@excalidraw/math"; +import { cropElement } from "../element/cropElement"; +import { wrapText } from "../element/textWrapping"; +import { actionCopyElementLink } from "../actions/actionElementLink"; +import { isElementLink, parseElementLinkFromURL } from "../element/elementLink"; +import { + isMeasureTextSupported, + normalizeText, + measureText, + getLineHeightInPx, + getApproxMinLineWidth, + getApproxMinLineHeight, + getMinTextElementWidth, +} from "../element/textMeasurements"; + +const AppContext = React.createContext(null!); +const AppPropsContext = React.createContext(null!); + +const deviceContextInitialValue = { + viewport: { + isMobile: false, + isLandscape: false, + }, + editor: { + isMobile: false, + canFitSidebar: false, + }, + isTouchScreen: false, +}; +const DeviceContext = React.createContext(deviceContextInitialValue); +DeviceContext.displayName = "DeviceContext"; + +export const ExcalidrawContainerContext = React.createContext<{ + container: HTMLDivElement | null; + id: string | null; +}>({ container: null, id: null }); +ExcalidrawContainerContext.displayName = "ExcalidrawContainerContext"; + +const ExcalidrawElementsContext = React.createContext< + readonly NonDeletedExcalidrawElement[] +>([]); +ExcalidrawElementsContext.displayName = "ExcalidrawElementsContext"; + +const ExcalidrawAppStateContext = React.createContext({ + ...getDefaultAppState(), + width: 0, + height: 0, + offsetLeft: 0, + offsetTop: 0, +}); +ExcalidrawAppStateContext.displayName = "ExcalidrawAppStateContext"; + +const ExcalidrawSetAppStateContext = React.createContext< + React.Component["setState"] +>(() => { + console.warn("Uninitialized ExcalidrawSetAppStateContext context!"); +}); +ExcalidrawSetAppStateContext.displayName = "ExcalidrawSetAppStateContext"; + +const ExcalidrawActionManagerContext = React.createContext( + null!, +); +ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext"; + +export const useApp = () => useContext(AppContext); +export const useAppProps = () => useContext(AppPropsContext); +export const useDevice = () => useContext(DeviceContext); +export const useExcalidrawContainer = () => + useContext(ExcalidrawContainerContext); +export const useExcalidrawElements = () => + useContext(ExcalidrawElementsContext); +export const useExcalidrawAppState = () => + useContext(ExcalidrawAppStateContext); +export const useExcalidrawSetAppState = () => + useContext(ExcalidrawSetAppStateContext); +export const useExcalidrawActionManager = () => + useContext(ExcalidrawActionManagerContext); + +let didTapTwice: boolean = false; +let tappedTwiceTimer = 0; +let isHoldingSpace: boolean = false; +let isPanning: boolean = false; +let isDraggingScrollBar: boolean = false; +let currentScrollBars: ScrollBars = { horizontal: null, vertical: null }; +let touchTimeout = 0; +let invalidateContextMenu = false; + +/** + * Map of youtube embed video states + */ +const YOUTUBE_VIDEO_STATES = new Map< + ExcalidrawElement["id"], + ValueOf +>(); + +let IS_PLAIN_PASTE = false; +let IS_PLAIN_PASTE_TIMER = 0; +let PLAIN_PASTE_TOAST_SHOWN = false; + +let lastPointerUp: (() => void) | null = null; +const gesture: Gesture = { + pointers: new Map(), + lastCenter: null, + initialDistance: null, + initialScale: null, +}; + +class App extends React.Component { + canvas: AppClassProperties["canvas"]; + interactiveCanvas: AppClassProperties["interactiveCanvas"] = null; + rc: RoughCanvas; + unmounted: boolean = false; + actionManager: ActionManager; + device: Device = deviceContextInitialValue; + + private excalidrawContainerRef = React.createRef(); + + public scene: Scene; + public fonts: Fonts; + public renderer: Renderer; + public visibleElements: readonly NonDeletedExcalidrawElement[]; + private resizeObserver: ResizeObserver | undefined; + private nearestScrollableContainer: HTMLElement | Document | undefined; + public library: AppClassProperties["library"]; + public libraryItemsFromStorage: LibraryItems | undefined; + public id: string; + private store: Store; + private history: History; + public excalidrawContainerValue: { + container: HTMLDivElement | null; + id: string; + }; + + public files: BinaryFiles = {}; + public imageCache: AppClassProperties["imageCache"] = new Map(); + private iFrameRefs = new Map(); + /** + * Indicates whether the embeddable's url has been validated for rendering. + * If value not set, indicates that the validation is pending. + * Initially or on url change the flag is not reset so that we can guarantee + * the validation came from a trusted source (the editor). + **/ + private embedsValidationStatus: EmbedsValidationStatus = new Map(); + /** embeds that have been inserted to DOM (as a perf optim, we don't want to + * insert to DOM before user initially scrolls to them) */ + private initializedEmbeds = new Set(); + + private elementsPendingErasure: ElementsPendingErasure = new Set(); + + public flowChartCreator: FlowChartCreator = new FlowChartCreator(); + private flowChartNavigator: FlowChartNavigator = new FlowChartNavigator(); + + hitLinkElement?: NonDeletedExcalidrawElement; + lastPointerDownEvent: React.PointerEvent | null = null; + lastPointerUpEvent: React.PointerEvent | PointerEvent | null = + null; + lastPointerMoveEvent: PointerEvent | null = null; + lastPointerMoveCoords: { x: number; y: number } | null = null; + lastViewportPosition = { x: 0, y: 0 }; + + animationFrameHandler = new AnimationFrameHandler(); + + laserTrails = new LaserTrails(this.animationFrameHandler, this); + eraserTrail = new AnimatedTrail(this.animationFrameHandler, this, { + streamline: 0.2, + size: 5, + keepHead: true, + sizeMapping: (c) => { + const DECAY_TIME = 200; + const DECAY_LENGTH = 10; + const t = Math.max(0, 1 - (performance.now() - c.pressure) / DECAY_TIME); + const l = + (DECAY_LENGTH - + Math.min(DECAY_LENGTH, c.totalLength - c.currentIndex)) / + DECAY_LENGTH; + + return Math.min(easeOut(l), easeOut(t)); + }, + fill: () => + this.state.theme === THEME.LIGHT + ? "rgba(0, 0, 0, 0.2)" + : "rgba(255, 255, 255, 0.2)", + }); + + onChangeEmitter = new Emitter< + [ + elements: readonly ExcalidrawElement[], + appState: AppState, + files: BinaryFiles, + ] + >(); + + onPointerDownEmitter = new Emitter< + [ + activeTool: AppState["activeTool"], + pointerDownState: PointerDownState, + event: React.PointerEvent, + ] + >(); + + onPointerUpEmitter = new Emitter< + [ + activeTool: AppState["activeTool"], + pointerDownState: PointerDownState, + event: PointerEvent, + ] + >(); + onUserFollowEmitter = new Emitter<[payload: OnUserFollowedPayload]>(); + onScrollChangeEmitter = new Emitter< + [scrollX: number, scrollY: number, zoom: AppState["zoom"]] + >(); + + missingPointerEventCleanupEmitter = new Emitter< + [event: PointerEvent | null] + >(); + onRemoveEventListenersEmitter = new Emitter<[]>(); + + constructor(props: AppProps) { + super(props); + const defaultAppState = getDefaultAppState(); + const { + excalidrawAPI, + viewModeEnabled = false, + zenModeEnabled = false, + gridModeEnabled = false, + objectsSnapModeEnabled = false, + theme = defaultAppState.theme, + name = `${t("labels.untitled")}-${getDateTime()}`, + } = props; + this.state = { + ...defaultAppState, + theme, + isLoading: true, + ...this.getCanvasOffsets(), + viewModeEnabled, + zenModeEnabled, + objectsSnapModeEnabled, + gridModeEnabled: gridModeEnabled ?? defaultAppState.gridModeEnabled, + name, + width: window.innerWidth, + height: window.innerHeight, + }; + + this.id = nanoid(); + this.library = new Library(this); + this.actionManager = new ActionManager( + this.syncActionResult, + () => this.state, + () => this.scene.getElementsIncludingDeleted(), + this, + ); + this.scene = new Scene(); + + this.canvas = document.createElement("canvas"); + this.rc = rough.canvas(this.canvas); + this.renderer = new Renderer(this.scene); + this.visibleElements = []; + + this.store = new Store(); + this.history = new History(); + + if (excalidrawAPI) { + const api: ExcalidrawImperativeAPI = { + updateScene: this.updateScene, + updateLibrary: this.library.updateLibrary, + addFiles: this.addFiles, + resetScene: this.resetScene, + getSceneElementsIncludingDeleted: this.getSceneElementsIncludingDeleted, + history: { + clear: this.resetHistory, + }, + scrollToContent: this.scrollToContent, + getSceneElements: this.getSceneElements, + getAppState: () => this.state, + getFiles: () => this.files, + getName: this.getName, + registerAction: (action: Action) => { + this.actionManager.registerAction(action); + }, + refresh: this.refresh, + setToast: this.setToast, + id: this.id, + setActiveTool: this.setActiveTool, + setCursor: this.setCursor, + resetCursor: this.resetCursor, + updateFrameRendering: this.updateFrameRendering, + toggleSidebar: this.toggleSidebar, + onChange: (cb) => this.onChangeEmitter.on(cb), + onPointerDown: (cb) => this.onPointerDownEmitter.on(cb), + onPointerUp: (cb) => this.onPointerUpEmitter.on(cb), + onScrollChange: (cb) => this.onScrollChangeEmitter.on(cb), + onUserFollow: (cb) => this.onUserFollowEmitter.on(cb), + } as const; + if (typeof excalidrawAPI === "function") { + excalidrawAPI(api); + } else { + console.error("excalidrawAPI should be a function!"); + } + } + + this.excalidrawContainerValue = { + container: this.excalidrawContainerRef.current, + id: this.id, + }; + + this.fonts = new Fonts(this.scene); + this.history = new History(); + + this.actionManager.registerAll(actions); + this.actionManager.registerAction( + createUndoAction(this.history, this.store), + ); + this.actionManager.registerAction( + createRedoAction(this.history, this.store), + ); + } + + private onWindowMessage(event: MessageEvent) { + if ( + event.origin !== "https://player.vimeo.com" && + event.origin !== "https://www.youtube.com" + ) { + return; + } + + let data = null; + try { + data = JSON.parse(event.data); + } catch (e) {} + if (!data) { + return; + } + + switch (event.origin) { + case "https://player.vimeo.com": + //Allowing for multiple instances of Excalidraw running in the window + if (data.method === "paused") { + let source: Window | null = null; + const iframes = document.body.querySelectorAll( + "iframe.excalidraw__embeddable", + ); + if (!iframes) { + break; + } + for (const iframe of iframes as NodeListOf) { + if (iframe.contentWindow === event.source) { + source = iframe.contentWindow; + } + } + source?.postMessage( + JSON.stringify({ + method: data.value ? "play" : "pause", + value: true, + }), + "*", + ); + } + break; + case "https://www.youtube.com": + if ( + data.event === "infoDelivery" && + data.info && + data.id && + typeof data.info.playerState === "number" + ) { + const id = data.id; + const playerState = data.info.playerState as number; + if ( + (Object.values(YOUTUBE_STATES) as number[]).includes(playerState) + ) { + YOUTUBE_VIDEO_STATES.set( + id, + playerState as ValueOf, + ); + } + } + break; + } + } + + private cacheEmbeddableRef( + element: ExcalidrawIframeLikeElement, + ref: HTMLIFrameElement | null, + ) { + if (ref) { + this.iFrameRefs.set(element.id, ref); + } + } + + /** + * Returns gridSize taking into account `gridModeEnabled`. + * If disabled, returns null. + */ + public getEffectiveGridSize = () => { + return ( + isGridModeEnabled(this) ? this.state.gridSize : null + ) as NullableGridSize; + }; + + private getHTMLIFrameElement( + element: ExcalidrawIframeLikeElement, + ): HTMLIFrameElement | undefined { + return this.iFrameRefs.get(element.id); + } + + private handleEmbeddableCenterClick(element: ExcalidrawIframeLikeElement) { + if ( + this.state.activeEmbeddable?.element === element && + this.state.activeEmbeddable?.state === "active" + ) { + return; + } + + // The delay serves two purposes + // 1. To prevent first click propagating to iframe on mobile, + // else the click will immediately start and stop the video + // 2. If the user double clicks the frame center to activate it + // without the delay youtube will immediately open the video + // in fullscreen mode + setTimeout(() => { + this.setState({ + activeEmbeddable: { element, state: "active" }, + selectedElementIds: { [element.id]: true }, + newElement: null, + selectionElement: null, + }); + }, 100); + + if (isIframeElement(element)) { + return; + } + + const iframe = this.getHTMLIFrameElement(element); + + if (!iframe?.contentWindow) { + return; + } + + if (iframe.src.includes("youtube")) { + const state = YOUTUBE_VIDEO_STATES.get(element.id); + if (!state) { + YOUTUBE_VIDEO_STATES.set(element.id, YOUTUBE_STATES.UNSTARTED); + iframe.contentWindow.postMessage( + JSON.stringify({ + event: "listening", + id: element.id, + }), + "*", + ); + } + switch (state) { + case YOUTUBE_STATES.PLAYING: + case YOUTUBE_STATES.BUFFERING: + iframe.contentWindow?.postMessage( + JSON.stringify({ + event: "command", + func: "pauseVideo", + args: "", + }), + "*", + ); + break; + default: + iframe.contentWindow?.postMessage( + JSON.stringify({ + event: "command", + func: "playVideo", + args: "", + }), + "*", + ); + } + } + + if (iframe.src.includes("player.vimeo.com")) { + iframe.contentWindow.postMessage( + JSON.stringify({ + method: "paused", //video play/pause in onWindowMessage handler + }), + "*", + ); + } + } + + private isIframeLikeElementCenter( + el: ExcalidrawIframeLikeElement | null, + event: React.PointerEvent | PointerEvent, + sceneX: number, + sceneY: number, + ) { + return ( + el && + !event.altKey && + !event.shiftKey && + !event.metaKey && + !event.ctrlKey && + (this.state.activeEmbeddable?.element !== el || + this.state.activeEmbeddable?.state === "hover" || + !this.state.activeEmbeddable) && + sceneX >= el.x + el.width / 3 && + sceneX <= el.x + (2 * el.width) / 3 && + sceneY >= el.y + el.height / 3 && + sceneY <= el.y + (2 * el.height) / 3 + ); + } + + private updateEmbedValidationStatus = ( + element: ExcalidrawEmbeddableElement, + status: boolean, + ) => { + this.embedsValidationStatus.set(element.id, status); + ShapeCache.delete(element); + }; + + private updateEmbeddables = () => { + const iframeLikes = new Set(); + + let updated = false; + this.scene.getNonDeletedElements().filter((element) => { + if (isEmbeddableElement(element)) { + iframeLikes.add(element.id); + if (!this.embedsValidationStatus.has(element.id)) { + updated = true; + + const validated = embeddableURLValidator( + element.link, + this.props.validateEmbeddable, + ); + + this.updateEmbedValidationStatus(element, validated); + } + } else if (isIframeElement(element)) { + iframeLikes.add(element.id); + } + return false; + }); + + if (updated) { + this.scene.triggerUpdate(); + } + + // GC + this.iFrameRefs.forEach((ref, id) => { + if (!iframeLikes.has(id)) { + this.iFrameRefs.delete(id); + } + }); + }; + + private renderEmbeddables() { + const scale = this.state.zoom.value; + const normalizedWidth = this.state.width; + const normalizedHeight = this.state.height; + + const embeddableElements = this.scene + .getNonDeletedElements() + .filter( + (el): el is Ordered> => + (isEmbeddableElement(el) && + this.embedsValidationStatus.get(el.id) === true) || + isIframeElement(el), + ); + + return ( + <> + {embeddableElements.map((el) => { + const { x, y } = sceneCoordsToViewportCoords( + { sceneX: el.x, sceneY: el.y }, + this.state, + ); + + const isVisible = isElementInViewport( + el, + normalizedWidth, + normalizedHeight, + this.state, + this.scene.getNonDeletedElementsMap(), + ); + const hasBeenInitialized = this.initializedEmbeds.has(el.id); + + if (isVisible && !hasBeenInitialized) { + this.initializedEmbeds.add(el.id); + } + const shouldRender = isVisible || hasBeenInitialized; + + if (!shouldRender) { + return null; + } + + let src: IframeData | null; + + if (isIframeElement(el)) { + src = null; + + const data: MagicGenerationData = (el.customData?.generationData ?? + this.magicGenerations.get(el.id)) || { + status: "error", + message: "No generation data", + code: "ERR_NO_GENERATION_DATA", + }; + + if (data.status === "done") { + const html = data.html; + src = { + intrinsicSize: { w: el.width, h: el.height }, + type: "document", + srcdoc: () => { + return html; + }, + } as const; + } else if (data.status === "pending") { + src = { + intrinsicSize: { w: el.width, h: el.height }, + type: "document", + srcdoc: () => { + return createSrcDoc(` + +
+ + + +
+
Generating...
+ `); + }, + } as const; + } else { + let message: string; + if (data.code === "ERR_GENERATION_INTERRUPTED") { + message = "Generation was interrupted..."; + } else { + message = data.message || "Generation failed"; + } + src = { + intrinsicSize: { w: el.width, h: el.height }, + type: "document", + srcdoc: () => { + return createSrcDoc(` + +

Error!

+

${message}

+ `); + }, + } as const; + } + } else { + src = getEmbedLink(toValidURL(el.link || "")); + } + + const isActive = + this.state.activeEmbeddable?.element === el && + this.state.activeEmbeddable?.state === "active"; + const isHovered = + this.state.activeEmbeddable?.element === el && + this.state.activeEmbeddable?.state === "hover"; + + return ( +
+
{ + if (!this.excalidrawContainerRef.current) { + return; + } + const container = this.excalidrawContainerRef.current; + const sh = container.scrollHeight; + const ch = container.clientHeight; + if (sh !== ch) { + container.style.height = `${sh}px`; + setTimeout(() => { + container.style.height = `100%`; + }); + } + }}*/ + className="excalidraw__embeddable-container__inner" + style={{ + width: isVisible ? `${el.width}px` : 0, + height: isVisible ? `${el.height}px` : 0, + transform: isVisible ? `rotate(${el.angle}rad)` : "none", + pointerEvents: isActive + ? POINTER_EVENTS.enabled + : POINTER_EVENTS.disabled, + }} + > + {isHovered && ( +
+ {t("buttons.embeddableInteractionButton")} +
+ )} +
+ {(isEmbeddableElement(el) + ? this.props.renderEmbeddable?.(el, this.state) + : null) ?? ( +