From 450d740f1abd67ef8e6af38dc4fc06ab52c6b39f Mon Sep 17 00:00:00 2001 From: Chris Villa Date: Sun, 11 Feb 2024 09:38:12 +0100 Subject: [PATCH] feat: add iframe support --- package.json | 3 + pnpm-lock.yaml | 277 ++++-------------- .../dimension-marshal/get-initial-publish.ts | 2 +- src/state/get-frame.ts | 1 + .../move-cross-axis/index.ts | 9 +- .../move-to-next-place/index.ts | 9 +- src/types.ts | 1 + src/view/event-bindings/bind-events.ts | 27 +- src/view/get-elements/find-drag-handle.ts | 9 +- src/view/get-elements/find-draggable.ts | 5 +- src/view/iframe/apply-offset.ts | 20 ++ src/view/iframe/get-iframe-offset.ts | 23 ++ src/view/iframe/get-offsetted-box.ts | 17 ++ src/view/iframe/offset-types.ts | 6 + src/view/iframe/query-selector-all-iframe.ts | 21 ++ .../use-draggable-publisher/get-dimension.ts | 7 +- .../use-droppable-publisher/get-dimension.ts | 3 +- .../sensors/use-mouse-sensor.ts | 21 +- .../sensors/use-touch-sensor.ts | 20 +- .../sensors/util/offset-point.ts | 24 ++ .../use-style-marshal/use-style-marshal.ts | 114 ++++--- src/view/window/get-viewport.ts | 16 +- stories/examples/61-iframe.stories.tsx | 19 ++ stories/src/board/column.tsx | 2 +- stories/src/board/iframe-board.tsx | 272 +++++++++++++++++ stories/src/primatives/quote-item.tsx | 2 +- 26 files changed, 626 insertions(+), 304 deletions(-) create mode 100644 src/view/iframe/apply-offset.ts create mode 100644 src/view/iframe/get-iframe-offset.ts create mode 100644 src/view/iframe/get-offsetted-box.ts create mode 100644 src/view/iframe/offset-types.ts create mode 100644 src/view/iframe/query-selector-all-iframe.ts create mode 100644 src/view/use-sensor-marshal/sensors/util/offset-point.ts create mode 100644 stories/examples/61-iframe.stories.tsx create mode 100644 stories/src/board/iframe-board.tsx diff --git a/package.json b/package.json index 5decfe59d..bef1752fa 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "@types/jsdom": "21.1.6", "@types/markdown-it": "13.0.7", "@types/node": "20.10.3", + "@types/object-hash": "^3.0.6", "@types/raf-schd": "4.0.3", "@types/react": "18.2.42", "@types/react-dom": "18.2.17", @@ -177,6 +178,7 @@ "lighthouse": "10.4.0", "markdown-it": "13.0.2", "memory-fs": "0.5.0", + "object-hash": "^3.0.0", "postcss-styled-syntax": "0.5.0", "prettier": "3.1.0", "raf-stub": "3.0.0", @@ -186,6 +188,7 @@ "react-dom": "18.2.0", "react-dom-16": "npm:react-dom@16.14.0", "react-dom-17": "npm:react-dom@17.0.2", + "react-frame-component": "^5.2.6", "react-virtualized": "9.22.5", "react-window": "1.8.10", "release-it": "16.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53ce2d41a..5c7607cd6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -172,6 +172,9 @@ devDependencies: '@types/node': specifier: 20.10.3 version: 20.10.3 + '@types/object-hash': + specifier: ^3.0.6 + version: 3.0.6 '@types/raf-schd': specifier: 4.0.3 version: 4.0.3 @@ -216,7 +219,7 @@ devDependencies: version: 5.0.0 commitizen: specifier: 4.3.0 - version: 4.3.0 + version: 4.3.0(typescript@4.9.5) cross-env: specifier: 7.0.3 version: 7.0.3 @@ -285,7 +288,7 @@ devDependencies: version: 8.0.3 jest: specifier: 29.7.0 - version: 29.7.0(@types/node@20.10.3)(ts-node@10.9.1) + version: 29.7.0(@types/node@20.10.3) jest-axe: specifier: 8.0.0 version: 8.0.0 @@ -310,6 +313,9 @@ devDependencies: memory-fs: specifier: 0.5.0 version: 0.5.0 + object-hash: + specifier: ^3.0.0 + version: 3.0.0 postcss-styled-syntax: specifier: 0.5.0 version: 0.5.0(postcss@8.4.31) @@ -337,6 +343,9 @@ devDependencies: react-dom-17: specifier: npm:react-dom@17.0.2 version: /react-dom@17.0.2(react@18.2.0) + react-frame-component: + specifier: ^5.2.6 + version: 5.2.6(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0) react-virtualized: specifier: 9.22.5 version: 9.22.5(react-dom@18.2.0)(react@18.2.0) @@ -2339,16 +2348,6 @@ packages: conventional-changelog-conventionalcommits: 7.0.2 dev: true - /@commitlint/config-validator@17.8.1: - resolution: {integrity: sha512-UUgUC+sNiiMwkyiuIFR7JG2cfd9t/7MV8VB4TZ+q02ZFkHoduUS4tJGsCBWvBOGD9Btev6IecPMvlWUfJorkEA==} - engines: {node: '>=v14'} - requiresBuild: true - dependencies: - '@commitlint/types': 17.8.1 - ajv: 8.12.0 - dev: true - optional: true - /@commitlint/config-validator@18.4.3: resolution: {integrity: sha512-FPZZmTJBARPCyef9ohRC9EANiQEKSWIdatx5OlgeHKu878dWwpyeFauVkhzuBRJFcCA4Uvz/FDtlDKs008IHcA==} engines: {node: '>=v18'} @@ -2368,7 +2367,7 @@ packages: '@commitlint/load': 18.4.3(typescript@4.9.5) '@commitlint/types': 18.4.3 chalk: 4.1.2 - commitizen: 4.3.0 + commitizen: 4.3.0(typescript@4.9.5) inquirer: 8.2.5 lodash.isplainobject: 4.0.6 word-wrap: 1.2.5 @@ -2388,13 +2387,6 @@ packages: lodash.upperfirst: 4.3.1 dev: true - /@commitlint/execute-rule@17.8.1: - resolution: {integrity: sha512-JHVupQeSdNI6xzA9SqMF+p/JjrHTcrJdI02PwesQIDCIGUrv04hicJgCcws5nzaoZbROapPs0s6zeVHoxpMwFQ==} - engines: {node: '>=v14'} - requiresBuild: true - dev: true - optional: true - /@commitlint/execute-rule@18.4.3: resolution: {integrity: sha512-t7FM4c+BdX9WWZCPrrbV5+0SWLgT3kCq7e7/GhHCreYifg3V8qyvO127HF796vyFql75n4TFF+5v1asOOWkV1Q==} engines: {node: '>=v18'} @@ -2426,31 +2418,6 @@ packages: '@commitlint/types': 18.4.3 dev: true - /@commitlint/load@17.8.1: - resolution: {integrity: sha512-iF4CL7KDFstP1kpVUkT8K2Wl17h2yx9VaR1ztTc8vzByWWcbO/WaKwxsnCOqow9tVAlzPfo1ywk9m2oJ9ucMqA==} - engines: {node: '>=v14'} - requiresBuild: true - dependencies: - '@commitlint/config-validator': 17.8.1 - '@commitlint/execute-rule': 17.8.1 - '@commitlint/resolve-extends': 17.8.1 - '@commitlint/types': 17.8.1 - '@types/node': 20.5.1 - chalk: 4.1.2 - cosmiconfig: 8.3.6(typescript@4.9.5) - cosmiconfig-typescript-loader: 4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6)(ts-node@10.9.1)(typescript@4.9.5) - lodash.isplainobject: 4.0.6 - lodash.merge: 4.6.2 - lodash.uniq: 4.5.0 - resolve-from: 5.0.0 - ts-node: 10.9.1(@types/node@20.10.3)(typescript@4.9.5) - typescript: 4.9.5 - transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - dev: true - optional: true - /@commitlint/load@18.4.3(typescript@4.9.5): resolution: {integrity: sha512-v6j2WhvRQJrcJaj5D+EyES2WKTxPpxENmNpNG3Ww8MZGik3jWRXtph0QTzia5ZJyPh2ib5aC/6BIDymkUUM58Q==} engines: {node: '>=v18'} @@ -2496,20 +2463,6 @@ packages: minimist: 1.2.8 dev: true - /@commitlint/resolve-extends@17.8.1: - resolution: {integrity: sha512-W/ryRoQ0TSVXqJrx5SGkaYuAaE/BUontL1j1HsKckvM6e5ZaG0M9126zcwL6peKSuIetJi7E87PRQF8O86EW0Q==} - engines: {node: '>=v14'} - requiresBuild: true - dependencies: - '@commitlint/config-validator': 17.8.1 - '@commitlint/types': 17.8.1 - import-fresh: 3.3.0 - lodash.mergewith: 4.6.2 - resolve-from: 5.0.0 - resolve-global: 1.0.0 - dev: true - optional: true - /@commitlint/resolve-extends@18.4.3: resolution: {integrity: sha512-30sk04LZWf8+SDgJrbJCjM90gTg2LxsD9cykCFeFu+JFHvBFq5ugzp2eO/DJGylAdVaqxej3c7eTSE64hR/lnw==} engines: {node: '>=v18'} @@ -2545,15 +2498,6 @@ packages: find-up: 5.0.0 dev: true - /@commitlint/types@17.8.1: - resolution: {integrity: sha512-PXDQXkAmiMEG162Bqdh9ChML/GJZo6vU+7F03ALKDK8zYc6SuAr47LjG7hGYRqUOz+WK0dU7bQ0xzuqFMdxzeQ==} - engines: {node: '>=v14'} - requiresBuild: true - dependencies: - chalk: 4.1.2 - dev: true - optional: true - /@commitlint/types@18.4.3: resolution: {integrity: sha512-cvzx+vtY/I2hVBZHCLrpoh+sA0hfuzHwDc+BAFPimYLjJkpHnghQM+z8W/KyLGkygJh3BtI3xXXq+dKjnSWEmA==} engines: {node: '>=v18'} @@ -2561,14 +2505,6 @@ packages: chalk: 4.1.2 dev: true - /@cspotcode/source-map-support@0.8.1: - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - requiresBuild: true - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - dev: true - /@csstools/css-parser-algorithms@2.3.2(@csstools/css-tokenizer@2.2.1): resolution: {integrity: sha512-sLYGdAdEY2x7TSw9FtmdaTrh2wFtRJO5VMbBrA8tEqEod7GEggFmxTSK9XqExib3yMuYNcvcTdCZIP6ukdjAIA==} engines: {node: ^14 || ^16 || >=18} @@ -2927,7 +2863,7 @@ packages: slash: 3.0.0 dev: true - /@jest/core@29.7.0(ts-node@10.9.1): + /@jest/core@29.7.0: resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -2948,7 +2884,7 @@ packages: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.10.3)(ts-node@10.9.1) + jest-config: 29.7.0(@types/node@20.10.3) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -3194,12 +3130,6 @@ packages: engines: {node: '>=6.0.0'} dev: true - /@jridgewell/resolve-uri@3.1.1: - resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} - engines: {node: '>=6.0.0'} - requiresBuild: true - dev: true - /@jridgewell/set-array@1.1.2: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} engines: {node: '>=6.0.0'} @@ -3227,14 +3157,6 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: true - /@jridgewell/trace-mapping@0.3.9: - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - requiresBuild: true - dependencies: - '@jridgewell/resolve-uri': 3.1.1 - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true - /@ljharb/through@2.3.9: resolution: {integrity: sha512-yN599ZBuMPPK4tdoToLlvgJB4CLK8fGl7ntfy0Wn7U6ttNvHYurd81bfUiK/6sMkiIwm65R6ck4L6+Y3DfVbNQ==} engines: {node: '>= 0.4'} @@ -5248,26 +5170,6 @@ packages: resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} dev: true - /@tsconfig/node10@1.0.9: - resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} - requiresBuild: true - dev: true - - /@tsconfig/node12@1.0.11: - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} - requiresBuild: true - dev: true - - /@tsconfig/node14@1.0.3: - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} - requiresBuild: true - dev: true - - /@tsconfig/node16@1.0.4: - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - requiresBuild: true - dev: true - /@types/aria-query@5.0.1: resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==} dev: true @@ -5541,12 +5443,6 @@ packages: undici-types: 5.26.5 dev: true - /@types/node@20.5.1: - resolution: {integrity: sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==} - requiresBuild: true - dev: true - optional: true - /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} dev: true @@ -5555,6 +5451,10 @@ packages: resolution: {integrity: sha512-WKG4gTr8przEZBiJ5r3s8ZIAoMXNbOgQ+j/d5O4X3x6kZJRLNvyUJuUK/KoG3+8BaOHPhp2m7WC6JKKeovDSzQ==} dev: true + /@types/object-hash@3.0.6: + resolution: {integrity: sha512-fOBV8C1FIu2ELinoILQ+ApxcUKz4ngq+IWUYrxSGjXzzjUALijilampwkMgEtJ+h2njAW3pi853QpzNVCHB73w==} + dev: true + /@types/parse-json@4.0.0: resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} dev: true @@ -6525,11 +6425,6 @@ packages: readable-stream: 3.6.2 dev: true - /arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - requiresBuild: true - dev: true - /argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: @@ -8085,13 +7980,13 @@ packages: engines: {node: '>= 12'} dev: true - /commitizen@4.3.0: + /commitizen@4.3.0(typescript@4.9.5): resolution: {integrity: sha512-H0iNtClNEhT0fotHvGV3E9tDejDeS04sN1veIebsKYGMuGscFaswRoYJKmT3eW85eIJAs0F28bG2+a/9wCOfPw==} engines: {node: '>= 12'} hasBin: true dependencies: cachedir: 2.3.0 - cz-conventional-changelog: 3.3.0 + cz-conventional-changelog: 3.3.0(typescript@4.9.5) dedent: 0.7.0 detect-indent: 6.1.0 find-node-modules: 2.1.3 @@ -8105,8 +8000,7 @@ packages: strip-bom: 4.0.0 strip-json-comments: 3.1.1 transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' + - typescript dev: true /common-path-prefix@3.0.0: @@ -8434,23 +8328,6 @@ packages: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} dev: true - /cosmiconfig-typescript-loader@4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6)(ts-node@10.9.1)(typescript@4.9.5): - resolution: {integrity: sha512-BabizFdC3wBHhbI4kJh0VkQP9GkBfoHPydD0COMce1nJ1kJAB3F2TmJ/I7diULBKtmEWSwEbuN/KDtgnmUUVmw==} - engines: {node: '>=v14.21.3'} - requiresBuild: true - peerDependencies: - '@types/node': '*' - cosmiconfig: '>=7' - ts-node: '>=10' - typescript: '>=4' - dependencies: - '@types/node': 20.5.1 - cosmiconfig: 8.3.6(typescript@4.9.5) - ts-node: 10.9.1(@types/node@20.10.3)(typescript@4.9.5) - typescript: 4.9.5 - dev: true - optional: true - /cosmiconfig-typescript-loader@5.0.0(@types/node@18.19.2)(cosmiconfig@8.3.6)(typescript@4.9.5): resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==} engines: {node: '>=v16'} @@ -8568,7 +8445,7 @@ packages: sha.js: 2.4.11 dev: true - /create-jest@29.7.0(@types/node@20.10.3)(ts-node@10.9.1): + /create-jest@29.7.0(@types/node@20.10.3): resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -8577,7 +8454,7 @@ packages: chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.10.3)(ts-node@10.9.1) + jest-config: 29.7.0(@types/node@20.10.3) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -8587,11 +8464,6 @@ packages: - ts-node dev: true - /create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - requiresBuild: true - dev: true - /cross-env@7.0.3: resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} @@ -8854,21 +8726,20 @@ packages: yauzl: 2.10.0 dev: true - /cz-conventional-changelog@3.3.0: + /cz-conventional-changelog@3.3.0(typescript@4.9.5): resolution: {integrity: sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==} engines: {node: '>= 10'} dependencies: chalk: 2.4.2 - commitizen: 4.3.0 + commitizen: 4.3.0(typescript@4.9.5) conventional-commit-types: 3.0.0 lodash.map: 4.6.0 longest: 2.0.1 word-wrap: 1.2.5 optionalDependencies: - '@commitlint/load': 17.8.1 + '@commitlint/load': 18.4.3(typescript@4.9.5) transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' + - typescript dev: true /damerau-levenshtein@1.0.8: @@ -9247,12 +9118,6 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} - engines: {node: '>=0.3.1'} - requiresBuild: true - dev: true - /diffie-hellman@5.0.3: resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==} dependencies: @@ -9893,7 +9758,7 @@ packages: '@typescript-eslint/eslint-plugin': 6.13.2(@typescript-eslint/parser@6.13.2)(eslint@8.55.0)(typescript@4.9.5) '@typescript-eslint/utils': 5.62.0(eslint@8.55.0)(typescript@4.9.5) eslint: 8.55.0 - jest: 29.7.0(@types/node@20.10.3)(ts-node@10.9.1) + jest: 29.7.0(@types/node@20.10.3) transitivePeerDependencies: - supports-color - typescript @@ -12804,7 +12669,7 @@ packages: - supports-color dev: true - /jest-cli@29.7.0(@types/node@20.10.3)(ts-node@10.9.1): + /jest-cli@29.7.0(@types/node@20.10.3): resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -12814,14 +12679,14 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.7.0(ts-node@10.9.1) + '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.10.3)(ts-node@10.9.1) + create-jest: 29.7.0(@types/node@20.10.3) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.10.3)(ts-node@10.9.1) + jest-config: 29.7.0(@types/node@20.10.3) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -12832,7 +12697,7 @@ packages: - ts-node dev: true - /jest-config@29.7.0(@types/node@20.10.3)(ts-node@10.9.1): + /jest-config@29.7.0(@types/node@20.10.3): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -12867,7 +12732,6 @@ packages: pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1(@types/node@20.10.3)(typescript@4.9.5) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -13325,7 +13189,7 @@ packages: dependencies: ansi-escapes: 6.2.0 chalk: 5.3.0 - jest: 29.7.0(@types/node@20.10.3)(ts-node@10.9.1) + jest: 29.7.0(@types/node@20.10.3) jest-regex-util: 29.4.3 jest-watcher: 29.6.1 slash: 5.1.0 @@ -13389,7 +13253,7 @@ packages: supports-color: 8.1.1 dev: true - /jest@29.7.0(@types/node@20.10.3)(ts-node@10.9.1): + /jest@29.7.0(@types/node@20.10.3): resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -13399,10 +13263,10 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.7.0(ts-node@10.9.1) + '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.10.3)(ts-node@10.9.1) + jest-cli: 29.7.0(@types/node@20.10.3) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -14113,11 +13977,6 @@ packages: semver: 7.5.4 dev: true - /make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - requiresBuild: true - dev: true - /makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} dependencies: @@ -14881,6 +14740,11 @@ packages: kind-of: 3.2.2 dev: true + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} dev: true @@ -16251,6 +16115,7 @@ packages: react: 18.2.0 scheduler: 0.19.1 dev: true + bundledDependencies: false /react-dom@17.0.2(react@18.2.0): resolution: {integrity: sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==} @@ -16285,6 +16150,18 @@ packages: react-is: 17.0.2 dev: true + /react-frame-component@5.2.6(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-CwkEM5VSt6nFwZ1Op8hi3JB5rPseZlmnp5CGiismVTauE6S4Jsc4TNMlT0O7Cts4WgIC3ZBAQ2p1Mm9XgLbj+w==} + peerDependencies: + prop-types: ^15.5.9 + react: '>= 16.3' + react-dom: '>= 16.3' + dependencies: + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + /react-inspector@5.1.1(react@18.2.0): resolution: {integrity: sha512-GURDaYzoLbW8pMGXwYPDBIv6nqei4kK7LPRZ9q9HCZF54wqXz/dnylBp/kfE9XmekBhHvLDdcYeyIwSrvtOiWg==} peerDependencies: @@ -16399,6 +16276,7 @@ packages: object-assign: 4.1.1 prop-types: 15.8.1 dev: true + bundledDependencies: false /react@17.0.2: resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==} @@ -18483,38 +18361,6 @@ packages: engines: {node: '>=6.10'} dev: true - /ts-node@10.9.1(@types/node@20.10.3)(typescript@4.9.5): - resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} - hasBin: true - requiresBuild: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.9 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 20.10.3 - acorn: 8.10.0 - acorn-walk: 8.2.0 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 4.9.5 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - dev: true - /ts-pnp@1.2.0(typescript@4.9.5): resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==} engines: {node: '>=6'} @@ -19081,11 +18927,6 @@ packages: hasBin: true dev: true - /v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - requiresBuild: true - dev: true - /v8-to-istanbul@9.1.0: resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} engines: {node: '>=10.12.0'} @@ -19761,12 +19602,6 @@ packages: fd-slicer: 1.1.0 dev: true - /yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} - requiresBuild: true - dev: true - /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} diff --git a/src/state/dimension-marshal/get-initial-publish.ts b/src/state/dimension-marshal/get-initial-publish.ts index d4b922bb2..048bff8d9 100644 --- a/src/state/dimension-marshal/get-initial-publish.ts +++ b/src/state/dimension-marshal/get-initial-publish.ts @@ -31,7 +31,7 @@ export default ({ }: Args): StartPublishingResult => { const timingKey = 'Initial collection from DOM'; timings.start(timingKey); - const viewport: Viewport = getViewport(); + const viewport: Viewport = getViewport(critical); const windowScroll: Position = viewport.scroll.current; const home: DroppableDescriptor = critical.droppable; diff --git a/src/state/get-frame.ts b/src/state/get-frame.ts index 525ab5db3..d36b6ecdf 100644 --- a/src/state/get-frame.ts +++ b/src/state/get-frame.ts @@ -5,5 +5,6 @@ import type { DroppableDimension, Scrollable } from '../types'; export default (droppable: DroppableDimension): Scrollable => { const frame: Scrollable | null = droppable.frame; invariant(frame, 'Expected Droppable to have a frame'); + return frame; }; diff --git a/src/state/move-in-direction/move-cross-axis/index.ts b/src/state/move-in-direction/move-cross-axis/index.ts index 28abdeb43..2deb7e8b4 100644 --- a/src/state/move-in-direction/move-cross-axis/index.ts +++ b/src/state/move-in-direction/move-cross-axis/index.ts @@ -16,6 +16,7 @@ import getDraggablesInsideDroppable from '../../get-draggables-inside-droppable' import getClientFromPageBorderBoxCenter from '../../get-center-from-impact/get-client-border-box-center/get-client-from-page-border-box-center'; import getPageBorderBoxCenter from '../../get-center-from-impact/get-page-border-box-center'; import moveToNewDroppable from './move-to-new-droppable'; +import { subtract } from '../../position'; interface Args { isMovingForward: boolean; @@ -100,8 +101,14 @@ export default ({ viewport, }); + // Offset viewport when moving along axis (such as for using keyboard sensor with iframes) + const offsetClientSelection = subtract(clientSelection, { + x: viewport.offset.x, + y: viewport.offset.y, + }); + return { - clientSelection, + clientSelection: offsetClientSelection, impact, scrollJumpRequest: null, }; diff --git a/src/state/move-in-direction/move-to-next-place/index.ts b/src/state/move-in-direction/move-to-next-place/index.ts index dd3f7e6c2..502213ea3 100644 --- a/src/state/move-in-direction/move-to-next-place/index.ts +++ b/src/state/move-in-direction/move-to-next-place/index.ts @@ -103,8 +103,15 @@ export default ({ draggable, viewport, }); + + // Offset viewport when moving along axis (such as for using keyboard sensor with iframes) + const offsetClientSelection = subtract(clientSelection, { + x: viewport.offset.x, + y: viewport.offset.y, + }); + return { - clientSelection, + clientSelection: offsetClientSelection, impact, scrollJumpRequest: null, }; diff --git a/src/types.ts b/src/types.ts index 9d2a22a02..eb48ad45b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -305,6 +305,7 @@ export interface Viewport { // live updates with the latest values frame: Rect; scroll: ScrollDetails; + offset: Rect; } export interface LiftEffect { diff --git a/src/view/event-bindings/bind-events.ts b/src/view/event-bindings/bind-events.ts index 990c2d486..c72cdaab7 100644 --- a/src/view/event-bindings/bind-events.ts +++ b/src/view/event-bindings/bind-events.ts @@ -1,3 +1,5 @@ +import { IframeHTMLAttributes } from 'react'; +import { querySelectorAll } from '../../query-selector-all'; import type { AnyEventBinding, EventBinding, @@ -21,15 +23,26 @@ export default function bindEvents( bindings: AnyEventBinding[], sharedOptions?: EventOptions, ): () => void { - const unbindings: UnbindFn[] = (bindings as EventBinding[]).map( - (binding): UnbindFn => { - const options = getOptions(sharedOptions, binding.options); + const unbindings: UnbindFn[] = (bindings as EventBinding[]).flatMap( + (binding): UnbindFn[] => { + const iframes: HTMLIFrameElement[] = querySelectorAll( + window.document, + 'iframe', + ) as HTMLIFrameElement[]; - el.addEventListener(binding.eventName, binding.fn, options); + const windows = [el, ...iframes.map((iframe) => iframe.contentWindow)]; - return function unbind() { - el.removeEventListener(binding.eventName, binding.fn, options); - }; + return windows.map((win) => { + if (!win) return function unbind() {}; + + const options = getOptions(sharedOptions, binding.options); + + win.addEventListener(binding.eventName, binding.fn, options); + + return function unbind() { + win.removeEventListener(binding.eventName, binding.fn, options); + }; + }); }, ); diff --git a/src/view/get-elements/find-drag-handle.ts b/src/view/get-elements/find-drag-handle.ts index de81d43a3..3aebb10c1 100644 --- a/src/view/get-elements/find-drag-handle.ts +++ b/src/view/get-elements/find-drag-handle.ts @@ -1,8 +1,8 @@ import type { DraggableId, ContextId } from '../../types'; import { dragHandle as dragHandleAttr } from '../data-attributes'; import { warning } from '../../dev-warning'; -import { querySelectorAll } from '../../query-selector-all'; import isHtmlElement from '../is-type-of-element/is-html-element'; +import querySelectorAllIframe from '../iframe/query-selector-all-iframe'; export default function findDragHandle( contextId: ContextId, @@ -10,10 +10,13 @@ export default function findDragHandle( ): HTMLElement | null { // cannot create a selector with the draggable id as it might not be a valid attribute selector const selector = `[${dragHandleAttr.contextId}="${contextId}"]`; - const possible = querySelectorAll(document, selector); + + const possible = querySelectorAllIframe(selector); if (!possible.length) { - warning(`Unable to find any drag handles in the context "${contextId}"`); + warning( + `Unable to find any drag handles in the context "${contextId}" ${selector}`, + ); return null; } diff --git a/src/view/get-elements/find-draggable.ts b/src/view/get-elements/find-draggable.ts index d1df0aad2..ffac459c4 100644 --- a/src/view/get-elements/find-draggable.ts +++ b/src/view/get-elements/find-draggable.ts @@ -1,8 +1,8 @@ import type { DraggableId, ContextId } from '../../types'; import * as attributes from '../data-attributes'; -import { querySelectorAll } from '../../query-selector-all'; import { warning } from '../../dev-warning'; import isHtmlElement from '../is-type-of-element/is-html-element'; +import querySelectorAllIframe from '../iframe/query-selector-all-iframe'; export default function findDraggable( contextId: ContextId, @@ -10,7 +10,8 @@ export default function findDraggable( ): HTMLElement | null { // cannot create a selector with the draggable id as it might not be a valid attribute selector const selector = `[${attributes.draggable.contextId}="${contextId}"]`; - const possible = querySelectorAll(document, selector); + + const possible = querySelectorAllIframe(selector); const draggable = possible.find((el): boolean => { return el.getAttribute(attributes.draggable.id) === draggableId; diff --git a/src/view/iframe/apply-offset.ts b/src/view/iframe/apply-offset.ts new file mode 100644 index 000000000..d3959a90a --- /dev/null +++ b/src/view/iframe/apply-offset.ts @@ -0,0 +1,20 @@ +import { Rect } from 'css-box-model'; +import { Offset } from './offset-types'; + +export default function applyOffset(rect: Partial, offset: Offset): Rect { + return { + ...rect, + top: (rect.top || 0) + offset.top, + left: (rect.left || 0) + offset.left, + right: (rect.right || 0) + offset.right, + bottom: (rect.bottom || 0) + offset.bottom, + x: (rect.x || 0) + offset.left, + y: (rect.y || 0) + offset.top, + center: { + x: (rect.center?.x || 0) + offset.left, + y: (rect.center?.y || 0) + offset.top, + }, + width: rect.width || 0, + height: rect.height || 0, + }; +} diff --git a/src/view/iframe/get-iframe-offset.ts b/src/view/iframe/get-iframe-offset.ts new file mode 100644 index 000000000..f7aec7d40 --- /dev/null +++ b/src/view/iframe/get-iframe-offset.ts @@ -0,0 +1,23 @@ +import { Offset } from './offset-types'; + +export default function getIframeOffset(el: HTMLElement) { + const offset: Offset = { + top: 0, + left: 0, + bottom: 0, + right: 0, + }; + + const refWindow = el.ownerDocument.defaultView; + + if (refWindow && refWindow.self !== refWindow.parent) { + const iframe = refWindow.frameElement as HTMLIFrameElement; + + offset.left = iframe.offsetLeft; + offset.top = iframe.offsetTop; + offset.right = iframe.offsetLeft; + offset.bottom = iframe.offsetTop; + } + + return offset; +} diff --git a/src/view/iframe/get-offsetted-box.ts b/src/view/iframe/get-offsetted-box.ts new file mode 100644 index 000000000..2b066c436 --- /dev/null +++ b/src/view/iframe/get-offsetted-box.ts @@ -0,0 +1,17 @@ +import { getBox } from 'css-box-model'; +import getIframeOffset from './get-iframe-offset'; +import applyOffset from './apply-offset'; + +export default function getOffsettedBox(el: HTMLElement) { + const box = getBox(el); + + const offset = getIframeOffset(el); + + return { + ...box, + borderBox: applyOffset(box.borderBox, offset), + marginBox: applyOffset(box.marginBox, offset), + paddingBox: applyOffset(box.paddingBox, offset), + contentBox: applyOffset(box.contentBox, offset), + }; +} diff --git a/src/view/iframe/offset-types.ts b/src/view/iframe/offset-types.ts new file mode 100644 index 000000000..8e7f5d514 --- /dev/null +++ b/src/view/iframe/offset-types.ts @@ -0,0 +1,6 @@ +export interface Offset { + top: number; + left: number; + bottom: number; + right: number; +} diff --git a/src/view/iframe/query-selector-all-iframe.ts b/src/view/iframe/query-selector-all-iframe.ts new file mode 100644 index 000000000..a94b048fc --- /dev/null +++ b/src/view/iframe/query-selector-all-iframe.ts @@ -0,0 +1,21 @@ +/** + * querySelectorAllIframe + * + * An proxy of querySelectorAll that also queries all iframes + */ + +import { querySelectorAll } from '../../query-selector-all'; + +export default function querySelectorAllIframe(selector: string) { + const iframes = querySelectorAll(document, 'iframe') as HTMLIFrameElement[]; + + const iframePossible = iframes.reduce( + (acc, iframe) => [ + ...acc, + ...querySelectorAll(iframe.contentWindow!.document, selector), + ], + [], + ); + + return [...querySelectorAll(document, selector), ...iframePossible]; +} diff --git a/src/view/use-draggable-publisher/get-dimension.ts b/src/view/use-draggable-publisher/get-dimension.ts index 29a7e4d55..967e49ccb 100644 --- a/src/view/use-draggable-publisher/get-dimension.ts +++ b/src/view/use-draggable-publisher/get-dimension.ts @@ -6,6 +6,8 @@ import type { Placeholder, } from '../../types'; import { origin } from '../../state/position'; +import getIframeOffset from '../iframe/get-iframe-offset'; +import applyOffset from '../iframe/apply-offset'; export default function getDimension( descriptor: DraggableDescriptor, @@ -13,7 +15,10 @@ export default function getDimension( windowScroll: Position = origin, ): DraggableDimension { const computedStyles: CSSStyleDeclaration = window.getComputedStyle(el); - const borderBox: ClientRect = el.getBoundingClientRect(); + + const offset = getIframeOffset(el); + const borderBox = applyOffset(el.getBoundingClientRect(), offset); + const client: BoxModel = calculateBox(borderBox, computedStyles); const page: BoxModel = withScroll(client, windowScroll); diff --git a/src/view/use-droppable-publisher/get-dimension.ts b/src/view/use-droppable-publisher/get-dimension.ts index 172bef3b3..c70e28d66 100644 --- a/src/view/use-droppable-publisher/get-dimension.ts +++ b/src/view/use-droppable-publisher/get-dimension.ts @@ -10,12 +10,13 @@ import type { ScrollSize, } from '../../types'; import getScroll from './get-scroll'; +import getOffsettedBox from '../iframe/get-offsetted-box'; const getClient = ( targetRef: HTMLElement, closestScrollable?: Element | null, ): BoxModel => { - const base: BoxModel = getBox(targetRef); + const base: BoxModel = getOffsettedBox(targetRef); // Droppable has no scroll parent if (!closestScrollable) { diff --git a/src/view/use-sensor-marshal/sensors/use-mouse-sensor.ts b/src/view/use-sensor-marshal/sensors/use-mouse-sensor.ts index d35857a06..f99a63b03 100644 --- a/src/view/use-sensor-marshal/sensors/use-mouse-sensor.ts +++ b/src/view/use-sensor-marshal/sensors/use-mouse-sensor.ts @@ -21,6 +21,7 @@ import preventStandardKeyEvents from './util/prevent-standard-key-events'; import supportedPageVisibilityEventName from './util/supported-page-visibility-event-name'; import useLayoutEffect from '../../use-isomorphic-layout-effect'; import { noop } from '../../../empty'; +import offsetPoint from './util/offset-point'; // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button export const primaryButton = 0; @@ -72,15 +73,16 @@ function getCaptureBindings({ { eventName: 'mousemove', fn: (event: MouseEvent) => { - const { button, clientX, clientY } = event; + const { button } = event; if (button !== primaryButton) { return; } - const point: Position = { - x: clientX, - y: clientY, - }; + const point = offsetPoint( + event.clientX, + event.clientY, + event.currentTarget as Window, + ); const phase: Phase = getPhase(); @@ -252,10 +254,11 @@ export default function useMouseSensor(api: SensorAPI) { // consuming the event event.preventDefault(); - const point: Position = { - x: event.clientX, - y: event.clientY, - }; + const point = offsetPoint( + event.clientX, + event.clientY, + event.currentTarget as Window, + ); // unbind this listener unbindEventsRef.current(); diff --git a/src/view/use-sensor-marshal/sensors/use-touch-sensor.ts b/src/view/use-sensor-marshal/sensors/use-touch-sensor.ts index 73b07a101..34feafd65 100644 --- a/src/view/use-sensor-marshal/sensors/use-touch-sensor.ts +++ b/src/view/use-sensor-marshal/sensors/use-touch-sensor.ts @@ -18,6 +18,7 @@ import * as keyCodes from '../../key-codes'; import supportedPageVisibilityEventName from './util/supported-page-visibility-event-name'; import { noop } from '../../../empty'; import useLayoutEffect from '../../use-isomorphic-layout-effect'; +import offsetPoint from './util/offset-point'; type TouchWithForce = Touch & { force: number; @@ -134,10 +135,11 @@ function getHandleBindings({ const { clientX, clientY } = event.touches[0]; - const point: Position = { - x: clientX, - y: clientY, - }; + const point = offsetPoint( + clientX, + clientY, + event.currentTarget as Window, + ); // We need to prevent the default event in order to block native scrolling // Also because we are using it as part of a drag we prevent the default action @@ -289,10 +291,12 @@ export default function useTouchSensor(api: SensorAPI) { const touch: Touch = event.touches[0]; const { clientX, clientY } = touch; - const point: Position = { - x: clientX, - y: clientY, - }; + + const point = offsetPoint( + clientX, + clientY, + event.currentTarget as Window, + ); // unbind this event handler unbindEventsRef.current(); diff --git a/src/view/use-sensor-marshal/sensors/util/offset-point.ts b/src/view/use-sensor-marshal/sensors/util/offset-point.ts new file mode 100644 index 000000000..7ccbd9237 --- /dev/null +++ b/src/view/use-sensor-marshal/sensors/util/offset-point.ts @@ -0,0 +1,24 @@ +import { Position } from 'css-box-model'; + +export default function offsetPoint(x: number, y: number, win: Window) { + // const { clientX, clientY } = event; + + // const win = event.currentTarget as Window; + + let offsetX = 0; + let offsetY = 0; + + if (win.parent !== win.self) { + const iframe = win.frameElement as HTMLIFrameElement; + + offsetX = iframe.offsetLeft; + offsetY = iframe.offsetTop; + } + + const point: Position = { + x: x + offsetX, + y: y + offsetY, + }; + + return point; +} diff --git a/src/view/use-style-marshal/use-style-marshal.ts b/src/view/use-style-marshal/use-style-marshal.ts index 7b2e64703..de91eb761 100644 --- a/src/view/use-style-marshal/use-style-marshal.ts +++ b/src/view/use-style-marshal/use-style-marshal.ts @@ -1,4 +1,3 @@ -import { useRef, MutableRefObject } from 'react'; import memoizeOne from 'memoize-one'; import { useMemo, useCallback } from 'use-memo-one'; import { invariant } from '../../invariant'; @@ -8,10 +7,11 @@ import getStyles from './get-styles'; import type { Styles } from './get-styles'; import { prefix } from '../data-attributes'; import useLayoutEffect from '../use-isomorphic-layout-effect'; +import { querySelectorAll } from '../../query-selector-all'; +import querySelectorAllIframe from '../iframe/query-selector-all-iframe'; -const getHead = (): HTMLHeadElement => { - const head: HTMLHeadElement | null = document.querySelector('head'); - invariant(head, 'Cannot find the head to append a style to'); +const getHead = (doc: Document): HTMLHeadElement | null => { + const head: HTMLHeadElement | null = doc.querySelector('head'); return head; }; @@ -24,64 +24,92 @@ const createStyleEl = (nonce?: string): HTMLStyleElement => { return el; }; +const alwaysDataAttr = `${prefix}-always`; +const alwaysSelector = `[${alwaysDataAttr}]`; +const dynamicDataAttr = `${prefix}-dynamic`; +const dynamicSelector = `[${dynamicDataAttr}]`; + export default function useStyleMarshal(contextId: ContextId, nonce?: string) { const styles: Styles = useMemo(() => getStyles(contextId), [contextId]); - const alwaysRef = useRef(null); - const dynamicRef = useRef(null); + // const alwaysMapRef = useRef({}); + // const dynamicMapRef = useRef({}); // eslint-disable-next-line react-hooks/exhaustive-deps const setDynamicStyle = useCallback( // Using memoizeOne to prevent frequent updates to textContext memoizeOne((proposed: string) => { - const el: HTMLStyleElement | null = dynamicRef.current; - invariant(el, 'Cannot set dynamic style element if it is not set'); - el.textContent = proposed; + const selector = `[data-rfd-dynamic="${contextId}"]`; + + querySelectorAllIframe(selector).forEach((el) => { + invariant(el, 'Cannot set dynamic style element if it is not set'); + el.textContent = proposed; + }); }), - [], + [contextId], ); - const setAlwaysStyle = useCallback((proposed: string) => { - const el: HTMLStyleElement | null = alwaysRef.current; - invariant(el, 'Cannot set dynamic style element if it is not set'); - el.textContent = proposed; - }, []); + const setAlwaysStyle = useCallback( + (proposed: string) => { + const selector = `[data-rfd-always="${contextId}"]`; + + querySelectorAllIframe(selector).forEach((el) => { + invariant(el, 'Cannot set dynamic style element if it is not set'); + el.textContent = proposed; + }); + }, + [contextId], + ); // using layout effect as programatic dragging might start straight away (such as for cypress) useLayoutEffect(() => { - invariant( - !alwaysRef.current && !dynamicRef.current, - 'style elements already mounted', - ); + const heads = [ + getHead(document), + // TODO make this based on data attribute + ...( + querySelectorAll(document, `[${prefix}-iframe]`) as HTMLIFrameElement[] + ).map((iframe) => getHead(iframe.contentWindow!.document)), + ]; + + heads.forEach((head) => { + const alwaysElements = querySelectorAllIframe(alwaysSelector); + const dynamicElements = querySelectorAllIframe(dynamicSelector); + + // console.log(dynamicElements); + + if ( + alwaysElements.length >= heads.length || + dynamicElements.length >= heads.length + ) { + return; + } - const always: HTMLStyleElement = createStyleEl(nonce); - const dynamic: HTMLStyleElement = createStyleEl(nonce); + const always: HTMLStyleElement = createStyleEl(nonce); + const dynamic: HTMLStyleElement = createStyleEl(nonce); - // store their refs - alwaysRef.current = always; - dynamicRef.current = dynamic; + // for easy identification + always.setAttribute(alwaysDataAttr, contextId); + dynamic.setAttribute(dynamicDataAttr, contextId); - // for easy identification - always.setAttribute(`${prefix}-always`, contextId); - dynamic.setAttribute(`${prefix}-dynamic`, contextId); + head?.appendChild(always); + head?.appendChild(dynamic); - // add style tags to head - getHead().appendChild(always); - getHead().appendChild(dynamic); - - // set initial style - setAlwaysStyle(styles.always); - setDynamicStyle(styles.resting); + // set initial style + setAlwaysStyle(styles.always); + setDynamicStyle(styles.resting); + }); return () => { - const remove = (ref: MutableRefObject) => { - const current: HTMLStyleElement | null = ref.current; - invariant(current, 'Cannot unmount ref as it is not set'); - getHead().removeChild(current); - ref.current = null; + const remove = (selector: string) => { + const elements = querySelectorAllIframe(selector); + + elements.forEach((el) => { + invariant(el, 'Cannot unmount element as it is not set'); + el.ownerDocument.head.removeChild(el); + }); }; - remove(alwaysRef); - remove(dynamicRef); + remove(alwaysSelector); + remove(dynamicSelector); }; }, [ nonce, @@ -107,10 +135,6 @@ export default function useStyleMarshal(contextId: ContextId, nonce?: string) { [setDynamicStyle, styles.dropAnimating, styles.userCancel], ); const resting = useCallback(() => { - // Can be called defensively - if (!dynamicRef.current) { - return; - } setDynamicStyle(styles.resting); }, [setDynamicStyle, styles.resting]); diff --git a/src/view/window/get-viewport.ts b/src/view/window/get-viewport.ts index 0b7d8f6a7..1c98c7f3a 100644 --- a/src/view/window/get-viewport.ts +++ b/src/view/window/get-viewport.ts @@ -1,12 +1,16 @@ import { getRect } from 'css-box-model'; import type { Rect, Position } from 'css-box-model'; -import type { Viewport } from '../../types'; +import type { Critical, Viewport } from '../../types'; import { origin } from '../../state/position'; import getWindowScroll from './get-window-scroll'; import getMaxWindowScroll from './get-max-window-scroll'; import getDocumentElement from '../get-document-element'; +import getIframeOffset from '../iframe/get-iframe-offset'; +import querySelectorAllIframe from '../iframe/query-selector-all-iframe'; +import { prefix } from '../data-attributes'; +import applyOffset from '../iframe/apply-offset'; -export default (): Viewport => { +export default (critical?: Critical): Viewport => { const scroll: Position = getWindowScroll(); const maxScroll: Position = getMaxWindowScroll(); @@ -32,6 +36,13 @@ export default (): Viewport => { bottom, }); + const droppables = querySelectorAllIframe( + `[${prefix}-droppable-id="${critical?.droppable.id}"]`, + ); + + const offset = getIframeOffset(droppables[0]); + const offsetFrame = applyOffset(frame, offset); + const viewport: Viewport = { frame, scroll: { @@ -43,6 +54,7 @@ export default (): Viewport => { displacement: origin, }, }, + offset: offsetFrame, }; return viewport; diff --git a/stories/examples/61-iframe.stories.tsx b/stories/examples/61-iframe.stories.tsx new file mode 100644 index 000000000..9860abdfc --- /dev/null +++ b/stories/examples/61-iframe.stories.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import IframeBoard from '../src/board/iframe-board'; +import { authorQuoteMap } from '../src/data'; + +storiesOf('Examples/iframe', module).add('simple', () => ( + ({ + ...acc, + [author]: authorQuoteMap[author].map((quote) => ({ + ...quote, + author: { ...quote.author, url: '' }, + })), + }), + {}, + )} + /> +)); diff --git a/stories/src/board/column.tsx b/stories/src/board/column.tsx index 3c8ffdebc..420a4fd73 100644 --- a/stories/src/board/column.tsx +++ b/stories/src/board/column.tsx @@ -51,7 +51,7 @@ export default class Column extends Component { const quotes: Quote[] = this.props.quotes; const index: number = this.props.index; return ( - + {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
diff --git a/stories/src/board/iframe-board.tsx b/stories/src/board/iframe-board.tsx new file mode 100644 index 000000000..c65902c5e --- /dev/null +++ b/stories/src/board/iframe-board.tsx @@ -0,0 +1,272 @@ +import React, { Component, ReactElement, ReactNode } from 'react'; +import styled from '@emotion/styled'; +import { Global, css } from '@emotion/react'; +import { colors } from '@atlaskit/theme'; +import type { + DropResult, + DraggableLocation, + DroppableProvided, +} from '@hello-pangea/dnd'; +import { DragDropContext, Droppable } from '@hello-pangea/dnd'; +import Frame, { useFrame } from 'react-frame-component'; +import hash from 'object-hash'; +import type { QuoteMap } from '../types'; +import { reorderQuoteMap } from '../reorder'; +import { PartialAutoScrollerOptions } from '../../../src/state/auto-scroller/fluid-scroller/auto-scroller-options-types'; +import useLayoutEffect from '../../../src/view/use-isomorphic-layout-effect'; +import QuoteList from '../primatives/quote-list'; +import Title from '../primatives/title'; +import { grid, borderRadius } from '../constants'; + +const Column = styled.div` + margin: ${grid}px; + display: flex; + flex-direction: column; +`; + +const Header = styled.div` + display: flex; + align-items: center; + justify-content: center; + border-top-left-radius: ${borderRadius}px; + border-top-right-radius: ${borderRadius}px; + background-color: ${colors.N30}; + transition: background-color 0.2s ease; + + &:hover { + background-color: ${colors.G50}; + } +`; + +interface Props { + initial: QuoteMap; + withScrollableColumns?: boolean; + isCombineEnabled?: boolean; + containerHeight?: string; + useClone?: boolean; + applyGlobalStyles?: boolean; + autoScrollerOptions?: PartialAutoScrollerOptions; +} + +interface State { + columns: QuoteMap; + ordered: string[]; +} + +// this covers development case as well as part of production +const styleSelector = + process && process.env.NODE_ENV === 'production' + ? 'style, link[as="style"], link[rel="stylesheet"' + : 'style'; + +const collectStyles = (doc: Document) => { + const collected: Node[] = []; + + doc.head.querySelectorAll(styleSelector).forEach((style) => { + collected.push(style); + }); + + return collected; +}; + +const CopyHostStyles = ({ children }: { children: ReactNode }) => { + const { document: doc, window: win } = useFrame(); + + useLayoutEffect(() => { + if (!win) { + return () => {}; + } + + const add = ( + el: HTMLElement, + contentHash: string = hash(el.textContent), + ) => { + if (doc?.head.querySelector(`[data-content-hash="${contentHash}"]`)) { + console.log( + `Style tag with same content (${contentHash}) already exists, skpping...`, + ); + + return; + } + + console.log(`Added style node with content hash ${contentHash}`); + + const frameStyles = el.cloneNode(true); + + (frameStyles as HTMLElement).setAttribute( + 'data-content-hash', + contentHash, + ); + + doc?.head.append(frameStyles); + }; + + const remove = (el: HTMLElement) => { + const contentHash = hash(el.textContent); + const frameStyles = el.cloneNode(true); + + (frameStyles as HTMLElement).setAttribute( + 'data-content-hash', + contentHash, + ); + + console.log( + `Removing node with content hash ${contentHash} as no longer present in parent`, + ); + + doc?.head.querySelector(`[data-content-hash="${contentHash}"]`)?.remove(); + }; + + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType !== Node.ELEMENT_NODE) { + return; + } + + const el = node as HTMLElement; + + if (el.matches(styleSelector)) { + add(el); + } + }); + + mutation.removedNodes.forEach((node) => { + if (node.nodeType !== Node.ELEMENT_NODE) { + return; + } + + const el = node as HTMLElement; + + if (el.matches('style')) { + remove(el); + } + }); + }); + }); + + const parentDocument = win!.parent.document; + + observer.observe(parentDocument.head, { childList: true, subtree: true }); + + const collectedStyles = collectStyles(parentDocument); + + // Add new style tags + collectedStyles.forEach((styleNode) => { + add(styleNode as HTMLElement); + }); + + return () => { + observer.disconnect(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return children; +}; + +const DndFrame = ({ children }: { children: ReactNode }) => { + return ( + + {children} + + ); +}; + +export default class IframeBoard extends Component { + /* eslint-disable react/sort-comp */ + static defaultProps = { + isCombineEnabled: false, + applyGlobalStyles: true, + }; + + state: State = { + columns: this.props.initial, + ordered: Object.keys(this.props.initial), + }; + + onDragEnd = (result: DropResult): void => { + // dropped nowhere + if (!result.destination) { + return; + } + + const source: DraggableLocation = result.source; + const destination: DraggableLocation = result.destination; + + // did not move anywhere - can bail early + if ( + source.droppableId === destination.droppableId && + source.index === destination.index + ) { + return; + } + + const data = reorderQuoteMap({ + quoteMap: this.state.columns, + source, + destination, + }); + + this.setState({ + columns: data.quoteMap, + }); + }; + + render(): ReactElement { + const columns: QuoteMap = this.state.columns; + const ordered: string[] = this.state.ordered; + + const board = ordered.map((key: string, index: number) => ( + + + {(provided: DroppableProvided) => ( + +
+ + {key} (iframe {index}) + +
+ + {provided.placeholder} +
+ )} +
+
+ )); + + return ( + + +
{board}
+
+ +
+ ); + } +} diff --git a/stories/src/primatives/quote-item.tsx b/stories/src/primatives/quote-item.tsx index ad9c82c1d..0a0b62519 100644 --- a/stories/src/primatives/quote-item.tsx +++ b/stories/src/primatives/quote-item.tsx @@ -182,7 +182,7 @@ function QuoteItem(props: Props) { return (