From 225db4a7805befe009fe055fc2ef5daedd6c04f9 Mon Sep 17 00:00:00 2001 From: kj_sh604 Date: Sun, 15 Mar 2026 16:19:35 -0400 Subject: refactor: examples/ --- examples/with-nextjs/.gitignore | 39 + examples/with-nextjs/README.md | 36 + examples/with-nextjs/next.config.js | 12 + examples/with-nextjs/package.json | 25 + examples/with-nextjs/public/images/doremon.png | Bin 0 -> 201946 bytes examples/with-nextjs/public/images/excalibot.png | Bin 0 -> 30330 bytes examples/with-nextjs/public/images/pika.jpeg | Bin 0 -> 6250 bytes examples/with-nextjs/public/images/rocket.jpeg | Bin 0 -> 40368 bytes examples/with-nextjs/src/app/favicon.ico | Bin 0 -> 25931 bytes examples/with-nextjs/src/app/layout.tsx | 11 + examples/with-nextjs/src/app/page.tsx | 26 + examples/with-nextjs/src/common.scss | 15 + examples/with-nextjs/src/excalidrawWrapper.tsx | 22 + .../with-nextjs/src/pages/excalidraw-in-pages.tsx | 22 + examples/with-nextjs/tsconfig.json | 28 + examples/with-nextjs/vercel.json | 3 + examples/with-nextjs/yarn.lock | 252 ++++++ .../with-script-in-browser/.codesandbox/Dockerfile | 5 + .../with-script-in-browser/.codesandbox/tasks.json | 35 + examples/with-script-in-browser/.gitignore | 2 + .../components/CustomFooter.tsx | 73 ++ .../components/ExampleApp.scss | 92 ++ .../components/ExampleApp.tsx | 961 ++++++++++++++++++++ .../components/MobileFooter.tsx | 28 + .../components/sidebar/ExampleSidebar.scss | 66 ++ .../components/sidebar/ExampleSidebar.tsx | 31 + examples/with-script-in-browser/index.html | 31 + examples/with-script-in-browser/index.tsx | 28 + examples/with-script-in-browser/initialData.tsx | 994 +++++++++++++++++++++ examples/with-script-in-browser/package.json | 21 + .../public/images/doremon.png | Bin 0 -> 201946 bytes .../public/images/excalibot.png | Bin 0 -> 30330 bytes .../with-script-in-browser/public/images/pika.jpeg | Bin 0 -> 6250 bytes .../public/images/rocket.jpeg | Bin 0 -> 40368 bytes examples/with-script-in-browser/tsconfig.json | 9 + examples/with-script-in-browser/utils.ts | 145 +++ examples/with-script-in-browser/vercel.json | 5 + examples/with-script-in-browser/vite.config.mts | 19 + 38 files changed, 3036 insertions(+) create mode 100644 examples/with-nextjs/.gitignore create mode 100644 examples/with-nextjs/README.md create mode 100644 examples/with-nextjs/next.config.js create mode 100644 examples/with-nextjs/package.json create mode 100644 examples/with-nextjs/public/images/doremon.png create mode 100644 examples/with-nextjs/public/images/excalibot.png create mode 100644 examples/with-nextjs/public/images/pika.jpeg create mode 100644 examples/with-nextjs/public/images/rocket.jpeg create mode 100644 examples/with-nextjs/src/app/favicon.ico create mode 100644 examples/with-nextjs/src/app/layout.tsx create mode 100644 examples/with-nextjs/src/app/page.tsx create mode 100644 examples/with-nextjs/src/common.scss create mode 100644 examples/with-nextjs/src/excalidrawWrapper.tsx create mode 100644 examples/with-nextjs/src/pages/excalidraw-in-pages.tsx create mode 100644 examples/with-nextjs/tsconfig.json create mode 100644 examples/with-nextjs/vercel.json create mode 100644 examples/with-nextjs/yarn.lock create mode 100644 examples/with-script-in-browser/.codesandbox/Dockerfile create mode 100644 examples/with-script-in-browser/.codesandbox/tasks.json create mode 100644 examples/with-script-in-browser/.gitignore create mode 100644 examples/with-script-in-browser/components/CustomFooter.tsx create mode 100644 examples/with-script-in-browser/components/ExampleApp.scss create mode 100644 examples/with-script-in-browser/components/ExampleApp.tsx create mode 100644 examples/with-script-in-browser/components/MobileFooter.tsx create mode 100644 examples/with-script-in-browser/components/sidebar/ExampleSidebar.scss create mode 100644 examples/with-script-in-browser/components/sidebar/ExampleSidebar.tsx create mode 100644 examples/with-script-in-browser/index.html create mode 100644 examples/with-script-in-browser/index.tsx create mode 100644 examples/with-script-in-browser/initialData.tsx create mode 100644 examples/with-script-in-browser/package.json create mode 100644 examples/with-script-in-browser/public/images/doremon.png create mode 100644 examples/with-script-in-browser/public/images/excalibot.png create mode 100644 examples/with-script-in-browser/public/images/pika.jpeg create mode 100644 examples/with-script-in-browser/public/images/rocket.jpeg create mode 100644 examples/with-script-in-browser/tsconfig.json create mode 100644 examples/with-script-in-browser/utils.ts create mode 100644 examples/with-script-in-browser/vercel.json create mode 100644 examples/with-script-in-browser/vite.config.mts (limited to 'examples') diff --git a/examples/with-nextjs/.gitignore b/examples/with-nextjs/.gitignore new file mode 100644 index 0000000..8f73eef --- /dev/null +++ b/examples/with-nextjs/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# copied assets +public/**/*.woff2 \ No newline at end of file diff --git a/examples/with-nextjs/README.md b/examples/with-nextjs/README.md new file mode 100644 index 0000000..9e8d9b9 --- /dev/null +++ b/examples/with-nextjs/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3005) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/examples/with-nextjs/next.config.js b/examples/with-nextjs/next.config.js new file mode 100644 index 0000000..701438e --- /dev/null +++ b/examples/with-nextjs/next.config.js @@ -0,0 +1,12 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + distDir: "build", + typescript: { + // The ts config doesn't work with `jsx: preserve" and if updated to `react-jsx` it gets ovewritten by next js throwing ts errors hence I am ignoring build errors until this is fixed. + ignoreBuildErrors: true, + }, + // This is needed as in pages router the code for importing types throws error as its outside next js app + transpilePackages: ["../"], +}; + +module.exports = nextConfig; diff --git a/examples/with-nextjs/package.json b/examples/with-nextjs/package.json new file mode 100644 index 0000000..ee8e555 --- /dev/null +++ b/examples/with-nextjs/package.json @@ -0,0 +1,25 @@ +{ + "name": "with-nextjs", + "version": "0.1.0", + "private": true, + "scripts": { + "build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm && yarn copy:assets", + "copy:assets": "cp -r ../../packages/excalidraw/dist/prod/fonts ./public", + "dev": "yarn build:workspace && next dev -p 3005", + "build": "yarn build:workspace && next build", + "start": "next start -p 3006", + "lint": "next lint" + }, + "dependencies": { + "next": "14.1", + "react": "19.0.0", + "react-dom": "19.0.0" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "19.0.10", + "@types/react-dom": "19.0.4", + "path2d-polyfill": "2.0.1", + "typescript": "^5" + } +} diff --git a/examples/with-nextjs/public/images/doremon.png b/examples/with-nextjs/public/images/doremon.png new file mode 100644 index 0000000..36208a4 Binary files /dev/null and b/examples/with-nextjs/public/images/doremon.png differ diff --git a/examples/with-nextjs/public/images/excalibot.png b/examples/with-nextjs/public/images/excalibot.png new file mode 100644 index 0000000..7928ec3 Binary files /dev/null and b/examples/with-nextjs/public/images/excalibot.png differ diff --git a/examples/with-nextjs/public/images/pika.jpeg b/examples/with-nextjs/public/images/pika.jpeg new file mode 100644 index 0000000..455ed52 Binary files /dev/null and b/examples/with-nextjs/public/images/pika.jpeg differ diff --git a/examples/with-nextjs/public/images/rocket.jpeg b/examples/with-nextjs/public/images/rocket.jpeg new file mode 100644 index 0000000..f17a74b Binary files /dev/null and b/examples/with-nextjs/public/images/rocket.jpeg differ diff --git a/examples/with-nextjs/src/app/favicon.ico b/examples/with-nextjs/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/examples/with-nextjs/src/app/favicon.ico differ diff --git a/examples/with-nextjs/src/app/layout.tsx b/examples/with-nextjs/src/app/layout.tsx new file mode 100644 index 0000000..225b603 --- /dev/null +++ b/examples/with-nextjs/src/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/examples/with-nextjs/src/app/page.tsx b/examples/with-nextjs/src/app/page.tsx new file mode 100644 index 0000000..191aca1 --- /dev/null +++ b/examples/with-nextjs/src/app/page.tsx @@ -0,0 +1,26 @@ +import dynamic from "next/dynamic"; +import Script from "next/script"; +import "../common.scss"; + +// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically +// with ssr false +const ExcalidrawWithClientOnly = dynamic( + async () => (await import("../excalidrawWrapper")).default, + { + ssr: false, + }, +); + +export default function Page() { + return ( + <> + Switch to Pages router +

