diff --git a/.editorconfig b/.editorconfig index 516f5ba597..348eaed0b4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,5 +3,5 @@ charset=utf-8 end_of_line=lf indent_size=4 indent_style=space -insert_final_newline=false +insert_final_newline=true trim_trailing_whitespace=true diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 179405cb58..bb2f9235f7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,56 +1,64 @@ +# project +.github @jikkai +docs @wzhudev + # core -packages/core @DR-Univer @wzhudev +packages/core @wzhudev +packages/core/src/services/resource-loader @Gggpound packages/core/src/services/resource-manager @Gggpound packages/core/src/services/permission @Gggpound -# engine -packages/engine-render @DR-Univer @wzhudev +# basics +packages/design @jikkai packages/engine-formula @DR-Univer packages/engine-numfmt @Gggpound - -# ui -packages/ui @jikkai @wzhudev @Jocs -packages/ui/src/services/clipboard @yuhongz -packages/design @jikkai @wzhudev - -# common +packages/engine-render @DR-Univer @lumixraku +packages/find-replace @siam-ese packages/image @DR-Univer +packages/network @wzhudev +packages/rpc @wzhudev +packages/ui @jikkai @Jocs +packages/ui/src/services/clipboard @ybzky +packages/ui/src/services/popup/canvas-popup.service.ts @weird94 packages/uniscript @wzhudev -packages/find-replace @wzhudev -# docs +# facade +packages/facade @dushusir @hexf00 + +# business - docs packages/docs @DR-Univer @Jocs packages/docs-ui @jikkai @Jocs -# sheets +# business - sheets packages/sheets @wzhudev @DR-Univer -packages/sheets/src/services/numfmt @Gggpound -packages/sheets/src/services/ref-range @Gggpound - packages/sheets-ui @jikkai @wzhudev packages/sheets-ui/src/controllers/clipboard @yuhongz packages/sheets-ui/src/controllers/editor @Jocs packages/sheets-ui/src/services/clipboard @yuhongz packages/sheets-ui/src/services/editor @Jocs packages/sheets-ui/src/services/editor-bridge.service.ts @Jocs - -packages/sheets-find-replace @wzhudev - +packages/sheets/src/services/numfmt @Gggpound +packages/sheets/src/services/ref-range @Gggpound +packages/sheets/src/services/ref-range @weird94 +packages/sheets/src/services/sheet-interceptor @Gggpound +## find-replace +packages/sheets-find-replace @siam-ese +## number-format packages/sheets-numfmt @Gggpound - -packages/sheets-import-xlsx @Gggpound - +## formula packages/sheets-formula @DR-Univer @Dushusir - +## conditional-formatting +packages/sheets-conditional-formatting @Gggpound +packages/sheets-conditional-formatting-ui @Gggpound +## data validation +packages/data-validation @weird94 +packages/sheet-data-validation @weird94 +## filter +packages/sheets-filter @ybzky @yuhongz +packages/sheets-filter-ui @ybzky @yuhongz +## misc packages/sheets-zen-editor @Jocs -# slides -packages/slides @wzhudev @DR-Univer @jikkai -packages/slides-ui @jikkai @wzhudev - -# formula -packages/formula @DR-Univer - -# network -packages/rpc @wzhudev -packages/network @wzhudev +# business - slides +packages/slides @DR-Univer @jikkai +packages/slides-ui @jikkai diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index 07cf8dfe49..8ff4fa64c5 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -7,7 +7,7 @@ runs: - name: Setup pnpm uses: pnpm/action-setup@v2 with: - version: 8 + version: 9 run_install: false - name: Setup Node.js diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 290cb841ae..e066ed1e1c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,8 +6,7 @@ - -close #xxx, #yyy, #zzzz +close #xxx @@ -18,3 +17,10 @@ close #xxx, #yyy, #zzzz Before: After: --> + +## Pull Request Checklist + +- [ ] Related tickets or issues have been linked in the PR description (or missing issue). +- [ ] [Naming convention](https://github.com/dream-num/univer/blob/dev/docs/NAMING_CONVENTION.md) is followed (**do please** check it especially when you created new plugins, commands and resources). +- [ ] Unit tests have been added for the changes (if applicable). +- [ ] Breaking changes have been documented (or no breaking changes introduced in this PR). diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1722aba0b4..fd3a339f23 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: uses: ./.github/actions/setup-node - name: 📦 Build - run: pnpm run build + run: pnpm build set-pr-info: name: set PR info diff --git a/.github/workflows/ci-lint.yml b/.github/workflows/ci-lint.yml deleted file mode 100644 index 1b79554532..0000000000 --- a/.github/workflows/ci-lint.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: 🩺 CI Lint - -on: - push: - branches: [main, dev] - pull_request: - branches: [main, dev] - -permissions: - contents: read - -jobs: - eslint: - runs-on: ubuntu-latest - - env: - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - TURBO_TEAM: ${{ vars.TURBO_TEAM }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: ./.github/actions/setup-node - - - name: 🌡️ Run ESLint - run: pnpm run lint - - typecheck: - runs-on: ubuntu-latest - - env: - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - TURBO_TEAM: ${{ vars.TURBO_TEAM }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: ./.github/actions/setup-node - - - name: 🧼 Type checking - run: npm run lint:types diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 29195fdae3..1629beb8c7 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -29,3 +29,37 @@ jobs: uses: codecov/codecov-action@v3 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + eslint: + runs-on: ubuntu-latest + + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: ./.github/actions/setup-node + + - name: 🌡️ Run ESLint + run: pnpm lint --quiet + + typecheck: + runs-on: ubuntu-latest + + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: ./.github/actions/setup-node + + - name: 🧼 Type checking + run: pnpm lint:types diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 3d7efc0d76..2ad9a90a5c 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -2,25 +2,37 @@ name: 🎭 Playwright Tests on: push: - branches: [main, master] + branches: [main, dev] pull_request: - branches: [main, master] + branches: [main, dev] jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} + steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + ref: ${{ github.head_ref }} - name: Setup Node.js uses: ./.github/actions/setup-node - name: Install Playwright Browsers - run: pnpm exec playwright install --with-deps + run: pnpm exec playwright install --with-deps chromium + + # build the example locally and run from the built files + - name: Build E2E Client + run: pnpm build:e2e - - name: Run Playwright tests + - name: Run Playwright Tests run: pnpm exec playwright test - uses: actions/upload-artifact@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a09e5d7432..6302c69562 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,19 +55,19 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v2 with: - version: 8 + version: 9 run_install: false - name: Install Node.js uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 registry-url: https://registry.npmjs.org - name: 🚚 Install dependencies and build run: | pnpm install - pnpm run build + pnpm build - name: 🐙 Publish run: | @@ -93,23 +93,23 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v2 with: - version: 8 + version: 9 run_install: false - name: Install Node.js uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 - name: 🚚 Install dependencies and build run: | pnpm install - pnpm run build + pnpm build - name: Install Node.js uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 registry-url: ${{ secrets.VERDACCIO_URL }} scope: '@univerjs' @@ -139,7 +139,7 @@ jobs: # ================= Deploy Demo ================= - name: 📦 Build demo - run: pnpm run build:demo + run: pnpm build:demo - name: Copy demo to workspace run: | diff --git a/.npmrc b/.npmrc index c27d73929e..5e202b15e7 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1,4 @@ auto-install-peers=true git-checks=false strict-peer-dependencies=false +package-manager-strict=false diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a0b02c08ec..03a155229d 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,12 +2,11 @@ "recommendations": [ "dbaeumer.vscode-eslint", "editorconfig.editorconfig", - "esbenp.prettier-vscode", "gruntfuggly.todo-tree", "intellsmi.comment-translate", + "kingwl.vscode-vitest-runner", "streetsidesoftware.code-spell-checker", "tldraw-org.tldraw-vscode", - "kingwl.vscode-vitest-runner", "usernamehw.errorlens" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 0fe39aab5f..ef48724005 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,16 +23,22 @@ "customd", "Datas", "DATEFMTLISG", + "demi", + "dppx", "Dushusir", "endindex", "esbuild", + "Ethi", "evented", + "excalidraw", "execa", + "EXTRALIGHT", "FONTFACE", "fract", "gainsboro", "GANTT", "Gggpound", + "Govett", "Gridlines", "Hanalei", "hdmx", @@ -52,6 +58,8 @@ "kcode", "keyof", "Lerp", + "letterform", + "liga", "linebreak", "linebreaker", "linebreaking", @@ -61,12 +69,17 @@ "Luckysheet", "makearray", "mengshukeji", + "Monoton", "Neue", "numfmt", + "OOXML", + "opencollective", + "opentype", "Overlines", "Pacifico", "Plass", "pnmp", + "Polyton", "ponyfill", "PWDEBUG", "Quan", @@ -98,6 +111,7 @@ "Xinwei", "Xlookup", "XMATCH", + "XSSI", "yuhongz" ], "vsicons.presets.angular": false, diff --git a/CHANGELOG.md b/CHANGELOG.md index 6760474944..a4556681d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,336 @@ +## [0.1.12](https://github.com/dream-num/univer/compare/v0.1.11...v0.1.12) (2024-05-24) + + +### Bug Fixes + +* **engine-render:** fix "require("fs")" issue ([#2278](https://github.com/dream-num/univer/issues/2278)) ([0e25997](https://github.com/dream-num/univer/commit/0e259973b50cfa8c261dbdc8db7425854e3aa98f)) +* export necessary types from ui package ([c648310](https://github.com/dream-num/univer/commit/c64831091a758eedb88baad8a4bb8e4488cfc697)) +* fix identifier name ([c1b4e8b](https://github.com/dream-num/univer/commit/c1b4e8b51d6f10bede03248723e4b1c75de4c3d7)) +* fix shortcut panel content not correct ([#2267](https://github.com/dream-num/univer/issues/2267)) ([bf5880a](https://github.com/dream-num/univer/commit/bf5880a8b2dabad6621ad80cb68e3ed7f79657bb)) +* resolve warning about nested component updates from render methods ([#2274](https://github.com/dream-num/univer/issues/2274)) ([4e7b4c5](https://github.com/dream-num/univer/commit/4e7b4c5c0b55382b3933d6e1b3f0a1b50e94508c)) +* **sheet:** add info type for Message component, use barColor in ProgressBar ([#2277](https://github.com/dream-num/univer/issues/2277)) ([7b411b3](https://github.com/dream-num/univer/commit/7b411b398d5a3dd18aeff871abc847a770eb57f3)) +* **sheets-thread-comment:** fix some ui issues & update readme.md ([#2294](https://github.com/dream-num/univer/issues/2294)) ([b230655](https://github.com/dream-num/univer/commit/b230655dfca8f8949269944b183417759ac71367)) +* **sheets-ui:** fix doc plugins not loaded before sheet editor ([#2279](https://github.com/dream-num/univer/issues/2279)) ([e467c1e](https://github.com/dream-num/univer/commit/e467c1efbd3849278c44c73e7fa8b33025fa7c19)) +* **ui:** fix canvas popup direction definition error ([0690697](https://github.com/dream-num/univer/commit/069069759cebc8818f99732e743e2df4e24d8377)) +* **ui:** fix position error ([57d4e8a](https://github.com/dream-num/univer/commit/57d4e8a980acb10ac446de66f2ca50f4f0c49e84)) +* **uniscript:** export module for pro ([#2293](https://github.com/dream-num/univer/issues/2293)) ([e7ad8d5](https://github.com/dream-num/univer/commit/e7ad8d5c3e1ddb7bf6071ebd18209ddfba27ac58)) + + +* feat(ui)!: add support for hiding context menu (#2275) ([3f12ad8](https://github.com/dream-num/univer/commit/3f12ad80188a83283bcd95c65e6c5dcc2d23ad72)), closes [#2275](https://github.com/dream-num/univer/issues/2275) + + +### Features + +* add support for customizable context menu & toolbar ([#2273](https://github.com/dream-num/univer/issues/2273)) ([b253997](https://github.com/dream-num/univer/commit/b253997ce3126fc99f2829a76a3b7cc5b1476a95)) +* **core:** command service support get command type ([6775a73](https://github.com/dream-num/univer/commit/6775a7310decfdad122b48344a6ff93d47651ea5)) +* extract debugger plugin to standalone package ([#2269](https://github.com/dream-num/univer/issues/2269)) ([f89e3bd](https://github.com/dream-num/univer/commit/f89e3bd2af1860a5081bdafae2b686c7fb5d04b0)) +* extract hooks for better customization ([#2301](https://github.com/dream-num/univer/issues/2301)) ([2b75400](https://github.com/dream-num/univer/commit/2b75400dd788b84b6b15872c5aea7b95b236aaeb)) +* **render-engine:** facade adds onCellPointerOver,onCellDragOver,onCellDrop ([#2240](https://github.com/dream-num/univer/issues/2240)) ([874fa27](https://github.com/dream-num/univer/commit/874fa271dae7d25acd6ef6c8e564da8b70d58aaa)) +* **sheets-conditional-formatting-ui:** refactor manage rule selection options ([ba56b60](https://github.com/dream-num/univer/commit/ba56b60960d1e0555484f115b190c7d9a836ff7e)) +* **sheets-thread-comment:** comment support for sheets ([#2228](https://github.com/dream-num/univer/issues/2228)) ([313ab79](https://github.com/dream-num/univer/commit/313ab796d7d951838d593d3801a1771d39e13153)), closes [#2121](https://github.com/dream-num/univer/issues/2121) [#2120](https://github.com/dream-num/univer/issues/2120) [#2114](https://github.com/dream-num/univer/issues/2114) [#2112](https://github.com/dream-num/univer/issues/2112) [#684](https://github.com/dream-num/univer/issues/684) [#2174](https://github.com/dream-num/univer/issues/2174) [#2162](https://github.com/dream-num/univer/issues/2162) [#715](https://github.com/dream-num/univer/issues/715) +* **sheets-thread-comment:** optimize comment mention source injector method ([#2303](https://github.com/dream-num/univer/issues/2303)) ([a5a7b33](https://github.com/dream-num/univer/commit/a5a7b33f37e413cf6f28d4a5c9c4428c9b53be3b)) +* **thread-comment:** add data-source-service export ([#2296](https://github.com/dream-num/univer/issues/2296)) ([41d0639](https://github.com/dream-num/univer/commit/41d0639e9a4393bfa7e727699e23712b0e070298)) +* **ui:** popup enhance direction ([#2281](https://github.com/dream-num/univer/issues/2281)) ([e9c27b7](https://github.com/dream-num/univer/commit/e9c27b70c66008f34ff7f27d3913cc3f866199bd)) +* **ui:** ui parts service support more generic situations ([#2286](https://github.com/dream-num/univer/issues/2286)) ([415d5b8](https://github.com/dream-num/univer/commit/415d5b83b246b51ec09ee0f76d6e74daf447df85)) +* **umd:** add thread comment support to the UMD bundle ([#2288](https://github.com/dream-num/univer/issues/2288)) ([d909b5f](https://github.com/dream-num/univer/commit/d909b5f01b0f285f31c5ecc2526231a20dc376bc)) + + +### BREAKING CHANGES + +* The default values for `header` and `footer` are now set to true. + +* feat: update examples + +## [0.1.11](https://github.com/dream-num/univer/compare/v0.1.10...v0.1.11) (2024-05-17) + + +### Bug Fixes + +* backspace in list when select all list content ([#2230](https://github.com/dream-num/univer/issues/2230)) ([e58a69e](https://github.com/dream-num/univer/commit/e58a69e97331494a2224909222b08c2c5efa4334)) +* edit cell and the content is not display ([#2245](https://github.com/dream-num/univer/issues/2245)) ([08f41cf](https://github.com/dream-num/univer/commit/08f41cfaee8ed86b5fb395817cb4f99bbbf96db4)) +* **editor:** focusing error ([#2264](https://github.com/dream-num/univer/issues/2264)) ([0831994](https://github.com/dream-num/univer/commit/0831994c658896227ded09e64332e1848b2c3560)) +* filterRenderController works after selectionRenderService is usable ([#2236](https://github.com/dream-num/univer/issues/2236)) ([3693e7a](https://github.com/dream-num/univer/commit/3693e7ad1b647f1ac3e8cae973d8eb27310b5280)) +* fix docs links ([#2224](https://github.com/dream-num/univer/issues/2224)) ([5f26e90](https://github.com/dream-num/univer/commit/5f26e9094799e387eaa76d037c83add044aa204a)) +* fix facade version ([71fcd08](https://github.com/dream-num/univer/commit/71fcd0873724284ea33f71eb8bfff503ee841fb3)) +* fix resource key of filter ([e832ce3](https://github.com/dream-num/univer/commit/e832ce376b2fa2970921be04a22f1c16c4ba63eb)) +* **formula:** formula string results are displayed as regular strings ([#2206](https://github.com/dream-num/univer/issues/2206)) ([1d1a45f](https://github.com/dream-num/univer/commit/1d1a45fb9d1cc1dbf2ec2c6ab595e181d561074d)) +* inline style undo error at the doc end ([#2241](https://github.com/dream-num/univer/issues/2241)) ([91e6fbc](https://github.com/dream-num/univer/commit/91e6fbceebbce06fa3ceb4cb65fa75f184f68b46)) +* lifecycle event handling in plugin holder ([#2244](https://github.com/dream-num/univer/issues/2244)) ([85af642](https://github.com/dream-num/univer/commit/85af642046feb12ecf842c7ed729be2a029f8789)) +* **sheet:** clear custom field of selection ([#2178](https://github.com/dream-num/univer/issues/2178)) ([edd2af1](https://github.com/dream-num/univer/commit/edd2af1cd893cd4bb9bbcd6e8e1959684145375e)) +* **sheet:** filterRenderController work after selectionRenderControll… ([#2229](https://github.com/dream-num/univer/issues/2229)) ([8f8e80d](https://github.com/dream-num/univer/commit/8f8e80d1051b2e4d8e8ef9c56a0c58fa8805d98b)) +* **sheet:** restore code in setStyleCommand ([#2225](https://github.com/dream-num/univer/issues/2225)) ([e1f4a37](https://github.com/dream-num/univer/commit/e1f4a3712749613e785c6e04efbe77c69e783cda)) +* **sheets-ui:** resolve issue where hidden worksheets cannot be unhidden ([#2258](https://github.com/dream-num/univer/issues/2258)) ([5e02b6e](https://github.com/dream-num/univer/commit/5e02b6ece21c92839d3bb56760d09e9e6c28be34)) +* **sheet:** save edit content when select other tab ([#2160](https://github.com/dream-num/univer/issues/2160)) ([3e02de5](https://github.com/dream-num/univer/commit/3e02de5e486e9716c1d4cf051eec4a0c9c16fb17)) +* **sheets:** bugfix for freeze & hover-manager-service & data-validation ([#2233](https://github.com/dream-num/univer/issues/2233)) ([9636037](https://github.com/dream-num/univer/commit/963603757c74e662899629cdc9e21c556e8b0df5)), closes [#684](https://github.com/dream-num/univer/issues/684) [#2174](https://github.com/dream-num/univer/issues/2174) [#2162](https://github.com/dream-num/univer/issues/2162) +* **sheet:** skip filtered row on setting style ([#2221](https://github.com/dream-num/univer/issues/2221)) ([206e080](https://github.com/dream-num/univer/commit/206e080fa5e7a935f8d90e46c5cdbe8308c578cb)) +* the cursor is displayed incorrectly in the presence of bg color ([#2218](https://github.com/dream-num/univer/issues/2218)) ([84620d4](https://github.com/dream-num/univer/commit/84620d44d37323526c5105ee247457718d364397)) +* **ui:** fix use observable not working in StrictMode ([#2235](https://github.com/dream-num/univer/issues/2235)) ([9929eff](https://github.com/dream-num/univer/commit/9929effb03a155575f2084cdedef0ab690394248)) +* use Singleton Pattern of Hyphen ([#2242](https://github.com/dream-num/univer/issues/2242)) ([ba853df](https://github.com/dream-num/univer/commit/ba853df98f8a213df41f5ca48dd4bbb511f4edcb)) + + +### Features + +* add Russian translation support ([#2248](https://github.com/dream-num/univer/issues/2248)) ([87e0f84](https://github.com/dream-num/univer/commit/87e0f8488006a75749ec2ed03cd75eb2a347aff8)) +* **design:** add borderless support for Select ([#2254](https://github.com/dream-num/univer/issues/2254)) ([c59b5a5](https://github.com/dream-num/univer/commit/c59b5a52501bab2b105af746fbf0d21f0fd61e35)) +* **design:** add vertical layout support for CheckboxGroup and RadioGroup ([#2252](https://github.com/dream-num/univer/issues/2252)) ([c638477](https://github.com/dream-num/univer/commit/c63847733a7d167aacf68c8b60a80a480fc3ccc2)) +* **design:** support multiple tree ([#2259](https://github.com/dream-num/univer/issues/2259)) ([1d11418](https://github.com/dream-num/univer/commit/1d11418f52fb522202facc4cde6ce56c02f4a06e)) +* **facade:** add API to generate HTML content ([#2219](https://github.com/dream-num/univer/issues/2219)) ([3a9afd9](https://github.com/dream-num/univer/commit/3a9afd963aa1bf17c3f3e8d4877e3e5b16a93075)) +* **network:** add fetch implementation ([#2226](https://github.com/dream-num/univer/issues/2226)) ([b970fe1](https://github.com/dream-num/univer/commit/b970fe1e93c5e66e1f80ea358681142d5e84b412)) +* **render-engine:** hyphenation in paragraph layout ([#2172](https://github.com/dream-num/univer/issues/2172)) ([2739fba](https://github.com/dream-num/univer/commit/2739fbae2c2de1b5b5d580641c09e0003975fee2)) +* **sheet:** add tooltip to FilterPanel ([#2234](https://github.com/dream-num/univer/issues/2234)) ([12d4aef](https://github.com/dream-num/univer/commit/12d4aeff18d0a8984b15625488b6092f4f137016)) +* **sheet:** allow menu scroll when it over viewport ([#2215](https://github.com/dream-num/univer/issues/2215)) ([184b98b](https://github.com/dream-num/univer/commit/184b98b19e54a054de8de265b9f5d846624d4ab8)) +* **sheets:** add set workbook name command ([#2249](https://github.com/dream-num/univer/issues/2249)) ([3c24cdd](https://github.com/dream-num/univer/commit/3c24cdd1df07c9b29957d71f4059bbac99b44e16)) + +## [0.1.10](https://github.com/dream-num/univer/compare/v0.1.9...v0.1.10) (2024-05-10) + + +### Bug Fixes + +* **conditional-formatting:** custom formula rendering error ([#2117](https://github.com/dream-num/univer/issues/2117)) ([d04a0f8](https://github.com/dream-num/univer/commit/d04a0f8fd496415b07bd761eaee85f716c23fbf0)) +* **conditional-formatting:** update icon set id ([#2109](https://github.com/dream-num/univer/issues/2109)) ([81a59fd](https://github.com/dream-num/univer/commit/81a59fdc1c52c054f7c014cda1ea39d27be66016)) +* **conditional-formatting:** update icon set id ([#2115](https://github.com/dream-num/univer/issues/2115)) ([21b7a14](https://github.com/dream-num/univer/commit/21b7a145d745d64874c6791821c8e5b02007e769)) +* **conditional-formatting:** viewmodel interception is not required at initialization time ([#2107](https://github.com/dream-num/univer/issues/2107)) ([305d235](https://github.com/dream-num/univer/commit/305d235091a0cfe77be02196bbcc44d39c1e37cf)) +* correct skeleton dispose timing ([#2196](https://github.com/dream-num/univer/issues/2196)) ([87e96df](https://github.com/dream-num/univer/commit/87e96df3204953aaf89d5cd9328c9026629992bb)) +* **docs:** text selection in multiple columns ([#2199](https://github.com/dream-num/univer/issues/2199)) ([5f9816e](https://github.com/dream-num/univer/commit/5f9816e060942ba642df41ee9bd63d6c883e65da)) +* fix ci playwright ([#2168](https://github.com/dream-num/univer/issues/2168)) ([714b244](https://github.com/dream-num/univer/commit/714b2448352712943e448cdb0c3c3e324c7bfe63)) +* fix slide rendering ([#2127](https://github.com/dream-num/univer/issues/2127)) ([300d5d6](https://github.com/dream-num/univer/commit/300d5d6959b43255bfc8a3e2e4ab7574b1c42ce4)) +* message service do not return disposable ([#2155](https://github.com/dream-num/univer/issues/2155)) ([d831996](https://github.com/dream-num/univer/commit/d83199680949108dca3309736ce6bf4b1106c796)) +* **render-engine:** fx column dose not display content ([#2161](https://github.com/dream-num/univer/issues/2161)) ([73e8c02](https://github.com/dream-num/univer/commit/73e8c0277bea79da6c7770684b02b40e919373e3)) +* **render:** memory leak ([#2166](https://github.com/dream-num/univer/issues/2166)) ([9b078e2](https://github.com/dream-num/univer/commit/9b078e2cfb4f7c714c1715fe1e067b490c93b187)) +* **render:** memory leak and capture ([#2171](https://github.com/dream-num/univer/issues/2171)) ([af27e38](https://github.com/dream-num/univer/commit/af27e38c59b5ac929684ed04013038cbd8047c8e)) +* **sheet:** find-replace replaceAll only effect on the active sheet ([#2202](https://github.com/dream-num/univer/issues/2202)) ([0ed9f56](https://github.com/dream-num/univer/commit/0ed9f560ac7f42560a3c21943313b097f127d005)) +* **sheets-data-validation:** data validation daily bugifx ([#2126](https://github.com/dream-num/univer/issues/2126)) ([3b45f89](https://github.com/dream-num/univer/commit/3b45f89548f000e5bf4939021c1f9bfa04fb9dca)), closes [#2121](https://github.com/dream-num/univer/issues/2121) [#2120](https://github.com/dream-num/univer/issues/2120) [#2114](https://github.com/dream-num/univer/issues/2114) [#2112](https://github.com/dream-num/univer/issues/2112) +* **sheet:** unhide hidden icon render incorrect when headers resize ([#2208](https://github.com/dream-num/univer/issues/2208)) ([a68bfb9](https://github.com/dream-num/univer/commit/a68bfb9bd14485d8e287eda6476490cc2463b452)) +* **slides-ui:** fix excessive re-render issue to prevent infinite loop errors ([#2159](https://github.com/dream-num/univer/issues/2159)) ([9edf924](https://github.com/dream-num/univer/commit/9edf924dfb33cc21d8dd43285927deee58e693d0)) + + +### Features + +* **docs:** image layout in doc ([#1958](https://github.com/dream-num/univer/issues/1958)) ([00d0b79](https://github.com/dream-num/univer/commit/00d0b7917d75433629fad629d635adc427b6b4a5)) +* **facade:** add sheet hooks, onCellPointerMove hook ([#2193](https://github.com/dream-num/univer/issues/2193)) ([476ffd3](https://github.com/dream-num/univer/commit/476ffd33c4425890e55e8dffdcb7a2563ed85f18)) +* **facade:** refactor f-univer newAPI and add getDependencies ([#2176](https://github.com/dream-num/univer/issues/2176)) ([94a86d3](https://github.com/dream-num/univer/commit/94a86d3097980dede0cca584af48c0df9855794e)) +* **formula:** report formula error message, check params number by minParams and maxParams ([#1876](https://github.com/dream-num/univer/issues/1876)) ([88f517b](https://github.com/dream-num/univer/commit/88f517be0411806982e4a54062e8d6c72326827e)) +* ui plugin support override dependencies ([#2125](https://github.com/dream-num/univer/issues/2125)) ([561f7aa](https://github.com/dream-num/univer/commit/561f7aae1bce9a337b55c9512515ddbf5e3eed73)) +* **user:** add user model ([#2137](https://github.com/dream-num/univer/issues/2137)) ([49c1a70](https://github.com/dream-num/univer/commit/49c1a70bb1013e5a04f9b9b3c0786858fee755b6)) + +## [0.1.9](https://github.com/dream-num/univer/compare/v0.1.8...v0.1.9) (2024-04-29) + + +### Bug Fixes + +* fix doc editor cannot focus ([ab4fe64](https://github.com/dream-num/univer/commit/ab4fe643ecc02180fb408c8f23ed06ff05660a3c)) +* global-zone componentKey initial value error ([#2100](https://github.com/dream-num/univer/issues/2100)) ([2404690](https://github.com/dream-num/univer/commit/240469054c462a6404c4bb40c3a9c6e1a743d28c)) +* **sheet-formula:** store themeColor to reuse ([#2079](https://github.com/dream-num/univer/issues/2079)) ([6c2e1d3](https://github.com/dream-num/univer/commit/6c2e1d3225d2fd18b5a3473f8ccbfc9193ae9595)) +* **sheet:** set bottom by default for vertical align ([#1929](https://github.com/dream-num/univer/issues/1929)) ([392f320](https://github.com/dream-num/univer/commit/392f320d98440ee20143e84d1508c35f28b21732)) +* skeleton change will remove autofill popupmenu ([#2092](https://github.com/dream-num/univer/issues/2092)) ([5b80ca7](https://github.com/dream-num/univer/commit/5b80ca734e9dc1cd14948a6110a9fa00049993f7)) +* **ui:** fix close ui plugin config and focus capturing ([#2086](https://github.com/dream-num/univer/issues/2086)) ([4a360f7](https://github.com/dream-num/univer/commit/4a360f7edb7c8af8d5e50707a260a497b9520a2e)), closes [#1914](https://github.com/dream-num/univer/issues/1914) [#1967](https://github.com/dream-num/univer/issues/1967) + + +### Features + +* **facade:** workbook and worksheet operation ([#2076](https://github.com/dream-num/univer/issues/2076)) ([2daefd7](https://github.com/dream-num/univer/commit/2daefd7f82b642182663abe4a58f3578bc017399)) + +## [0.1.8](https://github.com/dream-num/univer/compare/v0.1.7...v0.1.8) (2024-04-26) + + +### Bug Fixes + +* columns is not render properly ([#1952](https://github.com/dream-num/univer/issues/1952)) ([039e5e2](https://github.com/dream-num/univer/commit/039e5e2f6c6acaa2dd6590b9bea6ce475814c3be)) +* **conditional-formatting:** create cf rule error ([#1969](https://github.com/dream-num/univer/issues/1969)) ([ee8f8de](https://github.com/dream-num/univer/commit/ee8f8de7e4ecf0bc656da9abd1e2979083108446)) +* **conditional-formatting:** disregarding computation beyond the tables region ([#1891](https://github.com/dream-num/univer/issues/1891)) ([c14a3a8](https://github.com/dream-num/univer/commit/c14a3a8b831f36dacfd7955004acaef5849d9377)) +* **conditional-formatting:** gradient fills may conceal cell values ([#1898](https://github.com/dream-num/univer/issues/1898)) ([70d3a7e](https://github.com/dream-num/univer/commit/70d3a7e8f3b8a024a7b48b65ae57a40d1021025b)) +* **design:** fix checkbox group not updating visually on click ([#1989](https://github.com/dream-num/univer/issues/1989)) ([967eb39](https://github.com/dream-num/univer/commit/967eb399a2d4dc8ff82a184e5dc6a26321e81181)) +* **dv:** fix data validation plugin type to prevent loading error ([#2084](https://github.com/dream-num/univer/issues/2084)) ([937fa13](https://github.com/dream-num/univer/commit/937fa13628b701ccb0c2efda94e5b47e6ce311c8)) +* **editor:** formula input esc invalid ([#1902](https://github.com/dream-num/univer/issues/1902)) ([87f0994](https://github.com/dream-num/univer/commit/87f0994f914bc45d69d88f0f7018e288b955c140)) +* **facade:** fix handling of empty selections in `onSelectionChange` method ([#2066](https://github.com/dream-num/univer/issues/2066)) ([e440e04](https://github.com/dream-num/univer/commit/e440e045b232743bd886e46d82003156ff9c9b9f)) +* fix API not exported ([b4913f9](https://github.com/dream-num/univer/commit/b4913f93d748d0e9f1d785c64e36af74c5a928be)) +* fix current render handling in desktop controller ([#2067](https://github.com/dream-num/univer/issues/2067)) ([ddbeb02](https://github.com/dream-num/univer/commit/ddbeb022750811b07bcf32b23a9d0c6e4f68f80d)) +* fix lifecycle stages ([37777f1](https://github.com/dream-num/univer/commit/37777f1dc3fa2b4c6c6b0012e609c8517a91f722)) +* fix lint errors ([4e970d4](https://github.com/dream-num/univer/commit/4e970d417a904d4da4c50a936bac68cccd924a6a)) +* fix memory leak on dispose sheet unit ([#1900](https://github.com/dream-num/univer/issues/1900)) ([4a5eca1](https://github.com/dream-num/univer/commit/4a5eca1927dd247f0d849392d4af9942ab8f5746)) +* fix memory leaking in active cell ([761a372](https://github.com/dream-num/univer/commit/761a372ca9db59a03c55c7329e424df1369f7ab1)) +* fix non-sheet renderer should not be set container ([#2044](https://github.com/dream-num/univer/issues/2044)) ([00b30e5](https://github.com/dream-num/univer/commit/00b30e567c0f34469707ac3e1552dd13f8150d40)) +* fix plugin not added to seen list ([7591212](https://github.com/dream-num/univer/commit/75912125a76102c6786b0896247fbe0d35bd0386)) +* fix unit cannot be destroyed or recreate ([#2081](https://github.com/dream-num/univer/issues/2081)) ([b67a9f8](https://github.com/dream-num/univer/commit/b67a9f835700394843a6d0f935ec7b6cea31620b)) +* fix univer plugin lifecycle not triggered ([#2023](https://github.com/dream-num/univer/issues/2023)) ([827e5a3](https://github.com/dream-num/univer/commit/827e5a3239c042e2bfb1356b80c1ce092ee2205b)) +* fix univer plugin not started ([5f5b0a7](https://github.com/dream-num/univer/commit/5f5b0a76d9a4335bd61251915bb4e49786e8cca7)) +* getCurrent methods should possibly return null ([#1892](https://github.com/dream-num/univer/issues/1892)) ([859d7fc](https://github.com/dream-num/univer/commit/859d7fc2d90fb10ad1a77177ab6e8cae4ce6b326)) +* punctuation adjustment ([#1867](https://github.com/dream-num/univer/issues/1867)) ([e921128](https://github.com/dream-num/univer/commit/e921128858b7827d09c5a404cc584e300ca9f22b)) +* rect-popup event bind error ([#1922](https://github.com/dream-num/univer/issues/1922)) ([ac17c69](https://github.com/dream-num/univer/commit/ac17c692b1deb57c1cfa17516b3905c9b7d5fa91)) +* refocus sheet cell when create new sheet ([#1896](https://github.com/dream-num/univer/issues/1896)) ([db88447](https://github.com/dream-num/univer/commit/db884470755c2ee344051226e1cbfe93f6a93dee)) +* **render-engine:** punctuation render error in sheet cell ([#2034](https://github.com/dream-num/univer/issues/2034)) ([d7ddad1](https://github.com/dream-num/univer/commit/d7ddad11c3d8cc5512c40e5c087d6953afa1a4cc)) +* replace whitespace characters in html str ([#1904](https://github.com/dream-num/univer/issues/1904)) ([1ff1261](https://github.com/dream-num/univer/commit/1ff12617f33da57c4fd4c1a0594b4f309e2d6c54)) +* **sheet-formula:** fix error message on missing formula ([#1885](https://github.com/dream-num/univer/issues/1885)) ([0ab866e](https://github.com/dream-num/univer/commit/0ab866e2f6668a27de6ddc45908c315169ac6830)) +* **sheet:** add cell custom field ([#2021](https://github.com/dream-num/univer/issues/2021)) ([53b9041](https://github.com/dream-num/univer/commit/53b904140ae8ef4037eb0bc1693a0011b2037cd3)) +* **sheet:** cell custom supports updating from mutation ([#2058](https://github.com/dream-num/univer/issues/2058)) ([bec1944](https://github.com/dream-num/univer/commit/bec1944bd4d8e0a9bd77a759034c03325dd8f969)) +* **sheet:** defined name move ([#1888](https://github.com/dream-num/univer/issues/1888)) ([be2fec3](https://github.com/dream-num/univer/commit/be2fec37cbad9d8e0e7c3672347cc0222b3dfef8)) +* **sheet:** defined name update name ([#1917](https://github.com/dream-num/univer/issues/1917)) ([5b6e223](https://github.com/dream-num/univer/commit/5b6e2237a1d1f02ff29494737f077a7f4eb6b92a)) +* **sheet:** editor set rich error ([#1918](https://github.com/dream-num/univer/issues/1918)) ([d4f67f8](https://github.com/dream-num/univer/commit/d4f67f81877931848a8b059d6c82f6a4106b4ca2)) +* **sheet:** esc key for editor ([#1928](https://github.com/dream-num/univer/issues/1928)) ([54487b8](https://github.com/dream-num/univer/commit/54487b824ecb3e1f5122ddd94032a7f0dd5c42b5)) +* **sheet:** fix some copy/paste bugs ([#1754](https://github.com/dream-num/univer/issues/1754)) ([496dcb8](https://github.com/dream-num/univer/commit/496dcb8c2f6851be308043340a026307a040daf3)) +* **sheet:** header hidden ([#1954](https://github.com/dream-num/univer/issues/1954)) ([e3dc9ce](https://github.com/dream-num/univer/commit/e3dc9ce992b4d409bbca4ad07dd5a6811f505bda)) +* **sheet:** life cycle steady ([#1927](https://github.com/dream-num/univer/issues/1927)) ([fbabfaa](https://github.com/dream-num/univer/commit/fbabfaa6d9d92fccfc2df1e03c2d05229e55ee40)) +* **sheet:** move formula ref ([#2078](https://github.com/dream-num/univer/issues/2078)) ([fa4ebea](https://github.com/dream-num/univer/commit/fa4ebeaa1d9265bc84e518e0053c6a03001d12b3)) +* **sheet:** range selector drag row ([#1729](https://github.com/dream-num/univer/issues/1729)) ([530a852](https://github.com/dream-num/univer/commit/530a852970726ac788607d13f5d75ea2599661bc)) +* **sheet:** range selector error ([#1897](https://github.com/dream-num/univer/issues/1897)) ([a2c8cb6](https://github.com/dream-num/univer/commit/a2c8cb645bef11d1658a0eb613e1fa444c98c328)) +* **sheets-data-validation:** fix reject input incorrect ([#2082](https://github.com/dream-num/univer/issues/2082)) ([6e03118](https://github.com/dream-num/univer/commit/6e0311881a54880c46faf4bd3d48f802f7565c7c)) +* sheets-formula initialize time ([#1910](https://github.com/dream-num/univer/issues/1910)) ([a461d16](https://github.com/dream-num/univer/commit/a461d16fdf7af30052b02762d3dc2eea4a2647d4)) +* **sheets-ui:** after unhiding row or col icon doesn't disappear ([#2075](https://github.com/dream-num/univer/issues/2075)) ([f07c2d9](https://github.com/dream-num/univer/commit/f07c2d9bd5acfc95db8cde7cc5ee70a617a65ab7)) +* **sheets:** the text is aligned at editorial and non-editorial states ([#1874](https://github.com/dream-num/univer/issues/1874)) ([c7e26a0](https://github.com/dream-num/univer/commit/c7e26a04c27284c17a4cb8835090320459ec79ac)) +* text is cropped when rendered in italic style with background ([#1862](https://github.com/dream-num/univer/issues/1862)) ([80f43b8](https://github.com/dream-num/univer/commit/80f43b87755954018972a51013dccd35d486f72b)) +* the strickthough position is wrong when fontsize is different ([#1919](https://github.com/dream-num/univer/issues/1919)) ([2564456](https://github.com/dream-num/univer/commit/2564456e34137c2536a83d7aeb4af2ce7a2aa29d)) +* **ui:** stop onblur propagation at root to prevent external focusout conflicts ([#1894](https://github.com/dream-num/univer/issues/1894)) ([04abb1b](https://github.com/dream-num/univer/commit/04abb1b68237debc8dfec3510948386cfd0620c5)) +* univer plugin holder start immediately ([8c3bb90](https://github.com/dream-num/univer/commit/8c3bb904ee44a76e2ee46dd014766c56c4e979e4)) +* univer should auto start ([af032c8](https://github.com/dream-num/univer/commit/af032c806b09f7bcef0d71bc84ce236542504c04)) + + +### Features + +* **core:** refactoring resources load and snapshot ([#1853](https://github.com/dream-num/univer/issues/1853)) ([8ae91b7](https://github.com/dream-num/univer/commit/8ae91b7664396a2d6dbfc5185ca2f4ee80e5f2d2)) +* **core:** univer support override dependencies ([#2050](https://github.com/dream-num/univer/issues/2050)) ([cfad0da](https://github.com/dream-num/univer/commit/cfad0da04abb585b8ea6387c01e085af3bea7c53)) +* **data-validation:** transform support for data validation ([#1915](https://github.com/dream-num/univer/issues/1915)) ([22ccbb1](https://github.com/dream-num/univer/commit/22ccbb17e4b9037e90e320c2eae17c4c18d42991)) +* **doc:** refactor of text shaping and support kerning ([#1785](https://github.com/dream-num/univer/issues/1785)) ([e7f1036](https://github.com/dream-num/univer/commit/e7f103622f6c07ed6dccc31078e9fe9dc21cc851)) +* **engine-formula:** add iseven isodd ([#1873](https://github.com/dream-num/univer/issues/1873)) ([21d145c](https://github.com/dream-num/univer/commit/21d145c4091a5bdb1bbc7f6099992844794445cb)) +* **engine-render:** add render unit system ([#2025](https://github.com/dream-num/univer/issues/2025)) ([a5243da](https://github.com/dream-num/univer/commit/a5243da72008514c0810a79c9ad3719f32debc04)) +* export identifiers from packages index.ts ([#2049](https://github.com/dream-num/univer/issues/2049)) ([bced914](https://github.com/dream-num/univer/commit/bced9140bc6bdae9d5d8069dd09fbc1f0ef52cf8)) +* **facade:** f-worksheet add getName method ([#2042](https://github.com/dream-num/univer/issues/2042)) ([10cc6e1](https://github.com/dream-num/univer/commit/10cc6e1526b1c0c0f07c275c74e08f40842fd846)) +* **formula:** formula calculation progress ([#1985](https://github.com/dream-num/univer/issues/1985)) ([ad1d632](https://github.com/dream-num/univer/commit/ad1d6325122d3982070d74de0fc6453680633244)) +* **formula:** implement formula lower ([#1970](https://github.com/dream-num/univer/issues/1970)) ([db3fa45](https://github.com/dream-num/univer/commit/db3fa45f0b60c31254679ff540bc2a8bc6a62e5a)) +* **image:** add plugin ([#1962](https://github.com/dream-num/univer/issues/1962)) ([9c494ae](https://github.com/dream-num/univer/commit/9c494aecfedb85f6120ed3a3d8f2537c75435eb6)) +* **numfmt:** refactor the numfmt model ([#1945](https://github.com/dream-num/univer/issues/1945)) ([1840166](https://github.com/dream-num/univer/commit/18401663906e8b2a44814b3a8011128aa85f58b8)) +* **sheet:** force string adds popup ([#1934](https://github.com/dream-num/univer/issues/1934)) ([add1214](https://github.com/dream-num/univer/commit/add121435f52cd82e9d25763acc082cd33585e2d)) +* **sheet:** indirect support defined name ([#1899](https://github.com/dream-num/univer/issues/1899)) ([6c0833a](https://github.com/dream-num/univer/commit/6c0833af4eda76571da2fac3f0febb0d0142a86b)) +* **sheets-ui:** optimize popup service ([#1912](https://github.com/dream-num/univer/issues/1912)) ([a555160](https://github.com/dream-num/univer/commit/a555160a97f7aa92dd0891b5e805806c5398a99d)) +* **sheets:** filter ([#1465](https://github.com/dream-num/univer/issues/1465)) ([1f03b7a](https://github.com/dream-num/univer/commit/1f03b7a33975d0c8e4203c962a4aadd6e5b5240e)) +* **sheets:** register other formula ([#2045](https://github.com/dream-num/univer/issues/2045)) ([4a8ffd9](https://github.com/dream-num/univer/commit/4a8ffd944cc13958ffe03a8e2355f64dc3ebb69a)) +* **umd:** create all-in-one UMD bundle ([#2062](https://github.com/dream-num/univer/issues/2062)) ([e76fe4e](https://github.com/dream-num/univer/commit/e76fe4e601adf7c879f3e887ed18a919f7725aa1)) +* univer can take parent injector ([f8ea16a](https://github.com/dream-num/univer/commit/f8ea16ae08076458ada0fca7523684fb332610c6)) +* update codeowners ([#2019](https://github.com/dream-num/univer/issues/2019)) ([5b0a103](https://github.com/dream-num/univer/commit/5b0a103818d42c747d2ac178d3fdfe08d91d2f90)) + + +### Performance Improvements + +* **formula:** dependency maintain ([#1638](https://github.com/dream-num/univer/issues/1638)) ([b745845](https://github.com/dream-num/univer/commit/b745845c9ad93d9af4f42507c1025e564f54ea93)) + +## [0.1.7](https://github.com/dream-num/univer/compare/v0.1.6...v0.1.7) (2024-04-12) + + +### Bug Fixes + +* auto height is not work when at default column width ([#1840](https://github.com/dream-num/univer/issues/1840)) ([55e0869](https://github.com/dream-num/univer/commit/55e08698ec66e15fc0146e38f5099b0d50e8c41a)) +* **condiational-formatting:** rename `SheetsConditionalFormattingUiPlugin` ([#1801](https://github.com/dream-num/univer/issues/1801)) ([9b14a5a](https://github.com/dream-num/univer/commit/9b14a5a0ec2426fde4e841f747dae92ecdcfd4df)) +* **conditional-formatting:** fix the logic for hidden$ in conditional formatting ([#1813](https://github.com/dream-num/univer/issues/1813)) ([cd631af](https://github.com/dream-num/univer/commit/cd631af7b28b674489a961a93b417b43c8fd5746)) +* **design:** ensure popup component is appended to root to prevent offset from stacking contexts ([#1850](https://github.com/dream-num/univer/issues/1850)) ([1ad518a](https://github.com/dream-num/univer/commit/1ad518a8ba429ccfb5f6682a40c1a3bae3b540f2)) +* **design:** fix slider to stop responding to mouse movement after release during zoom operations ([#1834](https://github.com/dream-num/univer/issues/1834)) ([3d5a26e](https://github.com/dream-num/univer/commit/3d5a26e28c815440d307921abb8a5c44fb9088cf)) +* **design:** fix tooltip behavior ([#1845](https://github.com/dream-num/univer/issues/1845)) ([bd85759](https://github.com/dream-num/univer/commit/bd8575919f80a6b2adbba8084774efa5dec3d9ce)) +* **design:** set default language to zhCN ([#1863](https://github.com/dream-num/univer/issues/1863)) ([08e8d58](https://github.com/dream-num/univer/commit/08e8d58297ed29d3f365653ed1da2100fc7e8585)) +* display error in font family ([#1700](https://github.com/dream-num/univer/issues/1700)) ([8c2282b](https://github.com/dream-num/univer/commit/8c2282b174887bedafb8f9d29d2e73c03c0cfd91)) +* **docs:** list indent and hanging ([#1675](https://github.com/dream-num/univer/issues/1675)) ([4020055](https://github.com/dream-num/univer/commit/402005514924328161714f26207f72ed7781c9af)) +* **docs:** strikethrough position is incorrect ([#1836](https://github.com/dream-num/univer/issues/1836)) ([3f68158](https://github.com/dream-num/univer/commit/3f68158d69f3bf30c87b1d11618c7b86ccbb44a7)) +* **engine-render:** ignore media change on printing mode ([#1808](https://github.com/dream-num/univer/issues/1808)) ([f5fc6be](https://github.com/dream-num/univer/commit/f5fc6bef73166b132087ea5595916728e8b9fea4)) +* **formula:** copy paste range with formulas ([#1765](https://github.com/dream-num/univer/issues/1765)) ([58c7d3e](https://github.com/dream-num/univer/commit/58c7d3e722638c56a4ce91b3f2ab3c0fb7b42d78)) +* **formula:** null value return not zero ([#1851](https://github.com/dream-num/univer/issues/1851)) ([87d8e20](https://github.com/dream-num/univer/commit/87d8e207c97b17c872f82848237e61cca828a4c1)) +* **formula:** use ref range formula ([#1694](https://github.com/dream-num/univer/issues/1694)) ([d8f1dc4](https://github.com/dream-num/univer/commit/d8f1dc4100c7472e1e1a82205b853e879ce60311)) +* inline format error in cell ([#1843](https://github.com/dream-num/univer/issues/1843)) ([2002fdf](https://github.com/dream-num/univer/commit/2002fdfb1a52a44b97835fbfb8b068a3a88ba8fa)) +* **rpc:** skip missing mutations in remote replica ([#1826](https://github.com/dream-num/univer/issues/1826)) ([1e10cbf](https://github.com/dream-num/univer/commit/1e10cbfe72ba3eee8647aae09e92a6fbbac8d31b)) +* **sheet-ui:** make the default font family and font size correct ([#1827](https://github.com/dream-num/univer/issues/1827)) ([ea18b99](https://github.com/dream-num/univer/commit/ea18b99b5f0b01b1712c38f61f83c78d2b2a38c8)) +* **sheet:** defined name vertical ([#1832](https://github.com/dream-num/univer/issues/1832)) ([edf86f4](https://github.com/dream-num/univer/commit/edf86f45299de4ba8be30aaaed4e867861a50671)) +* **sheet:** editor and selection position ([#1830](https://github.com/dream-num/univer/issues/1830)) ([e23992f](https://github.com/dream-num/univer/commit/e23992fcb0d8ef69ec57a721dfc1c7d14c525bee)) +* **sheet:** remove set current mutation ([#1802](https://github.com/dream-num/univer/issues/1802)) ([79ce85d](https://github.com/dream-num/univer/commit/79ce85d648a57ceb40eeaf837794654ae9b626f0)) +* **sheets-ui:** data-validation event bind timing ([#1804](https://github.com/dream-num/univer/issues/1804)) ([d0cac23](https://github.com/dream-num/univer/commit/d0cac23b90bbe6c12ee32435a73579af96599c37)) +* **sheets-ui:** fix border panel icons ([#1815](https://github.com/dream-num/univer/issues/1815)) ([ea7636e](https://github.com/dream-num/univer/commit/ea7636ec81056423ef018bafdfe059be3ec16658)) +* **sheets:** fix the issue where the editor position is incorrect after unmerging cells ([#1717](https://github.com/dream-num/univer/issues/1717)) ([7d27f11](https://github.com/dream-num/univer/commit/7d27f1178f681e4b912326627b5e44dd77085048)) +* **sheet:** update internal id ([#1825](https://github.com/dream-num/univer/issues/1825)) ([fc4cc4c](https://github.com/dream-num/univer/commit/fc4cc4cb2db892d643c4b3ce1f5a48e72d1d164b)) +* ts-error ([#1858](https://github.com/dream-num/univer/issues/1858)) ([b8007cb](https://github.com/dream-num/univer/commit/b8007cb9e7f042a2085872b741933217d7c2e996)) +* uniscript entry is displayed in zen mode ([#1842](https://github.com/dream-num/univer/issues/1842)) ([337af7d](https://github.com/dream-num/univer/commit/337af7daac84d9ff3c32e8a8ab91f307b5cce16a)) +* **uniscript:** script editor service is not exposed ([68647a6](https://github.com/dream-num/univer/commit/68647a6d8019d14d255a34059bfc27463100770b)) + + +### Features + +* **conditional-formatting:** bugfix ([#1838](https://github.com/dream-num/univer/issues/1838)) ([c0b3dce](https://github.com/dream-num/univer/commit/c0b3dce0f713fe083523eff9b0fc5bda83853439)) +* **design:** add `indeterminate` property support to Checkbox component ([#1870](https://github.com/dream-num/univer/issues/1870)) ([f522a34](https://github.com/dream-num/univer/commit/f522a345e32f4a3fd4cf0db50ecdc22e8ab69c4a)) +* **docs:** support background color in doc ([#1846](https://github.com/dream-num/univer/issues/1846)) ([3a38828](https://github.com/dream-num/univer/commit/3a38828fafbb7c2b8681362bd6471367a559cde1)) +* **formula:** add the Maxifs function ([#1711](https://github.com/dream-num/univer/issues/1711)) ([52b2698](https://github.com/dream-num/univer/commit/52b26982ae3eb3c86dfc253166571ea37481f758)) +* **sheets-data-validation:** move draggable-list to design ([#1822](https://github.com/dream-num/univer/issues/1822)) ([3acf286](https://github.com/dream-num/univer/commit/3acf286af96752d44ea47c9287ed93d608ce33ef)) +* **sheets-ui:** add f2 to start editing ([#1875](https://github.com/dream-num/univer/issues/1875)) ([b740dfa](https://github.com/dream-num/univer/commit/b740dfa864162d9eabb99910b577cb78f8deb7ac)) +* **sheets-ui:** sheet popup service should respond to row col changes ([#1848](https://github.com/dream-num/univer/issues/1848)) ([6868a47](https://github.com/dream-num/univer/commit/6868a47cd22265b1718a9bf8c3318450b9d9dcfb)) +* **sheets:** add watch API to ref-range-service ([#1635](https://github.com/dream-num/univer/issues/1635)) ([5f7e9a2](https://github.com/dream-num/univer/commit/5f7e9a2f238efcb6b0b6598991b897d56d6ed549)) +* **sheets:** data-validation ref-range & optimize package orignize ([#1784](https://github.com/dream-num/univer/issues/1784)) ([a475474](https://github.com/dream-num/univer/commit/a4754741871698760f839c93051c0c6e8199016f)) +* **ui:** add disable auto focus config ([#1682](https://github.com/dream-num/univer/issues/1682)) ([6256c15](https://github.com/dream-num/univer/commit/6256c151caa8975053edc65e6bf3d54b37b0329c)) +* **ui:** optimize scrollbar ([#1856](https://github.com/dream-num/univer/issues/1856)) ([9e76a28](https://github.com/dream-num/univer/commit/9e76a28ec92715ab299049ad464f3a821bdc8607)) + +## [0.1.6](https://github.com/dream-num/univer/compare/v0.1.5...v0.1.6) (2024-04-03) + + +### Bug Fixes + +* backspace will cause error when doc is not ready ([#1725](https://github.com/dream-num/univer/issues/1725)) ([f24fdb1](https://github.com/dream-num/univer/commit/f24fdb17b7e967cb53f09d0c4a877b646b53c6b7)) +* **conditional-formatting:** configuration exceptions are specifically handled ([#1750](https://github.com/dream-num/univer/issues/1750)) ([37a7787](https://github.com/dream-num/univer/commit/37a77873c6e5757f125593e6babf5d408d457817)) +* **conditional-formatting:** support row/col hidden ([#1747](https://github.com/dream-num/univer/issues/1747)) ([7ed59d1](https://github.com/dream-num/univer/commit/7ed59d138b91440ae5c7ea9941c2c8f444c8dc1b)) +* data-validation i18n ([#1788](https://github.com/dream-num/univer/issues/1788)) ([73aae0b](https://github.com/dream-num/univer/commit/73aae0bbee39e0fac3c5059a18411679ef92b898)) +* delete undo redo ([#1781](https://github.com/dream-num/univer/issues/1781)) ([8d8e615](https://github.com/dream-num/univer/commit/8d8e6153308bdc574a0014924286d0715b70790c)) +* **design:** apply `pointer-events: none` to avoid `::selection` in Safari ([#1792](https://github.com/dream-num/univer/issues/1792)) ([86bb772](https://github.com/dream-num/univer/commit/86bb772224f8b4c19d9c296413ec8ac2ba7135e5)) +* **design:** change tooltip to not remain active after hover ([#1756](https://github.com/dream-num/univer/issues/1756)) ([2019a77](https://github.com/dream-num/univer/commit/2019a775cc712a62172a36f1f491e9f14b0c6450)) +* **facade:** fix set horizontal, vertical, warp ([#1766](https://github.com/dream-num/univer/issues/1766)) ([1876e68](https://github.com/dream-num/univer/commit/1876e68d23241f14faf6885cc5e8f2b074ff4c72)) +* **find-replace:** add locale exports ([#1760](https://github.com/dream-num/univer/issues/1760)) ([a573166](https://github.com/dream-num/univer/commit/a573166c48e160ea279ba3917fde04b375fe8c05)) +* **formula:** bracket nested ([#1799](https://github.com/dream-num/univer/issues/1799)) ([d84a618](https://github.com/dream-num/univer/commit/d84a6187d59a905abfd654e2d21d31e6fc380337)) +* **formula:** today fill error ([#1798](https://github.com/dream-num/univer/issues/1798)) ([4b35198](https://github.com/dream-num/univer/commit/4b35198d436ac1e2bc79e98c81904029b3871480)) +* punctuation adjustment in the middle of line ([#1686](https://github.com/dream-num/univer/issues/1686)) ([2382e3b](https://github.com/dream-num/univer/commit/2382e3bdb092f47342993b2bd5b723c0bf381111)) +* **sheet:** error while creating an empty subunit ([#1748](https://github.com/dream-num/univer/issues/1748)) ([662b4e0](https://github.com/dream-num/univer/commit/662b4e0b843d2f399257f689d51c065f232f44b6)) +* **sheet:** selection size and editor position ([#1743](https://github.com/dream-num/univer/issues/1743)) ([fd83cbf](https://github.com/dream-num/univer/commit/fd83cbfd168b383fb0dc604a8d6d0a149ee8e287)) +* **sheets:** fix shallow copy bugs of the sheet snapshot ([#1742](https://github.com/dream-num/univer/issues/1742)) ([83d910c](https://github.com/dream-num/univer/commit/83d910c328e91b196491fc3459de2744d3948b90)) +* **sheets:** fix the issue with selection during autofill ([#1707](https://github.com/dream-num/univer/issues/1707)) ([eb6f8d0](https://github.com/dream-num/univer/commit/eb6f8d00bb81118e4e642d17d024eb14c6f52087)) +* **sheets:** fix value type casting in set range values ([#1646](https://github.com/dream-num/univer/issues/1646)) ([227f5b0](https://github.com/dream-num/univer/commit/227f5b0ba1c4bc19257e3fae6ca8fc93cd1139ea)) +* **sheets:** merge disappear on hide row ([#1714](https://github.com/dream-num/univer/issues/1714)) ([e68d47f](https://github.com/dream-num/univer/commit/e68d47f1951dc9d5f7f76750c8afee413c9c457b)) +* **sheets:** some bugs about copy&paste / remove row&col / autofill ([#1561](https://github.com/dream-num/univer/issues/1561)) ([e1072c7](https://github.com/dream-num/univer/commit/e1072c743941019d51ed5f284c66d6f01bda333d)) +* the error clip the last char when linebreak ([#1745](https://github.com/dream-num/univer/issues/1745)) ([009b5b4](https://github.com/dream-num/univer/commit/009b5b4708a927939e76dd0936fe2e5244dfc14d)) + + +### Features + +* **condiational-formatting:** update enUS locale ([#1787](https://github.com/dream-num/univer/issues/1787)) ([785e141](https://github.com/dream-num/univer/commit/785e141f8b8a311bb1bf55922bac34ecd6765d8e)) +* **conditional-formatting:** support set cfId ([#1753](https://github.com/dream-num/univer/issues/1753)) ([4a277f9](https://github.com/dream-num/univer/commit/4a277f932c6e27f7bd354494ce13851b31b7eaa5)) +* **design:** add `Textarea` component ([#1778](https://github.com/dream-num/univer/issues/1778)) ([a2dd33d](https://github.com/dream-num/univer/commit/a2dd33d914580a067ea3149b1e17b31c8bd68973)) +* **design:** set default text color to prevent inheritance ([#1751](https://github.com/dream-num/univer/issues/1751)) ([71e1d94](https://github.com/dream-num/univer/commit/71e1d9406f7e82a30b37afb70b9a78ab0bdce480)) +* **facade:** add getMaxColumns and getMaxRows API on FWorksheet ([#1775](https://github.com/dream-num/univer/issues/1775)) ([c903780](https://github.com/dream-num/univer/commit/c90378045f0a1caf65b555ff933348d1599a4632)) +* **facade:** add getSheetBySheetId API on FWorkbook ([#1762](https://github.com/dream-num/univer/issues/1762)) ([436b1b4](https://github.com/dream-num/univer/commit/436b1b47f8461c6ed25f1d4a98a8a2d9b4b150c1)) +* **facade:** sheet api getId rename to getSheetId ([#1770](https://github.com/dream-num/univer/issues/1770)) ([dff654c](https://github.com/dream-num/univer/commit/dff654c156f8e0f4aa501a8ad655cf5baf21eaf7)) +* **sheet:** defined name ([#1737](https://github.com/dream-num/univer/issues/1737)) ([cfa9375](https://github.com/dream-num/univer/commit/cfa9375720ac6cdebc1e1572687a8849204fd372)) +* **sheet:** optimize data validation i18n & dropdown bugfix ([#1768](https://github.com/dream-num/univer/issues/1768)) ([a8c9452](https://github.com/dream-num/univer/commit/a8c945290283578029f09104082581a8bd1cceb0)) +* **sheets:** support data validation ([#1676](https://github.com/dream-num/univer/issues/1676)) ([9961b32](https://github.com/dream-num/univer/commit/9961b3243c339e8fee64dea145c1507524e03b49)) +* support char which length is great than 1 ([#1783](https://github.com/dream-num/univer/issues/1783)) ([32cfb3b](https://github.com/dream-num/univer/commit/32cfb3bf2e7c039813908a4523df3dc0d72ef227)) + +## [0.1.5](https://github.com/dream-num/univer/compare/v0.1.4...v0.1.5) (2024-03-29) + + +### Bug Fixes + +* **editor:** range selector and range drag ([#1713](https://github.com/dream-num/univer/issues/1713)) ([02e9647](https://github.com/dream-num/univer/commit/02e96473f309d03984a03e5a64e1e61bba5040fa)) +* **editor:** short key error and normal range show ([#1688](https://github.com/dream-num/univer/issues/1688)) ([571ec0b](https://github.com/dream-num/univer/commit/571ec0b17f8064953967e635331154e4e57856b9)) +* fixing the range of remove merged selection and filter empty ranges ([#1680](https://github.com/dream-num/univer/issues/1680)) ([117cbbe](https://github.com/dream-num/univer/commit/117cbbefbf86b7847f565fbcd3ea74b956f8d285)) +* **formula:** index function handles base value object ([#1692](https://github.com/dream-num/univer/issues/1692)) ([1f0b700](https://github.com/dream-num/univer/commit/1f0b7003f44e908f6c1a8e0a68343184c5658d91)) +* punctuation adjustment issues in slide ([#1690](https://github.com/dream-num/univer/issues/1690)) ([15cb6df](https://github.com/dream-num/univer/commit/15cb6df4f0224e18c318c00d6e972c5d06f01cbc)) +* **render:** media change for refresh canvas ([#1697](https://github.com/dream-num/univer/issues/1697)) ([dd6bfed](https://github.com/dream-num/univer/commit/dd6bfed0403dc87c9414ae90c1fe67c15cb30743)) +* **sheet:** active dirty dependency ui ([#1728](https://github.com/dream-num/univer/issues/1728)) ([d8c9e4b](https://github.com/dream-num/univer/commit/d8c9e4b241872b1470ae7a87bba4ba13cd782809)) +* **sheet:** fix the selection is incorrect when autofill intersects w… ([#1661](https://github.com/dream-num/univer/issues/1661)) ([ebdcc6c](https://github.com/dream-num/univer/commit/ebdcc6ccf66362d14863a5e9daf0c6dc070e57a9)) +* **sheet:** fix toolbar state when there's overlapping selection ([#1521](https://github.com/dream-num/univer/issues/1521)) ([1ebfe1a](https://github.com/dream-num/univer/commit/1ebfe1a7befe19ce0decd634d9bd69ca7bcaaaf2)) +* **sheet:** handle key value conflicts ([#1720](https://github.com/dream-num/univer/issues/1720)) ([9abc7c5](https://github.com/dream-num/univer/commit/9abc7c575c68017996d9f564cb5ae7e182f6a1c7)) +* **sheet:** null-value will not unexpected deleted when moving row/cols ([#1691](https://github.com/dream-num/univer/issues/1691)) ([1a1f7c8](https://github.com/dream-num/univer/commit/1a1f7c89cf562375e97cab9642e93415c6a02806)) +* **sheet:** set-tab-order-mutation should has fromIndex in parmas for transforming ([#1704](https://github.com/dream-num/univer/issues/1704)) ([6d05bd9](https://github.com/dream-num/univer/commit/6d05bd9adcbfd78fdec9a597c29fcbe1d9ff001d)) +* **slides:** export locale ([#1702](https://github.com/dream-num/univer/issues/1702)) ([403c529](https://github.com/dream-num/univer/commit/403c529312b04d5e233c5b3f750dae642060227a)) +* **ui:** canvas popup event bind error ([#1683](https://github.com/dream-num/univer/issues/1683)) ([8a0bfd6](https://github.com/dream-num/univer/commit/8a0bfd6be5632b94d30724906c4a2c2561a1cdb6)) +* **ui:** fix toolbar display issues by adjusting reactive hidden item filtering logic ([8b604eb](https://github.com/dream-num/univer/commit/8b604eb4c37538fc1176dc3baac28f4547cce261)) +* **ui:** fix toolbar responsiveness on small screens ([#1716](https://github.com/dream-num/univer/issues/1716)) ([a9755e8](https://github.com/dream-num/univer/commit/a9755e8c901259ff6fb9f0253d8802eb76ac2cd7)) +* **ui:** fix use observable ([#1719](https://github.com/dream-num/univer/issues/1719)) ([eabe6fb](https://github.com/dream-num/univer/commit/eabe6fbd6a8e51ff7a1f81cd7c292ca7767d3f32)) + + +### Features + +* **conditional-format:** support conditional format ([#1681](https://github.com/dream-num/univer/issues/1681)) ([50edd34](https://github.com/dream-num/univer/commit/50edd3475c6c75ef3491e1d52cda601768a0a321)), closes [#433](https://github.com/dream-num/univer/issues/433) [#439](https://github.com/dream-num/univer/issues/439) [#495](https://github.com/dream-num/univer/issues/495) [#489](https://github.com/dream-num/univer/issues/489) [#487](https://github.com/dream-num/univer/issues/487) [#485](https://github.com/dream-num/univer/issues/485) [#483](https://github.com/dream-num/univer/issues/483) [#480](https://github.com/dream-num/univer/issues/480) [#475](https://github.com/dream-num/univer/issues/475) [#472](https://github.com/dream-num/univer/issues/472) [#468](https://github.com/dream-num/univer/issues/468) [#458](https://github.com/dream-num/univer/issues/458) [#433](https://github.com/dream-num/univer/issues/433) [#437](https://github.com/dream-num/univer/issues/437) [#446](https://github.com/dream-num/univer/issues/446) [#486](https://github.com/dream-num/univer/issues/486) [#437](https://github.com/dream-num/univer/issues/437) [#461](https://github.com/dream-num/univer/issues/461) [#454](https://github.com/dream-num/univer/issues/454) [#480](https://github.com/dream-num/univer/issues/480) + ## [0.1.4](https://github.com/dream-num/univer/compare/v0.1.3...v0.1.4) (2024-03-25) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7607578cdd..17125e5fbc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ - + # Univer Contributing Guide @@ -52,7 +52,7 @@ pnpm dev:demo ### Architecture -Please refer to the architecture doc. The [Chinese version](https://univer.ai/guides/architecture/architecture/). +Please refer to the [architecture doc](https://github.com/dream-num/univer/wiki/Univer-Architecture). ### Source code organization @@ -85,7 +85,7 @@ During the refactoring process, it is recommended to remove legacy folders such Avoid creating barrel imports (index.ts) unless it is the main root index.ts file of a plugin. -### Contextual Connections +### Connecting context To effectively contribute as a member of a software engineering team (and community as well!), it is crucial to establish strong contextual connections. Providing links to relevant resources can greatly assist others in understanding the context. Consider the following practices: @@ -95,26 +95,9 @@ To effectively contribute as a member of a software engineering team (and commun By adopting these practices, you can enhance collaboration and facilitate a better understanding of the codebase within our community. -### Naming conventions +### Naming convention -To ensure code quality and consistency, please adhere to the following guidelines: - -- Use kebab-case for both file names and folder names. If the file contains a React component, it should be in PascalCase, e.g. `SheetTab.tsx`. -- Folder names should be in plural format. -- Interfaces should be named starting with a capital "I". -- Do use conventional type names including .service, .controller, .menu, .command, .mutation, and .operation. Invent additional type names if you must but take care not to create too many. - -Sometimes you need to defined a dependency injection token. Please adhere to the following naming convention: - -```typescript -export const IYourServiceOrControllerName = createIdentifier('..(service|controller)'); -``` - -For example: - -```typescript -export const ILogService = createIdentifier('core.log.service'); -``` +Please refer to [Univer Naming Convention](./docs/NAMING_CONVENTION.md). ### Submitting pull requests diff --git a/README-ja.md b/README-ja.md index e033cd3992..a6c4e09159 100644 --- a/README-ja.md +++ b/README-ja.md @@ -31,64 +31,56 @@ 日本語

- - > 🚧 このプロジェクトはまだ開発中です。API が大きく変更される可能性があることにご注意ください。問題や提案をお寄せください。 +> また、日本語の開発ドキュメントはまだ未完成です。英語のドキュメントをご参照ください。 ## はじめに -Univer は、スプレッドシート、ドキュメント、スライドを含む、企業向けドキュメントおよびデータコラボレーションソリューションのセットです。拡張性の高い設計により、開発者は Univer をベースにカスタマイズされた機能を利用することができます。 +Univer は、スプレッドシート、ドキュメント、スライドを含む、エンタープライズ向けドキュメントおよびデータコラボレーションソリューションです。高い拡張性を備えた設計により、開発者は Univer 上で独自の機能をカスタマイズできます。 -Univer の機能のハイライト: +特徴: -- 📈 Univer はスプレッドシートに対応しています。今後は文書やスライドにも対応する予定です。 -- 🌌 拡張性の高いアーキテクチャ設計。 - - 🔌 プラグインアーキテクチャにより、ドキュメントの機能をオンデマンドで組み合わせることができ、サードパーティのプラグインをサポートし、カスタマイズ開発を容易にします。 - - 💄 開発者が一貫したユーザー体験を提供できるよう、コンポーネント・ライブラリとアイコンを提供する。 -- ⚡ ハイパフォーマンス。 - - ✏️ Canvas をベースとした、統一された効率的なレンダリングエンジンと数式エンジン。 - - 🧮 ハイパフォーマンスフォーミュラエンジン、Web Worker をサポート。 -- 🌍 国際化サポート。 +- 📈 Univer は、**スプレッドシート**と**ドキュメント**の両方をサポートするように設計されています。将来的には**スライド**もサポートされる予定です。 +- ⚙️ Univer は簡単に**組み込む**ことができ、アプリケーションにシームレスに統合できます。 +- 🎇 Univer は**強力**で、**数式**、**条件付き書式**、**データ検証**、**フィルタリング**、**共同編集**、**印刷**、**インポート&エクスポート**、幅広い機能を提供しています。さらに、今後も多くの機能が追加される予定です。 +- 🔌 Univer は、*プラグインアーキテクチャ*と*ファサード API*のおかげで**高い拡張性**を持ち、開発者が Univer 上で独自の要件を実装するのが楽しみになります。 +- 💄 Univer は、*テーマ*を使用して外観をパーソナライズできるため、**高度にカスタマイズ可能**です。また、国際化(i18n)のサポートも提供しています。 +- ⚡ Univerは**パフォーマンス**に優れています。 + - ✏️ Univer は、Canvas ベースの効率的な*レンダリングエンジン*を搭載しており、さまざまなドキュメントタイプを完璧にレンダリングできます。レンダリングエンジンは、*句読点の圧縮*、*テキストと画像のレイアウト*、*スクロールバッファリング*などの高度な組版機能をサポートしています。 + - 🧮 Univer は、Web ワーカーやサーバーサイドでも動作可能な超高速の*数式エンジン*を搭載しています。 +- 🌌 Univer は**高度に統合された**システムです。ドキュメント、スプレッドシート、スライドは相互に連携でき、同じ Canvas 上にレンダリングすることもできるため、Univer 内で情報とデータの流れを実現できます。 ## Examples -|

📊 Univer Sheets

| | -|:---------------------------------------|--------------------------------| -| [Sheets](https://www.univer.ai/examples/sheets/)
Opened: cell styles, formulas. First quarter: conditional formatting, data validation, search and replace. Second quarter (tentative): floating pictures, filtering, sorting, annotations, charts, pivot tables, super tables (tables), shapes | [![](./docs/img/examples-sheets.gif)](https://www.univer.ai/examples/sheets/) | -| [Sheets Multi](https://www.univer.ai/examples/sheets-multi/)
Multiple Univer instances can be created on a page to allow interoperability between tables | [![](./docs/img/examples-sheets-multi.gif)](https://www.univer.ai/examples/sheets-multi/) | -| [Sheets Uniscript](https://www.univer.ai/examples/sheets-uniscript/)
In Univer Sheets, you can directly use JavaScript syntax to operate the data in the table to achieve automation. | [![](./docs/img/examples-sheets-uniscript.gif)](https://www.univer.ai/examples/sheets-uniscript/) | -| [Sheets Big Data](https://www.univer.ai/examples/sheets-big-data/)
Loading 10 million cells of data, completed within 500ms | [![](./docs/img/examples-sheets-big-data.gif)](https://www.univer.ai/examples/sheets-big-data/) | -| [Sheets Collaboration](https://univer.ai/pro-examples/sheets-collaboration/)
Please open two windows or invite friends to experience Univer Sheets collaboration together | [![](./docs/img/pro-examples-sheets-collaboration.gif)](https://univer.ai/pro-examples/sheets-collaboration/) | -| [Sheets Collaboration Playground](https://univer.ai/pro-examples/sheets-collaboration-playground/)
Demonstrate the process of collaboration. After A edits the form, how does B process it? Here is an interesting experiment | [![](./docs/img/pro-examples-sheets-collaboration-playground.gif)](https://univer.ai/pro-examples/sheets-collaboration-playground/) | -| [Sheets Import/Export](https://univer.ai/pro-examples/sheets-exchange/)
Supports xlsx file import and export | [![](./docs/img/pro-examples-sheets-exchange.gif)](https://univer.ai/pro-examples/sheets-exchange/) | -| [Sheets Print](https://univer.ai/pro-examples/sheets-print/)
Experience the HD printing capabilities of Univer Sheets | [![](./docs/img/pro-examples-sheets-print.gif)](https://univer.ai/pro-examples/sheets-print/) | -| [Sheets Data Validation / Conditional Formatting](https://univer-qqqkeqnw5-univer.vercel.app/sheets/)
Development preview of Univer Sheets data formats and conditional formatting | [![](./docs/img/examples-sheets-data-validation-conditional-format.png)](https://univer-qqqkeqnw5-univer.vercel.app/sheets/) | -|

📝 Univer Docs

| | -| [Docs](https://www.univer.ai/examples/docs/)
Already open: ordered and unordered lists, paragraph settings, mixed graphics and text, multi-column/single column display in sections (tentative): hyperlinks, comments, tables, charts | [![](./docs/img/examples-docs.gif)](https://www.univer.ai/examples/docs/) | -| [Docs Multi](https://www.univer.ai/examples/docs-multi/)
Multiple Univer instances can be created in a page so that doc can interoperate. | [![](./docs/img/examples-docs-multi.gif)](https://www.univer.ai/examples/docs-multi/) | -| [Docs Uniscript](https://www.univer.ai/examples/docs-uniscript/)
You can directly use JavaScript syntax to manipulate content in Univer Docs | [![](./docs/img/examples-docs-uniscript.gif)](https://www.univer.ai/examples/docs-uniscript/) | -| [Docs Big Data](https://www.univer.ai/examples/docs-big-data/)
1 million word Docs loading demo | [![](./docs/img/examples-docs-big-data.gif)](https://www.univer.ai/examples/docs-big-data/) | -| [Docs Collaboration](https://univer.ai/pro-examples/docs-collaboration/)
Please open two windows or invite friends to experience Univer Docs collaboration together | [![](./docs/img/pro-examples-docs-collaboration.gif)](https://univer.ai/pro-examples/docs-collaboration/) | -| [Docs Collaboration Playground](https://univer.ai/pro-examples/docs-collaboration-playground/)
Demonstrate the process of collaboration. After A edits the document, how does B process it? Here is an interesting experiment | [![](./docs/img/pro-examples-docs-collaboration-playground.gif)](https://univer.ai/pro-examples/docs-collaboration-playground/) | -|

🎨 Univer Slides

| | -| [Slides](https://www.univer.ai/examples/slides/)
A canvas presentation containing graphic text, floating pictures, tables and other elements | [![](./docs/img/examples-slides.gif)](https://www.univer.ai/examples/slides/) | -|

🧩 Univer Innovation

| | -| [Zen Mode](https://github.com/dream-num/univer)
The cell of Sheet is a Doc? | [![](./docs/img/zen-mode.gif)](https://github.com/dream-num/univer) | -| [Univer(SaaS version)](https://univer.ai/)
With Univer, we enable users to create 3 forms of page as they wish. By merging sheet, doc and slide's capabilities together, Univer empowers individuals and teams to create, organize and streamline workflows effortlessly. | [![](./docs/img/univer-workspace-drag-chart.gif)](https://youtu.be/kpV0MvQuFZA) | - -## 使用方法 - -Univer を npm パッケージとしてインポートすることをお勧めします。ドキュメントサイトの [Quick Start](https://univer.ai/ja-jp/guides/quick-start/) セクションをご覧ください。また、[オンラインプレイグラウンド](https://univer.ai/playground/)では、開発環境を構築することなく Univer をプレビューすることができます。 - -ユニバーはプラグインアーキテクチャを採用しています。以下のパッケージをインストールすることで、Univer の機能を拡張することができます。 +|   |   |   | +| :---: | :---: | :---: | +| 📊 Sheets | 📊 Sheets Multi | 📊 Sheets Uniscript | +| [![](./docs/img/examples-sheets.gif)](https://univer.ai/examples/sheets/) | [![](./docs/img/examples-sheets-multi.gif)](https://univer.ai/examples/sheets-multi/) | [![](./docs/img/examples-sheets-uniscript.gif)](https://univer.ai/examples/sheets-uniscript/) | +| 📊 Sheets Big Data | 📊 Sheets Collaboration (Pro) | 📊 Sheets Collaboration Playground (Pro) | +| [![](./docs/img/examples-sheets-big-data.gif)](https://univer.ai/examples/sheets-big-data/) | [![](./docs/img/pro-examples-sheets-collaboration.gif)](https://univer.ai/pro/examples/sheets-collaboration/) | [![](./docs/img/pro-examples-sheets-collaboration-playground.gif)](https://univer.ai/pro/examples/sheets-collaboration-playground/) | +| 📊 Sheets Import/Export (Pro) | 📊 Sheets Print (Pro) | 📝 Docs | +| [![](./docs/img/pro-examples-sheets-exchange.gif)](https://univer.ai/pro/examples/sheets-exchange/) | [![](./docs/img/pro-examples-sheets-print.gif)](https://univer.ai/pro/examples/sheets-print/) | [![](./docs/img/examples-docs.gif)](https://univer.ai/examples/docs/) | +| 📝 Docs Multi | 📝 Docs Uniscript | 📝 Docs Big Data | +| [![](./docs/img/examples-docs-multi.gif)](https://univer.ai/examples/docs-multi/) | [![](./docs/img/examples-docs-uniscript.gif)](https://univer.ai/examples/docs-uniscript/) | [![](./docs/img/examples-docs-big-data.gif)](https://univer.ai/examples/docs-big-data/) | +| 📝 Docs Collaboration (Pro) | 📝 Docs Collaboration Playground (Pro) | 📽️ Slides | +| [![](./docs/img/pro-examples-docs-collaboration.gif)](https://univer.ai/pro/examples/docs-collaboration/) | [![](./docs/img/pro-examples-docs-collaboration-playground.gif)](https://univer.ai/pro/examples/docs-collaboration-playground/) | [![](./docs/img/examples-slides.gif)](https://univer.ai/examples/slides/) | +| 📊 Zen Mode | Univer Workspace (SaaS version) |   | +| [![](./docs/img/zen-mode.gif)](https://univer.ai/zh-CN/guides/sheet/tutorials/zen-editor/#%E6%BC%94%E7%A4%BA) | [![](./docs/img/univer-workspace-drag-chart.gif)](https://youtu.be/kpV0MvQuFZA) |   | + +## 使い方 + +Univer を npm パッケージとしてインポートすることをお勧めします。ドキュメントサイトの [Quick Start](https://univer.ai/guides/sheet/getting-started/quickstart) セクションをご覧ください。また、[オンラインプレイグラウンド](https://univer.ai/playground/)では、開発環境を構築することなく Univer をプレビューすることができます。 + +Univer はプラグインアーキテクチャを採用しています。以下のパッケージをインストールすることで、Univer の機能を拡張することができます。 ### パッケージ | 名称 | 説明 | バージョン | | :-------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------- | | [core](./packages/core) | Univer のプラグインシステムとアーキテクチャを実装します。また、基本的なサービスや様々な種類のドキュメントのモデルを提供します。 | [![npm version](https://img.shields.io/npm/v/@univerjs/core)](https://npmjs.org/package/@univerjs/core) | +| [data-validation](./packages/data-validation) | Univer のデータ検証機能を実装します。 | [![npm version](https://img.shields.io/npm/v/@univerjs/data-validation)](https://npmjs.org/package/@univerjs/data-validation) | | [design](./packages/design) | Univer のデザインシステムを実装。CSS と React ベースのコンポーネントキットを提供します。 | [![npm version](https://img.shields.io/npm/v/@univerjs/design)](https://npmjs.org/package/@univerjs/design) | -| [docs](./packages/docs) | リッチテキスト編集機能の基本ロジックを実装し、また他の種類の文書でのテキスト編集を容易になります。 | [![npm version](https://img.shields.io/npm/v/@univerjs/docs)](https://npmjs.org/package/@univerjs/docs) | +| [docs](./packages/docs) | リッチテキスト編集機能の基本ロジックを実装し、他の種類の文書でもテキスト編集を容易にします。 | [![npm version](https://img.shields.io/npm/v/@univerjs/docs)](https://npmjs.org/package/@univerjs/docs) | | [docs-ui](./packages/docs-ui) | Univer ドキュメントのユーザーインターフェースを提供します。 | [![npm version](https://img.shields.io/npm/v/@univerjs/docs-ui)](https://npmjs.org/package/@univerjs/docs-ui) | | [engine-formula](./packages/engine-formula) | Canvas をベースとしたレンダリングエンジンを実装し、拡張可能です。 | [![npm version](https://img.shields.io/npm/v/@univerjs/engine-formula)](https://npmjs.org/package/@univerjs/engine-formula) | | [engine-numfmt](./packages/engine-numfmt) | ナンバーフォーマットエンジンを実装します。 | [![npm version](https://img.shields.io/npm/v/@univerjs/engine-numfmt)](https://npmjs.org/package/@univerjs/engine-numfmt) | @@ -98,12 +90,15 @@ Univer を npm パッケージとしてインポートすることをお勧め | [network](./packages/network) | WebSocket と HTTP をベースにしたネットワークサービスを実装します。 | [![npm version](https://img.shields.io/npm/v/@univerjs/network)](https://npmjs.org/package/@univerjs/network) | | [rpc](./packages/rpc) | Univer 文書の異なるレプリカ間でデータを同期するための RPC メカニズムとメソッドを実装します。 | [![npm version](https://img.shields.io/npm/v/@univerjs/rpc)](https://npmjs.org/package/@univerjs/rpc) | | [sheets](./packages/sheets) | スプレッドシート機能の基本ロジック。 | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets)](https://npmjs.org/package/@univerjs/sheets) | -| [sheets-find-replace](./packages/sheets-find-replace) | スプレッドシートの検索と置換機能を実装しています。 | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-find-replace)](https://npmjs.org/package/@univerjs/sheets-find-replace) | +| [sheets-conditional-formatting](./packages/sheets-conditional-formatting) | スプレッドシートの条件付き書式設定機能を実装します。 | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-conditional-formatting)](https://npmjs.org/package/@univerjs/sheets-conditional-formatting) | +| [sheets-conditional-formatting-ui](./packages/sheets-conditional-formatting-ui) | スプレッドシートの条件付き書式設定機能を実装します。 | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-conditional-formatting-ui)](https://npmjs.org/package/@univerjs/sheets-conditional-formatting-ui) | +| [sheets-data-validation](./packages/sheets-data-validation) | スプレッドシートのデータ検証機能を実装します。 | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-data-validation)](https://npmjs.org/package/@univerjs/sheets-data-validation) | +| [sheets-find-replace](./packages/sheets-find-replace) | スプレッドシートの検索と置換機能を実装します。 | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-find-replace)](https://npmjs.org/package/@univerjs/sheets-find-replace) | | [sheets-formula](./packages/sheets-formula) | スプレッドシートに数式を実装します。 | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-formula)](https://npmjs.org/package/@univerjs/sheets-formula) | | [sheets-numfmt](./packages/sheets-numfmt) | スプレッドシートの数値フォーマットを実装します。 | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-numfmt)](https://npmjs.org/package/@univerjs/sheets-numfmt) | | [sheets-zen-editor](./packages/sheets-zen-editor) | スプレッドシートの禅編集モードを実装します。 | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-zen-editor)](https://npmjs.org/package/@univerjs/sheets-zen-editor) | | [sheets-ui](./packages/sheets-ui) | Univer スプレッドシートのユーザーインターフェースを提供します。 | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-ui)](https://npmjs.org/package/@univerjs/sheets-ui) | -| [ui](./packages/ui) | React をベースにした Univer とワークベンチのレイアウトで、基本的なユーザーインタラクションを実装します。 | [![npm version](https://img.shields.io/npm/v/@univerjs/ui)](https://npmjs.org/package/@univerjs/ui) | +| [ui](./packages/ui) | React ベースの Univer とワークベンチのレイアウトを提供し、基本的なユーザーインタラクションを実装します。 | [![npm version](https://img.shields.io/npm/v/@univerjs/ui)](https://npmjs.org/package/@univerjs/ui) | | [uniscript](./packages/uniscript) (試験的) | Typescript に基づく DSL を実装し、より高度なタスクの実行を可能にします。 | [![npm version](https://img.shields.io/npm/v/@univerjs/uniscript)](https://npmjs.org/package/@univerjs/uniscript) | ## コントリビュート @@ -140,7 +135,7 @@ Univer プロジェクトの成長と開発は、バッカーやスポンサー ## リンク -- [ドキュメント](https://univer.ai/ja-jp/guides/introduction/) +- [ドキュメント](https://univer.ai/guides/sheet/introduction) - [Online Playground](https://univer.ai/playground/) - [公式 Website](https://univer.ai) @@ -154,4 +149,4 @@ Univer は Apache-2.0 ライセンスの下で配布されています。 --- -Copyright DreamNum Inc. 2023-現在 +Copyright © 2019-2024 Shanghai DreamNum Technology Co., Ltd. All rights reserved diff --git a/README-zh.md b/README-zh.md index d78aa639f8..9e38e18f3f 100644 --- a/README-zh.md +++ b/README-zh.md @@ -37,50 +37,40 @@ ## 介绍 -Univer 是一套企业文档与数据协同解决方案,包括电子表格、文档和幻灯片三大文档类型,高可扩展性设计使得开发者可以在 Univer 的基础上定制个性化功能。 +Univer 是一套企业文档与数据协同解决方案,融合了电子表格、文档和幻灯片。 -Univer 的功能特性包括: +Univer 的亮点包括: -- 📈 支持电子表格,后续还会支持文档和幻灯片 -- 🌌 高度可扩展的架构设计 - - 🔌 插件化架构,文档的能力可按需组合,支持自定义插件,方便二次开发 - - 💄 提供组件库和图标以帮助开发者呈现一致的用户体验 -- ⚡ 高性能 - - ✏️ 统一高效的渲染引擎和公式引擎,基于 Canvas - - 🧮 高性能的公式引擎,支持 Web Worker -- 🌍 国际化支持 - -点击[这里](https://univer.ai/guides/features)以了解 Univer 目前已经推出的功能。 +- 📈 **支持多种类文档** Univer 目前支持**电子表格**和**富文本文档**,未来还会增加对**幻灯片**的支持。 +- ⚙️ **易于集成** Univer 能够无缝集成到你的应用当中。 +- 🎇 **功能强大** Univer 支持非常多的功能,包括但不限于**公式计算**、**条件格式**、**数据验证**、**筛选**、**协同编辑**、**打印**、**导入导出**等等,更多的功能即将陆续发布。 +- 🔌 **高度可扩展**Univer 的 *插件化架构* 和 *Facade API* 使得扩展 Univer 的功能变得轻松容易,你可以在 Univer 之上实现自己的业务需求。 +- 💄 **高度可定制** 你可以通过*主题*来自定义 Univer 的外观,另外还支持国际化。 +- ⚡ **性能优越** + - ✏️ Univer 实现了基于 canvas 的 *渲染引擎*,能够高效地渲染不同类型的文档。渲染引擎支持 *标点挤压* *盘古之白* *图文混排* *滚动贴图* 等高级特性。 + - 🧮 自研的 *公式引擎* 拥有超快的计算速度,还能在 Web Worker 中运行,未来将会支持服务端计算。 +- 🌌 **高度集成** 文档、电子表格和幻灯片能够互操作,甚至是渲染在同一个画布上,使得信息和数据能够在 Univer 当中自由地流动。 ## 例子 -|

📊 Univer Sheets

| | -|:---------------------------------------|--------------------------------| -| [Sheets](https://www.univer.ai/examples/sheets/)
已开放:单元格样式、公式。一季度:条件格式、数据验证、查找替换。二季度(暂定):浮动图片、筛选、排序、批注、图表、数据透视表、超级表(table)、形状 | [![](./docs/img/examples-sheets.gif)](https://www.univer.ai/examples/sheets/) | -| [Sheets Multi](https://www.univer.ai/examples/sheets-multi/)
在一个页面中可以创建多个 Univer 实例,让表格间可以实现互操作 | [![](./docs/img/examples-sheets-multi.gif)](https://www.univer.ai/examples/sheets-multi/) | -| [Sheets Uniscript](https://www.univer.ai/examples/sheets-uniscript/)
在 Univer Sheets 中可以直接使用 JavaScript 语法操作表格中的数据,实现自动化 | [![](./docs/img/examples-sheets-uniscript.gif)](https://www.univer.ai/examples/sheets-uniscript/) | -| [Sheets Big Data](https://www.univer.ai/examples/sheets-big-data/)
加载 1000 万单元格数据量,在 500ms 内完成 | [![](./docs/img/examples-sheets-big-data.gif)](https://www.univer.ai/examples/sheets-big-data/) | -| [Sheets Collaboration](https://univer.ai/pro-examples/sheets-collaboration/)
请打开两个窗口或者邀请小伙伴一起体验 Univer Sheets 协同 | [![](./docs/img/pro-examples-sheets-collaboration.gif)](https://univer.ai/pro-examples/sheets-collaboration/) | -| [Sheets Collaboration Playground](https://univer.ai/pro-examples/sheets-collaboration-playground/)
演示协同的过程,A 编辑表格后,B 到底是如何处理的?这里是一个有趣的实验 | [![](./docs/img/pro-examples-sheets-collaboration-playground.gif)](https://univer.ai/pro-examples/sheets-collaboration-playground/) | -| [Sheets Import/Export](https://univer.ai/pro-examples/sheets-exchange/)
支持 xlsx 文件导入和导出 | [![](./docs/img/pro-examples-sheets-exchange.gif)](https://univer.ai/pro-examples/sheets-exchange/) | -| [Sheets Print](https://univer.ai/pro-examples/sheets-print/)
体验 Univer Sheets 的高清打印能力 | [![](./docs/img/pro-examples-sheets-print.gif)](https://univer.ai/pro-examples/sheets-print/) | -| [Sheets Data Validation / Conditional Formatting](https://univer-qqqkeqnw5-univer.vercel.app/sheets/)
Univer Sheets 数据格式和条件格式的开发预览版 | [![](./docs/img/examples-sheets-data-validation-conditional-format.png)](https://univer-qqqkeqnw5-univer.vercel.app/sheets/) | -|

📝 Univer Docs

| | -| [Docs](https://www.univer.ai/examples/docs/)
已开放:有序无序列表、段落设置、图文混排、分节展示多列/单列(暂定):超链接、批注、表格、图表 | [![](./docs/img/examples-docs.gif)](https://www.univer.ai/examples/docs/) | -| [Docs Multi](https://www.univer.ai/examples/docs-multi/)
在一个页面中可以创建多个 Univer 实例,让doc可以实现互操作 | [![](./docs/img/examples-docs-multi.gif)](https://www.univer.ai/examples/docs-multi/) | -| [Docs Uniscript](https://www.univer.ai/examples/docs-uniscript/)
在 Univer Docs 中可以直接使用 JavaScript 语法操作内容 | [![](./docs/img/examples-docs-uniscript.gif)](https://www.univer.ai/examples/docs-uniscript/) | -| [Docs Big Data](https://www.univer.ai/examples/docs-big-data/)
100 万字 Docs 加载演示 | [![](./docs/img/examples-docs-big-data.gif)](https://www.univer.ai/examples/docs-big-data/) | -| [Docs Collaboration](https://univer.ai/pro-examples/docs-collaboration/)
请打开两个窗口或者邀请小伙伴一起体验 Univer Docs 协同 | [![](./docs/img/pro-examples-docs-collaboration.gif)](https://univer.ai/pro-examples/docs-collaboration/) | -| [Docs Collaboration Playground](https://univer.ai/pro-examples/docs-collaboration-playground/)
演示协同的过程,A 编辑文档后,B 到底是如何处理的?这里是一个有趣的实验 | [![](./docs/img/pro-examples-docs-collaboration-playground.gif)](https://univer.ai/pro-examples/docs-collaboration-playground/) | -|

🎨 Univer Slides

| | -| [Slides](https://www.univer.ai/examples/slides/)
一个包含图文本、浮动图片、表格等元素的画布演示 | [![](./docs/img/examples-slides.gif)](https://www.univer.ai/examples/slides/) | -|

🧩 Univer Innovation

| | -| [Zen Mode](https://github.com/dream-num/univer)
Sheet 的单元格是一个 Doc? | [![](./docs/img/zen-mode.gif)](https://github.com/dream-num/univer) | -| [Univer(SaaS version)](https://univer.ai/)
通过 Univer,我们使用户能够根据自己的意愿创建 3 种形式的页面。 通过将工作表、文档和幻灯片的功能合并在一起,Univer 使个人和团队能够轻松创建、组织和简化工作流程。 | [![](./docs/img/univer-workspace-drag-chart.gif)](https://youtu.be/kpV0MvQuFZA) | +|   |   |   | +| :---: | :---: | :---: | +| 📊 Sheets | 📊 Sheets Multi | 📊 Sheets Uniscript | +| [![](./docs/img/examples-sheets.gif)](https://univer.ai/examples/sheets/) | [![](./docs/img/examples-sheets-multi.gif)](https://univer.ai/examples/sheets-multi/) | [![](./docs/img/examples-sheets-uniscript.gif)](https://univer.ai/examples/sheets-uniscript/) | +| 📊 Sheets Big Data | 📊 Sheets Collaboration (Pro) | 📊 Sheets Collaboration Playground (Pro) | +| [![](./docs/img/examples-sheets-big-data.gif)](https://univer.ai/examples/sheets-big-data/) | [![](./docs/img/pro-examples-sheets-collaboration.gif)](https://univer.ai/pro/examples/sheets-collaboration/) | [![](./docs/img/pro-examples-sheets-collaboration-playground.gif)](https://univer.ai/pro/examples/sheets-collaboration-playground/) | +| 📊 Sheets Import/Export (Pro) | 📊 Sheets Print (Pro) | 📝 Docs | +| [![](./docs/img/pro-examples-sheets-exchange.gif)](https://univer.ai/pro/examples/sheets-exchange/) | [![](./docs/img/pro-examples-sheets-print.gif)](https://univer.ai/pro/examples/sheets-print/) | [![](./docs/img/examples-docs.gif)](https://univer.ai/examples/docs/) | +| 📝 Docs Multi | 📝 Docs Uniscript | 📝 Docs Big Data | +| [![](./docs/img/examples-docs-multi.gif)](https://univer.ai/examples/docs-multi/) | [![](./docs/img/examples-docs-uniscript.gif)](https://univer.ai/examples/docs-uniscript/) | [![](./docs/img/examples-docs-big-data.gif)](https://univer.ai/examples/docs-big-data/) | +| 📝 Docs Collaboration (Pro) | 📝 Docs Collaboration Playground (Pro) | 📽️ Slides | +| [![](./docs/img/pro-examples-docs-collaboration.gif)](https://univer.ai/pro/examples/docs-collaboration/) | [![](./docs/img/pro-examples-docs-collaboration-playground.gif)](https://univer.ai/pro/examples/docs-collaboration-playground/) | [![](./docs/img/examples-slides.gif)](https://univer.ai/examples/slides/) | +| 📊 Zen Mode | Univer Workspace (SaaS version) |   | +| [![](./docs/img/zen-mode.gif)](https://univer.ai/zh-CN/guides/sheet/tutorials/zen-editor/#%E6%BC%94%E7%A4%BA) | [![](./docs/img/univer-workspace-drag-chart.gif)](https://youtu.be/kpV0MvQuFZA) |   | ## 使用 -我们建议通过将 Univer 作为 npm 包使用,请参考文档上的[快速开始](https://univer.ai/guides/quick-start/)小节。我们还准备了一个[在线 playground](https://univer.ai/playground/),你无需在本地安装 Univer 就可以体验使用 Univer 开发。 +我们建议通过将 Univer 作为 npm 包使用,请参考文档上的[快速开始](https://univer.ai/zh-CN/guides/sheet/getting-started/quickstart)小节。我们还准备了一个[在线 playground](https://univer.ai/playground/),你无需在本地安装 Univer 就可以体验使用 Univer 开发。 Univer 基于插件化架构设计,你可以安装以下包来增强 Univer 的功能。 @@ -89,6 +79,7 @@ Univer 基于插件化架构设计,你可以安装以下包来增强 Univer | 包名 | 描述 | 版本 | | :------------------------------------------- | :-------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------- | | [core](./packages/core) | Univer 核心包,实现 Univer 架构和插件机制、基础服务,以及各个文档类型的基本模型 | [![npm version](https://img.shields.io/npm/v/@univerjs/core)](https://npmjs.org/package/@univerjs/core) | +| [data-validation](./packages/data-validation) | 实现了 Univer 的数据验证功能 | [![npm version](https://img.shields.io/npm/v/@univerjs/data-validation)](https://npmjs.org/package/@univerjs/data-validation) | | [design](./packages/design) | 实现 Univer 设计语言,提供了一套 CSS 以及一套基于 React 的组件 | [![npm version](https://img.shields.io/npm/v/@univerjs/design)](https://npmjs.org/package/@univerjs/design) | | [docs](./packages/docs) | 实现了富文本文档的基本业务,同时支持其他业务的文本编辑 | [![npm version](https://img.shields.io/npm/v/@univerjs/docs)](https://npmjs.org/package/@univerjs/docs) | | [docs-ui](./packages/docs-ui) | 实现了富文本文档的用户交互 | [![npm version](https://img.shields.io/npm/v/@univerjs/docs-ui)](https://npmjs.org/package/@univerjs/docs-ui) | @@ -100,6 +91,9 @@ Univer 基于插件化架构设计,你可以安装以下包来增强 Univer | [network](./packages/network) | 实现了 Univer 的网络服务,包括 WebSocket 和 HTTP。 | [![npm version](https://img.shields.io/npm/v/@univerjs/network)](https://npmjs.org/package/@univerjs/network) | | [rpc](./packages/rpc) | 实现 RPC 机制,以及在主从文档副本之间同步数据的方法,方便 web worker 等跨线程场景的开发 | [![npm version](https://img.shields.io/npm/v/@univerjs/rpc)](https://npmjs.org/package/@univerjs/rpc) | | [sheets](./packages/sheets) | 实现电子表格的基本业务 | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets)](https://npmjs.org/package/@univerjs/sheets) | +| [sheets-conditional-formatting](./packages/sheets-conditional-formatting) | 实现电子表格的条件格式功能 | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-conditional-formatting)](https://npmjs.org/package/@univerjs/sheets-sheets-conditional-formatting) | +| [sheets-conditional-formatting-ui](./packages/sheets-conditional-formatting-ui) | 实现电子表格的条件格式功能 | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-conditional-formatting-ui)](https://npmjs.org/package/@univerjs/sheets-sheets-conditional-formatting-ui) | +| [sheets-data-validation](./packages/sheets-data-validation) | 实现电子表格的数据验证功能 | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-data-validation)](https://npmjs.org/package/@univerjs/sheets-data-validation) | | [sheets-find-replace](./packages/sheets-find-replace) | 实现电子表格的查找替换 | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-find-replace)](https://npmjs.org/package/@univerjs/sheets-find-replace) | | [sheets-formula](./packages/sheets-formula) | 实现电子表格的公式编辑 | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-formula)](https://npmjs.org/package/@univerjs/sheets-formula) | | [sheets-numfmt](./packages/sheets-numfmt) | 实现电子表格中的数字格式编辑 | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-numfmt)](https://npmjs.org/package/@univerjs/sheets-numfmt) | @@ -142,7 +136,7 @@ Univer 持续稳定发展离不开它的支持者和赞助者,如果你想要 ## 链接 -- [文档](https://univer.ai/guides/introduction/) +- [文档](https://univer.ai/zh-CN/guides/sheet/introduction) - [在线 Playground](https://univer.ai/playground/) - [官方网站](https://univer.ai) @@ -152,7 +146,7 @@ Univer 持续稳定发展离不开它的支持者和赞助者,如果你想要 - [Github Discussions](https://github.com/dream-num/univer/discussions) - 微信扫描下方二维码,加入 Univer 中文社群 -![wecom-qr-code](https://univer.ai/_astro/business-qr-code.3zPwMdHH_ZGnJEl.webp) +![wecom-qr-code](https://univer.ai/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fqrcode.45c72be6.png&w=828&q=75) ## 授权 @@ -160,4 +154,4 @@ Univer 基于 Apache-2.0 协议分发。 --- -上海梦数科技有限公司 2023 版权所有 +Copyright © 2019-2024 Shanghai DreamNum Technology Co., Ltd. All rights reserved diff --git a/README.md b/README.md index d1c7c50645..000c4e42c3 100644 --- a/README.md +++ b/README.md @@ -37,74 +37,70 @@ ## Introduction -Univer is a set of enterprise document and data collaboration solutions, including spreadsheets, documents, and slides. The highly extensible design allows developers to customize personalized functions based on Univer. +Univer is an open-source alternative to Google Sheets, Slides, and Docs. -Highlights of Univer: +Highlights: -- 📈 Univer supports spreadsheets. Documents and slides will be supported in the future. -- 🌌 Highly extensible architecture design. - - 🔌 Plug-in architecture, the capabilities of documents can be combined on demand, support third-party plug-ins, and facilitate customization development. - - 💄 Provide component library and icons to help developers present a consistent user experience. -- ⚡ High performance. - - ✏️ Unified and efficient rendering engine and formula engine, based on Canvas. - - 🧮 High-performance formula engine, supports Web Worker. -- 🌍 Internationalization support. +- 📈 Univer is designed to support both **spreadsheets** and **documents**. **Slides** will be supported as well in the future. +- ⚙️ Univer is easily **embeddable**, allowing seamless integration into your applications. +- 🎇 Univer is **powerful**, offering a wide range of features including **formulas**, **conditional formatting**, **data validation**, **filtering**, **collaborative editing**, **printing**, **import & export** and more features on the horizon. +- 🔌 Univer is **highly extensible**, thanks to its *plug-in architecture* and *Facade API* that makes it a delight for developers to implement their unique requirements on the top of Univer. +- 💄 Univer is **highly customizable**, allowing you to personalize its appearance using *themes*. It also provides support for internationalization (i18n). +- ⚡ Univer in **performant**. + - ✏️ Univer boasts an efficient *rendering engine* based on canvas, capable of rendering various document types flawlessly. The rendering engines supports advanced typesetting features such as *punctuation squeezing*, *text and image layout* and *scroll buffering*. + - 🧮 Univer incorporates a lightning-fast *formula engine* that can operate in Web Workers or even on the server side. +- 🌌 Univer is a **highly integrated** system. Documents, spreadsheets and slides can interoperate with each others and even rendered on the same canvas, allowing information and data flow within Univer. ## Examples -|

📊 Univer Sheets

| | -|:---------------------------------------|--------------------------------| -| [Sheets](https://www.univer.ai/examples/sheets/)
Opened: cell styles, formulas. First quarter: conditional formatting, data validation, search and replace. Second quarter (tentative): floating pictures, filtering, sorting, annotations, charts, pivot tables, super tables (tables), shapes | [![](./docs/img/examples-sheets.gif)](https://www.univer.ai/examples/sheets/) | -| [Sheets Multi](https://www.univer.ai/examples/sheets-multi/)
Multiple Univer instances can be created on a page to allow interoperability between tables | [![](./docs/img/examples-sheets-multi.gif)](https://www.univer.ai/examples/sheets-multi/) | -| [Sheets Uniscript](https://www.univer.ai/examples/sheets-uniscript/)
In Univer Sheets, you can directly use JavaScript syntax to operate the data in the table to achieve automation. | [![](./docs/img/examples-sheets-uniscript.gif)](https://www.univer.ai/examples/sheets-uniscript/) | -| [Sheets Big Data](https://www.univer.ai/examples/sheets-big-data/)
Loading 10 million cells of data, completed within 500ms | [![](./docs/img/examples-sheets-big-data.gif)](https://www.univer.ai/examples/sheets-big-data/) | -| [Sheets Collaboration](https://univer.ai/pro-examples/sheets-collaboration/)
Please open two windows or invite friends to experience Univer Sheets collaboration together | [![](./docs/img/pro-examples-sheets-collaboration.gif)](https://univer.ai/pro-examples/sheets-collaboration/) | -| [Sheets Collaboration Playground](https://univer.ai/pro-examples/sheets-collaboration-playground/)
Demonstrate the process of collaboration. After A edits the form, how does B process it? Here is an interesting experiment | [![](./docs/img/pro-examples-sheets-collaboration-playground.gif)](https://univer.ai/pro-examples/sheets-collaboration-playground/) | -| [Sheets Import/Export](https://univer.ai/pro-examples/sheets-exchange/)
Supports xlsx file import and export | [![](./docs/img/pro-examples-sheets-exchange.gif)](https://univer.ai/pro-examples/sheets-exchange/) | -| [Sheets Print](https://univer.ai/pro-examples/sheets-print/)
Experience the HD printing capabilities of Univer Sheets | [![](./docs/img/pro-examples-sheets-print.gif)](https://univer.ai/pro-examples/sheets-print/) | -| [Sheets Data Validation / Conditional Formatting](https://univer-qqqkeqnw5-univer.vercel.app/sheets/)
Development preview of Univer Sheets data formats and conditional formatting | [![](./docs/img/examples-sheets-data-validation-conditional-format.png)](https://univer-qqqkeqnw5-univer.vercel.app/sheets/) | -|

📝 Univer Docs

| | -| [Docs](https://www.univer.ai/examples/docs/)
Already open: ordered and unordered lists, paragraph settings, mixed graphics and text, multi-column/single column display in sections (tentative): hyperlinks, comments, tables, charts | [![](./docs/img/examples-docs.gif)](https://www.univer.ai/examples/docs/) | -| [Docs Multi](https://www.univer.ai/examples/docs-multi/)
Multiple Univer instances can be created in a page so that doc can interoperate. | [![](./docs/img/examples-docs-multi.gif)](https://www.univer.ai/examples/docs-multi/) | -| [Docs Uniscript](https://www.univer.ai/examples/docs-uniscript/)
You can directly use JavaScript syntax to manipulate content in Univer Docs | [![](./docs/img/examples-docs-uniscript.gif)](https://www.univer.ai/examples/docs-uniscript/) | -| [Docs Big Data](https://www.univer.ai/examples/docs-big-data/)
1 million word Docs loading demo | [![](./docs/img/examples-docs-big-data.gif)](https://www.univer.ai/examples/docs-big-data/) | -| [Docs Collaboration](https://univer.ai/pro-examples/docs-collaboration/)
Please open two windows or invite friends to experience Univer Docs collaboration together | [![](./docs/img/pro-examples-docs-collaboration.gif)](https://univer.ai/pro-examples/docs-collaboration/) | -| [Docs Collaboration Playground](https://univer.ai/pro-examples/docs-collaboration-playground/)
Demonstrate the process of collaboration. After A edits the document, how does B process it? Here is an interesting experiment | [![](./docs/img/pro-examples-docs-collaboration-playground.gif)](https://univer.ai/pro-examples/docs-collaboration-playground/) | -|

🎨 Univer Slides

| | -| [Slides](https://www.univer.ai/examples/slides/)
A canvas presentation containing graphic text, floating pictures, tables and other elements | [![](./docs/img/examples-slides.gif)](https://www.univer.ai/examples/slides/) | -|

🧩 Univer Innovation

| | -| [Zen Mode](https://github.com/dream-num/univer)
The cell of Sheet is a Doc? | [![](./docs/img/zen-mode.gif)](https://github.com/dream-num/univer) | -| [Univer(SaaS version)](https://univer.ai/)
With Univer, we enable users to create 3 forms of page as they wish. By merging sheet, doc and slide's capabilities together, Univer empowers individuals and teams to create, organize and streamline workflows effortlessly. | [![](./docs/img/univer-workspace-drag-chart.gif)](https://youtu.be/kpV0MvQuFZA) | +|   |   |   | +| :---: | :---: | :---: | +| 📊 Sheets | 📊 Sheets Multi | 📊 Sheets Uniscript | +| [![](./docs/img/examples-sheets.gif)](https://univer.ai/examples/sheets/) | [![](./docs/img/examples-sheets-multi.gif)](https://univer.ai/examples/sheets-multi/) | [![](./docs/img/examples-sheets-uniscript.gif)](https://univer.ai/examples/sheets-uniscript/) | +| 📊 Sheets Big Data | 📊 Sheets Collaboration (Pro) | 📊 Sheets Collaboration Playground (Pro) | +| [![](./docs/img/examples-sheets-big-data.gif)](https://univer.ai/examples/sheets-big-data/) | [![](./docs/img/pro-examples-sheets-collaboration.gif)](https://univer.ai/pro/examples/sheets-collaboration/) | [![](./docs/img/pro-examples-sheets-collaboration-playground.gif)](https://univer.ai/pro/examples/sheets-collaboration-playground/) | +| 📊 Sheets Import/Export (Pro) | 📊 Sheets Print (Pro) | 📝 Docs | +| [![](./docs/img/pro-examples-sheets-exchange.gif)](https://univer.ai/pro/examples/sheets-exchange/) | [![](./docs/img/pro-examples-sheets-print.gif)](https://univer.ai/pro/examples/sheets-print/) | [![](./docs/img/examples-docs.gif)](https://univer.ai/examples/docs/) | +| 📝 Docs Multi | 📝 Docs Uniscript | 📝 Docs Big Data | +| [![](./docs/img/examples-docs-multi.gif)](https://univer.ai/examples/docs-multi/) | [![](./docs/img/examples-docs-uniscript.gif)](https://univer.ai/examples/docs-uniscript/) | [![](./docs/img/examples-docs-big-data.gif)](https://univer.ai/examples/docs-big-data/) | +| 📝 Docs Collaboration (Pro) | 📝 Docs Collaboration Playground (Pro) | 📽️ Slides | +| [![](./docs/img/pro-examples-docs-collaboration.gif)](https://univer.ai/pro/examples/docs-collaboration/) | [![](./docs/img/pro-examples-docs-collaboration-playground.gif)](https://univer.ai/pro/examples/docs-collaboration-playground/) | [![](./docs/img/examples-slides.gif)](https://univer.ai/examples/slides/) | +| 📊 Zen Mode | Univer Workspace (SaaS version) |   | +| [![](./docs/img/zen-mode.gif)](https://univer.ai/zh-CN/guides/sheet/tutorials/zen-editor/#%E6%BC%94%E7%A4%BA) | [![](./docs/img/univer-workspace-drag-chart.gif)](https://youtu.be/kpV0MvQuFZA) |   | ## Usage -We recommend to import Univer as a npm package. Please checkout the [Quick Start](https://univer.ai/guides/quick-start/) section on the documentation website. We also have an [online playground](https://univer.ai/playground/) which can help you preview Univer without setting up the development environment. +We recommend to import Univer as a npm package. Please checkout the [Quick Start](https://univer.ai/guides/sheet/getting-started/quickstart) section on the documentation website. We also have an [online playground](https://univer.ai/playground/) which can help you preview Univer without setting up the development environment. Univer bases on a plugin architecture. You can install the following packages to enhance the functionality of Univer. ### Packages -| Name | Description | Version | -| :----------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------ | -| [core](./packages/core) | Implements plugin system and architecture of Univer. It also provides basic services and models of different types of documents. | [![npm version](https://img.shields.io/npm/v/@univerjs/core)](https://npmjs.org/package/@univerjs/core) | -| [design](./packages/design) | Implements the design system on Univer. It provides CSS and a component kit based on React. | [![npm version](https://img.shields.io/npm/v/@univerjs/design)](https://npmjs.org/package/@univerjs/design) | -| [docs](./packages/docs) | Implements basic logics of rich text editing features. It also facilitates text editing in other types of documents. | [![npm version](https://img.shields.io/npm/v/@univerjs/docs)](https://npmjs.org/package/@univerjs/docs) | -| [docs-ui](./packages/docs-ui) | Provides user interface of Univer Documents | [![npm version](https://img.shields.io/npm/v/@univerjs/docs-ui)](https://npmjs.org/package/@univerjs/docs-ui) | -| [engine-formula](./packages/engine-formula) | It implements a rendering engine based on Canvas and is extensible for | [![npm version](https://img.shields.io/npm/v/@univerjs/engine-formula)](https://npmjs.org/package/@univerjs/engine-formula) | -| [engine-numfmt](./packages/engine-numfmt) | It implements a number format engine. | [![npm version](https://img.shields.io/npm/v/@univerjs/engine-numfmt)](https://npmjs.org/package/@univerjs/engine-numfmt) | -| [engine-render](./packages/engine-render) | It implements a rendering engine based on canvas context2d. | [![npm version](https://img.shields.io/npm/v/@univerjs/engine-render)](https://npmjs.org/package/@univerjs/engine-render) | -| [facade](./packages/facade) | It serves as an API layer to make it easier to use Univer | [![npm version](https://img.shields.io/npm/v/@univerjs/facade)](https://npmjs.org/package/@univerjs/facade) | -| [find-replace](./packages/find-replace) | It implements find and replace features in Univer. | [![npm version](https://img.shields.io/npm/v/@univerjs/find-replace)](https://npmjs.org/package/@univerjs/find-replace) | -| [network](./packages/network) | It implements network services based on WebSocket and HTTP. | [![npm version](https://img.shields.io/npm/v/@univerjs/network)](https://npmjs.org/package/@univerjs/network) | -| [rpc](./packages/rpc) | It implements a RPC mechanism and methods to sync data between different replicas of Univer documents. | [![npm version](https://img.shields.io/npm/v/@univerjs/rpc)](https://npmjs.org/package/@univerjs/rpc) | -| [sheets](./packages/sheets) | Basic logics of spreadsheet features. | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets)](https://npmjs.org/package/@univerjs/sheets) | -| [sheets-find-replace](./packages/sheets-find-replace) | It implements find and replace features in Univer Spreadsheets. | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-find-replace)](https://npmjs.org/package/@univerjs/sheets-find-replace) | -| [sheets-formula](./packages/sheets-formula) | It implements formula in spreadsheets. | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-formula)](https://npmjs.org/package/@univerjs/sheets-formula) | -| [sheets-numfmt](./packages/sheets-numfmt) | It implements number format in spreadsheets. | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-numfmt)](https://npmjs.org/package/@univerjs/sheets-numfmt) | -| [sheets-zen-editor](./packages/sheets-zen-editor) | It implements Zen editing mode in spreadsheets. | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-zen-editor)](https://npmjs.org/package/@univerjs/sheets-zen-editor) | -| [sheets-ui](./packages/sheets-ui) | Provides user interface of Univer Spreadsheets | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-ui)](https://npmjs.org/package/@univerjs/sheets-ui) | -| [ui](./packages/ui) | Implements basic user interactions with Univer and workbench layout based on React. | [![npm version](https://img.shields.io/npm/v/@univerjs/ui)](https://npmjs.org/package/@univerjs/ui) | -| [uniscript](./packages/uniscript) (experimental) | Implements a DSL based on Typescript that empowers users to accomplish more sophisticated tasks | [![npm version](https://img.shields.io/npm/v/@univerjs/uniscript)](https://npmjs.org/package/@univerjs/uniscript) | +| Name | Description | Version | +| :-------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [core](./packages/core) | Implements plugin system and architecture of Univer. It also provides basic services and models of different types of documents. | [![npm version](https://img.shields.io/npm/v/@univerjs/core)](https://npmjs.org/package/@univerjs/core) | +| [data-validation](./packages/data-validation) | Implements data validation features in Univer. | [![npm version](https://img.shields.io/npm/v/@univerjs/data-validation)](https://npmjs.org/package/@univerjs/data-validation) | +| [design](./packages/design) | Implements the design system on Univer. It provides CSS and a component kit based on React. | [![npm version](https://img.shields.io/npm/v/@univerjs/design)](https://npmjs.org/package/@univerjs/design) | +| [docs](./packages/docs) | Implements basic logics of rich text editing features. It also facilitates text editing in other types of documents. | [![npm version](https://img.shields.io/npm/v/@univerjs/docs)](https://npmjs.org/package/@univerjs/docs) | +| [docs-ui](./packages/docs-ui) | Provides user interface of Univer Documents | [![npm version](https://img.shields.io/npm/v/@univerjs/docs-ui)](https://npmjs.org/package/@univerjs/docs-ui) | +| [engine-formula](./packages/engine-formula) | It implements a rendering engine based on Canvas and is extensible for | [![npm version](https://img.shields.io/npm/v/@univerjs/engine-formula)](https://npmjs.org/package/@univerjs/engine-formula) | +| [engine-numfmt](./packages/engine-numfmt) | It implements a number format engine. | [![npm version](https://img.shields.io/npm/v/@univerjs/engine-numfmt)](https://npmjs.org/package/@univerjs/engine-numfmt) | +| [engine-render](./packages/engine-render) | It implements a rendering engine based on canvas context2d. | [![npm version](https://img.shields.io/npm/v/@univerjs/engine-render)](https://npmjs.org/package/@univerjs/engine-render) | +| [facade](./packages/facade) | It serves as an API layer to make it easier to use Univer | [![npm version](https://img.shields.io/npm/v/@univerjs/facade)](https://npmjs.org/package/@univerjs/facade) | +| [find-replace](./packages/find-replace) | It implements find and replace features in Univer. | [![npm version](https://img.shields.io/npm/v/@univerjs/find-replace)](https://npmjs.org/package/@univerjs/find-replace) | +| [network](./packages/network) | It implements network services based on WebSocket and HTTP. | [![npm version](https://img.shields.io/npm/v/@univerjs/network)](https://npmjs.org/package/@univerjs/network) | +| [rpc](./packages/rpc) | It implements a RPC mechanism and methods to sync data between different replicas of Univer documents. | [![npm version](https://img.shields.io/npm/v/@univerjs/rpc)](https://npmjs.org/package/@univerjs/rpc) | +| [sheets](./packages/sheets) | Basic logics of spreadsheet features. | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets)](https://npmjs.org/package/@univerjs/sheets) | +| [sheets-conditional-formatting](./packages/sheets-conditional-formatting) | It implements conditional formatting in Univer Spreadsheets. | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-conditional-formatting)](https://npmjs.org/package/@univerjs/sheets-conditional-formatting) | +| [sheets-conditional-formatting-ui](./packages/sheets-conditional-formatting-ui) | It implements conditional formatting in Univer Spreadsheets. | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-conditional-formatting-ui)](https://npmjs.org/package/@univerjs/sheets-conditional-formatting-ui) | +| [sheets-data-validation](./packages/data-validation) | It implements data validation in Univer Spreadsheets. | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-data-validation)](https://npmjs.org/package/@univerjs/sheets-data-validation) | +| [sheets-find-replace](./packages/sheets-find-replace) | It implements find and replace features in Univer Spreadsheets. | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-find-replace)](https://npmjs.org/package/@univerjs/sheets-find-replace) | +| [sheets-formula](./packages/sheets-formula) | It implements formula in spreadsheets. | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-formula)](https://npmjs.org/package/@univerjs/sheets-formula) | +| [sheets-numfmt](./packages/sheets-numfmt) | It implements number format in spreadsheets. | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-numfmt)](https://npmjs.org/package/@univerjs/sheets-numfmt) | +| [sheets-zen-editor](./packages/sheets-zen-editor) | It implements Zen editing mode in spreadsheets. | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-zen-editor)](https://npmjs.org/package/@univerjs/sheets-zen-editor) | +| [sheets-ui](./packages/sheets-ui) | Provides user interface of Univer Spreadsheets | [![npm version](https://img.shields.io/npm/v/@univerjs/sheets-ui)](https://npmjs.org/package/@univerjs/sheets-ui) | +| [ui](./packages/ui) | Implements basic user interactions with Univer and workbench layout based on React. | [![npm version](https://img.shields.io/npm/v/@univerjs/ui)](https://npmjs.org/package/@univerjs/ui) | +| [uniscript](./packages/uniscript) (experimental) | Implements a DSL based on Typescript that empowers users to accomplish more sophisticated tasks | [![npm version](https://img.shields.io/npm/v/@univerjs/uniscript)](https://npmjs.org/package/@univerjs/uniscript) | ## Contribution @@ -140,7 +136,7 @@ Thanks to our sponsors, just part of them are listed here because of the space l ## Links -- [Documentation](https://univer.ai/guides/introduction/) +- [Documentation](https://univer.ai/guides/sheet/introduction) - [Online Playground](https://univer.ai/playground/) - [Official Website](https://univer.ai) @@ -154,4 +150,4 @@ Univer is distributed under the terms of the Apache-2.0 license. --- -Copyright DreamNum Inc. 2023-present +Copyright © 2019-2024 Shanghai DreamNum Technology Co., Ltd. All rights reserved diff --git a/common/shared/eslint/index.js b/common/shared/eslint/index.js index 8dbf4fc0c0..43f5b12f37 100644 --- a/common/shared/eslint/index.js +++ b/common/shared/eslint/index.js @@ -1,8 +1,10 @@ exports.baseRules = { curly: ['error', 'multi-line'], - 'import/no-cycle': 'error', + 'no-empty-function': ['error'], + 'eol-last': ['error', 'always'], 'import/no-self-import': 'error', 'ts/no-explicit-any': 'warn', + 'style/no-multiple-empty-lines': ['error', { max: 1, maxEOF: 1 }], 'style/brace-style': ['warn', '1tbs', { allowSingleLine: true }], 'style/comma-dangle': ['error', { arrays: 'always-multiline', @@ -12,6 +14,7 @@ exports.baseRules = { enums: 'always-multiline', functions: 'never', }], + 'no-empty-function': 'off', 'style/arrow-parens': ['error', 'always'], 'ts/no-redeclare': 'off', 'antfu/if-newline': 'off', @@ -35,6 +38,9 @@ exports.baseRules = { memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], }, ], + 'react/no-unstable-context-value': 'warn', + 'react/no-unstable-default-props': 'warn', + 'command/command': 'off', // TODO: debatable rules 'test/prefer-lowercase-title': 'off', diff --git a/common/shared/package.json b/common/shared/package.json index a52248f43b..4b336b84de 100644 --- a/common/shared/package.json +++ b/common/shared/package.json @@ -1,6 +1,6 @@ { "name": "@univerjs/shared", - "version": "0.1.4", + "version": "0.1.12", "private": true, "description": "Some infrastructures for univerjs", "author": "DreamNum ", @@ -17,12 +17,13 @@ "./vite": "./vite/index.js" }, "dependencies": { - "@typescript-eslint/parser": "^7.4.0", + "@typescript-eslint/parser": "^7.10.0", "@vitejs/plugin-react": "^4.2.1", - "@vitest/coverage-istanbul": "^1.4.0", - "happy-dom": "^13.3.8", - "vite": "^5.2.6", - "vite-plugin-dts": "^3.7.3", - "vitest": "^1.4.0" + "@vitest/coverage-istanbul": "^1.6.0", + "happy-dom": "13.3.8", + "javascript-obfuscator": "^4.1.0", + "vite": "^5.2.11", + "vite-plugin-dts": "^3.9.1", + "vitest": "^1.6.0" } } diff --git a/common/shared/vite/auto-externalize-dependency-plugin.js b/common/shared/vite/auto-externalize-dependency-plugin.js index eab4104173..40b81a549c 100644 --- a/common/shared/vite/auto-externalize-dependency-plugin.js +++ b/common/shared/vite/auto-externalize-dependency-plugin.js @@ -1,3 +1,19 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + const process = require('node:process'); const { writeFileSync } = require('node:fs'); const { convertLibNameFromPackageName } = require('./utils'); @@ -11,7 +27,7 @@ exports.autoExternalizeDependency = function autoExternalizeDependency() { '@wendellhu/redi': { global: '@wendellhu/redi', name: '@wendellhu/redi', - version: '0.13.0', + version: '0.15.2', }, '@wendellhu/redi/react-bindings': { global: '@wendellhu/redi/react-bindings', @@ -23,6 +39,11 @@ exports.autoExternalizeDependency = function autoExternalizeDependency() { name: 'clsx', version: '>=2.0.0', }, + dayjs: { + global: 'dayjs', + name: 'dayjs', + version: '>=1.11.0', + }, lodash: { global: 'lodash', name: 'lodash', @@ -41,12 +62,12 @@ exports.autoExternalizeDependency = function autoExternalizeDependency() { react: { global: 'React', name: 'react', - version: '>=16.9.0', + version: '^16.9.0 || ^17.0.0 || ^18.0.0', }, 'react-dom': { global: 'ReactDOM', name: 'react-dom', - version: '>=16.9.0', + version: '^16.9.0 || ^17.0.0 || ^18.0.0', }, rxjs: { global: 'rxjs', @@ -62,6 +83,7 @@ exports.autoExternalizeDependency = function autoExternalizeDependency() { global: 'Vue', name: 'vue', version: '>=3.0.0', + optional: true, }, }; @@ -112,15 +134,23 @@ exports.autoExternalizeDependency = function autoExternalizeDependency() { // generate peerDependencies const pkg = require(`${process.cwd()}/package.json`); const peerDependencies = {}; + let optionalDependencies; Array.from(externals) .sort() .forEach((ext) => { - const { version, name } = externalMap[ext] ?? {}; + const { version, name, optional } = externalMap[ext] ?? {}; if (version) { if (version !== name) { - peerDependencies[ext] = version; + if (optional) { + if (!optionalDependencies) { + optionalDependencies = {}; + } + optionalDependencies[ext] = version; + } else { + peerDependencies[ext] = version; + } } else { if (!peerDependencies[version]) { peerDependencies[name] = externalMap[version].version; @@ -132,6 +162,9 @@ exports.autoExternalizeDependency = function autoExternalizeDependency() { }); pkg.peerDependencies = peerDependencies; + if (optionalDependencies) { + pkg.optionalDependencies = optionalDependencies; + } writeFileSync( `${process.cwd()}/package.json`, diff --git a/common/shared/vite/build-locale.js b/common/shared/vite/build-locale.js new file mode 100644 index 0000000000..d4b8e51730 --- /dev/null +++ b/common/shared/vite/build-locale.js @@ -0,0 +1,66 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const process = require('node:process'); +const fs = require('node:fs'); +const path = require('node:path'); +const esbuild = require('esbuild'); + +exports.buildLocale = function buildLocale({ entryRoot, outDir }) { + return { + name: 'build-locale', + enforce: 'pre', + generateBundle() { + const srcDir = path.resolve(process.cwd(), entryRoot); + + if (!fs.existsSync(srcDir)) { + return; + } + + const outputDir = path.resolve(process.cwd(), outDir); + + fs.readdirSync(srcDir) + .filter((file) => file.includes('-') && fs.statSync(path.resolve(srcDir, file)).isFile()) + .forEach((file) => { + const fullPath = path.join(srcDir, file); + + const syncResult = esbuild.buildSync({ + entryPoints: [fullPath], + bundle: true, + platform: 'node', + format: 'cjs', + write: false, + }); + + // eslint-disable-next-line no-new-func + const module = new Function('module', `${syncResult.outputFiles[0].text};return module;`)({}); + const result = module.exports.default; + const data = JSON.stringify(result, null, 2); + + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + const outputPath = path.resolve(outputDir, file.replace('.ts', '.json')); + + fs.writeFileSync(outputPath, data); + + // eslint-disable-next-line no-console + console.log(`[vite:build-locale] ${outputPath} generated`); + }); + }, + }; +}; diff --git a/common/shared/vite/index.js b/common/shared/vite/index.js index ffda261732..7fb052bd41 100644 --- a/common/shared/vite/index.js +++ b/common/shared/vite/index.js @@ -21,6 +21,8 @@ const { defineConfig, mergeConfig } = require('vitest/config'); const { default: dts } = require('vite-plugin-dts'); const react = require('@vitejs/plugin-react'); const { autoExternalizeDependency } = require('./auto-externalize-dependency-plugin'); +const { obfuscator } = require('./obfuscator'); +const { buildLocale } = require('./build-locale'); const { convertLibNameFromPackageName } = require('./utils'); /** @@ -61,6 +63,11 @@ function createViteConfig(overrideConfig, /** @type {IOptions} */ options) { entryRoot: 'src', outDir: 'lib/types', }), + obfuscator(), + buildLocale({ + entryRoot: 'src/locale', + outDir: 'lib/locale', + }), ], define: { 'process.env.NODE_ENV': JSON.stringify(mode), diff --git a/common/shared/vite/obfuscator.js b/common/shared/vite/obfuscator.js new file mode 100644 index 0000000000..681e1d5605 --- /dev/null +++ b/common/shared/vite/obfuscator.js @@ -0,0 +1,36 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const process = require('node:process'); +const JavaScriptObfuscator = require('javascript-obfuscator'); + +exports.obfuscator = function obfuscator() { + return { + name: 'obfuscator', + enforce: 'post', + async generateBundle(_options, bundle) { + if (process.env.BUNDLE_TYPE === 'lite') { + for (const file in bundle) { + if (bundle[file].type === 'chunk' && /\.js$/.test(file)) { + const code = bundle[file].code; + const obfuscationResult = JavaScriptObfuscator.obfuscate(code); + bundle[file].code = obfuscationResult.getObfuscatedCode(); + } + } + } + }, + }; +}; diff --git a/common/shared/vite/utils.js b/common/shared/vite/utils.js index 7df873b2b8..b2a06169ce 100644 --- a/common/shared/vite/utils.js +++ b/common/shared/vite/utils.js @@ -1,3 +1,19 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * Convert package name to library name * @examples `@univerjs/core` -> `UniverCore` diff --git a/common/storybook/.storybook/main.ts b/common/storybook/.storybook/main.ts index 48a14e4204..2d98f3d8e4 100644 --- a/common/storybook/.storybook/main.ts +++ b/common/storybook/.storybook/main.ts @@ -39,8 +39,9 @@ const config: StorybookConfig = { const rootDir = resolve(__dirname, rootPath); if (existsSync(rootDir)) { readdirSync(rootDir).forEach((pkg) => { - const pkgDir = resolve(rootDir, pkg); - if (existsSync(pkgDir)) { + const pkgPath = resolve(rootDir, pkg, 'package.json'); + const srcDir = resolve(rootDir, pkg, 'src'); + if (existsSync(pkgPath) && existsSync(srcDir)) { stories.push({ directory: `${rootPath}/${pkg}/src`, files: '**/*.stories.@(js|jsx|mjs|ts|tsx)', diff --git a/common/storybook/package.json b/common/storybook/package.json index 7d2a98e8d6..57fc457bf6 100644 --- a/common/storybook/package.json +++ b/common/storybook/package.json @@ -1,6 +1,6 @@ { "name": "@univerjs/storybook", - "version": "0.1.4", + "version": "0.1.12", "private": true, "description": "Some infrastructures for univerjs", "author": "DreamNum ", @@ -16,28 +16,28 @@ "build": "storybook build" }, "dependencies": { - "@chromatic-com/storybook": "^1.2.25", - "@storybook/addon-essentials": "8.0.4", - "@storybook/addon-interactions": "8.0.4", - "@storybook/addon-links": "8.0.4", + "@chromatic-com/storybook": "^1.4.0", + "@storybook/addon-essentials": "^8.1.2", + "@storybook/addon-interactions": "^8.1.2", + "@storybook/addon-links": "^8.1.2", "@storybook/addon-styling-webpack": "^1.0.0", "@storybook/addon-webpack5-compiler-swc": "^1.0.2", - "@storybook/blocks": "8.0.4", + "@storybook/blocks": "^8.1.2", "@storybook/icons": "^1.2.9", - "@storybook/react": "8.0.4", - "@storybook/react-webpack5": "8.0.4", - "@storybook/types": "^8.0.4", + "@storybook/react": "^8.1.2", + "@storybook/react-webpack5": "^8.1.2", + "@storybook/types": "^8.1.2", "@univerjs/core": "workspace:*", "@univerjs/design": "workspace:*", "@univerjs/ui": "workspace:*", - "@wendellhu/redi": "^0.13.0", + "@wendellhu/redi": "0.15.2", "css-loader": "^6.10.0", "less-loader": "^12.2.0", - "storybook": "8.0.4", + "storybook": "^8.1.2", "storybook-addon-swc": "^1.2.0", "style-loader": "^3.3.4", "tsconfig-paths-webpack-plugin": "^4.1.0", - "typescript": "^5.4.3" + "typescript": "^5.4.5" }, "devDependencies": { "@univerjs/shared": "workspace:*" diff --git a/docs/NAMING_CONVENTION.md b/docs/NAMING_CONVENTION.md new file mode 100644 index 0000000000..49fb9c80c7 --- /dev/null +++ b/docs/NAMING_CONVENTION.md @@ -0,0 +1,156 @@ +# Univer Naming Convention + +To ensure code quality and consistency, please adhere to the following guidelines. + +## Files & Folders + +Use kebab-case for both file names and folder names. If the file contains a React component, it should be in PascalCase. For example: + +```txt +// ✅ +src/ + components/ + my-component/ + my-util.ts + MyComponent.tsx + +// 🚫 +src/ + components/ + myComponent/ + myUtil.ts + my-component.tsx +``` + +Folder names should be in plural format. Files names should be in singular format. For example: + +```txt +// ✅ +src/ + services/ + util.ts + +// 🚫 +src/ + service/ + utils.ts +``` + +Do use conventional type names including .service, .controller, .menu, .command, .mutation, and .operation. Invent additional type names if you must but take care not to create too many. For example: + +```txt +// ✅ +src/ + services/ + log.service.ts + user.service.ts + controllers/ + log.controller.ts + user.controller.ts +``` + +## Interfaces + +Interfaces should be named starting with a capital "I". For example: + +```typescript +// ✅ +export interface IMyInterface {} + +// 🚫 +export interface MyInterface {} +``` + +## Dependency Injection Token + +Sometimes you need to defined a dependency injection token. Please adhere to the following naming convention: + +```typescript +export const IYourServiceOrControllerName = createIdentifier('..(service|controller)'); +``` + +For example: + +```typescript +// ✅ +export const ILogService = createIdentifier('core.log.service'); + +// 🚫 +export const ILogService = createIdentifier('log-service'); +``` + +## Plugins' Names + +Plugin names should be all in format of `__PLUGIN`. Words should be separated by underscores and suffixed. For example: + +```typescript +// ✅ +export const SHEET_CONDITIONAL_FORMATTING_PLUGIN = 'SHEET_CONDITIONAL_FORMATTING_PLUGIN'; + +// 🚫 +export const SHEET_CONDITIONAL_FORMATTING_PLUGIN = 'SHEET_CONDITIONAL_FORMATTING'; + +// 🚫 +export const SHEET_CONDITIONAL_FORMATTING_PLUGIN = 'sheet-conditional-formatting-plugin'; +``` + +### Plugins' classes + +Plugin classes should be named in PascalCase and prefixed by `Univer`. For example: + +```typescript +// ✅ +export class UniverFilterPlugin extends Plugin {} + +// 🚫 +export class FilterPlugin extends Plugin {} +``` + +### Resource key + +Resource key should be identical to the corresponding plugin's name. + +## Commands + +Commands' names should follow the convention below: + +```typescript +export interface ISomeCommandParams { + // Define the parameters here +} + +export const SomeCommand = ICommand = { + id: '..', +}; +``` + +For example: + +```typescript +// ✅ +export const SetSelectionFrozenCommand: ICommand = { + id: 'sheet.command.set-selection-frozen', // note this should be in single format +} + +// 🚫 +export const SetSelectionFrozenCommand: ICommand = { + id: 'sheets.command.set-selection-frozen', +} + +// 🚫 +export const SetSelectionFrozenCommand: ICommand = { + id: 'SetSelectionFrozenCommand', +} +``` + +If this command is for general purpose, the `business-type` should be the plugin's name. For example: + +```typescript +export const ResolveCommentCommand: ICommand = { + id: 'thread-comment.command.resolve-comment', +} +``` + +## Id + +All IDs should be in pascal case: `id` or `Id`. diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index d9a3b800a8..0000000000 --- a/docs/architecture.md +++ /dev/null @@ -1,315 +0,0 @@ -# Architecture Nodes - -## Univer Architecture Introduction - -Univer is a web-based office collaboration and data processing SDK, mainly in the form of Office products (sheet / doc / slide). Univer organizes code in a plugin-based manner, allowing users to choose plugins according to their actual needs to form a Univer application. For example, users can add capabilities such as collaborative editing, macro recording, and AI-generated scripting languages to traditional spreadsheets in a plugin-based manner. Users can also embed Univer applications into their own applications, and integrate the capabilities of Univer and their own applications through plugins. And, with the help of Univer's official database connectors, user can load and process data in Univer, leveraging Univer's plugin ecosystem. - -## Core Requirements for Univer Architecture Design - -Some of these design requirements come from the product planning of Univer, and some come from the learning of team members participating in other project research and development. - -1. **100% embrace the web technology stack**. Univer needs to run in a considerable number of environments, meet the needs of rapid iteration, and allow customers, ISVs, and communities to have the ability to develop secondary development. The only technology stack that can meet these needs is the technology stack with web technology as its core. -2. **Pluginization and high scalability**. Univer's modules should be as plug-in as possible, and the coupling relationship between plug-ins should be decoupled as much as possible, so as to reduce the cost of adapting to different user needs and different operating environments, and reduce the threshold for secondary development. -3. **Hierarchical structure and one-way dependency**. Univer's modules are not allowed to have circular dependencies, which allows us to load the required levels and modules according to the needs of different environments. -4. **Design for multiple platforms**. Decouple the coupling relationship between code and specific operating environment to facilitate migration to different operating environments. -5. **Design for high testability**. The modules are based on interfaces as much as possible to establish dependency relationships, which is convenient for independent testing. - -## Plugins and Dependency Injection - -### Plugin - -Univer's modules should be considered from the perspective of **business type (Sheet / Doc / Slide), concern (configuration management / UI / shortcut keys / canvas rendering), function (Sheet basic operation / Sheet conditional format / Sheet filter, operating environment (desktop / mobile / Node.js, etc.)** and other factors, divided into various plugins (plugin), combined into a Univer application. - -For example, you could create a standard Spreadsheet application like this: - -```ts -import { UniverDocs } from '@univerjs/docs'; -import { UniverRenderEngine } from '@univerjs/engine-render'; -import { sheetsPlugin } from '@univerjs/sheets'; -import { UniverUI } from '@univerjs/ui'; -import { LocaleType, Univer } from '@univerjs/core'; -import { defaultTheme } from '@univerjs/design'; -import { FormulaPlugin } from '@univerjs/sheets-formula'; -import { UniverSheetsUI } from '@univerjs/sheets-ui'; - -const univer = new Univer({ - theme: defaultTheme, - locale: LocaleType.ZH_CN, -}); - -univer.registerPlugin(UniverDocs, { - hasScroll: false, -}); -univer.registerPlugin(UniverRenderEngine); -univer.registerPlugin(UniverUI, { - container: 'univer-container', // where to mount the UI - header: true, - footer: true, -}); -univer.registerPlugin(sheetsPlugin); -univer.registerPlugin(UniverSheetsUI); -univer.registerPlugin(FormulaPlugin); - -// call univer.createUniverSheet() to create a spreadsheet -``` - -The design based on plugins enables Univer to meet various operating environments (PC browser / Node / mobile terminal), different functional requirements, different configuration requirements, secondary development, third-party plugins and other needs. - -You can refer to the document [Plugin Extension Capability](./plugin-extension-capability.md) for more information about the plugin extension capability. - -### Dependency injection - - - -The plugin can divide the code into the various layers of modules introduced in the "Hierarchical Structure" section below according to actual needs. The service and controller in these modules need to be added to the dependency injection system of Univer, so that Univer can automatically resolve the dependency relationship between these modules and instantiate these modules. The documentation of the dependency injection system refers to [redi - redi](https://redi.wendell.fun/zh-CN). - -### Public and private modules of plugins and extension points - -You can export the identifier of these modules in the index.ts file of each plugin. If the identifier of a module is exported, then other plugins can import the identifier of these modules to establish a dependency relationship with these modules, and these modules become public modules of the previous plugin, otherwise it is a private module. If you are familiar with Angular, you will easily find that this is very similar to the concept of NgModule, except that we do not need to declare the exports field, but use the export of es module to distinguish public modules. - -### Plugin lifecycle - -Plugins have the following four lifecycles - -```ts -export const enum LifecycleStages { - Starting, - Ready, - Rendered, - Steady, -} -``` - -* `Starting` The first lifecycle of the plugin mounted on the Univer instance, at this time the Univer business instance has not been created. The plugin should add its own modules to the dependency injection system during this lifecycle. It is not recommended to initialize the internal modules of the plugin outside this lifecycle. -* `Ready` The first business instance of Univer has been created, and the plugin can do most of the initialization work during this lifecycle. -* `Rendered` The first rendering has been completed, and the plugin can do initialization work that requires DOM dependency during this lifecycle. -* `Steady` Triggered after a period of time after `Rendered`, the plugin can do non-first screen must work during this lifecycle to improve loading performance. - -Correspondingly, there are four lifecycle hooks on the Plugin type - -```ts -/** - * Plug-in base class, all plug-ins must inherit from this base class. Provide basic methods. - */ -export abstract class Plugin { - onStarting(_injector: Injector): void {} - - onReady(): void {} - - onRendered(): void {} - - onSteady(): void {} -} -``` - -In addition to these four lifecycle hooks, modules within the plugin can use the OnLifecycle decorator to declare that they need to be initialized at a specific lifecycle stage, for example: - -```ts -@OnLifecycle(LifecycleStages.Rendered, IMEInputController) -export class IMEInputController extends Disposable {} -``` - -You can also listen to lifecycle events by injecting `LifecycleService`. - -```ts -export class YourService { - constructor( - @Inject(LifecycleService) private _lifecycleService: LifecycleService, - ) { - super(); - - this._lifecycleService.lifecycle$.subscribe((stage) => this._initModulesOnStage(stage)); - } -} -``` - -### When should you write a plugin? - -The division of plugins is primarily based on whether certain modules need to be loaded in specific scenarios. For example, when running on the Node.js platform, UI-related modules may not be required, so these modules can be placed in a separate plugin and not loaded on the Node.js platform. Another example is when you want to allow users to choose whether to load a particular feature, you can place that feature in a separate plugin. - -## Layers - -![image](../img/layers.png) - -The modules within a plugin should generally belong to the following layers: - -* View: Handles rendering and interaction, including canvas rendering and React components. -* Controller: Encapsulates business logic, especially functional logic, and dispatches commands. -* Command: Executes logic using the command pattern, modifying the state or data of lower layers such as Service/Model. -* Service: Encapsulates functionality based on concerns for use by higher-level modules, stores internal application state, and manipulates underlying data, etc. -* Model: Stores business data. - -There should be a unidirectional dependency relationship between the layers. Except for some Controllers that act as view-models in MVVM and may hold references to UI layer objects, other layers are prohibited from referencing code from higher-level modules. - -Note: The code within a plugin is not limited to belonging to only one layer. For example, a plugin may provide both View and Controller simultaneously. - -## Command System - -Changes to the application state and data are executed through the command system. The Univer core provides a command service, with the dependency injection token ICommandService. Higher-level modules can encapsulate business logic within commands and execute the business logic by accessing other services through the command system. With the command system, Univer can easily implement collaborative editing, macro recording, undo/redo, and follow browsing capabilities. - -Plugins can register commands using the registerCommand interface provided by ICommandService and execute commands using the executeCommand interface. - -```ts -export interface ICommand

{ - /** - * ${businessName}.${type}.${name} - */ - readonly id: string; - readonly type: CommandType; - - handler(accessor: IAccessor, params?: P): Promise; - - /** When this command is unregistered, this function would be called. */ - onDispose?: () => void; -} - -export interface ICommandService { - registerCommand(command: ICommand): IDisposable; - - executeCommand

( - id: string, - params?: P, - options?: IExecutionOptions - ): Promise | R; -} -``` - -There are three types of commands in total: - -```ts -export const enum CommandType { - /** Command could generate some operations or mutations. */ - COMMAND = 0, - /** An operation that do not require conflict resolve. */ - OPERATION = 1, - /** An operation that need to be resolved before applied on peer client. */ - MUTATION = 2, -} -``` - -* `COMMAND` is responsible for creating, orchestrating, and executing `MUTATION` or `OPERATION` based on specific business logic. For example, a **Delete Row `COMMAND`** would generate a **Delete Row `MUTATION`**, an **Insert Row `MUTATION`** for undo, and a **Set Cell Content `MUTATION`**. - * `COMMAND` is the main carrier of business logic. If a _user action_ requires different _underlying behaviors_ based on the application state—for example, when a user clicks the bold text button and the effective range of the bold operation needs to be determined based on the current selection—the corresponding logic should be handled by the `COMMAND`. - * It can dispatch other `COMMAND`, `OPERATION`, or `MUTATION`. - * Asynchronous execution is allowed. -* `MUTATION` represents the changes made to the persisted data and involves conflict resolution in collaborative editing. Examples include inserting rows or columns, modifying cell content, changing filter ranges, and other operations. - * It cannot dispatch any other commands. - * **It must be executed synchronously**. -* `OPERATION` represents the changes made to non-persisted data (or application state) and does not involve conflict resolution. Examples include modifying scroll position, changing sidebar states, and other operations. - * It cannot dispatch any other commands. - * **It must be executed synchronously**. - -### Collaborative Editing - -`ICommandService` provides event listening interfaces that allow plugins to listen to which commands have been executed and what parameters were used for execution. In practice, an event is dispatched after a command is executed. The event looks like: - -```ts -/** - * The command info, only a command id and responsible params - */ -export interface ICommandInfo { - id: string; - - type: CommandType; - - /** - * Args should be serializable. - */ - params?: T; -} -``` - -For collaborative editing, the collaboration plugin can listen to all `MUTATION` type commands and, through collaborative editing algorithms, send these `MUTATION` to other collaborative clients. The plugin can then use `ICommandService` to reapply these `MUTATION`. - -### Operation Recording and Playback - -By listening to the execution of `OPERATION` and `MUTATION`, plugins can record user actions and implement features such as: - -* Collaborative cursors -* Magic Share, similar to Lark video -* Macro recording -* AppScript - -and more. - -## User Interface - -Univer provides mechanisms to simplify UI development and reduce the workload of menus, shortcuts, and interaction components across different devices. Functional plugins do not need to concern themselves with UI details; they can focus solely on business logic. - -### ShortcutService - -By injecting an `IShortcutItem` into the `IShortcutService`, you can register a shortcut key and configure its key combination, priority, trigger conditions, and the associated command to be executed. - -```ts -export interface IShortcutItem

{ - /** This should reuse the corresponding command's id. */ - id: string; - description?: string; - - priority?: number; - /** A callback that will be triggered to examine if the shortcut should be invoked. */ - preconditions?: (contextService: IContextService) => boolean; - - /** A command can be bound to several bindings, with different static parameters perhaps. */ - binding: number; - mac?: number; - win?: number; - linux?: number; - - /** Static parameters of this shortcut. Would be send to `CommandService.executeCommand`. */ - staticParameters?: P; -} - -export interface IShortcutService { - registerShortcut(shortcut: IShortcutItem): IDisposable; - - getCommandShortcut(id: string): string | null; -} -``` - -### MenuService - -By registering an IMenuItem with the IMenuService, you can configure a menu item. - -```ts -interface IMenuItemBase { - /** ID of the menu item. Normally it should be the same as the ID of the command that it would invoke. */ - id: string; - title: string; - description?: string; - icon?: string; - tooltip?: string; - - /** In what menu should the item display. */ - positions: OneOrMany; - - /** @deprecated this type seems unnecessary */ - type: MenuItemType; - /** - * Custom label component id. - * */ - label?: - | string - | { - name: string; - props?: Record; - }; - - hidden$?: Observable; - disabled$?: Observable; - - /** On observable value that should emit the value of the corresponding selection component. */ - value$?: Observable; -} - -export interface IMenuService { - menuChanged$: Observable; - - addMenuItem(item: IMenuItem): IDisposable; - - /** Get menu items for display at a given position or a submenu. */ - getMenuItems(position: MenuPosition | string): Array>; - getMenuItem(id: string): IMenuItem | null; -} -``` - - diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index e663f527a3..0000000000 --- a/docs/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Univer Documentation - -The documentation is still under construction. Sorry for the incovenience. - -If you prefer to read the documentation in Chinese, please to [here](./zh/index.md). diff --git a/docs/plugin-extension-capability.md b/docs/plugin-extension-capability.md deleted file mode 100644 index 7df3cd6557..0000000000 --- a/docs/plugin-extension-capability.md +++ /dev/null @@ -1,53 +0,0 @@ -# Plugin Extension Capability - -Univer follows a "small core" + "multiple plugins" architecture pattern. The core code (mainly the core package) and plugins can provide extension points to enrich the functionality of Univer. - -As mentioned in the Architecture Overview section, plugin extension points should be added to the dependency injection system as public modules. - -The following section provides a brief introduction to the core package of Univer and some of the main extension points exposed by plugins. Detailed API documentation will be generated using a documentation generation tool later. - -## Core Package - -The core package, as the lowest-level package, provides the Univer container type and core extension points, including: - -* Command System (ICommandService) - * Registering commands/mutations/operations - * Listening to command execution -* Context (IContextService) - * Recording application runtime state information -* Configuration Management (IConfigService) -* Lifecycle (LifecycleService) -* Logging (ILogService) - * Printing different types of logs - * Controlling log storage and reporting methods -* Internationalization (ILocaleService) -* Permissions (IPermissionService) - * Controlling execution permissions for commands - * Controlling permissions for Univer documents -* Undo/Redo (IUndoRedoService) - -## base-ui - -Provides basic operations and UI capabilities: - -* Provides basic React components -* Global interactions - * Popup dialogs or notifications (INotificationService / IMessageService) -* Toolbar and menu (IMenuService) - * Registering menu items in different locations (toolbar, context menu, etc.) -* Shortcuts (IShortcutService) - * Registering keyboard shortcuts - * Getting the shortcut keys associated with a command -* Component mounting point (IDesktopUIController) - * Rendering custom content at specified locations -* Copy and paste (IClipboardService) - * Supplementing clipboard content when copying to the clipboard, supplementing or modifying mutations when pasting from the clipboard -* Focus management (ILayoutService) - -## base-render - -Provides basic rendering capabilities: - -* Custom rendering -* Handling mouse interactions -* Rendering rich text content diff --git a/docs/tldr/ref-range/delete-range-move-other.tldr b/docs/tldr/ref-range/delete-range-move-other.tldr new file mode 100644 index 0000000000..a8a22e5dd8 --- /dev/null +++ b/docs/tldr/ref-range/delete-range-move-other.tldr @@ -0,0 +1,634 @@ +{ + "tldrawFileFormatVersion": 1, + "schema": { + "schemaVersion": 1, + "storeVersion": 4, + "recordVersions": { + "asset": { + "version": 1, + "subTypeKey": "type", + "subTypeVersions": { + "image": 3, + "video": 3, + "bookmark": 1 + } + }, + "camera": { + "version": 1 + }, + "document": { + "version": 2 + }, + "instance": { + "version": 24 + }, + "instance_page_state": { + "version": 5 + }, + "page": { + "version": 1 + }, + "shape": { + "version": 3, + "subTypeKey": "type", + "subTypeVersions": { + "group": 0, + "text": 1, + "bookmark": 2, + "draw": 1, + "geo": 8, + "note": 5, + "line": 4, + "frame": 0, + "arrow": 3, + "highlight": 0, + "embed": 4, + "image": 3, + "video": 2 + } + }, + "instance_presence": { + "version": 5 + }, + "pointer": { + "version": 1 + } + } + }, + "records": [ + { + "gridSize": 10, + "name": "", + "meta": {}, + "id": "document:document", + "typeName": "document" + }, + { + "id": "pointer:pointer", + "typeName": "pointer", + "x": 783.5026017162536, + "y": 457.5455634395281, + "lastActivityTimestamp": 1712492251467, + "meta": {} + }, + { + "meta": {}, + "id": "page:page", + "name": "Page 1", + "index": "a1", + "typeName": "page" + }, + { + "x": -61.666664216253594, + "y": 17.49999930461251, + "z": 1, + "meta": {}, + "id": "camera:page:page", + "typeName": "camera" + }, + { + "editingShapeId": null, + "croppingShapeId": null, + "selectedShapeIds": [ + "shape:NuDq3OtA6YXplZkvFH0VW", + "shape:lRJ8Rao0PE2SXXh7GBfpR", + "shape:sYS6jISnhOXWHXu_pcfTL", + "shape:yXoCMFfvOgod-F5gpgxWg", + "shape:-6z3WUiXJg8-s1M1O5OBT", + "shape:n8reRvSltdS8fWoW_S6Pe", + "shape:4fjIFqy2-YWVcedp2xUwT", + "shape:yreVzk07y5QjUX2vv4HhE", + "shape:Q-MRA9c4-1NEBAaNSlNIN", + "shape:eeU13ncMBu2f7GUYymKrf", + "shape:A3vESTBPDWb4E8QgY7Jmr", + "shape:RajK2iJ_pkbQzqtNpqfT0", + "shape:tuuFvVAg9iB-9XqfTXZpk", + "shape:O5NT_JV7uY04CLkQy8ky5", + "shape:DReHYeZJcP-IZdT3E1wNe", + "shape:Wm_ZP7XeR8vaQT-4GXR60", + "shape:x3hBc55qV-SZ1LxWcEcPl" + ], + "hoveredShapeId": null, + "erasingShapeIds": [], + "hintingShapeIds": [], + "focusedGroupId": null, + "meta": {}, + "id": "instance_page_state:page:page", + "pageId": "page:page", + "typeName": "instance_page_state" + }, + { + "followingUserId": null, + "opacityForNextShape": 1, + "stylesForNextShape": { + "tldraw:geo": "rectangle", + "tldraw:size": "s", + "tldraw:color": "light-blue" + }, + "brush": { + "x": 188.3984299633239, + "y": 62.17447731892265, + "w": 595.1041717529297, + "h": 395.37108612060547 + }, + "scribbles": [], + "cursor": { + "type": "default", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 908.3333129882812, + "h": 602.5 + }, + "insets": [ + false, + true, + true, + false + ], + "zoomBrush": null, + "isGridMode": false, + "isPenMode": false, + "chatMessage": "", + "isChatting": false, + "highlightedUserIds": [], + "canMoveCamera": true, + "isFocused": true, + "devicePixelRatio": 2.4000000953674316, + "isCoarsePointer": false, + "isHoveringCanvas": true, + "openMenus": [], + "isChangingStyle": false, + "isReadonly": false, + "meta": {}, + "duplicateProps": null, + "id": "instance:instance", + "currentPageId": "page:page", + "typeName": "instance" + }, + { + "x": 247.62368774414062, + "y": 125.703125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:NuDq3OtA6YXplZkvFH0VW", + "type": "geo", + "props": { + "w": 178.82485961914062, + "h": 142.32745361328125, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a1", + "typeName": "shape" + }, + { + "x": 247.9036407470703, + "y": 269.8893127441406, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:lRJ8Rao0PE2SXXh7GBfpR", + "type": "geo", + "props": { + "w": 179.2057342529297, + "h": 140.43295288085938, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a2", + "typeName": "shape" + }, + { + "x": 375.7421875, + "y": 225.49478149414062, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:sYS6jISnhOXWHXu_pcfTL", + "type": "geo", + "props": { + "w": 50.751953125, + "h": 41.953125, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a3", + "typeName": "shape" + }, + { + "x": 354.72003173828125, + "y": 196.5755157470703, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:yXoCMFfvOgod-F5gpgxWg", + "type": "geo", + "props": { + "w": 73.09246826171875, + "h": 127.86131286621094, + "geo": "rectangle", + "color": "black", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a4", + "typeName": "shape" + }, + { + "x": 251.77862548828125, + "y": 127.67578125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:-6z3WUiXJg8-s1M1O5OBT", + "type": "text", + "props": { + "color": "black", + "size": "s", + "w": 16, + "text": "1", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "a5", + "typeName": "shape" + }, + { + "x": 249.76040649414062, + "y": 267.3046875, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:n8reRvSltdS8fWoW_S6Pe", + "type": "text", + "props": { + "color": "black", + "size": "s", + "w": 16, + "text": "2", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "a6", + "typeName": "shape" + }, + { + "x": 374.5390625, + "y": 221.77734375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:4fjIFqy2-YWVcedp2xUwT", + "type": "text", + "props": { + "color": "black", + "size": "s", + "w": 16, + "text": "3", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "a7", + "typeName": "shape" + }, + { + "x": 444.6549232668349, + "y": 267.26560488674374, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:yreVzk07y5QjUX2vv4HhE", + "type": "arrow", + "parentId": "page:page", + "index": "a8", + "props": { + "dash": "draw", + "size": "xl", + "fill": "none", + "color": "black", + "labelColor": "black", + "bend": 0, + "start": { + "type": "point", + "x": 0, + "y": 0 + }, + "end": { + "type": "point", + "x": 72.431640625, + "y": 2.1484375 + }, + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "text": "", + "labelPosition": 0.5, + "font": "draw" + }, + "typeName": "shape" + }, + { + "x": 534.0527038574219, + "y": 124.814453125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:Q-MRA9c4-1NEBAaNSlNIN", + "type": "geo", + "props": { + "w": 179.08200073242188, + "h": 72.373046875, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a9", + "typeName": "shape" + }, + { + "x": 534.3326568603516, + "y": 269.0006408691406, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:eeU13ncMBu2f7GUYymKrf", + "type": "geo", + "props": { + "w": 106.71549987792969, + "h": 151.04495239257812, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aA", + "typeName": "shape" + }, + { + "x": 641.1490478515625, + "y": 195.6868438720703, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:A3vESTBPDWb4E8QgY7Jmr", + "type": "geo", + "props": { + "w": 73.09246826171875, + "h": 127.86131286621094, + "geo": "rectangle", + "color": "black", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aC", + "typeName": "shape" + }, + { + "x": 538.2076416015625, + "y": 126.787109375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:RajK2iJ_pkbQzqtNpqfT0", + "type": "text", + "props": { + "color": "black", + "size": "s", + "w": 16, + "text": "1", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aD", + "typeName": "shape" + }, + { + "x": 536.1894226074219, + "y": 266.416015625, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:tuuFvVAg9iB-9XqfTXZpk", + "type": "text", + "props": { + "color": "black", + "size": "s", + "w": 16, + "text": "2", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aE", + "typeName": "shape" + }, + { + "x": 534.6614540285536, + "y": 196.63410464260312, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:O5NT_JV7uY04CLkQy8ky5", + "type": "geo", + "props": { + "w": 105.90814208984374, + "h": 72.44140625, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aG", + "typeName": "shape" + }, + { + "x": 641.2890175051161, + "y": 197.5260418984625, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:DReHYeZJcP-IZdT3E1wNe", + "type": "geo", + "props": { + "w": 73.44403076171875, + "h": 70.21157836914062, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aH", + "typeName": "shape" + }, + { + "x": 533.5103920168349, + "y": 195.4882763955328, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:Wm_ZP7XeR8vaQT-4GXR60", + "type": "text", + "props": { + "color": "light-blue", + "size": "s", + "w": 16, + "text": "1", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aI", + "typeName": "shape" + }, + { + "x": 645.2259071535536, + "y": 195.2115887734625, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:x3hBc55qV-SZ1LxWcEcPl", + "type": "text", + "props": { + "color": "light-blue", + "size": "s", + "w": 16, + "text": "2", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aJ", + "typeName": "shape" + } + ] +} \ No newline at end of file diff --git a/docs/tldr/ref-range/insert-range-move-other.tldr b/docs/tldr/ref-range/insert-range-move-other.tldr new file mode 100644 index 0000000000..de012d5d2e --- /dev/null +++ b/docs/tldr/ref-range/insert-range-move-other.tldr @@ -0,0 +1,715 @@ +{ + "tldrawFileFormatVersion": 1, + "schema": { + "schemaVersion": 1, + "storeVersion": 4, + "recordVersions": { + "asset": { + "version": 1, + "subTypeKey": "type", + "subTypeVersions": { + "image": 3, + "video": 3, + "bookmark": 1 + } + }, + "camera": { + "version": 1 + }, + "document": { + "version": 2 + }, + "instance": { + "version": 24 + }, + "instance_page_state": { + "version": 5 + }, + "page": { + "version": 1 + }, + "shape": { + "version": 3, + "subTypeKey": "type", + "subTypeVersions": { + "group": 0, + "text": 1, + "bookmark": 2, + "draw": 1, + "geo": 8, + "note": 5, + "line": 4, + "frame": 0, + "arrow": 3, + "highlight": 0, + "embed": 4, + "image": 3, + "video": 2 + } + }, + "instance_presence": { + "version": 5 + }, + "pointer": { + "version": 1 + } + } + }, + "records": [ + { + "gridSize": 10, + "name": "", + "meta": {}, + "id": "document:document", + "typeName": "document" + }, + { + "id": "pointer:pointer", + "typeName": "pointer", + "x": 754.9478708108269, + "y": 432.87760141823037, + "lastActivityTimestamp": 1712492341185, + "meta": {} + }, + { + "meta": {}, + "id": "page:page", + "name": "Page 1", + "index": "a1", + "typeName": "page" + }, + { + "x": -129.9999948342644, + "y": -69.16666391823034, + "z": 1, + "meta": {}, + "id": "camera:page:page", + "typeName": "camera" + }, + { + "editingShapeId": null, + "croppingShapeId": null, + "selectedShapeIds": [], + "hoveredShapeId": null, + "erasingShapeIds": [], + "hintingShapeIds": [], + "focusedGroupId": null, + "meta": {}, + "id": "instance_page_state:page:page", + "pageId": "page:page", + "typeName": "instance_page_state" + }, + { + "followingUserId": null, + "opacityForNextShape": 1, + "stylesForNextShape": { + "tldraw:color": "light-blue", + "tldraw:geo": "rectangle", + "tldraw:size": "s" + }, + "brush": null, + "scribbles": [], + "cursor": { + "type": "default", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 908.3333129882812, + "h": 602.5 + }, + "insets": [ + false, + true, + true, + false + ], + "zoomBrush": null, + "isGridMode": false, + "isPenMode": false, + "chatMessage": "", + "isChatting": false, + "highlightedUserIds": [], + "canMoveCamera": true, + "isFocused": true, + "devicePixelRatio": 2.4000000953674316, + "isCoarsePointer": false, + "isHoveringCanvas": true, + "openMenus": [], + "isChangingStyle": false, + "isReadonly": false, + "meta": {}, + "duplicateProps": null, + "id": "instance:instance", + "currentPageId": "page:page", + "typeName": "instance" + }, + { + "x": 247.62368774414062, + "y": 125.703125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:mmPoVopnA6ljcdfSYV59a", + "type": "geo", + "props": { + "w": 178.82485961914062, + "h": 142.32745361328125, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a1", + "typeName": "shape" + }, + { + "x": 247.9036407470703, + "y": 269.8893127441406, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:eIcKsf7fIzKEDf8ohaqHg", + "type": "geo", + "props": { + "w": 177.9850311279297, + "h": 127.77670288085938, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a2", + "typeName": "shape" + }, + { + "x": 375.7421875, + "y": 225.49478149414062, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:3APps3g9p0w1NE_41YT1Q", + "type": "geo", + "props": { + "w": 50.751953125, + "h": 41.953125, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a3", + "typeName": "shape" + }, + { + "x": 354.72003173828125, + "y": 196.5755157470703, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:qNFcjE6S1pY0inD9H4ng5", + "type": "geo", + "props": { + "w": 73.09246826171875, + "h": 127.86131286621094, + "geo": "rectangle", + "color": "black", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a4", + "typeName": "shape" + }, + { + "x": 251.77862548828125, + "y": 127.67578125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:NlhysTac33RgFW0D84gFG", + "type": "text", + "props": { + "color": "black", + "size": "s", + "w": 16, + "text": "1", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "a5", + "typeName": "shape" + }, + { + "x": 249.76040649414062, + "y": 267.3046875, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:VZt11K02f8Jf8K6vGnnbs", + "type": "text", + "props": { + "color": "black", + "size": "s", + "w": 16, + "text": "2", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "a6", + "typeName": "shape" + }, + { + "x": 374.5390625, + "y": 221.77734375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:AGviKyccFrcgBseTWEUvQ", + "type": "text", + "props": { + "color": "black", + "size": "s", + "w": 16, + "text": "3", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "a7", + "typeName": "shape" + }, + { + "x": 444.6549232668349, + "y": 267.26560488674374, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:wbS5ruNc-IW0DGNG473HE", + "type": "arrow", + "parentId": "page:page", + "index": "a8", + "props": { + "dash": "draw", + "size": "xl", + "fill": "none", + "color": "black", + "labelColor": "black", + "bend": 0, + "start": { + "type": "point", + "x": 0, + "y": 0 + }, + "end": { + "type": "point", + "x": 72.431640625, + "y": 2.1484375 + }, + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "text": "", + "labelPosition": 0.5, + "font": "draw" + }, + "typeName": "shape" + }, + { + "x": 534.0527038574219, + "y": 124.814453125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:anZpGYIScPMNgS6qnraRI", + "type": "geo", + "props": { + "w": 179.08200073242188, + "h": 72.373046875, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a9", + "typeName": "shape" + }, + { + "x": 534.3326568603516, + "y": 269.0006408691406, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:g3Ow6Eabc74-0TpSaZ5ml", + "type": "geo", + "props": { + "w": 106.46482849121094, + "h": 125.9505615234375, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aA", + "typeName": "shape" + }, + { + "x": 641.1490478515625, + "y": 195.6868438720703, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:yB6c6qLpGVgUWHY5GWdVP", + "type": "geo", + "props": { + "w": 73.09246826171875, + "h": 127.86131286621094, + "geo": "rectangle", + "color": "black", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aB", + "typeName": "shape" + }, + { + "x": 538.2076416015625, + "y": 126.787109375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:Vau4D_HwAk-Nq6wVAxDHQ", + "type": "text", + "props": { + "color": "black", + "size": "s", + "w": 16, + "text": "1", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aC", + "typeName": "shape" + }, + { + "x": 536.1894226074219, + "y": 266.416015625, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:NszqQRZNQqB_SfSv4ul8V", + "type": "text", + "props": { + "color": "black", + "size": "s", + "w": 16, + "text": "2", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aD", + "typeName": "shape" + }, + { + "x": 534.6614540285536, + "y": 196.63410464260312, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:qSHE5B2jyduHjYW8lGcUe", + "type": "geo", + "props": { + "w": 105.90814208984374, + "h": 72.44140625, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aE", + "typeName": "shape" + }, + { + "x": 641.2922523683974, + "y": 323.8736981484625, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:M0kdP5q8uV1YxTBfYohmU", + "type": "geo", + "props": { + "w": 73.44403076171875, + "h": 70.21157836914062, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aF", + "typeName": "shape" + }, + { + "x": 533.5103920168349, + "y": 195.4882763955328, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:PfQgIf7GFHeJhklUQ-fgL", + "type": "text", + "props": { + "color": "light-blue", + "size": "s", + "w": 16, + "text": "1", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aG", + "typeName": "shape" + }, + { + "x": 645.5221107668349, + "y": 323.0599011513922, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:S2nneJa6uMFkx8l4_wJg0", + "type": "text", + "props": { + "color": "light-blue", + "size": "s", + "w": 16, + "text": "1", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aH", + "typeName": "shape" + }, + { + "x": 641.2304635842644, + "y": 396.33463266823037, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:6MLqK6YXKOUXZ2TMz24_c", + "type": "geo", + "props": { + "w": 75.24737548828125, + "h": 109.94464111328125, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aI", + "typeName": "shape" + }, + { + "x": 643.6633859475456, + "y": 395.60221079323037, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:EkEWMdwXKZVTrWMKfNgNT", + "type": "text", + "props": { + "color": "light-blue", + "size": "s", + "w": 16, + "text": "2", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aJ", + "typeName": "shape" + }, + { + "x": 663.5969848632812, + "y": 350.98631286621094, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:qNJm7SiL5BR0PiMzU2q88", + "type": "geo", + "props": { + "w": 50.751953125, + "h": 41.953125, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aK", + "typeName": "shape" + }, + { + "x": 663.9986520608269, + "y": 349.095039162371, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:DBeP4hqxqXQy3RhfTXkGt", + "type": "text", + "props": { + "color": "light-blue", + "size": "s", + "w": 16, + "text": "3", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aL", + "typeName": "shape" + } + ] +} \ No newline at end of file diff --git a/docs/tldr/ref-range/move-range-other.tldr b/docs/tldr/ref-range/move-range-other.tldr new file mode 100644 index 0000000000..c03697fce2 --- /dev/null +++ b/docs/tldr/ref-range/move-range-other.tldr @@ -0,0 +1,987 @@ +{ + "tldrawFileFormatVersion": 1, + "schema": { + "schemaVersion": 1, + "storeVersion": 4, + "recordVersions": { + "asset": { + "version": 1, + "subTypeKey": "type", + "subTypeVersions": { + "image": 3, + "video": 3, + "bookmark": 1 + } + }, + "camera": { + "version": 1 + }, + "document": { + "version": 2 + }, + "instance": { + "version": 24 + }, + "instance_page_state": { + "version": 5 + }, + "page": { + "version": 1 + }, + "shape": { + "version": 3, + "subTypeKey": "type", + "subTypeVersions": { + "group": 0, + "text": 1, + "bookmark": 2, + "draw": 1, + "geo": 8, + "note": 5, + "line": 4, + "frame": 0, + "arrow": 3, + "highlight": 0, + "embed": 4, + "image": 3, + "video": 2 + } + }, + "instance_presence": { + "version": 5 + }, + "pointer": { + "version": 1 + } + } + }, + "records": [ + { + "gridSize": 10, + "name": "", + "meta": {}, + "id": "document:document", + "typeName": "document" + }, + { + "id": "pointer:pointer", + "typeName": "pointer", + "x": 421.8819878759555, + "y": 1106.349397374137, + "lastActivityTimestamp": 1712490939341, + "meta": {} + }, + { + "meta": {}, + "id": "page:page", + "name": "Page 1", + "index": "a1", + "typeName": "page" + }, + { + "x": 374.8665834686408, + "y": -91.09704365731895, + "z": 0.5871192680004972, + "meta": {}, + "id": "camera:page:page", + "typeName": "camera" + }, + { + "editingShapeId": null, + "croppingShapeId": null, + "selectedShapeIds": [], + "hoveredShapeId": null, + "erasingShapeIds": [], + "hintingShapeIds": [], + "focusedGroupId": null, + "meta": {}, + "id": "instance_page_state:page:page", + "pageId": "page:page", + "typeName": "instance_page_state" + }, + { + "followingUserId": null, + "opacityForNextShape": 1, + "stylesForNextShape": { + "tldraw:color": "light-blue", + "tldraw:geo": "rectangle", + "tldraw:size": "s", + "tldraw:dash": "dashed" + }, + "brush": null, + "scribbles": [], + "cursor": { + "type": "default", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 958.3333129882812, + "h": 602.5 + }, + "insets": [ + false, + true, + true, + false + ], + "zoomBrush": null, + "isGridMode": false, + "isPenMode": false, + "chatMessage": "", + "isChatting": false, + "highlightedUserIds": [], + "canMoveCamera": true, + "isFocused": true, + "devicePixelRatio": 2.4000000953674316, + "isCoarsePointer": false, + "isHoveringCanvas": false, + "openMenus": [], + "isChangingStyle": false, + "isReadonly": false, + "meta": {}, + "duplicateProps": null, + "id": "instance:instance", + "currentPageId": "page:page", + "typeName": "instance" + }, + { + "x": 36.62252443180677, + "y": 176.13338185011935, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:Z7xkMeeNXDzNin2Lw3W4B", + "type": "geo", + "props": { + "w": 309.417660085907, + "h": 129.1754945779055, + "geo": "rectangle", + "color": "black", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a1", + "typeName": "shape" + }, + { + "x": 561.796875, + "y": 158.87890625, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:HXtCy6rMZ1V_Ti7WAx8fS", + "type": "geo", + "props": { + "w": 302.83203125, + "h": 137.01171875, + "geo": "rectangle", + "color": "black", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a2", + "typeName": "shape" + }, + { + "x": 142.80624866569656, + "y": 196.2152141803311, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:WnvWq34NHMyf6CKwBOvYH", + "type": "geo", + "props": { + "w": 62.37890625, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "5", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a4", + "typeName": "shape" + }, + { + "x": -33.453125, + "y": 140.28018818537623, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:RQ6pvlUHxkfHIwE2VblAj", + "type": "geo", + "props": { + "w": 434.5127179371963, + "h": 359.604832486845, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a6", + "typeName": "shape" + }, + { + "x": 153.91418558699297, + "y": 469.72340478018043, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:qqMcpCBEVRurWJwUo1jsC", + "type": "text", + "props": { + "color": "black", + "size": "m", + "w": 17.80338478088379, + "text": "3", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "a7", + "typeName": "shape" + }, + { + "x": 670.328125, + "y": 206.125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:RWzgXvy0T1kpUTQS6j8h8", + "type": "geo", + "props": { + "w": 101.7578125, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "6", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aA", + "typeName": "shape" + }, + { + "x": 504.3515625, + "y": 133.2734375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:_EEHxCoV1f0GCpdnd9c13", + "type": "geo", + "props": { + "w": 438.6465149490318, + "h": 365.84093880847024, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aB", + "typeName": "shape" + }, + { + "x": 735.7846767987926, + "y": 463.3469788073762, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:LpDTkDTmnOrQRjRWIsVuT", + "type": "text", + "props": { + "color": "black", + "size": "m", + "w": 18.54557228088379, + "text": "4", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aC", + "typeName": "shape" + }, + { + "x": -176.70483075582942, + "y": 80.13551038325613, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:aZ1fOq7m3B3bJRqVCADtw", + "type": "geo", + "props": { + "w": 1160.1568202167912, + "h": 511.1298111516582, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aD", + "typeName": "shape" + }, + { + "x": 416.3944036995879, + "y": 562.9777716706179, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:kg-0xEoFEL72VyabbD_lu", + "type": "text", + "props": { + "color": "black", + "size": "m", + "w": 16, + "text": "1", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aE", + "typeName": "shape" + }, + { + "x": 232.16000398280585, + "y": 188.0785920544024, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:-tf6EyMBUgH6oUE5g_und", + "type": "geo", + "props": { + "w": 401.86133470949346, + "h": 358.1623468320988, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aF", + "typeName": "shape" + }, + { + "x": 436.47888907043784, + "y": 508.79678364274645, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:gDvMZAb4Rf6GA5eA76xfo", + "type": "text", + "props": { + "color": "black", + "size": "m", + "w": 20.57773056511335, + "text": "2", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1.155832490190769 + }, + "parentId": "page:page", + "index": "aG", + "typeName": "shape" + }, + { + "x": 160.4110698891767, + "y": 648.9019921690615, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:6UXQihpNfMNAcH4XUhm_B", + "type": "text", + "props": { + "color": "black", + "size": "m", + "w": 799.125, + "text": "5 被迁移到镜像位置\n6 被清除\n3 的 o区域迁移到镜像位置,其余的部分拆分成多个小的range (=3-o+o')\n4 o'区域被清除 (=4-o')\n7 的7.4迁移到镜像的8.4\n8 的8.4 被清除\n2.1 迁移到2.1‘,2.2被清除", + "font": "draw", + "align": "start", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aH", + "typeName": "shape" + }, + { + "x": -27.381971286011108, + "y": 111.44075153624149, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:FX3hwp2fAi5auYGmtUwyN", + "type": "geo", + "props": { + "w": 62.82253446711813, + "h": 61.69400978088379, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "7.1", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aJ", + "typeName": "shape" + }, + { + "x": 77.38466666973028, + "y": 296.55130999429696, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:qLW3-92vPvSEaJq3XKgZf", + "type": "text", + "props": { + "color": "black", + "size": "m", + "w": 16, + "text": "o", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aU", + "typeName": "shape" + }, + { + "x": 650.487335065217, + "y": 296.3863891776495, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:llNh-FmikyNSyn2dX2gwS", + "type": "text", + "props": { + "color": "black", + "size": "m", + "w": 20.661457061767578, + "text": "o'", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aV", + "typeName": "shape" + }, + { + "x": 36.34216398634031, + "y": 111.38823822359635, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:WktGMRojfOh09ytX4Vrgt", + "type": "geo", + "props": { + "w": 59.90364456176758, + "h": 60.867081405132986, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "7.2", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aW", + "typeName": "shape" + }, + { + "x": -24.7408753522907, + "y": 174.86916476534546, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:1IVPDNlZbNdJF7k3eZWhx", + "type": "geo", + "props": { + "w": 60.60792809050062, + "h": 67.14085015453615, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "7.3", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aX", + "typeName": "shape" + }, + { + "x": 37.17118520224808, + "y": 174.99903612020003, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:WyJJjUEveJC3Cnt0K3VDC", + "type": "geo", + "props": { + "w": 61.44487981334692, + "h": 64.25095219066625, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "7.4", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aY", + "typeName": "shape" + }, + { + "x": -27.96773690185489, + "y": 104.62874708300444, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:7gAf9ldUffUmi9Tryow7T", + "type": "geo", + "props": { + "w": 126.69101573829815, + "h": 136.92114401932835, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aZ", + "typeName": "shape" + }, + { + "x": 15.710536373735692, + "y": 77.98792281791688, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:i-4t1QKTI7I-KRkqt9Eqt", + "type": "text", + "props": { + "color": "black", + "size": "m", + "w": 17.7578125, + "text": "7", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aa", + "typeName": "shape" + }, + { + "x": 502.11456401846743, + "y": 90.34457305135983, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:4oMV0zdRJ7Hitpb-Zmk-C", + "type": "geo", + "props": { + "w": 59.9345346046502, + "h": 65.2026964715675, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "8.1", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "ab", + "typeName": "shape" + }, + { + "x": 563.4845548665834, + "y": 88.04600059623392, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:ooQN7A-VB4tFD2_Mm_vVg", + "type": "geo", + "props": { + "w": 61.264898001174515, + "h": 67.68059026946725, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "8.2", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "ac", + "typeName": "shape" + }, + { + "x": 497.8270649955389, + "y": 157.28167297114751, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:_wQcHqRyFbN7UN3aDt5AH", + "type": "geo", + "props": { + "w": 61.56793884418107, + "h": 67.01377969102066, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "8.3", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "ad", + "typeName": "shape" + }, + { + "x": 499.17465397838816, + "y": 87.0412552888065, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:3pHgX6adpLkidrJ_UFmgl", + "type": "geo", + "props": { + "w": 126.69101573829815, + "h": 136.92114401932835, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "ae", + "typeName": "shape" + }, + { + "x": 542.8301411135369, + "y": 60.40043102371894, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:IaSDq4wgW9naMxfYTlLl7", + "type": "text", + "props": { + "color": "black", + "size": "m", + "w": 17.80338478088379, + "text": "8", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "af", + "typeName": "shape" + }, + { + "x": 562.4121211816865, + "y": 159.39077441911633, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:JdYLLwDGRsi8wp32-PN6X", + "type": "geo", + "props": { + "w": 63.16826544555806, + "h": 63.103088964583435, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "8.4", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "ag", + "typeName": "shape" + }, + { + "x": 276.7467140331292, + "y": 218.34524188697964, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:Usl50hONQS9BSzS5zSgCS", + "type": "text", + "props": { + "color": "black", + "size": "s", + "w": 24.118488311767578, + "text": "2.1", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "ah", + "typeName": "shape" + }, + { + "x": 763.3710549959251, + "y": 189.08828302066794, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:AsZwMCjgc1bzNYQFpN4B6", + "type": "geo", + "props": { + "w": 97.78611008717803, + "h": 104.810848254783, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "dashed", + "size": "s", + "font": "draw", + "text": "2.1‘", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "ai", + "typeName": "shape" + }, + { + "x": 579.4879858872264, + "y": 239.6245375565262, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:Og45Ay_n74-R9Y7qJyQqF", + "type": "text", + "props": { + "color": "light-blue", + "size": "s", + "w": 30.55729103088379, + "text": "2.2", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aj", + "typeName": "shape" + }, + { + "x": 561.0568077085461, + "y": 189.38767917334172, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:S_F8i-VlBd3Mzo5BGmGnY", + "type": "geo", + "props": { + "w": 70.1862549615455, + "h": 104.17878971024942, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "dashed", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "ak", + "typeName": "shape" + } + ] +} \ No newline at end of file diff --git a/docs/tldr/ref-range/move-rows-cols-other.tldr b/docs/tldr/ref-range/move-rows-cols-other.tldr new file mode 100644 index 0000000000..adb25ee234 --- /dev/null +++ b/docs/tldr/ref-range/move-rows-cols-other.tldr @@ -0,0 +1,785 @@ +{ + "tldrawFileFormatVersion": 1, + "schema": { + "schemaVersion": 1, + "storeVersion": 4, + "recordVersions": { + "asset": { + "version": 1, + "subTypeKey": "type", + "subTypeVersions": { + "image": 3, + "video": 3, + "bookmark": 1 + } + }, + "camera": { + "version": 1 + }, + "document": { + "version": 2 + }, + "instance": { + "version": 24 + }, + "instance_page_state": { + "version": 5 + }, + "page": { + "version": 1 + }, + "shape": { + "version": 3, + "subTypeKey": "type", + "subTypeVersions": { + "group": 0, + "text": 1, + "bookmark": 2, + "draw": 1, + "geo": 8, + "note": 5, + "line": 4, + "frame": 0, + "arrow": 3, + "highlight": 0, + "embed": 4, + "image": 3, + "video": 2 + } + }, + "instance_presence": { + "version": 5 + }, + "pointer": { + "version": 1 + } + } + }, + "records": [ + { + "gridSize": 10, + "name": "", + "meta": {}, + "id": "document:document", + "typeName": "document" + }, + { + "id": "pointer:pointer", + "typeName": "pointer", + "x": 1255.3905494482942, + "y": 167.71229914279735, + "lastActivityTimestamp": 1712497349510, + "meta": {} + }, + { + "meta": {}, + "id": "page:page", + "name": "Page 1", + "index": "a1", + "typeName": "page" + }, + { + "x": -674.531795241653, + "y": 22.109098806580008, + "z": 0.8913257676681506, + "meta": {}, + "id": "camera:page:page", + "typeName": "camera" + }, + { + "editingShapeId": null, + "croppingShapeId": null, + "selectedShapeIds": [], + "hoveredShapeId": null, + "erasingShapeIds": [], + "hintingShapeIds": [], + "focusedGroupId": null, + "meta": {}, + "id": "instance_page_state:page:page", + "pageId": "page:page", + "typeName": "instance_page_state" + }, + { + "followingUserId": null, + "opacityForNextShape": 1, + "stylesForNextShape": { + "tldraw:geo": "rectangle", + "tldraw:color": "light-blue", + "tldraw:size": "s" + }, + "brush": null, + "scribbles": [], + "cursor": { + "type": "default", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 908.3333129882812, + "h": 602.5 + }, + "insets": [ + false, + true, + true, + false + ], + "zoomBrush": null, + "isGridMode": false, + "isPenMode": false, + "chatMessage": "", + "isChatting": false, + "highlightedUserIds": [], + "canMoveCamera": true, + "isFocused": true, + "devicePixelRatio": 2.4000000953674316, + "isCoarsePointer": false, + "isHoveringCanvas": true, + "openMenus": [], + "isChangingStyle": false, + "isReadonly": false, + "meta": {}, + "duplicateProps": null, + "id": "instance:instance", + "currentPageId": "page:page", + "typeName": "instance" + }, + { + "x": 166.2044219970703, + "y": 11.871749877929688, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:34yHP5cqUyj1sUzXYvGG_", + "type": "geo", + "props": { + "w": 57.05731201171876, + "h": 539.8860626220703, + "geo": "rectangle", + "color": "black", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a1", + "typeName": "shape" + }, + { + "x": 105.42317199707031, + "y": 112.09635162353516, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:S0lUSLL7krS7mJ6Ke2Jt1", + "type": "geo", + "props": { + "w": 181.1490936279297, + "h": 78.40169525146484, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a2", + "typeName": "shape" + }, + { + "x": 171.39928478199744, + "y": 250.23697756122317, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:hZ2vPygFFYyLnhcuBemCv", + "type": "geo", + "props": { + "w": 48.88801956176758, + "h": 66.14581298828125, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "5", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a3", + "typeName": "shape" + }, + { + "x": 67.22004699707031, + "y": 60.592445373535156, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:HluOJgiOyqopIqXDF5S9R", + "type": "geo", + "props": { + "w": 247.7897186279297, + "h": 463.42122650146484, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a4", + "typeName": "shape" + }, + { + "x": 515.6966094970703, + "y": 5.9798126220703125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:EFGA1-V22biG3ATmjL6Cn", + "type": "geo", + "props": { + "w": 57.91015625, + "h": 554.8632659912108, + "geo": "rectangle", + "color": "black", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a5", + "typeName": "shape" + }, + { + "x": 416.7122344970703, + "y": 61.37042999267578, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:QabyY2x9yG9CXCbImwk65", + "type": "geo", + "props": { + "w": 247.7897186279297, + "h": 463.42122650146484, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a8", + "typeName": "shape" + }, + { + "x": 83.52343314223816, + "y": 67.49348390102391, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:mvJKUtoBmicgrdwEQld72", + "type": "text", + "props": { + "color": "light-blue", + "size": "s", + "w": 16, + "text": "1", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "a9", + "typeName": "shape" + }, + { + "x": 115.18614605047046, + "y": 117.47362519973734, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:iUViMjV9iCWSVEVPyueNQ", + "type": "text", + "props": { + "color": "light-blue", + "size": "s", + "w": 16, + "text": "3", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aB", + "typeName": "shape" + }, + { + "x": 1002.4517246923492, + "y": 4.889925326311641, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:DRUWAUHqsFDK-Geg5wgdo", + "type": "geo", + "props": { + "w": 57.05731201171876, + "h": 539.8860626220703, + "geo": "rectangle", + "color": "black", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aD", + "typeName": "shape" + }, + { + "x": 941.6704746923492, + "y": 105.11452707191711, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:A9xIt9L9jNGc0oweLZPnW", + "type": "geo", + "props": { + "w": 60.26514566415449, + "h": 74.1340945400321, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aE", + "typeName": "shape" + }, + { + "x": 903.4673496923492, + "y": 53.61062082191711, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:2uCxXh6cnNAxnZyb_KuDH", + "type": "geo", + "props": { + "w": 99.36425064789591, + "h": 461.26928683828396, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aG", + "typeName": "shape" + }, + { + "x": 1351.9439121923492, + "y": -1.0020119295477343, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:YX46lpEXwsP_hDEfdMsD9", + "type": "geo", + "props": { + "w": 57.91015625, + "h": 554.8632659912108, + "geo": "rectangle", + "color": "black", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aH", + "typeName": "shape" + }, + { + "x": 1356.7978541724624, + "y": 244.31275517707417, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:8Svx_N7tJ-qYJ0llQp1IU", + "type": "geo", + "props": { + "w": 48.88801956176758, + "h": 65.26476272972224, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "5", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aJ", + "typeName": "shape" + }, + { + "x": 919.7707358375171, + "y": 60.51165934940586, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:6s2XH02MusvP6oFWgMQcb", + "type": "text", + "props": { + "color": "light-blue", + "size": "s", + "w": 16, + "text": "1", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aL", + "typeName": "shape" + }, + { + "x": 951.4334487457494, + "y": 110.4918006481193, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:GnvsePN4zv2AJZm61LBQz", + "type": "text", + "props": { + "color": "light-blue", + "size": "s", + "w": 16, + "text": "3", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aN", + "typeName": "shape" + }, + { + "x": 701.2647232635863, + "y": 263.38123474876966, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:bWvcwdw9py4Vu5H4zO4T_", + "type": "arrow", + "parentId": "page:page", + "index": "aP", + "props": { + "dash": "draw", + "size": "xl", + "fill": "none", + "color": "light-blue", + "labelColor": "black", + "bend": 0, + "start": { + "type": "point", + "x": 0, + "y": 0 + }, + "end": { + "type": "point", + "x": 133.3045156202329, + "y": -0.21178503374801494 + }, + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "text": "", + "labelPosition": 0.5, + "font": "draw" + }, + "typeName": "shape" + }, + { + "x": 1059.4133089551478, + "y": 105.57886256417618, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:oCQTPmRoVscXxrOwK5pK7", + "type": "geo", + "props": { + "w": 56.05233069734595, + "h": 71.63415732885807, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aQ", + "typeName": "shape" + }, + { + "x": 1357.016832647147, + "y": 103.64725591791213, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:N9IB_pAyKkgHU9d8pg-N9", + "type": "text", + "props": { + "color": "light-blue", + "size": "s", + "w": 16, + "text": "3", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aR", + "typeName": "shape" + }, + { + "x": 1060.1204957350876, + "y": 53.31898405311833, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:sYztnUSygK7T27htroeVd", + "type": "geo", + "props": { + "w": 99.36425064789591, + "h": 461.26928683828396, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aW", + "typeName": "shape" + }, + { + "x": 1066.5314514261336, + "y": 55.051774724258536, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:YTzysZoy6xyaBVXBeK8IS", + "type": "text", + "props": { + "color": "light-blue", + "size": "s", + "w": 16, + "text": "1", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aX", + "typeName": "shape" + }, + { + "x": 1059.4133089551478, + "y": 105.57886256417618, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:h32QlyTDcBmBR6nKNU5Fy", + "type": "geo", + "props": { + "w": 56.05233069734595, + "h": 71.63415732885807, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aY", + "typeName": "shape" + }, + { + "x": 1352.9496098313912, + "y": 103.66690984048108, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:qa04yggT2SX3P5k3GYmhJ", + "type": "geo", + "props": { + "w": 56.05233069734595, + "h": 71.63415732885807, + "geo": "rectangle", + "color": "light-blue", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "s", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aZ", + "typeName": "shape" + }, + { + "x": 1059.790925494965, + "y": 106.88667183369284, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "id": "shape:9wqvv0_37l2RzNsssd5yo", + "type": "text", + "props": { + "color": "light-blue", + "size": "s", + "w": 16, + "text": "3", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aa", + "typeName": "shape" + } + ] +} \ No newline at end of file diff --git a/docs/tldr/ref-range/move-rows-cols.tldr b/docs/tldr/ref-range/move-rows-cols.tldr index dbe97b313c..c7a621b07e 100644 --- a/docs/tldr/ref-range/move-rows-cols.tldr +++ b/docs/tldr/ref-range/move-rows-cols.tldr @@ -1,2161 +1,2168 @@ { - "tldrawFileFormatVersion": 1, - "schema": { - "schemaVersion": 1, - "storeVersion": 4, - "recordVersions": { - "asset": { - "version": 1, - "subTypeKey": "type", - "subTypeVersions": { - "image": 3, - "video": 3, - "bookmark": 1 - } - }, - "camera": { - "version": 1 - }, - "document": { - "version": 2 - }, - "instance": { - "version": 22 - }, - "instance_page_state": { - "version": 5 - }, - "page": { - "version": 1 - }, - "shape": { - "version": 3, - "subTypeKey": "type", - "subTypeVersions": { - "group": 0, - "text": 1, - "bookmark": 2, - "draw": 1, - "geo": 8, - "note": 5, - "line": 1, - "frame": 0, - "arrow": 2, - "highlight": 0, - "embed": 4, - "image": 3, - "video": 2 - } - }, - "instance_presence": { - "version": 5 - }, - "pointer": { - "version": 1 - } - } - }, - "records": [ - { - "gridSize": 10, - "name": "", - "meta": {}, - "id": "document:document", - "typeName": "document" - }, - { - "id": "pointer:pointer", - "typeName": "pointer", - "x": 1284.651115464451, - "y": 1103.7717774000166, - "lastActivityTimestamp": 1705137252973, - "meta": {} - }, - { - "meta": {}, - "id": "page:page", - "name": "Page 1", - "index": "a1", - "typeName": "page" - }, - { - "x": 42.323994221064424, - "y": 459.3820565153594, - "z": 0.35291732043940677, - "meta": {}, - "id": "camera:page:page", - "typeName": "camera" - }, - { - "editingShapeId": null, - "croppingShapeId": null, - "selectedShapeIds": [], - "hoveredShapeId": null, - "erasingShapeIds": [], - "hintingShapeIds": [], - "focusedGroupId": null, - "meta": {}, - "id": "instance_page_state:page:page", - "pageId": "page:page", - "typeName": "instance_page_state" - }, - { - "followingUserId": null, - "opacityForNextShape": 1, - "stylesForNextShape": { - "tldraw:geo": "rectangle", - "tldraw:color": "violet" - }, - "brush": null, - "scribbles": [], - "cursor": { - "type": "default", - "rotation": 0 - }, - "isFocusMode": false, - "exportBackground": true, - "isDebugMode": false, - "isToolLocked": false, - "screenBounds": { - "x": 0, - "y": 0, - "w": 1502, - "h": 686 - }, - "zoomBrush": null, - "isGridMode": false, - "isPenMode": false, - "chatMessage": "", - "isChatting": false, - "highlightedUserIds": [], - "canMoveCamera": true, - "isFocused": true, - "devicePixelRatio": 2, - "isCoarsePointer": false, - "isHoveringCanvas": true, - "openMenus": [], - "isChangingStyle": false, - "isReadonly": false, - "meta": {}, - "id": "instance:instance", - "currentPageId": "page:page", - "typeName": "instance" - }, - { - "x": 472.0625, - "y": 57.3203125, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 84.44837816408484, - "h": 701.5329274578631, - "geo": "rectangle", - "color": "black", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "a1", - "id": "shape:P62MdUY0Dy81VSeTUIONl", - "typeName": "shape" - }, - { - "x": 864.109375, - "y": 56.1796875, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 86.28864137419669, - "h": 702.5900322883235, - "geo": "rectangle", - "color": "black", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "a2", - "id": "shape:vbhz70wsL10JtI9Xtq6KC", - "typeName": "shape" - }, - { - "x": 320.3671875, - "y": 88.24255859375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 103.96875, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "1", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "a3", - "id": "shape:CNSeMR_BSUg9frAXK1VUW", - "typeName": "shape" - }, - { - "x": 639.6796875, - "y": 84.82849609375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 157.28515625, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "2", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "a4", - "id": "shape:2D7Oin0ZthSKRprVWSjVh", - "typeName": "shape" - }, - { - "x": 966.7734375, - "y": 84.06287109375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 190.19921875, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "3", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "a5", - "id": "shape:53jPM-Q_ZIMhF73t2JUAf", - "typeName": "shape" - }, - { - "x": 399.375, - "y": 194.39880859375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 87.9921875, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "4", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "a6", - "id": "shape:OTEkfsumv6I3__FfNszb2", - "typeName": "shape" - }, - { - "x": 518.28125, - "y": 191.81287109375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 134.328125, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "5", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "a7", - "id": "shape:dppGVZY6bvPOfkCbWEVD2", - "typeName": "shape" - }, - { - "x": 736.765625, - "y": 184.00818359375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 143.05859375, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "6", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "a8", - "id": "shape:Lns1atnS5NiJaz2IZHgpq", - "typeName": "shape" - }, - { - "x": 898.875, - "y": 183.56287109375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 257.0078125, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "7", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "a9", - "id": "shape:o1rgYhARe-nUdq9dbtGsh", - "typeName": "shape" - }, - { - "x": 524.6484375, - "y": 275.00818359375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 359.55859375, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "8", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aA", - "id": "shape:cqr-o-7eDFKCQE_xUV5AK", - "typeName": "shape" - }, - { - "x": 425.1171875, - "y": 346.78162109375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 162.98828125, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "9", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aB", - "id": "shape:GnlPTdf2n5aP-oPABBbwn", - "typeName": "shape" - }, - { - "x": 816.5078125, - "y": 343.54724609375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 206.76953125, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "10", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aC", - "id": "shape:I14tOD8CBmS4i_lCfqeec", - "typeName": "shape" - }, - { - "x": 425.8359375, - "y": 429.18787109375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 645.6875, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "11", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aD", - "id": "shape:LMKxPzywvA9bwQCoBHNd8", - "typeName": "shape" - }, - { - "x": 477.8671875, - "y": 499.591875, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 61.6953125, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "12", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aE", - "id": "shape:N2o98gLAI0Tu_RY-eG82P", - "typeName": "shape" - }, - { - "x": 878.48828125, - "y": 483.75984375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 56.3984375, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "13", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aF", - "id": "shape:OrzEyM2qRUb3rCIMm4W2M", - "typeName": "shape" - }, - { - "x": 444.5685039964794, - "y": 768.473474749199, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "text", - "props": { - "color": "black", - "size": "m", - "w": 139.078125, - "text": "fromRange", - "font": "draw", - "align": "middle", - "autoSize": true, - "scale": 1 - }, - "parentId": "page:page", - "index": "aG", - "id": "shape:WplwFUreuMXbzmz9nImMi", - "typeName": "shape" - }, - { - "x": 858.4978139432524, - "y": 778.3026825648035, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "text", - "props": { - "color": "black", - "size": "m", - "w": 105.640625, - "text": "toRange", - "font": "draw", - "align": "middle", - "autoSize": true, - "scale": 1 - }, - "parentId": "page:page", - "index": "aH", - "id": "shape:_-jyV9-75YjoYv27BrN_5", - "typeName": "shape" - }, - { - "x": 515.6932320933668, - "y": 850.4239069704852, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "text", - "props": { - "color": "violet", - "size": "m", - "w": 413.828125, - "text": "1,3,13 unchnged\n2 move forward fromRamge step\n6,7,10 unhandle \n12 move to mirror position\n4,5,8,9,11,14,15 reduce", - "font": "draw", - "align": "middle", - "autoSize": true, - "scale": 1 - }, - "parentId": "page:page", - "index": "aI", - "id": "shape:yqgyKvkqk2c8BIR2xSo3D", - "typeName": "shape" - }, - { - "x": 1375.4013671875, - "y": 56.40625, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 75.6285915297724, - "h": 718.8506485950418, - "geo": "rectangle", - "color": "black", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aJ", - "id": "shape:yDkvUKbSh4fbDNHNjTJu3", - "typeName": "shape" - }, - { - "x": 1767.4482421875, - "y": 55.265625, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 85.63974594789352, - "h": 726.4901486299218, - "geo": "rectangle", - "color": "black", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aK", - "id": "shape:NRWw5GJfSaVB5oqekU-VR", - "typeName": "shape" - }, - { - "x": 1223.7060546875, - "y": 87.32849609375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 103.96875, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "1", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aL", - "id": "shape:ZA4OG8Xf5kERNkScCteNM", - "typeName": "shape" - }, - { - "x": 1543.0185546875, - "y": 83.91443359375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 157.28515625, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "2", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aM", - "id": "shape:8W2uiQV7-ZI-Oi0bJ6kPZ", - "typeName": "shape" - }, - { - "x": 1870.1123046875, - "y": 83.14880859375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 190.19921875, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "3", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aN", - "id": "shape:nhiHR5tm-TbwLmhGXSFiA", - "typeName": "shape" - }, - { - "x": 1302.7138671875, - "y": 193.48474609375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 87.9921875, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "4", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aO", - "id": "shape:tskMS3Y2tlTNzsQ_-7Htr", - "typeName": "shape" - }, - { - "x": 1421.6201171875, - "y": 190.89880859375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 134.328125, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "5", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aP", - "id": "shape:I44AWzgcfCWBlp7mP14oc", - "typeName": "shape" - }, - { - "x": 1640.1044921875, - "y": 183.09412109375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 143.05859375, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "6", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aQ", - "id": "shape:9gwPFFe06d3O9CJFbT8mI", - "typeName": "shape" - }, - { - "x": 1802.2138671875, - "y": 182.64880859375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 257.0078125, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "7", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aR", - "id": "shape:nRlZeaz1AJNOzYS4P6FOv", - "typeName": "shape" - }, - { - "x": 1427.9873046875, - "y": 274.09412109375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 359.55859375, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "8", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aS", - "id": "shape:dFeveS-GehMaRYzINPKui", - "typeName": "shape" - }, - { - "x": 1328.4560546875, - "y": 345.86755859375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 162.98828125, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "9", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aT", - "id": "shape:CGFibHbFXYd3EGJD1wCj_", - "typeName": "shape" - }, - { - "x": 1719.8466796875, - "y": 342.63318359375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 206.76953125, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "10", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aU", - "id": "shape:zBCzM8t3iAco9S6K64qPY", - "typeName": "shape" - }, - { - "x": 1329.1748046875, - "y": 428.27380859375, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 645.6875, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "11", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aV", - "id": "shape:lc5YAIGjb5W4e3_nmEQvq", - "typeName": "shape" - }, - { - "x": 1381.2060546875, - "y": 498.6778125, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 61.6953125, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "12", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aW", - "id": "shape:_5tQs7Nrv6rkhqjTlhk99", - "typeName": "shape" - }, - { - "x": 1781.8271484375, - "y": 482.84578125, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 56.3984375, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "13", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aX", - "id": "shape:8IcZnSV-nKKph8Ub7CIfA", - "typeName": "shape" - }, - { - "x": 1361.3712617256426, - "y": 778.6840754377405, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "text", - "props": { - "color": "black", - "size": "m", - "w": 105.640625, - "text": "toRange", - "font": "draw", - "align": "middle", - "autoSize": true, - "scale": 1 - }, - "parentId": "page:page", - "index": "aY", - "id": "shape:cjcW0RQgdyn5A2ejCmZcE", - "typeName": "shape" - }, - { - "x": 1768.9557535114254, - "y": 785.0923065658747, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "text", - "props": { - "color": "black", - "size": "m", - "w": 139.078125, - "text": "fromRange", - "font": "draw", - "align": "middle", - "autoSize": true, - "scale": 1 - }, - "parentId": "page:page", - "index": "aZ", - "id": "shape:4cbs5XzURQifPCxvKBL7k", - "typeName": "shape" - }, - { - "x": 1455.8719806874733, - "y": 836.7164980741185, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "text", - "props": { - "color": "violet", - "size": "m", - "w": 363.140625, - "text": "1,3 unchanged\n2 move back fromRange step\n4,5,9 unhandle\n13 move to mirror position\n12 move right\n6,8,10,11,14,15 reduce\n7 reduce and move left", - "font": "draw", - "align": "middle", - "autoSize": true, - "scale": 1 - }, - "parentId": "page:page", - "index": "aa", - "id": "shape:G0zg7fkZTEce7PNcOTl_v", - "typeName": "shape" - }, - { - "x": 2230.7672502938017, - "y": 72.59615047955401, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 298.453125, - "h": 650.8844989933119, - "geo": "rectangle", - "color": "black", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "ad", - "id": "shape:USoQ-Gz6lgAtx9o6_Nvz1", - "typeName": "shape" - }, - { - "x": 2431.6695940438017, - "y": 42.813859204113896, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 317.4137666022398, - "h": 729.1924324265369, - "geo": "rectangle", - "color": "black", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "ae", - "id": "shape:5iRoVaumyCh18yyPrqIW9", - "typeName": "shape" - }, - { - "x": 2126.7438127938017, - "y": 99.17196787658833, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 77.66796875, - "h": 70.6796875, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "1", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "af", - "id": "shape:06hUQvNWG9Azznfv69KfK", - "typeName": "shape" - }, - { - "x": 2779.350247715548, - "y": 94.60580533242506, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 152.3046875, - "h": 69.30078125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "2", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "ag", - "id": "shape:k39Bg4CT2Qj4J-5FXUTdS", - "typeName": "shape" - }, - { - "x": 2179.2516252938017, - "y": 222.57040537658833, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 100.0234375, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "3", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "ah", - "id": "shape:wtWUB9L6S3Wi85fCTLoaw", - "typeName": "shape" - }, - { - "x": 2679.455948511778, - "y": 218.3948443270014, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 144.76953125, - "h": 62.0625, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "4", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "ai", - "id": "shape:qNw6a4kSCL8NxgcwwAafk", - "typeName": "shape" - }, - { - "x": 2355.3028112169095, - "y": 298.6087647508237, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 274.9375, - "h": 62.77734375, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "5", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aj", - "id": "shape:gifZmd76T4qNd-XZ6bCq8", - "typeName": "shape" - }, - { - "x": 2371.2438127938017, - "y": 388.2344678765883, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 97.76656061206359, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "6", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "ak", - "id": "shape:-I6rWzltrZxkhEOkMtJPz", - "typeName": "shape" - }, - { - "x": 2499.4235002938017, - "y": 387.8360303765883, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 83.0234375, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "7", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "al", - "id": "shape:W-MKxZH0RNPTkyvMqCyIu", - "typeName": "shape" - }, - { - "x": 2453.503049756205, - "y": 471.276221493116, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 61.6953125, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "8", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "am", - "id": "shape:-il-WBxPjKs9gOfbOv5ka", - "typeName": "shape" - }, - { - "x": 2180.0044017017103, - "y": 609.2202258074503, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 650.80859375, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "11", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "an", - "id": "shape:9LKJHT6UeDwlvTiY6cDB8", - "typeName": "shape" - }, - { - "x": 2288.568090018054, - "y": 542.7594195869685, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 85.77842662415833, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "9", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "ao", - "id": "shape:cX0-RvXN_8tutum0qQavY", - "typeName": "shape" - }, - { - "x": 2571.402629065568, - "y": 537.4819353558903, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 119.31619531702245, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "10", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "ap", - "id": "shape:2abj8uSErOBSgdH7zQDjb", - "typeName": "shape" - }, - { - "x": 2322.4499606706718, - "y": 842.8417831541492, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "text", - "props": { - "color": "violet", - "size": "m", - "w": 334.2421875, - "text": "1,2,11,10 unchnged\n3,4,5,6,7 deestroy\n8,9 move to mirror position", - "font": "draw", - "align": "middle", - "autoSize": true, - "scale": 1 - }, - "parentId": "page:page", - "index": "ar", - "id": "shape:kHLcCXu7Z_DEvFwcu5fKj", - "typeName": "shape" - }, - { - "x": 2544.490208833556, - "y": 778.892978148866, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "text", - "props": { - "color": "black", - "size": "m", - "w": 105.640625, - "text": "toRange", - "font": "draw", - "align": "middle", - "autoSize": true, - "scale": 1 - }, - "parentId": "page:page", - "index": "as", - "id": "shape:SD0M6P51K2i9BtIu5lBw7", - "typeName": "shape" - }, - { - "x": 2270.981507056962, - "y": 726.9030949443218, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "text", - "props": { - "color": "black", - "size": "m", - "w": 139.078125, - "text": "fromRange", - "font": "draw", - "align": "middle", - "autoSize": true, - "scale": 1 - }, - "parentId": "page:page", - "index": "at", - "id": "shape:oh9mpeiYHfgjPYG4bWKS6", - "typeName": "shape" - }, - { - "x": 3150.05147464424, - "y": 69.71067771276694, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 298.453125, - "h": 650.8844989933119, - "geo": "rectangle", - "color": "black", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "au", - "id": "shape:pcN9670sdxI7rxL_3elxq", - "typeName": "shape" - }, - { - "x": 3350.95381839424, - "y": 39.92838643732682, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 317.4137666022398, - "h": 729.1924324265369, - "geo": "rectangle", - "color": "black", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "av", - "id": "shape:1uCMJS6cqH2YYXg2Zhyec", - "typeName": "shape" - }, - { - "x": 3046.02803714424, - "y": 96.28649510980125, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 77.66796875, - "h": 70.6796875, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "1", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "aw", - "id": "shape:-z4w1mDbtiEJ3Fp-qtPWU", - "typeName": "shape" - }, - { - "x": 3698.6344720659863, - "y": 91.72033256563799, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 152.3046875, - "h": 69.30078125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "2", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "ax", - "id": "shape:6A3shFz50wuzMN3DwcvWz", - "typeName": "shape" - }, - { - "x": 3098.53584964424, - "y": 219.68493260980125, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 100.0234375, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "3", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "ay", - "id": "shape:8UUQWaChGsQRQk-icYNY5", - "typeName": "shape" - }, - { - "x": 3598.7401728622162, - "y": 215.50937156021433, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 144.76953125, - "h": 62.0625, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "4", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "az", - "id": "shape:nQZ22tV8Ar4AJLEe_1ch0", - "typeName": "shape" - }, - { - "x": 3274.587035567348, - "y": 295.7232919840366, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 274.9375, - "h": 62.77734375, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "5", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "b00", - "id": "shape:f-INTEQ9H8xLd4Qq06Lpr", - "typeName": "shape" - }, - { - "x": 3290.52803714424, - "y": 385.34899510980125, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 97.76656061206359, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "6", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "b01", - "id": "shape:OCTXq7gpWl4evpdEFLhgJ", - "typeName": "shape" - }, - { - "x": 3418.70772464424, - "y": 384.95055760980125, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 83.0234375, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "7", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "b02", - "id": "shape:PwZsY2RNJ2yBJixaYY5Mw", - "typeName": "shape" - }, - { - "x": 3372.7872741066435, - "y": 468.3907487263289, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 61.6953125, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "8", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "b03", - "id": "shape:ArXaxW0gJt4ZQquiPTXk3", - "typeName": "shape" - }, - { - "x": 3099.2886260521486, - "y": 606.3347530406633, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 650.80859375, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "11", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "b04", - "id": "shape:LKIX_v9CFTfWNenI7thrn", - "typeName": "shape" - }, - { - "x": 3207.8523143684924, - "y": 539.8739468201816, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 85.77842662415833, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "9", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "b05", - "id": "shape:eHQ0-nx7uSVvg-lPsc4wf", - "typeName": "shape" - }, - { - "x": 3490.686853416006, - "y": 534.5964625891033, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 119.31619531702245, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "10", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "b06", - "id": "shape:_TrxMAgk1Ht0XiP9d3jYZ", - "typeName": "shape" - }, - { - "x": 3264.085190882668, - "y": 854.8051637107698, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "text", - "props": { - "color": "violet", - "size": "m", - "w": 367.171875, - "text": "1,2,11 unchnged\n3,4,5,6,7, deestroy\n10,8 move to mirror position\n9 move right fromRange step", - "font": "draw", - "align": "middle", - "autoSize": true, - "scale": 1 - }, - "parentId": "page:page", - "index": "b07", - "id": "shape:IBxxUJ_KBA1FL58U-YuYC", - "typeName": "shape" - }, - { - "x": 3198.0026982253244, - "y": 731.4582032883303, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "text", - "props": { - "color": "black", - "size": "m", - "w": 105.640625, - "text": "toRange", - "font": "draw", - "align": "middle", - "autoSize": true, - "scale": 1 - }, - "parentId": "page:page", - "index": "b08", - "id": "shape:NQ57JvxaZgtDxCqrvsVB-", - "typeName": "shape" - }, - { - "x": 3448.554074086629, - "y": 774.4834277782564, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "text", - "props": { - "color": "black", - "size": "m", - "w": 139.078125, - "text": "fromRange", - "font": "draw", - "align": "middle", - "autoSize": true, - "scale": 1 - }, - "parentId": "page:page", - "index": "b09", - "id": "shape:gZmLskcNZ51K8RSqmNq6N", - "typeName": "shape" - }, - { - "x": 442.4586613526074, - "y": 584.1852181749076, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 464.9205950376922, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "14", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "b0A", - "id": "shape:l53ETguoSEd5vAAEhqVFC", - "typeName": "shape" - }, - { - "x": 515.0415081571743, - "y": 658.2527377148562, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 523.6482266998339, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "15", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "b0B", - "id": "shape:DW-wjdlDyxBwAzCLgeISU", - "typeName": "shape" - }, - { - "x": 1333.2519202547724, - "y": 584.2475121358327, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 464.9205950376922, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "14", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "b0C", - "id": "shape:uqtIqThhennukeWGwDFQ3", - "typeName": "shape" - }, - { - "x": 1405.8347670593394, - "y": 658.3150316757813, - "rotation": 0, - "isLocked": false, - "opacity": 1, - "meta": {}, - "type": "geo", - "props": { - "w": 523.6482266998339, - "h": 61.6953125, - "geo": "rectangle", - "color": "violet", - "labelColor": "black", - "fill": "none", - "dash": "draw", - "size": "m", - "font": "draw", - "text": "15", - "align": "middle", - "verticalAlign": "middle", - "growY": 0, - "url": "" - }, - "parentId": "page:page", - "index": "b0D", - "id": "shape:bT5sc_XKqn6PuZFDg-x2M", - "typeName": "shape" - } - ] + "tldrawFileFormatVersion": 1, + "schema": { + "schemaVersion": 1, + "storeVersion": 4, + "recordVersions": { + "asset": { + "version": 1, + "subTypeKey": "type", + "subTypeVersions": { + "image": 3, + "video": 3, + "bookmark": 1 + } + }, + "camera": { + "version": 1 + }, + "document": { + "version": 2 + }, + "instance": { + "version": 24 + }, + "instance_page_state": { + "version": 5 + }, + "page": { + "version": 1 + }, + "shape": { + "version": 3, + "subTypeKey": "type", + "subTypeVersions": { + "group": 0, + "text": 1, + "bookmark": 2, + "draw": 1, + "geo": 8, + "note": 5, + "line": 4, + "frame": 0, + "arrow": 3, + "highlight": 0, + "embed": 4, + "image": 3, + "video": 2 + } + }, + "instance_presence": { + "version": 5 + }, + "pointer": { + "version": 1 + } + } + }, + "records": [ + { + "gridSize": 10, + "name": "", + "meta": {}, + "id": "document:document", + "typeName": "document" + }, + { + "id": "pointer:pointer", + "typeName": "pointer", + "x": 1064.1143908049876, + "y": 368.4612106924942, + "lastActivityTimestamp": 1711104635736, + "meta": {} + }, + { + "meta": {}, + "id": "page:page", + "name": "Page 1", + "index": "a1", + "typeName": "page" + }, + { + "x": -87.85485456341856, + "y": 146.87197094313137, + "z": 0.5432165247180497, + "meta": {}, + "id": "camera:page:page", + "typeName": "camera" + }, + { + "editingShapeId": null, + "croppingShapeId": null, + "selectedShapeIds": [], + "hoveredShapeId": null, + "erasingShapeIds": [], + "hintingShapeIds": [], + "focusedGroupId": null, + "meta": {}, + "id": "instance_page_state:page:page", + "pageId": "page:page", + "typeName": "instance_page_state" + }, + { + "followingUserId": null, + "opacityForNextShape": 1, + "stylesForNextShape": { + "tldraw:geo": "rectangle", + "tldraw:color": "violet" + }, + "brush": null, + "scribbles": [], + "cursor": { + "type": "default", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1655, + "h": 723 + }, + "zoomBrush": null, + "isGridMode": false, + "isPenMode": false, + "chatMessage": "", + "isChatting": false, + "highlightedUserIds": [], + "canMoveCamera": true, + "isFocused": true, + "devicePixelRatio": 2, + "isCoarsePointer": false, + "isHoveringCanvas": true, + "openMenus": [], + "isChangingStyle": false, + "isReadonly": false, + "meta": {}, + "currentPageId": "page:page", + "insets": [ + false, + false, + true, + false + ], + "duplicateProps": null, + "id": "instance:instance", + "typeName": "instance" + }, + { + "x": 472.0625, + "y": 57.3203125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 84.44837816408484, + "h": 701.5329274578631, + "geo": "rectangle", + "color": "black", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a1", + "id": "shape:P62MdUY0Dy81VSeTUIONl", + "typeName": "shape" + }, + { + "x": 864.109375, + "y": 56.1796875, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 86.28864137419669, + "h": 702.5900322883235, + "geo": "rectangle", + "color": "black", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a2", + "id": "shape:vbhz70wsL10JtI9Xtq6KC", + "typeName": "shape" + }, + { + "x": 320.3671875, + "y": 88.24255859375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 103.96875, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "1", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a3", + "id": "shape:CNSeMR_BSUg9frAXK1VUW", + "typeName": "shape" + }, + { + "x": 639.6796875, + "y": 84.82849609375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 157.28515625, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "2", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a4", + "id": "shape:2D7Oin0ZthSKRprVWSjVh", + "typeName": "shape" + }, + { + "x": 966.7734375, + "y": 84.06287109375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 190.19921875, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "3", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a5", + "id": "shape:53jPM-Q_ZIMhF73t2JUAf", + "typeName": "shape" + }, + { + "x": 399.375, + "y": 194.39880859375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 87.9921875, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "4", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a6", + "id": "shape:OTEkfsumv6I3__FfNszb2", + "typeName": "shape" + }, + { + "x": 518.28125, + "y": 191.81287109375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 134.328125, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "5", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a7", + "id": "shape:dppGVZY6bvPOfkCbWEVD2", + "typeName": "shape" + }, + { + "x": 736.765625, + "y": 184.00818359375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 143.05859375, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "6", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a8", + "id": "shape:Lns1atnS5NiJaz2IZHgpq", + "typeName": "shape" + }, + { + "x": 898.875, + "y": 183.56287109375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 257.0078125, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "7", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "a9", + "id": "shape:o1rgYhARe-nUdq9dbtGsh", + "typeName": "shape" + }, + { + "x": 524.6484375, + "y": 275.00818359375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 359.55859375, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "8", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aA", + "id": "shape:cqr-o-7eDFKCQE_xUV5AK", + "typeName": "shape" + }, + { + "x": 425.1171875, + "y": 346.78162109375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 162.98828125, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "9", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aB", + "id": "shape:GnlPTdf2n5aP-oPABBbwn", + "typeName": "shape" + }, + { + "x": 816.5078125, + "y": 343.54724609375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 206.76953125, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "10", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aC", + "id": "shape:I14tOD8CBmS4i_lCfqeec", + "typeName": "shape" + }, + { + "x": 425.8359375, + "y": 429.18787109375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 645.6875, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "11", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aD", + "id": "shape:LMKxPzywvA9bwQCoBHNd8", + "typeName": "shape" + }, + { + "x": 477.8671875, + "y": 499.591875, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 61.6953125, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "12", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aE", + "id": "shape:N2o98gLAI0Tu_RY-eG82P", + "typeName": "shape" + }, + { + "x": 878.48828125, + "y": 483.75984375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 56.3984375, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "13", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aF", + "id": "shape:OrzEyM2qRUb3rCIMm4W2M", + "typeName": "shape" + }, + { + "x": 444.5685039964794, + "y": 768.473474749199, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "text", + "props": { + "color": "black", + "size": "m", + "w": 139.078125, + "text": "fromRange", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aG", + "id": "shape:WplwFUreuMXbzmz9nImMi", + "typeName": "shape" + }, + { + "x": 858.4978139432524, + "y": 778.3026825648035, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "text", + "props": { + "color": "black", + "size": "m", + "w": 105.640625, + "text": "toRange", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aH", + "id": "shape:_-jyV9-75YjoYv27BrN_5", + "typeName": "shape" + }, + { + "x": 1375.4013671875, + "y": 56.40625, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 75.6285915297724, + "h": 718.8506485950418, + "geo": "rectangle", + "color": "black", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aJ", + "id": "shape:yDkvUKbSh4fbDNHNjTJu3", + "typeName": "shape" + }, + { + "x": 1767.4482421875, + "y": 55.265625, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 85.63974594789352, + "h": 726.4901486299218, + "geo": "rectangle", + "color": "black", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aK", + "id": "shape:NRWw5GJfSaVB5oqekU-VR", + "typeName": "shape" + }, + { + "x": 1223.7060546875, + "y": 87.32849609375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 103.96875, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "1", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aL", + "id": "shape:ZA4OG8Xf5kERNkScCteNM", + "typeName": "shape" + }, + { + "x": 1543.0185546875, + "y": 83.91443359375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 157.28515625, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "2", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aM", + "id": "shape:8W2uiQV7-ZI-Oi0bJ6kPZ", + "typeName": "shape" + }, + { + "x": 1870.1123046875, + "y": 83.14880859375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 190.19921875, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "3", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aN", + "id": "shape:nhiHR5tm-TbwLmhGXSFiA", + "typeName": "shape" + }, + { + "x": 1302.7138671875, + "y": 193.48474609375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 87.9921875, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "4", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aO", + "id": "shape:tskMS3Y2tlTNzsQ_-7Htr", + "typeName": "shape" + }, + { + "x": 1421.6201171875, + "y": 190.89880859375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 134.328125, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "5", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aP", + "id": "shape:I44AWzgcfCWBlp7mP14oc", + "typeName": "shape" + }, + { + "x": 1640.1044921875, + "y": 183.09412109375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 143.05859375, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "6", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aQ", + "id": "shape:9gwPFFe06d3O9CJFbT8mI", + "typeName": "shape" + }, + { + "x": 1802.2138671875, + "y": 182.64880859375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 257.0078125, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "7", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aR", + "id": "shape:nRlZeaz1AJNOzYS4P6FOv", + "typeName": "shape" + }, + { + "x": 1427.9873046875, + "y": 274.09412109375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 359.55859375, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "8", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aS", + "id": "shape:dFeveS-GehMaRYzINPKui", + "typeName": "shape" + }, + { + "x": 1328.4560546875, + "y": 345.86755859375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 162.98828125, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "9", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aT", + "id": "shape:CGFibHbFXYd3EGJD1wCj_", + "typeName": "shape" + }, + { + "x": 1719.8466796875, + "y": 342.63318359375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 206.76953125, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "10", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aU", + "id": "shape:zBCzM8t3iAco9S6K64qPY", + "typeName": "shape" + }, + { + "x": 1329.1748046875, + "y": 428.27380859375, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 645.6875, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "11", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aV", + "id": "shape:lc5YAIGjb5W4e3_nmEQvq", + "typeName": "shape" + }, + { + "x": 1381.2060546875, + "y": 498.6778125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 61.6953125, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "12", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aW", + "id": "shape:_5tQs7Nrv6rkhqjTlhk99", + "typeName": "shape" + }, + { + "x": 1781.8271484375, + "y": 482.84578125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 56.3984375, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "13", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aX", + "id": "shape:8IcZnSV-nKKph8Ub7CIfA", + "typeName": "shape" + }, + { + "x": 1361.3712617256426, + "y": 778.6840754377405, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "text", + "props": { + "color": "black", + "size": "m", + "w": 105.640625, + "text": "toRange", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aY", + "id": "shape:cjcW0RQgdyn5A2ejCmZcE", + "typeName": "shape" + }, + { + "x": 1768.9557535114254, + "y": 785.0923065658747, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "text", + "props": { + "color": "black", + "size": "m", + "w": 139.078125, + "text": "fromRange", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aZ", + "id": "shape:4cbs5XzURQifPCxvKBL7k", + "typeName": "shape" + }, + { + "x": 1455.8719806874733, + "y": 836.7164980741185, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "text", + "props": { + "color": "violet", + "size": "m", + "w": 363.140625, + "text": "1,3 11 unchanged\n2 move back fromRange step\n4 9 14 expend\n5, 12 15 move right\n13 move to mirror position\n12 move right\n6,8,10,14,15 reduce\n7 reduce and move left", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aa", + "id": "shape:G0zg7fkZTEce7PNcOTl_v", + "typeName": "shape" + }, + { + "x": 2230.7672502938017, + "y": 72.59615047955401, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 298.453125, + "h": 650.8844989933119, + "geo": "rectangle", + "color": "black", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "ad", + "id": "shape:USoQ-Gz6lgAtx9o6_Nvz1", + "typeName": "shape" + }, + { + "x": 2431.6695940438017, + "y": 42.813859204113896, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 317.4137666022398, + "h": 729.1924324265369, + "geo": "rectangle", + "color": "black", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "ae", + "id": "shape:5iRoVaumyCh18yyPrqIW9", + "typeName": "shape" + }, + { + "x": 2126.7438127938017, + "y": 99.17196787658833, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 77.66796875, + "h": 70.6796875, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "1", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "af", + "id": "shape:06hUQvNWG9Azznfv69KfK", + "typeName": "shape" + }, + { + "x": 2779.350247715548, + "y": 94.60580533242506, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 152.3046875, + "h": 69.30078125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "2", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "ag", + "id": "shape:k39Bg4CT2Qj4J-5FXUTdS", + "typeName": "shape" + }, + { + "x": 2179.2516252938017, + "y": 222.57040537658833, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 100.0234375, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "3", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "ah", + "id": "shape:wtWUB9L6S3Wi85fCTLoaw", + "typeName": "shape" + }, + { + "x": 2679.455948511778, + "y": 218.3948443270014, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 144.76953125, + "h": 62.0625, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "4", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "ai", + "id": "shape:qNw6a4kSCL8NxgcwwAafk", + "typeName": "shape" + }, + { + "x": 2355.3028112169095, + "y": 298.6087647508237, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 274.9375, + "h": 62.77734375, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "5", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aj", + "id": "shape:gifZmd76T4qNd-XZ6bCq8", + "typeName": "shape" + }, + { + "x": 2371.2438127938017, + "y": 388.2344678765883, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 97.76656061206359, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "6", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "ak", + "id": "shape:-I6rWzltrZxkhEOkMtJPz", + "typeName": "shape" + }, + { + "x": 2499.4235002938017, + "y": 387.8360303765883, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 83.0234375, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "7", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "al", + "id": "shape:W-MKxZH0RNPTkyvMqCyIu", + "typeName": "shape" + }, + { + "x": 2453.503049756205, + "y": 471.276221493116, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 61.6953125, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "8", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "am", + "id": "shape:-il-WBxPjKs9gOfbOv5ka", + "typeName": "shape" + }, + { + "x": 2180.0044017017103, + "y": 609.2202258074503, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 650.80859375, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "11", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "an", + "id": "shape:9LKJHT6UeDwlvTiY6cDB8", + "typeName": "shape" + }, + { + "x": 2288.568090018054, + "y": 542.7594195869685, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 85.77842662415833, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "9", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "ao", + "id": "shape:cX0-RvXN_8tutum0qQavY", + "typeName": "shape" + }, + { + "x": 2571.402629065568, + "y": 537.4819353558903, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 119.31619531702245, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "10", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "ap", + "id": "shape:2abj8uSErOBSgdH7zQDjb", + "typeName": "shape" + }, + { + "x": 2322.4499606706718, + "y": 842.8417831541492, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "text", + "props": { + "color": "violet", + "size": "m", + "w": 334.2421875, + "text": "1,2,11,10 unchnged\n3,4,5,6,7 deestroy\n8,9 move to mirror position", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "ar", + "id": "shape:kHLcCXu7Z_DEvFwcu5fKj", + "typeName": "shape" + }, + { + "x": 2544.490208833556, + "y": 778.892978148866, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "text", + "props": { + "color": "black", + "size": "m", + "w": 105.640625, + "text": "toRange", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "as", + "id": "shape:SD0M6P51K2i9BtIu5lBw7", + "typeName": "shape" + }, + { + "x": 2270.981507056962, + "y": 726.9030949443218, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "text", + "props": { + "color": "black", + "size": "m", + "w": 139.078125, + "text": "fromRange", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "at", + "id": "shape:oh9mpeiYHfgjPYG4bWKS6", + "typeName": "shape" + }, + { + "x": 3150.05147464424, + "y": 69.71067771276694, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 298.453125, + "h": 650.8844989933119, + "geo": "rectangle", + "color": "black", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "au", + "id": "shape:pcN9670sdxI7rxL_3elxq", + "typeName": "shape" + }, + { + "x": 3350.95381839424, + "y": 39.92838643732682, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 317.4137666022398, + "h": 729.1924324265369, + "geo": "rectangle", + "color": "black", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "av", + "id": "shape:1uCMJS6cqH2YYXg2Zhyec", + "typeName": "shape" + }, + { + "x": 3046.02803714424, + "y": 96.28649510980125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 77.66796875, + "h": 70.6796875, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "1", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "aw", + "id": "shape:-z4w1mDbtiEJ3Fp-qtPWU", + "typeName": "shape" + }, + { + "x": 3698.6344720659863, + "y": 91.72033256563799, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 152.3046875, + "h": 69.30078125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "2", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "ax", + "id": "shape:6A3shFz50wuzMN3DwcvWz", + "typeName": "shape" + }, + { + "x": 3098.53584964424, + "y": 219.68493260980125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 100.0234375, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "3", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "ay", + "id": "shape:8UUQWaChGsQRQk-icYNY5", + "typeName": "shape" + }, + { + "x": 3598.7401728622162, + "y": 215.50937156021433, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 144.76953125, + "h": 62.0625, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "4", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "az", + "id": "shape:nQZ22tV8Ar4AJLEe_1ch0", + "typeName": "shape" + }, + { + "x": 3274.587035567348, + "y": 295.7232919840366, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 274.9375, + "h": 62.77734375, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "5", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "b00", + "id": "shape:f-INTEQ9H8xLd4Qq06Lpr", + "typeName": "shape" + }, + { + "x": 3290.52803714424, + "y": 385.34899510980125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 97.76656061206359, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "6", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "b01", + "id": "shape:OCTXq7gpWl4evpdEFLhgJ", + "typeName": "shape" + }, + { + "x": 3418.70772464424, + "y": 384.95055760980125, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 83.0234375, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "7", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "b02", + "id": "shape:PwZsY2RNJ2yBJixaYY5Mw", + "typeName": "shape" + }, + { + "x": 3372.7872741066435, + "y": 468.3907487263289, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 61.6953125, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "8", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "b03", + "id": "shape:ArXaxW0gJt4ZQquiPTXk3", + "typeName": "shape" + }, + { + "x": 3099.2886260521486, + "y": 606.3347530406633, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 650.80859375, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "11", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "b04", + "id": "shape:LKIX_v9CFTfWNenI7thrn", + "typeName": "shape" + }, + { + "x": 3207.8523143684924, + "y": 539.8739468201816, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 85.77842662415833, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "9", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "b05", + "id": "shape:eHQ0-nx7uSVvg-lPsc4wf", + "typeName": "shape" + }, + { + "x": 3490.686853416006, + "y": 534.5964625891033, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 119.31619531702245, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "10", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "b06", + "id": "shape:_TrxMAgk1Ht0XiP9d3jYZ", + "typeName": "shape" + }, + { + "x": 3264.085190882668, + "y": 854.8051637107698, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "text", + "props": { + "color": "violet", + "size": "m", + "w": 367.171875, + "text": "1,2,11 unchnged\n3,4,5,6,7, deestroy\n10,8 move to mirror position\n9 move right fromRange step", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "b07", + "id": "shape:IBxxUJ_KBA1FL58U-YuYC", + "typeName": "shape" + }, + { + "x": 3198.0026982253244, + "y": 731.4582032883303, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "text", + "props": { + "color": "black", + "size": "m", + "w": 105.640625, + "text": "toRange", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "b08", + "id": "shape:NQ57JvxaZgtDxCqrvsVB-", + "typeName": "shape" + }, + { + "x": 3448.554074086629, + "y": 774.4834277782564, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "text", + "props": { + "color": "black", + "size": "m", + "w": 139.078125, + "text": "fromRange", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "b09", + "id": "shape:gZmLskcNZ51K8RSqmNq6N", + "typeName": "shape" + }, + { + "x": 442.4586613526074, + "y": 584.1852181749076, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 464.9205950376922, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "14", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "b0A", + "id": "shape:l53ETguoSEd5vAAEhqVFC", + "typeName": "shape" + }, + { + "x": 515.0415081571743, + "y": 658.2527377148562, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 523.6482266998339, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "15", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "b0B", + "id": "shape:DW-wjdlDyxBwAzCLgeISU", + "typeName": "shape" + }, + { + "x": 1333.2519202547724, + "y": 584.2475121358327, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 464.9205950376922, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "14", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "b0C", + "id": "shape:uqtIqThhennukeWGwDFQ3", + "typeName": "shape" + }, + { + "x": 1405.8347670593394, + "y": 658.3150316757813, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "geo", + "props": { + "w": 523.6482266998339, + "h": 61.6953125, + "geo": "rectangle", + "color": "violet", + "labelColor": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "font": "draw", + "text": "15", + "align": "middle", + "verticalAlign": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:page", + "index": "b0D", + "id": "shape:bT5sc_XKqn6PuZFDg-x2M", + "typeName": "shape" + }, + { + "x": 515.6932320933668, + "y": 850.4239069704852, + "rotation": 0, + "isLocked": false, + "opacity": 1, + "meta": {}, + "type": "text", + "props": { + "color": "violet", + "size": "m", + "w": 413.828125, + "text": "1,3,11 unchnged\n2 move forward fromRamge step\n12 move to mirror position\n4,5,9,11,14 reduce\n6 8 15 expend", + "font": "draw", + "align": "middle", + "autoSize": true, + "scale": 1 + }, + "parentId": "page:page", + "index": "aI", + "id": "shape:yqgyKvkqk2c8BIR2xSo3D", + "typeName": "shape" + } + ] } \ No newline at end of file diff --git a/docs/zh/facade.md b/docs/zh/facade.md deleted file mode 100644 index 3b3a1317b8..0000000000 --- a/docs/zh/facade.md +++ /dev/null @@ -1,108 +0,0 @@ -# Facade - -Facade 的意图是作为 - -1. 用户使用 Univer 的简单接口。如果用户没有自定义开发的需要而只是简单地使用一个表格组件,那么可以直接使用 Facade 提供的接口。 -2. 用户使用 Apps Scripts 的接口。 - -Facade 和 Microsoft Excel 的 Office Scripts 以及 Google 的 Apps Scripts 有着相似的设计思路。它们都是为了简化用户的开发流程,让用户可以直接使用简单的接口来完成复杂的操作。中国国内的部分产品也提供了基于 API 的访问,例如钉钉文档的表格。 - -### Microsoft Excel 的 Office Scripts - -[官方文档链接](https://learn.microsoft.com/en-us/office/dev/scripts/) - -语法上使用了现代 TypeScript 语法,并且支持 async await 等语言特性。 - -```ts -function main(workbook: ExcelScript.Workbook) { - // Add a new worksheet to store our email table - let emailsSheet = workbook.addWorksheet("Emails"); - - // Add data and create a table - emailsSheet.getRange("A1:D1").setValues([ - ["Date", "Day of the week", "Email address", "Subject"] - ]); - let newTable = workbook.addTable(emailsSheet.getRange("A1:D2"), true); - newTable.setName("EmailTable"); - - // Add a new PivotTable to a new worksheet - let pivotWorksheet = workbook.addWorksheet("Subjects"); - let newPivotTable = workbook.addPivotTable("Pivot", "EmailTable", pivotWorksheet.getRange("A3:C20")); - - // Setup the pivot hierarchies - newPivotTable.addRowHierarchy(newPivotTable.getHierarchy("Day of the week")); - newPivotTable.addRowHierarchy(newPivotTable.getHierarchy("Email address")); - newPivotTable.addDataHierarchy(newPivotTable.getHierarchy("Subject")); -} -``` - -另外值得注意的是 Excel 还有另外一套 API 叫做 office-js,这套 API 的语法较为复杂,官方文档声明 office-js 面向开发者而 Office Scripts 面向一般用户。 - -据了解 office-js 是 Office Scripts 的底层,Office Scripts 对需要异步转同步的地方都做了一次类似于添加 await 语法的预编译。 - -### 钉钉表格的 API - -[官方文档链接](https://open.dingtalk.com/document/orgapp/overview-of-dingtalk-scripts) - -钉钉的语法设计和 Office Scripts 非常类似,除了它不用从一个 main 函数开始执行而是直接自顶向下执行脚本。 - -### Google 的 Apps Scripts - -[官方文档](https://developers.google.com/apps-script/reference/spreadsheet?hl=zh-cn) - -Apps Scripts 不仅可以操作表格,还可以操作 Google 的其他产品,例如 Google Docs,Google Drive 等等。它的语法和 JavaScript 非常类似,但是它的语法不支持 async await 等语言特性。实际上 Apps Scripts 的代码是同步阻塞执行的,并且 Apps Scripts 并非运行在浏览器中,而是运行在 Google 的服务器上,猜想应该是 hack 了解释器或者别的什么方式控制了脚本的执行过程。 - -```js -function createAndSendDocument() { - try { - // Create a new Google Doc named 'Hello, world!' - const doc = DocumentApp.create('Hello, world!'); - - // Access the body of the document, then add a paragraph. - doc.getBody().appendParagraph('This document was created by Google Apps Script.'); - - // Get the URL of the document. - const url = doc.getUrl(); - - // Get the email address of the active user - that's you. - const email = Session.getActiveUser().getEmail(); - - // Get the name of the document to use as an email subject line. - const subject = doc.getName(); - - // Append a new string to the "url" variable to use as an email body. - const body = 'Link to your doc: ' + url; - - // Send yourself an email with a link to the document. - GmailApp.sendEmail(email, subject, body); - } catch (err) { - // TODO (developer) - Handle exception - console.log('Failed with error %s', err.message); - } -} -``` - -## 可选的方案以及优缺点分析 - -我们设计 Facade 的 API 时受到以下条件的约束(或者不?) - -1. 为了和 Apps Scripts 的语法保持一致(一定吗?),API 看起来要像是同步执行的 -2. 必然有一些操作行为是异步的,例如读取文件,网络请求等等 - -所以我们需要设计一个机制,用户编写它时,看似是同步执行的,但是实际上是异步执行的。从这种思路出发有以下方案 - -1. 预编译。在用户编写代码之后,我们对代码进行预编译,将异步的操作转换为同步的操作(补充 async await 操作符)。 - 1. 优点是:实现起来可能较为简单 - 2. 缺点是:Facade 作为简单 API 和 Scripts 语法时语法不一致,并且可能无法给 Scripts 用户正确的类型信息 -2. 同步阻塞式调用。在 Facade 内将所有需要用到异步语法 API 的地方全部改为一个同步的 XHRHttpRequest 调用,Facade 本身在 web worker 内运行并向主线程请求操作(通过 service worker 实现)。 - 1. 优点是:可以实现真正的同步 API,而且可以给 Scripts 用户正确的类型信息 - 2. 缺点是:只能跑在 web worker 里,无法作为简单 API 使用;复杂度很高 -3. 自己控制脚本执行过程。暂时还没有明确的方案。 - 1. 优点是:可以实现真正的同步 API,而且可以给 Scripts 用户正确的类型信息 - 2. 缺点是:只能运行在服务器环境,无法作为简单 API 使用;复杂度极高 - -如果我们愿意放弃和 Apps Scripts 语法保持一致的前提,引入 async await 语法,那么我们可以使用以下方案 - -1. 引入 async await 语法。 - 1. 优点是:Facade 作为简单 API 和 Scripts 语法效果完全一致;Scripts 用户可以得到完善的代码编辑提示;实现非常简单 - 2. 需要引导用户使用 async await 语法;生成 Scripts 的 AI 模型需要额外的工作(现在有这样的模型吗?) diff --git a/docs/zh/index.md b/docs/zh/index.md deleted file mode 100644 index 323f0702c1..0000000000 --- a/docs/zh/index.md +++ /dev/null @@ -1,6 +0,0 @@ -# Univer 开发文档 - -## 架构设计 - -- [架构概要](./achitecture.md) -- [Univer Sheet 架构](./sheet-architecture.md) diff --git a/e2e/demo-todo-app.spec.ts b/e2e/demo-todo-app.spec.ts deleted file mode 100644 index 6bc2ad79f5..0000000000 --- a/e2e/demo-todo-app.spec.ts +++ /dev/null @@ -1,452 +0,0 @@ -/** - * Copyright 2023-present DreamNum Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, type Page, test } from '@playwright/test'; - -test.beforeEach(async ({ page }) => { - await page.goto('https://demo.playwright.dev/todomvc'); -}); - -const TODO_ITEMS = [ - 'buy some cheese', - 'feed the cat', - 'book a doctors appointment', -]; - -test.describe('New Todo', () => { - test('should allow me to add todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create 1st todo. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Make sure the list only has one todo item. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0], - ]); - - // Create 2nd todo. - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - - // Make sure the list now has two todo items. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[1], - ]); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); - - test('should clear text input field when an item is added', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create one todo item. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Check that input is empty. - await expect(newTodo).toBeEmpty(); - await checkNumberOfTodosInLocalStorage(page, 1); - }); - - test('should append new items to the bottom of the list', async ({ page }) => { - // Create 3 items. - await createDefaultTodos(page); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count'); - - // Check test using different methods. - await expect(page.getByText('3 items left')).toBeVisible(); - await expect(todoCount).toHaveText('3 items left'); - await expect(todoCount).toContainText('3'); - await expect(todoCount).toHaveText(/3/); - - // Check all items in one call. - await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); - await checkNumberOfTodosInLocalStorage(page, 3); - }); -}); - -test.describe('Mark all as completed', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test.afterEach(async ({ page }) => { - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should allow me to mark all items as completed', async ({ page }) => { - // Complete all todos. - await page.getByLabel('Mark all as complete').check(); - - // Ensure all todos have 'completed' class. - await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - }); - - test('should allow me to clear the complete state of all items', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - // Check and then immediately uncheck. - await toggleAll.check(); - await toggleAll.uncheck(); - - // Should be no completed classes. - await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); - }); - - test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - await toggleAll.check(); - await expect(toggleAll).toBeChecked(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Uncheck first todo. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').uncheck(); - - // Reuse toggleAll locator and make sure its not checked. - await expect(toggleAll).not.toBeChecked(); - - await firstTodo.getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Assert the toggle all is checked again. - await expect(toggleAll).toBeChecked(); - }); -}); - -test.describe('Item', () => { - test('should allow me to mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - // Check first item. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').check(); - await expect(firstTodo).toHaveClass('completed'); - - // Check second item. - const secondTodo = page.getByTestId('todo-item').nth(1); - await expect(secondTodo).not.toHaveClass('completed'); - await secondTodo.getByRole('checkbox').check(); - - // Assert completed class. - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).toHaveClass('completed'); - }); - - test('should allow me to un-mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const firstTodo = page.getByTestId('todo-item').nth(0); - const secondTodo = page.getByTestId('todo-item').nth(1); - const firstTodoCheckbox = firstTodo.getByRole('checkbox'); - - await firstTodoCheckbox.check(); - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await firstTodoCheckbox.uncheck(); - await expect(firstTodo).not.toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 0); - }); - - test('should allow me to edit an item', async ({ page }) => { - await createDefaultTodos(page); - - const todoItems = page.getByTestId('todo-item'); - const secondTodo = todoItems.nth(1); - await secondTodo.dblclick(); - await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); - await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); - - // Explicitly assert the new text value. - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); -}); - -test.describe('Editing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should hide other controls when editing', async ({ page }) => { - const todoItem = page.getByTestId('todo-item').nth(1); - await todoItem.dblclick(); - await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); - await expect(todoItem.locator('label', { - hasText: TODO_ITEMS[1], - })).not.toBeVisible(); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should save edits on blur', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should trim entered text', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should remove the item if an empty text string was entered', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[2], - ]); - }); - - test('should cancel edits on escape', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); - await expect(todoItems).toHaveText(TODO_ITEMS); - }); -}); - -test.describe('Counter', () => { - test('should display the current number of todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count'); - - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - await expect(todoCount).toContainText('1'); - - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - await expect(todoCount).toContainText('2'); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); -}); - -test.describe('Clear completed button', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - }); - - test('should display the correct text', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); - }); - - test('should remove completed items when clicked', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).getByRole('checkbox').check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(todoItems).toHaveCount(2); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should be hidden when there are no items that are completed', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); - }); -}); - -test.describe('Persistence', () => { - test('should persist its data', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const todoItems = page.getByTestId('todo-item'); - const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); - await firstTodoCheck.check(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - - // Ensure there is 1 completed item. - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - // Now reload. - await page.reload(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - }); -}); - -test.describe('Routing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - // make sure the app had a chance to save updated todos in storage - // before navigating to a new view, otherwise the items can get lost :( - // in some frameworks like Durandal - await checkTodosInLocalStorage(page, TODO_ITEMS[0]); - }); - - test('should allow me to display active items', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await expect(todoItem).toHaveCount(2); - await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should respect the back button', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await test.step('Showing all items', async () => { - await page.getByRole('link', { name: 'All' }).click(); - await expect(todoItem).toHaveCount(3); - }); - - await test.step('Showing active items', async () => { - await page.getByRole('link', { name: 'Active' }).click(); - }); - - await test.step('Showing completed items', async () => { - await page.getByRole('link', { name: 'Completed' }).click(); - }); - - await expect(todoItem).toHaveCount(1); - await page.goBack(); - await expect(todoItem).toHaveCount(2); - await page.goBack(); - await expect(todoItem).toHaveCount(3); - }); - - test('should allow me to display completed items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Completed' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(1); - }); - - test('should allow me to display all items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await page.getByRole('link', { name: 'Completed' }).click(); - await page.getByRole('link', { name: 'All' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(3); - }); - - test('should highlight the currently applied filter', async ({ page }) => { - await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); - - //create locators for active and completed links - const activeLink = page.getByRole('link', { name: 'Active' }); - const completedLink = page.getByRole('link', { name: 'Completed' }); - await activeLink.click(); - - // Page change - active items. - await expect(activeLink).toHaveClass('selected'); - await completedLink.click(); - - // Page change - completed items. - await expect(completedLink).toHaveClass('selected'); - }); -}); - -async function createDefaultTodos(page: Page) { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } -} - -async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction((e) => { - return JSON.parse(localStorage['react-todos']).length === e; - }, expected); -} - -async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction((e) => { - return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; - }, expected); -} - -async function checkTodosInLocalStorage(page: Page, title: string) { - return await page.waitForFunction((t) => { - return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); - }, title); -} diff --git a/e2e/memory/memory.spec.ts b/e2e/memory/memory.spec.ts new file mode 100644 index 0000000000..29f53d6317 --- /dev/null +++ b/e2e/memory/memory.spec.ts @@ -0,0 +1,80 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable no-console */ + +import type { Page } from '@playwright/test'; +import { expect, test } from '@playwright/test'; + +// The type definition is copied from: +// examples/src/plugins/debugger/controllers/e2e/e2e-memory.controller.ts +export interface IE2EMemoryControllerAPI { + loadAndRelease(id: number): Promise; + getHeapMemoryUsage(): number; +} +declare global { + // eslint-disable-next-line ts/naming-convention + interface Window { + E2EMemoryAPI: IE2EMemoryControllerAPI; + } +} + +test('memory', async ({ page }) => { + await page.goto('http://localhost:3000/sheets/'); + await page.waitForTimeout(2000); + + const memoryBeforeLoad = (await getMetrics(page)).JSHeapUsedSize; + console.log('Memory before load:', memoryBeforeLoad); + + await page.evaluate(() => window.E2EMemoryAPI.loadAndRelease(1)); + await page.waitForTimeout(5000); // wait for long enough to let the GC do its job + const memoryAfterFirstLoad = (await getMetrics(page)).JSHeapUsedSize; + console.log('Memory after first load:', memoryAfterFirstLoad); + + await page.evaluate(() => window.E2EMemoryAPI.loadAndRelease(2)); + const memoryAfterSecondLoad = (await getMetrics(page)).JSHeapUsedSize; + console.log('Memory after second load:', memoryAfterSecondLoad); + + // max overflow for 3MB + const notLeaking = (memoryAfterSecondLoad <= memoryAfterFirstLoad) + || (memoryAfterSecondLoad - memoryAfterFirstLoad <= 2_500_000); + expect(notLeaking).toBeTruthy(); +}); + +interface IMetrics { + JSHeapUsedSize: number; +} + +/** + * Return a performance metric from the chrome cdp session. + * Note: Chrome-only + * @param {Page} page page to attach cdpClient + * @return {IMetrics} + * @see {@link https://github.com/microsoft/playwright/issues/18071 Github RFE} + */ +async function getMetrics(page: Page): Promise { + const client = await page.context().newCDPSession(page); + await client.send('Performance.enable'); + const perfMetricObject = await client.send('Performance.getMetrics'); + const extractedMetric = perfMetricObject?.metrics; + const metricObject = extractedMetric.reduce((acc, { name, value }) => { + acc[name] = value; + return acc; + }, {}); + + return metricObject as unknown as IMetrics; +} + diff --git a/eslint.config.js b/eslint.config.js index ab38cc93fc..c03b759639 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,5 +1,6 @@ import antfu from '@antfu/eslint-config'; import header from 'eslint-plugin-header'; +import barrel from 'eslint-plugin-no-barrel-import'; import { baseRules, typescriptPreset } from '@univerjs/shared/eslint'; export default antfu({ @@ -7,6 +8,7 @@ export default antfu({ indent: 4, semi: true, }, + regexp: false, react: true, yaml: { overrides: { @@ -20,13 +22,32 @@ export default antfu({ html: true, }, rules: baseRules, +}, { + files: ['**/*.ts', '**/*.tsx'], + ignores: [ + 'packages/engine-render/src/components/docs/**/*.ts', + '**/*.tsx', + '**/*.d.ts', + '**/vite.config.ts', + 'playwright.config.ts', + '**/*.spec.ts', + '**/*.spec.tsx', + '**/*.test.ts', + '**/*.test.tsx', + ], // do not check test files + rules: { + complexity: ['warn', { max: 20 }], + 'max-lines-per-function': ['warn', 80], + }, }, { files: ['**/*.ts', '**/*.tsx'], ignores: ['**/*.d.ts', '**/vite.config.ts', 'playwright.config.ts'], plugins: { header, + barrel, }, rules: { + 'barrel/no-barrel-import': 2, 'header/header': [ 2, 'block', diff --git a/examples/esbuild.config.mjs b/examples/esbuild.config.mjs index 31bb5a7c00..d666324785 100644 --- a/examples/esbuild.config.mjs +++ b/examples/esbuild.config.mjs @@ -96,6 +96,7 @@ const ctx = await esbuild[args.watch ? 'context' : 'build']({ 'process.env.GIT_COMMIT_HASH': `"${gitCommitHash}"`, 'process.env.GIT_REF_NAME': `"${gitRefName}"`, 'process.env.BUILD_TIME': `"${new Date().toISOString()}"`, + 'process.env.IS_E2E': args.e2e ? 'true' : 'false', }, }); diff --git a/examples/package.json b/examples/package.json index de223cf933..6f1d5cf0ac 100644 --- a/examples/package.json +++ b/examples/package.json @@ -7,10 +7,14 @@ "scripts": { "build:demo": "node ./esbuild.config.mjs", "dev:demo": "node ./esbuild.config.mjs --watch", + "dev:e2e": "node ./esbuild.config.mjs --watch --e2e", + "build:e2e": "node ./esbuild.config.mjs --e2e", "lint:types": "tsc --noEmit" }, "dependencies": { "@univerjs/core": "workspace:*", + "@univerjs/data-validation": "workspace:*", + "@univerjs/debugger": "workspace:*", "@univerjs/design": "workspace:*", "@univerjs/docs": "workspace:*", "@univerjs/docs-ui": "workspace:*", @@ -18,38 +22,46 @@ "@univerjs/engine-render": "workspace:*", "@univerjs/facade": "workspace:*", "@univerjs/find-replace": "workspace:*", - "@univerjs/icons": "^0.1.42", + "@univerjs/icons": "^0.1.52", + "@univerjs/image": "workspace:*", "@univerjs/rpc": "workspace:*", "@univerjs/sheets": "workspace:*", + "@univerjs/sheets-conditional-formatting": "workspace:*", + "@univerjs/sheets-conditional-formatting-ui": "workspace:*", + "@univerjs/sheets-data-validation": "workspace:*", + "@univerjs/sheets-filter": "workspace:*", + "@univerjs/sheets-filter-ui": "workspace:*", "@univerjs/sheets-find-replace": "workspace:*", "@univerjs/sheets-formula": "workspace:*", "@univerjs/sheets-numfmt": "workspace:*", + "@univerjs/sheets-thread-comment": "workspace:*", "@univerjs/sheets-ui": "workspace:*", "@univerjs/sheets-zen-editor": "workspace:*", "@univerjs/slides": "workspace:*", "@univerjs/slides-ui": "workspace:*", + "@univerjs/thread-comment": "workspace:*", + "@univerjs/thread-comment-ui": "workspace:*", "@univerjs/ui": "workspace:*", "@univerjs/uniscript": "workspace:*", - "@wendellhu/redi": "^0.13.0", - "clsx": "^2.1.0", - "monaco-editor": "0.47.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "@wendellhu/redi": "0.15.2", + "clsx": "^2.1.1", + "monaco-editor": "0.48.0", + "react": "18.2.0", + "react-dom": "18.2.0", "react-mosaic-component": "^6.1.0", - "rxjs": "^7.8.1", - "vue": "^3.4.21" + "rxjs": "^7.8.1" }, "devDependencies": { - "@types/react": "^18.2.72", - "@types/react-dom": "^18.2.22", + "@types/react": "^18.2.79", + "@types/react-dom": "^18.2.25", "@univerjs/shared": "workspace:*", - "esbuild": "^0.20.2", + "esbuild": "^0.21.3", "esbuild-plugin-clean": "^1.0.1", "esbuild-plugin-copy": "^2.1.1", "esbuild-plugin-vue3": "^0.4.2", "esbuild-style-plugin": "^1.6.3", "less": "^4.2.0", "minimist": "^1.2.8", - "typescript": "^5.4.3" + "typescript": "^5.4.5" } } diff --git a/examples/src/data/docs/default-document-data-cn.ts b/examples/src/data/docs/default-document-data-cn.ts index 62317fe896..91fa570ce0 100644 --- a/examples/src/data/docs/default-document-data-cn.ts +++ b/examples/src/data/docs/default-document-data-cn.ts @@ -15,14 +15,135 @@ */ import type { IDocumentData } from '@univerjs/core'; -import { BooleanNumber, HorizontalAlign } from '@univerjs/core'; +import { BooleanNumber, ColumnSeparatorType, ObjectRelativeFromH, ObjectRelativeFromV, PositionedObjectLayoutType, SectionType, WrapTextType } from '@univerjs/core'; import { ptToPixel } from '@univerjs/engine-render'; export const DEFAULT_DOCUMENT_DATA_CN: IDocumentData = { - id: 'default-document-id', + id: 'd', + drawings: { + shapeTest1: { + objectId: 'shapeTest1', + title: 'test shape', + description: 'test shape', + objectTransform: { + size: { + width: 1484 * 0.12, + height: 864 * 0.15, + }, + positionH: { + relativeFrom: ObjectRelativeFromH.MARGIN, + posOffset: 100, + }, + positionV: { + relativeFrom: ObjectRelativeFromV.PAGE, + posOffset: 230, + }, + angle: 0, + }, + layoutType: PositionedObjectLayoutType.WRAP_NONE, + behindDoc: BooleanNumber.TRUE, + wrapText: WrapTextType.BOTH_SIDES, + distT: 0, + distB: 0, + distL: 0, + distR: 0, + }, + shapeTest2: { + objectId: 'shapeTest2', + title: 'test shape', + description: 'test shape', + objectTransform: { + size: { + width: 1484 * 0.3, + height: 864 * 0.3, + }, + positionH: { + relativeFrom: ObjectRelativeFromH.PAGE, + posOffset: 100, + }, + positionV: { + relativeFrom: ObjectRelativeFromV.PARAGRAPH, + posOffset: 20, + }, + angle: 0, + }, + layoutType: PositionedObjectLayoutType.WRAP_NONE, + behindDoc: BooleanNumber.FALSE, + wrapText: WrapTextType.BOTH_SIDES, + }, + shapeTest3: { + objectId: 'shapeTest3', + title: 'test shape', + description: 'test shape', + objectTransform: { + size: { + width: 1484 * 0.3, + height: 864 * 0.3, + }, + positionH: { + relativeFrom: ObjectRelativeFromH.PAGE, + posOffset: 100, + }, + positionV: { + relativeFrom: ObjectRelativeFromV.PARAGRAPH, + posOffset: 200, + }, + angle: 0, + }, + layoutType: PositionedObjectLayoutType.INLINE, + behindDoc: BooleanNumber.FALSE, + wrapText: WrapTextType.BOTH_SIDES, + }, + shapeTest4: { + objectId: 'shapeTest4', + title: 'test shape', + description: 'test shape', + objectTransform: { + size: { + width: 1484 * 0.3, + height: 864 * 0.3, + }, + positionH: { + relativeFrom: ObjectRelativeFromH.PAGE, + posOffset: 100, + }, + positionV: { + relativeFrom: ObjectRelativeFromV.LINE, + posOffset: 200, + }, + angle: 0, + }, + layoutType: PositionedObjectLayoutType.INLINE, + behindDoc: BooleanNumber.FALSE, + wrapText: WrapTextType.BOTH_SIDES, + }, + shapeTest5: { + objectId: 'shapeTest5', + title: 'test shape', + description: 'test shape', + objectTransform: { + size: { + width: 1484 * 0.3, + height: 864 * 0.3, + }, + positionH: { + relativeFrom: ObjectRelativeFromH.PAGE, + posOffset: 100, + }, + positionV: { + relativeFrom: ObjectRelativeFromV.PAGE, + posOffset: 200, + }, + angle: 0, + }, + layoutType: PositionedObjectLayoutType.INLINE, + behindDoc: BooleanNumber.FALSE, + wrapText: WrapTextType.BOTH_SIDES, + }, + }, body: { dataStream: - '荷塘月色\r\r作者:朱自清\r\r这几天心里颇不宁静。今晚在院子里坐着乘凉,忽然想起日日走过的荷塘,在这满月的光里,总该另有一番样子吧。月亮渐渐地升高了,墙外马路上孩子们的欢笑,已经听不见了;妻在屋里拍着闰儿,迷迷糊糊地哼着眠歌。我悄悄地披了大衫,带上门出去。\r\r沿着荷塘,是一条曲折的小煤屑路。这是一条幽僻的路;白天也少人走,夜晚更加寂寞。荷塘四面,长着许多树,蓊蓊郁郁的。路的一旁,是些杨柳,和一些不知道名字的树。没有月光的晚上,这路上阴森森的,有些怕人。今晚却很好,虽然月光也还是淡淡的。\r\r路上只我一个人,背着手踱着。这一片天地好像是我的;我也像超出了平常的自己,到了另一个世界里。我爱热闹,也爱冷静;爱群居,也爱独处。像今晚上,一个人在这苍茫的月下,什么都可以想,什么都可以不想,便觉是个自由的人。白天里一定要做的事,一定要说的话,现在都可不理。这是独处的妙处,我且受用这无边的荷香月色好了。\r\r曲曲折折的荷塘上面,弥望的是田田的叶子。叶子出水很高,像亭亭的舞女的裙。层层的叶子中间,零星地点缀着些白花,有袅娜地开着的,有羞涩地打着朵儿的;正如一粒粒的明珠,又如碧天里的星星,又如刚出浴的美人。微风过处,送来缕缕清香,仿佛远处高楼上渺茫的歌声似的。这时候叶子与花也有一丝的颤动,像闪电般,霎时传过荷塘的那边去了。叶子本是肩并肩密密地挨着,这便宛然有了一道凝碧的波痕。叶子底下是脉脉的流水,遮住了,不能见一些颜色;而叶子却更见风致了。\r\r月光如流水一般,静静地泻在这一片叶子和花上。薄薄的青雾浮起在荷塘里。叶子和花仿佛在牛乳中洗过一样;又像笼着轻纱的梦。虽然是满月,天上却有一层淡淡的云,所以不能朗照;但我以为这恰是到了好处——酣眠固不可少,小睡也别有风味的。月光是隔了树照过来的,高处丛生的灌木,落下参差的斑驳的黑影,峭楞楞如鬼一般;弯弯的杨柳的稀疏的倩影,却又像是画在荷叶上。塘中的月色并不均匀;但光与影有着和谐的旋律,如梵婀玲上奏着的名曲。\r\r荷塘的四面,远远近近,高高低低都是树,而杨柳最多。这些树将一片荷塘重重围住;只在小路一旁,漏着几段空隙,像是特为月光留下的。树色一例是阴阴的,乍看像一团烟雾;但杨柳的丰姿,便在烟雾里也辨得出。树梢上隐隐约约的是一带远山,只有些大意罢了。树缝里也漏着一两点路灯光,没精打采的,是渴睡人的眼。这时候最热闹的,要数树上的蝉声与水里的蛙声;但热闹是它们的,我什么也没有。\r\r忽然想起采莲的事情来了。采莲是江南的旧俗,似乎很早就有,而六朝时为盛;从诗歌里可以约略知道。采莲的是少年的女子,她们是荡着小船,唱着艳歌去的。采莲人不用说很多,还有看采莲的人。那是一个热闹的季节,也是一个风流的季节。梁元帝《采莲赋》里说得好:\r\r于是妖童女,荡舟心许;鷁首徐回,兼传羽杯;櫂将移而藻挂,船欲动而萍开。尔其纤腰束素,迁延顾步;夏始春余,叶嫩花初,恐沾裳而浅笑,畏倾船而敛裾。\r\r可见当时嬉游的光景了。这真是有趣的事,可惜我们现在早已无福消受了。\r\r于是又记起,《西洲曲》里的句子:\r\r采莲南塘秋,莲花过人头;低头弄莲子,莲子清如水。\r\r今晚若有采莲人,这儿的莲花也算得“过人头”了;只不见一些流水的影子,是不行的。这令我到底惦着江南了。——这样想着,猛一抬头,不觉已是自己的门前;轻轻地推门进去,什么声息也没有,妻已睡熟好久了。\r\r一九二七年七月,北京清华园。\r\r\r\r《荷塘月色》语言朴素典雅,准确生动,贮满诗意,满溢着朱自清的散文语言一贯有朴素的美,不用浓墨重彩,画的是淡墨水彩。\r\r朱自清先生一笔写景一笔说情,看起来松散不知所云,可仔细体会下,就能感受到先生在字里行间表述出的苦闷,而随之读者也被先生的文字所感染,被带进了他当时那苦闷而无法明喻的心情。这就是优异散文的必须品质之一。\r\r扩展资料:\r一首长诗《毁灭》奠定了朱自清在文坛新诗人的地位,而《桨声灯影里的秦淮河》则被公认为白话美文的典范。朱自清用白话美文向复古派宣战,有力地回击了复古派“白话不能作美文”之说,他是“五四”新文学运动的开拓者之一。\r\r朱自清的美文影响了一代又一代人。作家贾平凹说:来到扬州,第一个想到的人是朱自清,他是知识分子中最最了不起的人物。\r\r实际上,朱自清的写作路程是非常曲折的,他早期的时候大多数作品都是诗歌,但是他的诗歌和我国古代诗人的诗有很大区别,他的诗是用白话文写的,这其实也是他写作的惯用风格。\r\r后来,朱自清开始写一些关于社会的文章,因为那个时候社会比较混乱,这时候的作品大多抨击社会的黑暗面,文体风格大多硬朗,基调伉俪。到了后期,大多是写关于山水的文章,这类文章的写作格调大多以清丽雅致为主。\r\r朱自清的写作风格虽然在不同的时期随着他的人生阅历和社会形态的不同而发生着变化,但是他文章的主基调是没有变的,他这一生,所写的所有文章风格上都有一个非常显著的特点,那就是简约平淡,他不是类似古代花间词派的诗人们,不管是他的诗词还是他的文章从来都不用过于华丽的辞藻,他崇尚的是平淡。\r\r英国友人戴立克试过英译朱自清几篇散文,译完一读显得单薄,远远不如原文流利。他不服气,改用稍微古奥的英文重译,好多了:“那是说,朱先生外圆内方,文字尽管浅白,心思却很深沉,译笔只好朝深处经营。”朱自清的很多文章,譬如《背影》《祭亡妇》,读来自有一番只可意会不可言传的东西。\r\r平淡就是朱自清的写作风格。他不是豪放派的作家,他在创作的时候钟情于清新的风格,给人耳目一新的感觉。在他的文章中包含了他对生活的向往,由此可见他的写作风格和他待人处事的态度也是有几分相似的。他的文章非常优美,但又不会让人觉得狭隘,给人一种豁达渊博的感觉,这就是朱自清的写作风格,更是朱自清的为人品质。\r\r写有《荷塘月色》《背影》等名篇的著名散文家朱自清先生,不仅自己一生风骨正气,还用无形的家风涵养子孙。良好的家风家规意蕴深远,催人向善,是凝聚情感、涵养德行、砥砺成才的人生信条。“北有朱自清,南有朱物华,一文一武,一南一北,双星闪耀”,这是中国知识界、教育界对朱家两兄弟的赞誉。\r\r朱自清性格温和,为人和善,对待年轻人平易近人,是个平和的人。他取字“佩弦”,意思要像弓弦那样将自己绷紧,给人的感觉是自我要求高,偶尔有呆气。朱自清教学负责,对学生要求严格,修他的课的学生都受益不少。\r\r1948 年 6 月,患胃病多年的朱自清,在《抗议美国扶日政策并拒绝领取美援面粉宣言》上,一丝不苟地签下了自己的名字。随后,朱自清还将面粉配购证以及面粉票退了回去。1948 年 8 月 12 日,朱自清因不堪胃病折磨,离开人世。在新的时代即将到来时,朱自清却匆匆地离人们远去。他为人们留下了无数经典的诗歌和文字,还有永不屈服的精神。\r\r朱自清没有豪言壮语,他只是用坚定的行动、朴实的语言,向世人展示了中国知识分子在祖国危难之际坚定的革命性,体现了中国人的骨气,表现了无比高贵的民族气节,呈现了人生最有价值的一面,谱就了生命中最华丽的乐章。\r\r他以“自清”为名,自勉在困境中不丧志;他身患重病,至死拒领美援面粉,其气节令世人感佩;他的《背影》《荷塘月色》《匆匆》脍炙人口;他的文字追求“真”,没有半点矫饰,却蕴藏着动人心弦的力量。\r\r朱自清不但在文学创作方面有很高的造诣,也是一名革命民主主义战士,在反饥饿、反内战的斗争中,他始终保持着一个正直的爱国知识分子的气节和情操。毛泽东对朱自清宁肯饿死不领美国“救济粉”的精神给予称赞,赞扬他“表现了我们民族的英雄气概”。\r\n', + '荷塘月色\r\r作者:朱自清\r\r这几天心里颇不宁静。今晚在院子里坐着乘凉,忽然想起日日走过的荷塘,在这满月的光里,总该另有一番样子吧。月亮渐渐地升高了,墙外马路上孩子们的欢笑,已经听不见了;妻在屋里拍着闰儿,迷迷糊糊地哼着眠歌。我悄悄地披了大衫,带上门出去。\r\r沿着荷塘,是一条曲折的小煤屑路。这是一条幽僻的路;白天也少人走,夜晚更加寂寞。荷塘四面,长着许多树,蓊蓊郁郁的。路图片一\b是些杨柳,和一些不知道名字的树。没有月光的晚上,这路上阴森森的,有些怕人。今晚却很好,虽然月光也还是淡淡的。\r\r路上只我一个人,背着手踱着。这一片天地好像是我的;我也像超出了平常的自己,到了另一个世界里。我爱热闹,也爱冷静;爱群居,也爱独处。像今晚上,一个人在这苍茫的月下,什么都可以想,什么都可以不想,便觉是个自由的人。白天里一定要做的事,一定要说的话\b现在都可不理。这是独处的妙处,我且受用这无边的荷香月色好了。\r\r曲曲折折的荷塘上面,弥望的是田田的叶子。叶子出水很高,像亭亭的舞女的裙。层层的叶子中间,零星地点缀着些白花,有袅娜地开着的,有羞涩地打着朵儿的;正如一粒粒的明珠,又如碧天里的星星\b又如刚出浴的美人。微风过处,送来缕缕清香,仿佛远处高楼上渺茫的歌声似的。这时候叶子与花也有一丝的颤动,像闪电般,霎时传过荷塘的那边去了。叶子本是肩并肩密密地挨着,这便宛然有了一道凝碧的波痕。叶子底下是脉脉的流水,遮住了,不能见一些颜色;而叶子却更见风致了。\r\r月光如流水一般,静静地泻在这一片叶子和花上。薄薄的青雾浮起在荷塘里。叶子和花仿佛在牛乳中洗过一样\b又像笼着轻纱的梦。虽然是满月,天上却有一层淡淡的云,所以不能朗照;但我以为这恰是到了好处——酣眠固不可少,小睡也别有风味的。月光是隔了树照过来的,高处丛生的灌木,落下参差的斑驳的黑影,峭楞楞如鬼一般;弯弯的杨柳的稀疏的倩影,却又像是画在荷叶上。塘中的月色并不均匀;但光与影有着和谐的旋律,如梵婀玲上奏着的名曲。\r\r荷塘的四面,远远近近,高高低低都是树,而杨柳最多。这些树将一片荷塘重重围住;只在小路一旁,漏着几段空隙,像是特为月光留下的。树色一例是阴阴的,乍看像一团烟雾;但杨柳的丰姿,便在烟雾里也辨得出。树梢上隐隐约约的是一带远山,只有些大意罢了。树缝里也漏着一两点路灯光,没精打采的\b是渴睡人的眼。这时候最热闹的,要数树上的蝉声与水里的蛙声;但热闹是它们的,我什么也没有。\r\r忽然想起采莲的事情来了。采莲是江南的旧俗,似乎很早就有,而六朝时为盛;从诗歌里可以约略知道。采莲的是少年的女子,她们是荡着小船,唱着艳歌去的。采莲人不用说很多,还有看采莲的人。那是一个热闹的季节,也是一个风流的季节。梁元帝《采莲赋》里说得好:\r\r于是妖童女,荡舟心许;鷁首徐回,兼传羽杯;櫂将移而藻挂,船欲动而萍开。尔其纤腰束素,迁延顾步;夏始春余,叶嫩花初,恐沾裳而浅笑,畏倾船而敛裾。\r\r可见当时嬉游的光景了。这真是有趣的事,可惜我们现在早已无福消受了。\r\r于是又记起,《西洲曲》里的句子:\r\r采莲南塘秋,莲花过人头;低头弄莲子,莲子清如水。\r\r今晚若有采莲人,这儿的莲花也算得“过人头”了;只不见一些流水的影子,是不行的。这令我到底惦着江南了。——这样想着,猛一抬头,不觉已是自己的门前;轻轻地推门进去,什么声息也没有,妻已睡熟好久了。\r\r一九二七年七月,北京清华园。\r\r\r\r《荷塘月色》语言朴素典雅,准确生动,贮满诗意,满溢着朱自清的散文语言一贯有朴素的美,不用浓墨重彩,画的是淡墨水彩。\r\r朱自清先生一笔写景一笔说情,看起来松散不知所云,可仔细体会下,就能感受到先生在字里行间表述出的苦闷,而随之读者也被先生的文字所感染,被带进了他当时那苦闷而无法明喻的心情。这就是优异散文的必须品质之一。\r\r扩展资料:\r一首长诗《毁灭》奠定了朱自清在文坛新诗人的地位,而《桨声灯影里的秦淮河》则被公认为白话美文的典范。朱自清用白话美文向复古派宣战,有力地回击了复古派“白话不能作美文”之说,他是“五四”新文学运动的开拓者之一。\r\r朱自清的美文影响了一代又一代人。作家贾平凹说:来到扬州,第一个想到的人是朱自清,他是知识分子中最最了不起的人物。\r\r实际上,朱自清的写作路程是非常曲折的,他早期的时候大多数作品都是诗歌,但是他的诗歌和我国古代诗人的诗有很大区别,他的诗是用白话文写的,这其实也是他写作的惯用风格。\r\r后来,朱自清开始写一些关于社会的文章,因为那个时候社会比较混乱,这时候的作品大多抨击社会的黑暗面,文体风格大多硬朗,基调伉俪。到了后期,大多是写关于山水的文章,这类文章的写作格调大多以清丽雅致为主。\r\r朱自清的写作风格虽然在不同的时期随着他的人生阅历和社会形态的不同而发生着变化,但是他文章的主基调是没有变的,他这一生,所写的所有文章风格上都有一个非常显著的特点,那就是简约平淡,他不是类似古代花间词派的诗人们,不管是他的诗词还是他的文章从来都不用过于华丽的辞藻,他崇尚的是平淡。\r\r英国友人戴立克试过英译朱自清几篇散文,译完一读显得单薄,远远不如原文流利。他不服气,改用稍微古奥的英文重译,好多了:“那是说,朱先生外圆内方,文字尽管浅白,心思却很深沉,译笔只好朝深处经营。”朱自清的很多文章,譬如《背影》《祭亡妇》,读来自有一番只可意会不可言传的东西。\r\r平淡就是朱自清的写作风格。他不是豪放派的作家,他在创作的时候钟情于清新的风格,给人耳目一新的感觉。在他的文章中包含了他对生活的向往,由此可见他的写作风格和他待人处事的态度也是有几分相似的。他的文章非常优美,但又不会让人觉得狭隘,给人一种豁达渊博的感觉,这就是朱自清的写作风格,更是朱自清的为人品质。\r\r写有《荷塘月色》《背影》等名篇的著名散文家朱自清先生,不仅自己一生风骨正气,还用无形的家风涵养子孙。良好的家风家规意蕴深远,催人向善,是凝聚情感、涵养德行、砥砺成才的人生信条。“北有朱自清,南有朱物华,一文一武,一南一北,双星闪耀”,这是中国知识界、教育界对朱家两兄弟的赞誉。\r\r朱自清性格温和,为人和善,对待年轻人平易近人,是个平和的人。他取字“佩弦”,意思要像弓弦那样将自己绷紧,给人的感觉是自我要求高,偶尔有呆气。朱自清教学负责,对学生要求严格,修他的课的学生都受益不少。\r\r1948 年 6 月,患胃病多年的朱自清,在《抗议美国扶日政策并拒绝领取美援面粉宣言》上,一丝不苟地签下了自己的名字。随后,朱自清还将面粉配购证以及面粉票退了回去。1948 年 8 月 12 日,朱自清因不堪胃病折磨,离开人世。在新的时代即将到来时,朱自清却匆匆地离人们远去。他为人们留下了无数经典的诗歌和文字,还有永不屈服的精神。\r\r朱自清没有豪言壮语,他只是用坚定的行动、朴实的语言,向世人展示了中国知识分子在祖国危难之际坚定的革命性,体现了中国人的骨气,表现了无比高贵的民族气节,呈现了人生最有价值的一面,谱就了生命中最华丽的乐章。\r\r他以“自清”为名,自勉在困境中不丧志;他身患重病,至死拒领美援面粉,其气节令世人感佩;他的《背影》《荷塘月色》《匆匆》脍炙人口;他的文字追求“真”,没有半点矫饰,却蕴藏着动人心弦的力量。\r\r朱自清不但在文学创作方面有很高的造诣,也是一名革命民主主义战士,在反饥饿、反内战的斗争中,他始终保持着一个正直的爱国知识分子的气节和情操。毛泽东对朱自清宁肯饿死不领美国“救济粉”的精神给予称赞,赞扬他“表现了我们民族的英雄气概”。\r\n', textRuns: [ { st: 0, @@ -37,6 +158,7 @@ export const DEFAULT_DOCUMENT_DATA_CN: IDocumentData = { bg: { rgb: '#FF6670', }, + it: BooleanNumber.TRUE, }, }, { @@ -65,7 +187,7 @@ export const DEFAULT_DOCUMENT_DATA_CN: IDocumentData = { }, { st: 14, - ed: 3057, + ed: 3064, ts: { fs: 12, ff: 'Microsoft YaHei', @@ -80,7 +202,7 @@ export const DEFAULT_DOCUMENT_DATA_CN: IDocumentData = { { startIndex: 4, paragraphStyle: { - spaceAbove: 10, + spaceAbove: 0, lineSpacing: 2, spaceBelow: 0, }, @@ -115,7 +237,10 @@ export const DEFAULT_DOCUMENT_DATA_CN: IDocumentData = { spaceAbove: 10, lineSpacing: 2, spaceBelow: 0, - horizontalAlign: HorizontalAlign.JUSTIFIED, + // hanging: 20, + // indentStart: 50, + // indentEnd: 50, + // indentFirstLine: 50, }, }, { @@ -577,19 +702,41 @@ export const DEFAULT_DOCUMENT_DATA_CN: IDocumentData = { ], sectionBreaks: [ { - startIndex: 3058, + startIndex: 3066, // columnProperties: [ // { - // width: 250, - // paddingEnd: 15, + // width: ptToPixel(240), + // paddingEnd: ptToPixel(15), // }, // ], - // columnSeparatorType: ColumnSeparatorType.NONE, - // sectionType: SectionType.SECTION_TYPE_UNSPECIFIED, + columnSeparatorType: ColumnSeparatorType.NONE, + sectionType: SectionType.SECTION_TYPE_UNSPECIFIED, // textDirection: textDirectionDocument, // contentDirection: textDirection!, }, ], + customBlocks: [ + // { + // startIndex: 189, + // blockId: 'shapeTest1', + // }, + // { + // startIndex: 367, + // blockId: 'shapeTest2', + // }, + // { + // startIndex: 489, + // blockId: 'shapeTest3', + // }, + // { + // startIndex: 668, + // blockId: 'shapeTest4', + // }, + // { + // startIndex: 962, + // blockId: 'shapeTest5', + // }, + ], }, documentStyle: { pageSize: { @@ -598,8 +745,8 @@ export const DEFAULT_DOCUMENT_DATA_CN: IDocumentData = { }, marginTop: ptToPixel(50), marginBottom: ptToPixel(50), - marginRight: ptToPixel(40), - marginLeft: ptToPixel(40), + marginRight: ptToPixel(50), + marginLeft: ptToPixel(50), renderConfig: { vertexAngle: 0, centerAngle: 0, diff --git a/examples/src/data/docs/default-document-data-dreamer.ts b/examples/src/data/docs/default-document-data-dreamer.ts new file mode 100644 index 0000000000..dfd86ed969 --- /dev/null +++ b/examples/src/data/docs/default-document-data-dreamer.ts @@ -0,0 +1,280 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IDocumentData } from '@univerjs/core'; +import { + BooleanNumber, + ColumnSeparatorType, + ObjectRelativeFromH, + ObjectRelativeFromV, + PositionedObjectLayoutType, + SectionType, + WrapTextType, +} from '@univerjs/core'; +import { ptToPixel } from '@univerjs/engine-render'; + +export const DEFAULT_DOCUMENT_DATA_DREAMER: IDocumentData = { + id: 'd', + drawings: { + shapeTest1: { + objectId: 'shapeTest1', + title: 'test shape', + description: 'test shape', + objectTransform: { + size: { + width: 1484 * 0.15, + height: 864 * 0.15, + }, + positionH: { + relativeFrom: ObjectRelativeFromH.COLUMN, + posOffset: 130, + }, + positionV: { + relativeFrom: ObjectRelativeFromV.PAGE, + posOffset: 510, + }, + angle: 0, + }, + layoutType: PositionedObjectLayoutType.WRAP_SQUARE, + behindDoc: BooleanNumber.FALSE, + wrapText: WrapTextType.BOTH_SIDES, + }, + shapeTest2: { + objectId: 'shapeTest2', + title: 'test shape', + description: 'test shape', + objectTransform: { + size: { + width: 2548 * 0.1, + height: 2343 * 0.1, + }, + positionH: { + relativeFrom: ObjectRelativeFromH.COLUMN, + posOffset: 130, + }, + positionV: { + relativeFrom: ObjectRelativeFromV.PAGE, + posOffset: 200, + }, + angle: 0, + }, + layoutType: PositionedObjectLayoutType.WRAP_SQUARE, + behindDoc: BooleanNumber.FALSE, + wrapText: WrapTextType.BOTH_SIDES, + }, + }, + body: { + dataStream: + 'The Dreamer\r\rGo with your passion.It has been said that who see the invisible can do the impossible.\r\rWhen I was nine years old living in a smaIl town in North Carolina I found an ad for selling greeting cards in the back of a children\'s magazine.I thought to myself I can do this.I begged my mother to let me send for the kit.Two weeks later when the kit arrived,I ripped off the brown paper wrapper,grabbed the cards and dashed from the house.Three hours later.I returned home with no card and a pocket full of money proclaiming,“Mama.all the people couldn\'t wait to buy my cards!”A salesperson was born.when I was twelve years old,my father took me to see Zig Ziegler.I remember sitting in that dark auditorium listening to Mr,Zigler raise everyone\'s spirits up to the ceiling,I left there feeling like I could do anything.When we got to the car,I turned to my father and said.“Dad.I want to make people feel like that.”My father asked me what I meant."I want to be a motivational speaker just like Mr.Zigler,“I replied.A dream was born.\r\rRecently,I began pursuing my dream of motivating others.After a four-year relationship with a major furtune 100company beginning as a salestrainer and ending as a regional sales manager,I left the company at the height of my career,Many people were astounded that I would leave after earning a six-figure income.And they asked why I would risk everything for a dream.\r\rI made my decision to start my own company and leave my secure position after attending a regional sales meeting.The vice-president of our company delivered a speech that changed my life.He asked us,“If a genie would grant you three wishes what would they be?”After giving us a moment to write down the three wishes.he then asked us,"why do you need a genie ?"I would never forget the empowerment I felt at that moment.\r\rI realized that everything I had accomplished一the graduate degree,the successful sales career,speaking engagements,training and managing for a fortune l00company had prepared me for this moment.I was ready and did not need a genie\'s help to become a motivational speaker.\r\rWhen I tearfully told my boss my plans this incredible leader whom Irespect so much replied,"Precede with reckless abandon and you will be successful“\r\rHaving made that decision,I was immediately tested.One week after I gave notice,my husband was“laid off from his job.We had recently bought a new home and needed both incomes to make the monthly mortgage payment and now we were done to no income.It was tempting to turn back to my former company,knowing they wanted me to stays but l was certain that if I went back,I would never leave.I decided I still wanted to move forward rather than end up with a mouth full of”if onlys"later on.A motivational speaker was born.\r\rWhen l held fast to my dream,even during the tough times.The miracles really began to happen.In a short time period my husband found a better job.We didn\'t miss a mortgage payment.And I was able to book several speaking engagements with new clients.I discovered the incredible power of dreams.I loved my old job,my peers and the company I left,but it was time to get on with my dream.To celebrate my success I had a local artist paint my new offlce as a garden.At the top of one wall she stenciled,“The world always makes way for the dreamer.\r\n”', + textRuns: [ + { + st: 0, + ed: 11, + ts: { + bl: BooleanNumber.TRUE, + fs: 24 * 0.75, + cl: { + rgb: 'rgb(0, 40, 86)', + }, + }, + }, + { + st: 13, + ed: 3318, + ts: { + fs: 14 * 0.75, + ff: 'Times New Roman', + cl: { + rgb: 'rgb(0, 0, 0)', + }, + }, + }, + ], + paragraphs: [ + { + startIndex: 11, + paragraphStyle: { + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, + }, + }, + { + startIndex: 12, + paragraphStyle: { + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, + }, + }, + { + startIndex: 100, + paragraphStyle: { + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, + }, + }, + { + startIndex: 101, + paragraphStyle: { + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, + }, + }, + { + startIndex: 1040, + paragraphStyle: { + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, + }, + }, + { + startIndex: 1041, + paragraphStyle: { + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, + }, + }, + { + startIndex: 1409, + paragraphStyle: { + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, + }, + }, + { + startIndex: 1410, + paragraphStyle: { + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, + }, + }, + { + startIndex: 1830, + paragraphStyle: { + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, + }, + }, + { + startIndex: 1831, + paragraphStyle: { + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, + }, + }, + { + startIndex: 2103, + paragraphStyle: { + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, + }, + }, + { + startIndex: 2104, + paragraphStyle: { + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, + }, + }, + { + startIndex: 2255, + paragraphStyle: { + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, + }, + }, + { + startIndex: 2256, + paragraphStyle: { + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, + }, + }, + { + startIndex: 2774, + paragraphStyle: { + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, + }, + }, + { + startIndex: 2775, + paragraphStyle: { + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, + }, + }, + { + startIndex: 3318, + paragraphStyle: { + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, + }, + }, + ], + sectionBreaks: [ + { + startIndex: 3319, + columnProperties: [ + { + width: ptToPixel(240), + paddingEnd: ptToPixel(15), + }, + ], + columnSeparatorType: ColumnSeparatorType.NONE, + sectionType: SectionType.SECTION_TYPE_UNSPECIFIED, + // textDirection: textDirectionDocument, + // contentDirection: textDirection!, + }, + ], + customBlocks: [ + { + startIndex: 1243, + blockId: 'shapeTest1', + }, + ], + }, + documentStyle: { + pageSize: { + width: ptToPixel(595), + height: ptToPixel(842), + }, + marginTop: ptToPixel(50), + marginBottom: ptToPixel(50), + marginRight: ptToPixel(50), + marginLeft: ptToPixel(50), + renderConfig: { + vertexAngle: 0, + centerAngle: 0, + }, + }, +}; diff --git a/examples/src/data/docs/default-document-data-en.ts b/examples/src/data/docs/default-document-data-en.ts index 1ba4cce6c2..1190a7e93f 100644 --- a/examples/src/data/docs/default-document-data-en.ts +++ b/examples/src/data/docs/default-document-data-en.ts @@ -25,6 +25,7 @@ import { SectionType, WrapTextType, } from '@univerjs/core'; +import { ptToPixel } from '@univerjs/engine-render'; export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = { id: 'd', @@ -44,10 +45,14 @@ export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = { }, positionV: { relativeFrom: ObjectRelativeFromV.PAGE, - posOffset: 510, + posOffset: 560, }, angle: 0, }, + distL: 10, + distB: 10, + distR: 10, + distT: 10, layoutType: PositionedObjectLayoutType.WRAP_SQUARE, behindDoc: BooleanNumber.FALSE, wrapText: WrapTextType.BOTH_SIDES, @@ -67,7 +72,7 @@ export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = { }, positionV: { relativeFrom: ObjectRelativeFromV.PAGE, - posOffset: 200, + posOffset: 220, }, angle: 0, }, @@ -78,7 +83,7 @@ export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = { }, body: { dataStream: - 'What’s New in the 2022 Gartner Hype Cycle for Emerging Technologies\rEmerging technologies for 2022 fit into three main themes: evolving/expanding immersive experiences, accelerated artificial intelligence automation, and optimized technologist delivery.\rThe 2022 Gartner Hype Cycle identifies 25 must-know emerging technologies designed to help enterprise architecture and technology innovation leaders: \rExpand immersive experiences\rAccelerate artificial intelligence (AI) automation\rOptimize technologist delivery \rThese technologies are expected to greatly impact business and society over the next two to 10 years, but will especially enable CIOs and IT leaders to deliver on digital business transformation. \rThree Hype Cycle themes to think about in 2022 and beyond\rThe 2022 Gartner Hype Cycle features emerging technologies and distills insights from more than 2,000 technologies into a succinct high-potential set. Most technologies have multiple use cases but enterprise architecture and technology innovation leaders should prioritize those with the greatest potential benefit for their organization. (They will also need to launch a proof-of-concept project to demonstrate the feasibility of a technology for their target use case.)\b\r\nThe benefit of these technologies is that they provide individuals with more control over their identities and data, and expand their range of experiences into virtual venues and ecosystems that can be integrated with digital currencies. These technologies also provide new ways to reach customers to strengthen or open up new revenue streams.\rDigital twin of the customer (DToC) is a dynamic virtual representation of a customer that simulates and learns to emulate and anticipate behavior. It can be used to modify and enhance the customer experience (CX) and support new digitalization efforts, products, services and opportunities. DToC will take five to 10 years until mainstream adoption but will be transformational to organizations.\rOther critical technologies in immersive experiences include the following:\rDecentralized identity (DCI) allows an entity (typically a human user) to control their own digital identity by leveraging technologies such as blockchain or other distributed ledger technologies (DLTs), along with digital wallets.\rDigital humans are interactive, AI-driven representations that have some of the characteristics, personality, knowledge and mindset of a human.\rInternal talent marketplaces match internal employees and, in some cases, a pool of contingent workers, to time-boxed projects and various work opportunities, with no recruiter involvement.\r\n', + 'What’s New in the 2022 Gartner Hype Cycle for Emerging Technologies\rEmerging technologies for 2022 fit into three main themes: evolving/expanding immersive experiences, accelerated artificial intelligence automation, and OPTIMIZED technologist delivery.\rThe 2022 Gartner Hype Cycle identifies 25 must-know emerging technologies designed to help enterprise architecture and technology innovation leaders: \rExpand immersive experiences\rAccelerate artificial intelligence (AI) automation\rOptimize technologist delivery \rThese technologies are expected to greatly impact business and society over the next two to 10 years, but will especially enable CIOs and IT leaders to deliver on digital business transformation. \rThree Hype Cycle themes to think about in 2022 and beyond\rThe 2022 Gartner Hype Cycle features emerging technologies and distills insights from more than 2,000 technologies into a succinct high-potential set. Most technologies have multiple use cases but enterprise architecture and technology innovation leaders should prioritize those with the greatest potential benefit for their organization. (They will also need to launch a proof-of-concept project to demonstrate the feasibility of a technology for their target use case.)\b\r\nThe benefit of these technologies is that they provide individuals with more control over their identities and data, and expand their range of experiences into virtual venues and ecosystems that can be integrated with digital currencies. These technologies also provide new ways to reach customers to strengthen or open up new revenue streams.\rDigital twin of the customer (DToC) is a dynamic virtual representation of a customer that simulates and learns to emulate and anticipate behavior. It can be used to modify and enhance the customer experience (CX) and support new digitalization efforts, products, services and opportunities. DToC will take five to 10 years until mainstream adoption but will be transformational to organizations.\rOther critical technologies in immersive experiences include the following:\rDecentralized identity (DCI) allows an entity (typically a human user) to control their own digital identity by leveraging technologies such as blockchain or other distributed ledger technologies (DLTs), along with digital wallets.\rDigital humans are interactive, AI-driven representations that have some of the characteristics, personality, knowledge and mindset of a human.\rInternal talent marketplaces match internal employees and, in some cases, a pool of contingent workers, to time-boxed projects and various work opportunities, with no recruiter involvement.\r\n', textRuns: [ { st: 0, @@ -250,8 +255,9 @@ export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = { { startIndex: 253, paragraphStyle: { - spaceAbove: 20, - indentFirstLine: 20, + spaceAbove: 30, + lineSpacing: 1.5, + suppressHyphenation: BooleanNumber.FALSE, }, }, { @@ -259,6 +265,7 @@ export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = { paragraphStyle: { spaceAbove: 20, indentFirstLine: 20, + lineSpacing: 1.5, }, }, { @@ -276,6 +283,7 @@ export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = { }, paragraphStyle: { lineSpacing: 1.5, + spaceAbove: 20, }, }, { @@ -289,6 +297,7 @@ export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = { }, }, paragraphStyle: { + spaceAbove: 10, lineSpacing: 1.5, }, }, @@ -303,25 +312,30 @@ export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = { }, }, paragraphStyle: { + spaceAbove: 10, lineSpacing: 1.5, }, }, { startIndex: 713, paragraphStyle: { + spaceAbove: 20, indentFirstLine: 20, + lineSpacing: 1.5, }, }, { startIndex: 771, paragraphStyle: { spaceAbove: 20, + lineSpacing: 1.5, }, }, { startIndex: 1244, paragraphStyle: { spaceAbove: 20, + lineSpacing: 1.5, indentFirstLine: 20, }, }, @@ -329,24 +343,28 @@ export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = { startIndex: 1589, paragraphStyle: { indentFirstLine: 20, + lineSpacing: 1.5, }, }, { startIndex: 1986, paragraphStyle: { indentFirstLine: 20, + lineSpacing: 1.5, }, }, { startIndex: 2062, paragraphStyle: { indentFirstLine: 20, + lineSpacing: 1.5, }, }, { startIndex: 2294, paragraphStyle: { indentFirstLine: 20, + lineSpacing: 1.5, }, bullet: { listType: PresetListType.BULLET_LIST, @@ -361,6 +379,7 @@ export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = { startIndex: 2438, paragraphStyle: { indentFirstLine: 20, + lineSpacing: 1.5, }, bullet: { listType: PresetListType.BULLET_LIST, @@ -375,6 +394,7 @@ export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = { startIndex: 2628, paragraphStyle: { indentFirstLine: 20, + lineSpacing: 1.5, }, bullet: { listType: PresetListType.BULLET_LIST, @@ -389,9 +409,6 @@ export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = { sectionBreaks: [ { startIndex: 1245, - columnProperties: [], - columnSeparatorType: ColumnSeparatorType.NONE, - sectionType: SectionType.SECTION_TYPE_UNSPECIFIED, // textDirection: textDirectionDocument, // contentDirection: textDirection!, }, @@ -399,7 +416,7 @@ export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = { startIndex: 2629, columnProperties: [ { - width: 200, + width: 300, paddingEnd: 5, }, ], @@ -418,13 +435,21 @@ export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = { }, documentStyle: { pageSize: { - width: 594.3, - height: 840.51, + width: ptToPixel(595), + height: ptToPixel(842), + }, + marginTop: ptToPixel(50), + marginBottom: ptToPixel(50), + marginRight: ptToPixel(50), + marginLeft: ptToPixel(50), + renderConfig: { + vertexAngle: 0, + centerAngle: 0, }, - marginTop: 72, - marginBottom: 72, - marginRight: 90, - marginLeft: 90, + autoHyphenation: BooleanNumber.TRUE, + doNotHyphenateCaps: BooleanNumber.FALSE, + consecutiveHyphenLimit: 2, + // hyphenationZone: 50, // gridType: GridType.LINES_AND_CHARS, // linePitch: 24, // charSpace: 12, diff --git a/examples/src/data/docs/default-document.data-simple.ts b/examples/src/data/docs/default-document.data-simple.ts index a3cb23de78..4f7e0202be 100644 --- a/examples/src/data/docs/default-document.data-simple.ts +++ b/examples/src/data/docs/default-document.data-simple.ts @@ -20,7 +20,7 @@ import { BooleanNumber } from '@univerjs/core'; export const DEFAULT_DOCUMENT_DATA_SIMPLE: IDocumentData = { id: 'default-document-id', body: { - dataStream: '荷塘月色\r作者:朱自清\r\n', + dataStream: '荷塘𠮷\r作者:朱自清 👨‍👩‍👧‍👦 Today Office\r\n', textRuns: [ { st: 0, @@ -36,10 +36,10 @@ export const DEFAULT_DOCUMENT_DATA_SIMPLE: IDocumentData = { }, { st: 5, - ed: 11, + ed: 36, ts: { fs: 18, - ff: 'Microsoft YaHei', + ff: 'Times New Roman', cl: { rgb: 'rgb(30, 30, 30)', }, @@ -57,7 +57,7 @@ export const DEFAULT_DOCUMENT_DATA_SIMPLE: IDocumentData = { }, }, { - startIndex: 11, + startIndex: 36, paragraphStyle: { spaceAbove: 10, lineSpacing: 2, @@ -67,7 +67,7 @@ export const DEFAULT_DOCUMENT_DATA_SIMPLE: IDocumentData = { ], sectionBreaks: [ { - startIndex: 12, + startIndex: 37, // columnProperties: [ // { // width: 250, diff --git a/examples/src/data/sheets/demo/default-workbook-data-demo.ts b/examples/src/data/sheets/demo/default-workbook-data-demo.ts index 59bcef3d05..5e298fcf91 100644 --- a/examples/src/data/sheets/demo/default-workbook-data-demo.ts +++ b/examples/src/data/sheets/demo/default-workbook-data-demo.ts @@ -15,8 +15,9 @@ */ import type { IDocumentData, IWorkbookData } from '@univerjs/core'; -import { BooleanNumber, LocaleType } from '@univerjs/core'; +import { BooleanNumber, DataValidationErrorStyle, DataValidationOperator, DataValidationType, LocaleType } from '@univerjs/core'; +import { DATA_VALIDATION_PLUGIN_NAME } from '@univerjs/sheets-data-validation'; import { PAGE5_RICHTEXT_1 } from '../../slides/rich-text/page5-richtext1'; const richTextDemo: IDocumentData = { @@ -100,6 +101,79 @@ const richTextDemo1: IDocumentData = { }, }; +const dataValidation = [ + { + uid: 'xxx-1', + type: DataValidationType.DECIMAL, + ranges: [{ + startRow: 0, + endRow: 5, + startColumn: 0, + endColumn: 2, + }], + operator: DataValidationOperator.GREATER_THAN, + formula1: '111', + errorStyle: DataValidationErrorStyle.STOP, + }, + { + uid: 'xxx-0', + type: DataValidationType.DATE, + ranges: [{ + startRow: 0, + endRow: 5, + startColumn: 3, + endColumn: 5, + }], + operator: DataValidationOperator.NOT_BETWEEN, + formula1: '2024/04/10', + formula2: '2024/10/10', + errorStyle: DataValidationErrorStyle.STOP, + }, + { + uid: 'xxx-2', + type: DataValidationType.CHECKBOX, + ranges: [{ + startRow: 6, + endRow: 10, + startColumn: 0, + endColumn: 5, + }], + }, + { + uid: 'xxx-3', + type: DataValidationType.LIST, + ranges: [{ + startRow: 11, + endRow: 15, + startColumn: 0, + endColumn: 5, + }], + formula1: '1,2,3,hahaha,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18', + }, + { + uid: 'xxx-4', + type: DataValidationType.CUSTOM, + ranges: [{ + startRow: 16, + endRow: 20, + startColumn: 0, + endColumn: 5, + }], + formula1: '=A1', + }, + { + uid: 'xxx-5', + type: DataValidationType.LIST_MULTIPLE, + ranges: [{ + startRow: 21, + endRow: 21, + startColumn: 0, + endColumn: 0, + }], + formula1: '1,2,3,4,5,哈哈哈哈', + }, +]; + export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = { id: 'workbook-01', locale: LocaleType.ZH_CN, @@ -13875,7 +13949,7 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = { id: 'sheet-0011', tabColor: '', hidden: 0, - rowCount: 1000000, + rowCount: 1000, columnCount: 20, zoomRatio: 1, cellData: { @@ -13885,9 +13959,25 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = { v: 'A Schedule of Items', }, }, + 1: { + 0: { + s: { + n: { + pattern: 'yyyy-mm-dd;@', + }, + }, + v: 1, + }, + }, + 2: { + 0: { + f: '=A2', + }, + }, 5: { 5: { s: 'uJSelZ11', + v: 'sadf', }, 6: { s: 'uJSelZ11', @@ -13905,6 +13995,7 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = { 6: { 5: { s: 'uJSelZ22', + v: '123123', }, 6: { s: 'uJSelZ22', @@ -13917,8 +14008,100 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = { }, }, 10: {}, - 11: {}, + 11: { + 4: { + v: 123, + t: 2, + }, + }, + 12: { + 4: { + v: '123tu', + t: 1, + }, + 5: { + v: 'sdfj', + t: 1, + }, + }, + 13: { + 4: { + v: 'ghj', + t: 1, + }, + 5: { + v: 'ghk', + t: 1, + }, + }, + 17: { + 4: { + v: 'fh', + t: 1, + }, + 5: { + v: 'jk', + t: 1, + }, + }, + 18: { + 4: { + v: 'dfg', + t: 1, + }, + 5: { + v: 'l', + t: 1, + }, + }, + 19: { + 4: { + v: 'sdfg', + t: 1, + }, + 5: { + v: 'h', + t: 1, + }, + }, + 20: { + 4: { + v: 'fdgh', + t: 1, + }, + 5: { + v: 345, + t: 2, + }, + }, + 21: { + 4: { + v: 'sgh', + t: 1, + }, + 5: { + v: 'fgs', + t: 1, + }, + }, + 22: { + 4: { + v: 'sdfh', + t: 1, + }, + 5: { + v: 'gth', + t: 1, + }, + }, + 23: { + 5: { + v: 'iop', + t: 1, + }, + }, }, + freeze: { xSplit: 0, ySplit: 0, @@ -13931,6 +14114,21 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = { defaultRowHeight: 19, mergeData: [], rowData: { + 11: { + hd: 0, + h: 19, + ah: 19, + }, + 12: { + hd: 0, + h: 19, + ah: 19, + }, + 13: { + hd: 0, + h: 19, + ah: 19, + }, 14: { hd: 1, }, @@ -13940,6 +14138,41 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = { 16: { hd: 1, }, + 17: { + hd: 0, + h: 19, + ah: 19, + }, + 18: { + hd: 0, + h: 19, + ah: 19, + }, + 19: { + hd: 0, + h: 19, + ah: 19, + }, + 20: { + hd: 0, + h: 19, + ah: 19, + }, + 21: { + hd: 0, + h: 19, + ah: 19, + }, + 22: { + hd: 0, + h: 19, + ah: 19, + }, + 23: { + hd: 0, + h: 19, + ah: 19, + }, }, columnData: {}, showGridlines: 1, @@ -13951,7 +14184,9 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = { height: 20, hidden: 0, }, - selections: ['A1'], + selections: [ + 'A1', + ], rightToLeft: 0, }, 'sheet-0010': { @@ -15147,9 +15382,11 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = { 21: { 0: { s: 'u5otPe', + v: '1,2', }, 1: { s: 'u5otPe', + v: '1,2,3', }, 2: { s: 'u5otPe', @@ -23303,6 +23540,31 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = { // }, // }, }, + resources: [ + { + name: DATA_VALIDATION_PLUGIN_NAME, + data: JSON.stringify({ + 'sheet-0011': dataValidation, + }), + }, + { + name: 'SHEET_FILTER_PLUGIN', + data: JSON.stringify({ + 'sheet-0011': { + ref: { + startRow: 11, + endRow: 23, + startColumn: 4, + endColumn: 6, + }, + }, + }), + }, + { + name: 'SHEET_THREAD_COMMENT_PLUGIN', + data: '{"sheet-0011":[{"text":{"textRuns":[],"paragraphs":[{"startIndex":3,"paragraphStyle":{}}],"sectionBreaks":[{"startIndex":4}],"dataStream":"123\\n\\r","customRanges":[]},"dT":"2024/05/17 21:16","id":"jwV0QtHwUbhG3o--iy1qa","ref":"H9","personId":"mockId","unitId":"workbook-01","subUnitId":"sheet-0011"}]}', + }, + ], // namedRanges: [ // { // namedRangeId: 'named-rang', diff --git a/examples/src/data/sheets/demo/default-workbook-data-validation.ts b/examples/src/data/sheets/demo/default-workbook-data-validation.ts new file mode 100644 index 0000000000..58e04ab803 --- /dev/null +++ b/examples/src/data/sheets/demo/default-workbook-data-validation.ts @@ -0,0 +1,174 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const DEFAULT_WORKBOOK_DATA_VALIDATION = { + id: 'workbook-01', + sheetOrder: [ + '80FIpJ1SdQLVIUEbrnC3J', + ], + name: 'UniverSheet Demo', + appVersion: '3.0.0-alpha', + locale: 'zhCN', + styles: {}, + sheets: { + '80FIpJ1SdQLVIUEbrnC3J': { + name: '工作表1', + id: '80FIpJ1SdQLVIUEbrnC3J', + tabColor: '', + hidden: 0, + rowCount: 1000, + columnCount: 20, + zoomRatio: 1, + freeze: { + xSplit: 0, + ySplit: 0, + startRow: -1, + startColumn: -1, + }, + scrollTop: 0, + scrollLeft: 0, + defaultColumnWidth: 73, + defaultRowHeight: 19, + mergeData: [], + cellData: { + 0: { + 0: { + v: 'YES', + t: 1, + }, + 2: { + v: 'IF', + t: 1, + }, + 5: { + v: '中文', + t: 1, + }, + 6: { + v: '一', + t: 1, + }, + 7: { + v: '二', + t: 1, + }, + 8: { + v: '三', + t: 1, + }, + }, + 1: { + 5: { + v: '数字', + t: 1, + }, + 6: { + v: 1, + t: 2, + }, + 7: { + v: 2, + t: 2, + }, + 8: { + v: 3, + t: 2, + }, + }, + 2: { + 2: { + v: '级联', + t: 1, + }, + 3: { + v: '中文', + t: 1, + }, + 4: { + v: '一', + t: 1, + }, + 5: null, + 6: null, + 7: null, + 8: null, + }, + 3: { + 3: null, + 4: null, + 5: null, + 6: null, + 7: null, + 8: null, + }, + }, + rowData: { + 0: { + hd: 0, + h: 19, + ah: 20, + }, + 2: { + hd: 0, + h: 19, + ah: 20, + }, + 3: { + hd: 0, + h: 19, + ah: 19, + }, + }, + columnData: {}, + showGridlines: 1, + rowHeader: { + width: 46, + hidden: 0, + }, + columnHeader: { + height: 20, + hidden: 0, + }, + selections: [ + 'A1', + ], + rightToLeft: 0, + }, + }, + resources: [ + { + name: 'SHEET_NUMFMT_PLUGIN', + data: '', + }, + { + name: 'SHEET_DEFINED_NAME_PLUGIN', + data: '', + }, + { + name: 'SHEET_DATA_VALIDATION', + data: '{"sheet-0011":[{"uid":"xxx-1","type":"decimal","ranges":[{"startRow":0,"endRow":5,"startColumn":0,"endColumn":2}],"operator":"greaterThan","formula1":"111","errorStyle":1},{"uid":"xxx-0","type":"date","ranges":[{"startRow":0,"endRow":5,"startColumn":3,"endColumn":5}],"operator":"greaterThan","formula1":"100","errorStyle":1},{"uid":"xxx-2","type":"checkbox","ranges":[{"startRow":6,"endRow":10,"startColumn":0,"endColumn":5}]},{"uid":"xxx-3","type":"list","ranges":[{"startRow":11,"endRow":15,"startColumn":0,"endColumn":5}],"formula1":"1,2,3,hahaha"},{"uid":"xxx-4","type":"custom","ranges":[{"startRow":16,"endRow":20,"startColumn":0,"endColumn":5}],"formula1":"=A1"},{"uid":"xxx-5","type":"listMultiple","ranges":[{"startRow":21,"endRow":21,"startColumn":0,"endColumn":0}],"formula1":"1,2,3,4,5,哈哈哈哈"}],"80FIpJ1SdQLVIUEbrnC3J":[{"uid":"pQCe2q","type":"list","formula1":"=IF(A1=\\"YES\\",G1:I1,G2:I2)","ranges":[{"startRow":0,"endRow":0,"startColumn":3,"endColumn":3}],"formula2":""},{"uid":"JL6foF","type":"list","formula1":"=F1:F2","ranges":[{"startRow":2,"startColumn":3,"endRow":2,"endColumn":3,"rangeType":0}],"formula2":""},{"uid":"XlFuI6","type":"list","formula1":"=IF(D3=\\"中文\\",G1:I1,G2:I2)","ranges":[{"startRow":2,"startColumn":4,"endRow":2,"endColumn":4,"rangeType":0}],"formula2":""}]}', + }, + { + name: 'SHEET_CONDITIONAL_FORMATTING_PLUGIN', + data: '', + }, + ], + __env__: { + gitHash: '840580212', + gitBranch: 'feat/dv-list-formula', + buildTime: '2024-04-10T11:19:00.298Z', + }, +}; diff --git a/examples/src/docs-uniscript/main.ts b/examples/src/docs-uniscript/main.ts index ea17ade5e3..3d86d00007 100644 --- a/examples/src/docs-uniscript/main.ts +++ b/examples/src/docs-uniscript/main.ts @@ -26,11 +26,17 @@ import { UniverUniscriptPlugin } from '@univerjs/uniscript'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { DEFAULT_DOCUMENT_DATA_CN } from '../data'; +import { enUS, ruRU, zhCN } from '../locales'; // univer const univer = new Univer({ theme: defaultTheme, locale: LocaleType.ZH_CN, + locales: { + [LocaleType.ZH_CN]: zhCN, + [LocaleType.EN_US]: enUS, + [LocaleType.RU_RU]: ruRU, + }, logLevel: LogLevel.VERBOSE, }); @@ -40,7 +46,7 @@ univer.registerPlugin(UniverRenderEnginePlugin); univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverUIPlugin, { container: 'app', - header: true, + footer: false, }); univer.registerPlugin(UniverDocsPlugin); diff --git a/examples/src/docs/main.ts b/examples/src/docs/main.ts index 786c657e2e..8d4ff1a400 100644 --- a/examples/src/docs/main.ts +++ b/examples/src/docs/main.ts @@ -21,11 +21,12 @@ import { UniverDocsPlugin } from '@univerjs/docs'; import { UniverDocsUIPlugin } from '@univerjs/docs-ui'; import { UniverRenderEnginePlugin } from '@univerjs/engine-render'; import { UniverUIPlugin } from '@univerjs/ui'; - +import { UniverImagePlugin } from '@univerjs/image'; import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'; +import { UniverDebuggerPlugin } from '@univerjs/debugger'; + import { DEFAULT_DOCUMENT_DATA_CN } from '../data'; -import { DebuggerPlugin } from '../plugins/debugger'; -import { locales } from './locales'; +import { enUS, ruRU, zhCN } from '../locales'; // package info // eslint-disable-next-line no-console @@ -40,16 +41,20 @@ console.table({ const univer = new Univer({ theme: defaultTheme, locale: LocaleType.ZH_CN, - locales, + locales: { + [LocaleType.ZH_CN]: zhCN, + [LocaleType.EN_US]: enUS, + [LocaleType.RU_RU]: ruRU, + }, }); // core plugins univer.registerPlugin(UniverRenderEnginePlugin); univer.registerPlugin(UniverFormulaEnginePlugin); -univer.registerPlugin(DebuggerPlugin); +univer.registerPlugin(UniverDebuggerPlugin); univer.registerPlugin(UniverUIPlugin, { container: 'app', - header: true, + footer: false, }); univer.registerPlugin(UniverDocsPlugin); univer.registerPlugin(UniverDocsUIPlugin, { @@ -61,6 +66,8 @@ univer.registerPlugin(UniverDocsUIPlugin, { }, }); +univer.registerPlugin(UniverImagePlugin); + univer.createUniverDoc(DEFAULT_DOCUMENT_DATA_CN); // use for console test diff --git a/examples/src/locales.ts b/examples/src/locales.ts new file mode 100644 index 0000000000..dc18100795 --- /dev/null +++ b/examples/src/locales.ts @@ -0,0 +1,116 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Tools } from '@univerjs/core'; +import DesignEnUS from '@univerjs/design/locale/en-US'; +import DesignRuRU from '@univerjs/design/locale/ru-RU'; +import DesignZhCN from '@univerjs/design/locale/zh-CN'; +import DocsUIEnUS from '@univerjs/docs-ui/locale/en-US'; +import DocsUIRuRU from '@univerjs/docs-ui/locale/ru-RU'; +import DocsUIZhCN from '@univerjs/docs-ui/locale/zh-CN'; +import FindReplaceEnUS from '@univerjs/find-replace/locale/en-US'; +import FindReplaceRuRU from '@univerjs/find-replace/locale/ru-RU'; +import FindReplaceZhCN from '@univerjs/find-replace/locale/zh-CN'; +import SheetsEnUS from '@univerjs/sheets/locale/en-US'; +import SheetsRuRU from '@univerjs/sheets/locale/ru-RU'; +import SheetsZhCN from '@univerjs/sheets/locale/zh-CN'; +import SheetsUIEnUS from '@univerjs/sheets-ui/locale/en-US'; +import SheetsUIRuRU from '@univerjs/sheets-ui/locale/ru-RU'; +import SheetsUIZhCN from '@univerjs/sheets-ui/locale/zh-CN'; +import SheetsFormulaEnUS from '@univerjs/sheets-formula/locale/en-US'; +import SheetsFormulaRuRU from '@univerjs/sheets-formula/locale/ru-RU'; +import SheetsFormulaZhCN from '@univerjs/sheets-formula/locale/zh-CN'; +import SheetsDataValidationEnUS from '@univerjs/sheets-data-validation/locale/en-US'; +import SheetsDataValidationRuRU from '@univerjs/sheets-data-validation/locale/ru-RU'; +import SheetsDataValidationZhCN from '@univerjs/sheets-data-validation/locale/zh-CN'; +import SheetsConditionalFormattingUIEnUS from '@univerjs/sheets-conditional-formatting-ui/locale/en-US'; +import SheetsConditionalFormattingUIRuRU from '@univerjs/sheets-conditional-formatting-ui/locale/ru-RU'; +import SheetsConditionalFormattingUIZhCN from '@univerjs/sheets-conditional-formatting-ui/locale/zh-CN'; +import SheetsZenEditorEnUS from '@univerjs/sheets-zen-editor/locale/en-US'; +import SheetsZenEditorRuRU from '@univerjs/sheets-zen-editor/locale/ru-RU'; +import SheetsZenEditorZhCN from '@univerjs/sheets-zen-editor/locale/zh-CN'; +import UIEnUS from '@univerjs/ui/locale/en-US'; +import UIRuRU from '@univerjs/ui/locale/ru-RU'; +import UIZhCN from '@univerjs/ui/locale/zh-CN'; +import SheetsFilterUIEnUS from '@univerjs/sheets-filter-ui/locale/en-US'; +import SheetsFilterUIRuRU from '@univerjs/sheets-filter-ui/locale/ru-RU'; +import SheetsFilterUIZhCN from '@univerjs/sheets-filter-ui/locale/zh-CN'; +import SheetsThreadCommentEnUS from '@univerjs/sheets-thread-comment/locale/en-US'; +import SheetsThreadCommentRuRU from '@univerjs/sheets-thread-comment/locale/ru-RU'; +import SheetsThreadCommentZhCN from '@univerjs/sheets-thread-comment/locale/zh-CN'; +import ThreadCommentUIEnUS from '@univerjs/thread-comment-ui/locale/en-US'; +import ThreadCommentUIRuRU from '@univerjs/thread-comment-ui/locale/ru-RU'; +import ThreadCommentUIZhCN from '@univerjs/thread-comment-ui/locale/zh-CN'; +import SheetsNumfmtEnUS from '@univerjs/sheets-numfmt/locale/en-US'; +import SheetsNumfmtRuRU from '@univerjs/sheets-numfmt/locale/ru-RU'; +import SheetsNumfmtZhCN from '@univerjs/sheets-numfmt/locale/zh-CN'; +import UniscriptEnUS from '@univerjs/uniscript/locale/en-US'; +import UniscriptRuRU from '@univerjs/uniscript/locale/ru-RU'; +import UniscriptZhCN from '@univerjs/uniscript/locale/zh-CN'; + +export const zhCN = Tools.deepMerge( + SheetsZhCN, + DocsUIZhCN, + FindReplaceZhCN, + SheetsUIZhCN, + SheetsFormulaZhCN, + SheetsDataValidationZhCN, + SheetsConditionalFormattingUIZhCN, + SheetsZenEditorZhCN, + UIZhCN, + DesignZhCN, + SheetsFilterUIZhCN, + SheetsThreadCommentZhCN, + ThreadCommentUIZhCN, + SheetsNumfmtZhCN, + UniscriptZhCN +); + +export const enUS = Tools.deepMerge( + SheetsEnUS, + DocsUIEnUS, + FindReplaceEnUS, + SheetsUIEnUS, + SheetsFormulaEnUS, + SheetsDataValidationEnUS, + SheetsConditionalFormattingUIEnUS, + SheetsZenEditorEnUS, + UIEnUS, + DesignEnUS, + SheetsFilterUIEnUS, + SheetsThreadCommentEnUS, + ThreadCommentUIEnUS, + SheetsNumfmtEnUS, + UniscriptEnUS +); + +export const ruRU = Tools.deepMerge( + SheetsRuRU, + DocsUIRuRU, + FindReplaceRuRU, + SheetsUIRuRU, + SheetsFormulaRuRU, + SheetsDataValidationRuRU, + SheetsConditionalFormattingUIRuRU, + SheetsZenEditorRuRU, + UIRuRU, + DesignRuRU, + SheetsFilterUIRuRU, + SheetsThreadCommentRuRU, + ThreadCommentUIRuRU, + SheetsNumfmtRuRU, + UniscriptRuRU +); diff --git a/examples/src/plugins/debugger/debugger-plugin.ts b/examples/src/plugins/debugger/debugger-plugin.ts deleted file mode 100644 index 66a268503c..0000000000 --- a/examples/src/plugins/debugger/debugger-plugin.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright 2023-present DreamNum Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { LocaleService, Plugin, PluginType } from '@univerjs/core'; -import type { Dependency } from '@wendellhu/redi'; -import { Inject, Injector } from '@wendellhu/redi'; - -import { DebuggerController } from './controllers/debugger.controller'; -import { PerformanceMonitorController } from './controllers/performance-monitor.controller'; - -export interface IDebuggerPluginConfig {} - -export class DebuggerPlugin extends Plugin { - static override type = PluginType.Doc; - - private _debuggerController!: DebuggerController; - - constructor( - config: IDebuggerPluginConfig, - @Inject(Injector) override readonly _injector: Injector, - @Inject(LocaleService) private readonly _localeService: LocaleService - ) { - super('debugger'); - this._initializeDependencies(_injector); - } - - initialize(): void { - this._debuggerController = this._injector.createInstance(DebuggerController); - this._injector.add([DebuggerController, { useValue: this._debuggerController }]); - - this.registerExtension(); - } - - registerExtension() {} - - private _initializeDependencies(injector: Injector) { - ([[PerformanceMonitorController]] as Dependency[]).forEach((d) => injector.add(d)); - } - - override onRendered(): void { - this.initialize(); - } - - override onDestroy(): void {} - - getDebuggerController() { - return this._debuggerController; - } -} diff --git a/examples/src/plugins/local-save/services/local-snapshot.service.ts b/examples/src/plugins/local-save/services/local-snapshot.service.ts deleted file mode 100644 index d8bc55f9a7..0000000000 --- a/examples/src/plugins/local-save/services/local-snapshot.service.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright 2023-present DreamNum Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { ISnapshotPersistenceService, Workbook } from '@univerjs/core'; -import { Disposable, IResourceManagerService, IUniverInstanceService } from '@univerjs/core'; -import { Inject } from '@wendellhu/redi'; - -export class LocalSnapshotService extends Disposable implements ISnapshotPersistenceService { - constructor( - @Inject(IResourceManagerService) private _resourceManagerService: IResourceManagerService, - @Inject(IUniverInstanceService) private _univerInstanceService: IUniverInstanceService - ) { - super(); - this._initWorkBook(); - } - - private _initWorkBook() { - this._univerInstanceService.sheetAdded$.subscribe((workbook) => this._initWorkbookFromSnapshot(workbook)); - const workbooks = this._univerInstanceService.getAllUniverSheetsInstance(); - workbooks.forEach((workbook) => { - this._initWorkbookFromSnapshot(workbook); - }); - } - - private _initWorkbookFromSnapshot(workbook: Workbook) { - const unitId = workbook.getUnitId(); - const snapshot = workbook.getSnapshot(); - const resources = this._resourceManagerService.getAllResource(unitId); - resources.forEach((resource) => { - const resourceSnapshot = (snapshot.resources || []).find((item) => item.name === resource.resourceName); - if (resourceSnapshot) { - const model = resource.hook.parseJson(resourceSnapshot.data); - resource.hook.onChange(unitId, model); - } - }); - } - - saveWorkbook(workbook: Workbook) { - const snapshot = { ...workbook.getSnapshot() }; - const unitId = workbook.getUnitId(); - const resourceHooks = this._resourceManagerService.getAllResource(workbook.getUnitId()); - const resources = resourceHooks.map((resourceHook) => { - const data = resourceHook.hook.toJson(unitId); - return { - name: resourceHook.resourceName, - data, - }; - }); - snapshot.resources = resources; - return snapshot; - } -} diff --git a/examples/src/sheets-multi/main.tsx b/examples/src/sheets-multi/main.tsx index f43a202b4e..bc9aaf9031 100644 --- a/examples/src/sheets-multi/main.tsx +++ b/examples/src/sheets-multi/main.tsx @@ -33,20 +33,24 @@ import { createRoot } from 'react-dom/client'; import { Mosaic, MosaicWindow } from 'react-mosaic-component'; import { DEFAULT_WORKBOOK_DATA_DEMO } from '../data'; +import { enUS, ruRU, zhCN } from '../locales'; function factory(id: string) { return function createUniverOnContainer() { const univer = new Univer({ theme: defaultTheme, locale: LocaleType.ZH_CN, + locales: { + [LocaleType.ZH_CN]: zhCN, + [LocaleType.EN_US]: enUS, + [LocaleType.RU_RU]: ruRU, + }, logLevel: LogLevel.VERBOSE, }); univer.registerPlugin(UniverRenderEnginePlugin); univer.registerPlugin(UniverUIPlugin, { container: id, - header: true, - footer: true, }); univer.registerPlugin(UniverDocsPlugin, { hasScroll: false, diff --git a/examples/src/sheets-uniscript/main.ts b/examples/src/sheets-uniscript/main.ts index 5cb02597c9..5df1106ffe 100644 --- a/examples/src/sheets-uniscript/main.ts +++ b/examples/src/sheets-uniscript/main.ts @@ -26,14 +26,20 @@ import { UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; import { UniverUIPlugin } from '@univerjs/ui'; import { UniverUniscriptPlugin } from '@univerjs/uniscript'; +import { UniverDebuggerPlugin } from '@univerjs/debugger'; import { UNISCRIT_WORKBOOK_DATA_DEMO } from '../data/sheets/uniscript-data'; -import { DebuggerPlugin } from '../plugins/debugger'; +import { enUS, ruRU, zhCN } from '../locales'; // univer const univer = new Univer({ theme: defaultTheme, locale: LocaleType.ZH_CN, + locales: { + [LocaleType.ZH_CN]: zhCN, + [LocaleType.EN_US]: enUS, + [LocaleType.RU_RU]: ruRU, + }, logLevel: LogLevel.VERBOSE, }); @@ -42,8 +48,6 @@ const univer = new Univer({ univer.registerPlugin(UniverRenderEnginePlugin); univer.registerPlugin(UniverUIPlugin, { container: 'app', - header: true, - footer: true, }); univer.registerPlugin(UniverDocsPlugin, { @@ -56,7 +60,7 @@ univer.registerPlugin(UniverSheetsUIPlugin); // sheet feature plugins univer.registerPlugin(UniverSheetsNumfmtPlugin); -univer.registerPlugin(DebuggerPlugin); +univer.registerPlugin(UniverDebuggerPlugin); univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverSheetsFormulaPlugin); univer.registerPlugin(UniverUniscriptPlugin, { diff --git a/examples/src/sheets/lazy.ts b/examples/src/sheets/lazy.ts index 5f69d2cda4..f642604d63 100644 --- a/examples/src/sheets/lazy.ts +++ b/examples/src/sheets/lazy.ts @@ -15,6 +15,7 @@ */ import type { Plugin, PluginCtor } from '@univerjs/core'; +import { UniverSheetsFilterUIPlugin } from '@univerjs/sheets-filter-ui'; import { UniverUniscriptPlugin } from '@univerjs/uniscript'; export default function getLazyPlugins(): Array<[PluginCtor] | [PluginCtor, unknown]> { @@ -31,5 +32,6 @@ export default function getLazyPlugins(): Array<[PluginCtor] | [PluginCt }, }, ], + [UniverSheetsFilterUIPlugin], ]; } diff --git a/examples/src/sheets/main.ts b/examples/src/sheets/main.ts index b3f505e704..34c1ea1ca7 100644 --- a/examples/src/sheets/main.ts +++ b/examples/src/sheets/main.ts @@ -14,13 +14,14 @@ * limitations under the License. */ -import { LocaleType, LogLevel, Univer } from '@univerjs/core'; +import { LocaleType, LogLevel, Univer, UniverInstanceType, UserManagerService } from '@univerjs/core'; import { defaultTheme } from '@univerjs/design'; import { UniverDocsPlugin } from '@univerjs/docs'; import { UniverDocsUIPlugin } from '@univerjs/docs-ui'; import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'; import { UniverRenderEnginePlugin } from '@univerjs/engine-render'; import { UniverFindReplacePlugin } from '@univerjs/find-replace'; +import { UniverSheetsFilterPlugin } from '@univerjs/sheets-filter'; import type { IUniverRPCMainThreadConfig } from '@univerjs/rpc'; import { UniverRPCMainThreadPlugin } from '@univerjs/rpc'; import { UniverSheetsPlugin } from '@univerjs/sheets'; @@ -30,17 +31,31 @@ import { UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; import { UniverSheetsZenEditorPlugin } from '@univerjs/sheets-zen-editor'; import { UniverUIPlugin } from '@univerjs/ui'; +import { UniverDataValidationPlugin } from '@univerjs/data-validation'; +import { UniverSheetsDataValidationPlugin } from '@univerjs/sheets-data-validation'; +import { UniverSheetsConditionalFormattingUIPlugin } from '@univerjs/sheets-conditional-formatting-ui'; +import { UniverSheetsThreadCommentPlugin } from '@univerjs/sheets-thread-comment'; +import { UniverDebuggerPlugin } from '@univerjs/debugger'; -import { DebuggerPlugin } from '../plugins/debugger'; +import { FUniver } from '@univerjs/facade'; +import { IThreadCommentMentionDataService } from '@univerjs/thread-comment-ui'; import { DEFAULT_WORKBOOK_DATA_DEMO } from '../data/sheets/demo/default-workbook-data-demo'; -import { locales } from './locales'; +import { enUS, ruRU, zhCN } from '../locales'; + +/* eslint-disable-next-line node/prefer-global/process */ +const IS_E2E: boolean = !!process.env.IS_E2E; const LOAD_LAZY_PLUGINS_TIMEOUT = 1_000; + // univer const univer = new Univer({ theme: defaultTheme, locale: LocaleType.ZH_CN, - locales, + locales: { + [LocaleType.ZH_CN]: zhCN, + [LocaleType.EN_US]: enUS, + [LocaleType.RU_RU]: ruRU, + }, logLevel: LogLevel.VERBOSE, }); @@ -51,21 +66,16 @@ univer.registerPlugin(UniverDocsPlugin, { univer.registerPlugin(UniverRenderEnginePlugin); univer.registerPlugin(UniverUIPlugin, { container: 'app', - header: true, - footer: true, }); univer.registerPlugin(UniverDocsUIPlugin); -univer.registerPlugin(UniverSheetsPlugin, { - notExecuteFormula: true, -}); +univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); // sheet feature plugins univer.registerPlugin(UniverSheetsNumfmtPlugin); -univer.registerPlugin(DebuggerPlugin); univer.registerPlugin(UniverSheetsZenEditorPlugin); univer.registerPlugin(UniverFormulaEnginePlugin, { notExecuteFormula: true, @@ -79,12 +89,65 @@ univer.registerPlugin(UniverRPCMainThreadPlugin, { univer.registerPlugin(UniverFindReplacePlugin); univer.registerPlugin(UniverSheetsFindReplacePlugin); +// data validation +univer.registerPlugin(UniverDataValidationPlugin); +univer.registerPlugin(UniverSheetsDataValidationPlugin); + +// filter +univer.registerPlugin(UniverSheetsFilterPlugin); + +// sheet condition formatting +univer.registerPlugin(UniverSheetsConditionalFormattingUIPlugin); + // create univer sheet instance -univer.createUniverSheet(DEFAULT_WORKBOOK_DATA_DEMO); +if (!IS_E2E) { + univer.createUnit(UniverInstanceType.UNIVER_SHEET, DEFAULT_WORKBOOK_DATA_DEMO); +} + +const mockUser = { + userID: 'mockId', + name: 'MockUser', + avatar: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAInSURBVHgBtZU9TxtBEIbfWRzFSIdkikhBSqRQkJqkCKTCFkqVInSUSaT0wC8w/gXxD4gU2nRJkXQWhAZowDUUWKIwEgWWbEEB3mVmx3dn4DA2nB/ppNuPeWd29mMIPXDr+RxwtgRHeW6+guNPRxogqnL7Dwz9psJ27S4NShaeZTH3kwXy6I81dlRKcmRui88swdq9AcSFL7Buz1Vmlns64MiLsCjzwnIYHLH57tbfFbs7KRaXyEU8FVZofqccOfA5l7Q8LPIkGrwnb2RPNEXWFVMUF3L+kDCk0btDDAMzOm5YfAHDwp4tG74wnzAsiOYMnJ3GoDybA7IT98/jm5+JNnfiIzAS6LlqHQBN/i6b2t/cV1Hh6BfwYlHnHP4AXi5q/8kmMMpOs8+BixZw/Fd6xUEHEbnkgclvQP2fGp7uShRKnQ3G32rkjV1th8JhIGG7tR/JyjGteSOZELwGMmNqIIigRCLRh2OZIE6BjItdd7pCW6Uhm1zzkUtungSxwEUzNpQ+GQumtH1ej1MqgmNT6vwmhCq5yuwq56EYTbgeQUz3yvrpV1b4ok3nYJ+eYhgYmjRUqErx2EDq0Fr8FhG++iqVGqxlUJI/70Ar0UgJaWHj6hYVHJrfKssAHot1JfqwE9WVWzXZVd5z2Ws/4PnmtEjkXeKJDvxUecLbWOXH/DP6QQ4J72NS0adedp1aseBfXP8odlZFfPvBF7SN/8hky1TYuPOAXAEipMx15u5ToAAAAABJRU5ErkJggg==', + anonymous: false, + canBindAnonymous: false, +}; + +class CustomMentionDataService implements IThreadCommentMentionDataService { + trigger: string = '@'; + + async getMentions(search: string) { + return [ + { + id: mockUser.userID, + label: mockUser.name, + type: 'user', + icon: mockUser.avatar, + }, + { + id: '2', + label: 'User2', + type: 'user', + icon: mockUser.avatar, + }, + ]; + } +} + +univer.registerPlugin(UniverSheetsThreadCommentPlugin, { + overrides: [[IThreadCommentMentionDataService, { useClass: CustomMentionDataService }]], +}); + +// debugger plugin +univer.registerPlugin(UniverDebuggerPlugin); + +const injector = univer.__getInjector(); +const userManagerService = injector.get(UserManagerService); +userManagerService.setCurrentUser(mockUser); declare global { interface Window { univer?: Univer; + univerAPI?: ReturnType; } } @@ -96,3 +159,4 @@ setTimeout(() => { }, LOAD_LAZY_PLUGINS_TIMEOUT); window.univer = univer; +window.univerAPI = FUniver.newAPI(univer); diff --git a/examples/src/sheets/worker.ts b/examples/src/sheets/worker.ts index 7c3d25aeec..c4f1b80c06 100644 --- a/examples/src/sheets/worker.ts +++ b/examples/src/sheets/worker.ts @@ -18,6 +18,7 @@ import { LocaleType, Univer } from '@univerjs/core'; import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'; import { UniverRPCWorkerThreadPlugin } from '@univerjs/rpc'; import { UniverSheetsPlugin } from '@univerjs/sheets'; +import { UniverSheetsFilterPlugin } from '@univerjs/sheets-filter'; // Univer web worker is also a univer application. const univer = new Univer({ @@ -27,6 +28,7 @@ const univer = new Univer({ univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverRPCWorkerThreadPlugin); +univer.registerPlugin(UniverSheetsFilterPlugin); declare let self: WorkerGlobalScope & typeof globalThis & { univer: Univer }; self.univer = univer; diff --git a/examples/src/slides/main.ts b/examples/src/slides/main.ts index 8df416c77b..c4f6e2e6e0 100644 --- a/examples/src/slides/main.ts +++ b/examples/src/slides/main.ts @@ -14,38 +14,35 @@ * limitations under the License. */ -import { LocaleType, Univer } from '@univerjs/core'; +import { LocaleType, Univer, UniverInstanceType } from '@univerjs/core'; import { greenTheme } from '@univerjs/design'; import { UniverRenderEnginePlugin } from '@univerjs/engine-render'; import { UniverSlidesPlugin } from '@univerjs/slides'; import { UniverSlidesUIPlugin } from '@univerjs/slides-ui'; import { UniverUIPlugin } from '@univerjs/ui'; +import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'; import { DEFAULT_SLIDE_DATA } from '../data'; +import { enUS, ruRU, zhCN } from '../locales'; // univer const univer = new Univer({ - locale: LocaleType.ZH_CN, theme: greenTheme, + locale: LocaleType.ZH_CN, + locales: { + [LocaleType.ZH_CN]: zhCN, + [LocaleType.EN_US]: enUS, + [LocaleType.RU_RU]: ruRU, + }, }); // base-render univer.registerPlugin(UniverRenderEnginePlugin); +univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverUIPlugin, { container: 'univer-container', - header: true, - footer: true, }); univer.registerPlugin(UniverSlidesPlugin); univer.registerPlugin(UniverSlidesUIPlugin); -univer.createUniverSlide(DEFAULT_SLIDE_DATA); - -// use for console test -declare global { - interface Window { - univer?: Univer; - } -} - -window.univer = univer; +univer.createUnit(UniverInstanceType.UNIVER_SLIDE, DEFAULT_SLIDE_DATA); diff --git a/package.json b/package.json index 254953160f..f28ee9e587 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "univer", "type": "module", - "version": "0.1.4", + "version": "0.1.12", "private": true, - "packageManager": "pnpm@8.6.2", + "packageManager": "pnpm@9.0.5", "author": "DreamNum Inc. ", "license": "Apache-2.0", "funding": { @@ -20,17 +20,21 @@ }, "engines": { "node": ">=18.0.0", - "pnpm": ">=8.5.0" + "pnpm": ">=8.5.0 || >=9.0.0" }, "scripts": { "prepare": "husky install", "pre-commit": "lint-staged", "dev": "turbo dev:demo", + "dev:e2e": "pnpm --filter univer-examples dev:e2e", "lint:types": "turbo lint:types", "test": "turbo test -- --passWithNoTests", "coverage": "turbo coverage -- --passWithNoTests", - "build": "turbo build", + "build": "turbo build && pnpm --filter @univerjs/umd build:umd", "build:demo": "turbo build:demo", + "build:e2e": "pnpm --filter univer-examples build:e2e", + "serve:e2e": "serve ./examples/local", + "test:e2e": "playwright test", "lint": "eslint .", "lint:fix": "eslint . --fix", "storybook:dev": "pnpm --filter @univerjs/storybook dev", @@ -41,31 +45,35 @@ "release:rc": "release-it prerelease --preRelease=rc" }, "devDependencies": { - "@antfu/eslint-config": "^2.11.0", - "@commitlint/cli": "^19.2.1", - "@commitlint/config-conventional": "^19.1.0", - "@playwright/test": "^1.42.1", + "@antfu/eslint-config": "^2.18.1", + "@commitlint/cli": "^19.3.0", + "@commitlint/config-conventional": "^19.2.2", + "@eslint-react/eslint-plugin": "^1.5.12", + "@playwright/test": "^1.44.0", "@release-it-plugins/workspaces": "^4.2.0", "@release-it/conventional-changelog": "^8.0.1", - "@storybook/react": "8.0.4", - "@types/node": "^20.11.30", - "@types/react": "^18.2.72", + "@storybook/react": "8.1.2", + "@types/node": "^20.12.12", + "@types/react": "^18.3.2", "@univerjs/design": "workspace:*", "@univerjs/shared": "workspace:*", "@vitejs/plugin-react": "^4.2.1", - "eslint": "^8.57.0", - "eslint-plugin-format": "^0.1.0", + "eslint": "8.57.0", + "eslint-plugin-format": "^0.1.1", "eslint-plugin-header": "^3.1.1", + "eslint-plugin-no-barrel-import": "^0.0.2", "eslint-plugin-react": "^7.34.1", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.6", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", "husky": "^9.0.11", - "lint-staged": "^15.2.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "release-it": "^17.1.1", - "turbo": "^1.13.0", - "typescript": "^5.4.3" + "lint-staged": "^15.2.4", + "react": "18.2.0", + "react-dom": "18.2.0", + "release-it": "^17.3.0", + "serve": "^14.2.3", + "turbo": "^1.13.3", + "typescript": "^5.4.5", + "vitest": "^1.6.0" }, "lint-staged": { "*": "eslint --fix" diff --git a/packages/core/README-zh.md b/packages/core/README-zh.md index 9a181d41a9..ef66fb9337 100644 --- a/packages/core/README-zh.md +++ b/packages/core/README-zh.md @@ -34,3 +34,25 @@ npm install @univerjs/core # 使用 pnpm pnpm add @univerjs/core ``` + +### 配置 + +```typescript +import { Univer } from '@univerjs/core'; + +new Univer({ + theme: defaultTheme, + locale: LocaleType.ZH_CN, + locales, + logLevel: LogLevel.VERBOSE, +}); +``` + +#### 选项 + +| 名称 | 类型 | 默认值 | 描述 | +| --- | --- | --- | --- | +| theme | [Theme](https://univer.ai/api/design/#built-in-themes) | - | 应用的主题,用于控制应用的外观。 | +| locale | [LocaleType](https://univer.ai/api/core/enums/LocaleType.html) | `LocaleType.ZH_CN` | 应用的语言环境,默认值为 `LocaleType.ZH_CN`。 | +| locales | [ILocales](https://univer.ai/api/core/interfaces/ILocales.html) | - | 应用支持的语言环境,默认支持中文。 | +| logLevel | [LogLevel](https://univer.ai/api/core/enums/LogLevel.html) | `LogLevel.SILENT` | 应用的日志级别。 | diff --git a/packages/core/README.md b/packages/core/README.md index 7e0cc75980..930fa315eb 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -34,3 +34,25 @@ npm install @univerjs/core # Using pnpm pnpm add @univerjs/core ``` + +### Configuration + +```typescript +import { Univer } from '@univerjs/core'; + +new Univer({ + theme: defaultTheme, + locale: LocaleType.ZH_CN, + locales, + logLevel: LogLevel.VERBOSE, +}); +``` + +#### Options + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| theme | [Theme](https://univer.ai/api/design/#built-in-themes) | - | The theme of the application, which is used to control the appearance of the application. | +| locale | [LocaleType](https://univer.ai/api/core/enums/LocaleType.html) | `LocaleType.ZH_CN` | The locale of the application. The default value is `LocaleType.ZH_CN`. +| locales | [ILocales](https://univer.ai/api/core/interfaces/ILocales.html) | - | The supported locales of the application. By default, the application supports Chinese. +| logLevel | [LogLevel](https://univer.ai/api/core/enums/LogLevel.html) | `LogLevel.SILENT` | The log level of the application. | diff --git a/packages/core/package.json b/packages/core/package.json index ecee1f64f4..5caedac0f0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@univerjs/core", - "version": "0.1.4", + "version": "0.1.12", "private": false, "description": "Core library for Univer.", "author": "DreamNum ", @@ -63,22 +63,22 @@ "build": "tsc && vite build" }, "peerDependencies": { - "@wendellhu/redi": "0.13.0", + "@wendellhu/redi": "0.15.2", "rxjs": ">=7.0.0" }, "dependencies": { - "@univerjs/protocol": "^0.1.14", - "dayjs": "^1.11.10", - "nanoid": "5.0.6", + "@univerjs/protocol": "^0.1.32", + "nanoid": "5.0.7", "numeral": "^2.0.6" }, "devDependencies": { "@types/numeral": "^2.0.5", "@univerjs/shared": "workspace:*", - "@wendellhu/redi": "^0.13.0", + "@wendellhu/redi": "0.15.2", + "dayjs": "^1.11.11", "rxjs": "^7.8.1", - "typescript": "^5.4.3", - "vite": "^5.1.6", - "vitest": "^1.4.0" + "typescript": "^5.4.5", + "vite": "^5.2.11", + "vitest": "^1.6.0" } } diff --git a/packages/core/src/basics/plugin-holder.ts b/packages/core/src/basics/plugin-holder.ts deleted file mode 100644 index dfbc5f3882..0000000000 --- a/packages/core/src/basics/plugin-holder.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright 2023-present DreamNum Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Ctor, Injector } from '@wendellhu/redi'; - -import type { Plugin, PluginCtor } from '../plugin/plugin'; -import { LifecycleStages } from '../services/lifecycle/lifecycle'; -import type { LifecycleInitializerService, LifecycleService } from '../services/lifecycle/lifecycle.service'; -import { Disposable, toDisposable } from '../shared/lifecycle'; - -export abstract class PluginHolder extends Disposable { - protected abstract get _lifecycleService(): LifecycleService; - protected abstract get _lifecycleInitializerService(): LifecycleInitializerService; - protected abstract get _injector(): Injector; - - protected _started: boolean = false; - - addPlugins(plugins: Array<[PluginCtor, any]>): void { - if (!this._started) { - const pluginInstances = plugins.map(([plugin, options]) => this._initPlugin(plugin, options)); - this._takePluginsThroughLifecycle(pluginInstances); - this._started = true; - } else { - const lazyPlugins = plugins.map(([plugin, options]) => this._initPlugin(plugin, options)); - this._pluginsRunLifecycle(lazyPlugins, LifecycleStages.Starting); - - setTimeout(() => this._takePluginsThroughLifecycle(lazyPlugins, true)); - } - } - - protected _takePluginsThroughLifecycle(plugins: Plugin[], skipStarting = false): void { - this.disposeWithMe( - toDisposable( - this._lifecycleService.subscribeWithPrevious().subscribe((stage) => { - if (skipStarting && stage === LifecycleStages.Starting) { - return; - } - - this._pluginsRunLifecycle(plugins, stage); - }) - ) - ); - } - - protected _pluginsRunLifecycle(plugins: Plugin[], lifecycle: LifecycleStages): void { - plugins.forEach((p) => { - switch (lifecycle) { - case LifecycleStages.Starting: - p.onStarting(this._injector); - break; - case LifecycleStages.Ready: - p.onReady(); - break; - case LifecycleStages.Rendered: - p.onRendered(); - break; - case LifecycleStages.Steady: - p.onSteady(); - break; - } - }); - - this._lifecycleInitializerService.initModulesOnStage(lifecycle); - } - - protected _initPlugin(plugin: PluginCtor, options: any): Plugin { - const pluginInstance: Plugin = this._injector.createInstance(plugin as unknown as Ctor, options); - return pluginInstance; - } -} diff --git a/packages/core/src/basics/univer-doc.ts b/packages/core/src/basics/univer-doc.ts deleted file mode 100644 index c59f87b6d7..0000000000 --- a/packages/core/src/basics/univer-doc.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright 2023-present DreamNum Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Inject, Injector } from '@wendellhu/redi'; - -import { DocumentDataModel } from '../docs/data-model/document-data-model'; -import { LifecycleInitializerService, LifecycleService } from '../services/lifecycle/lifecycle.service'; -import type { IDocumentData } from '../types/interfaces/i-document-data'; -import { PluginHolder } from './plugin-holder'; - -/** - * Externally provided UniverDoc root instance - */ -export class UniverDoc extends PluginHolder { - constructor( - @Inject(Injector) protected readonly _injector: Injector, - @Inject(LifecycleService) protected readonly _lifecycleService: LifecycleService, - @Inject(LifecycleInitializerService) - protected readonly _lifecycleInitializerService: LifecycleInitializerService - ) { - super(); - } - - createDoc(docData: Partial): DocumentDataModel { - return this._injector.createInstance(DocumentDataModel, docData); - } -} diff --git a/packages/core/src/basics/univer-sheet.ts b/packages/core/src/basics/univer-sheet.ts deleted file mode 100644 index adba49c8ff..0000000000 --- a/packages/core/src/basics/univer-sheet.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright 2023-present DreamNum Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { IDisposable } from '@wendellhu/redi'; -import { Inject, Injector } from '@wendellhu/redi'; - -import { LifecycleInitializerService, LifecycleService } from '../services/lifecycle/lifecycle.service'; -import { Workbook } from '../sheets/workbook'; -import type { IWorkbookData } from '../types/interfaces/i-workbook-data'; -import { PluginHolder } from './plugin-holder'; - -/** - * Externally provided UniverSheet root instance - */ -export class UniverSheet extends PluginHolder implements IDisposable { - constructor( - @Inject(Injector) protected readonly _injector: Injector, - @Inject(LifecycleService) protected readonly _lifecycleService: LifecycleService, - @Inject(LifecycleInitializerService) - protected readonly _lifecycleInitializerService: LifecycleInitializerService - ) { - super(); - } - - createSheet(workbookConfig: Partial): Workbook { - const workbook = this._injector.createInstance(Workbook, workbookConfig); - return workbook; - } -} diff --git a/packages/core/src/basics/univer-slide.ts b/packages/core/src/basics/univer-slide.ts deleted file mode 100644 index 72c806982a..0000000000 --- a/packages/core/src/basics/univer-slide.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright 2023-present DreamNum Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Inject, Injector } from '@wendellhu/redi'; - -import { LifecycleInitializerService, LifecycleService } from '../services/lifecycle/lifecycle.service'; -import { SlideDataModel } from '../slides/domain/slide-model'; -import type { ISlideData } from '../types/interfaces/i-slide-data'; -import { PluginHolder } from './plugin-holder'; - -/** - * Externally provided UniverSlide root instance - */ -export class UniverSlide extends PluginHolder { - constructor( - @Inject(Injector) protected readonly _injector: Injector, - @Inject(LifecycleService) protected readonly _lifecycleService: LifecycleService, - @Inject(LifecycleInitializerService) - protected readonly _lifecycleInitializerService: LifecycleInitializerService - ) { - super(); - } - - createSlide(data: Partial): SlideDataModel { - const slide = this._injector.createInstance(SlideDataModel, data); - return slide; - } -} diff --git a/packages/core/src/basics/univer.ts b/packages/core/src/basics/univer.ts deleted file mode 100644 index b51207f722..0000000000 --- a/packages/core/src/basics/univer.ts +++ /dev/null @@ -1,341 +0,0 @@ -/** - * Copyright 2023-present DreamNum Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Injector } from '@wendellhu/redi'; - -import type { DocumentDataModel } from '../docs/data-model/document-data-model'; -import type { Plugin, PluginCtor } from '../plugin/plugin'; -import { PluginRegistry, PluginStore, PluginType } from '../plugin/plugin'; -import { CommandService, ICommandService } from '../services/command/command.service'; -import { ConfigService, IConfigService } from '../services/config/config.service'; -import { ContextService, IContextService } from '../services/context/context.service'; -import { ErrorService } from '../services/error/error.service'; -import { - FloatingObjectManagerService, - IFloatingObjectManagerService, -} from '../services/floating-object/floating-object-manager.service'; -import { IUniverInstanceService, UniverInstanceService } from '../services/instance/instance.service'; -import { LifecycleStages } from '../services/lifecycle/lifecycle'; -import { LifecycleInitializerService, LifecycleService } from '../services/lifecycle/lifecycle.service'; -import { LocaleService } from '../services/locale/locale.service'; -import { DesktopLogService, ILogService } from '../services/log/log.service'; -import { IPermissionService, PermissionService } from '../services/permission/permission.service'; -import { UniverPermissionService } from '../services/permission/univer.permission.service'; -import { ResourceManagerService } from '../services/resource-manager/resource-manager.service'; -import { IResourceManagerService } from '../services/resource-manager/type'; -import { ThemeService } from '../services/theme/theme.service'; -import { IUndoRedoService, LocalUndoRedoService } from '../services/undoredo/undoredo.service'; -import type { Workbook } from '../sheets/workbook'; -import type { SlideDataModel } from '../slides/domain/slide-model'; -import type { LocaleType } from '../types/enum/locale-type'; -import type { IDocumentData, ISlideData, IUniverData, IWorkbookData } from '../types/interfaces'; -import { PluginHolder } from './plugin-holder'; -import { UniverDoc } from './univer-doc'; -import { UniverSheet } from './univer-sheet'; -import { UniverSlide } from './univer-slide'; - -const INIT_LAZY_PLUGINS_TIMEOUT = 200; - -export class Univer extends PluginHolder { - protected readonly _injector: Injector; - - private readonly _univerPluginStore = new PluginStore(); - private readonly _univerPluginRegistry = new PluginRegistry(); - - private _univerSheet: UniverSheet | null = null; - private _univerDoc: UniverDoc | null = null; - private _univerSlide: UniverSlide | null = null; - - private get _univerInstanceService(): IUniverInstanceService { - return this._injector.get(IUniverInstanceService); - } - - protected get _lifecycleService(): LifecycleService { - return this._injector.get(LifecycleService); - } - - protected get _lifecycleInitializerService(): LifecycleInitializerService { - return this._injector.get(LifecycleInitializerService); - } - - constructor(univerData: Partial = {}) { - super(); - - this._injector = this._initDependencies(); - - const { theme, locale, locales, logLevel } = univerData; - - theme && this._injector.get(ThemeService).setTheme(theme); - locales && this._injector.get(LocaleService).load(locales); - locale && this._injector.get(LocaleService).setLocale(locale); - logLevel && this._injector.get(ILogService).setLogLevel(logLevel); - } - - __getInjector(): Injector { - return this._injector; - } - - override dispose(): void { - this._injector.dispose(); - - super.dispose(); - } - - setLocale(locale: LocaleType) { - this._injector.get(LocaleService).setLocale(locale); - } - - /** - * Create a univer sheet instance with internal dependency injection. - */ - createUniverSheet(config: Partial): Workbook { - let workbook: Workbook; - const addSheet = () => { - workbook = this._univerSheet!.createSheet(config); - this._univerInstanceService.addSheet(workbook); - }; - - if (!this._univerSheet) { - this._tryProgressToStart(); - - const univerSheet = (this._univerSheet = this._injector.createInstance(UniverSheet)); - const sheetPlugins: Array<[PluginCtor, any]> = this._univerPluginRegistry - .getRegisterPlugins(PluginType.Sheet) - .map((p) => [p.plugin, p.options]); - this._univerPluginRegistry.clearPluginsOfType(PluginType.Sheet); - univerSheet.addPlugins(sheetPlugins); - - addSheet(); - - this._tryProgressToReady(); - } else { - addSheet(); - } - - return workbook!; - } - - createUniverDoc(config: Partial): DocumentDataModel { - let doc: DocumentDataModel; - const addDoc = () => { - doc = this._univerDoc!.createDoc(config); - this._univerInstanceService.addDoc(doc); - }; - - if (!this._univerDoc) { - this._tryProgressToStart(); - - const univerDoc = (this._univerDoc = this._injector.createInstance(UniverDoc)); - const docPlugins: Array<[PluginCtor, any]> = this._univerPluginRegistry - .getRegisterPlugins(PluginType.Doc) - .map((p) => [p.plugin, p.options]); - this._univerPluginRegistry.clearPluginsOfType(PluginType.Doc); - univerDoc.addPlugins(docPlugins); - - addDoc(); - - this._tryProgressToReady(); - } else { - addDoc(); - } - - return doc!; - } - - createUniverSlide(config: Partial): SlideDataModel { - let slide: SlideDataModel; - const addSlide = () => { - slide = this._univerSlide!.createSlide(config); - this._univerInstanceService.addSlide(slide); - }; - - if (!this._univerSlide) { - this._tryProgressToStart(); - - const univerSlide = (this._univerSlide = this._injector.createInstance(UniverSlide)); - const slidePlugins: Array<[PluginCtor, any]> = this._univerPluginRegistry - .getRegisterPlugins(PluginType.Slide) - .map((p) => [p.plugin, p.options]); - this._univerPluginRegistry.clearPluginsOfType(PluginType.Slide); - univerSlide.addPlugins(slidePlugins); - - addSlide(); - - this._tryProgressToReady(); - } else { - addSlide(); - } - - return slide!; - } - - private _initDependencies(): Injector { - return new Injector([ - [ - IUniverInstanceService, - { - useFactory: (contextService: IContextService) => - new UniverInstanceService( - { - createUniverDoc: (data) => this.createUniverDoc(data), - createUniverSheet: (data) => this.createUniverSheet(data), - createUniverSlide: (data) => this.createUniverSlide(data), - }, - contextService - ), - deps: [IContextService], - }, - ], - [ErrorService], - [LocaleService], - [ThemeService], - [LifecycleService], - [LifecycleInitializerService], - [IPermissionService, { useClass: PermissionService }], - [UniverPermissionService], - [ILogService, { useClass: DesktopLogService, lazy: true }], - [ICommandService, { useClass: CommandService, lazy: true }], - [IUndoRedoService, { useClass: LocalUndoRedoService, lazy: true }], - [IConfigService, { useClass: ConfigService }], - [IContextService, { useClass: ContextService }], - [IFloatingObjectManagerService, { useClass: FloatingObjectManagerService, lazy: true }], - [IResourceManagerService, { useClass: ResourceManagerService, lazy: true }], - ]); - } - - /** - * Initialize modules provided by Univer-type plugins. - */ - private _tryProgressToStart(): void { - if (this._started) { - return; - } - - this._injector.get(LifecycleInitializerService).start(); - this._started = true; - } - - private _tryProgressToReady(): void { - const lifecycleService = this._injector.get(LifecycleService); - if (lifecycleService.stage < LifecycleStages.Ready) { - this._injector.get(LifecycleService).stage = LifecycleStages.Ready; - this._univerPluginStore.forEachPlugin((p) => p.onReady()); - } - } - - // #region register plugins - - /** Register a plugin into univer. */ - registerPlugin>(plugin: T, config?: ConstructorParameters[0]): void { - if (plugin.type === PluginType.Univer) { - this._registerUniverPlugin(plugin, config); - } else if (plugin.type === PluginType.Sheet) { - this._registerSheetsPlugin(plugin, config); - } else if (plugin.type === PluginType.Doc) { - this._registerDocsPlugin(plugin, config); - } else if (plugin.type === PluginType.Slide) { - this._registerSlidesPlugin(plugin, config); - } else { - throw new Error(`Unimplemented plugin system for business: "${plugin.type}".`); - } - - // If Univer has already started, we should manually call onStarting for the plugin. - // We do that in an asynchronous way, because user may lazy load several plugins at the same time. - if (this._started) { - return this._scheduleInitPluginAfterStarted(); - } - } - - private _initLazyPluginsTimer?: number; - - private _scheduleInitPluginAfterStarted() { - if (this._initLazyPluginsTimer === undefined) { - this._initLazyPluginsTimer = setTimeout( - () => this._flushLazyPlugins(), - INIT_LAZY_PLUGINS_TIMEOUT - ) as unknown as number; - } - } - - private _flushLazyPlugins() { - this._initLazyPluginsTimer = undefined; - - const univerLazyPlugins = this._univerPluginRegistry.getRegisterPlugins(PluginType.Univer); - if (univerLazyPlugins.length) { - this._univerPluginRegistry.clearPluginsOfType(PluginType.Univer); - const pluginInstances = univerLazyPlugins.map((p) => this._injector.createInstance(p.plugin, p.options)); - this._takePluginsThroughLifecycle(pluginInstances); - } - - if (this._univerSheet) { - const sheetPlugins: Array<[PluginCtor, any]> = this._univerPluginRegistry - .getRegisterPlugins(PluginType.Sheet) - .map((p) => [p.plugin, p.options]); - - if (sheetPlugins.length) { - this._univerSheet.addPlugins(sheetPlugins); - this._univerPluginRegistry.clearPluginsOfType(PluginType.Sheet); - } - } - - if (this._univerDoc) { - const docPlugins: Array<[PluginCtor, any]> = this._univerPluginRegistry - .getRegisterPlugins(PluginType.Doc) - .map((p) => [p.plugin, p.options]); - - if (docPlugins.length) { - this._univerDoc.addPlugins(docPlugins); - this._univerPluginRegistry.clearPluginsOfType(PluginType.Doc); - } - } - - if (this._univerSlide) { - const slidePlugins: Array<[PluginCtor, any]> = this._univerPluginRegistry - .getRegisterPlugins(PluginType.Slide) - .map((p) => [p.plugin, p.options]); - - if (slidePlugins.length) { - this._univerSlide.addPlugins(slidePlugins); - this._univerPluginRegistry.clearPluginsOfType(PluginType.Slide); - } - } - } - - private _registerUniverPlugin(pluginCtor: PluginCtor, options?: any): void { - if (this._started) { - this._univerPluginRegistry.registerPlugin(pluginCtor, options); - } else { - // For plugins at Univer level. Plugins would be initialized immediately so they can register dependencies. - const pluginInstance: Plugin = this._injector.createInstance(pluginCtor, options); - pluginInstance.onStarting(this._injector); - this._univerPluginStore.addPlugin(pluginInstance); - } - } - - private _registerSheetsPlugin(pluginCtor: PluginCtor, options?: any) { - this._univerPluginRegistry.registerPlugin(pluginCtor, options); - } - - private _registerDocsPlugin(pluginCtor: PluginCtor, options?: any) { - this._univerPluginRegistry.registerPlugin(pluginCtor, options); - } - - private _registerSlidesPlugin(pluginCtor: PluginCtor, options?: any) { - this._univerPluginRegistry.registerPlugin(pluginCtor, options); - } - - // #endregion -} diff --git a/packages/core/src/common/boolean.ts b/packages/core/src/common/boolean.ts new file mode 100644 index 0000000000..295daeef48 --- /dev/null +++ b/packages/core/src/common/boolean.ts @@ -0,0 +1,19 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function isBooleanString(str: string): boolean { + return ['true', 'false'].includes(str.toLowerCase()); +} diff --git a/packages/core/src/common/const.ts b/packages/core/src/common/const.ts index 6a5bc0e048..46c68bd3f5 100644 --- a/packages/core/src/common/const.ts +++ b/packages/core/src/common/const.ts @@ -19,3 +19,11 @@ export const DOCS_NORMAL_EDITOR_UNIT_ID_KEY = '__defaultDocumentNormalEditorSpec export const DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY = '__defaultDocumentFormulaBarEditorSpecialUnitId_20231012__'; export const DEFAULT_EMPTY_DOCUMENT_VALUE = '\r\n'; + +export function createInternalEditorID(id: string) { + return `__internalEditorId__${id}`; +} + +export function isInternalEditorID(id: string) { + return id.startsWith('__'); +} diff --git a/packages/core/src/common/equal.ts b/packages/core/src/common/equal.ts new file mode 100644 index 0000000000..5d0c30e13d --- /dev/null +++ b/packages/core/src/common/equal.ts @@ -0,0 +1,81 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Rectangle } from '../shared/rectangle'; +import type { IRange, IUnitRange } from '../types/interfaces'; + +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const isRangesEqual = (oldRanges: IRange[], ranges: IRange[]) => { + return ranges.length === oldRanges.length && !oldRanges.some((oldRange) => ranges.some((range) => !Rectangle.equals(range, oldRange))); +}; + +export const isUnitRangesEqual = (oldRanges: IUnitRange[], ranges: IUnitRange[]) => { + return ranges.length === oldRanges.length && oldRanges.every((oldRange, i) => { + const current = ranges[i]; + return current.unitId === oldRange.unitId && current.sheetId === oldRange.sheetId && Rectangle.equals(oldRange.range, current.range); + }); +}; + +export function shallowEqual(objA: any, objB: any) { + if (Object.is(objA, objB)) { + return true; + } + + if (typeof objA !== 'object' || !objA || typeof objB !== 'object' || !objB) { + return false; + } + + const keysA = Object.keys(objA); + const keysB = Object.keys(objB); + + if (keysA.length !== keysB.length) { + return false; + } + + const bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB); + + // Test for A's keys different from B. + for (let idx = 0; idx < keysA.length; idx++) { + const key = keysA[idx]; + + if (!bHasOwnProperty(key)) { + return false; + } + + const valueA = objA[key]; + const valueB = objB[key]; + if (valueA !== valueB) { + return false; + } + } + + return true; +} diff --git a/packages/core/src/common/interceptor.ts b/packages/core/src/common/interceptor.ts index 3164485630..9773770050 100644 --- a/packages/core/src/common/interceptor.ts +++ b/packages/core/src/common/interceptor.ts @@ -15,7 +15,7 @@ */ import { remove } from './array'; -import type { Nullable } from './type-utils'; +import type { Nullable } from './type-util'; export type InterceptorHandler = ( value: Nullable, @@ -28,9 +28,9 @@ export interface IInterceptor { handler: InterceptorHandler; } -export const createInterceptorKey = (key: string) => { +export function createInterceptorKey(key: string): IInterceptor { const symbol = `sheet_interceptor_${key}`; - return symbol as unknown as IInterceptor; + return symbol as unknown as IInterceptor; // FIXME: priority and handler is completely missing? }; export type IComposeInterceptors = ( diff --git a/packages/core/src/shared/__test__/common.spec.ts b/packages/core/src/common/number.ts similarity index 68% rename from packages/core/src/shared/__test__/common.spec.ts rename to packages/core/src/common/number.ts index d33930e9ed..53424aca06 100644 --- a/packages/core/src/shared/__test__/common.spec.ts +++ b/packages/core/src/common/number.ts @@ -14,11 +14,15 @@ * limitations under the License. */ -import { describe, expect, it } from 'vitest'; -import { cellToRange } from '../common'; +export function isNumeric(str: string): boolean { + return /^-?\d+(\.\d+)?$/.test(str); +} -describe('Test common', () => { - it('Test cellToRange', () => { - expect(cellToRange(0, 1)).toStrictEqual({ startRow: 0, startColumn: 1, endRow: 0, endColumn: 1 }); - }); -}); +export function isSafeNumeric(str: string): boolean { + const numeric = isNumeric(str); + if (!numeric) { + return false; + } + + return Number(str) <= Number.MAX_SAFE_INTEGER; +} diff --git a/packages/core/src/basics/registry.ts b/packages/core/src/common/registry.ts similarity index 100% rename from packages/core/src/basics/registry.ts rename to packages/core/src/common/registry.ts diff --git a/packages/core/src/common/set.ts b/packages/core/src/common/set.ts new file mode 100644 index 0000000000..8cc37d0cfc --- /dev/null +++ b/packages/core/src/common/set.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Merge the second set to the first set. + * @param s1 the first set + * @param s2 the second set + * @returns the merged set + */ +export function mergeSets(s1: Set, s2: Set): Set { + s2.forEach((s) => s1.add(s)); + return s1; +} diff --git a/packages/core/src/common/type-util.ts b/packages/core/src/common/type-util.ts new file mode 100644 index 0000000000..4757c3d8b5 --- /dev/null +++ b/packages/core/src/common/type-util.ts @@ -0,0 +1,17 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type Nullable = T | null | undefined | void; diff --git a/packages/engine-formula/src/engine/utils/object-covert.ts b/packages/core/src/common/unit.ts similarity index 62% rename from packages/engine-formula/src/engine/utils/object-covert.ts rename to packages/core/src/common/unit.ts index a19fca15db..35e8092f7f 100644 --- a/packages/engine-formula/src/engine/utils/object-covert.ts +++ b/packages/core/src/common/unit.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import type { BaseValueObject } from '../value-object/base-value-object'; -import { NumberValueObject } from '../value-object/primitive-object'; +import type { UniverType } from '@univerjs/protocol'; +import { Disposable } from '../shared'; -export function convertTonNumber(valueObject: BaseValueObject) { - const currentValue = valueObject.getValue(); - let result = 0; - if (currentValue) { - result = 1; - } - return NumberValueObject.create(result); +export { UniverType as UniverInstanceType } from '@univerjs/protocol'; + +export type UnitType = UniverType | number; + +export abstract class UnitModel<_D = object, T extends UnitType = UnitType> extends Disposable { + abstract readonly type: T; + abstract getUnitId(): string; } diff --git a/packages/core/src/docs/data-model/__tests__/apply-utils.spec.ts b/packages/core/src/docs/data-model/__tests__/apply-utils.spec.ts index 0929d2d527..c22b2b3502 100644 --- a/packages/core/src/docs/data-model/__tests__/apply-utils.spec.ts +++ b/packages/core/src/docs/data-model/__tests__/apply-utils.spec.ts @@ -91,7 +91,7 @@ describe('test case in apply utils', () => { it('the inserted textRun is between two testRuns', async () => { const removedTextRuns = deleteTextRuns(body as IDocumentBody, 10, 20); - expect(removedTextRuns.length).toBe(0); + expect(removedTextRuns.length).toBe(1); expect(body?.textRuns![2].st).toBe(20); expect(body?.textRuns![2].ed).toBe(30); }); diff --git a/packages/core/src/docs/data-model/apply-utils/common.ts b/packages/core/src/docs/data-model/apply-utils/common.ts index c8f40e14b8..085bb89f54 100644 --- a/packages/core/src/docs/data-model/apply-utils/common.ts +++ b/packages/core/src/docs/data-model/apply-utils/common.ts @@ -378,6 +378,7 @@ export function insertCustomRanges( } } +// eslint-disable-next-line max-lines-per-function export function deleteTextRuns(body: IDocumentBody, textLength: number, currentIndex: number) { const { textRuns } = body; const startIndex = currentIndex; @@ -454,6 +455,16 @@ export function deleteTextRuns(body: IDocumentBody, textLength: number, currentI body.textRuns = newTextRuns; } + // In the case of no style before, add the style, removeTextRuns will be empty, + // in this case, you need to add an empty textRun for undo. + if (removeTextRuns.length === 0) { + removeTextRuns.push({ + st: 0, + ed: textLength, + ts: {}, + }); + } + return removeTextRuns; } diff --git a/packages/core/src/docs/data-model/apply-utils/update-apply.ts b/packages/core/src/docs/data-model/apply-utils/update-apply.ts index 52cf9e4cd4..2b3b35f98c 100644 --- a/packages/core/src/docs/data-model/apply-utils/update-apply.ts +++ b/packages/core/src/docs/data-model/apply-utils/update-apply.ts @@ -97,6 +97,7 @@ function updateTextRuns( return removeTextRuns; } +// eslint-disable-next-line max-lines-per-function export function coverTextRuns( updateDataTextRuns: ITextRun[], removeTextRuns: ITextRun[], diff --git a/packages/core/src/docs/data-model/document-data-model.ts b/packages/core/src/docs/data-model/document-data-model.ts index 002c3e180e..42f5f8fd8c 100644 --- a/packages/core/src/docs/data-model/document-data-model.ts +++ b/packages/core/src/docs/data-model/document-data-model.ts @@ -26,6 +26,7 @@ import type { IDocumentStyle, } from '../../types/interfaces/i-document-data'; import type { IPaddingData } from '../../types/interfaces/i-style-data'; +import { UnitModel, UniverInstanceType } from '../../common/unit'; import { updateAttributeByDelete } from './apply-utils/delete-apply'; import { updateAttributeByInsert } from './apply-utils/insert-apply'; import { updateAttribute } from './apply-utils/update-apply'; @@ -45,10 +46,18 @@ interface IDrawingUpdateConfig { width: number; } -class DocumentDataModelSimple { +class DocumentDataModelSimple extends UnitModel { + override type: UniverInstanceType.UNIVER_DOC = UniverInstanceType.UNIVER_DOC; + + override getUnitId(): string { + throw new Error('Method not implemented.'); + } + snapshot: IDocumentData; constructor(snapshot: Partial) { + super(); + this.snapshot = { ...DEFAULT_DOC, ...snapshot }; } @@ -89,10 +98,6 @@ class DocumentDataModelSimple { return this.snapshot.container; } - getParentRenderUnitId() { - return this.snapshot.parentRenderUnitId; - } - getSnapshot() { return this.snapshot; } @@ -207,10 +212,7 @@ export class DocumentDataModel extends DocumentDataModelSimple { footerModelMap: Map = new Map(); constructor(snapshot: Partial) { - if (Tools.isEmptyObject(snapshot)) { - snapshot = getEmptySnapshot(); - } - super(snapshot); + super(Tools.isEmptyObject(snapshot) ? getEmptySnapshot() : snapshot); const UNIT_ID_LENGTH = 6; @@ -219,7 +221,7 @@ export class DocumentDataModel extends DocumentDataModelSimple { this._initializeHeaderFooterModel(); } - dispose() { + override dispose() { this.headerModelMap.forEach((header) => { header.dispose(); }); @@ -265,7 +267,7 @@ export class DocumentDataModel extends DocumentDataModelSimple { return this as DocumentDataModel; } - getUnitId(): string { + override getUnitId(): string { return this._unitId; } diff --git a/packages/core/src/docs/data-model/text-x/action-iterator.ts b/packages/core/src/docs/data-model/text-x/action-iterator.ts index 9ed28f276d..03b70eb0ef 100644 --- a/packages/core/src/docs/data-model/text-x/action-iterator.ts +++ b/packages/core/src/docs/data-model/text-x/action-iterator.ts @@ -23,7 +23,9 @@ export class ActionIterator { private _index = 0; private _offset = 0; - constructor(private _actions: TextXAction[]) {} + constructor(private _actions: TextXAction[]) { + // empty + } hasNext() { return this.peekLength() < Number.POSITIVE_INFINITY; diff --git a/packages/core/src/docs/data-model/types.ts b/packages/core/src/docs/data-model/types.ts index 35397be759..825e22ddfd 100644 --- a/packages/core/src/docs/data-model/types.ts +++ b/packages/core/src/docs/data-model/types.ts @@ -24,7 +24,7 @@ export enum DataStreamTreeNodeType { TABLE, TABLE_ROW, TABLE_CELL, - // CUSTOM_BLOCK, // \b 图片 mention等不参与文档流的场景 + CUSTOM_BLOCK, // \b 图片 mention 等不参与文档流的场景 // TABLE_START, // \x1A 表格开始 // TABLE_ROW_START, // \x1B 表格开始 // TABLE_CELL_START, // \x1C 表格开始 diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f68de6d8f1..8dbf74e7df 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -16,12 +16,21 @@ import { installShims } from './common/shims'; -export * from './basics'; +export { type UnitType, UnitModel, UniverInstanceType } from './common/unit'; +export { Registry, RegistryAsMap } from './common/registry'; +export { Univer } from './univer'; +export { PluginHolder } from './services/plugin/plugin-holder'; +export { shallowEqual, isRangesEqual, isUnitRangesEqual } from './common/equal'; +export { isNumeric, isSafeNumeric } from './common/number'; +export { isBooleanString } from './common/boolean'; export { dedupe, remove, rotate, groupBy } from './common/array'; +export { mergeSets } from './common/set'; export { DEFAULT_EMPTY_DOCUMENT_VALUE, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, + createInternalEditorID, + isInternalEditorID, } from './common/const'; export { throttle } from './common/function'; export { MemoryCursor } from './common/memory-cursor'; @@ -35,12 +44,14 @@ export { type IInsertAction, type IRetainAction, } from './docs/data-model/action-types'; +export { DataValidationRenderMode } from './types/enum/data-validation-render-mode'; export { ActionIterator } from './docs/data-model/text-x/action-iterator'; export { getBodySlice, composeBody } from './docs/data-model/text-x/utils'; export { TextX } from './docs/data-model/text-x/text-x'; export { replaceInDocumentBody } from './docs/data-model/replacement'; export * from './observer'; -export { Plugin, PluginType } from './plugin/plugin'; +export { Plugin } from './services/plugin/plugin'; +export { PluginService } from './services/plugin/plugin.service'; export { type CommandListener, CommandService, @@ -71,7 +82,7 @@ export { type IFloatingObjectManagerSearchItemParam, IFloatingObjectManagerService, } from './services/floating-object/floating-object-manager.service'; -export { IUniverInstanceService, UniverInstanceType } from './services/instance/instance.service'; +export { IUniverInstanceService } from './services/instance/instance.service'; export { LifecycleStages, OnLifecycle, runOnLifecycle } from './services/lifecycle/lifecycle'; export { LifecycleService } from './services/lifecycle/lifecycle.service'; export { ILocalStorageService } from './services/local-storage/local-storage.service'; @@ -84,12 +95,14 @@ export { UniverEditablePermissionPoint, UniverPermissionService, } from './services/permission'; +export { IResourceLoaderService } from './services/resource-loader/type'; export { ResourceManagerService } from './services/resource-manager/resource-manager.service'; export type { IResourceHook } from './services/resource-manager/type'; -export { IResourceManagerService, ISnapshotPersistenceService } from './services/resource-manager/type'; +export { IResourceManagerService } from './services/resource-manager/type'; export { type IStyleSheet, ThemeService } from './services/theme/theme.service'; export { type IUndoRedoCommandInfos, + type IUndoRedoCommandInfosByInterceptor, type IUndoRedoItem, IUndoRedoService, type IUndoRedoStatus, @@ -101,12 +114,24 @@ export { } from './services/undoredo/undoredo.service'; export * from './shared'; export { fromCallback } from './shared/rxjs'; +export { UserManagerService } from './services/user-manager/user-manager.service'; // #region sheet + export type { IComposeInterceptors, IInterceptor, InterceptorHandler } from './common/interceptor'; export { composeInterceptors, createInterceptorKey, InterceptorManager } from './common/interceptor'; export { normalizeTextRuns } from './docs/data-model/apply-utils/common'; -export type { PluginCtor } from './plugin/plugin'; +export type { PluginCtor } from './services/plugin/plugin'; +export { type DependencyOverride, mergeOverrideWithDependencies } from './services/plugin/plugin-override'; +export * from './types/const'; +export * from './types/enum'; +export * from './types/interfaces'; +export { UniverInstanceService } from './services/instance/instance.service'; +export { LifecycleInitializerService } from './services/lifecycle/lifecycle.service'; +export { ConfigService } from './services/config/config.service'; + +// #region sheet + export { Range } from './sheets/range'; export { Styles } from './sheets/styles'; export { @@ -127,13 +152,10 @@ export { export { SheetViewModel } from './sheets/view-model'; export { getWorksheetUID, Workbook } from './sheets/workbook'; export { Worksheet, extractPureTextFromCell } from './sheets/worksheet'; -export * from './slides/domain'; +export { SlideDataModel } from './slides/slide-model'; export * from './types/const'; export * from './types/enum'; export * from './types/interfaces'; -export { UniverInstanceService } from './services/instance/instance.service'; -export { LifecycleInitializerService } from './services/lifecycle/lifecycle.service'; -export { ConfigService } from './services/config/config.service'; export { ISnapshotServerService } from './services/snapshot/snapshot-server.service'; export { transformSnapshotToWorkbookData, @@ -151,4 +173,15 @@ export { getSheetBlocksFromSnapshot } from './services/snapshot/snapshot-transfo export { isBlackColor, isWhiteColor } from './shared/color/color-kit'; export { cellToRange } from './shared/common'; +// #endregion + +export type { IDataValidationRule, IDataValidationRuleBase, IDataValidationRuleInfo, IDataValidationRuleOptions, ISheetDataValidationRule } from './types/interfaces/i-data-validation'; +export type { ICellCustomRender, ICellRenderContext } from './types/interfaces/i-cell-custom-render'; + +export { DataValidationErrorStyle } from './types/enum/data-validation-error-style'; +export { DataValidationImeMode } from './types/enum/data-validation-ime-mode'; +export { DataValidationOperator } from './types/enum/data-validation-operator'; +export { DataValidationType } from './types/enum/data-validation-type'; +export { DataValidationStatus } from './types/enum/data-validation-status'; + installShims(); diff --git a/packages/core/src/observer/observable.ts b/packages/core/src/observer/observable.ts index ed63973f2e..41c1ff7597 100644 --- a/packages/core/src/observer/observable.ts +++ b/packages/core/src/observer/observable.ts @@ -98,7 +98,9 @@ export class Observer { */ public callback: (eventData: T, eventState: EventState) => void, public observable: Observable - ) {} + ) { + // empty + } } /** diff --git a/packages/core/src/services/command/command.service.ts b/packages/core/src/services/command/command.service.ts index 57f70e03ce..ef14856ed5 100644 --- a/packages/core/src/services/command/command.service.ts +++ b/packages/core/src/services/command/command.service.ts @@ -131,6 +131,13 @@ export interface IExecutionOptions { export type CommandListener = (commandInfo: Readonly, options?: IExecutionOptions) => void; export interface ICommandService { + /** + * Check if a command is already registered at the current command service. + * + * @param commandId The id of the command. + */ + hasCommand(commandId: string): boolean; + registerCommand(command: ICommand): IDisposable; registerMultipleCommand(command: ICommand): IDisposable; @@ -141,6 +148,8 @@ export interface ICommandService { options?: IExecutionOptions ): Promise; + hasCommand(id: string): boolean; + syncExecuteCommand

(id: string, params?: P, options?: IExecutionOptions): R; /** @@ -162,6 +171,7 @@ export const ICommandService = createIdentifier('anywhere.comma */ export class CommandRegistry { private readonly _commands = new Map(); + private readonly _commandTypes = new Map(); registerCommand(command: ICommand): IDisposable { if (this._commands.has(command.id)) { @@ -169,14 +179,20 @@ export class CommandRegistry { } this._commands.set(command.id, command); + this._commandTypes.set(command.id, command.type); return toDisposable(() => { this._commands.delete(command.id); + this._commandTypes.delete(command.id); command.onDispose?.(); }); } + hasCommand(id: string): boolean { + return this._commands.has(id); + } + getCommand(id: string): [ICommand] | null { if (!this._commands.has(id)) { return null; @@ -184,12 +200,22 @@ export class CommandRegistry { return [this._commands.get(id)!]; } + + getCommandType(id: string): CommandType | undefined { + return this._commandTypes.get(id); + } } interface ICommandExecutionStackItem extends ICommandInfo {} +export const NilCommand: ICommand = { + id: 'nil', + type: CommandType.COMMAND, + handler: () => true, +}; + export class CommandService implements ICommandService { - private readonly _commandRegistry: CommandRegistry; + protected readonly _commandRegistry: CommandRegistry; private readonly _beforeCommandExecutionListeners: CommandListener[] = []; private readonly _commandExecutedListeners: CommandListener[] = []; @@ -208,6 +234,10 @@ export class CommandService implements ICommandService { this._registerCommand(NilCommand); } + hasCommand(commandId: string): boolean { + return this._commandRegistry.hasCommand(commandId); + } + registerCommand(command: ICommand): IDisposable { return this._registerCommand(command); } @@ -467,9 +497,3 @@ export function sequenceExecuteAsync( const promises = tasks.map((task) => () => commandService.executeCommand(task.id, task.params, options)); return sequenceAsync(promises); } - -export const NilCommand: ICommand = { - id: 'nil', - type: CommandType.COMMAND, - handler: () => true, -}; diff --git a/packages/core/src/services/context/context.ts b/packages/core/src/services/context/context.ts index cc087bed61..8a1014c2b0 100644 --- a/packages/core/src/services/context/context.ts +++ b/packages/core/src/services/context/context.ts @@ -23,6 +23,8 @@ export const FOCUSING_EDITOR_BUT_HIDDEN = 'FOCUSING_EDITOR_BUT_HIDDEN'; export const EDITOR_ACTIVATED = 'EDITOR_ACTIVATED'; export const FOCUSING_EDITOR_INPUT_FORMULA = 'FOCUSING_EDITOR_INPUT_FORMULA'; + +/** The focusing state of the formula editor (Fx bar). */ export const FOCUSING_FORMULA_EDITOR = 'FOCUSING_FORMULA_EDITOR'; export const FOCUSING_UNIVER_EDITOR = 'FOCUSING_UNIVER_EDITOR'; diff --git a/packages/core/src/services/floating-object/floating-object-manager.service.ts b/packages/core/src/services/floating-object/floating-object-manager.service.ts index 1c80aad2e2..e0001927c4 100644 --- a/packages/core/src/services/floating-object/floating-object-manager.service.ts +++ b/packages/core/src/services/floating-object/floating-object-manager.service.ts @@ -19,7 +19,7 @@ import { createIdentifier } from '@wendellhu/redi'; import type { Observable } from 'rxjs'; import { Subject } from 'rxjs'; -import type { Nullable } from '../../common/type-utils'; +import type { Nullable } from '../../common/type-util'; import type { ITransformState } from './floating-object-interfaces'; export const DEFAULT_DOCUMENT_SUB_COMPONENT_ID = '__default_document_sub_component_id20231101__'; @@ -34,6 +34,8 @@ export interface IFloatingObjectManagerSearchItemParam extends IFloatingObjectMa } export interface IFloatingObjectManagerParam extends IFloatingObjectManagerSearchItemParam { + // TODO: Maybe it shouldn't be here? + behindText?: boolean; // If it's true, put the float object behind the text, otherwise, put it in front of the text. floatingObject: ITransformState; } @@ -61,9 +63,7 @@ export interface IFloatingObjectManagerService { remove(searchItem: IFloatingObjectManagerSearchItemParam): void; - BatchAddOrUpdate(insertParam: IFloatingObjectManagerParam[]): void; - - remove(searchItem: IFloatingObjectManagerSearchItemParam): void; + batchAddOrUpdate(insertParam: IFloatingObjectManagerParam[]): void; pluginUpdateRefresh(searchObjects: IFloatingObjectManagerParam[]): void; } @@ -132,7 +132,7 @@ export class FloatingObjectManagerService implements IDisposable, IFloatingObjec this._andOrUpdate$.next(searchObjects); } - BatchAddOrUpdate(insertParams: IFloatingObjectManagerParam[]): void { + batchAddOrUpdate(insertParams: IFloatingObjectManagerParam[]): void { const searchObjects: IFloatingObjectManagerParam[] = []; insertParams.forEach((insertParam) => { searchObjects.push(...this._addByParam(insertParam)); @@ -166,7 +166,7 @@ export class FloatingObjectManagerService implements IDisposable, IFloatingObjec } private _addByParam(insertParam: IFloatingObjectManagerParam): IFloatingObjectManagerParam[] { - const { unitId, subUnitId, floatingObject, floatingObjectId } = insertParam; + const { unitId, subUnitId, floatingObject, floatingObjectId, behindText } = insertParam; if (!this._managerInfo.has(unitId)) { this._managerInfo.set(unitId, new Map()); @@ -180,7 +180,7 @@ export class FloatingObjectManagerService implements IDisposable, IFloatingObjec subComponentData.get(subUnitId)!.set(floatingObjectId, floatingObject); - return [{ unitId, subUnitId, floatingObjectId, floatingObject }]; + return [{ unitId, subUnitId, floatingObjectId, floatingObject, behindText }]; } private _clearByParam(param: IFloatingObjectManagerSearchParam): IFloatingObjectManagerParam[] { diff --git a/packages/core/src/services/instance/instance.service.ts b/packages/core/src/services/instance/instance.service.ts index 3a3edacba2..04232ac3cc 100644 --- a/packages/core/src/services/instance/instance.service.ts +++ b/packages/core/src/services/instance/instance.service.ts @@ -14,118 +14,79 @@ * limitations under the License. */ -import { createIdentifier } from '@wendellhu/redi'; +import type { IDisposable } from '@wendellhu/redi'; +import { createIdentifier, Inject, Injector } from '@wendellhu/redi'; import type { Observable } from 'rxjs'; -import { BehaviorSubject, Subject } from 'rxjs'; +import { BehaviorSubject, distinctUntilChanged, filter, map, Subject } from 'rxjs'; import { DocumentDataModel } from '../../docs/data-model/document-data-model'; import type { Nullable } from '../../shared'; import { Disposable } from '../../shared/lifecycle'; import { Workbook } from '../../sheets/workbook'; -import { SlideDataModel } from '../../slides/domain/slide-model'; -import type { IDocumentData, ISlideData, IWorkbookData } from '../../types/interfaces'; +import { SlideDataModel } from '../../slides/slide-model'; import { FOCUSING_DOC, FOCUSING_SHEET, FOCUSING_SLIDE } from '../context/context'; import { IContextService } from '../context/context.service'; - -export enum UniverInstanceType { - UNKNOWN = 0, - - DOC = 1, - - SHEET = 2, - - SLIDE = 3, -} - -export interface IUniverHandler { - createUniverDoc(data: Partial): DocumentDataModel; - createUniverSheet(data: Partial): Workbook; - createUniverSlide(data: Partial): SlideDataModel; -} +import type { UnitModel, UnitType } from '../../common/unit'; +import { UniverInstanceType } from '../../common/unit'; /** - * IUniverInstanceService holds all the current univer instances. And it also manages - * the focused univer instance. + * IUniverInstanceService holds all the current univer instances and provides a set of + * methods to add and remove univer instances. + * + * It also manages the focused univer instance. */ export interface IUniverInstanceService { + /** Omits value when a new UnitModel is created. */ + unitAdded$: Observable; + /** Subscribe to curtain type of units' creation. */ + getTypeOfUnitAdded$(type: UnitType): Observable; + + /** @interal */ + __addUnit(unit: UnitModel): void; + + /** Omits value when a UnitModel is disposed. */ + unitDisposed$: Observable; + /** Subscribe to curtain type of units' disposing. */ + getTypeOfUnitDisposed$(type: UnitType): Observable; + focused$: Observable>; + focusUnit(unitId: string | null): void; - currentSheet$: Observable>; - currentDoc$: Observable>; - currentSlide$: Observable>; + getFocusedUnit(): Nullable; - sheetAdded$: Observable; - docAdded$: Observable; - slideAdded$: Observable; + getCurrentUnitForType(type: UnitType): Nullable; + setCurrentUnitForType(unitId: string): void; - sheetDisposed$: Observable; - docDisposed$: Observable; - slideDisposed$: Observable; + getCurrentTypeOfUnit$(type: UnitType): Observable>; - focusUniverInstance(id: string | null): void; - getFocusedUniverInstance(): Nullable; + /** Create a unit with snapshot info. */ + createUnit(type: UnitType, data: Partial): U; + /** Dispose a unit */ + disposeUnit(unitId: string): boolean; - createDoc(data: Partial): DocumentDataModel; - createSheet(data: Partial): Workbook; - createSlide(data: Partial): SlideDataModel; + registerCtorForType(type: UnitType, ctor: new (...args: any[]) => T): IDisposable; + /** @deprecated */ changeDoc(unitId: string, doc: DocumentDataModel): void; - addDoc(doc: DocumentDataModel): void; - addSheet(sheet: Workbook): void; - addSlide(slide: SlideDataModel): void; - - getUniverSheetInstance(id: string): Nullable; - getUniverDocInstance(id: string): Nullable; - getUniverSlideInstance(id: string): Nullable; - - getCurrentUniverSheetInstance(): Workbook; - getCurrentUniverDocInstance(): DocumentDataModel; - getCurrentUniverSlideInstance(): SlideDataModel; - setCurrentUniverSheetInstance(id: string): void; - setCurrentUniverDocInstance(id: string): void; - setCurrentUniverSlideInstance(id: string): void; - - getAllUniverSheetsInstance(): Workbook[]; - getAllUniverDocsInstance(): DocumentDataModel[]; - getAllUniverSlidesInstance(): SlideDataModel[]; - - getDocumentType(unitId: string): UniverInstanceType; - disposeDocument(unitId: string): boolean; + + getUnit(id: string, type?: UnitType): Nullable; + getAllUnitsForType(type: UnitType): T[]; + getUnitType(unitId: string): UnitType; + + /** @deprecated */ + getUniverSheetInstance(unitId: string): Nullable; + /** @deprecated */ + getUniverDocInstance(unitId: string): Nullable; + /** @deprecated */ + getCurrentUniverDocInstance(): Nullable; } export const IUniverInstanceService = createIdentifier('univer.current'); export class UniverInstanceService extends Disposable implements IUniverInstanceService { - private _focused: DocumentDataModel | Workbook | SlideDataModel | null = null; - private readonly _focused$ = new BehaviorSubject>(null); - readonly focused$ = this._focused$.asObservable(); - - private readonly _currentSheet$ = new BehaviorSubject>(null); - readonly currentSheet$ = this._currentSheet$.asObservable(); - private readonly _currentDoc$ = new BehaviorSubject>(null); - readonly currentDoc$ = this._currentDoc$.asObservable(); - private readonly _currentSlide$ = new BehaviorSubject>(null); - readonly currentSlide$ = this._currentSlide$.asObservable(); - - private readonly _sheetAdded$ = new Subject(); - readonly sheetAdded$ = this._sheetAdded$.asObservable(); - private readonly _docAdded$ = new Subject(); - readonly docAdded$ = this._docAdded$.asObservable(); - private readonly _slideAdded$ = new Subject(); - readonly slideAdded$ = this._slideAdded$.asObservable(); - - private readonly _sheetDisposed$ = new Subject(); - readonly sheetDisposed$ = this._sheetDisposed$.asObservable(); - private readonly _docDisposed$ = new Subject(); - readonly docDisposed$ = this._docDisposed$.asObservable(); - private readonly _slideDisposed$ = new Subject(); - readonly slideDisposed$ = this._slideDisposed$.asObservable(); - - private readonly _sheets: Workbook[] = []; - private readonly _docs: DocumentDataModel[] = []; - private readonly _slides: SlideDataModel[] = []; + private readonly _unitsByType = new Map(); constructor( - private readonly _handler: IUniverHandler, + @Inject(Injector) private readonly _injector: Injector, @IContextService private readonly _contextService: IContextService ) { super(); @@ -135,193 +96,165 @@ export class UniverInstanceService extends Disposable implements IUniverInstance super.dispose(); this._focused$.complete(); - - this._currentDoc$.complete(); - this._currentSheet$.complete(); - this._currentSlide$.complete(); - - this._sheetAdded$.complete(); - this._docAdded$.complete(); - this._slideAdded$.complete(); - - this._sheetDisposed$.complete(); - this._docDisposed$.complete(); - this._slideDisposed$.complete(); } - createDoc(data: Partial): DocumentDataModel { - return this._handler.createUniverDoc(data); + private _createHandler!: (type: UnitType, data: unknown, ctor: new (...args: any[]) => UnitModel) => UnitModel; + __setCreateHandler(handler: (type: UnitType, data: unknown, ctor: new (...args: any[]) => UnitModel) => UnitModel): void { + this._createHandler = handler; } - createSheet(data: Partial): Workbook { - return this._handler.createUniverSheet(data); + createUnit(type: UnitType, data: T): U { + const model = this._createHandler(type, data, this._ctorByType.get(type)!); + return model as U; } - createSlide(data: Partial): SlideDataModel { - return this._handler.createUniverSlide(data); - } + private readonly _ctorByType = new Map UnitModel>(); + registerCtorForType(type: UnitType, ctor: new () => T): IDisposable { + this._ctorByType.set(type, ctor); - addSheet(sheet: Workbook): void { - this._sheets.push(sheet); - this._sheetAdded$.next(sheet); - this.setCurrentUniverSheetInstance(sheet.getUnitId()); + return { + dispose: () => { + this._ctorByType.delete(type); + }, + }; } - changeDoc(unitId: string, doc: DocumentDataModel): void { - const oldDoc = this._docs.find((doc) => doc.getUnitId() === unitId); - - if (oldDoc != null) { - const index = this._docs.indexOf(oldDoc); - this._docs.splice(index, 1); - } - - this.addDoc(doc); + private readonly _currentUnits$ = new BehaviorSubject<{ [type: UnitType]: Nullable }>({}); + readonly currentUnits$ = this._currentUnits$.asObservable(); + getCurrentTypeOfUnit$(type: number): Observable> { + return this.currentUnits$.pipe(map((units) => units[type] ?? null), distinctUntilChanged()) as Observable>; } - addDoc(doc: DocumentDataModel): void { - this._docs.push(doc); - this._docAdded$.next(doc); - this.setCurrentUniverDocInstance(doc.getUnitId()); + getCurrentUnitForType(type: UnitType): Nullable { + return this._currentUnits$.getValue()[type] as Nullable; } - addSlide(slide: SlideDataModel): void { - this._slides.push(slide); - this._slideAdded$.next(slide); - this.setCurrentUniverSlideInstance(slide.getUnitId()); - } + setCurrentUnitForType(unitId: string): void { + const result = this._getUnitById(unitId); + if (!result) throw new Error(`[UniverInstanceService]: no document with unitId ${unitId}!`); - getUniverSheetInstance(id: string): Nullable { - return this._sheets.find((sheet) => sheet.getUnitId() === id); + this._currentUnits$.next({ ...this._currentUnits$.getValue(), [result[1]]: result[0] }); } - getUniverDocInstance(id: string): Nullable { - return this._docs.find((doc) => doc.getUnitId() === id); + private readonly _unitAdded$ = new Subject(); + readonly unitAdded$ = this._unitAdded$.asObservable(); + getTypeOfUnitAdded$>(type: UnitType): Observable { + return this._unitAdded$.pipe(filter((unit) => unit.type === type)) as Observable; } - getUniverSlideInstance(id: string): Nullable { - return this._slides.find((slide) => slide.getUnitId() === id); - } + __addUnit(unit: UnitModel): void { + const type = unit.type; + + if (!this._unitsByType.has(type)) { + this._unitsByType.set(type, []); + } + + this._unitsByType.get(type)!.push(unit); - getAllUniverSheetsInstance() { - return this._sheets; + this._currentUnits$.next({ ...this._currentUnits$.getValue(), [type]: unit }); + this._unitAdded$.next(unit); } - getAllUniverDocsInstance() { - return this._docs; + private _unitDisposed$ = new Subject(); + unitDisposed$ = this._unitDisposed$.asObservable(); + getTypeOfUnitDisposed$>(type: UniverInstanceType): Observable { + return this.unitDisposed$.pipe(filter((unit) => unit.type === type)) as Observable; } - getAllUniverSlidesInstance() { - return this._slides; + getUnit(id: string, type?: UnitType): Nullable { + const unit = this._getUnitById(id)?.[0] as Nullable; + if (type && unit?.type !== type) return null; + return unit; } - setCurrentUniverSheetInstance(id: string): void { - this._currentSheet$.next(this.getUniverSheetInstance(id) || null); + getCurrentUniverDocInstance(): Nullable { + return this.getCurrentUnitForType(UniverInstanceType.UNIVER_DOC) as Nullable; } - setCurrentUniverSlideInstance(id: string): void { - this._currentSlide$.next(this.getUniverSlideInstance(id) || null); + getUniverDocInstance(unitId: string): Nullable { + return this.getUnit(unitId, UniverInstanceType.UNIVER_DOC); } - setCurrentUniverDocInstance(id: string): void { - this._currentDoc$.next(this.getUniverDocInstance(id) || null); + getUniverSheetInstance(unitId: string): Nullable { + return this.getUnit(unitId, UniverInstanceType.UNIVER_SHEET); } - getCurrentUniverSheetInstance(): Workbook { - const sheet = this._currentSheet$.getValue(); - if (!sheet) { - throw new Error('No current sheet!'); - } - return sheet; + getAllUnitsForType(type: UnitType): T[] { + return (this._unitsByType.get(type) ?? []) as T[]; } - getCurrentUniverDocInstance(): DocumentDataModel { - const doc = this._currentDoc$.getValue(); + changeDoc(unitId: string, doc: DocumentDataModel): void { + const allDocs = this.getAllUnitsForType(UniverInstanceType.UNIVER_DOC); + const oldDoc = allDocs.find((doc) => doc.getUnitId() === unitId); - if (!doc) { - throw new Error('No current doc!'); + if (oldDoc != null) { + const index = allDocs.indexOf(oldDoc); + allDocs.splice(index, 1); } - return doc; + this.__addUnit(doc); } - getCurrentUniverSlideInstance() { - const slide = this._currentSlide$.getValue(); - if (!slide) { - throw new Error('No current slide!'); - } - return slide; - } + private readonly _focused$ = new BehaviorSubject>(null); + readonly focused$ = this._focused$.asObservable(); + get focused(): Nullable { + const id = this._focused$.getValue(); + if (!id) return null; - focusUniverInstance(id: string | null): void { - if (id) { - this._focused = - this.getUniverSheetInstance(id) || - this.getUniverDocInstance(id) || - this.getUniverSlideInstance(id) || - null; - } + return this._getUnitById(id)?.[0]; + } + focusUnit(id: string | null): void { this._focused$.next(id); - [FOCUSING_DOC, FOCUSING_SHEET, FOCUSING_SLIDE].forEach((k) => this._contextService.setContextValue(k, false)); - - if (this._focused instanceof Workbook) { + if (this.focused instanceof Workbook) { + this._contextService.setContextValue(FOCUSING_DOC, false); this._contextService.setContextValue(FOCUSING_SHEET, true); - } else if (this._focused instanceof DocumentDataModel) { + this._contextService.setContextValue(FOCUSING_SLIDE, false); + } else if (this.focused instanceof DocumentDataModel) { this._contextService.setContextValue(FOCUSING_DOC, true); - } else if (this._focused instanceof SlideDataModel) { + this._contextService.setContextValue(FOCUSING_SHEET, false); + this._contextService.setContextValue(FOCUSING_SLIDE, false); + } else if (this.focused instanceof SlideDataModel) { + this._contextService.setContextValue(FOCUSING_DOC, false); + this._contextService.setContextValue(FOCUSING_SHEET, false); this._contextService.setContextValue(FOCUSING_SLIDE, true); } } - getFocusedUniverInstance(): Nullable { - return this._focused; + getFocusedUnit(): Nullable { + return this.focused; } - getDocumentType(unitId: string): UniverInstanceType { - if (this.getUniverDocInstance(unitId)) { - return UniverInstanceType.DOC; - } + getUnitType(unitId: string): UniverInstanceType { + const result = this._getUnitById(unitId); + if (!result) throw new Error(`[UniverInstanceService]: No document with unitId ${unitId}`); - if (this.getUniverSheetInstance(unitId)) { - return UniverInstanceType.SHEET; - } + return result[1]; + } - if (this.getUniverSlideInstance(unitId)) { - return UniverInstanceType.SLIDE; - } + disposeUnit(unitId: string): boolean { + const result = this._getUnitById(unitId); + if (!result) return false; - throw new Error(`[UniverInstanceService]: No document with unitId ${unitId}`); - } + const [unit, type] = result; + const units = this._unitsByType.get(type)!; + const index = units.indexOf(unit); + units.splice(index, 1); - disposeDocument(unitId: string): boolean { - const doc = this.getUniverDocInstance(unitId); - if (doc) { - const index = this._docs.indexOf(doc); - this._docs.splice(index, 1); - this._docDisposed$.next(doc); - // this.focusUniverInstance(null); - return true; - } + this._unitDisposed$.next(unit); + this._currentUnits$.next({ ...this._currentUnits$.getValue(), [type]: null }); + this._focused$.next(null); - const sheet = this.getUniverSheetInstance(unitId); - if (sheet) { - const index = this._sheets.indexOf(sheet); - this._sheets.splice(index, 1); - this._sheetDisposed$.next(sheet); - // this.focusUniverInstance(null); - return true; - } + return true; + } - const slide = this.getUniverSlideInstance(unitId); - if (slide) { - const index = this._slides.indexOf(slide); - this._slides.splice(index, 1); - this._slideDisposed$.next(slide); - // this.focusUniverInstance(null); - return true; + private _getUnitById(unitId: string): Nullable<[UnitModel, UnitType]> { + for (const [type, units] of this._unitsByType) { + const unit = units.find((unit) => unit.getUnitId() === unitId); + if (unit) { + return [unit, type]; + } } - - return false; } } diff --git a/packages/core/src/services/lifecycle/__tests__/lifecycle.service.spec.ts b/packages/core/src/services/lifecycle/__tests__/lifecycle.service.spec.ts index 15773c1932..34eb21a170 100644 --- a/packages/core/src/services/lifecycle/__tests__/lifecycle.service.spec.ts +++ b/packages/core/src/services/lifecycle/__tests__/lifecycle.service.spec.ts @@ -18,7 +18,7 @@ import { Injector } from '@wendellhu/redi'; import { afterEach, describe, expect, it } from 'vitest'; import { DesktopLogService, ILogService } from '../../log/log.service'; -import { LifecycleStages, OnLifecycle } from '../lifecycle'; +import { LifecycleStages } from '../lifecycle'; import { LifecycleInitializerService, LifecycleService } from '../lifecycle.service'; function createLifecycleTestBed() { @@ -118,33 +118,33 @@ describe('Test LifecycleService', () => { expect(lifecycleStages5).toEqual(steadyStages); }); - describe('Test automatically instantiate modules on lifecycle stages', () => { - let initializer: LifecycleInitializerService; + // describe('Test automatically instantiate modules on lifecycle stages', () => { + // let initializer: LifecycleInitializerService; - it('Should instantiate modules on lifecycle stages', () => { - injector = createLifecycleTestBed().injector; - initializer = injector.get(LifecycleInitializerService); - lifecycleService = injector.get(LifecycleService); + // it('Should instantiate modules on lifecycle stages', () => { + // injector = createLifecycleTestBed().injector; + // initializer = injector.get(LifecycleInitializerService); + // lifecycleService = injector.get(LifecycleService); - initializer.start(); - initializer.start(); // For just test coverage. + // initializer.start(); + // initializer.start(); // For just test coverage. - const initModules: string[] = []; + // const initModules: string[] = []; - @OnLifecycle(LifecycleStages.Rendered, TestModule1) - class TestModule1 { - constructor() { - initModules.push('test1'); - } - } - injector.add([TestModule1]); + // @OnLifecycle(LifecycleStages.Rendered, TestModule1) + // class TestModule1 { + // constructor() { + // initModules.push('test1'); + // } + // } + // injector.add([TestModule1]); - lifecycleService.stage = LifecycleStages.Starting; - lifecycleService.stage = LifecycleStages.Ready; - expect(initModules).toEqual([]); + // lifecycleService.stage = LifecycleStages.Starting; + // lifecycleService.stage = LifecycleStages.Ready; + // expect(initModules).toEqual([]); - lifecycleService.stage = LifecycleStages.Rendered; - expect(initModules).toEqual(['test1']); - }); - }); + // lifecycleService.stage = LifecycleStages.Rendered; + // expect(initModules).toEqual(['test1']); + // }); + // }); }); diff --git a/packages/core/src/services/lifecycle/lifecycle.service.ts b/packages/core/src/services/lifecycle/lifecycle.service.ts index c5cefa1ce9..cc75848deb 100644 --- a/packages/core/src/services/lifecycle/lifecycle.service.ts +++ b/packages/core/src/services/lifecycle/lifecycle.service.ts @@ -14,10 +14,11 @@ * limitations under the License. */ +import type { DependencyIdentifier } from '@wendellhu/redi'; import { Inject, Injector } from '@wendellhu/redi'; import { BehaviorSubject, Observable } from 'rxjs'; -import { Disposable, toDisposable } from '../../shared/lifecycle'; +import { Disposable } from '../../shared/lifecycle'; import { ILogService } from '../log/log.service'; import { LifecycleNameMap, LifecycleStages, LifecycleToModules } from './lifecycle'; @@ -30,6 +31,8 @@ export class LifecycleService extends Disposable { private _lifecycle$ = new BehaviorSubject(LifecycleStages.Starting); readonly lifecycle$ = this._lifecycle$.asObservable(); + private _lock = false; + constructor(@ILogService private readonly _logService: ILogService) { super(); @@ -49,8 +52,15 @@ export class LifecycleService extends Disposable { return; } + if (this._lock) { + throw new Error('[LifecycleService]: cannot set new stage when related logic is all handled!'); + } + this._lock = true; + this._reportProgress(stage); this._lifecycle$.next(stage); + + this._lock = false; } override dispose(): void { @@ -82,7 +92,13 @@ export class LifecycleService extends Disposable { subscriber.next(LifecycleStages.Rendered); } - return this._lifecycle$.subscribe(subscriber); + this._lifecycle$.subscribe((stage) => { + subscriber.next(stage); + + if (stage === LifecycleStages.Steady) { + subscriber.complete(); + } + }); }); } @@ -98,7 +114,7 @@ export class LifecycleService extends Disposable { * @internal */ export class LifecycleInitializerService extends Disposable { - private _started = false; + private _seenTokens = new Set>(); constructor( @Inject(LifecycleService) private _lifecycleService: LifecycleService, @@ -107,24 +123,13 @@ export class LifecycleInitializerService extends Disposable { super(); } - start(): void { - if (this._started) { - return; - } - - this._started = true; - this.disposeWithMe( - toDisposable( - this._lifecycleService.subscribeWithPrevious().subscribe((stage) => this.initModulesOnStage(stage)) - ) - ); - } - initModulesOnStage(stage: LifecycleStages): void { - const modules = LifecycleToModules.get(stage); - modules?.forEach((m) => { - if (this._injector.has(m)) { + LifecycleToModules.get(stage)?.forEach((m) => { + if (this._injector.has(m) && !this._seenTokens.has(m)) { this._injector.get(m); + + // swap these two lines and they will be fixed + this._seenTokens.add(m); } }); } diff --git a/packages/core/src/services/lifecycle/lifecycle.ts b/packages/core/src/services/lifecycle/lifecycle.ts index 103173d301..d0e06f395d 100644 --- a/packages/core/src/services/lifecycle/lifecycle.ts +++ b/packages/core/src/services/lifecycle/lifecycle.ts @@ -52,7 +52,24 @@ export const LifecycleNameMap = { export const LifecycleToModules = new Map>>(); /** - * Register some modules here that will automatically run when Univer progressed to a certain lifecycle stage + * Register the decorated class to be automatically instantiated when Univer progresses to the certain lifecycle stage. + * + * @param lifecycleStage The lifecycle stage to instantiate this class. + * @param identifier The dependency identifier of the class. Usually, it is the class itself unless you bind this class + * with another injection identifier. + * + * + * @example + * // Ignore the `\` below. This is JSDoc's bug. + * \@OnLifecycle(LifecycleStages.Ready, MyService) + * class MyService { + * } + * + * @example + * // Ignore the `\` below. This is JSDoc's bug. + * \@OnLifecycle(LifecycleStages.Rendered, IMyService) + * class MyService implements IMyService { + * } */ export function OnLifecycle(lifecycleStage: LifecycleStages, identifier: DependencyIdentifier) { const decorator = function decorator(_: Ctor) { @@ -62,6 +79,19 @@ export function OnLifecycle(lifecycleStage: LifecycleStages, identifier: Depende return decorator; } +/** + * Register a dependency to be automatically instantiated when Univer progresses to the certain lifecycle stage. + * + * @param lifecycleStage The lifecycle stage to instantiate this dependency. + * @param identifier The dependencies' identifier. **Beware** that if the dependency (e.g. a class) is bound to an + * identifier, you should register the identifier instead of the dependency itself. + * + * @example + * runOnLifecycle(LifecycleStages.Ready, MyService); + * + * @example + * runOnLifecycle(LifecycleStages.Rendered, IMyService); + */ export function runOnLifecycle(lifecycleStage: LifecycleStages, identifier: DependencyIdentifier) { if (!LifecycleToModules.has(lifecycleStage)) { LifecycleToModules.set(lifecycleStage, []); diff --git a/packages/core/src/services/locale/locale.service.ts b/packages/core/src/services/locale/locale.service.ts index 774e50f2ad..c54f305950 100644 --- a/packages/core/src/services/locale/locale.service.ts +++ b/packages/core/src/services/locale/locale.service.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Subject } from 'rxjs'; +import { BehaviorSubject, Subject } from 'rxjs'; import { Disposable, toDisposable } from '../../shared/lifecycle'; import type { ILanguagePack, ILocales, LanguageValue } from '../../shared/locale'; @@ -25,7 +25,9 @@ import { LocaleType } from '../../types/enum/locale-type'; * This service provides i18n and timezone / location features to other modules. */ export class LocaleService extends Disposable { - private _currentLocale: LocaleType = LocaleType.ZH_CN; + private _currentLocale$ = new BehaviorSubject(LocaleType.ZH_CN); + readonly currentLocale$ = this._currentLocale$.asObservable(); + private get _currentLocale(): LocaleType { return this._currentLocale$.value; } private _locales: ILocales | null = null; @@ -74,25 +76,8 @@ export class LocaleService extends Disposable { t = (key: string, ...args: string[]): string => { if (!this._locales) throw new Error('Locale not initialized'); - function resolveKeyPath(obj: ILanguagePack, keys: string[]): LanguageValue | null { - const currentKey = keys.shift(); - - if (currentKey && obj && currentKey in obj) { - const nextObj = (obj as ILanguagePack)[currentKey]; - - if (keys.length > 0 && (typeof nextObj === 'object' || Array.isArray(nextObj))) { - return resolveKeyPath(nextObj as ILanguagePack, keys); - } else { - return nextObj; - } - } - - return null; - } - const keys = key.split('.'); - const resolvedValue = resolveKeyPath(this._locales[this._currentLocale], keys); - + const resolvedValue = this.resolveKeyPath(this._locales[this._currentLocale], keys); if (typeof resolvedValue === 'string') { let result = resolvedValue; args.forEach((arg, index) => { @@ -105,7 +90,7 @@ export class LocaleService extends Disposable { }; setLocale(locale: LocaleType) { - this._currentLocale = locale; + this._currentLocale$.next(locale); this.localeChanged$.next(); } @@ -116,4 +101,20 @@ export class LocaleService extends Disposable { getCurrentLocale() { return this._currentLocale; } + + public resolveKeyPath(obj: ILanguagePack, keys: string[]): LanguageValue | null { + const currentKey = keys.shift(); + + if (currentKey && obj && currentKey in obj) { + const nextObj = (obj as ILanguagePack)[currentKey]; + + if (keys.length > 0 && (typeof nextObj === 'object' || Array.isArray(nextObj))) { + return this.resolveKeyPath(nextObj as ILanguagePack, keys); + } else { + return nextObj; + } + } + + return null; + } } diff --git a/packages/core/src/services/permission/permission.service.ts b/packages/core/src/services/permission/permission.service.ts index 764362c15d..aca6c2c939 100644 --- a/packages/core/src/services/permission/permission.service.ts +++ b/packages/core/src/services/permission/permission.service.ts @@ -20,7 +20,7 @@ import { BehaviorSubject, combineLatest } from 'rxjs'; import { map } from 'rxjs/operators'; import type { PermissionPoint } from '../../shared'; -import { Disposable, PermissionStatus, toDisposable } from '../../shared'; +import { Disposable, PermissionStatus } from '../../shared'; import type { Nullable } from '../../shared/types'; import { IUniverInstanceService } from '../instance/instance.service'; import { LifecycleStages, OnLifecycle } from '../lifecycle/lifecycle'; @@ -50,34 +50,34 @@ export class PermissionService extends Disposable implements IPermissionService // this._init(); } - private _init() { - this.disposeWithMe( - toDisposable( - this._univerInstanceService.sheetAdded$.subscribe((workbook) => { - this._resourceManagerService.registerPluginResource(workbook.getUnitId(), resourceKey, { - onChange: (unitID, value) => { - (value as PermissionPoint[]).forEach((permissionPoint) => { - if (this.getPermissionPoint(unitID, permissionPoint.id)) { - this.updatePermissionPoint(unitID, permissionPoint.id, permissionPoint.value); - } else { - this.addPermissionPoint(unitID, permissionPoint); - } - }); - }, - toJson: (unitID: string) => this._toJson(unitID), - parseJson: (json: string) => this._parseJson(json), - }); - }) - ) - ); - this.disposeWithMe( - toDisposable( - this._univerInstanceService.sheetDisposed$.subscribe((workbook) => { - this._resourceManagerService.disposePluginResource(workbook.getUnitId(), resourceKey); - }) - ) - ); - } + // private _init() { + // this.disposeWithMe( + // toDisposable( + // this._univerInstanceService.getTypeOfUnitAdded$(UniverInstanceType.UNIVER_SHEET).subscribe((workbook) => { + // this._resourceManagerService.registerPluginResource(workbook.getUnitId(), resourceKey, { + // onChange: (unitID, value) => { + // (value as PermissionPoint[]).forEach((permissionPoint) => { + // if (this.getPermissionPoint(unitID, permissionPoint.id)) { + // this.updatePermissionPoint(unitID, permissionPoint.id, permissionPoint.value); + // } else { + // this.addPermissionPoint(unitID, permissionPoint); + // } + // }); + // }, + // toJson: (unitID: string) => this._toJson(unitID), + // parseJson: (json: string) => this._parseJson(json), + // }); + // }) + // ) + // ); + // this.disposeWithMe( + // toDisposable( + // this._univerInstanceService.getTypeOfUnitDisposed$(UniverInstanceType.UNIVER_SHEET).subscribe((workbook) => { + // this._resourceManagerService.disposePluginResource(workbook.getUnitId(), resourceKey); + // }) + // ) + // ); + // } private _toJson(unitID: string) { const permissionMap = this._permissionPointMap.get(unitID); diff --git a/packages/core/src/services/permission/univer.permission.service.ts b/packages/core/src/services/permission/univer.permission.service.ts index df28247310..330a80c7cf 100644 --- a/packages/core/src/services/permission/univer.permission.service.ts +++ b/packages/core/src/services/permission/univer.permission.service.ts @@ -19,6 +19,7 @@ import { Inject } from '@wendellhu/redi'; import { Disposable } from '../../shared'; import { IUniverInstanceService } from '../instance/instance.service'; import { LifecycleStages, OnLifecycle } from '../lifecycle/lifecycle'; +import { UniverInstanceType } from '../../common/unit'; import { IPermissionService } from './permission.service'; import { UniverEditablePermission } from './permission-point'; @@ -33,24 +34,24 @@ export class UniverPermissionService extends Disposable { } private _init() { - this._univerInstanceService.sheetAdded$.subscribe((workbook) => { + this._univerInstanceService.getTypeOfUnitAdded$(UniverInstanceType.UNIVER_SHEET).subscribe((workbook) => { const univerEditablePermission = new UniverEditablePermission(workbook.getUnitId()); this._permissionService.addPermissionPoint(workbook.getUnitId(), univerEditablePermission); }); } - getEditable(unitID?: string) { - let unitId = unitID; + getEditable(unitId = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET)?.getUnitId()) { if (!unitId) { - unitId = this._univerInstanceService.getCurrentUniverSheetInstance().getUnitId(); + return; } + const univerEditablePermission = new UniverEditablePermission(unitId); const permission = this._permissionService.getPermissionPoint(unitId, univerEditablePermission.id); return permission?.value; } - setEditable(unitID: string, v: boolean) { - const univerEditablePermission = new UniverEditablePermission(unitID); - this._permissionService.updatePermissionPoint(unitID, univerEditablePermission.id, v); + setEditable(unitId: string, v: boolean) { + const univerEditablePermission = new UniverEditablePermission(unitId); + this._permissionService.updatePermissionPoint(unitId, univerEditablePermission.id, v); } } diff --git a/packages/core/src/services/plugin/__tests__/plugin-override.spec.ts b/packages/core/src/services/plugin/__tests__/plugin-override.spec.ts new file mode 100644 index 0000000000..1b3f57945b --- /dev/null +++ b/packages/core/src/services/plugin/__tests__/plugin-override.spec.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createIdentifier } from '@wendellhu/redi'; +import { describe, expect, it } from 'vitest'; +import { mergeOverrideWithDependencies } from '../plugin-override'; + +describe('test dependency override', () => { + it('should override dependencies', () => { + class A {} + interface IA {} + const IA = createIdentifier('IA'); + + expect(mergeOverrideWithDependencies([[A]], [[A, null]])) + .toEqual([]); + + expect(mergeOverrideWithDependencies([[IA, { useClass: A }]], [[IA, null]])) + .toEqual([]); + + expect(mergeOverrideWithDependencies([[A], [IA, { useClass: A }]], [[IA, { useValue: {} }]])) + .toEqual([[A], [IA, { useValue: {} }]]); + }); +}); diff --git a/packages/core/src/services/plugin/plugin-holder.ts b/packages/core/src/services/plugin/plugin-holder.ts new file mode 100644 index 0000000000..1424347174 --- /dev/null +++ b/packages/core/src/services/plugin/plugin-holder.ts @@ -0,0 +1,111 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable ts/no-explicit-any */ + +import type { Ctor } from '@wendellhu/redi'; +import { Inject, Injector } from '@wendellhu/redi'; + +import { finalize } from 'rxjs'; +import { LifecycleStages } from '../lifecycle/lifecycle'; +import { LifecycleInitializerService, LifecycleService } from '../lifecycle/lifecycle.service'; +import { Disposable } from '../../shared/lifecycle'; +import { ILogService } from '../log/log.service'; +import { type Plugin, type PluginCtor, PluginRegistry, PluginStore } from './plugin'; + +export class PluginHolder extends Disposable { + protected _started: boolean = false; + get started(): boolean { return this._started; } + + protected readonly _pluginRegistered = new Set(); + protected readonly _pluginStore = new PluginStore(); + protected readonly _pluginRegistry = new PluginRegistry(); + + constructor( + @ILogService protected readonly _logService: ILogService, + @Inject(Injector) protected readonly _injector: Injector, + @Inject(LifecycleService) protected readonly _lifecycleService: LifecycleService, + @Inject(LifecycleInitializerService) protected readonly _lifecycleInitializerService: LifecycleInitializerService + ) { + super(); + } + + override dispose(): void { + super.dispose(); + + this._pluginStore.forEachPlugin((plugin) => plugin.dispose()); + this._pluginStore.removePlugins(); + this._pluginRegistry.removePlugins(); + this._pluginRegistered.clear(); + } + + registerPlugin>(pluginCtor: T, config?: ConstructorParameters[0]): void { + const { pluginName } = pluginCtor; + if (this._pluginRegistered.has(pluginName)) { + this._logService.warn('[PluginService]', `plugin ${pluginName} has already been registered. This registration will be ignored.`); + return; + } + + this._pluginRegistered.add(pluginName); + this._pluginRegistry.registerPlugin(pluginCtor, config); + } + + start(): void { + if (this._started) return; + this._started = true; + + this.flush(); + } + + flush(): void { + if (!this._started) return; + + const plugins = this._pluginRegistry.getRegisterPlugins().map(({ plugin, options }) => this._initPlugin(plugin, options)); + this._pluginRegistry.removePlugins(); + + const subscription = this.disposeWithMe(this._lifecycleService.subscribeWithPrevious() + // It has to been async because the finalize may execute synchronously after we + // make the subscription. For example, the lifecycle service is already in stage "steady". + .pipe(finalize(() => { Promise.resolve().then(() => subscription.dispose()); })) + .subscribe((stage) => { this._pluginsRunLifecycle(plugins, stage); })); + } + + protected _pluginsRunLifecycle(plugins: Plugin[], lifecycle: LifecycleStages): void { + plugins.forEach((p) => { + switch (lifecycle) { + case LifecycleStages.Starting: + p.onStarting(this._injector); + break; + case LifecycleStages.Ready: + p.onReady(); + break; + case LifecycleStages.Rendered: + p.onRendered(); + break; + case LifecycleStages.Steady: + p.onSteady(); + break; + } + }); + + this._lifecycleInitializerService.initModulesOnStage(lifecycle); + } + + protected _initPlugin(plugin: PluginCtor, options: any): Plugin { + const pluginInstance: Plugin = this._injector.createInstance(plugin as unknown as Ctor, options); + return pluginInstance; + } +} diff --git a/packages/core/src/services/plugin/plugin-override.ts b/packages/core/src/services/plugin/plugin-override.ts new file mode 100644 index 0000000000..7c6306a934 --- /dev/null +++ b/packages/core/src/services/plugin/plugin-override.ts @@ -0,0 +1,43 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Dependency, DependencyIdentifier, DependencyItem } from '@wendellhu/redi'; + +export type NullableDependencyPair = [DependencyIdentifier, DependencyItem | null]; + +/** + * Overrides the dependencies defined in the plugin. Only dependencies that are identified by `IdentifierDecorator` can be overridden. + * If you override a dependency with `null`, the original dependency will be removed. + */ +// eslint-disable-next-line ts/no-explicit-any +export type DependencyOverride = NullableDependencyPair[]; + +export function mergeOverrideWithDependencies(dependencies: Dependency[], override?: DependencyOverride): Dependency[] { + if (!override) return dependencies; + + const result: Dependency[] = []; + for (const dependency of dependencies) { + const overrideItem = override.find(([identifier]) => identifier === dependency[0]); + if (overrideItem) { + if (overrideItem[1] === null) continue; + result.push([dependency[0], overrideItem[1]]); + } else { + result.push(dependency); + } + } + + return result; +} diff --git a/packages/core/src/services/plugin/plugin.service.ts b/packages/core/src/services/plugin/plugin.service.ts new file mode 100644 index 0000000000..3d6abba425 --- /dev/null +++ b/packages/core/src/services/plugin/plugin.service.ts @@ -0,0 +1,125 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IDisposable } from '@wendellhu/redi'; +import { Inject, Injector } from '@wendellhu/redi'; +import { type UnitType, UniverInstanceType } from '../../common/unit'; +import { PluginHolder } from './plugin-holder'; +import type { Plugin, PluginCtor } from './plugin'; + +const INIT_LAZY_PLUGINS_TIMEOUT = 4; + +/** + * This service manages plugin registration. + */ +export class PluginService implements IDisposable { + private readonly _pluginHolderForUniver: PluginHolder; + private readonly _pluginHoldersForTypes = new Map(); + + constructor( + @Inject(Injector) private readonly _injector: Injector + ) { + this._pluginHolderForUniver = this._injector.createInstance(PluginHolder); + this._pluginHolderForUniver.start(); + } + + dispose(): void { + this._clearFlushTimer(); + + for (const holder of this._pluginHoldersForTypes.values()) { + holder.dispose(); + } + + this._pluginHolderForUniver.dispose(); + } + + /** Register a plugin into univer. */ + registerPlugin>(plugin: T, config?: ConstructorParameters[0]): void { + this._assertPluginValid(plugin); + + this._scheduleInitPlugin(); + + const { type } = plugin; + if (type === UniverInstanceType.UNIVER_UNKNOWN) { + this._pluginHolderForUniver.registerPlugin(plugin, config); + this._pluginHolderForUniver.flush(); + } else { + // If it's type is for specific document, we should run them at specific time. + const holder = this._ensurePluginHolderForType(type); + holder.registerPlugin(plugin, config); + } + } + + startPluginForType(type: UniverInstanceType): void { + const holder = this._ensurePluginHolderForType(type); + holder.start(); + } + + _ensurePluginHolderForType(type: UnitType): PluginHolder { + if (!this._pluginHoldersForTypes.has(type)) { + const pluginHolder = this._injector.createInstance(PluginHolder); + this._pluginHoldersForTypes.set(type, pluginHolder); + return pluginHolder; + } + + return this._pluginHoldersForTypes.get(type)!; + } + + private _assertPluginValid(plugin: PluginCtor): void { + const { type, pluginName } = plugin; + + if (type === UniverInstanceType.UNRECOGNIZED) { + throw new Error(`[PluginService]: invalid plugin type for ${plugin}. Please assign a "type" to your plugin.`); + } + + if (pluginName === '') { + throw new Error(`[PluginService]: no plugin name for ${plugin}. Please assign a "pluginName" to your plugin.`); + } + } + + private _flushTimer?: number; + private _scheduleInitPlugin() { + if (this._flushTimer === undefined) { + this._flushTimer = setTimeout( + () => { + if (!this._pluginHolderForUniver.started) { + this._pluginHolderForUniver.start(); + } + + this._flushPlugins(); + this._clearFlushTimer(); + }, + INIT_LAZY_PLUGINS_TIMEOUT + ) as unknown as number; + } + } + + private _clearFlushTimer() { + if (this._flushTimer) { + clearTimeout(this._flushTimer); + this._flushTimer = undefined; + } + } + + private _flushPlugins() { + this._pluginHolderForUniver.flush(); + for (const [_, holder] of this._pluginHoldersForTypes) { + if (holder.started) { + holder.flush(); + } + } + } +} diff --git a/packages/core/src/plugin/plugin.ts b/packages/core/src/services/plugin/plugin.ts similarity index 55% rename from packages/core/src/plugin/plugin.ts rename to packages/core/src/services/plugin/plugin.ts index 74f27aa547..2accf2f527 100644 --- a/packages/core/src/plugin/plugin.ts +++ b/packages/core/src/services/plugin/plugin.ts @@ -15,48 +15,49 @@ */ import type { Ctor, Injector } from '@wendellhu/redi'; +import { Disposable } from '../../shared'; +import { UniverInstanceType } from '../../common/unit'; -export type PluginCtor = Ctor & { type: PluginType }; - -/** Plugin types for different kinds of business. */ -export enum PluginType { - Univer, - Doc, - Sheet, - Slide, -} +export type PluginCtor = Ctor & { type: UniverInstanceType; pluginName: string }; /** * Plug-in base class, all plug-ins must inherit from this base class. Provide basic methods. */ -export abstract class Plugin { - static type: PluginType = PluginType.Univer; +export abstract class Plugin extends Disposable { + static pluginName: string; - protected abstract _injector: Injector; + static type: UniverInstanceType = UniverInstanceType.UNIVER_UNKNOWN; - private _name: string; + protected abstract _injector: Injector; - protected constructor(name: string) { - this._name = name; + onStarting(_injector: Injector): void { + // empty } - onStarting(injector: Injector): void {} - - onReady(): void {} + onReady(): void { + // empty + } - onRendered(): void {} + onRendered(): void { + // empty + } - onSteady(): void {} + onSteady(): void { + // empty + } - onDestroy(): void {} + getUniverInstanceType(): UniverInstanceType { + return (this.constructor as typeof Plugin).type; + } getPluginName(): string { - return this._name; + return (this.constructor as typeof Plugin).pluginName; } } interface IPluginRegistryItem { plugin: PluginCtor; + // eslint-disable-next-line ts/no-explicit-any options: any; } @@ -85,22 +86,18 @@ export class PluginStore { * Store plugin registry items. */ export class PluginRegistry { - private readonly _pluginsRegisteredByBusiness = new Map(); + private _pluginsRegistered: IPluginRegistryItem[] = []; + // eslint-disable-next-line ts/no-explicit-any registerPlugin(pluginCtor: PluginCtor, options: any) { - const type = pluginCtor.type; - if (!this._pluginsRegisteredByBusiness.has(type)) { - this._pluginsRegisteredByBusiness.set(type, [] as unknown[] as [IPluginRegistryItem]); - } - - this._pluginsRegisteredByBusiness.get(type)!.push({ plugin: pluginCtor, options }); + this._pluginsRegistered.push({ plugin: pluginCtor, options }); } - getRegisterPlugins(type: PluginType): [IPluginRegistryItem] { - return this._pluginsRegisteredByBusiness.get(type) || ([] as unknown[] as [IPluginRegistryItem]); + getRegisterPlugins(): IPluginRegistryItem[] { + return this._pluginsRegistered.slice(); } - clearPluginsOfType(type: PluginType): void { - this._pluginsRegisteredByBusiness.delete(type); + removePlugins(): void { + this._pluginsRegistered = []; } } diff --git a/packages/core/src/services/resource-loader/resource-loader.service.ts b/packages/core/src/services/resource-loader/resource-loader.service.ts new file mode 100644 index 0000000000..85f17f645f --- /dev/null +++ b/packages/core/src/services/resource-loader/resource-loader.service.ts @@ -0,0 +1,98 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Inject } from '@wendellhu/redi'; +import type { Workbook } from '../../sheets/workbook'; +import type { IWorkbookData } from '../../types/interfaces'; +import type { IResourceHook } from '../resource-manager/type'; +import { IResourceManagerService } from '../resource-manager/type'; +import { IUniverInstanceService } from '../instance/instance.service'; +import { Disposable, toDisposable } from '../../shared/lifecycle'; +import { UniverInstanceType } from '../../common/unit'; +import type { IResourceLoaderService } from './type'; + +export class ResourceLoaderService extends Disposable implements IResourceLoaderService { + constructor( + @Inject(IResourceManagerService) private readonly _resourceManagerService: IResourceManagerService, + @Inject(IUniverInstanceService) private readonly _univerInstanceService: IUniverInstanceService + ) { + super(); + this._init(); + } + + private _init() { + const handleHookAdd = (hook: IResourceHook) => { + hook.businesses.forEach((business) => { + switch (business) { + case UniverInstanceType.UNRECOGNIZED: + case UniverInstanceType.UNIVER_UNKNOWN: + case UniverInstanceType.UNIVER_SLIDE: + case UniverInstanceType.UNIVER_DOC: { + // TODO@gggpound: wait to support. + break; + } + case UniverInstanceType.UNIVER_SHEET: { + this._univerInstanceService.getAllUnitsForType(UniverInstanceType.UNIVER_SHEET).forEach((workbook) => { + const snapshotResource = workbook.getSnapshot().resources || []; + const plugin = snapshotResource.find((r) => r.name === hook.pluginName); + if (plugin) { + try { + const data = hook.parseJson(plugin.data); + hook.onLoad(workbook.getUnitId(), data); + } catch (err) { + console.error(`Load Workbook{${workbook.getUnitId()}} Resources{${hook.pluginName}} Data Error.`); + } + } + }); + } + } + }); + }; + + const allResourceHooks = this._resourceManagerService.getAllResourceHooks(); + allResourceHooks.forEach((hook) => { + handleHookAdd(hook); + }); + + this.disposeWithMe(this._resourceManagerService.register$.subscribe((hook) => { + handleHookAdd(hook); + })); + + this.disposeWithMe( + toDisposable( + this._univerInstanceService.getTypeOfUnitAdded$(UniverInstanceType.UNIVER_SHEET).subscribe((workbook) => { + this._resourceManagerService.loadResources(workbook.getUnitId(), workbook.getSnapshot().resources); + }) + ) + ); + + this.disposeWithMe( + toDisposable( + this._univerInstanceService.getTypeOfUnitDisposed$(UniverInstanceType.UNIVER_SHEET).subscribe((workbook) => { + this._resourceManagerService.unloadResources(workbook.getUnitId()); + }) + ) + ); + } + + saveWorkbook: (workbook: Workbook) => IWorkbookData = (workbook) => { + const unitId = workbook.getUnitId(); + const resources = this._resourceManagerService.getResources(unitId) || []; + const snapshot = workbook.getSnapshot(); + snapshot.resources = resources; + return snapshot; + }; +} diff --git a/packages/core/src/services/resource-loader/type.ts b/packages/core/src/services/resource-loader/type.ts new file mode 100644 index 0000000000..c7358ed445 --- /dev/null +++ b/packages/core/src/services/resource-loader/type.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createIdentifier } from '@wendellhu/redi'; +import type { Workbook } from '../../sheets/workbook'; +import type { IWorkbookData } from '../../types/interfaces/i-workbook-data'; +import { LifecycleStages, runOnLifecycle } from '../lifecycle/lifecycle'; + +export interface IResourceLoaderService { + saveWorkbook: (workbook: Workbook) => IWorkbookData; +} +export const IResourceLoaderService = createIdentifier('resource-loader-service'); +runOnLifecycle(LifecycleStages.Ready, IResourceLoaderService); diff --git a/packages/core/src/services/resource-manager/resource-manager.service.ts b/packages/core/src/services/resource-manager/resource-manager.service.ts index 5cd7126305..717d63e3d0 100644 --- a/packages/core/src/services/resource-manager/resource-manager.service.ts +++ b/packages/core/src/services/resource-manager/resource-manager.service.ts @@ -15,56 +15,65 @@ */ import { Subject } from 'rxjs'; - import { Disposable, toDisposable } from '../../shared/lifecycle'; -import type { IResourceHook, IResourceManagerService } from './type'; +import type { IWorkbookData } from '../../types/interfaces/i-workbook-data'; +import type { IResourceHook, IResourceManagerService, IResourceName } from './type'; export class ResourceManagerService extends Disposable implements IResourceManagerService { - private _resourceMap = new Map>(); + private _resourceMap = new Map(); - private _register$ = new Subject<{ resourceName: string; hook: IResourceHook; unitID: string }>(); - register$ = this._register$.asObservable(); + private _register$ = new Subject(); + public register$ = this._register$.asObservable(); - getAllResource(unitID: string) { - const resourceMap = this._resourceMap.get(unitID); - if (resourceMap) { - return [...resourceMap.keys()].reduce( - (list, resourceName) => { - const hook = resourceMap.get(resourceName); - if (hook) { - list.push({ - unitID, - resourceName, - hook, - }); - } - return list; - }, - [] as Array<{ unitID: string; resourceName: string; hook: IResourceHook }> - ); - } - return []; + public getAllResourceHooks() { + const list = [...this._resourceMap.values()]; + return list; + } + + public getResources(unitId: string) { + const resourceHooks = this.getAllResourceHooks(); + const resources = resourceHooks.map((resourceHook) => { + const data = resourceHook.toJson(unitId); + return { + name: resourceHook.pluginName, + data, + }; + }); + return resources; } - /** - * the pluginName is map to resourceId which is created by serve. - * @param {string} pluginName - * @param {ResourceHook} hook - */ - registerPluginResource(unitID: string, resourceName: string, hook: IResourceHook) { - const resourceMap = this._resourceMap.get(unitID) || new Map(); - if (resourceMap.has(resourceName)) { - throw new Error('the pluginName is registered'); + public registerPluginResource(hook: IResourceHook) { + const resourceName = hook.pluginName; + if (this._resourceMap.has(resourceName)) { + throw new Error(`the pluginName is registered {${resourceName}}`); } - resourceMap.set(resourceName, hook); - this._resourceMap.set(unitID, resourceMap); - this._register$.next({ unitID, resourceName, hook }); - return toDisposable(() => resourceMap.delete(resourceName)); + this._resourceMap.set(resourceName, hook); + this._register$.next(hook); + return toDisposable(() => this._resourceMap.delete(resourceName)); + } + + public disposePluginResource(pluginName: IResourceName) { + this._resourceMap.delete(pluginName); + } + + public loadResources(unitId: string, resources: IWorkbookData['resources']) { + this.getAllResourceHooks().forEach((hook) => { + const data = resources?.find((resource) => resource.name === hook.pluginName)?.data; + if (data) { + try { + const model = hook.parseJson(data); + hook.onLoad(unitId, model); + } catch (err) { + console.error('LoadResources Error!'); + } + } + }); } - disposePluginResource(unitID: string, pluginName: string) { - const resourceMap = this._resourceMap.get(unitID); - resourceMap?.delete(pluginName); + public unloadResources(unitId: string) { + this.getAllResourceHooks().forEach((hook) => { + hook.onUnLoad(unitId); + }); } override dispose(): void { diff --git a/packages/core/src/services/resource-manager/type.ts b/packages/core/src/services/resource-manager/type.ts index 14675f521b..1cef7403af 100644 --- a/packages/core/src/services/resource-manager/type.ts +++ b/packages/core/src/services/resource-manager/type.ts @@ -17,28 +17,29 @@ import type { IDisposable } from '@wendellhu/redi'; import { createIdentifier } from '@wendellhu/redi'; import type { Observable } from 'rxjs'; - -import type { Workbook } from '../../sheets/workbook'; +import type { UniverInstanceType } from '@univerjs/core'; import type { IWorkbookData } from '../../types/interfaces/i-workbook-data'; -import { LifecycleStages, runOnLifecycle } from '../lifecycle/lifecycle'; +type IBusinessName = 'SHEET' | 'DOC'; +export type IResourceName = `${IBusinessName}_${string}_PLUGIN`; export interface IResourceHook { - onChange: (unitID: string, resource: T) => void; + pluginName: IResourceName; + businesses: UniverInstanceType[]; + onLoad: (unitID: string, resource: T) => void; + onUnLoad: (unitID: string) => void; toJson: (unitID: string) => string; parseJson: (bytes: string) => T; } export interface IResourceManagerService { - registerPluginResource: (unitID: string, pluginName: string, hook: IResourceHook) => IDisposable; - disposePluginResource: (unitID: string, pluginName: string) => void; - getAllResource: (unitID: string) => Array<{ unitID: string; resourceName: string; hook: IResourceHook }>; - register$: Observable<{ resourceName: string; hook: IResourceHook; unitID: string }>; -} + register$: Observable; + registerPluginResource: (hook: IResourceHook) => IDisposable; + disposePluginResource: (pluginName: IResourceName) => void; + getAllResourceHooks: () => IResourceHook[]; + getResources: (unitId: string) => IWorkbookData['resources']; + loadResources: (unitId: string, resources: IWorkbookData['resources']) => void; -export const IResourceManagerService = createIdentifier('resource-manager-service'); -export interface ISnapshotPersistenceService { - saveWorkbook: (workbook: Workbook) => IWorkbookData; + unloadResources(unitId: string): void; } -export const ISnapshotPersistenceService = createIdentifier('ResourcePersistenceService'); -runOnLifecycle(LifecycleStages.Ready, ISnapshotPersistenceService); +export const IResourceManagerService = createIdentifier('resource-manager-service'); diff --git a/packages/core/src/services/theme/theme.service.ts b/packages/core/src/services/theme/theme.service.ts index d4471d1d07..ac1cfe38f1 100644 --- a/packages/core/src/services/theme/theme.service.ts +++ b/packages/core/src/services/theme/theme.service.ts @@ -17,7 +17,7 @@ import type { Observable } from 'rxjs'; import { BehaviorSubject } from 'rxjs'; -import type { Nullable } from '../../common/type-utils'; +import type { Nullable } from '../../common/type-util'; import { Disposable, toDisposable } from '../../shared/lifecycle'; export interface IStyleSheet { diff --git a/packages/core/src/services/undoredo/undoredo.service.ts b/packages/core/src/services/undoredo/undoredo.service.ts index 8da5174bb7..81236f4cbd 100644 --- a/packages/core/src/services/undoredo/undoredo.service.ts +++ b/packages/core/src/services/undoredo/undoredo.service.ts @@ -26,7 +26,7 @@ import { CommandType, ICommandService, sequenceExecute } from '../command/comman import { EDITOR_ACTIVATED, FOCUSING_FORMULA_EDITOR, FOCUSING_SHEET } from '../context/context'; import { IContextService } from '../context/context.service'; import { IUniverInstanceService } from '../instance/instance.service'; -import type { Nullable } from '../../common/type-utils'; +import type { Nullable } from '../../common/type-util'; export interface IUndoRedoItem { /** unitID maps to unitId for UniverSheet / UniverDoc / UniverSlide */ @@ -65,6 +65,17 @@ enum BatchingStatus { CREATED, } +export interface IUndoRedoCommandInfosByInterceptor { + /** + * Sometimes, mutations generated by interceptors need to ensure a certain execution order + * PreMutations run before user's intent to make sure the undo/redo works correctly. + */ + preUndos?: IMutationInfo[]; + undos: IMutationInfo[]; + redos: IMutationInfo[]; + preRedos?: IMutationInfo[]; +} + export interface IUndoRedoCommandInfos { undos: IMutationInfo[]; redos: IMutationInfo[]; @@ -80,7 +91,9 @@ export interface IUndoRedoStatus { const STACK_CAPACITY = 20; abstract class MultiImplementationCommand implements IDisposable { - dispose(): void { } + dispose(): void { + // empty + } async dispatchToHandlers(): Promise { return false; @@ -215,12 +228,12 @@ export class LocalUndoRedoService extends Disposable implements IUndoRedoService } pitchTopUndoElement(): Nullable { - const unitID = this._getFocusedUniverInstanceId(); + const unitID = this._getFocusedUnitId(); return this._pitchUndoElement(unitID); } pitchTopRedoElement(): Nullable { - const unitID = this._getFocusedUniverInstanceId(); + const unitID = this._getFocusedUnitId(); return this._pitchRedoElement(unitID); } @@ -264,7 +277,7 @@ export class LocalUndoRedoService extends Disposable implements IUndoRedoService } protected _updateStatus(): void { - const unitID = this._getFocusedUniverInstanceId(); + const unitID = this._getFocusedUnitId(); const undos = (unitID && this._undoStacks.get(unitID)?.length) || 0; const redos = (unitID && this._redoStacks.get(unitID)?.length) || 0; @@ -299,7 +312,7 @@ export class LocalUndoRedoService extends Disposable implements IUndoRedoService } protected _getUndoStackForFocused(): IUndoRedoItem[] { - const unitID = this._getFocusedUniverInstanceId(); + const unitID = this._getFocusedUnitId(); if (!unitID) { throw new Error('No focused univer instance!'); @@ -309,7 +322,7 @@ export class LocalUndoRedoService extends Disposable implements IUndoRedoService } protected _getRedoStackForFocused(): IUndoRedoItem[] { - const unitID = this._getFocusedUniverInstanceId(); + const unitID = this._getFocusedUnitId(); if (!unitID) { throw new Error('No focused univer instance!'); @@ -324,7 +337,7 @@ export class LocalUndoRedoService extends Disposable implements IUndoRedoService item.undoMutations.push(...newItem.undoMutations); } - private _getFocusedUniverInstanceId() { + private _getFocusedUnitId() { let unitID: string = ''; const isFocusSheet = this._contextService.getContextValue(FOCUSING_SHEET); @@ -337,10 +350,10 @@ export class LocalUndoRedoService extends Disposable implements IUndoRedoService } else if (isFocusEditor) { unitID = DOCS_NORMAL_EDITOR_UNIT_ID_KEY; } else { - unitID = this._univerInstanceService.getFocusedUniverInstance()?.getUnitId() ?? ''; + unitID = this._univerInstanceService.getFocusedUnit()?.getUnitId() ?? ''; } } else { - unitID = this._univerInstanceService.getFocusedUniverInstance()?.getUnitId() ?? ''; + unitID = this._univerInstanceService.getFocusedUnit()?.getUnitId() ?? ''; } return unitID; diff --git a/packages/core/src/services/user-manager/user-manager.service.ts b/packages/core/src/services/user-manager/user-manager.service.ts new file mode 100644 index 0000000000..e0e4202421 --- /dev/null +++ b/packages/core/src/services/user-manager/user-manager.service.ts @@ -0,0 +1,62 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IUser } from '@univerjs/protocol'; +import { BehaviorSubject, Subject } from 'rxjs'; + +export class UserManagerService { + private _model = new Map(); + private _userChange$ = new Subject<{ type: 'add' | 'delete'; user: IUser } | { type: 'clear' }>(); + public userChange$ = this._userChange$.asObservable(); + + private _currentUser: IUser | null; + private _currentUser$ = new BehaviorSubject(null); + public currentUser$ = this._currentUser$.asObservable(); + + getCurrentUser() { + return this._currentUser; + } + + setCurrentUser(user: IUser) { + this._currentUser = user; + this.addUser(user); + this._currentUser$.next(user); + } + + addUser(user: IUser) { + this._model.set(user.userID, user); + this._userChange$.next({ type: 'add', user }); + } + + getUser(userId: string, callBack?: () => void) { + const user = this._model.get(userId); + if (user) { + return user; + } + callBack && callBack(); + } + + delete(userId: string) { + const user = this.getUser(userId); + this._model.delete(userId); + user && this._userChange$.next({ type: 'delete', user }); + } + + clear() { + this._model.clear(); + this._userChange$.next({ type: 'clear' }); + } +} diff --git a/packages/core/src/shared/__test__/rectangle.spec.ts b/packages/core/src/shared/__test__/rectangle.spec.ts deleted file mode 100644 index fd7fb70085..0000000000 --- a/packages/core/src/shared/__test__/rectangle.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright 2023-present DreamNum Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { describe, expect, it } from 'vitest'; - -import { Rectangle } from '../rectangle'; - -describe('test "Rectangle"', () => { - it('test "subtract"', () => { - // completely covered - const rect1 = { - startRow: 1, - startColumn: 1, - endRow: 1, - endColumn: 1, - }; - - const rect2 = { - startRow: 1, - startColumn: 1, - endRow: 1, - endColumn: 1, - }; - expect(Rectangle.subtract(rect1, rect2)).toEqual([]); - - // partly covered - const rect3 = { - startRow: 1, - startColumn: 1, - endRow: 3, - endColumn: 3, - }; - - const rect4 = { - startRow: 1, - startColumn: 1, - endRow: 1, - endColumn: 1, - }; - expect(Rectangle.subtract(rect3, rect4)).toStrictEqual([ - { startRow: 2, startColumn: 1, endRow: 3, endColumn: 3 }, - { startRow: 1, startColumn: 2, endRow: 1, endColumn: 3 }, - ]); - - // covered at center point - const rect5 = { - startRow: 1, - startColumn: 1, - endRow: 3, - endColumn: 3, - }; - - const rect6 = { - startRow: 2, - startColumn: 2, - endRow: 2, - endColumn: 2, - }; - - expect(Rectangle.subtract(rect5, rect6)).toStrictEqual([ - { startRow: 1, startColumn: 1, endRow: 1, endColumn: 3 }, - { startRow: 3, startColumn: 1, endRow: 3, endColumn: 3 }, - { startRow: 2, startColumn: 1, endRow: 2, endColumn: 1 }, - { startRow: 2, startColumn: 3, endRow: 2, endColumn: 3 }, - ]); - }); -}); diff --git a/packages/core/src/shared/__tests__/common.spec.ts b/packages/core/src/shared/__tests__/common.spec.ts new file mode 100644 index 0000000000..f12ed29d0a --- /dev/null +++ b/packages/core/src/shared/__tests__/common.spec.ts @@ -0,0 +1,48 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, expect, it } from 'vitest'; +import { cellToRange, isFormulaId, isFormulaString } from '../common'; + +describe('Test common', () => { + it('Test cellToRange', () => { + expect(cellToRange(0, 1)).toStrictEqual({ startRow: 0, startColumn: 1, endRow: 0, endColumn: 1 }); + }); + + it('Test isFormulaString', () => { + expect(isFormulaString('=SUM(1)')).toBe(true); + expect(isFormulaString('SUM(1)')).toBe(false); + expect(isFormulaString('=')).toBe(false); + expect(isFormulaString('')).toBe(false); + expect(isFormulaString(1)).toBe(false); + expect(isFormulaString(null)).toBe(false); + expect(isFormulaString(undefined)).toBe(false); + expect(isFormulaString(true)).toBe(false); + expect(isFormulaString({})).toBe(false); + expect(isFormulaString({ f: '' })).toBe(false); + }); + + it('Test isFormulaId', () => { + expect(isFormulaId('id1')).toBe(true); + expect(isFormulaId('')).toBe(false); + expect(isFormulaId(1)).toBe(false); + expect(isFormulaId(null)).toBe(false); + expect(isFormulaId(undefined)).toBe(false); + expect(isFormulaId(true)).toBe(false); + expect(isFormulaId({})).toBe(false); + expect(isFormulaId({ f: '' })).toBe(false); + }); +}); diff --git a/packages/core/src/shared/__test__/object-matrix.spec.ts b/packages/core/src/shared/__tests__/object-matrix.spec.ts similarity index 82% rename from packages/core/src/shared/__test__/object-matrix.spec.ts rename to packages/core/src/shared/__tests__/object-matrix.spec.ts index 341bd010ee..2127ab4caa 100644 --- a/packages/core/src/shared/__test__/object-matrix.spec.ts +++ b/packages/core/src/shared/__tests__/object-matrix.spec.ts @@ -16,7 +16,7 @@ import { describe, expect, it } from 'vitest'; -import { moveMatrixArray, ObjectMatrix } from '../object-matrix'; +import { moveMatrixArray, ObjectMatrix, spliceArray } from '../object-matrix'; describe('test ObjectMatrix', () => { const getPrimitiveObj = () => ({ @@ -90,4 +90,22 @@ describe('test ObjectMatrix', () => { 1: '111', 2: '333', 3: null, }); }); + it('test spliceMatrix row', () => { + const matrix = new ObjectMatrix(getPrimitiveObj()); + spliceArray(1, 1, matrix.getMatrix()); + expect(matrix.getMatrix()).toStrictEqual({ + 1: { 1: '111', 2: '121', 3: '313' }, + }); + }); + + it('test spliceMatrix col', () => { + const matrix = new ObjectMatrix(getPrimitiveObj()); + matrix.forEach((row, value) => { + spliceArray(1, 1, value); + }); + expect(matrix.getMatrix()).toStrictEqual({ + 1: { 1: '222', 2: '333' }, + 2: { 1: '121', 2: '313' }, + }); + }); }); diff --git a/packages/core/src/shared/__tests__/range.spec.ts b/packages/core/src/shared/__tests__/range.spec.ts new file mode 100644 index 0000000000..eb73ad88f8 --- /dev/null +++ b/packages/core/src/shared/__tests__/range.spec.ts @@ -0,0 +1,78 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, expect, it } from 'vitest'; + +import { AbsoluteRefType } from '../../types/interfaces/i-range'; +import { moveRangeByOffset } from '../range'; + +describe('test moveRangeByOffset', () => { + it('test normal', () => { + const range = { + startRow: 1, + startColumn: 1, + endRow: 3, + endColumn: 3, + }; + const newRange = moveRangeByOffset(range, 1, 1); + expect(newRange).toEqual({ + startRow: 2, + startColumn: 2, + endRow: 4, + endColumn: 4, + }); + }); + + it('test absolute', () => { + const range = { + startRow: 1, + startColumn: 1, + endRow: 3, + endColumn: 3, + startAbsoluteRefType: AbsoluteRefType.ROW, + endAbsoluteRefType: AbsoluteRefType.COLUMN, + }; + const newRange = moveRangeByOffset(range, 1, 1); + expect(newRange).toEqual({ + startRow: 1, + startColumn: 2, + endRow: 4, + endColumn: 3, + startAbsoluteRefType: AbsoluteRefType.ROW, + endAbsoluteRefType: AbsoluteRefType.COLUMN, + }); + }); + + it('test ignoreAbsolute', () => { + const range = { + startRow: 1, + startColumn: 1, + endRow: 3, + endColumn: 3, + startAbsoluteRefType: AbsoluteRefType.ROW, + endAbsoluteRefType: AbsoluteRefType.COLUMN, + }; + const newRange = moveRangeByOffset(range, 1, 1, true); + expect(newRange).toEqual({ + startRow: 2, + startColumn: 2, + endRow: 4, + endColumn: 4, + startAbsoluteRefType: AbsoluteRefType.ROW, + endAbsoluteRefType: AbsoluteRefType.COLUMN, + }); + }); +}); diff --git a/packages/core/src/shared/__tests__/rectangle.spec.ts b/packages/core/src/shared/__tests__/rectangle.spec.ts new file mode 100644 index 0000000000..9b5ec2af1d --- /dev/null +++ b/packages/core/src/shared/__tests__/rectangle.spec.ts @@ -0,0 +1,113 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, expect, it } from 'vitest'; + +import { Rectangle } from '../rectangle'; +import { AbsoluteRefType } from '../../types/interfaces/i-range'; +import type { IRange } from '../../types/interfaces/i-range'; + +const cellToRange = (row: number, col: number) => ({ startRow: row, endRow: row, startColumn: col, endColumn: col } as IRange); +describe('test "Rectangle"', () => { + it('test "subtract"', () => { + // completely covered + const rect1 = { + startRow: 1, + startColumn: 1, + endRow: 1, + endColumn: 1, + }; + + const rect2 = { + startRow: 1, + startColumn: 1, + endRow: 1, + endColumn: 1, + }; + expect(Rectangle.subtract(rect1, rect2)).toEqual([]); + + // partly covered + const rect3 = { + startRow: 1, + startColumn: 1, + endRow: 3, + endColumn: 3, + }; + + const rect4 = { + startRow: 1, + startColumn: 1, + endRow: 1, + endColumn: 1, + }; + expect(Rectangle.subtract(rect3, rect4)).toStrictEqual([ + { startRow: 2, startColumn: 1, endRow: 3, endColumn: 3 }, + { startRow: 1, startColumn: 2, endRow: 1, endColumn: 3 }, + ]); + + // covered at center point + const rect5 = { + startRow: 1, + startColumn: 1, + endRow: 3, + endColumn: 3, + }; + + const rect6 = { + startRow: 2, + startColumn: 2, + endRow: 2, + endColumn: 2, + }; + + expect(Rectangle.subtract(rect5, rect6)).toStrictEqual([ + { startRow: 1, startColumn: 1, endRow: 1, endColumn: 3 }, + { startRow: 3, startColumn: 1, endRow: 3, endColumn: 3 }, + { startRow: 2, startColumn: 1, endRow: 2, endColumn: 1 }, + { startRow: 2, startColumn: 3, endRow: 2, endColumn: 3 }, + ]); + }); + + it('test getRelativeRange', () => { + const relativeRange = Rectangle.getRelativeRange({ startRow: 5, endRow: 6, startColumn: 5, endColumn: 6 }, cellToRange(10, 10)); + expect(relativeRange).toEqual({ + endColumn: 1, + endRow: 1, + startColumn: -5, + startRow: -5, + }); + }); + it('test getPositionRange', () => { + const originRange = { startRow: 5, endRow: 6, startColumn: 5, endColumn: 6 }; + const relativeRange = Rectangle.getRelativeRange(originRange, cellToRange(10, 10)); + const positionRange = Rectangle.getPositionRange(relativeRange, cellToRange(11, 11)); + expect(positionRange).toEqual({ startRow: 6, endRow: 7, startColumn: 6, endColumn: 7 }); + const positionRangeWithAbsoluteStartAll = Rectangle.getPositionRange(relativeRange, cellToRange(11, 11), { ...originRange, startAbsoluteRefType: AbsoluteRefType.ALL }); + const positionRangeWithAbsoluteStartRow = Rectangle.getPositionRange(relativeRange, cellToRange(11, 11), { ...originRange, startAbsoluteRefType: AbsoluteRefType.ROW }); + const positionRangeWithAbsoluteStartCol = Rectangle.getPositionRange(relativeRange, cellToRange(11, 11), { ...originRange, startAbsoluteRefType: AbsoluteRefType.COLUMN }); + const positionRangeWithAbsoluteEndALl = Rectangle.getPositionRange(relativeRange, cellToRange(11, 11), { ...originRange, endAbsoluteRefType: AbsoluteRefType.ALL }); + const positionRangeWithAbsoluteEndRow = Rectangle.getPositionRange(relativeRange, cellToRange(11, 11), { ...originRange, endAbsoluteRefType: AbsoluteRefType.ROW }); + const positionRangeWithAbsoluteEndCol = Rectangle.getPositionRange(relativeRange, cellToRange(11, 11), { ...originRange, endAbsoluteRefType: AbsoluteRefType.COLUMN }); + const positionRangeWithALl = Rectangle.getPositionRange(relativeRange, cellToRange(11, 11), { ...originRange, endAbsoluteRefType: AbsoluteRefType.ALL, startAbsoluteRefType: AbsoluteRefType.ALL }); + expect(positionRangeWithAbsoluteStartAll).toEqual({ ...originRange, endColumn: 7, endRow: 7, startAbsoluteRefType: AbsoluteRefType.ALL }); + expect(positionRangeWithAbsoluteStartRow).toEqual({ ...originRange, startColumn: 6, endColumn: 7, endRow: 7, startAbsoluteRefType: AbsoluteRefType.ROW }); + expect(positionRangeWithAbsoluteStartCol).toEqual({ ...originRange, startRow: 6, endColumn: 7, endRow: 7, startAbsoluteRefType: AbsoluteRefType.COLUMN }); + expect(positionRangeWithAbsoluteEndALl).toEqual({ ...originRange, startRow: 6, startColumn: 6, endAbsoluteRefType: AbsoluteRefType.ALL }); + expect(positionRangeWithAbsoluteEndRow).toEqual({ ...originRange, endColumn: 7, startRow: 6, startColumn: 6, endAbsoluteRefType: AbsoluteRefType.ROW }); + expect(positionRangeWithAbsoluteEndCol).toEqual({ ...originRange, endRow: 7, startRow: 6, startColumn: 6, endAbsoluteRefType: AbsoluteRefType.COLUMN }); + expect(positionRangeWithALl).toEqual({ ...originRange, endAbsoluteRefType: AbsoluteRefType.ALL, startAbsoluteRefType: AbsoluteRefType.ALL }); + }); +}); diff --git a/packages/core/src/shared/__test__/ref-alias.spec.ts b/packages/core/src/shared/__tests__/ref-alias.spec.ts similarity index 84% rename from packages/core/src/shared/__test__/ref-alias.spec.ts rename to packages/core/src/shared/__tests__/ref-alias.spec.ts index 379fab9f86..98f36e38ec 100644 --- a/packages/core/src/shared/__test__/ref-alias.spec.ts +++ b/packages/core/src/shared/__tests__/ref-alias.spec.ts @@ -95,4 +95,15 @@ describe('test for RefAlias', () => { expect(newInstance.getValue('aa')).toEqual(instance.getValue('aa')); expect(newInstance.getValues()).toEqual(instance.getValues()); }); + + it('test the same key and value', () => { + const instance = createInstance(); + const obj = { a: 'cc', c: 'aa', b: 'b', d: 'd' }; + instance.addValue({ a: 'cc', c: 'aa', b: 'b', d: 'd' }); + // When the second getValue parameter is not set, the order of values is determined by the key order at construction time + // Retrieves from an object with a key of' A' first + expect(instance.getValue('cc')).toEqual(obj); + // When setting the second parameter of getValue, the order of values is determined by the second parameter. + expect(instance.getValue('cc', ['c'])).toEqual({ a: 'aa', b: 'bb', c: 'cc', d: 'dd' }); + }); }); diff --git a/packages/core/src/shared/after-init-apply.ts b/packages/core/src/shared/after-init-apply.ts new file mode 100644 index 0000000000..6b3b7d055d --- /dev/null +++ b/packages/core/src/shared/after-init-apply.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { merge, timer } from 'rxjs'; +import { debounceTime, filter, first } from 'rxjs/operators'; +import type { ICommandService } from '../services/command/command.service'; +import { CommandType } from '../services/command/command.service'; +import { fromCallback } from './rxjs'; + +export const afterInitApply = (commandService: ICommandService) => { + return new Promise((res) => { + merge( + fromCallback(commandService.onCommandExecuted).pipe(filter(([info]) => { + return info.type === CommandType.MUTATION; + })), + timer(300) + ).pipe(debounceTime(16), first()).subscribe(() => { + res(); + }); + }); +}; diff --git a/packages/core/src/shared/clipboard.ts b/packages/core/src/shared/clipboard.ts new file mode 100644 index 0000000000..e2f616b37e --- /dev/null +++ b/packages/core/src/shared/clipboard.ts @@ -0,0 +1,166 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BaselineOffset, BooleanNumber } from '../types/enum'; +import type { IDocumentBody, ITextRun } from '../types/interfaces'; +import { Tools } from './tools'; + +export function getBodySliceHtml(body: IDocumentBody, startIndex: number, endIndex: number) { + const { dataStream, textRuns = [] } = body; + let cursorIndex = startIndex; + const spanList: string[] = []; + + for (const textRun of textRuns) { + const { st, ed } = textRun; + if (Tools.hasIntersectionBetweenTwoRanges(startIndex, endIndex, st, ed)) { + if (st > cursorIndex) { + spanList.push(dataStream.slice(cursorIndex, st)); + + spanList.push(covertTextRunToHtml(dataStream, { + ...textRun, + ed: Math.min(ed, endIndex), + })); + } else { + spanList.push(covertTextRunToHtml(dataStream, { + ...textRun, + st: cursorIndex, + ed: Math.min(ed, endIndex), + })); + } + } + + cursorIndex = Math.max(startIndex, Math.min(ed, endIndex)); + } + + if (cursorIndex !== endIndex) { + spanList.push(dataStream.slice(cursorIndex, endIndex)); + } + + return spanList.join(''); +} + +export function convertBodyToHtml(body: IDocumentBody, withParagraphInfo: boolean = true): string { + if (withParagraphInfo && body.paragraphs?.length) { + const { dataStream, paragraphs = [] } = body; + let result = ''; + let cursorIndex = -1; + for (const paragraph of paragraphs) { + const { startIndex, paragraphStyle = {} } = paragraph; + const { spaceAbove, spaceBelow, lineSpacing } = paragraphStyle; + const style = []; + + if (spaceAbove != null) { + if (typeof spaceAbove === 'number') { + style.push(`margin-top: ${spaceAbove}px`); + } else { + style.push(`margin-top: ${spaceAbove.v}px`); + } + } + + if (spaceBelow != null) { + if (typeof spaceBelow === 'number') { + style.push(`margin-bottom: ${spaceBelow}px`); + } else { + style.push(`margin-bottom: ${spaceBelow.v}px`); + } + } + + if (lineSpacing != null) { + style.push(`line-height: ${lineSpacing}`); + } + + if (startIndex > cursorIndex + 1) { + result += `

${getBodySliceHtml(body, cursorIndex + 1, startIndex)}

`; + } else { + result += `

`; + } + + cursorIndex = startIndex; + } + + if (cursorIndex !== dataStream.length) { + result += getBodySliceHtml(body, cursorIndex, dataStream.length); + } + + return result; + } else { + return getBodySliceHtml(body, 0, body.dataStream.length); + } +} + +export function covertTextRunToHtml(dataStream: string, textRun: ITextRun): string { + const { st: start, ed, ts = {} } = textRun; + const { ff, fs, it, bl, ul, st, ol, bg, cl, va } = ts; + + let html = dataStream.slice(start, ed); + const style: string[] = []; + + // italic + if (it === BooleanNumber.TRUE) { + html = `${html}`; + } + + // subscript and superscript + if (va === BaselineOffset.SUPERSCRIPT) { + html = `${html}`; + } else if (va === BaselineOffset.SUBSCRIPT) { + html = `${html}`; + } + + // underline + if (ul?.s === BooleanNumber.TRUE) { + html = `${html}`; + } + + // strick-through + if (st?.s === BooleanNumber.TRUE) { + html = `${html}`; + } + + // bold + if (bl === BooleanNumber.TRUE) { + html = `${html}`; + } + + // font family + if (ff) { + style.push(`font-family: ${ff}`); + } + + // font color + if (cl) { + style.push(`color: ${cl.rgb}`); + } + + // font size + if (fs) { + style.push(`font-size: ${fs}pt`); + } + + // overline + if (ol) { + style.push('text-decoration: overline'); + } + + // background color + if (bg) { + style.push(`background: ${bg.rgb}`); + } + + return style.length ? `${html}` : html; +} diff --git a/packages/core/src/shared/color/color.ts b/packages/core/src/shared/color/color.ts index 8c162f3e15..3c6052477c 100644 --- a/packages/core/src/shared/color/color.ts +++ b/packages/core/src/shared/color/color.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Nullable } from '../../common/type-utils'; +import type { Nullable } from '../../common/type-util'; import { THEME_COLORS } from '../../types/const/theme-color-map'; import { ColorType, ThemeColors, ThemeColorType } from '../../types/enum'; diff --git a/packages/core/src/shared/common.ts b/packages/core/src/shared/common.ts index 67745cd6ae..18068f4b52 100644 --- a/packages/core/src/shared/common.ts +++ b/packages/core/src/shared/common.ts @@ -22,7 +22,7 @@ import { VerticalAlign, WrapStrategy, } from '../types/enum'; -import type { IRange } from '../types/interfaces'; +import { type IRange, RANGE_TYPE } from '../types/interfaces'; import type { ICellData } from '../types/interfaces/i-cell-data'; import type { IDocumentData } from '../types/interfaces/i-document-data'; import type { IRangeWithCoord, ISelectionCell, ISelectionCellWithCoord } from '../types/interfaces/i-selection-data'; @@ -146,12 +146,22 @@ export function getColorStyle(color: Nullable): Nullable { return null; } +/** + * A string starting with an equal sign is a formula + * @param value + * @returns + */ export function isFormulaString(value: any): boolean { return Tools.isString(value) && value.substring(0, 1) === '=' && value.length > 1; } +/** + * any string + * @param value + * @returns + */ export function isFormulaId(value: any): boolean { - return Tools.isString(value) && value.indexOf('=') === -1 && value.length === 6; + return Tools.isString(value) && value.length > 0; } /** @@ -393,11 +403,9 @@ export function handleStyleToString(style: IStyleData, isCell: boolean = false) 'tb', () => { if (style.tb === WrapStrategy.CLIP) { - str += 'text-overflow: clip; '; - } else if (style.tb === WrapStrategy.OVERFLOW) { - str += 'text-break: overflow; '; + str += 'white-space: clip; '; } else if (style.tb === WrapStrategy.WRAP) { - str += 'word-wrap: break-word;'; + str += 'white-space: normal;'; } }, ], @@ -498,6 +506,8 @@ export function getBorderStyleType(type: string) { str = BorderStyleTypes.MEDIUM_DASH_DOT_DOT; } else if (type === '1.5pt solid') { str = BorderStyleTypes.THICK; + } else if (!type.includes('none')) { + str = BorderStyleTypes.THIN; } else { return BorderStyleTypes.NONE; } @@ -521,8 +531,21 @@ export function getDocsUpdateBody(model: IDocumentData, segmentId?: string) { } export function isValidRange(range: IRange): boolean { - const { startRow, endRow, startColumn, endColumn } = range; - if (startRow < 0 || startColumn < 0 || endRow < 0 || endColumn < 0) { + const { startRow, endRow, startColumn, endColumn, rangeType } = range; + if ( + startRow < 0 + || startColumn < 0 + || endRow < 0 + || endColumn < 0 + ) { + return false; + } + + if (!(Number.isNaN(startRow) && Number.isNaN(endRow)) && rangeType === RANGE_TYPE.COLUMN) { + return false; + } + + if (!(Number.isNaN(startColumn) && Number.isNaN(endColumn)) && rangeType === RANGE_TYPE.ROW) { return false; } diff --git a/packages/core/src/shared/index.ts b/packages/core/src/shared/index.ts index c6203cef75..ae46a1e1b1 100644 --- a/packages/core/src/shared/index.ts +++ b/packages/core/src/shared/index.ts @@ -37,3 +37,7 @@ export * from './sort-rules'; export * from './tools'; export * from './types'; export * from './debounce'; +export * from './clipboard'; +export { queryObjectMatrix } from './object-matrix-query'; +export { moveRangeByOffset } from './range'; +export { afterInitApply } from './after-init-apply'; diff --git a/packages/core/src/shared/lifecycle.ts b/packages/core/src/shared/lifecycle.ts index 0ca3554a1f..e149eb3b28 100644 --- a/packages/core/src/shared/lifecycle.ts +++ b/packages/core/src/shared/lifecycle.ts @@ -19,26 +19,29 @@ import type { Subscription, SubscriptionLike } from 'rxjs'; import { Subject } from 'rxjs'; import { isSubscription } from 'rxjs/internal/Subscription'; -import type { Nullable } from '../common/type-utils'; +import type { Nullable } from '../common/type-util'; import type { Observer } from '../observer/observable'; import { isObserver } from '../observer/observable'; +type DisposableLike = IDisposable | Nullable> | SubscriptionLike | (() => void); + +export function toDisposable(disposable: IDisposable): IDisposable; export function toDisposable(observer: Nullable>): IDisposable; export function toDisposable(subscription: SubscriptionLike): IDisposable; export function toDisposable(callback: () => void): IDisposable; -export function toDisposable(v: SubscriptionLike | (() => void) | Nullable>): IDisposable { +export function toDisposable(v: DisposableLike): IDisposable; +export function toDisposable(v: DisposableLike): IDisposable { let disposed = false; + if (!v) { + return toDisposable(() => { + // empty + }); + } + if (isSubscription(v)) { return { - dispose: () => { - if (disposed) { - return; - } - - disposed = true; - v.unsubscribe(); - }, + dispose: () => v.unsubscribe(), }; } @@ -61,16 +64,20 @@ export function toDisposable(v: SubscriptionLike | (() => void) | Nullable { - if (disposed) { - return; - } + if (typeof v === 'function') { + return { + dispose: () => { + if (disposed) { + return; + } + + disposed = true; + (v as () => void)(); + }, + }; + } - disposed = true; - (v as () => void)(); - }, - }; + return v as IDisposable; } /** @@ -85,12 +92,14 @@ export function fromObservable(subscription: Subscription) { export class DisposableCollection implements IDisposable { private readonly _disposables = new Set(); - add(disposable: IDisposable): IDisposable { - this._disposables.add(disposable); + add(disposable: DisposableLike): IDisposable { + const d = toDisposable(disposable); + this._disposables.add(d); + return { dispose: () => { - disposable.dispose(); - this._disposables.delete(disposable); + d.dispose(); + this._disposables.delete(d); }, }; } @@ -98,8 +107,9 @@ export class DisposableCollection implements IDisposable { dispose(): void { this._disposables.forEach((item) => { item.dispose(); - this._disposables.delete(item); }); + + this._disposables.clear(); } } @@ -107,9 +117,14 @@ export class Disposable implements IDisposable { protected _disposed = false; private readonly _collection = new DisposableCollection(); - protected disposeWithMe(disposable: IDisposable | SubscriptionLike): IDisposable { - const d = isSubscription(disposable) ? toDisposable(disposable) : (disposable as IDisposable); - return this._collection.add(d); + protected disposeWithMe(disposable: DisposableLike): IDisposable { + return this._collection.add(disposable); + } + + protected ensureNotDisposed(): void { + if (this._disposed) { + throw new Error('[Disposable]: object is disposed!'); + } } dispose(): void { diff --git a/packages/core/src/shared/object-matrix-query.ts b/packages/core/src/shared/object-matrix-query.ts new file mode 100644 index 0000000000..d40ed5855c --- /dev/null +++ b/packages/core/src/shared/object-matrix-query.ts @@ -0,0 +1,101 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Nullable } from '../common/type-util'; +import { Range } from '../sheets/range'; +import type { IRange } from '../types/interfaces'; +import type { ObjectMatrix } from './object-matrix'; + +function maximalRectangle(matrix: T[][], match: (val: T) => boolean) { + if (matrix.length === 0 || matrix[0].length === 0) return null; + + const heights = new Array(matrix[0].length).fill(0); + let maxArea = 0; + let maxRect = null; + + for (let row = 0; row < matrix.length; row++) { + for (let col = 0; col < matrix[0].length; col++) { + heights[col] = match(matrix[row][col]) ? heights[col] + 1 : 0; + } + + const areaWithRect = largestRectangleArea(heights); + if (areaWithRect.area > maxArea) { + maxArea = areaWithRect.area; + // Adjust the rectangle's top row to the current row minus the height plus one + maxRect = { + startColumn: areaWithRect.start, + startRow: row - areaWithRect.height + 1, + endColumn: areaWithRect.end, + endRow: row, + }; + } + } + + return maxRect; +} + +function largestRectangleArea(heights: number[]) { + const stack: number[] = []; + let maxArea = 0; + let maxRect = { area: 0, height: 0, start: 0, end: 0 }; + let index = 0; + + while (index < heights.length) { + if (stack.length === 0 || heights[index] >= heights[stack[stack.length - 1]]) { + stack.push(index++); + } else { + const height = heights[stack.pop()!]; + const width = stack.length === 0 ? index : index - stack[stack.length - 1] - 1; + if (height * width > maxArea) { + maxArea = height * width; + maxRect = { area: maxArea, height, start: stack.length === 0 ? 0 : stack[stack.length - 1] + 1, end: index - 1 }; + } + } + } + + while (stack.length > 0) { + const height = heights[stack.pop()!]; + const width = stack.length === 0 ? index : index - stack[stack.length - 1] - 1; + if (height * width > maxArea) { + maxArea = height * width; + maxRect = { area: maxArea, height, start: stack.length === 0 ? 0 : stack[stack.length - 1] + 1, end: index - 1 }; + } + } + + return maxRect; +} + +function resetMatrix(matrix: Nullable[][], range: IRange) { + Range.foreach(range, (row, col) => { + matrix[row][col] = undefined; + }); +} + +export function queryObjectMatrix(matrix: ObjectMatrix, match: (value: T) => boolean) { + const arrayMatrix = matrix.toFullArray(); + const results: IRange[] = []; + while (true) { + const rectangle = maximalRectangle(arrayMatrix, match); + if (!rectangle) { + break; + } + + results.push(rectangle); + resetMatrix(arrayMatrix, rectangle); + } + + return results; +} diff --git a/packages/core/src/shared/object-matrix.ts b/packages/core/src/shared/object-matrix.ts index 89dd11a1b6..b42ad0ff74 100644 --- a/packages/core/src/shared/object-matrix.ts +++ b/packages/core/src/shared/object-matrix.ts @@ -58,52 +58,23 @@ export function insertMatrixArray( export function spliceArray( start: number, count: number, - o: IObjectArrayPrimitiveType | IObjectMatrixPrimitiveType, - insert?: IObjectArrayPrimitiveType | IObjectMatrixPrimitiveType + o: IObjectArrayPrimitiveType | IObjectMatrixPrimitiveType ) { - const end = start + count; - const length = getArrayLength(o); - const array = o; - let effective = 0; - const splice: IObjectArrayPrimitiveType | IObjectMatrixPrimitiveType = {}; - for (let i = start; i < end; i++) { - const item = array[i]; - if (item !== undefined) { - delete array[i]; - splice[effective] = item; - effective++; - } - } - const insertLength = insert ? getArrayLength(insert) : 0; - - const diff = start - end + insertLength; - const last = length; - - if (diff > 0) { - for (let i = last - 1; i >= end; i--) { - const item = array[i]; - if (item !== undefined) { - array[i + diff] = array[i]; - delete array[i]; - } - } - } else if (diff < 0) { - for (let i = end; i < last; i++) { - const item = array[i]; - if (item !== undefined) { - array[i + diff] = array[i]; - delete array[i]; + const length = Object.keys(o).reduce((max, key) => Math.max(max, Number.parseInt(key)), 0) + 1; + + // Delete elements within the specified range + for (let i = start; i < length; i++) { + if (i < start + count) { + // If within deletion range, delete directly + delete o[i]; + } else { + // If not within deletion range, move forward count positions + if (o[i] !== undefined) { + o[i - count] = o[i]; + delete o[i]; } } } - - if (insert) { - for (let i = 0; i < insertLength; i++) { - array[start + i] = insert[i]; - } - } - - return splice; } export function concatMatrixArray(source: IObjectArrayPrimitiveType, target: IObjectArrayPrimitiveType) { @@ -557,6 +528,22 @@ export class ObjectMatrix { return array; } + toFullArray(): T[][] { + const range = this.getRange(); + const { endColumn, endRow } = range; + const array: T[][] = []; + for (let i = 0; i <= endRow; i++) { + const subArr = Array(endColumn + 1).fill(undefined); + array.push(subArr); + } + + this.forValue((row, col, value) => { + array[row][col] = value; + }); + + return array; + } + /** * @deprecated Use getMatrix as a substitute. */ @@ -620,6 +607,10 @@ export class ObjectMatrix { startRow = rowIndex; } + if (row == null) { + return; + } + const rowSize = getArrayLength(row) - 1; if (rowSize > endColumn) { endColumn = rowSize; diff --git a/packages/core/src/shared/range.ts b/packages/core/src/shared/range.ts new file mode 100644 index 0000000000..1a4ce726a4 --- /dev/null +++ b/packages/core/src/shared/range.ts @@ -0,0 +1,51 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AbsoluteRefType, type IRange } from '../types/interfaces/i-range'; +import { Rectangle } from './rectangle'; + +export function moveRangeByOffset(range: IRange, refOffsetX: number, refOffsetY: number, ignoreAbsolute = false): IRange { + let newRange = { ...range }; + + const startAbsoluteRefType = newRange.startAbsoluteRefType || AbsoluteRefType.NONE; + const endAbsoluteRefType = newRange.endAbsoluteRefType || AbsoluteRefType.NONE; + + if (!ignoreAbsolute && startAbsoluteRefType === AbsoluteRefType.ALL && endAbsoluteRefType === AbsoluteRefType.ALL) { + return newRange; + } + + if (ignoreAbsolute || (startAbsoluteRefType === AbsoluteRefType.NONE && endAbsoluteRefType === AbsoluteRefType.NONE)) { + return Rectangle.moveOffset(newRange, refOffsetX, refOffsetY); + } + + if (startAbsoluteRefType === AbsoluteRefType.NONE) { + newRange = { ...newRange, startRow: newRange.startRow + refOffsetY, startColumn: newRange.startColumn + refOffsetX }; + } else if (startAbsoluteRefType === AbsoluteRefType.COLUMN) { + newRange = { ...newRange, startRow: newRange.startRow + refOffsetY }; + } else if (startAbsoluteRefType === AbsoluteRefType.ROW) { + newRange = { ...newRange, startColumn: newRange.startColumn + refOffsetX }; + } + + if (endAbsoluteRefType === AbsoluteRefType.NONE) { + newRange = { ...newRange, endRow: newRange.endRow + refOffsetY, endColumn: newRange.endColumn + refOffsetX }; + } else if (endAbsoluteRefType === AbsoluteRefType.COLUMN) { + newRange = { ...newRange, endRow: newRange.endRow + refOffsetY }; + } else if (endAbsoluteRefType === AbsoluteRefType.ROW) { + newRange = { ...newRange, endColumn: newRange.endColumn + refOffsetX }; + } + + return newRange; +} diff --git a/packages/core/src/shared/rectangle.ts b/packages/core/src/shared/rectangle.ts index c1a8bd1220..c2f0194dcb 100644 --- a/packages/core/src/shared/rectangle.ts +++ b/packages/core/src/shared/rectangle.ts @@ -15,7 +15,7 @@ */ import type { IRange } from '../types/interfaces/i-range'; -import { RANGE_TYPE } from '../types/interfaces/i-range'; +import { AbsoluteRefType, RANGE_TYPE } from '../types/interfaces/i-range'; import type { Nullable } from './types'; /** @@ -193,13 +193,15 @@ export class Rectangle { endColumn: range.endColumn - range.startColumn, }) as IRange; - static getPositionRange = (relativeRange: IRange, originRange: IRange) => - ({ - startRow: relativeRange.startRow + originRange.startRow, - endRow: relativeRange.endRow + relativeRange.startRow + originRange.startRow, - startColumn: relativeRange.startColumn + originRange.startColumn, - endColumn: relativeRange.endColumn + relativeRange.startColumn + originRange.startColumn, + static getPositionRange = (relativeRange: IRange, originRange: IRange, absoluteRange?: IRange) => { + return ({ + ...(absoluteRange || {}), + startRow: absoluteRange ? ([AbsoluteRefType.ROW, AbsoluteRefType.ALL].includes(absoluteRange.startAbsoluteRefType || 0) ? absoluteRange.startRow : relativeRange.startRow + originRange.startRow) : (relativeRange.startRow + originRange.startRow), + endRow: absoluteRange ? ([AbsoluteRefType.ROW, AbsoluteRefType.ALL].includes(absoluteRange.endAbsoluteRefType || 0) ? absoluteRange.endRow : relativeRange.endRow + relativeRange.startRow + originRange.startRow) : (relativeRange.endRow + relativeRange.startRow + originRange.startRow), + startColumn: absoluteRange ? ([AbsoluteRefType.COLUMN, AbsoluteRefType.ALL].includes(absoluteRange.startAbsoluteRefType || 0) ? absoluteRange.startColumn : relativeRange.startColumn + originRange.startColumn) : relativeRange.startColumn + originRange.startColumn, + endColumn: absoluteRange ? ([AbsoluteRefType.COLUMN, AbsoluteRefType.ALL].includes(absoluteRange.endAbsoluteRefType || 0) ? absoluteRange.endColumn : relativeRange.endColumn + relativeRange.startColumn + originRange.startColumn) : relativeRange.endColumn + relativeRange.startColumn + originRange.startColumn, }) as IRange; + }; static moveHorizontal = (range: IRange, step: number = 0, length: number = 0): IRange => ({ ...range, @@ -281,4 +283,30 @@ export class Rectangle { return result; } + + static hasIntersectionBetweenTwoBounds( + rect1: { + left: number; + top: number; + right: number; + bottom: number; + }, + rect2: { + left: number; + top: number; + right: number; + bottom: number; + } + ) { + if ( + rect1.left > rect2.right || // rect1 在 rect2 右侧 + rect1.right < rect2.left || // rect1 在 rect2 左侧 + rect1.top > rect2.bottom || // rect1 在 rect2 下方 + rect1.bottom < rect2.top // rect1 在 rect2 上方 + ) { + return false; + } + + return true; + } } diff --git a/packages/core/src/shared/ref-alias.ts b/packages/core/src/shared/ref-alias.ts index dc76ffa5d3..673a803082 100644 --- a/packages/core/src/shared/ref-alias.ts +++ b/packages/core/src/shared/ref-alias.ts @@ -36,9 +36,17 @@ export class RefAlias, K extends keyof T = key }); } - getValue(key: string) { - for (let index = 0; index < this._keys.length; index++) { - const keyMap = this._keyMaps.get(this._keys[index]); + /** + * If a key group is specified, the order of values is determined by the key group, otherwise it depends on the keys at initialization + * @param {string} key + * @param {K[]} [keyGroup] + * @return {*} + * @memberof RefAlias + */ + getValue(key: string, keyGroup?: K[]) { + const keys = keyGroup || this._keys; + for (let index = 0; index < keys.length; index++) { + const keyMap = this._keyMaps.get(keys[index]); if (keyMap?.has(key)) { return keyMap.get(key); } diff --git a/packages/core/src/shared/tools.ts b/packages/core/src/shared/tools.ts index 2fe9ada525..5ab4695b73 100644 --- a/packages/core/src/shared/tools.ts +++ b/packages/core/src/shared/tools.ts @@ -652,4 +652,23 @@ export class Tools { ) { return range1End >= range2Start && range2End >= range1Start; } + + static isStartValidPosition(name: string): boolean { + const startsWithLetterOrUnderscore = /^[A-Za-z_]/.test(name); + + return startsWithLetterOrUnderscore; + } + + static isValidParameter(name: string): boolean { + /** + *Validates that the name does not contain spaces or disallowed characters + *Assuming the set of disallowed characters includes some special characters, + *you can modify the regex below according to the actual requirements + */ + const containsInvalidChars = /[~!@#$%^&*()+=\-{}\[\]\|:;"'<>,?\/ ]+/.test(name); + + const isValidLength = name.length <= 255; + + return !containsInvalidChars && isValidLength; + } } diff --git a/packages/core/src/sheets/__tests__/create-core-test-bed.ts b/packages/core/src/sheets/__tests__/create-core-test-bed.ts index 10d8b4bd0b..382a2485fe 100644 --- a/packages/core/src/sheets/__tests__/create-core-test-bed.ts +++ b/packages/core/src/sheets/__tests__/create-core-test-bed.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { Univer } from '../../basics/univer'; +import { Univer } from '../../univer'; import { IUniverInstanceService } from '../../services/instance/instance.service'; import { ILogService, LogLevel } from '../../services/log/log.service'; import { LocaleType } from '../../types/enum/locale-type'; import type { IWorkbookData } from '../../types/interfaces/i-workbook-data'; -const TEST_WORKBOOK_DATA: IWorkbookData = { +const testWorkbookDataFactory: () => IWorkbookData = () => ({ id: 'test', appVersion: '3.0.0-alpha', sheets: { @@ -43,16 +43,17 @@ const TEST_WORKBOOK_DATA: IWorkbookData = { name: '', sheetOrder: [], styles: {}, -}; +}); -export function createCoreTestBed(workbookConfig?: IWorkbookData) { +export function createCoreTestBed(workbookData?: IWorkbookData) { const univer = new Univer(); const injector = univer.__getInjector(); const get = injector.get.bind(injector); - const sheet = univer.createUniverSheet(workbookConfig || TEST_WORKBOOK_DATA); + const sheet = univer.createUniverSheet(workbookData || testWorkbookDataFactory()); + const univerInstanceService = get(IUniverInstanceService); - univerInstanceService.focusUniverInstance(workbookConfig?.id ?? 'test'); + univerInstanceService.focusUnit(workbookData?.id ?? 'test'); const logService = get(ILogService); logService.setLogLevel(LogLevel.SILENT); diff --git a/packages/core/src/sheets/__tests__/workbook.spec.ts b/packages/core/src/sheets/__tests__/workbook.spec.ts index ddb1bae54f..87af8e1e37 100644 --- a/packages/core/src/sheets/__tests__/workbook.spec.ts +++ b/packages/core/src/sheets/__tests__/workbook.spec.ts @@ -16,7 +16,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { Univer } from '../../basics/univer'; +import type { Univer } from '../../univer'; import type { Workbook } from '../workbook'; import { createCoreTestBed } from './create-core-test-bed'; diff --git a/packages/core/src/sheets/__tests__/worksheet.spec.ts b/packages/core/src/sheets/__tests__/worksheet.spec.ts index 14934ac0b2..c26175296f 100644 --- a/packages/core/src/sheets/__tests__/worksheet.spec.ts +++ b/packages/core/src/sheets/__tests__/worksheet.spec.ts @@ -16,7 +16,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { Univer } from '../../basics/univer'; +import type { Univer } from '../../univer'; import type { IWorkbookData } from '../../types/interfaces/i-workbook-data'; import { LocaleType } from '../../types/enum/locale-type'; import { extractPureTextFromCell, type Worksheet } from '../worksheet'; @@ -99,7 +99,7 @@ describe('test worksheet', () => { }); const range: IRange = { startRow: 0, startColumn: 0, endRow: 1, endColumn: 2, rangeType: RANGE_TYPE.NORMAL }; - const iterator1 = worksheet.iterateByRow(range); + const iterator1 = worksheet.iterateByRow(range)[Symbol.iterator](); const value1 = iterator1.next(); expect(value1.done).toBeFalsy(); @@ -183,7 +183,7 @@ describe('test worksheet', () => { }); const range: IRange = { startRow: 0, startColumn: 0, endRow: 2, endColumn: 2, rangeType: RANGE_TYPE.NORMAL }; - const iterator1 = worksheet.iterateByColumn(range); + const iterator1 = worksheet.iterateByColumn(range)[Symbol.iterator](); const value1 = iterator1.next(); expect(value1.done).toBeFalsy(); diff --git a/packages/core/src/sheets/column-manager.ts b/packages/core/src/sheets/column-manager.ts index 98ba5419c8..5c8eeeb83c 100644 --- a/packages/core/src/sheets/column-manager.ts +++ b/packages/core/src/sheets/column-manager.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Nullable } from '../common/type-utils'; +import type { Nullable } from '../common/type-util'; import type { IObjectArrayPrimitiveType } from '../shared/object-matrix'; import { getArrayLength } from '../shared/object-matrix'; import { BooleanNumber } from '../types/enum'; diff --git a/packages/core/src/sheets/empty-snapshot.ts b/packages/core/src/sheets/empty-snapshot.ts new file mode 100644 index 0000000000..64b80db686 --- /dev/null +++ b/packages/core/src/sheets/empty-snapshot.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LocaleType } from '../types/enum/locale-type'; +import type { IWorkbookData } from '../types/interfaces'; +import { version } from '../../package.json'; + +export function getEmptySnapshot( + unitID = '', + locale = LocaleType.ZH_CN, + name = '' +): IWorkbookData { + const DEFAULT_WORKBOOK_DATA: IWorkbookData = { + id: unitID, + sheetOrder: [], + name, + appVersion: version, + locale, + styles: {}, + sheets: {}, + resources: [], + }; + + return DEFAULT_WORKBOOK_DATA; +} diff --git a/packages/core/src/sheets/row-manager.ts b/packages/core/src/sheets/row-manager.ts index b69e636ba9..15edc27830 100644 --- a/packages/core/src/sheets/row-manager.ts +++ b/packages/core/src/sheets/row-manager.ts @@ -19,6 +19,7 @@ import type { Nullable } from '../shared/types'; import { BooleanNumber } from '../types/enum'; import type { IRange, IRowData, IWorksheetData } from '../types/interfaces'; import { RANGE_TYPE } from '../types/interfaces'; +import type { SheetViewModel } from './view-model'; /** * Manage configuration information of all rows, get row height, row length, set row height, etc. @@ -28,6 +29,7 @@ export class RowManager { constructor( private readonly _config: IWorksheetData, + private readonly _viewModel: SheetViewModel, data: IObjectArrayPrimitiveType> ) { this._rowData = data; @@ -112,7 +114,7 @@ export class RowManager { let startRow = -1; for (let i = start; i <= end; i++) { - const visible = this.getRowVisible(i); + const visible = this.getRowRawVisible(i); if (inHiddenRange && visible) { inHiddenRange = false; hiddenRows.push({ @@ -148,7 +150,7 @@ export class RowManager { let startRow = -1; for (let i = start; i <= end; i++) { - const visible = this.getRowVisible(i); + const visible = this.getRowRawVisible(i); if (inVisibleRange && !visible) { inVisibleRange = false; visibleRows.push({ @@ -171,13 +173,13 @@ export class RowManager { return visibleRows; } - getRowVisible(rowPos: number): boolean { - const row = this.getRow(rowPos); - if (!row) { + getRowRawVisible(row: number): boolean { + const rowData = this.getRow(row); + if (!rowData) { return true; } - return row.hd !== BooleanNumber.TRUE; + return rowData.hd !== BooleanNumber.TRUE; } /** diff --git a/packages/core/src/sheets/sheet-snapshot-utils.ts b/packages/core/src/sheets/sheet-snapshot-utils.ts index 8d99ef1081..58954c42ce 100644 --- a/packages/core/src/sheets/sheet-snapshot-utils.ts +++ b/packages/core/src/sheets/sheet-snapshot-utils.ts @@ -26,10 +26,10 @@ export const DEFAULT_WORKSHEET_COLUMN_COUNT_KEY = 'DEFAULT_WORKSHEET_COLUMN_COUN export const DEFAULT_WORKSHEET_COLUMN_COUNT = 20; export const DEFAULT_WORKSHEET_ROW_HEIGHT_KEY = 'DEFAULT_WORKSHEET_ROW_HEIGHT'; -export const DEFAULT_WORKSHEET_ROW_HEIGHT = 19; +export const DEFAULT_WORKSHEET_ROW_HEIGHT = 24; export const DEFAULT_WORKSHEET_COLUMN_WIDTH_KEY = 'DEFAULT_WORKSHEET_COLUMN_WIDTH'; -export const DEFAULT_WORKSHEET_COLUMN_WIDTH = 73; +export const DEFAULT_WORKSHEET_COLUMN_WIDTH = 88; export const DEFAULT_WORKSHEET_ROW_TITLE_WIDTH_KEY = 'DEFAULT_WORKSHEET_ROW_TITLE_WIDTH'; export const DEFAULT_WORKSHEET_ROW_TITLE_WIDTH = 46; @@ -46,7 +46,7 @@ export const DEFAULT_WORKSHEET_COLUMN_TITLE_HEIGHT = 20; */ export function mergeWorksheetSnapshotWithDefault(snapshot: Partial): IWorksheetData { const defaultSnapshot: IWorksheetData = { - name: 'Sheet1', + name: 'Sheet1', // TODO: name should have i18n id: 'sheet-01', tabColor: '', hidden: BooleanNumber.FALSE, diff --git a/packages/core/src/sheets/view-model.ts b/packages/core/src/sheets/view-model.ts index 7f85c6f038..a84cdca60f 100644 --- a/packages/core/src/sheets/view-model.ts +++ b/packages/core/src/sheets/view-model.ts @@ -16,13 +16,15 @@ import type { IDisposable } from '@wendellhu/redi'; -import { remove } from '../common/array'; -import type { Nullable } from '../common/type-utils'; +import type { Nullable } from '../common/type-util'; import { Disposable, toDisposable } from '../shared/lifecycle'; -import type { ICellDataForSheetInterceptor } from '../types/interfaces/i-cell-data'; +import type { ICellData, ICellDataForSheetInterceptor } from '../types/interfaces/i-cell-data'; +/** + * @internal + */ export interface ICellContentInterceptor { - getCell(row: number, col: number): Nullable; + getCell: (row: number, col: number) => Nullable; } export interface IRowFilteredInterceptor {} @@ -32,76 +34,68 @@ export interface IRowVisibleInterceptor {} export interface IColVisibleInterceptor {} export interface ISheetViewModel { - registerCellContentInterceptor(interceptor: ICellContentInterceptor): IDisposable; - registerRowFilteredInterceptor(interceptor: IRowFilteredInterceptor): IDisposable; - registerRowVisibleInterceptor(interceptor: IRowVisibleInterceptor): IDisposable; - registerColVisibleInterceptor(interceptor: IColVisibleInterceptor): IDisposable; + registerCellContentInterceptor: (interceptor: ICellContentInterceptor) => IDisposable; + registerRowFilteredInterceptor: (interceptor: IRowFilteredInterceptor) => IDisposable; + registerRowVisibleInterceptor: (interceptor: IRowVisibleInterceptor) => IDisposable; + registerColVisibleInterceptor: (interceptor: IColVisibleInterceptor) => IDisposable; - getCell(row: number, col: number): Nullable; + getCell: (row: number, col: number) => Nullable; +} + +/** + * @internal + */ +export interface IRowFilteredInterceptor { + getRowFiltered(row: number): boolean; } /** - * SheetViewModel + * @internal */ -export class SheetViewModel extends Disposable implements ISheetViewModel { - private readonly _cellContentInterceptors: ICellContentInterceptor[] = []; - private readonly _rowFilteredInterceptors: IRowFilteredInterceptor[] = []; - private readonly _rowVisibleInterceptors: IRowVisibleInterceptor[] = []; - private readonly _colVisibleInterceptors: IColVisibleInterceptor[] = []; +export class SheetViewModel extends Disposable { + private _cellContentInterceptor: Nullable = null; + private _rowFilteredInterceptor: Nullable = null; + + constructor( + private readonly getRawCell: (row: number, col: number) => Nullable + ) { + super(); + } override dispose(): void { super.dispose(); - this._cellContentInterceptors.length = 0; - this._rowFilteredInterceptors.length = 0; - this._rowVisibleInterceptors.length = 0; - this._colVisibleInterceptors.length = 0; + this._cellContentInterceptor = null; + this._rowFilteredInterceptor = null; } getCell(row: number, col: number): Nullable { - for (const interceptor of this._cellContentInterceptors) { - const result = interceptor.getCell(row, col); - if (typeof result !== 'undefined') { - return result; - } + if (this._cellContentInterceptor) { + return this._cellContentInterceptor.getCell(row, col); } - return null; + return this.getRawCell(row, col); } - registerCellContentInterceptor(interceptor: ICellContentInterceptor): IDisposable { - if (this._cellContentInterceptors.includes(interceptor)) { - throw new Error('[SheetViewModel]: Interceptor already registered.'); - } - - this._cellContentInterceptors.push(interceptor); - return toDisposable(() => remove(this._cellContentInterceptors, interceptor)); + getRowFiltered(row: number): boolean { + return this._rowFilteredInterceptor?.getRowFiltered(row) ?? false; } - registerRowFilteredInterceptor(interceptor: IRowFilteredInterceptor): IDisposable { - if (this._rowFilteredInterceptors.includes(interceptor)) { - throw new Error('[SheetViewModel]: Interceptor already registered.'); - } - - this._rowFilteredInterceptors.push(interceptor); - return toDisposable(() => remove(this._rowFilteredInterceptors, interceptor)); - } - - registerRowVisibleInterceptor(interceptor: IRowVisibleInterceptor): IDisposable { - if (this._rowVisibleInterceptors.includes(interceptor)) { + registerCellContentInterceptor(interceptor: ICellContentInterceptor): IDisposable { + if (this._cellContentInterceptor) { throw new Error('[SheetViewModel]: Interceptor already registered.'); } - this._rowVisibleInterceptors.push(interceptor); - return toDisposable(() => remove(this._rowVisibleInterceptors, interceptor)); + this._cellContentInterceptor = interceptor; + return toDisposable(() => this._cellContentInterceptor = null); } - registerColVisibleInterceptor(interceptor: IColVisibleInterceptor): IDisposable { - if (this._colVisibleInterceptors.includes(interceptor)) { + registerRowFilteredInterceptor(interceptor: IRowFilteredInterceptor): IDisposable { + if (this._rowFilteredInterceptor) { throw new Error('[SheetViewModel]: Interceptor already registered.'); } - this._colVisibleInterceptors.push(interceptor); - return toDisposable(() => remove(this._colVisibleInterceptors, interceptor)); + this._rowFilteredInterceptor = interceptor; + return toDisposable(() => this._rowFilteredInterceptor = null); } } diff --git a/packages/core/src/sheets/workbook.ts b/packages/core/src/sheets/workbook.ts index d5cf25e5bf..d16717b0d3 100644 --- a/packages/core/src/sheets/workbook.ts +++ b/packages/core/src/sheets/workbook.ts @@ -19,8 +19,7 @@ import { BehaviorSubject, Subject } from 'rxjs'; import { ILogService } from '../services/log/log.service'; import type { Nullable } from '../shared'; import { Tools } from '../shared'; -import { Disposable } from '../shared/lifecycle'; -import { DEFAULT_RANGE_ARRAY, DEFAULT_WORKBOOK } from '../types/const'; +import { DEFAULT_RANGE_ARRAY } from '../types/const'; import { BooleanNumber } from '../types/enum'; import type { IColumnStartEndData, @@ -32,8 +31,10 @@ import type { IWorkbookData, IWorksheetData, } from '../types/interfaces'; +import { UnitModel, UniverInstanceType } from '../common/unit'; import { Styles } from './styles'; import { Worksheet } from './worksheet'; +import { getEmptySnapshot } from './empty-snapshot'; export function getWorksheetUID(workbook: Workbook, worksheet: Worksheet): string { return `${workbook.getUnitId()}|${worksheet.getSheetId()}`; @@ -42,7 +43,9 @@ export function getWorksheetUID(workbook: Workbook, worksheet: Worksheet): strin /** * Access and create Univer Sheets files */ -export class Workbook extends Disposable { +export class Workbook extends UnitModel { + override type: UniverInstanceType.UNIVER_SHEET = UniverInstanceType.UNIVER_SHEET; + private readonly _sheetCreated$ = new Subject(); readonly sheetCreated$ = this._sheetCreated$.asObservable(); @@ -87,7 +90,12 @@ export class Workbook extends Disposable { ) { super(); - this._snapshot = Tools.commonExtend(DEFAULT_WORKBOOK, workbookData); + const DEFAULT_WORKBOOK = getEmptySnapshot(); + if (Tools.isEmptyObject(workbookData)) { + this._snapshot = DEFAULT_WORKBOOK; + } else { + this._snapshot = Tools.commonExtend(DEFAULT_WORKBOOK, workbookData); + } const { styles } = this._snapshot; if (this._snapshot.id == null || this._snapshot.id.length === 0) { @@ -126,6 +134,10 @@ export class Workbook extends Disposable { return this._snapshot.name; } + setName(name: string): void { + this._snapshot.name = name; + } + getUnitId() { return this._unitId; } @@ -158,17 +170,13 @@ export class Workbook extends Disposable { sheets[id] = worksheetSnapshot; sheetOrder.splice(index, 0, id); - const worksheet = new Worksheet(worksheetSnapshot, this._styles); + const worksheet = new Worksheet(this._unitId, worksheetSnapshot, this._styles); this._worksheets.set(id, worksheet); this._sheetCreated$.next(worksheet); return true; } - getParentRenderUnitId() { - return this._snapshot.parentRenderUnitId; - } - getSheetOrders(): Readonly { return this._snapshot.sheetOrder; } @@ -530,7 +538,7 @@ export class Workbook extends Disposable { this._logService.debug('[Workbook]', `The worksheet name ${name} is duplicated, we changed it to ${worksheetSnapshot.name}. Please fix the problem in your snapshot.`); } - const worksheet = new Worksheet(worksheetSnapshot, this._styles); + const worksheet = new Worksheet(this._unitId, worksheetSnapshot, this._styles); _worksheets.set(sheetId, worksheet); if (!sheetOrder.includes(sheetId)) { diff --git a/packages/core/src/sheets/worksheet.ts b/packages/core/src/sheets/worksheet.ts index edc8f32f02..7a5ce040ed 100644 --- a/packages/core/src/sheets/worksheet.ts +++ b/packages/core/src/sheets/worksheet.ts @@ -14,12 +14,10 @@ * limitations under the License. */ -import { CellValueType } from '@univerjs/protocol'; - import type { Nullable } from '../shared'; import { ObjectMatrix, Rectangle, Tools } from '../shared'; import { createRowColIter } from '../shared/row-col-iter'; -import type { BooleanNumber } from '../types/enum'; +import { type BooleanNumber, CellValueType } from '../types/enum'; import type { ICellData, ICellDataForSheetInterceptor, IFreeze, IRange, IWorksheetData } from '../types/interfaces'; import { ColumnManager } from './column-manager'; import { Range } from './range'; @@ -42,6 +40,7 @@ export class Worksheet { protected readonly _viewModel: SheetViewModel; constructor( + public readonly unitId: string, snapshot: Partial, private readonly _styles: Styles ) { @@ -50,11 +49,11 @@ export class Worksheet { const { columnData, rowData, cellData } = this._snapshot; this._sheetId = this._snapshot.id ?? Tools.generateRandomId(6); this._cellData = new ObjectMatrix(cellData); - this._rowManager = new RowManager(this._snapshot, rowData); - this._columnManager = new ColumnManager(this._snapshot, columnData); // This view model will immediately injected with hooks from SheetViewModel service as Worksheet is constructed. - this._viewModel = new SheetViewModel(); + this._viewModel = new SheetViewModel((row, col) => this.getCellRaw(row, col)); + this._rowManager = new RowManager(this._snapshot, this._viewModel, rowData); + this._columnManager = new ColumnManager(this._snapshot, columnData); } /** @@ -170,6 +169,13 @@ export class Worksheet { return this._rowManager; } + /** + * Returns the ID of its parent unit. + */ + getUnitId(): string { + return this.unitId; + } + /** * Returns the ID of the sheet represented by this object. * @returns ID of the sheet @@ -203,7 +209,7 @@ export class Worksheet { const { _snapshot: _config } = this; const copy = Tools.deepClone(_config); - return new Worksheet(copy, this._styles); + return new Worksheet(this.unitId, copy, this._styles); } getMergeData(): IRange[] { @@ -244,6 +250,10 @@ export class Worksheet { return this.getCellMatrix().getValue(row, col); } + getRowFiltered(row: number): boolean { + return this._viewModel.getRowFiltered(row); + } + /** * Get cell matrix from a given range and pick out non-first cells of merged cells. * @@ -254,7 +264,8 @@ export class Worksheet { row: number, col: number, endRow: number, - endCol: number + endCol: number, + isRaw = false ): ObjectMatrix { const matrix = this.getCellMatrix(); @@ -263,13 +274,12 @@ export class Worksheet { Rectangle.intersects({ startRow: row, startColumn: col, endRow, endColumn: endCol }, rect) ); - const ret = new ObjectMatrix(); - // iterate all cells in the range + const returnCellMatrix = new ObjectMatrix(); createRowColIter(row, endRow, col, endCol).forEach((row, col) => { - const v = matrix.getValue(row, col); + const v = isRaw ? this.getCellRaw(row, col) : this.getCell(row, col); if (v) { - ret.setValue(row, col, v); + returnCellMatrix.setValue(row, col, v); } }); @@ -279,7 +289,7 @@ export class Worksheet { const { startColumn, startRow, endColumn, endRow } = mergedCell; createRowColIter(startRow, endRow, startColumn, endColumn).forEach((row, col) => { if (row === startRow && col === startColumn) { - ret.setValue(row, col, { + returnCellMatrix.setValue(row, col, { ...matrix.getValue(row, col), rowSpan: endRow - startRow + 1, colSpan: endColumn - startColumn + 1, @@ -287,12 +297,12 @@ export class Worksheet { } if (row !== startRow || col !== startColumn) { - ret.realDeleteValue(row, col); + returnCellMatrix.realDeleteValue(row, col); } }); }); - return ret; + return returnCellMatrix; } getRange(range: IRange): Range; @@ -425,15 +435,35 @@ export class Worksheet { /** * Gets the height in pixels of the given row. - * @param rowPosition row index + * @param row row index * @returns Gets the height in pixels of the given row. */ - getRowHeight(rowPosition: number): number { - return this.getRowManager().getRowHeight(rowPosition); + getRowHeight(row: number): number { + const filtered = this._viewModel.getRowFiltered(row); + if (filtered) return 0; + + return this.getRowManager().getRowHeight(row); } + /** + * Get if the row in visible. It may be affected by features like filter and view. + * @param row the row index + * @returns if the row in visible to the user + */ getRowVisible(row: number): boolean { - return this.getRowManager().getRowVisible(row); + const filtered = this._viewModel.getRowFiltered(row); + if (filtered) return false; + + return this.getRowRawVisible(row); + } + + /** + * Get if the row does not have `hidden` property. + * @param row the row index + * @returns if the row does not have `hidden` property + */ + getRowRawVisible(row: number): boolean { + return this.getRowManager().getRowRawVisible(row); } getHiddenRows(start?: number, end?: number): IRange[] { @@ -513,52 +543,67 @@ export class Worksheet { * Iterate a range row by row. * * Performance intensive. + * + * @param range the iterate range + * @param skipEmpty whether to skip empty cells, default to be `true` */ - iterateByRow(range: IRange): Iterator, Readonly> { + iterateByRow(range: IRange, skipEmpty = true): Iterable> { const { startRow, startColumn, endRow, endColumn } = range; // eslint-disable-next-line ts/no-this-alias const worksheet = this; - let rowIndex = startRow; - let columnIndex = startColumn; - return { - next(): IteratorResult> { - while (true) { - if (columnIndex > endColumn) { - rowIndex += 1; - columnIndex = startColumn; - } - - if (rowIndex > endRow) { - return { done: true, value: undefined }; - } - - // search for the next cell that is not non-top-left cell of a merged cell - const cellValue = worksheet.getCell(rowIndex, columnIndex); - const mergedCell = worksheet.getMergedCell(rowIndex, columnIndex); - if ( - mergedCell && - (!cellValue || rowIndex !== mergedCell.startRow || columnIndex !== mergedCell.startColumn) - ) { - columnIndex = mergedCell.endColumn + 1; - // continue searching - } else if (!cellValue) { - columnIndex += 1; - // continue searching - } else { - const value: ICell = { row: rowIndex, col: columnIndex, value: cellValue }; - if (mergedCell) { - value.colSpan = mergedCell.endColumn - mergedCell.startColumn + 1; - value.rowSpan = mergedCell.endRow - mergedCell.startRow + 1; + [Symbol.iterator]: () => { + let rowIndex = startRow; + let columnIndex = startColumn; + + return { + next(): IteratorResult> { + while (true) { + if (columnIndex > endColumn) { + rowIndex += 1; + columnIndex = startColumn; + } + + if (rowIndex > endRow) { + return { done: true, value: undefined }; + } + + // search for the next cell that is not non-top-left cell of a merged cell + const cellValue = worksheet.getCell(rowIndex, columnIndex); + const isEmptyCell = !cellValue; + const mergedCell = worksheet.getMergedCell(rowIndex, columnIndex); + + if (mergedCell) { + const isNotTopLeft = rowIndex !== mergedCell.startRow || columnIndex !== mergedCell.startColumn; + if (isNotTopLeft) { + columnIndex = mergedCell.endColumn + 1; + continue; + } + + if (isEmptyCell && skipEmpty) { + columnIndex = mergedCell.endColumn + 1; + continue; + } + + const value: ICell = { row: rowIndex, col: columnIndex, value: cellValue }; + value.colSpan = mergedCell.endColumn - mergedCell.startColumn + 1; + value.rowSpan = mergedCell.endRow - mergedCell.startRow + 1; + columnIndex = mergedCell.endColumn + 1; + return { done: false, value }; + } + + if (isEmptyCell && skipEmpty) { + columnIndex += 1; + } else { + const value: ICell = { row: rowIndex, col: columnIndex, value: cellValue }; + columnIndex += 1; + return { done: false, value }; + } } - - // we still need to move to the next position by leave searching to the next time `next` get called - columnIndex += 1; - return { done: false, value }; - } - } + }, + }; }, }; } @@ -567,57 +612,77 @@ export class Worksheet { * Iterate a range column by column. This is pretty similar to `iterateByRow` but with different order. * * Performance intensive. + * + * @param range The iterate range. + * @param skipEmpty Whether to skip empty cells, default to be `true`. + * @param skipNonTopLeft Whether to skip non-top-left cells of merged cells, default to be `true`. If the + * parameter is set to `false`, the iterator will return cells in the top row. */ - iterateByColumn(range: IRange): Iterator, Readonly> { + iterateByColumn(range: IRange, skipEmpty = true, skipNonTopLeft = true): Iterable> { const { startRow, startColumn, endRow, endColumn } = range; // eslint-disable-next-line ts/no-this-alias const worksheet = this; - let rowIndex = startRow; - let columnIndex = startColumn; - return { - next(): IteratorResult> { - while (true) { - if (rowIndex > endRow) { - columnIndex += 1; - rowIndex = startRow; - } - - if (columnIndex > endColumn) { - return { done: true, value: undefined }; - } - - // search for the next cell that is not non-top-left cell of a merged cell - const cellValue = worksheet.getCell(rowIndex, columnIndex); - const mergedCell = worksheet.getMergedCell(rowIndex, columnIndex); - if ( - mergedCell && - (!cellValue || rowIndex !== mergedCell.startRow || columnIndex !== mergedCell.startColumn) - ) { - rowIndex = mergedCell.endRow + 1; - // continue searching - } else if (!cellValue) { - rowIndex += 1; - // continue searching - } else { - const value: ICell = { row: rowIndex, col: columnIndex, value: cellValue }; - if (mergedCell) { - value.colSpan = mergedCell.endColumn - mergedCell.startColumn + 1; - value.rowSpan = mergedCell.endRow - mergedCell.startRow + 1; + [Symbol.iterator]: () => { + let rowIndex = startRow; + let columnIndex = startColumn; + + return { + next(): IteratorResult> { + while (true) { + if (rowIndex > endRow) { + columnIndex += 1; + rowIndex = startRow; + } + + if (columnIndex > endColumn) { + return { done: true, value: undefined }; + } + + // search for the next cell that is not non-top-left cell of a merged cell + const mergedCell = worksheet.getMergedCell(rowIndex, columnIndex); + + if (mergedCell) { + const isNotTop = rowIndex !== mergedCell.startRow; + const isNotTopLeft = isNotTop || columnIndex !== mergedCell.startColumn; + if ((skipNonTopLeft && isNotTopLeft) || (!skipNonTopLeft && isNotTop)) { + rowIndex = mergedCell.endRow + 1; + continue; + } + + const cellValue = worksheet.getCell(mergedCell.startRow, mergedCell.startColumn); + const isEmptyCell = !cellValue; + if (isEmptyCell && skipEmpty) { + rowIndex = mergedCell.endRow + 1; + continue; + } + + const value: ICell = { row: rowIndex, col: mergedCell.startColumn, value: cellValue }; + value.colSpan = mergedCell.endColumn - mergedCell.startColumn + 1; + value.rowSpan = mergedCell.endRow - mergedCell.startRow + 1; + rowIndex = mergedCell.endRow + 1; + return { done: false, value }; + } + + const cellValue = worksheet.getCell(rowIndex, columnIndex); + const isEmptyCell = !cellValue; + if (isEmptyCell && skipEmpty) { + rowIndex += 1; + } else { + const value: ICell = { row: rowIndex, col: columnIndex, value: cellValue }; + rowIndex += 1; + return { done: false, value }; + } } - - // we still need to move to the next position by leave searching to the next time `next` get called - rowIndex += 1; - return { done: false, value }; - } - } + }, + }; }, }; - } - // #endregion + // #endregion + } } /** @@ -628,7 +693,7 @@ export interface ICell { col: number; rowSpan?: number; colSpan?: number; - value: ICellData; + value: Nullable; } /** @@ -636,7 +701,9 @@ export interface ICell { * @param cell * @returns pure text in this cell */ -export function extractPureTextFromCell(cell: ICellData): string { +export function extractPureTextFromCell(cell: Nullable): string { + if (!cell) return ''; + const richTextValue = cell.p?.body?.dataStream; if (richTextValue) return richTextValue; diff --git a/packages/core/src/slides/domain/slide-model.ts b/packages/core/src/slides/slide-model.ts similarity index 80% rename from packages/core/src/slides/domain/slide-model.ts rename to packages/core/src/slides/slide-model.ts index 79696e0bb8..02ccc40069 100644 --- a/packages/core/src/slides/domain/slide-model.ts +++ b/packages/core/src/slides/slide-model.ts @@ -14,17 +14,22 @@ * limitations under the License. */ -import { Tools } from '../../shared'; -import { DEFAULT_SLIDE } from '../../types/const'; -import type { ISlideData, ISlidePage } from '../../types/interfaces'; -import { PageType } from '../../types/interfaces'; +import { UnitModel, UniverInstanceType } from '../common/unit'; +import { Tools } from '../shared'; +import { DEFAULT_SLIDE } from '../types/const'; +import type { ISlideData, ISlidePage } from '../types/interfaces'; +import { PageType } from '../types/interfaces'; + +export class SlideDataModel extends UnitModel { + override type: UniverInstanceType.UNIVER_SLIDE = UniverInstanceType.UNIVER_SLIDE; -export class SlideDataModel { private _snapshot: ISlideData; private _unitId: string; constructor(snapshot: Partial) { + super(); + this._snapshot = { ...DEFAULT_SLIDE, ...snapshot }; this._unitId = this._snapshot.id ?? Tools.generateRandomId(6); } @@ -33,10 +38,6 @@ export class SlideDataModel { return this._snapshot.container; } - getParentRenderUnitId() { - return this._snapshot.parentRenderUnitId; - } - getSnapshot() { return this._snapshot; } diff --git a/packages/core/src/types/const/const.ts b/packages/core/src/types/const/const.ts index 55afc5e502..465c7608ac 100644 --- a/packages/core/src/types/const/const.ts +++ b/packages/core/src/types/const/const.ts @@ -17,13 +17,10 @@ import { BooleanNumber, HorizontalAlign, - LocaleType, TextDirection, VerticalAlign, WrapStrategy, } from '../enum'; -import type { IWorkbookData } from '../interfaces'; -import { version } from '../../../package.json'; /** * Used as an illegal range array return value @@ -65,20 +62,6 @@ export const DEFAULT_CELL = { column: 0, }; -/** - * Used as an init workbook return value - */ -export const DEFAULT_WORKBOOK: IWorkbookData = { - id: '', - sheetOrder: [], - name: '', - appVersion: version, - locale: LocaleType.ZH_CN, - styles: {}, - sheets: {}, - resources: [], -}; - /** * Default styles. */ @@ -86,11 +69,11 @@ export const DEFAULT_STYLES = { /** * fontFamily */ - ff: 'Times New Roman', + ff: 'Arial', /** * fontSize */ - fs: 14, + fs: 11, /** * italic * 0: false diff --git a/packages/core/src/types/enum/condition-type.ts b/packages/core/src/types/enum/condition-type.ts deleted file mode 100644 index 3a5784d6c6..0000000000 --- a/packages/core/src/types/enum/condition-type.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright 2023-present DreamNum Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export enum ConditionType { - CONDITION_TYPE_UNSPECIFIED, // The default value, do not use. - NUMNUMBER_BETWEENR_GREATER, // The cell's value must be greater than the condition's value. Supported by data validation, conditional formatting and filters. Requires a single ConditionValue - NUMBER_GREATER_THAN_EQ, // The cell's value must be greater than or equal to the condition's value. Supported by data validation, conditional formatting and filters. Requires a single ConditionValue . - NUMBER_LESS, // The cell's value must be less than the condition's value. Supported by data validation, conditional formatting and filters. Requires a single ConditionValue - NUMBER_LESS_THAN_EQ, // The cell's value must be less than or equal to the condition's value. Supported by data validation, conditional formatting and filters. Requires a single ConditionValue . - NUMBER_EQ, // The cell's value must be equal to the condition's value. Supported by data validation, conditional formatting and filters. Requires a single ConditionValue for data validation, conditional formatting, and filters on non-data source objects and at least one ConditionValue for filters on data source objects. - NUMBER_NOT_EQ, // The cell's value must be not equal to the condition's value. Supported by data validation, conditional formatting and filters. Requires a single ConditionValue for data validation, conditional formatting, and filters on non-data source objects and at least one ConditionValue for filters on data source objects. - NUMBER_BETWEEN, // The cell's value must be between the two condition values. Supported by data validation, conditional formatting and filters. Requires exactly two ConditionValues . - NUMBER_NOT_BETWEEN, // The cell's value must not be between the two condition values. Supported by data validation, conditional formatting and filters. Requires exactly two ConditionValues . - TEXT_CONTAINS, // The cell's value must contain the condition's value. Supported by data validation, conditional formatting and filters. Requires a single ConditionValue . - TEXT_NOT_CONTAINS, // The cell's value must not contain the condition's value. Supported by data validation, conditional formatting and filters. Requires a single ConditionValue - TEXT_STARTS_WITH, // The cell's value must start with the condition's value. Supported by conditional formatting and filters. Requires a single ConditionValue . - TEXT_ENDS_WITH, // The cell's value must end with the condition's value. Supported by conditional formatting and filters. Requires a single ConditionValue . - TEXT_EQ, // The cell's value must be exactly the condition's value. Supported by data validation, conditional formatting and filters. Requires a single ConditionValue for data validation, conditional formatting, and filters on non-data source objects and at least one ConditionValue for filters on data source objects. - TEXT_IS_EMAIL, // The cell's value must be a valid email address. Supported by data validation. Requires no ConditionValues . - TEXT_IS_URL, // The cell's value must be a valid URL. Supported by data validation. Requires no ConditionValues . - DATE_EQ, // The cell's value must be the same date as the condition's value. Supported by data validation, conditional formatting and filters. Requires a single ConditionValue for data validation, conditional formatting, and filters on non-data source objects and at least one ConditionValue for filters on data source objects. - DATE_BEFORE, // The cell's value must be before the date of the condition's value. Supported by data validation, conditional formatting and filters. Requires a single ConditionValue that may be a relative date . - DATE_AFTER, // The cell's value must be after the date of the condition's value. Supported by data validation, conditional formatting and filters. Requires a single ConditionValue that may be a relative date . - DATE_ON_OR_BEFORE, // The cell's value must be on or before the date of the condition's value. Supported by data validation. Requires a single ConditionValue that may be a relative date . - DATE_ON_OR_AFTER, // The cell's value must be on or after the date of the condition's value. Supported by data validation. Requires a single ConditionValue that may be a relative date . - DATE_BETWEEN, // The cell's value must be between the dates of the two condition values. Supported by data validation. Requires exactly two ConditionValues . - DATE_NOT_BETWEEN, // The cell's value must be outside the dates of the two condition values. Supported by data validation. Requires exactly two ConditionValues . - DATE_IS_VALID, // The cell's value must be a date. Supported by data validation. Requires no ConditionValues . - ONE_OF_RANGE, // The cell's value must be listed in the grid in condition value's range. Supported by data validation. Requires a single ConditionValue , and the value must be a valid range in A1 notation. - ONE_OF_LIST, // The cell's value must be in the list of condition values. Supported by data validation. Supports any number of condition values , one per item in the list. Formulas are not supported in the values. - BLANK, // The cell's value must be empty. Supported by conditional formatting and filters. Requires no ConditionValues . - NOT_BLANK, // The cell's value must not be empty. Supported by conditional formatting and filters. Requires no ConditionValues . - CUSTOM_FORMULA, // The condition's formula must evaluate to true. Supported by data validation, conditional formatting and filters. Not supported by data source sheet filters. Requires a single ConditionValue . - BOOLEAN, // The cell's value must be TRUE/FALSE or in the list of condition values. Supported by data validation. Renders as a cell checkbox. Supports zero, one or two ConditionValues . No values indicates the cell must be TRUE or FALSE, where TRUE renders as checked and FALSE renders as unchecked. One value indicates the cell will render as checked when it contains that value and unchecked when it is blank. Two values indicate that the cell will render as checked when it contains the first value and unchecked when it contains the second value. For example, ["Yes","No"] indicates that the cell will render a checked box when it has the value "Yes" and an unchecked box when it has the value "No". - TEXT_NOT_EQ, // The cell's value must be exactly not the condition's value. Supported by filters on data source objects. Requires at least one ConditionValue . - DATE_NOT_EQ, // The cell's value must be exactly not the condition's value. Supported by filters on data source objects. Requires at least one ConditionValue . -} diff --git a/packages/core/src/types/enum/data-validation-error-style.ts b/packages/core/src/types/enum/data-validation-error-style.ts new file mode 100644 index 0000000000..befa6c3e18 --- /dev/null +++ b/packages/core/src/types/enum/data-validation-error-style.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export enum DataValidationErrorStyle { + INFO, + STOP, + WARNING, +} diff --git a/packages/core/src/types/enum/data-validation-ime-mode.ts b/packages/core/src/types/enum/data-validation-ime-mode.ts new file mode 100644 index 0000000000..a85677d30a --- /dev/null +++ b/packages/core/src/types/enum/data-validation-ime-mode.ts @@ -0,0 +1,29 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export enum DataValidationImeMode { + DISABLED, + FULL_ALPHA, + FULL_HANGUL, + FULL_KATAKANA, + HALF_ALPHA, + HALF_HANGUL, + HALF_KATAKANA, + HIRAGANA, + NO_CONTROL, + OFF, + ON, +} diff --git a/packages/core/src/types/enum/data-validation-operator.ts b/packages/core/src/types/enum/data-validation-operator.ts new file mode 100644 index 0000000000..fc989b1ffc --- /dev/null +++ b/packages/core/src/types/enum/data-validation-operator.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export enum DataValidationOperator { + BETWEEN = 'between', + EQUAL = 'equal', + GREATER_THAN = 'greaterThan', + GREATER_THAN_OR_EQUAL = 'greaterThanOrEqual', + LESS_THAN = 'lessThan', + LESS_THAN_OR_EQUAL = 'lessThanOrEqual', + NOT_BETWEEN = 'notBetween', + NOT_EQUAL = 'notEqual', +} diff --git a/packages/docs-ui/src/locale/index.ts b/packages/core/src/types/enum/data-validation-render-mode.ts similarity index 87% rename from packages/docs-ui/src/locale/index.ts rename to packages/core/src/types/enum/data-validation-render-mode.ts index 1eff5fc204..b06170f525 100644 --- a/packages/docs-ui/src/locale/index.ts +++ b/packages/core/src/types/enum/data-validation-render-mode.ts @@ -14,5 +14,8 @@ * limitations under the License. */ -export { default as enUS } from './en-US'; -export { default as zhCN } from './zh-CN'; +export enum DataValidationRenderMode { + TEXT = 0, + ARROW = 1, + CUSTOM = 2, +} diff --git a/packages/engine-render/src/components/docs/layout/linebreak/break.ts b/packages/core/src/types/enum/data-validation-status.ts similarity index 84% rename from packages/engine-render/src/components/docs/layout/linebreak/break.ts rename to packages/core/src/types/enum/data-validation-status.ts index 467286aaac..71d23e1858 100644 --- a/packages/engine-render/src/components/docs/layout/linebreak/break.ts +++ b/packages/core/src/types/enum/data-validation-status.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -export class Break { - constructor( - public position: number, - public required = false - ) {} +export enum DataValidationStatus { + VALID = 'valid', + INVALID = 'invalid', + VALIDATING = 'validating', } diff --git a/packages/core/src/types/enum/data-validation-type.ts b/packages/core/src/types/enum/data-validation-type.ts new file mode 100644 index 0000000000..fe026a2924 --- /dev/null +++ b/packages/core/src/types/enum/data-validation-type.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export enum DataValidationType { + /** + * custom formula + */ + CUSTOM = 'custom', + LIST = 'list', + LIST_MULTIPLE = 'listMultiple', + NONE = 'none', + TEXT_LENGTH = 'textLength', + DATE = 'date', + TIME = 'time', + /** + * integer number + */ + WHOLE = 'whole', + /** + * decimal number + */ + DECIMAL = 'decimal', + CHECKBOX = 'checkbox', +} diff --git a/packages/core/src/types/enum/index.ts b/packages/core/src/types/enum/index.ts index 35ad9877cf..eebd4f3dc2 100644 --- a/packages/core/src/types/enum/index.ts +++ b/packages/core/src/types/enum/index.ts @@ -18,7 +18,6 @@ export * from './auto-fill-series'; export * from './border-style-types'; export * from './color-type'; export * from './common-hide-types'; -export * from './condition-type'; export * from './copy-paste-type'; export * from './developer-metadata-visibility'; export * from './dimension'; diff --git a/packages/core/src/types/enum/interpolation-point-type.ts b/packages/core/src/types/enum/interpolation-point-type.ts index e2807f3fa6..cd3a3e912a 100644 --- a/packages/core/src/types/enum/interpolation-point-type.ts +++ b/packages/core/src/types/enum/interpolation-point-type.ts @@ -16,9 +16,9 @@ export enum InterpolationPointType { INTERPOLATION_POINT_TYPE_UNSPECIFIED, // The default value, do not use. - MIN, // The interpolation point uses the minimum value in the cells over the range of the conditional format. - MAX, // The interpolation point uses the maximum value in the cells over the range of the conditional format. + MIN, // The interpolation point uses the minimum value in the cells over the range of the conditional formatting. + MAX, // The interpolation point uses the maximum value in the cells over the range of the conditional formatting. NUMBER, // The interpolation point uses exactly the value in InterpolationPoint.value - PERCENT, // The interpolation point is the given percentage over all the cells in the range of the conditional format. This is equivalent to NUMBER if the value was: =(MAX(FLATTEN(range)) * (value / 100)) + (MIN(FLATTEN(range)) * (1 - (value / 100))) (where errors in the range are ignored when flattening). - PERCENTILE, // The interpolation point is the given percentile over all the cells in the range of the conditional format. This is equivalent to NUMBER if the value was: =PERCENTILE(FLATTEN(range), value / 100) (where errors in the range are ignored when flattening). + PERCENT, // The interpolation point is the given percentage over all the cells in the range of the conditional formatting. This is equivalent to NUMBER if the value was: =(MAX(FLATTEN(range)) * (value / 100)) + (MIN(FLATTEN(range)) * (1 - (value / 100))) (where errors in the range are ignored when flattening). + PERCENTILE, // The interpolation point is the given percentile over all the cells in the range of the conditional formatting. This is equivalent to NUMBER if the value was: =PERCENTILE(FLATTEN(range), value / 100) (where errors in the range are ignored when flattening). } diff --git a/packages/core/src/types/enum/locale-type.ts b/packages/core/src/types/enum/locale-type.ts index b574765118..5b12424c3b 100644 --- a/packages/core/src/types/enum/locale-type.ts +++ b/packages/core/src/types/enum/locale-type.ts @@ -17,6 +17,7 @@ export enum LocaleType { EN_US = 'enUS', ZH_CN = 'zhCN', + RU_RU = 'ruRU', } export type LocaleTypes = `${LocaleType}`; diff --git a/packages/core/src/types/enum/text-style.ts b/packages/core/src/types/enum/text-style.ts index 6bb68c25ba..cbe0f8be45 100644 --- a/packages/core/src/types/enum/text-style.ts +++ b/packages/core/src/types/enum/text-style.ts @@ -123,8 +123,8 @@ export enum BaselineOffset { * General Boolean Enum */ export enum BooleanNumber { - FALSE, - TRUE, + FALSE = 0, + TRUE = 1, } /** diff --git a/packages/core/src/types/interfaces/__tests__/i-cell-data.spec.ts b/packages/core/src/types/interfaces/__tests__/i-cell-data.spec.ts index 2b87b547fa..d77b8581d1 100644 --- a/packages/core/src/types/interfaces/__tests__/i-cell-data.spec.ts +++ b/packages/core/src/types/interfaces/__tests__/i-cell-data.spec.ts @@ -16,10 +16,54 @@ import { describe, expect, it } from 'vitest'; -import { isCellV, isICellData } from '../i-cell-data'; +import { isCellV, isICellData, isNullCell } from '../i-cell-data'; + +const DOCUMENT_DATA = { + id: 'd', + body: { + dataStream: 'No.2824163\r\n', + textRuns: [ + { + st: 0, + ed: 2, + ts: { + cl: { + rgb: '#000', + }, + fs: 20, + }, + }, + { + st: 3, + ed: 10, + ts: { + cl: { + rgb: 'rgb(255, 0, 0)', + }, + fs: 20, + }, + }, + ], + paragraphs: [ + { + startIndex: 10, + }, + ], + }, + documentStyle: { + pageSize: { + width: Number.POSITIVE_INFINITY, + height: Number.POSITIVE_INFINITY, + }, + marginTop: 0, + marginBottom: 0, + marginRight: 2, + marginLeft: 2, + }, +}; describe('Test cell data', () => { - it('function isICellData', () => { + it('Function isICellData', () => { expect(isICellData({ s: '1' })).toBeTruthy(); expect(isICellData({ p: { id: '1' } })).toBeTruthy(); expect(isICellData({ v: '1' })).toBeTruthy(); @@ -33,7 +77,7 @@ describe('Test cell data', () => { expect(isICellData({})).toBeFalsy(); }); - it('function isCellV', () => { + it('Function isCellV', () => { expect(isCellV('1')).toBeTruthy(); expect(isCellV(1)).toBeTruthy(); expect(isCellV(true)).toBeTruthy(); @@ -43,4 +87,25 @@ describe('Test cell data', () => { expect(isCellV(undefined)).toBeFalsy(); expect(isCellV({})).toBeFalsy(); }); + + it('Function isNullCell', () => { + expect(isNullCell(null)).toBeTruthy(); + expect(isNullCell(undefined)).toBeTruthy(); + expect(isNullCell({ v: null })).toBeTruthy(); + expect(isNullCell({ v: undefined })).toBeTruthy(); + expect(isNullCell({ v: '' })).toBeTruthy(); + expect(isNullCell({ p: null })).toBeTruthy(); + expect(isNullCell({ p: undefined })).toBeTruthy(); + expect(isNullCell({ f: null })).toBeTruthy(); + expect(isNullCell({ f: undefined })).toBeTruthy(); + expect(isNullCell({ f: '' })).toBeTruthy(); + expect(isNullCell({ si: null })).toBeTruthy(); + expect(isNullCell({ si: undefined })).toBeTruthy(); + expect(isNullCell({ si: '' })).toBeTruthy(); + + expect(isNullCell({ v: 1 })).toBeFalsy(); + expect(isNullCell({ p: DOCUMENT_DATA })).toBeFalsy(); + expect(isNullCell({ f: '=SUM(A1)' })).toBeFalsy(); + expect(isNullCell({ si: 'id1' })).toBeFalsy(); + }); }); diff --git a/packages/core/src/types/interfaces/i-cell-custom-render.ts b/packages/core/src/types/interfaces/i-cell-custom-render.ts new file mode 100644 index 0000000000..7ff17e7cad --- /dev/null +++ b/packages/core/src/types/interfaces/i-cell-custom-render.ts @@ -0,0 +1,46 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable ts/no-explicit-any */ + +import type { Nullable } from '../../shared'; +import type { ISelectionCellWithCoord } from './i-selection-data'; +import type { IStyleData } from './i-style-data'; +import type { ICellDataForSheetInterceptor } from './i-cell-data'; + +export interface ICellRenderContext { + data: ICellDataForSheetInterceptor; + style: Nullable; + primaryWithCoord: ISelectionCellWithCoord; + unitId?: string; + subUnitId: string; + row: number; + col: number; +} + +/** + * @debt This shouldn't exist in core package. + * + * @deprecated This interface is subject to change in the future. + */ +export interface ICellCustomRender { + drawWith(ctx: CanvasRenderingContext2D, info: ICellRenderContext, skeleton: any, spreadsheets: any): void; + zIndex?: number; + isHit?: (position: { x: number;y: number }, info: ICellRenderContext) => boolean; + onPointerDown?: (info: ICellRenderContext, evt: any) => void; + onPointerEnter?: (info: ICellRenderContext, evt: any) => void; + onPointerLeave?: (info: ICellRenderContext, evt: any) => void; +} diff --git a/packages/core/src/types/interfaces/i-cell-data.ts b/packages/core/src/types/interfaces/i-cell-data.ts index 809ea390db..df7f1b51ee 100644 --- a/packages/core/src/types/interfaces/i-cell-data.ts +++ b/packages/core/src/types/interfaces/i-cell-data.ts @@ -16,8 +16,10 @@ import type { Nullable } from '../../shared/types'; import { CellValueType } from '../enum/text-style'; +import type { ICellCustomRender } from './i-cell-custom-render'; import type { IDocumentData } from './i-document-data'; import type { IStyleData } from './i-style-data'; +import type { ICellValidationData } from './i-cell-validation-data'; /** * Cell value type @@ -54,11 +56,32 @@ export interface ICellData { * Id of the formula. */ si?: Nullable; + + /** + * User stored custom fields + */ + custom?: Nullable>; +} + +export interface ICellMarksStyle { + color: string; + size: number; +} + +export interface ICellMarks { + tl?: ICellMarksStyle; + tr?: ICellMarksStyle; + bl?: ICellMarksStyle; + br?: ICellMarksStyle; } export interface ICellDataForSheetInterceptor extends ICellData { interceptorStyle?: Nullable; isInArrayFormulaRange?: Nullable; + dataValidation?: Nullable; + markers?: ICellMarks; + customRender?: Nullable; + interceptorAutoHeight?: number; } export function isICellData(value: any): value is ICellData { @@ -93,7 +116,7 @@ export function isNullCell(cell: Nullable) { return true; } - const { v, f, si, p, s } = cell; + const { v, f, si, p, s, custom } = cell; if (!(v == null || (typeof v === 'string' && v.length === 0))) { return false; @@ -107,6 +130,10 @@ export function isNullCell(cell: Nullable) { return false; } + if (custom != null) { + return false; + } + return true; } diff --git a/packages/core/src/types/interfaces/i-cell-validation-data.ts b/packages/core/src/types/interfaces/i-cell-validation-data.ts new file mode 100644 index 0000000000..6a91445542 --- /dev/null +++ b/packages/core/src/types/interfaces/i-cell-validation-data.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DataValidationStatus } from '../enum/data-validation-status'; +import type { IDataValidationRule } from './i-data-validation'; + +export interface ICellValidationData { + ruleId: string; + validStatus: DataValidationStatus; + rule: IDataValidationRule; + validator: any; +} diff --git a/packages/core/src/types/interfaces/i-data-validation.ts b/packages/core/src/types/interfaces/i-data-validation.ts new file mode 100644 index 0000000000..7df6c56ab5 --- /dev/null +++ b/packages/core/src/types/interfaces/i-data-validation.ts @@ -0,0 +1,65 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DataValidationOperator } from '../enum/data-validation-operator'; +import type { DataValidationType } from '../enum/data-validation-type'; +import type { DataValidationImeMode } from '../enum/data-validation-ime-mode'; +import type { DataValidationErrorStyle } from '../enum/data-validation-error-style'; +import type { DataValidationRenderMode } from '../enum/data-validation-render-mode'; +import type { IRange } from './i-range'; + +export interface IDataValidationRuleBase { + type: DataValidationType; + allowBlank?: boolean; + formula1?: string; + formula2?: string; + operator?: DataValidationOperator; +} + +export interface IDataValidationRuleOptions { + error?: string; + errorStyle?: DataValidationErrorStyle; + errorTitle?: string; + imeMode?: DataValidationImeMode; + prompt?: string; + promptTitle?: string; + showDropDown?: boolean; + showErrorMessage?: boolean; + showInputMessage?: boolean; + /** + * cell render mode of data validation + */ + renderMode?: DataValidationRenderMode; +} + +export interface IDataValidationRule extends IDataValidationRuleBase, IDataValidationRuleOptions { + uid: string; + /** + * @debt should using specific type cover sheet、doc、slide range type + */ + ranges: any; + +} + +export interface ISheetDataValidationRule extends IDataValidationRule { + ranges: IRange[]; +} + +export interface IDataValidationRuleInfo { + rule: IDataValidationRule; + unitId: string; + subUnitId: string; +} diff --git a/packages/core/src/types/interfaces/i-document-data.ts b/packages/core/src/types/interfaces/i-document-data.ts index 9b7941cddb..7819be05bd 100644 --- a/packages/core/src/types/interfaces/i-document-data.ts +++ b/packages/core/src/types/interfaces/i-document-data.ts @@ -26,10 +26,8 @@ import type { IColorStyle, IStyleBase } from './i-style-data'; export interface IDocumentData extends IReferenceSource, IExtraModelData { /** unit ID */ id: string; - /** Revision of this document. Would be used in collaborated editing. Starts with zero. */ rev?: number; - locale?: LocaleType; title?: string; body?: IDocumentBody; @@ -118,11 +116,16 @@ export interface IDocumentBody { textRuns?: ITextRun[]; // textRun style,interaction paragraphs?: IParagraph[]; // paragraph + sectionBreaks?: ISectionBreak[]; // SectionBreak https://support.microsoft.com/en-us/office/insert-a-section-break-eef20fd8-e38c-4ba6-a027-e503bdf8375c + customBlocks?: ICustomBlock[]; // customBlock user-defined block through plug-in + tables?: ITable[]; // Table + // tableOfContents?: { [index: number]: ITableOfContent }; // tableOfContents // links?: { [index: number]: IHyperlink }; // links + customRanges?: ICustomRange[]; // plugin register,implement special logic for streams, hyperlink, field,structured document tags, bookmark,comment } @@ -274,6 +277,7 @@ export enum CustomRangeType { BOOKMARK, COMMENT, CUSTOM, + MENTION, } /** @@ -282,6 +286,7 @@ export enum CustomRangeType { export interface ICustomBlock { startIndex: number; blockType?: BlockType; + // A unique ID associated with a custom block. blockId: string; } @@ -329,6 +334,11 @@ export interface IDocumentLayout { characterSpacingControl?: characterSpacingControlType; // characterSpacingControl 17.18.7 ST_CharacterSpacing (Character-Level Whitespace Compression Settings),default compressPunctuation paragraphLineGapDefault?: number; // paragraphLineGapDefault default line spacing spaceWidthEastAsian?: BooleanNumber; // add space between east asian and English + + autoHyphenation?: BooleanNumber; // 17.15.1.10 autoHyphenation (Automatically Hyphenate Document Contents When Displayed) + consecutiveHyphenLimit?: number; // 17.15.1.22 consecutiveHyphenLimit (Maximum Number of Consecutively Hyphenated Lines) + doNotHyphenateCaps?: BooleanNumber; // 17.15.1.37 doNotHyphenateCaps (Do Not Hyphenate Words in ALL CAPITAL LETTERS) + hyphenationZone?: number; // 17.15.1.53 hyphenationZone (Hyphenation Zone) } export enum GridType { @@ -501,7 +511,9 @@ export interface IDrawing { objectId: string; title: string; + description: string; + // embeddedObjectBorder?: IDocsBorder; objectTransform: IObjectTransform; @@ -546,8 +558,8 @@ export enum PositionedObjectLayoutType { */ export interface IObjectTransform { size: ISize; - positionH: ObjectPositionH; - positionV: ObjectPositionV; + positionH: IObjectPositionH; + positionV: IObjectPositionV; angle: number; // Union field properties can be only one of the following: // shapeProperties?: IShapeProperties; @@ -607,6 +619,7 @@ export interface IParagraphStyle extends IIndentStart { wordWrap?: BooleanNumber; // 17.3.1.45 wordWrap (Allow Line Breaking At Character Level) widowControl?: BooleanNumber; // 17.3.1.44 widowControl (Allow First/Last Line to Display on a Separate Page) shading?: IShading; // shading + suppressHyphenation?: BooleanNumber; // 17.3.1.34 suppressAutoHyphens (Suppress Hyphenation for Paragraph) } /** @@ -790,7 +803,7 @@ export interface INumberUnit { u: NumberUnitType; // unit } -export interface ObjectPositionH { +export interface IObjectPositionH { relativeFrom: ObjectRelativeFromH; // Union field properties can be only one of the following: align?: AlignTypeH; @@ -798,7 +811,7 @@ export interface ObjectPositionH { percent?: number; } -export interface ObjectPositionV { +export interface IObjectPositionV { relativeFrom: ObjectRelativeFromV; // Union field properties can be only one of the following: align?: AlignTypeV; @@ -808,26 +821,26 @@ export interface ObjectPositionV { // 20.4.3.4 ST_RelFromH (Horizontal Relative Positioning) export enum ObjectRelativeFromH { - CHARACTER, + PAGE, COLUMN, - INSIDE_MARGIN, - LEFT_MARGIN, + CHARACTER, MARGIN, + INSIDE_MARGIN, OUTSIDE_MARGIN, - PAGE, + LEFT_MARGIN, RIGHT_MARGIN, } // 20.4.3.4 ST_RelFromH (Horizontal Relative Positioning) export enum ObjectRelativeFromV { - BOTTOM_MARGIN, - INSIDE_MARGIN, - LINE, - MARGIN, - OUTSIDE_MARGIN, PAGE, PARAGRAPH, + LINE, + MARGIN, TOP_MARGIN, + BOTTOM_MARGIN, + INSIDE_MARGIN, + OUTSIDE_MARGIN, } export enum NumberUnitType { POINT, diff --git a/packages/core/src/types/interfaces/i-extra-model-data.ts b/packages/core/src/types/interfaces/i-extra-model-data.ts index 62c5995094..b4d502c0bd 100644 --- a/packages/core/src/types/interfaces/i-extra-model-data.ts +++ b/packages/core/src/types/interfaces/i-extra-model-data.ts @@ -27,11 +27,4 @@ export interface IExtraModelData { * default is null */ container?: string; - - /** - * Specify the unitID of the parent render. In this case, the scene will enter - * the sceneViewer and become a child application of the parent scene. - * default is null - */ - parentRenderUnitId?: string; } diff --git a/packages/core/src/types/interfaces/i-range.ts b/packages/core/src/types/interfaces/i-range.ts index bb5f308755..3d6babeb28 100644 --- a/packages/core/src/types/interfaces/i-range.ts +++ b/packages/core/src/types/interfaces/i-range.ts @@ -113,6 +113,10 @@ export interface IUnitRange extends IGridRange { unitId: string; } +export interface IUnitRangeWithName extends IUnitRange { + sheetName: string; +} + /** * One of the range types, * @@ -213,9 +217,6 @@ export interface IOptionData { * */ contentsOnly?: boolean; - /** - * Whether to clear only the comments. - */ } /** diff --git a/packages/core/src/types/interfaces/i-row-data.ts b/packages/core/src/types/interfaces/i-row-data.ts index 5099dac1b3..362438102e 100644 --- a/packages/core/src/types/interfaces/i-row-data.ts +++ b/packages/core/src/types/interfaces/i-row-data.ts @@ -24,14 +24,17 @@ export interface IRowData { * height in pixel */ h?: number; + /** * is current row self-adaptive to its content, use `ah` to set row height when true, else use `h`. */ ia?: BooleanNumber; // pre name `isAutoHeight` + /** * auto height */ ah?: number; + /** * hidden */ diff --git a/packages/core/src/types/interfaces/i-style-data.ts b/packages/core/src/types/interfaces/i-style-data.ts index 31dd29602f..2a0d17d184 100644 --- a/packages/core/src/types/interfaces/i-style-data.ts +++ b/packages/core/src/types/interfaces/i-style-data.ts @@ -175,6 +175,11 @@ export interface IStyleBase { * (Subscript 下标 /Superscript上标 Text) */ va?: Nullable; + + /** + * Numfmt pattern + */ + n?: Nullable<{ pattern: string }>; } /** @@ -205,4 +210,5 @@ export interface IStyleData extends IStyleBase { * padding */ pd?: Nullable; + } diff --git a/packages/core/src/types/interfaces/i-univer-data.ts b/packages/core/src/types/interfaces/i-univer-data.ts index 01a7e7c29f..11aa48b46d 100644 --- a/packages/core/src/types/interfaces/i-univer-data.ts +++ b/packages/core/src/types/interfaces/i-univer-data.ts @@ -15,6 +15,7 @@ */ import type { LogLevel } from '../../services/log/log.service'; +import type { DependencyOverride } from '../../services/plugin/plugin-override'; import type { IStyleSheet } from '../../services/theme/theme.service'; import type { ILocales } from '../../shared/locale'; import type { LocaleType } from '../enum'; @@ -25,19 +26,6 @@ export interface IUniverData { locales: ILocales; logLevel: LogLevel; id: string; -} - -/** - * Toolbar Observer generic interface, convenient for plug-ins to define their own types - */ -export interface UIObserver { - /** - * fontSize, fontFamily,color... - */ - name: string; - /** - * fontSize:number, fontFamily:string ... - */ - value?: T; + override?: DependencyOverride; } diff --git a/packages/core/src/types/interfaces/i-workbook-data.ts b/packages/core/src/types/interfaces/i-workbook-data.ts index 46e20a62d8..92758099da 100644 --- a/packages/core/src/types/interfaces/i-workbook-data.ts +++ b/packages/core/src/types/interfaces/i-workbook-data.ts @@ -28,22 +28,27 @@ export interface IWorkbookData extends IExtraModelData { * unit id */ id: string; + /** * Revision of this spreadsheet. Would be used in collaborated editing. Starts from one. */ rev?: number; + /** * Name of the spreadsheet. */ name: string; + /** * Version of Univer model definition. */ appVersion: string; + locale: LocaleType; /** Style reference. */ styles: IKeyType>; + sheetOrder: string[]; // sheet id order list ['xxxx-sheet3', 'xxxx-sheet1','xxxx-sheet2'] sheets: { [sheetId: string]: Partial }; diff --git a/packages/core/src/types/interfaces/i-worksheet-data.ts b/packages/core/src/types/interfaces/i-worksheet-data.ts index a62ec0d89b..08f753251c 100644 --- a/packages/core/src/types/interfaces/i-worksheet-data.ts +++ b/packages/core/src/types/interfaces/i-worksheet-data.ts @@ -48,7 +48,9 @@ export interface IWorksheetData { * @defaultValue `BooleanNumber.FALSE` */ hidden: BooleanNumber; + freeze: IFreeze; + rowCount: number; columnCount: number; zoomRatio: number; diff --git a/packages/core/src/univer.ts b/packages/core/src/univer.ts new file mode 100644 index 0000000000..9ab61a18f4 --- /dev/null +++ b/packages/core/src/univer.ts @@ -0,0 +1,189 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Dependency } from '@wendellhu/redi'; +import { Injector } from '@wendellhu/redi'; + +import { DocumentDataModel } from './docs/data-model/document-data-model'; +import { CommandService, ICommandService } from './services/command/command.service'; +import { ConfigService, IConfigService } from './services/config/config.service'; +import { ContextService, IContextService } from './services/context/context.service'; +import { ErrorService } from './services/error/error.service'; +import { + FloatingObjectManagerService, + IFloatingObjectManagerService, +} from './services/floating-object/floating-object-manager.service'; +import { IUniverInstanceService, UniverInstanceService } from './services/instance/instance.service'; +import { LifecycleStages } from './services/lifecycle/lifecycle'; +import { LifecycleInitializerService, LifecycleService } from './services/lifecycle/lifecycle.service'; +import { LocaleService } from './services/locale/locale.service'; +import { DesktopLogService, ILogService } from './services/log/log.service'; +import { IPermissionService, PermissionService } from './services/permission/permission.service'; +import { UniverPermissionService } from './services/permission/univer.permission.service'; +import { ResourceManagerService } from './services/resource-manager/resource-manager.service'; +import { IResourceManagerService } from './services/resource-manager/type'; +import { ResourceLoaderService } from './services/resource-loader/resource-loader.service'; +import { IResourceLoaderService } from './services/resource-loader/type'; +import { ThemeService } from './services/theme/theme.service'; +import { IUndoRedoService, LocalUndoRedoService } from './services/undoredo/undoredo.service'; +import { Workbook } from './sheets/workbook'; +import { SlideDataModel } from './slides/slide-model'; +import type { LocaleType } from './types/enum/locale-type'; +import type { IDocumentData, ISlideData, IUniverData, IWorkbookData } from './types/interfaces'; +import type { UnitModel, UnitType } from './common/unit'; +import { UniverInstanceType } from './common/unit'; +import { PluginService } from './services/plugin/plugin.service'; +import type { Plugin, PluginCtor } from './services/plugin/plugin'; +import type { DependencyOverride } from './services/plugin/plugin-override'; +import { mergeOverrideWithDependencies } from './services/plugin/plugin-override'; +import { UserManagerService } from './services/user-manager/user-manager.service'; + +export class Univer { + private _startedTypes = new Set(); + private _injector: Injector; + + private get _univerInstanceService(): IUniverInstanceService { + return this._injector.get(IUniverInstanceService); + } + + private get _pluginService(): PluginService { + return this._injector.get(PluginService); + } + + /** + * Create a Univer instance. + * @param config Configuration data for Univer + * @param parentInjector An optional parent injector of the Univer injector. For more information, see https://redi.wendell.fun/docs/hierarchy. + */ + constructor(config: Partial = {}, parentInjector?: Injector) { + const injector = this._injector = createUniverInjector(parentInjector, config?.override); + + const { theme, locale, locales, logLevel } = config; + + theme && this._injector.get(ThemeService).setTheme(theme); + locales && this._injector.get(LocaleService).load(locales); + locale && this._injector.get(LocaleService).setLocale(locale); + logLevel && this._injector.get(ILogService).setLogLevel(logLevel); + + this._init(injector); + const _a = this._pluginService; + } + + __getInjector(): Injector { + return this._injector; + } + + dispose(): void { + this._injector.dispose(); + } + + setLocale(locale: LocaleType) { + this._injector.get(LocaleService).setLocale(locale); + } + + createUnit(type: UnitType, data: Partial): U { + return this._univerInstanceService.createUnit(type, data); + } + + /** + * Create a univer sheet instance with internal dependency injection. + * + * @deprecated use `createUnit` instead + */ + createUniverSheet(data: Partial): Workbook { + return this._univerInstanceService.createUnit(UniverInstanceType.UNIVER_SHEET, data); + } + + /** + * @deprecated use `createUnit` instead + */ + createUniverDoc(data: Partial): DocumentDataModel { + return this._univerInstanceService.createUnit(UniverInstanceType.UNIVER_DOC, data); + } + + /** + * @deprecated use `createUnit` instead + */ + createUniverSlide(data: Partial): SlideDataModel { + return this._univerInstanceService.createUnit(UniverInstanceType.UNIVER_SLIDE, data); + } + + private _init(injector: Injector): void { + this._univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_SHEET, Workbook); + this._univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_DOC, DocumentDataModel); + this._univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_SLIDE, SlideDataModel); + + const univerInstanceService = injector.get(IUniverInstanceService) as UniverInstanceService; + univerInstanceService.__setCreateHandler( + (type: UnitType, data, ctor) => { + if (!this._startedTypes.has(type)) { + this._pluginService.startPluginForType(type); + this._startedTypes.add(type); + + const model = injector.createInstance(ctor, data); + univerInstanceService.__addUnit(model); + + this._tryProgressToReady(); + + return model; + } + + const model = injector.createInstance(ctor, data); + univerInstanceService.__addUnit(model); + return model; + } + ); + } + + private _tryProgressToReady(): void { + const lifecycleService = this._injector.get(LifecycleService); + if (lifecycleService.stage < LifecycleStages.Ready) { + this._injector.get(LifecycleService).stage = LifecycleStages.Ready; + } + } + + /** Register a plugin into univer. */ + registerPlugin>(plugin: T, config?: ConstructorParameters[0]): void { + this._pluginService.registerPlugin(plugin, config); + } +} + +function createUniverInjector(parentInjector?: Injector, override?: DependencyOverride) { + const dependencies: Dependency[] = mergeOverrideWithDependencies([ + [ErrorService], + [LocaleService], + [ThemeService], + [LifecycleService], + [LifecycleInitializerService], + [UniverPermissionService], + [PluginService], + [UserManagerService], + + // abstract services + [IUniverInstanceService, { useClass: UniverInstanceService }], + [IPermissionService, { useClass: PermissionService }], + [ILogService, { useClass: DesktopLogService, lazy: true }], + [ICommandService, { useClass: CommandService, lazy: true }], + [IUndoRedoService, { useClass: LocalUndoRedoService, lazy: true }], + [IConfigService, { useClass: ConfigService }], + [IContextService, { useClass: ContextService }], + [IFloatingObjectManagerService, { useClass: FloatingObjectManagerService, lazy: true }], + [IResourceManagerService, { useClass: ResourceManagerService, lazy: true }], + [IResourceLoaderService, { useClass: ResourceLoaderService, lazy: true }], + ], override); + + return parentInjector ? parentInjector.createChild(dependencies) : new Injector(dependencies); +} diff --git a/packages/data-validation/README-zh.md b/packages/data-validation/README-zh.md new file mode 100644 index 0000000000..d166a180db --- /dev/null +++ b/packages/data-validation/README-zh.md @@ -0,0 +1,28 @@ +# @univerjs/data-validation + +[![npm version](https://img.shields.io/npm/v/@univerjs/data-validation)](https://npmjs.org/packages/@univerjs/data-validation) +[![license](https://img.shields.io/npm/l/@univerjs/data-validation)](https://img.shields.io/npm/l/@univerjs/data-validation) + +## 简介 + +> `@univerjs/data-validation` 提供了基础的数据验证功能. + +## 使用指南 + +### 安装 + +```shell +# 使用 npm +npm install @univerjs/data-validation + +# 使用 pnpm +pnpm add @univerjs/data-validation +``` + +### 注册插件 + +```typescript +import { UniverDataValidationPlugin } from '@univerjs/data-validation'; + +univer.registerPlugin(UniverDataValidationPlugin); +``` diff --git a/packages/data-validation/README.md b/packages/data-validation/README.md new file mode 100644 index 0000000000..dd604836cc --- /dev/null +++ b/packages/data-validation/README.md @@ -0,0 +1,28 @@ +# @univerjs/data-validation + +[![npm version](https://img.shields.io/npm/v/@univerjs/data-validation)](https://npmjs.org/packages/@univerjs/data-validation) +[![license](https://img.shields.io/npm/l/@univerjs/data-validation)](https://img.shields.io/npm/l/@univerjs/data-validation) + +## Introduction + +> `@univerjs/data-validation` provides Univer Sheet with basic capabilities for data validation computation. + +## Usage + +### Installation + +```shell +# Using npm +npm i @univerjs/data-validation + +# Using pnpm +pnpm add @univerjs/data-validation +``` + +### Register the plugin + +```typescript +import { UniverDataValidationPlugin } from '@univerjs/data-validation'; + +univer.registerPlugin(UniverDataValidationPlugin); +``` diff --git a/packages/data-validation/package.json b/packages/data-validation/package.json new file mode 100644 index 0000000000..2212e816e5 --- /dev/null +++ b/packages/data-validation/package.json @@ -0,0 +1,80 @@ +{ + "name": "@univerjs/data-validation", + "version": "0.1.12", + "private": false, + "description": "Data validation library for Univer", + "author": "DreamNum ", + "license": "Apache-2.0", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/univer" + }, + "homepage": "https://univer.ai", + "repository": { + "type": "git", + "url": "https://github.com/dream-num/univer" + }, + "bugs": { + "url": "https://github.com/dream-num/univer/issues" + }, + "keywords": [ + "univer" + ], + "sideEffects": [ + "**/*.css" + ], + "exports": { + ".": "./src/index.ts", + "./*": "./src/*" + }, + "main": "./lib/cjs/index.js", + "module": "./lib/esm/index.js", + "types": "./lib/types/index.d.ts", + "publishConfig": { + "access": "public", + "main": "./lib/cjs/index.js", + "module": "./lib/es/index.js", + "exports": { + ".": { + "import": "./lib/es/index.js", + "require": "./lib/cjs/index.js", + "types": "./lib/types/index.d.ts" + }, + "./*": { + "import": "./lib/es/*", + "require": "./lib/cjs/*", + "types": "./lib/types/index.d.ts" + }, + "./lib/*": "./lib/*" + } + }, + "directories": { + "lib": "lib" + }, + "files": [ + "lib" + ], + "scripts": { + "dev": "vite", + "test": "vitest run", + "test:watch": "vitest", + "coverage": "vitest run --coverage", + "build": "tsc && vite build" + }, + "peerDependencies": { + "@univerjs/core": "workspace:*", + "@univerjs/sheets": "workspace:*", + "@wendellhu/redi": "0.15.2", + "rxjs": ">=7.0.0" + }, + "devDependencies": { + "@univerjs/core": "workspace:*", + "@univerjs/shared": "workspace:*", + "@univerjs/sheets": "workspace:*", + "@wendellhu/redi": "0.15.2", + "rxjs": "^7.8.1", + "typescript": "^5.4.5", + "vite": "^5.2.11", + "vitest": "^1.6.0" + } +} diff --git a/packages/data-validation/src/commands/commands/data-validation.command.ts b/packages/data-validation/src/commands/commands/data-validation.command.ts new file mode 100644 index 0000000000..170df816fc --- /dev/null +++ b/packages/data-validation/src/commands/commands/data-validation.command.ts @@ -0,0 +1,327 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommandType, ICommandService, IUndoRedoService } from '@univerjs/core'; +import type { ICommand, IDataValidationRule, IDataValidationRuleBase, IDataValidationRuleOptions, IMutationInfo, IRange, ISheetDataValidationRule } from '@univerjs/core'; +import type { Injector } from '@wendellhu/redi'; +import type { ISheetCommandSharedParams } from '@univerjs/sheets'; +import type { IAddDataValidationMutationParams, IRemoveDataValidationMutationParams, IUpdateDataValidationMutationParams } from '../mutations/data-validation.mutation'; +import { AddDataValidationMutation, RemoveDataValidationMutation, UpdateDataValidationMutation } from '../mutations/data-validation.mutation'; +import { DataValidatorRegistryService } from '../../services/data-validator-registry.service'; +import { DataValidationModel } from '../../models/data-validation-model'; +import { getRuleOptions, getRuleSetting } from '../../common/util'; +import { UpdateRuleType } from '../../types/enum/update-rule-type'; + +export interface IAddDataValidationCommandParams extends ISheetCommandSharedParams { + rule: Omit & { + range: IRange; + }; + index?: number; +} + +export const AddDataValidationCommand: ICommand = { + type: CommandType.COMMAND, + id: 'data-validation.command.addRule', + async handler(accessor, params) { + if (!params) { + return false; + } + const { rule, unitId, subUnitId } = params; + const commandService = accessor.get(ICommandService); + const undoRedoService = accessor.get(IUndoRedoService); + const mutationParams: IAddDataValidationMutationParams = { + ...params, + rule: { + ...params.rule, + ranges: [params.rule.range], + }, + }; + const redoMutations: IMutationInfo[] = [{ + id: AddDataValidationMutation.id, + params: mutationParams, + }]; + + const undoMutations: IMutationInfo[] = [{ + id: RemoveDataValidationMutation.id, + params: { + unitId, + subUnitId, + ruleId: rule.uid, + }, + }]; + undoRedoService.pushUndoRedo({ + unitID: unitId, + redoMutations, + undoMutations, + }); + + await commandService.executeCommand(AddDataValidationMutation.id, mutationParams); + return true; + }, +}; + +export interface IRemoveDataValidationCommandParams extends ISheetCommandSharedParams { + ruleId: string; +} + +export const removeDataValidationUndoFactory = (accessor: Injector, redoParams: IRemoveDataValidationMutationParams) => { + const dataValidationModel = accessor.get(DataValidationModel); + const { unitId, subUnitId, ruleId } = redoParams; + if (Array.isArray(ruleId)) { + const rules = ruleId.map((id) => dataValidationModel.getRuleById(unitId, subUnitId, id)).filter(Boolean) as ISheetDataValidationRule[]; + return [{ + id: AddDataValidationMutation.id, + params: { + unitId, + subUnitId, + rule: rules, + } as IAddDataValidationMutationParams, + }]; + } + + const undoMutations: IMutationInfo[] = [{ + id: AddDataValidationMutation.id, + params: { + unitId, + subUnitId, + rule: { + ...dataValidationModel.getRuleById(unitId, subUnitId, ruleId), + }, + index: dataValidationModel.getRuleIndex(unitId, subUnitId, ruleId), + } as IAddDataValidationMutationParams, + }]; + + return undoMutations; +}; + +export const RemoveDataValidationCommand: ICommand = { + type: CommandType.COMMAND, + id: 'data-validation.command.removeRule', + handler(accessor, params) { + if (!params) { + return false; + } + const { unitId, subUnitId, ruleId } = params; + const commandService = accessor.get(ICommandService); + const undoRedoService = accessor.get(IUndoRedoService); + const dataValidationModel = accessor.get(DataValidationModel); + + const redoMutations: IMutationInfo[] = [{ + id: RemoveDataValidationMutation.id, + params, + }]; + const undoMutations: IMutationInfo[] = [{ + id: AddDataValidationMutation.id, + params: { + unitId, + subUnitId, + rule: { + ...dataValidationModel.getRuleById(unitId, subUnitId, ruleId), + }, + index: dataValidationModel.getRuleIndex(unitId, subUnitId, ruleId), + } as IAddDataValidationMutationParams, + }]; + + undoRedoService.pushUndoRedo({ + undoMutations, + redoMutations, + unitID: params.unitId, + }); + + commandService.executeCommand(RemoveDataValidationMutation.id, params); + return true; + }, +}; + +export interface IUpdateDataValidationOptionsCommandParams extends ISheetCommandSharedParams { + ruleId: string; + options: IDataValidationRuleOptions; +} + +export const UpdateDataValidationOptionsCommand: ICommand = { + type: CommandType.COMMAND, + id: 'data-validation.command.updateDataValidationSetting', + handler(accessor, params) { + if (!params) { + return false; + } + const commandService = accessor.get(ICommandService); + const redoUndoService = accessor.get(IUndoRedoService); + const dataValidationModel = accessor.get(DataValidationModel); + + const { unitId, subUnitId, ruleId, options } = params; + + const rule = dataValidationModel.getRuleById(unitId, subUnitId, ruleId); + if (!rule) { + return false; + } + + const mutationParams: IUpdateDataValidationMutationParams = { + unitId, + subUnitId, + ruleId, + payload: { + type: UpdateRuleType.OPTIONS, + payload: options, + }, + }; + + const redoMutations: IMutationInfo[] = [{ + id: UpdateDataValidationMutation.id, + params: mutationParams, + }]; + const undoMutationParams: IUpdateDataValidationMutationParams = { + unitId, + subUnitId, + ruleId, + payload: { + type: UpdateRuleType.OPTIONS, + payload: getRuleOptions(rule), + }, + }; + const undoMutations: IMutationInfo[] = [{ + id: UpdateDataValidationMutation.id, + params: undoMutationParams, + }]; + + redoUndoService.pushUndoRedo({ + unitID: unitId, + redoMutations, + undoMutations, + }); + + commandService.executeCommand(UpdateDataValidationMutation.id, mutationParams); + return true; + }, +}; + +export interface IUpdateDataValidationSettingCommandParams extends ISheetCommandSharedParams { + ruleId: string; + setting: IDataValidationRuleBase; +} + +export const UpdateDataValidationSettingCommand: ICommand = { + type: CommandType.COMMAND, + id: 'data-validation.command.updateDataValidationOptions', + handler(accessor, params) { + if (!params) { + return false; + } + const commandService = accessor.get(ICommandService); + const redoUndoService = accessor.get(IUndoRedoService); + const dataValidationModel = accessor.get(DataValidationModel); + const dataValidatorRegistryService = accessor.get(DataValidatorRegistryService); + + const { unitId, subUnitId, ruleId, setting } = params; + const validator = dataValidatorRegistryService.getValidatorItem(setting.type); + + if (!validator) { + return false; + } + + if (!validator.validatorFormula(setting).success) { + return false; + } + + const rule = dataValidationModel.getRuleById(unitId, subUnitId, ruleId); + if (!rule) { + return false; + } + + const mutationParams: IUpdateDataValidationMutationParams = { + unitId, + subUnitId, + ruleId, + payload: { + type: UpdateRuleType.SETTING, + payload: setting, + }, + }; + + const redoMutations: IMutationInfo[] = [{ + id: UpdateDataValidationMutation.id, + params: mutationParams, + }]; + const undoMutationParams: IUpdateDataValidationMutationParams = { + unitId, + subUnitId, + ruleId, + payload: { + type: UpdateRuleType.SETTING, + payload: getRuleSetting(rule), + }, + }; + const undoMutations: IMutationInfo[] = [{ + id: UpdateDataValidationMutation.id, + params: undoMutationParams, + }]; + + redoUndoService.pushUndoRedo({ + unitID: unitId, + redoMutations, + undoMutations, + }); + + commandService.executeCommand(UpdateDataValidationMutation.id, mutationParams); + return true; + }, +}; + +export interface IRemoveAllDataValidationCommandParams extends ISheetCommandSharedParams { +} + +export const RemoveAllDataValidationCommand: ICommand = { + type: CommandType.COMMAND, + id: 'data-validation.command.removeAll', + handler(accessor, params) { + if (!params) { + return false; + } + const { unitId, subUnitId } = params; + const commandService = accessor.get(ICommandService); + const dataValidationModel = accessor.get(DataValidationModel); + const undoRedoService = accessor.get(IUndoRedoService); + const currentRules = [...dataValidationModel.getRules(unitId, subUnitId)]; + + const redoParams: IRemoveDataValidationMutationParams = { + unitId, + subUnitId, + ruleId: currentRules.map((rule) => rule.uid), + }; + const redoMutations: IMutationInfo[] = [{ + id: RemoveDataValidationMutation.id, + params: redoParams, + }]; + + const undoMutations: IMutationInfo[] = [{ + id: AddDataValidationMutation.id, + params: { + unitId, + subUnitId, + rule: currentRules, + }, + }]; + + undoRedoService.pushUndoRedo({ + redoMutations, + undoMutations, + unitID: unitId, + }); + + commandService.executeCommand(RemoveDataValidationMutation.id, redoParams); + return true; + }, +}; diff --git a/packages/data-validation/src/commands/mutations/data-validation.mutation.ts b/packages/data-validation/src/commands/mutations/data-validation.mutation.ts new file mode 100644 index 0000000000..5e75267b74 --- /dev/null +++ b/packages/data-validation/src/commands/mutations/data-validation.mutation.ts @@ -0,0 +1,87 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommandType } from '@univerjs/core'; +import type { ICommand, IDataValidationRule } from '@univerjs/core'; +import type { ISheetCommandSharedParams } from '@univerjs/sheets'; +import type { IUpdateRulePayload } from '../../types/interfaces/i-update-rule-payload'; +import { DataValidationModel } from '../../models/data-validation-model'; + +export interface IAddDataValidationMutationParams extends ISheetCommandSharedParams { + rule: IDataValidationRule | IDataValidationRule[]; + index?: number; +} + +export const AddDataValidationMutation: ICommand = { + type: CommandType.MUTATION, + id: 'data-validation.mutation.addRule', + handler(accessor, params) { + if (!params) { + return false; + } + const { unitId, subUnitId, rule, index } = params; + const dataValidationModel = accessor.get(DataValidationModel); + dataValidationModel.addRule(unitId, subUnitId, rule, index); + + return true; + }, +}; + +export interface IRemoveDataValidationMutationParams extends ISheetCommandSharedParams { + ruleId: string | string[]; +} + +export const RemoveDataValidationMutation: ICommand = { + type: CommandType.MUTATION, + id: 'data-validation.mutation.removeRule', + handler(accessor, params) { + if (!params) { + return false; + } + + const { unitId, subUnitId, ruleId } = params; + const dataValidationModel = accessor.get(DataValidationModel); + if (Array.isArray(ruleId)) { + ruleId.forEach((item) => { + dataValidationModel.removeRule(unitId, subUnitId, item); + }); + } else { + dataValidationModel.removeRule(unitId, subUnitId, ruleId); + } + + return true; + }, +}; + +export interface IUpdateDataValidationMutationParams extends ISheetCommandSharedParams { + payload: IUpdateRulePayload; + ruleId: string; +} + +export const UpdateDataValidationMutation: ICommand = { + type: CommandType.MUTATION, + id: 'data-validation.mutation.updateRule', + handler(accessor, params) { + if (!params) { + return false; + } + + const { unitId, subUnitId, ruleId, payload } = params; + const dataValidationModel = accessor.get(DataValidationModel); + dataValidationModel.updateRule(unitId, subUnitId, ruleId, payload); + return true; + }, +}; diff --git a/packages/data-validation/src/common/util.ts b/packages/data-validation/src/common/util.ts new file mode 100644 index 0000000000..5c6592c69d --- /dev/null +++ b/packages/data-validation/src/common/util.ts @@ -0,0 +1,59 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DataValidationOperator, DataValidationType, type IDataValidationRuleBase, type IDataValidationRuleOptions, Tools } from '@univerjs/core'; +import { SelectionManagerService } from '@univerjs/sheets'; +import type { IAccessor } from '@wendellhu/redi'; + +export function getRuleSetting(rule: IDataValidationRuleBase): IDataValidationRuleBase { + return { + type: rule.type, + operator: rule.operator, + formula1: rule.formula1, + formula2: rule.formula2, + allowBlank: rule.allowBlank, + }; +} + +export function getRuleOptions(rule: IDataValidationRuleOptions): IDataValidationRuleOptions { + return { + error: rule.error, + errorStyle: rule.errorStyle, + errorTitle: rule.errorTitle, + imeMode: rule.imeMode, + prompt: rule.prompt, + promptTitle: rule.promptTitle, + showDropDown: rule.showDropDown, + showErrorMessage: rule.showErrorMessage, + showInputMessage: rule.showInputMessage, + renderMode: rule.renderMode, + }; +} + +export function createDefaultNewRule(accessor: IAccessor) { + const selectionManagerService = accessor.get(SelectionManagerService); + const currentRanges = selectionManagerService.getSelectionRanges(); + const uid = Tools.generateRandomId(6); + const rule = { + uid, + type: DataValidationType.DECIMAL, + operator: DataValidationOperator.EQUAL, + formula1: '100', + ranges: currentRanges ?? [{ startColumn: 0, endColumn: 0, startRow: 0, endRow: 0 }], + }; + + return rule; +} diff --git a/packages/data-validation/src/controllers/dv-resource.controller.ts b/packages/data-validation/src/controllers/dv-resource.controller.ts new file mode 100644 index 0000000000..625d32e3ca --- /dev/null +++ b/packages/data-validation/src/controllers/dv-resource.controller.ts @@ -0,0 +1,79 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ISheetDataValidationRule } from '@univerjs/core'; +import { Disposable, IResourceManagerService, IUniverInstanceService, LifecycleStages, OnLifecycle, UniverInstanceType } from '@univerjs/core'; +import { Inject } from '@wendellhu/redi'; +import { DataValidationModel } from '../models/data-validation-model'; + +type DataValidationJSON = Record; + +const DATA_VALIDATION_PLUGIN_NAME = 'SHEET_DATA_VALIDATION_PLUGIN'; + +@OnLifecycle(LifecycleStages.Ready, DataValidationResourceController) +export class DataValidationResourceController extends Disposable { + constructor( + @IResourceManagerService private readonly _resourceManagerService: IResourceManagerService, + @IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService, + @Inject(DataValidationModel) private readonly _dataValidationModel: DataValidationModel + ) { + super(); + this._initSnapshot(); + } + + private _initSnapshot() { + const toJson = (unitID: string) => { + const map = this._dataValidationModel.getUnitRules(unitID); + const resultMap: DataValidationJSON = {}; + if (map) { + map.forEach(([key, v]) => { + resultMap[key] = v; + }); + return JSON.stringify(resultMap); + } + return ''; + }; + const parseJson = (json: string): DataValidationJSON => { + if (!json) { + return {}; + } + try { + return JSON.parse(json); + } catch (err) { + return {}; + } + }; + this.disposeWithMe( + this._resourceManagerService.registerPluginResource({ + pluginName: DATA_VALIDATION_PLUGIN_NAME, + businesses: [UniverInstanceType.UNIVER_SHEET], + toJson: (unitID) => toJson(unitID), + parseJson: (json) => parseJson(json), + onUnLoad: (unitID) => { + this._dataValidationModel.deleteUnitRules(unitID); + }, + onLoad: (unitID, value) => { + Object.keys(value).forEach((subunitId) => { + const ruleList = value[subunitId]; + ruleList.forEach((rule) => { + this._dataValidationModel.addRule(unitID, subunitId, rule); + }); + }); + }, + }) + ); + } +} diff --git a/packages/data-validation/src/controllers/dv-sheet.controller.ts b/packages/data-validation/src/controllers/dv-sheet.controller.ts new file mode 100644 index 0000000000..ebe9010120 --- /dev/null +++ b/packages/data-validation/src/controllers/dv-sheet.controller.ts @@ -0,0 +1,83 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Workbook } from '@univerjs/core'; +import { Disposable, IUniverInstanceService, LifecycleStages, OnLifecycle, UniverInstanceType } from '@univerjs/core'; +import type { IRemoveSheetCommandParams } from '@univerjs/sheets'; +import { RemoveSheetCommand, SheetInterceptorService } from '@univerjs/sheets'; +import { Inject } from '@wendellhu/redi'; +import type { IAddDataValidationMutationParams, IRemoveDataValidationMutationParams } from '../commands/mutations/data-validation.mutation'; +import { AddDataValidationMutation, RemoveDataValidationMutation } from '../commands/mutations/data-validation.mutation'; +import { DataValidationModel } from '../models/data-validation-model'; + +@OnLifecycle(LifecycleStages.Ready, DataValidationSheetController) +export class DataValidationSheetController extends Disposable { + constructor( + @Inject(SheetInterceptorService) private _sheetInterceptorService: SheetInterceptorService, + @Inject(IUniverInstanceService) private _univerInstanceService: IUniverInstanceService, + @Inject(DataValidationModel) private readonly _dataValidationModel: DataValidationModel + ) { + super(); + this._initSheetChange(); + } + + private _initSheetChange() { + this.disposeWithMe( + this._sheetInterceptorService.interceptCommand({ + getMutations: (commandInfo) => { + if (commandInfo.id === RemoveSheetCommand.id) { + const params = commandInfo.params as IRemoveSheetCommandParams; + const unitId = params.unitId || this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET)!.getUnitId(); + const workbook = this._univerInstanceService.getUniverSheetInstance(unitId); + if (!workbook) { + return { redos: [], undos: [] }; + } + const subUnitId = params.subUnitId || workbook.getActiveSheet().getSheetId(); + const manager = this._dataValidationModel.ensureManager(unitId, subUnitId); + if (!manager) { + return { redos: [], undos: [] }; + } + + const rules = manager.getDataValidations(); + const ids = rules.map((i) => i.uid); + const redoParams: IRemoveDataValidationMutationParams = { + unitId, + subUnitId, + ruleId: ids, + }; + const undoParams: IAddDataValidationMutationParams = { + unitId, + subUnitId, + rule: rules, + }; + + return { + redos: [{ + id: RemoveDataValidationMutation.id, + params: redoParams, + }], + undos: [{ + id: AddDataValidationMutation.id, + params: undoParams, + }], + }; + } + return { redos: [], undos: [] }; + }, + }) + ); + } +} diff --git a/packages/data-validation/src/index.ts b/packages/data-validation/src/index.ts new file mode 100644 index 0000000000..b9753b37fc --- /dev/null +++ b/packages/data-validation/src/index.ts @@ -0,0 +1,70 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { UniverDataValidationPlugin } from './plugin'; +export { DataValidatorRegistryService, DataValidatorRegistryScope } from './services/data-validator-registry.service'; +export { DataValidationModel } from './models/data-validation-model'; + +export { + AddDataValidationCommand, + RemoveDataValidationCommand, + RemoveAllDataValidationCommand, + UpdateDataValidationOptionsCommand, + UpdateDataValidationSettingCommand, +} from './commands/commands/data-validation.command'; + +export type { + IRemoveDataValidationCommandParams, + IAddDataValidationCommandParams, + IUpdateDataValidationOptionsCommandParams, + IUpdateDataValidationSettingCommandParams, + IRemoveAllDataValidationCommandParams, +} from './commands/commands/data-validation.command'; + +export { + AddDataValidationMutation, + RemoveDataValidationMutation, + UpdateDataValidationMutation, + +} from './commands/mutations/data-validation.mutation'; + +export type { + IAddDataValidationMutationParams, + IRemoveDataValidationMutationParams, + IUpdateDataValidationMutationParams, +} from './commands/mutations/data-validation.mutation'; + +export { + createDefaultNewRule, + getRuleOptions, + getRuleSetting, +} from './common/util'; + +export { UpdateRuleType } from './types/enum/update-rule-type'; +export type { IDataValidatorOperatorConfig } from './types/interfaces/i-data-validator-operator-config'; +export type { IFormulaInputProps, IFormulaValue, FormulaInputType } from './types/interfaces/i-formula-input'; +export type { IUpdateRuleOptionsPayload, IUpdateRulePayload, IUpdateRuleRangePayload, IUpdateRuleSettingPayload } from './types/interfaces/i-update-rule-payload'; +export type { IDataValidationDropdownProps } from './types/interfaces/i-data-validation-drop-down'; +export { BaseDataValidator } from './validators/base-data-validator'; +export type { IFormulaResult, IValidatorCellInfo } from './validators/base-data-validator'; +export type { IBaseDataValidationWidget } from './validators/base-widget'; +export { DataValidationManager } from './models/data-validation-manager'; +export type { IFormulaValidResult } from './validators/base-data-validator'; +export { removeDataValidationUndoFactory } from './commands/commands/data-validation.command'; +export { TWO_FORMULA_OPERATOR_COUNT } from './types/const/two-formula-operators'; +export { DataValidationResourceController } from './controllers/dv-resource.controller'; +export { DataValidationSheetController } from './controllers/dv-sheet.controller'; +export { TextLengthErrorTitleMap } from './types/const/operator-text-map'; diff --git a/packages/data-validation/src/models/data-validation-manager.ts b/packages/data-validation/src/models/data-validation-manager.ts new file mode 100644 index 0000000000..8042a45106 --- /dev/null +++ b/packages/data-validation/src/models/data-validation-manager.ts @@ -0,0 +1,141 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CellValue, IDataValidationRule, Nullable } from '@univerjs/core'; +import { DataValidationStatus, Disposable } from '@univerjs/core'; +import { Subject } from 'rxjs'; +import type { IUpdateRulePayload } from '../types/interfaces/i-update-rule-payload'; +import { UpdateRuleType } from '../types/enum/update-rule-type'; +import { getRuleOptions, getRuleSetting } from '../common/util'; + +export class DataValidationManager extends Disposable { + private _dataValidations: T[]; + private _dataValidationMap = new Map(); + private _dataValidations$ = new Subject(); + + readonly unitId: string; + readonly subUnitId: string; + readonly dataValidations$ = this._dataValidations$.asObservable(); + + constructor(unitId: string, subUnitId: string, dataValidations: T[] | undefined) { + super(); + this.unitId = unitId; + this.subUnitId = subUnitId; + if (!dataValidations) { + return; + } + + this._insertRules(dataValidations); + this._notice(); + + this.disposeWithMe({ + dispose: () => { + this._dataValidations$.complete(); + }, + }); + } + + private _notice() { + this._dataValidations$.next(this._dataValidations); + } + + private _insertRules(dataValidations: T[]) { + this._dataValidations = dataValidations; + dataValidations.forEach((validation) => { + this._dataValidationMap.set(validation.uid, validation); + }); + } + + getRuleById(id: string) { + return this._dataValidationMap.get(id); + } + + getRuleIndex(id: string) { + return this._dataValidations.findIndex((rule) => rule.uid === id); + } + + addRule(rule: T | T[], index?: number) { + const _rules = Array.isArray(rule) ? rule : [rule]; + const rules = _rules.filter((item) => !this._dataValidationMap.has(item.uid)); + + if (typeof index === 'number' && index < this._dataValidations.length) { + this._dataValidations.splice(index, 0, ...rules); + } else { + this._dataValidations.push(...rules); + } + + rules.forEach((item) => { + this._dataValidationMap.set(item.uid, item); + }); + + this._notice(); + } + + removeRule(ruleId: string) { + const index = this._dataValidations.findIndex((item) => item.uid === ruleId); + if (index > -1) { + this._dataValidations.splice(index, 1); + this._dataValidationMap.delete(ruleId); + this._notice(); + } + } + + updateRule(ruleId: string, payload: IUpdateRulePayload) { + const oldRule = this._dataValidationMap.get(ruleId); + const index = this._dataValidations.findIndex((rule) => ruleId === rule.uid); + + if (!oldRule) { + throw new Error(`Data validation rule is not found, ruleId: ${ruleId}.`); + } + + const rule = { ...oldRule }; + + switch (payload.type) { + case UpdateRuleType.RANGE: { + rule.ranges = payload.payload; + break; + } + case UpdateRuleType.SETTING: { + Object.assign(rule, getRuleSetting(payload.payload)); + break; + } + + case UpdateRuleType.OPTIONS: { + Object.assign(rule, getRuleOptions(payload.payload)); + break; + } + default: + break; + } + + this._dataValidations[index] = rule; + this._dataValidationMap.set(ruleId, rule); + this._notice(); + return rule; + } + + getDataValidations() { + return this._dataValidations; + } + + toJSON() { + return this._dataValidations; + } + + validator(_content: Nullable, _rule: IDataValidationRule, _pos: any, _onCompele: (status: DataValidationStatus) => void) { + return DataValidationStatus.VALID; + } +} diff --git a/packages/data-validation/src/models/data-validation-model.ts b/packages/data-validation/src/models/data-validation-model.ts new file mode 100644 index 0000000000..5e728a2883 --- /dev/null +++ b/packages/data-validation/src/models/data-validation-model.ts @@ -0,0 +1,188 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { type CellValue, type DataValidationStatus, Disposable, type IDataValidationRule, ILogService, type Nullable } from '@univerjs/core'; +import { debounceTime, Subject } from 'rxjs'; +import type { IUpdateRulePayload } from '../types/interfaces/i-update-rule-payload'; +import { DataValidationManager } from './data-validation-manager'; + +type ManagerCreator = (unitId: string, subUnitId: string) => DataValidationManager; +type RuleChangeType = 'update' | 'add' | 'remove'; + +export interface IRuleChange { + rule?: T; + type: RuleChangeType; + unitId: string; + subUnitId: string; +} + +export interface IValidStatusChange { + unitId: string; + subUnitId: string; + ruleId: string; + status: DataValidationStatus; +} + +export class DataValidationModel extends Disposable { + private readonly _model = new Map>>(); + private _managerCreator: ManagerCreator = (unitId: string, subUnitId: string) => new DataValidationManager(unitId, subUnitId, []); + private readonly _ruleChange$ = new Subject>(); + private readonly _validStatusChange$ = new Subject(); + + ruleChange$ = this._ruleChange$.asObservable(); + ruleChangeDebounce$ = this.ruleChange$.pipe(debounceTime(20)); + validStatusChange$ = this._validStatusChange$.asObservable().pipe(debounceTime(20)); + + constructor( + @ILogService private readonly _logService: ILogService + ) { + super(); + + this.disposeWithMe({ + dispose: () => { + this._ruleChange$.complete(); + this._validStatusChange$.complete(); + }, + }); + } + + setManagerCreator(creator: (unitId: string, subUnitId: string) => DataValidationManager) { + this._managerCreator = creator; + } + + ensureManager(unitId: string, subUnitId: string) { + if (!this._model.has(unitId)) { + this._model.set(unitId, new Map()); + } + const unitMap = this._model.get(unitId)!; + + if (unitMap.has(subUnitId)) { + return unitMap.get(subUnitId)!; + } + + const manager = this._managerCreator(unitId, subUnitId); + unitMap.set(subUnitId, manager); + return manager; + } + + private _addRuleSideEffect(unitId: string, subUnitId: string, rule: T) { + const manager = this.ensureManager(unitId, subUnitId); + const oldRule = manager.getRuleById(rule.uid); + if (oldRule) { + return; + } + this._ruleChange$.next({ + rule, + type: 'add', + unitId, + subUnitId, + }); + } + + addRule(unitId: string, subUnitId: string, rule: T | T[], index?: number) { + try { + const manager = this.ensureManager(unitId, subUnitId); + const rules = Array.isArray(rule) ? rule : [rule]; + rules.forEach((item) => { + this._addRuleSideEffect(unitId, subUnitId, item); + }); + + manager.addRule(rule, index); + } catch (error) { + this._logService.error(error); + } + } + + updateRule(unitId: string, subUnitId: string, ruleId: string, payload: IUpdateRulePayload) { + try { + const manager = this.ensureManager(unitId, subUnitId); + const rule = manager.updateRule(ruleId, payload); + + this._ruleChange$.next({ + rule, + type: 'update', + unitId, + subUnitId, + }); + } catch (error) { + this._logService.error(error); + } + } + + removeRule(unitId: string, subUnitId: string, ruleId: string) { + try { + const manager = this.ensureManager(unitId, subUnitId); + const oldRule = manager.getRuleById(ruleId); + if (oldRule) { + manager.removeRule(ruleId); + this._ruleChange$.next({ + rule: oldRule, + type: 'remove', + unitId, + subUnitId, + }); + } + } catch (error) { + this._logService.error(error); + } + } + + getRuleById(unitId: string, subUnitId: string, ruleId: string) { + const manager = this.ensureManager(unitId, subUnitId); + return manager.getRuleById(ruleId); + } + + getRuleIndex(unitId: string, subUnitId: string, ruleId: string) { + const manager = this.ensureManager(unitId, subUnitId); + return manager.getRuleIndex(ruleId); + } + + getRules(unitId: string, subUnitId: string) { + const manager = this.ensureManager(unitId, subUnitId); + return manager.getDataValidations(); + } + + validator(content: Nullable, rule: T, pos: any) { + const { unitId, subUnitId } = pos; + const manager = this.ensureManager(unitId, subUnitId); + return manager.validator(content, rule, pos, (status: DataValidationStatus) => { + this._validStatusChange$.next({ + unitId, + subUnitId, + ruleId: rule.uid, + status, + }); + }); + } + + getUnitRules(unitId: string) { + const unitMap = this._model.get(unitId); + if (!unitMap) { + return []; + } + const res = [] as [string, IDataValidationRule[]][]; + + unitMap.forEach((manager) => { + res.push([manager.subUnitId, manager.getDataValidations()]); + }); + + return res; + } + + deleteUnitRules(unitId: string) { + this._model.delete(unitId); + } +} diff --git a/packages/data-validation/src/plugin.ts b/packages/data-validation/src/plugin.ts new file mode 100644 index 0000000000..5bf651022e --- /dev/null +++ b/packages/data-validation/src/plugin.ts @@ -0,0 +1,72 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ICommandService, Plugin, UniverInstanceType } from '@univerjs/core'; +import type { Dependency } from '@wendellhu/redi'; +import { Inject, Injector } from '@wendellhu/redi'; +import { DataValidatorRegistryService } from './services/data-validator-registry.service'; +import { DataValidationModel } from './models/data-validation-model'; +import { AddDataValidationCommand, RemoveAllDataValidationCommand, RemoveDataValidationCommand, UpdateDataValidationOptionsCommand, UpdateDataValidationSettingCommand } from './commands/commands/data-validation.command'; +import { AddDataValidationMutation, RemoveDataValidationMutation, UpdateDataValidationMutation } from './commands/mutations/data-validation.mutation'; +import { DataValidationResourceController } from './controllers/dv-resource.controller'; +import { DataValidationSheetController } from './controllers/dv-sheet.controller'; + +const PLUGIN_NAME = 'data-validation'; + +export class UniverDataValidationPlugin extends Plugin { + static override pluginName = PLUGIN_NAME; + static override type = UniverInstanceType.UNIVER_SHEET; + + constructor( + _config: unknown, + @Inject(Injector) protected _injector: Injector, + @ICommandService private _commandService: ICommandService + ) { + super(); + } + + override onStarting(injector: Injector): void { + ([ + // model + [DataValidationModel], + // service + [DataValidatorRegistryService], + + [DataValidationResourceController], + [DataValidationSheetController], + ] as Dependency[]).forEach( + (d) => { + injector.add(d); + } + ); + + [ + // command + AddDataValidationCommand, + RemoveAllDataValidationCommand, + UpdateDataValidationOptionsCommand, + UpdateDataValidationSettingCommand, + RemoveDataValidationCommand, + + // mutation + AddDataValidationMutation, + UpdateDataValidationMutation, + RemoveDataValidationMutation, + ].forEach((command) => { + this._commandService.registerCommand(command); + }); + } +} diff --git a/packages/data-validation/src/services/data-validator-registry.service.ts b/packages/data-validation/src/services/data-validator-registry.service.ts new file mode 100644 index 0000000000..376eb8fd3f --- /dev/null +++ b/packages/data-validation/src/services/data-validator-registry.service.ts @@ -0,0 +1,95 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { toDisposable } from '@univerjs/core'; +import { BehaviorSubject } from 'rxjs'; +import type { BaseDataValidator } from '../validators/base-data-validator'; + +export enum DataValidatorRegistryScope { + SHEET = 'sheet', +} + +/** + * Register data validator + */ +export class DataValidatorRegistryService { + private _validatorByScopes = new Map>(); + + private _validatorMap = new Map(); + + private _validatorsChange$ = new BehaviorSubject(undefined); + validatorsChange$ = this._validatorsChange$.asObservable(); + + private _addValidatorToScope(validator: BaseDataValidator, scope: string) { + if (!this._validatorByScopes.has(scope)) { + this._validatorByScopes.set(scope, []); + } + + const validators = this._validatorByScopes.get(scope)!; + if (validators.findIndex((m) => m.id === validator.id) > -1) { + throw new Error(`Validator item with the same id ${validator.id} has already been added!`); + } + validators.push(validator); + } + + private _removeValidatorFromScope(validator: BaseDataValidator, scope: string) { + const validators = this._validatorByScopes.get(scope); + if (!validators) { + return; + } + + const index = validators.findIndex((v) => v.id === validator.id); + if (index > -1) { + validators.splice(index, 1); + } + } + + register(validator: BaseDataValidator) { + this._validatorMap.set(validator.id, validator); + + if (Array.isArray(validator.scopes)) { + validator.scopes.forEach((scope) => { + this._addValidatorToScope(validator, scope); + }); + } else { + this._addValidatorToScope(validator, validator.scopes); + } + + this._validatorsChange$.next(); + + return toDisposable(() => { + this._validatorMap.delete(validator.id); + + if (Array.isArray(validator.scopes)) { + validator.scopes.forEach((scope) => { + this._removeValidatorFromScope(validator, scope); + }); + } else { + this._removeValidatorFromScope(validator, validator.scopes); + } + + this._validatorsChange$.next(); + }); + } + + getValidatorItem(id: string) { + return this._validatorMap.get(id); + } + + getValidatorsByScope(scope: string) { + return this._validatorByScopes.get(scope); + } +} diff --git a/packages/data-validation/src/types/const/operator-text-map.ts b/packages/data-validation/src/types/const/operator-text-map.ts new file mode 100644 index 0000000000..3b50c50a62 --- /dev/null +++ b/packages/data-validation/src/types/const/operator-text-map.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DataValidationOperator } from '@univerjs/core'; + +export const OperatorTextMap: Record = { + [DataValidationOperator.BETWEEN]: 'dataValidation.operators.between', + [DataValidationOperator.EQUAL]: 'dataValidation.operators.equal', + [DataValidationOperator.GREATER_THAN]: 'dataValidation.operators.greaterThan', + [DataValidationOperator.GREATER_THAN_OR_EQUAL]: 'dataValidation.operators.greaterThanOrEqual', + [DataValidationOperator.LESS_THAN]: 'dataValidation.operators.lessThan', + [DataValidationOperator.LESS_THAN_OR_EQUAL]: 'dataValidation.operators.lessThanOrEqual', + [DataValidationOperator.NOT_BETWEEN]: 'dataValidation.operators.notBetween', + [DataValidationOperator.NOT_EQUAL]: 'dataValidation.operators.notEqual', +}; + +export const OperatorTitleMap: Record = { + [DataValidationOperator.BETWEEN]: 'dataValidation.ruleName.between', + [DataValidationOperator.EQUAL]: 'dataValidation.ruleName.equal', + [DataValidationOperator.GREATER_THAN]: 'dataValidation.ruleName.greaterThan', + [DataValidationOperator.GREATER_THAN_OR_EQUAL]: 'dataValidation.ruleName.greaterThanOrEqual', + [DataValidationOperator.LESS_THAN]: 'dataValidation.ruleName.lessThan', + [DataValidationOperator.LESS_THAN_OR_EQUAL]: 'dataValidation.ruleName.lessThanOrEqual', + [DataValidationOperator.NOT_BETWEEN]: 'dataValidation.ruleName.notBetween', + [DataValidationOperator.NOT_EQUAL]: 'dataValidation.ruleName.notEqual', +}; + +export const OperatorErrorTitleMap: Record = { + [DataValidationOperator.BETWEEN]: 'dataValidation.errorMsg.between', + [DataValidationOperator.EQUAL]: 'dataValidation.errorMsg.equal', + [DataValidationOperator.GREATER_THAN]: 'dataValidation.errorMsg.greaterThan', + [DataValidationOperator.GREATER_THAN_OR_EQUAL]: 'dataValidation.errorMsg.greaterThanOrEqual', + [DataValidationOperator.LESS_THAN]: 'dataValidation.errorMsg.lessThan', + [DataValidationOperator.LESS_THAN_OR_EQUAL]: 'dataValidation.errorMsg.lessThanOrEqual', + [DataValidationOperator.NOT_BETWEEN]: 'dataValidation.errorMsg.notBetween', + [DataValidationOperator.NOT_EQUAL]: 'dataValidation.errorMsg.notEqual', +}; + +export const TextLengthErrorTitleMap: Record = { + [DataValidationOperator.BETWEEN]: 'dataValidation.textLength.errorMsg.between', + [DataValidationOperator.EQUAL]: 'dataValidation.textLength.errorMsg.equal', + [DataValidationOperator.GREATER_THAN]: 'dataValidation.textLength.errorMsg.greaterThan', + [DataValidationOperator.GREATER_THAN_OR_EQUAL]: 'dataValidation.textLength.errorMsg.greaterThanOrEqual', + [DataValidationOperator.LESS_THAN]: 'dataValidation.textLength.errorMsg.lessThan', + [DataValidationOperator.LESS_THAN_OR_EQUAL]: 'dataValidation.textLength.errorMsg.lessThanOrEqual', + [DataValidationOperator.NOT_BETWEEN]: 'dataValidation.textLength.errorMsg.notBetween', + [DataValidationOperator.NOT_EQUAL]: 'dataValidation.textLength.errorMsg.notEqual', +}; diff --git a/packages/data-validation/src/types/const/two-formula-operators.ts b/packages/data-validation/src/types/const/two-formula-operators.ts new file mode 100644 index 0000000000..5c263a039d --- /dev/null +++ b/packages/data-validation/src/types/const/two-formula-operators.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DataValidationOperator } from '@univerjs/core'; + +export const TWO_FORMULA_OPERATOR_COUNT: DataValidationOperator[] = [ + DataValidationOperator.BETWEEN, + DataValidationOperator.NOT_BETWEEN, +]; diff --git a/packages/data-validation/src/types/enum/update-rule-type.ts b/packages/data-validation/src/types/enum/update-rule-type.ts new file mode 100644 index 0000000000..a92a537861 --- /dev/null +++ b/packages/data-validation/src/types/enum/update-rule-type.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export enum UpdateRuleType { + SETTING, + RANGE, + OPTIONS, +} diff --git a/packages/data-validation/src/types/interfaces/i-data-validation-drop-down.ts b/packages/data-validation/src/types/interfaces/i-data-validation-drop-down.ts new file mode 100644 index 0000000000..7098e59041 --- /dev/null +++ b/packages/data-validation/src/types/interfaces/i-data-validation-drop-down.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface IDataValidationDropdownProps { + top: number; + left: number; + width: number; + height: number; + unitId: string; + subUnitId: string; + row: number; + col: number; +} diff --git a/packages/data-validation/src/types/interfaces/i-data-validator-operator-config.ts b/packages/data-validation/src/types/interfaces/i-data-validator-operator-config.ts new file mode 100644 index 0000000000..81ee23ff83 --- /dev/null +++ b/packages/data-validation/src/types/interfaces/i-data-validator-operator-config.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DataValidationOperator } from '@univerjs/core'; + +export interface IDataValidatorOperatorConfig { + operator: DataValidationOperator; + text: string; + placeholder1?: string; + placeholder2?: string; + formulaCount: 1 | 2; +} diff --git a/packages/data-validation/src/types/interfaces/i-formula-input.ts b/packages/data-validation/src/types/interfaces/i-formula-input.ts new file mode 100644 index 0000000000..1c4c3c0c03 --- /dev/null +++ b/packages/data-validation/src/types/interfaces/i-formula-input.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IFormulaValidResult } from '../../validators/base-data-validator'; + +export interface IFormulaValue { + formula1?: string; + formula2?: string; +} + +export interface IFormulaInputProps { + isTwoFormula?: boolean; + value?: IFormulaValue; + onChange?: (value?: IFormulaValue) => void; + unitId: string; + subUnitId: string; + showError?: boolean; + validResult?: IFormulaValidResult; + ruleId: string; +} + +export type FormulaInputType = React.ComponentType; diff --git a/packages/data-validation/src/types/interfaces/i-update-rule-payload.ts b/packages/data-validation/src/types/interfaces/i-update-rule-payload.ts new file mode 100644 index 0000000000..4a35826787 --- /dev/null +++ b/packages/data-validation/src/types/interfaces/i-update-rule-payload.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IDataValidationRuleBase, IDataValidationRuleOptions } from '@univerjs/core'; +import type { UpdateRuleType } from '../enum/update-rule-type'; + +export interface IUpdateRuleRangePayload { + type: UpdateRuleType.RANGE; + payload: any[]; +} + +export interface IUpdateRuleSettingPayload { + type: UpdateRuleType.SETTING; + payload: IDataValidationRuleBase; +} + +export interface IUpdateRuleOptionsPayload { + type: UpdateRuleType.OPTIONS; + payload: Partial; +} + +export type IUpdateRulePayload = + IUpdateRuleRangePayload + | IUpdateRuleSettingPayload + | IUpdateRuleOptionsPayload; diff --git a/packages/data-validation/src/validators/base-data-validator.ts b/packages/data-validation/src/validators/base-data-validator.ts new file mode 100644 index 0000000000..a4aea2ae54 --- /dev/null +++ b/packages/data-validation/src/validators/base-data-validator.ts @@ -0,0 +1,211 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CellValue, IDataValidationRule, IDataValidationRuleBase, Nullable } from '@univerjs/core'; +import { DataValidationOperator, LocaleService, Tools } from '@univerjs/core'; +import { Inject, Injector } from '@wendellhu/redi'; +import { OperatorErrorTitleMap, OperatorTitleMap } from '../types/const/operator-text-map'; +import type { IBaseDataValidationWidget } from './base-widget'; + +const FORMULA1 = '{FORMULA1}'; +const FORMULA2 = '{FORMULA2}'; + +const operatorNameMap: Record = { + [DataValidationOperator.BETWEEN]: 'dataValidation.operators.between', + [DataValidationOperator.EQUAL]: 'dataValidation.operators.equal', + [DataValidationOperator.GREATER_THAN]: 'dataValidation.operators.greaterThan', + [DataValidationOperator.GREATER_THAN_OR_EQUAL]: 'dataValidation.operators.greaterThanOrEqual', + [DataValidationOperator.LESS_THAN]: 'dataValidation.operators.lessThan', + [DataValidationOperator.LESS_THAN_OR_EQUAL]: 'dataValidation.operators.lessThanOrEqual', + [DataValidationOperator.NOT_BETWEEN]: 'dataValidation.operators.notBetween', + [DataValidationOperator.NOT_EQUAL]: 'dataValidation.operators.notEqual', +}; + +export interface IValidatorCellInfo> { + value: DataType; + row: number; + column: number; + unitId: string; + subUnitId: string; +} + +export interface IFormulaResult { + formula1: T; + formula2: T; +} + +export interface IFormulaValidResult { + success: boolean; + formula1?: string; + formula2?: string; +} + +export abstract class BaseDataValidator { + abstract id: string; + + abstract title: string; + + abstract operators: DataValidationOperator[]; + + abstract scopes: string[] | string; + + abstract formulaInput: string; + + canvasRender: Nullable = null; + + dropdown: string | undefined = undefined; + + optionsInput: string | undefined = undefined; + + constructor( + @Inject(LocaleService) readonly localeService: LocaleService, + @Inject(Injector) readonly injector: Injector + ) { + // empty + } + + get operatorNames() { + return this.operators.map((operator) => this.localeService.t(operatorNameMap[operator])); + } + + get titleStr() { + return this.localeService.t(this.title); + } + + skipDefaultFontRender(rule: IDataValidationRule, cellValue: Nullable, pos: any) { + return false; + }; + + generateRuleName(rule: IDataValidationRuleBase): string { + if (!rule.operator) { + return this.titleStr; + } + + const ruleName = this.localeService.t(OperatorTitleMap[rule.operator]).replace(FORMULA1, rule.formula1 ?? '').replace(FORMULA2, rule.formula2 ?? ''); + return `${this.titleStr} ${ruleName}`; + } + + generateRuleErrorMessage(rule: IDataValidationRuleBase) { + if (!rule.operator) { + return this.titleStr; + } + + const errorMsg = this.localeService.t(OperatorErrorTitleMap[rule.operator]).replace(FORMULA1, rule.formula1 ?? '').replace(FORMULA2, rule.formula2 ?? ''); + return `${errorMsg}`; + } + + getRuleFinalError(rule: IDataValidationRule) { + if (rule.showInputMessage && rule.error) { + return rule.error; + } + + return this.generateRuleErrorMessage(rule); + } + + isEmptyCellValue(cellValue: Nullable): cellValue is null | undefined | void { + if (cellValue === '' || cellValue === undefined || cellValue === null) { + return true; + } + + return false; + } + + abstract parseFormula(rule: IDataValidationRule, unitId: string, subUnitId: string): Promise; + + abstract validatorFormula(rule: IDataValidationRuleBase): IFormulaValidResult; + + async isValidType(cellInfo: IValidatorCellInfo, formula: IFormulaResult, rule: IDataValidationRule): Promise { + return true; + }; + + transform(cellInfo: IValidatorCellInfo, formula: IFormulaResult, rule: IDataValidationRule): IValidatorCellInfo { + return cellInfo as IValidatorCellInfo; + }; + + async validatorIsEqual(cellInfo: IValidatorCellInfo, formula: IFormulaResult, rule: IDataValidationRule): Promise { + return true; + }; + + async validatorIsNotEqual(cellInfo: IValidatorCellInfo, formula: IFormulaResult, rule: IDataValidationRule): Promise { + return true; + }; + + async validatorIsBetween(cellInfo: IValidatorCellInfo, formula: IFormulaResult, rule: IDataValidationRule): Promise { + return true; + }; + + async validatorIsNotBetween(cellInfo: IValidatorCellInfo, formula: IFormulaResult, rule: IDataValidationRule): Promise { + return true; + }; + + async validatorIsGreaterThan(cellInfo: IValidatorCellInfo, formula: IFormulaResult, rule: IDataValidationRule): Promise { + return true; + }; + + async validatorIsGreaterThanOrEqual(cellInfo: IValidatorCellInfo, formula: IFormulaResult, rule: IDataValidationRule): Promise { + return true; + }; + + async validatorIsLessThan(cellInfo: IValidatorCellInfo, formula: IFormulaResult, rule: IDataValidationRule): Promise { + return true; + }; + + async validatorIsLessThanOrEqual(cellInfo: IValidatorCellInfo, formula: IFormulaResult, rule: IDataValidationRule): Promise { + return true; + }; + + async validator(cellInfo: IValidatorCellInfo, rule: IDataValidationRule): Promise { + const { value: cellValue, unitId, subUnitId } = cellInfo; + const isEmpty = this.isEmptyCellValue(cellValue); + const { allowBlank = true, operator } = rule; + if (isEmpty) { + return allowBlank; + } + + const formulaInfo = await this.parseFormula(rule, unitId, subUnitId); + + if (!(await this.isValidType(cellInfo, formulaInfo, rule))) { + return false; + } + + if (!Tools.isDefine(operator)) { + return true; + } + + const transformedCell = this.transform(cellInfo, formulaInfo, rule); + + switch (operator) { + case DataValidationOperator.BETWEEN: + return this.validatorIsBetween(transformedCell, formulaInfo, rule); + case DataValidationOperator.EQUAL: + return this.validatorIsEqual(transformedCell, formulaInfo, rule); + case DataValidationOperator.GREATER_THAN: + return this.validatorIsGreaterThan(transformedCell, formulaInfo, rule); + case DataValidationOperator.GREATER_THAN_OR_EQUAL: + return this.validatorIsGreaterThanOrEqual(transformedCell, formulaInfo, rule); + case DataValidationOperator.LESS_THAN: + return this.validatorIsLessThan(transformedCell, formulaInfo, rule); + case DataValidationOperator.LESS_THAN_OR_EQUAL: + return this.validatorIsLessThanOrEqual(transformedCell, formulaInfo, rule); + case DataValidationOperator.NOT_BETWEEN: + return this.validatorIsNotBetween(transformedCell, formulaInfo, rule); + case DataValidationOperator.NOT_EQUAL: + return this.validatorIsNotEqual(transformedCell, formulaInfo, rule); + default: + throw new Error('Unknown operator.'); + } + } +} diff --git a/packages/data-validation/src/validators/base-widget.ts b/packages/data-validation/src/validators/base-widget.ts new file mode 100644 index 0000000000..2505002a6e --- /dev/null +++ b/packages/data-validation/src/validators/base-widget.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ICellCustomRender, ICellRenderContext } from '@univerjs/core'; + +export interface IBaseDataValidationWidget extends ICellCustomRender { + calcCellAutoHeight(info: ICellRenderContext): number | undefined; +} diff --git a/packages/data-validation/src/vite-env.d.ts b/packages/data-validation/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/packages/data-validation/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/data-validation/tsconfig.json b/packages/data-validation/tsconfig.json new file mode 100644 index 0000000000..d676ad2a20 --- /dev/null +++ b/packages/data-validation/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@univerjs/shared/tsconfigs/base", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "references": [{ "path": "./tsconfig.node.json" }], + "include": ["src"] +} diff --git a/packages/data-validation/tsconfig.node.json b/packages/data-validation/tsconfig.node.json new file mode 100644 index 0000000000..e53dac8868 --- /dev/null +++ b/packages/data-validation/tsconfig.node.json @@ -0,0 +1,4 @@ +{ + "extends": "@univerjs/shared/tsconfigs/node", + "include": ["vite.config.ts"] +} diff --git a/packages/data-validation/vite.config.ts b/packages/data-validation/vite.config.ts new file mode 100644 index 0000000000..e38cd8530b --- /dev/null +++ b/packages/data-validation/vite.config.ts @@ -0,0 +1,12 @@ +import createViteConfig from '@univerjs/shared/vite'; +import pkg from './package.json'; + +export default ({ mode }) => createViteConfig({}, { + mode, + pkg, + features: { + react: true, + css: true, + dom: true, + }, +}); diff --git a/packages/debugger/README.md b/packages/debugger/README.md new file mode 100644 index 0000000000..b26b2a260b --- /dev/null +++ b/packages/debugger/README.md @@ -0,0 +1,16 @@ +# @univerjs/debugger + +[![npm version](https://img.shields.io/npm/v/@univerjs/debugger)](https://npmjs.org/packages/@univerjs/debugger) +[![license](https://img.shields.io/npm/l/@univerjs/debugger)](https://img.shields.io/npm/l/@univerjs/debugger) + +## Introduction + +> This plugin provides a lot of utilities to help you debug Univer. **Never use this plugin in your production environment!** + +## Usage + +### Installation + +```shell +npm i @univerjs/debugger +``` diff --git a/packages/debugger/package.json b/packages/debugger/package.json new file mode 100644 index 0000000000..3d3142d5c5 --- /dev/null +++ b/packages/debugger/package.json @@ -0,0 +1,83 @@ +{ + "name": "@univerjs/debugger", + "version": "0.1.12", + "private": false, + "description": "", + "author": "DreamNum ", + "license": "Apache-2.0", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/univer" + }, + "homepage": "https://univer.ai", + "repository": { + "type": "git", + "url": "https://github.com/dream-num/univer" + }, + "bugs": { + "url": "https://github.com/dream-num/univer/issues" + }, + "keywords": [], + "exports": { + ".": "./src/index.ts", + "./*": "./src/*" + }, + "main": "./lib/cjs/index.js", + "module": "./lib/es/index.js", + "types": "./lib/types/index.d.ts", + "publishConfig": { + "access": "public", + "main": "./lib/cjs/index.js", + "module": "./lib/es/index.js", + "exports": { + ".": { + "import": "./lib/es/index.js", + "require": "./lib/cjs/index.js", + "types": "./lib/types/index.d.ts" + }, + "./*": { + "import": "./lib/es/*", + "require": "./lib/cjs/*", + "types": "./lib/types/index.d.ts" + }, + "./lib/*": "./lib/*" + } + }, + "directories": { + "lib": "lib" + }, + "files": [ + "lib" + ], + "scripts": { + "lint:types": "tsc --noEmit", + "build": "tsc && vite build" + }, + "peerDependencies": { + "@univerjs/core": "workspace:*", + "@univerjs/design": "workspace:*", + "@univerjs/docs-ui": "workspace:*", + "@univerjs/sheets": "workspace:*", + "@univerjs/ui": "workspace:*", + "@wendellhu/redi": "0.15.2", + "react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "rxjs": ">=7.0.0" + }, + "optionalDependencies": { + "vue": ">=3.0.0" + }, + "devDependencies": { + "@univerjs/core": "workspace:*", + "@univerjs/docs-ui": "workspace:*", + "@univerjs/shared": "workspace:*", + "@univerjs/sheets": "workspace:*", + "@univerjs/ui": "workspace:*", + "@vitejs/plugin-vue": "^5.0.4", + "@wendellhu/redi": "0.15.2", + "rxjs": "^7.8.1", + "typescript": "^5.4.5", + "vite": "^5.2.11", + "vitest": "^1.6.0", + "vue": "^3.4.27" + } +} diff --git a/packages/debugger/src/commands/commands/unit.command.ts b/packages/debugger/src/commands/commands/unit.command.ts new file mode 100644 index 0000000000..4b6c6c73ce --- /dev/null +++ b/packages/debugger/src/commands/commands/unit.command.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommandType, type ICommand, IUniverInstanceService, UniverInstanceType } from '@univerjs/core'; + +export const DisposeCurrentUnitCommand: ICommand = { + id: 'debugger.command.dispose-current-unit', + type: CommandType.COMMAND, + handler: async (accessor) => { + const univerInstanceService = accessor.get(IUniverInstanceService); + const focused = univerInstanceService.getFocusedUnit(); + if (!focused) return false; + + return univerInstanceService.disposeUnit(focused.getUnitId()); + }, +}; + +export const CreateEmptySheetCommand: ICommand = { + id: 'debugger.command.create-empty-sheet', + type: CommandType.COMMAND, + handler: async (accessor) => { + const univerInstanceService = accessor.get(IUniverInstanceService); + univerInstanceService.createUnit(UniverInstanceType.UNIVER_SHEET, {}); + return true; + }, +}; diff --git a/examples/src/plugins/debugger/commands/operations/confirm.operation.ts b/packages/debugger/src/commands/operations/confirm.operation.ts similarity index 100% rename from examples/src/plugins/debugger/commands/operations/confirm.operation.ts rename to packages/debugger/src/commands/operations/confirm.operation.ts diff --git a/examples/src/plugins/debugger/commands/operations/dialog.operation.ts b/packages/debugger/src/commands/operations/dialog.operation.ts similarity index 100% rename from examples/src/plugins/debugger/commands/operations/dialog.operation.ts rename to packages/debugger/src/commands/operations/dialog.operation.ts diff --git a/examples/src/plugins/debugger/commands/operations/locale.operation.ts b/packages/debugger/src/commands/operations/locale.operation.ts similarity index 100% rename from examples/src/plugins/debugger/commands/operations/locale.operation.ts rename to packages/debugger/src/commands/operations/locale.operation.ts diff --git a/examples/src/plugins/debugger/commands/operations/message.operation.ts b/packages/debugger/src/commands/operations/message.operation.ts similarity index 100% rename from examples/src/plugins/debugger/commands/operations/message.operation.ts rename to packages/debugger/src/commands/operations/message.operation.ts diff --git a/examples/src/plugins/debugger/commands/operations/notification.operation.ts b/packages/debugger/src/commands/operations/notification.operation.ts similarity index 100% rename from examples/src/plugins/debugger/commands/operations/notification.operation.ts rename to packages/debugger/src/commands/operations/notification.operation.ts diff --git a/examples/src/plugins/debugger/commands/operations/numfmt.operations.ts b/packages/debugger/src/commands/operations/numfmt.operations.ts similarity index 100% rename from examples/src/plugins/debugger/commands/operations/numfmt.operations.ts rename to packages/debugger/src/commands/operations/numfmt.operations.ts diff --git a/examples/src/plugins/debugger/commands/operations/save-snapshot.operations.ts b/packages/debugger/src/commands/operations/save-snapshot.operations.ts similarity index 81% rename from examples/src/plugins/debugger/commands/operations/save-snapshot.operations.ts rename to packages/debugger/src/commands/operations/save-snapshot.operations.ts index da9f4ed086..9d4785a9f0 100644 --- a/examples/src/plugins/debugger/commands/operations/save-snapshot.operations.ts +++ b/packages/debugger/src/commands/operations/save-snapshot.operations.ts @@ -15,11 +15,11 @@ */ /* eslint-disable node/prefer-global/process */ -import type { ICommand, IStyleData, IWorkbookData } from '@univerjs/core'; -import { CommandType, ISnapshotPersistenceService, IUniverInstanceService, ObjectMatrix } from '@univerjs/core'; +import type { ICommand, IStyleData, IWorkbookData, Workbook } from '@univerjs/core'; +import { CommandType, IResourceLoaderService, IUniverInstanceService, ObjectMatrix, UniverInstanceType } from '@univerjs/core'; import type { IAccessor } from '@wendellhu/redi'; - -import { ExportController, RecordController } from '../../../local-save'; +import { RecordController } from '../../controllers/local-save/record.controller'; +import { ExportController } from '../../controllers/local-save/export.controller'; export interface ISaveSnapshotParams { value: 'sheet' | 'workbook' | 'record'; @@ -48,13 +48,13 @@ export const SaveSnapshotOptions: ICommand = { type: CommandType.OPERATION, handler: async (accessor: IAccessor, params: ISaveSnapshotParams) => { const univerInstanceService = accessor.get(IUniverInstanceService); - const localPersistenceService = accessor.get(ISnapshotPersistenceService); + const resourceLoaderService = accessor.get(IResourceLoaderService); const exportController = accessor.get(ExportController); const recordController = accessor.get(RecordController); - const workbook = univerInstanceService.getCurrentUniverSheetInstance(); + const workbook = univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET)!; const worksheet = workbook.getActiveSheet(); - const snapshot = localPersistenceService.saveWorkbook(workbook); + const snapshot = resourceLoaderService.saveWorkbook(workbook); const gitHash = process.env.GIT_COMMIT_HASH; const gitBranch = process.env.GIT_REF_NAME; const buildTime = process.env.BUILD_TIME; @@ -66,12 +66,13 @@ export const SaveSnapshotOptions: ICommand = { const sheet = snapshot.sheets[sheetId]; snapshot.sheets = { [sheetId]: sheet }; snapshot.sheetOrder = [sheetId]; + const text = JSON.stringify(filterStyle(snapshot), null, 2); + exportController.exportJson(text, `${preName} snapshot`); break; } case 'workbook': { const text = JSON.stringify(filterStyle(snapshot), null, 2); - // navigator.clipboard.writeText(text); exportController.exportJson(text, `${preName} snapshot`); break; diff --git a/examples/src/plugins/debugger/commands/operations/set.editable.operation.ts b/packages/debugger/src/commands/operations/set.editable.operation.ts similarity index 84% rename from examples/src/plugins/debugger/commands/operations/set.editable.operation.ts rename to packages/debugger/src/commands/operations/set.editable.operation.ts index 41ba48f0b7..ef590a0566 100644 --- a/examples/src/plugins/debugger/commands/operations/set.editable.operation.ts +++ b/packages/debugger/src/commands/operations/set.editable.operation.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import type { ICommand } from '@univerjs/core'; -import { CommandType, IUniverInstanceService, UniverPermissionService } from '@univerjs/core'; +import type { ICommand, Workbook } from '@univerjs/core'; +import { CommandType, IUniverInstanceService, UniverInstanceType, UniverPermissionService } from '@univerjs/core'; import { SheetPermissionService } from '@univerjs/sheets'; import type { IAccessor } from '@wendellhu/redi'; @@ -34,7 +34,7 @@ export const SetEditable: ICommand = { } else { const univerPermissionService = accessor.get(UniverPermissionService); const univerInstanceService = accessor.get(IUniverInstanceService); - const unitId = univerInstanceService.getCurrentUniverSheetInstance().getUnitId(); + const unitId = univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET)!.getUnitId(); const editable = univerPermissionService.getEditable(unitId); univerPermissionService.setEditable(unitId, !editable); } diff --git a/examples/src/plugins/debugger/commands/operations/sidebar.operation.ts b/packages/debugger/src/commands/operations/sidebar.operation.ts similarity index 90% rename from examples/src/plugins/debugger/commands/operations/sidebar.operation.ts rename to packages/debugger/src/commands/operations/sidebar.operation.ts index 3eac6f35d2..652fa52db3 100644 --- a/examples/src/plugins/debugger/commands/operations/sidebar.operation.ts +++ b/packages/debugger/src/commands/operations/sidebar.operation.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import type { ICommand } from '@univerjs/core'; -import { CommandType, IUniverInstanceService } from '@univerjs/core'; +import type { ICommand, Workbook } from '@univerjs/core'; +import { CommandType, IUniverInstanceService, UniverInstanceType } from '@univerjs/core'; import { IEditorService, ISidebarService } from '@univerjs/ui'; import type { IAccessor } from '@wendellhu/redi'; import { TEST_EDITOR_CONTAINER_COMPONENT } from '../../views/test-editor/component-name'; @@ -31,7 +31,7 @@ export const SidebarOperation: ICommand = { const sidebarService = accessor.get(ISidebarService); const editorService = accessor.get(IEditorService); const univerInstanceService = accessor.get(IUniverInstanceService); - const unit = univerInstanceService.getCurrentUniverSheetInstance(); + const unit = univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET)!; switch (params.value) { case 'open': editorService.setOperationSheetUnitId(unit.getUnitId()); diff --git a/examples/src/plugins/debugger/commands/operations/theme.operation.ts b/packages/debugger/src/commands/operations/theme.operation.ts similarity index 100% rename from examples/src/plugins/debugger/commands/operations/theme.operation.ts rename to packages/debugger/src/commands/operations/theme.operation.ts diff --git a/examples/src/plugins/debugger/components/VueI18nIcon.vue b/packages/debugger/src/components/VueI18nIcon.vue similarity index 100% rename from examples/src/plugins/debugger/components/VueI18nIcon.vue rename to packages/debugger/src/components/VueI18nIcon.vue diff --git a/examples/src/plugins/debugger/controllers/debugger.controller.ts b/packages/debugger/src/controllers/debugger.controller.ts similarity index 79% rename from examples/src/plugins/debugger/controllers/debugger.controller.ts rename to packages/debugger/src/controllers/debugger.controller.ts index e2c2c49ad1..ebff79f685 100644 --- a/examples/src/plugins/debugger/controllers/debugger.controller.ts +++ b/packages/debugger/src/controllers/debugger.controller.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { Disposable, ICommandService, ISnapshotPersistenceService, IUniverInstanceService } from '@univerjs/core'; +import { Disposable, ICommandService, IUniverInstanceService } from '@univerjs/core'; +import type { IMenuItemFactory, MenuConfig } from '@univerjs/ui'; import { ComponentManager, IMenuService } from '@univerjs/ui'; import { Inject, Injector } from '@wendellhu/redi'; -import { ExportController, LocalSnapshotService, RecordController } from '../../local-save'; import { ConfirmOperation } from '../commands/operations/confirm.operation'; import { DialogOperation } from '../commands/operations/dialog.operation'; import { LocaleOperation } from '../commands/operations/locale.operation'; @@ -34,9 +34,12 @@ import { TEST_EDITOR_CONTAINER_COMPONENT } from '../views/test-editor/component- // @ts-ignore import VueI18nIcon from '../components/VueI18nIcon.vue'; +import { CreateEmptySheetCommand, DisposeCurrentUnitCommand } from '../commands/commands/unit.command'; import { ConfirmMenuItemFactory, + CreateEmptySheetMenuItemFactory, DialogMenuItemFactory, + DisposeCurrentUnitMenuItemFactory, LocaleMenuItemFactory, MessageMenuItemFactory, NotificationMenuItemFactory, @@ -44,10 +47,20 @@ import { SetEditableMenuItemFactory, SidebarMenuItemFactory, ThemeMenuItemFactory, + UnitMenuItemFactory, } from './menu'; +import { RecordController } from './local-save/record.controller'; +import { ExportController } from './local-save/export.controller'; + +export interface IUniverDebuggerConfig { + menu: MenuConfig; +} + +export const DefaultDebuggerConfig = {}; export class DebuggerController extends Disposable { constructor( + private readonly _config: Partial, @Inject(Injector) private readonly _injector: Injector, @IMenuService private readonly _menuService: IMenuService, @ICommandService private readonly _commandService: ICommandService, @@ -69,15 +82,18 @@ export class DebuggerController extends Disposable { SidebarOperation, SetEditable, SaveSnapshotOptions, + DisposeCurrentUnitCommand, + CreateEmptySheetCommand, ].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command))); - this._injector.add([ISnapshotPersistenceService, { useClass: LocalSnapshotService }]); this._injector.add([ExportController]); this._injector.add([RecordController]); } private _initializeContextMenu() { - [ + const { menu = {} } = this._config; + + ([ LocaleMenuItemFactory, ThemeMenuItemFactory, NotificationMenuItemFactory, @@ -87,8 +103,11 @@ export class DebuggerController extends Disposable { SidebarMenuItemFactory, SetEditableMenuItemFactory, SaveSnapshotSetEditableMenuItemFactory, - ].forEach((factory) => { - this.disposeWithMe(this._menuService.addMenuItem(this._injector.invoke(factory))); + UnitMenuItemFactory, + DisposeCurrentUnitMenuItemFactory, + CreateEmptySheetMenuItemFactory, + ] as IMenuItemFactory[]).forEach((factory) => { + this.disposeWithMe(this._menuService.addMenuItem(this._injector.invoke(factory), menu)); }); } diff --git a/packages/debugger/src/controllers/e2e/e2e-memory.controller.ts b/packages/debugger/src/controllers/e2e/e2e-memory.controller.ts new file mode 100644 index 0000000000..cb2586870b --- /dev/null +++ b/packages/debugger/src/controllers/e2e/e2e-memory.controller.ts @@ -0,0 +1,106 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { IUniverInstanceService, LifecycleStages, LocaleType, OnLifecycle, UniverInstanceType } from '@univerjs/core'; + +const DEFAULT_WORKBOOK_DATA_DEMO = { + id: 'test', + appVersion: '3.0.0-alpha', + sheets: { + sheet1: { + id: 'sheet1', + name: 'sheet1', + cellData: { + 0: { + 3: { + f: '=SUM(A1)', + si: '3e4r5t', + }, + }, + 1: { + 3: { + f: '=SUM(A2)', + si: 'OSPtzm', + }, + }, + 2: { + 3: { + si: 'OSPtzm', + }, + }, + 3: { + 3: { + si: 'OSPtzm', + }, + }, + }, + rowCount: 100, + columnCount: 100, + }, + }, + locale: LocaleType.ZH_CN, + name: '', + sheetOrder: [], + styles: {}, + resources: [ + ], +}; + +const AWAIT_LOADING_TIMEOUT = 5000; +const AWAIT_DISPOSING_TIMEOUT = 5000; + +export interface IE2EMemoryControllerAPI { + loadAndRelease(id: number): Promise; +} + +declare global { + // eslint-disable-next-line ts/naming-convention + interface Window { + E2EMemoryAPI: IE2EMemoryControllerAPI; + } +} + +/** + * This controller expose a API on `Window` for the E2E memory test. + */ +@OnLifecycle(LifecycleStages.Starting, E2EMemoryController) +export class E2EMemoryController { + constructor( + @IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService + ) { + this._initPlugin(); + } + + private _initPlugin(): void { + window.E2EMemoryAPI = { + loadAndRelease: (id) => this._releaseAndLoad(id), + }; + } + + private async _releaseAndLoad(releaseId: number): Promise { + const unitId = `e2e${releaseId}`; + this._univerInstanceService.createUnit(UniverInstanceType.UNIVER_SHEET, { ...DEFAULT_WORKBOOK_DATA_DEMO, id: unitId }); + await timer(AWAIT_LOADING_TIMEOUT); + this._univerInstanceService.disposeUnit(unitId); + await timer(AWAIT_DISPOSING_TIMEOUT); + } +} + +function timer(timeout: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, timeout); + }); +} diff --git a/examples/src/plugins/local-save/controller/export.controller.ts b/packages/debugger/src/controllers/local-save/export.controller.ts similarity index 94% rename from examples/src/plugins/local-save/controller/export.controller.ts rename to packages/debugger/src/controllers/local-save/export.controller.ts index ccc3e26002..8ee48ef0f6 100644 --- a/examples/src/plugins/local-save/controller/export.controller.ts +++ b/packages/debugger/src/controllers/local-save/export.controller.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createDownloadElement } from '../util/index'; +import { createDownloadElement } from './util'; export class ExportController { exportJson(data: string, fileName: string) { diff --git a/examples/src/plugins/local-save/controller/record.controller.ts b/packages/debugger/src/controllers/local-save/record.controller.ts similarity index 98% rename from examples/src/plugins/local-save/controller/record.controller.ts rename to packages/debugger/src/controllers/local-save/record.controller.ts index 2cd41e06bb..e8e6a3f8b1 100644 --- a/examples/src/plugins/local-save/controller/record.controller.ts +++ b/packages/debugger/src/controllers/local-save/record.controller.ts @@ -19,7 +19,9 @@ import { Inject } from '@wendellhu/redi'; import { Observable } from 'rxjs'; export class RecordController { - constructor(@Inject(ICommandService) private _commandService: ICommandService) {} + constructor(@Inject(ICommandService) private _commandService: ICommandService) { + // empty + } record() { return new Observable<{ type: 'start' } | { type: 'finish'; data: Blob }>((subscribe) => { diff --git a/examples/src/plugins/local-save/util/index.ts b/packages/debugger/src/controllers/local-save/util.ts similarity index 100% rename from examples/src/plugins/local-save/util/index.ts rename to packages/debugger/src/controllers/local-save/util.ts diff --git a/examples/src/plugins/debugger/controllers/menu.ts b/packages/debugger/src/controllers/menu.ts similarity index 82% rename from examples/src/plugins/debugger/controllers/menu.ts rename to packages/debugger/src/controllers/menu.ts index 8ac1a1096e..a319916e6a 100644 --- a/examples/src/plugins/debugger/controllers/menu.ts +++ b/packages/debugger/src/controllers/menu.ts @@ -16,7 +16,7 @@ import { LocaleType } from '@univerjs/core'; import { defaultTheme, greenTheme } from '@univerjs/design'; -import type { IMenuSelectorItem } from '@univerjs/ui'; +import type { IMenuButtonItem, IMenuSelectorItem } from '@univerjs/ui'; import { MenuItemType, MenuPosition } from '@univerjs/ui'; import type { IAccessor } from '@wendellhu/redi'; @@ -29,6 +29,7 @@ import { SaveSnapshotOptions } from '../commands/operations/save-snapshot.operat import { SetEditable } from '../commands/operations/set.editable.operation'; import { SidebarOperation } from '../commands/operations/sidebar.operation'; import { ThemeOperation } from '../commands/operations/theme.operation'; +import { CreateEmptySheetCommand, DisposeCurrentUnitCommand } from '../commands/commands/unit.command'; export function LocaleMenuItemFactory(accessor: IAccessor): IMenuSelectorItem { return { @@ -46,6 +47,10 @@ export function LocaleMenuItemFactory(accessor: IAccessor): IMenuSelectorItem { label: '简体中文', value: LocaleType.ZH_CN, }, + { + label: 'Русский', + value: LocaleType.RU_RU, + }, ], }; } @@ -60,11 +65,11 @@ export function ThemeMenuItemFactory(accessor: IAccessor): IMenuSelectorItem { selections: [ { label: 'green', - value: greenTheme, + value: greenTheme as any, }, { label: 'default', - value: defaultTheme, + value: defaultTheme as any, }, ], }; @@ -212,3 +217,37 @@ export function SaveSnapshotSetEditableMenuItemFactory(accessor: IAccessor): IMe ], }; } + +const UNIT_ITEM_MENU_ID = 'debugger.unit-menu-item'; + +export function UnitMenuItemFactory(accessor: IAccessor): IMenuSelectorItem { + return { + id: UNIT_ITEM_MENU_ID, + title: 'Unit', + tooltip: 'Unit Commands', + type: MenuItemType.SUBITEMS, + positions: [MenuPosition.TOOLBAR_OTHERS], + }; +} + +export function DisposeCurrentUnitMenuItemFactory(accessor: IAccessor): IMenuButtonItem { + return { + id: DisposeCurrentUnitCommand.id, + title: 'Dispose Current Unit', + tooltip: 'Dispose Current Unit', + icon: 'DS', + type: MenuItemType.BUTTON, + positions: [UNIT_ITEM_MENU_ID], + }; +} + +export function CreateEmptySheetMenuItemFactory(accessor: IAccessor): IMenuButtonItem { + return { + id: CreateEmptySheetCommand.id, + title: 'Create Another Sheet', + tooltip: 'Create Another Sheet', + icon: 'CR', + type: MenuItemType.BUTTON, + positions: [UNIT_ITEM_MENU_ID], + }; +} diff --git a/examples/src/plugins/debugger/controllers/performance-monitor.controller.ts b/packages/debugger/src/controllers/performance-monitor.controller.ts similarity index 83% rename from examples/src/plugins/debugger/controllers/performance-monitor.controller.ts rename to packages/debugger/src/controllers/performance-monitor.controller.ts index 22e72da777..bb077b9575 100644 --- a/examples/src/plugins/debugger/controllers/performance-monitor.controller.ts +++ b/packages/debugger/src/controllers/performance-monitor.controller.ts @@ -16,13 +16,12 @@ import { IUniverInstanceService, LifecycleStages, OnLifecycle, RxDisposable, UniverInstanceType } from '@univerjs/core'; import { DocCanvasView } from '@univerjs/docs-ui'; -import { SheetCanvasView } from '@univerjs/sheets-ui'; import { Inject, Injector } from '@wendellhu/redi'; import { interval, takeUntil, throttle } from 'rxjs'; @OnLifecycle(LifecycleStages.Rendered, PerformanceMonitorController) export class PerformanceMonitorController extends RxDisposable { - private _documentType: UniverInstanceType = UniverInstanceType.UNKNOWN; + private _documentType: UniverInstanceType = UniverInstanceType.UNIVER_UNKNOWN; private _hasWatched = false; private _container!: HTMLDivElement; private _styleElement!: HTMLStyleElement; @@ -47,7 +46,7 @@ export class PerformanceMonitorController extends RxDisposable { private _listenDocumentTypeChange() { this._instanceService.focused$.pipe(takeUntil(this.dispose$)).subscribe((unitId) => { if (unitId != null) { - const univerType = this._instanceService.getDocumentType(unitId); + const univerType = this._instanceService.getUnitType(unitId); this._documentType = univerType; @@ -87,21 +86,23 @@ export class PerformanceMonitorController extends RxDisposable { this._styleElement = document.createElement('style'); document.head.appendChild(this._styleElement).innerText = style; - if (this._documentType === UniverInstanceType.DOC) { + if (this._documentType === UniverInstanceType.UNIVER_DOC) { this._docCanvasView.fps$ .pipe(takeUntil(this.dispose$)) .pipe(throttle(() => interval(THROTTLE_TIME))) .subscribe((fps) => { container.innerText = `FPS: ${fps}`; }); - } else if (this._documentType === UniverInstanceType.SHEET) { - this._injector - .get(SheetCanvasView) - .fps$.pipe(takeUntil(this.dispose$)) - .pipe(throttle(() => interval(THROTTLE_TIME))) - .subscribe((fps) => { - container.innerText = `FPS: ${fps}`; - }); } + // TODO@wzhudev: restore fps monitor + // else if (this._documentType === UniverInstanceType.UNIVER_SHEET) { + // this._injector + // .get(SheetCanvasView) + // .fps$.pipe(takeUntil(this.dispose$)) + // .pipe(throttle(() => interval(THROTTLE_TIME))) + // .subscribe((fps) => { + // container.innerText = `FPS: ${fps}`; + // }); + // } } } diff --git a/packages/debugger/src/debugger-plugin.ts b/packages/debugger/src/debugger-plugin.ts new file mode 100644 index 0000000000..8e7772afb2 --- /dev/null +++ b/packages/debugger/src/debugger-plugin.ts @@ -0,0 +1,60 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Plugin, Tools } from '@univerjs/core'; +import type { Dependency } from '@wendellhu/redi'; +import { Inject, Injector } from '@wendellhu/redi'; + +import type { IUniverDebuggerConfig } from './controllers/debugger.controller'; +import { DebuggerController, DefaultDebuggerConfig } from './controllers/debugger.controller'; +import { PerformanceMonitorController } from './controllers/performance-monitor.controller'; +import { E2EMemoryController } from './controllers/e2e/e2e-memory.controller'; + +export class UniverDebuggerPlugin extends Plugin { + static override pluginName = 'DEBUGGER_PLUGIN'; + + private _debuggerController!: DebuggerController; + + constructor( + private readonly _config: Partial = {}, + @Inject(Injector) override readonly _injector: Injector + ) { + super(); + + this._config = Tools.deepMerge({}, DefaultDebuggerConfig, this._config); + } + + override onStarting(injector: Injector): void { + ([ + [PerformanceMonitorController], + [E2EMemoryController], + ] as Dependency[]).forEach((d) => injector.add(d)); + } + + override onRendered(): void { + this._injector.add([ + DebuggerController, + { + useFactory: () => this._injector.createInstance(DebuggerController, this._config), + }, + ]); + this._debuggerController = this._injector.get(DebuggerController); + } + + getDebuggerController() { + return this._debuggerController; + } +} diff --git a/examples/src/plugins/debugger/index.ts b/packages/debugger/src/index.ts similarity index 100% rename from examples/src/plugins/debugger/index.ts rename to packages/debugger/src/index.ts diff --git a/examples/src/plugins/debugger/views/test-editor/TestTextEditor.tsx b/packages/debugger/src/views/test-editor/TestTextEditor.tsx similarity index 52% rename from examples/src/plugins/debugger/views/test-editor/TestTextEditor.tsx rename to packages/debugger/src/views/test-editor/TestTextEditor.tsx index 6eabbf5e63..eeef91157f 100644 --- a/examples/src/plugins/debugger/views/test-editor/TestTextEditor.tsx +++ b/packages/debugger/src/views/test-editor/TestTextEditor.tsx @@ -17,7 +17,8 @@ import React, { useState } from 'react'; import { RangeSelector, TextEditor } from '@univerjs/ui'; -import { IUniverInstanceService } from '@univerjs/core'; +import type { Workbook } from '@univerjs/core'; +import { createInternalEditorID, IUniverInstanceService, UniverInstanceType } from '@univerjs/core'; import { useDependency } from '@wendellhu/redi/react-bindings'; import { Input } from '@univerjs/design'; @@ -39,7 +40,7 @@ const editorStyle: React.CSSProperties = { */ export const TestEditorContainer = () => { const univerInstanceService = useDependency(IUniverInstanceService); - const workbook = univerInstanceService.getCurrentUniverSheetInstance(); + const workbook = univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET)!; if (workbook == null) { return; } @@ -54,17 +55,17 @@ export const TestEditorContainer = () => {
- +

- +

- +

- +

- +

- +



diff --git a/examples/src/plugins/debugger/views/test-editor/component-name.ts b/packages/debugger/src/views/test-editor/component-name.ts similarity index 100% rename from examples/src/plugins/debugger/views/test-editor/component-name.ts rename to packages/debugger/src/views/test-editor/component-name.ts diff --git a/packages/debugger/src/vite-env.d.ts b/packages/debugger/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/packages/debugger/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/debugger/tsconfig.json b/packages/debugger/tsconfig.json new file mode 100644 index 0000000000..d676ad2a20 --- /dev/null +++ b/packages/debugger/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@univerjs/shared/tsconfigs/base", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "references": [{ "path": "./tsconfig.node.json" }], + "include": ["src"] +} diff --git a/packages/debugger/tsconfig.node.json b/packages/debugger/tsconfig.node.json new file mode 100644 index 0000000000..e53dac8868 --- /dev/null +++ b/packages/debugger/tsconfig.node.json @@ -0,0 +1,4 @@ +{ + "extends": "@univerjs/shared/tsconfigs/node", + "include": ["vite.config.ts"] +} diff --git a/packages/debugger/vite.config.ts b/packages/debugger/vite.config.ts new file mode 100644 index 0000000000..bf96140bac --- /dev/null +++ b/packages/debugger/vite.config.ts @@ -0,0 +1,17 @@ +import vue from '@vitejs/plugin-vue'; +import createViteConfig from '@univerjs/shared/vite'; +import pkg from './package.json'; + +export default ({ mode }) => createViteConfig({ + plugins: [vue()], +}, { + mode, + pkg, + features: { + react: true, + css: true, + dom: true, + vue: true, + }, +}); + diff --git a/packages/design/README-zh.md b/packages/design/README-zh.md index d3fe90c4de..f982b2ca61 100644 --- a/packages/design/README-zh.md +++ b/packages/design/README-zh.md @@ -14,7 +14,7 @@ ![](./assets/design.jpeg) :::note -如果你只需要扩展工具栏、右键菜单等,可以直接使用 @univerjs/ui 提供的扩展接口,无需自行实现 UI。请参考[扩展 UI](https://univer.ai/zh-cn/guides/customization/ui)。 +如果你只需要扩展工具栏、右键菜单等,可以直接使用 @univerjs/ui 提供的扩展接口,无需自行实现 UI。请参考[扩展 UI](https://univer.ai/zh-CN/guides/sheet/customization/ui)。 ::: ## 使用指南 @@ -30,3 +30,18 @@ pnpm add @univerjs/design ``` 本包含有 CSS 文件,且具有最高优先级,请在引入其他 Univer 样式文件之前引入本包的样式。 + +### 内置主题 + +本包提供了两个内置主题:`defaultTheme` 和 `greenTheme`。你可以直接引入并在你的应用中使用。 + +```typescript +import { defaultTheme } from '@univerjs/design'; +// import { greenTheme } from '@univerjs/design'; + +// Use the default theme +new Univer({ + theme: defaultTheme, + // theme: greenTheme, +}); +``` diff --git a/packages/design/README.md b/packages/design/README.md index c637049c1e..b1b8c0f5e9 100644 --- a/packages/design/README.md +++ b/packages/design/README.md @@ -14,7 +14,7 @@ The components are developed using React and less, and you can find out more inf ![](./assets/design.jpeg) :::note -If you only need to extend the toolbar, context menu, and so on, you can directly use the extension interfaces provided by `@univerjs/ui` without implementing the UI yourself. For more information, please refer to [Extending UI](https://univer.ai/guides/customization/ui). +If you only need to extend the toolbar, context menu, and so on, you can directly use the extension interfaces provided by `@univerjs/ui` without implementing the UI yourself. For more information, please refer to [Extending UI](https://univer.ai/guides/sheet/customization/ui). ::: ## Usage @@ -30,3 +30,18 @@ pnpm add @univerjs/design ``` This package contains CSS and has the highest priority. Please import it before importing any other Univer style files. + +### Built-in themes + +The package provides two built-in themes: `defaultTheme` and `greenTheme`. You can import them directly and use them in your application. + +```typescript +import { defaultTheme } from '@univerjs/design'; +// import { greenTheme } from '@univerjs/design'; + +// Use the default theme +new Univer({ + theme: defaultTheme, + // theme: greenTheme, +}); +``` diff --git a/packages/design/package.json b/packages/design/package.json index 85356a2883..ea2c50f97e 100644 --- a/packages/design/package.json +++ b/packages/design/package.json @@ -1,6 +1,6 @@ { "name": "@univerjs/design", - "version": "0.1.4", + "version": "0.1.12", "private": false, "description": "UI component library for building exceptional Univer.", "author": "DreamNum ", @@ -25,7 +25,8 @@ ], "exports": { ".": "./src/index.ts", - "./*": "./src/*" + "./*": "./src/*", + "./locale/*": "./src/locale/*.ts" }, "main": "./lib/cjs/index.js", "module": "./lib/es/index.js", @@ -45,6 +46,7 @@ "require": "./lib/cjs/*", "types": "./lib/types/index.d.ts" }, + "./locale/*": "./lib/locale/*.json", "./lib/*": "./lib/*" } }, @@ -67,39 +69,45 @@ }, "peerDependencies": { "clsx": ">=2.0.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" + "dayjs": ">=1.11.0", + "react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.9.0 || ^17.0.0 || ^18.0.0" }, "dependencies": { "@rc-component/color-picker": "^1.5.3", "@rc-component/trigger": "^1.18.3", - "@univerjs/icons": "^0.1.42", - "dayjs": ">=1.11.10", + "@types/react-mentions": "^4.1.13", + "@univerjs/icons": "^0.1.52", "rc-dialog": "^9.4.0", "rc-dropdown": "^4.2.0", "rc-input": "^1.4.5", "rc-input-number": "^9.0.0", "rc-menu": "^9.13.0", - "rc-picker": "^4.3.0", + "rc-picker": "^4.5.0", "rc-segmented": "^2.3.0", - "rc-select": "^14.13.0", + "rc-select": "^14.13.3", + "rc-textarea": "^1.6.3", "rc-tooltip": "^6.2.0", - "rc-util": "^5.39.1", + "rc-util": "^5.39.3", "react-draggable": "^4.4.6", + "react-grid-layout": "^1.4.4", + "react-mentions": "^4.4.10", "react-transition-group": "^4.4.5" }, "devDependencies": { - "@testing-library/react": "^14.2.1", - "@types/react": "^18.2.72", - "@types/react-dom": "^18.2.22", + "@testing-library/react": "^14.2.2", + "@types/react": "^18.2.79", + "@types/react-dom": "^18.2.25", + "@types/react-grid-layout": "^1.3.5", "@types/react-transition-group": "^4.4.10", "@univerjs/shared": "workspace:*", - "clsx": "^2.1.0", + "clsx": "^2.1.1", + "dayjs": "^1.11.11", "less": "^4.2.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "typescript": "^5.4.3", - "vite": "^5.1.6", - "vitest": "^1.4.0" + "react": "18.2.0", + "react-dom": "18.2.0", + "typescript": "^5.4.5", + "vite": "^5.2.11", + "vitest": "^1.6.0" } } diff --git a/packages/design/src/components/button/Button.tsx b/packages/design/src/components/button/Button.tsx index 20a15b6bc6..4f8a9743e5 100644 --- a/packages/design/src/components/button/Button.tsx +++ b/packages/design/src/components/button/Button.tsx @@ -60,6 +60,8 @@ export interface IButtonProps { /** Set the handler to handle `click` event */ onClick?: (e: React.MouseEvent) => void; + + id?: string; } export function Button(props: IButtonProps) { diff --git a/packages/design/src/components/checkbox-group/CheckboxGroup.stories.tsx b/packages/design/src/components/checkbox-group/CheckboxGroup.stories.tsx index 9e1f086362..40eab16509 100644 --- a/packages/design/src/components/checkbox-group/CheckboxGroup.stories.tsx +++ b/packages/design/src/components/checkbox-group/CheckboxGroup.stories.tsx @@ -47,3 +47,20 @@ export const Playground = { ); }, }; + +export const CheckboxGroupVertical = { + render() { + const [value, setValue] = useState([]); + + function handleChange(value: Array) { + setValue(value as string[]); + } + + return ( + + test + test1 + + ); + }, +}; diff --git a/packages/design/src/components/checkbox-group/CheckboxGroup.tsx b/packages/design/src/components/checkbox-group/CheckboxGroup.tsx index 6a659ee9da..8b383e0b14 100644 --- a/packages/design/src/components/checkbox-group/CheckboxGroup.tsx +++ b/packages/design/src/components/checkbox-group/CheckboxGroup.tsx @@ -44,6 +44,12 @@ export interface ICheckboxGroupProps { */ disabled?: boolean; + /** + * Direction of the radio group + * @default 'horizontal' + */ + direction?: 'horizontal' | 'vertical'; + /** * The callback function triggered when switching options */ @@ -54,7 +60,7 @@ export interface ICheckboxGroupProps { * CheckboxGroup Component */ export function CheckboxGroup(props: ICheckboxGroupProps) { - const { children, className, style, value, disabled, onChange } = props; + const { children, className, style, value, disabled, direction = 'horizontal', onChange } = props; const handleChange = (item: string | number | boolean) => { if (value.includes(item)) { @@ -64,7 +70,9 @@ export function CheckboxGroup(props: ICheckboxGroupProps) { } }; - const _className = clsx(className, styles.checkboxGroup); + const _className = clsx(className, styles.checkboxGroup, { + [styles.checkboxGroupDirectionVertical]: direction === 'vertical', + }); return (
@@ -73,7 +81,7 @@ export function CheckboxGroup(props: ICheckboxGroupProps) { return React.cloneElement(child, { key: index, children: child.props.children, - value: child.props.value, + checked: child.props.value ? value.includes(child.props.value) : false, disabled: disabled ?? child.props.disabled, onChange: handleChange, }); diff --git a/packages/design/src/components/checkbox-group/index.module.less b/packages/design/src/components/checkbox-group/index.module.less index d78d9d8179..bd20d6a032 100644 --- a/packages/design/src/components/checkbox-group/index.module.less +++ b/packages/design/src/components/checkbox-group/index.module.less @@ -1,4 +1,10 @@ .checkbox-group { display: flex; gap: var(--margin-sm); + + &-direction { + &-vertical { + flex-direction: column; + } + } } diff --git a/packages/design/src/components/checkbox/Checkbox.stories.tsx b/packages/design/src/components/checkbox/Checkbox.stories.tsx index 2a48438c04..f14880ceb5 100644 --- a/packages/design/src/components/checkbox/Checkbox.stories.tsx +++ b/packages/design/src/components/checkbox/Checkbox.stories.tsx @@ -15,7 +15,7 @@ */ import type { Meta } from '@storybook/react'; -import React from 'react'; +import React, { useState } from 'react'; import { Checkbox } from './Checkbox'; @@ -32,7 +32,12 @@ export default meta; export const CheckboxBasic = { render() { - return checkbox 1; + const [checked, setChecked] = useState(false); + function handleChange(value: string | number | boolean) { + setChecked(value as boolean); + } + + return checkbox 1; }, }; @@ -40,10 +45,28 @@ export const CheckboxDisabled = { render() { return ( <> - + checkbox 1 - + + checkbox 2 + + + ); + }, +}; + +export const CheckboxIndeterminate = { + render() { + const [checked, setChecked] = useState(false); + function handleChange(value: string | number | boolean) { + setChecked(value as boolean); + } + + return ( + <> + checkbox 1 + checkbox 2 diff --git a/packages/design/src/components/checkbox/Checkbox.tsx b/packages/design/src/components/checkbox/Checkbox.tsx index 00487f3714..770968d9bb 100644 --- a/packages/design/src/components/checkbox/Checkbox.tsx +++ b/packages/design/src/components/checkbox/Checkbox.tsx @@ -38,6 +38,12 @@ export interface ICheckboxProps { */ checked?: boolean; + /** + * Used for setting the checkbox to indeterminate + * @default false + */ + indeterminate?: boolean; + /** * Used for setting the currently selected value * Only used when the checkbox is in a group @@ -60,7 +66,7 @@ export interface ICheckboxProps { * Checkbox Component */ export function Checkbox(props: ICheckboxProps) { - const { children, className, style, checked, value, disabled = false, onChange } = props; + const { children, className, style, checked = false, indeterminate = false, value, disabled = false, onChange } = props; const inputRef = useRef(null); @@ -79,6 +85,7 @@ export function Checkbox(props: ICheckboxProps) { const _className = clsx(className, styles.checkbox, { [styles.checkboxDisabled]: disabled, + [styles.checkboxIndeterminate]: indeterminate && !checked, }); return ( diff --git a/packages/design/src/components/checkbox/__tests__/index.spec.tsx b/packages/design/src/components/checkbox/__tests__/index.spec.tsx index 03bed94bc1..2852857571 100644 --- a/packages/design/src/components/checkbox/__tests__/index.spec.tsx +++ b/packages/design/src/components/checkbox/__tests__/index.spec.tsx @@ -15,21 +15,31 @@ */ import { fireEvent, render } from '@testing-library/react'; -import React from 'react'; +import React, { useState } from 'react'; import { describe, expect, it } from 'vitest'; import { Checkbox } from '../Checkbox'; describe('Checkbox', () => { - const component = text; - it('click Checkbox', async () => { - const { container } = render(component); + function Component() { + const [checked, setChecked] = useState(false); + + function handleChange(value: string | number | boolean) { + setChecked(value as boolean); + } + + return text; + } - fireEvent.click(container.querySelector('input')!); + const root = render(); - const $input = container.querySelector('input'); + fireEvent.click(root.container.querySelector('input')!); + + const $input = root.container.querySelector('input'); expect($input?.checked).toBe(true); + + root.unmount(); }); }); diff --git a/packages/design/src/components/checkbox/index.module.less b/packages/design/src/components/checkbox/index.module.less index eadb9137f6..172c34f358 100644 --- a/packages/design/src/components/checkbox/index.module.less +++ b/packages/design/src/components/checkbox/index.module.less @@ -49,6 +49,32 @@ border-color: rgb(var(--grey-300)); } } + + &.checkbox-indeterminate { + .checkbox-target-inner { + background-color: rgb(var(--grey-300)); + } + } + } + + &-indeterminate { + .checkbox-target { + &-inner { + background-color: rgb(var(--primary-color)); + + &::after { + top: 50%; + left: 50%; + width: calc(100% - 2px); + height: 2px; + background-color: rgb(var(--bg-color-secondary)); + border: none; + border-radius: var(--border-radius-base); + opacity: 1; + transform: translate(-50%, -50%); + } + } + } } &-target { diff --git a/packages/design/src/components/config-provider/ConfigProvider.stories.tsx b/packages/design/src/components/config-provider/ConfigProvider.stories.tsx index 3199985436..022c1a9c50 100644 --- a/packages/design/src/components/config-provider/ConfigProvider.stories.tsx +++ b/packages/design/src/components/config-provider/ConfigProvider.stories.tsx @@ -17,7 +17,9 @@ import type { Meta } from '@storybook/react'; import React, { useState } from 'react'; -import { enUS, type ILocale, zhCN } from '../../locale'; +import enUS from '../../locale/en-US'; +import zhCN from '../../locale/zh-CN'; +import type { ILocale } from '../../locale/interface'; import { Button } from '../button/Button'; import { Confirm } from '../confirm/Confirm'; import { ConfigProvider } from './ConfigProvider'; @@ -47,7 +49,7 @@ export const Playground = { <> - + setVisible(false)}> xx diff --git a/packages/design/src/components/config-provider/ConfigProvider.tsx b/packages/design/src/components/config-provider/ConfigProvider.tsx index 84330d47d3..d8aa1b0dc6 100644 --- a/packages/design/src/components/config-provider/ConfigProvider.tsx +++ b/packages/design/src/components/config-provider/ConfigProvider.tsx @@ -14,33 +14,34 @@ * limitations under the License. */ -import React, { createContext } from 'react'; +import React, { createContext, useMemo } from 'react'; import canUseDom from 'rc-util/lib/Dom/canUseDom'; -import type { ILocale } from '../../locale'; -import { enUS } from '../../locale'; +import type { ILocale } from '../../locale/interface'; export interface IConfigProviderProps { children: React.ReactNode; - locale: ILocale; + locale?: ILocale['design']; mountContainer: HTMLElement | null; } export const ConfigContext = createContext>({ - locale: enUS, mountContainer: canUseDom() ? document.body : null, }); export function ConfigProvider(props: IConfigProviderProps) { const { children, locale, mountContainer } = props; - // set default locale to enUS - let _locale: ILocale; - if (Object.prototype.hasOwnProperty.call(locale, 'design')) { - _locale = locale as ILocale; - } else { - _locale = enUS; - } + const value = useMemo(() => { + return { + locale, + mountContainer, + }; + }, [locale, mountContainer]); - return {children}; + return ( + + {children} + + ); } diff --git a/packages/design/src/components/config-provider/__tests__/index.spec.tsx b/packages/design/src/components/config-provider/__tests__/index.spec.tsx index b379e52440..a25c96b64a 100644 --- a/packages/design/src/components/config-provider/__tests__/index.spec.tsx +++ b/packages/design/src/components/config-provider/__tests__/index.spec.tsx @@ -18,14 +18,14 @@ import { render } from '@testing-library/react'; import React, { useContext } from 'react'; import { describe, expect, it } from 'vitest'; -import type { ILocale } from '../../../locale'; -import { enUS, zhCN } from '../../../locale'; +import zhCN from '../../../locale/zh-CN'; +import type { ILocale } from '../../../locale/interface'; import { ConfigContext, ConfigProvider } from '../ConfigProvider'; describe('ConfigProvider', () => { it('should render correctly', () => { let _mountContainer: HTMLElement | null = null; - let _locale: ILocale | null = null; + let _locale: ILocale['design'] | undefined; function Empty() { const { locale, mountContainer } = useContext(ConfigContext); @@ -33,16 +33,16 @@ describe('ConfigProvider', () => { _locale = locale; _mountContainer = mountContainer; - return <>; + return null; } const root = render( - + ); - expect(_locale).equals(zhCN); + expect(_locale).equals(zhCN.design); expect(_mountContainer).equals(document.body); root.unmount(); @@ -59,11 +59,11 @@ describe('ConfigProvider', () => { _mountContainer = mountContainer; - return <>; + return null; } const root = render( - + ); @@ -72,29 +72,4 @@ describe('ConfigProvider', () => { root.unmount(); }); - - it('should render correctly when locale is invalid', () => { - const mountContainer = document.createElement('div'); - document.body.appendChild(mountContainer); - - let _locale: ILocale | null = null; - - function Empty() { - const { locale } = useContext(ConfigContext); - - _locale = locale; - - return <>; - } - - const root = render( - - - - ); - - expect(_locale).equals(enUS); - - root.unmount(); - }); }); diff --git a/packages/design/src/components/confirm/Confirm.tsx b/packages/design/src/components/confirm/Confirm.tsx index fea0a10624..183865d42a 100644 --- a/packages/design/src/components/confirm/Confirm.tsx +++ b/packages/design/src/components/confirm/Confirm.tsx @@ -19,6 +19,7 @@ import React, { useContext } from 'react'; import { Button } from '../button/Button'; import { ConfigContext } from '../config-provider/ConfigProvider'; import { Dialog } from '../dialog/Dialog'; +import type { ILocale } from '../../locale/interface'; import styles from './index.module.less'; export interface IConfirmProps { @@ -56,24 +57,39 @@ export interface IConfirmProps { onConfirm?: () => void; } +function Footer(props: { locale: ILocale['design']; cancelText?: string; confirmText?: string; onClose: (() => void) | undefined; onConfirm: (() => void) | undefined }) { + const { locale, cancelText, confirmText, onClose, onConfirm } = props; + + return ( +
+ + +
+ ); +} + export function Confirm(props: IConfirmProps) { const { children, visible = false, title, cancelText, confirmText, onClose, onConfirm } = props; const { locale } = useContext(ConfigContext); - function Footer() { - return ( -
- - -
- ); - } - return ( - } onClose={onClose}> + + )} + onClose={onClose} + > {children} ); diff --git a/packages/design/src/components/date-picker/DatePanel.tsx b/packages/design/src/components/date-picker/DatePanel.tsx new file mode 100644 index 0000000000..e67fa8f19b --- /dev/null +++ b/packages/design/src/components/date-picker/DatePanel.tsx @@ -0,0 +1,38 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useContext } from 'react'; +import type { Dayjs } from 'dayjs'; +import generateConfig from 'rc-picker/lib/generate/dayjs'; +import type { BasePickerPanelProps } from 'rc-picker'; +import { PickerPanel } from 'rc-picker'; +import { ConfigContext } from '../config-provider'; +import styles from './index.module.less'; + +export type IDatePanelProps = Omit, 'prefixCls' | 'locale' | 'generateConfig'>; + +export const DatePanel = (props: IDatePanelProps) => { + const { locale } = useContext(ConfigContext); + + return ( + + ); +}; diff --git a/packages/design/src/components/date-picker/DatePicker.tsx b/packages/design/src/components/date-picker/DatePicker.tsx index e2f336d287..d983f0b084 100644 --- a/packages/design/src/components/date-picker/DatePicker.tsx +++ b/packages/design/src/components/date-picker/DatePicker.tsx @@ -15,6 +15,7 @@ */ import React, { useContext } from 'react'; +import type { PickerProps } from 'rc-picker'; import RcPicker from 'rc-picker'; import generateConfig from 'rc-picker/lib/generate/dayjs'; import { CalendarSingle } from '@univerjs/icons'; @@ -22,7 +23,7 @@ import type { Dayjs } from 'dayjs'; import { ConfigContext } from '../config-provider/ConfigProvider'; import styles from './index.module.less'; -export interface IDatePickerProps { +export interface IDatePickerProps extends Omit, 'value' | 'onChange' | 'locale' | 'generateConfig' | 'prefixCls'> { /** * The value of the date picker. */ @@ -35,7 +36,7 @@ export interface IDatePickerProps { } export function DatePicker(props: IDatePickerProps) { - const { value, onChange } = props; + const { value, onChange, ...ext } = props; const { locale } = useContext(ConfigContext); @@ -47,10 +48,11 @@ export function DatePicker(props: IDatePickerProps) { return ( + {...ext} value={value} prefixCls={styles.datePicker} generateConfig={generateConfig} - locale={locale.design.Picker} + locale={locale?.Picker!} suffixIcon={} onChange={handleChange} /> diff --git a/packages/design/src/components/date-picker/index.ts b/packages/design/src/components/date-picker/index.ts index 69bf782f2f..3e9c72240b 100644 --- a/packages/design/src/components/date-picker/index.ts +++ b/packages/design/src/components/date-picker/index.ts @@ -15,3 +15,4 @@ */ export { DatePicker, type IDatePickerProps } from './DatePicker'; +export { DatePanel, type IDatePanelProps } from './DatePanel'; diff --git a/packages/design/src/components/date-range-picker/DateRangePicker.tsx b/packages/design/src/components/date-range-picker/DateRangePicker.tsx index a4652bce1c..7060cd1407 100644 --- a/packages/design/src/components/date-range-picker/DateRangePicker.tsx +++ b/packages/design/src/components/date-range-picker/DateRangePicker.tsx @@ -40,7 +40,7 @@ export function DateRangePicker(props: IDateRangePickerProps) { const { locale } = useContext(ConfigContext); - function handleChange(date: NoUndefinedRangeValueType, dateString: [string, string]) { + function handleChange(date: NoUndefinedRangeValueType | null, dateString: [string, string]) { if (Array.isArray(date) && Array.isArray(dateString)) { onChange(date, dateString); } @@ -51,7 +51,7 @@ export function DateRangePicker(props: IDateRangePickerProps) { value={value} prefixCls={styles.dateRangePicker} generateConfig={generateConfig} - locale={locale.design.Picker} + locale={locale?.Picker!} separator={} suffixIcon={} onChange={handleChange} diff --git a/packages/design/src/components/dialog/Dialog.tsx b/packages/design/src/components/dialog/Dialog.tsx index 78debcb17b..56844255c1 100644 --- a/packages/design/src/components/dialog/Dialog.tsx +++ b/packages/design/src/components/dialog/Dialog.tsx @@ -124,8 +124,12 @@ export function Dialog(props: IDialogProps) { onMouseOut={() => { setDragDisabled(true); }} - onFocus={() => {}} - onBlur={() => {}} + onFocus={() => { + // empty + }} + onBlur={() => { + // empty + }} > {title}
diff --git a/packages/design/src/components/draggable-list/DraggableList.stories.tsx b/packages/design/src/components/draggable-list/DraggableList.stories.tsx new file mode 100644 index 0000000000..870b161f81 --- /dev/null +++ b/packages/design/src/components/draggable-list/DraggableList.stories.tsx @@ -0,0 +1,54 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Meta } from '@storybook/react'; +import React, { useState } from 'react'; + +import { DraggableList } from './DraggableList'; + +const meta: Meta = { + title: 'Components / DraggableList', + component: DraggableList, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +}; + +export default meta; + +export const DraggableListDemo = { + render() { + const [list, setList] = useState([{ title: '1', key: '1' }, { title: '11', key: '2' }]); + + return ( + ( +
+ {item.title} +
+ )} + rowHeight={32} + margin={[0, 12]} + /> + ); + }, +}; diff --git a/packages/design/src/components/draggable-list/DraggableList.tsx b/packages/design/src/components/draggable-list/DraggableList.tsx new file mode 100644 index 0000000000..e4f6cbbc36 --- /dev/null +++ b/packages/design/src/components/draggable-list/DraggableList.tsx @@ -0,0 +1,71 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useMemo } from 'react'; +import RGL, { WidthProvider } from 'react-grid-layout'; + +const ReactGridLayout = WidthProvider(RGL); + +export interface IDraggableListProps extends Omit { + list: T[]; + onListChange: (list: T[]) => void; + idKey: keyof T; + itemRender: (item: T, index: number) => React.ReactNode; +} + +export function DraggableList(props: IDraggableListProps) { + const { list, onListChange, idKey, itemRender, ...gridProps } = props; + + const listMap = useMemo(() => { + const listMap = new Map(); + list.forEach((item: T) => { + const key = item[idKey]; + listMap.set(key, item); + }); + return listMap; + }, [idKey, list]); + + const layouts: RGL.Layout[] = useMemo(() => { + return list.map((item, index) => ({ + i: item[idKey] as string, + w: 12, + h: 1, + x: 0, + y: index, + col: 12, + })); + }, [idKey, list]); + + return ( + { + const newList = layout.sort((prev, aft) => prev.y - aft.y).map((item) => listMap.get(item.i)!); + onListChange(newList); + }} + > + {layouts.map((item, index) => ( +
+ {itemRender(listMap.get(item.i)!, index)} +
+ ))} +
+ ); +} diff --git a/packages/find-replace/src/locale/index.ts b/packages/design/src/components/draggable-list/index.ts similarity index 84% rename from packages/find-replace/src/locale/index.ts rename to packages/design/src/components/draggable-list/index.ts index 1eff5fc204..b0c81ad2db 100644 --- a/packages/find-replace/src/locale/index.ts +++ b/packages/design/src/components/draggable-list/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { default as enUS } from './en-US'; -export { default as zhCN } from './zh-CN'; +export { DraggableList } from './DraggableList'; +export type { IDraggableListProps } from './DraggableList'; diff --git a/packages/design/src/components/dropdown/Dropdown.tsx b/packages/design/src/components/dropdown/Dropdown.tsx index 26642cb869..9e6db494f7 100644 --- a/packages/design/src/components/dropdown/Dropdown.tsx +++ b/packages/design/src/components/dropdown/Dropdown.tsx @@ -74,20 +74,26 @@ export interface IDropdownProps { * @param visible */ onVisibleChange?: (visible: boolean) => void; + + /** Disable dropdown from showing up. */ + disabled?: boolean; } export function Dropdown(props: IDropdownProps) { const { className, - trigger = ['click'], placement, children, overlay, alignPoint = false, align, + disabled, onVisibleChange, } = props; + // eslint-disable-next-line react/prefer-destructuring-assignment + const trigger = disabled ? [] : (props.trigger || ['click']); + const { mountContainer } = useContext(ConfigContext); return mountContainer && ( diff --git a/packages/design/src/components/form-layout/FormLayout.stories.tsx b/packages/design/src/components/form-layout/FormLayout.stories.tsx index 74f5db78b0..7ac650019d 100644 --- a/packages/design/src/components/form-layout/FormLayout.stories.tsx +++ b/packages/design/src/components/form-layout/FormLayout.stories.tsx @@ -38,8 +38,8 @@ function PagerDemo() { - - { /* empty */ }} /> ); diff --git a/packages/design/src/components/form-layout/FormLayout.tsx b/packages/design/src/components/form-layout/FormLayout.tsx index 2e269486c2..b8e70d081b 100644 --- a/packages/design/src/components/form-layout/FormLayout.tsx +++ b/packages/design/src/components/form-layout/FormLayout.tsx @@ -27,15 +27,26 @@ export interface IFormLayoutProps { style?: React.CSSProperties; className?: string; + + error?: string; } export const FormLayout = (props: IFormLayoutProps) => { - const { label, desc, children, style, className } = props; + const { label, desc, children, style, className, error } = props; return (
{label &&
{label}
} {desc &&
{desc}
} -
{children}
+
+ {children} + {error + ? ( +
+ {error} +
+ ) + : null} +
); }; diff --git a/packages/design/src/components/form-layout/index.module.less b/packages/design/src/components/form-layout/index.module.less index 5d6d2178d5..6dd58f112c 100644 --- a/packages/design/src/components/form-layout/index.module.less +++ b/packages/design/src/components/form-layout/index.module.less @@ -5,7 +5,7 @@ &-label { font-size: var(--font-size-sm); min-height: var(--font-size-sm); - color: rgba(var(--grey-400)); + color: rgba(var(--color-black)); } &-desc { @@ -27,8 +27,23 @@ } .range-selector { - width: 100%; + width: 100% !important; + } + } + + &-content-error { + .input-affix-wrapper { + border: 1px solid rgba(var(--red-400)); } + + .select-single:not(.select-customize-input) .select-selector { + border: 1px solid rgba(var(--red-400)); + } + } + + &-error { + color: rgba(var(--red-400)); + font-size: var(--font-size-sm); } } diff --git a/packages/design/src/components/input-number/index.module.less b/packages/design/src/components/input-number/index.module.less index d918ba437d..bce28c90e9 100644 --- a/packages/design/src/components/input-number/index.module.less +++ b/packages/design/src/components/input-number/index.module.less @@ -1,6 +1,8 @@ .input-number { display: inline-block; + // Should not exceed its parent's height. + max-height: 100%; height: 34px; margin: 0; padding: 0; diff --git a/packages/design/src/components/input/Input.stories.tsx b/packages/design/src/components/input/Input.stories.tsx index 05a11c6913..da393c18cc 100644 --- a/packages/design/src/components/input/Input.stories.tsx +++ b/packages/design/src/components/input/Input.stories.tsx @@ -45,7 +45,6 @@ export const InputSize = { render() { return ( <> - diff --git a/packages/design/src/components/input/Input.tsx b/packages/design/src/components/input/Input.tsx index a4e20fdd64..379221942e 100644 --- a/packages/design/src/components/input/Input.tsx +++ b/packages/design/src/components/input/Input.tsx @@ -59,7 +59,7 @@ export interface IInputProps extends Pick { * The input size * @default middle */ - size?: 'mini' | 'small' | 'middle' | 'large'; + size?: 'small' | 'middle' | 'large'; /** * Whether the input is clearable @@ -90,6 +90,7 @@ export interface IInputProps extends Pick { * @param value */ onChange?: (value: string) => void; + } export function Input(props: IInputProps) { @@ -115,11 +116,10 @@ export function Input(props: IInputProps) { } const _className = clsx(className, { - [styles.inputAffixWrapperMini]: size === 'mini', [styles.inputAffixWrapperSmall]: size === 'small', [styles.inputAffixWrapperMiddle]: size === 'middle', [styles.inputAffixWrapperLarge]: size === 'large', - }); + }, className); return ( = { + title: 'Components / Mentions', + component: Mentions, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +}; + +export default meta; + +export const InputBasic = { + + render() { + const [value, onChange] = useState(''); + + return ( +
+ onChange(e.target.value)} + > + + +
+ + ); + }, +}; + diff --git a/packages/design/src/components/mentions/Mentions.tsx b/packages/design/src/components/mentions/Mentions.tsx new file mode 100644 index 0000000000..a3ae171f08 --- /dev/null +++ b/packages/design/src/components/mentions/Mentions.tsx @@ -0,0 +1,33 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { forwardRef } from 'react'; +import type { MentionsInputProps } from 'react-mentions'; +import { MentionsInput } from 'react-mentions'; +import styles from './index.module.less'; + +export interface IMentionsProps extends MentionsInputProps {} + +export const Mentions = forwardRef, IMentionsProps>((props, ref) => { + return ( + + ); +}); + diff --git a/packages/design/src/components/mentions/index.module.less b/packages/design/src/components/mentions/index.module.less new file mode 100644 index 0000000000..9a85dbadb3 --- /dev/null +++ b/packages/design/src/components/mentions/index.module.less @@ -0,0 +1,71 @@ +.mentions { + width: 100%; +} + +.mentions__control { + min-height: 32px; +} + +.mentions__highlighter { + border-radius: 6px; + background: rgba(var(--color-white)); + padding: 6px 10px; + font-size: 13px !important; + line-height: 20px !important; + max-height: 114px; + + strong { + color: rgb(var(--blue-500)); + } +} + +.mentions__highlighter__substring { + visibility: inherit !important; + color: rgb(var(--color-black)); +} + +.mentions__input { + width: 100%; + caret-color: red; + background-color: transparent; + color: transparent; + padding: 6px 10px; + border: 1px solid rgb(var(--border-color)); + border-radius: 6px; + font-size: 13px !important; + line-height: 20px !important; + max-height: 114px; +} + +.mentions__input:focus { + border: 1px solid rgb(var(--blue-500)); + outline: none !important; +} + +.mentions__suggestions { + border-radius: 8px; + overflow: hidden; + background: rgb(var(--color-white)) !important; + border: 1px solid rgb(var(--grey-200)) !important; + box-shadow: var(--box-shadow-base) !important; + width: 100%; + box-sizing: border-box; + margin-top: 20px !important; +} + +.mentions__suggestions__list { + display: flex; + flex-direction: column; + padding: 8px !important; + width: 100%; + box-sizing: border-box; +} + +.mentions__suggestions__item { + padding: 4px 8px; + border-radius: 6px; +} + +.mentions__suggestions__item--focused { + background-color: rgb(var(--grey-50)); +} diff --git a/packages/core/src/basics/index.ts b/packages/design/src/components/mentions/index.ts similarity index 80% rename from packages/core/src/basics/index.ts rename to packages/design/src/components/mentions/index.ts index 2eec29a5d5..fd9b5794db 100644 --- a/packages/core/src/basics/index.ts +++ b/packages/design/src/components/mentions/index.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -export * from './registry'; -export * from './univer'; -export * from './univer-doc'; -export * from './univer-sheet'; -export * from './univer-slide'; +export { Mentions } from './Mentions'; +export type { IMentionsProps } from './Mentions'; +export { Mention, type MentionProps } from 'react-mentions'; diff --git a/packages/design/src/components/menu/Menu.tsx b/packages/design/src/components/menu/Menu.tsx index e0ba904c37..e708d4ac69 100644 --- a/packages/design/src/components/menu/Menu.tsx +++ b/packages/design/src/components/menu/Menu.tsx @@ -14,20 +14,19 @@ * limitations under the License. */ -import type { MenuItemGroupProps, MenuItemProps, MenuProps, SubMenuProps } from 'rc-menu'; +import type { MenuItemGroupProps, MenuItemProps, MenuProps, MenuRef, SubMenuProps } from 'rc-menu'; import RcMenu, { MenuItem as RcMenuItem, MenuItemGroup as RcMenuItemGroup, SubMenu as RcSubMenu } from 'rc-menu'; import React, { useContext } from 'react'; import { ConfigContext } from '../config-provider/ConfigProvider'; import styles from './index.module.less'; -export function Menu(props: MenuProps) { +export const Menu = React.forwardRef((props, ref) => { const { mountContainer } = useContext(ConfigContext); - - return mountContainer && React.cloneElement( mountContainer} />, { + return mountContainer && React.cloneElement( mountContainer} />, { ...props, }); -} +}); export function MenuItem(props: MenuItemProps) { return React.cloneElement(, { ...props }); diff --git a/packages/design/src/components/menu/index.module.less b/packages/design/src/components/menu/index.module.less index b44ce2b807..cee9cd09a5 100644 --- a/packages/design/src/components/menu/index.module.less +++ b/packages/design/src/components/menu/index.module.less @@ -94,6 +94,7 @@ &.menu-item-disabled, &.menu-submenu-disabled { color: rgb(var(--grey-200)) !important; + cursor: not-allowed; } } diff --git a/packages/design/src/components/menu/index.ts b/packages/design/src/components/menu/index.ts index 31c08e20bc..cd8bad9b56 100644 --- a/packages/design/src/components/menu/index.ts +++ b/packages/design/src/components/menu/index.ts @@ -15,3 +15,4 @@ */ export { Menu, MenuItem, MenuItemGroup, SubMenu } from './Menu'; +export type { MenuRef } from 'rc-menu'; diff --git a/packages/design/src/components/message/Message.stories.tsx b/packages/design/src/components/message/Message.stories.tsx index f13bfa1a8c..d24b78054a 100644 --- a/packages/design/src/components/message/Message.stories.tsx +++ b/packages/design/src/components/message/Message.stories.tsx @@ -46,6 +46,9 @@ export const Playground = { + diff --git a/packages/design/src/components/message/Message.tsx b/packages/design/src/components/message/Message.tsx index f1b5204622..60ee2dee27 100644 --- a/packages/design/src/components/message/Message.tsx +++ b/packages/design/src/components/message/Message.tsx @@ -14,15 +14,19 @@ * limitations under the License. */ +/* eslint-disable react-refresh/only-export-components */ + import { ErrorSingle, SuccessSingle, WarningSingle } from '@univerjs/icons'; import { render } from 'rc-util/lib/React/render'; import React from 'react'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; +import type { IDisposable } from '../../type'; import styles from './index.module.less'; export enum MessageType { Success = 'success', + Info = 'info', Warning = 'warning', Error = 'error', } @@ -40,6 +44,7 @@ export interface IMessageMethodOptions { const iconMap = { [MessageType.Success]: , + [MessageType.Info]: , [MessageType.Warning]: , [MessageType.Error]: , }; @@ -83,17 +88,18 @@ const MessageContainer = (props: { messages: IMessageProps[] }) => { }; export class Message { - private _div: HTMLDivElement; - private _messages: IMessageProps[] = []; + protected _container: HTMLDivElement; + + protected _messages: IMessageProps[] = []; constructor(container: HTMLElement) { - this._div = document.createElement('div'); - container.appendChild(this._div); + this._container = document.createElement('div'); + container.appendChild(this._container); this.render(); } - append(type: MessageType, options: IMessageMethodOptions) { + append(type: MessageType, options: IMessageMethodOptions): IDisposable { const { content, delay = 3000 } = options; const key = Date.now(); @@ -103,11 +109,10 @@ export class Message { content, }); - setTimeout(() => { - this.teardown(key); - }, delay); - this.render(); + + setTimeout(() => this.teardown(key), delay); + return { dispose: () => this.teardown(key) }; } teardown(key: number) { @@ -117,18 +122,22 @@ export class Message { } render() { - render(, this._div); + render(, this._container); + } + + success(options: IMessageMethodOptions): IDisposable { + return this.append(MessageType.Success, options); } - success(options: IMessageMethodOptions) { - this.append(MessageType.Success, options); + info(options: IMessageMethodOptions): IDisposable { + return this.append(MessageType.Info, options); } - warning(options: IMessageMethodOptions) { - this.append(MessageType.Warning, options); + warning(options: IMessageMethodOptions): IDisposable { + return this.append(MessageType.Warning, options); } - error(options: IMessageMethodOptions) { - this.append(MessageType.Error, options); + error(options: IMessageMethodOptions): IDisposable { + return this.append(MessageType.Error, options); } } diff --git a/packages/design/src/components/message/index.module.less b/packages/design/src/components/message/index.module.less index ffbfd87432..49496f2b5e 100644 --- a/packages/design/src/components/message/index.module.less +++ b/packages/design/src/components/message/index.module.less @@ -44,6 +44,10 @@ color: rgb(var(--success-color)); } + &-info { + color: rgb(var(--info-color)); + } + &-warning { color: rgb(var(--warning-color)); } diff --git a/packages/design/src/components/popup/Popup.tsx b/packages/design/src/components/popup/Popup.tsx index 3887fdf34c..9caf386852 100644 --- a/packages/design/src/components/popup/Popup.tsx +++ b/packages/design/src/components/popup/Popup.tsx @@ -14,9 +14,11 @@ * limitations under the License. */ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useContext, useEffect, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { CSSTransition } from 'react-transition-group'; +import { ConfigContext } from '../config-provider/ConfigProvider'; import styles from './index.module.less'; export interface IPopupProps { @@ -42,6 +44,8 @@ export function Popup(props: IPopupProps) { const [realOffset, setRealOffset] = useState<[number, number]>(offset); + const { mountContainer } = useContext(ConfigContext); + useEffect(() => { if (!visible) { setRealOffset([-9999, -9999]); @@ -62,7 +66,7 @@ export function Popup(props: IPopupProps) { event.preventDefault(); } - return ( + return createPortal( {children} - + , + mountContainer! ); } diff --git a/packages/design/src/components/popup/RectPopup.tsx b/packages/design/src/components/popup/RectPopup.tsx index e2a8d83a75..5311833af3 100644 --- a/packages/design/src/components/popup/RectPopup.tsx +++ b/packages/design/src/components/popup/RectPopup.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import React, { useEffect, useRef, useState } from 'react'; +import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { useEvent } from 'rc-util'; import styles from './index.module.less'; interface IAbsolutePosition { @@ -24,6 +25,8 @@ interface IAbsolutePosition { bottom: number; } +const RectPopupContext = createContext({ top: 0, bottom: 0, left: 0, right: 0 }); + export interface IRectPopupProps { children?: React.ReactNode; @@ -32,74 +35,97 @@ export interface IRectPopupProps { */ anchorRect: IAbsolutePosition; - direction?: 'horizontal' | 'vertical'; + direction?: 'vertical' | 'horizontal' | 'left' | 'top' | 'right' | 'left' | 'bottom'; + + // #region closing behavior + closeOnSelfTarget?: boolean; onClickOutside?: (e: MouseEvent) => void; + excludeOutside?: HTMLElement[]; + + // #endregion } -export interface IPopupLayoutInfo { +export interface IPopupLayoutInfo extends Pick { position: IAbsolutePosition; width: number; height: number; containerWidth: number; containerHeight: number; - direction?: 'horizontal' | 'vertical'; } -const calcPopupPosition = (layout: IPopupLayoutInfo) => { - const { position, width, height, containerHeight, containerWidth, direction = 'vertical' } = layout; - if (direction === 'vertical') { - const { left: startX, top: startY, right: endX, bottom: endY } = position; +/** The popup should have a minimum edge to the boundary. */ +const PUSHING_MINIMUM_GAP = 4; - const verticalStyle = (endY + height) > containerHeight ? { top: startY - height } : { top: endY }; - const horizontalStyle = (startX + width) > containerWidth ? { left: endX - width } : { left: startX }; +function calcPopupPosition(layout: IPopupLayoutInfo): { top: number; left: number } { + const { position, width, height, containerHeight, containerWidth, direction = 'vertical' } = layout; - return { - ...verticalStyle, - ...horizontalStyle, - }; - } else { + // In y-axis + if (direction === 'vertical' || direction === 'top' || direction === 'bottom') { const { left: startX, top: startY, right: endX, bottom: endY } = position; + const verticalStyle = ((endY + height) > containerHeight || direction === 'top') + ? { top: startY - height } + : { top: endY }; - const verticalStyle = ((startY + height) > containerHeight) ? { top: endY - height } : { top: startY }; - const horizontalStyle = ((endX + width) > containerWidth) ? { left: startX - width } : { left: endX }; + // If the popup element exceed the visible area. We should "push" it back. + const horizontalStyle = (startX + width) > containerWidth + ? { left: Math.max(endX - width, PUSHING_MINIMUM_GAP) } // on left + : { left: Math.min(startX, containerWidth - width - PUSHING_MINIMUM_GAP) }; // on right - return { - ...verticalStyle, - ...horizontalStyle, - }; + return { ...verticalStyle, ...horizontalStyle }; } + + // In x-axis + const { left: startX, top: startY, right: endX, bottom: endY } = position; + const horizontalStyle = ((endX + width) > containerWidth || direction === 'left') + ? { left: startX - width } + : { left: endX }; + + // If the popup element exceed the visible area. We should "push" it back. + const verticalStyle = ((startY + height) > containerHeight) + ? { top: Math.max(endY - height, PUSHING_MINIMUM_GAP) } // on top + : { top: Math.min(startY, containerHeight - height - PUSHING_MINIMUM_GAP) }; // on bottom + + return { ...verticalStyle, ...horizontalStyle }; }; function RectPopup(props: IRectPopupProps) { - const { children, anchorRect, direction = 'vertical', onClickOutside } = props; - const mounted = useRef(false); - const nodeRef = useRef(null); - const clickOtherFn = useRef(onClickOutside); - - clickOtherFn.current = onClickOutside; + const { children, anchorRect, direction = 'vertical', onClickOutside, excludeOutside } = props; + const nodeRef = useRef(null); + const clickOtherFn = useEvent(onClickOutside ?? (() => { /* empty */ })); const [position, setPosition] = useState>({ top: -9999, left: -9999, }); + const style = useMemo(() => ({ ...position }), [position]); useEffect(() => { - const { clientWidth, clientHeight } = nodeRef.current!; - const { innerWidth, innerHeight } = window; - setPosition( - calcPopupPosition( - { - position: anchorRect, - width: clientWidth, - height: clientHeight, - containerWidth: innerWidth, - containerHeight: innerHeight, - direction, - } - ) - ); + requestAnimationFrame(() => { + if (!nodeRef.current) { + return; + } + const { clientWidth, clientHeight } = nodeRef.current; + const parent = nodeRef.current.parentElement; + if (!parent) { + return; + } + const { clientWidth: innerWidth, clientHeight: innerHeight } = parent; + + setPosition( + calcPopupPosition( + { + position: anchorRect, + width: clientWidth, + height: clientHeight, + containerWidth: innerWidth, + containerHeight: innerHeight, + direction, + } + ) + ); + }); }, - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps [ anchorRect.left, anchorRect.top, @@ -109,35 +135,50 @@ function RectPopup(props: IRectPopupProps) { ]); useEffect(() => { - mounted.current = true; - const handleClick = (e: MouseEvent) => { - clickOtherFn.current?.(e); - }; - setTimeout(() => { - if (mounted.current) { - window.addEventListener('click', handleClick); + const handleClickOther = (e: MouseEvent) => { + if ( + excludeOutside && + ( + (excludeOutside.indexOf(e.target as any) > -1) || + excludeOutside.some((item) => item.contains(e.target as any) + ) + ) + ) { + return; } - }, 100); + const x = e.offsetX; + const y = e.offsetY; + if (x <= anchorRect.right && x >= anchorRect.left && y <= anchorRect.bottom && y >= anchorRect.top) { + return; + } + clickOtherFn(e); + }; + + window.addEventListener('click', handleClickOther); + return () => { - mounted.current = false; - window.removeEventListener('click', handleClick); + window.removeEventListener('click', handleClickOther); }; - }, [clickOtherFn]); + }, [anchorRect, anchorRect.bottom, anchorRect.left, anchorRect.right, anchorRect.top, clickOtherFn, excludeOutside]); return (
{ e.stopPropagation(); }} > - {children} + + {children} +
); } RectPopup.calcPopupPosition = calcPopupPosition; +RectPopup.useContext = () => useContext(RectPopupContext); + export { RectPopup }; diff --git a/packages/design/src/components/popup/index.module.less b/packages/design/src/components/popup/index.module.less index 2a3442a92c..14af4e449c 100644 --- a/packages/design/src/components/popup/index.module.less +++ b/packages/design/src/components/popup/index.module.less @@ -1,8 +1,19 @@ +.popup-absolute { + position: absolute; + z-index: 100; + top: -9999px; + left: -9999px; +} + .popup { position: fixed; z-index: 1070; top: -9999px; left: -9999px; + background-color: rgb(var(--bg-color-secondary)); + border-radius: 6px; + overflow: hidden; + box-shadow: var(--box-shadow-base); &-enter { .effect(); diff --git a/packages/design/src/components/radio-group/RadioGroup.stories.tsx b/packages/design/src/components/radio-group/RadioGroup.stories.tsx index ac7df2c73c..fd546b5f00 100644 --- a/packages/design/src/components/radio-group/RadioGroup.stories.tsx +++ b/packages/design/src/components/radio-group/RadioGroup.stories.tsx @@ -47,3 +47,20 @@ export const Playground = { ); }, }; + +export const RadioGroupVertical = { + render() { + const [value, setValue] = useState(''); + + function handleChange(value: string | number | boolean) { + setValue(value as string); + } + + return ( + + test + test1 + + ); + }, +}; diff --git a/packages/design/src/components/radio-group/RadioGroup.tsx b/packages/design/src/components/radio-group/RadioGroup.tsx index 47660efdd7..8889a68157 100644 --- a/packages/design/src/components/radio-group/RadioGroup.tsx +++ b/packages/design/src/components/radio-group/RadioGroup.tsx @@ -16,12 +16,23 @@ import React from 'react'; +import clsx from 'clsx'; import type { IRadioProps } from '../radio/Radio'; import styles from './index.module.less'; export interface IRadioGroupProps { children: React.ReactNode[]; + /** + * The class name of the checkbox group + */ + className?: string; + + /** + * The style of the checkbox group + */ + style?: React.CSSProperties; + /** * Define which radio is selected */ @@ -33,6 +44,12 @@ export interface IRadioGroupProps { */ disabled?: boolean; + /** + * Direction of the radio group + * @default 'horizontal' + */ + direction?: 'horizontal' | 'vertical'; + /** * The callback function triggered when switching options */ @@ -43,14 +60,18 @@ export interface IRadioGroupProps { * RadioGroup Component */ export function RadioGroup(props: IRadioGroupProps) { - const { children, value, disabled = false, onChange } = props; + const { children, className, style, value, disabled = false, direction = 'horizontal', onChange } = props; const handleChange = (value: string | number | boolean) => { onChange(value); }; + const _className = clsx(className, styles.radioGroup, { + [styles.radioGroupDirectionVertical]: direction === 'vertical', + }); + return ( -
+
{React.Children.map(children, (child, index) => { if (React.isValidElement(child)) { return React.cloneElement(child, { diff --git a/packages/design/src/components/radio-group/index.module.less b/packages/design/src/components/radio-group/index.module.less index 3652f85857..d9ba546637 100644 --- a/packages/design/src/components/radio-group/index.module.less +++ b/packages/design/src/components/radio-group/index.module.less @@ -1,4 +1,10 @@ .radio-group { display: flex; gap: var(--margin-sm); + + &-direction { + &-vertical { + flex-direction: column; + } + } } diff --git a/packages/design/src/components/scrollbar/Scrollbar.stories.tsx b/packages/design/src/components/scrollbar/Scrollbar.stories.tsx index c0dbd006bd..5f8b560ace 100644 --- a/packages/design/src/components/scrollbar/Scrollbar.stories.tsx +++ b/packages/design/src/components/scrollbar/Scrollbar.stories.tsx @@ -34,7 +34,7 @@ export const ScrollbarBasic: StoryObj = { render() { return (
-
+
top
diff --git a/packages/design/src/components/scrollbar/Scrollbar.tsx b/packages/design/src/components/scrollbar/Scrollbar.tsx index 414f2edbd8..d09ccb77f1 100644 --- a/packages/design/src/components/scrollbar/Scrollbar.tsx +++ b/packages/design/src/components/scrollbar/Scrollbar.tsx @@ -88,30 +88,23 @@ export function Scrollbar(props: IScrollbarProps) { const [thumbHeight, setThumbHeight] = useState(0); const [thumbTop, setThumbTop] = useState(0); useEffect(() => { - const observer = new ResizeObserver(() => { - let timer: number | undefined = requestIdleCallback(() => { - if (!timer) return; + const { height: containerHeight } = containerRef.current!.parentElement!.getBoundingClientRect(); - const { height: containerHeight } = containerRef.current!.getBoundingClientRect(); + const { scrollHeight } = contentRef.current!; + setThumbHeight((containerHeight / scrollHeight) * 100); + containerRef.current!.style.height = `${containerHeight}px`; - const { scrollHeight } = contentRef.current!; - setThumbHeight((containerHeight / scrollHeight) * 100); - - timer = undefined; - }); - }); - - observer.observe(document.body); - - // handle scroll event - contentRef.current!.addEventListener('scroll', (e) => { + function handleScroll(e: Event) { const { scrollTop, scrollHeight } = e.target as HTMLDivElement; const thumbTop = (scrollTop / scrollHeight) * 100; setThumbTop(thumbTop); - }); + } + + // handle scroll event + contentRef.current!.addEventListener('scroll', handleScroll); return () => { - observer.unobserve(document.body); + contentRef.current?.removeEventListener('scroll', handleScroll); }; }, []); diff --git a/packages/design/src/components/scrollbar/index.module.less b/packages/design/src/components/scrollbar/index.module.less index dbecb5eae2..2718324ec9 100644 --- a/packages/design/src/components/scrollbar/index.module.less +++ b/packages/design/src/components/scrollbar/index.module.less @@ -23,7 +23,7 @@ &-thumb { position: absolute; - width: 100%; + width: calc(100% - 2px); background-color: rgb(var(--scrollbar-color)); border-radius: var(--border-radius-lg); diff --git a/packages/design/src/components/segmented/index.module.less b/packages/design/src/components/segmented/index.module.less index c6ebe63819..82d82d0f4d 100644 --- a/packages/design/src/components/segmented/index.module.less +++ b/packages/design/src/components/segmented/index.module.less @@ -21,6 +21,7 @@ width: 100%; border-radius: 6px; background-color: rgb(var(--grey-50)); + box-sizing: border-box; &-group { position: relative; @@ -34,7 +35,7 @@ &-item { position: relative; - min-height: 28px; + height: 100%; padding: 4px 10px; border-radius: 4px; height: 100%; @@ -66,7 +67,7 @@ &-label { z-index: 2; - line-height: 24px; + line-height: 20px; } &-input { diff --git a/packages/design/src/components/select-list/SelectList.stories.tsx b/packages/design/src/components/select-list/SelectList.stories.tsx index 8cb55c5772..b832bc3451 100644 --- a/packages/design/src/components/select-list/SelectList.stories.tsx +++ b/packages/design/src/components/select-list/SelectList.stories.tsx @@ -48,7 +48,7 @@ export const Playground = { { label: 'Option 11', value: 'option11' }, ]; - function handleChange(value: string | number | boolean) { + function handleChange(value: string | string[] | undefined) { setValue(value as string); } diff --git a/packages/design/src/components/select-list/SelectList.tsx b/packages/design/src/components/select-list/SelectList.tsx index 6ecc370791..be45c53691 100644 --- a/packages/design/src/components/select-list/SelectList.tsx +++ b/packages/design/src/components/select-list/SelectList.tsx @@ -24,7 +24,7 @@ export interface ISelectListProps { /** * The value of select */ - value: string; + value: string | string[]; /** * The options of select @@ -45,33 +45,54 @@ export interface ISelectListProps { /** * The callback function that is triggered when the value is changed */ - onChange: (value: string) => void; + onChange: (value: string | string[] | undefined) => void; + + multiple?: boolean; } export function SelectList(props: ISelectListProps) { - const { value, options = [], hideCheckMark = false, onChange } = props; + const { value: _value, options = [], hideCheckMark = false, onChange, multiple } = props; + + const value = Array.isArray(_value) ? _value : [_value]; + + function handleSelect(newValue: string) { + const index = value.indexOf(newValue); - function handleSelect(value: string) { - onChange(value); + if (!multiple) { + if (index > -1) { + onChange(undefined); + } else { + onChange(newValue); + } + } else { + if (index > -1) { + onChange(value.filter((i) => i === newValue)); + } else { + onChange([...value, newValue]); + } + } } return ( ); } diff --git a/packages/design/src/components/select/Select.stories.tsx b/packages/design/src/components/select/Select.stories.tsx index 0534432ac3..0bc4177ba1 100644 --- a/packages/design/src/components/select/Select.stories.tsx +++ b/packages/design/src/components/select/Select.stories.tsx @@ -48,6 +48,24 @@ export const SelectBasic = { }, }; +export const SelectBorderless = { + render() { + const [value, setValue] = useState(''); + + const options = [ + { label: 'Option 1', value: 'option1' }, + { label: 'Option 2', value: 'option2' }, + { label: 'Option 3', value: 'option3' }, + ]; + + function handleChange(value: string | number | boolean) { + setValue(value as string); + } + + return