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 (
+ handleOnDrop(event, props.MainContainer, props.AddContainer)}
+ onDragOver={(event) => handleDragOver(event, props.MainContainer)}
+ onDragLeave={(event) => handleDragLeave(event)}
+ onClick={() => props.SelectContainer(container)}
+ >
+ { text }
+
+ );
+ };
+
+ 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 (
-
props.jumpTo(move)}
+ key={reversedIndex}
+ style={style}
+ onClick={() => props.jumpTo(reversedIndex)}
+ title={step.LastAction}
className={
- `w-full elements-sidebar-row whitespace-pre
+ `w-full elements-sidebar-row whitespace-pre overflow-hidden
text-left text-sm font-medium transition-all ${selectedClass}`
}
>
- {desc}{isCurrentText}
+ {desc}
);
- });
-
- // 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) => {
setWindowState(WindowState.MAIN)}
- className='block text-sm
- mt-8 py-2 px-4
- rounded-full border-0
- font-semibold
- transition-all
- bg-blue-100 text-blue-700
- hover:bg-blue-200'
+ className='normal-btn block
+ mt-8 '
>
Go back
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 }
+
+ :
+ ;
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(
-
- {key}
- {
- if (type === 'checkbox') {
- onChange(key, event.target.checked);
- return;
- }
- onChange(key, event.target.value);
- }}
- disabled={isDisabled}
- />
-
+
+ {key}
+
);
+ 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 (
+
+
+
+
+
+ { props.text }
+
+
+
+
+ );
+};
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" }]
}