App Router

+ + {/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */} + + + ); +} diff --git a/examples/with-nextjs/src/common.scss b/examples/with-nextjs/src/common.scss new file mode 100644 index 0000000..456bc76 --- /dev/null +++ b/examples/with-nextjs/src/common.scss @@ -0,0 +1,15 @@ +* { + box-sizing: border-box; + font-family: sans-serif; +} + +a { + color: #1c7ed6; + font-size: 20px; + text-decoration: none; + font-weight: 500; +} + +.page-title { + text-align: center; +} diff --git a/examples/with-nextjs/src/excalidrawWrapper.tsx b/examples/with-nextjs/src/excalidrawWrapper.tsx new file mode 100644 index 0000000..b4c45fa --- /dev/null +++ b/examples/with-nextjs/src/excalidrawWrapper.tsx @@ -0,0 +1,22 @@ +"use client"; +import * as excalidrawLib from "@excalidraw/excalidraw"; +import { Excalidraw } from "@excalidraw/excalidraw"; +import App from "../../with-script-in-browser/components/ExampleApp"; + +import "@excalidraw/excalidraw/index.css"; + +const ExcalidrawWrapper: React.FC = () => { + return ( + <> + {}} + excalidrawLib={excalidrawLib} + > + + + + ); +}; + +export default ExcalidrawWrapper; diff --git a/examples/with-nextjs/src/pages/excalidraw-in-pages.tsx b/examples/with-nextjs/src/pages/excalidraw-in-pages.tsx new file mode 100644 index 0000000..527a346 --- /dev/null +++ b/examples/with-nextjs/src/pages/excalidraw-in-pages.tsx @@ -0,0 +1,22 @@ +import dynamic from "next/dynamic"; +import "../common.scss"; + +// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically +// with ssr false +const Excalidraw = dynamic( + async () => (await import("../excalidrawWrapper")).default, + { + ssr: false, + }, +); + +export default function Page() { + return ( + <> + Switch to App router +

Pages Router

+ {/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */} + + + ); +} diff --git a/examples/with-nextjs/tsconfig.json b/examples/with-nextjs/tsconfig.json new file mode 100644 index 0000000..09ae73d --- /dev/null +++ b/examples/with-nextjs/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + }, + "forceConsistentCasingInFileNames": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "build/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/with-nextjs/vercel.json b/examples/with-nextjs/vercel.json new file mode 100644 index 0000000..bd885f4 --- /dev/null +++ b/examples/with-nextjs/vercel.json @@ -0,0 +1,3 @@ +{ + "outputDirectory": "build" +} diff --git a/examples/with-nextjs/yarn.lock b/examples/with-nextjs/yarn.lock new file mode 100644 index 0000000..0072235 --- /dev/null +++ b/examples/with-nextjs/yarn.lock @@ -0,0 +1,252 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@excalidraw/excalidraw@workspace:^": + version "0.17.2" + resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.17.2.tgz#9a636a1e6bb3c88c5883347d3a7e75e9cce8ab96" + integrity sha512-7pqUWD8+mPjDhF4XxG3gw4rvE2JGaLW3Vss5UZfTbITPxAtFaGEc1K081bncitnaYhUwN9ENJE0i87QB3poDwQ== + +"@next/env@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.4.tgz#d5cda0c4a862d70ae760e58c0cd96a8899a2e49a" + integrity sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ== + +"@next/swc-darwin-arm64@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz#27b1854c2cd04eb1d5e75081a1a792ad91526618" + integrity sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg== + +"@next/swc-darwin-x64@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz#9940c449e757d0ee50bb9e792d2600cc08a3eb3b" + integrity sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw== + +"@next/swc-linux-arm64-gnu@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz#0eafd27c8587f68ace7b4fa80695711a8434de21" + integrity sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w== + +"@next/swc-linux-arm64-musl@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz#2b0072adb213f36dada5394ea67d6e82069ae7dd" + integrity sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ== + +"@next/swc-linux-x64-gnu@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz#68c67d20ebc8e3f6ced6ff23a4ba2a679dbcec32" + integrity sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A== + +"@next/swc-linux-x64-musl@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz#67cd81b42fb2caf313f7992fcf6d978af55a1247" + integrity sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw== + +"@next/swc-win32-arm64-msvc@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz#be06585906b195d755ceda28f33c633e1443f1a3" + integrity sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w== + +"@next/swc-win32-ia32-msvc@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz#e76cabefa9f2d891599c3d85928475bd8d3f6600" + integrity sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg== + +"@next/swc-win32-x64-msvc@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz#e74892f1a9ccf41d3bf5979ad6d3d77c07b9cba1" + integrity sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A== + +"@swc/helpers@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" + integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw== + dependencies: + tslib "^2.4.0" + +"@types/node@^20": + version "20.11.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.0.tgz#8e0b99e70c0c1ade1a86c4a282f7b7ef87c9552f" + integrity sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ== + dependencies: + undici-types "~5.26.4" + +"@types/prop-types@*": + version "15.7.11" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563" + integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng== + +"@types/react-dom@^18": + version "18.2.18" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd" + integrity sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^18": + version "18.2.47" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.47.tgz#85074b27ab563df01fbc3f68dc64bf7050b0af40" + integrity sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.8" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" + integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== + +busboy@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + +caniuse-lite@^1.0.30001406: + version "1.0.30001576" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz#893be772cf8ee6056d6c1e2d07df365b9ec0a5c4" + integrity sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg== + +client-only@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" + integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== + +csstype@^3.0.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +graceful-fs@^4.1.2, graceful-fs@^4.2.11: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +loose-envify@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +nanoid@^3.3.6: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +next@14.0.4: + version "14.0.4" + resolved "https://registry.yarnpkg.com/next/-/next-14.0.4.tgz#bf00b6f835b20d10a5057838fa2dfced1d0d84dc" + integrity sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA== + dependencies: + "@next/env" "14.0.4" + "@swc/helpers" "0.5.2" + busboy "1.6.0" + caniuse-lite "^1.0.30001406" + graceful-fs "^4.2.11" + postcss "8.4.31" + styled-jsx "5.1.1" + watchpack "2.4.0" + optionalDependencies: + "@next/swc-darwin-arm64" "14.0.4" + "@next/swc-darwin-x64" "14.0.4" + "@next/swc-linux-arm64-gnu" "14.0.4" + "@next/swc-linux-arm64-musl" "14.0.4" + "@next/swc-linux-x64-gnu" "14.0.4" + "@next/swc-linux-x64-musl" "14.0.4" + "@next/swc-win32-arm64-msvc" "14.0.4" + "@next/swc-win32-ia32-msvc" "14.0.4" + "@next/swc-win32-x64-msvc" "14.0.4" + +path2d-polyfill@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz#24c554a738f42700d6961992bf5f1049672f2391" + integrity sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +postcss@8.4.31: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +react-dom@^18: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + +react@^18: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + +styled-jsx@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f" + integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw== + dependencies: + client-only "0.0.1" + +tslib@^2.4.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + +typescript@^5: + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +watchpack@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" diff --git a/examples/with-script-in-browser/.codesandbox/Dockerfile b/examples/with-script-in-browser/.codesandbox/Dockerfile new file mode 100644 index 0000000..fd5b38d --- /dev/null +++ b/examples/with-script-in-browser/.codesandbox/Dockerfile @@ -0,0 +1,5 @@ +FROM node:18-bullseye + +# Vite wants to open the browser using `open`, so we +# need to install those utils. +RUN apt update -y && apt install -y xdg-utils diff --git a/examples/with-script-in-browser/.codesandbox/tasks.json b/examples/with-script-in-browser/.codesandbox/tasks.json new file mode 100644 index 0000000..990c21a --- /dev/null +++ b/examples/with-script-in-browser/.codesandbox/tasks.json @@ -0,0 +1,35 @@ +{ + // These tasks will run in order when initializing your CodeSandbox project. + "setupTasks": [ + { + "name": "Install Dependencies", + "command": "yarn install" + } + ], + + // These tasks can be run from CodeSandbox. Running one will open a log in the app. + "tasks": { + "build": { + "name": "Build", + "command": "yarn build", + "runAtStart": false + }, + "start": { + "name": "Start Example", + "command": "yarn start", + "runAtStart": true, + "preview": { + "port": 3001 + } + }, + "install-deps": { + "name": "Install Dependencies", + "command": "yarn install", + "restartOn": { + "files": ["yarn.lock"], + "branch": false, + "resume": false + } + } + } +} diff --git a/examples/with-script-in-browser/.gitignore b/examples/with-script-in-browser/.gitignore new file mode 100644 index 0000000..f1b2f5e --- /dev/null +++ b/examples/with-script-in-browser/.gitignore @@ -0,0 +1,2 @@ +# copied assets +public/**/*.woff2 \ No newline at end of file diff --git a/examples/with-script-in-browser/components/CustomFooter.tsx b/examples/with-script-in-browser/components/CustomFooter.tsx new file mode 100644 index 0000000..72fd199 --- /dev/null +++ b/examples/with-script-in-browser/components/CustomFooter.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import type * as TExcalidraw from "@excalidraw/excalidraw"; +import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types"; + +const COMMENT_SVG = ( + + + +); + +const CustomFooter = ({ + excalidrawAPI, + excalidrawLib, +}: { + excalidrawAPI: ExcalidrawImperativeAPI; + excalidrawLib: typeof TExcalidraw; +}) => { + const { Button, MIME_TYPES } = excalidrawLib; + + return ( + <> + + + + ); +}; + +export default CustomFooter; diff --git a/examples/with-script-in-browser/components/ExampleApp.scss b/examples/with-script-in-browser/components/ExampleApp.scss new file mode 100644 index 0000000..e41a77c --- /dev/null +++ b/examples/with-script-in-browser/components/ExampleApp.scss @@ -0,0 +1,92 @@ +.App { + font-family: sans-serif; + text-align: center; + + .comment-avatar { + background: #faa2c1; + border-radius: 66px 67px 67px 0px; + width: 2rem; + height: 2rem; + padding: 4px; + margin: 4px; + img { + width: 100%; + height: 100%; + border-radius: 50%; + } + } + .app-title { + margin-block-start: 0.83em; + margin-block-end: 0.83em; + } +} + +.button-wrapper { + input[type="checkbox"] { + margin: 5px; + } + button { + z-index: 1; + height: 40px; + max-width: 200px; + margin: 10px; + padding: 5px; + } +} + +.excalidraw .App-menu_top .buttonList { + display: flex; +} + +.excalidraw-wrapper { + height: 800px; + margin: 50px; + position: relative; + overflow: hidden; +} + +:root[dir="ltr"] + .excalidraw + .layer-ui__wrapper + .zen-mode-transition.App-menu_bottom--transition-left { + transform: none; +} + +.excalidraw .panelColumn { + text-align: left; +} + +.export-wrapper { + display: flex; + flex-direction: column; + margin: 50px; + + &__checkbox { + display: flex; + } +} + +.excalidraw { + --color-primary: #faa2c1; + --color-primary-darker: #f783ac; + --color-primary-darkest: #e64980; + --color-primary-light: #fcc2d7; + + button.custom-element { + width: 2rem; + height: 2rem; + } + + .custom-footer, + .custom-element { + padding: 0.1rem; + margin: 0 8px; + } + .layer-ui__wrapper__footer.App-menu_bottom { + align-items: stretch; + } + // till its merged in OSS + .App-toolbar-container .mobile-misc-tools-container { + position: absolute; + } +} diff --git a/examples/with-script-in-browser/components/ExampleApp.tsx b/examples/with-script-in-browser/components/ExampleApp.tsx new file mode 100644 index 0000000..08c8032 --- /dev/null +++ b/examples/with-script-in-browser/components/ExampleApp.tsx @@ -0,0 +1,961 @@ +import React, { + useEffect, + useState, + useRef, + useCallback, + Children, + cloneElement, +} from "react"; +import ExampleSidebar from "./sidebar/ExampleSidebar"; + +import type * as TExcalidraw from "@excalidraw/excalidraw"; + +import { nanoid } from "nanoid"; + +import type { ResolvablePromise } from "../utils"; +import { + resolvablePromise, + distance2d, + fileOpen, + withBatchedUpdates, + withBatchedUpdatesThrottled, +} from "../utils"; + +import CustomFooter from "./CustomFooter"; +import MobileFooter from "./MobileFooter"; +import initialData from "../initialData"; + +import type { + AppState, + BinaryFileData, + ExcalidrawImperativeAPI, + ExcalidrawInitialDataState, + Gesture, + LibraryItems, + PointerDownState as ExcalidrawPointerDownState, +} from "@excalidraw/excalidraw/types"; +import type { + NonDeletedExcalidrawElement, + Theme, +} from "@excalidraw/excalidraw/element/types"; +import type { ImportedLibraryData } from "@excalidraw/excalidraw/data/types"; + +import "./ExampleApp.scss"; + +type Comment = { + x: number; + y: number; + value: string; + id?: string; +}; + +type PointerDownState = { + x: number; + y: number; + hitElement: Comment; + onMove: any; + onUp: any; + hitElementOffsets: { + x: number; + y: number; + }; +}; + +const COMMENT_ICON_DIMENSION = 32; +const COMMENT_INPUT_HEIGHT = 50; +const COMMENT_INPUT_WIDTH = 150; + +export interface AppProps { + appTitle: string; + useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void; + customArgs?: any[]; + children: React.ReactNode; + excalidrawLib: typeof TExcalidraw; +} + +export default function ExampleApp({ + appTitle, + useCustom, + customArgs, + children, + excalidrawLib, +}: AppProps) { + const { + exportToCanvas, + exportToSvg, + exportToBlob, + exportToClipboard, + useHandleLibrary, + MIME_TYPES, + sceneCoordsToViewportCoords, + viewportCoordsToSceneCoords, + restoreElements, + Sidebar, + Footer, + WelcomeScreen, + MainMenu, + LiveCollaborationTrigger, + convertToExcalidrawElements, + TTDDialog, + TTDDialogTrigger, + ROUNDNESS, + loadSceneOrLibraryFromBlob, + } = excalidrawLib; + const appRef = useRef(null); + const [viewModeEnabled, setViewModeEnabled] = useState(false); + const [zenModeEnabled, setZenModeEnabled] = useState(false); + const [gridModeEnabled, setGridModeEnabled] = useState(false); + const [blobUrl, setBlobUrl] = useState(""); + const [canvasUrl, setCanvasUrl] = useState(""); + const [exportWithDarkMode, setExportWithDarkMode] = useState(false); + const [exportEmbedScene, setExportEmbedScene] = useState(false); + const [theme, setTheme] = useState("light"); + const [disableImageTool, setDisableImageTool] = useState(false); + const [isCollaborating, setIsCollaborating] = useState(false); + const [commentIcons, setCommentIcons] = useState<{ [id: string]: Comment }>( + {}, + ); + const [comment, setComment] = useState(null); + + const initialStatePromiseRef = useRef<{ + promise: ResolvablePromise; + }>({ promise: null! }); + if (!initialStatePromiseRef.current.promise) { + initialStatePromiseRef.current.promise = + resolvablePromise(); + } + + const [excalidrawAPI, setExcalidrawAPI] = + useState(null); + + useCustom(excalidrawAPI, customArgs); + + useHandleLibrary({ excalidrawAPI }); + + useEffect(() => { + if (!excalidrawAPI) { + return; + } + const fetchData = async () => { + const res = await fetch("/images/rocket.jpeg"); + const imageData = await res.blob(); + const reader = new FileReader(); + reader.readAsDataURL(imageData); + + reader.onload = function () { + const imagesArray: BinaryFileData[] = [ + { + id: "rocket" as BinaryFileData["id"], + dataURL: reader.result as BinaryFileData["dataURL"], + mimeType: MIME_TYPES.jpg, + created: 1644915140367, + lastRetrieved: 1644915140367, + }, + ]; + + //@ts-ignore + initialStatePromiseRef.current.promise.resolve({ + ...initialData, + elements: convertToExcalidrawElements(initialData.elements), + }); + excalidrawAPI.addFiles(imagesArray); + }; + }; + fetchData(); + }, [excalidrawAPI, convertToExcalidrawElements, MIME_TYPES]); + + const renderExcalidraw = (children: React.ReactNode) => { + const Excalidraw: any = Children.toArray(children).find( + (child) => + React.isValidElement(child) && + typeof child.type !== "string" && + //@ts-ignore + child.type.displayName === "Excalidraw", + ); + if (!Excalidraw) { + return; + } + const newElement = cloneElement( + Excalidraw, + { + excalidrawAPI: (api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api), + initialData: initialStatePromiseRef.current.promise, + onChange: ( + elements: NonDeletedExcalidrawElement[], + state: AppState, + ) => { + console.info("Elements :", elements, "State : ", state); + }, + onPointerUpdate: (payload: { + pointer: { x: number; y: number }; + button: "down" | "up"; + pointersMap: Gesture["pointers"]; + }) => setPointerData(payload), + viewModeEnabled, + zenModeEnabled, + gridModeEnabled, + theme, + name: "Custom name of drawing", + UIOptions: { + canvasActions: { + loadScene: false, + }, + tools: { image: !disableImageTool }, + }, + renderTopRightUI, + onLinkOpen, + onPointerDown, + onScrollChange: rerenderCommentIcons, + validateEmbeddable: true, + }, + <> + {excalidrawAPI && ( +
+ +
+ )} + + + + + Tab one! + Tab two! + + One + Two + + + + + Toggle Custom Sidebar + + {renderMenu()} + {excalidrawAPI && ( + 😀}> + Text to diagram + + )} + { + console.info("submit"); + // sleep for 2s + await new Promise((resolve) => setTimeout(resolve, 2000)); + throw new Error("error, go away now"); + // return "dummy"; + }} + /> + , + ); + return newElement; + }; + const renderTopRightUI = (isMobile: boolean) => { + return ( + <> + {!isMobile && ( + { + window.alert("Collab dialog clicked"); + }} + /> + )} + + + ); + }; + + const loadSceneOrLibrary = async () => { + const file = await fileOpen({ description: "Excalidraw or library file" }); + const contents = await loadSceneOrLibraryFromBlob(file, null, null); + if (contents.type === MIME_TYPES.excalidraw) { + excalidrawAPI?.updateScene(contents.data as any); + } else if (contents.type === MIME_TYPES.excalidrawlib) { + excalidrawAPI?.updateLibrary({ + libraryItems: (contents.data as ImportedLibraryData).libraryItems!, + openLibraryMenu: true, + }); + } + }; + + const updateScene = () => { + const sceneData = { + elements: restoreElements( + convertToExcalidrawElements([ + { + type: "rectangle", + id: "rect-1", + fillStyle: "hachure", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + angle: 0, + x: 100.50390625, + y: 93.67578125, + strokeColor: "#c92a2a", + width: 186.47265625, + height: 141.9765625, + seed: 1968410350, + roundness: { + type: ROUNDNESS.ADAPTIVE_RADIUS, + value: 32, + }, + }, + { + type: "arrow", + x: 300, + y: 150, + start: { id: "rect-1" }, + end: { type: "ellipse" }, + }, + { + type: "text", + x: 300, + y: 100, + text: "HELLO WORLD!", + }, + ]), + null, + ), + appState: { + viewBackgroundColor: "#edf2ff", + }, + }; + excalidrawAPI?.updateScene(sceneData); + }; + + const onLinkOpen = useCallback( + ( + element: NonDeletedExcalidrawElement, + event: CustomEvent<{ + nativeEvent: MouseEvent | React.PointerEvent; + }>, + ) => { + const link = element.link!; + const { nativeEvent } = event.detail; + const isNewTab = nativeEvent.ctrlKey || nativeEvent.metaKey; + const isNewWindow = nativeEvent.shiftKey; + const isInternalLink = + link.startsWith("/") || link.includes(window.location.origin); + if (isInternalLink && !isNewTab && !isNewWindow) { + // signal that we're handling the redirect ourselves + event.preventDefault(); + // do a custom redirect, such as passing to react-router + // ... + } + }, + [], + ); + + const onCopy = async (type: "png" | "svg" | "json") => { + if (!excalidrawAPI) { + return false; + } + await exportToClipboard({ + elements: excalidrawAPI.getSceneElements(), + appState: excalidrawAPI.getAppState(), + files: excalidrawAPI.getFiles(), + type, + }); + window.alert(`Copied to clipboard as ${type} successfully`); + }; + + const [pointerData, setPointerData] = useState<{ + pointer: { x: number; y: number }; + button: "down" | "up"; + pointersMap: Gesture["pointers"]; + } | null>(null); + + const onPointerDown = ( + activeTool: AppState["activeTool"], + pointerDownState: ExcalidrawPointerDownState, + ) => { + if (activeTool.type === "custom" && activeTool.customType === "comment") { + const { x, y } = pointerDownState.origin; + setComment({ x, y, value: "" }); + } + }; + + const rerenderCommentIcons = () => { + if (!excalidrawAPI) { + return false; + } + const commentIconsElements = appRef.current.querySelectorAll( + ".comment-icon", + ) as HTMLElement[]; + commentIconsElements.forEach((ele) => { + const id = ele.id; + const appstate = excalidrawAPI.getAppState(); + const { x, y } = sceneCoordsToViewportCoords( + { sceneX: commentIcons[id].x, sceneY: commentIcons[id].y }, + appstate, + ); + ele.style.left = `${ + x - COMMENT_ICON_DIMENSION / 2 - appstate!.offsetLeft + }px`; + ele.style.top = `${ + y - COMMENT_ICON_DIMENSION / 2 - appstate!.offsetTop + }px`; + }); + }; + + const onPointerMoveFromPointerDownHandler = ( + pointerDownState: PointerDownState, + ) => { + return withBatchedUpdatesThrottled((event) => { + if (!excalidrawAPI) { + return false; + } + const { x, y } = viewportCoordsToSceneCoords( + { + clientX: event.clientX - pointerDownState.hitElementOffsets.x, + clientY: event.clientY - pointerDownState.hitElementOffsets.y, + }, + excalidrawAPI.getAppState(), + ); + setCommentIcons({ + ...commentIcons, + [pointerDownState.hitElement.id!]: { + ...commentIcons[pointerDownState.hitElement.id!], + x, + y, + }, + }); + }); + }; + const onPointerUpFromPointerDownHandler = ( + pointerDownState: PointerDownState, + ) => { + return withBatchedUpdates((event) => { + window.removeEventListener("pointermove", pointerDownState.onMove); + window.removeEventListener("pointerup", pointerDownState.onUp); + excalidrawAPI?.setActiveTool({ type: "selection" }); + const distance = distance2d( + pointerDownState.x, + pointerDownState.y, + event.clientX, + event.clientY, + ); + if (distance === 0) { + if (!comment) { + setComment({ + x: pointerDownState.hitElement.x + 60, + y: pointerDownState.hitElement.y, + value: pointerDownState.hitElement.value, + id: pointerDownState.hitElement.id, + }); + } else { + setComment(null); + } + } + }); + }; + + const renderCommentIcons = () => { + return Object.values(commentIcons).map((commentIcon) => { + if (!excalidrawAPI) { + return false; + } + const appState = excalidrawAPI.getAppState(); + const { x, y } = sceneCoordsToViewportCoords( + { sceneX: commentIcon.x, sceneY: commentIcon.y }, + excalidrawAPI.getAppState(), + ); + return ( +
{ + event.preventDefault(); + if (comment) { + commentIcon.value = comment.value; + saveComment(); + } + const pointerDownState: any = { + x: event.clientX, + y: event.clientY, + hitElement: commentIcon, + hitElementOffsets: { x: event.clientX - x, y: event.clientY - y }, + }; + const onPointerMove = + onPointerMoveFromPointerDownHandler(pointerDownState); + const onPointerUp = + onPointerUpFromPointerDownHandler(pointerDownState); + window.addEventListener("pointermove", onPointerMove); + window.addEventListener("pointerup", onPointerUp); + + pointerDownState.onMove = onPointerMove; + pointerDownState.onUp = onPointerUp; + + excalidrawAPI?.setActiveTool({ + type: "custom", + customType: "comment", + }); + }} + > +
+ doremon +
+
+ ); + }); + }; + + const saveComment = () => { + if (!comment) { + return; + } + if (!comment.id && !comment.value) { + setComment(null); + return; + } + const id = comment.id || nanoid(); + setCommentIcons({ + ...commentIcons, + [id]: { + x: comment.id ? comment.x - 60 : comment.x, + y: comment.y, + id, + value: comment.value, + }, + }); + setComment(null); + }; + + const renderComment = () => { + if (!comment) { + return null; + } + const appState = excalidrawAPI?.getAppState()!; + const { x, y } = sceneCoordsToViewportCoords( + { sceneX: comment.x, sceneY: comment.y }, + appState, + ); + let top = y - COMMENT_ICON_DIMENSION / 2 - appState.offsetTop; + let left = x - COMMENT_ICON_DIMENSION / 2 - appState.offsetLeft; + + if ( + top + COMMENT_INPUT_HEIGHT < + appState.offsetTop + COMMENT_INPUT_HEIGHT + ) { + top = COMMENT_ICON_DIMENSION / 2; + } + if (top + COMMENT_INPUT_HEIGHT > appState.height) { + top = appState.height - COMMENT_INPUT_HEIGHT - COMMENT_ICON_DIMENSION / 2; + } + if ( + left + COMMENT_INPUT_WIDTH < + appState.offsetLeft + COMMENT_INPUT_WIDTH + ) { + left = COMMENT_ICON_DIMENSION / 2; + } + if (left + COMMENT_INPUT_WIDTH > appState.width) { + left = appState.width - COMMENT_INPUT_WIDTH - COMMENT_ICON_DIMENSION / 2; + } + + return ( +