diff --git a/dev-docs/package.json b/dev-docs/package.json index 1e8745910..0aee8e01f 100644 --- a/dev-docs/package.json +++ b/dev-docs/package.json @@ -18,7 +18,7 @@ "@docusaurus/core": "2.2.0", "@docusaurus/preset-classic": "2.2.0", "@docusaurus/theme-live-codeblock": "2.2.0", - "@excalidraw/excalidraw": "0.15.0", + "@excalidraw/excalidraw": "0.15.2", "@mdx-js/react": "^1.6.22", "clsx": "^1.2.1", "docusaurus-plugin-sass": "0.2.3", diff --git a/dev-docs/yarn.lock b/dev-docs/yarn.lock index ee3d50cbf..6206a60e9 100644 --- a/dev-docs/yarn.lock +++ b/dev-docs/yarn.lock @@ -1631,10 +1631,10 @@ url-loader "^4.1.1" webpack "^5.73.0" -"@excalidraw/excalidraw@0.15.0": - version "0.15.0" - resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.0.tgz#47170de8d3ff006e9d09dfede2815682b0d4485b" - integrity sha512-PJmh1VcuRHG4l+Zgt9qhezxrJ16tYCZFZ8if5IEfmTL9A/7c5mXxY/qrPTqiGlVC7jYs+ciePXQ0YUDzfOfbzw== +"@excalidraw/excalidraw@0.15.2": + version "0.15.2" + resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2.tgz#7dba4f6e10c52015a007efb75a9fc1afe598574c" + integrity sha512-rTI02kgWSTXiUdIkBxt9u/581F3eXcqQgJdIxmz54TFtG3ughoxO5fr4t7Fr2LZIturBPqfocQHGKZ0t2KLKgw== "@hapi/hoek@^9.0.0": version "9.3.0" @@ -7159,9 +7159,9 @@ typescript@^4.7.4: integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== ua-parser-js@^0.7.30: - version "0.7.31" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" - integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ== + version "0.7.33" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532" + integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw== unescape@^1.0.1: version "1.0.1" diff --git a/package.json b/package.json index 5816786e3..57bdfe562 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ ] }, "dependencies": { - "@dwelle/tunnel-rat": "0.1.1", + "@radix-ui/react-tabs": "1.0.2", "@sentry/browser": "6.2.5", "@sentry/integrations": "6.2.5", "@testing-library/jest-dom": "5.16.2", @@ -51,7 +51,7 @@ "roughjs": "4.5.2", "sass": "1.51.0", "socket.io-client": "2.3.1", - "tunnel-rat": "0.1.0", + "tunnel-rat": "0.1.2", "workbox-background-sync": "^6.5.4", "workbox-broadcast-update": "^6.5.4", "workbox-cacheable-response": "^6.5.4", diff --git a/public/index.html b/public/index.html index a8633fc4d..f65e481f3 100644 --- a/public/index.html +++ b/public/index.html @@ -150,6 +150,14 @@ <% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %> + + + + <% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %> - <% } %> - - <% } %> @@ -244,5 +227,17 @@

Excalidraw

