diff options
38 files changed, 3036 insertions, 0 deletions
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 Binary files differnew file mode 100644 index 0000000..36208a4 --- /dev/null +++ b/examples/with-nextjs/public/images/doremon.png diff --git a/examples/with-nextjs/public/images/excalibot.png b/examples/with-nextjs/public/images/excalibot.png Binary files differnew file mode 100644 index 0000000..7928ec3 --- /dev/null +++ b/examples/with-nextjs/public/images/excalibot.png diff --git a/examples/with-nextjs/public/images/pika.jpeg b/examples/with-nextjs/public/images/pika.jpeg Binary files differnew file mode 100644 index 0000000..455ed52 --- /dev/null +++ b/examples/with-nextjs/public/images/pika.jpeg diff --git a/examples/with-nextjs/public/images/rocket.jpeg b/examples/with-nextjs/public/images/rocket.jpeg Binary files differnew file mode 100644 index 0000000..f17a74b --- /dev/null +++ b/examples/with-nextjs/public/images/rocket.jpeg diff --git a/examples/with-nextjs/src/app/favicon.ico b/examples/with-nextjs/src/app/favicon.ico Binary files differnew file mode 100644 index 0000000..718d6fe --- /dev/null +++ b/examples/with-nextjs/src/app/favicon.ico 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 ( + <html lang="en"> + <body>{children}</body> + </html> + ); +} 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 ( + <> + <a href="/excalidraw-in-pages">Switch to Pages router</a> + <h1 className="page-title">App Router</h1> + <Script id="load-env-variables" strategy="beforeInteractive"> + {`window["EXCALIDRAW_ASSET_PATH"] = window.origin;`} + </Script> + {/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */} + <ExcalidrawWithClientOnly /> + </> + ); +} 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 ( + <> + <App + appTitle={"Excalidraw with Nextjs Example"} + useCustom={(api: any, args?: any[]) => {}} + excalidrawLib={excalidrawLib} + > + <Excalidraw /> + </App> + </> + ); +}; + +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 ( + <> + <a href="/">Switch to App router</a> + <h1 className="page-title">Pages Router</h1> + {/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */} + <Excalidraw /> + </> + ); +} 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 = ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" + className="feather feather-message-circle" + > + <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path> + </svg> +); + +const CustomFooter = ({ + excalidrawAPI, + excalidrawLib, +}: { + excalidrawAPI: ExcalidrawImperativeAPI; + excalidrawLib: typeof TExcalidraw; +}) => { + const { Button, MIME_TYPES } = excalidrawLib; + + return ( + <> + <Button + onSelect={() => alert("General Kenobi!")} + style={{ marginLeft: "1rem", width: "auto" }} + title="Hello there!" + > + Hit me + </Button> + <Button + className="custom-element" + onSelect={() => { + excalidrawAPI?.setActiveTool({ + type: "custom", + customType: "comment", + }); + const url = `data:${MIME_TYPES.svg},${encodeURIComponent( + `<svg + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + class="feather feather-message-circle" + > + <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path> + </svg>`, + )}`; + excalidrawAPI?.setCursor(`url(${url}), auto`); + }} + title="Comments!" + > + {COMMENT_SVG} + </Button> + </> + ); +}; + +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<any>(null); + const [viewModeEnabled, setViewModeEnabled] = useState(false); + const [zenModeEnabled, setZenModeEnabled] = useState(false); + const [gridModeEnabled, setGridModeEnabled] = useState(false); + const [blobUrl, setBlobUrl] = useState<string>(""); + const [canvasUrl, setCanvasUrl] = useState<string>(""); + const [exportWithDarkMode, setExportWithDarkMode] = useState(false); + const [exportEmbedScene, setExportEmbedScene] = useState(false); + const [theme, setTheme] = useState<Theme>("light"); + const [disableImageTool, setDisableImageTool] = useState(false); + const [isCollaborating, setIsCollaborating] = useState(false); + const [commentIcons, setCommentIcons] = useState<{ [id: string]: Comment }>( + {}, + ); + const [comment, setComment] = useState<Comment | null>(null); + + const initialStatePromiseRef = useRef<{ + promise: ResolvablePromise<ExcalidrawInitialDataState | null>; + }>({ promise: null! }); + if (!initialStatePromiseRef.current.promise) { + initialStatePromiseRef.current.promise = + resolvablePromise<ExcalidrawInitialDataState | null>(); + } + + const [excalidrawAPI, setExcalidrawAPI] = + useState<ExcalidrawImperativeAPI | null>(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 && ( + <Footer> + <CustomFooter + excalidrawAPI={excalidrawAPI} + excalidrawLib={excalidrawLib} + /> + </Footer> + )} + <WelcomeScreen /> + <Sidebar name="custom"> + <Sidebar.Tabs> + <Sidebar.Header /> + <Sidebar.Tab tab="one">Tab one!</Sidebar.Tab> + <Sidebar.Tab tab="two">Tab two!</Sidebar.Tab> + <Sidebar.TabTriggers> + <Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger> + <Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger> + </Sidebar.TabTriggers> + </Sidebar.Tabs> + </Sidebar> + <Sidebar.Trigger + name="custom" + tab="one" + style={{ + position: "absolute", + left: "50%", + transform: "translateX(-50%)", + bottom: "20px", + zIndex: 9999999999999999, + }} + > + Toggle Custom Sidebar + </Sidebar.Trigger> + {renderMenu()} + {excalidrawAPI && ( + <TTDDialogTrigger icon={<span>😀</span>}> + Text to diagram + </TTDDialogTrigger> + )} + <TTDDialog + onTextSubmit={async (_) => { + 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 && ( + <LiveCollaborationTrigger + isCollaborating={isCollaborating} + onSelect={() => { + window.alert("Collab dialog clicked"); + }} + /> + )} + <button + onClick={() => alert("This is an empty top right UI")} + style={{ height: "2.5rem" }} + > + Click me + </button> + </> + ); + }; + + 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<HTMLCanvasElement>; + }>, + ) => { + 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 ( + <div + id={commentIcon.id} + key={commentIcon.id} + style={{ + top: `${y - COMMENT_ICON_DIMENSION / 2 - appState!.offsetTop}px`, + left: `${x - COMMENT_ICON_DIMENSION / 2 - appState!.offsetLeft}px`, + position: "absolute", + zIndex: 1, + width: `${COMMENT_ICON_DIMENSION}px`, + height: `${COMMENT_ICON_DIMENSION}px`, + cursor: "pointer", + touchAction: "none", + }} + className="comment-icon" + onPointerDown={(event) => { + 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", + }); + }} + > + <div className="comment-avatar"> + <img src="images/doremon.png" alt="doremon" /> + </div> + </div> + ); + }); + }; + + 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 ( + <textarea + className="comment" + style={{ + top: `${top}px`, + left: `${left}px`, + position: "absolute", + zIndex: 1, + height: `${COMMENT_INPUT_HEIGHT}px`, + width: `${COMMENT_INPUT_WIDTH}px`, + }} + ref={(ref) => { + setTimeout(() => ref?.focus()); + }} + placeholder={comment.value ? "Reply" : "Comment"} + value={comment.value} + onChange={(event) => { + setComment({ ...comment, value: event.target.value }); + }} + onBlur={saveComment} + onKeyDown={(event) => { + if (!event.shiftKey && event.key === "Enter") { + event.preventDefault(); + saveComment(); + } + }} + /> + ); + }; + + const renderMenu = () => { + return ( + <MainMenu> + <MainMenu.DefaultItems.SaveAsImage /> + <MainMenu.DefaultItems.Export /> + <MainMenu.Separator /> + <MainMenu.DefaultItems.LiveCollaborationTrigger + isCollaborating={isCollaborating} + onSelect={() => window.alert("You clicked on collab button")} + /> + <MainMenu.Group title="Excalidraw links"> + <MainMenu.DefaultItems.Socials /> + </MainMenu.Group> + <MainMenu.Separator /> + <MainMenu.ItemCustom> + <button + style={{ height: "2rem" }} + onClick={() => window.alert("custom menu item")} + > + custom item + </button> + </MainMenu.ItemCustom> + <MainMenu.DefaultItems.Help /> + + {excalidrawAPI && ( + <MobileFooter + excalidrawLib={excalidrawLib} + excalidrawAPI={excalidrawAPI} + /> + )} + </MainMenu> + ); + }; + + return ( + <div className="App" ref={appRef}> + <h1>{appTitle}</h1> + {/* TODO fix type */} + <ExampleSidebar> + <div className="button-wrapper"> + <button onClick={loadSceneOrLibrary}>Load Scene or Library</button> + <button className="update-scene" onClick={updateScene}> + Update Scene + </button> + <button + className="reset-scene" + onClick={() => { + excalidrawAPI?.resetScene(); + }} + > + Reset Scene + </button> + <button + onClick={() => { + const libraryItems: LibraryItems = [ + { + status: "published", + id: "1", + created: 1, + elements: initialData.libraryItems[1] as any, + }, + { + status: "unpublished", + id: "2", + created: 2, + elements: initialData.libraryItems[1] as any, + }, + ]; + excalidrawAPI?.updateLibrary({ + libraryItems, + }); + }} + > + Update Library + </button> + + <label> + <input + type="checkbox" + checked={viewModeEnabled} + onChange={() => setViewModeEnabled(!viewModeEnabled)} + /> + View mode + </label> + <label> + <input + type="checkbox" + checked={zenModeEnabled} + onChange={() => setZenModeEnabled(!zenModeEnabled)} + /> + Zen mode + </label> + <label> + <input + type="checkbox" + checked={gridModeEnabled} + onChange={() => setGridModeEnabled(!gridModeEnabled)} + /> + Grid mode + </label> + <label> + <input + type="checkbox" + checked={theme === "dark"} + onChange={() => { + setTheme(theme === "light" ? "dark" : "light"); + }} + /> + Switch to Dark Theme + </label> + <label> + <input + type="checkbox" + checked={disableImageTool === true} + onChange={() => { + setDisableImageTool(!disableImageTool); + }} + /> + Disable Image Tool + </label> + <label> + <input + type="checkbox" + checked={isCollaborating} + onChange={() => { + if (!isCollaborating) { + const collaborators = new Map(); + collaborators.set("id1", { + username: "Doremon", + avatarUrl: "images/doremon.png", + }); + collaborators.set("id2", { + username: "Excalibot", + avatarUrl: "images/excalibot.png", + }); + collaborators.set("id3", { + username: "Pika", + avatarUrl: "images/pika.jpeg", + }); + collaborators.set("id4", { + username: "fallback", + avatarUrl: "https://example.com", + }); + excalidrawAPI?.updateScene({ collaborators }); + } else { + excalidrawAPI?.updateScene({ + collaborators: new Map(), + }); + } + setIsCollaborating(!isCollaborating); + }} + /> + Show collaborators + </label> + <div> + <button onClick={onCopy.bind(null, "png")}> + Copy to Clipboard as PNG + </button> + <button onClick={onCopy.bind(null, "svg")}> + Copy to Clipboard as SVG + </button> + <button onClick={onCopy.bind(null, "json")}> + Copy to Clipboard as JSON + </button> + </div> + <div + style={{ + display: "flex", + gap: "1em", + justifyContent: "center", + marginTop: "1em", + }} + > + <div>x: {pointerData?.pointer.x ?? 0}</div> + <div>y: {pointerData?.pointer.y ?? 0}</div> + </div> + </div> + <div className="excalidraw-wrapper"> + {renderExcalidraw(children)} + {Object.keys(commentIcons || []).length > 0 && renderCommentIcons()} + {comment && renderComment()} + </div> + + <div className="export-wrapper button-wrapper"> + <label className="export-wrapper__checkbox"> + <input + type="checkbox" + checked={exportWithDarkMode} + onChange={() => setExportWithDarkMode(!exportWithDarkMode)} + /> + Export with dark mode + </label> + <label className="export-wrapper__checkbox"> + <input + type="checkbox" + checked={exportEmbedScene} + onChange={() => setExportEmbedScene(!exportEmbedScene)} + /> + Export with embed scene + </label> + <button + onClick={async () => { + if (!excalidrawAPI) { + return; + } + const svg = await exportToSvg({ + elements: excalidrawAPI?.getSceneElements(), + appState: { + ...initialData.appState, + exportWithDarkMode, + exportEmbedScene, + width: 300, + height: 100, + }, + files: excalidrawAPI?.getFiles(), + }); + appRef.current.querySelector(".export-svg").innerHTML = + svg.outerHTML; + }} + > + Export to SVG + </button> + <div className="export export-svg"></div> + + <button + onClick={async () => { + if (!excalidrawAPI) { + return; + } + const blob = await exportToBlob({ + elements: excalidrawAPI?.getSceneElements(), + mimeType: "image/png", + appState: { + ...initialData.appState, + exportEmbedScene, + exportWithDarkMode, + }, + files: excalidrawAPI?.getFiles(), + }); + setBlobUrl(window.URL.createObjectURL(blob)); + }} + > + Export to Blob + </button> + <div className="export export-blob"> + <img src={blobUrl} alt="" /> + </div> + <button + onClick={async () => { + if (!excalidrawAPI) { + return; + } + const canvas = await exportToCanvas({ + elements: excalidrawAPI.getSceneElements(), + appState: { + ...initialData.appState, + exportWithDarkMode, + }, + files: excalidrawAPI.getFiles(), + }); + const ctx = canvas.getContext("2d")!; + ctx.font = "30px Excalifont"; + ctx.strokeText("My custom text", 50, 60); + setCanvasUrl(canvas.toDataURL()); + }} + > + Export to Canvas + </button> + <button + onClick={async () => { + if (!excalidrawAPI) { + return; + } + const canvas = await exportToCanvas({ + elements: excalidrawAPI.getSceneElements(), + appState: { + ...initialData.appState, + exportWithDarkMode, + }, + files: excalidrawAPI.getFiles(), + }); + const ctx = canvas.getContext("2d")!; + ctx.font = "30px Excalifont"; + ctx.strokeText("My custom text", 50, 60); + setCanvasUrl(canvas.toDataURL()); + }} + > + Export to Canvas + </button> + <button + type="button" + onClick={() => { + if (!excalidrawAPI) { + return; + } + + const elements = excalidrawAPI.getSceneElements(); + excalidrawAPI.scrollToContent(elements[0], { + fitToViewport: true, + }); + }} + > + Fit to viewport, first element + </button> + <button + type="button" + onClick={() => { + if (!excalidrawAPI) { + return; + } + + const elements = excalidrawAPI.getSceneElements(); + excalidrawAPI.scrollToContent(elements[0], { + fitToContent: true, + }); + + excalidrawAPI.scrollToContent(elements[0], { + fitToContent: true, + }); + }} + > + Fit to content, first element + </button> + <button + type="button" + onClick={() => { + if (!excalidrawAPI) { + return; + } + + const elements = excalidrawAPI.getSceneElements(); + excalidrawAPI.scrollToContent(elements[0], { + fitToContent: true, + }); + + excalidrawAPI.scrollToContent(elements[0]); + }} + > + Scroll to first element, no fitToContent, no fitToViewport + </button> + <div className="export export-canvas"> + <img src={canvasUrl} alt="" /> + </div> + </div> + </ExampleSidebar> + </div> + ); +} diff --git a/examples/with-script-in-browser/components/MobileFooter.tsx b/examples/with-script-in-browser/components/MobileFooter.tsx new file mode 100644 index 0000000..c8fc0f1 --- /dev/null +++ b/examples/with-script-in-browser/components/MobileFooter.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types"; +import CustomFooter from "./CustomFooter"; +import type * as TExcalidraw from "@excalidraw/excalidraw"; + +const MobileFooter = ({ + excalidrawAPI, + excalidrawLib, +}: { + excalidrawAPI: ExcalidrawImperativeAPI; + excalidrawLib: typeof TExcalidraw; +}) => { + const { useDevice, Footer } = excalidrawLib; + + const device = useDevice(); + if (device.editor.isMobile) { + return ( + <Footer> + <CustomFooter + excalidrawAPI={excalidrawAPI} + excalidrawLib={excalidrawLib} + /> + </Footer> + ); + } + return null; +}; +export default MobileFooter; diff --git a/examples/with-script-in-browser/components/sidebar/ExampleSidebar.scss b/examples/with-script-in-browser/components/sidebar/ExampleSidebar.scss new file mode 100644 index 0000000..773a8ff --- /dev/null +++ b/examples/with-script-in-browser/components/sidebar/ExampleSidebar.scss @@ -0,0 +1,66 @@ +.sidebar { + height: 100%; + width: 0; + position: absolute; + z-index: 1; + top: 0; + left: 0; + background-color: #111; + overflow-x: hidden; + transition: 0.5s; + padding-top: 60px; + + &.open { + width: 300px; + } + + &-links { + display: flex; + flex-direction: column; + padding: 20px; + + button { + padding: 10px; + margin: 10px; + background: #faa2c1; + color: #fff; + border: none; + cursor: pointer; + } + } +} + +.sidebar a { + padding: 8px 8px 8px 32px; + text-decoration: none; + font-size: 25px; + color: #818181; + display: block; + transition: 0.3s; +} + +.sidebar a:hover { + color: #f1f1f1; +} + +.sidebar .closebtn { + position: absolute; + top: 0; + right: 0; + font-size: 36px; + margin-left: 50px; +} + +.openbtn { + font-size: 20px; + cursor: pointer; + background-color: #111; + color: white; + padding: 10px 15px; + border: none; + display: flex; + margin-left: 50px; +} +.sidebar-open { + margin-left: 300px; +} diff --git a/examples/with-script-in-browser/components/sidebar/ExampleSidebar.tsx b/examples/with-script-in-browser/components/sidebar/ExampleSidebar.tsx new file mode 100644 index 0000000..1939134 --- /dev/null +++ b/examples/with-script-in-browser/components/sidebar/ExampleSidebar.tsx @@ -0,0 +1,31 @@ +import React, { useState } from "react"; +import "./ExampleSidebar.scss"; + +export default function Sidebar({ children }: { children: React.ReactNode }) { + const [open, setOpen] = useState(false); + + return ( + <> + <div id="mySidebar" className={`sidebar ${open ? "open" : ""}`}> + <button className="closebtn" onClick={() => setOpen(false)}> + x + </button> + <div className="sidebar-links"> + <button>Empty Home</button> + <button>Empty About</button> + </div> + </div> + <div className={`${open ? "sidebar-open" : ""}`}> + <button + className="openbtn" + onClick={() => { + setOpen(!open); + }} + > + Open Sidebar + </button> + {children} + </div> + </> + ); +} diff --git a/examples/with-script-in-browser/index.html b/examples/with-script-in-browser/index.html new file mode 100644 index 0000000..dbbc4ca --- /dev/null +++ b/examples/with-script-in-browser/index.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta + name="viewport" + content="width=device-width, initial-scale=1, shrink-to-fit=no" + /> + <meta name="theme-color" content="#000000" /> + + <title>React App</title> + <script> + window.name = "codesandbox"; + window.EXCALIDRAW_ASSET_PATH = + "https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/prod/"; + </script> + </head> + + <body> + <noscript> You need to enable JavaScript to run this app. </noscript> + <div id="root"></div> + + <script type="module"> + import * as ExcalidrawLib from "@excalidraw/excalidraw"; + + console.log(ExcalidrawLib); + window.ExcalidrawLib = ExcalidrawLib; + </script> + <script type="module" src="index.tsx"></script> + </body> +</html> diff --git a/examples/with-script-in-browser/index.tsx b/examples/with-script-in-browser/index.tsx new file mode 100644 index 0000000..25ad96e --- /dev/null +++ b/examples/with-script-in-browser/index.tsx @@ -0,0 +1,28 @@ +import App from "./components/ExampleApp"; +import React, { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; + +import type * as TExcalidraw from "@excalidraw/excalidraw"; + +import "@excalidraw/excalidraw/index.css"; + +declare global { + interface Window { + ExcalidrawLib: typeof TExcalidraw; + } +} + +const rootElement = document.getElementById("root")!; +const root = createRoot(rootElement); +const { Excalidraw } = window.ExcalidrawLib; +root.render( + <StrictMode> + <App + appTitle={"Excalidraw Example"} + useCustom={(api: any, args?: any[]) => {}} + excalidrawLib={window.ExcalidrawLib} + > + <Excalidraw /> + </App> + </StrictMode>, +); diff --git a/examples/with-script-in-browser/initialData.tsx b/examples/with-script-in-browser/initialData.tsx new file mode 100644 index 0000000..0db23d5 --- /dev/null +++ b/examples/with-script-in-browser/initialData.tsx @@ -0,0 +1,994 @@ +import type { ExcalidrawElementSkeleton } from "@excalidraw/excalidraw/data/transform"; +import type { FileId } from "@excalidraw/excalidraw/element/types"; + +const elements: ExcalidrawElementSkeleton[] = [ + { + type: "rectangle", + x: 10, + y: 10, + strokeWidth: 2, + id: "1", + }, + { + type: "diamond", + x: 120, + y: 20, + backgroundColor: "#fff3bf", + strokeWidth: 2, + label: { + text: "HELLO EXCALIDRAW", + strokeColor: "#099268", + fontSize: 30, + }, + id: "2", + }, + { + type: "arrow", + x: 100, + y: 200, + label: { text: "HELLO WORLD!!" }, + start: { type: "rectangle" }, + end: { type: "ellipse" }, + }, + { + type: "image", + x: 606.1042326312408, + y: 153.57729779411773, + width: 230, + height: 230, + fileId: "rocket" as FileId, + }, + { + type: "frame", + children: ["1", "2"], + name: "My frame", + }, +]; +export default { + elements, + appState: { viewBackgroundColor: "#AFEEEE", currentItemFontFamily: 5 }, + scrollToContent: true, + libraryItems: [ + [ + { + type: "line", + + x: 209.72304760646858, + y: 338.83587294718825, + strokeColor: "#881fa3", + backgroundColor: "#be4bdb", + width: 116.42036295658873, + height: 103.65107323746608, + strokeSharpness: "sharp", + points: [ + [-92.28090097254909, 7.105427357601002e-15], + [-154.72281841151394, 19.199290805487394], + [-155.45758928571422, 79.43840749607878], + [-99.89923520113778, 103.6510732374661], + [-40.317783799181804, 79.1587107641305], + [-39.037226329125524, 21.285677238400705], + [-92.28090097254909, 7.105427357601002e-15], + ], + }, + ], + [ + { + type: "line", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + x: -249.48446738689245, + y: 374.851387389359, + strokeColor: "#0a11d3", + backgroundColor: "#228be6", + width: 88.21658171083376, + height: 113.8575037534261, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + points: [ + [-0.22814350714115691, -43.414939319563715], + [0.06274947619197979, 42.63794490105306], + [-0.21453039840335475, 52.43469208825097], + [4.315205554872581, 56.66774540453215], + [20.089784992984285, 60.25027917349701], + [46.7532926683984, 61.365826671969444], + [72.22851104292477, 59.584691681394986], + [85.76368213524371, 55.325139565662596], + [87.67263486434864, 51.7342924478499], + [87.94074036468018, 43.84700272879395], + [87.73030872197806, -36.195582644606276], + [87.2559282533682, -43.758132174307036], + [81.5915337527493, -47.984890854524416], + [69.66352776578219, -50.4328058257654], + [42.481213744224995, -52.49167708145666], + [20.68789182864576, -51.26396751574663], + [3.5475921483286084, -47.099726468136254], + [-0.2758413461535838, -43.46664538034193], + [-0.22814350714115691, -43.414939319563715], + ], + }, + { + type: "line", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -249.02524930453623, + y: 398.8804363713438, + strokeColor: "#0a11d3", + backgroundColor: "transparent", + width: 88.30808627974527, + height: 9.797916664247975, + seed: 683951089, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "round", + + points: [ + [0, -2.1538602707609424], + [2.326538897826852, 1.751753055375216], + [12.359939318521995, 5.028526743934819], + [25.710950037209347, 7.012921076245119], + [46.6269757640547, 7.193749997581346], + [71.03526003420632, 5.930375670950649], + [85.2899738827162, 1.3342483900732343], + [88.30808627974527, -2.6041666666666288], + ], + }, + { + type: "line", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -250.11899081659772, + y: 365.80628180927204, + strokeColor: "#0a11d3", + backgroundColor: "transparent", + width: 88.30808627974527, + height: 9.797916664247975, + seed: 1817746897, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "round", + + points: [ + [0, -2.1538602707609424], + [2.326538897826852, 1.751753055375216], + [12.359939318521995, 5.028526743934819], + [25.710950037209347, 7.012921076245119], + [46.6269757640547, 7.193749997581346], + [71.03526003420632, 5.930375670950649], + [85.2899738827162, 1.3342483900732343], + [88.30808627974527, -2.6041666666666288], + ], + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -251.23981350275943, + y: 323.4117518426986, + strokeColor: "#0a11d3", + backgroundColor: "#fff", + width: 87.65074610854188, + height: 17.72670397681366, + seed: 1409727409, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "sharp", + boundElementIds: ["bxuMGTzXLn7H-uBCptINx"], + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -179.73008120217884, + y: 347.98755471983213, + strokeColor: "#0a11d3", + backgroundColor: "#fff", + width: 12.846057046979809, + height: 13.941904362416096, + seed: 1073094033, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "sharp", + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -179.73008120217884, + y: 378.5900085788926, + strokeColor: "#0a11d3", + backgroundColor: "#fff", + width: 12.846057046979809, + height: 13.941904362416096, + seed: 526271345, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "sharp", + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -179.73008120217884, + y: 411.8508097533892, + strokeColor: "#0a11d3", + backgroundColor: "#fff", + width: 12.846057046979809, + height: 13.941904362416096, + seed: 243707217, + groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], + strokeSharpness: "sharp", + }, + ], + [ + { + type: "diamond", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -109.55894395256101, + y: 381.22641397493356, + strokeColor: "#c92a2a", + backgroundColor: "#fd8888", + width: 112.64736525303451, + height: 36.77344700318558, + seed: 511870335, + groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], + strokeSharpness: "sharp", + }, + { + type: "diamond", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -109.55894395256101, + y: 372.354634046675, + strokeColor: "#c92a2a", + backgroundColor: "#fd8888", + width: 112.64736525303451, + height: 36.77344700318558, + seed: 1283079231, + groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], + strokeSharpness: "sharp", + }, + { + type: "diamond", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -109.55894395256101, + y: 359.72407445196296, + strokeColor: "#c92a2a", + backgroundColor: "#fd8888", + width: 112.64736525303451, + height: 36.77344700318558, + seed: 996251633, + groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], + strokeSharpness: "sharp", + }, + { + type: "diamond", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -109.55894395256101, + y: 347.1924021546656, + strokeColor: "#c92a2a", + backgroundColor: "#fd8888", + width: 112.64736525303451, + height: 36.77344700318558, + seed: 1764842481, + groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], + strokeSharpness: "sharp", + }, + ], + [ + { + type: "line", + fillStyle: "hachure", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 1.5707963267948957, + x: -471.6208001976387, + y: 520.7681448415112, + strokeColor: "#087f5b", + backgroundColor: "#40c057", + width: 52.317507746132115, + height: 154.56722543646003, + seed: 1424381745, + groupIds: ["HSrtfEf-CssQTf160Fb6R"], + strokeSharpness: "round", + + points: [ + [-0.24755378372925183, -40.169554027464216], + [-0.07503751055611152, 76.6515171914404], + [-0.23948042713317108, 89.95108885873196], + [2.446913573036335, 95.69766931810295], + [11.802146636255692, 100.56113713047068], + [27.615140546177496, 102.07554835500338], + [42.72341054254274, 99.65756899883291], + [50.75054563137204, 93.87501510096598], + [51.88266441510958, 89.00026150397161], + [52.04166639997853, 78.29287333983132], + [51.916868330459295, -30.36891819848148], + [51.635533423123285, -40.63545540065934], + [48.27622163143906, -46.37349057843314], + [41.202227904674494, -49.69665692879073], + [25.081551986374073, -52.49167708145666], + [12.15685839679867, -50.825000270901], + [1.9916746648394732, -45.171835889467935], + [-0.2758413461535838, -40.23974757720194], + [-0.24755378372925183, -40.169554027464216], + ], + }, + { + type: "line", + version: 2405, + versionNonce: 2120341087, + isDeleted: false, + id: "TYsYe2VvJ60T_yKa3kyOw", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 1.5707963267948957, + x: -496.3957643857249, + y: 541.7241190920508, + strokeColor: "#087f5b", + backgroundColor: "transparent", + width: 50.7174766392476, + height: 12.698053371678215, + seed: 726657713, + groupIds: ["HSrtfEf-CssQTf160Fb6R"], + strokeSharpness: "round", + + points: [ + [0, -2.0205717204386002], + [1.3361877396713384, 3.0410845646550486], + [7.098613049589299, 7.287767671898479], + [14.766422451441104, 9.859533283467512], + [26.779003528407447, 10.093886705011586], + [40.79727342221974, 8.456559589697127], + [48.98410145879092, 2.500000505196364], + [50.7174766392476, -2.6041666666666288], + ], + }, + { + type: "line", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 1.5707963267948957, + x: -450.969983237283, + y: 542.1789894334747, + strokeColor: "#087f5b", + backgroundColor: "transparent", + width: 50.57247907260371, + height: 10.178760037658167, + seed: 1977326481, + groupIds: ["HSrtfEf-CssQTf160Fb6R"], + strokeSharpness: "round", + + points: [ + [0, -2.136356936862347], + [1.332367676378171, 1.9210669226078037], + [7.078318632616268, 5.325208253515953], + [14.724206326638113, 7.386735659885842], + [26.70244431044034, 7.574593370991538], + [40.68063699304561, 6.262111896696538], + [48.84405948536458, 1.4873339211608216], + [50.57247907260371, -2.6041666666666288], + ], + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 1.5707963267948957, + x: -404.36521010516793, + y: 534.1894365757241, + strokeColor: "#087f5b", + backgroundColor: "#fff", + width: 51.27812853552538, + height: 22.797152568995934, + seed: 1774660383, + groupIds: ["HSrtfEf-CssQTf160Fb6R"], + strokeSharpness: "sharp", + boundElementIds: ["bxuMGTzXLn7H-uBCptINx"], + }, + ], + [ + { + type: "rectangle", + fillStyle: "solid", + strokeWidth: 2, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -393.3000561423187, + y: 338.9742643666818, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 70.67858069123133, + height: 107.25081879410921, + seed: 371096063, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "sharp", + boundElementIds: [ + "CFu0B4Mw_1wC1Hbgx8Fs0", + "XIl_NhaFtRO00pX5Pq6VU", + "EndiSTFlx1AT7vcBVjgve", + ], + }, + { + type: "rectangle", + fillStyle: "solid", + strokeWidth: 2, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -400.8474891780329, + y: 331.95417508096745, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 70.67858069123133, + height: 107.25081879410921, + seed: 685932433, + groupIds: ["0RJwA-yKP5dqk5oMiSeot", "9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "sharp", + boundElementIds: [ + "CFu0B4Mw_1wC1Hbgx8Fs0", + "XIl_NhaFtRO00pX5Pq6VU", + "EndiSTFlx1AT7vcBVjgve", + ], + }, + { + type: "rectangle", + fillStyle: "solid", + strokeWidth: 2, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -410.24257846374826, + y: 323.7002688309677, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 70.67858069123133, + height: 107.25081879410921, + seed: 58634943, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "sharp", + boundElementIds: [ + "CFu0B4Mw_1wC1Hbgx8Fs0", + "XIl_NhaFtRO00pX5Pq6VU", + "EndiSTFlx1AT7vcBVjgve", + ], + }, + { + type: "draw", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -398.2561518768373, + y: 371.84603609547054, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 46.57983585730082, + height: 3.249953844290203, + seed: 1673003743, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "round", + + points: [ + [0, 0.6014697828497827], + [40.42449133807562, 0.7588628355182573], + [46.57983585730082, -2.491091008771946], + ], + }, + { + type: "draw", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -396.400899638823, + y: 340.9822185794818, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 45.567415680676426, + height: 2.8032978840147194, + seed: 1821527807, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "round", + + points: [ + [0, 0], + [16.832548902953302, -2.8032978840147194], + [45.567415680676426, -0.3275477042019195], + ], + }, + { + type: "draw", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -396.4774991551924, + y: 408.37659284983897, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 48.33668263438425, + height: 4.280657518731036, + seed: 1485707039, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "round", + + points: [ + [0, 0], + [26.41225578429045, -0.2552319773002338], + [37.62000339651456, 2.3153712935189787], + [48.33668263438425, -1.9652862252120569], + ], + }, + { + type: "draw", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -399.6615463367227, + y: 419.61974125811776, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 54.40694982784246, + height: 2.9096445412231735, + seed: 1042012991, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "round", + + points: [ + [0, 0], + [10.166093050596771, -1.166642430373031], + [16.130660965377448, -0.8422655250909383], + [46.26079588567538, 0.6125567455206506], + [54.40694982784246, -2.297087795702523], + ], + }, + { + type: "draw", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -399.3767034411569, + y: 356.042820132743, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 46.92865289294453, + height: 2.4757501798128, + seed: 295443295, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "round", + + points: [ + [0, 0], + [18.193786115221407, -0.5912874140789839], + [46.92865289294453, 1.884462765733816], + ], + }, + { + type: "draw", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -399.26921524500654, + y: 390.5261491685826, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 46.92865289294453, + height: 2.4757501798128, + seed: 1734301567, + groupIds: ["9ppmKFUbA4iKjt8FaDFox"], + strokeSharpness: "round", + + points: [ + [0, 0], + [8.093938105125233, 1.4279702913643746], + [18.193786115221407, -0.5912874140789839], + [46.92865289294453, 1.884462765733816], + ], + }, + ], + [ + { + type: "rectangle", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -593.9896997899341, + y: 343.9798351106279, + strokeColor: "#000000", + backgroundColor: "transparent", + width: 127.88383573213892, + height: 76.53703389977764, + seed: 106569279, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "sharp", + }, + { + type: "line", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -595.0652975408293, + y: 354.6963695028721, + strokeColor: "#000000", + backgroundColor: "transparent", + width: 128.84193229844433, + height: 0, + seed: 73916127, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "round", + + points: [ + [0, 0], + [128.84193229844433, 0], + ], + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 0, + opacity: 100, + angle: 0, + x: -589.5016643209792, + y: 348.2514049106367, + strokeColor: "#000000", + backgroundColor: "#fa5252", + width: 5.001953125, + height: 5.001953125, + seed: 387857791, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "sharp", + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 0, + opacity: 100, + angle: 0, + x: -579.2389690084792, + y: 348.2514049106367, + strokeColor: "#000000", + backgroundColor: "#fab005", + width: 5.001953125, + height: 5.001953125, + seed: 1486370207, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "sharp", + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 0, + opacity: 100, + angle: 0, + x: -568.525552542133, + y: 348.7021260644829, + strokeColor: "#000000", + backgroundColor: "#40c057", + width: 5.001953125, + height: 5.001953125, + seed: 610150847, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "sharp", + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 90, + angle: 0, + x: -552.4984915525058, + y: 364.75449494249875, + strokeColor: "#000000", + backgroundColor: "#04aaf7", + width: 42.72020253937572, + height: 42.72020253937572, + seed: 144280593, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "sharp", + }, + { + type: "draw", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + x: -530.327851842306, + y: 378.9357912947449, + strokeColor: "#087f5b", + backgroundColor: "#40c057", + width: 28.226201983883442, + height: 24.44112284281997, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "round", + points: [ + [4.907524351775825, 2.043055712211473], + [3.0769604829149455, 1.6284171290602836], + [-2.66472604008681, -4.228569719133945], + [-6.450168189798415, -2.304577297733668], + [-7.704241049212052, 4.416384506147983], + [-6.361372181234263, 8.783101300254884], + [-12.516984713388897, 10.9291595737194], + [-12.295677738198286, 15.686226498407976], + [-7.473371426945252, 15.393030178104425], + [-3.787654025313423, 11.5207568827343], + [1.2873793872375165, 19.910682356036197], + [4.492232250183542, 20.212553123686025], + [1.1302787567009416, 6.843494873631317], + [6.294108177816019, 6.390688722156585], + [8.070028349098962, 7.910451897221202], + [14.143675334886687, 7.910451897221202], + [15.709217270494545, 2.6780252579576427], + [9.128749989671498, 3.1533849725326517], + [10.393751588600717, -3.7167773257046695], + [7.380151667177483, -3.30213874255348], + [4.669824267311791, 1.1200945145694894], + [4.907524351775825, 2.043055712211473], + ], + }, + { + type: "line", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 90, + angle: 0, + x: -551.4394290784783, + y: 385.71736850567976, + strokeColor: "#000000", + backgroundColor: "#99bcff", + width: 42.095115772272244, + height: 0, + seed: 1443027377, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "round", + + points: [ + [0, 0], + [42.095115772272244, 0], + ], + }, + { + type: "line", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 0, + opacity: 90, + angle: 0, + x: -546.3441000487039, + y: 372.6245229061568, + strokeColor: "#000000", + backgroundColor: "#99bcff", + width: 29.31860660384862, + height: 5.711199931375845, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "round", + points: [ + [0, -2.341683327443203], + [0.7724193963150375, -0.06510358900749044], + [4.103544916365185, 1.84492589414448], + [8.536129150893453, 3.0016281808630056], + [15.480325949120388, 3.1070332647092163], + [23.583965316012858, 2.3706131055211244], + [28.316582284417855, -0.3084668090492442], + [29.31860660384862, -2.6041666666666288], + ], + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 90, + angle: 0, + x: -538.2701841247845, + y: 363.37196531290607, + strokeColor: "#000000", + backgroundColor: "transparent", + width: 15.528434353116108, + height: 44.82230388130942, + seed: 683572113, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "sharp", + }, + { + type: "line", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + opacity: 90, + x: -544.828148539078, + y: 402.0199316371545, + strokeColor: "#000000", + backgroundColor: "#99bcff", + width: 29.31860660384862, + height: 5.896061363392446, + seed: 318798801, + groupIds: ["TC0RSM64Cxmu17MlE12-o"], + strokeSharpness: "round", + + points: [ + [0, 0], + [4.103544916365185, -4.322122351104391], + [8.536129150893453, -5.516265043290966], + [15.480325949120388, -5.625081903117008], + [23.583965316012858, -4.8648251269605955], + [28.316582284417855, -2.0990281379671547], + [29.31860660384862, 0.2709794602754383], + ], + }, + ], + [ + { + type: "rectangle", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -715.1043446306466, + y: 330.4231266309418, + strokeColor: "#000000", + backgroundColor: "#ced4da", + width: 70.81644178885557, + height: 108.30428902193904, + seed: 1914896753, + groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], + strokeSharpness: "sharp", + }, + { + type: "rectangle", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -706.996640540555, + y: 338.68030798133873, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 55.801163535143246, + height: 82.83278895375764, + seed: 1306468145, + groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], + strokeSharpness: "sharp", + }, + { + type: "ellipse", + fillStyle: "solid", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -684.8099707762028, + y: 425.0579911039235, + strokeColor: "#000000", + backgroundColor: "#fff", + width: 11.427824006438863, + height: 11.427824006438863, + seed: 93422161, + groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], + strokeSharpness: "sharp", + }, + { + type: "rectangle", + fillStyle: "cross-hatch", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + angle: 0, + x: -698.7169501405845, + y: 349.2244646574789, + strokeColor: "#000000", + backgroundColor: "#fab005", + width: 39.2417827352022, + height: 19.889460471185775, + seed: 11646495, + groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], + strokeSharpness: "sharp", + }, + { + type: "rectangle", + fillStyle: "cross-hatch", + strokeWidth: 1, + strokeStyle: "solid", + x: -698.7169501405845, + y: 384.7822247024333, + strokeColor: "#000000", + backgroundColor: "#fab005", + width: 39.2417827352022, + height: 19.889460471185775, + seed: 291717649, + groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], + strokeSharpness: "sharp", + }, + ], + ], +}; diff --git a/examples/with-script-in-browser/package.json b/examples/with-script-in-browser/package.json new file mode 100644 index 0000000..3d61f1a --- /dev/null +++ b/examples/with-script-in-browser/package.json @@ -0,0 +1,21 @@ +{ + "name": "with-script-in-browser", + "version": "1.0.0", + "private": true, + "dependencies": { + "react": "19.0.0", + "react-dom": "19.0.0", + "@excalidraw/excalidraw": "*", + "browser-fs-access": "0.29.1" + }, + "devDependencies": { + "vite": "5.0.12", + "typescript": "^5" + }, + "scripts": { + "start": "vite", + "build": "vite build", + "build:preview": "yarn build && vite preview --port 5002", + "build:package": "yarn workspace @excalidraw/excalidraw run build:esm" + } +} diff --git a/examples/with-script-in-browser/public/images/doremon.png b/examples/with-script-in-browser/public/images/doremon.png Binary files differnew file mode 100644 index 0000000..36208a4 --- /dev/null +++ b/examples/with-script-in-browser/public/images/doremon.png diff --git a/examples/with-script-in-browser/public/images/excalibot.png b/examples/with-script-in-browser/public/images/excalibot.png Binary files differnew file mode 100644 index 0000000..7928ec3 --- /dev/null +++ b/examples/with-script-in-browser/public/images/excalibot.png diff --git a/examples/with-script-in-browser/public/images/pika.jpeg b/examples/with-script-in-browser/public/images/pika.jpeg Binary files differnew file mode 100644 index 0000000..455ed52 --- /dev/null +++ b/examples/with-script-in-browser/public/images/pika.jpeg diff --git a/examples/with-script-in-browser/public/images/rocket.jpeg b/examples/with-script-in-browser/public/images/rocket.jpeg Binary files differnew file mode 100644 index 0000000..f17a74b --- /dev/null +++ b/examples/with-script-in-browser/public/images/rocket.jpeg diff --git a/examples/with-script-in-browser/tsconfig.json b/examples/with-script-in-browser/tsconfig.json new file mode 100644 index 0000000..be262d1 --- /dev/null +++ b/examples/with-script-in-browser/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "module": "ES2022", + "moduleResolution": "Bundler", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "skipLibCheck": true + } +} diff --git a/examples/with-script-in-browser/utils.ts b/examples/with-script-in-browser/utils.ts new file mode 100644 index 0000000..a77b93f --- /dev/null +++ b/examples/with-script-in-browser/utils.ts @@ -0,0 +1,145 @@ +import { unstable_batchedUpdates } from "react-dom"; +import { fileOpen as _fileOpen } from "browser-fs-access"; +import { MIME_TYPES } from "@excalidraw/excalidraw"; + +type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">; + +const INPUT_CHANGE_INTERVAL_MS = 500; + +export type ResolvablePromise<T> = Promise<T> & { + resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void; + reject: (error: Error) => void; +}; +export const resolvablePromise = <T>() => { + let resolve!: any; + let reject!: any; + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + (promise as any).resolve = resolve; + (promise as any).reject = reject; + return promise as ResolvablePromise<T>; +}; + +export const distance2d = (x1: number, y1: number, x2: number, y2: number) => { + const xd = x2 - x1; + const yd = y2 - y1; + return Math.hypot(xd, yd); +}; + +export const fileOpen = <M extends boolean | undefined = false>(opts: { + extensions?: FILE_EXTENSION[]; + description: string; + multiple?: M; +}): Promise<M extends false | undefined ? File : File[]> => { + // an unsafe TS hack, alas not much we can do AFAIK + type RetType = M extends false | undefined ? File : File[]; + + const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => { + mimeTypes.push(MIME_TYPES[type]); + + return mimeTypes; + }, [] as string[]); + + const extensions = opts.extensions?.reduce((acc, ext) => { + if (ext === "jpg") { + return acc.concat(".jpg", ".jpeg"); + } + return acc.concat(`.${ext}`); + }, [] as string[]); + + return _fileOpen({ + description: opts.description, + extensions, + mimeTypes, + multiple: opts.multiple ?? false, + legacySetup: (resolve, reject, input) => { + const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS); + const focusHandler = () => { + checkForFile(); + document.addEventListener("keyup", scheduleRejection); + document.addEventListener("pointerup", scheduleRejection); + scheduleRejection(); + }; + const checkForFile = () => { + // this hack might not work when expecting multiple files + if (input.files?.length) { + const ret = opts.multiple ? [...input.files] : input.files[0]; + resolve(ret as RetType); + } + }; + requestAnimationFrame(() => { + window.addEventListener("focus", focusHandler); + }); + const interval = window.setInterval(() => { + checkForFile(); + }, INPUT_CHANGE_INTERVAL_MS); + return (rejectPromise) => { + clearInterval(interval); + scheduleRejection.cancel(); + window.removeEventListener("focus", focusHandler); + document.removeEventListener("keyup", scheduleRejection); + document.removeEventListener("pointerup", scheduleRejection); + if (rejectPromise) { + // so that something is shown in console if we need to debug this + console.warn("Opening the file was canceled (legacy-fs)."); + rejectPromise(new Error("Request Aborted")); + } + }; + }, + }) as Promise<RetType>; +}; + +export const debounce = <T extends any[]>( + fn: (...args: T) => void, + timeout: number, +) => { + let handle = 0; + let lastArgs: T | null = null; + const ret = (...args: T) => { + lastArgs = args; + clearTimeout(handle); + handle = window.setTimeout(() => { + lastArgs = null; + fn(...args); + }, timeout); + }; + ret.flush = () => { + clearTimeout(handle); + if (lastArgs) { + const _lastArgs = lastArgs; + lastArgs = null; + fn(..._lastArgs); + } + }; + ret.cancel = () => { + lastArgs = null; + clearTimeout(handle); + }; + return ret; +}; + +export const withBatchedUpdates = < + TFunction extends ((event: any) => void) | (() => void), +>( + func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never, +) => + ((event) => { + unstable_batchedUpdates(func as TFunction, event); + }) as TFunction; + +/** + * barches React state updates and throttles the calls to a single call per + * animation frame + */ +export const withBatchedUpdatesThrottled = < + TFunction extends ((event: any) => void) | (() => void), +>( + func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never, +) => { + // @ts-ignore + return throttleRAF<Parameters<TFunction>>(((event) => { + unstable_batchedUpdates(func, event); + }) as TFunction); +}; diff --git a/examples/with-script-in-browser/vercel.json b/examples/with-script-in-browser/vercel.json new file mode 100644 index 0000000..99a5811 --- /dev/null +++ b/examples/with-script-in-browser/vercel.json @@ -0,0 +1,5 @@ +{ + "outputDirectory": "dist", + "installCommand": "yarn install", + "buildCommand": "yarn build:package && yarn build" +} diff --git a/examples/with-script-in-browser/vite.config.mts b/examples/with-script-in-browser/vite.config.mts new file mode 100644 index 0000000..3ff0ac6 --- /dev/null +++ b/examples/with-script-in-browser/vite.config.mts @@ -0,0 +1,19 @@ +import { defineConfig } from "vite"; + +// https://vitejs.dev/config/ +export default defineConfig({ + server: { + port: 3001, + // open the browser + open: true, + }, + publicDir: "public", + optimizeDeps: { + esbuildOptions: { + // Bumping to 2022 due to "Arbitrary module namespace identifier names" not being + // supported in Vite's default browser target https://github.com/vitejs/vite/issues/13556 + target: "es2022", + treeShaking: true, + }, + }, +}); |
