From 6e52f2d511661a0d01e101c589412e9fee024cad Mon Sep 17 00:00:00 2001 From: Duc-Quan Do <99700700+ducquando@users.noreply.github.com> Date: Sat, 20 Apr 2024 14:14:08 +0700 Subject: [PATCH] Add Codecov bundle (#6) - [IMPORTANT] Upgrade to align with MyDraft - Add codecov badges and bundle - Modify relative path for code coverage - Add autoprefixer for cross-browser compatibility - Change Github Actions - Add initial PWA supports --- .github/workflows/build.yml | 4 + .github/workflows/test.yml | 9 +- README.md | 3 + package-lock.json | 223 +++++++++++++++--- package.json | 8 +- public/site.webmanifest | 21 +- src/App.tsx | 20 +- src/core/utils/color-palette.spec.ts | 2 +- src/core/utils/color.spec.ts | 82 +++---- src/core/utils/immutable-list.spec.ts | 24 +- src/core/utils/immutable-list.ts | 14 +- src/core/utils/immutable-map.spec.ts | 150 ++++++------ src/core/utils/immutable-map.ts | 103 ++++---- src/core/utils/immutable-set.spec.ts | 8 +- src/core/utils/immutable-set.ts | 76 +++--- src/core/utils/math-helper.ts | 9 - src/core/utils/types.spec.ts | 24 +- src/core/utils/types.ts | 52 +++- src/core/utils/vec2.spec.ts | 2 + src/index.tsx | 60 +---- src/store.ts | 60 +++++ src/wireframes/components/AnimationView.tsx | 4 +- src/wireframes/components/EditorView.tsx | 46 ++-- src/wireframes/components/PagesView.tsx | 4 +- src/wireframes/components/RecentView.tsx | 4 +- src/wireframes/components/ShapeView.tsx | 4 +- src/wireframes/components/ToolView.tsx | 25 +- .../components/actions/Components.tsx | 29 +++ src/wireframes/components/actions/shared.ts | 22 +- .../components/actions/use-alignment.ts | 18 +- .../components/actions/use-clipboard.ts | 19 +- .../components/actions/use-grouping.ts | 14 +- .../components/actions/use-history.ts | 4 +- .../components/actions/use-loading.ts | 4 +- .../components/actions/use-remove.ts | 12 +- .../components/actions/use-server.ts | 4 +- .../components/headers/ArrangeHeader.tsx | 4 +- .../components/headers/FileHeader.tsx | 6 +- .../components/headers/IdHeader.tsx | 12 +- .../components/headers/ModeHeader.tsx | 4 +- .../components/headers/PresentHeader.tsx | 4 +- .../components/menu/ContextMenu.tsx | 59 +++++ src/wireframes/components/menu/index.ts | 1 + .../components/settings/ColorSetting.tsx | 4 +- .../components/settings/DiagramSetting.tsx | 4 +- .../components/settings/PresentSetting.tsx | 4 +- .../components/styles/EditorView.scss | 1 + .../components/tools/GraphicTool.tsx | 13 +- src/wireframes/components/tools/LineTool.tsx | 4 +- src/wireframes/components/tools/TableTool.tsx | 11 +- src/wireframes/components/tools/TextTool.tsx | 8 +- .../components/tools/VisualTool.tsx | 8 +- src/wireframes/components/tools/ZoomTool.tsx | 4 +- src/wireframes/model/actions/api.ts | 4 +- src/wireframes/model/actions/appearance.ts | 14 +- src/wireframes/model/actions/grouping.spec.ts | 8 +- src/wireframes/model/actions/items.spec.ts | 8 +- src/wireframes/model/actions/items.ts | 8 +- src/wireframes/model/actions/loading.spec.ts | 17 +- src/wireframes/model/actions/loading.ts | 8 +- src/wireframes/model/actions/utils.ts | 2 +- src/wireframes/model/diagram-item-set.spec.ts | 50 +++- src/wireframes/model/diagram-item-set.ts | 138 ++++++----- src/wireframes/model/diagram-item.ts | 6 +- src/wireframes/model/diagram.spec.ts | 39 ++- src/wireframes/model/diagram.ts | 117 +++++---- src/wireframes/model/editor-state.ts | 16 +- src/wireframes/model/projections.ts | 47 +--- src/wireframes/model/serializer.spec.ts | 8 +- src/wireframes/model/serializer.ts | 18 +- src/wireframes/model/snap-manager.ts | 72 +++--- src/wireframes/renderer/Editor.tsx | 22 +- src/wireframes/renderer/SelectionAdorner.tsx | 21 +- src/wireframes/renderer/TextAdorner.tsx | 6 +- src/wireframes/renderer/TransformAdorner.tsx | 29 +-- .../renderer/interaction-overlays.ts | 4 +- src/wireframes/shapes/neutral/textbox.ts | 2 +- .../shapes/utils/abstract-control.ts | 140 ++--------- .../shapes/utils/svg-renderer2.spec.ts | 4 +- sw.js | 16 -- vite.config.ts | 15 ++ 81 files changed, 1239 insertions(+), 918 deletions(-) create mode 100644 src/store.ts create mode 100644 src/wireframes/components/menu/ContextMenu.tsx delete mode 100644 sw.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3a06ce9..7afeebe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,6 +26,9 @@ jobs: - name: Run Tests run: npm run test:ci + + - name: Run Tests Again + run: npm run test:ci - name: Upload storybook to Chromatic uses: chromaui/action@v1 @@ -36,6 +39,7 @@ jobs: uses: codecov/codecov-action@v4.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} + slug: code-slide/ui - name: Run Build run: npm run build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ccad2c8..57fc088 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,10 +27,17 @@ jobs: - name: Run Tests run: npm run test:ci + - name: Run Tests Again + run: npm run test:ci + + - name: Fix coverage paths + run: npm run fix-coverage-paths + - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v4.1.1 + uses: codecov/codecov-action@v4.0.1 with: token: ${{ secrets.CODECOV_TOKEN }} + slug: code-slide/ui - name: Run Build run: npm run build \ No newline at end of file diff --git a/README.md b/README.md index 67d4783..00f6c0f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # codeslide.net: A presentation tool for simplified sequential animations +[![Build & Deploy](https://github.com/code-slide/ui/actions/workflows/build.yml/badge.svg)](https://github.com/code-slide/ui/actions/workflows/build.yml) +[![codecov](https://codecov.io/gh/code-slide/ui/graph/badge.svg?token=WLF3XX4XNP)](https://codecov.io/gh/code-slide/ui) + This project is to integrate coding capabilities into a presentation creation tool. Ideal for streamlining the process, especially when dealing with multiple items on a slide. Try it out at: https://www.codeslide.net/ diff --git a/package-lock.json b/package-lock.json index 942e6b2..7d83969 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,6 @@ "@vitest/utils": "^0.34.6", "antd": "^5.15.4", "classnames": "^2.3.1", - "connected-react-router": "6.9.3", "date-fns": "^2.28.0", "deep-object-diff": "^1.1.9", "file-saver": "^2.0.5", @@ -46,6 +45,7 @@ "tslib": "2.3.1" }, "devDependencies": { + "@codecov/vite-plugin": "^0.0.1-beta.6", "@storybook/addon-actions": "^7.5.1", "@storybook/addon-essentials": "^7.5.1", "@storybook/addon-links": "^7.5.1", @@ -73,6 +73,7 @@ "@vitest/browser": "^0.34.6", "@vitest/coverage-istanbul": "^0.34.6", "@vitest/coverage-v8": "^0.34.6", + "autoprefixer": "^10.4.19", "chromatic": "^7.4.0", "eslint": "^8.51.0", "eslint-config-airbnb-typescript": "^17.1.0", @@ -83,6 +84,8 @@ "eslint-plugin-sort-keys-fix": "^1.1.2", "eslint-plugin-storybook": "^0.6.15", "jsdom": "^24.0.0", + "postcss": "^8.4.38", + "replace-in-file": "^7.1.0", "sass": "^1.72.0", "storybook": "^7.5.1", "typescript": "^5.2.2", @@ -2336,6 +2339,69 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@codecov/bundler-plugin-core": { + "version": "0.0.1-beta.6", + "resolved": "https://registry.npmjs.org/@codecov/bundler-plugin-core/-/bundler-plugin-core-0.0.1-beta.6.tgz", + "integrity": "sha512-dGk/s90fZm7D1O00gRtuE2gqM/CW9nglaPzcIT0v10VOV2tj+aHRrdB+yjas+RW8QP8Qf/sMqSmjkufP1iqggw==", + "dev": true, + "dependencies": { + "chalk": "4.1.2", + "semver": "^7.5.4", + "unplugin": "^1.6.0", + "zod": "^3.22.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@codecov/bundler-plugin-core/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@codecov/bundler-plugin-core/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@codecov/bundler-plugin-core/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@codecov/vite-plugin": { + "version": "0.0.1-beta.6", + "resolved": "https://registry.npmjs.org/@codecov/vite-plugin/-/vite-plugin-0.0.1-beta.6.tgz", + "integrity": "sha512-eIi3M9R1om/T+6FoZGaffWMmwSZ0nx1fIGgidFRFkQdXtKAwHU0//YjCXOg17vJsPXlaSatM81EQBU/qmSXQ1Q==", + "dev": true, + "dependencies": { + "@codecov/bundler-plugin-core": "^0.0.1-beta.6" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "vite": "4.x || 5.x" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -7545,6 +7611,43 @@ "node": ">= 4.0.0" } }, + "node_modules/autoprefixer": { + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -8662,26 +8765,6 @@ "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", "dev": true }, - "node_modules/connected-react-router": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/connected-react-router/-/connected-react-router-6.9.3.tgz", - "integrity": "sha512-4ThxysOiv/R2Dc4Cke1eJwjKwH1Y51VDwlOrOfs1LjpdYOVvCNjNkZDayo7+sx42EeGJPQUNchWkjAIJdXGIOQ==", - "dependencies": { - "lodash.isequalwith": "^4.4.0", - "prop-types": "^15.7.2" - }, - "optionalDependencies": { - "immutable": "^3.8.1 || ^4.0.0", - "seamless-immutable": "^7.1.3" - }, - "peerDependencies": { - "history": "^4.7.2", - "react": "^16.4.0 || ^17.0.0", - "react-redux": "^6.0.0 || ^7.1.0", - "react-router": "^4.3.1 || ^5.0.0", - "redux": "^3.6.0 || ^4.0.0" - } - }, "node_modules/consola": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", @@ -11228,6 +11311,19 @@ "node": ">= 0.6" } }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -12159,7 +12255,7 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", - "devOptional": true + "dev": true }, "node_modules/import-fresh": { "version": "3.3.0", @@ -13672,11 +13768,6 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, - "node_modules/lodash.isequalwith": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.isequalwith/-/lodash.isequalwith-4.4.0.tgz", - "integrity": "sha512-dcZON0IalGBpRmJBmMkaoV7d3I80R2O+FrzsZyHdNSFrANq/cgDqKQNmAHE8UEj4+QYWwwhkQOVdLHiAopzlsQ==" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -14334,6 +14425,15 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-url": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", @@ -15171,6 +15271,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -16934,6 +17040,54 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/replace-in-file": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-7.1.0.tgz", + "integrity": "sha512-1uZmJ78WtqNYCSuPC9IWbweXkGxPOtk2rKuar8diTw7naVIQZiE3Tm8ACx2PCMXDtVH6N+XxwaRY2qZ2xHPqXw==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "glob": "^8.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "replace-in-file": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/replace-in-file/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/replace-in-file/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -17331,12 +17485,6 @@ "compute-scroll-into-view": "^3.0.2" } }, - "node_modules/seamless-immutable": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/seamless-immutable/-/seamless-immutable-7.1.4.tgz", - "integrity": "sha512-XiUO1QP4ki4E2PHegiGAlu6r82o5A+6tRh7IkGGTVg/h+UoeX4nFBeCGPOhb4CYjvkqsfm/TUtvOMYC1xmV30A==", - "optional": true - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -20766,6 +20914,15 @@ "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } + }, + "node_modules/zod": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.5.tgz", + "integrity": "sha512-HqnGsCdVZ2xc0qWPLdO25WnseXThh0kEYKIdV5F/hTHO75hNZFp8thxSeHhiPrHZKrFTo1SOgkAj9po5bexZlw==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 2307122..6e56120 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "@vitest/utils": "^0.34.6", "antd": "^5.15.4", "classnames": "^2.3.1", - "connected-react-router": "6.9.3", "date-fns": "^2.28.0", "deep-object-diff": "^1.1.9", "file-saver": "^2.0.5", @@ -51,9 +50,11 @@ "test:cv": "vitest --coverage", "test:ci": "vitest run --coverage --browser.name=chromium --browser.provider=playwright --browser.headless", "storybook": "storybook dev -p 6006", - "chromatic": "npx chromatic --project-token=chpt_fb075605340d316" + "chromatic": "npx chromatic --project-token=chpt_fb075605340d316", + "fix-coverage-paths": "replace-in-file '/home/runner/work/ui/ui/' './' coverage/coverage-final.json coverage/clover.xml" }, "devDependencies": { + "@codecov/vite-plugin": "^0.0.1-beta.6", "@storybook/addon-actions": "^7.5.1", "@storybook/addon-essentials": "^7.5.1", "@storybook/addon-links": "^7.5.1", @@ -81,6 +82,7 @@ "@vitest/browser": "^0.34.6", "@vitest/coverage-istanbul": "^0.34.6", "@vitest/coverage-v8": "^0.34.6", + "autoprefixer": "^10.4.19", "chromatic": "^7.4.0", "eslint": "^8.51.0", "eslint-config-airbnb-typescript": "^17.1.0", @@ -91,6 +93,8 @@ "eslint-plugin-sort-keys-fix": "^1.1.2", "eslint-plugin-storybook": "^0.6.15", "jsdom": "^24.0.0", + "postcss": "^8.4.38", + "replace-in-file": "^7.1.0", "sass": "^1.72.0", "storybook": "^7.5.1", "typescript": "^5.2.2", diff --git a/public/site.webmanifest b/public/site.webmanifest index 45dc8a2..e6b628e 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -1 +1,20 @@ -{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file +{ + "name": "CodeSlide", + "short_name": "CodeSlide", + "description": "A presentation tool that simplifies sequential animations with drawing and coding", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#d9e8fc", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index bda85d7..62451bc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,23 +8,22 @@ import { ConfigProvider, Layout, Tour, TourProps } from 'antd'; import * as React from 'react'; -import { useDispatch } from 'react-redux'; import { useRouteMatch } from 'react-router'; import { ClipboardContainer } from '@app/core'; import { EditorView, ShapeView, PagesView, HeaderView, AnimationView, ToolView } from '@app/wireframes/components'; -import { getSelectedItems, getSelectedShape, loadDiagramFromServer, newDiagram, setIsTourOpen, useStore } from '@app/wireframes/model'; +import { getSelection, loadDiagramFromServer, newDiagram, setIsTourOpen, useStore } from '@app/wireframes/model'; import { vogues } from './const'; import { CustomDragLayer } from './wireframes/components/CustomDragLayer'; import { OverlayContainer } from './wireframes/contexts/OverlayContext'; import { useEffect, useRef } from 'react'; +import { useAppDispatch } from './store'; export const App = () => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const route = useRouteMatch<{ token?: string }>(); const routeToken = route.params.token || null; const routeTokenSnapshot = React.useRef(routeToken); - const selectedItem = useStore(getSelectedShape); - const selectedSet = useStore(getSelectedItems); + const selectedSet = useStore(getSelection); const sidebarWidth = useStore(s => s.ui.sidebarSize); const footerHeight = useStore(s => s.ui.footerSize); const applicationMode = useStore(s => s.ui.selectedMode); @@ -41,15 +40,6 @@ export const App = () => { } }, [dispatch]); - useEffect(() => { - const handleContextmenu = (e: MouseEvent) => { e.preventDefault() }; - document.addEventListener('contextmenu', handleContextmenu); - - return function cleanup() { - document.removeEventListener('contextmenu', handleContextmenu) - } - }, []) - const margin = { tool: `${vogues.common.editorPad}px 0`, sideLeft: `${vogues.common.editorPad}px ${vogues.common.editorPad}px ${vogues.common.editorPad}px 0`, @@ -128,7 +118,7 @@ export const App = () => { ref={tourRefs[3]} className='header-toolbar-left' style={{ margin: margin.tool }}> - + diff --git a/src/core/utils/color-palette.spec.ts b/src/core/utils/color-palette.spec.ts index 9ec5efb..1e08a6c 100644 --- a/src/core/utils/color-palette.spec.ts +++ b/src/core/utils/color-palette.spec.ts @@ -14,4 +14,4 @@ describe('ColorPalatte', () => { expect(palette.colors.length).toBeGreaterThan(20); }); -}); +}); \ No newline at end of file diff --git a/src/core/utils/color.spec.ts b/src/core/utils/color.spec.ts index 91b93ee..490005c 100644 --- a/src/core/utils/color.spec.ts +++ b/src/core/utils/color.spec.ts @@ -155,52 +155,40 @@ describe('Color', () => { expect(color.b).toBe(0); }); - it('should convert from hsv with red', () => { - const color = Color.fromHsv(0, 1, 1); - - expect(color.r).toBe(1); - expect(color.g).toBe(0); - expect(color.b).toBe(0); - }); - - it('should convert from hsv with yellow', () => { - const color = Color.fromHsv(60, 1, 1); - - expect(color.r).toBe(1); - expect(color.g).toBe(1); - expect(color.b).toBe(0); - }); - - it('should convert from hsv with blue', () => { - const color = Color.fromHsv(120, 1, 1); - - expect(color.r).toBe(0); - expect(color.g).toBe(1); - expect(color.b).toBe(0); - }); - - it('should convert from hsv with turkis', () => { - const color = Color.fromHsv(180, 1, 1); - - expect(color.r).toBe(0); - expect(color.g).toBe(1); - expect(color.b).toBe(1); - }); - - it('should convert from hsv with red', () => { - const color = Color.fromHsv(240, 1, 1); - - expect(color.r).toBe(0); - expect(color.g).toBe(0); - expect(color.b).toBe(1); - }); - - it('should convert from hsv with pink', () => { - const color = Color.fromHsv(300, 1, 1); - - expect(color.r).toBe(1); - expect(color.g).toBe(0); - expect(color.b).toBe(1); + [ + { r: 1, g: 0, b: 0, h: 0, name: 'red' }, + { r: 1, g: 1, b: 0, h: 60, name: 'yellow' }, + { r: 0, g: 1, b: 0, h: 120, name: 'green' }, + { r: 0, g: 1, b: 1, h: 180, name: 'turkis' }, + { r: 0, g: 0, b: 1, h: 240, name: 'blue' }, + { r: 1, g: 0, b: 1, h: 300, name: 'pink' }, + { r: 1, g: 0, b: 0, h: 360, name: 'red2' }, + ].forEach(test => { + it(`should convert from hsl ${test.name}`, () => { + const color = Color.fromHsl(test.h, 1, 0.5); + + expect(color.r).toBeCloseTo(test.r); + expect(color.g).toBeCloseTo(test.g); + expect(color.b).toBeCloseTo(test.b); + }); + }); + + [ + { r: 1, g: 0, b: 0, h: 0, name: 'red' }, + { r: 1, g: 1, b: 0, h: 60, name: 'yellow' }, + { r: 0, g: 1, b: 0, h: 120, name: 'green' }, + { r: 0, g: 1, b: 1, h: 180, name: 'turkis' }, + { r: 0, g: 0, b: 1, h: 240, name: 'blue' }, + { r: 1, g: 0, b: 1, h: 300, name: 'pink' }, + { r: 1, g: 0, b: 0, h: 360, name: 'red2' }, + ].forEach(test => { + it(`should convert from hsv ${test.name}`, () => { + const color = Color.fromHsv(test.h, 1, 1); + + expect(color.r).toBe(test.r); + expect(color.g).toBe(test.g); + expect(color.b).toBe(test.b); + }); }); it('should be valid black', () => { @@ -271,4 +259,4 @@ describe('Color', () => { it('should throw error for invalid string', () => { expect(() => Color.fromValue('INVALID')).toThrowError('Color is not in a valid format.'); }); -}); +}); \ No newline at end of file diff --git a/src/core/utils/immutable-list.spec.ts b/src/core/utils/immutable-list.spec.ts index 1cf604b..fa9335d 100644 --- a/src/core/utils/immutable-list.spec.ts +++ b/src/core/utils/immutable-list.spec.ts @@ -18,9 +18,11 @@ describe('ImmutableList', () => { }); it('should cache empty instance', () => { - const list = ImmutableList.of([]); + const list_1 = ImmutableList.of([]); + const list_2 = ImmutableList.of(undefined); - expect(list).toBe(ImmutableList.empty()); + expect(list_1).toBe(ImmutableList.empty()); + expect(list_2).toBe(ImmutableList.empty()); }); it('should instantiate from array of items', () => { @@ -32,6 +34,13 @@ describe('ImmutableList', () => { expect(list_1.at(2)).toBe(3); }); + it('should instantiate from another ImmutableList', () => { + const list_1 = ImmutableList.of([1, 2, 3]); + const list_2 = ImmutableList.of(list_1); + + expect(list_1).toBe(list_2); + }); + it('should add indexes', () => { const list_1 = ImmutableList.of([1, 2, 3]); @@ -189,4 +198,13 @@ describe('ImmutableList', () => { expect(list_a.equals(null!)).toBeFalsy(); }); -}); + + it('should modify list only if remove item (-> undefined)', () => { + const list = ImmutableList.of([1, 2, 3]); + + expect(list.set(2, undefined).values).toEqual([1, 2, undefined]); + expect(list.set(1, 2)).toBe(list); + expect(list.set(-1, 4)).toBe(list); + expect(list.set(4, 4)).toBe(list); + }); +}); \ No newline at end of file diff --git a/src/core/utils/immutable-list.ts b/src/core/utils/immutable-list.ts index 6e4a6bd..12455b2 100644 --- a/src/core/utils/immutable-list.ts +++ b/src/core/utils/immutable-list.ts @@ -16,14 +16,14 @@ export class ImmutableList { return this.items.length; } - public get values() { - return this.items; + public get values(): ReadonlyArray { + return Array.from(this.items); } public at(index: number): T | undefined { return this.items[index]; } - + public indexOf(item: T) { return this.items.indexOf(item); } @@ -52,7 +52,7 @@ export class ImmutableList { } public add(...items: ReadonlyArray) { - if (items.length === 0) { + if (!items || items.length === 0) { return this; } @@ -62,7 +62,7 @@ export class ImmutableList { } public remove(...items: ReadonlyArray) { - if (items.length === 0) { + if (!items || items.length === 0) { return this; } @@ -110,6 +110,10 @@ export class ImmutableList { } public moveTo(items: ReadonlyArray, target: number, relative = false) { + if (!items) { + return this; + } + return this.replace(moveItems(this.items, items, target, relative)); } diff --git a/src/core/utils/immutable-map.spec.ts b/src/core/utils/immutable-map.spec.ts index eb1da92..44d1ee3 100644 --- a/src/core/utils/immutable-map.spec.ts +++ b/src/core/utils/immutable-map.spec.ts @@ -12,155 +12,147 @@ import { ImmutableMap } from '@app/core/utils'; describe('ImmutableMap', () => { it('should instantiate without arguments', () => { - const list = ImmutableMap.empty(); + const map = ImmutableMap.empty(); - expect(list.size).toBe(0); + expect(map.size).toBe(0); }); it('should return empty instance if creating map from empty object', () => { - const list = ImmutableMap.of({}); + const map = ImmutableMap.of({}); - expect(list).toBe(ImmutableMap.empty()); + expect(map).toBe(ImmutableMap.empty()); }); it('should add items', () => { - const set_1 = ImmutableMap.empty(); - const set_2 = set_1.set('1', 10); - const set_3 = set_2.set('1', 10); - const set_4 = set_3.set('2', 20); - const set_5 = set_4.set('3', 30); + const map_1 = ImmutableMap.empty(); + const map_2 = map_1.set('1', 10); + const map_3 = map_2.set('1', 10); + const map_4 = map_3.set('2', 20); + const map_5 = map_4.set('3', 30); - expect(set_5.size).toBe(3); - expect(set_5.has('1')).toBeTruthy(); - expect(set_5.has('2')).toBeTruthy(); - expect(set_5.has('3')).toBeTruthy(); + expect(map_5.size).toBe(3); + expect(map_5.has('1')).toBeTruthy(); + expect(map_5.has('2')).toBeTruthy(); + expect(map_5.has('3')).toBeTruthy(); }); - it('should convert to key array', () => { - const set_1 = ImmutableMap.of({ 1: 10, 2: 20 }); + it('should convert to keys', () => { + const map_1 = ImmutableMap.of({ 1: 10, 2: 20 }); - const array = set_1.keys; - - expect(array.length).toBe(2); - expect(array).toContain('1'); - expect(array).toContain('2'); + expect(map_1.keys).toEqual(['1', '2']); }); - it('should convert to value array', () => { - const set_1 = ImmutableMap.of({ 1: 10, 2: 20 }); - - const array = set_1.values; + it('should convert to values', () => { + const map_1 = ImmutableMap.of({ 1: 10, 2: 20 }); - expect(array.length).toBe(2); - expect(array).toContain(10); - expect(array).toContain(20); + expect(map_1.values).toEqual([10, 20]); }); it('should return original set when key to add is null', () => { - const set_1 = ImmutableMap.empty(); - const set_2 = set_1.set(null!, 10); + const map_1 = ImmutableMap.empty(); + const map_2 = map_1.set(null!, 10); - expect(set_2).toBe(set_1); + expect(map_2).toBe(map_1); }); it('should return original set when item to add already has the same value', () => { - const set_1 = ImmutableMap.empty(); - const set_2 = set_1.set('1', 10); - const set_3 = set_2.set('1', 10); + const map_1 = ImmutableMap.empty(); + const map_2 = map_1.set('1', 10); + const map_3 = map_2.set('1', 10); - expect(set_3).toBe(set_2); + expect(map_3).toBe(map_2); }); it('should update item', () => { - const set_1 = ImmutableMap.empty(); - const set_2 = set_1.set('1', 10); - const set_3 = set_2.update('1', x => x * x); + const map_1 = ImmutableMap.empty(); + const map_2 = map_1.set('1', 10); + const map_3 = map_2.update('1', x => x * x); - expect(set_3.get('1')).toEqual(100); + expect(map_3.get('1')).toEqual(100); }); it('should return original set when item to update is not found', () => { - const set_1 = ImmutableMap.empty(); - const set_2 = set_1.set('1', 10); - const set_3 = set_2.update('unknown', x => x * x); + const map_1 = ImmutableMap.empty(); + const map_2 = map_1.set('1', 10); + const map_3 = map_2.update('unknown', x => x * x); - expect(set_3).toBe(set_2); + expect(map_3).toBe(map_2); }); it('should return original set when update returns same item', () => { - const set_1 = ImmutableMap.empty(); - const set_2 = set_1.set('1', 10); - const set_3 = set_2.update('unknown', x => x); + const map_1 = ImmutableMap.empty(); + const map_2 = map_1.set('1', 10); + const map_3 = map_2.update('unknown', x => x); - expect(set_3).toBe(set_2); + expect(map_3).toBe(map_2); }); it('should update items', () => { - const set_1 = ImmutableMap.empty(); - const set_2 = set_1.set('1', 10); - const set_3 = set_2.updateAll(x => x * x); + const map_1 = ImmutableMap.empty(); + const map_2 = map_1.set('1', 10); + const map_3 = map_2.updateAll(x => x * x); - expect(set_3.get('1')).toEqual(100); + expect(map_3.get('1')).toEqual(100); }); it('should return original set when update returns same items', () => { - const set_1 = ImmutableMap.empty(); - const set_2 = set_1.set('1', 10); - const set_3 = set_2.updateAll(x => x); + const map_1 = ImmutableMap.empty(); + const map_2 = map_1.set('1', 10); + const map_3 = map_2.updateAll(x => x); - expect(set_3).toBe(set_2); + expect(map_3).toBe(map_2); }); it('should remove item', () => { - const set_1 = ImmutableMap.empty(); - const set_2 = set_1.set('1', 10); - const set_3 = set_2.remove('1'); + const map_1 = ImmutableMap.empty(); + const map_2 = map_1.set('1', 10); + const map_3 = map_2.remove('1'); - expect(set_3.size).toBe(0); + expect(map_3.size).toBe(0); }); it('should return original set when item to remove is not found', () => { - const set_1 = ImmutableMap.empty(); - const set_2 = set_1.set('1', 10); - const set_3 = set_2.remove('unknown'); + const map_1 = ImmutableMap.empty(); + const map_2 = map_1.set('1', 10); + const map_3 = map_2.remove('unknown'); - expect(set_3).toBe(set_2); + expect(map_3).toBe(map_2); }); it('should remove item', () => { - const set_1 = ImmutableMap.of({ 1: 10, 2: 20, 3: 30 }); - const set_2 = set_1.remove('2'); + const map_1 = ImmutableMap.of({ 1: 10, 2: 20, 3: 30 }); + const map_2 = map_1.remove('2'); - expect(set_2.size).toBe(2); - expect(set_2.has('1')).toBeTruthy(); - expect(set_2.has('3')).toBeTruthy(); + expect(map_2.size).toBe(2); + expect(map_2.has('1')).toBeTruthy(); + expect(map_2.has('3')).toBeTruthy(); }); it('should return original set when item to remove is null', () => { - const set_1 = ImmutableMap.of({ 1: 10, 2: 20 }); - const set_2 = set_1.remove(null!); + const map_1 = ImmutableMap.of({ 1: 10, 2: 20 }); + const map_2 = map_1.remove(null!); - expect(set_2).toBe(set_1); + expect(map_2).toBe(map_1); }); - it('should mutate set', () => { - const set_1 = ImmutableMap.of({ 1: 10, 2: 20, 3: 30 }); - const set_2 = set_1.mutate(m => { + it('should mutate map', () => { + const map_1 = ImmutableMap.of({ 1: 10, 2: 20, 3: 30 }); + const map_2 = map_1.mutate(m => { m.set('4', 4); m.remove('2'); m.remove('3'); }); - expect(set_2.size).toBe(2); - expect(set_2.has('1')).toBeTruthy(); - expect(set_2.has('4')).toBeTruthy(); + expect(map_2.size).toBe(2); + expect(map_2.has('1')).toBeTruthy(); + expect(map_2.has('4')).toBeTruthy(); }); it('should return orginal set when nothing has been mutated', () => { - const set_1 = ImmutableMap.of({ 1: 10, 2: 20, 3: 30 }); - const set_2 = set_1.mutate(() => false); + const map_1 = ImmutableMap.of({ 1: 10, 2: 20, 3: 30 }); + const map_2 = map_1.mutate(() => false); - expect(set_2).toBe(set_1); + expect(map_2).toBe(map_1); }); it('should return true for equals when maps have same values', () => { diff --git a/src/core/utils/immutable-map.ts b/src/core/utils/immutable-map.ts index bd8d099..fceb06e 100644 --- a/src/core/utils/immutable-map.ts +++ b/src/core/utils/immutable-map.ts @@ -6,45 +6,45 @@ * Copyright (c) Do Duc Quan. All rights reserved. */ -import { Types, without } from './types'; +import { Types } from './types'; -type Mutator = { +type Mutator = { remove: (key: string) => void; - set: (key: string, value: T) => void; + set: (key: string, value: V) => void; - update: (key: string, updater: (value: T) => T) => void; + update: (key: string, updater: (value: V) => V) => void; }; -export class ImmutableMap { - private static readonly EMPTY = new ImmutableMap([]); +export class ImmutableMap { + private static readonly EMPTY = new ImmutableMap(new Map()); public get size() { - return Object.keys(this.items).length; + return this.items.size; } - public get keys() { - return Object.keys(this.items); + public get keys(): ReadonlyArray { + return Array.from(this.items.keys()); } - public get values() { - return this.keys.map(k => this.items[k]); + public get values(): ReadonlyArray { + return Array.from(this.items.values()); } - public get raw() { - return this.items; + public get entries(): ReadonlyArray { + return Array.from(this.items.entries()); } - public get(key: string): T | undefined { - return this.items[key]; + public get(key: string): V | undefined { + return this.items.get(key); } public has(key: string) { - return key && this.items.hasOwnProperty(key); + return this.items.has(key); } private constructor( - private readonly items: Record, + private readonly items: Map, ) { Object.freeze(this); Object.freeze(items); @@ -62,11 +62,11 @@ export class ImmutableMap { } else if (Object.keys(items).length === 0) { return ImmutableMap.EMPTY; } else { - return new ImmutableMap(items); + return new ImmutableMap(new Map(Object.entries(items))); } } - public update(key: string, updater: (value: T) => T) { + public update(key: string, updater: (value: V) => V) { if (!this.has(key)) { return this; } @@ -74,45 +74,47 @@ export class ImmutableMap { return this.set(key, updater(this.get(key)!)); } - public updateAll(updater: (value: T) => T) { + public updateAll(updater: (value: V) => V) { if (this.size === 0) { return this; } - let updatedItems: { [key: string]: T } | undefined = undefined; + let updatedItems: Map | undefined = undefined; - for (const [key, current] of Object.entries(this.items)) { + for (const [key, current] of this.items) { const newValue = updater(current); if (Types.equals(current, newValue)) { continue; } - updatedItems ||= { ...this.items }; - updatedItems[key] = newValue; + updatedItems ||= new Map(this.items); + updatedItems.set(key, newValue); } if (!updatedItems) { return this; } - return new ImmutableMap(updatedItems); + return new ImmutableMap(updatedItems); } - public set(key: string, value: T) { + public set(key: string, value: V) { if (!key) { return this; } - const current = this.items[key]; + const current = this.items.get(key); if (Types.equals(current, value)) { return this; } - const items = { ...this.items, [key]: value }; + const items = new Map(this.items); - return new ImmutableMap(items); + items.set(key, value); + + return new ImmutableMap(items); } public remove(key: string) { @@ -120,45 +122,52 @@ export class ImmutableMap { return this; } - const items = without(this.items, key); + const items = new Map(this.items); + + items.delete(key); - return new ImmutableMap(items); + return new ImmutableMap(items); } - public mutate(updater: (mutator: Mutator) => void): ImmutableMap { - let updatedItems: { [key: string]: T } | undefined = undefined; + public mutate(updater: (mutator: Mutator) => void): ImmutableMap { + let updatedItems: Map | undefined = undefined; let updateCount = 0; - const mutator: Mutator = { + const mutator: Mutator = { set: (k, v) => { if (k) { - updatedItems ||= { ...this.items }; + updatedItems ||= new Map(this.items); - const current = this.items[k]; + const current = updatedItems.get(k); if (!Types.equals(current, v)) { updateCount++; - updatedItems[k] = v; + updatedItems.set(k, v); } } }, remove: (k) => { if (k) { - updatedItems ||= { ...this.items }; + updatedItems ||= new Map(this.items); - if (updatedItems.hasOwnProperty(k)) { + if (updatedItems.delete(k)) { updateCount++; - - delete updatedItems[k]; } } }, - update: (k, updater: (value: T) => T) => { + update: (k, updater: (value: V) => V) => { if (k) { - updatedItems ||= { ...this.items }; + updatedItems ||= new Map(this.items); + + const current = updatedItems.get(k); - if (updatedItems.hasOwnProperty(k)) { - mutator.set(k, updater(updatedItems[k])); + if (current) { + const updated = updater(current); + + if (!Types.equals(current, updated)) { + updateCount++; + updatedItems.set(k, updated); + } } } }, @@ -173,11 +182,11 @@ export class ImmutableMap { return new ImmutableMap(updatedItems); } - public equals(other: ImmutableMap) { + public equals(other: ImmutableMap) { if (!other) { return false; } - return Types.equalsObject(this.items, other.items); + return Types.equalsMap(this.items, other.items); } } \ No newline at end of file diff --git a/src/core/utils/immutable-set.spec.ts b/src/core/utils/immutable-set.spec.ts index 984f832..1515e05 100644 --- a/src/core/utils/immutable-set.spec.ts +++ b/src/core/utils/immutable-set.spec.ts @@ -39,14 +39,10 @@ describe('ImmutableSet', () => { expect(set_5.has('3')).toBeTruthy(); }); - it('should convert to aray', () => { + it('should convert to values', () => { const set_1 = ImmutableSet.of('a', 'b'); - const array = set_1.values; - - expect(array.length).toBe(2); - expect(array).toContain('a'); - expect(array).toContain('b'); + expect(set_1.values).toEqual(['a', 'b']); }); it('should return original set when item to add is null', () => { diff --git a/src/core/utils/immutable-set.ts b/src/core/utils/immutable-set.ts index 250788c..2d55166 100644 --- a/src/core/utils/immutable-set.ts +++ b/src/core/utils/immutable-set.ts @@ -6,112 +6,110 @@ * Copyright (c) Do Duc Quan. All rights reserved. */ -import { Types, without } from './types'; +import { Types } from './types'; -type Mutator = { - add: (item: string) => void; +type Mutator = { + add: (item: V) => void; - remove: (item: string) => void; + remove: (item: V) => void; }; -export class ImmutableSet { - private static readonly EMPTY = new ImmutableSet({}); +export class ImmutableSet { + private static readonly EMPTY = new ImmutableSet(new Set()); public get size() { - return Object.keys(this.items).length; + return this.items.size; } - public get values() { - return Object.keys(this.items); + public get values(): ReadonlyArray { + return Array.from(this.items.values()); } - public has(item: string) { - return this.items.hasOwnProperty(item); + public has(item: V) { + return this.items.has(item); } private constructor( - private readonly items: { [item: string]: boolean }, + private readonly items: Set, ) { Object.freeze(this); Object.freeze(items); } - public static empty(): ImmutableSet { + public static empty(): ImmutableSet { return ImmutableSet.EMPTY; } - public static of(...items: string[]): ImmutableSet { + public static of(...items: V[]): ImmutableSet { if (!items || items.length === 0) { return ImmutableSet.EMPTY; } else { - const itemMap: Record = {}; - - for (const item of items) { - itemMap[item] = true; - } - - return new ImmutableSet(itemMap); + return new ImmutableSet(new Set(items)); } } - public add(item: string): ImmutableSet { + public add(item: V): ImmutableSet { if (!item || this.has(item)) { return this; } - const items = { ...this.items, [item]: true }; + const items = new Set(this.items); + + items.add(item); return new ImmutableSet(items); } - public remove(item: string): ImmutableSet { + public remove(item: V): ImmutableSet { if (!item || !this.has(item)) { return this; } - const items = without(this.items, item); + const items = new Set(this.items); + + items.delete(item); return new ImmutableSet(items); } - public mutate(updater: (mutator: Mutator) => void) { - const items = { ...this.items }; - - let updated = false; + public mutate(updater: (mutator: Mutator) => void): ImmutableSet { + let updatedItems: Set | undefined = undefined; + let updateCount = 0; updater({ add: (k) => { if (k) { - if (!items.hasOwnProperty(k)) { - updated = true; + updatedItems ||= new Set(this.items); - items[k] = true; + if (!updatedItems.has(k)) { + updatedItems.add(k); + updateCount++; } } }, remove: (k) => { if (k) { - if (items.hasOwnProperty(k)) { - updated = true; + updatedItems ||= new Set(this.items); - delete items[k]; + if (updatedItems.delete(k)) { + updateCount++; } } }, }); - if (!updated) { + if (!updatedItems || updateCount === 0) { return this; } - return new ImmutableSet(items); + return new ImmutableSet(updatedItems); } - public equals(other: ImmutableSet) { + public equals(other: ImmutableSet) { if (!other) { return false; } - return Types.equalsObject(this.items, other.items); + return Types.equalsSet(this.items, other.items); } } diff --git a/src/core/utils/math-helper.ts b/src/core/utils/math-helper.ts index 9731e57..04c8838 100644 --- a/src/core/utils/math-helper.ts +++ b/src/core/utils/math-helper.ts @@ -15,15 +15,6 @@ export module MathHelper { return CURRENT_ID.toString(); } - export function nextCIId(isbegining:boolean, CURRENT_INSTANCE_ID?: any) { - if (isbegining ) { - CURRENT_INSTANCE_ID = 0; - } - CURRENT_INSTANCE_ID++; - - return CURRENT_INSTANCE_ID.toString(); - } - export function guid() { return `${s4() + s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; } diff --git a/src/core/utils/types.spec.ts b/src/core/utils/types.spec.ts index b90ba24..0fd4fad 100644 --- a/src/core/utils/types.spec.ts +++ b/src/core/utils/types.spec.ts @@ -61,6 +61,18 @@ describe('Types', () => { expect(Types.isObject([])).toBeFalsy(); }); + it('should make Map check', () => { + expect(Types.isMap(new Map())).toBeTruthy(); + + expect(Types.isMap({})).toBeFalsy(); + }); + + it('should make Set check', () => { + expect(Types.isSet(new Set())).toBeTruthy(); + + expect(Types.isSet({})).toBeFalsy(); + }); + it('should make RegExp check', () => { expect(Types.isRegExp(/[.*]/)).toBeTruthy(); @@ -122,7 +134,17 @@ describe('Types', () => { it('should compare objects', () => { expect(Types.equals({ a: 1, b: 2 }, { a: 2, b: 3 })).toBeFalsy(); - expect(Types.equals({ a: 1, b: 2 }, { a: 1, b: 2 })).toBeTruthy(); + expect(Types.equals({ a: 1, b: 2 }, { b: 2, a: 1 })).toBeTruthy(); + }); + + it('should compare maps', () => { + expect(Types.equals(new Map([['a', 1], ['b', 2]]), new Map([['a', 1], ['b', 3]]))).toBeFalsy(); + expect(Types.equals(new Map([['a', 1], ['b', 2]]), new Map([['b', 2], ['a', 1]]))).toBeTruthy(); + }); + + it('should compare sets', () => { + expect(Types.equals(new Set([1, 2]), new Set([2, 3]))).toBeFalsy(); + expect(Types.equals(new Set([1, 2]), new Set([2, 1]))).toBeTruthy(); }); it('should compare nested objects', () => { diff --git a/src/core/utils/types.ts b/src/core/utils/types.ts index c84cce4..30d7ed1 100644 --- a/src/core/utils/types.ts +++ b/src/core/utils/types.ts @@ -46,12 +46,20 @@ export module Types { return value && typeof value === 'object' && value.constructor === RegExp; } + export function isMap(value: any): value is Map { + return value instanceof Map; + } + + export function isSet(value: any): value is Set { + return value instanceof Set; + } + export function isDate(value: any): value is Date { return value instanceof Date; } - export function is(x: any, c: new (...args: any[]) => TClass): x is TClass { - return x instanceof c; + export function is(value: any, Constructor: new (...args: any[]) => TClass): value is TClass { + return value instanceof Constructor; } export function isArrayOfNumber(value: any): value is Array { @@ -107,6 +115,10 @@ export module Types { return equalsArray(lhs, rhs, options); } else if (Types.isObject(lhs) && Types.isObject(rhs)) { return equalsObject(lhs, rhs, options); + } else if (Types.isMap(lhs) && Types.isMap(rhs)) { + return equalsMap(lhs, rhs, options); + } else if (Types.isSet(lhs) && Types.isSet(rhs)) { + return equalsSet(lhs, rhs); } return false; @@ -142,17 +154,37 @@ export module Types { return true; } - export function isValueObject(value: any) { - return value && Types.isFunction(value.equals); + export function equalsMap(lhs: Map, rhs: Map, options?: EqualsOptions) { + if (lhs.size !== rhs.size) { + return false; + } + + for (const [key, value] of lhs) { + if (!equals(value, rhs.get(key), options)) { + return false; + } + } + + return true; } -} -type EqualsOptions = { lazyString?: boolean }; + export function equalsSet(lhs: Set, rhs: Set) { + if (lhs.size !== rhs.size) { + return false; + } -export function without(obj: { [key: string]: T }, key: string) { - const copy = { ...obj }; + for (const value of lhs) { + if (!rhs.has(value)) { + return false; + } + } - delete copy[key]; + return true; + } - return copy; + export function isValueObject(value: any) { + return value && Types.isFunction(value.equals); + } } + +type EqualsOptions = { lazyString?: boolean }; \ No newline at end of file diff --git a/src/core/utils/vec2.spec.ts b/src/core/utils/vec2.spec.ts index b7fa1ff..f378633 100644 --- a/src/core/utils/vec2.spec.ts +++ b/src/core/utils/vec2.spec.ts @@ -16,6 +16,8 @@ describe('Vec2', () => { expect(v.y).toBe(20); expect(v.toString()).toBe('(10, 20)'); + expect(v.getX()).toBe('10'); + expect(v.getY()).toBe('20'); }); it('should make valid equal comparisons', () => { diff --git a/src/index.tsx b/src/index.tsx index 9093ad7..7c3918e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,74 +8,22 @@ /* eslint-disable @typescript-eslint/indent */ -import { ConnectedRouter, connectRouter, routerMiddleware } from 'connected-react-router'; -import { createBrowserHistory } from 'history'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import * as ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; -import { Route } from 'react-router'; -import { applyMiddleware, combineReducers, compose, createStore } from 'redux'; -import thunk from 'redux-thunk'; -import { createInitialAssetsState, createInitialLoadingState, createInitialUIState, EditorState, selectDiagram, selectItems } from '@app/wireframes/model'; -import * as Reducers from '@app/wireframes/model/actions'; -import { registerRenderers } from '@app/wireframes/shapes'; +import { Route, Router } from 'react-router'; import { App } from './App'; import { registerServiceWorker } from './registerServiceWorker'; -import { createClassReducer } from './wireframes/model/actions/utils'; import './index.scss'; - -registerRenderers(); - -const editorState = EditorState.create(); - -const editorReducer = createClassReducer(editorState, builder => { - Reducers.buildAlignment(builder); - Reducers.buildAppearance(builder); - Reducers.buildDiagrams(builder); - Reducers.buildGrouping(builder); - Reducers.buildItems(builder); - Reducers.buildOrdering(builder); -}); - -const undoableReducer = Reducers.undoable( - editorReducer, - editorState, { - actionMerger: Reducers.mergeAction, - actionsToIgnore: [ - selectDiagram.name, - selectItems.name, - ], - }); - -const history = createBrowserHistory(); - -const composeEnhancers = (window as any)['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'] || compose; - -const store = createStore( - combineReducers({ - assets: Reducers.assets(createInitialAssetsState()), - editor: Reducers.rootLoading(undoableReducer, editorReducer), - loading: Reducers.loading(createInitialLoadingState()), - router: connectRouter(history), - ui: Reducers.ui(createInitialUIState()), - }), - composeEnhancers( - applyMiddleware( - thunk, - routerMiddleware(history), - Reducers.toastMiddleware(), - Reducers.loadingMiddleware(), - ), - ), -); +import { history, store } from './store'; const Root = ( - + - + ); diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..2679507 --- /dev/null +++ b/src/store.ts @@ -0,0 +1,60 @@ +/* + * codeslide.net + * + * @license + * Forked from mydraft.cc by Sebastian Stehle + * Copyright (c) Do Duc Quan. All rights reserved. +*/ + +import { configureStore } from '@reduxjs/toolkit'; +import { createBrowserHistory } from 'history'; +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import { assets, buildAlignment, buildAppearance, buildDiagrams, buildGrouping, buildItems, buildOrdering, createClassReducer, loading, loadingMiddleware, mergeAction, rootLoading, selectDiagram, selectItems, toastMiddleware, ui, undoable } from './wireframes/model/actions'; +import { EditorState } from './wireframes/model/editor-state'; +import { createInitialAssetsState, createInitialLoadingState, createInitialUIState } from './wireframes/model/internal'; +import { registerRenderers } from './wireframes/shapes'; + +registerRenderers(); + +const editorState = EditorState.create(); + +const editorReducer = createClassReducer(editorState, builder => { + buildAlignment(builder); + buildAppearance(builder); + buildDiagrams(builder); + buildGrouping(builder); + buildItems(builder); + buildOrdering(builder); +}); + +const undoableReducer = undoable( + editorReducer, + editorState, { + actionMerger: mergeAction, + actionsToIgnore: [ + selectDiagram.name, + selectItems.name, + ], + }); + +export const history = createBrowserHistory(); + +export const store = configureStore({ + reducer: { + // Contains the store for the left sidebar. + assets: assets(createInitialAssetsState()), + // Actual editor content. + editor: rootLoading(undoableReducer, editorReducer), + // Loading state, e.g. when something has been loaded. + loading: loading(createInitialLoadingState()), + // General UI behavior. + ui: ui(createInitialUIState()), + }, + middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false }).concat(toastMiddleware(), loadingMiddleware(history)), +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; + +export const useAppDispatch: () => AppDispatch = useDispatch; +export const useAppSelector: TypedUseSelectorHook = useSelector; \ No newline at end of file diff --git a/src/wireframes/components/AnimationView.tsx b/src/wireframes/components/AnimationView.tsx index 6010757..a010790 100644 --- a/src/wireframes/components/AnimationView.tsx +++ b/src/wireframes/components/AnimationView.tsx @@ -9,16 +9,16 @@ import Prism from 'prismjs'; import { useEffect, useState } from 'react'; import { getDiagram, useStore, changeScript } from "@app/wireframes/model"; -import { useDispatch } from "react-redux"; import { default as CodeEditor } from 'react-simple-code-editor'; import { vogues, texts } from '@app/const'; +import { useAppDispatch } from '@app/store'; import 'prismjs/components/prism-python'; import 'prismjs/themes/prism.css'; import './styles/AnimationView.scss'; export const AnimationView = () => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const diagram = useStore(getDiagram); const animation = useStore(s => s.ui.selectedAnimation); const isFooter = useStore(s => s.ui.footerSize) == vogues.common.previewHeight ? 1 : 0; diff --git a/src/wireframes/components/EditorView.tsx b/src/wireframes/components/EditorView.tsx index 78f2100..47f0a67 100644 --- a/src/wireframes/components/EditorView.tsx +++ b/src/wireframes/components/EditorView.tsx @@ -10,12 +10,15 @@ import * as React from 'react'; import { DropTargetMonitor, useDrop } from 'react-dnd'; import { NativeTypes } from 'react-dnd-html5-backend'; import { findDOMNode } from 'react-dom'; -import { useDispatch } from 'react-redux'; +import { Dropdown } from 'antd'; +import { useAppDispatch } from '@app/store'; import { loadImagesToClipboardItems, sizeInPx, useClipboard as useClipboardProvider, useEventCallback } from '@app/core'; -import { addShape, changeItemsAppearance, Diagram, getDiagram, getDiagramId, getEditor, getMasterDiagram, getSelectedItems, getSelectedItemsWithLocked, RendererService, selectItems, Transform, transformItems, useStore } from '@app/wireframes/model'; +import { addShape, changeItemsAppearance, Diagram, getDiagram, getDiagramId, getEditor, getMasterDiagram, getSelection, RendererService, selectItems, Transform, transformItems, useStore } from '@app/wireframes/model'; import { Editor } from '@app/wireframes/renderer/Editor'; +import { useContextMenu } from './menu'; import { DiagramRef, ItemsRef } from '../model/actions/utils'; import { ShapeSource } from '../interface'; + import './styles/EditorView.scss'; export interface EditorViewProps { @@ -36,7 +39,7 @@ export const EditorView = (props: EditorViewProps) => { }; export const EditorViewInner = ({ diagram, spacing }: EditorViewProps & { diagram: Diagram }) => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const editor = useStore(getEditor); const editorColor = editor.color; const editorSize = editor.size; @@ -47,6 +50,8 @@ export const EditorViewInner = ({ diagram, spacing }: EditorViewProps & { diagra const state = useStore(s => s); const zoom = useStore(s => s.ui.zoom); const zoomedSize = editorSize.mul(zoom); + const [menuVisible, setMenuVisible] = React.useState(false); + const contextMenu = useContextMenu(menuVisible); const doChangeItemsAppearance = useEventCallback((diagram: DiagramRef, visuals: ItemsRef, key: string, value: any) => { dispatch(changeItemsAppearance(diagram, visuals, key, value)); @@ -159,23 +164,24 @@ export const EditorViewInner = ({ diagram, spacing }: EditorViewProps & { diagra const padding = sizeInPx(spacing); return ( -
-
- + +
+
+ +
-
+ ); }; diff --git a/src/wireframes/components/PagesView.tsx b/src/wireframes/components/PagesView.tsx index c6c8bd0..776d3ce 100644 --- a/src/wireframes/components/PagesView.tsx +++ b/src/wireframes/components/PagesView.tsx @@ -7,10 +7,10 @@ */ import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'; -import { useDispatch } from 'react-redux'; import { useEventCallback, Vec2 } from '@app/core'; import { addDiagram, duplicateDiagram, getDiagramId, getFilteredDiagrams, moveDiagram, removeDiagram, selectDiagram, useStore } from '@app/wireframes/model'; import { vogues } from '@app/const'; +import { useAppDispatch } from '@app/store'; import { PageThumbnail, PageAdd, PageAction } from './menu'; import './styles/PagesView.scss'; @@ -26,7 +26,7 @@ export const PagesView = (props: PagesViewProps) => { const { prevWidth, prevHeight } = props; const viewSize = new Vec2(prevWidth, prevHeight); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const diagramId = useStore(getDiagramId); const diagrams = useStore(getFilteredDiagrams); diff --git a/src/wireframes/components/RecentView.tsx b/src/wireframes/components/RecentView.tsx index f07e03c..ae78943 100644 --- a/src/wireframes/components/RecentView.tsx +++ b/src/wireframes/components/RecentView.tsx @@ -8,15 +8,15 @@ import { Empty } from 'antd'; import * as React from 'react'; -import { useDispatch } from 'react-redux'; import { useEventCallback } from '@app/core'; import { texts } from '@app/const'; import { loadDiagramFromServer, RecentDiagram, useStore } from '@app/wireframes/model'; +import { useAppDispatch } from '@app/store'; import { RecentItem } from './menu'; import './styles/RecentView.scss'; export const RecentView = () => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const recent = useStore(x => x.loading.recentDiagrams); const doLoad = useEventCallback((item: RecentDiagram) => { diff --git a/src/wireframes/components/ShapeView.tsx b/src/wireframes/components/ShapeView.tsx index f4b044b..bf72762 100644 --- a/src/wireframes/components/ShapeView.tsx +++ b/src/wireframes/components/ShapeView.tsx @@ -5,12 +5,12 @@ * 8 Nov 2023 */ -import { useDispatch } from 'react-redux'; import { Button, Dropdown, Form, Input } from 'antd'; import type { MenuProps } from 'antd'; import { getDiagramId, useStore, addShape } from '@app/wireframes/model'; import * as React from 'react'; import { ArrowIcon, CircleIcon, FunctionIcon, ImageIcon, RectangleIcon, TableIcon, TextIcon, TriangleIcon, ShapesIcon, LinkIcon, HeadingIcon, SubHeadingIcon, ParagraphIcon, DiamondIcon, VectorIcon, LineUpIcon, BezierIcon } from '@app/icons/icon'; +import { useAppDispatch } from '@app/store'; import './styles/ShapeView.scss'; import { useState } from 'react'; import classNames from 'classnames'; @@ -18,7 +18,7 @@ import { FormModal, ShapeModal } from './modal/FormModal'; import { shapes } from '@app/const'; export const ShapeView = React.memo(() => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const selectedDiagramId = useStore(getDiagramId); const [selectedCell, setSelectedCell] = useState(0); const [isShapeModal, setIsShapeModal] = useState(''); diff --git a/src/wireframes/components/ToolView.tsx b/src/wireframes/components/ToolView.tsx index 8d89117..861d3f1 100644 --- a/src/wireframes/components/ToolView.tsx +++ b/src/wireframes/components/ToolView.tsx @@ -1,10 +1,10 @@ -import { DiagramItem, setAnimation } from '@app/wireframes/model'; +import { DiagramItem, DiagramItemSet, setAnimation } from '@app/wireframes/model'; import { ClipboardTool } from './tools/ClipboardTool'; import { TableTool } from './tools/TableTool'; import './styles/ToolView.scss'; import { AlignmentTool, GraphicTool, HistoryTool, LineTool, OrderingTool, TextTool, VisualTool, ZoomTool } from './tools'; import { Segmented } from 'antd'; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from '@app/store'; import { SegmentedValue } from "antd/es/segmented"; import { shapes } from '@app/const'; import { ModeType } from '../interface'; @@ -13,16 +13,17 @@ export interface ToolViewProps { // Application's mode mode: ModeType; - // Item - item: DiagramItem | null; - // Group - set: DiagramItem[] | null; + set: DiagramItemSet | null; } export const ToolView = (props: ToolViewProps) => { - const { item, set } = props; - const dispatch = useDispatch(); + const { set } = props; + const dispatch = useAppDispatch(); + + const item = set?.selectedItems[0]; + const isMultiItems = set != null && set.selection.size > 1; + const isSingleItem = !(!item) && item != null; const MoreTools = (props: {item: DiagramItem}) => { const renderer = props.item.renderer; @@ -91,10 +92,10 @@ export const ToolView = (props: ToolViewProps) => { - 1) || item != null} /> - { (item != null) && } - { (set != null && set.length > 1) && } - { (item != null) && } + + { isSingleItem && } + { isMultiItems && } + { isSingleItem && }
diff --git a/src/wireframes/components/actions/Components.tsx b/src/wireframes/components/actions/Components.tsx index 32245aa..51d3acc 100644 --- a/src/wireframes/components/actions/Components.tsx +++ b/src/wireframes/components/actions/Components.tsx @@ -8,9 +8,11 @@ import { Button, Tooltip } from 'antd'; import { ButtonProps } from 'antd/lib/button'; +import { MenuItemType } from 'antd/lib/menu/hooks/useItems'; import * as React from 'react'; import { isMac, Shortcut, Types } from '@app/core'; import { UIAction } from './shared'; +import Icon from '@ant-design/icons'; type ActionDisplayMode = 'Icon' | 'IconLabel' | 'Label'; @@ -103,6 +105,14 @@ const ButtonContent = ({ displayMode, label, icon }: { icon?: string | JSX.Eleme ); }; +function buildIcon(icon: string | JSX.Element | undefined, displayMode?: ActionDisplayMode) { + if (displayMode === 'Label') { + return null; + } + + return Types.isString(icon) ? ( } />) : icon; +} + function buildTitle(shortcut: string | undefined, tooltip: string) { function getModKey(): string { // Mac users expect to use the command key for shortcuts rather than the control key @@ -111,3 +121,22 @@ function buildTitle(shortcut: string | undefined, tooltip: string) { return shortcut ? `${tooltip} (${shortcut.replace('MOD', getModKey())})` : tooltip; } + +export function buildMenuItem(action: UIAction, key: string) { + const { + disabled, + label, + onAction, + icon, + } = action; + + const item: MenuItemType = { + key, + disabled, + label, + onClick: onAction, + icon: buildIcon(icon), + }; + + return item; +} diff --git a/src/wireframes/components/actions/shared.ts b/src/wireframes/components/actions/shared.ts index 6607797..d45246e 100644 --- a/src/wireframes/components/actions/shared.ts +++ b/src/wireframes/components/actions/shared.ts @@ -10,7 +10,7 @@ /* eslint-disable one-var-declaration-per-line */ import * as React from 'react'; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from '@app/store'; import { Color, Types, useEventCallback } from '@app/core'; import { changeItemsAppearance, DiagramItemSet } from '@app/wireframes/model'; @@ -39,6 +39,10 @@ export type UniqueConverter = { parse: (value: any) => TInput; write: (v const DEFAULT_CONVERTER = { parse: (value: any) => { + if (value === 'undefined') { + return undefined!; + } + return value; }, write: (value: any) => { @@ -48,6 +52,10 @@ const DEFAULT_CONVERTER = { const COLOR_CONVERTER: UniqueConverter = { parse: (value: any) => { + if (value === 'undefined') { + return undefined!; + } + return Color.fromValue(value); }, write: (value: Color) => { @@ -69,7 +77,7 @@ export function useAppearance(selectedDiagramId: RefDiagramId, selectedSet: R } export function useAppearanceCore(selectedDiagramId: RefDiagramId, selectedSet: RefDiagramItemSet, key: string, converter: UniqueConverter, allowUndefined = false, force = false): Result { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const value = React.useMemo(() => { if (!selectedSet) { @@ -78,8 +86,12 @@ export function useAppearanceCore(selectedDiagramId: RefDiagramId, selectedSe let value: T | undefined, empty = true; - for (const shape of selectedSet.allShapes) { - const appearance = shape.appearance.get(key); + for (const item of selectedSet.nested.values()) { + if (item.type === 'Group') { + continue; + } + + const appearance = item.appearance.get(key); if (!Types.isUndefined(appearance) || allowUndefined) { empty = false; @@ -99,7 +111,7 @@ export function useAppearanceCore(selectedDiagramId: RefDiagramId, selectedSe const doChangeAppearance = useEventCallback((value: T) => { if (selectedDiagramId && selectedSet) { - dispatch(changeItemsAppearance(selectedDiagramId, selectedSet.allShapes, key, converter.write(value), force)); + dispatch(changeItemsAppearance(selectedDiagramId, selectedSet.deepEditableItems, key, converter.write(value), force)); } }); diff --git a/src/wireframes/components/actions/use-alignment.ts b/src/wireframes/components/actions/use-alignment.ts index 25d3256..7e7fc0c 100644 --- a/src/wireframes/components/actions/use-alignment.ts +++ b/src/wireframes/components/actions/use-alignment.ts @@ -9,29 +9,29 @@ /* eslint-disable react-hooks/exhaustive-deps */ import * as React from 'react'; -import { useDispatch } from 'react-redux'; import { useEventCallback } from '@app/core'; import { texts } from '@app/const'; -import { alignItems, AlignmentMode, getDiagramId, getSelectedItems, orderItems, OrderMode, useStore } from '@app/wireframes/model'; +import { useAppDispatch } from '@app/store'; +import { alignItems, AlignmentMode, getDiagramId, getSelection, orderItems, OrderMode, useStore } from '@app/wireframes/model'; import { UIAction } from './shared'; export function useAlignment() { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const selectedDiagramId = useStore(getDiagramId); - const selectedItems = useStore(getSelectedItems); - const canAlign = selectedItems.length > 1; - const canDistribute = selectedItems.length > 2; - const canOrder = selectedItems.length > 0; + const selectedItems = useStore(getSelection); + const canAlign = selectedItems.selection.size > 1; + const canDistribute = selectedItems.selection.size > 2; + const canOrder = selectedItems.selection.size > 0; const doAlign = useEventCallback((mode: AlignmentMode) => { if (selectedDiagramId) { - dispatch(alignItems(mode, selectedDiagramId, selectedItems)); + dispatch(alignItems(mode, selectedDiagramId, selectedItems.selectedItems)); } }); const doOrder = useEventCallback((mode: OrderMode) => { if (selectedDiagramId) { - dispatch(orderItems(mode, selectedDiagramId, selectedItems)); + dispatch(orderItems(mode, selectedDiagramId, selectedItems.selectedItems)); } }); diff --git a/src/wireframes/components/actions/use-clipboard.ts b/src/wireframes/components/actions/use-clipboard.ts index cb3f7b5..7e10d76 100644 --- a/src/wireframes/components/actions/use-clipboard.ts +++ b/src/wireframes/components/actions/use-clipboard.ts @@ -7,20 +7,20 @@ */ import * as React from 'react'; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from '@app/store'; import { ClipboardCopyEvent, ClipboardPasteEvent, useClipboard as useClipboardProvider } from '@app/core'; import { texts, vogues } from '@app/const'; -import { DiagramItemSet, getDiagram, getSelectedItems, pasteItems, removeItems, Serializer, useStore } from '@app/wireframes/model'; +import { getDiagram, getSelection, pasteItems, removeItems, Serializer, useStore } from '@app/wireframes/model'; import { UIAction } from './shared'; const prefix = `${texts.common.prefix}:`; export function useClipboard() { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const offset = React.useRef(0); const selectedDiagram = useStore(getDiagram); - const selectedItems = useStore(getSelectedItems); - const canCopy = selectedItems.length > 0; + const selectedItems = useStore(getSelection); + const canCopy = selectedItems.selection.size > 0; const clipboard = useClipboardProvider({ onPaste: (event: ClipboardPasteEvent) => { @@ -37,15 +37,10 @@ export function useClipboard() { }, onCopy: (event: ClipboardCopyEvent) => { if (selectedDiagram) { - const set = - DiagramItemSet.createFromDiagram( - selectedItems, - selectedDiagram); - - event.clipboard.set(`${prefix}${JSON.stringify(Serializer.serializeSet(set))}`); + event.clipboard.set(`${prefix}${JSON.stringify(Serializer.serializeSet(selectedItems))}`); if (event.isCut) { - dispatch(removeItems(selectedDiagram, selectedItems)); + dispatch(removeItems(selectedDiagram, selectedItems.selectedItems)); } offset.current = 0; diff --git a/src/wireframes/components/actions/use-grouping.ts b/src/wireframes/components/actions/use-grouping.ts index 5e130f5..377a5ac 100644 --- a/src/wireframes/components/actions/use-grouping.ts +++ b/src/wireframes/components/actions/use-grouping.ts @@ -7,26 +7,26 @@ */ import * as React from 'react'; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from '@app/store'; import { IDHelper } from '@app/core'; import { useEventCallback } from '@app/core'; import { texts } from '@app/const'; -import { getDiagram, getSelectedGroups, getSelectedItems, groupItems, ungroupItems, useStore } from '@app/wireframes/model'; +import { getDiagram, getSelection, groupItems, ungroupItems, useStore } from '@app/wireframes/model'; import { keys } from '@app/const'; import { UIAction } from './shared'; export function useGrouping() { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const selectedDiagram = useStore(getDiagram); const selectedDiagramId = selectedDiagram?.id; - const selectedGroups = useStore(getSelectedGroups); - const selectedItems = useStore(getSelectedItems); - const canGroup = selectedItems.length > 1; + const selectedItems = useStore(getSelection); + const selectedGroups = React.useMemo(() => selectedItems.selectedItems.filter(x => x.type === 'Group'), [selectedItems]); + const canGroup = selectedItems.selectedItems.length > 1; const canUngroup = selectedGroups.length > 0; const doGroup = useEventCallback(() => { if (selectedDiagramId) { - dispatch(groupItems(selectedDiagramId, selectedItems, IDHelper.nextId(selectedDiagram, 'Group').id)); + dispatch(groupItems(selectedDiagramId, selectedItems.selectedItems, IDHelper.nextId(selectedDiagram, 'Group').id)); } }); diff --git a/src/wireframes/components/actions/use-history.ts b/src/wireframes/components/actions/use-history.ts index 4201c21..5627c52 100644 --- a/src/wireframes/components/actions/use-history.ts +++ b/src/wireframes/components/actions/use-history.ts @@ -7,7 +7,7 @@ */ import * as React from 'react'; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from '@app/store'; import { useEventCallback } from '@app/core'; import { texts } from '@app/const'; import { redo, undo, useStore } from '@app/wireframes/model'; @@ -15,7 +15,7 @@ import { keys } from '@app/const'; import { UIAction } from './shared'; export function useHistory() { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const canRedo = useStore(s => s.editor.canRedo); const canUndo = useStore(s => s.editor.canUndo); diff --git a/src/wireframes/components/actions/use-loading.ts b/src/wireframes/components/actions/use-loading.ts index 4dca563..604a5a0 100644 --- a/src/wireframes/components/actions/use-loading.ts +++ b/src/wireframes/components/actions/use-loading.ts @@ -7,7 +7,7 @@ */ import * as React from 'react'; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from '@app/store'; import { useEventCallback, useOpenFile } from '@app/core'; import { texts } from '@app/const'; import { downloadDiagramToFile, getDiagrams, loadDiagramFromFile, newDiagram, useStore } from '@app/wireframes/model'; @@ -15,7 +15,7 @@ import { keys } from '@app/const'; import { UIAction } from './shared'; export function useLoading() { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const diagrams = useStore(getDiagrams); const canSave = React.useMemo(() => { diff --git a/src/wireframes/components/actions/use-remove.ts b/src/wireframes/components/actions/use-remove.ts index f92f9fe..2cb3ad8 100644 --- a/src/wireframes/components/actions/use-remove.ts +++ b/src/wireframes/components/actions/use-remove.ts @@ -7,21 +7,21 @@ */ import * as React from 'react'; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from '@app/store'; import { useEventCallback } from '@app/core'; import { texts } from '@app/const'; -import { getDiagramId, getSelectedItems, removeItems, useStore } from '@app/wireframes/model'; +import { getDiagramId, getSelection, removeItems, useStore } from '@app/wireframes/model'; import { UIAction } from './shared'; export function useRemove() { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const selectedDiagramId = useStore(getDiagramId); - const selectedItems = useStore(getSelectedItems); - const canRemove = selectedItems.length > 0; + const selectedItems = useStore(getSelection); + const canRemove = selectedItems.selectedItems.length > 0; const doRemove = useEventCallback(() => { if (selectedDiagramId) { - dispatch(removeItems(selectedDiagramId, selectedItems)); + dispatch(removeItems(selectedDiagramId, selectedItems.selectedItems)); } }); diff --git a/src/wireframes/components/actions/use-server.ts b/src/wireframes/components/actions/use-server.ts index 36885a6..41308bf 100644 --- a/src/wireframes/components/actions/use-server.ts +++ b/src/wireframes/components/actions/use-server.ts @@ -5,10 +5,10 @@ import * as svg from '@svgdotjs/svg.js'; import { Color } from "@app/core/utils/color"; import { shapes } from "@app/const"; import { MessageInstance } from "antd/es/message/interface"; -import { useDispatch } from "react-redux"; +import { useAppDispatch } from '@app/store'; export function useServer() { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const diagrams = useStore(getFilteredDiagrams); const editor = useStore(getEditor); diff --git a/src/wireframes/components/headers/ArrangeHeader.tsx b/src/wireframes/components/headers/ArrangeHeader.tsx index d815da6..a3c42fe 100644 --- a/src/wireframes/components/headers/ArrangeHeader.tsx +++ b/src/wireframes/components/headers/ArrangeHeader.tsx @@ -7,14 +7,14 @@ */ import * as React from 'react'; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from '@app/store'; import { Shortcut, useEventCallback } from '@app/core'; import { calculateSelection, getDiagram, selectItems, useStore } from '@app/wireframes/model'; import { keys } from '@app/const'; import { useRemove } from '../actions'; export const ArrangeHeader = React.memo(() => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const forRemove = useRemove(); const selectedDiagram = useStore(getDiagram); diff --git a/src/wireframes/components/headers/FileHeader.tsx b/src/wireframes/components/headers/FileHeader.tsx index 7cb8e94..926a407 100644 --- a/src/wireframes/components/headers/FileHeader.tsx +++ b/src/wireframes/components/headers/FileHeader.tsx @@ -11,13 +11,13 @@ import { Button, Dropdown, Form, Input, message } from 'antd'; import { useEffect, useState } from 'react'; import { useLoading, useServer } from '../actions'; import { texts } from '@app/const/texts'; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from '@app/store'; import { FormModal, SettingModal } from '../modal'; import type { MenuProps } from 'antd'; import { MenuIcon } from '@app/style/icomoon/icomoon_icon'; export const FileHeader = () => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const forLoading = useLoading(); const forServer = useServer(); const editor = useStore(getEditor); @@ -120,7 +120,7 @@ export const FileHeader = () => { } else if (key == forLoading.downloadDiagram.label) { dispatch(forLoading.downloadDiagram.onAction); } else if (key == texts.common.saveDiagramToFileTooltip) { - dispatch(forServer.pdf(messageApi, messageKey)); + forServer.pdf(messageApi, messageKey); } else if (key == texts.common.documentation) { window.open('https://github.com/code-slide/ui/wiki'); } else if (key == texts.common.walkthrough) { diff --git a/src/wireframes/components/headers/IdHeader.tsx b/src/wireframes/components/headers/IdHeader.tsx index 7819455..0e2dacb 100644 --- a/src/wireframes/components/headers/IdHeader.tsx +++ b/src/wireframes/components/headers/IdHeader.tsx @@ -1,16 +1,16 @@ import * as React from 'react'; -import { getDiagram, getSelectedItems, replaceId, useStore } from '@app/wireframes/model'; +import { getDiagram, getSelection, replaceId, useStore } from '@app/wireframes/model'; import { Button, Input, Space, message } from "antd"; import { useState } from "react"; import '../styles/HeaderView.scss' -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from '@app/store'; import { CheckOutlined } from '@ant-design/icons'; export const IdHeader = () => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const diagram = useStore(getDiagram); - const [ selectedItem ] = useStore(getSelectedItems); - const id = !selectedItem ? '' : selectedItem.id; + const selectedItems = useStore(getSelection); + const id = (!selectedItems || selectedItems.selection.size != 1) ? '' : selectedItems.selectedItems[0].id; const [newId, setNewId] = useState(id); const [isUpdate, setIsUpdate] = useState(false); const [messageApi, contextHolder] = message.useMessage(); @@ -43,7 +43,7 @@ export const IdHeader = () => { cancelUpdateId(); }, [id]); - if (!selectedItem) return <>; + if (!selectedItems || selectedItems.selection.size != 1) return <>; return ( <> {contextHolder} diff --git a/src/wireframes/components/headers/ModeHeader.tsx b/src/wireframes/components/headers/ModeHeader.tsx index 9c0b561..0b67419 100644 --- a/src/wireframes/components/headers/ModeHeader.tsx +++ b/src/wireframes/components/headers/ModeHeader.tsx @@ -10,11 +10,11 @@ import { Button, Tooltip } from "antd"; import * as React from "react"; import { setFooterSize, setMode, setSidebarSize, useStore } from "@app/wireframes/model"; import { AnimationIcon, PageIcon, IconOutline } from "@app/icons/icon"; -import { useDispatch } from "react-redux"; +import { useAppDispatch } from '@app/store'; import { vogues } from "@app/const"; export const ModeHeader = React.memo(() => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const isAnimationOn = useStore(s => s.ui.sidebarSize) !== vogues.common.close; const isPageOn = useStore(s => s.ui.footerSize) !== vogues.common.close; diff --git a/src/wireframes/components/headers/PresentHeader.tsx b/src/wireframes/components/headers/PresentHeader.tsx index f3df387..e147d5f 100644 --- a/src/wireframes/components/headers/PresentHeader.tsx +++ b/src/wireframes/components/headers/PresentHeader.tsx @@ -11,10 +11,8 @@ import { Button, message } from "antd"; import * as React from "react"; import { useState } from "react"; import { useServer } from "../actions"; -import { useDispatch } from "react-redux"; export const PresentHeader = React.memo(() => { - const dispatch = useDispatch(); const forServer = useServer(); const [messageApi, contextHolder] = message.useMessage(); const [loading, setLoading] = useState(false); @@ -28,7 +26,7 @@ export const PresentHeader = React.memo(() => { onClick={() => { setLoading(true); try { - dispatch(forServer.slide(messageApi, messageKey)); + forServer.slide(messageApi, messageKey); } finally { setLoading(false) } diff --git a/src/wireframes/components/menu/ContextMenu.tsx b/src/wireframes/components/menu/ContextMenu.tsx new file mode 100644 index 0000000..66ded57 --- /dev/null +++ b/src/wireframes/components/menu/ContextMenu.tsx @@ -0,0 +1,59 @@ +/* + * mydraft.cc + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved. +*/ + +import { MenuProps } from 'antd/lib'; +import { texts } from '@app/const'; +import { buildMenuItem, useAlignment, useClipboard, useRemove } from '../actions'; + +export const useContextMenu = (isOpen: boolean) => { + const forAlignment = useAlignment(); + const forClipboard = useClipboard(); + const forRemove = useRemove(); + + if (!isOpen) { + return DEFAULT_MENU; + } + + const items: MenuProps['items'] = [ + buildMenuItem(forClipboard.cut, 'clipboardCut'), + buildMenuItem(forClipboard.copy, 'clipboardCopy'), + buildMenuItem(forClipboard.paste, 'clipboarPaste'), + { type: 'divider' }, + buildMenuItem(forRemove.remove, 'remove'), + { + label: texts.common.alignment, + children: [ + buildMenuItem(forAlignment.alignHorizontalLeft, 'alignHorizontalLeft'), + buildMenuItem(forAlignment.alignHorizontalCenter, 'alignHorizontalCenter'), + buildMenuItem(forAlignment.alignHorizontalRight, 'alignHorizontalRight'), + + buildMenuItem(forAlignment.alignVerticalTop, 'alignVerticalTop'), + buildMenuItem(forAlignment.alignVerticalCenter, 'alignVerticalCenter'), + buildMenuItem(forAlignment.alignVerticalBottom, 'alignVerticalBottom'), + + buildMenuItem(forAlignment.distributeHorizontally, 'distributeHorizontally'), + buildMenuItem(forAlignment.distributeVertically, 'distributeVertically'), + + ], + key: 'alignment', + }, + { + label: texts.common.ordering, + children: [ + buildMenuItem(forAlignment.bringToFront, 'bringToFront'), + buildMenuItem(forAlignment.bringForwards, 'bringForwards'), + buildMenuItem(forAlignment.sendBackwards, 'sendBackwards'), + buildMenuItem(forAlignment.sendToBack, 'sendToBack'), + ], + key: 'layers', + }, + ]; + + return { items, mode: 'vertical' } as MenuProps; +}; + +const DEFAULT_MENU: MenuProps = { items: [], mode: 'vertical' }; \ No newline at end of file diff --git a/src/wireframes/components/menu/index.ts b/src/wireframes/components/menu/index.ts index 64992f8..e3afbd3 100644 --- a/src/wireframes/components/menu/index.ts +++ b/src/wireframes/components/menu/index.ts @@ -1,2 +1,3 @@ +export * from './ContextMenu'; export * from './PageMenu'; export * from './RecentMenu'; \ No newline at end of file diff --git a/src/wireframes/components/settings/ColorSetting.tsx b/src/wireframes/components/settings/ColorSetting.tsx index ea6c556..3a82945 100644 --- a/src/wireframes/components/settings/ColorSetting.tsx +++ b/src/wireframes/components/settings/ColorSetting.tsx @@ -7,12 +7,12 @@ */ import * as React from 'react'; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from '@app/store'; import { Color, ColorPicker } from '@app/core'; import { changeColors, getColors, useStore } from '@app/wireframes/model'; export const ColorSetting = () => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const recentColors = useStore(getColors); const doChangeColor = React.useCallback((oldColor: Color, newColor: Color) => { diff --git a/src/wireframes/components/settings/DiagramSetting.tsx b/src/wireframes/components/settings/DiagramSetting.tsx index e4a3c45..add2182 100644 --- a/src/wireframes/components/settings/DiagramSetting.tsx +++ b/src/wireframes/components/settings/DiagramSetting.tsx @@ -8,13 +8,13 @@ import { Col, InputNumber, Row } from 'antd'; import * as React from 'react'; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from '@app/store'; import { Color, ColorPicker, useEventCallback } from '@app/core'; import { texts } from '@app/const'; import { changeColor, changeSize, getColors, getEditor, useStore } from '@app/wireframes/model'; export const DiagramSetting = React.memo(() => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const editor = useStore(getEditor); const editorSize = editor.size; const editorColor = editor.color; diff --git a/src/wireframes/components/settings/PresentSetting.tsx b/src/wireframes/components/settings/PresentSetting.tsx index 57afb52..3d96d53 100644 --- a/src/wireframes/components/settings/PresentSetting.tsx +++ b/src/wireframes/components/settings/PresentSetting.tsx @@ -9,7 +9,7 @@ import * as React from 'react'; import Prism from 'prismjs'; import { useState, useEffect } from 'react'; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from '@app/store'; import { default as RevealEditor } from 'react-simple-code-editor'; import { changeRevealConfig, getEditor, useStore } from '@app/wireframes/model'; @@ -18,7 +18,7 @@ import { Button, Space, message } from 'antd'; import { vogues } from '@app/const'; export const PresentSetting = React.memo(() => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const editor = useStore(getEditor); const [revealScr, setRevealScr] = useState(''); const [isScrChange, setIsScrChange] = useState(false); diff --git a/src/wireframes/components/styles/EditorView.scss b/src/wireframes/components/styles/EditorView.scss index ee4eee4..b1fb1e3 100644 --- a/src/wireframes/components/styles/EditorView.scss +++ b/src/wireframes/components/styles/EditorView.scss @@ -12,5 +12,6 @@ &-diagram { margin: auto; + user-select: none; } } \ No newline at end of file diff --git a/src/wireframes/components/tools/GraphicTool.tsx b/src/wireframes/components/tools/GraphicTool.tsx index b17b421..d95aae6 100644 --- a/src/wireframes/components/tools/GraphicTool.tsx +++ b/src/wireframes/components/tools/GraphicTool.tsx @@ -8,8 +8,8 @@ import { Button, Form, Input, Tooltip } from 'antd'; import * as React from 'react'; -import { useDispatch } from 'react-redux'; -import { Diagram, changeItemsAppearance, getDiagram, getSelectedShape, useStore } from '@app/wireframes/model'; +import { useAppDispatch } from '@app/store'; +import { Diagram, changeItemsAppearance, getDiagram, useStore, getSelection } from '@app/wireframes/model'; import { useEffect, useState } from 'react'; import { AspectRatioIcon, IconOutline, LinkIcon, VectorIcon } from '@app/icons/icon'; import { texts, shapes } from '@app/const'; @@ -27,10 +27,13 @@ interface ChangeProps { } export const GraphicTool = React.memo(() => { - const dispatch = useDispatch(); - const selectedItem = useStore(getSelectedShape); + const dispatch = useAppDispatch(); + const selectedItems = useStore(getSelection); const selectedDiagram = useStore(getDiagram); + if (selectedItems.selection.size != 1) return; + + const selectedItem = selectedItems.selectedItems[0]; const [isKeepAspect, setIsKeepAspect] = useState(!selectedItem ? true : selectedItem.getAppearance(shapes.key.aspectRatio)); useEffect(() => { @@ -60,7 +63,7 @@ export const GraphicTool = React.memo(() => { }); const SourceTool = (props: ChangeProps) => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const [isShapeModal, setIsShapeModal] = useState(''); const handleChange = (key: string, value: string) => { diff --git a/src/wireframes/components/tools/LineTool.tsx b/src/wireframes/components/tools/LineTool.tsx index 3e97d53..d433065 100644 --- a/src/wireframes/components/tools/LineTool.tsx +++ b/src/wireframes/components/tools/LineTool.tsx @@ -8,7 +8,7 @@ import { Button, Tooltip } from 'antd'; import * as React from 'react'; -import { getDiagramId, getSelectionSet, useStore } from '@app/wireframes/model'; +import { getDiagramId, getSelection, useStore } from '@app/wireframes/model'; import { useAppearance } from '../actions'; import { IconOutline, CurveBotLeftIcon, CurveUpRightIcon, LineDownIcon, LineEndArrowIcon, LineEndNoneIcon, LineEndTriangleIcon, LineStartArrowIcon, LineStartNoneIcon, LineStartTriangleIcon, LineUpIcon, CurveBotRightIcon, CurveUpLeftIcon } from '@app/icons/icon'; import { shapes } from '@app/const'; @@ -16,7 +16,7 @@ import { LineCurve, LineEdge, LineNode, LinePivot } from '@app/wireframes/interf export const LineTool = React.memo((props: {lineType: LineEdge}) => { const selectedDiagramId = useStore(getDiagramId); - const selectedSet = useStore(getSelectionSet); + const selectedSet = useStore(getSelection); const [lineStart, setLineStart] = useAppearance(selectedDiagramId, selectedSet, shapes.key.lineStart); const [lineEnd, setLineEnd] = useAppearance(selectedDiagramId, selectedSet, shapes.key.lineEnd); diff --git a/src/wireframes/components/tools/TableTool.tsx b/src/wireframes/components/tools/TableTool.tsx index 06f1048..8e5dc85 100644 --- a/src/wireframes/components/tools/TableTool.tsx +++ b/src/wireframes/components/tools/TableTool.tsx @@ -9,17 +9,20 @@ import { DeleteColumnOutlined, DeleteRowOutlined, InsertRowAboveOutlined, InsertRowBelowOutlined, InsertRowLeftOutlined, InsertRowRightOutlined } from '@ant-design/icons'; import { useEventCallback } from '@app/core'; import { Button, Tooltip } from 'antd'; -import { useStore, getSelectedShape, changeItemsAppearance, getDiagram } from '@app/wireframes/model'; +import { useStore, getSelection, changeItemsAppearance, getDiagram } from '@app/wireframes/model'; import * as React from 'react'; import { getAddToTable, getRemoveFromTable } from '@app/wireframes/shapes/neutral/table'; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from '@app/store'; import { texts, shapes } from '@app/const'; export const TableTool = React.memo(() => { - const dispatch = useDispatch(); - const selectedItem = useStore(getSelectedShape); + const dispatch = useAppDispatch(); + const selectedItems = useStore(getSelection); const selectedDiagram = useStore(getDiagram); + if (selectedItems.selection.size !== 1) return; + const selectedItem = useStore(getSelection).selectedItems[0]; + const modifyTable = useEventCallback((mode: string, type: string) => { if (selectedItem && selectedDiagram) { let delimiter: string; diff --git a/src/wireframes/components/tools/TextTool.tsx b/src/wireframes/components/tools/TextTool.tsx index 5091f7a..fb72888 100644 --- a/src/wireframes/components/tools/TextTool.tsx +++ b/src/wireframes/components/tools/TextTool.tsx @@ -8,19 +8,19 @@ import { Button, Dropdown, InputNumber, Tooltip } from 'antd'; import * as React from 'react'; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from '@app/store'; import { ColorPicker, useEventCallback } from '@app/core'; -import { getColors, getDiagramId, getSelectionSet, selectColorTab, useStore } from '@app/wireframes/model'; +import { getColors, getDiagramId, getSelection, selectColorTab, useStore } from '@app/wireframes/model'; import { useAppearance, useColorAppearance } from '../actions'; import { ColorTextFill, IconOutline } from '@app/icons/icon'; import { vogues, shapes } from '@app/const'; export const TextTool = React.memo(() => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const recentColors = useStore(getColors); const selectedColorTab = useStore(s => s.ui.selectedColor as any); const selectedDiagramId = useStore(getDiagramId); - const selectedSet = useStore(getSelectionSet); + const selectedSet = useStore(getSelection); const [fontSize, setFontSize] = useAppearance(selectedDiagramId, selectedSet, diff --git a/src/wireframes/components/tools/VisualTool.tsx b/src/wireframes/components/tools/VisualTool.tsx index 241e732..fbe7200 100644 --- a/src/wireframes/components/tools/VisualTool.tsx +++ b/src/wireframes/components/tools/VisualTool.tsx @@ -8,19 +8,19 @@ import { Button, Dropdown, Tooltip } from 'antd'; import * as React from 'react'; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from '@app/store'; import { ColorPicker, useEventCallback } from '@app/core'; -import { getColors, getDiagramId, getSelectionSet, selectColorTab, useStore } from '@app/wireframes/model'; +import { getColors, getDiagramId, getSelection, selectColorTab, useStore } from '@app/wireframes/model'; import { useAppearance, useColorAppearance } from '../actions'; import { BorderWidthIcon, ColorBackgroundFill, ColorBorderFill, IconOutline } from '@app/icons/icon'; import { vogues, shapes } from '@app/const'; export const VisualTool = React.memo(() => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const recentColors = useStore(getColors); const selectedColorTab = useStore(s => s.ui.selectedColor as any); const selectedDiagramId = useStore(getDiagramId); - const selectedSet = useStore(getSelectionSet); + const selectedSet = useStore(getSelection); const [backgroundColor, setBackgroundColor] = useColorAppearance(selectedDiagramId, selectedSet, diff --git a/src/wireframes/components/tools/ZoomTool.tsx b/src/wireframes/components/tools/ZoomTool.tsx index 1805094..35c609a 100644 --- a/src/wireframes/components/tools/ZoomTool.tsx +++ b/src/wireframes/components/tools/ZoomTool.tsx @@ -8,15 +8,15 @@ import * as React from 'react'; import { useEffect, useState } from 'react'; -import { useDispatch } from 'react-redux'; import { useStore, getEditor, setZoom } from '@app/wireframes/model'; import { Button, Dropdown } from 'antd'; import { Vec2 } from '@app/core'; import { vogues } from '@app/const'; +import { useAppDispatch } from '@app/store'; import type { MenuProps } from 'antd'; export const ZoomTool = React.memo(() => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const editorSize = useStore(getEditor).size; const sidebarWidth = useStore(s => s.ui.sidebarSize); const isFooter = useStore(s => s.ui.footerSize) == vogues.common.previewHeight ? 1 : 0; diff --git a/src/wireframes/model/actions/api.ts b/src/wireframes/model/actions/api.ts index 587d7df..0dbc8be 100644 --- a/src/wireframes/model/actions/api.ts +++ b/src/wireframes/model/actions/api.ts @@ -43,7 +43,7 @@ export const parseFrames = async (script: string) => { const response = await fetch(`${SERVER_URL}/parser`, { method: 'POST', headers: { - 'Content-type': 'application/json; charset=UTF-8', + ['Content-Type']: 'application/json; charset=UTF-8', }, body: JSON.stringify({ script: script @@ -61,7 +61,7 @@ export const compileSlides = async (fileName: string, title: string, size: numbe const response = await fetch(`${SERVER_URL}/compiler`, { method: 'POST', headers: { - 'Content-type': 'application/json; charset=UTF-8', + ['Content-Type']: 'application/json; charset=UTF-8', }, body: JSON.stringify({ fileName: fileName, diff --git a/src/wireframes/model/actions/appearance.ts b/src/wireframes/model/actions/appearance.ts index 0dcfabb..440434b 100644 --- a/src/wireframes/model/actions/appearance.ts +++ b/src/wireframes/model/actions/appearance.ts @@ -8,7 +8,7 @@ import { ActionReducerMapBuilder, createAction } from '@reduxjs/toolkit'; import { Color, Types } from '@app/core/utils'; -import { DiagramItemSet, EditorState, RendererService, Transform } from './../internal'; +import { EditorState, RendererService, Transform } from './../internal'; import { createItemsAction, DiagramRef, ItemsRef } from './utils'; export const changeColors = @@ -41,7 +41,7 @@ export function buildAppearance(builder: ActionReducerMapBuilder) { } const appearance = item.appearance.mutate(mutator => { - for (const [key, value] of Object.entries(item.appearance.raw)) { + for (const [key, value] of item.appearance.entries) { if (key.endsWith('COLOR')) { const parsedColor = Color.fromValue(value); @@ -62,9 +62,7 @@ export function buildAppearance(builder: ActionReducerMapBuilder) { return state.updateDiagram(diagramId, diagram => { const { key, value } = appearance; - const set = DiagramItemSet.createFromDiagram(itemIds, diagram); - - return diagram.updateItems(set.allShapes.map(x => x.id), item => { + return diagram.updateItems(itemIds, item => { const rendererInstance = RendererService.get(item.renderer); if (!rendererInstance) { @@ -84,11 +82,9 @@ export function buildAppearance(builder: ActionReducerMapBuilder) { return state.updateDiagram(diagramId, diagram => { const boundsOld = Transform.fromJS(action.payload.oldBounds); - const boundsNew = Transform.fromJS(action.payload.newBounds); - - const set = DiagramItemSet.createFromDiagram(itemIds, diagram); + const boundsNew = Transform.fromJS(action.payload.newBounds);; - return diagram.updateItems(set.allItems.map(x => x.id), item => { + return diagram.updateItems(itemIds, item => { return item.transformByBounds(boundsOld, boundsNew); }); }); diff --git a/src/wireframes/model/actions/grouping.spec.ts b/src/wireframes/model/actions/grouping.spec.ts index 485e0df..e0d113d 100644 --- a/src/wireframes/model/actions/grouping.spec.ts +++ b/src/wireframes/model/actions/grouping.spec.ts @@ -79,9 +79,7 @@ describe('GroupingReducer', () => { const newDiagram = state_2.diagrams.get(diagram.id)!; - const ids = shapeGroup.keys; - - expect(newDiagram.selectedIds.values).toEqual(ids); - expect(newDiagram.rootIds.values).toEqual(ids); + expect(newDiagram.selectedIds.values).toEqual(shapeGroup.keys); + expect(newDiagram.rootIds.values).toEqual(shapeGroup.keys); }); -}); +}); \ No newline at end of file diff --git a/src/wireframes/model/actions/items.spec.ts b/src/wireframes/model/actions/items.spec.ts index d55fb0b..a49ae48 100644 --- a/src/wireframes/model/actions/items.spec.ts +++ b/src/wireframes/model/actions/items.spec.ts @@ -76,7 +76,7 @@ describe('ItemsReducer', () => { const state_1 = EditorState.create().addDiagram(diagram); const state_2 = reducer(state_1, action); - const newShape = state_2.diagrams.get(diagram.id)!.items.get('1')!; + const newShape = state_2.diagrams.get(diagram.id)!.items.get(shape1.id)!; expect(newShape.name).toEqual('Name'); }); @@ -87,7 +87,7 @@ describe('ItemsReducer', () => { const state_1 = EditorState.create().addDiagram(diagram); const state_2 = reducer(state_1, action); - const newShape = state_2.diagrams.get(diagram.id)!.items.get('1')!; + const newShape = state_2.diagrams.get(diagram.id)!.items.get(shape1.id)!; expect(newShape.isLocked).toBeTruthy(); }); @@ -98,7 +98,7 @@ describe('ItemsReducer', () => { const state_1 = EditorState.create().addDiagram(diagram); const state_2 = reducer(state_1, action); - const newShape = state_2.diagrams.get(diagram.id)!.items.get('2')!; + const newShape = state_2.diagrams.get(diagram.id)!.items.get(shape2.id)!; expect(newShape.isLocked).toBeFalsy(); }); @@ -209,7 +209,7 @@ describe('ItemsReducer', () => { const itemIds = calculateSelection([shape3], selectedDiagram, true, true); - expect(itemIds).toEqual([shape3.id, groupId]); + expect(itemIds).toEqual([groupId, shape3.id]); }); it('should remove item from selection list', () => { diff --git a/src/wireframes/model/actions/items.ts b/src/wireframes/model/actions/items.ts index f3f0102..170f6c8 100644 --- a/src/wireframes/model/actions/items.ts +++ b/src/wireframes/model/actions/items.ts @@ -78,7 +78,7 @@ export function buildItems(builder: ActionReducerMapBuilder) { return state.updateDiagram(diagramId, diagram => { const set = DiagramItemSet.createFromDiagram(itemIds, diagram); - return diagram.updateItems(set.allItems.map(x => x.id), item => { + return diagram.updateItems([...set.nested.keys()], item => { return item.lock(); }); }); @@ -89,7 +89,7 @@ export function buildItems(builder: ActionReducerMapBuilder) { return state.updateDiagram(diagramId, diagram => { const set = DiagramItemSet.createFromDiagram(itemIds, diagram); - return diagram.updateItems(set.allItems.map(x => x.id), item => { + return diagram.updateItems([...set.nested.keys()], item => { return item.unlock(); }); }); @@ -141,7 +141,7 @@ export function buildItems(builder: ActionReducerMapBuilder) { diagram = diagram.addItems(set); - diagram = diagram.updateItems(set.allShapes.map(x => x.id), item => { + diagram = diagram.updateItems([...set.nested.keys()], item => { const boundsOld = item.bounds(diagram); const boundsNew = boundsOld.moveBy(new Vec2(offsetByX, offsetByY)); @@ -190,7 +190,7 @@ export function buildItems(builder: ActionReducerMapBuilder) { }); } -export function calculateSelection(items: DiagramItem[], diagram: Diagram, isSingleSelection?: boolean, isCtrl?: boolean): string[] { +export function calculateSelection(items: ReadonlyArray, diagram: Diagram, isSingleSelection?: boolean, isCtrl?: boolean): ReadonlyArray { if (!items) { return []; } diff --git a/src/wireframes/model/actions/loading.spec.ts b/src/wireframes/model/actions/loading.spec.ts index f40dbe8..7464bae 100644 --- a/src/wireframes/model/actions/loading.spec.ts +++ b/src/wireframes/model/actions/loading.spec.ts @@ -10,7 +10,7 @@ import { diff } from 'deep-object-diff'; import { Types } from '@app/core'; -import { EditorState, loadDiagramInternal, RendererService, selectDiagram, selectItems } from '@app/wireframes/model'; +import { EditorState, loadDiagramInternal, RendererService, selectDiagram, selectItems, UndoableState } from '@app/wireframes/model'; import * as Reducers from '@app/wireframes/model/actions'; import { Button } from '@app/wireframes/shapes/neutral/button'; import { AbstractControl } from '@app/wireframes/shapes/utils/abstract-control'; @@ -78,8 +78,11 @@ describe('LoadingReducer', () => { it('should load from old and new format V2', () => { const initial = undoableReducer(undefined, { type: 'NOOP' }); - const editorV1 = rootReducer(initial, loadDiagramInternal(JSON.parse(v1), '1')); - const editorV2 = rootReducer(initial, loadDiagramInternal(JSON.parse(v2), '2')); + const editorV1 = rootReducer(initial, loadDiagramInternal(JSON.parse(v1), '1')) as UndoableState; + const editorV2 = rootReducer(initial, loadDiagramInternal(JSON.parse(v2), '2')) as UndoableState; + + expect(editorV1.present.diagrams.values[0].items.size).toEqual(10); + expect(editorV2.present.diagrams.values[0].items.size).toEqual(10); const diffsV2 = cleanupDiffs(diff(editorV1.present, editorV2.present), []); @@ -89,11 +92,11 @@ describe('LoadingReducer', () => { it('should load from old and new format V3', () => { const initial = undoableReducer(undefined, { type: 'NOOP' }); - const editorV1 = rootReducer(initial, loadDiagramInternal(JSON.parse(v1), '1')); - const editorV3 = rootReducer(initial, loadDiagramInternal(JSON.parse(v3), '2')); + const editorV1 = rootReducer(initial, loadDiagramInternal(JSON.parse(v1), '1')) as UndoableState; + const editorV3 = rootReducer(initial, loadDiagramInternal(JSON.parse(v3), '2')) as UndoableState; - expect(editorV1.present.diagrams.values[0].items.values.length).toEqual(10); - expect(editorV3.present.diagrams.values[0].items.values.length).toEqual(10); + expect(editorV1.present.diagrams.values[0].items.size).toEqual(10); + expect(editorV3.present.diagrams.values[0].items.size).toEqual(10); const diffsV3 = cleanupDiffs(diff(editorV1.present, editorV3.present), []); diff --git a/src/wireframes/model/actions/loading.ts b/src/wireframes/model/actions/loading.ts index 3ca1133..9688a08 100644 --- a/src/wireframes/model/actions/loading.ts +++ b/src/wireframes/model/actions/loading.ts @@ -7,7 +7,7 @@ */ import { createAction, createAsyncThunk, createReducer, Middleware } from '@reduxjs/toolkit'; -import { push } from 'connected-react-router'; +import { History } from 'history'; import { saveAs } from 'file-saver'; import { AnyAction, Reducer } from 'redux'; import { texts } from '@app/const'; @@ -52,7 +52,7 @@ export const downloadDiagramToFile = saveAs(bodyBlob, 'diagram.json'); }); -export function loadingMiddleware(): Middleware { +export function loadingMiddleware(history: History): Middleware { const middleware: Middleware = store => next => action => { if (loadDiagramFromServer.pending.match(action) || loadDiagramFromFile.pending.match(action)) { store.dispatch(showToast(texts.common.loadingDiagram, 'loading', action.meta.requestId)); @@ -63,11 +63,11 @@ export function loadingMiddleware(): Middleware { if (newDiagram.match(action) ) { if (action.payload.navigate) { - store.dispatch(push('')); + history.push(''); } } else if (loadDiagramFromServer.fulfilled.match(action)) { if (action.meta.arg.navigate) { - store.dispatch(push(action.payload.tokenToRead)); + history.push(action.payload.tokenToRead); } store.dispatch(loadDiagramInternal(action.payload.stored, action.meta.requestId)); diff --git a/src/wireframes/model/actions/utils.ts b/src/wireframes/model/actions/utils.ts index 7c90533..1d0ebc0 100644 --- a/src/wireframes/model/actions/utils.ts +++ b/src/wireframes/model/actions/utils.ts @@ -13,7 +13,7 @@ import { Diagram, DiagramItem } from './../internal'; export type DiagramRef = string | Diagram; export type ItemRef = string | DiagramItem; -export type ItemsRef = ItemRef[]; +export type ItemsRef = ReadonlyArray; interface DiagramAction { readonly diagramId: string; diff --git a/src/wireframes/model/diagram-item-set.spec.ts b/src/wireframes/model/diagram-item-set.spec.ts index e32d093..9a46526 100644 --- a/src/wireframes/model/diagram-item-set.spec.ts +++ b/src/wireframes/model/diagram-item-set.spec.ts @@ -12,9 +12,9 @@ import { Diagram, DiagramItem, DiagramItemSet } from '@app/wireframes/model'; describe('Diagram Item Set', () => { const groupId = 'group-1'; const root1 = DiagramItem.createShape({ id: '1', renderer: shapes.id.button }); - const root2 = DiagramItem.createShape({ id: '2', renderer: shapes.id.button }); + const root2 = DiagramItem.createShape({ id: '2', renderer: shapes.id.button, isLocked: true }); const child1 = DiagramItem.createShape({ id: '3', renderer: shapes.id.button }); - const child2 = DiagramItem.createShape({ id: '4', renderer: shapes.id.button }); + const child2 = DiagramItem.createShape({ id: '4', renderer: shapes.id.button, isLocked: true }); const diagram = Diagram.create() @@ -24,33 +24,61 @@ describe('Diagram Item Set', () => { .addShape(child2) .group(groupId, [child1.id, child2.id]); + const group = diagram.items.get(groupId)!; + it('should create from root items', () => { const set = DiagramItemSet.createFromDiagram([groupId], diagram); - expect(set.allItems.map(x => x.id)).toEqual([groupId, child1.id, child2.id]); + expect([...set.nested.keys()]).toEqual([groupId, child1.id, child2.id]); }); it('should create from child items', () => { - const set = DiagramItemSet.createFromDiagram([child1.id], diagram); + const set = DiagramItemSet.createFromDiagram([child1], diagram); - expect(set.allItems.map(x => x.id)).toEqual([child1.id]); + expect([...set.nested.keys()]).toEqual([child1.id]); }); it('should keep the order in children intact', () => { - const set = DiagramItemSet.createFromDiagram([child2.id, child1.id], diagram); + const set = DiagramItemSet.createFromDiagram([child2, child1], diagram); - expect(set.allItems.map(x => x.id)).toEqual([child1.id, child2.id]); + expect([...set.nested.keys()]).toEqual([child1.id, child2.id]); }); it('should keep the order in root intact', () => { - const set = DiagramItemSet.createFromDiagram([root2.id, root2.id], diagram); + const set = DiagramItemSet.createFromDiagram([root1, root2], diagram); - expect(set.allItems.map(x => x.id)).toEqual([root2.id, root2.id]); + expect([...set.nested.keys()]).toEqual([root1.id, root2.id]); }); it('should keep the order in mixed items intact', () => { - const set = DiagramItemSet.createFromDiagram([root2.id, child2.id, root2.id, child1.id], diagram); + const set = DiagramItemSet.createFromDiagram([ child2, child1, root2], diagram); + + expect([...set.nested.keys()]).toEqual([root2.id, child1.id, child2.id]); + }); + + it('should be invalid if group is not complete', () => { + const set = new DiagramItemSet(new Map([[group.id, group], [child1.id, child1]]), new Map()); + + expect(set.isComplete).toBeFalsy(); + }); + + it('should calculate selection once', () => { + const set = DiagramItemSet.createFromDiagram([root1, root2, groupId], diagram); + + const selection1 = set.selectedItems; + const selection2 = set.selectedItems; + + expect(selection1.map(x => x.id)).toEqual([root1.id, groupId]); + expect(selection1).toBe(selection2); + }); + + it('should calculate editable once', () => { + const set = DiagramItemSet.createFromDiagram([root1, root2, groupId], diagram); + + const editable1 = set.deepEditableItems; + const editable2 = set.deepEditableItems; - expect(set.allItems.map(x => x.id)).toEqual([root2.id, root2.id, child1.id, child2.id]); + expect(editable1.map(x => x.id)).toEqual([root1.id, groupId, child1.id, child2.id]); + expect(editable1).toBe(editable2); }); }); \ No newline at end of file diff --git a/src/wireframes/model/diagram-item-set.ts b/src/wireframes/model/diagram-item-set.ts index 0cf3b21..844052d 100644 --- a/src/wireframes/model/diagram-item-set.ts +++ b/src/wireframes/model/diagram-item-set.ts @@ -11,58 +11,64 @@ import { Diagram } from './diagram'; import { DiagramItem } from './diagram-item'; export class DiagramItemSet { - public readonly allItems: DiagramItem[] = []; - public readonly allShapes: DiagramItem[] = []; - public readonly allGroups: DiagramItem[] = []; + private cachedSelectedItems?: ReadonlyArray; + private cachedDeepEditableItems?: ReadonlyArray; + + public static EMPTY = new DiagramItemSet(new Map(), new Map()); + public readonly rootIds: string[] = []; + public readonly isComplete: boolean = true; - public isValid = true; + public get selectedItems() { + return this.cachedSelectedItems ||= Array.from(this.selection.values()).filter(x => !x.isLocked); + } - constructor(source: DiagramItem[]) { - const parents: { [id: string]: boolean } = {}; + public get deepEditableItems() { + return this.cachedDeepEditableItems ||= Array.from(this.nested.values()).filter(x => !this.selection.has(x.id) || !x.isLocked); + } - for (const item of source) { - this.allItems.push(item); + constructor( + public readonly nested: Map, + public readonly selection: Map, + ) { + const parents: { [id: string]: boolean } = {}; + for (const item of nested.values()) { if (item.type !== 'Group') { - this.allShapes.push(item); - } else { - this.allGroups.push(item); - - for (const childId of item.childIds.values) { - if (!source.find(i => i.id === childId) || parents[childId]) { - this.isValid = false; - } - - parents[childId] = true; + continue; + } + + for (const childId of item.childIds.values) { + if (!nested.get(childId) || parents[childId]) { + this.isComplete = false; } + parents[childId] = true; } } - for (const item of source) { + for (const item of nested.values()) { if (!parents[item.id]) { this.rootIds.push(item.id); } } - - Object.freeze(this); } public static createFromDiagram(items: ReadonlyArray, diagram: Diagram): DiagramItemSet { - const allItems: DiagramItem[] = []; + const allItems = new Map(); + const allSources = new Map(); - flattenRootItems(items, diagram, allItems); + flattenRootItems(items, diagram, allItems, allSources); - return new DiagramItemSet(allItems); + return new DiagramItemSet(allItems, allSources); } public canAdd(diagram: Diagram): boolean { - if (!this.isValid) { + if (!this.isComplete) { return false; } - for (const item of this.allItems) { + for (const item of this.nested.values()) { if (diagram.items.has(item.id)) { return false; } @@ -72,11 +78,11 @@ export class DiagramItemSet { } public canRemove(diagram: Diagram): boolean { - if (!this.isValid) { + if (!this.isComplete) { return false; } - for (const item of this.allItems) { + for (const item of this.nested.values()) { if (!diagram.items.has(item.id)) { return false; } @@ -88,11 +94,11 @@ export class DiagramItemSet { type OrderedItems = { item: DiagramItem; orderIndex: number }[]; -function flattenRootItems(source: ReadonlyArray, diagram: Diagram, allItems: DiagramItem[]) { +function flattenRootItems(items: ReadonlyArray, diagram: Diagram, allItems: Map, source: Map) { const byRoot: OrderedItems = []; - const byParents: { [id: string]: OrderedItems } = {}; + const byParents = new Map(); - for (const itemOrId of source) { + for (const itemOrId of items) { let item = itemOrId; if (Types.isString(itemOrId)) { @@ -101,52 +107,56 @@ function flattenRootItems(source: ReadonlyArray, diagram: item = itemOrId; } - if (item) { - const parent = diagram.parent(item); - - if (parent) { - let byParent = byParents[parent.id]; - - if (!byParent) { - byParent = []; - byParents[parent.id] = byParent; - } + if (!item) { + continue; + } - const orderIndex = parent.childIds.values.indexOf(item.id); + const parent = diagram.parent(item); - byParent.push({ orderIndex, item }); - } else { - const orderIndex = diagram.rootIds.values.indexOf(item.id); + if (parent) { + let byParent = byParents.get(parent.id); - byRoot.push({ orderIndex, item }); + if (!byParent) { + byParent = []; + byParents.set(parent.id, byParent); } - } - } - function handleParent(byParent: OrderedItems, diagram: Diagram, allItems: DiagramItem[]) { - if (byParent.length === 0) { - return; + const orderIndex = parent.childIds.indexOf(item.id); + + byParent.push({ orderIndex, item }); + } else { + const orderIndex = diagram.rootIds.indexOf(item.id); + + byRoot.push({ orderIndex, item }); } - byParent.sort((a, b) => a.orderIndex - b.orderIndex); + source.set(item.id, item); + } - for (const { item } of byParent) { - allItems.push(item); + unrollParent(byRoot, diagram, allItems); - if (item.type === 'Group') { - flattenItems(item.childIds.values, diagram, allItems); - } - } + for (const byParent of byParents.values()) { + unrollParent(byParent, diagram, allItems); + } +} + +function unrollParent(byParent: OrderedItems, diagram: Diagram, allItems: Map) { + if (byParent.length === 0) { + return; } - handleParent(byRoot, diagram, allItems); + byParent.sort((a, b) => a.orderIndex - b.orderIndex); - for (const byParent of Object.values(byParents)) { - handleParent(byParent, diagram, allItems); + for (const { item } of byParent) { + allItems.set(item.id, item); + + if (item.type === 'Group') { + unrollItems(item.childIds.values, diagram, allItems); + } } } -function flattenItems(source: ReadonlyArray, diagram: Diagram, allItems: DiagramItem[]) { +function unrollItems(source: ReadonlyArray, diagram: Diagram, allItems: Map) { for (const itemOrId of source) { let item = diagram.items.get(itemOrId); @@ -154,10 +164,10 @@ function flattenItems(source: ReadonlyArray, diagram: Diagram, allItems: continue; } - allItems.push(item); + allItems.set(item.id, item); if (item.type === 'Group') { - flattenItems(item.childIds.values, diagram, allItems); + unrollItems(item.childIds.values, diagram, allItems); } } } \ No newline at end of file diff --git a/src/wireframes/model/diagram-item.ts b/src/wireframes/model/diagram-item.ts index 9b1d162..3d47124 100644 --- a/src/wireframes/model/diagram-item.ts +++ b/src/wireframes/model/diagram-item.ts @@ -289,13 +289,13 @@ export class DiagramItem extends Record implements Shape { let cached = this.cachedBounds[cacheId]; if (!cached) { - const set = DiagramItemSet.createFromDiagram([this.id], diagram); + const allShapes = DiagramItemSet.createFromDiagram([this.id], diagram).nested; - if (set.allShapes.length === 0) { + if (allShapes.size === 0) { return Transform.ZERO; } - const transforms = set.allShapes.map(x => x.transform); + const transforms = Array.from(allShapes.values(), x => x.transform).filter(x => !!x); this.cachedBounds[cacheId] = cached = Transform.createFromTransformationsAndRotation(transforms, this.rotation); } diff --git a/src/wireframes/model/diagram.spec.ts b/src/wireframes/model/diagram.spec.ts index e9ff8ec..b71ccaf 100644 --- a/src/wireframes/model/diagram.spec.ts +++ b/src/wireframes/model/diagram.spec.ts @@ -49,16 +49,31 @@ describe('Diagram', () => { }); it('should add items to diagram', () => { - const diagram_2 = diagram_1.addItems(new DiagramItemSet([shape1, shape2, shape3])); + const diagram_2 = diagram_1.addItems(DiagramItemSet.createFromDiagram([shape1, shape2, shape3], diagram_1)!); expect(diagram_2.items.has(shape1.id)).toBeTruthy(); expect(diagram_2.items.has(shape2.id)).toBeTruthy(); expect(diagram_2.items.has(shape3.id)).toBeTruthy(); }); + it('should not add items if one them is already added', () => { + const diagram_2 = diagram_1.addShape(shape1); + const diagram_3 = diagram_2.addItems(DiagramItemSet.createFromDiagram([shape1, shape2, shape3], diagram_1)!); + + expect(diagram_3).toBe(diagram_2); + }); + + it('should not add items set is not complete', () => { + const group = DiagramItem.createGroup({ childIds: [shape1.id, shape2.id] }); + + const diagram_2 = diagram_1.addItems(new DiagramItemSet(new Map([[ group.id, group ]]), new Map())); + + expect(diagram_2).toBe(diagram_1); + }); + it('should remove shape from items', () => { const diagram_2 = diagram_1.addShape(shape1); - const diagram_3 = diagram_2.removeItems(DiagramItemSet.createFromDiagram([shape1.id], diagram_2)!); + const diagram_3 = diagram_2.removeItems(DiagramItemSet.createFromDiagram([shape1], diagram_2)!); expect(diagram_3.items.has(shape1.id)).toBeFalsy(); }); @@ -66,7 +81,7 @@ describe('Diagram', () => { it('should remove selected shape from items', () => { const diagram_2 = diagram_1.addShape(shape1); const diagram_3 = diagram_2.selectItems([shape1.id]); - const diagram_4 = diagram_3.removeItems(DiagramItemSet.createFromDiagram([shape1.id], diagram_2)!); + const diagram_4 = diagram_3.removeItems(DiagramItemSet.createFromDiagram([shape1], diagram_2)!); expect(diagram_3.selectedIds.has(shape1.id)).toBeTruthy(); expect(diagram_4.selectedIds.has(shape1.id)).toBeFalsy(); @@ -84,6 +99,24 @@ describe('Diagram', () => { expect(diagram_5.items.size).toBe(0); }); + it('should not remove items when one item is not part of set', () => { + const diagram_2 = diagram_1.addShape(shape1); + const diagram_3 = diagram_2.addItems(new DiagramItemSet(new Map([[ shape1.id, shape1 ], [shape2.id, shape2]]), new Map())); + + expect(diagram_3).toBe(diagram_2); + }); + + it('should not remove items when set is not complete', () => { + const groupId = 'group-1'; + + const diagram_2 = diagram_1.addShape(shape1); + const diagram_3 = diagram_2.addShape(shape2); + const diagram_4 = diagram_3.group(groupId, [shape1.id, shape2.id]); + const diagram_5 = diagram_4.addItems(new DiagramItemSet(new Map([[ groupId, diagram_4.items.get(groupId)! ]]), new Map())); + + expect(diagram_5).toBe(diagram_4); + }); + it('should update items', () => { const shapeOld = shape4; const shapeNew = shapeOld.setAppearance('border-width', 10); diff --git a/src/wireframes/model/diagram.ts b/src/wireframes/model/diagram.ts index c1a21d7..bcf0ba6 100644 --- a/src/wireframes/model/diagram.ts +++ b/src/wireframes/model/diagram.ts @@ -23,7 +23,7 @@ type UpdateProps = { itemIds: ItemIds; // The selected ids. - selectedIds: ImmutableSet; + selectedIds: ImmutableSet; // The map of renderers' next id. nextIds: NextIds; @@ -46,7 +46,7 @@ type Props = { rootIds: ItemIds; // The selected ids. - selectedIds: ImmutableSet; + selectedIds: ImmutableSet; // The animation script script: string; @@ -82,7 +82,9 @@ export type InitialDiagramProps = { }; export class Diagram extends Record { - private parents: { [id: string]: DiagramItem } = {}; + private cachedParents?: { [id: string]: DiagramItem }; + + private cachedRootItems?: DiagramItem[]; public get id() { return this.get('id'); @@ -121,7 +123,7 @@ export class Diagram extends Record { } public get rootItems(): ReadonlyArray { - return this.rootIds.values.map(x => this.items.get(x)).filter(x => !!x) as DiagramItem[]; + return this.cachedRootItems ||= this.findItems(this.rootIds.values); } public static create(setup: InitialDiagramProps = {}) { @@ -142,7 +144,21 @@ export class Diagram extends Record { } public children(item: DiagramItem): ReadonlyArray { - return item.childIds.values.map(x => this.items.get(x)!).filter(x => !!x)!; + return this.findItems(item.childIds.values); + } + + public findItems(ids: ReadonlyArray) { + const result: DiagramItem[] = []; + + for (const id of ids) { + const item = this.items.get(id); + + if (item) { + result.push(item); + } + } + + return result; } public rename(title: string | undefined) { @@ -170,21 +186,21 @@ export class Diagram extends Record { id = id.id; } - if (!this.parents) { - this.parents = {}; + if (!this.cachedParents) { + this.cachedParents = {}; for (const key of this.items.keys) { const item = this.items.get(key); if (item?.type === 'Group') { for (const childId of item.childIds.values) { - this.parents[childId] = item; + this.cachedParents[childId] = item; } } } } - return this.parents[id]; + return this.cachedParents[id]; } public addShape(shape: DiagramItem) { @@ -192,7 +208,7 @@ export class Diagram extends Record { return this; } - return this.mutate([], update => { + return this.arrange(EMPTY_SELECTION, update => { update.items = update.items.set(shape.id, shape); if (update.items !== this.items) { @@ -202,13 +218,13 @@ export class Diagram extends Record { } public updateNextId(renderer: string, newCount: number) { - return this.mutate([], update => { + return this.arrange(EMPTY_SELECTION, update => { update.nextIds.set(renderer, newCount); }); } public updateItems(ids: ReadonlyArray, updater: (value: DiagramItem) => DiagramItem) { - return this.mutate(ids, update => { + return this.arrange(EMPTY_SELECTION, update => { update.items = update.items.mutate(mutator => { for (const id of ids) { mutator.update(id, updater); @@ -218,51 +234,53 @@ export class Diagram extends Record { } public selectItems(ids: ReadonlyArray) { - return this.mutate(ids, update => { + return this.arrange(ids, update => { update.selectedIds = ImmutableSet.of(...ids); }); } public moveItems(ids: ReadonlyArray, index: number) { - return this.mutate(ids, update => { + return this.arrange(ids, update => { update.itemIds = update.itemIds.moveTo(ids, index); - }); + }, 'SameParent'); } public bringToFront(ids: ReadonlyArray) { - return this.mutate(ids, update => { + return this.arrange(ids, update => { update.itemIds = update.itemIds.bringToFront(ids); - }); + }, 'SameParent'); } public bringForwards(ids: ReadonlyArray) { - return this.mutate(ids, update => { + return this.arrange(ids, update => { update.itemIds = update.itemIds.bringForwards(ids); - }); + }, 'SameParent'); } public sendToBack(ids: ReadonlyArray) { - return this.mutate(ids, update => { + return this.arrange(ids, update => { update.itemIds = update.itemIds.sendToBack(ids); - }); + }, 'SameParent'); } public sendBackwards(ids: ReadonlyArray) { - return this.mutate(ids, update => { + return this.arrange(ids, update => { update.itemIds = update.itemIds.sendBackwards(ids); - }); + }, 'SameParent'); } public group(groupId: string, ids: ReadonlyArray) { - return this.mutate(ids, update => { + return this.arrange(ids, update => { update.itemIds = update.itemIds.add(groupId).remove(...ids); update.items = update.items.set(groupId, DiagramItem.createGroup({ id: groupId, childIds: ids })); - }); + }, 'SameParent'); } public ungroup(groupId: string) { - return this.mutate([groupId], (update, targetItems) => { - update.itemIds = update.itemIds.add(...targetItems[0].childIds?.values).remove(groupId); + return this.arrange([groupId], update => { + const group = this.items.get(groupId)!; + + update.itemIds = update.itemIds.add(...group.childIds.values).remove(groupId); update.items = update.items.remove(groupId); }); } @@ -272,9 +290,9 @@ export class Diagram extends Record { return this; } - return this.mutate([], update => { + return this.arrange(EMPTY_SELECTION, update => { update.items = update.items.mutate(mutator => { - for (const item of set.allItems) { + for (const item of set.nested.values()) { mutator.set(item.id, item); } }); @@ -288,16 +306,16 @@ export class Diagram extends Record { return this; } - return this.mutate([], update => { + return this.arrange(EMPTY_SELECTION, update => { update.items = update.items.mutate(m => { - for (const item of set.allItems) { + for (const item of set.nested.values()) { m.remove(item.id); } }); update.selectedIds = update.selectedIds.mutate(m => { - for (const item of set.allItems) { - m.remove(item.id); + for (const id of set.nested.keys()) { + m.remove(id); } }); @@ -313,13 +331,13 @@ export class Diagram extends Record { return this.set('script', script); } - private mutate(targetIds: ReadonlyArray, updater: (diagram: UpdateProps, targetItems: DiagramItem[]) => void): Diagram { + private arrange(targetIds: Iterable, updater: (diagram: UpdateProps) => void, condition?: 'NoCondition' | 'SameParent'): Diagram { if (!targetIds) { return this; } - const resultItems: DiagramItem[] = []; - const resultParent = this.parent(targetIds[0]); + let resultParent: DiagramItem | undefined = undefined; + let index = 0; // All items must have the same parent for the update. for (const itemId of targetIds) { @@ -329,45 +347,46 @@ export class Diagram extends Record { return this; } - if (this.parent(itemId) !== resultParent) { + const parent = this.parent(itemId); + + if (index === 0) { + resultParent = parent; + } else if (parent !== resultParent && condition === 'SameParent') { return this; } - resultItems.push(item); + index++; } - let update: UpdateProps; - if (resultParent) { - update = { + const update = { items: this.items, itemIds: resultParent.childIds, selectedIds: this.selectedIds, nextIds: this.nextIds, }; - updater(update, resultItems); + updater(update); if (update.itemIds !== resultParent.childIds) { update.items = update.items || this.items; update.items = update.items.update(resultParent.id, p => p.set('childIds', update.itemIds)); } + return this.merge({ items: update.items, selectedIds: update.selectedIds }); } else { - update = { + const update = { items: this.items, itemIds: this.rootIds, selectedIds: this.selectedIds, nextIds: this.nextIds, }; - updater(update, resultItems); + updater(update); - (update as any)['rootIds'] = update.itemIds; + return this.merge({ items: update.items, selectedIds: update.selectedIds, rootIds: update.itemIds }); } - - delete (update as any).itemIds; - - return this.merge(update); } } + +const EMPTY_SELECTION: ReadonlyArray = []; \ No newline at end of file diff --git a/src/wireframes/model/editor-state.ts b/src/wireframes/model/editor-state.ts index 799e37e..d85fa0a 100644 --- a/src/wireframes/model/editor-state.ts +++ b/src/wireframes/model/editor-state.ts @@ -94,7 +94,7 @@ export class EditorState extends Record { } public get orderedDiagrams(): ReadonlyArray { - return this.diagramIds.values.map(x => this.diagrams.get(x)).filter(x => !!x) as Diagram[]; + return this.findDiagrams(this.diagramIds.values); } public static create(setup: InitialEditorProps = {}): EditorState { @@ -117,6 +117,20 @@ export class EditorState extends Record { return this.set('name', name); } + public findDiagrams(ids: ReadonlyArray) { + const result: Diagram[] = []; + + for (const id of ids) { + const item = this.diagrams.get(id); + + if (item) { + result.push(item); + } + } + + return result; + } + public changeSize(size: Vec2) { return this.set('size', size); } diff --git a/src/wireframes/model/projections.ts b/src/wireframes/model/projections.ts index b1f92b2..c642003 100644 --- a/src/wireframes/model/projections.ts +++ b/src/wireframes/model/projections.ts @@ -12,18 +12,14 @@ import { Color, ColorPalette, Types } from '@app/core/utils'; import { texts } from '@app/const'; import { addDiagram, addShape, changeColor, changeColors, changeItemsAppearance, pasteItems, removeDiagram, removeItems } from './actions'; import { AssetsStateInStore } from './assets-state'; -import { Configurable } from './configurables'; import { Diagram } from './diagram'; -import { DiagramItem } from './diagram-item'; import { DiagramItemSet } from './diagram-item-set'; import { EditorState, EditorStateInStore } from './editor-state'; import { LoadingStateInStore } from './loading-state'; import { UIStateInStore } from './ui-state'; import { UndoableState } from './undoable-state'; -const EMPTY_STRING_ARRAY: string[] = []; -const EMPTY_ITEMS_ARRAY: DiagramItem[] = []; -const EMPTY_CONFIGURABLES: Configurable[] = []; +const EMPTY_SELECTION_SET = DiagramItemSet.EMPTY; export const getDiagramId = (state: EditorStateInStore) => state.editor.present.selectedDiagramId; export const getDiagrams = (state: EditorStateInStore) => state.editor.present.diagrams; @@ -93,44 +89,9 @@ export const getMasterDiagram = createSelector( (diagrams, diagram) => diagrams.get(diagram?.master!), ); -export const getSelectionSet = createSelector( +export const getSelection = createSelector( getDiagram, - diagram => (diagram ? DiagramItemSet.createFromDiagram(diagram.selectedIds.values, diagram) : null), -); - -export const getSelectedIds = createSelector( - getDiagram, - diagram => diagram?.selectedIds.values || EMPTY_STRING_ARRAY, -); - -export const getSelectedItemsWithLocked = createSelector( - getDiagram, - diagram => diagram?.selectedIds.values.map(i => diagram!.items.get(i)).filter(x => !!x) as DiagramItem[] || EMPTY_ITEMS_ARRAY, -); - -export const getSelectedItems = createSelector( - getSelectedItemsWithLocked, - items => items.filter(x => !x.isLocked), -); - -export const getSelectedGroups = createSelector( - getSelectedItems, - items => items.filter(i => i.type === 'Group'), -); - -export const getSelectedItemWithLocked = createSelector( - getSelectedItemsWithLocked, - items => (items.length === 1 ? items[0] : null), -); - -export const getSelectedShape = createSelector( - getSelectedItems, - items => (items.length === 1 && items[0].type === 'Shape' ? items[0] : null), -); - -export const getSelectedConfigurables = createSelector( - getSelectedShape, - shape => (shape ? shape.configurables : EMPTY_CONFIGURABLES), + diagram => (diagram ? DiagramItemSet.createFromDiagram(diagram.selectedIds.values, diagram) : EMPTY_SELECTION_SET), ); export const getColors = createSelector( @@ -160,7 +121,7 @@ export const getColors = createSelector( continue; } - for (const [key, value] of Object.entries(shape.appearance.raw)) { + for (const [key, value] of shape.appearance.entries) { if (key.endsWith('COLOR')) { addColor(value); } diff --git a/src/wireframes/model/serializer.spec.ts b/src/wireframes/model/serializer.spec.ts index 33ea79e..59ec84f 100644 --- a/src/wireframes/model/serializer.spec.ts +++ b/src/wireframes/model/serializer.spec.ts @@ -82,7 +82,7 @@ describe('Serializer', () => { const newValue = Serializer.deserializeSet(Serializer.serializeSet(original)); - expect(newValue.allItems.length).toEqual(3); + expect(newValue.nested.size).toEqual(3); }); it('should serialize and deserialize editor', () => { @@ -138,10 +138,10 @@ describe('Serializer', () => { function compareSets(newValue: DiagramItemSet | undefined, original: DiagramItemSet) { expect(newValue).toBeDefined(); - expect(newValue?.allItems.length).toEqual(original.allItems.length); + expect(newValue?.nested.size).toEqual(original.nested.size); - for (const item of original.allItems) { - compareShapes(newValue?.allItems.find(x => x.id === item.id), item); + for (const item of Object.values(original.nested)) { + compareShapes(newValue?.nested.get(item.id), item); } } diff --git a/src/wireframes/model/serializer.ts b/src/wireframes/model/serializer.ts index 1f7de68..05ccb8d 100644 --- a/src/wireframes/model/serializer.ts +++ b/src/wireframes/model/serializer.ts @@ -52,13 +52,13 @@ export module Serializer { } export function deserializeSet(input: any): DiagramItemSet { - const allItems: DiagramItem[] = []; + const allItems = new Map(); for (const inputVisual of input.visuals) { const item = readDiagramItem(inputVisual, 'Shape'); if (item) { - allItems.push(item); + allItems.set(item.id, item); } } @@ -66,17 +66,17 @@ export module Serializer { const item = readDiagramItem(inputGroup, 'Group'); if (item) { - allItems.push(item); + allItems.set(item.id, item); } } - return new DiagramItemSet(allItems); + return new DiagramItemSet(allItems, allItems); } export function serializeSet(set: DiagramItemSet) { const output: any = { visuals: [], groups: [] }; - for (const item of Object.values(set.allItems)) { + for (const item of set.nested.values()) { const serialized = writeDiagramItem(item); if (item.type === 'Shape') { @@ -186,7 +186,7 @@ const EDITOR_SERIALIZERS: PropertySerializers = { set: (source) => source, }, 'diagrams': { - get: (source: ImmutableMap) => source.values.map(writeDiagram), + get: (source: ImmutableMap) => Array.from(source.values, writeDiagram), set: (source: any[]) => buildObject(source.map(readDiagram), x => x.id), }, 'diagramIds': { @@ -209,7 +209,7 @@ const DIAGRAM_SERIALIZERS: PropertySerializers = { set: (source) => source, }, 'items': { - get: (source: ImmutableMap) => source.values.map(writeDiagramItem), + get: (source: ImmutableMap) => Array.from(source.values, writeDiagramItem), set: (source: any[]) => buildObject(source.map(readDiagramItem), x => x.id), }, 'rootIds': { @@ -224,11 +224,11 @@ const DIAGRAM_SERIALIZERS: PropertySerializers = { const DIAGRAM_ITEM_SERIALIZERS: PropertySerializers = { 'appearance': { - get: (source: ImmutableMap) => source.raw, + get: (source: ImmutableMap) => Object.fromEntries(source.entries), set: (source: any) => source, }, 'childIds': { - get: (source: ImmutableList) => source.values, + get: (source: ImmutableList) => Array.from(source.values), set: (source) => source, }, 'id': { diff --git a/src/wireframes/model/snap-manager.ts b/src/wireframes/model/snap-manager.ts index 256b248..77592c6 100644 --- a/src/wireframes/model/snap-manager.ts +++ b/src/wireframes/model/snap-manager.ts @@ -27,7 +27,7 @@ export type SnapResult = { }; export type SnapLine = { - // The actual position ofthe line. + // The actual position of the line. value: number; // The side. @@ -67,7 +67,7 @@ type GridItem = { bottomIndex: number; // The bounds. - bound: Rect2; + aabb: Rect2; }; export class SnapManager { @@ -98,7 +98,7 @@ export class SnapManager { } } - public snapResizing(transform: Transform, delta: Vec2, snapMode: SnapMode, xMode = 1, yMode = 1): SnapResult { + public snapResizing(transform: Transform, delta: Vec2, snapMode: SnapMode, xMode = 1, yMode = 1, ignoreList: Record = {}): SnapResult { const result: SnapResult = { delta }; let dw = delta.x; @@ -107,7 +107,7 @@ export class SnapManager { if (snapMode === 'Shapes' && transform.rotation.degree === 0) { const aabb = transform.aabb; - const { xLines, yLines } = this.getSnapLines(transform); + const { xLines, yLines } = this.getSnapLines(ignoreList); // Compute the new x and y-positions once. const l = -delta.x + aabb.left; @@ -201,7 +201,7 @@ export class SnapManager { return result; } - public snapMoving(transform: Transform, delta: Vec2, snapMode: SnapMode): SnapResult { + public snapMoving(transform: Transform, delta: Vec2, snapMode: SnapMode, ignoreList: Record = {}): SnapResult { const result: SnapResult = { delta }; const aabb = transform.aabb; @@ -210,7 +210,7 @@ export class SnapManager { let y = aabb.y + delta.y; if (snapMode === 'Shapes') { - const { xLines, yLines, grid } = this.getSnapLines(transform); + const { xLines, yLines, grid } = this.getSnapLines(ignoreList); // Compute the new x and y-positions once. const l = x; @@ -237,7 +237,7 @@ export class SnapManager { for (const line of xLines) { // Distance lines have a bounds that must be close. - if (line.gridItem?.bound && !isOverlapY(cy, aabb.height, line.gridItem?.bound)) { + if (line.gridItem?.aabb && !isOverlapY(cy, aabb.height, line.gridItem?.aabb)) { continue; } @@ -274,7 +274,7 @@ export class SnapManager { for (const line of yLines) { // Distance lines have a bounds that must be close. - if (line.gridItem?.bound && !isOverlapX(cx, aabb.width, line.gridItem?.bound)) { + if (line.gridItem?.aabb && !isOverlapX(cx, aabb.width, line.gridItem?.aabb)) { continue; } @@ -308,7 +308,7 @@ export class SnapManager { return result; } - public getSnapLines(referenceTransform?: Transform) { + public getSnapLines(ignoreList: Record) { if (this.xLines && this.yLines && this.grid) { return { xLines: this.xLines, yLines: this.yLines, grid: this.grid }; } @@ -318,7 +318,7 @@ export class SnapManager { const xLines: SnapLine[] = this.xLines = []; const yLines: SnapLine[] = this.yLines = []; - if (!currentDiagram || !currentView || !referenceTransform) { + if (!currentDiagram || !currentView) { // This should actually never happen, because we call prepare first. const grid = this.grid = []; @@ -326,7 +326,7 @@ export class SnapManager { } // Compute the bounding boxes once. - const bounds = currentDiagram!.items.values.filter(x => x.transform !== referenceTransform).map(x => x.bounds(currentDiagram).aabb); + const bounds = Array.from(currentDiagram!.items.values).filter(x => !ignoreList[x.id]).map(x => x.bounds(currentDiagram).aabb); const grid = this.grid = computeGrid(bounds); @@ -336,18 +336,18 @@ export class SnapManager { yLines.push({ value: 0 }); yLines.push({ value: currentView.y }); - for (const bound of bounds) { - xLines.push({ value: bound.left, side: 'Left' }); - xLines.push({ value: bound.right, side: 'Right' }); - xLines.push({ value: bound.cx, isCenter: true }); + for (const aabb of bounds) { + xLines.push({ value: aabb.left, side: 'Left' }); + xLines.push({ value: aabb.right, side: 'Right' }); + xLines.push({ value: aabb.cx, isCenter: true }); - yLines.push({ value: bound.top, side: 'Top' }); - yLines.push({ value: bound.bottom, side: 'Bottom' }); - yLines.push({ value: bound.cy, isCenter: true }); + yLines.push({ value: aabb.top, side: 'Top' }); + yLines.push({ value: aabb.bottom, side: 'Bottom' }); + yLines.push({ value: aabb.cy, isCenter: true }); } for (const gridItem of grid) { - const bound = gridItem.bound; + const bound = gridItem.aabb; if (gridItem.leftDistance !== Number.MAX_VALUE) { xLines.push({ @@ -389,8 +389,8 @@ export class SnapManager { return { xLines, yLines, grid }; } - public getDebugLines(referenceTransform: Transform) { - const { xLines, yLines } = this.getSnapLines(referenceTransform); + public getDebugLines(ignoreList: Record = {}) { + const { xLines, yLines } = this.getSnapLines(ignoreList); if (this.grid) { for (const line of xLines) { @@ -416,8 +416,8 @@ function enrichLine(line: SnapLine | undefined, grid: GridItem[]) { let current = line.gridItem; // Compute the vertical offsets once to save some compute time. - const x = current.bound.cx; - const y = current.bound.cy; + const x = current.aabb.cx; + const y = current.aabb.cy; switch (line.gridSide) { case 'Left': { @@ -428,7 +428,7 @@ function enrichLine(line: SnapLine | undefined, grid: GridItem[]) { // Travel to the left while the right are the same. while (current) { - positions.push({ x: current.bound.left - distance, y }); + positions.push({ x: current.aabb.left - distance, y }); if (!areSimilar(current.rightDistance, distance)) { break; @@ -447,7 +447,7 @@ function enrichLine(line: SnapLine | undefined, grid: GridItem[]) { // Travel to the left while the distances are the same. while (current) { - positions.push({ x: current.bound.right, y }); + positions.push({ x: current.aabb.right, y }); if (!areSimilar(current.leftDistance, distance)) { break; @@ -467,7 +467,7 @@ function enrichLine(line: SnapLine | undefined, grid: GridItem[]) { // Travel to the bottom while the distances are the same. while (current) { - positions.push({ y: current.bound.top - distance, x }); + positions.push({ y: current.aabb.top - distance, x }); if (!areSimilar(current.bottomDistance, distance)) { break; @@ -486,7 +486,7 @@ function enrichLine(line: SnapLine | undefined, grid: GridItem[]) { // Travel to the top while the distances are the same. while (current) { - positions.push({ y: current.bound.bottom, x }); + positions.push({ y: current.aabb.bottom, x }); if (!areSimilar(current.topDistance, distance)) { break; @@ -507,10 +507,10 @@ function enrichLine(line: SnapLine | undefined, grid: GridItem[]) { function computeGrid(bounds: Rect2[]) { const grid: GridItem[] = []; - for (const bound of bounds) { + for (const aabb of bounds) { // Search for the minimum distance to the left or right. const gridItem: GridItem = { - bound, + aabb, bottomDistance: Number.MAX_VALUE, bottomIndex: -1, leftDistance: Number.MAX_VALUE, @@ -525,12 +525,12 @@ function computeGrid(bounds: Rect2[]) { for (const other of bounds) { j++; - if (other === bound) { + if (other === aabb) { continue; } - if (isOverlapY(other.cy, other.height, bound)) { - const dl = bound.left - other.right; + if (isOverlapY(other.cy, other.height, aabb)) { + const dl = aabb.left - other.right; // If the distance to the left is positive, the other element is on the left side. if (dl > 0 && dl < gridItem.leftDistance) { @@ -538,7 +538,7 @@ function computeGrid(bounds: Rect2[]) { gridItem.leftIndex = j; } - const dr = other.left - bound.right; + const dr = other.left - aabb.right; // If the distance to the right is positive, the other element is on the right side. if (dr > 0 && dr < gridItem.rightDistance) { @@ -547,8 +547,8 @@ function computeGrid(bounds: Rect2[]) { } } - if (isOverlapX(other.cx, other.width, bound)) { - const dt = bound.top - other.bottom; + if (isOverlapX(other.cx, other.width, aabb)) { + const dt = aabb.top - other.bottom; // If the distance to the right is top, the other element is on the top side. if (dt > 0 && dt < gridItem.topDistance) { @@ -556,7 +556,7 @@ function computeGrid(bounds: Rect2[]) { gridItem.topIndex = j; } - const db = other.top - bound.bottom; + const db = other.top - aabb.bottom; // If the distance to the right is bottom, the other element is on the bottom side. if (db > 0 && db < gridItem.bottomDistance) { diff --git a/src/wireframes/renderer/Editor.tsx b/src/wireframes/renderer/Editor.tsx index 9e59341..9d07ff4 100644 --- a/src/wireframes/renderer/Editor.tsx +++ b/src/wireframes/renderer/Editor.tsx @@ -11,7 +11,7 @@ import * as svg from '@svgdotjs/svg.js'; import * as React from 'react'; import { Color, Rect2, Subscription, SVGHelper, Vec2 } from '@app/core'; -import { Diagram, DiagramItem, Transform } from '@app/wireframes/model'; +import { Diagram, DiagramItem, DiagramItemSet, Transform } from '@app/wireframes/model'; import { useOverlayContext } from './../contexts/OverlayContext'; import { CanvasView } from './CanvasView'; import { NavigateAdorner } from './NavigateAdorner'; @@ -32,10 +32,7 @@ export interface EditorProps { masterDiagram?: Diagram; // The selected items. - selectedItems: DiagramItem[]; - - // The selected items including locked items. - selectedItemsWithLocked: DiagramItem[]; + selectionSet: DiagramItemSet; // The zoomed width of the canvas. zoomedSize: Vec2; @@ -59,16 +56,16 @@ export interface EditorProps { onRender?: () => void; // A function to select a set of items. - onSelectItems?: (diagram: Diagram, itemIds: string[]) => any; + onSelectItems?: (diagram: Diagram, itemIds: ReadonlyArray) => any; // A function to change the appearance of a visual. - onChangeItemsAppearance?: (diagram: Diagram, visuals: DiagramItem[], key: string, val: any) => any; + onChangeItemsAppearance?: (diagram: Diagram, visuals: ReadonlyArray, key: string, val: any) => any; // A function that is invoked when the user clicked a link. onNavigate?: (item: DiagramItem, link: string) => void; // A function to transform a set of items. - onTransformItems?: (diagram: Diagram, items: DiagramItem[], oldBounds: Transform, newBounds: Transform) => any; + onTransformItems?: (diagram: Diagram, items: ReadonlyArray, oldBounds: Transform, newBounds: Transform) => any; } export const Editor = React.memo((props: EditorProps) => { @@ -82,8 +79,7 @@ export const Editor = React.memo((props: EditorProps) => { onRender, onSelectItems, onTransformItems, - selectedItems, - selectedItemsWithLocked, + selectionSet, viewBox, viewSize, zoom, @@ -183,7 +179,7 @@ export const Editor = React.memo((props: EditorProps) => { overlayManager={overlayContext.overlayManager} previewStream={renderPreview.current} selectedDiagram={diagram} - selectedItems={selectedItems} + selectionSet={selectionSet} snapManager={overlayContext.snapManager} viewSize={viewSize} zoom={zoom} @@ -197,7 +193,7 @@ export const Editor = React.memo((props: EditorProps) => { onSelectItems={onSelectItems} previewStream={renderPreview.current} selectedDiagram={diagram} - selectedItems={selectedItemsWithLocked} + selectionSet={selectionSet} overlayManager={overlayContext.overlayManager} onChangeItemsAppearance={onChangeItemsAppearance} zoom={zoom} @@ -209,7 +205,7 @@ export const Editor = React.memo((props: EditorProps) => { interactionService={interactionMainService} onChangeItemsAppearance={onChangeItemsAppearance} selectedDiagram={diagram} - selectedItems={selectedItems} + selectionSet={selectionSet} zoom={zoom} /> } diff --git a/src/wireframes/renderer/SelectionAdorner.tsx b/src/wireframes/renderer/SelectionAdorner.tsx index 48cdcd0..3eb62e2 100644 --- a/src/wireframes/renderer/SelectionAdorner.tsx +++ b/src/wireframes/renderer/SelectionAdorner.tsx @@ -9,7 +9,7 @@ import * as svg from '@svgdotjs/svg.js'; import * as React from 'react'; import { isModKey, Rect2, Subscription, SVGHelper, Vec2 } from '@app/core'; -import { calculateSelection, Diagram, DiagramItem, Transform } from '@app/wireframes/model'; +import { calculateSelection, Diagram, DiagramItem, Transform, DiagramItemSet } from '@app/wireframes/model'; import { vogues, shapes } from '@app/const'; import { InteractionHandler, InteractionService, SvgEvent } from './interaction-service'; import { PreviewEvent } from './preview'; @@ -27,7 +27,7 @@ export interface SelectionAdornerProps { selectedDiagram: Diagram; // The selected items. - selectedItems: DiagramItem[]; + selectionSet: DiagramItemSet; // The interaction service. interactionService: InteractionService; @@ -39,7 +39,7 @@ export interface SelectionAdornerProps { previewStream: Subscription; // A function to select a set of items. - onSelectItems: (diagram: Diagram, itemIds: string[]) => any; + onSelectItems: (diagram: Diagram, itemIds: ReadonlyArray) => any; // A function to change the appearance of a visual. onChangeItemsAppearance: (diagram: Diagram, visuals: DiagramItem[], key: string, val: any) => any; @@ -56,7 +56,7 @@ export class SelectionAdorner extends React.Component imp // Use a stream of preview updates to bypass react for performance reasons. this.props.previewStream.subscribe(event => { if (event.type === 'Update') { - this.markItems(event.items); + this.markItems({}); } else { this.markItems(); } @@ -153,21 +153,20 @@ export class SelectionAdorner extends React.Component imp this.dragStart = null; } - private selectMultiple(rect: Rect2, diagram: Diagram): string[] { + private selectMultiple(rect: Rect2, diagram: Diagram): ReadonlyArray { const selectedItems = diagram.rootItems.filter(i => rect.contains(i.bounds(diagram).aabb)); return calculateSelection(selectedItems, diagram, false); } - private selectSingle(event: SvgEvent, diagram: Diagram): string[] { + private selectSingle(event: SvgEvent, diagram: Diagram): ReadonlyArray { const isMod = isModKey(event.event); if (isMod) { event.event.preventDefault(); } - const aabb = event.shape?.bounds(diagram).aabb; - if (aabb?.contains(event.position) && event.shape) { + if (event.shape) { return calculateSelection([event.shape], diagram, true, isMod); } else { return []; @@ -179,7 +178,7 @@ export class SelectionAdorner extends React.Component imp adorner.hide(); } - const selection = this.props.selectedItems; + const selection = preview ? Object.values(preview) : Array.from(this.props.selectionSet.selection.values()); // Add more markers if we do not have enough. while (this.selectionMarkers.length < selection.length) { @@ -191,7 +190,7 @@ export class SelectionAdorner extends React.Component imp // Use the inverted zoom level as stroke width. const strokeWidth = 1 / this.props.zoom; - this.props.selectedItems.forEach((item, i) => { + selection.forEach((item, i) => { const marker = this.selectionMarkers[i]; const color = @@ -202,7 +201,7 @@ export class SelectionAdorner extends React.Component imp // Use the inverted zoom level as stroke width to have a constant stroke style. marker.stroke({ color, width: strokeWidth }); - const actualItem = preview?.[item.id] || item; + const actualItem = item; const actualBounds = actualItem.bounds(this.props.selectedDiagram); // Also adjust the bounds by the border width, to show the border outside of the shape. diff --git a/src/wireframes/renderer/TextAdorner.tsx b/src/wireframes/renderer/TextAdorner.tsx index 92219da..2266156 100644 --- a/src/wireframes/renderer/TextAdorner.tsx +++ b/src/wireframes/renderer/TextAdorner.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import { Keys, Vec2, sizeInPx } from '@app/core'; -import { Diagram, DiagramItem } from '@app/wireframes/model'; +import { Diagram, DiagramItem, DiagramItemSet } from '@app/wireframes/model'; import { InteractionHandler, InteractionService, SvgEvent } from './interaction-service'; import { getSelectedCell, getTableAttributes } from '../shapes/dependencies'; import { texts, shapes } from '@app/const'; @@ -21,7 +21,7 @@ export interface TextAdornerProps { selectedDiagram: Diagram; // The selected items. - selectedItems: DiagramItem[]; + selectionSet: DiagramItemSet; // The interaction service. interactionService: InteractionService; @@ -50,7 +50,7 @@ export class TextAdorner extends React.PureComponent implement } public componentDidUpdate(prevProps: TextAdornerProps) { - if (this.props.selectedItems !== prevProps.selectedItems) { + if (this.props.selectionSet !== prevProps.selectionSet) { this.updateText(); } } diff --git a/src/wireframes/renderer/TransformAdorner.tsx b/src/wireframes/renderer/TransformAdorner.tsx index 3933591..da468a3 100644 --- a/src/wireframes/renderer/TransformAdorner.tsx +++ b/src/wireframes/renderer/TransformAdorner.tsx @@ -9,7 +9,7 @@ import * as svg from '@svgdotjs/svg.js'; import * as React from 'react'; import { Rotation, Subscription, SVGHelper, Timer, Vec2 } from '@app/core'; -import { Diagram, DiagramItem, SnapManager, SnapMode, Transform } from '@app/wireframes/model'; +import { Diagram, DiagramItem, SnapManager, SnapMode, Transform, DiagramItemSet } from '@app/wireframes/model'; import { vogues } from '@app/const'; import { OverlayManager } from './../contexts/OverlayContext'; import { SVGRenderer2 } from './../shapes/utils/svg-renderer2'; @@ -35,7 +35,7 @@ export interface TransformAdornerProps { selectedDiagram: Diagram; // The selected items. - selectedItems: DiagramItem[]; + selectionSet: DiagramItemSet; // The interaction service. interactionService: InteractionService; @@ -50,7 +50,7 @@ export interface TransformAdornerProps { previewStream: Subscription; // A function to transform a set of items. - onTransformItems: (diagram: Diagram, items: DiagramItem[], oldBounds: Transform, newBounds: Transform) => void; + onTransformItems: (diagram: Diagram, items: ReadonlyArray, oldBounds: Transform, newBounds: Transform) => void; } export class TransformAdorner extends React.PureComponent implements InteractionHandler { @@ -105,18 +105,18 @@ export class TransformAdorner extends React.PureComponent } private hasSelection(): boolean { - return this.props.selectedItems.length > 0; + return this.props.selectionSet.selectedItems.length > 0; } private calculateInitializeTransform() { let transform: Transform; - if (this.props.selectedItems.length === 0) { + if (this.props.selectionSet.selectedItems.length === 0) { transform = Transform.ZERO; - } else if (this.props.selectedItems.length === 1) { - transform = this.props.selectedItems[0].bounds(this.props.selectedDiagram); + } else if (this.props.selectionSet.selectedItems.length === 1) { + transform = this.props.selectionSet.selectedItems[0].bounds(this.props.selectedDiagram); } else { - const bounds = this.props.selectedItems.map(x => x.bounds(this.props.selectedDiagram)); + const bounds = this.props.selectionSet.selectedItems.map(x => x.bounds(this.props.selectedDiagram)); transform = Transform.createFromTransformationsAndRotation(bounds, this.rotation); } @@ -128,7 +128,7 @@ export class TransformAdorner extends React.PureComponent this.canResizeX = false; this.canResizeY = false; - for (const item of this.props.selectedItems) { + for (const item of this.props.selectionSet.selectedItems) { if (item.constraint) { if (!item.constraint.calculateSizeX()) { this.canResizeX = true; @@ -229,7 +229,7 @@ export class TransformAdorner extends React.PureComponent this.props.onTransformItems( this.props.selectedDiagram, - this.props.selectedItems, + this.props.selectionSet.selectedItems, this.startTransform, this.transform ); @@ -322,7 +322,7 @@ export class TransformAdorner extends React.PureComponent private renderPreview() { const items: Record = {}; - for (const item of this.props.selectedItems) { + for (const item of this.props.selectionSet.selectedItems) { items[item.id] = item.transformByBounds(this.startTransform, this.transform); } @@ -331,7 +331,7 @@ export class TransformAdorner extends React.PureComponent } private move(delta: Vec2, snapMode: SnapMode, showOverlay = false) { - const snapResult = this.props.snapManager.snapMoving(this.startTransform, delta, snapMode); + const snapResult = this.props.snapManager.snapMoving(this.startTransform, delta, snapMode, this.props.selectionSet.deepEditableItems); this.transform = this.startTransform.moveBy(snapResult.delta); @@ -391,7 +391,8 @@ export class TransformAdorner extends React.PureComponent const snapResult = this.props.snapManager.snapResizing(this.startTransform, delta, snapMode, this.manipulationOffset.x, - this.manipulationOffset.y); + this.manipulationOffset.y, + this.props.selectionSet); return snapResult; } @@ -448,7 +449,7 @@ export class TransformAdorner extends React.PureComponent this.props.onTransformItems( this.props.selectedDiagram, - this.props.selectedItems, + this.props.selectionSet.deepEditableItems, this.startTransform, this.transform ); diff --git a/src/wireframes/renderer/interaction-overlays.ts b/src/wireframes/renderer/interaction-overlays.ts index 2807198..0fde4e6 100644 --- a/src/wireframes/renderer/interaction-overlays.ts +++ b/src/wireframes/renderer/interaction-overlays.ts @@ -10,8 +10,8 @@ import * as svg from '@svgdotjs/svg.js'; import { vogues } from '@app/const'; import { SnapLine, SnapResult, Transform } from '@app/wireframes/model'; -const MIN_VALUE = -1000; -const MAX_VALUE = 1000; +const MIN_VALUE = -10000; +const MAX_VALUE = 10000; export class InteractionOverlays { private readonly lines: svg.Line[] = []; diff --git a/src/wireframes/shapes/neutral/textbox.ts b/src/wireframes/shapes/neutral/textbox.ts index d6d7d19..19bffd9 100644 --- a/src/wireframes/shapes/neutral/textbox.ts +++ b/src/wireframes/shapes/neutral/textbox.ts @@ -13,7 +13,7 @@ const DEFAULT_APPEARANCE = { [shapes.key.fontSize]: shapes.common.fontSize, [shapes.key.foregroundColor]: shapes.common.textColor, [shapes.key.textAlignment]: 'left', - [shapes.key.text]: 'Text', + [shapes.key.text]: '', }; export class Textbox implements ShapePlugin { diff --git a/src/wireframes/shapes/utils/abstract-control.ts b/src/wireframes/shapes/utils/abstract-control.ts index 6cb720f..ad40834 100644 --- a/src/wireframes/shapes/utils/abstract-control.ts +++ b/src/wireframes/shapes/utils/abstract-control.ts @@ -8,7 +8,7 @@ import * as svg from '@svgdotjs/svg.js'; import { Rect2, SVGHelper } from '@app/core'; -import { ConfigurableFactory, Constraint, ConstraintFactory, RenderContext, ShapePlugin, Size } from '@app/wireframes/interface'; +import { ConfigurableFactory, Constraint, ConstraintFactory, RenderContext, ShapePlugin } from '@app/wireframes/interface'; import { ColorConfigurable, DiagramItem, MinSizeConstraint, NumberConfigurable, Renderer, SelectionConfigurable, SizeConstraint, SliderConfigurable, TextConfigurable, TextHeightConstraint, ToggleConfigurable } from '@app/wireframes/model'; import { SVGRenderer2 } from './svg-renderer2'; import { TextSizeConstraint } from './text-size-contraint'; @@ -97,11 +97,13 @@ export class AbstractControl implements Renderer { } public render(shape: DiagramItem, existing: svg.G | undefined, options?: { debug?: boolean; noOpacity?: boolean; noTransform?: boolean }): any { + const localRect = new Rect2(0, 0, shape.transform.size.x, shape.transform.size.y); + + // Reuse a global context to make the code easier. GLOBAL_CONTEXT.shape = shape; - GLOBAL_CONTEXT.rect = new Rect2(0, 0, shape.transform.size.x, shape.transform.size.y); + GLOBAL_CONTEXT.rect = localRect; const container = SVGRenderer2.INSTANCE.getContainer(); - const index = (options?.debug) ? 2 : 1; // Use full color codes here to avoid the conversion in svg.js if (!existing) { @@ -113,11 +115,27 @@ export class AbstractControl implements Renderer { } } - for (let i = 0; i < index; i++) { - SVGHelper.transformByRect(existing.get(i), GLOBAL_CONTEXT.rect); + // Calculate a special selection rect, that is slightly bigger than the bounds to make selection easier. + let selectionRect = GLOBAL_CONTEXT.rect; + + const diffW = Math.max(0, MIN_DIMENSIONS - selectionRect.width); + const diffH = Math.max(0, MIN_DIMENSIONS - selectionRect.height); + + if (diffW > 0 || diffH > 0) { + selectionRect = selectionRect.inflate(diffW * 0.5, diffH * 0.5); } - SVGRenderer2.INSTANCE.setContainer(existing, index); + SVGHelper.transformByRect(existing.get(0), selectionRect); + + // The index of the main element that holds the reference. + let mainIndex = 1; + + if (options?.debug) { + SVGHelper.transformByRect(existing.get(1), localRect); + mainIndex++; + } + + SVGRenderer2.INSTANCE.setContainer(existing, mainIndex); this.shapePlugin.render(GLOBAL_CONTEXT); @@ -146,112 +164,6 @@ export class AbstractControl implements Renderer { } } -export class AbstractControlCells implements Renderer { - constructor( - private readonly shapePlugin: ShapePlugin[], - ) { - } - - public identifier() { - return this.shapePlugin[0].identifier(); - } - - public plugin() { - return this.shapePlugin[0]; - } - - public defaultAppearance() { - return this.shapePlugin[0].defaultAppearance?.(); - } - - public setContext(context: any): Renderer { - SVGRenderer2.INSTANCE.setContainer(context); - return this; - } - - public createDefaultShape() { - const appearance = this.shapePlugin[0].defaultAppearance(); - const constraint = this.shapePlugin[0].constraint?.(DefaultConstraintFactory.INSTANCE); - const configurables = this.shapePlugin[0].configurables?.(DefaultConfigurableFactory.INSTANCE); - const renderer = this.identifier(); - const size: Size = { - x: this.shapePlugin[0].defaultSize().x * 2, - y: this.shapePlugin[0].defaultSize().y, - }; - - return { renderer, size, appearance, configurables, constraint }; - } - - public render(shape: DiagramItem, existing: svg.G | undefined, options?: { debug?: boolean; noOpacity?: boolean; noTransform?: boolean }): any { - const numShapePlugin = this.shapePlugin.length; - const index = (options?.debug) ? 2 : 1; - const container = SVGRenderer2.INSTANCE.getContainer(); - GLOBAL_CONTEXT.shape = shape; - - // Use full color codes here to avoid the conversion in svg.js - if (!existing) { - existing = new svg.G(); - existing.add(new svg.Rect().fill('#ffffff').opacity(0.001)); - - if (options?.debug) { - existing.rect().fill('#ffffff').stroke({ color: '#ff0000' }); - } - - for (let i = 0; i < this.shapePlugin.length; i++) { - var subExisting = new svg.G(); - subExisting.add(new svg.Rect().fill('#ffffff').opacity(0.001)); - - if (options?.debug) { - subExisting.rect().fill('#ffffff').stroke({ color: '#ff0000' }); - } - - SVGRenderer2.INSTANCE.setContainer(subExisting, index); - - const shapePlugin = this.shapePlugin[i]; - const width = shape.transform.size.x / numShapePlugin; - - GLOBAL_CONTEXT.rect = new Rect2(i * width, 0, width, shape.transform.size.y); - shapePlugin.render(GLOBAL_CONTEXT); - - for (let i = 0; i < index; i++) { - SVGHelper.transformByRect(subExisting.get(i), GLOBAL_CONTEXT.rect); - } - - if (!options?.noTransform) { - SVGHelper.transformBy(subExisting, { - x: i * width, - w: width, - }); - } - - existing.add(subExisting); - } - } - - GLOBAL_CONTEXT.rect = new Rect2(0, 0, shape.transform.size.x, shape.transform.size.y); - for (let i = 0; i < index; i++) { - SVGHelper.transformByRect(existing.get(i), GLOBAL_CONTEXT.rect); - } - - if (!options?.noTransform) { - const to = shape.transform; - - SVGHelper.transformBy(existing, { - x: to.position.x - 0.5 * to.size.x, - y: to.position.y - 0.5 * to.size.y, - w: to.size.x, - h: to.size.y, - rx: to.position.x, - ry: to.position.y, - rotation: to.rotation.degree, - }); - } - - SVGRenderer2.INSTANCE.cleanupAll(); - SVGRenderer2.INSTANCE.setContainer(container); - - return existing; - } -} +const MIN_DIMENSIONS = 10; -type Writeable = { -readonly [P in keyof T]: T[P] }; +type Writeable = { -readonly [P in keyof T]: T[P] }; \ No newline at end of file diff --git a/src/wireframes/shapes/utils/svg-renderer2.spec.ts b/src/wireframes/shapes/utils/svg-renderer2.spec.ts index 5ec8e05..2f67825 100644 --- a/src/wireframes/shapes/utils/svg-renderer2.spec.ts +++ b/src/wireframes/shapes/utils/svg-renderer2.spec.ts @@ -366,7 +366,7 @@ describe('SVGRenderer2', () => { }); }); - expect((svgGroup.get(0).node.children[0]).textContent).toEqual('Text'); + expect((svgGroup.get(0).node.children[0] as HTMLDivElement).innerText).toEqual('Text'); }); it('should render text from shape', () => { @@ -376,7 +376,7 @@ describe('SVGRenderer2', () => { }); }); - expect((svgGroup.get(0).node.children[0]).textContent).toEqual('Text'); + expect((svgGroup.get(0).node.children[0] as HTMLDivElement).innerText).toEqual('Text'); }); }); diff --git a/sw.js b/sw.js deleted file mode 100644 index 09d7587..0000000 --- a/sw.js +++ /dev/null @@ -1,16 +0,0 @@ -self.addEventListener('install', function (event) { - // Perform install steps -}); - -self.addEventListener('fetch', function (event) { - event.respondWith( - caches.match(event.request) - .then(function (response) { - // Cache hit - return response - if (response) { - return response; - } - return fetch(event.request); - }) - ); -}); \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 35b7365..eae4dfa 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,8 @@ import path from 'path'; import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; import { VitePWA } from 'vite-plugin-pwa'; +import { codecovVitePlugin } from "@codecov/vite-plugin"; +import autoprefixer from 'autoprefixer'; // https://vitejs.dev/config/ export default defineConfig({ @@ -19,5 +21,18 @@ export default defineConfig({ ... (process.env.NODE_ENV === 'production' ? [VitePWA({ injectRegister: null })] : []), react({}), + codecovVitePlugin({ + enableBundleAnalysis: process.env.CODECOV_TOKEN !== undefined, + bundleName: "CodeSlide", + uploadToken: process.env.CODECOV_TOKEN, + }), ], + + css: { + postcss: { + plugins: [ + autoprefixer({}) // add options if needed + ], + } + } }); \ No newline at end of file