+ + + diff --git a/src/actions/actionClipboard.tsx b/src/actions/actionClipboard.tsx index 0b0f3c05a..a59804a0f 100644 --- a/src/actions/actionClipboard.tsx +++ b/src/actions/actionClipboard.tsx @@ -18,7 +18,7 @@ export const actionCopy = register({ perform: (elements, appState, _, app) => { const selectedElements = getSelectedElements(elements, appState, true); - copyToClipboard(selectedElements, appState, app.files); + copyToClipboard(selectedElements, app.files); return { commitToHistory: false, diff --git a/src/actions/actionProperties.tsx b/src/actions/actionProperties.tsx index ed714816b..382e964b9 100644 --- a/src/actions/actionProperties.tsx +++ b/src/actions/actionProperties.tsx @@ -84,7 +84,7 @@ import { isSomeElementSelected, } from "../scene"; import { hasStrokeColor } from "../scene/comparisons"; -import { arrayToMap } from "../utils"; +import { arrayToMap, getShortcutKey } from "../utils"; import { register } from "./register"; const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1; @@ -314,9 +314,9 @@ export const actionChangeFillStyle = register({ }, PanelComponent: ({ elements, appState, updateData }) => { const selectedElements = getSelectedElements(elements, appState); - const allElementsZigZag = selectedElements.every( - (el) => el.fillStyle === "zigzag", - ); + const allElementsZigZag = + selectedElements.length > 0 && + selectedElements.every((el) => el.fillStyle === "zigzag"); return (
@@ -326,7 +326,9 @@ export const actionChangeFillStyle = register({ options={[ { value: "hachure", - text: t("labels.hachure"), + text: `${ + allElementsZigZag ? t("labels.zigzag") : t("labels.hachure") + } (${getShortcutKey("Alt-Click")})`, icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon, active: allElementsZigZag ? true : undefined, }, diff --git a/src/analytics.ts b/src/analytics.ts index 1e9a429b6..e952bc680 100644 --- a/src/analytics.ts +++ b/src/analytics.ts @@ -20,9 +20,20 @@ export const trackEvent = ( }); } - // MATOMO event tracking _paq must be same as the one in index.html - if (window._paq) { - window._paq.push(["trackEvent", category, action, label, value]); + if (window.sa_event) { + window.sa_event(action, { + category, + label, + value, + }); + } + + if (window.fathom) { + window.fathom.trackEvent(action, { + category, + label, + value, + }); } } catch (error) { console.error("error during analytics", error); diff --git a/src/appState.ts b/src/appState.ts index ccdf2d692..20232e15f 100644 --- a/src/appState.ts +++ b/src/appState.ts @@ -59,7 +59,7 @@ export const getDefaultAppState = (): Omit< fileHandle: null, gridSize: null, isBindingEnabled: true, - isSidebarDocked: false, + defaultSidebarDockedPreference: false, isLoading: false, isResizing: false, isRotating: false, @@ -151,7 +151,11 @@ const APP_STATE_STORAGE_CONF = (< gridSize: { browser: true, export: true, server: true }, height: { browser: false, export: false, server: false }, isBindingEnabled: { browser: false, export: false, server: false }, - isSidebarDocked: { browser: true, export: false, server: false }, + defaultSidebarDockedPreference: { + browser: true, + export: false, + server: false, + }, isLoading: { browser: false, export: false, server: false }, isResizing: { browser: false, export: false, server: false }, isRotating: { browser: false, export: false, server: false }, diff --git a/src/clipboard.ts b/src/clipboard.ts index 5f7950c53..c0f5844dd 100644 --- a/src/clipboard.ts +++ b/src/clipboard.ts @@ -2,12 +2,12 @@ import { ExcalidrawElement, NonDeletedExcalidrawElement, } from "./element/types"; -import { AppState, BinaryFiles } from "./types"; +import { BinaryFiles } from "./types"; import { SVG_EXPORT_TAG } from "./scene/export"; import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts"; import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants"; import { isInitializedImageElement } from "./element/typeChecks"; -import { isPromiseLike } from "./utils"; +import { isPromiseLike, isTestEnv } from "./utils"; type ElementsClipboard = { type: typeof EXPORT_DATA_TYPES.excalidrawClipboard; @@ -55,24 +55,40 @@ const clipboardContainsElements = ( export const copyToClipboard = async ( elements: readonly NonDeletedExcalidrawElement[], - appState: AppState, files: BinaryFiles | null, ) => { + let foundFile = false; + + const _files = elements.reduce((acc, element) => { + if (isInitializedImageElement(element)) { + foundFile = true; + if (files && files[element.fileId]) { + acc[element.fileId] = files[element.fileId]; + } + } + return acc; + }, {} as BinaryFiles); + + if (foundFile && !files) { + console.warn( + "copyToClipboard: attempting to file element(s) without providing associated `files` object.", + ); + } + // select binded text elements when copying const contents: ElementsClipboard = { type: EXPORT_DATA_TYPES.excalidrawClipboard, elements, - files: files - ? elements.reduce((acc, element) => { - if (isInitializedImageElement(element) && files[element.fileId]) { - acc[element.fileId] = files[element.fileId]; - } - return acc; - }, {} as BinaryFiles) - : undefined, + files: files ? _files : undefined, }; const json = JSON.stringify(contents); + + if (isTestEnv()) { + return json; + } + CLIPBOARD = json; + try { PREFER_APP_CLIPBOARD = false; await copyTextToSystemClipboard(json); diff --git a/src/components/App.tsx b/src/components/App.tsx index 68d6d0672..7f73a9d7b 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -60,6 +60,7 @@ import { ENV, EVENT, GRID_SIZE, + IMAGE_MIME_TYPES, IMAGE_RENDER_TIMEOUT, isAndroid, isBrave, @@ -209,6 +210,8 @@ import { PointerDownState, SceneData, Device, + SidebarName, + SidebarTabName, } from "../types"; import { debounce, @@ -298,6 +301,9 @@ import { activeConfirmDialogAtom } from "./ActiveConfirmDialog"; import { actionWrapTextInContainer } from "../actions/actionBoundText"; import BraveMeasureTextError from "./BraveMeasureTextError"; +const AppContext = React.createContext(null!); +const AppPropsContext = React.createContext(null!); + const deviceContextInitialValue = { isSmScreen: false, isMobile: false, @@ -339,6 +345,8 @@ const ExcalidrawActionManagerContext = React.createContext( ); ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext"; +export const useApp = () => useContext(AppContext); +export const useAppProps = () => useContext(AppPropsContext); export const useDevice = () => useContext(DeviceContext); export const useExcalidrawContainer = () => useContext(ExcalidrawContainerContext); @@ -399,7 +407,7 @@ class App extends React.Component { private nearestScrollableContainer: HTMLElement | Document | undefined; public library: AppClassProperties["library"]; public libraryItemsFromStorage: LibraryItems | undefined; - private id: string; + public id: string; private history: History; private excalidrawContainerValue: { container: HTMLDivElement | null; @@ -437,7 +445,7 @@ class App extends React.Component { width: window.innerWidth, height: window.innerHeight, showHyperlinkPopup: false, - isSidebarDocked: false, + defaultSidebarDockedPreference: false, }; this.id = nanoid(); @@ -468,7 +476,7 @@ class App extends React.Component { setActiveTool: this.setActiveTool, setCursor: this.setCursor, resetCursor: this.resetCursor, - toggleMenu: this.toggleMenu, + toggleSidebar: this.toggleSidebar, } as const; if (typeof excalidrawRef === "function") { excalidrawRef(api); @@ -576,101 +584,91 @@ class App extends React.Component { this.props.handleKeyboardGlobally ? undefined : this.onKeyDown } > - - - - - - - - this.addElementsFromPasteOrLibrary({ - elements, - position: "center", - files: null, - }) - } - langCode={getLanguage().code} - renderTopRightUI={renderTopRightUI} - renderCustomStats={renderCustomStats} - renderCustomSidebar={this.props.renderSidebar} - showExitZenModeBtn={ - typeof this.props?.zenModeEnabled === "undefined" && - this.state.zenModeEnabled - } - libraryReturnUrl={this.props.libraryReturnUrl} - UIOptions={this.props.UIOptions} - focusContainer={this.focusContainer} - library={this.library} - id={this.id} - onImageAction={this.onImageAction} - renderWelcomeScreen={ - !this.state.isLoading && - this.state.showWelcomeScreen && - this.state.activeTool.type === "selection" && - !this.scene.getElementsIncludingDeleted().length - } + + + + + + + - {this.props.children} - -
-
- {selectedElement.length === 1 && - !this.state.contextMenu && - this.state.showHyperlinkPopup && ( - + - )} - {this.state.toast !== null && ( - this.setToast(null)} - duration={this.state.toast.duration} - closable={this.state.toast.closable} - /> - )} - {this.state.contextMenu && ( - - )} -
{this.renderCanvas()}
- - {" "} - - - - + actionManager={this.actionManager} + elements={this.scene.getNonDeletedElements()} + onLockToggle={this.toggleLock} + onPenModeToggle={this.togglePenMode} + onHandToolToggle={this.onHandToolToggle} + langCode={getLanguage().code} + renderTopRightUI={renderTopRightUI} + renderCustomStats={renderCustomStats} + showExitZenModeBtn={ + typeof this.props?.zenModeEnabled === "undefined" && + this.state.zenModeEnabled + } + UIOptions={this.props.UIOptions} + onImageAction={this.onImageAction} + renderWelcomeScreen={ + !this.state.isLoading && + this.state.showWelcomeScreen && + this.state.activeTool.type === "selection" && + !this.scene.getElementsIncludingDeleted().length + } + > + {this.props.children} +
+
+
+ {selectedElement.length === 1 && + !this.state.contextMenu && + this.state.showHyperlinkPopup && ( + + )} + {this.state.toast !== null && ( + this.setToast(null)} + duration={this.state.toast.duration} + closable={this.state.toast.closable} + /> + )} + {this.state.contextMenu && ( + + )} +
{this.renderCanvas()}
+ + {" "} + + + + + +
); } public focusContainer: AppClassProperties["focusContainer"] = () => { - if (this.props.autoFocus) { - this.excalidrawContainerRef.current?.focus(); - } + this.excalidrawContainerRef.current?.focus(); }; public getSceneElementsIncludingDeleted = () => { @@ -681,6 +679,14 @@ class App extends React.Component { return this.scene.getNonDeletedElements(); }; + public onInsertElements = (elements: readonly ExcalidrawElement[]) => { + this.addElementsFromPasteOrLibrary({ + elements, + position: "center", + files: null, + }); + }; + private syncActionResult = withBatchedUpdates( (actionResult: ActionResult) => { if (this.unmounted || actionResult === false) { @@ -950,7 +956,7 @@ class App extends React.Component { this.scene.addCallback(this.onSceneUpdated); this.addEventListeners(); - if (this.excalidrawContainerRef.current) { + if (this.props.autoFocus && this.excalidrawContainerRef.current) { this.focusContainer(); } @@ -1589,6 +1595,7 @@ class App extends React.Component { elements: data.elements, files: data.files || null, position: "cursor", + retainSeed: isPlainPaste, }); } else if (data.text) { this.addTextFromPaste(data.text, isPlainPaste); @@ -1602,6 +1609,7 @@ class App extends React.Component { elements: readonly ExcalidrawElement[]; files: BinaryFiles | null; position: { clientX: number; clientY: number } | "cursor" | "center"; + retainSeed?: boolean; }) => { const elements = restoreElements(opts.elements, null); const [minX, minY, maxX, maxY] = getCommonBounds(elements); @@ -1639,6 +1647,9 @@ class App extends React.Component { y: element.y + gridY - minY, }); }), + { + randomizeSeed: !opts.retainSeed, + }, ); const nextElements = [ @@ -1673,7 +1684,7 @@ class App extends React.Component { openSidebar: this.state.openSidebar && this.device.canDeviceFitSidebar && - this.state.isSidebarDocked + this.state.defaultSidebarDockedPreference ? this.state.openSidebar : null, selectedElementIds: newElements.reduce( @@ -2011,30 +2022,24 @@ class App extends React.Component { /** * @returns whether the menu was toggled on or off */ - public toggleMenu = ( - type: "library" | "customSidebar", - force?: boolean, - ): boolean => { - if (type === "customSidebar" && !this.props.renderSidebar) { - console.warn( - `attempting to toggle "customSidebar", but no "props.renderSidebar" is defined`, - ); - return false; + public toggleSidebar = ({ + name, + tab, + force, + }: { + name: SidebarName; + tab?: SidebarTabName; + force?: boolean; + }): boolean => { + let nextName; + if (force === undefined) { + nextName = this.state.openSidebar?.name === name ? null : name; + } else { + nextName = force ? name : null; } + this.setState({ openSidebar: nextName ? { name: nextName, tab } : null }); - if (type === "library" || type === "customSidebar") { - let nextValue; - if (force === undefined) { - nextValue = this.state.openSidebar === type ? null : type; - } else { - nextValue = force ? type : null; - } - this.setState({ openSidebar: nextValue }); - - return !!nextValue; - } - - return false; + return !!nextName; }; private updateCurrentCursorPosition = withBatchedUpdates( @@ -2744,6 +2749,7 @@ class App extends React.Component { containerId: shouldBindToContainer ? container?.id : undefined, groupIds: container?.groupIds ?? [], lineHeight, + angle: container?.angle ?? 0, }); if (!existingTextElement && shouldBindToContainer && container) { @@ -4719,7 +4725,12 @@ class App extends React.Component { pointerDownState.drag.hasOccurred = true; // prevent dragging even if we're no longer holding cmd/ctrl otherwise // it would have weird results (stuff jumping all over the screen) - if (selectedElements.length > 0 && !pointerDownState.withCmdOrCtrl) { + // Checking for editingElement to avoid jump while editing on mobile #6503 + if ( + selectedElements.length > 0 && + !pointerDownState.withCmdOrCtrl && + !this.state.editingElement + ) { const [dragX, dragY] = getGridPoint( pointerCoords.x - pointerDownState.drag.offset.x, pointerCoords.y - pointerDownState.drag.offset.y, @@ -5742,7 +5753,9 @@ class App extends React.Component { const imageFile = await fileOpen({ description: "Image", - extensions: ["jpg", "png", "svg", "gif"], + extensions: Object.keys( + IMAGE_MIME_TYPES, + ) as (keyof typeof IMAGE_MIME_TYPES)[], }); const imageElement = this.createImageElement({ diff --git a/src/components/BraveMeasureTextError.tsx b/src/components/BraveMeasureTextError.tsx index 8a4a71e4f..1932d7a29 100644 --- a/src/components/BraveMeasureTextError.tsx +++ b/src/components/BraveMeasureTextError.tsx @@ -1,39 +1,40 @@ -import { t } from "../i18n"; +import Trans from "./Trans"; + const BraveMeasureTextError = () => { return (

- {t("errors.brave_measure_text_error.start")}   - - {t("errors.brave_measure_text_error.aggressive_block_fingerprint")} - {" "} - {t("errors.brave_measure_text_error.setting_enabled")}. -
-
- {t("errors.brave_measure_text_error.break")}{" "} - - {t("errors.brave_measure_text_error.text_elements")} - {" "} - {t("errors.brave_measure_text_error.in_your_drawings")}. + {el}} + />

- {t("errors.brave_measure_text_error.strongly_recommend")}{" "} - - {" "} - {t("errors.brave_measure_text_error.steps")} - {" "} - {t("errors.brave_measure_text_error.how")}. + {el}} + />

- {t("errors.brave_measure_text_error.disable_setting")}{" "} - - {t("errors.brave_measure_text_error.issue")} - {" "} - {t("errors.brave_measure_text_error.write")}{" "} - - {t("errors.brave_measure_text_error.discord")} - - . + ( + + {el} + + )} + /> +

+

+ ( + + {el} + + )} + discordLink={(el) => {el}.} + />

); diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 3303c3ebf..bf548d72f 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,8 +1,12 @@ +import clsx from "clsx"; +import { composeEventHandlers } from "../utils"; import "./Button.scss"; interface ButtonProps extends React.HTMLAttributes { type?: "button" | "submit" | "reset"; onSelect: () => any; + /** whether button is in active state */ + selected?: boolean; children: React.ReactNode; className?: string; } @@ -15,18 +19,18 @@ interface ButtonProps extends React.HTMLAttributes { export const Button = ({ type = "button", onSelect, + selected, children, className = "", ...rest }: ButtonProps) => { return (
@@ -334,21 +325,21 @@ const LayerUI = ({ }; const renderSidebars = () => { - return appState.openSidebar === "customSidebar" ? ( - renderCustomSidebar?.() || null - ) : appState.openSidebar === "library" ? ( - { + trackEvent( + "sidebar", + `toggleDock (${docked ? "dock" : "undock"})`, + `(${device.isMobile ? "mobile" : "desktop"})`, + ); + }} /> - ) : null; + ); }; - const [hostSidebarCounters] = useAtom(hostSidebarCountersAtom, jotaiScope); + const isSidebarDocked = useAtomValue(isSidebarDockedAtom, jotaiScope); const layerUIJSX = ( <> @@ -358,8 +349,25 @@ const LayerUI = ({ {children} {/* render component fallbacks. Can be rendered anywhere as they'll be tunneled away. We only render tunneled components that actually - have defaults when host do not render anything. */} + have defaults when host do not render anything. */} + { + if (open) { + trackEvent( + "sidebar", + `${DEFAULT_SIDEBAR.name} (open)`, + `button (${device.isMobile ? "mobile" : "desktop"})`, + ); + } + }} + tab={DEFAULT_SIDEBAR.defaultTab} + > + {t("toolBar.library")} + {/* ------------------------------------------------------------------ */} {appState.isLoading && } @@ -382,7 +390,6 @@ const LayerUI = ({ setAppState({ pasteDialog: { shown: false, data: null }, @@ -410,7 +417,6 @@ const LayerUI = ({ renderWelcomeScreen={renderWelcomeScreen} /> )} - {!device.isMobile && ( <>
- {renderWelcomeScreen && } + {renderWelcomeScreen && } {renderFixedSideContainer()}