diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 33736e7..13ac012 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -19,6 +19,7 @@ module.exports = { plugins: [ 'only-warn', 'react', + 'react-hooks', '@typescript-eslint' ], rules: { @@ -29,5 +30,7 @@ module.exports = { '@typescript-eslint/semi': ['warn', 'always'], 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'error', + 'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks + 'react-hooks/exhaustive-deps': 'warn' // Checks effect dependencies } }; diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2def8a6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.drawio filter=lfs diff=lfs merge=lfs -text diff --git a/docs/ComponentStructure.drawio b/docs/ComponentStructure.drawio new file mode 100644 index 0000000..2fdcefc --- /dev/null +++ b/docs/ComponentStructure.drawio @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59a34bc3428280ecd661efa7b4756bf9f4930f36c077984a40e7fdc6983aeeff +size 2063 diff --git a/index.html b/index.html index e0d1c84..d9f5a08 100644 --- a/index.html +++ b/index.html @@ -3,6 +3,7 @@ + Vite + React + TS diff --git a/package-lock.json b/package-lock.json index 285432c..b7e9f41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,18 +9,20 @@ "version": "0.0.0", "dependencies": { "@heroicons/react": "^1.0.6", - "framer-motion": "^6.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-svg-pan-zoom": "^3.11.0" + "react-svg-pan-zoom": "^3.11.0", + "react-window": "^1.8.7" }, "devDependencies": { + "@testing-library/dom": "^8.16.1", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.3.0", "@testing-library/user-event": "^14.4.1", "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", "@types/react-svg-pan-zoom": "^3.3.5", + "@types/react-window": "^1.8.5", "@typescript-eslint/eslint-plugin": "^5.31.0", "@typescript-eslint/parser": "^5.31.0", "@vitejs/plugin-react": "^2.0.0", @@ -31,8 +33,10 @@ "eslint-config-standard-with-typescript": "^22.0.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-n": "^15.2.4", + "eslint-plugin-only-warn": "^1.0.3", "eslint-plugin-promise": "^6.0.0", "eslint-plugin-react": "^7.30.1", + "eslint-plugin-react-hooks": "^4.6.0", "jsdom": "^20.0.0", "postcss": "^8.4.14", "sass": "^1.54.0", @@ -421,7 +425,6 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.13.4" }, @@ -477,21 +480,6 @@ "node": ">=6.9.0" } }, - "node_modules/@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "optional": true, - "dependencies": { - "@emotion/memoize": "0.7.4" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "optional": true - }, "node_modules/@eslint/eslintrc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", @@ -614,89 +602,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@motionone/animation": { - "version": "10.13.1", - "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.13.1.tgz", - "integrity": "sha512-dxQ+1wWxL6iFHDy1uv6hhcPjIdOg36eDT56jN4LI7Z5HZRyLpq8x1t7JFQclo/IEIb+6Bk4atmyinGFdXVECuA==", - "dependencies": { - "@motionone/easing": "^10.13.1", - "@motionone/types": "^10.13.0", - "@motionone/utils": "^10.13.1", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/animation/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, - "node_modules/@motionone/dom": { - "version": "10.12.0", - "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.12.0.tgz", - "integrity": "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==", - "dependencies": { - "@motionone/animation": "^10.12.0", - "@motionone/generators": "^10.12.0", - "@motionone/types": "^10.12.0", - "@motionone/utils": "^10.12.0", - "hey-listen": "^1.0.8", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/dom/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, - "node_modules/@motionone/easing": { - "version": "10.13.1", - "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.13.1.tgz", - "integrity": "sha512-INEsInHHDHVgx0dp5qlXi1lMXBqYicgLMMSn3zfGzaIvcaEbI1Uz8BoyNV4BiclTupG7RYIh+T6BU83ZcEe74g==", - "dependencies": { - "@motionone/utils": "^10.13.1", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/easing/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, - "node_modules/@motionone/generators": { - "version": "10.13.1", - "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.13.1.tgz", - "integrity": "sha512-+HK5u2YcNJCckTTqfOLgSVcrWv2z1dVwrSZEMVJuAh0EnWEWGDJRvMBoPc0cFf/osbkA2Rq9bH2+vP0Ex/D8uw==", - "dependencies": { - "@motionone/types": "^10.13.0", - "@motionone/utils": "^10.13.1", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/generators/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, - "node_modules/@motionone/types": { - "version": "10.13.0", - "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.13.0.tgz", - "integrity": "sha512-qegk4qg8U1N9ZwAJ187BG3TkZz1k9LP/pvNtCSlqdq/PMUDKlCFG4ZnjJ481P0IOH/vIw1OzIbKIuyg0A3rk9g==" - }, - "node_modules/@motionone/utils": { - "version": "10.13.1", - "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.13.1.tgz", - "integrity": "sha512-TjDPTIppaf3ofBXQv4ZzAketJgN0sclALXfZ6mfrkjJkOy83mLls9744F+6S+VKCpBmvbZcBY4PQfrfhAfeMtA==", - "dependencies": { - "@motionone/types": "^10.13.0", - "hey-listen": "^1.0.8", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/utils/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -745,9 +650,9 @@ "dev": true }, "node_modules/@testing-library/dom": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.16.0.tgz", - "integrity": "sha512-uxF4zmnLHHDlmW4l+0WDjcgLVwCvH+OVLpD8Dfp+Bjfz85prwxWGbwXgJdLtkgjD0qfOzkJF9SmA6YZPsMYX4w==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.17.1.tgz", + "integrity": "sha512-KnH2MnJUzmFNPW6RIKfd+zf2Wue8mEKX0M3cpX6aKl5ZXrJM1/c/Pc8c2xDNYQCnJO48Sm5ITbMXgqTr3h4jxQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", @@ -1079,6 +984,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-window": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz", + "integrity": "sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -2879,6 +2793,15 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-only-warn": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-only-warn/-/eslint-plugin-only-warn-1.0.3.tgz", + "integrity": "sha512-XQOX/TfLoLw6h8ky51d29uUjXRTQHqBGXPylDEmy5fe/w7LIOnp8MA24b1OSMEn9BQoKow1q3g1kLe5/9uBTvw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/eslint-plugin-promise": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz", @@ -2919,6 +2842,18 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, "node_modules/eslint-plugin-react/node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -3331,44 +3266,6 @@ "url": "https://www.patreon.com/infusion" } }, - "node_modules/framer-motion": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.5.1.tgz", - "integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==", - "dependencies": { - "@motionone/dom": "10.12.0", - "framesync": "6.0.1", - "hey-listen": "^1.0.8", - "popmotion": "11.0.3", - "style-value-types": "5.0.0", - "tslib": "^2.1.0" - }, - "optionalDependencies": { - "@emotion/is-prop-valid": "^0.8.2" - }, - "peerDependencies": { - "react": ">=16.8 || ^17.0.0 || ^18.0.0", - "react-dom": ">=16.8 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/framer-motion/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, - "node_modules/framesync": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.0.1.tgz", - "integrity": "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/framesync/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3606,11 +3503,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hey-listen": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", - "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" - }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -4428,6 +4320,11 @@ "node": ">=12" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4836,22 +4733,6 @@ "node": ">=0.10.0" } }, - "node_modules/popmotion": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.3.tgz", - "integrity": "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==", - "dependencies": { - "framesync": "6.0.1", - "hey-listen": "^1.0.8", - "style-value-types": "5.0.0", - "tslib": "^2.1.0" - } - }, - "node_modules/popmotion/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, "node_modules/postcss": { "version": "8.4.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", @@ -5129,6 +5010,22 @@ "react": ">=17.0.0" } }, + "node_modules/react-window": { + "version": "1.8.7", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.7.tgz", + "integrity": "sha512-JHEZbPXBpKMmoNO1bNhoXOOLg/ujhL/BU4IqVU9r8eQPcy5KQnGHIHDRkJ0ns9IM5+Aq5LNwt3j8t3tIrePQzA==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + }, + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -5166,8 +5063,7 @@ "node_modules/regenerator-runtime": { "version": "0.13.9", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" }, "node_modules/regexp.prototype.flags": { "version": "1.4.3", @@ -5536,20 +5432,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/style-value-types": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz", - "integrity": "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==", - "dependencies": { - "hey-listen": "^1.0.8", - "tslib": "^2.1.0" - } - }, - "node_modules/style-value-types/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -6419,7 +6301,6 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", - "dev": true, "requires": { "regenerator-runtime": "^0.13.4" } @@ -6463,21 +6344,6 @@ "to-fast-properties": "^2.0.0" } }, - "@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "optional": true, - "requires": { - "@emotion/memoize": "0.7.4" - } - }, - "@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "optional": true - }, "@eslint/eslintrc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", @@ -6576,99 +6442,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "@motionone/animation": { - "version": "10.13.1", - "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.13.1.tgz", - "integrity": "sha512-dxQ+1wWxL6iFHDy1uv6hhcPjIdOg36eDT56jN4LI7Z5HZRyLpq8x1t7JFQclo/IEIb+6Bk4atmyinGFdXVECuA==", - "requires": { - "@motionone/easing": "^10.13.1", - "@motionone/types": "^10.13.0", - "@motionone/utils": "^10.13.1", - "tslib": "^2.3.1" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, - "@motionone/dom": { - "version": "10.12.0", - "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.12.0.tgz", - "integrity": "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==", - "requires": { - "@motionone/animation": "^10.12.0", - "@motionone/generators": "^10.12.0", - "@motionone/types": "^10.12.0", - "@motionone/utils": "^10.12.0", - "hey-listen": "^1.0.8", - "tslib": "^2.3.1" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, - "@motionone/easing": { - "version": "10.13.1", - "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.13.1.tgz", - "integrity": "sha512-INEsInHHDHVgx0dp5qlXi1lMXBqYicgLMMSn3zfGzaIvcaEbI1Uz8BoyNV4BiclTupG7RYIh+T6BU83ZcEe74g==", - "requires": { - "@motionone/utils": "^10.13.1", - "tslib": "^2.3.1" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, - "@motionone/generators": { - "version": "10.13.1", - "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.13.1.tgz", - "integrity": "sha512-+HK5u2YcNJCckTTqfOLgSVcrWv2z1dVwrSZEMVJuAh0EnWEWGDJRvMBoPc0cFf/osbkA2Rq9bH2+vP0Ex/D8uw==", - "requires": { - "@motionone/types": "^10.13.0", - "@motionone/utils": "^10.13.1", - "tslib": "^2.3.1" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, - "@motionone/types": { - "version": "10.13.0", - "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.13.0.tgz", - "integrity": "sha512-qegk4qg8U1N9ZwAJ187BG3TkZz1k9LP/pvNtCSlqdq/PMUDKlCFG4ZnjJ481P0IOH/vIw1OzIbKIuyg0A3rk9g==" - }, - "@motionone/utils": { - "version": "10.13.1", - "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.13.1.tgz", - "integrity": "sha512-TjDPTIppaf3ofBXQv4ZzAketJgN0sclALXfZ6mfrkjJkOy83mLls9744F+6S+VKCpBmvbZcBY4PQfrfhAfeMtA==", - "requires": { - "@motionone/types": "^10.13.0", - "hey-listen": "^1.0.8", - "tslib": "^2.3.1" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6708,9 +6481,9 @@ "dev": true }, "@testing-library/dom": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.16.0.tgz", - "integrity": "sha512-uxF4zmnLHHDlmW4l+0WDjcgLVwCvH+OVLpD8Dfp+Bjfz85prwxWGbwXgJdLtkgjD0qfOzkJF9SmA6YZPsMYX4w==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.17.1.tgz", + "integrity": "sha512-KnH2MnJUzmFNPW6RIKfd+zf2Wue8mEKX0M3cpX6aKl5ZXrJM1/c/Pc8c2xDNYQCnJO48Sm5ITbMXgqTr3h4jxQ==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", @@ -6976,6 +6749,15 @@ "@types/react": "*" } }, + "@types/react-window": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz", + "integrity": "sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -8254,6 +8036,12 @@ "semver": "^7.3.7" } }, + "eslint-plugin-only-warn": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-only-warn/-/eslint-plugin-only-warn-1.0.3.tgz", + "integrity": "sha512-XQOX/TfLoLw6h8ky51d29uUjXRTQHqBGXPylDEmy5fe/w7LIOnp8MA24b1OSMEn9BQoKow1q3g1kLe5/9uBTvw==", + "dev": true + }, "eslint-plugin-promise": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz", @@ -8311,6 +8099,13 @@ } } }, + "eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "requires": {} + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -8510,42 +8305,6 @@ "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", "dev": true }, - "framer-motion": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.5.1.tgz", - "integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==", - "requires": { - "@emotion/is-prop-valid": "^0.8.2", - "@motionone/dom": "10.12.0", - "framesync": "6.0.1", - "hey-listen": "^1.0.8", - "popmotion": "11.0.3", - "style-value-types": "5.0.0", - "tslib": "^2.1.0" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, - "framesync": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.0.1.tgz", - "integrity": "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==", - "requires": { - "tslib": "^2.1.0" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -8710,11 +8469,6 @@ "has-symbols": "^1.0.2" } }, - "hey-listen": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", - "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" - }, "html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -9313,6 +9067,11 @@ "sourcemap-codec": "^1.4.8" } }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -9610,24 +9369,6 @@ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true }, - "popmotion": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.3.tgz", - "integrity": "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==", - "requires": { - "framesync": "6.0.1", - "hey-listen": "^1.0.8", - "style-value-types": "5.0.0", - "tslib": "^2.1.0" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, "postcss": { "version": "8.4.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", @@ -9796,6 +9537,15 @@ "transformation-matrix": "^2.11.1" } }, + "react-window": { + "version": "1.8.7", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.7.tgz", + "integrity": "sha512-JHEZbPXBpKMmoNO1bNhoXOOLg/ujhL/BU4IqVU9r8eQPcy5KQnGHIHDRkJ0ns9IM5+Aq5LNwt3j8t3tIrePQzA==", + "requires": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + } + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -9827,8 +9577,7 @@ "regenerator-runtime": { "version": "0.13.9", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" }, "regexp.prototype.flags": { "version": "1.4.3", @@ -10085,22 +9834,6 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, - "style-value-types": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz", - "integrity": "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==", - "requires": { - "hey-listen": "^1.0.8", - "tslib": "^2.1.0" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/package.json b/package.json index 1c45135..5fdcfa7 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,10 @@ }, "dependencies": { "@heroicons/react": "^1.0.6", - "framer-motion": "^6.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-svg-pan-zoom": "^3.11.0" + "react-svg-pan-zoom": "^3.11.0", + "react-window": "^1.8.7" }, "devDependencies": { "@testing-library/dom": "^8.16.1", @@ -27,6 +27,7 @@ "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", "@types/react-svg-pan-zoom": "^3.3.5", + "@types/react-window": "^1.8.5", "@typescript-eslint/eslint-plugin": "^5.31.0", "@typescript-eslint/parser": "^5.31.0", "@vitejs/plugin-react": "^2.0.0", @@ -40,6 +41,7 @@ "eslint-plugin-only-warn": "^1.0.3", "eslint-plugin-promise": "^6.0.0", "eslint-plugin-react": "^7.30.1", + "eslint-plugin-react-hooks": "^4.6.0", "jsdom": "^20.0.0", "postcss": "^8.4.14", "sass": "^1.54.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10ead3b..be1cc19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,7 @@ specifiers: '@types/react': ^18.0.15 '@types/react-dom': ^18.0.6 '@types/react-svg-pan-zoom': ^3.3.5 + '@types/react-window': ^1.8.5 '@typescript-eslint/eslint-plugin': ^5.31.0 '@typescript-eslint/parser': ^5.31.0 '@vitejs/plugin-react': ^2.0.0 @@ -22,12 +23,13 @@ specifiers: eslint-plugin-only-warn: ^1.0.3 eslint-plugin-promise: ^6.0.0 eslint-plugin-react: ^7.30.1 - framer-motion: ^6.5.1 + eslint-plugin-react-hooks: ^4.6.0 jsdom: ^20.0.0 postcss: ^8.4.14 react: ^18.2.0 react-dom: ^18.2.0 react-svg-pan-zoom: ^3.11.0 + react-window: ^1.8.7 sass: ^1.54.0 tailwindcss: ^3.1.7 typescript: ^4.6.4 @@ -36,10 +38,10 @@ specifiers: dependencies: '@heroicons/react': 1.0.6_react@18.2.0 - framer-motion: 6.5.1_biqbaboplfbrettd7655fr4n2y react: 18.2.0 react-dom: 18.2.0_react@18.2.0 react-svg-pan-zoom: 3.11.0_react@18.2.0 + react-window: 1.8.7_biqbaboplfbrettd7655fr4n2y devDependencies: '@testing-library/dom': 8.16.1 @@ -49,6 +51,7 @@ devDependencies: '@types/react': 18.0.17 '@types/react-dom': 18.0.6 '@types/react-svg-pan-zoom': 3.3.5 + '@types/react-window': 1.8.5 '@typescript-eslint/eslint-plugin': 5.32.0_iosr3hrei2tubxveewluhu5lhy '@typescript-eslint/parser': 5.32.0_qugx7qdu5zevzvxaiqyxfiwquq '@vitejs/plugin-react': 2.0.0_vite@3.0.4 @@ -62,6 +65,7 @@ devDependencies: eslint-plugin-only-warn: 1.0.3 eslint-plugin-promise: 6.0.0_eslint@8.21.0 eslint-plugin-react: 7.30.1_eslint@8.21.0 + eslint-plugin-react-hooks: 4.6.0_eslint@8.21.0 jsdom: 20.0.0 postcss: 8.4.16 sass: 1.54.3 @@ -312,7 +316,6 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.13.9 - dev: true /@babel/template/7.18.10: resolution: {integrity: sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==} @@ -350,19 +353,6 @@ packages: to-fast-properties: 2.0.0 dev: true - /@emotion/is-prop-valid/0.8.8: - resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} - requiresBuild: true - dependencies: - '@emotion/memoize': 0.7.4 - dev: false - optional: true - - /@emotion/memoize/0.7.4: - resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} - dev: false - optional: true - /@esbuild/linux-loong64/0.14.54: resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==} engines: {node: '>=12'} @@ -461,53 +451,6 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: true - /@motionone/animation/10.13.2: - resolution: {integrity: sha512-YGWss58IR2X4lOjW89rv1Q+/Nq/QhfltaggI7i8sZTpKC1yUvM+XYDdvlRpWc6dk8LviMBrddBJAlLdbaqeRmw==} - dependencies: - '@motionone/easing': 10.13.2 - '@motionone/types': 10.13.2 - '@motionone/utils': 10.13.2 - tslib: 2.4.0 - dev: false - - /@motionone/dom/10.12.0: - resolution: {integrity: sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==} - dependencies: - '@motionone/animation': 10.13.2 - '@motionone/generators': 10.13.2 - '@motionone/types': 10.13.2 - '@motionone/utils': 10.13.2 - hey-listen: 1.0.8 - tslib: 2.4.0 - dev: false - - /@motionone/easing/10.13.2: - resolution: {integrity: sha512-3HqctS5NyDfDQ+8+cZqc3Pu7I6amFCt9zDUjcozHyFXHh4PKYHK4+GJDFjJIS8bCAF2BrJmpmduDQ2V7lFEYeQ==} - dependencies: - '@motionone/utils': 10.13.2 - tslib: 2.4.0 - dev: false - - /@motionone/generators/10.13.2: - resolution: {integrity: sha512-QMoXV1MXEEhR6D3dct/RMMS1FwJlAsW+kMPbFGzBA4NbweblgeYQCft9DcDAVpV9wIwD6qvlBG9u99sOXLfHiA==} - dependencies: - '@motionone/types': 10.13.2 - '@motionone/utils': 10.13.2 - tslib: 2.4.0 - dev: false - - /@motionone/types/10.13.2: - resolution: {integrity: sha512-yYV4q5v5F0iADhab4wHfqaRJnM/eVtQLjUPhyEcS72aUz/xyOzi09GzD/Gu+K506BDfqn5eULIilUI77QNaqhw==} - dev: false - - /@motionone/utils/10.13.2: - resolution: {integrity: sha512-6Lw5bDA/w7lrPmT/jYWQ76lkHlHs9fl2NZpJ22cVy1kKDdEH+Cl1U6hMTpdphO6VQktQ6v2APngag91WBKLqlA==} - dependencies: - '@motionone/types': 10.13.2 - hey-listen: 1.0.8 - tslib: 2.4.0 - dev: false - /@nodelib/fs.scandir/2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -643,6 +586,12 @@ packages: '@types/react': 18.0.17 dev: true + /@types/react-window/1.8.5: + resolution: {integrity: sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==} + dependencies: + '@types/react': 18.0.17 + dev: true + /@types/react/18.0.17: resolution: {integrity: sha512-38ETy4tL+rn4uQQi7mB81G7V1g0u2ryquNmsVIOKUAEIDK+3CUjZ6rSRpdvS99dNBnkLFL83qfmtLacGOTIhwQ==} dependencies: @@ -1740,6 +1689,15 @@ packages: eslint: 8.21.0 dev: true + /eslint-plugin-react-hooks/4.6.0_eslint@8.21.0: + resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + dependencies: + eslint: 8.21.0 + dev: true + /eslint-plugin-react/7.30.1_eslint@8.21.0: resolution: {integrity: sha512-NbEvI9jtqO46yJA3wcRF9Mo0lF9T/jhdHqhCHXiXtD+Zcb98812wvokjWpU7Q4QH5edo6dmqrukxVvWWXHlsUg==} engines: {node: '>=4'} @@ -1986,30 +1944,6 @@ packages: resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} dev: true - /framer-motion/6.5.1_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==} - peerDependencies: - react: '>=16.8 || ^17.0.0 || ^18.0.0' - react-dom: '>=16.8 || ^17.0.0 || ^18.0.0' - dependencies: - '@motionone/dom': 10.12.0 - framesync: 6.0.1 - hey-listen: 1.0.8 - popmotion: 11.0.3 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - style-value-types: 5.0.0 - tslib: 2.4.0 - optionalDependencies: - '@emotion/is-prop-valid': 0.8.8 - dev: false - - /framesync/6.0.1: - resolution: {integrity: sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==} - dependencies: - tslib: 2.4.0 - dev: false - /fs.realpath/1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true @@ -2161,10 +2095,6 @@ packages: function-bind: 1.1.1 dev: true - /hey-listen/1.0.8: - resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} - dev: false - /html-encoding-sniffer/3.0.0: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} @@ -2549,6 +2479,10 @@ packages: sourcemap-codec: 1.4.8 dev: true + /memoize-one/5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + dev: false + /merge2/1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2819,15 +2753,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /popmotion/11.0.3: - resolution: {integrity: sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==} - dependencies: - framesync: 6.0.1 - hey-listen: 1.0.8 - style-value-types: 5.0.0 - tslib: 2.4.0 - dev: false - /postcss-import/14.1.0_postcss@8.4.16: resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} engines: {node: '>=10.0.0'} @@ -2987,6 +2912,19 @@ packages: transformation-matrix: 2.12.0 dev: false + /react-window/1.8.7_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-JHEZbPXBpKMmoNO1bNhoXOOLg/ujhL/BU4IqVU9r8eQPcy5KQnGHIHDRkJ0ns9IM5+Aq5LNwt3j8t3tIrePQzA==} + engines: {node: '>8.0.0'} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.18.9 + memoize-one: 5.2.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + dev: false + /react/18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -3016,7 +2954,6 @@ packages: /regenerator-runtime/0.13.9: resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} - dev: true /regexp.prototype.flags/1.4.3: resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} @@ -3227,13 +3164,6 @@ packages: engines: {node: '>=8'} dev: true - /style-value-types/5.0.0: - resolution: {integrity: sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==} - dependencies: - hey-listen: 1.0.8 - tslib: 2.4.0 - dev: false - /supports-color/5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -3352,10 +3282,6 @@ packages: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: true - /tslib/2.4.0: - resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} - dev: false - /tsutils/3.21.0_typescript@4.7.4: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} diff --git a/public/Interfaces.d.ts b/public/Interfaces.d.ts new file mode 100644 index 0000000..5a466a1 --- /dev/null +++ b/public/Interfaces.d.ts @@ -0,0 +1,65 @@ +declare interface IHistoryState { + LastAction: string + MainContainer: IContainerModel + SelectedContainer: IContainerModel | null + SelectedContainerId: string + TypeCounters: Record +} + +declare interface IAvailableContainer { + Type: string + Width: number + Height: number + XPositionReference?: XPositionReference + Style: React.CSSProperties +} + +declare interface IEditorState { + history: IHistoryState[] + historyCurrentStep: number + configuration: IConfiguration +} + +declare interface IConfiguration { + AvailableContainers: IAvailableContainer[] + AvailableSymbols: IAvailableSymbol[] + MainContainer: IAvailableContainer +} + +declare interface IContainerModel { + children: IContainerModel[] + parent: IContainerModel | null + properties: IProperties + userData: Record +} + +declare interface IProperties extends React.CSSProperties { + id: string + parentId: string | null + x: number + y: number + isRigidBody: boolean + XPositionReference?: XPositionReference +} + +declare enum XPositionReference { + Left, + Center, + Right +} + +declare interface IAvailableSymbol { + Name: string + XPositionReference: XPositionReference + Image: IImage + Width: number + Height: number +} + +declare interface IImage { + Name: string + Url: string + Base64Image: string + Svg: string +} + diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..a732b5f --- /dev/null +++ b/public/style.css @@ -0,0 +1,6 @@ +html, +body, +#root { + width: 100%; + height: 100%; +} diff --git a/src/Components/API/api.ts b/src/Components/API/api.ts index e113cc9..9acea24 100644 --- a/src/Components/API/api.ts +++ b/src/Components/API/api.ts @@ -1,10 +1,10 @@ -import { Configuration } from '../../Interfaces/Configuration'; +import { IConfiguration } from '../../Interfaces/IConfiguration'; /** * Fetch the configuration from the API * @returns {Configation} The model of the configuration for the application */ -export async function fetchConfiguration(): Promise { +export async function fetchConfiguration(): Promise { const url = `${import.meta.env.VITE_API_URL}`; // The test library cannot use the Fetch API // @ts-expect-error @@ -15,7 +15,7 @@ export async function fetchConfiguration(): Promise { }) .then(async(response) => await response.json() - ) as Configuration; + ) as IConfiguration; } return await new Promise((resolve) => { const xhr = new XMLHttpRequest(); diff --git a/src/Components/App/App.scss b/src/Components/App/App.scss index 2cb61ee..e69de29 100644 --- a/src/Components/App/App.scss +++ b/src/Components/App/App.scss @@ -1,6 +0,0 @@ -html, -body, -#root { - width: 100%; - height: 100%; -} \ No newline at end of file diff --git a/src/Components/App/App.tsx b/src/Components/App/App.tsx index c25c37e..c85d322 100644 --- a/src/Components/App/App.tsx +++ b/src/Components/App/App.tsx @@ -1,8 +1,9 @@ import React, { useEffect, useState } from 'react'; import './App.scss'; import { MainMenu } from '../MainMenu/MainMenu'; -import { ContainerModel } from '../../Interfaces/ContainerModel'; -import Editor, { IEditorState } from '../Editor/Editor'; +import { ContainerModel } from '../../Interfaces/IContainerModel'; +import Editor from '../Editor/Editor'; +import { IEditorState } from '../../Interfaces/IEditorState'; import { LoadState } from './Load'; import { LoadEditor, NewEditor } from './MenuActions'; import { DEFAULT_CONFIG, DEFAULT_MAINCONTAINER_PROPS } from '../../utils/default'; diff --git a/src/Components/App/Load.ts b/src/Components/App/Load.ts index b5459c7..909dfc9 100644 --- a/src/Components/App/Load.ts +++ b/src/Components/App/Load.ts @@ -1,6 +1,6 @@ import { Dispatch, SetStateAction } from 'react'; import { Revive } from '../../utils/saveload'; -import { IEditorState } from '../Editor/Editor'; +import { IEditorState } from '../../Interfaces/IEditorState'; export function LoadState( editorState: IEditorState, diff --git a/src/Components/App/MenuActions.ts b/src/Components/App/MenuActions.ts index 2669c1a..c144acc 100644 --- a/src/Components/App/MenuActions.ts +++ b/src/Components/App/MenuActions.ts @@ -1,8 +1,8 @@ import { Dispatch, SetStateAction } from 'react'; -import { Configuration } from '../../Interfaces/Configuration'; -import { ContainerModel } from '../../Interfaces/ContainerModel'; +import { IConfiguration } from '../../Interfaces/IConfiguration'; +import { ContainerModel } from '../../Interfaces/IContainerModel'; import { fetchConfiguration } from '../API/api'; -import { IEditorState } from '../Editor/Editor'; +import { IEditorState } from '../../Interfaces/IEditorState'; import { LoadState } from './Load'; export function NewEditor( @@ -11,7 +11,7 @@ export function NewEditor( ): void { // Fetch the configuration from the API fetchConfiguration() - .then((configuration: Configuration) => { + .then((configuration: IConfiguration) => { // Set the main container from the given properties of the API const MainContainer = new ContainerModel( null, @@ -23,6 +23,7 @@ export function NewEditor( width: configuration.MainContainer.Width, height: configuration.MainContainer.Height, isRigidBody: false, + isAnchor: false, fillOpacity: 0, stroke: 'black' } diff --git a/src/Components/Bar/Bar.tsx b/src/Components/Bar/Bar.tsx index 1738a3f..77cb259 100644 --- a/src/Components/Bar/Bar.tsx +++ b/src/Components/Bar/Bar.tsx @@ -15,7 +15,7 @@ export const BAR_WIDTH = 64; // 4rem export const Bar: React.FC = (props) => { return ( -
+
child.properties.isRigidBody && !child.properties.isAnchor + ); + + const overlappingContainers = getOverlappingContainers(container, rigidBodies); + for (const overlappingContainer of overlappingContainers) { + constraintBodyInsideUnallocatedWidth(overlappingContainer); + } + return container; +} + +/** + * Returns the overlapping containers with container + * @param container A container + * @param containers A list of containers + * @returns A list of overlapping containers + */ +function getOverlappingContainers( + container: IContainerModel, + containers: IContainerModel[] +): IContainerModel[] { + const min1 = container.properties.x; + const max1 = container.properties.x + Number(container.properties.width); + const overlappingContainers: IContainerModel[] = []; + for (const other of containers) { + if (other === container) { + continue; + } + + const min2 = other.properties.x; + const max2 = other.properties.x + Number(other.properties.width); + const isOverlapping = Math.min(max1, max2) - Math.max(min1, min2) > 0; + + if (!isOverlapping) { + continue; + } + + overlappingContainers.push(other); + } + return overlappingContainers; +} diff --git a/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts new file mode 100644 index 0000000..eabec85 --- /dev/null +++ b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts @@ -0,0 +1,312 @@ +/** + * @module RigidBodyBehaviors + * Apply the following contraints to the `container` : + * - The container must be kept inside its parent + * - The container must find an unallocated space within the parent + * If the contraints fails, an error message will be returned + */ + +import { IContainerModel } from '../../../Interfaces/IContainerModel'; +import { ISizePointer } from '../../../Interfaces/ISizePointer'; + +/** + * "Transform the container into a rigid body" + * Apply the following contraints to the `container` : + * - The container must be kept inside its parent + * - The container must find an unallocated space within the parent + * If the contraints fails, an error message will be returned + * @param container Container to apply its rigid body properties + * @returns A rigid body container + */ +export function RecalculatePhysics( + container: IContainerModel +): IContainerModel { + container = constraintBodyInsideParent(container); + container = constraintBodyInsideUnallocatedWidth(container); + return container; +} + +/** + * Limit a rect inside a parent rect by applying the following rules : + * it cannot be bigger than the parent + * it cannot go out of bound + * Mutates and returns the container + * @param container + * @returns Updated container + */ +function constraintBodyInsideParent( + container: IContainerModel +): IContainerModel { + if (container.parent === null || container.parent === undefined) { + return container; + } + + const parentProperties = container.parent.properties; + const parentWidth = Number(parentProperties.width); + const parentHeight = Number(parentProperties.height); + + return constraintBodyInsideSpace(container, 0, 0, parentWidth, parentHeight); +} + +/** + * Limit a container inside a rectangle + * Mutates and returns the container + * @param container A container + * @param x x of top left of the rectangle + * @param y y of top left of the rectangle + * @param width width of the rectangle + * @param height height of the rectangle + * @returns Updated container + */ +function constraintBodyInsideSpace( + container: IContainerModel, + x: number, + y: number, + width: number, + height: number +): IContainerModel { + const containerProperties = container.properties; + const containerX = Number(containerProperties.x); + const containerY = Number(containerProperties.y); + const containerWidth = Number(containerProperties.width); + const containerHeight = Number(containerProperties.height); + + // Check size bigger than parent + const isBodyLargerThanParent = containerWidth > width; + const isBodyTallerThanParentHeight = containerHeight > height; + if (isBodyLargerThanParent || isBodyTallerThanParentHeight) { + if (isBodyLargerThanParent) { + containerProperties.x = x; + containerProperties.width = width; + } + if (isBodyTallerThanParentHeight) { + containerProperties.y = y; + containerProperties.height = height; + } + return container; + } + + // Check horizontal out of bound + if (containerX < x) { + containerProperties.x = x; + } + if (containerX + containerWidth > x + width) { + containerProperties.x = x + width - containerWidth; + } + + // Check vertical out of bound + if (containerY < y) { + containerProperties.y = y; + } + if (containerY + containerHeight > y + height) { + containerProperties.y = y + height - containerHeight; + } + + return container; +} + +/** + * Constraint the container inside unallocated width/space of the parent container + * If there is no unalloacted width/space, an error will be thrown + * Mutates and returns the container + * @param container + * @returns Updated container + */ +export function constraintBodyInsideUnallocatedWidth( + container: IContainerModel +): IContainerModel { + if (container.parent === null) { + return container; + } + + // Get the available spaces of the parent + const availableWidths = getAvailableWidths(container.parent, container); + const containerX = Number(container.properties.x); + const containerWidth = Number(container.properties.width); + + // Check if there is still some space + if (availableWidths.length === 0) { + throw new Error( + 'No available space found on the parent container. Try to free the parent a little before placing it inside.' + ); + } + + const middle = containerX + containerWidth / 2; + // Sort the available width to find the space with the closest position + availableWidths.sort( + (width1, width2) => { + let compared1X = width1.x; + if (width1.x < containerX) { + compared1X = width1.x + width1.width - containerWidth; + } + + let compared2X = width2.x; + if (width2.x < containerX) { + compared2X = width2.x + width2.width - containerWidth; + } + + return Math.abs(compared1X - middle) - Math.abs(compared2X - middle); + } + ); + + // Check if the container actually fit inside + // It will usually fit if it was alrady fitting + const availableWidthFound = availableWidths.find((width) => + isFitting(container, width) + ); + + if (availableWidthFound === undefined) { + // Otherwise, it is possible that it does not fit + // There is two way to reach this part of the code + // 1) Enable isRigidBody such as width > availableWidth.width + // 2) Resize a container such as width > availableWidth.width + + // We want the container to fit automatically inside the available space + // even if it means to resize the container + // The end goal is that the code never show the error message no matter what action is done + // TODO: Actually give an option to not fit and show the error message shown below + const availableWidth = availableWidths[0]; + container.properties.x = availableWidth.x; + container.properties.width = availableWidth.width; + // throw new Error('[constraintBodyInsideUnallocatedWidth] BIGERR: No available space found on the parent container, even though there is some.'); + return container; + } + + return constraintBodyInsideSpace( + container, + availableWidthFound.x, + 0, + availableWidthFound.width, + Number(container.parent.properties.height) + ); +} + +/** + * Get the unallocated widths inside a container + * An allocated width is defined by its the widths of the children that are rigid bodies. + * An example of this allocation system is the disk space of an hard drive + * (except the fact that disk space is divided by block). + * @param container Container where to find an available width + * @param exception Container to exclude of the widths (since a container will be moved, it might need to be excluded) + * @returns {ISizePointer[]} Array of unallocated widths (x=position of the unallocated space, width=size of the allocated space) + */ +function getAvailableWidths( + container: IContainerModel, + exception: IContainerModel +): ISizePointer[] { + // Initialize the first size pointer + // which takes full width of the available space + const x = 0; + const width = Number(container.properties.width); + let unallocatedSpaces: ISizePointer[] = [{ x, width }]; + + // We will only uses containers that also are rigid or are anchors + const solidBodies = container.children.filter( + (child) => child.properties.isRigidBody || child.properties.isAnchor + ); + + for (const child of solidBodies) { + // Ignore the exception + if (child === exception) { + continue; + } + const childX = child.properties.x; + const childWidth = Number(child.properties.width); + + // get the space of the child that is inside the parent + let newUnallocatedSpace: ISizePointer[] = []; + + // We will iterate on a mutable variable in order to divide it + for (const unallocatedSpace of unallocatedSpaces) { + // In order to find unallocated space, + // We need to calculate the overlap between the two containers + // We only works with widths meaning in 1D (with lines) + const newUnallocatedWidths = getAvailableWidthsTwoLines( + unallocatedSpace.x, + unallocatedSpace.x + unallocatedSpace.width, + childX, + childX + childWidth + ); + + // Concat the new list of SizePointer pointing to availables spaces + newUnallocatedSpace = newUnallocatedSpace.concat(newUnallocatedWidths); + } + // Finally update the availables spaces found, loop again with it + unallocatedSpaces = newUnallocatedSpace; + } + + return unallocatedSpaces; +} + +/** + * Returns the unallocated widths between two lines in 1D + * @param unalloctedSpaceLeft left of the first line + * @param unallocatedSpaceRight rigth of the first line + * @param rectLeft left of the second line + * @param rectRight right of the second line + * @returns Available widths + */ +function getAvailableWidthsTwoLines( + unalloctedSpaceLeft: number, + unallocatedSpaceRight: number, + rectLeft: number, + rectRight: number +): ISizePointer[] { + if (unallocatedSpaceRight < rectLeft || + unalloctedSpaceLeft > rectRight + ) { + // object 1 and 2 are not overlapping + return [{ + x: unalloctedSpaceLeft, + width: unallocatedSpaceRight - unalloctedSpaceLeft + }]; + } + + if (rectLeft < unalloctedSpaceLeft && rectRight > unallocatedSpaceRight) { + // object 2 is overlapping full width + return []; + } + + if (unalloctedSpaceLeft >= rectLeft) { + // object 2 is partially overlapping on the left + return [ + { + x: rectRight, + width: unallocatedSpaceRight - rectRight + } + ]; + } + + if (rectRight >= unallocatedSpaceRight) { + // object 2 is partially overlapping on the right + return [ + { + x: unalloctedSpaceLeft, + width: rectRight - unalloctedSpaceLeft + } + ]; + } + + // object 2 is overlapping in the middle + return [ + { + x: unalloctedSpaceLeft, + width: rectLeft - unalloctedSpaceLeft + }, + { + x: rectRight, + width: unallocatedSpaceRight - rectRight + } + ]; +} + +/** + * Check if a container can fit inside a size space + * @param container Container to check + * @param sizePointer Size space to check + * @returns + */ +const isFitting = ( + container: IContainerModel, + sizePointer: ISizePointer +): boolean => Number(container.properties.width) <= sizePointer.width; diff --git a/src/Components/Editor/ContainerOperations.ts b/src/Components/Editor/ContainerOperations.ts index ea4db42..f1736fc 100644 --- a/src/Components/Editor/ContainerOperations.ts +++ b/src/Components/Editor/ContainerOperations.ts @@ -1,10 +1,10 @@ import { Dispatch, SetStateAction } from 'react'; -import { HistoryState } from '../../Interfaces/HistoryState'; -import { Configuration } from '../../Interfaces/Configuration'; -import { ContainerModel, IContainerModel } from '../../Interfaces/ContainerModel'; +import { IHistoryState } from '../../Interfaces/IHistoryState'; +import { IConfiguration } from '../../Interfaces/IConfiguration'; +import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel'; import { findContainerById } from '../../utils/itertools'; import { getCurrentHistory } from './Editor'; -import { SizePointer } from '../../Interfaces/SizePointer'; +import IProperties from '../../Interfaces/IProperties'; /** * Select a container @@ -12,9 +12,9 @@ import { SizePointer } from '../../Interfaces/SizePointer'; */ export function SelectContainer( container: ContainerModel, - fullHistory: HistoryState[], + fullHistory: IHistoryState[], historyCurrentStep: number, - setHistory: Dispatch>, + setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { const history = getCurrentHistory(fullHistory, historyCurrentStep); @@ -27,21 +27,30 @@ export function SelectContainer( throw new Error('[SelectContainer] Cannot find container among children of main container!'); } - setHistory(history.concat([{ - LastAction: `Select container ${selectedContainer.properties.id}`, + history.push({ + LastAction: `Select ${selectedContainer.properties.id}`, MainContainer: mainContainerClone, SelectedContainer: selectedContainer, SelectedContainerId: selectedContainer.properties.id, TypeCounters: Object.assign({}, current.TypeCounters) - }])); - setHistoryCurrentStep(history.length); + }); + setHistory(history); + setHistoryCurrentStep(history.length - 1); } +/** + * Delete a container + * @param containerId containerId of the container to delete + * @param fullHistory History of the editor + * @param historyCurrentStep Current step + * @param setHistory State setter for History + * @param setHistoryCurrentStep State setter for current step + */ export function DeleteContainer( containerId: string, - fullHistory: HistoryState[], + fullHistory: IHistoryState[], historyCurrentStep: number, - setHistory: Dispatch>, + setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { const history = getCurrentHistory(fullHistory, historyCurrentStep); @@ -54,43 +63,58 @@ export function DeleteContainer( throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`); } - if (container === mainContainerClone) { + if (container === mainContainerClone || + container.parent === undefined || + container.parent === null) { // TODO: Implement alert throw new Error('[DeleteContainer] Tried to delete the main container! Deleting the main container is not allowed!'); } if (container === null || container === undefined) { - throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); + throw new Error('[DeleteContainer] Container model was not found among children of the main container!'); } - if (container.parent != null) { - const index = container.parent.children.indexOf(container); - if (index > -1) { - container.parent.children.splice(index, 1); - } + const index = container.parent.children.indexOf(container); + if (index > -1) { + container.parent.children.splice(index, 1); + } else { + throw new Error('[DeleteContainer] Could not find container among parent\'s children'); } - setHistory(history.concat([{ - LastAction: `Delete container ${containerId}`, + // Select the previous container + // or select the one above + const SelectedContainer = findContainerById(mainContainerClone, current.SelectedContainerId) ?? + container.parent.children.at(index - 1) ?? + container.parent; + const SelectedContainerId = SelectedContainer.properties.id; + + history.push({ + LastAction: `Delete ${containerId}`, MainContainer: mainContainerClone, - SelectedContainer: null, - SelectedContainerId: '', + SelectedContainer, + SelectedContainerId, TypeCounters: Object.assign({}, current.TypeCounters) - }])); - setHistoryCurrentStep(history.length); + }); + setHistory(history); + setHistoryCurrentStep(history.length - 1); } /** * Add a new container to a selected container * @param type The type of container + * @param configuration Configuration of the App + * @param fullHistory History of the editor + * @param historyCurrentStep Current step + * @param setHistory State setter for History + * @param setHistoryCurrentStep State setter for current step * @returns void */ export function AddContainerToSelectedContainer( type: string, - configuration: Configuration, - fullHistory: HistoryState[], + configuration: IConfiguration, + fullHistory: IHistoryState[], historyCurrentStep: number, - setHistory: Dispatch>, + setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { const history = getCurrentHistory(fullHistory, historyCurrentStep); @@ -114,14 +138,26 @@ export function AddContainerToSelectedContainer( ); } +/** + * Create and add a new container at `index` in children of parent of `parentId` + * @param index Index where to insert to the new container + * @param type Type of container + * @param parentId Parent in which to insert the new container + * @param configuration Configuration of the app + * @param fullHistory History of the editor + * @param historyCurrentStep Current step + * @param setHistory State setter of History + * @param setHistoryCurrentStep State setter of the current step + * @returns void + */ export function AddContainer( index: number, type: string, parentId: string, - configuration: Configuration, - fullHistory: HistoryState[], + configuration: IConfiguration, + fullHistory: IHistoryState[], historyCurrentStep: number, - setHistory: Dispatch>, + setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { const history = getCurrentHistory(fullHistory, historyCurrentStep); @@ -170,19 +206,23 @@ export function AddContainer( } } + const defaultProperties: IProperties = { + id: `${type}-${count}`, + parentId: parentClone.properties.id, + x, + y: 0, + width: properties.Width, + height: parentClone.properties.height, + isRigidBody: false, + isAnchor: false, + XPositionReference: properties.XPositionReference, + ...properties.Style + }; + // Create the container const newContainer = new ContainerModel( parentClone, - { - id: `${type}-${count}`, - parentId: parentClone.properties.id, - x, - y: 0, - width: properties?.Width, - height: parentClone.properties.height, - isRigidBody: false, - ...properties.Style - }, + defaultProperties, [], { type @@ -197,278 +237,13 @@ export function AddContainer( } // Update the state - setHistory(history.concat([{ + history.push({ LastAction: 'Add container', MainContainer: clone, SelectedContainer: parentClone, SelectedContainerId: parentClone.properties.id, TypeCounters: newCounters - }])); - setHistoryCurrentStep(history.length); -} - -/** - * Handled the property change event in the properties form - * @param key Property name - * @param value New value of the property - * @returns void - */ -export function OnPropertyChange( - key: string, - value: string | number | boolean, - fullHistory: HistoryState[], - historyCurrentStep: number, - setHistory: Dispatch>, - setHistoryCurrentStep: Dispatch> -): void { - const history = getCurrentHistory(fullHistory, historyCurrentStep); - const current = history[history.length - 1]; - - if (current.SelectedContainer === null || - current.SelectedContainer === undefined) { - throw new Error('[OnPropertyChange] Property was changed before selecting a Container'); - } - - if (parent === null) { - const selectedContainerClone: IContainerModel = structuredClone(current.SelectedContainer); - (selectedContainerClone.properties as any)[key] = value; - setHistory(history.concat([{ - LastAction: 'Change property of main', - MainContainer: selectedContainerClone, - SelectedContainer: selectedContainerClone, - SelectedContainerId: selectedContainerClone.properties.id, - TypeCounters: Object.assign({}, current.TypeCounters) - }])); - setHistoryCurrentStep(history.length); - return; - } - - const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); - const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id); - - if (container === null || container === undefined) { - throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); - } - - (container.properties as any)[key] = value; - - if (container.properties.isRigidBody) { - RecalculatePhysics(container); - } - - setHistory(history.concat([{ - LastAction: `Change property of container ${container.properties.id}`, - MainContainer: mainContainerClone, - SelectedContainer: container, - SelectedContainerId: container.properties.id, - TypeCounters: Object.assign({}, current.TypeCounters) - }])); - setHistoryCurrentStep(history.length); -} - -// TODO put this in a different file - -export function RecalculatePhysics(container: IContainerModel): IContainerModel { - container = constraintBodyInsideParent(container); - container = constraintBodyInsideUnallocatedWidth(container); - return container; -} - -/** - * Limit a rect inside a parent rect by applying the following rules : - * it cannot be bigger than the parent - * it cannot go out of bound - * @param container - * @returns - */ -function constraintBodyInsideParent(container: IContainerModel): IContainerModel { - if (container.parent === null || container.parent === undefined) { - return container; - } - - const parentProperties = container.parent.properties; - const parentWidth = Number(parentProperties.width); - const parentHeight = Number(parentProperties.height); - - return constraintBodyInsideSpace(container, 0, 0, parentWidth, parentHeight); -} - -function constraintBodyInsideSpace( - container: IContainerModel, - x: number, - y: number, - width: number, - height: number -): IContainerModel { - const containerProperties = container.properties; - const containerX = Number(containerProperties.x); - const containerY = Number(containerProperties.y); - const containerWidth = Number(containerProperties.width); - const containerHeight = Number(containerProperties.height); - - // Check size bigger than parent - const isBodyLargerThanParent = containerWidth > width; - const isBodyTallerThanParentHeight = containerHeight > height; - if (isBodyLargerThanParent || isBodyTallerThanParentHeight) { - if (isBodyLargerThanParent) { - containerProperties.x = x; - containerProperties.width = width; - } - if (isBodyTallerThanParentHeight) { - containerProperties.y = y; - containerProperties.height = height; - } - return container; - } - - // Check horizontal out of bound - if (containerX < x) { - containerProperties.x = x; - } - if (containerX + containerWidth > width) { - containerProperties.x = x + width - containerWidth; - } - - // Check vertical out of bound - if (containerY < y) { - containerProperties.y = y; - } - if (containerY + containerHeight > height) { - containerProperties.y = y + height - containerHeight; - } - - return container; -} - -/** - * Get the unallocated widths inside a container - * An allocated width is defined by its the widths of the children that are rigid bodies. - * An example of this allocation system is the disk space - * (except the fact that disk space is divided by block). - * @param container - * @returns {SizePointer[]} Array of unallocated widths (x=position of the unallocated space, width=size of the allocated space) - */ -function getAvailableWidths(container: IContainerModel, exception: IContainerModel): SizePointer[] { - const x = 0; - const width = Number(container.properties.width); - let unallocatedSpaces: SizePointer[] = [{ x, width }]; - - const rigidBodies = container.children.filter(child => child.properties.isRigidBody); - for (const child of rigidBodies) { - if (child === exception) { - continue; - } - - // get the space of the child that is inside the parent - let newUnallocatedSpace: SizePointer[] = []; - for (const unallocatedSpace of unallocatedSpaces) { - const newUnallocatedWidths = getAvailableWidthsTwoLines( - unallocatedSpace.x, - unallocatedSpace.x + unallocatedSpace.width, - child.properties.x, - child.properties.x + Number(child.properties.width)); - newUnallocatedSpace = newUnallocatedSpace.concat(newUnallocatedWidths); - } - unallocatedSpaces = newUnallocatedSpace; - } - - return unallocatedSpaces; -} - -/** - * Returns the unallocated widths between two lines in 1D - * @param min1 left of the first line - * @param max1 rigth of the first line - * @param min2 left of the second line - * @param max2 right of the second line - * @returns Available widths - */ -function getAvailableWidthsTwoLines(min1: number, max1: number, min2: number, max2: number): SizePointer[] { - if (min2 < min1 && max2 > max1) { - // object 2 is overlapping full width - return []; - } - - if (min1 >= min2) { - // object 2 is partially overlapping on the left - return [{ - x: max2, - width: max1 - max2 - }]; - } - - if (max2 >= max1) { - // object 2 is partially overlapping on the right - return [{ - x: min2, - width: max2 - min1 - }]; - } - - // object 2 is overlapping in the middle - return [ - { - x: min1, - width: min2 - min1 - }, - { - x: min2, - width: max1 - max2 - } - ]; -} - -/** - * - * @param container - * @returns - */ -function constraintBodyInsideUnallocatedWidth(container: IContainerModel): IContainerModel { - if (container.parent === null) { - return container; - } - - const availableWidths = getAvailableWidths(container.parent, container); - const containerX = Number(container.properties.x); - - // Sort the available width - availableWidths - .sort((width1, width2) => Math.abs(width1.x - containerX) - Math.abs(width2.x - containerX)); - - if (availableWidths.length === 0) { - throw new Error('No available space found on the parent container. Try to free the parent a little before placing it inside.'); - } - - const availableWidthFound = availableWidths.find( - width => isFitting(container, width) - ); - - if (availableWidthFound === undefined) { - // There is two way to reach this part of the code - // 1) toggle the isRigidBody such as width > availableWidth.width - // 2) resize a container such as width > availableWidth.width - // We want the container to fit automatically inside the available space - // even if it means to resize the container - // The end goal is that the code never show the error message no matter what action is done - // TODO: Actually give an option to not fit and show the error message shown below - const availableWidth = availableWidths[0]; - container.properties.x = availableWidth.x; - container.properties.width = availableWidth.width; - // throw new Error('[constraintBodyInsideUnallocatedWidth] BIGERR: No available space found on the parent container, even though there is some.'); - return container; - } - - return constraintBodyInsideSpace( - container, - availableWidthFound.x, - 0, - availableWidthFound.width, - Number(container.parent.properties.height) - ); -} - -function isFitting(container: IContainerModel, sizePointer: SizePointer): boolean { - const containerWidth = Number(container.properties.width); - - return containerWidth <= sizePointer.width; + }); + setHistory(history); + setHistoryCurrentStep(history.length - 1); } diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx index ad27c22..3a728e3 100644 --- a/src/Components/Editor/Editor.tsx +++ b/src/Components/Editor/Editor.tsx @@ -1,31 +1,35 @@ -import React from 'react'; +import React, { useRef } from 'react'; import './Editor.scss'; -import { Configuration } from '../../Interfaces/Configuration'; +import { IConfiguration } from '../../Interfaces/IConfiguration'; import { SVG } from '../SVG/SVG'; -import { HistoryState } from '../../Interfaces/HistoryState'; +import { IHistoryState } from '../../Interfaces/IHistoryState'; import { UI } from '../UI/UI'; -import { SelectContainer, DeleteContainer, OnPropertyChange, AddContainerToSelectedContainer, AddContainer } from './ContainerOperations'; +import { SelectContainer, DeleteContainer, AddContainerToSelectedContainer, AddContainer } from './ContainerOperations'; import { SaveEditorAsJSON, SaveEditorAsSVG } from './Save'; import { onKeyDown } from './Shortcuts'; +import { OnPropertyChange, OnPropertiesSubmit } from './PropertiesOperations'; +import EditorEvents from '../../Events/EditorEvents'; +import { IEditorState } from '../../Interfaces/IEditorState'; +import { MAX_HISTORY } from '../../utils/default'; interface IEditorProps { - configuration: Configuration - history: HistoryState[] + configuration: IConfiguration + history: IHistoryState[] historyCurrentStep: number } -export interface IEditorState { - history: HistoryState[] - historyCurrentStep: number - configuration: Configuration -} +export const getCurrentHistory = (history: IHistoryState[], historyCurrentStep: number): IHistoryState[] => + history.slice( + Math.max(0, history.length - MAX_HISTORY), // change this to 0 for unlimited (not recommanded because of overflow) + historyCurrentStep + 1 + ); -export const getCurrentHistory = (history: HistoryState[], historyCurrentStep: number): HistoryState[] => history.slice(0, historyCurrentStep + 1); -export const getCurrentHistoryState = (history: HistoryState[], historyCurrentStep: number): HistoryState => history[historyCurrentStep]; +export const getCurrentHistoryState = (history: IHistoryState[], historyCurrentStep: number): IHistoryState => history[historyCurrentStep]; const Editor: React.FunctionComponent = (props) => { - const [history, setHistory] = React.useState(structuredClone(props.history)); + const [history, setHistory] = React.useState(structuredClone(props.history)); const [historyCurrentStep, setHistoryCurrentStep] = React.useState(props.historyCurrentStep); + const editorRef = useRef(null); React.useEffect(() => { const onKeyUp = (event: KeyboardEvent): void => onKeyDown( @@ -37,15 +41,37 @@ const Editor: React.FunctionComponent = (props) => { window.addEventListener('keyup', onKeyUp); + const events = EditorEvents; + const editorState: IEditorState = { + history, + historyCurrentStep, + configuration: props.configuration + }; + + const funcs = new Map void>(); + for (const event of events) { + const func = (): void => event.func(editorState); + editorRef.current?.addEventListener(event.name, func); + funcs.set(event.name, func); + } + return () => { window.removeEventListener('keyup', onKeyUp); + + for (const event of events) { + const func = funcs.get(event.name); + if (func === undefined) { + continue; + } + editorRef.current?.removeEventListener(event.name, func); + } }; }); const configuration = props.configuration; const current = getCurrentHistoryState(history, historyCurrentStep); return ( -
+
= (props) => { setHistory, setHistoryCurrentStep )} + OnPropertiesSubmit={(event, properties) => OnPropertiesSubmit( + event, + properties, + history, + historyCurrentStep, + setHistory, + setHistoryCurrentStep + )} AddContainerToSelectedContainer={(type) => AddContainerToSelectedContainer( type, configuration, diff --git a/src/Components/Editor/PropertiesOperations.ts b/src/Components/Editor/PropertiesOperations.ts new file mode 100644 index 0000000..4bd4c2d --- /dev/null +++ b/src/Components/Editor/PropertiesOperations.ts @@ -0,0 +1,120 @@ +import { Dispatch, SetStateAction } from 'react'; +import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerModel'; +import { IHistoryState } from '../../Interfaces/IHistoryState'; +import IProperties from '../../Interfaces/IProperties'; +import { findContainerById } from '../../utils/itertools'; +import { getCurrentHistory } from './Editor'; +import { RecalculatePhysics } from './Behaviors/RigidBodyBehaviors'; +import { INPUT_TYPES } from '../Properties/PropertiesInputTypes'; +import { ImposePosition } from './Behaviors/AnchorBehaviors'; + +/** + * Handled the property change event in the properties form + * @param key Property name + * @param value New value of the property + * @returns void + */ +export function OnPropertyChange( + key: string, + value: string | number | boolean, + fullHistory: IHistoryState[], + historyCurrentStep: number, + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch> +): void { + const history = getCurrentHistory(fullHistory, historyCurrentStep); + const current = history[history.length - 1]; + + if (current.SelectedContainer === null || + current.SelectedContainer === undefined) { + throw new Error('[OnPropertyChange] Property was changed before selecting a Container'); + } + + const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); + const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id); + + if (container === null || container === undefined) { + throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); + } + + if (INPUT_TYPES[key] === 'number') { + (container.properties as any)[key] = Number(value); + } else { + (container.properties as any)[key] = value; + } + + if (container.properties.isAnchor) { + ImposePosition(container); + } + + if (container.properties.isRigidBody) { + RecalculatePhysics(container); + } + + history.push({ + LastAction: `Change ${key} of ${container.properties.id}`, + MainContainer: mainContainerClone, + SelectedContainer: container, + SelectedContainerId: container.properties.id, + TypeCounters: Object.assign({}, current.TypeCounters) + }); + setHistory(history); + setHistoryCurrentStep(history.length - 1); +} + +/** + * Handled the property change event in the properties form + * @param key Property name + * @param properties Properties of the selected container + * @returns void + */ +export function OnPropertiesSubmit( + event: React.SyntheticEvent, + properties: IProperties, + fullHistory: IHistoryState[], + historyCurrentStep: number, + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch> +): void { + event.preventDefault(); + const history = getCurrentHistory(fullHistory, historyCurrentStep); + const current = history[history.length - 1]; + + if (current.SelectedContainer === null || + current.SelectedContainer === undefined) { + throw new Error('[OnPropertyChange] Property was changed before selecting a Container'); + } + + const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); + const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id); + + if (container === null || container === undefined) { + throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); + } + + for (const property in properties) { + const input = (event.target as HTMLFormElement).querySelector(`#${property}`); + if (input instanceof HTMLInputElement) { + (container.properties as any)[property] = input.value; + if (INPUT_TYPES[property] === 'number') { + (container.properties as any)[property] = Number(input.value); + } else { + (container.properties as any)[property] = input.value; + } + } + } + + if (container.properties.isRigidBody) { + RecalculatePhysics(container); + } + + history.push({ + LastAction: `Change properties of ${container.properties.id}`, + MainContainer: mainContainerClone, + SelectedContainer: container, + SelectedContainerId: container.properties.id, + TypeCounters: Object.assign({}, current.TypeCounters) + }); + setHistory(history); + setHistoryCurrentStep(history.length - 1); +} diff --git a/src/Components/Editor/Save.ts b/src/Components/Editor/Save.ts index 91cee2f..159959a 100644 --- a/src/Components/Editor/Save.ts +++ b/src/Components/Editor/Save.ts @@ -1,29 +1,40 @@ -import { HistoryState } from "../../Interfaces/HistoryState"; -import { Configuration } from '../../Interfaces/Configuration'; +import { IHistoryState } from '../../Interfaces/IHistoryState'; +import { IConfiguration } from '../../Interfaces/IConfiguration'; import { getCircularReplacer } from '../../utils/saveload'; import { ID } from '../SVG/SVG'; -import { IEditorState } from './Editor'; +import { IEditorState } from '../../Interfaces/IEditorState'; +import Worker from '../../workers/worker?worker'; export function SaveEditorAsJSON( - history: HistoryState[], + history: IHistoryState[], historyCurrentStep: number, - configuration: Configuration + configuration: IConfiguration ): void { - const exportName = 'state'; + const exportName = 'state.json'; const spaces = import.meta.env.DEV ? 4 : 0; const editorState: IEditorState = { history, historyCurrentStep, configuration }; + + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (window.Worker) { + // use webworker for the stringify to avoid freezing + const myWorker = new Worker(); + myWorker.postMessage({ editorState, spaces }); + myWorker.onmessage = (event) => { + const data = event.data; + const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`; + createDownloadNode(exportName, dataStr); + myWorker.terminate(); + }; + return; + } + const data = JSON.stringify(editorState, getCircularReplacer(), spaces); const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`; - const downloadAnchorNode = document.createElement('a'); - downloadAnchorNode.setAttribute('href', dataStr); - downloadAnchorNode.setAttribute('download', `${exportName}.json`); - document.body.appendChild(downloadAnchorNode); // required for firefox - downloadAnchorNode.click(); - downloadAnchorNode.remove(); + createDownloadNode(exportName, dataStr); } export function SaveEditorAsSVG(): void { @@ -32,10 +43,14 @@ export function SaveEditorAsSVG(): void { const preface = '\r\n'; const svgBlob = new Blob([preface, svg.outerHTML], { type: 'image/svg+xml;charset=utf-8' }); const svgUrl = URL.createObjectURL(svgBlob); - const downloadLink = document.createElement('a'); - downloadLink.href = svgUrl; - downloadLink.download = 'newesttree.svg'; - document.body.appendChild(downloadLink); - downloadLink.click(); - document.body.removeChild(downloadLink); + createDownloadNode('state.svg', svgUrl); +} + +function createDownloadNode(filename: string, datastring: string) { + const downloadAnchorNode = document.createElement('a'); + downloadAnchorNode.href = datastring; + downloadAnchorNode.download = filename; + document.body.appendChild(downloadAnchorNode); // required for firefox + downloadAnchorNode.click(); + downloadAnchorNode.remove(); } diff --git a/src/Components/Editor/Shortcuts.ts b/src/Components/Editor/Shortcuts.ts index 8ac6790..aa20d96 100644 --- a/src/Components/Editor/Shortcuts.ts +++ b/src/Components/Editor/Shortcuts.ts @@ -1,9 +1,9 @@ import { Dispatch, SetStateAction } from 'react'; -import { HistoryState } from '../../Interfaces/HistoryState'; +import { IHistoryState } from '../../Interfaces/IHistoryState'; export function onKeyDown( event: KeyboardEvent, - history: HistoryState[], + history: IHistoryState[], historyCurrentStep: number, setHistoryCurrentStep: Dispatch> ): void { diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index c1372b1..8d34480 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'; import * as React from 'react'; import { fireEvent, render, screen } from '../../utils/test-utils'; import { ElementsSidebar } from './ElementsSidebar'; -import { IContainerModel } from '../../Interfaces/ContainerModel'; +import { IContainerModel } from '../../Interfaces/IContainerModel'; describe.concurrent('Elements sidebar', () => { it('With a MainContainer', () => { @@ -17,7 +17,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 2000, height: 100, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} }} @@ -25,6 +26,7 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={null} OnPropertyChange={() => {}} + OnPropertiesSubmit={() => {}} SelectContainer={() => {}} DeleteContainer={() => {}} AddContainer={() => {}} @@ -46,7 +48,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 2000, height: 100, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} }; @@ -57,6 +60,7 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={MainContainer} OnPropertyChange={() => {}} + OnPropertiesSubmit={() => {}} SelectContainer={() => {}} DeleteContainer={() => {}} AddContainer={() => {}} @@ -70,12 +74,12 @@ describe.concurrent('Elements sidebar', () => { expect(screen.queryByText('y')).toBeDefined(); expect(screen.queryByText('width')).toBeDefined(); expect(screen.queryByText('height')).toBeDefined(); - const propertyId = container.querySelector('#property-id'); - const propertyParentId = container.querySelector('#property-parentId'); - const propertyX = container.querySelector('#property-x'); - const propertyY = container.querySelector('#property-y'); - const propertyWidth = container.querySelector('#property-width'); - const propertyHeight = container.querySelector('#property-height'); + const propertyId = container.querySelector('#id'); + const propertyParentId = container.querySelector('#parentId'); + const propertyX = container.querySelector('#x'); + const propertyY = container.querySelector('#y'); + const propertyWidth = container.querySelector('#width'); + const propertyHeight = container.querySelector('#height'); expect((propertyId as HTMLInputElement).value).toBe(MainContainer.properties.id.toString()); expect(propertyParentId).toBeDefined(); expect((propertyParentId as HTMLInputElement).value).toBe(''); @@ -101,7 +105,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 2000, height: 100, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} }; @@ -117,7 +122,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 0, height: 0, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} } @@ -134,7 +140,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 0, height: 0, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} } @@ -146,6 +153,7 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={MainContainer} OnPropertyChange={() => {}} + OnPropertiesSubmit={() => {}} SelectContainer={() => {}} DeleteContainer={() => {}} AddContainer={() => {}} @@ -170,7 +178,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 2000, height: 100, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} }; @@ -185,7 +194,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 0, height: 0, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} }; @@ -202,6 +212,7 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={SelectedContainer} OnPropertyChange={() => {}} + OnPropertiesSubmit={() => {}} SelectContainer={selectContainer} DeleteContainer={() => {}} AddContainer={() => {}} @@ -212,8 +223,8 @@ describe.concurrent('Elements sidebar', () => { expect(screen.getByText(/main/i)); const child1 = screen.getByText(/child-1/i); expect(child1); - const propertyId = container.querySelector('#property-id'); - const propertyParentId = container.querySelector('#property-parentId'); + const propertyId = container.querySelector('#id'); + const propertyParentId = container.querySelector('#parentId'); expect((propertyId as HTMLInputElement).value).toBe(MainContainer.properties.id.toString()); expect((propertyParentId as HTMLInputElement).value).toBe(''); @@ -225,6 +236,7 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={SelectedContainer} OnPropertyChange={() => {}} + OnPropertiesSubmit={() => {}} SelectContainer={selectContainer} DeleteContainer={() => {}} AddContainer={() => {}} diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index 873161d..30f1617 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -1,12 +1,13 @@ import * as React from 'react'; -import { motion } from 'framer-motion'; +import { FixedSizeList as List } from 'react-window'; import { Properties } from '../Properties/Properties'; -import { IContainerModel } from '../../Interfaces/ContainerModel'; +import ContainerProperties from '../../Interfaces/IProperties'; +import { IContainerModel } from '../../Interfaces/IContainerModel'; import { getDepth, MakeIterator } from '../../utils/itertools'; import { Menu } from '../Menu/Menu'; import { MenuItem } from '../Menu/MenuItem'; import { handleDragLeave, handleDragOver, handleLeftClick, handleOnDrop, handleRightClick } from './MouseEventHandlers'; -import { Point } from '../../Interfaces/Point'; +import { IPoint } from '../../Interfaces/IPoint'; interface IElementsSidebarProps { MainContainer: IContainerModel @@ -14,55 +15,17 @@ interface IElementsSidebarProps { isHistoryOpen: boolean SelectedContainer: IContainerModel | null OnPropertyChange: (key: string, value: string | number | boolean) => void + OnPropertiesSubmit: (event: React.FormEvent, properties: ContainerProperties) => void SelectContainer: (container: IContainerModel) => void DeleteContainer: (containerid: string) => void AddContainer: (index: number, type: string, parent: string) => void } -function createRows( - container: IContainerModel, - props: IElementsSidebarProps, - containerRows: React.ReactNode[] -): void { - const depth: number = getDepth(container); - const key = container.properties.id.toString(); - const text = '|\t'.repeat(depth) + key; - const selectedClass: string = props.SelectedContainer !== undefined && - props.SelectedContainer !== null && - props.SelectedContainer.properties.id === container.properties.id - ? 'border-l-4 bg-slate-400/60 hover:bg-slate-400' - : 'bg-slate-300/60 hover:bg-slate-300'; - - containerRows.push( - handleOnDrop(event, props.MainContainer, props.AddContainer)} - onDragOver={(event) => handleDragOver(event, props.MainContainer)} - onDragLeave={(event) => handleDragLeave(event)} - onClick={() => props.SelectContainer(container)} - > - { text } - - ); -}; - export const ElementsSidebar: React.FC = (props: IElementsSidebarProps): JSX.Element => { // States const [isContextMenuOpen, setIsContextMenuOpen] = React.useState(false); const [onClickContainerId, setOnClickContainerId] = React.useState(''); - const [contextMenuPosition, setContextMenuPosition] = React.useState({ + const [contextMenuPosition, setContextMenuPosition] = React.useState({ x: 0, y: 0 }); @@ -105,7 +68,7 @@ export const ElementsSidebar: React.FC = (props: IElement onLeftClick ); }; - }, []); + }); // Render let isOpenClasses = '-right-64'; @@ -115,24 +78,55 @@ export const ElementsSidebar: React.FC = (props: IElement : 'right-0'; } - const containerRows: React.ReactNode[] = []; - const it = MakeIterator(props.MainContainer); - for (const container of it) { - createRows( - container, - props, - containerRows - ); - } + const containers = [...it]; + const Row = ({ index, style }: {index: number, style: React.CSSProperties}): JSX.Element => { + const container = containers[index]; + const depth: number = getDepth(container); + const key = container.properties.id.toString(); + const text = '|\t'.repeat(depth) + key; + const selectedClass: string = props.SelectedContainer !== undefined && + props.SelectedContainer !== null && + props.SelectedContainer.properties.id === container.properties.id + ? 'border-l-4 bg-slate-400/60 hover:bg-slate-400' + : 'bg-slate-300/60 hover:bg-slate-300'; + return ( + + ); + }; + + const ROW_HEIGHT = 35; + const NUMBERS_OF_ROWS = 10; return ( -
+
Elements
-
- { containerRows } +
+ + { Row } +
= (props: IElement props.DeleteContainer(onClickContainerId); }} /> - +
); }; diff --git a/src/Components/ElementsSidebar/MouseEventHandlers.ts b/src/Components/ElementsSidebar/MouseEventHandlers.ts index 53fb14e..1a814d2 100644 --- a/src/Components/ElementsSidebar/MouseEventHandlers.ts +++ b/src/Components/ElementsSidebar/MouseEventHandlers.ts @@ -1,12 +1,12 @@ -import { IContainerModel } from '../../Interfaces/ContainerModel'; -import { Point } from '../../Interfaces/Point'; +import { IContainerModel } from '../../Interfaces/IContainerModel'; +import { IPoint } from '../../Interfaces/IPoint'; import { findContainerById } from '../../utils/itertools'; export function handleRightClick( event: MouseEvent, setIsContextMenuOpen: React.Dispatch>, setOnClickContainerId: React.Dispatch>, - setContextMenuPosition: React.Dispatch> + setContextMenuPosition: React.Dispatch> ): void { event.preventDefault(); @@ -16,7 +16,7 @@ export function handleRightClick( return; } - const contextMenuPosition: Point = { x: event.pageX, y: event.pageY }; + const contextMenuPosition: IPoint = { x: event.pageX, y: event.pageY }; setIsContextMenuOpen(true); setOnClickContainerId(event.target.id); setContextMenuPosition(contextMenuPosition); diff --git a/src/Components/History/History.tsx b/src/Components/History/History.tsx index 067a867..36e8e80 100644 --- a/src/Components/History/History.tsx +++ b/src/Components/History/History.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; -import { HistoryState } from "../../Interfaces/HistoryState"; +import { FixedSizeList as List } from 'react-window'; +import { IHistoryState } from '../../Interfaces/IHistoryState'; interface IHistoryProps { - history: HistoryState[] + history: IHistoryState[] historyCurrentStep: number isOpen: boolean jumpTo: (move: number) => void @@ -10,47 +11,45 @@ interface IHistoryProps { export const History: React.FC = (props: IHistoryProps) => { const isOpenClasses = props.isOpen ? 'right-0' : '-right-64'; + const Row = ({ index, style }: {index: number, style: React.CSSProperties}): JSX.Element => { + const reversedIndex = (props.history.length - 1) - index; + const step = props.history[reversedIndex]; + const desc = step.LastAction; - const states = props.history.map((step, move) => { - const desc = move > 0 - ? `Go to modification n°${move}` - : 'Go to the beginning'; - - const isCurrent = move === props.historyCurrentStep; - - const selectedClass = isCurrent + const selectedClass = reversedIndex === props.historyCurrentStep ? 'bg-blue-500 hover:bg-blue-600' : 'bg-slate-500 hover:bg-slate-700'; - const isCurrentText = isCurrent - ? ' (current)' - : ''; return ( - ); - }); - - // recent first - states.reverse(); + }; return ( -
+
Timeline
-
- { states } -
+ + { Row } +
); }; diff --git a/src/Components/MainMenu/MainMenu.tsx b/src/Components/MainMenu/MainMenu.tsx index 1d84f9a..9c8b27f 100644 --- a/src/Components/MainMenu/MainMenu.tsx +++ b/src/Components/MainMenu/MainMenu.tsx @@ -38,13 +38,8 @@ export const MainMenu: React.FC = (props) => { diff --git a/src/Components/Properties/Properties.test.tsx b/src/Components/Properties/Properties.test.tsx index 9f0fe13..9fbef37 100644 --- a/src/Components/Properties/Properties.test.tsx +++ b/src/Components/Properties/Properties.test.tsx @@ -8,6 +8,7 @@ describe.concurrent('Properties', () => { render( {}} + onSubmit={() => {}} />); expect(screen.queryByText('id')).toBeNull(); @@ -16,13 +17,14 @@ describe.concurrent('Properties', () => { expect(screen.queryByText('y')).toBeNull(); }); - it('Some properties', () => { + it('Some properties, change values with dynamic input', () => { const prop = { id: 'stuff', parentId: 'parentId', x: 1, y: 1, - isRigidBody: false + isRigidBody: false, + isAnchor: false }; const handleChange = vi.fn((key, value) => { @@ -32,6 +34,7 @@ describe.concurrent('Properties', () => { const { container, rerender } = render( {}} />); expect(screen.queryByText('id')).toBeDefined(); @@ -39,10 +42,10 @@ describe.concurrent('Properties', () => { expect(screen.queryByText('x')).toBeDefined(); expect(screen.queryByText('y')).toBeDefined(); - let propertyId = container.querySelector('#property-id'); - let propertyParentId = container.querySelector('#property-parentId'); - let propertyX = container.querySelector('#property-x'); - let propertyY = container.querySelector('#property-y'); + let propertyId = container.querySelector('#id'); + let propertyParentId = container.querySelector('#parentId'); + let propertyX = container.querySelector('#x'); + let propertyY = container.querySelector('#y'); expect(propertyId).toBeDefined(); expect((propertyId as HTMLInputElement).value).toBe('stuff'); expect(propertyParentId).toBeDefined(); @@ -65,12 +68,13 @@ describe.concurrent('Properties', () => { rerender( {}} />); - propertyId = container.querySelector('#property-id'); - propertyParentId = container.querySelector('#property-parentId'); - propertyX = container.querySelector('#property-x'); - propertyY = container.querySelector('#property-y'); + propertyId = container.querySelector('#id'); + propertyParentId = container.querySelector('#parentId'); + propertyX = container.querySelector('#x'); + propertyY = container.querySelector('#y'); expect(propertyId).toBeDefined(); expect((propertyId as HTMLInputElement).value).toBe('stuffed'); expect(propertyParentId).toBeDefined(); diff --git a/src/Components/Properties/Properties.tsx b/src/Components/Properties/Properties.tsx index c1ac966..516f23d 100644 --- a/src/Components/Properties/Properties.tsx +++ b/src/Components/Properties/Properties.tsx @@ -1,13 +1,17 @@ -import * as React from 'react'; -import ContainerProperties from '../../Interfaces/Properties'; +import React, { useState } from 'react'; +import ContainerProperties from '../../Interfaces/IProperties'; +import { ToggleButton } from '../ToggleButton/ToggleButton'; import { INPUT_TYPES } from './PropertiesInputTypes'; interface IPropertiesProps { properties?: ContainerProperties onChange: (key: string, value: string | number | boolean) => void + onSubmit: (event: React.FormEvent, properties: ContainerProperties) => void } export const Properties: React.FC = (props: IPropertiesProps) => { + const [isDynamicInput, setIsDynamicInput] = useState(true); + if (props.properties === undefined) { return
; } @@ -15,11 +19,33 @@ export const Properties: React.FC = (props: IPropertiesProps) const groupInput: React.ReactNode[] = []; Object .entries(props.properties) - .forEach((pair) => handleProperties(pair, groupInput, props.onChange)); + .forEach((pair) => handleProperties(pair, groupInput, isDynamicInput, props.onChange)); + + const form = isDynamicInput + ?
+ { groupInput } +
+ :
props.onSubmit(event, props.properties as ContainerProperties)} + > + +
+ { groupInput } +
+
+ ; return ( -
- { groupInput } +
+ setIsDynamicInput(!isDynamicInput)} + /> + { form }
); }; @@ -27,6 +53,7 @@ export const Properties: React.FC = (props: IPropertiesProps) const handleProperties = ( [key, value]: [string, string | number], groupInput: React.ReactNode[], + isDynamicInput: boolean, onChange: (key: string, value: string | number | boolean) => void ): void => { const id = `property-${key}`; @@ -42,31 +69,48 @@ const handleProperties = ( type = INPUT_TYPES[key]; } + const className = ` + w-full + text-xs font-medium transition-all text-gray-800 mt-1 px-3 py-2 + bg-white border-2 border-white rounded-lg placeholder-gray-800 + focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 + disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none`; const isDisabled = ['id', 'parentId'].includes(key); - /// + const input = isDynamicInput + ? { + if (type === 'checkbox') { + onChange(key, event.target.checked); + return; + } + onChange(key, event.target.value); + }} + disabled={isDisabled} + /> + : ; groupInput.push( -
- - { - if (type === 'checkbox') { - onChange(key, event.target.checked); - return; - } - onChange(key, event.target.value); - }} - disabled={isDisabled} - /> -
+ ); + groupInput.push(input); }; diff --git a/src/Components/Properties/PropertiesInputTypes.tsx b/src/Components/Properties/PropertiesInputTypes.tsx index c62a8fd..d91ddbc 100644 --- a/src/Components/Properties/PropertiesInputTypes.tsx +++ b/src/Components/Properties/PropertiesInputTypes.tsx @@ -3,5 +3,6 @@ export const INPUT_TYPES: Record = { y: 'number', width: 'number', height: 'number', - isRigidBody: 'checkbox' + isRigidBody: 'checkbox', + isAnchor: 'checkbox' }; diff --git a/src/Components/SVG/Elements/Container.tsx b/src/Components/SVG/Elements/Container.tsx index 02dea6f..6417570 100644 --- a/src/Components/SVG/Elements/Container.tsx +++ b/src/Components/SVG/Elements/Container.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; -import { IContainerModel } from '../../../Interfaces/ContainerModel'; +import { XPositionReference } from '../../../Enums/XPositionReference'; +import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { getDepth } from '../../../utils/itertools'; import { Dimension } from './Dimension'; -export interface IContainerProps { +interface IContainerProps { model: IContainerModel } @@ -17,7 +18,14 @@ export const Container: React.FC = (props: IContainerProps) => const containersElements = props.model.children.map(child => ); const xText = Number(props.model.properties.width) / 2; const yText = Number(props.model.properties.height) / 2; - const transform = `translate(${Number(props.model.properties.x)}, ${Number(props.model.properties.y)})`; + + const [transformedX, transformedY] = transformPosition( + Number(props.model.properties.x), + Number(props.model.properties.y), + Number(props.model.properties.width), + props.model.properties.XPositionReference + ); + const transform = `translate(${transformedX}, ${transformedY})`; // g style const defaultStyle: React.CSSProperties = { @@ -54,7 +62,8 @@ export const Container: React.FC = (props: IContainerProps) => id={id} xStart={xStart} xEnd={xEnd} - y={y} + yStart={y} + yEnd={y} strokeWidth={strokeWidth} text={text} /> @@ -74,3 +83,13 @@ export const Container: React.FC = (props: IContainerProps) => ); }; + +function transformPosition(x: number, y: number, width: number, xPositionReference = XPositionReference.Left): [number, number] { + let transformedX = x; + if (xPositionReference === XPositionReference.Center) { + transformedX -= width / 2; + } else if (xPositionReference === XPositionReference.Right) { + transformedX -= width; + } + return [transformedX, y]; +} diff --git a/src/Components/SVG/Elements/Dimension.tsx b/src/Components/SVG/Elements/Dimension.tsx index ec51873..c5f6f86 100644 --- a/src/Components/SVG/Elements/Dimension.tsx +++ b/src/Components/SVG/Elements/Dimension.tsx @@ -1,47 +1,83 @@ import * as React from 'react'; +import { NOTCHES_LENGTH } from '../../../utils/default'; interface IDimensionProps { id: string xStart: number + yStart: number xEnd: number - y: number + yEnd: number text: string strokeWidth: number } +/** + * 2D Parametric function. Returns a new coordinate from the origin coordinate + * See for more details https://en.wikipedia.org/wiki/Parametric_equation. + * TL;DR a parametric function is a function with a parameter + * @param x0 Origin coordinate + * @param t The parameter + * @param vx Transform vector + * @returns Returns a new coordinate from the origin coordinate + */ +const applyParametric = (x0: number, t: number, vx: number): number => x0 + t * vx; + export const Dimension: React.FC = (props: IDimensionProps) => { const style: React.CSSProperties = { stroke: 'black' }; + + /// We need to find the points of the notches + // Get the vector of the line + const [deltaX, deltaY] = [(props.xEnd - props.xStart), (props.yEnd - props.yStart)]; + + // Get the unit vector + const norm = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + const [unitX, unitY] = [deltaX / norm, deltaY / norm]; + + // Get the perpandicular vector + const [perpVecX, perpVecY] = [unitY, -unitX]; + + // Use the parametric function to get the coordinates (x = x0 + t * v.x) + const startTopX = applyParametric(props.xStart, NOTCHES_LENGTH, perpVecX); + const startTopY = applyParametric(props.yStart, NOTCHES_LENGTH, perpVecY); + const startBottomX = applyParametric(props.xStart, -NOTCHES_LENGTH, perpVecX); + const startBottomY = applyParametric(props.yStart, -NOTCHES_LENGTH, perpVecY); + + const endTopX = applyParametric(props.xEnd, NOTCHES_LENGTH, perpVecX); + const endTopY = applyParametric(props.yEnd, NOTCHES_LENGTH, perpVecY); + const endBottomX = applyParametric(props.xEnd, -NOTCHES_LENGTH, perpVecX); + const endBottomY = applyParametric(props.yEnd, -NOTCHES_LENGTH, perpVecY); + return ( {props.text} diff --git a/src/Components/SVG/Elements/DimensionLayer.tsx b/src/Components/SVG/Elements/DimensionLayer.tsx deleted file mode 100644 index 85f1a43..0000000 --- a/src/Components/SVG/Elements/DimensionLayer.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import * as React from 'react'; -import { ContainerModel } from '../../../Interfaces/ContainerModel'; -import { getDepth, MakeIterator } from '../../../utils/itertools'; -import { Dimension } from './Dimension'; - -interface IDimensionLayerProps { - isHidden: boolean - roots: ContainerModel | ContainerModel[] | null -} - -const GAP: number = 50; - -const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => { - const it = MakeIterator(root); - const dimensions: React.ReactNode[] = []; - for (const container of it) { - // WARN: this might be dangerous later when using other units/rules - const width = Number(container.properties.width); - - const id = `dim-${container.properties.id}`; - const xStart: number = container.properties.x; - const xEnd = xStart + width; - const y = -(GAP * (getDepth(container) + 1)); - const strokeWidth = 1; - const text = width.toString(); - dimensions.push( - - ); - } - return dimensions; -}; - -/** - * A layer containing all dimension - * - * @deprecated In order to avoid adding complexity - * with computing the position in a group hierarchy, - * use Dimension directly inside the Container, - * Currently it is glitched as - * it does not take parents into account, - * and will not work correctly - * @param props - * @returns - */ -export const DimensionLayer: React.FC = (props: IDimensionLayerProps) => { - let dimensions: React.ReactNode[] = []; - if (Array.isArray(props.roots)) { - props.roots.forEach(child => { - dimensions.concat(getDimensionsNodes(child)); - }); - } else if (props.roots !== null) { - dimensions = getDimensionsNodes(props.roots); - } - return ( - - { dimensions } - - ); -}; diff --git a/src/Components/SVG/Elements/Selector.tsx b/src/Components/SVG/Elements/Selector.tsx index c5937e0..e70ca79 100644 --- a/src/Components/SVG/Elements/Selector.tsx +++ b/src/Components/SVG/Elements/Selector.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { IContainerModel } from '../../../Interfaces/ContainerModel'; +import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { getAbsolutePosition } from '../../../utils/itertools'; interface ISelectorProps { diff --git a/src/Components/SVG/SVG.tsx b/src/Components/SVG/SVG.tsx index 94fb746..652258f 100644 --- a/src/Components/SVG/SVG.tsx +++ b/src/Components/SVG/SVG.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { UncontrolledReactSVGPanZoom } from 'react-svg-pan-zoom'; import { Container } from './Elements/Container'; -import { ContainerModel } from '../../Interfaces/ContainerModel'; +import { ContainerModel } from '../../Interfaces/IContainerModel'; import { Selector } from './Elements/Selector'; import { BAR_WIDTH } from '../Bar/Bar'; @@ -30,7 +30,7 @@ function resizeViewBox( export const SVG: React.FC = (props: ISVGProps) => { const [viewer, setViewer] = React.useState({ - viewerWidth: window.innerWidth, + viewerWidth: window.innerWidth - BAR_WIDTH, viewerHeight: window.innerHeight }); diff --git a/src/Components/Sidebar/Sidebar.tsx b/src/Components/Sidebar/Sidebar.tsx index dc2f1a7..c39b235 100644 --- a/src/Components/Sidebar/Sidebar.tsx +++ b/src/Components/Sidebar/Sidebar.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; -import { AvailableContainer } from '../../Interfaces/AvailableContainer'; +import { IAvailableContainer } from '../../Interfaces/IAvailableContainer'; import { truncateString } from '../../utils/stringtools'; interface ISidebarProps { - componentOptions: AvailableContainer[] + componentOptions: IAvailableContainer[] isOpen: boolean buttonOnClick: (type: string) => void } @@ -30,7 +30,7 @@ export const Sidebar: React.FC = (props: ISidebarProps) => { const isOpenClasses = props.isOpen ? 'left-16' : '-left-64'; return (
Components diff --git a/src/Components/ToggleButton/ToggleButton.scss b/src/Components/ToggleButton/ToggleButton.scss new file mode 100644 index 0000000..0948f52 --- /dev/null +++ b/src/Components/ToggleButton/ToggleButton.scss @@ -0,0 +1,8 @@ +input:checked ~ .dot { + transform: translateX(100%); +} +input:checked ~ .line { + background-color: #3B82F6; +} + + diff --git a/src/Components/ToggleButton/ToggleButton.tsx b/src/Components/ToggleButton/ToggleButton.tsx new file mode 100644 index 0000000..198bf99 --- /dev/null +++ b/src/Components/ToggleButton/ToggleButton.tsx @@ -0,0 +1,52 @@ +import React, { FC } from 'react'; +import './ToggleButton.scss'; + +interface IToggleButtonProps { + id: string + text: string + type?: TOGGLE_TYPE + title: string + checked: boolean + onChange: React.ChangeEventHandler +} + +export enum TOGGLE_TYPE { + MATERIAL, + IOS +} + +export const ToggleButton: FC = (props) => { + const id = `toggle-${props.id}`; + const type = props.type ?? TOGGLE_TYPE.MATERIAL; + let classLine = 'line w-10 h-4 bg-gray-400 rounded-full shadow-inner'; + let classDot = 'dot absolute w-6 h-6 bg-white rounded-full shadow -left-1 -top-1 transition'; + if (type === TOGGLE_TYPE.IOS) { + classLine = 'line block bg-gray-600 w-14 h-8 rounded-full'; + classDot = 'dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition'; + } + + return ( +
+
+ +
+
+ ); +}; diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index 6d6adbe..4fa36e6 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -2,21 +2,23 @@ import * as React from 'react'; import { ElementsSidebar } from '../ElementsSidebar/ElementsSidebar'; import { Sidebar } from '../Sidebar/Sidebar'; import { History } from '../History/History'; -import { AvailableContainer } from '../../Interfaces/AvailableContainer'; -import { ContainerModel } from '../../Interfaces/ContainerModel'; -import { HistoryState } from '../../Interfaces/HistoryState'; +import { IAvailableContainer } from '../../Interfaces/IAvailableContainer'; +import { ContainerModel } from '../../Interfaces/IContainerModel'; +import { IHistoryState } from '../../Interfaces/IHistoryState'; import { PhotographIcon, UploadIcon } from '@heroicons/react/outline'; import { FloatingButton } from '../FloatingButton/FloatingButton'; import { Bar } from '../Bar/Bar'; +import IProperties from '../../Interfaces/IProperties'; interface IUIProps { - current: HistoryState - history: HistoryState[] + current: IHistoryState + history: IHistoryState[] historyCurrentStep: number - AvailableContainers: AvailableContainer[] + AvailableContainers: IAvailableContainer[] SelectContainer: (container: ContainerModel) => void DeleteContainer: (containerId: string) => void OnPropertyChange: (key: string, value: string | number | boolean) => void + OnPropertiesSubmit: (event: React.FormEvent, properties: IProperties) => void AddContainerToSelectedContainer: (type: string) => void AddContainer: (index: number, type: string, parentId: string) => void SaveEditorAsJSON: () => void @@ -59,6 +61,7 @@ export const UI: React.FunctionComponent = (props: IUIProps) => { isOpen={isElementsSidebarOpen} isHistoryOpen={isHistoryOpen} OnPropertyChange={props.OnPropertyChange} + OnPropertiesSubmit={props.OnPropertiesSubmit} SelectContainer={props.SelectContainer} DeleteContainer={props.DeleteContainer} AddContainer={props.AddContainer} diff --git a/src/Enums/AddingBehavior.ts b/src/Enums/AddingBehavior.ts deleted file mode 100644 index fb6ae67..0000000 --- a/src/Enums/AddingBehavior.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum AddingBehavior { - InsertInto, - Replace -} diff --git a/src/Events/EditorEvents.ts b/src/Events/EditorEvents.ts new file mode 100644 index 0000000..316b7b8 --- /dev/null +++ b/src/Events/EditorEvents.ts @@ -0,0 +1,26 @@ +import { IEditorState } from '../Interfaces/IEditorState'; +import { IHistoryState } from '../Interfaces/IHistoryState'; + +const getEditorState = (editorState: IEditorState): void => { + const customEvent = new CustomEvent('getEditorState', { detail: editorState }); + document.dispatchEvent(customEvent); +}; + +const getCurrentHistoryState = (editorState: IEditorState): void => { + const customEvent = new CustomEvent( + 'getCurrentHistoryState', + { detail: editorState.history[editorState.historyCurrentStep] }); + document.dispatchEvent(customEvent); +}; + +export interface IEditorEvent { + name: string + func: (editorState: IEditorState) => void +} + +const events: IEditorEvent[] = [ + { name: 'getEditorState', func: getEditorState }, + { name: 'getCurrentHistoryState', func: getCurrentHistoryState } +]; + +export default events; diff --git a/src/Interfaces/Configuration.ts b/src/Interfaces/Configuration.ts deleted file mode 100644 index f8d4854..0000000 --- a/src/Interfaces/Configuration.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AvailableContainer } from './AvailableContainer'; -import { AvailableSymbolModel } from './AvailableSymbol'; - -/** Model of configuration for the application to configure it */ -export interface Configuration { - AvailableContainers: AvailableContainer[] - AvailableSymbols: AvailableSymbolModel[] - MainContainer: AvailableContainer -} diff --git a/src/Interfaces/AvailableContainer.ts b/src/Interfaces/IAvailableContainer.ts similarity index 54% rename from src/Interfaces/AvailableContainer.ts rename to src/Interfaces/IAvailableContainer.ts index d7bb22f..c7ad3c1 100644 --- a/src/Interfaces/AvailableContainer.ts +++ b/src/Interfaces/IAvailableContainer.ts @@ -1,9 +1,11 @@ import React from 'react'; +import { XPositionReference } from '../Enums/XPositionReference'; /** Model of available container used in application configuration */ -export interface AvailableContainer { +export interface IAvailableContainer { Type: string Width: number Height: number + XPositionReference?: XPositionReference Style: React.CSSProperties } diff --git a/src/Interfaces/AvailableSymbol.ts b/src/Interfaces/IAvailableSymbol.ts similarity index 71% rename from src/Interfaces/AvailableSymbol.ts rename to src/Interfaces/IAvailableSymbol.ts index e1d518d..3f3176a 100644 --- a/src/Interfaces/AvailableSymbol.ts +++ b/src/Interfaces/IAvailableSymbol.ts @@ -1,12 +1,12 @@ import { XPositionReference } from '../Enums/XPositionReference'; -import { Image } from './Image'; +import { IImage } from './IImage'; /** * Model of available symbol to configure the application */ -export interface AvailableSymbolModel { +export interface IAvailableSymbol { Name: string XPositionReference: XPositionReference - Image: Image + Image: IImage Width: number Height: number } diff --git a/src/Interfaces/IConfiguration.ts b/src/Interfaces/IConfiguration.ts new file mode 100644 index 0000000..a37647d --- /dev/null +++ b/src/Interfaces/IConfiguration.ts @@ -0,0 +1,9 @@ +import { IAvailableContainer } from './IAvailableContainer'; +import { IAvailableSymbol } from './IAvailableSymbol'; + +/** Model of configuration for the application to configure it */ +export interface IConfiguration { + AvailableContainers: IAvailableContainer[] + AvailableSymbols: IAvailableSymbol[] + MainContainer: IAvailableContainer +} diff --git a/src/Interfaces/ContainerModel.ts b/src/Interfaces/IContainerModel.ts similarity index 81% rename from src/Interfaces/ContainerModel.ts rename to src/Interfaces/IContainerModel.ts index 1c70ae3..b180486 100644 --- a/src/Interfaces/ContainerModel.ts +++ b/src/Interfaces/IContainerModel.ts @@ -1,21 +1,21 @@ -import Properties from './Properties'; +import IProperties from './IProperties'; export interface IContainerModel { children: IContainerModel[] parent: IContainerModel | null - properties: Properties + properties: IProperties userData: Record } export class ContainerModel implements IContainerModel { public children: IContainerModel[]; public parent: IContainerModel | null; - public properties: Properties; + public properties: IProperties; public userData: Record; constructor( parent: IContainerModel | null, - properties: Properties, + properties: IProperties, children: IContainerModel[] = [], userData = {}) { this.parent = parent; diff --git a/src/Interfaces/IEditorState.ts b/src/Interfaces/IEditorState.ts new file mode 100644 index 0000000..495a868 --- /dev/null +++ b/src/Interfaces/IEditorState.ts @@ -0,0 +1,8 @@ +import { IConfiguration } from './IConfiguration'; +import { IHistoryState } from './IHistoryState'; + +export interface IEditorState { + history: IHistoryState[] + historyCurrentStep: number + configuration: IConfiguration +} diff --git a/src/Interfaces/HistoryState.ts b/src/Interfaces/IHistoryState.ts similarity index 66% rename from src/Interfaces/HistoryState.ts rename to src/Interfaces/IHistoryState.ts index da1d74b..fd46fbc 100644 --- a/src/Interfaces/HistoryState.ts +++ b/src/Interfaces/IHistoryState.ts @@ -1,6 +1,6 @@ -import { IContainerModel } from './ContainerModel'; +import { IContainerModel } from './IContainerModel'; -export interface HistoryState { +export interface IHistoryState { LastAction: string MainContainer: IContainerModel SelectedContainer: IContainerModel | null diff --git a/src/Interfaces/Image.ts b/src/Interfaces/IImage.ts similarity index 81% rename from src/Interfaces/Image.ts rename to src/Interfaces/IImage.ts index b839b09..7432440 100644 --- a/src/Interfaces/Image.ts +++ b/src/Interfaces/IImage.ts @@ -1,5 +1,5 @@ /** Model of an image with multiple source */ -export interface Image { +export interface IImage { Name: string Url: string Base64Image: string diff --git a/src/Interfaces/Point.ts b/src/Interfaces/IPoint.ts similarity index 50% rename from src/Interfaces/Point.ts rename to src/Interfaces/IPoint.ts index 43fd673..d2e202a 100644 --- a/src/Interfaces/Point.ts +++ b/src/Interfaces/IPoint.ts @@ -1,4 +1,4 @@ -export interface Point { +export interface IPoint { x: number y: number } diff --git a/src/Interfaces/IProperties.ts b/src/Interfaces/IProperties.ts new file mode 100644 index 0000000..ef2db7e --- /dev/null +++ b/src/Interfaces/IProperties.ts @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { XPositionReference } from '../Enums/XPositionReference'; + +/** + * Properties of a container + * @property id id of the container + * @property parentId id of the parent container + * @property x horizontal offset of the container + * @property y vertical offset of the container + * @property isRigidBody if true apply rigid body behaviors + * @property isAnchor if true apply anchor behaviors + */ +export default interface IProperties extends React.CSSProperties { + id: string + parentId: string | null + x: number + y: number + isRigidBody: boolean + isAnchor: boolean + XPositionReference?: XPositionReference +} diff --git a/src/Interfaces/ISizePointer.ts b/src/Interfaces/ISizePointer.ts new file mode 100644 index 0000000..05f880c --- /dev/null +++ b/src/Interfaces/ISizePointer.ts @@ -0,0 +1,9 @@ +/** + * A SizePointer is a pointer in a 1 dimensional array of width/space + * x being the address where the pointer is pointing + * width being the overall (un)allocated space affected to the address + */ +export interface ISizePointer { + x: number + width: number +} diff --git a/src/Interfaces/Properties.ts b/src/Interfaces/Properties.ts deleted file mode 100644 index ea5f54e..0000000 --- a/src/Interfaces/Properties.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as React from 'react'; - -export default interface Properties extends React.CSSProperties { - id: string - parentId: string | null - x: number - y: number - isRigidBody: boolean -} diff --git a/src/Interfaces/SizePointer.ts b/src/Interfaces/SizePointer.ts deleted file mode 100644 index 9a80057..0000000 --- a/src/Interfaces/SizePointer.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface SizePointer { - x: number - width: number -} diff --git a/src/index.scss b/src/index.scss index 1bc3361..f653990 100644 --- a/src/index.scss +++ b/src/index.scss @@ -23,6 +23,16 @@ @apply transition-all bg-blue-100 hover:bg-blue-200 text-blue-700 text-lg font-semibold p-8 rounded-lg } + .normal-btn { + @apply text-sm + py-2 px-4 + rounded-full border-0 + font-semibold + transition-all + bg-blue-100 text-blue-700 + hover:bg-blue-200 + } + .floating-btn { @apply h-full w-full text-white align-middle items-center justify-center } diff --git a/src/utils/default.ts b/src/utils/default.ts index 552b8a0..027c540 100644 --- a/src/utils/default.ts +++ b/src/utils/default.ts @@ -1,7 +1,7 @@ -import { Configuration } from '../Interfaces/Configuration'; -import Properties from '../Interfaces/Properties'; +import { IConfiguration } from '../Interfaces/IConfiguration'; +import IProperties from '../Interfaces/IProperties'; -export const DEFAULT_CONFIG: Configuration = { +export const DEFAULT_CONFIG: IConfiguration = { AvailableContainers: [ { Type: 'Container', @@ -25,14 +25,19 @@ export const DEFAULT_CONFIG: Configuration = { } }; -export const DEFAULT_MAINCONTAINER_PROPS: Properties = { +export const DEFAULT_MAINCONTAINER_PROPS: IProperties = { id: 'main', parentId: 'null', x: 0, y: 0, - isRigidBody: false, width: DEFAULT_CONFIG.MainContainer.Width, height: DEFAULT_CONFIG.MainContainer.Height, + isRigidBody: false, + isAnchor: false, fillOpacity: 0, stroke: 'black' }; + +export const NOTCHES_LENGTH = 4; + +export const MAX_HISTORY = 200; diff --git a/src/utils/itertools.ts b/src/utils/itertools.ts index 67cd40c..d50a089 100644 --- a/src/utils/itertools.ts +++ b/src/utils/itertools.ts @@ -1,4 +1,4 @@ -import { IContainerModel } from '../Interfaces/ContainerModel'; +import { IContainerModel } from '../Interfaces/IContainerModel'; /** * Returns a Generator iterating of over the children depth-first diff --git a/src/utils/saveload.ts b/src/utils/saveload.ts index a33652e..356dd50 100644 --- a/src/utils/saveload.ts +++ b/src/utils/saveload.ts @@ -1,5 +1,5 @@ import { findContainerById, MakeIterator } from './itertools'; -import { IEditorState } from '../Components/Editor/Editor'; +import { IEditorState } from '../Interfaces/IEditorState'; /** * Revive the Editor state diff --git a/src/workers/worker.js b/src/workers/worker.js new file mode 100644 index 0000000..c11fa7d --- /dev/null +++ b/src/workers/worker.js @@ -0,0 +1,25 @@ +onmessage = (e) => { + const data = JSON.stringify(e.data.editorState, getCircularReplacer(), e.data.spaces); + postMessage(data); +}; + +const getCircularReplacer = () => { + const seen = new WeakSet(); + return (key, value) => { + if (key === 'parent') { + return; + } + + if (key === 'SelectedContainer') { + return; + } + + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return; + } + seen.add(value); + } + return value; + }; +}; diff --git a/test-server/http.js b/test-server/http.js index c4f3238..69088c9 100644 --- a/test-server/http.js +++ b/test-server/http.js @@ -76,9 +76,6 @@ const GetSVGLayoutConfiguration = () => { fillOpacity: 0, borderWidth: 2, stroke: 'blue', - transform: 'translateX(-50%)', - transformOrigin: 'center', - transformBox: 'fill-box' } } ], diff --git a/tsconfig.json b/tsconfig.json index 034ee0e..2fe4d5a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ "noEmit": true, "jsx": "react-jsx" }, - "include": ["src"], + "include": ["src", "src/workers"], "exclude": ["test-server"], "references": [{ "path": "./tsconfig.node.json" }] }