diff --git a/.github/ISSUE_TEMPLATE/feature_template.md b/.github/ISSUE_TEMPLATE/feature_template.md new file mode 100644 index 000000000000..a41ca627fd9b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_template.md @@ -0,0 +1,27 @@ +# Feature Title: + +Example: Delete multiple rows. + +## Feature description: + +[Please detail the feature suggestion and explain why it's needed] + +Example: Select and delete multiple rows in a table. Since is possible to select multiple rows, it's a benefit to the user to add a table operation to delete multiple rows. +Then there is no need to delete each row at a time. + +## Expected behavior: + +[Describe the feature behavior!] + +Example: + +- Insert a table +- Select multiple rows +- Click delete +- All the selected rows are deleted + +## Suggest how this feature can be developed + +[If it's possible, describe with technical details how this feature can be developed] + +Example: Adapt the Delete Row operation to recognize the selection of cells in the VTable class and edit api. diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index ae3110799dd0..1bdf7c1b6226 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -15,10 +15,10 @@ jobs: - name: Set Node Version uses: actions/setup-node@v2 with: - node-version: 'v14.18.2' + node-version: 'v18.16.0' - name: Install dependencies - run: npm install + run: yarn - name: Build run: npm run-script build:ci @@ -37,6 +37,3 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: gh-pages FOLDER: dist/deploy - - - name: Publish - run: node tools/build.js publish --token ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 34967d99707a..714cc6bd7903 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,11 +1,5 @@ name: Build and Test -on: - push: - branches-ignore: - - master - pull_request: - branches-ignore: - - master +on: [push, pull_request] jobs: build: @@ -19,10 +13,10 @@ jobs: - name: Set Node Version uses: actions/setup-node@v2 with: - node-version: 'v14.18.2' + node-version: 'v18.16.0' - name: Install dependencies - run: npm install + run: yarn - name: Build run: npm run-script build:ci @@ -37,10 +31,10 @@ jobs: - name: Set Node Version uses: actions/setup-node@v2 with: - node-version: 'v14.18.2' + node-version: 'v18.16.0' - name: Install dependencies - run: npm install + run: yarn - name: Install Chrome uses: browser-actions/setup-chrome@latest @@ -48,7 +42,7 @@ jobs: - name: Test with Chrome uses: GabrielBB/xvfb-action@v1 with: - run: npm run-script test:chrome + run: npm run-script test test-on-firefox: runs-on: ubuntu-latest steps: @@ -60,10 +54,10 @@ jobs: - name: Set Node Version uses: actions/setup-node@v2 with: - node-version: 'v14.18.2' + node-version: 'v18.16.0' - name: Install dependencies - run: npm install + run: yarn - name: Install Firefox uses: browser-actions/setup-firefox@latest @@ -71,4 +65,4 @@ jobs: - name: Test with Firefox uses: GabrielBB/xvfb-action@v1 with: - run: npm run-script test + run: npm run-script test:firefox diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000000..f0585b3f86a8 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,28 @@ +name: Publish +on: + push: + branches: + - release +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2.3.1 + with: + ref: release + persist-credentials: false + + - name: Set Node Version + uses: actions/setup-node@v2 + with: + node-version: 'v18.16.0' + + - name: Install dependencies + run: yarn + + - name: Build + run: npm run-script build:ci + + - name: Publish + run: node tools/build.js publish --token ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 0b675dfbd62a..bec590a84902 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,8 @@ node_modules/ # Distribution dist/ -#Mac files -.DS_Store \ No newline at end of file +# Mac files +.DS_Store + +# Temp files +packages/roosterjs-editor-types/lib/compatibleEnum/ diff --git a/.vscode/launch.json b/.vscode/launch.json index 083e93f229d7..297b6d99801d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -33,6 +33,44 @@ "args": ["start", "--chrome", "--components", "${fileBasenameNoExtension}"], "console": "integratedTerminal" }, + { + "type": "node", + "request": "launch", + "name": "Debug All Unit Tests", + "program": "${workspaceFolder}/node_modules/karma/bin/karma", + "args": ["start", "--no-single-run"], + "console": "integratedTerminal" + }, + { + "type": "node", + "request": "launch", + "name": "Debug All Unit Tests (Chrome)", + "program": "${workspaceFolder}/node_modules/karma/bin/karma", + "args": ["start", "--chrome", "--no-single-run"], + "console": "integratedTerminal" + }, + { + "type": "node", + "request": "launch", + "name": "Debug current unit test file", + "program": "${workspaceFolder}/node_modules/karma/bin/karma", + "args": ["start", "--components", "${fileBasenameNoExtension}", "--no-single-run"], + "console": "integratedTerminal" + }, + { + "type": "node", + "request": "launch", + "name": "Debug current unit test file (Chrome)", + "program": "${workspaceFolder}/node_modules/karma/bin/karma", + "args": [ + "start", + "--chrome", + "--components", + "${fileBasenameNoExtension}", + "--no-single-run" + ], + "console": "integratedTerminal" + }, { "type": "chrome", "request": "launch", diff --git a/.vscode/settings.json b/.vscode/settings.json index 09b6d60d5e13..fe47b63b3444 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -40,6 +40,7 @@ "datetime", "dompurify", "endregion", + "fluentui", "Hilite", "inputevent", "KHTML", @@ -72,6 +73,7 @@ "textinput", "Toggleable", "toposort", + "unlocalized", "usemap", "valign", "vlist", diff --git a/README.md b/README.md index 2004f2b3d01c..f80f1f642066 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Please see [here](https://github.com/microsoft/roosterjs/wiki/RoosterJs-8). ### Packages -Rooster contains 6 packages. +Rooster contains 6 basic packages. 1. [roosterjs](https://microsoft.github.io/roosterjs/docs/modules/roosterjs.html): A facade of all Rooster code for those who want a quick start. Use the @@ -44,6 +44,19 @@ Rooster contains 6 packages. 6. [roosterjs-editor-types](https://microsoft.github.io/roosterjs/docs/modules/roosterjs_editor_types.html): Defines public interfaces and enumerations. +There are also some extension packages to provide additional functionalities. + +1. [roosterjs-color-utils](https://microsoft.github.io/roosterjs/docs/modules/roosterjs_color_utils.html): + Provide color transformation utility to make editor work under dark mode. + +2. [roosterjs-react](https://microsoft.github.io/roosterjs/docs/modules/roosterjs_react.html): + Provide a React wrapper of roosterjs so it can be easily used with React. + +3. [roosterjs-editor-types-compatible](https://microsoft.github.io/roosterjs/docs/modules/roosterjs_editor_types_compatible.html): + Provide types that are compatible with isolatedModules mode. When using isolatedModules mode, + "const enum" will not work correctly, this package provides enums with prefix "Compatible" in + their names and they have the same value with const enums in roosterjs-editor-types package + ### APIs Rooster provides DOM level APIs (in `roosterjs-editor-dom`), core APIs (in `roosterjs-editor-core`), and formatting APIs @@ -99,10 +112,6 @@ Install via NPM or Yarn: `yarn add roosterjs` -or - -`npm install roosterjs --save` - You can also install sub packages separately: `yarn add roosterjs-editor-core` @@ -111,22 +120,10 @@ You can also install sub packages separately: `...` -or - -`npm install roosterjs-editor-core --save` - -`npm install roosterjs-editor-api --save` - -`...` - In order to run the code below, you may also need to install [webpack](https://webpack.js.org/): `yarn add webpack -g` -or - -`npm install webpack -g` - ## Usage ### A quick start @@ -219,12 +216,6 @@ To build the sample site code yourself, follow these instructions: yarn ``` - or - - ```cmd - npm install - ``` - 2. Build the source code, and start the sample editor: ``` @@ -298,7 +289,7 @@ Currently we have very few external dependencies. Before adding any new dependen A dependency package under MIT license is good to be used for RoosterJs. For other licenses, we need to review and see if we can take it as a dependency. -If you still feel a new dependency is required after checking these 3 questions, we can review it and +If you still feel a new dependency is required after checking these questions, we can review it and finally decide whether we should add the new dependency. For build time dependencies, it is more flexable to add new dependencies since it won't increase runtime diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000000..cfd2fcf50ce3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/msrc/pgp-key-msrc). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + +- Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) +- Full paths of source file(s) related to the manifestation of the issue +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- Step-by-step instructions to reproduce the issue +- Proof-of-concept or exploit code (if possible) +- Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/msrc/cvd). + + diff --git a/assets/design-charts/BackwardDeleteWord.png b/assets/design-charts/BackwardDeleteWord.png new file mode 100644 index 000000000000..8b9d18d9ac48 Binary files /dev/null and b/assets/design-charts/BackwardDeleteWord.png differ diff --git a/assets/design-charts/ForwardDeleteWord.png b/assets/design-charts/ForwardDeleteWord.png new file mode 100644 index 000000000000..4654dd0a1501 Binary files /dev/null and b/assets/design-charts/ForwardDeleteWord.png differ diff --git a/demo/index.html b/demo/index.html index 836a16ea27fb..f655db7f2d44 100644 --- a/demo/index.html +++ b/demo/index.html @@ -1,7 +1,7 @@ - RoosterJs Demo Page + RoosterJs Demo Site - - - - - - - - - - diff --git a/demo/scripts/controls/svg/alignleft.svg b/demo/scripts/controls/svg/alignleft.svg deleted file mode 100644 index 72c022d51f2a..000000000000 --- a/demo/scripts/controls/svg/alignleft.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - diff --git a/demo/scripts/controls/svg/alignright.svg b/demo/scripts/controls/svg/alignright.svg deleted file mode 100644 index d15c3ef0807c..000000000000 --- a/demo/scripts/controls/svg/alignright.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - diff --git a/demo/scripts/controls/svg/backcolor.svg b/demo/scripts/controls/svg/backcolor.svg deleted file mode 100644 index e4c74055c8b3..000000000000 --- a/demo/scripts/controls/svg/backcolor.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - Highlight - - - - - - \ No newline at end of file diff --git a/demo/scripts/controls/svg/blockquote.svg b/demo/scripts/controls/svg/blockquote.svg deleted file mode 100644 index 030cfd6c0f92..000000000000 --- a/demo/scripts/controls/svg/blockquote.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - diff --git a/demo/scripts/controls/svg/bold.svg b/demo/scripts/controls/svg/bold.svg deleted file mode 100644 index 72688c90a368..000000000000 --- a/demo/scripts/controls/svg/bold.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - diff --git a/demo/scripts/controls/svg/bullets.svg b/demo/scripts/controls/svg/bullets.svg deleted file mode 100644 index abc90f1f92a2..000000000000 --- a/demo/scripts/controls/svg/bullets.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/demo/scripts/controls/svg/capitalization.svg b/demo/scripts/controls/svg/capitalization.svg deleted file mode 100644 index ca9e7395030f..000000000000 --- a/demo/scripts/controls/svg/capitalization.svg +++ /dev/null @@ -1,70 +0,0 @@ - - - Roman letter A (capital and lower case) - - - - - image/svg+xml - - Roman letter A (capital and lower case) - 2014-05-26 - - - Hydrargyrum - - - - - Wikipedia - - - Times New Roman capital "A" and Abadi MT Condensed Light lowercase "a" - - - - - - - - - - - - - - - diff --git a/demo/scripts/controls/svg/code.svg b/demo/scripts/controls/svg/code.svg deleted file mode 100644 index 00f08f5fbdff..000000000000 --- a/demo/scripts/controls/svg/code.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/demo/scripts/controls/svg/createlink.svg b/demo/scripts/controls/svg/createlink.svg deleted file mode 100644 index 7d84dd3565b8..000000000000 --- a/demo/scripts/controls/svg/createlink.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - diff --git a/demo/scripts/controls/svg/fontname.svg b/demo/scripts/controls/svg/fontname.svg deleted file mode 100644 index 956881fc3b6d..000000000000 --- a/demo/scripts/controls/svg/fontname.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - diff --git a/demo/scripts/controls/svg/fontsize.svg b/demo/scripts/controls/svg/fontsize.svg deleted file mode 100644 index b165093b9c2b..000000000000 --- a/demo/scripts/controls/svg/fontsize.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - fontsize16 - - - - - - - - - \ No newline at end of file diff --git a/demo/scripts/controls/svg/header.svg b/demo/scripts/controls/svg/header.svg deleted file mode 100644 index 0bdd9fd29750..000000000000 --- a/demo/scripts/controls/svg/header.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/demo/scripts/controls/svg/indent.svg b/demo/scripts/controls/svg/indent.svg deleted file mode 100644 index 078e39352924..000000000000 --- a/demo/scripts/controls/svg/indent.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/demo/scripts/controls/svg/inlineimage.svg b/demo/scripts/controls/svg/inlineimage.svg deleted file mode 100644 index c8750c55895e..000000000000 --- a/demo/scripts/controls/svg/inlineimage.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - diff --git a/demo/scripts/controls/svg/italic.svg b/demo/scripts/controls/svg/italic.svg deleted file mode 100644 index 3f50d758c9f4..000000000000 --- a/demo/scripts/controls/svg/italic.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - diff --git a/demo/scripts/controls/svg/ltr.svg b/demo/scripts/controls/svg/ltr.svg deleted file mode 100644 index 3deb78e9769e..000000000000 --- a/demo/scripts/controls/svg/ltr.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - diff --git a/demo/scripts/controls/svg/moon.svg b/demo/scripts/controls/svg/moon.svg deleted file mode 100644 index 2b22d3c7b597..000000000000 --- a/demo/scripts/controls/svg/moon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/demo/scripts/controls/svg/more.svg b/demo/scripts/controls/svg/more.svg deleted file mode 100644 index 2c3c11d61113..000000000000 --- a/demo/scripts/controls/svg/more.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/demo/scripts/controls/svg/numbering.svg b/demo/scripts/controls/svg/numbering.svg deleted file mode 100644 index 836e7620298f..000000000000 --- a/demo/scripts/controls/svg/numbering.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - diff --git a/demo/scripts/controls/svg/outdent.svg b/demo/scripts/controls/svg/outdent.svg deleted file mode 100644 index 866f6f7c0fa2..000000000000 --- a/demo/scripts/controls/svg/outdent.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/demo/scripts/controls/svg/paste.svg b/demo/scripts/controls/svg/paste.svg deleted file mode 100644 index 607f04b64573..000000000000 --- a/demo/scripts/controls/svg/paste.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/demo/scripts/controls/svg/redo.svg b/demo/scripts/controls/svg/redo.svg deleted file mode 100644 index f707197a12a1..000000000000 --- a/demo/scripts/controls/svg/redo.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/demo/scripts/controls/svg/removeformat.svg b/demo/scripts/controls/svg/removeformat.svg deleted file mode 100644 index 4544f77ac04a..000000000000 --- a/demo/scripts/controls/svg/removeformat.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - diff --git a/demo/scripts/controls/svg/rtl.svg b/demo/scripts/controls/svg/rtl.svg deleted file mode 100644 index 1ec1876824ca..000000000000 --- a/demo/scripts/controls/svg/rtl.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - diff --git a/demo/scripts/controls/svg/strikethrough.svg b/demo/scripts/controls/svg/strikethrough.svg deleted file mode 100644 index 640e6ea6101e..000000000000 --- a/demo/scripts/controls/svg/strikethrough.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - diff --git a/demo/scripts/controls/svg/subscript.svg b/demo/scripts/controls/svg/subscript.svg deleted file mode 100644 index 70688643b0aa..000000000000 --- a/demo/scripts/controls/svg/subscript.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - diff --git a/demo/scripts/controls/svg/superscript.svg b/demo/scripts/controls/svg/superscript.svg deleted file mode 100644 index d4a2970d7fb9..000000000000 --- a/demo/scripts/controls/svg/superscript.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - diff --git a/demo/scripts/controls/svg/table.svg b/demo/scripts/controls/svg/table.svg deleted file mode 100644 index cb54dee835e0..000000000000 --- a/demo/scripts/controls/svg/table.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - diff --git a/demo/scripts/controls/svg/textcolor.svg b/demo/scripts/controls/svg/textcolor.svg deleted file mode 100644 index f57c6197ef64..000000000000 --- a/demo/scripts/controls/svg/textcolor.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - diff --git a/demo/scripts/controls/svg/underline.svg b/demo/scripts/controls/svg/underline.svg deleted file mode 100644 index 21c00ea69fe4..000000000000 --- a/demo/scripts/controls/svg/underline.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - diff --git a/demo/scripts/controls/svg/undo.svg b/demo/scripts/controls/svg/undo.svg deleted file mode 100644 index f708fb0e822a..000000000000 --- a/demo/scripts/controls/svg/undo.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/demo/scripts/controls/svg/unlink.svg b/demo/scripts/controls/svg/unlink.svg deleted file mode 100644 index beb00687dd92..000000000000 --- a/demo/scripts/controls/svg/unlink.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - diff --git a/demo/scripts/controls/svg/zoom.svg b/demo/scripts/controls/svg/zoom.svg deleted file mode 100644 index e2b3fcce93db..000000000000 --- a/demo/scripts/controls/svg/zoom.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/demo/scripts/controls/theme/contentModelEditorTheme.scss b/demo/scripts/controls/theme/contentModelEditorTheme.scss new file mode 100644 index 000000000000..8525642752c9 --- /dev/null +++ b/demo/scripts/controls/theme/contentModelEditorTheme.scss @@ -0,0 +1,27 @@ +$primaryColor: #cc6688; +$primaryLighter: lighten($primaryColor, 5%); +$primaryLighter2: lighten($primaryColor, 50%); +$primaryBorder: #cc6688; +$primaryBackgroundColor: white; + +$primaryColorDark: #cb6587; +$primaryLighterDark: lighten($primaryColorDark, 5%); +$primaryLighter2Dark: lighten($primaryColorDark, 50%); +$primaryBorderDark: #cb6587; +$primaryBackgroundColorDark: #333333; + +@media (prefers-color-scheme: dark) { + button { + background-color: $primaryColorDark; + color: $primaryLighter2; + border: solid 1px $primaryBorderDark; + } + + select, + input, + textarea { + background-color: $primaryBackgroundColorDark; + color: $primaryLighter2; + border: solid 1px $primaryBorderDark; + } +} diff --git a/demo/scripts/controls/theme/theme.scss b/demo/scripts/controls/theme/theme.scss index 0a38ce31c76f..58848643e356 100644 --- a/demo/scripts/controls/theme/theme.scss +++ b/demo/scripts/controls/theme/theme.scss @@ -3,3 +3,25 @@ $primaryLighter: lighten($primaryColor, 5%); $primaryLighter2: lighten($primaryColor, 50%); $primaryBorder: #00bbcc; $primaryBackgroundColor: white; + +$primaryColorDark: #0091a1; +$primaryLighterDark: lighten($primaryColorDark, 5%); +$primaryLighter2Dark: lighten($primaryColorDark, 50%); +$primaryBorderDark: #007b8b; +$primaryBackgroundColorDark: #333333; + +@media (prefers-color-scheme: dark) { + button { + background-color: $primaryColorDark; + color: $primaryLighter2; + border: solid 1px $primaryBorderDark; + } + + select, + input, + textarea { + background-color: $primaryBackgroundColorDark; + color: $primaryLighter2; + border: solid 1px $primaryBorderDark; + } +} diff --git a/demo/scripts/controls/titleBar/ContentModelTitleBar.scss b/demo/scripts/controls/titleBar/ContentModelTitleBar.scss new file mode 100644 index 000000000000..76324d4c2474 --- /dev/null +++ b/demo/scripts/controls/titleBar/ContentModelTitleBar.scss @@ -0,0 +1,58 @@ +@import '../theme//contentModelEditorTheme.scss'; + +.titleBar { + display: flex; + background-color: $primaryColor; + padding: 5px 10px; + margin-bottom: 10px; + border-radius: 10px; + align-items: center; +} + +.title { + flex: 0 0 auto; + font-size: 24pt; + font-family: Arial; + font-weight: bold; + font-style: italic; + color: white; + text-shadow: 2px 2px 2px black; +} + +.version { + flex: 1 1 auto; + color: white; + font-family: Calibri; + font-size: 14pt; + margin: 10px 0 0 10px; +} + +.links { + color: white; + flex: 0 0 auto; + text-align: right; + font-size: 14pt; + font-family: Calibri; +} + +.link { + color: white; + text-decoration: none; + &:hover { + text-decoration: underline; + } +} + +.externalLink { + vertical-align: middle; +} + +@media (prefers-color-scheme: dark) { + .titleBar { + background-color: $primaryColorDark; + } + .title, + .link { + color: #bbd1e1; + } +} diff --git a/demo/scripts/controls/titleBar/TitleBar.scss b/demo/scripts/controls/titleBar/TitleBar.scss index 87144c52c8b5..1e492c942aa4 100644 --- a/demo/scripts/controls/titleBar/TitleBar.scss +++ b/demo/scripts/controls/titleBar/TitleBar.scss @@ -46,3 +46,13 @@ .externalLink { vertical-align: middle; } + +@media (prefers-color-scheme: dark) { + .titleBar { + background-color: $primaryColorDark; + } + .title, + .link { + color: #bbd1e1; + } +} diff --git a/demo/scripts/controls/titleBar/TitleBar.tsx b/demo/scripts/controls/titleBar/TitleBar.tsx index 4659c6a77ce1..bf7437018c67 100644 --- a/demo/scripts/controls/titleBar/TitleBar.tsx +++ b/demo/scripts/controls/titleBar/TitleBar.tsx @@ -1,28 +1,41 @@ import * as React from 'react'; -const styles = require('./TitleBar.scss'); -const github = require('../svg/iconmonstr-github-1.svg'); +const classicalStyles = require('./TitleBar.scss'); +const contentModelStyles = require('./ContentModelTitleBar.scss'); -interface WindowHack extends Window { - roosterJsVer: string; -} +const github = require('./iconmonstr-github-1.svg'); export interface TitleBarProps { className?: string; + isContentModelPane: boolean; } export default class TitleBar extends React.Component { render() { - let className = styles.titleBar + ' ' + (this.props.className || ''); + const { isContentModelPane, className: baseClassName } = this.props; + const styles = isContentModelPane ? contentModelStyles : classicalStyles; + const className = styles.titleBar + ' ' + (baseClassName || ''); + const titleText = isContentModelPane + ? 'RoosterJs Content Model Demo Site' + : 'RoosterJs Demo Site'; + const switchLink = isContentModelPane ? ( + + Switch to classical demo + + ) : ( + + Switch to Content Model demo + + ); + return (
- RoosterJs Demo Site -
-
- {((window as any) as WindowHack).roosterJsVer || ''} + {titleText}
+
+ {switchLink} {' | '} x == 'cm=1')) { + mountContentModelEditorMainPane(document.getElementById('mainPane')); +} else { + mountClassicalEditorMainPane(document.getElementById('mainPane')); +} diff --git a/demo/scripts/tsconfig.json b/demo/scripts/tsconfig.json index 2494e21edd75..e717a526fb68 100644 --- a/demo/scripts/tsconfig.json +++ b/demo/scripts/tsconfig.json @@ -24,7 +24,11 @@ "roosterjs-editor-types": ["packages/roosterjs-editor-types/lib/index"], "roosterjs-editor-types/lib/*": ["packages/roosterjs-editor-types/lib/*"], "roosterjs-color-utils": ["packages/roosterjs-color-utils/lib/index"], - "roosterjs-color-utils/lib/*": ["packages/roosterjs-color-utils/lib/*"] + "roosterjs-color-utils/lib/*": ["packages/roosterjs-color-utils/lib/*"], + "roosterjs-content-model": ["packages/roosterjs-content-model/lib/index"], + "roosterjs-content-model/lib/*": ["packages/roosterjs-content-model/lib/*"], + "roosterjs-react": ["packages-ui/roosterjs-react/lib/index"], + "roosterjs-react/lib/*": ["packages-ui/roosterjs-react/lib/*"] } }, "include": ["./**/*.ts", "./**/*.tsx"] diff --git a/demo/scripts/utils/cssMonitor.ts b/demo/scripts/utils/cssMonitor.ts new file mode 100644 index 000000000000..bd0cf25b0f3a --- /dev/null +++ b/demo/scripts/utils/cssMonitor.ts @@ -0,0 +1,54 @@ +import { Stylesheet } from '@fluentui/merge-styles/lib/Stylesheet'; + +let isCssMonitorStarted: boolean = false; +const activeWindows: Window[] = []; + +function startCssMonitor() { + if (!isCssMonitorStarted) { + isCssMonitorStarted = true; + Stylesheet.getInstance().setConfig({ + onInsertRule: (cssText: string) => { + activeWindows.forEach(win => { + const style = win.document.createElement('style'); + style.textContent = cssText; + win.document.head.appendChild(style); + }); + }, + }); + } +} + +export function registerWindowForCss(win: Window) { + startCssMonitor(); + + activeWindows.push(win); + + const styles = document.getElementsByTagName('STYLE'); + const fragment = win.document.createDocumentFragment(); + + for (let i = 0; i < styles.length; i++) { + const style = win.document.createElement('style'); + fragment.appendChild(style); + + const originalStyle = styles[i] as HTMLStyleElement; + const rules = originalStyle.sheet.cssRules; + let cssText = ''; + + for (let j = 0; j < rules.length; j++) { + const rule = rules[j] as CSSStyleRule; + cssText += rule.cssText; + } + + style.textContent = cssText; + } + + win.document.head.appendChild(fragment); +} + +export function unregisterWindowForCss(win: Window) { + const index = activeWindows.indexOf(win); + + if (index >= 0) { + activeWindows.splice(index, 1); + } +} diff --git a/demo/scripts/utils/trustedHTMLHandler.ts b/demo/scripts/utils/trustedHTMLHandler.ts index a4f0dbb3debd..89da362344c4 100644 --- a/demo/scripts/utils/trustedHTMLHandler.ts +++ b/demo/scripts/utils/trustedHTMLHandler.ts @@ -2,10 +2,11 @@ import * as DOMPurify from 'dompurify'; export function trustedHTMLHandler(html: string): string { const result = DOMPurify.sanitize(html, { - ADD_TAGS: ['head', 'meta'], + ADD_TAGS: ['head', 'meta', '#comment', 'iframe'], ADD_ATTR: ['name', 'content'], WHOLE_DOCUMENT: true, RETURN_TRUSTED_TYPE: true, + ALLOW_UNKNOWN_PROTOCOLS: true, }); return (result); } diff --git a/index.html b/index.html index 7068a631d978..daf62dcaad3f 100644 --- a/index.html +++ b/index.html @@ -1,7 +1,7 @@ - RoosterJs Demo Page + RoosterJs Demo Site
test
', - image: null, - snapshotBeforePaste: null, - imageDataUri: null, - customValues: {}, - }; - const fragment = createPasteFragment(core, clipboardData, null, false, false); - - expect(getHTML(fragment)).toBe('
test
'); - expect(sanitizingOption.additionalGlobalStyleNodes.length).toBe(2); - expect(sanitizingOption.additionalGlobalStyleNodes[0].outerHTML).toBe( - '' - ); - expect(sanitizingOption.additionalGlobalStyleNodes[1].outerHTML).toBe( - '' - ); - }); - - it('html input, with one img tag', () => { - const triggerEvent = jasmine.createSpy(); - const core = createEditorCore(div, { - coreApiOverride: { - triggerEvent, - }, - }); - - const clipboardData: ClipboardData = { - types: ['image/png', 'text/html'], - text: '', - image: null, - rawHtml: '\r\n\r\n\r\n\r\n', - customValues: {}, - imageDataUri: null, - }; - const fragment = createPasteFragment(core, clipboardData, null, false, false); - const html = getHTML(fragment); - expect(html).toBe(''); - expect(clipboardData.htmlFirstLevelChildTags).toEqual(['IMG']); - }); - - it('html input, with one img tag and text nodes with value', () => { - const triggerEvent = jasmine.createSpy(); - const core = createEditorCore(div, { - coreApiOverride: { - triggerEvent, - }, - }); - - const clipboardData: ClipboardData = { - types: ['image/png', 'text/html'], - text: '', - image: null, - rawHtml: '\r\nteststringteststring\r\n', - customValues: {}, - imageDataUri: null, - }; - const fragment = createPasteFragment(core, clipboardData, null, false, false); - const html = getHTML(fragment); - expect(html.trim()).toBe('teststringteststring'); - expect(clipboardData.htmlFirstLevelChildTags).toEqual(['', 'IMG', '']); - }); -}); - -function getHTML(fragment: DocumentFragment) { - let result = ''; - for (let node = fragment.firstChild; node; node = node.nextSibling) { - if (node.nodeType == Node.TEXT_NODE) { - result += node.nodeValue; - } else if (node.nodeType == Node.ELEMENT_NODE) { - result += (node).outerHTML; - } - } - return result; -} +import * as createDefaultHtmlSanitizerOptions from 'roosterjs-editor-dom/lib/htmlSanitizer/createDefaultHtmlSanitizerOptions'; +import createEditorCore from './createMockEditorCore'; +import { ClipboardData, PasteType, PluginEventType } from 'roosterjs-editor-types'; +import { createPasteFragment } from '../../lib/coreApi/createPasteFragment'; +import { itChromeOnly, itFirefoxOnly } from '../TestHelper'; + +describe('createPasteFragment', () => { + let div: HTMLDivElement; + beforeEach(() => { + div = document.createElement('div'); + document.body.appendChild(div); + }); + + afterEach(() => { + document.body.removeChild(div); + div = null; + }); + + it('null input', () => { + const core = createEditorCore(div, {}); + const fragment = createPasteFragment(core, null, null, false, false, false); + expect(fragment).toBeNull(); + }); + + it('plain text input, html output', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + const clipboardData: ClipboardData = { + types: [], + text: 'This is a test', + rawHtml: null, + image: null, + snapshotBeforePaste: null, + imageDataUri: null, + customValues: {}, + }; + const fragment = createPasteFragment(core, clipboardData, null, false, false, false); + const html = getHTML(fragment); + expect(html).toBe('This is a test'); + }); + + it('two lines plain text input, html output', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + const clipboardData: ClipboardData = { + types: [], + text: 'This is a test\nthis is line 2', + rawHtml: null, + image: null, + snapshotBeforePaste: null, + imageDataUri: null, + customValues: {}, + }; + const fragment = createPasteFragment(core, clipboardData, null, false, false, false); + const html = getHTML(fragment); + expect(html).toBe('This is a test
this is line 2'); + }); + + it('multi-line plain text input, html output', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + const clipboardData: ClipboardData = { + types: [], + text: 'This is a test\nthis is line 2\nthis is line 3', + rawHtml: null, + image: null, + snapshotBeforePaste: null, + imageDataUri: null, + customValues: {}, + }; + const fragment = createPasteFragment(core, clipboardData, null, false, false, false); + const html = getHTML(fragment); + expect(html).toBe('This is a test
this is line 2
this is line 3'); + }); + + it('two lines plain text input with empty lines, html output, 1', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + const clipboardData: ClipboardData = { + types: [], + text: '\nthis is line 2', + rawHtml: null, + image: null, + snapshotBeforePaste: null, + imageDataUri: null, + customValues: {}, + }; + const fragment = createPasteFragment(core, clipboardData, null, false, false, false); + const html = getHTML(fragment); + expect(html).toBe('
this is line 2'); + }); + + it('two lines plain text input with empty lines, html output, 2', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + const clipboardData: ClipboardData = { + types: [], + text: 'this is line 1\n', + rawHtml: null, + image: null, + snapshotBeforePaste: null, + imageDataUri: null, + customValues: {}, + }; + const fragment = createPasteFragment(core, clipboardData, null, false, false, false); + const html = getHTML(fragment); + expect(html).toBe('this is line 1
'); + }); + + it('multi-line plain text input with empty lines, html output, 1', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + const clipboardData: ClipboardData = { + types: [], + text: '\nthis is line 2\nthis is line 3', + rawHtml: null, + image: null, + snapshotBeforePaste: null, + imageDataUri: null, + customValues: {}, + }; + const fragment = createPasteFragment(core, clipboardData, null, false, false, false); + const html = getHTML(fragment); + expect(html).toBe('
this is line 2
this is line 3'); + }); + + it('multi-line plain text input with empty lines, html output, 2', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + const clipboardData: ClipboardData = { + types: [], + text: 'this is line 1\n\nthis is line 3', + rawHtml: null, + image: null, + snapshotBeforePaste: null, + imageDataUri: null, + customValues: {}, + }; + const fragment = createPasteFragment(core, clipboardData, null, false, false, false); + const html = getHTML(fragment); + expect(html).toBe('this is line 1

this is line 3'); + }); + + it('multi-line plain text input with empty lines, html output, 3', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + const clipboardData: ClipboardData = { + types: [], + text: 'this is line 1\nthis is line 2\n', + rawHtml: null, + image: null, + snapshotBeforePaste: null, + imageDataUri: null, + customValues: {}, + }; + const fragment = createPasteFragment(core, clipboardData, null, false, false, false); + const html = getHTML(fragment); + expect(html).toBe('this is line 1
this is line 2
'); + }); + + it('multi-line plain text input, text output', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + const clipboardData: ClipboardData = { + types: [], + text: 'This is a test\nthis is line 2\nthis is line 3', + rawHtml: null, + image: null, + snapshotBeforePaste: null, + imageDataUri: null, + customValues: {}, + }; + const fragment = createPasteFragment(core, clipboardData, null, true, false, false); + const html = getHTML(fragment); + expect(html).toBe('This is a test
this is line 2
this is line 3'); + }); + + itFirefoxOnly('image input, html output', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + const clipboardData: ClipboardData = { + types: [], + text: '', + rawHtml: null, + image: null, + snapshotBeforePaste: null, + imageDataUri: 'test', + customValues: {}, + }; + const fragment = createPasteFragment(core, clipboardData, null, false, false, false); + const html = getHTML(fragment); + expect(html).toBe(''); + }); + + it('image input, text output', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + const clipboardData: ClipboardData = { + types: [], + text: 'test', + rawHtml: null, + image: null, + snapshotBeforePaste: null, + imageDataUri: 'test', + customValues: {}, + }; + const fragment = createPasteFragment(core, clipboardData, null, false, false, false); + const html = getHTML(fragment); + expect(html).toBe('test'); + }); + + it('image input, force text output', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + const clipboardData: ClipboardData = { + types: [], + text: '', + rawHtml: null, + image: null, + snapshotBeforePaste: null, + imageDataUri: 'test', + customValues: {}, + }; + const fragment = createPasteFragment(core, clipboardData, null, true, false, false); + const html = getHTML(fragment); + expect(html).toBe(''); + }); + + itChromeOnly('create image fragment', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + const clipboardData: ClipboardData = { + types: [], + text: '', + rawHtml: null, + image: null, + snapshotBeforePaste: null, + imageDataUri: 'test', + customValues: {}, + }; + const fragment = createPasteFragment(core, clipboardData, null, true, false, true); + const html = getHTML(fragment); + expect(html).toBe(''); + }); + + itFirefoxOnly('create image fragment', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + const clipboardData: ClipboardData = { + types: [], + text: '', + rawHtml: null, + image: null, + snapshotBeforePaste: null, + imageDataUri: 'test', + customValues: {}, + }; + const fragment = createPasteFragment(core, clipboardData, null, true, false, true); + const html = getHTML(fragment); + expect(html).toBe(''); + }); + + it('html input, html output', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + const clipboardData: ClipboardData = { + types: [], + text: 'test text', + rawHtml: '
test html
', + image: null, + snapshotBeforePaste: null, + imageDataUri: 'test', + customValues: {}, + }; + const fragment = createPasteFragment(core, clipboardData, null, false, false, false); + const html = getHTML(fragment); + expect(html).toBe('
test html
'); + }); + + it('html input, force text output', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + const clipboardData: ClipboardData = { + types: [], + text: 'test text', + rawHtml: '
test html
', + image: null, + snapshotBeforePaste: null, + imageDataUri: 'test', + customValues: {}, + }; + const fragment = createPasteFragment(core, clipboardData, null, true, false, false); + const html = getHTML(fragment); + expect(html).toBe('test text'); + }); + + it('html input with html attributes and meta', () => { + const sanitizingOption: any = { + cssStyleCallbacks: {}, + }; + spyOn(createDefaultHtmlSanitizerOptions, 'default').and.returnValue(sanitizingOption); + + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + const clipboardData: ClipboardData = { + types: [], + text: '', + rawHtml: + '
test
', + image: null, + snapshotBeforePaste: null, + imageDataUri: null, + customValues: {}, + }; + const fragment = createPasteFragment(core, clipboardData, null, false, false, false); + + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.BeforePaste, + clipboardData, + fragment, + sanitizingOption, + htmlBefore: '', + htmlAfter: '', + htmlAttributes: { + attrname1: 'attrValue1', + attrname2: 'attrValue2', + metaName1: 'metaContent1', + metaName2: 'metaContent2', + }, + pasteType: PasteType.Normal, + }, + true + ); + const html = getHTML(fragment); + expect(html).toBe('
test
'); + }); + + it('html input, make sure STYLE tags are properly handled', () => { + const sanitizingOption: any = { additionalGlobalStyleNodes: [], cssStyleCallbacks: {} }; + spyOn(createDefaultHtmlSanitizerOptions, 'default').and.returnValue(sanitizingOption); + + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + const clipboardData: ClipboardData = { + types: [], + text: '', + rawHtml: + '
test
', + image: null, + snapshotBeforePaste: null, + imageDataUri: null, + customValues: {}, + }; + const fragment = createPasteFragment(core, clipboardData, null, false, false, false); + + expect(getHTML(fragment)).toBe('
test
'); + expect(sanitizingOption.additionalGlobalStyleNodes.length).toBe(2); + expect(sanitizingOption.additionalGlobalStyleNodes[0].outerHTML).toBe( + '' + ); + expect(sanitizingOption.additionalGlobalStyleNodes[1].outerHTML).toBe( + '' + ); + }); + + it('html input, with one img tag', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + + const clipboardData: ClipboardData = { + types: ['image/png', 'text/html'], + text: '', + image: null, + rawHtml: '\r\n\r\n\r\n\r\n', + customValues: {}, + imageDataUri: null, + }; + const fragment = createPasteFragment(core, clipboardData, null, false, false, false); + const html = getHTML(fragment); + expect(html).toBe(''); + expect(clipboardData.htmlFirstLevelChildTags).toEqual(['IMG']); + }); + + it('html input, with one img tag and text nodes with value', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + + const clipboardData: ClipboardData = { + types: ['image/png', 'text/html'], + text: '', + image: null, + rawHtml: '\r\nteststringteststring\r\n', + customValues: {}, + imageDataUri: null, + }; + const fragment = createPasteFragment(core, clipboardData, null, false, false, false); + const html = getHTML(fragment); + expect(html.trim()).toBe('teststringteststring'); + expect(clipboardData.htmlFirstLevelChildTags).toEqual(['', 'IMG', '']); + }); +}); + +function getHTML(fragment: DocumentFragment) { + let result = ''; + for (let node = fragment.firstChild; node; node = node.nextSibling) { + if (node.nodeType == Node.TEXT_NODE) { + result += node.nodeValue; + } else if (node.nodeType == Node.ELEMENT_NODE) { + result += (node).outerHTML; + } + } + return result; +} diff --git a/packages/roosterjs-editor-core/test/coreApi/ensureTypeInContainerTest.ts b/packages/roosterjs-editor-core/test/coreApi/ensureTypeInContainerTest.ts new file mode 100644 index 000000000000..9ecb561cfbed --- /dev/null +++ b/packages/roosterjs-editor-core/test/coreApi/ensureTypeInContainerTest.ts @@ -0,0 +1,199 @@ +import * as createRange from 'roosterjs-editor-dom/lib/selection/createRange'; +import createEditorCore from './createMockEditorCore'; +import { DefaultFormat, NodePosition, PositionType } from 'roosterjs-editor-types'; +import { ensureTypeInContainer } from '../../lib/coreApi/ensureTypeInContainer'; +import { Position } from 'roosterjs-editor-dom'; + +describe('ensureTypeInContainer', () => { + let div: HTMLDivElement; + + beforeEach(() => { + div = document.createElement('div'); + document.body.appendChild(div); + spyOn(createRange, 'default').and.callFake(pos => (pos as any) as Range); + }); + + afterEach(() => { + document.body.removeChild(div!); + div = null!; + }); + + function runTest( + content: string, + defaultFormat: DefaultFormat | undefined, + event: KeyboardEvent | undefined, + applyFormatToSpan: boolean, + expectedHtml: string, + expectedPos?: () => NodePosition + ) { + const position = new Position(div, PositionType.Begin); + const selectRange = jasmine.createSpy('selectRange'); + const core = createEditorCore(div, { + coreApiOverride: { + selectRange: selectRange, + }, + defaultFormat: defaultFormat, + }); + core.api.setContent(core, content, true); + + ensureTypeInContainer(core, position, event, applyFormatToSpan); + + expect(div.innerHTML).toBe(expectedHtml); + + if (expectedPos) { + expect(selectRange).toHaveBeenCalledWith(core, expectedPos()); + } else { + expect(selectRange).not.toHaveBeenCalled(); + } + } + + it('empty', () => { + runTest('', undefined, undefined, true, '

'); + }); + + it('empty, no format span', () => { + runTest('', undefined, undefined, false, '

'); + }); + + it('pure text', () => { + runTest('text', undefined, undefined, false, '
text
'); + }); + + it('div with text', () => { + runTest('
text
', undefined, undefined, false, '
text
'); + }); + + it('empty editor with format', () => { + runTest( + '', + { + fontSize: '10pt', + }, + undefined, + true, + '

' + ); + }); + + it('empty editor with format, no format span', () => { + runTest( + '', + { + fontSize: '10pt', + }, + undefined, + false, + '

' + ); + }); + + it('pure text with format', () => { + runTest( + 'text', + { + fontSize: '10pt', + }, + undefined, + false, + '
text
' + ); + }); + + it('div with text and format', () => { + runTest( + '
text
', + { + fontSize: '10pt', + }, + undefined, + false, + '
text
' + ); + }); + + it('div with format and event', () => { + runTest( + '
a
', + { + fontSize: '10pt', + }, + ({ + target: div, + key: 'a', + } as any) as KeyboardEvent, + false, + '
a
', + () => new Position(div.firstChild?.firstChild, PositionType.Begin) + ); + }); + + it('div with format and event and use span', () => { + runTest( + '
a
', + { + fontSize: '10pt', + }, + ({ + target: div, + key: 'a', + } as any) as KeyboardEvent, + true, + '
a
', + () => + new Position(({ + element: div.firstChild?.firstChild, + isAtEnd: false, + offset: 0, + node: div.firstChild?.firstChild, + })) + ); + }); + + it('div with format and event and use span, div is not created from the event', () => { + runTest( + '
a
', + { + fontSize: '10pt', + }, + ({ + target: div, + key: 'b', + } as any) as KeyboardEvent, + true, + '
a
', + () => new Position(div.firstChild!.firstChild, PositionType.Begin) + ); + }); + + it('table with format and event and not use span', () => { + runTest( + '
', + { + fontSize: '10pt', + }, + ({ + target: div, + key: 'a', + } as any) as KeyboardEvent, + false, + '

', + () => new Position(div.querySelector('#td'), PositionType.Begin) + ); + }); + + it('table with format and event and use span', () => { + runTest( + '
', + { + fontSize: '10pt', + }, + ({ + target: div, + key: 'a', + } as any) as KeyboardEvent, + true, + '

', + () => new Position(div.querySelector('#td')!, PositionType.Begin) + ); + }); +}); diff --git a/packages/roosterjs-editor-core/test/coreApi/focusTest.ts b/packages/roosterjs-editor-core/test/coreApi/focusTest.ts index ab87e4b89869..cd4c96e3cdc8 100644 --- a/packages/roosterjs-editor-core/test/coreApi/focusTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/focusTest.ts @@ -23,6 +23,7 @@ describe('focus', () => { stopPrintableKeyboardEventPropagation: false, contextMenuProviders: [], tableSelectionRange: null, + imageSelectionRange: null, }; focus(core); diff --git a/packages/roosterjs-editor-core/test/coreApi/getContentTest.ts b/packages/roosterjs-editor-core/test/coreApi/getContentTest.ts index 04924c9cb395..6779c3d40d8b 100644 --- a/packages/roosterjs-editor-core/test/coreApi/getContentTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/getContentTest.ts @@ -110,7 +110,7 @@ describe('getContent', () => { expect(html1).toBe('test0'); const html2 = getContent(core, GetContentMode.RawHTMLWithSelection); - expect(html2).toBe('test0'); + expect(html2).toBe('test0'); }); it('getContent with empty text node', () => { diff --git a/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeExTest.ts b/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeExTest.ts index b8d5ab55625a..ee4b78138249 100644 --- a/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeExTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeExTest.ts @@ -1,6 +1,7 @@ import createEditorCore from './createMockEditorCore'; import { focus } from '../../lib/coreApi/focus'; import { getSelectionRangeEx } from '../../lib/coreApi/getSelectionRangeEx'; +import { SelectionRangeTypes } from 'roosterjs-editor-types'; import { selectNode } from '../TestHelper'; describe('getSelectionRangeEx', () => { @@ -21,7 +22,7 @@ describe('getSelectionRangeEx', () => { document.body.appendChild(input); input.focus(); const selection = getSelectionRangeEx(core); - expect(selection.ranges[0]).toBeNull(); + expect(selection.ranges.length).toBe(0); document.body.removeChild(input); }); @@ -52,6 +53,7 @@ describe('getSelectionRangeEx', () => { stopPrintableKeyboardEventPropagation: false, contextMenuProviders: [], tableSelectionRange: null, + imageSelectionRange: null, }; const input = document.createElement('input'); document.body.appendChild(input); @@ -85,6 +87,33 @@ describe('getSelectionRangeEx', () => { }); }); + it('image selection', () => { + div.innerHTML = ''; + const image = div.querySelector('img'); + const core = createEditorCore(div, {}); + const range = new Range(); + range.selectNode(image!); + core.domEvent = { + selectionRange: range, + isInIME: false, + scrollContainer: null, + stopPrintableKeyboardEventPropagation: false, + contextMenuProviders: [], + tableSelectionRange: null, + imageSelectionRange: { + type: SelectionRangeTypes.ImageSelection, + ranges: [range], + image: image, + areAllCollapsed: range.collapsed, + }, + }; + focus(core); + + const selectionEx = getSelectionRangeEx(core); + expect(selectionEx.type).toBe(SelectionRangeTypes.ImageSelection); + expect(selectionEx.ranges).toEqual([range]); + }); + function runTest(input: string, id: string, expectedRangesLength: number[][]) { const core = createEditorCore(div, {}); diff --git a/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeTest.ts b/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeTest.ts index 1c56c52a6b9d..949027f45ff0 100644 --- a/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeTest.ts @@ -48,6 +48,7 @@ describe('getSelectionRange', () => { stopPrintableKeyboardEventPropagation: false, contextMenuProviders: [], tableSelectionRange: null, + imageSelectionRange: null, }; const input = document.createElement('input'); document.body.appendChild(input); diff --git a/packages/roosterjs-editor-core/test/coreApi/getStyleBasedFormatStateTest.ts b/packages/roosterjs-editor-core/test/coreApi/getStyleBasedFormatStateTest.ts index b50cecb77d88..ec486f84461b 100644 --- a/packages/roosterjs-editor-core/test/coreApi/getStyleBasedFormatStateTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/getStyleBasedFormatStateTest.ts @@ -1,4 +1,5 @@ import createEditorCore from './createMockEditorCore'; +import { DarkColorHandler } from 'roosterjs-editor-types'; import { getStyleBasedFormatState } from '../../lib/coreApi/getStyleBasedFormatState'; describe('getStyleBasedFormatState', () => { @@ -21,8 +22,8 @@ describe('getStyleBasedFormatState', () => { const style = getStyleBasedFormatState(core, node); expect(style.fontName).toBe('arial'); expect(style.fontSize).toBe('12pt'); - expect(style.textColor).toBe('rgb(0, 0, 0)'); - expect(style.backgroundColor).toBe('rgba(0, 0, 0, 0)'); + expect(style.textColor).toBe('black'); + expect(style.backgroundColor).toBe('white'); expect(style.textColors).toBeUndefined(); expect(style.backgroundColors).toBeUndefined(); }); @@ -35,69 +36,91 @@ describe('getStyleBasedFormatState', () => { const style = getStyleBasedFormatState(core, node); expect(style.fontName).toBe('arial'); expect(style.fontSize).toBe('12pt'); - expect(style.textColor).toBe('rgb(0, 0, 0)'); - expect(style.backgroundColor).toBe('rgba(0, 0, 0, 0)'); + expect(style.textColor).toBe('black'); + expect(style.backgroundColor).toBe('white'); expect(style.textColors).toBeUndefined(); expect(style.backgroundColors).toBeUndefined(); }); +}); + +describe('getStyleBasedFormatState with var based color', () => { + let div: HTMLDivElement; + beforeEach(() => { + div = document.createElement('div'); + document.body.appendChild(div); + }); + + afterEach(() => { + document.body.removeChild(div); + div = null; + }); + + it('light mode', () => { + const core = createEditorCore(div, {}); + + core.darkColorHandler = ({ + parseColorValue: (color: string) => ({ + lightModeColor: color == 'black' ? 'green' : color == 'white' ? 'yellow' : 'brown', + darkModeColor: color == 'black' ? 'blue' : color == 'white' ? 'red' : 'gray', + }), + } as any) as DarkColorHandler; - it('dark mode, has ogsb/ogsc', () => { - const core = createEditorCore(div, { inDarkMode: true }); div.innerHTML = - '
test
'; + '
test
'; const node = document.getElementById('div1'); const style = getStyleBasedFormatState(core, node); expect(style.fontName).toBe('arial'); expect(style.fontSize).toBe('12pt'); - expect(style.textColor).toBe('rgb(0, 0, 0)'); - expect(style.backgroundColor).toBe('rgba(0, 0, 0, 0)'); + expect(style.textColor).toBe('green'); + expect(style.backgroundColor).toBe('yellow'); expect(style.textColors).toEqual({ - darkModeColor: 'rgb(0, 0, 0)', - lightModeColor: 'ogsc', + lightModeColor: 'green', + darkModeColor: 'blue', }); expect(style.backgroundColors).toEqual({ - darkModeColor: 'rgba(0, 0, 0, 0)', - lightModeColor: 'ogsb', + lightModeColor: 'yellow', + darkModeColor: 'red', }); }); - it('dark mode, has ogab/ogac', () => { + it('dark mode, no color node', () => { const core = createEditorCore(div, { inDarkMode: true }); + core.darkColorHandler = ({ + parseColorValue: (color: string) => ({ + lightModeColor: color == 'black' ? 'green' : color == 'white' ? 'yellow' : 'brown', + darkModeColor: color == 'black' ? 'blue' : color == 'white' ? 'red' : 'gray', + }), + } as any) as DarkColorHandler; + div.innerHTML = - '
test
'; + '
test
'; const node = document.getElementById('div1'); const style = getStyleBasedFormatState(core, node); expect(style.fontName).toBe('arial'); expect(style.fontSize).toBe('12pt'); - expect(style.textColor).toBe('rgb(0, 0, 0)'); - expect(style.backgroundColor).toBe('rgba(0, 0, 0, 0)'); + expect(style.textColor).toBe('green'); + expect(style.backgroundColor).toBe('yellow'); expect(style.textColors).toEqual({ - darkModeColor: 'rgb(0, 0, 0)', - lightModeColor: 'ogac', + lightModeColor: 'green', + darkModeColor: 'blue', }); expect(style.backgroundColors).toEqual({ - darkModeColor: 'rgba(0, 0, 0, 0)', - lightModeColor: 'ogab', + lightModeColor: 'yellow', + darkModeColor: 'red', }); }); - it('dark mode, has both ogab/ogac and ogsb/ogsc', () => { + it('dark mode, no color', () => { const core = createEditorCore(div, { inDarkMode: true }); div.innerHTML = - '
test
'; + '
test
'; const node = document.getElementById('div1'); const style = getStyleBasedFormatState(core, node); expect(style.fontName).toBe('arial'); expect(style.fontSize).toBe('12pt'); - expect(style.textColor).toBe('rgb(0, 0, 0)'); - expect(style.backgroundColor).toBe('rgba(0, 0, 0, 0)'); - expect(style.textColors).toEqual({ - darkModeColor: 'rgb(0, 0, 0)', - lightModeColor: 'ogsc', - }); - expect(style.backgroundColors).toEqual({ - darkModeColor: 'rgba(0, 0, 0, 0)', - lightModeColor: 'ogab', - }); + expect(style.textColor).toBe(''); + expect(style.backgroundColor).toBe(''); + expect(style.textColors).toBeUndefined(); + expect(style.backgroundColors).toBeUndefined(); }); }); diff --git a/packages/roosterjs-editor-core/test/coreApi/insertNodeTest.ts b/packages/roosterjs-editor-core/test/coreApi/insertNodeTest.ts index 055208175b15..0921f429cd33 100644 --- a/packages/roosterjs-editor-core/test/coreApi/insertNodeTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/insertNodeTest.ts @@ -1,8 +1,8 @@ import createEditorCore from './createMockEditorCore'; +import { addRange, itFirefoxOnly, selectNode } from '../TestHelper'; import { ContentPosition } from 'roosterjs-editor-types'; import { getSelectionRange } from '../../lib/coreApi/getSelectionRange'; import { insertNode } from '../../lib/coreApi/insertNode'; -import { itFirefoxOnly, selectNode } from '../TestHelper'; describe('insertNode', () => { let div: HTMLDivElement; @@ -313,4 +313,181 @@ describe('insertNode', () => { expect((div.nextSibling).outerHTML).toBe(''); node.parentNode.removeChild(node); }); + + itFirefoxOnly( + 'insert at selection with focus, no replace, no new line, update cursor in a not content editable element', + () => { + const core = createEditorCore(div, {}); + const node = document.createElement('span'); + node.id = 'span1'; + div.contentEditable = 'true'; + div.innerHTML = + '
'; + div.focus(); + selectNode(document.getElementById('span2')); + insertNode(core, node, { + position: ContentPosition.SelectionStart, + insertOnNewLine: false, + updateCursor: true, + replaceSelection: true, + }); + + expect(div.innerHTML).toBe( + '
' + ); + + const range = getSelectionRange(core, false); + const span2 = document.getElementById('span1'); + + expect(range.startContainer).toBe(span2); + expect(range.endContainer).toBe(span2); + expect(range.startOffset).toBe(0); + expect(range.endOffset).toBe(0); + } + ); + + itFirefoxOnly('Insert Node in new line when selection inside of a table cell', () => { + const core = createEditorCore(div, {}); + const node = document.createElement('span'); + node.id = 'span1'; + div.contentEditable = 'true'; + div.innerHTML = '
'; + div.focus(); + selectNode(document.getElementById('span2')!); + insertNode(core, node, { + position: ContentPosition.SelectionStart, + insertOnNewLine: true, + updateCursor: true, + replaceSelection: true, + }); + + expect(div.innerHTML).toBe( + '
' + ); + + const range = getSelectionRange(core, false); + const span2 = document.getElementById('span1'); + + expect(range.startContainer).toBe(span2); + expect(range.endContainer).toBe(span2); + expect(range.startOffset).toBe(0); + expect(range.endOffset).toBe(0); + }); + + itFirefoxOnly( + 'Insert Node in new line when selection inside of a table cell between spans', + () => { + const core = createEditorCore(div, {}); + const node = document.createElement('span'); + node.id = 'span1'; + div.contentEditable = 'true'; + div.innerHTML = + '
Test1Test2
'; + div.focus(); + selectNode(document.getElementById('span2')!); + insertNode(core, node, { + position: ContentPosition.SelectionStart, + insertOnNewLine: true, + updateCursor: true, + replaceSelection: true, + }); + + expect(div.innerHTML).toBe( + '
Test1
Test2
' + ); + + const range = getSelectionRange(core, false); + const span2 = document.getElementById('span1'); + + expect(range.startContainer).toBe(span2); + expect(range.endContainer).toBe(span2); + expect(range.startOffset).toBe(0); + expect(range.endOffset).toBe(0); + } + ); + + itFirefoxOnly( + 'Insert Node in new line when selection inside of a table cell between spans', + () => { + const core = createEditorCore(div, {}); + const node = document.createElement('span'); + node.id = 'span1'; + div.contentEditable = 'true'; + div.innerHTML = + '
Test1
'; + div.focus(); + const sel = document.createRange(); + sel.setStart(document.getElementById('span2')!.firstChild!, 2); + addRange(sel); + + insertNode(core, node, { + position: ContentPosition.SelectionStart, + insertOnNewLine: true, + updateCursor: true, + replaceSelection: true, + }); + + expect(div.innerHTML).toBe( + '
Te
st1
' + ); + + const range = getSelectionRange(core, false); + const span2 = document.getElementById('span1'); + + expect(range.startContainer).toBe(span2); + expect(range.endContainer).toBe(span2); + expect(range.startOffset).toBe(0); + expect(range.endOffset).toBe(0); + } + ); + + it('Add a line between tables', () => { + const core = createEditorCore(div, {}); + const node = document.createElement('table'); + node.id = 'table1'; + div.contentEditable = 'true'; + div.innerHTML = '
'; + div.focus(); + const sel = document.createRange(); + sel.setStart(document.getElementById('div')!, 1); + addRange(sel); + + insertNode(core, node, { + position: ContentPosition.SelectionStart, + insertOnNewLine: false, + updateCursor: true, + replaceSelection: true, + }); + expect(div.innerHTML).toBe( + '

' + ); + }); + + it('Insert node at root of region', () => { + const core = createEditorCore(div, {}); + div.contentEditable = 'true'; + div.innerHTML = + '
textBefore
text
textAfter
'; + div.focus(); + + const text = div.querySelector('#innerDiv')!.firstChild!; + const sel = document.createRange(); + sel.setStart(text, 2); + sel.setEnd(text, 2); + addRange(sel); + + const nodeToInsert = document.createElement('div'); + nodeToInsert.id = 'newDiv'; + + insertNode(core, nodeToInsert, { + position: ContentPosition.SelectionStart, + insertOnNewLine: true, + updateCursor: true, + replaceSelection: true, + insertToRegionRoot: true, + }); + expect(div.innerHTML).toBe( + '
textBefore
te
xt
textAfter
' + ); + }); }); diff --git a/packages/roosterjs-editor-core/test/coreApi/restoreUndoSnapshotTest.ts b/packages/roosterjs-editor-core/test/coreApi/restoreUndoSnapshotTest.ts new file mode 100644 index 000000000000..95ae3726c462 --- /dev/null +++ b/packages/roosterjs-editor-core/test/coreApi/restoreUndoSnapshotTest.ts @@ -0,0 +1,145 @@ +import * as getEntityFromElement from 'roosterjs-editor-dom/lib/entity/getEntityFromElement'; +import * as queryElements from 'roosterjs-editor-dom/lib/utils/queryElements'; +import createEditorCore from './createMockEditorCore'; +import { ChangeSource, EntityOperation, PluginEventType } from 'roosterjs-editor-types'; +import { restoreUndoSnapshot } from '../../lib/coreApi/restoreUndoSnapshot'; + +describe('restoreUndoSnapshot', () => { + let div: HTMLDivElement | null; + + beforeEach(() => { + div = document.createElement('div'); + document.body.appendChild(div); + }); + + afterEach(() => { + document.body.removeChild(div!); + div = null; + }); + + it('Restore snapshot with -1', () => { + const addUndoSnapshot = jasmine.createSpy('addUndoSnapshot'); + const setContent = jasmine.createSpy('setContent'); + const html = 'test'; + const metadata = {}; + const move = jasmine.createSpy('move').and.returnValue({ + html, + metadata, + knownColors: [], + }); + + const core = createEditorCore(div!, { + coreApiOverride: { + addUndoSnapshot, + setContent, + }, + }); + core.undo.hasNewContent = true; + core.undo.snapshotsService.move = move; + + restoreUndoSnapshot(core, -1); + + expect(addUndoSnapshot).toHaveBeenCalledWith(core, null, null, false); + expect(move).toHaveBeenCalledWith(-1); + expect(setContent).toHaveBeenCalledWith(core, html, true, metadata); + expect(core.undo.isRestoring).toBeFalse(); + }); + + it('Restore snapshot with 1', () => { + const addUndoSnapshot = jasmine.createSpy('addUndoSnapshot'); + const setContent = jasmine.createSpy('setContent'); + const html = 'test'; + const metadata = {}; + const move = jasmine.createSpy('move').and.returnValue({ + html, + metadata, + knownColors: [], + }); + + const core = createEditorCore(div!, { + coreApiOverride: { + addUndoSnapshot, + setContent, + }, + }); + core.undo.hasNewContent = true; + core.undo.snapshotsService.move = move; + + restoreUndoSnapshot(core, 1); + + expect(addUndoSnapshot).not.toHaveBeenCalled(); + expect(move).toHaveBeenCalledWith(1); + expect(setContent).toHaveBeenCalledWith(core, html, true, metadata); + expect(core.undo.isRestoring).toBeFalse(); + }); + + it('Restore snapshot with entityState', () => { + const triggerEvent = jasmine.createSpy('triggerEvent'); + const move = jasmine.createSpy('move').and.returnValue({ + html: 'test', + metadata: {}, + knownColors: [], + entityStates: [ + { + type: 'Entity1', + id: 'Entity1_1', + state: 'state1', + }, + { + type: 'Entity2', + id: 'Entity2_1', + state: 'state2', + }, + ], + }); + const core = createEditorCore(div!, { + coreApiOverride: { + triggerEvent, + }, + }); + core.undo.snapshotsService.move = move; + + spyOn(queryElements, 'default').and.callFake((root: any, selector: any) => [selector]); + spyOn(getEntityFromElement, 'default').and.callFake((wrapper: any) => wrapper); + + restoreUndoSnapshot(core, 1); + + expect(triggerEvent).toHaveBeenCalledTimes(4); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.BeforeSetContent, + newContent: 'test', + }, + true + ); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.ContentChanged, + source: ChangeSource.SetContent, + }, + false + ); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.EntityOperation, + operation: EntityOperation.UpdateEntityState, + entity: '._Entity._EType_Entity1._EId_Entity1_1' as any, + state: 'state1', + }, + false + ); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.EntityOperation, + operation: EntityOperation.UpdateEntityState, + entity: '._Entity._EType_Entity2._EId_Entity2_1' as any, + state: 'state2', + }, + false + ); + }); +}); diff --git a/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts b/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts new file mode 100644 index 000000000000..4fe6b9853d9b --- /dev/null +++ b/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts @@ -0,0 +1,57 @@ +import createEditorCore from './createMockEditorCore'; +import { EditorCore, SelectionRangeTypes } from 'roosterjs-editor-types'; +import { selectImage } from '../../lib/coreApi/selectImage'; + +describe('selectImage |', () => { + let div: HTMLDivElement; + let image: HTMLImageElement | null; + let core: EditorCore | null; + + beforeEach(() => { + document.body.innerHTML = ''; + div = document.createElement('div'); + div.innerHTML = ''; + image = div.querySelector('img'); + document.body.appendChild(div); + core = createEditorCore(div!, {}); + }); + + afterEach(() => { + document.body.removeChild(div); + let style = document.getElementById('imageStylecontentDiv_0'); + if (style) { + document.head.removeChild(style); + } + div.parentElement?.removeChild(div); + core = null; + }); + + it('selectImage', () => { + const selectedInfo = selectImage(core, image); + const range = new Range(); + range.selectNode(image!); + + expect(selectedInfo).toEqual({ + type: SelectionRangeTypes.ImageSelection, + ranges: [range], + image: image, + areAllCollapsed: range.collapsed, + }); + }); + + it('image should have an unique id', () => { + selectImage(core, image); + expect(image!.id).toBe('imageSelected0'); + }); + + it('contentDiv should have an unique id', () => { + selectImage(core, image); + expect(core.contentDiv.id).toBe('contentDiv_0'); + }); + + it('styleTag should be created', () => { + selectImage(core, image); + const style = document.getElementById('imageStylecontentDiv_0'); + expect(style?.tagName).toBe('STYLE'); + }); +}); diff --git a/packages/roosterjs-editor-core/test/coreApi/selectTableTest.ts b/packages/roosterjs-editor-core/test/coreApi/selectTableTest.ts new file mode 100644 index 000000000000..5a147c275b28 --- /dev/null +++ b/packages/roosterjs-editor-core/test/coreApi/selectTableTest.ts @@ -0,0 +1,283 @@ +import createEditorCore from './createMockEditorCore'; +import { Browser } from 'roosterjs-editor-dom'; +import { EditorCore, TableSelection } from 'roosterjs-editor-types'; +import { selectTable } from '../../lib/coreApi/selectTable'; + +xdescribe('selectTable |', () => { + let div: HTMLDivElement | null; + let table: HTMLTableElement | null; + let core: EditorCore | null; + + beforeEach(() => { + div = document.createElement('div'); + div!.innerHTML = buildTableHTML(true /* tbody */); + + table = div!.querySelector('table'); + document.body.appendChild(div!); + + core = createEditorCore(div!, {}); + }); + + afterEach(() => { + let styles = document.querySelectorAll('#tableStylecontentDiv_0'); + styles.forEach(s => s.parentElement?.removeChild(s)); + + core = null; + div = null; + table = null; + document.body.innerHTML = ''; + }); + + it('Select Table Cells TR under Table Tag', () => { + div!.innerHTML = + '
TestTest
TestTest

'; + + selectTable(core, table, { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 1, y: 1 }, + }); + + const style = document.getElementById('tableStylecontentDiv_0') as HTMLStyleElement; + expect(style).toBeDefined(); + expect(style.sheet.cssRules[0]).toBeDefined(); + expect(style.sheet.cssRules[0].cssText).toEqual( + Browser.isFirefox + ? '#contentDiv_0 #tableSelected0 > TBODY > tr:nth-child(1) > TD:nth-child(1), #contentDiv_0 #tableSelected0 > TBODY > tr:nth-child(1) > TD:nth-child(2), #contentDiv_0 #tableSelected0 > TBODY > tr:nth-child(2) > TD:nth-child(1), #contentDiv_0 #tableSelected0 > TBODY > tr:nth-child(2) > TD:nth-child(2) { background-color: rgba(198, 198, 198, 0.7) !important; }' + : '#contentDiv_0 #tableSelected0 > tbody > tr:nth-child(1) > td:nth-child(1), #contentDiv_0 #tableSelected0 > tbody > tr:nth-child(1) > td:nth-child(2), #contentDiv_0 #tableSelected0 > tbody > tr:nth-child(2) > td:nth-child(1), #contentDiv_0 #tableSelected0 > tbody > tr:nth-child(2) > td:nth-child(2) { background-color: rgba(198, 198, 198, 0.7) !important; }' + ); + }); + + it('Select Table Cells TBODY', () => { + selectTable(core, table, { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 1, y: 1 }, + }); + + const style = document.getElementById('tableStylecontentDiv_0') as HTMLStyleElement; + expect(style).toBeDefined(); + expect(style.sheet.cssRules[0]).toBeDefined(); + expect(style.sheet.cssRules[0].cssText).toEqual( + Browser.isFirefox + ? '#contentDiv_0 #tableSelected0 > TBODY > tr:nth-child(1) > TD:nth-child(1), #contentDiv_0 #tableSelected0 > TBODY > tr:nth-child(1) > TD:nth-child(2), #contentDiv_0 #tableSelected0 > TBODY > tr:nth-child(2) > TD:nth-child(1), #contentDiv_0 #tableSelected0 > TBODY > tr:nth-child(2) > TD:nth-child(2) { background-color: rgba(198, 198, 198, 0.7) !important; }' + : '#contentDiv_0 #tableSelected0 > tbody > tr:nth-child(1) > td:nth-child(1), #contentDiv_0 #tableSelected0 > tbody > tr:nth-child(1) > td:nth-child(2), #contentDiv_0 #tableSelected0 > tbody > tr:nth-child(2) > td:nth-child(1), #contentDiv_0 #tableSelected0 > tbody > tr:nth-child(2) > td:nth-child(2) { background-color: rgba(198, 198, 198, 0.7) !important; }' + ); + }); + + it('Select TH and TR in the same row', () => { + div!.innerHTML = + '
TestTest
TestTest

'; + table = div!.querySelector('table'); + + selectTable(core, table, { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 1, y: 1 }, + }); + + const style = document.getElementById('tableStylecontentDiv_0') as HTMLStyleElement; + expect(style).toBeDefined(); + expect(style.sheet.cssRules[0]).toBeDefined(); + expect(style.sheet.cssRules[0].cssText).toEqual( + Browser.isFirefox + ? '#contentDiv_0 #tableSelected0 > TBODY > tr:nth-child(1) > TH:nth-child(1), #contentDiv_0 #tableSelected0 > TBODY > tr:nth-child(1) > TD:nth-child(2), #contentDiv_0 #tableSelected0 > TBODY > tr:nth-child(2) > TH:nth-child(1), #contentDiv_0 #tableSelected0 > TBODY > tr:nth-child(2) > TD:nth-child(2) { background-color: rgba(198, 198, 198, 0.7) !important; }' + : '#contentDiv_0 #tableSelected0 > tbody > tr:nth-child(1) > th:nth-child(1), #contentDiv_0 #tableSelected0 > tbody > tr:nth-child(1) > td:nth-child(2), #contentDiv_0 #tableSelected0 > tbody > tr:nth-child(2) > th:nth-child(1), #contentDiv_0 #tableSelected0 > tbody > tr:nth-child(2) > td:nth-child(2) { background-color: rgba(198, 198, 198, 0.7) !important; }' + ); + }); + + it('Select Table Cells THEAD, TBODY', () => { + div!.innerHTML = buildTableHTML(true /* tbody */, true /* thead */); + + table = div!.querySelector('table'); + + selectTable(core, table, { + firstCell: { x: 1, y: 1 }, + lastCell: { x: 2, y: 2 }, + }); + + const style = document.getElementById('tableStylecontentDiv_0') as HTMLStyleElement; + expect(style).toBeDefined(); + expect(style.sheet.cssRules[0]).toBeDefined(); + expect(style.sheet.cssRules[0].cssText).toEqual( + Browser.isFirefox + ? '#contentDiv_0 #tableSelected0 > THEAD > tr:nth-child(2) > TD:nth-child(2), #contentDiv_0 #tableSelected0 > TBODY > tr:nth-child(1) > TD:nth-child(2) { background-color: rgba(198, 198, 198, 0.7) !important; }' + : '#contentDiv_0 #tableSelected0 > thead > tr:nth-child(2) > td:nth-child(2), #contentDiv_0 #tableSelected0 > tbody > tr:nth-child(1) > td:nth-child(2) { background-color: rgba(198, 198, 198, 0.7) !important; }' + ); + }); + + it('Select Table Cells TBODY, TFOOT', () => { + div!.innerHTML = buildTableHTML(true /* tbody */, false /* thead */, true /* tfoot */); + + table = div!.querySelector('table'); + + selectTable(core, table, { + firstCell: { x: 1, y: 1 }, + lastCell: { x: 2, y: 2 }, + }); + + const style = document.getElementById('tableStylecontentDiv_0') as HTMLStyleElement; + expect(style).toBeDefined(); + expect(style.sheet.cssRules[0]).toBeDefined(); + expect(style.sheet.cssRules[0].cssText).toEqual( + Browser.isFirefox + ? '#contentDiv_0 #tableSelected0 > TBODY > tr:nth-child(2) > TD:nth-child(2), #contentDiv_0 #tableSelected0 > TFOOT > tr:nth-child(1) > TD:nth-child(2) { background-color: rgba(198, 198, 198, 0.7) !important; }' + : '#contentDiv_0 #tableSelected0 > tbody > tr:nth-child(2) > td:nth-child(2), #contentDiv_0 #tableSelected0 > tfoot > tr:nth-child(1) > td:nth-child(2) { background-color: rgba(198, 198, 198, 0.7) !important; }' + ); + }); + + it('Select Table Cells THEAD, TBODY, TFOOT', () => { + div!.innerHTML = buildTableHTML(true /* tbody */, true /* thead */, true /* tfoot */); + table = div!.querySelector('table'); + + selectTable(core, table, { + firstCell: { x: 1, y: 1 }, + lastCell: { x: 1, y: 4 }, + }); + + const style = document.getElementById('tableStylecontentDiv_0') as HTMLStyleElement; + expect(style).toBeDefined(); + expect(style.sheet.cssRules[0]).toBeDefined(); + expect(style.sheet.cssRules[0].cssText).toEqual( + Browser.isFirefox + ? '#contentDiv_0 #tableSelected0 > THEAD > tr:nth-child(2) > TD:nth-child(2), #contentDiv_0 #tableSelected0 > TBODY > tr:nth-child(1) > TD:nth-child(2), #contentDiv_0 #tableSelected0 > TBODY > tr:nth-child(2) > TD:nth-child(2), #contentDiv_0 #tableSelected0 > TFOOT > tr:nth-child(1) > TD:nth-child(2) { background-color: rgba(198, 198, 198, 0.7) !important; }' + : '#contentDiv_0 #tableSelected0 > thead > tr:nth-child(2) > td:nth-child(2), #contentDiv_0 #tableSelected0 > tbody > tr:nth-child(1) > td:nth-child(2), #contentDiv_0 #tableSelected0 > tbody > tr:nth-child(2) > td:nth-child(2), #contentDiv_0 #tableSelected0 > tfoot > tr:nth-child(1) > td:nth-child(2) { background-color: rgba(198, 198, 198, 0.7) !important; }' + ); + }); + + it('Select Table Cells THEAD, TFOOT', () => { + div!.innerHTML = buildTableHTML(false /* tbody */, true /* thead */, true /* tfoot */); + table = div!.querySelector('table'); + + selectTable(core, table, { + firstCell: { x: 1, y: 1 }, + lastCell: { x: 1, y: 2 }, + }); + + const style = document.getElementById('tableStylecontentDiv_0') as HTMLStyleElement; + expect(style).toBeDefined(); + expect(style.sheet.cssRules[0]).toBeDefined(); + expect(style.sheet.cssRules[0].cssText).toEqual( + Browser.isFirefox + ? '#contentDiv_0 #tableSelected0 > THEAD > tr:nth-child(2) > TD:nth-child(2), #contentDiv_0 #tableSelected0 > TFOOT > tr:nth-child(1) > TD:nth-child(2) { background-color: rgba(198, 198, 198, 0.7) !important; }' + : '#contentDiv_0 #tableSelected0 > thead > tr:nth-child(2) > td:nth-child(2), #contentDiv_0 #tableSelected0 > tfoot > tr:nth-child(1) > td:nth-child(2) { background-color: rgba(198, 198, 198, 0.7) !important; }' + ); + }); + + it('remove duplicated ID', () => { + const tableHTML = buildTableHTML(true); + div!.innerHTML = tableHTML + '' + tableHTML; + + const tables = div!.querySelectorAll('table'); + table = tables[0]; + tables.forEach(table => (table.id = 'DuplicatedId')); + + selectTable(core, table, { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 0, y: 0 }, + }); + + expect(table.id).not.toEqual(tables[1].id); + }); + + describe('Null scenarios |', () => { + it('Null table selection', () => { + const core = createEditorCore(div!, {}); + selectTable(core, table, null); + + expect(document.getElementById('tableStylecontentDiv_0')).toBeNull(); + }); + + it('Null first cell coordinates', () => { + selectTable(core, table, { + firstCell: null, + lastCell: { x: 1, y: 1 }, + }); + + expect(document.getElementById('tableStylecontentDiv_0')).toBeNull(); + }); + + it('Null last cell coordinates', () => { + selectTable(core, table, { + firstCell: { x: 1, y: 1 }, + lastCell: null, + }); + + expect(document.getElementById('tableStylecontentDiv_0')).toBeNull(); + }); + + it('Null first cell y coordinate', () => { + selectTable(core, table, { + firstCell: { x: 0, y: null }, + lastCell: { x: 1, y: 1 }, + }); + + expect(document.getElementById('tableStylecontentDiv_0')).toBeNull(); + }); + + it('Null first cell x coordinate', () => { + selectTable(core, table, { + firstCell: { x: null, y: 0 }, + lastCell: { x: 1, y: 1 }, + }); + + expect(document.getElementById('tableStylecontentDiv_0')).toBeNull(); + }); + + it('Null last cell y coordinate', () => { + selectTable(core, table, { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 1, y: null }, + }); + + expect(document.getElementById('tableStylecontentDiv_0')).toBeNull(); + }); + + xit('Null last cell x coordinate', () => { + selectTable(core, table, { + firstCell: { x: 0, y: 0 }, + lastCell: { x: null, y: 1 }, + }); + + expect(document.getElementById('tableStylecontentDiv_0')).toBeNull(); + }); + + it('Null last cell x & y coordinate', () => { + selectTable(core, table, { + firstCell: { x: 0, y: 0 }, + lastCell: { x: null, y: null }, + }); + + expect(document.getElementById('tableStylecontentDiv_0')).toBeNull(); + }); + + it('Null first cell x & y coordinate', () => { + selectTable(core, table, { + lastCell: { x: 0, y: 0 }, + firstCell: { x: null, y: null }, + }); + + expect(document.getElementById('tableStylecontentDiv_0')).toBeNull(); + }); + }); +}); + +function buildTableHTML(tbody: boolean, thead: boolean = false, tfoot: boolean = false) { + let table = '
'; + + if (thead) { + table += + ''; + } + + if (tbody) { + table += + ''; + } + + if (tfoot) { + table += + ''; + } + + table += '
TestTest
TestTest
TestTest
TestTest
TestTest
TestTest

'; + + return table; +} diff --git a/packages/roosterjs-editor-core/test/coreApi/setContentTest.ts b/packages/roosterjs-editor-core/test/coreApi/setContentTest.ts index 9939187c5b9e..4a117d80f629 100644 --- a/packages/roosterjs-editor-core/test/coreApi/setContentTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/setContentTest.ts @@ -1,6 +1,13 @@ +import * as createRange from 'roosterjs-editor-dom/lib/selection/createRange'; +import * as entityPlaceholderUtils from 'roosterjs-editor-dom/lib/entity/entityPlaceholderUtils'; import createEditorCore from './createMockEditorCore'; -import { ChangeSource, PluginEventType } from 'roosterjs-editor-types'; import { setContent } from '../../lib/coreApi/setContent'; +import { + ChangeSource, + ContentMetadata, + PluginEventType, + SelectionRangeTypes, +} from 'roosterjs-editor-types'; describe('setContent', () => { let div: HTMLDivElement; @@ -67,7 +74,7 @@ describe('setContent', () => { expect(div.innerHTML).toBe('
test
'); const range = core.domEvent.selectionRange; - const textNode = div.firstChild.firstChild; + const textNode = div.firstChild!.firstChild; expect(range.startContainer).toBe(textNode); expect(range.endContainer).toBe(textNode); expect(range.startOffset).toBe(1); @@ -76,17 +83,13 @@ describe('setContent', () => { it('dark mode', () => { const triggerEvent = jasmine.createSpy(); - const onExternalContentTransform = jasmine.createSpy(); const core = createEditorCore(div, { coreApiOverride: { triggerEvent }, inDarkMode: true, - onExternalContentTransform, }); div.innerHTML = 'test'; setContent(core, '
test
', true); expect(div.innerHTML).toBe('
test
'); - expect(onExternalContentTransform).toHaveBeenCalledTimes(1); - expect(onExternalContentTransform).toHaveBeenCalledWith(div.firstChild); expect(triggerEvent).toHaveBeenCalledWith( core, { @@ -96,4 +99,125 @@ describe('setContent', () => { false ); }); + + it('setContent with entity map', () => { + const core = createEditorCore(div, { + coreApiOverride: {}, + }); + const entity = document.createElement('div'); + + entity.id = 'div1'; + + const entityMapMock = 'ENTITYMAP' as any; + core.entity.entityMap = entityMapMock; + + const restoreContentWithEntityPlaceholderSpy = spyOn( + entityPlaceholderUtils, + 'restoreContentWithEntityPlaceholder' + ); + + setContent(core, 'test', false); + + expect(restoreContentWithEntityPlaceholderSpy).toHaveBeenCalledTimes(1); + expect(restoreContentWithEntityPlaceholderSpy.calls.argsFor(0)[2]).toBe(entityMapMock); + }); +}); + +describe('setContent and metadata', () => { + let div: HTMLDivElement; + beforeEach(() => { + div = document.createElement('div'); + document.body.appendChild(div); + }); + + afterEach(() => { + document.body.removeChild(div); + div = null; + }); + + const selectionPath = { start: [1], end: [2] }; + const htmlContent = '
test
'; + + function runNormalMetadataTest(newContent: string, metadata: ContentMetadata) { + const triggerEvent = jasmine.createSpy('triggerEvent'); + const selectRange = jasmine.createSpy('selectRange'); + const transformColor = jasmine.createSpy('transformColor'); + const core = createEditorCore(div, { + coreApiOverride: { triggerEvent, selectRange }, + inDarkMode: true, + }); + const range = {}; + div.innerHTML = 'test'; + + spyOn(createRange, 'default').and.returnValue(range); + + setContent(core, newContent, true, metadata); + + expect(div.innerHTML).toBe(htmlContent); + expect(triggerEvent).toHaveBeenCalledTimes(2); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.BeforeSetContent, + newContent: newContent, + }, + true + ); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.ContentChanged, + source: ChangeSource.SetContent, + }, + false + ); + expect(createRange.default).toHaveBeenCalledWith( + div, + selectionPath.start, + selectionPath.end + ); + expect(selectRange).toHaveBeenCalledWith(core, range); + expect(transformColor).not.toHaveBeenCalled(); + } + + it('setContent with metadata - standalone metadata', () => { + runNormalMetadataTest(htmlContent, { + type: SelectionRangeTypes.Normal, + isDarkMode: false, + ...selectionPath, + }); + }); + + it('setContent with metadata - embedded metadata', () => { + runNormalMetadataTest( + htmlContent + + '', + undefined + ); + }); + + it('setContent with metadata - both', () => { + runNormalMetadataTest( + htmlContent + + '', + { + type: SelectionRangeTypes.Normal, + isDarkMode: false, + ...selectionPath, + } + ); + }); }); diff --git a/packages/roosterjs-editor-core/test/coreApi/transformColorTest.ts b/packages/roosterjs-editor-core/test/coreApi/transformColorTest.ts index efecb943f1f5..6a7b0db98f8a 100644 --- a/packages/roosterjs-editor-core/test/coreApi/transformColorTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/transformColorTest.ts @@ -1,11 +1,12 @@ import createEditorCore from './createMockEditorCore'; -import { ColorTransformDirection } from 'roosterjs-editor-types'; +import { ColorTransformDirection, DarkColorHandler } from 'roosterjs-editor-types'; import { getDarkColor } from 'roosterjs-color-utils'; -import { itFirefoxOnly } from '../TestHelper'; +import { itChromeOnly } from '../TestHelper'; import { transformColor } from '../../lib/coreApi/transformColor'; -describe('transformColor Dark to light', () => { +describe('transform to dark mode v2', () => { let div: HTMLDivElement; + beforeEach(() => { div = document.createElement('div'); document.body.appendChild(div); @@ -16,130 +17,101 @@ describe('transformColor Dark to light', () => { div = null; }); - it('null input', () => { - const core = createEditorCore(div, {}); - transformColor(core, null, true, null, ColorTransformDirection.DarkToLight); - expect(); - }); + function runTest( + element: HTMLElement, + expectedHtml: string, + expectedParseValueCalls: string[], + expectedRegisterColorCalls: [string, boolean, string][] + ) { + const core = createEditorCore(div, { + inDarkMode: false, + getDarkColor, + }); + const parseColorValue = jasmine + .createSpy('parseColorValue') + .and.callFake((color: string) => ({ + lightModeColor: color == 'red' ? 'blue' : color == 'green' ? 'yellow' : '', + })); + const registerColor = jasmine + .createSpy('registerColor') + .and.callFake((color: string) => color); - it('light mode, no need to transform', () => { - const core = createEditorCore(div, { inDarkMode: false }); - const element = document.createElement('div'); - element.dataset.ogsc = '#123456'; - transformColor(core, element, true, null, ColorTransformDirection.DarkToLight); - expect(element.outerHTML).toBe('
'); - }); + core.darkColorHandler = ({ parseColorValue, registerColor } as any) as DarkColorHandler; - it('callback must be called', () => { - const core = createEditorCore(div, { inDarkMode: false }); - const callback = jasmine.createSpy('callback'); - transformColor(core, null, true, callback, ColorTransformDirection.DarkToLight); - expect(callback).toHaveBeenCalled(); - }); + transformColor(core, element, true, null, ColorTransformDirection.LightToDark, true); - it('no dataset, no style, no attr', () => { - const core = createEditorCore(div, { inDarkMode: true }); - const element = document.createElement('div'); - transformColor(core, element, true, null, ColorTransformDirection.DarkToLight); - expect(element.outerHTML).toBe('
'); - }); - - it('no dataset, no style, has attr', () => { - const core = createEditorCore(div, { inDarkMode: true }); - const element = document.createElement('div'); - element.setAttribute('color', 'red'); - transformColor(core, element, true, null, ColorTransformDirection.DarkToLight); - expect(element.outerHTML).toBe('
'); - }); + expect(element.outerHTML).toBe(expectedHtml); + expect(parseColorValue).toHaveBeenCalledTimes(expectedParseValueCalls.length); + expect(registerColor).toHaveBeenCalledTimes(expectedRegisterColorCalls.length); - it('no dataset, has style, no attr', () => { - const core = createEditorCore(div, { inDarkMode: true }); - const element = document.createElement('div'); - element.style.color = 'red'; - transformColor(core, element, true, null, ColorTransformDirection.DarkToLight); - expect(element.outerHTML).toBe('
'); - }); - - it('has dataset, no style, no attr', () => { - const core = createEditorCore(div, { inDarkMode: true }); - const element = document.createElement('div'); - element.dataset.ogsc = 'red'; - transformColor(core, element, true, null, ColorTransformDirection.DarkToLight); - expect(element.outerHTML).toBe('
'); - }); + expectedParseValueCalls.forEach(v => { + expect(parseColorValue).toHaveBeenCalledWith(v, false); + }); + expectedRegisterColorCalls.forEach(v => { + expect(registerColor).toHaveBeenCalledWith(...v); + }); + } - it('has dataset, has style, no attr', () => { - const core = createEditorCore(div, { inDarkMode: true }); + it('no color', () => { const element = document.createElement('div'); - element.dataset.ogsc = 'red'; - element.style.color = 'black'; - transformColor(core, element, true, null, ColorTransformDirection.DarkToLight); - expect(element.outerHTML).toBe('
'); - }); - it('has dataset, no style, has attr', () => { - const core = createEditorCore(div, { inDarkMode: true }); - const element = document.createElement('div'); - element.dataset.ogsc = 'red'; - element.setAttribute('color', 'black'); - transformColor(core, element, true, null, ColorTransformDirection.DarkToLight); - expect(element.outerHTML).toBe('
'); + runTest(element, '
', [null!, null!], []); }); - it('has dataset, has style, has attr', () => { - const core = createEditorCore(div, { inDarkMode: true }); + it('has style colors', () => { const element = document.createElement('div'); - element.dataset.ogsc = 'red'; - element.setAttribute('color', 'black'); - element.style.color = 'green'; - transformColor(core, element, true, null, ColorTransformDirection.DarkToLight); - expect(element.outerHTML).toBe('
'); - }); + element.style.color = 'red'; + element.style.backgroundColor = 'green'; - it('has dataset for ogsc and ogac, has style, has attr', () => { - const core = createEditorCore(div, { inDarkMode: true }); - const element = document.createElement('div'); - element.dataset.ogsc = 'red'; - element.dataset.ogac = 'yellow'; - element.setAttribute('color', 'black'); - element.style.color = 'green'; - transformColor(core, element, true, null, ColorTransformDirection.DarkToLight); - expect(element.outerHTML).toBe('
'); + runTest( + element, + '
', + ['red', 'green'], + [ + ['blue', true, undefined!], + ['yellow', true, undefined!], + ] + ); }); - it('has dataset for ogsc, ogac, ogsb, ogab, has style, has attr', () => { - const core = createEditorCore(div, { inDarkMode: true }); + it('has attribute colors', () => { const element = document.createElement('div'); - element.dataset.ogsc = 'red'; - element.dataset.ogac = 'yellow'; - element.dataset.ogsb = 'blue'; - element.dataset.ogab = 'gray'; - element.setAttribute('color', 'black'); - element.setAttribute('bgcolor', '#012345'); - element.style.color = 'green'; - element.style.backgroundColor = '#654321'; - transformColor(core, element, true, null, ColorTransformDirection.DarkToLight); - expect(element.outerHTML).toBe( - '
' + element.setAttribute('color', 'red'); + element.setAttribute('bgcolor', 'green'); + + runTest( + element, + '
', + ['red', 'green'], + [ + ['blue', true, undefined!], + ['yellow', true, undefined!], + ] ); }); - it('do not include self', () => { - const core = createEditorCore(div, { inDarkMode: true }); + itChromeOnly('has both css and attribute colors', () => { const element = document.createElement('div'); - element.dataset.ogsc = 'red'; - const child = document.createElement('div'); - child.dataset.ogsc = 'green'; - element.appendChild(child); - transformColor(core, element, false, null, ColorTransformDirection.DarkToLight); - expect(element.outerHTML).toBe( - '
' + element.style.color = 'red'; + element.style.backgroundColor = 'green'; + element.setAttribute('color', 'gray'); + element.setAttribute('bgcolor', 'brown'); + + runTest( + element, + '
', + ['red', 'green'], + [ + ['blue', true, undefined!], + ['yellow', true, undefined!], + ] ); }); }); -describe('transformColor Light to dark', () => { +describe('transform to light mode v2', () => { let div: HTMLDivElement; + beforeEach(() => { div = document.createElement('div'); document.body.appendChild(div); @@ -150,65 +122,101 @@ describe('transformColor Light to dark', () => { div = null; }); - it('null input', () => { - const core = createEditorCore(div, { inDarkMode: true }); - transformColor(core, null, true, null, ColorTransformDirection.LightToDark); - expect(); - }); + function runTest( + element: HTMLElement, + expectedHtml: string, + expectedParseValueCalls: string[], + expectedRegisterColorCalls: [string, boolean, string][] + ) { + const core = createEditorCore(div, { + getDarkColor, + }); + const parseColorValue = jasmine + .createSpy('parseColorValue') + .and.callFake((color: string) => ({ + lightModeColor: color == 'red' ? 'blue' : color == 'green' ? 'yellow' : '', + })); + const registerColor = jasmine + .createSpy('registerColor') + .and.callFake((color: string) => color); + + core.darkColorHandler = ({ parseColorValue, registerColor } as any) as DarkColorHandler; + + transformColor( + core, + element, + true /*includeSelf*/, + null /*callback*/, + ColorTransformDirection.DarkToLight, + true /*forceTransform*/, + true /*fromDark*/ + ); - it('light mode, no need to transform', () => { - const core = createEditorCore(div, { inDarkMode: false }); - const element = document.createElement('div'); - element.style.color = 'rgb(18, 52, 86)'; - transformColor(core, element, true, null, ColorTransformDirection.LightToDark); - expect(element.outerHTML).toBe('
'); - }); + expect(element.outerHTML).toBe(expectedHtml); + expect(parseColorValue).toHaveBeenCalledTimes(expectedParseValueCalls.length); + expect(registerColor).toHaveBeenCalledTimes(expectedRegisterColorCalls.length); - it('single element, no transform function', () => { - const core = createEditorCore(div, { inDarkMode: true }); + expectedParseValueCalls.forEach(v => { + expect(parseColorValue).toHaveBeenCalledWith(v, true); + }); + expectedRegisterColorCalls.forEach(v => { + expect(registerColor).toHaveBeenCalledWith(...v); + }); + } + + it('no color', () => { const element = document.createElement('div'); - transformColor(core, element, true, null, ColorTransformDirection.LightToDark); - expect(element.outerHTML).toBe('
'); + + runTest(element, '
', [null!, null!], []); }); - it('single element with color and background color, no transform function', () => { - const core = createEditorCore(div, { inDarkMode: true, getDarkColor }); + it('has style colors', () => { const element = document.createElement('div'); element.style.color = 'red'; element.style.backgroundColor = 'green'; - transformColor(core, element, true, null, ColorTransformDirection.LightToDark); - expect(element.outerHTML).toBe( - '
' + + runTest( + element, + '
', + ['red', 'green'], + [ + ['blue', false, undefined!], + ['yellow', false, undefined!], + ] ); }); - itFirefoxOnly('single element with color and background color, has transform function', () => { - const core = createEditorCore(div, { - inDarkMode: true, - onExternalContentTransform: element => { - element.dataset.ogsc = element.style.color; - element.dataset.ogsb = element.style.backgroundColor; - element.style.color = 'white'; - element.style.backgroundColor = 'black'; - }, - }); + it('has attribute colors', () => { const element = document.createElement('div'); - element.style.color = 'red'; - element.style.backgroundColor = 'green'; - transformColor(core, element, true, null, ColorTransformDirection.LightToDark); - expect(element.outerHTML).toBe( - '
' + element.setAttribute('color', 'red'); + element.setAttribute('bgcolor', 'green'); + + runTest( + element, + '
', + ['red', 'green'], + [ + ['blue', false, undefined!], + ['yellow', false, undefined!], + ] ); }); - it('single element with inherit color', () => { - const core = createEditorCore(div, { inDarkMode: true, getDarkColor }); + itChromeOnly('has both css and attribute colors', () => { const element = document.createElement('div'); - element.style.color = 'inherit'; - element.style.backgroundColor = 'inherit'; - transformColor(core, element, true, null, ColorTransformDirection.LightToDark); - expect(element.outerHTML).toBe( - '
' + element.style.color = 'red'; + element.style.backgroundColor = 'green'; + element.setAttribute('color', 'gray'); + element.setAttribute('bgcolor', 'brown'); + + runTest( + element, + '
', + ['red', 'green'], + [ + ['blue', false, undefined!], + ['yellow', false, undefined!], + ] ); }); }); diff --git a/packages/roosterjs-editor-core/test/coreApi/triggerEventTest.ts b/packages/roosterjs-editor-core/test/coreApi/triggerEventTest.ts index 3e5d99c5f98c..a494cdeea98a 100644 --- a/packages/roosterjs-editor-core/test/coreApi/triggerEventTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/triggerEventTest.ts @@ -84,7 +84,7 @@ describe('triggerEvent', () => { const core = createEditorCore(div, { plugins: [createPlugin(onPluginEvent)], }); - const event = createDefaultEvent(PluginEventType.EditorReady); + const event = createDefaultEvent(PluginEventType.KeyDown); core.lifecycle.shadowEditFragment = document.createDocumentFragment(); triggerEvent(core, event, false); expect(onPluginEvent).not.toHaveBeenCalled(); @@ -105,9 +105,10 @@ describe('triggerEvent', () => { function createDefaultEvent( type: | PluginEventType.EditorReady - | PluginEventType.BeforeDispose = PluginEventType.BeforeDispose + | PluginEventType.BeforeDispose + | PluginEventType.KeyDown = PluginEventType.BeforeDispose ): PluginEvent { - return { eventType: type }; + return ({ eventType: type }); } function createPlugin(onPluginEvent: any, willHandleEventExclusively?: any): EditorPlugin { diff --git a/packages/roosterjs-editor-core/test/coreApi/utils/addUniqueIdTest.ts b/packages/roosterjs-editor-core/test/coreApi/utils/addUniqueIdTest.ts new file mode 100644 index 000000000000..e9cfe39bbd8e --- /dev/null +++ b/packages/roosterjs-editor-core/test/coreApi/utils/addUniqueIdTest.ts @@ -0,0 +1,40 @@ +import addUniqueId from '../../../lib/coreApi/utils/addUniqueId'; + +describe('addUniqueId', () => { + let div: HTMLDivElement; + beforeEach(() => { + div = document.createElement('div'); + document.body.appendChild(div); + }); + + afterEach(() => { + document.body.removeChild(div); + div = null; + }); + + it('should add an id ', () => { + addUniqueId(div, 'test'); + expect(div.id).toBe('test0'); + }); + + it('should unique id ', () => { + const span = document.createElement('span'); + document.body.appendChild(span); + addUniqueId(div, 'test'); + addUniqueId(span, 'test'); + expect(div.id).toBe('test0'); + expect(span.id).toBe('test1'); + document.body.removeChild(span); + }); + + it('should replace existing ids', () => { + const span = document.createElement('span'); + span.id = 'test0'; + document.body.appendChild(span); + addUniqueId(div, 'test'); + addUniqueId(span, 'test'); + expect(div.id).toBe('test1'); + expect(span.id).toBe('test0'); + document.body.removeChild(span); + }); +}); diff --git a/packages/roosterjs-editor-core/test/corePlugins/copyPastePluginTest.ts b/packages/roosterjs-editor-core/test/corePlugins/copyPastePluginTest.ts index 8a1f63883e9c..87b5c8c80caa 100644 --- a/packages/roosterjs-editor-core/test/corePlugins/copyPastePluginTest.ts +++ b/packages/roosterjs-editor-core/test/corePlugins/copyPastePluginTest.ts @@ -1,249 +1,298 @@ -import * as addRangeToSelection from 'roosterjs-editor-dom/lib/selection/addRangeToSelection'; -import * as extractClipboardEvent from 'roosterjs-editor-dom/lib/clipboard/extractClipboardEvent'; -import CopyPastePlugin from '../../lib/corePlugins/CopyPastePlugin'; -import { Position } from 'roosterjs-editor-dom'; -import { - ClipboardData, - DOMEventHandlerFunction, - IEditor, - PluginEventType, - SelectionRangeTypes, -} from 'roosterjs-editor-types'; - -describe('CopyPastePlugin paste', () => { - let plugin: CopyPastePlugin; - let handler: Record; - let paste: jasmine.Spy; - let tempNode: HTMLElement = null; - let addDomEventHandler: jasmine.Spy; - - beforeEach(() => { - handler = null; - plugin = new CopyPastePlugin({}); - - addDomEventHandler = jasmine - .createSpy('addDomEventHandler') - .and.callFake((handlerParam: Record) => { - handler = handlerParam; - return () => { - handler = null; - }; - }); - - paste = jasmine.createSpy('paste'); - - plugin.initialize(({ - addDomEventHandler: addDomEventHandler, - paste, - getSelectionRange: (): Range => null, - getCustomData: (key: string, getter: () => any) => getter(), - insertNode: (node: HTMLElement) => { - tempNode = node; - document.body.appendChild(node); - }, - runAsync: (callback: () => void) => { - if (tempNode) { - tempNode.innerHTML = 'test html'; - } - callback(); - }, - getDocument: () => document, - select: () => {}, - isFeatureEnabled: () => false, - })); - }); - - afterEach(() => { - plugin.dispose(); - if (tempNode) { - tempNode.parentNode.removeChild(tempNode); - tempNode = null; - } - }); - - it('init and dispose', () => { - expect(addDomEventHandler).toHaveBeenCalled(); - const parameter = addDomEventHandler.calls.argsFor(0)[0]; - expect(Object.keys(parameter)).toEqual(['paste', 'copy', 'cut']); - }); - - it('trigger paste event for html', () => { - const items: ClipboardData = { - rawHtml: '', - text: '', - image: null, - types: [], - customValues: {}, - }; - spyOn(extractClipboardEvent, 'default').and.callFake((event, callback) => { - callback(items); - }); - - handler.paste({}); - expect(paste).toHaveBeenCalledWith(items); - expect(tempNode).toBeNull(); - }); -}); - -describe('CopyPastePlugin copy', () => { - let plugin: CopyPastePlugin; - let handler: Record; - let editor: IEditor; - let tempNode: HTMLElement = null; - let addDomEventHandler: jasmine.Spy; - let triggerPluginEvent: jasmine.Spy; - - beforeEach(() => { - handler = null; - plugin = new CopyPastePlugin({}); - - addDomEventHandler = jasmine - .createSpy('addDomEventHandler') - .and.callFake((handlerParam: Record) => { - handler = handlerParam; - return () => { - handler = null; - }; - }); - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - spyOn(addRangeToSelection, 'default'); - - editor = ({ - addDomEventHandler, - triggerPluginEvent, - getSelectionRange: () => { collapsed: false }, - getContent: () => '
test
', - getCustomData: (key: string, getter: () => any) => getter(), - insertNode: (node: HTMLElement) => { - tempNode = node; - document.body.appendChild(node); - }, - getDocument: () => document, - select: () => {}, - addUndoSnapshot: (f: () => void) => f(), - focus: () => {}, - getTrustedHTMLHandler: (html: string) => html, - getSelectionRangeEx: () => { - return { - type: SelectionRangeTypes.Normal, - ranges: [{ collapsed: false }], - areAllCollapsed: false, - }; - }, - }); - - plugin.initialize(editor); - }); - - afterEach(() => { - plugin.dispose(); - if (tempNode) { - tempNode.parentNode.removeChild(tempNode); - tempNode = null; - } - }); - - it('before copy', () => { - editor.runAsync = () => null; - - const event = {}; - handler.copy(event); - - expect(triggerPluginEvent).toHaveBeenCalledTimes(1); - expect(triggerPluginEvent.calls.argsFor(0)[0]).toBe(PluginEventType.BeforeCutCopy); - expect(tempNode.innerHTML).toBe('
test
'); - - const range = (addRangeToSelection.default).calls.argsFor(0)[0]; - expect(range.startContainer).toBe(tempNode.firstChild); - expect(range.endContainer).toBe(tempNode.firstChild); - expect(range.startOffset).toBe(0); - expect(range.endOffset).toBe(1); - }); - - it('after copy', () => { - editor.runAsync = callback => { - callback(editor); - return null; - }; - - const event = {}; - handler.copy(event); - - expect(triggerPluginEvent).toHaveBeenCalledTimes(1); - expect(tempNode.innerHTML).toBe(''); - }); - - it('after cut with text content', () => { - editor.runAsync = callback => { - callback(editor); - return null; - }; - const contentDiv = document.createElement('div'); - contentDiv.innerHTML = 'This is a test'; - - editor.getSelectedRegions = () => [ - { - rootNode: contentDiv, - nodeBefore: null, - nodeAfter: null, - skipTags: [], - fullSelectionStart: new Position(contentDiv.firstChild, 3), - fullSelectionEnd: new Position(contentDiv.firstChild, 10), - }, - ]; - - editor.deleteSelectedContent = () => { - let html = contentDiv.innerHTML; - html = html.substr(0, 3) + html.substr(10); - contentDiv.innerHTML = html; - return null; - }; - - const event = {}; - handler.cut(event); - - expect(triggerPluginEvent).toHaveBeenCalledTimes(1); - expect(tempNode.innerHTML).toBe(''); - expect(contentDiv.innerHTML).toBe('Thitest'); - }); - - it('after cut with html content', () => { - editor.runAsync = callback => { - callback(editor); - return null; - }; - const contentDiv = document.createElement('div'); - contentDiv.innerHTML = - '
  1. line1
  2. line2
    line3
  3. line4
line5
'; - - editor.getSelectedRegions = () => [ - { - rootNode: contentDiv, - nodeBefore: null, - nodeAfter: null, - skipTags: [], - fullSelectionStart: new Position( - contentDiv.childNodes[0].childNodes[1].childNodes[1].childNodes[0], - 3 - ), - fullSelectionEnd: new Position(contentDiv.childNodes[1].childNodes[0], 2), - }, - ]; - - editor.deleteSelectedContent = () => { - let html = contentDiv.innerHTML; - html = html.substr(0, 46) + '
' + html.substr(85); - contentDiv.innerHTML = html; - return null; - }; - - const event = {}; - handler.cut(event); - - expect(triggerPluginEvent).toHaveBeenCalledTimes(1); - expect(tempNode.innerHTML).toBe(''); - expect(contentDiv.innerHTML).toBe( - '
  1. line1
  2. line2
    lin
ne5
' - ); - }); -}); +import * as addRangeToSelection from 'roosterjs-editor-dom/lib/selection/addRangeToSelection'; +import * as extractClipboardEvent from 'roosterjs-editor-dom/lib/clipboard/extractClipboardEvent'; +import CopyPastePlugin from '../../lib/corePlugins/CopyPastePlugin'; +import { Position } from 'roosterjs-editor-dom'; +import { + ClipboardData, + DOMEventHandlerFunction, + IEditor, + PluginEventType, + SelectionRangeTypes, + BeforeCutCopyEvent, +} from 'roosterjs-editor-types'; + +describe('CopyPastePlugin paste', () => { + let plugin: CopyPastePlugin; + let handler: Record; + let paste: jasmine.Spy; + let tempNode: HTMLElement = null; + let addDomEventHandler: jasmine.Spy; + + function getEditor(disposeResult: boolean = false) { + handler = null; + plugin = new CopyPastePlugin({}); + + addDomEventHandler = jasmine + .createSpy('addDomEventHandler') + .and.callFake((handlerParam: Record) => { + handler = handlerParam; + return () => { + handler = null; + }; + }); + + paste = jasmine.createSpy('paste'); + + return ({ + addDomEventHandler: addDomEventHandler, + paste, + getSelectionRange: (): Range => null, + getCustomData: (key: string, getter: () => any) => getter(), + insertNode: (node: HTMLElement) => { + tempNode = node; + document.body.appendChild(node); + }, + runAsync: (callback: () => void) => { + if (tempNode) { + tempNode.innerHTML = 'test html'; + } + callback(); + }, + getDocument: () => document, + select: () => {}, + isFeatureEnabled: () => false, + isDisposed: () => disposeResult, + }); + } + + beforeEach(() => { + plugin = new CopyPastePlugin({}); + }); + + afterEach(() => { + plugin.dispose(); + if (tempNode) { + tempNode.parentNode.removeChild(tempNode); + tempNode = null; + } + }); + + it('init and dispose', () => { + plugin.initialize(getEditor()); + expect(addDomEventHandler).toHaveBeenCalled(); + const parameter = addDomEventHandler.calls.argsFor(0)[0]; + expect(Object.keys(parameter)).toEqual(['paste', 'copy', 'cut']); + }); + + it('trigger paste event for html', () => { + plugin.initialize(getEditor()); + const items: ClipboardData = { + rawHtml: '', + text: '', + image: null, + files: [], + types: [], + customValues: {}, + }; + spyOn(extractClipboardEvent, 'default').and.callFake((event, callback) => { + callback(items); + }); + + handler.paste({}); + expect(paste).toHaveBeenCalledWith(items); + expect(tempNode).toBeNull(); + }); + + it('Editor disposed, do not handle.', () => { + plugin.initialize(getEditor(true /* disposeResult */)); + const items: ClipboardData = { + rawHtml: '', + text: '', + image: null, + files: [], + types: [], + customValues: {}, + }; + spyOn(extractClipboardEvent, 'default').and.callFake((event, callback) => { + callback(items); + }); + + handler.paste({}); + expect(paste).not.toHaveBeenCalled(); + expect(tempNode).toBeNull(); + }); +}); + +describe('CopyPastePlugin copy', () => { + let plugin: CopyPastePlugin; + let handler: Record; + let editor: IEditor; + let tempNode: HTMLElement = null; + let addDomEventHandler: jasmine.Spy; + let triggerPluginEvent: jasmine.Spy; + + beforeEach(() => { + handler = null; + plugin = new CopyPastePlugin({}); + + addDomEventHandler = jasmine + .createSpy('addDomEventHandler') + .and.callFake((handlerParam: Record) => { + handler = handlerParam; + return () => { + handler = null; + }; + }); + triggerPluginEvent = jasmine + .createSpy('triggerPluginEvent') + .and.callFake( + ( + eventType: PluginEventType.BeforeCutCopy, + data: Pick< + BeforeCutCopyEvent, + 'eventDataCache' | 'rawEvent' | 'clonedRoot' | 'range' | 'isCut' + >, + broadcast?: boolean + ) => { + return { + clonedRoot: tempNode, + range: { collapsed: false }, + rawEvent: event as ClipboardEvent, + isCut: false, + }; + } + ); + spyOn(addRangeToSelection, 'default'); + + editor = ({ + addDomEventHandler, + triggerPluginEvent, + getSelectionRange: () => { collapsed: false }, + getContent: () => '
test
', + getCustomData: (key: string, getter: () => any) => { + tempNode = getter(); + return tempNode; + }, + insertNode: (node: HTMLElement) => { + tempNode = node; + document.body.appendChild(node); + }, + getDocument: () => document, + select: () => {}, + addUndoSnapshot: (f: () => void) => f(), + focus: () => {}, + getTrustedHTMLHandler: (html: string) => html, + getSelectionRangeEx: () => { + return { + type: SelectionRangeTypes.Normal, + ranges: [{ collapsed: false }], + areAllCollapsed: false, + }; + }, + }); + + plugin.initialize(editor); + }); + + afterEach(() => { + plugin.dispose(); + if (tempNode) { + tempNode.parentNode.removeChild(tempNode); + tempNode = null; + } + }); + + it('before copy', () => { + editor.runAsync = () => null; + + const event = {}; + handler.copy(event); + + expect(triggerPluginEvent).toHaveBeenCalledTimes(1); + expect(triggerPluginEvent.calls.argsFor(0)[0]).toBe(PluginEventType.BeforeCutCopy); + expect(tempNode.innerHTML).toBe('
test
'); + + const range = (addRangeToSelection.default).calls.argsFor(0)[0]; + expect(range.startContainer).toBe(tempNode.firstChild); + expect(range.endContainer).toBe(tempNode.firstChild); + expect(range.startOffset).toBe(0); + expect(range.endOffset).toBe(1); + }); + + it('after copy', () => { + editor.runAsync = callback => { + callback(editor); + return null; + }; + + const event = {}; + handler.copy(event); + + expect(triggerPluginEvent).toHaveBeenCalledTimes(1); + expect(tempNode.innerHTML).toBe(''); + }); + + it('after cut with text content', () => { + editor.runAsync = callback => { + callback(editor); + return null; + }; + const contentDiv = document.createElement('div'); + contentDiv.innerHTML = 'This is a test'; + + editor.getSelectedRegions = () => [ + { + rootNode: contentDiv, + nodeBefore: null, + nodeAfter: null, + skipTags: [], + fullSelectionStart: new Position(contentDiv.firstChild, 3), + fullSelectionEnd: new Position(contentDiv.firstChild, 10), + }, + ]; + + editor.deleteSelectedContent = () => { + let html = contentDiv.innerHTML; + html = html.substr(0, 3) + html.substr(10); + contentDiv.innerHTML = html; + return null; + }; + + const event = {}; + handler.cut(event); + + expect(triggerPluginEvent).toHaveBeenCalledTimes(1); + expect(tempNode.innerHTML).toBe(''); + expect(contentDiv.innerHTML).toBe('Thitest'); + }); + + it('after cut with html content', () => { + editor.runAsync = callback => { + callback(editor); + return null; + }; + const contentDiv = document.createElement('div'); + contentDiv.innerHTML = + '
  1. line1
  2. line2
    line3
  3. line4
line5
'; + + editor.getSelectedRegions = () => [ + { + rootNode: contentDiv, + nodeBefore: null, + nodeAfter: null, + skipTags: [], + fullSelectionStart: new Position( + contentDiv.childNodes[0].childNodes[1].childNodes[1].childNodes[0], + 3 + ), + fullSelectionEnd: new Position(contentDiv.childNodes[1].childNodes[0], 2), + }, + ]; + + editor.deleteSelectedContent = () => { + let html = contentDiv.innerHTML; + html = html.substr(0, 46) + '
' + html.substr(85); + contentDiv.innerHTML = html; + return null; + }; + + const event = {}; + handler.cut(event); + + expect(triggerPluginEvent).toHaveBeenCalledTimes(1); + expect(tempNode.innerHTML).toBe(''); + expect(contentDiv.innerHTML).toBe( + '
  1. line1
  2. line2
    lin
ne5
' + ); + }); +}); diff --git a/packages/roosterjs-editor-core/test/corePlugins/domEventPluginTest.ts b/packages/roosterjs-editor-core/test/corePlugins/domEventPluginTest.ts index ea007fbeefe6..cb17db99cff3 100644 --- a/packages/roosterjs-editor-core/test/corePlugins/domEventPluginTest.ts +++ b/packages/roosterjs-editor-core/test/corePlugins/domEventPluginTest.ts @@ -38,6 +38,7 @@ describe('DOMEventPlugin', () => { stopPrintableKeyboardEventPropagation: true, contextMenuProviders: [], tableSelectionRange: null, + imageSelectionRange: null, }); expect(addDomEventHandler).toHaveBeenCalled(); @@ -88,6 +89,7 @@ describe('DOMEventPlugin', () => { stopPrintableKeyboardEventPropagation: false, contextMenuProviders: [], tableSelectionRange: null, + imageSelectionRange: null, }); expect(addDomEventHandler).toHaveBeenCalled(); diff --git a/packages/roosterjs-editor-core/test/corePlugins/entityPluginTest.ts b/packages/roosterjs-editor-core/test/corePlugins/entityPluginTest.ts index 1207336f73ac..219f1eaa072a 100644 --- a/packages/roosterjs-editor-core/test/corePlugins/entityPluginTest.ts +++ b/packages/roosterjs-editor-core/test/corePlugins/entityPluginTest.ts @@ -1,21 +1,17 @@ import * as commitEntity from 'roosterjs-editor-dom/lib/entity/commitEntity'; import * as getEntityFromElement from 'roosterjs-editor-dom/lib/entity/getEntityFromElement'; import EntityPlugin from '../../lib/corePlugins/EntityPlugin'; -import { - createDefaultHtmlSanitizerOptions, - moveChildNodes, - createElement, -} from 'roosterjs-editor-dom'; +import { createDefaultHtmlSanitizerOptions } from 'roosterjs-editor-dom'; import { ChangeSource, EntityClasses, EntityOperation, - EntityOperationEvent, EntityPluginState, + KnownEntityItem, IEditor, Keys, + PasteType, PluginEventType, - PluginKeyboardEvent, QueryScope, } from 'roosterjs-editor-types'; @@ -34,6 +30,7 @@ describe('EntityPlugin', () => { getElementAtCursor: (selector: string, node: Node) => node, addContentEditFeature: () => {}, triggerPluginEvent, + isFeatureEnabled: () => false, }); plugin.initialize(editor); }); @@ -46,8 +43,7 @@ describe('EntityPlugin', () => { it('init', () => { expect(state).toEqual({ - knownEntityElements: [], - shadowEntityCache: {}, + entityMap: {}, }); }); @@ -99,28 +95,24 @@ describe('EntityPlugin', () => { operation: EntityOperation.Overwrite, entity: entityReadonly, rawEvent, - contentForShadowEntity: undefined, }); expect(triggerPluginEvent.calls.argsFor(1)[0]).toBe(PluginEventType.EntityOperation); expect(triggerPluginEvent.calls.argsFor(1)[1]).toEqual({ operation: EntityOperation.Overwrite, entity: entityOnSelection1, rawEvent, - contentForShadowEntity: undefined, }); expect(triggerPluginEvent.calls.argsFor(2)[0]).toBe(PluginEventType.EntityOperation); expect(triggerPluginEvent.calls.argsFor(2)[1]).toEqual({ operation: EntityOperation.PartialOverwrite, entity: entityOnSelection2, rawEvent, - contentForShadowEntity: undefined, }); expect(triggerPluginEvent.calls.argsFor(3)[0]).toBe(PluginEventType.EntityOperation); expect(triggerPluginEvent.calls.argsFor(3)[1]).toEqual({ operation: EntityOperation.Overwrite, entity: entityInSelection, rawEvent, - contentForShadowEntity: undefined, }); } @@ -146,7 +138,6 @@ describe('EntityPlugin', () => { operation: EntityOperation.Click, rawEvent, entity: target, - contentForShadowEntity: undefined, }); }); @@ -216,6 +207,7 @@ describe('EntityPlugin', () => { htmlBefore: '', htmlAfter: '', htmlAttributes: {}, + pasteType: PasteType.Normal, }) ); @@ -252,7 +244,6 @@ describe('EntityPlugin', () => { isReadonly: true, }, rawEvent: undefined, - contentForShadowEntity: undefined, }); expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { @@ -264,7 +255,6 @@ describe('EntityPlugin', () => { isReadonly: false, }, rawEvent: undefined, - contentForShadowEntity: undefined, }); }); @@ -274,7 +264,7 @@ describe('EntityPlugin', () => { let node2: HTMLElement; let node3: HTMLElement; let containedNodes: Node[]; - let fragment: DocumentFragment = document.createDocumentFragment(); + let commitEntitySpy: jasmine.Spy; beforeEach(() => { node1 = document.createElement('div'); @@ -303,31 +293,37 @@ describe('EntityPlugin', () => { return containedNodes; }); - spyOn(commitEntity, 'default'); - spyOn(document, 'createDocumentFragment').and.returnValue(fragment); + commitEntitySpy = spyOn(commitEntity, 'default'); }); - function verify(inStateNodes: HTMLElement[], commitedNodes: HTMLElement[]) { - expect(state.knownEntityElements).toEqual(inStateNodes); - expect(commitEntity.default).toHaveBeenCalledTimes(commitedNodes.length); - commitedNodes.forEach(node => { - expect(commitEntity.default).toHaveBeenCalledWith(node, entityType, false, node.id); + function verify( + inStateNodes: Record, + committedNodes: { element: HTMLElement; id: string }[] + ) { + expect(state.entityMap).toEqual(inStateNodes); + + expect(commitEntitySpy).toHaveBeenCalledTimes(committedNodes.length); + committedNodes.forEach(node => { + expect(commitEntitySpy).toHaveBeenCalledWith( + node.element, + entityType, + false, + node.id + ); expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { operation: EntityOperation.NewEntity, entity: { - wrapper: node, + wrapper: node.element, id: node.id, type: entityType, isReadonly: false, }, rawEvent: undefined, - contentForShadowEntity: fragment, }); }); } it('content changed event, no existing known nodes', () => { - state.knownEntityElements = []; containedNodes = [node1]; plugin.onPluginEvent({ @@ -335,11 +331,13 @@ describe('EntityPlugin', () => { source: '', }); - verify([node1], [node1]); + verify({ node1: { element: node1 } }, [{ element: node1, id: 'node1' }]); }); it('content changed event, has existing known nodes', () => { - state.knownEntityElements = [node1, node2]; + state.entityMap.node1 = { element: node1 }; + state.entityMap.node2 = { element: node2 }; + containedNodes = [node2, node3]; plugin.onPluginEvent({ @@ -347,397 +345,313 @@ describe('EntityPlugin', () => { source: '', }); - verify([node2, node3], [node3]); + verify( + { + node1: { element: node1, isDeleted: true }, + node2: { element: node2 }, + node3: { element: node3 }, + }, + [{ element: node3, id: 'node3' }] + ); }); - xit('content changed event, reset known nodes', () => { - state.knownEntityElements = [node1, node2]; + it('content changed event, reset known nodes', () => { + state.entityMap.node1 = { element: node1 }; + state.entityMap.node2 = { element: node2 }; + containedNodes = [node2, node3]; editor.queryElements = ((selector: string, callback: (e: HTMLElement) => void) => { - containedNodes.forEach(callback); return containedNodes; }); - expect(state.knownEntityElements).toEqual([]); - plugin.onPluginEvent({ eventType: PluginEventType.ContentChanged, source: ChangeSource.SetContent, }); - verify([node2, node3], [node2, node3]); + verify( + { + node1: { element: node1, isDeleted: true }, + node2: { element: node2 }, + node3: { element: node3 }, + }, + [{ element: node3, id: 'node3' }] + ); }); + it('editor ready event', () => { - state.knownEntityElements = [node1, node2]; + state.entityMap.node1 = { element: node1 }; + state.entityMap.node2 = { element: node2 }; + containedNodes = [node2, node3]; plugin.onPluginEvent({ eventType: PluginEventType.EditorReady, }); - verify([node2, node3], [node3]); + verify( + { + node1: { element: node1, isDeleted: true }, + node2: { element: node2 }, + node3: { element: node3 }, + }, + [{ element: node3, id: 'node3' }] + ); }); - xit('handle id duplication', () => { + it('handle id duplication', () => { node3.id = node2.id; - state.knownEntityElements = [node1, node2]; + state.entityMap.node1 = { element: node1 }; + state.entityMap.node2 = { element: node2 }; + containedNodes = [node2, node3]; + commitEntitySpy.and.callFake((wrapper: any, type: any, isReadonly: any, id: any) => { + wrapper.id = id; + }); + plugin.onPluginEvent({ eventType: PluginEventType.EditorReady, }); - node3.id = 'node2_1'; - verify([node2, node3], [node3]); + verify( + { + node1: { element: node1, isDeleted: true }, + node2: { element: node2 }, + node2_1: { element: node3 }, + }, + [{ element: node3, id: 'node2_1' }] + ); - expect(commitEntity.default).toHaveBeenCalledTimes(1); - expect(commitEntity.default).toHaveBeenCalledWith(node2, entityType, false, 'node2_1'); + expect(commitEntitySpy).toHaveBeenCalledTimes(1); + expect(commitEntitySpy).toHaveBeenCalledWith(node3, entityType, false, 'node2_1'); }); }); -}); - -describe('Shadow DOM Entity', () => { - it('Key press event should be handled exclusively when focus to shadow DOM entity', () => { - const plugin = new EntityPlugin(); - const event: PluginKeyboardEvent = { - eventType: PluginEventType.KeyPress, - rawEvent: { - target: { - shadowRoot: {}, - }, - }, - }; - expect(plugin.willHandleEventExclusively(event)).toBeTrue(); - }); - - it('Cache shadow entity before set content', () => { - const plugin = new EntityPlugin(); - const entity1 = document.createElement('span'); - const entity2 = document.createElement('span'); - const editor: IEditor = { - getDocument: () => document, - queryElements: () => [entity1, entity2], - addContentEditFeature: () => {}, - }; - const state = plugin.getState(); - const textNode = document.createTextNode('text'); - commitEntity.default(entity1, 'ENTITY1', false, 'TEST1'); - commitEntity.default(entity2, 'ENTITY2', false, 'TEST2'); - entity2.attachShadow({ mode: 'open' }).appendChild(textNode); + describe('entity lifecycle', () => { + it('No new entity', () => { + editor.queryElements = jasmine.createSpy('queryElements').and.returnValue([]); - expect(state).toEqual({ - knownEntityElements: [], - shadowEntityCache: {}, - }); + plugin.onPluginEvent({ + eventType: PluginEventType.ContentChanged, + source: '', + }); - plugin.initialize(editor); - plugin.onPluginEvent({ - eventType: PluginEventType.BeforeSetContent, - newContent: '', + expect(state).toEqual({ + entityMap: {}, + }); + expect(triggerPluginEvent).not.toHaveBeenCalled(); }); - expect(Object.keys(state.shadowEntityCache)).toEqual(['TEST2']); - expect(state.shadowEntityCache.TEST2).toBe(entity2); - expect(entity2.shadowRoot.firstChild).toBe(textNode); - }); + it('Add new entity', () => { + const entity1 = document.createElement('div'); - it('ContentChange - Check removed shadow entity', () => { - const plugin = new EntityPlugin(); - const state = plugin.getState(); - const entity1 = document.createElement('span'); - const triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - const editor: IEditor = { - triggerPluginEvent, - queryElements: () => [], - contains: () => false, - addContentEditFeature: () => {}, - }; + commitEntity.default(entity1, 'Entity', true, 'E1'); + editor.queryElements = jasmine.createSpy('queryElements').and.returnValue([entity1]); - state.knownEntityElements.push(entity1); - plugin.initialize(editor); - commitEntity.default(entity1, 'TEST', false, 'TEST1'); - entity1.attachShadow({ mode: 'open' }); + plugin.onPluginEvent({ + eventType: PluginEventType.ContentChanged, + source: '', + }); - plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, - source: '', + expect(state).toEqual({ + entityMap: { + E1: { + element: entity1, + }, + }, + }); + expect(triggerPluginEvent).toHaveBeenCalledTimes(1); + expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { + operation: EntityOperation.NewEntity, + rawEvent: undefined, + entity: { + wrapper: entity1, + id: 'E1', + type: 'Entity', + isReadonly: true, + }, + }); }); - expect(state.knownEntityElements).toEqual([]); - expect(state.shadowEntityCache).toEqual({}); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.RemoveShadowRoot, - rawEvent: undefined, - entity: getEntityFromElement.default(entity1), - contentForShadowEntity: undefined, - }); - }); + it('Add new entity with conflict id', () => { + const entity1 = document.createElement('div'); + const entity2 = document.createElement('div'); - it('ContentChange - hydrate new entity', () => { - const plugin = new EntityPlugin(); - const state = plugin.getState(); - const entity1 = document.createElement('span'); - const textNode = document.createTextNode('test'); - const triggerPluginEvent = jasmine - .createSpy('triggerPluginEvent') - .and.callFake((type: PluginEventType, param: EntityOperationEvent) => { - if ( - type == PluginEventType.EntityOperation && - param.operation == EntityOperation.NewEntity - ) { - param.contentForShadowEntity.appendChild(textNode); - } - }); - const editor: IEditor = { - triggerPluginEvent, - queryElements: () => [entity1], - contains: (node: Node) => node == entity1, - addContentEditFeature: () => {}, - getDocument: () => document, - }; + commitEntity.default(entity1, 'Entity', true, 'E1'); + commitEntity.default(entity2, 'Entity', true, 'E1'); - plugin.initialize(editor); - commitEntity.default(entity1, 'TEST', false, 'TEST1'); + state.entityMap.E1 = { + element: entity1, + }; - plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, - source: '', - }); + editor.queryElements = jasmine + .createSpy('queryElements') + .and.returnValue([entity1, entity2]); + editor.contains = jasmine.createSpy('contains').and.returnValue(true); - expect(state.knownEntityElements).toEqual([entity1]); - expect(state.shadowEntityCache).toEqual({}); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.NewEntity, - rawEvent: undefined, - entity: getEntityFromElement.default(entity1), - contentForShadowEntity: document.createDocumentFragment(), - }); - expect(entity1.shadowRoot.firstChild).toBe(textNode); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.AddShadowRoot, - rawEvent: undefined, - entity: getEntityFromElement.default(entity1), - contentForShadowEntity: undefined, - }); - }); + plugin.onPluginEvent({ + eventType: PluginEventType.ContentChanged, + source: '', + }); - it('ContentChange - hydrate new entity with known entity', () => { - const plugin = new EntityPlugin(); - const state = plugin.getState(); - const entity1 = document.createElement('span'); - const textNode = document.createTextNode('test'); - const triggerPluginEvent = jasmine - .createSpy('triggerPluginEvent') - .and.callFake((type: PluginEventType, param: EntityOperationEvent) => { - if ( - type == PluginEventType.EntityOperation && - param.operation == EntityOperation.NewEntity - ) { - param.contentForShadowEntity.appendChild(textNode); - } + expect(state).toEqual({ + entityMap: { + E1: { + element: entity1, + }, + E1_1: { + element: entity2, + }, + }, }); - const editor: IEditor = { - triggerPluginEvent, - contains: (node: Node) => node == entity1, - addContentEditFeature: () => {}, - getDocument: () => document, - }; + expect(state.entityMap.E1.element).toBe(entity1); + expect(state.entityMap.E1_1.element).toBe(entity2); + expect(triggerPluginEvent).toHaveBeenCalledTimes(1); + expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { + operation: EntityOperation.NewEntity, + rawEvent: undefined, + entity: { + wrapper: entity2, + id: 'E1_1', + type: 'Entity', + isReadonly: true, + }, + }); + }); - plugin.initialize(editor); - commitEntity.default(entity1, 'TEST', false, 'TEST1'); + it('Add new entity with conflict id with deleted entity', () => { + const entity1 = document.createElement('div'); + const entity2 = document.createElement('div'); - plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, - source: ChangeSource.InsertEntity, - data: getEntityFromElement.default(entity1), - }); + commitEntity.default(entity1, 'Entity', true, 'E1'); + commitEntity.default(entity2, 'Entity', true, 'E1'); - expect(state.knownEntityElements).toEqual([entity1]); - expect(state.shadowEntityCache).toEqual({}); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.NewEntity, - rawEvent: undefined, - entity: getEntityFromElement.default(entity1), - contentForShadowEntity: document.createDocumentFragment(), - }); - expect(entity1.shadowRoot.firstChild).toBe(textNode); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.AddShadowRoot, - rawEvent: undefined, - entity: getEntityFromElement.default(entity1), - contentForShadowEntity: undefined, - }); - }); + state.entityMap.E1 = { + element: entity1, + }; - it('ContentChange - dehydrate existing entity', () => { - const plugin = new EntityPlugin(); - const state = plugin.getState(); - const entity1 = document.createElement('span'); - let newEntity: HTMLElement; - const triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - const editor: IEditor = { - triggerPluginEvent, - queryElements: () => [entity1], - contains: (node: Node) => node == entity1, - replaceNode: (oldNode: HTMLElement, newNode: HTMLElement) => { - newEntity = newNode; - }, - addContentEditFeature: () => {}, - getDocument: () => document, - }; + editor.queryElements = jasmine.createSpy('queryElements').and.returnValue([entity2]); + editor.contains = jasmine.createSpy('contains').and.callFake(node => node == entity2); - plugin.initialize(editor); - commitEntity.default(entity1, 'TEST', false, 'TEST1'); - entity1.attachShadow({ mode: 'open' }); + plugin.onPluginEvent({ + eventType: PluginEventType.ContentChanged, + source: ChangeSource.SetContent, + }); - plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, - source: '', + expect(state).toEqual({ + entityMap: { + E1: { + element: entity1, + isDeleted: true, + }, + E1_1: { + element: entity2, + }, + }, + }); + expect(state.entityMap.E1.element).toBe(entity1); + expect(state.entityMap.E1_1.element).toBe(entity2); + expect(triggerPluginEvent).toHaveBeenCalledTimes(2); + expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { + operation: EntityOperation.Overwrite, + rawEvent: undefined, + entity: { + wrapper: entity1, + id: 'E1', + type: 'Entity', + isReadonly: true, + }, + }); + expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { + operation: EntityOperation.NewEntity, + rawEvent: undefined, + entity: { + wrapper: entity2, + id: 'E1_1', + type: 'Entity', + isReadonly: true, + }, + }); }); - expect(state.knownEntityElements.length).toBe(1); - expect(state.knownEntityElements[0]).not.toBe(entity1); - expect(state.knownEntityElements[1]).not.toBe(newEntity); - expect(state.shadowEntityCache).toEqual({}); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.NewEntity, - rawEvent: undefined, - entity: getEntityFromElement.default(entity1), - contentForShadowEntity: document.createDocumentFragment(), - }); - expect(newEntity.shadowRoot).toBe(null); - }); + it('Add back deleted entity', () => { + const entity1 = document.createElement('div'); - it('ContentChange - rehydrate existing entity with different content', () => { - const plugin = new EntityPlugin(); - const state = plugin.getState(); - const entity1 = document.createElement('span'); - const entity2 = document.createElement('span'); - const triggerPluginEvent = jasmine - .createSpy('triggerPluginEvent') - .and.callFake((type: PluginEventType, param: EntityOperationEvent) => { - if ( - type == PluginEventType.EntityOperation && - param.operation == EntityOperation.NewEntity - ) { - moveChildNodes( - param.contentForShadowEntity, - createElement( - { - tag: 'span', - children: ['test2'], - }, - document - ) - ); - } - }); - const editor: IEditor = { - triggerPluginEvent, - queryElements: () => [entity1], - contains: (node: Node) => node == entity2, - getDocument: () => document, - addContentEditFeature: () => {}, - }; + commitEntity.default(entity1, 'Entity', true, 'E1'); - plugin.initialize(editor); - commitEntity.default(entity1, 'TEST', false, 'TEST1'); - commitEntity.default(entity2, 'TEST', false, 'TEST1'); - entity1.attachShadow({ mode: 'open' }).appendChild(document.createTextNode('test')); - state.knownEntityElements.push(entity1); + state.entityMap.E1 = { + element: entity1, + isDeleted: true, + }; - plugin.onPluginEvent({ - eventType: PluginEventType.BeforeSetContent, - newContent: '', - }); + editor.queryElements = jasmine.createSpy('queryElements').and.returnValue([entity1]); + editor.contains = jasmine.createSpy('contains').and.callFake(node => node == entity1); - editor.queryElements = () => [entity2]; - plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, - source: '', - }); + plugin.onPluginEvent({ + eventType: PluginEventType.ContentChanged, + source: ChangeSource.SetContent, + }); - expect(state.knownEntityElements.length).toBe(1); - expect(state.knownEntityElements[0]).toBe(entity2); - expect(state.shadowEntityCache).toEqual({}); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.NewEntity, - rawEvent: undefined, - entity: getEntityFromElement.default(entity2), - contentForShadowEntity: document.createDocumentFragment(), - }); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.RemoveShadowRoot, - rawEvent: undefined, - entity: getEntityFromElement.default(entity1), - contentForShadowEntity: undefined, + expect(state).toEqual({ + entityMap: { + E1: { + element: entity1, + }, + }, + }); + expect(state.entityMap.E1.element).toBe(entity1); + expect(triggerPluginEvent).toHaveBeenCalledTimes(1); + expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { + operation: EntityOperation.NewEntity, + rawEvent: undefined, + entity: { + wrapper: entity1, + id: 'E1', + type: 'Entity', + isReadonly: true, + }, + }); }); - }); - it('EntityOperation event', () => { - const plugin = new EntityPlugin(); - const state = plugin.getState(); - const entity1 = document.createElement('span'); - const triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - const editor: IEditor = { - triggerPluginEvent, - queryElements: () => [], - contains: () => false, - runAsync: (callback: Function) => callback(), - addContentEditFeature: () => {}, - }; + it('Mark new entity as canPersist', () => { + const entity1 = document.createElement('div'); - state.knownEntityElements.push(entity1); - commitEntity.default(entity1, 'TEST', false, 'TEST1'); - entity1.attachShadow({ mode: 'open' }); - plugin.initialize(editor); - plugin.onPluginEvent({ - eventType: PluginEventType.EntityOperation, - operation: EntityOperation.Overwrite, - entity: getEntityFromElement.default(entity1), - }); + commitEntity.default(entity1, 'Entity', true, 'E1'); - expect(state.knownEntityElements).toEqual([]); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.RemoveShadowRoot, - entity: getEntityFromElement.default(entity1), - rawEvent: undefined, - contentForShadowEntity: undefined, - }); - }); + editor.queryElements = jasmine.createSpy('queryElements').and.returnValue([entity1]); + editor.contains = jasmine.createSpy('contains').and.callFake(node => node == entity1); - it('Id management', () => { - const plugin = new EntityPlugin(); - const entity1 = document.createElement('span'); - const entity2 = document.createElement('span'); - const entity3 = document.createElement('span'); - const entity4 = document.createElement('span'); - const state = plugin.getState(); - const editor: IEditor = { - triggerPluginEvent: jasmine.createSpy('triggerPluginEvent'), - queryElements: () => [entity1, entity2, entity3, entity4], - contains: (node: Node) => - node == entity1 || node == entity2 || node == entity3 || node == entity4, - addContentEditFeature: () => {}, - getDocument: () => document, - }; + triggerPluginEvent.and.returnValue({ + shouldPersist: true, + }); - commitEntity.default(entity1, 'TEST', false, 'Test'); - commitEntity.default(entity2, 'TEST', false, 'Test_2'); - commitEntity.default(entity3, 'TEST', false, 'Test'); - commitEntity.default(entity4, 'TEST', false, 'Test_2'); - state.knownEntityElements.push(entity1); - plugin.initialize(editor); - plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, - source: '', - }); + plugin.onPluginEvent({ + eventType: PluginEventType.ContentChanged, + source: ChangeSource.SetContent, + }); - expect(getEntityFromElement.default(entity1).id).toBe('Test'); - expect(getEntityFromElement.default(entity2).id).toBe('Test_2'); - expect(getEntityFromElement.default(entity3).id).toBe('Test_1'); - expect(getEntityFromElement.default(entity4).id).toBe('Test_3'); + expect(state).toEqual({ + entityMap: { + E1: { + element: entity1, + canPersist: true, + }, + }, + }); + expect(state.entityMap.E1.element).toBe(entity1); + expect(triggerPluginEvent).toHaveBeenCalledTimes(1); + expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { + operation: EntityOperation.NewEntity, + rawEvent: undefined, + entity: { + wrapper: entity1, + id: 'E1', + type: 'Entity', + isReadonly: true, + }, + }); + }); }); }); diff --git a/packages/roosterjs-editor-core/test/corePlugins/imageSelectionTest.ts b/packages/roosterjs-editor-core/test/corePlugins/imageSelectionTest.ts new file mode 100644 index 000000000000..99972753678d --- /dev/null +++ b/packages/roosterjs-editor-core/test/corePlugins/imageSelectionTest.ts @@ -0,0 +1,217 @@ +import Editor from '../../lib/editor/Editor'; +import ImageSelection from '../../lib/corePlugins/ImageSelection'; +import { + IEditor, + EditorOptions, + SelectionRangeTypes, + ImageSelectionRange, + PluginEvent, + PluginEventType, +} from 'roosterjs-editor-types'; +export * from 'roosterjs-editor-dom/test/DomTestHelper'; + +const Escape = 'Escape'; +const Space = ' '; +const Delete = 'Delete'; + +describe('ImageSelectionPlugin |', () => { + let editor: IEditor; + let id = 'imageSelectionContainerId'; + let imageId = 'imageSelectionId'; + let imageId2 = 'imageSelectionId2'; + let imageSelection: ImageSelection; + let editorIsFeatureEnabled: any; + + beforeEach(() => { + let node = document.createElement('div'); + node.id = id; + document.body.insertBefore(node, document.body.childNodes[0]); + imageSelection = new ImageSelection(); + + let options: EditorOptions = { + plugins: [imageSelection], + defaultFormat: { + fontFamily: 'Calibri,Arial,Helvetica,sans-serif', + fontSize: '11pt', + textColor: '#000000', + }, + corePluginOverride: {}, + }; + + editor = new Editor(node as HTMLDivElement, options); + + editor.runAsync = callback => { + callback(editor); + return null; + }; + editorIsFeatureEnabled = spyOn(editor, 'isFeatureEnabled'); + }); + + afterEach(() => { + editor.dispose(); + editor = null; + const div = document.getElementById(id); + div.parentNode.removeChild(div); + }); + + it('should be triggered in mouse up left click', () => { + editor.setContent(``); + const target = document.getElementById(imageId); + editorIsFeatureEnabled.and.returnValue(true); + simulateMouseEvent('mousedown', target!, 0); + simulateMouseEvent('mouseup', target!, 0); + editor.focus(); + + const selection = editor.getSelectionRangeEx(); + expect(selection.type).toBe(SelectionRangeTypes.ImageSelection); + expect(selection.areAllCollapsed).toBe(false); + }); + + it('should be triggered in shadow Edit', () => { + editor.setContent(``); + const target = document.getElementById(imageId); + editorIsFeatureEnabled.and.returnValue(true); + editor.focus(); + editor.select(target); + + editor.startShadowEdit(); + + let selection = editor.getSelectionRangeEx(); + expect(selection.type).toBe(SelectionRangeTypes.ImageSelection); + expect(selection.areAllCollapsed).toBe(false); + + editor.stopShadowEdit(); + + selection = editor.getSelectionRangeEx(); + expect(selection.type).toBe(SelectionRangeTypes.ImageSelection); + expect(selection.areAllCollapsed).toBe(false); + }); + + it('should handle a ESCAPE KEY in a image', () => { + editor.setContent(``); + const target = document.getElementById(imageId); + editorIsFeatureEnabled.and.returnValue(true); + editor.focus(); + editor.select(target); + const range = document.createRange(); + range.selectNode(target!); + imageSelection.onPluginEvent(keyDown(Escape)); + imageSelection.onPluginEvent(keyUp(Escape)); + const selection = editor.getSelectionRangeEx(); + expect(selection.type).toBe(SelectionRangeTypes.Normal); + expect(selection.areAllCollapsed).toBe(true); + }); + + it('should handle a DELETE KEY in a image', () => { + editor.setContent(``); + const target = document.getElementById(imageId); + editorIsFeatureEnabled.and.returnValue(true); + editor.focus(); + editor.select(target); + const range = document.createRange(); + range.selectNode(target!); + imageSelection.onPluginEvent(keyDown(Delete)); + imageSelection.onPluginEvent(keyUp(Delete)); + expect(editor.getContent()).toBe(''); + }); + + it('should handle any key in a image', () => { + editor.setContent(``); + const target = document.getElementById(imageId); + editorIsFeatureEnabled.and.returnValue(true); + editor.focus(); + editor.select(target); + const range = document.createRange(); + range.selectNode(target!); + imageSelection.onPluginEvent(keyDown(Space)); + imageSelection.onPluginEvent(keyUp(Space)); + const selection = editor.getSelectionRangeEx(); + expect(selection.type).toBe(SelectionRangeTypes.Normal); + expect(selection.areAllCollapsed).toBe(false); + }); + + it('should handle contextMenu', () => { + editor.setContent(``); + const target = document.getElementById(imageId); + editorIsFeatureEnabled.and.returnValue(true); + editor.focus(); + const contextMenuEvent = contextMenu(target!); + imageSelection.onPluginEvent(contextMenuEvent); + const selection = editor.getSelectionRangeEx(); + expect(selection.type).toBe(SelectionRangeTypes.ImageSelection); + expect(selection.areAllCollapsed).toBe(false); + }); + + it('should change image selection contextMenu', () => { + editor.setContent(``); + const target = document.getElementById(imageId); + const secondTarget = document.getElementById(imageId2); + editorIsFeatureEnabled.and.returnValue(true); + editor.focus(); + editor.select(secondTarget); + const contextMenuEvent = contextMenu(target!); + imageSelection.onPluginEvent(contextMenuEvent); + const selection = editor.getSelectionRangeEx() as ImageSelectionRange; + expect(selection.type).toBe(SelectionRangeTypes.ImageSelection); + + expect(selection.image.id).toBe(imageId); + }); + + it('should not change image selection contextMenu', () => { + editor.setContent(``); + const target = document.getElementById(imageId); + editorIsFeatureEnabled.and.returnValue(true); + editor.focus(); + editor.select(target); + const contextMenuEvent = contextMenu(target!); + imageSelection.onPluginEvent(contextMenuEvent); + spyOn(editor, 'select'); + expect(editor.select).not.toHaveBeenCalled(); + }); + + const keyDown = (key: string): PluginEvent => { + return { + eventType: PluginEventType.KeyDown, + rawEvent: { + key: key, + preventDefault: () => {}, + stopPropagation: () => {}, + }, + }; + }; + + const keyUp = (key: string): PluginEvent => { + return { + eventType: PluginEventType.KeyUp, + rawEvent: { + key: key, + preventDefault: () => {}, + stopPropagation: () => {}, + }, + }; + }; + + const contextMenu = (target: HTMLElement): PluginEvent => { + return { + eventType: PluginEventType.ContextMenu, + rawEvent: { + target: target, + }, + items: [], + }; + }; + + function simulateMouseEvent(mouseEvent: string, target: HTMLElement, keyNumber: number) { + const rect = target.getBoundingClientRect(); + var event = new MouseEvent(mouseEvent, { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left, + clientY: rect.top, + shiftKey: false, + button: keyNumber, + }); + target.dispatchEvent(event); + } +}); diff --git a/packages/roosterjs-editor-core/test/corePlugins/inlineEntityOnPluginEventTest.ts b/packages/roosterjs-editor-core/test/corePlugins/inlineEntityOnPluginEventTest.ts new file mode 100644 index 000000000000..2d25ea9d35a8 --- /dev/null +++ b/packages/roosterjs-editor-core/test/corePlugins/inlineEntityOnPluginEventTest.ts @@ -0,0 +1,738 @@ +import * as splitTextNode from 'roosterjs-editor-dom/lib/utils/splitTextNode'; +import { inlineEntityOnPluginEvent } from '../../lib/corePlugins/utils/inlineEntityOnPluginEvent'; +import { + BeforeCutCopyEvent, + BeforePasteEvent, + ChangeSource, + ContentChangedEvent, + DelimiterClasses, + EditorReadyEvent, + Entity, + ExtractContentWithDomEvent, + IEditor, + NormalSelectionRange, + PluginEvent, + PluginEventType, + PluginKeyDownEvent, + SelectionRangeTypes, +} from 'roosterjs-editor-types'; +import { + addDelimiters, + commitEntity, + findClosestElementAncestor, + getBlockElementAtNode, + Position, +} from 'roosterjs-editor-dom'; + +const ZERO_WIDTH_SPACE = '\u200B'; +const DELIMITER_SELECTOR = + '.' + DelimiterClasses.DELIMITER_AFTER + ',.' + DelimiterClasses.DELIMITER_BEFORE; + +describe('Inline Entity On Plugin Event |', () => { + let wrapper: HTMLElement; + let editor: IEditor; + let testContainer: HTMLElement; + let selectSpy: jasmine.Spy; + + beforeEach(() => { + wrapper = document.createElement('span'); + wrapper.innerHTML = 'Test'; + + testContainer = document.createElement('div'); + testContainer.appendChild(wrapper); + document.body.appendChild(testContainer); + spyOn(splitTextNode, 'default').and.callThrough(); + + editor = ({ + getDocument: () => document, + getElementAtCursor: (selector: string, node: Node) => node, + addContentEditFeature: () => {}, + queryElements: (selector: string) => { + return document.querySelectorAll(selector); + }, + runAsync: (callback: () => void) => callback(), + getSelectionRange: () => + { + collapsed: true, + }, + getSelectionRangeEx: () => { + return { + areAllCollapsed: true, + type: SelectionRangeTypes.Normal, + }; + }, + select: selectSpy = jasmine.createSpy('select'), + getBlockElementAtNode: (node: Node) => getBlockElementAtNode(document.body, node), + }); + }); + + afterEach(() => { + wrapper.parentElement?.removeChild(wrapper); + document.body.childNodes.forEach(cn => { + document.body.removeChild(cn); + }); + }); + + describe('Handle Key Up & Down |', () => { + let entity: Entity; + let delimiterAfter: Element | null; + let delimiterBefore: Element | null; + let textToAdd: Node; + + beforeEach(() => { + ({ entity, delimiterAfter, delimiterBefore } = addEntityBeforeEach(entity, wrapper)); + + textToAdd = document.createTextNode('Text'); + + editor.getElementAtCursor = (selector: string, selectFrom: Node) => + findClosestElementAncestor(selectFrom, document.body, selector); + }); + + describe('Element Before |', () => { + afterEach(() => { + document.body.childNodes.forEach(cn => { + document.body.removeChild(cn); + }); + }); + function arrangeAndAct( + which: number = 66 /* B */, + addElementOnRunAsync: boolean = true + ) { + editor.getFocusedPosition = () => new Position(delimiterBefore!, 0); + + editor.runAsync = (callback: (editor: IEditor) => void) => { + if (addElementOnRunAsync) { + delimiterBefore?.insertBefore(textToAdd, delimiterBefore.firstChild); + } + + callback(editor); + return () => {}; + }; + inlineEntityOnPluginEvent( + { + eventType: PluginEventType.KeyDown, + rawEvent: { + which, + key: 'B', + }, + }, + editor + ); + } + + it('Is Delimiter', () => { + arrangeAndAct(); + + expect(delimiterBefore?.textContent).toEqual(ZERO_WIDTH_SPACE); + expect(delimiterBefore?.textContent?.length).toEqual(1); + expect(delimiterBefore?.childNodes.length).toEqual(1); + expect(splitTextNode.default).toHaveBeenCalled(); + }); + + it('Is not Delimiter', () => { + delimiterBefore?.removeAttribute('class'); + delimiterBefore?.insertBefore(textToAdd, delimiterBefore.firstChild); + + arrangeAndAct(); + + expect(delimiterBefore?.textContent).not.toEqual(ZERO_WIDTH_SPACE); + expect(delimiterBefore?.textContent?.length).toEqual(5); + expect(delimiterBefore?.childNodes.length).toEqual(2); + expect(splitTextNode.default).not.toHaveBeenCalled(); + }); + + it('Selection collapsed and not Normal Selection', () => { + editor.getSelectionRangeEx = () => { + return ({ + areAllCollapsed: true, + type: SelectionRangeTypes.TableSelection, + }); + }; + spyOn(editor, 'getElementAtCursor').and.returnValue(delimiterAfter as HTMLElement); + + arrangeAndAct(); + + expect(editor.getElementAtCursor).not.toHaveBeenCalled(); + expect(splitTextNode.default).not.toHaveBeenCalled(); + }); + + it('Selection not collapsed and not Normal Selection', () => { + editor.getSelectionRangeEx = () => { + return ({ + areAllCollapsed: false, + type: SelectionRangeTypes.TableSelection, + }); + }; + spyOn(editor, 'getElementAtCursor').and.returnValue(delimiterAfter as HTMLElement); + + arrangeAndAct(); + + expect(editor.getElementAtCursor).not.toHaveBeenCalled(); + expect(splitTextNode.default).not.toHaveBeenCalled(); + }); + + it('Enter on delimiter before, clear previous block delimiter', () => { + const div = document.createElement('div'); + testContainer.insertAdjacentElement('beforebegin', div); + div.appendChild(delimiterBefore!.cloneNode(true /* deep */)); + + arrangeAndAct(13 /* ENTER */, false /* addElementOnRunAsync */); + + expect(div.firstElementChild?.className).toEqual(''); + expect(div.firstElementChild?.textContent).not.toEqual(ZERO_WIDTH_SPACE); + expect(delimiterBefore!.className).toEqual(DelimiterClasses.DELIMITER_BEFORE); + }); + + it('Key press when selection is not collapsed, delimiter before is the endContainer', () => { + const testElement = document.createElement('span'); + testElement.appendChild(document.createTextNode('Test')); + delimiterBefore?.parentElement?.insertBefore(testElement, delimiterBefore); + + const range = new Range(); + range.setStart(testElement, 0); + range.setEnd(delimiterBefore!, 0); + + editor.getSelectionRangeEx = () => { + return { + areAllCollapsed: false, + type: SelectionRangeTypes.Normal, + ranges: [range], + }; + }; + arrangeAndAct(13 /* ENTER */, false /* addElementOnRunAsync */); + + expect(selectSpy).toHaveBeenCalledWith( + new Position(testElement, 0), + new Position(testContainer, 1) + ); + }); + + it('Key press when selection is not collapsed, delimiter before is the startContainer', () => { + const testElement = document.createElement('span'); + testElement.appendChild(document.createTextNode('Test')); + delimiterBefore?.parentElement?.insertBefore(testElement, null); + + const range = new Range(); + range.setStart(delimiterBefore!, 0); + range.setEnd(testElement, 0); + + editor.getSelectionRangeEx = () => { + return { + areAllCollapsed: false, + type: SelectionRangeTypes.Normal, + ranges: [range], + }; + }; + arrangeAndAct(13 /* ENTER */, false /* addElementOnRunAsync */); + + expect(selectSpy).toHaveBeenCalledWith( + new Position(testContainer, 0), + new Position(testElement, 0) + ); + }); + + it('Key press when selection is not collapsed, delimiter after is not part of the selection', () => { + const testElement = document.createElement('span'); + testElement.appendChild(document.createTextNode('Test')); + delimiterBefore?.parentElement?.insertBefore(testElement, null); + + const range = new Range(); + range.setStart(testElement, 0); + range.setEnd(testElement, 1); + + editor.getSelectionRangeEx = () => { + return { + areAllCollapsed: false, + type: SelectionRangeTypes.Normal, + ranges: [range], + }; + }; + arrangeAndAct(13 /* ENTER */, false /* addElementOnRunAsync */); + + expect(selectSpy).not.toHaveBeenCalled(); + }); + }); + + describe('Element After |', () => { + afterEach(() => { + document.body.childNodes.forEach(cn => { + document.body.removeChild(cn); + }); + }); + function arrangeAndAct( + which: number = 66 /* B */, + addElementOnRunAsync: boolean = true + ) { + editor.getFocusedPosition = () => new Position(delimiterAfter!, 0); + + editor.runAsync = (callback: (editor: IEditor) => void) => { + if (addElementOnRunAsync) { + delimiterAfter?.appendChild(textToAdd); + } + + callback(editor); + return () => {}; + }; + + inlineEntityOnPluginEvent( + { + eventType: PluginEventType.KeyDown, + rawEvent: { + which, + key: 'B', + }, + }, + editor + ); + } + + it('Is Delimiter', () => { + arrangeAndAct(); + + delimiterAfter = getDelimiter(entity, true); + + expect(delimiterAfter.textContent).toEqual(ZERO_WIDTH_SPACE); + expect(delimiterAfter.textContent?.length).toEqual(1); + expect(delimiterAfter.childNodes.length).toEqual(1); + expect(delimiterAfter.id).toBeDefined(); + expect(splitTextNode.default).toHaveBeenCalled(); + }); + + it('Is not Delimiter', () => { + delimiterAfter?.removeAttribute('class'); + delimiterAfter?.insertBefore(textToAdd, delimiterAfter.firstChild); + + arrangeAndAct(); + + expect(delimiterAfter?.textContent).not.toEqual(ZERO_WIDTH_SPACE); + expect(delimiterAfter?.textContent?.length).toEqual(5); + expect(delimiterAfter?.childNodes.length).toEqual(2); + expect(splitTextNode.default).not.toHaveBeenCalled(); + }); + + it('Selection collapsed and not Normal Selection', () => { + editor.getSelectionRangeEx = () => { + return ({ + areAllCollapsed: true, + type: SelectionRangeTypes.TableSelection, + }); + }; + spyOn(editor, 'getElementAtCursor').and.returnValue(delimiterAfter as HTMLElement); + + arrangeAndAct(); + + expect(editor.getElementAtCursor).not.toHaveBeenCalled(); + expect(splitTextNode.default).not.toHaveBeenCalled(); + }); + + it('Selection not collapsed and not Normal Selection', () => { + editor.getSelectionRangeEx = () => { + return ({ + areAllCollapsed: false, + type: SelectionRangeTypes.TableSelection, + }); + }; + spyOn(editor, 'getElementAtCursor').and.returnValue(delimiterAfter as HTMLElement); + + arrangeAndAct(); + + expect(editor.getElementAtCursor).not.toHaveBeenCalled(); + expect(splitTextNode.default).not.toHaveBeenCalled(); + }); + + it('Enter on delimiter after, clear the previous sibling class', () => { + const div = document.createElement('div'); + testContainer.insertAdjacentElement('afterend', div); + div.appendChild(delimiterAfter!.cloneNode(true /* deep */)); + arrangeAndAct(13 /* ENTER */, false /* addElementOnRunAsync */); + + expect(div.firstElementChild?.className).toEqual(''); + expect(div.firstElementChild?.textContent).not.toEqual(ZERO_WIDTH_SPACE); + expect(delimiterAfter!.className).toEqual(DelimiterClasses.DELIMITER_AFTER); + }); + + it('Key press when selection is not collapsed, delimiter after is the endContainer', () => { + const testElement = document.createElement('span'); + testElement.appendChild(document.createTextNode('Test')); + delimiterBefore?.parentElement?.insertBefore(testElement, delimiterBefore); + + const range = new Range(); + range.setStart(testElement, 0); + range.setEnd(delimiterAfter!, 0); + + editor.getSelectionRangeEx = () => { + return { + areAllCollapsed: false, + type: SelectionRangeTypes.Normal, + ranges: [range], + }; + }; + arrangeAndAct(13 /* ENTER */, false /* addElementOnRunAsync */); + + expect(selectSpy).toHaveBeenCalledWith( + new Position(testElement, 0), + new Position(testContainer, 4) + ); + }); + + it('Key press when selection is not collapsed, delimiter after is the startContainer', () => { + const testElement = document.createElement('span'); + testElement.appendChild(document.createTextNode('Test')); + delimiterBefore?.parentElement?.insertBefore(testElement, null); + + const range = new Range(); + range.setStart(delimiterAfter!, 0); + range.setEnd(testElement, 0); + + editor.getSelectionRangeEx = () => { + return { + areAllCollapsed: false, + type: SelectionRangeTypes.Normal, + ranges: [range], + }; + }; + arrangeAndAct(13 /* ENTER */, false /* addElementOnRunAsync */); + + expect(selectSpy).toHaveBeenCalledWith( + new Position(testContainer, 3), + new Position(testElement, 0) + ); + }); + + it('Key press when selection is not collapsed, delimiter after is not part of the selection', () => { + const testElement = document.createElement('span'); + testElement.appendChild(document.createTextNode('Test')); + delimiterBefore?.parentElement?.insertBefore(testElement, null); + + const range = new Range(); + range.setStart(testElement, 0); + range.setEnd(testElement, 1); + + editor.getSelectionRangeEx = () => { + return { + areAllCollapsed: false, + type: SelectionRangeTypes.Normal, + ranges: [range], + }; + }; + arrangeAndAct(13 /* ENTER */, false /* addElementOnRunAsync */); + + expect(selectSpy).not.toHaveBeenCalled(); + }); + }); + }); + + describe('ExtractDOM and Before Cut Copy', () => { + it('Before CutCopyEvent', () => { + const rootDiv = document.createElement('div'); + const element1 = document.createElement('span'); + rootDiv.appendChild(element1); + addDelimiters(element1); + + inlineEntityOnPluginEvent( + { + eventType: PluginEventType.BeforeCutCopy, + clonedRoot: rootDiv, + }, + editor + ); + + expect(rootDiv.querySelectorAll(DELIMITER_SELECTOR).length).toBe(0); + }); + + it('Before CutCopyEvent, dont remove delimiter with additional content', () => { + const rootDiv = document.createElement('div'); + const element1 = document.createElement('span'); + rootDiv.appendChild(element1); + const [after, before]: Element[] = addDelimiters(element1); + + after.appendChild(document.createTextNode('testAfter')); + before.appendChild(document.createTextNode('testBefore')); + + inlineEntityOnPluginEvent( + { + eventType: PluginEventType.BeforeCutCopy, + clonedRoot: rootDiv, + }, + editor + ); + + expect(rootDiv.querySelectorAll(DELIMITER_SELECTOR).length).toBe(0); + expect(after).toBeDefined(); + expect(before).toBeDefined(); + expect(after.innerHTML).toEqual('testAfter'); + expect(before.innerHTML).toEqual('testBefore'); + }); + + it('ExtractContentWithDOM', () => { + const rootDiv = document.createElement('div'); + const element1 = document.createElement('span'); + rootDiv.appendChild(element1); + addDelimiters(element1); + + inlineEntityOnPluginEvent( + { + eventType: PluginEventType.ExtractContentWithDom, + clonedRoot: rootDiv, + }, + editor + ); + + expect(rootDiv.querySelectorAll(DELIMITER_SELECTOR).length).toBe(0); + }); + + it('ExtractContentWithDOM, dont remove delimiter with additional content', () => { + const rootDiv = document.createElement('div'); + const element1 = document.createElement('span'); + rootDiv.appendChild(element1); + const [after, before]: Element[] = addDelimiters(element1); + + after.appendChild(document.createTextNode('testAfter')); + before.appendChild(document.createTextNode('testBefore')); + + inlineEntityOnPluginEvent( + { + eventType: PluginEventType.ExtractContentWithDom, + clonedRoot: rootDiv, + }, + editor + ); + + expect(rootDiv.querySelectorAll(DELIMITER_SELECTOR).length).toBe(0); + expect(after).toBeDefined(); + expect(before).toBeDefined(); + expect(after.innerHTML).toEqual('testAfter'); + expect(before.innerHTML).toEqual('testBefore'); + }); + }); + + function runEditorReadyContentChangedTest( + expectedDelimiters: number, + elementToUse: Node, + eventParam: PluginEvent, + updateCallback?: (node: Node) => void + ) { + const rootDiv = document.createElement('div'); + + spyOn(editor, 'queryElements').and.callFake((selector: string) => + Array.from(rootDiv.querySelectorAll(selector)) + ); + + if (elementToUse) { + rootDiv.appendChild(elementToUse); + } + updateCallback?.(elementToUse); + + inlineEntityOnPluginEvent(eventParam, editor); + + expect(rootDiv.querySelectorAll(DELIMITER_SELECTOR).length).toBe(expectedDelimiters); + } + + describe('Editor Ready |', () => { + let event: EditorReadyEvent; + + beforeEach(() => { + event = { + eventType: PluginEventType.EditorReady, + }; + }); + + it('New Editor with Read only Inline Entity in content', () => { + const element = document.createElement('span'); + commitEntity(element, '123', true /* ReadOnly */, '1'); + + runEditorReadyContentChangedTest(2, element, event); + }); + + it('New Editor with Read only Block Entity in content', () => { + const element = document.createElement('div'); + commitEntity(element, '123', true /* ReadOnly */, '1'); + + runEditorReadyContentChangedTest(0, element, event); + }); + + it('New Editor with Editable Inline Entity in content', () => { + const element = document.createElement('span'); + commitEntity(element, '123', false /* ReadOnly */, '1'); + + runEditorReadyContentChangedTest(0, element, event); + }); + + it('New Editor with Editable Block Entity in content', () => { + const element = document.createElement('div'); + commitEntity(element, '123', false /* ReadOnly */, '1'); + + runEditorReadyContentChangedTest(0, element, event); + }); + + it('New Editor with Normal Element', () => { + const element = document.createElement('div'); + runEditorReadyContentChangedTest(0, element, event); + }); + + it('New Editor with no elements', () => { + const element = document.createElement('div'); + runEditorReadyContentChangedTest(0, element, event); + }); + + it('New Editor with invalid delimiters', () => { + const element = document.createElement('div'); + runEditorReadyContentChangedTest(0, element, event, node => { + addDelimiters(node as HTMLElement); + node.parentElement?.removeChild(node); + }); + }); + }); + + describe('Content Changed |', () => { + let event: ContentChangedEvent; + + beforeEach(() => { + event = { + eventType: PluginEventType.ContentChanged, + source: ChangeSource.SetContent, + }; + }); + + it('ContentChanged with Read only Inline Entity in content', () => { + const element = document.createElement('span'); + commitEntity(element, '123', true /* ReadOnly */, '1'); + + runEditorReadyContentChangedTest(2, element, event); + }); + + it('ContentChanged source not SetContent', () => { + const element = document.createElement('span'); + commitEntity(element, '123', true /* ReadOnly */, '1'); + + event.source = ChangeSource.AutoLink; + + runEditorReadyContentChangedTest(0, element, event); + }); + + it('ContentChanged with Read only Block Entity in content', () => { + const element = document.createElement('div'); + commitEntity(element, '123', true /* ReadOnly */, '1'); + + runEditorReadyContentChangedTest(0, element, event); + }); + + it('ContentChanged with Editable Inline Entity in content', () => { + const element = document.createElement('span'); + commitEntity(element, '123', false /* ReadOnly */, '1'); + + runEditorReadyContentChangedTest(0, element, event); + }); + + it('ContentChanged with Editable Block Entity in content', () => { + const element = document.createElement('div'); + commitEntity(element, '123', false /* ReadOnly */, '1'); + + runEditorReadyContentChangedTest(0, element, event); + }); + + it('ContentChanged with Normal Element', () => { + const element = document.createElement('div'); + runEditorReadyContentChangedTest(0, element, event); + }); + + it('ContentChanged with no elements', () => { + const element = document.createElement('div'); + runEditorReadyContentChangedTest(0, element, event); + }); + + it('ContentChanged with invalid delimiters', () => { + const element = document.createElement('div'); + runEditorReadyContentChangedTest(0, element, event, node => { + addDelimiters(node as HTMLElement); + node.parentElement?.removeChild(node); + }); + }); + }); + + describe('Before Paste |', () => { + function runTest(expectedDelimiters: number, elementToUse?: Node) { + const rootDiv = document.createElement('div'); + if (elementToUse) { + rootDiv.appendChild(elementToUse); + } + const additionalAllowedCssClasses: string[] = []; + inlineEntityOnPluginEvent( + ({ + eventType: PluginEventType.BeforePaste, + clipboardData: {}, + fragment: rootDiv, + sanitizingOption: { + additionalAllowedCssClasses, + }, + }), + editor + ); + expect(rootDiv.querySelectorAll(DELIMITER_SELECTOR).length).toBe(expectedDelimiters); + expect(additionalAllowedCssClasses).toContain(DelimiterClasses.DELIMITER_AFTER); + expect(additionalAllowedCssClasses).toContain(DelimiterClasses.DELIMITER_BEFORE); + } + + it('Before Paste with Read only Inline Entity in content', () => { + const element = document.createElement('span'); + commitEntity(element, '123', true /* ReadOnly */, '1'); + + runTest(2, element); + }); + + it('Before Paste with Read only Block Entity in content', () => { + const element = document.createElement('div'); + commitEntity(element, '123', true /* ReadOnly */, '1'); + + runTest(0, element); + }); + + it('Before Paste with Editable Inline Entity in content', () => { + const element = document.createElement('span'); + commitEntity(element, '123', false /* ReadOnly */, '1'); + + runTest(0, element); + }); + + it('Before Paste with Editable Block Entity in content', () => { + const element = document.createElement('div'); + commitEntity(element, '123', false /* ReadOnly */, '1'); + + runTest(0, element); + }); + + it('Before Paste with Normal Element', () => { + const element = document.createElement('div'); + runTest(0, element); + }); + + it('Before Paste with no elements', () => { + const element = document.createElement('div'); + runTest(0, element); + }); + }); +}); + +function addEntityBeforeEach(entity: Entity, wrapper: HTMLElement) { + entity = { + id: 'test', + isReadonly: true, + type: 'Test', + wrapper, + }; + + commitEntity(wrapper, 'test', true, 'test'); + addDelimiters(wrapper); + + return { + entity, + delimiterAfter: wrapper.nextElementSibling, + delimiterBefore: wrapper.previousElementSibling, + }; +} + +function getDelimiter(entity: Entity, after: boolean) { + return (after + ? entity.wrapper.nextElementSibling! + : entity.wrapper.previousElementSibling!) as HTMLElement; +} diff --git a/packages/roosterjs-editor-core/test/corePlugins/lifecyclePluginTest.ts b/packages/roosterjs-editor-core/test/corePlugins/lifecyclePluginTest.ts index 77f25db6548e..66ff6c30c5a3 100644 --- a/packages/roosterjs-editor-core/test/corePlugins/lifecyclePluginTest.ts +++ b/packages/roosterjs-editor-core/test/corePlugins/lifecyclePluginTest.ts @@ -1,5 +1,5 @@ import LifecyclePlugin from '../../lib/corePlugins/LifecyclePlugin'; -import { ChangeSource, IEditor, NodePosition, PluginEventType } from 'roosterjs-editor-types'; +import { DarkColorHandler, IEditor, PluginEventType } from 'roosterjs-editor-types'; describe('LifecyclePlugin', () => { const getDarkColor = (color: string) => color; @@ -13,32 +13,22 @@ describe('LifecyclePlugin', () => { triggerPluginEvent, setContent: (content: string) => (div.innerHTML = content), getFocusedPosition: () => null, + getDarkColorHandler: () => null, })); - expect(state.defaultFormat.textColor).toBe(''); - expect(state.defaultFormat.backgroundColor).toBe(''); - - // Reset these getters, we can ignore them since we have already verified them - delete state.defaultFormat.textColor; - delete state.defaultFormat.backgroundColor; + expect(state.defaultFormat).toBeNull(); expect(state).toEqual({ customData: {}, - defaultFormat: { - fontFamily: '', - fontSize: '', - textColors: undefined, - backgroundColors: undefined, - bold: undefined, - italic: undefined, - underline: undefined, - }, + defaultFormat: null, isDarkMode: false, - onExternalContentTransform: undefined, + onExternalContentTransform: null, experimentalFeatures: [], shadowEditSelectionPath: null, shadowEditFragment: null, shadowEditTableSelectionPath: null, + shadowEditImageSelectionPath: null, + shadowEditEntities: null, getDarkColor, }); @@ -71,27 +61,22 @@ describe('LifecyclePlugin', () => { triggerPluginEvent, setContent: (content: string) => (div.innerHTML = content), getFocusedPosition: () => null, + getDarkColorHandler: () => null, })); expect(state).toEqual({ customData: {}, defaultFormat: { fontFamily: 'arial', - fontSize: '', - textColor: '', - textColors: undefined, - backgroundColor: '', - backgroundColors: undefined, - bold: undefined, - italic: undefined, - underline: undefined, }, isDarkMode: false, - onExternalContentTransform: undefined, + onExternalContentTransform: null, experimentalFeatures: [], shadowEditFragment: null, shadowEditSelectionPath: null, shadowEditTableSelectionPath: null, + shadowEditImageSelectionPath: null, + shadowEditEntities: null, getDarkColor, }); @@ -115,6 +100,7 @@ describe('LifecyclePlugin', () => { triggerPluginEvent, setContent: (content: string) => (div.innerHTML = content), getFocusedPosition: () => null, + getDarkColorHandler: () => null, })); expect(div.isContentEditable).toBeTrue(); @@ -136,6 +122,7 @@ describe('LifecyclePlugin', () => { triggerPluginEvent, setContent: (content: string) => (div.innerHTML = content), getFocusedPosition: () => null, + getDarkColorHandler: () => null, })); expect(div.isContentEditable).toBeFalse(); @@ -147,192 +134,3 @@ describe('LifecyclePlugin', () => { expect(div.isContentEditable).toBeFalse(); }); }); - -describe('recalculateDefaultFormat', () => { - let div: HTMLDivElement; - let plugin: LifecyclePlugin; - - beforeEach(() => { - div = document.createElement('div'); - document.body.appendChild(div); - div.style.fontFamily = 'arial'; - div.style.fontSize = '14pt'; - div.style.color = 'black'; - }); - - afterEach(() => { - document.body.removeChild(div); - div = null; - }); - - it('get default format', () => { - plugin = new LifecyclePlugin({}, div); - expect(plugin.getState().defaultFormat).toBeNull(); - }); - - it('no default format, light mode', () => { - plugin = new LifecyclePlugin({}, div); - plugin.initialize(({ - setContent: () => {}, - triggerPluginEvent: () => {}, - getFocusedPosition: () => null, - })); - - expect(plugin.getState().defaultFormat).toEqual({ - fontFamily: 'arial', - fontSize: '14pt', - textColor: 'rgb(0, 0, 0)', - textColors: undefined, - backgroundColor: '', - backgroundColors: undefined, - bold: undefined, - italic: undefined, - underline: undefined, - }); - }); - - it('no default format, dark mode', () => { - plugin = new LifecyclePlugin({ inDarkMode: true }, div); - plugin.initialize(({ - setContent: () => {}, - triggerPluginEvent: () => {}, - getFocusedPosition: () => null, - })); - - // First time it initials the default format - expect(plugin.getState().defaultFormat).toEqual({ - fontFamily: 'arial', - fontSize: '14pt', - textColor: 'rgb(0, 0, 0)', - textColors: undefined, - backgroundColor: '', - backgroundColors: undefined, - bold: undefined, - italic: undefined, - underline: undefined, - }); - - // Second time it calculate default format for dark mode - plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, - source: ChangeSource.SwitchToDarkMode, - }); - expect(plugin.getState().isDarkMode).toBeTrue(); - expect(plugin.getState().defaultFormat).toEqual({ - fontFamily: 'arial', - fontSize: '14pt', - textColor: 'rgb(255,255,255)', - textColors: { - darkModeColor: 'rgb(255,255,255)', - lightModeColor: 'rgb(0,0,0)', - }, - backgroundColor: 'rgb(51,51,51)', - backgroundColors: { - darkModeColor: 'rgb(51,51,51)', - lightModeColor: 'rgb(255,255,255)', - }, - bold: undefined, - italic: undefined, - underline: undefined, - }); - }); - - it('has default format, light mode', () => { - plugin = new LifecyclePlugin( - { - defaultFormat: { - bold: true, - fontFamily: 'arial', - }, - }, - div - ); - plugin.initialize(({ - setContent: () => {}, - triggerPluginEvent: () => {}, - getFocusedPosition: () => null, - })); - - expect(plugin.getState().defaultFormat).toEqual({ - fontFamily: 'arial', - fontSize: '14pt', - textColor: 'rgb(0, 0, 0)', - textColors: undefined, - backgroundColor: '', - backgroundColors: undefined, - bold: true, - italic: undefined, - underline: undefined, - }); - }); - - it('has default format, dark mode', () => { - plugin = new LifecyclePlugin( - { - inDarkMode: true, - defaultFormat: { - bold: true, - fontFamily: 'arial', - }, - }, - div - ); - plugin.initialize(({ - setContent: () => {}, - triggerPluginEvent: () => {}, - getFocusedPosition: () => null, - })); - - expect(plugin.getState().defaultFormat).toEqual({ - fontFamily: 'arial', - fontSize: '14pt', - textColor: 'rgb(255,255,255)', - textColors: { darkModeColor: 'rgb(255,255,255)', lightModeColor: 'rgb(0,0,0)' }, - backgroundColor: 'rgb(51,51,51)', - backgroundColors: { - darkModeColor: 'rgb(51,51,51)', - lightModeColor: 'rgb(255,255,255)', - }, - bold: true, - italic: undefined, - underline: undefined, - }); - }); - - it('has empty default format', () => { - plugin = new LifecyclePlugin( - { - defaultFormat: {}, - }, - div - ); - plugin.initialize(({ - setContent: () => {}, - triggerPluginEvent: () => {}, - getFocusedPosition: () => null, - })); - - expect(plugin.getState().defaultFormat).toEqual({}); - - plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, - source: ChangeSource.SwitchToDarkMode, - }); - - expect(plugin.getState().isDarkMode).toBeTrue(); - expect(plugin.getState().defaultFormat).toEqual({ - fontFamily: 'arial', - fontSize: '14pt', - textColor: 'rgb(255,255,255)', - textColors: { darkModeColor: 'rgb(255,255,255)', lightModeColor: 'rgb(0,0,0)' }, - backgroundColor: 'rgb(51,51,51)', - backgroundColors: { - darkModeColor: 'rgb(51,51,51)', - lightModeColor: 'rgb(255,255,255)', - }, - bold: undefined, - italic: undefined, - underline: undefined, - }); - }); -}); diff --git a/packages/roosterjs-editor-core/test/corePlugins/normalizeTablePluginTest.ts b/packages/roosterjs-editor-core/test/corePlugins/normalizeTablePluginTest.ts new file mode 100644 index 000000000000..976a4c29ce01 --- /dev/null +++ b/packages/roosterjs-editor-core/test/corePlugins/normalizeTablePluginTest.ts @@ -0,0 +1,408 @@ +import NormalizeTablePlugin from '../../lib/corePlugins/NormalizeTablePlugin'; +import { createElement } from 'roosterjs-editor-dom'; +import { + IEditor, + PluginEventType, + SelectionRangeTypes, + CreateElementData, +} from 'roosterjs-editor-types'; + +describe('NormalizeTablePlugin', () => { + let plugin: NormalizeTablePlugin; + let editor: IEditor; + let getSelectionRangeEx: jasmine.Spy; + + beforeEach(() => { + getSelectionRangeEx = jasmine.createSpy('getSelectionRangeEx'); + editor = ({ + getSelectionRangeEx, + }); + + plugin = new NormalizeTablePlugin(); + plugin.initialize(editor); + }); + + afterEach(() => { + plugin.dispose(); + plugin = null; + editor = null; + }); + + it('No table 1', () => { + editor.queryElements = () => []; + + plugin.onPluginEvent({ + eventType: PluginEventType.EditorReady, + }); + plugin.onPluginEvent({ + eventType: PluginEventType.ContentChanged, + source: '', + }); + + expect(getSelectionRangeEx).not.toHaveBeenCalled(); + }); + + it('No table 2', () => { + editor.getElementAtCursor = () => null; + + plugin.onPluginEvent({ + eventType: PluginEventType.BeforePaste, + fragment: document.createDocumentFragment(), + }); + plugin.onPluginEvent({ + eventType: PluginEventType.MouseDown, + rawEvent: {}, + }); + plugin.onPluginEvent({ + eventType: PluginEventType.KeyDown, + rawEvent: {}, + }); + expect(getSelectionRangeEx).not.toHaveBeenCalled(); + }); + + it('Only query for keyboard event when SHIFT is pressed', () => { + const getElementAtCursor = jasmine.createSpy('getElementAtCursor'); + editor.getElementAtCursor = getElementAtCursor; + + plugin.onPluginEvent({ + eventType: PluginEventType.KeyDown, + rawEvent: { + shiftKey: false, + }, + }); + + expect(getElementAtCursor).not.toHaveBeenCalled(); + + plugin.onPluginEvent({ + eventType: PluginEventType.KeyDown, + rawEvent: { + shiftKey: true, + }, + }); + + expect(getElementAtCursor).toHaveBeenCalled(); + }); + + function runTest(input: CreateElementData, expected: string) { + const table = createElement(input, document); + + editor.queryElements = () => [table]; + plugin.onPluginEvent({ + eventType: PluginEventType.EditorReady, + }); + + expect(table.outerHTML).toBe(expected); + } + + function createTr(text: string): CreateElementData { + return { + tag: 'tr', + children: [ + { + tag: 'td', + children: [text], + }, + ], + }; + } + + function createTableSection(tag: string, ...texts: string[]): CreateElementData { + return { + tag, + children: texts.map(createTr), + }; + } + + function createTable(...args: CreateElementData[]): CreateElementData { + return { + tag: 'table', + children: args, + }; + } + + it('Table already has THEAD/TBODY/TFOOT', () => { + const html = + '
test1
test2
test3
test4
'; + runTest( + createTable( + createTableSection('thead', 'test1'), + createTableSection('tbody', 'test2', 'test3'), + createTableSection('tfoot', 'test4') + ), + html + ); + }); + + it('Table only has TR', () => { + runTest( + createTable(createTr('test1'), createTr('test2')), + '
test1
test2
' + ); + }); + + it('Table has TR and TBODY 1', () => { + runTest( + createTable(createTr('test1'), createTableSection('tbody', 'test2')), + '
test1
test2
' + ); + }); + + it('Table has TR and TBODY 2', () => { + runTest( + createTable(createTableSection('tbody', 'test1'), createTr('test2')), + '
test1
test2
' + ); + }); + + it('Table has TR and TBODY and TR', () => { + runTest( + createTable(createTr('test1'), createTableSection('tbody', 'test2'), createTr('test3')), + '
test1
test2
test3
' + ); + }); + + it('Table has THEAD and TR and TFOOT', () => { + runTest( + createTable( + createTableSection('thead', 'test1'), + createTr('test2'), + createTr('test3'), + createTableSection('tfoot', 'test4') + ), + '
test1
test2
test3
test4
' + ); + }); + + it('Table has THEAD and TR and TBODY and TR and TFOOT', () => { + runTest( + createTable( + createTableSection('thead', 'test1'), + createTr('test2'), + createTableSection('tbody', 'test3'), + createTr('test4'), + createTableSection('tfoot', 'test5') + ), + '' + + '' + + '' + + '' + + '
test1
test2
test3
test4
test5
' + ); + }); + + it('Table has THEAD and TBODY and TR and TBODY and TFOOT', () => { + runTest( + createTable( + createTableSection('thead', 'test1'), + createTableSection('tbody', 'test2'), + createTr('test3'), + createTableSection('tbody', 'test4'), + createTableSection('tfoot', 'test5') + ), + '' + + '' + + '' + + '' + + '
test1
test2
test3
test4
test5
' + ); + }); + + it('Restore selection after normalization', () => { + const table = createElement(createTable(createTr('test1')), document); + const startContainer = {}; + const endContainer = {}; + const startOffset = 1; + const endOffset = 2; + const select = jasmine.createSpy('select'); + + editor.queryElements = () => [table]; + editor.select = select; + + getSelectionRangeEx.and.returnValue({ + type: SelectionRangeTypes.Normal, + ranges: [ + { + startContainer, + endContainer, + startOffset, + endOffset, + }, + ], + }); + plugin.onPluginEvent({ + eventType: PluginEventType.EditorReady, + }); + + expect(table.outerHTML).toBe('
test1
'); + expect(select).toHaveBeenCalledWith(startContainer, startOffset, endContainer, endOffset); + }); + + it('Restore table selection after normalization', () => { + const table = createElement(createTable(createTr('test1')), document); + const coordinates = { + firstCell: { x: 1, y: 2 }, + lastCell: { x: 3, y: 4 }, + }; + const select = jasmine.createSpy('select'); + + editor.queryElements = () => [table]; + editor.select = select; + + getSelectionRangeEx.and.returnValue({ + type: SelectionRangeTypes.TableSelection, + table, + coordinates, + }); + plugin.onPluginEvent({ + eventType: PluginEventType.EditorReady, + }); + + expect(table.outerHTML).toBe('
test1
'); + expect(select).toHaveBeenCalledWith(table, coordinates); + }); + + it('Normalize table with THEAD With colgroup, Tbody, Tfoot', () => { + runTest( + createTable( + getTheadWithColgroup(createTableSection), + createTableSection('tbody', 'test2'), + createTr('test3'), + createTableSection('tbody', 'test4'), + createTableSection('tfoot', 'test5') + ), + '' + + '' + + '' + + '' + + '
test1
test2
test3
test4
test5
' + ); + }); + + it('Table already has THEAD With colgroup/TBODY/TFOOT', () => { + runTest( + createTable( + getTheadWithColgroup(createTableSection), + createTableSection('tbody', 'test2', 'test3'), + createTableSection('tfoot', 'test4') + ), + '' + + '' + + '' + + '
test1
test2
test3
test4
' + ); + }); + + it('Table has THEAD With colgroup and TR and TFOOT', () => { + runTest( + createTable( + getTheadWithColgroup(createTableSection), + createTr('test2'), + createTr('test3'), + createTableSection('tfoot', 'test4') + ), + '' + + '' + + '' + + '
test1
test2
test3
test4
' + ); + }); + + it('Table has THEAD With colgroup and TR and TBODY and TR and TFOOT', () => { + runTest( + createTable( + getTheadWithColgroup(createTableSection), + createTr('test2'), + createTableSection('tbody', 'test3'), + createTr('test4'), + createTableSection('tfoot', 'test5') + ), + '' + + '' + + '' + + '' + + '
test1
test2
test3
test4
test5
' + ); + }); + + it('Table has THEAD With colgroup and TR and TBODY and TR and TFOOT 2', () => { + runTest( + createTable( + createTableSection('thead', 'test1'), + getColgroup(), + createTr('test2'), + createTableSection('tbody', 'test3'), + createTr('test4'), + createTableSection('tfoot', 'test5') + ), + '' + + '' + + '' + + '' + + '
test1
test2
test3
test4
test5
' + ); + }); + + it('Table has THEAD With colgroup and TBODY and TR and TBODY and TFOOT', () => { + runTest( + createTable( + getTheadWithColgroup(createTableSection), + createTableSection('tbody', 'test2'), + createTr('test3'), + createTableSection('tbody', 'test4'), + createTableSection('tfoot', 'test5') + ), + '' + + '' + + '' + + '' + + '
test1
test2
test3
test4
test5
' + ); + }); + + it('Table has TR and TBODY and a orphaned colgroup 1', () => { + runTest( + createTable( + getColgroup(), + createTr('test1'), + getColgroup(), + createTableSection('tbody', 'test2'), + getColgroup() + ), + '' + + '' + + '' + + '
test1
test2
' + ); + }); + + it('Table has TR and TBODY and a orphaned colgroup 2', () => { + runTest( + createTable(createTableSection('tbody', 'test1'), createTr('test2'), getColgroup()), + '' + + '' + + '
test1
test2
' + ); + }); +}); + +function getTheadWithColgroup( + createTableSection: (tag: string, ...texts: string[]) => CreateElementData +) { + const thead = createTableSection('thead', 'test1'); + thead.children?.push(getColgroup()); + return thead; +} + +function getColgroup(): CreateElementData { + return { + tag: 'colgroup', + children: [ + { + tag: 'col', + }, + { + tag: 'col', + }, + ], + }; +} diff --git a/packages/roosterjs-editor-core/test/corePlugins/pendingFormatStateTest.ts b/packages/roosterjs-editor-core/test/corePlugins/pendingFormatStateTest.ts index c9b9a71d5698..0f6c161b8d18 100644 --- a/packages/roosterjs-editor-core/test/corePlugins/pendingFormatStateTest.ts +++ b/packages/roosterjs-editor-core/test/corePlugins/pendingFormatStateTest.ts @@ -21,6 +21,7 @@ describe('PendingFormatStatePlugin', () => { expect(state).toEqual({ pendableFormatPosition: null, pendableFormatState: null, + pendableFormatSpan: null, }); }); @@ -31,7 +32,7 @@ describe('PendingFormatStatePlugin', () => { state.pendableFormatState = {}; plugin.onPluginEvent({ eventType: PluginEventType.KeyDown, - rawEvent: null, + rawEvent: ({} as any) as KeyboardEvent, }); expect(state.pendableFormatPosition).toBeNull(); expect(state.pendableFormatState).toBeNull(); @@ -46,7 +47,7 @@ describe('PendingFormatStatePlugin', () => { state.pendableFormatState = formatState; plugin.onPluginEvent({ eventType: PluginEventType.KeyDown, - rawEvent: null, + rawEvent: ({} as any) as KeyboardEvent, }); expect(state.pendableFormatPosition).toBe(position); diff --git a/packages/roosterjs-editor-core/test/corePlugins/typeInContainerPluginTest.ts b/packages/roosterjs-editor-core/test/corePlugins/typeInContainerPluginTest.ts index 116f0755571b..fcaa13d46570 100644 --- a/packages/roosterjs-editor-core/test/corePlugins/typeInContainerPluginTest.ts +++ b/packages/roosterjs-editor-core/test/corePlugins/typeInContainerPluginTest.ts @@ -13,7 +13,9 @@ describe('TypeInContainerPlugin', () => { }; beforeEach(() => { - runAsync = jasmine.createSpy('runAsync').and.callFake((callback: () => any) => callback()); + runAsync = jasmine + .createSpy('runAsync') + .and.callFake((callback: (editor: IEditor) => any) => callback(editor)); select = jasmine.createSpy('select'); ensureTypeInContainer = jasmine.createSpy('ensureTypeInContainer'); editor = ({ diff --git a/packages/roosterjs-editor-core/test/corePlugins/undoPluginTest.ts b/packages/roosterjs-editor-core/test/corePlugins/undoPluginTest.ts index 366c2cd9c328..24fd07f656c4 100644 --- a/packages/roosterjs-editor-core/test/corePlugins/undoPluginTest.ts +++ b/packages/roosterjs-editor-core/test/corePlugins/undoPluginTest.ts @@ -1,7 +1,14 @@ import UndoPlugin from '../../lib/corePlugins/UndoPlugin'; -import { IEditor, Keys, PluginEventType, UndoPluginState } from 'roosterjs-editor-types'; -import { Position } from 'roosterjs-editor-dom'; import { itChromeOnly } from '../TestHelper'; +import { Position } from 'roosterjs-editor-dom'; +import { + ChangeSource, + IEditor, + Keys, + PluginEventType, + SelectionRangeTypes, + UndoPluginState, +} from 'roosterjs-editor-types'; describe('UndoPlugin', () => { let plugin: UndoPlugin; @@ -18,6 +25,7 @@ describe('UndoPlugin', () => { editor = ({ isInIME, addUndoSnapshot, + getFocusedPosition: jasmine.createSpy().and.returnValue({}), }); plugin.initialize(editor); }); @@ -437,24 +445,54 @@ describe('UndoPlugin', () => { }); it('can undo autoComplete', () => { - state.snapshotsService.addSnapshot('snapshot 1', false); - state.snapshotsService.addSnapshot('snapshot 2', true); - state.snapshotsService.addSnapshot('snapshot 3', false); + state.snapshotsService.addSnapshot( + { html: 'snapshot 1', metadata: null, knownColors: [] }, + false + ); + state.snapshotsService.addSnapshot( + { html: 'snapshot 2', metadata: null, knownColors: [] }, + true + ); + state.snapshotsService.addSnapshot( + { html: 'snapshot 3', metadata: null, knownColors: [] }, + false + ); expect(state.snapshotsService.canUndoAutoComplete()).toBeTrue(); }); it('cannot undo autoComplete', () => { - state.snapshotsService.addSnapshot('snapshot 1', false); - state.snapshotsService.addSnapshot('snapshot 2', true); - state.snapshotsService.addSnapshot('snapshot 3', false); - state.snapshotsService.addSnapshot('snapshot 4', false); + state.snapshotsService.addSnapshot( + { html: 'snapshot 1', metadata: null, knownColors: [] }, + false + ); + state.snapshotsService.addSnapshot( + { html: 'snapshot 2', metadata: null, knownColors: [] }, + true + ); + state.snapshotsService.addSnapshot( + { html: 'snapshot 3', metadata: null, knownColors: [] }, + false + ); + state.snapshotsService.addSnapshot( + { html: 'snapshot 4', metadata: null, knownColors: [] }, + false + ); expect(state.snapshotsService.canUndoAutoComplete()).toBeFalse(); }); it('Backspace trigger undo when can undo autoComplete', () => { - state.snapshotsService.addSnapshot('snapshot 1', false); - state.snapshotsService.addSnapshot('snapshot 2', true); - state.snapshotsService.addSnapshot('snapshot 3', false); + state.snapshotsService.addSnapshot( + { html: 'snapshot 1', metadata: null, knownColors: [] }, + false + ); + state.snapshotsService.addSnapshot( + { html: 'snapshot 2', metadata: null, knownColors: [] }, + true + ); + state.snapshotsService.addSnapshot( + { html: 'snapshot 3', metadata: null, knownColors: [] }, + false + ); const undo = jasmine.createSpy('undo'); const preventDefault = jasmine.createSpy('preventDefault'); @@ -479,9 +517,18 @@ describe('UndoPlugin', () => { }); it('Other key does not trigger undo auto complete', () => { - state.snapshotsService.addSnapshot('snapshot 1', false); - state.snapshotsService.addSnapshot('snapshot 2', true); - state.snapshotsService.addSnapshot('snapshot 3', false); + state.snapshotsService.addSnapshot( + { html: 'snapshot 1', metadata: null, knownColors: [] }, + false + ); + state.snapshotsService.addSnapshot( + { html: 'snapshot 2', metadata: null, knownColors: [] }, + true + ); + state.snapshotsService.addSnapshot( + { html: 'snapshot 3', metadata: null, knownColors: [] }, + false + ); const undo = jasmine.createSpy('undo'); const preventDefault = jasmine.createSpy('preventDefault'); @@ -507,9 +554,18 @@ describe('UndoPlugin', () => { }); it('Another undo snapshot is added, cannot undo autocomplete any more', () => { - state.snapshotsService.addSnapshot('snapshot 1', false); - state.snapshotsService.addSnapshot('snapshot 2', true); - state.snapshotsService.addSnapshot('snapshot 3', false); + state.snapshotsService.addSnapshot( + { html: 'snapshot 1', metadata: null, knownColors: [] }, + false + ); + state.snapshotsService.addSnapshot( + { html: 'snapshot 2', metadata: null, knownColors: [] }, + true + ); + state.snapshotsService.addSnapshot( + { html: 'snapshot 3', metadata: null, knownColors: [] }, + false + ); const undo = jasmine.createSpy('undo'); const preventDefault = jasmine.createSpy('preventDefault'); @@ -518,7 +574,11 @@ describe('UndoPlugin', () => { editor.undo = undo; editor.getSelectionRange = () => range; editor.getFocusedPosition = () => pos; - editor.addUndoSnapshot = () => state.snapshotsService.addSnapshot('snapshot 4', false); + editor.addUndoSnapshot = () => + state.snapshotsService.addSnapshot( + { html: 'snapshot 4', metadata: null, knownColors: [] }, + false + ); state.autoCompletePosition = pos; plugin.onPluginEvent({ @@ -537,7 +597,10 @@ describe('UndoPlugin', () => { }); it('Position changed, cannot undo autocomplete for Backspace', () => { - state.snapshotsService.addSnapshot('snapshot 1', false); + state.snapshotsService.addSnapshot( + { html: 'snapshot 1', metadata: null, knownColors: [] }, + false + ); const undo = jasmine.createSpy('undo'); const preventDefault = jasmine.createSpy('preventDefault'); @@ -550,7 +613,11 @@ describe('UndoPlugin', () => { (pos2).offset++; // hack, just want to make pos2 different from pos editor.getFocusedPosition = () => pos2; - editor.addUndoSnapshot = () => state.snapshotsService.addSnapshot('snapshot 4', false); + editor.addUndoSnapshot = () => + state.snapshotsService.addSnapshot( + { html: 'snapshot 4', metadata: null, knownColors: [] }, + false + ); // Press backspace first time, to let plugin remember last pressed key plugin.onPluginEvent({ @@ -561,8 +628,14 @@ describe('UndoPlugin', () => { }), }); - state.snapshotsService.addSnapshot('snapshot 2', true); - state.snapshotsService.addSnapshot('snapshot 3', false); + state.snapshotsService.addSnapshot( + { html: 'snapshot 2', metadata: null, knownColors: [] }, + true + ); + state.snapshotsService.addSnapshot( + { html: 'snapshot 3', metadata: null, knownColors: [] }, + false + ); state.autoCompletePosition = pos; plugin.onPluginEvent({ @@ -578,4 +651,258 @@ describe('UndoPlugin', () => { expect(state.autoCompletePosition).not.toBeNull(); expect(state.snapshotsService.canUndoAutoComplete()).toBeTrue(); }); + + it('Pass in undoSnapshotService', () => { + const canMove = jasmine.createSpy('canMove').and.returnValue(true); + const move = jasmine.createSpy('move').and.returnValue('test'); + const addSnapshot = jasmine.createSpy('addSnapshot'); + const clearRedo = jasmine.createSpy('clearRedo'); + const canUndoAutoComplete = jasmine.createSpy('canUndoAutoComplete').and.returnValue(true); + + const plugin = new UndoPlugin({ + undoSnapshotService: { canMove, move, addSnapshot, clearRedo, canUndoAutoComplete }, + }); + const state = plugin.getState(); + + const canMoveResult = state.snapshotsService.canMove(1); + const moveResult = state.snapshotsService.move(2); + state.snapshotsService.addSnapshot( + { + html: 'test', + metadata: { + type: SelectionRangeTypes.Normal, + isDarkMode: false, + start: [1], + end: [2], + }, + knownColors: [], + }, + false + ); + state.snapshotsService.clearRedo(); + const canUndoAutoCompleteResult = state.snapshotsService.canUndoAutoComplete(); + + expect(canMove).toHaveBeenCalledWith(1); + expect(move).toHaveBeenCalledWith(2); + expect(addSnapshot).toHaveBeenCalledWith( + 'test', + false + ); + expect(clearRedo).toHaveBeenCalled(); + expect(canUndoAutoComplete).toHaveBeenCalled(); + + expect(canMoveResult).toBe(true); + expect(moveResult).toEqual({ html: 'test', metadata: null, knownColors: [] }); + expect(canUndoAutoCompleteResult).toBe(true); + }); + + it('Pass in undoSnapshotService', () => { + const snapshot = { + html: 'test', + metadata: { + type: SelectionRangeTypes.Normal, + isDarkMode: false, + start: [1], + end: [2], + }, + }; + const canMove = jasmine.createSpy('canMove').and.returnValue(true); + const move = jasmine.createSpy('move').and.returnValue(snapshot); + const addSnapshot = jasmine.createSpy('addSnapshot'); + const clearRedo = jasmine.createSpy('clearRedo'); + const canUndoAutoComplete = jasmine.createSpy('canUndoAutoComplete').and.returnValue(true); + + const plugin = new UndoPlugin({ + undoMetadataSnapshotService: { + canMove, + move, + addSnapshot, + clearRedo, + canUndoAutoComplete, + }, + }); + const state = plugin.getState(); + + const canMoveResult = state.snapshotsService.canMove(1); + const moveResult = state.snapshotsService.move(2); + state.snapshotsService.addSnapshot(snapshot, false); + state.snapshotsService.clearRedo(); + const canUndoAutoCompleteResult = state.snapshotsService.canUndoAutoComplete(); + + expect(canMove).toHaveBeenCalledWith(1); + expect(move).toHaveBeenCalledWith(2); + expect(addSnapshot).toHaveBeenCalledWith(snapshot, false); + expect(clearRedo).toHaveBeenCalled(); + expect(canUndoAutoComplete).toHaveBeenCalled(); + + expect(canMoveResult).toBe(true); + expect(moveResult).toEqual(snapshot); + expect(canUndoAutoCompleteResult).toBe(true); + }); + + it('Pass in undoSnapshotService and undoSnapshotService', () => { + const snapshot = { + html: 'test', + metadata: { + type: SelectionRangeTypes.Normal, + isDarkMode: false, + start: [1], + end: [2], + }, + }; + + const canMove1 = jasmine.createSpy('canMove'); + const move1 = jasmine.createSpy('move'); + const addSnapshot1 = jasmine.createSpy('addSnapshot'); + const clearRedo1 = jasmine.createSpy('clearRedo'); + const canUndoAutoComplete1 = jasmine.createSpy('canUndoAutoComplete'); + + const canMove2 = jasmine.createSpy('canMove'); + const move2 = jasmine.createSpy('move'); + const addSnapshot2 = jasmine.createSpy('addSnapshot'); + const clearRedo2 = jasmine.createSpy('clearRedo'); + const canUndoAutoComplete2 = jasmine.createSpy('canUndoAutoComplete'); + + const plugin = new UndoPlugin({ + undoMetadataSnapshotService: { + canMove: canMove1, + move: move1, + addSnapshot: addSnapshot1, + clearRedo: clearRedo1, + canUndoAutoComplete: canUndoAutoComplete1, + }, + undoSnapshotService: { + canMove: canMove2, + move: move2, + addSnapshot: addSnapshot2, + clearRedo: clearRedo2, + canUndoAutoComplete: canUndoAutoComplete2, + }, + }); + const state = plugin.getState(); + + state.snapshotsService.canMove(1); + state.snapshotsService.move(2); + state.snapshotsService.addSnapshot(snapshot, false); + state.snapshotsService.clearRedo(); + state.snapshotsService.canUndoAutoComplete(); + + expect(canMove1).toHaveBeenCalled(); + expect(move1).toHaveBeenCalled(); + expect(addSnapshot1).toHaveBeenCalled(); + expect(clearRedo1).toHaveBeenCalled(); + expect(canUndoAutoComplete1).toHaveBeenCalled(); + + expect(canMove2).not.toHaveBeenCalled(); + expect(move2).not.toHaveBeenCalled(); + expect(addSnapshot2).not.toHaveBeenCalled(); + expect(clearRedo2).not.toHaveBeenCalled(); + expect(canUndoAutoComplete2).not.toHaveBeenCalled(); + }); + + it('Handle BeforeKeyboardEditing event', () => { + const canMove1 = jasmine.createSpy('canMove'); + const move1 = jasmine.createSpy('move'); + const addSnapshot1 = jasmine.createSpy('addSnapshot'); + const clearRedo1 = jasmine.createSpy('clearRedo'); + const canUndoAutoComplete1 = jasmine.createSpy('canUndoAutoComplete'); + const addUndoSnapshot = jasmine.createSpy('addUndoSnapshot'); + const plugin = new UndoPlugin({ + undoMetadataSnapshotService: { + canMove: canMove1, + move: move1, + addSnapshot: addSnapshot1, + clearRedo: clearRedo1, + canUndoAutoComplete: canUndoAutoComplete1, + }, + }); + + const mockedEditor = ({ + isInIME: () => false, + addUndoSnapshot, + } as any) as IEditor; + (plugin).lastKeyPress = 1; + + plugin.initialize(mockedEditor); + plugin.onPluginEvent({ + eventType: PluginEventType.BeforeKeyboardEditing, + rawEvent: { + which: Keys.DELETE, + } as any, + }); + + expect((plugin).lastKeyPress).toBe(Keys.DELETE); + expect(addSnapshot1).not.toHaveBeenCalled(); + expect(addUndoSnapshot).toHaveBeenCalledTimes(1); + }); + + it('Handle ContentChanged event with Keyboard source', () => { + const canMove1 = jasmine.createSpy('canMove'); + const move1 = jasmine.createSpy('move'); + const addSnapshot1 = jasmine.createSpy('addSnapshot'); + const clearRedo1 = jasmine.createSpy('clearRedo'); + const canUndoAutoComplete1 = jasmine.createSpy('canUndoAutoComplete'); + const addUndoSnapshot = jasmine.createSpy('addUndoSnapshot'); + const plugin = new UndoPlugin({ + undoMetadataSnapshotService: { + canMove: canMove1, + move: move1, + addSnapshot: addSnapshot1, + clearRedo: clearRedo1, + canUndoAutoComplete: canUndoAutoComplete1, + }, + }); + + const mockedEditor = ({ + isInIME: () => false, + addUndoSnapshot, + } as any) as IEditor; + (plugin).lastKeyPress = 1; + + plugin.initialize(mockedEditor); + plugin.onPluginEvent({ + eventType: PluginEventType.ContentChanged, + data: Keys.DELETE, + source: ChangeSource.Keyboard, + }); + + expect((plugin).lastKeyPress).toBe(1); + expect(addSnapshot1).not.toHaveBeenCalled(); + expect(addUndoSnapshot).not.toHaveBeenCalled(); + }); + + it('Handle ContentChanged event with Keyboard source and same key code', () => { + const canMove1 = jasmine.createSpy('canMove'); + const move1 = jasmine.createSpy('move'); + const addSnapshot1 = jasmine.createSpy('addSnapshot'); + const clearRedo1 = jasmine.createSpy('clearRedo'); + const canUndoAutoComplete1 = jasmine.createSpy('canUndoAutoComplete'); + const addUndoSnapshot = jasmine.createSpy('addUndoSnapshot'); + const plugin = new UndoPlugin({ + undoMetadataSnapshotService: { + canMove: canMove1, + move: move1, + addSnapshot: addSnapshot1, + clearRedo: clearRedo1, + canUndoAutoComplete: canUndoAutoComplete1, + }, + }); + + const mockedEditor = ({ + isInIME: () => false, + addUndoSnapshot, + } as any) as IEditor; + (plugin).lastKeyPress = Keys.DELETE; + + plugin.initialize(mockedEditor); + plugin.onPluginEvent({ + eventType: PluginEventType.ContentChanged, + data: Keys.DELETE, + source: ChangeSource.Keyboard, + }); + + expect((plugin).lastKeyPress).toBe(Keys.DELETE); + expect(addSnapshot1).not.toHaveBeenCalled(); + expect(addUndoSnapshot).not.toHaveBeenCalled(); + }); }); diff --git a/packages/roosterjs-editor-core/test/editor/DarkColorHandlerImplTest.ts b/packages/roosterjs-editor-core/test/editor/DarkColorHandlerImplTest.ts new file mode 100644 index 000000000000..d74087234df3 --- /dev/null +++ b/packages/roosterjs-editor-core/test/editor/DarkColorHandlerImplTest.ts @@ -0,0 +1,363 @@ +import DarkColorHandlerImpl from '../../lib/editor/DarkColorHandlerImpl'; +import { ColorKeyAndValue } from 'roosterjs-editor-types'; + +describe('DarkColorHandlerImpl.parseColorValue', () => { + function getDarkColor(color: string) { + return color + color; + } + + let div: HTMLElement; + let handler: DarkColorHandlerImpl; + + beforeEach(() => { + div = document.createElement('div'); + handler = new DarkColorHandlerImpl(div, getDarkColor); + }); + + function runTest(input: string, expectedOutput: ColorKeyAndValue) { + const result = handler.parseColorValue(input); + + expect(result).toEqual(expectedOutput); + } + + it('empty color', () => { + runTest(null!, { + key: undefined, + lightModeColor: '', + darkModeColor: undefined, + }); + }); + + it('simple color', () => { + runTest('aa', { + key: undefined, + lightModeColor: 'aa', + darkModeColor: undefined, + }); + }); + + it('var color without fallback', () => { + runTest('var(--bb)', { + key: undefined, + lightModeColor: '', + darkModeColor: undefined, + }); + }); + + it('var color with fallback', () => { + runTest('var(--bb,cc)', { + key: '--bb', + lightModeColor: 'cc', + darkModeColor: undefined, + }); + }); + + it('var color with fallback, has dark color', () => { + (handler as any).knownColors = { + '--bb': { + lightModeColor: 'dd', + darkModeColor: 'ee', + }, + }; + runTest('var(--bb,cc)', { + key: '--bb', + lightModeColor: 'cc', + darkModeColor: 'ee', + }); + }); + + function runDarkTest(input: string, expectedOutput: ColorKeyAndValue) { + const result = handler.parseColorValue(input, true); + + expect(result).toEqual(expectedOutput); + } + + it('simple color in dark mode', () => { + runDarkTest('aa', { + key: undefined, + lightModeColor: '', + darkModeColor: undefined, + }); + }); + + it('var color in dark mode', () => { + runDarkTest('var(--aa, bb)', { + key: '--aa', + lightModeColor: 'bb', + darkModeColor: undefined, + }); + }); + + it('known simple color in dark mode', () => { + (handler as any).knownColors = { + '--bb': { + lightModeColor: '#ff0000', + darkModeColor: '#00ffff', + }, + }; + runDarkTest('#00ffff', { + key: undefined, + lightModeColor: '#ff0000', + darkModeColor: '#00ffff', + }); + }); +}); + +describe('DarkColorHandlerImpl.registerColor', () => { + function getDarkColor(color: string) { + return color + color; + } + + let setProperty: jasmine.Spy; + let handler: DarkColorHandlerImpl; + + beforeEach(() => { + setProperty = jasmine.createSpy('setProperty'); + const div = ({ + style: { + setProperty, + }, + } as any) as HTMLElement; + handler = new DarkColorHandlerImpl(div, getDarkColor); + }); + + function runTest( + input: string, + isDark: boolean, + darkColor: string | undefined, + expectedOutput: string, + expectedKnownColors: Record, + expectedSetPropertyCalls: [string, string][] + ) { + const result = handler.registerColor(input, isDark, darkColor); + + expect(result).toEqual(expectedOutput); + expect((handler as any).knownColors).toEqual(expectedKnownColors); + expect(setProperty).toHaveBeenCalledTimes(expectedSetPropertyCalls.length); + + expectedSetPropertyCalls.forEach(v => { + expect(setProperty).toHaveBeenCalledWith(...v); + }); + } + + it('empty color, light mode', () => { + runTest('', false, undefined, '', {}, []); + }); + + it('simple color, light mode', () => { + runTest('red', false, undefined, 'red', {}, []); + }); + + it('empty color, dark mode', () => { + runTest('', true, undefined, '', {}, []); + }); + + it('simple color, dark mode', () => { + runTest( + 'red', + true, + undefined, + 'var(--darkColor_red, red)', + { + '--darkColor_red': { + lightModeColor: 'red', + darkModeColor: 'redred', + }, + }, + [['--darkColor_red', 'redred']] + ); + }); + + it('simple color, dark mode, with dark color', () => { + runTest( + 'red', + true, + 'blue', + 'var(--darkColor_red, red)', + { + '--darkColor_red': { + lightModeColor: 'red', + darkModeColor: 'blue', + }, + }, + [['--darkColor_red', 'blue']] + ); + }); + + it('var color, light mode', () => { + runTest('var(--aa, bb)', false, undefined, 'bb', {}, []); + }); + + it('var color, dark mode', () => { + runTest( + 'var(--aa, bb)', + true, + undefined, + 'var(--aa, bb)', + { + '--aa': { + lightModeColor: 'bb', + darkModeColor: 'bbbb', + }, + }, + [['--aa', 'bbbb']] + ); + }); + + it('var color, dark mode with dark color', () => { + runTest( + 'var(--aa, bb)', + true, + 'cc', + 'var(--aa, bb)', + { + '--aa': { + lightModeColor: 'bb', + darkModeColor: 'cc', + }, + }, + [['--aa', 'cc']] + ); + }); + + it('var color, dark mode with dark color and existing dark color', () => { + (handler as any).knownColors['--aa'] = { + lightModeColor: 'dd', + darkModeColor: 'ee', + }; + runTest( + 'var(--aa, bb)', + true, + 'cc', + 'var(--aa, bb)', + { + '--aa': { + lightModeColor: 'dd', + darkModeColor: 'ee', + }, + }, + [] + ); + }); +}); + +describe('DarkColorHandlerImpl.reset', () => { + it('Reset', () => { + const removeProperty = jasmine.createSpy('removeProperty'); + const div = ({ + style: { + removeProperty, + }, + } as any) as HTMLElement; + const handler = new DarkColorHandlerImpl(div, null!); + + (handler as any).knownColors = { + '--aa': { + lightModeColor: 'bb', + darkModeColor: 'cc', + }, + '--dd': { + lightModeColor: 'ee', + darkModeColor: 'ff', + }, + }; + + handler.reset(); + + expect((handler as any).knownColors).toEqual({}); + expect(removeProperty).toHaveBeenCalledTimes(2); + expect(removeProperty).toHaveBeenCalledWith('--aa'); + expect(removeProperty).toHaveBeenCalledWith('--dd'); + }); +}); + +describe('DarkColorHandlerImpl.findLightColorFromDarkColor', () => { + it('Not found', () => { + const div = ({} as any) as HTMLElement; + const handler = new DarkColorHandlerImpl(div, null!); + + const result = handler.findLightColorFromDarkColor('#010203'); + + expect(result).toEqual(null); + }); + + it('Found: HEX to RGB', () => { + const div = ({} as any) as HTMLElement; + const handler = new DarkColorHandlerImpl(div, null!); + + (handler as any).knownColors = { + '--bb': { + lightModeColor: 'bb', + darkModeColor: 'rgb(4,5,6)', + }, + '--aa': { + lightModeColor: 'aa', + darkModeColor: 'rgb(1,2,3)', + }, + }; + + const result = handler.findLightColorFromDarkColor('#010203'); + + expect(result).toEqual('aa'); + }); + + it('Found: HEX to HEX', () => { + const div = ({} as any) as HTMLElement; + const handler = new DarkColorHandlerImpl(div, null!); + + (handler as any).knownColors = { + '--bb': { + lightModeColor: 'bb', + darkModeColor: 'rgb(4,5,6)', + }, + '--aa': { + lightModeColor: 'aa', + darkModeColor: '#010203', + }, + }; + + const result = handler.findLightColorFromDarkColor('#010203'); + + expect(result).toEqual('aa'); + }); + + it('Found: RGB to HEX', () => { + const div = ({} as any) as HTMLElement; + const handler = new DarkColorHandlerImpl(div, null!); + + (handler as any).knownColors = { + '--bb': { + lightModeColor: 'bb', + darkModeColor: 'rgb(4,5,6)', + }, + '--aa': { + lightModeColor: 'aa', + darkModeColor: '#010203', + }, + }; + + const result = handler.findLightColorFromDarkColor('rgb(1,2,3)'); + + expect(result).toEqual('aa'); + }); + + it('Found: RGB to RGB', () => { + const div = ({} as any) as HTMLElement; + const handler = new DarkColorHandlerImpl(div, null!); + + (handler as any).knownColors = { + '--bb': { + lightModeColor: 'bb', + darkModeColor: 'rgb(4,5,6)', + }, + '--aa': { + lightModeColor: 'aa', + darkModeColor: 'rgb(1, 2, 3)', + }, + }; + + const result = handler.findLightColorFromDarkColor('rgb(1,2,3)'); + + expect(result).toEqual('aa'); + }); +}); diff --git a/packages/roosterjs-editor-core/test/editor/newEditorTest.ts b/packages/roosterjs-editor-core/test/editor/newEditorTest.ts index 9a7805861f2f..0502e02e9474 100644 --- a/packages/roosterjs-editor-core/test/editor/newEditorTest.ts +++ b/packages/roosterjs-editor-core/test/editor/newEditorTest.ts @@ -52,6 +52,8 @@ describe('Editor', () => { 'MouseUp', 'CopyPaste', 'Entity', + 'ImageSelection', + 'NormalizeTable', 'Lifecycle', ]); @@ -62,6 +64,7 @@ describe('Editor', () => { stopPrintableKeyboardEventPropagation: true, contextMenuProviders: [], tableSelectionRange: null, + imageSelectionRange: null, }); if (!Browser.isChrome) { expect(core.edit).toEqual({ @@ -69,16 +72,16 @@ describe('Editor', () => { }); } expect(core.entity).toEqual({ - knownEntityElements: [], - shadowEntityCache: {}, + entityMap: {}, }); expect(core.lifecycle.customData).toEqual({}); expect(core.lifecycle.isDarkMode).toBeFalse(); - expect(core.lifecycle.onExternalContentTransform).toBeUndefined(); + expect(core.lifecycle.onExternalContentTransform).toBeNull(); expect(core.lifecycle.defaultFormat).toBeDefined(); expect(core.pendingFormatState).toEqual({ pendableFormatPosition: null, pendableFormatState: null, + pendableFormatSpan: null, }); expect(core.undo.isRestoring).toBeFalse(); expect(core.undo.hasNewContent).toBeFalse(); @@ -158,6 +161,8 @@ describe('Editor', () => { 'test mouse up', 'CopyPaste', 'Entity', + 'ImageSelection', + 'NormalizeTable', 'Lifecycle', ]); @@ -168,6 +173,7 @@ describe('Editor', () => { stopPrintableKeyboardEventPropagation: false, contextMenuProviders: [], tableSelectionRange: null, + imageSelectionRange: null, }); if (!Browser.isChrome) { expect(core.edit).toEqual({ @@ -175,8 +181,7 @@ describe('Editor', () => { }); } expect(core.entity).toEqual({ - knownEntityElements: [], - shadowEntityCache: {}, + entityMap: {}, }); expect(core.lifecycle.customData).toEqual({}); expect(core.lifecycle.isDarkMode).toBeTrue(); @@ -186,6 +191,7 @@ describe('Editor', () => { expect(core.pendingFormatState).toEqual({ pendableFormatPosition: null, pendableFormatState: null, + pendableFormatSpan: null, }); expect(core.undo.isRestoring).toBeFalse(); expect(core.undo.hasNewContent).toBeFalse(); @@ -197,4 +203,16 @@ describe('Editor', () => { expect(core.undo.snapshotsService.clearRedo).toBeDefined(); expect(core.undo.snapshotsService.move).toBeDefined(); }); + + it('create Editor with initial content as a table with colgroup', () => { + const div = document.createElement('div'); + const editor = new Editor(div, { + initialContent: + '
col 1col 2
col 1col 2
', + }); + + expect(editor.getContent()).toEqual( + '
col 1col 2
col 1col 2
' + ); + }); }); diff --git a/packages/roosterjs-editor-core/tsconfig.child.json b/packages/roosterjs-editor-core/tsconfig.child.json deleted file mode 100644 index d265759eea8f..000000000000 --- a/packages/roosterjs-editor-core/tsconfig.child.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "strict": false - }, - "extends": "../tsconfig.json", - "include": ["./lib/**/*.ts"], - "references": [ - { "path": "../roosterjs-editor-types/tsconfig.child.json" }, - { "path": "../roosterjs-editor-dom/tsconfig.child.json" } - ] -} diff --git a/packages/roosterjs-editor-dom/lib/blockElements/NodeBlockElement.ts b/packages/roosterjs-editor-dom/lib/blockElements/NodeBlockElement.ts index a45231ed0fdc..3075a1ff4f77 100644 --- a/packages/roosterjs-editor-dom/lib/blockElements/NodeBlockElement.ts +++ b/packages/roosterjs-editor-dom/lib/blockElements/NodeBlockElement.ts @@ -62,6 +62,6 @@ export default class NodeBlockElement implements BlockElement { * Get the text content of this block element */ public getTextContent(): string { - return this.element ? this.element.textContent : ''; + return this.element?.textContent || ''; } } diff --git a/packages/roosterjs-editor-dom/lib/blockElements/StartEndBlockElement.ts b/packages/roosterjs-editor-dom/lib/blockElements/StartEndBlockElement.ts index 5d1902bd64c1..cf118de165d2 100644 --- a/packages/roosterjs-editor-dom/lib/blockElements/StartEndBlockElement.ts +++ b/packages/roosterjs-editor-dom/lib/blockElements/StartEndBlockElement.ts @@ -22,11 +22,12 @@ const STRUCTURE_NODE_TAGS = ['TD', 'TH', 'LI', 'BLOCKQUOTE']; export default class StartEndBlockElement implements BlockElement { constructor(private rootNode: Node, private startNode: Node, private endNode: Node) {} - static getBlockContext(node: Node): HTMLElement { - while (node && !isBlockElement(node)) { - node = node.parentNode; + static getBlockContext(node: Node): HTMLElement | null { + let currentNode: Node | null = node; + while (currentNode && !isBlockElement(currentNode)) { + currentNode = currentNode.parentNode; } - return node as HTMLElement; + return currentNode as HTMLElement; } /** @@ -35,12 +36,10 @@ export default class StartEndBlockElement implements BlockElement { * If the content nodes are included in root node with other nodes, split root node */ public collapseToSingleElement(): HTMLElement { - let nodes = collapseNodes( - StartEndBlockElement.getBlockContext(this.startNode), - this.startNode, - this.endNode, - true /*canSplitParent*/ - ); + const nodeContext = StartEndBlockElement.getBlockContext(this.startNode); + let nodes = nodeContext + ? collapseNodes(nodeContext, this.startNode, this.endNode, true /*canSplitParent*/) + : []; let blockContext = StartEndBlockElement.getBlockContext(this.startNode); while ( nodes[0] && @@ -48,7 +47,12 @@ export default class StartEndBlockElement implements BlockElement { nodes[0].parentNode != this.rootNode && STRUCTURE_NODE_TAGS.indexOf(getTagOfNode(nodes[0].parentNode)) < 0 ) { - nodes = [splitBalancedNodeRange(nodes)]; + const newNode = splitBalancedNodeRange(nodes); + if (newNode) { + nodes = [newNode]; + } else { + break; + } } return nodes.length == 1 && isBlockElement(nodes[0]) ? (nodes[0] as HTMLElement) diff --git a/packages/roosterjs-editor-dom/lib/blockElements/getBlockElementAtNode.ts b/packages/roosterjs-editor-dom/lib/blockElements/getBlockElementAtNode.ts index 9c954220c47a..a024f72e0a81 100644 --- a/packages/roosterjs-editor-dom/lib/blockElements/getBlockElementAtNode.ts +++ b/packages/roosterjs-editor-dom/lib/blockElements/getBlockElementAtNode.ts @@ -31,7 +31,10 @@ import { BlockElement } from 'roosterjs-editor-types'; * @param rootNode Root node of the scope, the block element will be inside of this node * @param node The node to get BlockElement start from */ -export default function getBlockElementAtNode(rootNode: Node, node: Node): BlockElement { +export default function getBlockElementAtNode( + rootNode: Node, + node: Node | null +): BlockElement | null { if (!contains(rootNode, node)) { return null; } @@ -39,14 +42,20 @@ export default function getBlockElementAtNode(rootNode: Node, node: Node): Block // Identify the containing block. This serves as ceiling for traversing down below // NOTE: this container block could be just the rootNode, // which cannot be used to create block element. We will special case handle it later on - let containerBlockNode = StartEndBlockElement.getBlockContext(node); - if (containerBlockNode == node) { + let containerBlockNode = StartEndBlockElement.getBlockContext(node!); + if (!containerBlockNode) { + return null; + } else if (containerBlockNode == node) { return new NodeBlockElement(containerBlockNode); } // Find the head and leaf node in the block - let headNode = findHeadTailLeafNode(node, containerBlockNode, false /*isTail*/); - let tailNode = findHeadTailLeafNode(node, containerBlockNode, true /*isTail*/); + let headNode = findHeadTailLeafNode(node!, containerBlockNode, false /*isTail*/); + let tailNode = findHeadTailLeafNode(node!, containerBlockNode, true /*isTail*/); + + if (!headNode || !tailNode) { + return null; + } // At this point, we have the head and tail of a block, here are some examples and where head and tail point to // 1) <root><div>hello<br></div></root>, head: hello, tail: <br> @@ -54,6 +63,11 @@ export default function getBlockElementAtNode(rootNode: Node, node: Node): Block // Both are actually completely and exclusively wrapped in a parent div, and can be represented with a Node block // So we shall try to collapse as much as we can to the nearest common ancestor let nodes = collapseNodes(rootNode, headNode, tailNode, false /*canSplitParent*/); + + if (nodes.length === 0) { + return null; + } + headNode = nodes[0]; tailNode = nodes[nodes.length - 1]; @@ -71,7 +85,7 @@ export default function getBlockElementAtNode(rootNode: Node, node: Node): Block headNode = tailNode = parentNode; } break; - } else if (parentNode != rootNode) { + } else if (parentNode && parentNode != rootNode) { // Continue collapsing to parent headNode = tailNode = parentNode; } else { @@ -102,8 +116,8 @@ function findHeadTailLeafNode(node: Node, containerBlockNode: Node, isTail: bool } while (result) { - let sibling = node; - while (!(sibling = isTail ? node.nextSibling : node.previousSibling)) { + let sibling: Node | null = node; + while (node.parentNode && !(sibling = isTail ? node.nextSibling : node.previousSibling)) { node = node.parentNode; if (node == containerBlockNode) { return result; diff --git a/packages/roosterjs-editor-dom/lib/blockElements/getFirstLastBlockElement.ts b/packages/roosterjs-editor-dom/lib/blockElements/getFirstLastBlockElement.ts index fc7c7fbed858..4ff668828192 100644 --- a/packages/roosterjs-editor-dom/lib/blockElements/getFirstLastBlockElement.ts +++ b/packages/roosterjs-editor-dom/lib/blockElements/getFirstLastBlockElement.ts @@ -7,10 +7,13 @@ import { BlockElement } from 'roosterjs-editor-types'; * @param rootNode The root node to get BlockElement from * @param isFirst True to get first BlockElement, false to get last BlockElement */ -export default function getFirstLastBlockElement(rootNode: Node, isFirst: boolean): BlockElement { - let node = rootNode; +export default function getFirstLastBlockElement( + rootNode: Node, + isFirst: boolean +): BlockElement | null { + let node: Node | null = rootNode; do { node = node && (isFirst ? node.firstChild : node.lastChild); } while (node && node.firstChild); - return node && getBlockElementAtNode(rootNode, node); + return (node && getBlockElementAtNode(rootNode, node)) || null; } diff --git a/packages/roosterjs-editor-dom/lib/blockElements/tsconfig.child.json b/packages/roosterjs-editor-dom/lib/blockElements/tsconfig.child.json deleted file mode 100644 index bb32e970fa80..000000000000 --- a/packages/roosterjs-editor-dom/lib/blockElements/tsconfig.child.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "strict": false - }, - "extends": "../../../tsconfig.json", - "include": ["./**/*.ts"], - "references": [ - { "path": "../../../roosterjs-editor-types/tsconfig.child.json" }, - { "path": "../selection/tsconfig.child.json" }, - { "path": "../utils/tsconfig.child.json" } - ] -} diff --git a/packages/roosterjs-editor-dom/lib/clipboard/createFragmentFromClipboardData.ts b/packages/roosterjs-editor-dom/lib/clipboard/createFragmentFromClipboardData.ts new file mode 100644 index 000000000000..0ea053caf3e2 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/clipboard/createFragmentFromClipboardData.ts @@ -0,0 +1,84 @@ +import applyFormat from '../utils/applyFormat'; +import applyTextStyle from '../inlineElements/applyTextStyle'; +import handleImagePaste from './handleImagePaste'; +import handleTextPaste from './handleTextPaste'; +import moveChildNodes from '../utils/moveChildNodes'; +import retrieveMetadataFromClipboard from './retrieveMetadataFromClipboard'; +import sanitizeContent from './sanitizePasteContent'; +import { + BeforePasteEvent, + ClipboardData, + DefaultFormat, + EditorCore, + NodePosition, +} from 'roosterjs-editor-types'; + +/** + * Create a DocumentFragment for paste from a ClipboardData + * @param core The EditorCore object. + * @param clipboardData Clipboard data retrieved from clipboard + * @param position The position to paste to + * @param pasteAsText True to force use plain text as the content to paste, false to choose HTML or Image if any + * @param applyCurrentStyle True if apply format of current selection to the pasted content, + * @param pasteAsImage Whether to force paste as image + * @param event Event to trigger. + * false to keep original format + */ +export default function createFragmentFromClipboardData( + core: EditorCore, + clipboardData: ClipboardData, + position: NodePosition | null, + pasteAsText: boolean, + applyCurrentStyle: boolean, + pasteAsImage: boolean, + event: BeforePasteEvent +) { + const { fragment } = event; + const { rawHtml, text, imageDataUri } = clipboardData; + let doc: Document | undefined = rawHtml + ? new DOMParser().parseFromString(core.trustedHTMLHandler(rawHtml), 'text/html') + : undefined; + + // Step 2: Retrieve Metadata from Html and the Html that was copied. + retrieveMetadataFromClipboard(doc, event, core.trustedHTMLHandler); + + // Step 3: Fill the BeforePasteEvent object, especially the fragment for paste + if ((pasteAsImage && imageDataUri) || (!pasteAsText && !text && imageDataUri)) { + // Paste image + handleImagePaste(imageDataUri, fragment); + } else if (!pasteAsText && rawHtml && doc ? doc.body : false) { + moveChildNodes(fragment, doc?.body); + + if (applyCurrentStyle && position) { + const format = getCurrentFormat(core, position.node); + applyTextStyle(fragment, node => applyFormat(node, format)); + } + } else if (text) { + // Paste text + handleTextPaste(text, position, fragment); + } + + // Step 4: Trigger BeforePasteEvent so that plugins can do proper change before paste + core.api.triggerEvent(core, event, true /*broadcast*/); + + // Step 5. Sanitize the fragment before paste to make sure the content is safe + sanitizeContent(event, position); + + return fragment; +} + +function getCurrentFormat(core: EditorCore, node: Node): DefaultFormat { + const pendableFormat = core.api.getPendableFormatState(core, true /** forceGetStateFromDOM*/); + const styleBasedFormat = core.api.getStyleBasedFormatState(core, node); + return { + fontFamily: styleBasedFormat.fontName, + fontSize: styleBasedFormat.fontSize, + textColor: styleBasedFormat.textColor, + backgroundColor: styleBasedFormat.backgroundColor, + textColors: styleBasedFormat.textColors, + backgroundColors: styleBasedFormat.backgroundColors, + bold: pendableFormat.isBold, + italic: pendableFormat.isItalic, + underline: pendableFormat.isUnderline, + }; +} diff --git a/packages/roosterjs-editor-dom/lib/clipboard/extractClipboardEvent.ts b/packages/roosterjs-editor-dom/lib/clipboard/extractClipboardEvent.ts index cd61005f894f..20e4a0c047a6 100644 --- a/packages/roosterjs-editor-dom/lib/clipboard/extractClipboardEvent.ts +++ b/packages/roosterjs-editor-dom/lib/clipboard/extractClipboardEvent.ts @@ -1,6 +1,7 @@ import extractClipboardItems from './extractClipboardItems'; import extractClipboardItemsForIE from './extractClipboardItemsForIE'; -import toArray from '../utils/toArray'; +import toArray from '../jsUtils/toArray'; +import { Browser } from '../utils/Browser'; import { ClipboardData, ExtractClipboardEventOption } from 'roosterjs-editor-types'; interface WindowForIE extends Window { @@ -13,6 +14,7 @@ interface WindowForIE extends Window { * @param event The paste event * @param callback Callback function when data is ready * @param options Options to retrieve more items from the event, including HTML string and other customized items + * @param rangeBeforePaste Optional range to be removed when pasting in Android * @returns An object with the following properties: * types: Available types from the clipboard event * text: Plain text from the clipboard event @@ -24,16 +26,28 @@ interface WindowForIE extends Window { export default function extractClipboardEvent( event: ClipboardEvent, callback: (clipboardData: ClipboardData) => void, - options?: ExtractClipboardEventOption + options?: ExtractClipboardEventOption, + rangeBeforePaste?: Range ) { const dataTransfer = event.clipboardData || - (((event.target).ownerDocument.defaultView)).clipboardData; + (((event.target).ownerDocument?.defaultView)).clipboardData; if (dataTransfer.items) { event.preventDefault(); - extractClipboardItems(toArray(dataTransfer.items), options).then(callback); + extractClipboardItems(toArray(dataTransfer.items), options).then( + (clipboardData: ClipboardData) => { + removeContents(rangeBeforePaste); + callback(clipboardData); + } + ); } else { extractClipboardItemsForIE(dataTransfer, callback, options); } } + +function removeContents(range?: Range) { + if (Browser.isAndroid && range) { + range.deleteContents(); + } +} diff --git a/packages/roosterjs-editor-dom/lib/clipboard/extractClipboardItems.ts b/packages/roosterjs-editor-dom/lib/clipboard/extractClipboardItems.ts index d28189f5f188..93a1ad631e9d 100644 --- a/packages/roosterjs-editor-dom/lib/clipboard/extractClipboardItems.ts +++ b/packages/roosterjs-editor-dom/lib/clipboard/extractClipboardItems.ts @@ -1,125 +1,136 @@ -import readFile from '../utils/readFile'; -import { Browser } from '../utils/Browser'; -import { - ClipboardData, - ContentType, - ContentTypePrefix, - EdgeLinkPreview, - ExtractClipboardItemsOption, -} from 'roosterjs-editor-types'; - -// HTML header to indicate where is the HTML content started from. -// Sample header: -// Version:0.9 -// StartHTML:71 -// EndHTML:170 -// StartFragment:140 -// EndFragment:160 -// StartSelection:140 -// EndSelection:160 -const CLIPBOARD_HTML_HEADER_REGEX = /^Version:[0-9\.]+\s+StartHTML:\s*([0-9]+)\s+EndHTML:\s*([0-9]+)\s+/i; -const OTHER_TEXT_TYPE = ContentTypePrefix.Text + '*'; -const EDGE_LINK_PREVIEW = 'link-preview'; -const ContentHandlers: { - [contentType: string]: (data: ClipboardData, value: string, type: string) => void; -} = { - [ContentType.HTML]: (data, value) => - (data.rawHtml = Browser.isEdge ? workaroundForEdge(value) : value), - [ContentType.PlainText]: (data, value) => (data.text = value), - [OTHER_TEXT_TYPE]: (data, value, type) => (data.customValues[type] = value), -}; - -/** - * Extract clipboard items to be a ClipboardData object for IE - * @param items The clipboard items retrieve from a DataTransfer object - * @param callback Callback function when data is ready - * @returns An object with the following properties: - * types: Available types from the clipboard event - * text: Plain text from the clipboard event - * image: Image file from the clipboard event - * html: Html string from the clipboard event. When set to null, it means there's no HTML found from the event. - * When set to undefined, it means can't retrieve HTML string, there may be HTML string but direct retrieving is - * not supported by browser. - */ -export default function extractClipboardItems( - items: DataTransferItem[], - options?: ExtractClipboardItemsOption -): Promise { - const data: ClipboardData = { - types: [], - text: '', - image: null, - rawHtml: null, - customValues: {}, - }; - - const contentHandlers = { ...ContentHandlers }; - - if (options?.allowLinkPreview) { - contentHandlers[ContentTypePrefix.Text + EDGE_LINK_PREVIEW] = tryParseLinkPreview; - } - - return Promise.all( - (items || []).map(item => { - const type = item.type; - - if (type.indexOf(ContentTypePrefix.Image) == 0 && !data.image && item.kind == 'file') { - data.types.push(type); - data.image = item.getAsFile(); - return new Promise(resolve => { - readFile(data.image, dataUrl => { - data.imageDataUri = dataUrl; - resolve(); - }); - }); - } else { - const customType = getAllowedCustomType(type, options?.allowedCustomPasteType); - const handler = - contentHandlers[type] || (customType ? contentHandlers[OTHER_TEXT_TYPE] : null); - return new Promise(resolve => - handler - ? item.getAsString(value => { - data.types.push(type); - handler(data, value, customType); - resolve(); - }) - : resolve() - ); - } - }) - ).then(() => data); -} - -/** - * Edge sometimes doesn't remove the headers, which cause we paste more things then expected. - * So we need to remove it in our code - * @param html The HTML string got from clipboard - */ -function workaroundForEdge(html: string) { - const headerValues = CLIPBOARD_HTML_HEADER_REGEX.exec(html); - - if (headerValues?.length == 3) { - const start = parseInt(headerValues[1]); - const end = parseInt(headerValues[2]); - if (start > 0 && end > start) { - html = html.substring(start, end); - } - } - - return html; -} - -function tryParseLinkPreview(data: ClipboardData, value: string) { - try { - data.customValues[EDGE_LINK_PREVIEW] = value; - data.linkPreview = JSON.parse(value) as EdgeLinkPreview; - } catch {} -} - -function getAllowedCustomType(type: string, allowedCustomPasteType: string[]) { - let textType = - type.indexOf(ContentTypePrefix.Text) == 0 - ? type.substr(ContentTypePrefix.Text.length) - : null; - return textType && allowedCustomPasteType?.indexOf(textType) >= 0 ? textType : null; -} +import readFile from '../utils/readFile'; +import { Browser } from '../utils/Browser'; +import { + ClipboardData, + ContentType, + ContentTypePrefix, + EdgeLinkPreview, + ExtractClipboardItemsOption, +} from 'roosterjs-editor-types'; + +// HTML header to indicate where is the HTML content started from. +// Sample header: +// Version:0.9 +// StartHTML:71 +// EndHTML:170 +// StartFragment:140 +// EndFragment:160 +// StartSelection:140 +// EndSelection:160 +const CLIPBOARD_HTML_HEADER_REGEX = /^Version:[0-9\.]+\s+StartHTML:\s*([0-9]+)\s+EndHTML:\s*([0-9]+)\s+/i; +const OTHER_TEXT_TYPE = ContentTypePrefix.Text + '*'; +const EDGE_LINK_PREVIEW = 'link-preview'; +const ContentHandlers: { + [contentType: string]: (data: ClipboardData, value: string, type?: string) => void; +} = { + [ContentType.HTML]: (data, value) => + (data.rawHtml = Browser.isEdge ? workaroundForEdge(value) : value), + [ContentType.PlainText]: (data, value) => (data.text = value), + [OTHER_TEXT_TYPE]: (data, value, type?) => !!type && (data.customValues[type] = value), + [ContentTypePrefix.Text + EDGE_LINK_PREVIEW]: tryParseLinkPreview, +}; + +/** + * Extract clipboard items to be a ClipboardData object for IE + * @param items The clipboard items retrieve from a DataTransfer object + * @param callback Callback function when data is ready + * @returns An object with the following properties: + * types: Available types from the clipboard event + * text: Plain text from the clipboard event + * image: Image file from the clipboard event + * html: Html string from the clipboard event. When set to null, it means there's no HTML found from the event. + * When set to undefined, it means can't retrieve HTML string, there may be HTML string but direct retrieving is + * not supported by browser. + */ +export default function extractClipboardItems( + items: DataTransferItem[], + options?: ExtractClipboardItemsOption +): Promise { + const data: ClipboardData = { + types: [], + text: '', + image: null, + files: [], + rawHtml: null, + customValues: {}, + }; + + return Promise.all( + (items || []).map(item => { + const type = item.type; + + if (type.indexOf(ContentTypePrefix.Image) == 0 && !data.image && item.kind == 'file') { + data.types.push(type); + data.image = item.getAsFile(); + return new Promise(resolve => { + if (data.image) { + readFile(data.image, dataUrl => { + data.imageDataUri = dataUrl; + resolve(); + }); + } else { + resolve(); + } + }); + } else if (item.kind == 'file') { + return new Promise(resolve => { + const file = item.getAsFile(); + if (!!file) { + data.types.push(type); + data.files!.push(file); + } + resolve(); + }); + } else { + const customType = getAllowedCustomType(type, options?.allowedCustomPasteType); + const handler = + ContentHandlers[type] || (customType ? ContentHandlers[OTHER_TEXT_TYPE] : null); + return new Promise(resolve => + handler + ? item.getAsString(value => { + data.types.push(type); + handler(data, value, customType); + resolve(); + }) + : resolve() + ); + } + }) + ).then(() => data); +} + +/** + * Edge sometimes doesn't remove the headers, which cause we paste more things then expected. + * So we need to remove it in our code + * @param html The HTML string got from clipboard + */ +function workaroundForEdge(html: string) { + const headerValues = CLIPBOARD_HTML_HEADER_REGEX.exec(html); + + if (headerValues?.length == 3) { + const start = parseInt(headerValues[1]); + const end = parseInt(headerValues[2]); + if (start > 0 && end > start) { + html = html.substring(start, end); + } + } + + return html; +} + +function tryParseLinkPreview(data: ClipboardData, value: string) { + try { + data.customValues[EDGE_LINK_PREVIEW] = value; + data.linkPreview = JSON.parse(value) as EdgeLinkPreview; + } catch {} +} + +function getAllowedCustomType(type: string, allowedCustomPasteType?: string[]) { + const textType = + type.indexOf(ContentTypePrefix.Text) == 0 + ? type.substring(ContentTypePrefix.Text.length) + : null; + const index = + allowedCustomPasteType && textType ? allowedCustomPasteType.indexOf(textType) : -1; + return textType && index >= 0 ? textType : undefined; +} diff --git a/packages/roosterjs-editor-dom/lib/clipboard/extractClipboardItemsForIE.ts b/packages/roosterjs-editor-dom/lib/clipboard/extractClipboardItemsForIE.ts index 6a46469df431..2ec9776b5417 100644 --- a/packages/roosterjs-editor-dom/lib/clipboard/extractClipboardItemsForIE.ts +++ b/packages/roosterjs-editor-dom/lib/clipboard/extractClipboardItemsForIE.ts @@ -1,67 +1,68 @@ -import readFile from '../utils/readFile'; -import toArray from '../utils/toArray'; -import { - ClipboardData, - ContentTypePrefix, - ExtractClipboardItemsForIEOptions, -} from 'roosterjs-editor-types'; - -/** - * Extract clipboard items to be a ClipboardData object for IE - * @param dataTransfer The clipboard items retrieve from a DataTransfer object - * @param callback Callback function when data is ready - * @returns An object with the following properties: - * types: Available types from the clipboard event - * text: Plain text from the clipboard event - * image: Image file from the clipboard event - * html: Html string from the clipboard event. When set to null, it means there's no HTML found from the event. - * When set to undefined, it means can't retrieve HTML string, there may be HTML string but direct retrieving is - * not supported by browser. - */ -export default function extractClipboardItemsForIE( - dataTransfer: DataTransfer, - callback: (data: ClipboardData) => void, - options: ExtractClipboardItemsForIEOptions -) { - const clipboardData: ClipboardData = { - types: dataTransfer.types ? toArray(dataTransfer.types) : [], - text: dataTransfer.getData('text'), - image: null, - rawHtml: null, - customValues: {}, - }; - - for (let i = 0; i < (dataTransfer.files ? dataTransfer.files.length : 0); i++) { - let file = dataTransfer.files.item(i); - if (file.type?.indexOf(ContentTypePrefix.Image) == 0) { - clipboardData.image = file; - break; - } - } - - const nextStep = () => { - if (clipboardData.image) { - readFile(clipboardData.image, dataUrl => { - clipboardData.imageDataUri = dataUrl; - callback(clipboardData); - }); - } else { - callback(clipboardData); - } - }; - - if (options?.getTempDiv && options?.removeTempDiv) { - const div = options.getTempDiv(); - div.contentEditable = 'true'; - div.innerHTML = ''; - div.focus(); - div.ownerDocument.defaultView.setTimeout(() => { - clipboardData.rawHtml = div.innerHTML; - options.removeTempDiv(div); - nextStep(); - }, 0); - } else { - clipboardData.rawHtml = undefined; - nextStep(); - } -} +import readFile from '../utils/readFile'; +import toArray from '../jsUtils/toArray'; +import { + ClipboardData, + ContentTypePrefix, + ExtractClipboardItemsForIEOptions, +} from 'roosterjs-editor-types'; + +/** + * Extract clipboard items to be a ClipboardData object for IE + * @param dataTransfer The clipboard items retrieve from a DataTransfer object + * @param callback Callback function when data is ready + * @returns An object with the following properties: + * types: Available types from the clipboard event + * text: Plain text from the clipboard event + * image: Image file from the clipboard event + * html: Html string from the clipboard event. When set to null, it means there's no HTML found from the event. + * When set to undefined, it means can't retrieve HTML string, there may be HTML string but direct retrieving is + * not supported by browser. + */ +export default function extractClipboardItemsForIE( + dataTransfer: DataTransfer, + callback: (data: ClipboardData) => void, + options?: ExtractClipboardItemsForIEOptions +) { + const clipboardData: ClipboardData = { + types: dataTransfer.types ? toArray(dataTransfer.types) : [], + text: dataTransfer.getData('text'), + image: null, + files: [], + rawHtml: null, + customValues: {}, + }; + + for (let i = 0; i < (dataTransfer.files ? dataTransfer.files.length : 0); i++) { + let file = dataTransfer.files.item(i); + if (file?.type?.indexOf(ContentTypePrefix.Image) == 0) { + clipboardData.image = file; + break; + } + } + + const nextStep = () => { + if (clipboardData.image) { + readFile(clipboardData.image, dataUrl => { + clipboardData.imageDataUri = dataUrl; + callback(clipboardData); + }); + } else { + callback(clipboardData); + } + }; + + if (options?.getTempDiv && options?.removeTempDiv) { + const div = options.getTempDiv(); + div.contentEditable = 'true'; + div.innerHTML = ''; + div.focus(); + div.ownerDocument?.defaultView?.setTimeout(() => { + clipboardData.rawHtml = div.innerHTML; + options.removeTempDiv?.(div); + nextStep(); + }, 0); + } else { + clipboardData.rawHtml = undefined; + nextStep(); + } +} diff --git a/packages/roosterjs-editor-dom/lib/clipboard/getPasteType.ts b/packages/roosterjs-editor-dom/lib/clipboard/getPasteType.ts new file mode 100644 index 000000000000..da6863426db8 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/clipboard/getPasteType.ts @@ -0,0 +1,24 @@ +import { PasteType } from 'roosterjs-editor-types'; + +/** + * Get the paste type that will be used corresponding to the configuration + * @param pasteAsText Whether to paste as Text + * @param applyCurrentStyle Whether to apply the current format to the content + * @param pasteAsImage Whether to only paste the image + * @returns + */ +export default function getPasteType( + pasteAsText: boolean, + applyCurrentStyle: boolean, + pasteAsImage: boolean +) { + if (pasteAsText) { + return PasteType.AsPlainText; + } else if (applyCurrentStyle) { + return PasteType.MergeFormat; + } else if (pasteAsImage) { + return PasteType.AsImage; + } else { + return PasteType.Normal; + } +} diff --git a/packages/roosterjs-editor-dom/lib/clipboard/handleImagePaste.ts b/packages/roosterjs-editor-dom/lib/clipboard/handleImagePaste.ts new file mode 100644 index 000000000000..f0e005b87087 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/clipboard/handleImagePaste.ts @@ -0,0 +1,11 @@ +/** + * Handles the content when using the Image Paste Option + * @param imageDataUri the image uri to use for the image + * @param fragment fragment that will contain the content to paste. + */ +export default function handleImagePaste(imageDataUri: string, fragment: DocumentFragment) { + const img = fragment.ownerDocument.createElement('img'); + img.style.maxWidth = '100%'; + img.src = imageDataUri; + fragment.appendChild(img); +} diff --git a/packages/roosterjs-editor-dom/lib/clipboard/handleTextPaste.ts b/packages/roosterjs-editor-dom/lib/clipboard/handleTextPaste.ts new file mode 100644 index 000000000000..fe1a5e84859e --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/clipboard/handleTextPaste.ts @@ -0,0 +1,68 @@ +import wrap from '../utils/wrap'; +import { NodePosition } from 'roosterjs-editor-types'; + +const NBSP_HTML = '\u00A0'; +const ENSP_HTML = '\u2002'; +const TAB_SPACES = 6; + +/** + * handle the content when using the text only option + * @param text Text from clipboard + * @param position current position of the clipboard + * @param fragment fragment that contains the paste content. + */ +export default function handleTextPaste( + text: string, + position: NodePosition | null, + fragment: DocumentFragment +) { + const document = fragment.ownerDocument; + text.split('\n').forEach((line, index, lines) => { + line = line + .replace(/^ /g, NBSP_HTML) + .replace(/\r/g, '') + .replace(/ {2}/g, ' ' + NBSP_HTML); + + if (line.includes('\t')) { + line = transformTabCharacters(line, index === 0 ? position?.offset : 0); + } + + const textNode = document.createTextNode(line); + + // There are 3 scenarios: + // 1. Single line: Paste as it is + // 2. Two lines: Add
between the lines + // 3. 3 or More lines, For first and last line, paste as it is. For middle lines, wrap with DIV, and add BR if it is empty line + if (lines.length == 2 && index == 0) { + // 1 of 2 lines scenario, add BR + fragment.appendChild(textNode); + fragment.appendChild(document.createElement('br')); + } else if (index > 0 && index < lines.length - 1) { + // Middle line of >=3 lines scenario, wrap with DIV + fragment.appendChild(wrap(line == '' ? document.createElement('br') : textNode)); + } else { + // All others, paste as it is + fragment.appendChild(textNode); + } + }); +} + +/** + * @internal + * Transform \t characters into EN SPACE characters + * @param input string NOT containing \n characters + * @example t("\thello", 2) => "    hello" + */ + +export function transformTabCharacters(input: string, initialOffset: number = 0) { + let line = input; + let tIndex: number; + while ((tIndex = line.indexOf('\t')) != -1) { + const lineBefore = line.slice(0, tIndex); + const lineAfter = line.slice(tIndex + 1); + const tabCount = TAB_SPACES - ((lineBefore.length + initialOffset) % TAB_SPACES); + const tabStr = Array(tabCount).fill(ENSP_HTML).join(''); + line = lineBefore + tabStr + lineAfter; + } + return line; +} diff --git a/packages/roosterjs-editor-dom/lib/clipboard/retrieveMetadataFromClipboard.ts b/packages/roosterjs-editor-dom/lib/clipboard/retrieveMetadataFromClipboard.ts new file mode 100644 index 000000000000..f1edc3f81728 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/clipboard/retrieveMetadataFromClipboard.ts @@ -0,0 +1,75 @@ +import getTagOfNode from '../utils/getTagOfNode'; +import toArray from '../jsUtils/toArray'; +import { BeforePasteEvent, TrustedHTMLHandler } from 'roosterjs-editor-types'; + +const START_FRAGMENT = ''; +const END_FRAGMENT = ''; + +/** + * Retrieves the metadata from the content inside of the clipboard + * @param doc Document parsed from the clipboard + * @param event Before Paste event + * @param trustedHTMLHandler the trusted html handler to sanitize the content. + */ +export default function retrieveMetadataFromClipboard( + doc: Document | undefined, + event: BeforePasteEvent, + trustedHTMLHandler: TrustedHTMLHandler +) { + const { clipboardData, sanitizingOption } = event; + const { rawHtml } = clipboardData; + if (rawHtml && doc?.body) { + const attributes = doc.querySelector('html')?.attributes; + (attributes ? toArray(attributes) : []).reduce((attrs, attr) => { + attrs[attr.name] = attr.value; + return attrs; + }, event.htmlAttributes); + toArray(doc.querySelectorAll('meta')).reduce((attrs, meta) => { + attrs[meta.name] = meta.content; + return attrs; + }, event.htmlAttributes); + + clipboardData.htmlFirstLevelChildTags = []; + doc?.body.normalize(); + + for (let i = 0; i < doc?.body.childNodes.length; i++) { + const node = doc?.body.childNodes.item(i); + if (node.nodeType == Node.TEXT_NODE) { + const trimmedString = node.nodeValue?.replace(/(\r\n|\r|\n)/gm, '').trim(); + if (!trimmedString) { + continue; + } + } + const nodeTag = getTagOfNode(node); + if (node.nodeType != Node.COMMENT_NODE) { + clipboardData.htmlFirstLevelChildTags.push(nodeTag); + } + } + // Move all STYLE nodes into header, and save them into sanitizing options. + // Because if we directly move them into a fragment, all sheets under STYLE will be lost. + processStyles(doc, style => { + doc?.head.appendChild(style); + sanitizingOption.additionalGlobalStyleNodes.push(style); + }); + + const startIndex = rawHtml.indexOf(START_FRAGMENT); + const endIndex = rawHtml.lastIndexOf(END_FRAGMENT); + + if (startIndex >= 0 && endIndex >= startIndex + START_FRAGMENT.length) { + event.htmlBefore = rawHtml.substr(0, startIndex); + event.htmlAfter = rawHtml.substr(endIndex + END_FRAGMENT.length); + clipboardData.html = rawHtml.substring(startIndex + START_FRAGMENT.length, endIndex); + doc.body.innerHTML = trustedHTMLHandler(clipboardData.html); + + // Remove style nodes just added by setting innerHTML of body since we already have all + // style nodes in header. + // Here we use doc.body instead of doc because we only want to remove STYLE nodes under BODY + // and the nodes under HEAD are still used when convert global CSS to inline + processStyles(doc.body, style => style.parentNode?.removeChild(style)); + } + } +} + +function processStyles(node: ParentNode, callback: (style: HTMLStyleElement) => void) { + toArray(node.querySelectorAll('style')).forEach(callback); +} diff --git a/packages/roosterjs-editor-dom/lib/clipboard/sanitizePasteContent.ts b/packages/roosterjs-editor-dom/lib/clipboard/sanitizePasteContent.ts new file mode 100644 index 000000000000..f754f26620ab --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/clipboard/sanitizePasteContent.ts @@ -0,0 +1,18 @@ +import getInheritableStyles from '../htmlSanitizer/getInheritableStyles'; +import HtmlSanitizer from '../htmlSanitizer/HtmlSanitizer'; +import { BeforePasteEvent, NodePosition } from 'roosterjs-editor-types'; + +/** + * Sanitize the content from the pasted content + * @param event The before paste event + * @param position the position of the cursor + */ +export default function sanitizePasteContent( + event: BeforePasteEvent, + position: NodePosition | null +) { + const { fragment } = event; + const sanitizer = new HtmlSanitizer(event.sanitizingOption); + sanitizer.convertGlobalCssToInlineCss(fragment); + sanitizer.sanitize(fragment, position ? getInheritableStyles(position.element) : undefined); +} diff --git a/packages/roosterjs-editor-dom/lib/clipboard/tsconfig.child.json b/packages/roosterjs-editor-dom/lib/clipboard/tsconfig.child.json deleted file mode 100644 index fa643fa641b2..000000000000 --- a/packages/roosterjs-editor-dom/lib/clipboard/tsconfig.child.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "strict": false - }, - "extends": "../../../tsconfig.json", - "include": ["./**/*.ts"], - "references": [ - { "path": "../../../roosterjs-editor-types/tsconfig.child.json" }, - { "path": "../utils/tsconfig.child.json" } - ] -} diff --git a/packages/roosterjs-editor-dom/lib/contentTraverser/BodyScoper.ts b/packages/roosterjs-editor-dom/lib/contentTraverser/BodyScoper.ts index 22efdc9cac7e..e46b53f46e41 100644 --- a/packages/roosterjs-editor-dom/lib/contentTraverser/BodyScoper.ts +++ b/packages/roosterjs-editor-dom/lib/contentTraverser/BodyScoper.ts @@ -11,7 +11,7 @@ import { getFirstInlineElement } from '../inlineElements/getFirstLastInlineEleme * provides a scope object for traversing the entire editor body starting from the beginning */ export default class BodyScoper implements TraversingScoper { - private startNode: Node; + private startNode: Node | null; /** * Construct a new instance of BodyScoper class @@ -19,13 +19,13 @@ export default class BodyScoper implements TraversingScoper { * @param startNode The node to start from. If not passed, it will start from the beginning of the body */ constructor(public rootNode: Node, startNode?: Node) { - this.startNode = contains(rootNode, startNode) ? startNode : null; + this.startNode = contains(rootNode, startNode) ? startNode! : null; } /** * Get the start block element */ - public getStartBlockElement(): BlockElement { + public getStartBlockElement(): BlockElement | null { return this.startNode ? getBlockElementAtNode(this.rootNode, this.startNode) : getFirstLastBlockElement(this.rootNode, true /*isFirst*/); @@ -34,7 +34,7 @@ export default class BodyScoper implements TraversingScoper { /** * Get the start inline element */ - public getStartInlineElement(): InlineElement { + public getStartInlineElement(): InlineElement | null { return this.startNode ? getInlineElementAtNode(this.rootNode, this.startNode) : getFirstInlineElement(this.rootNode); diff --git a/packages/roosterjs-editor-dom/lib/contentTraverser/ContentTraverser.ts b/packages/roosterjs-editor-dom/lib/contentTraverser/ContentTraverser.ts index cd2536b3bd96..edaa619b2f22 100644 --- a/packages/roosterjs-editor-dom/lib/contentTraverser/ContentTraverser.ts +++ b/packages/roosterjs-editor-dom/lib/contentTraverser/ContentTraverser.ts @@ -8,6 +8,7 @@ import SelectionScoper from './SelectionScoper'; import TraversingScoper from './TraversingScoper'; import { getInlineElementBeforeAfter } from '../inlineElements/getInlineElementBeforeAfter'; import { getLeafSibling } from '../utils/getLeafSibling'; +import type { CompatibleContentPosition } from 'roosterjs-editor-types/lib/compatibleTypes'; import { BlockElement, ContentPosition, @@ -23,8 +24,8 @@ import { * the current inline element position */ export default class ContentTraverser implements IContentTraverser { - private currentInline: InlineElement; - private currentBlock: BlockElement; + private currentInline: InlineElement | null = null; + private currentBlock: BlockElement | null = null; /** * Create a content traverser for the whole body of given root node @@ -72,7 +73,7 @@ export default class ContentTraverser implements IContentTraverser { public static createBlockTraverser( rootNode: Node, position: NodePosition | Range, - start: ContentPosition = ContentPosition.SelectionStart, + start: ContentPosition | CompatibleContentPosition = ContentPosition.SelectionStart, skipTags?: string[] ): IContentTraverser { return new ContentTraverser(new SelectionBlockScoper(rootNode, position, start)); @@ -81,7 +82,7 @@ export default class ContentTraverser implements IContentTraverser { /** * Get current block */ - public get currentBlockElement(): BlockElement { + public get currentBlockElement(): BlockElement | null { // Prepare currentBlock from the scoper if (!this.currentBlock) { this.currentBlock = this.scoper.getStartBlockElement(); @@ -93,18 +94,18 @@ export default class ContentTraverser implements IContentTraverser { /** * Get next block element */ - public getNextBlockElement(): BlockElement { + public getNextBlockElement(): BlockElement | null { return this.getPreviousNextBlockElement(true /*isNext*/); } /** * Get previous block element */ - public getPreviousBlockElement(): BlockElement { + public getPreviousBlockElement(): BlockElement | null { return this.getPreviousNextBlockElement(false /*isNext*/); } - private getPreviousNextBlockElement(isNext: boolean): BlockElement { + private getPreviousNextBlockElement(isNext: boolean): BlockElement | null { let current = this.currentBlockElement; if (!current) { @@ -139,7 +140,7 @@ export default class ContentTraverser implements IContentTraverser { /** * Current inline element getter */ - public get currentInlineElement(): InlineElement { + public get currentInlineElement(): InlineElement | null { // Retrieve a start inline from scoper if (!this.currentInline) { this.currentInline = this.scoper.getStartInlineElement(); @@ -151,20 +152,20 @@ export default class ContentTraverser implements IContentTraverser { /** * Get next inline element */ - public getNextInlineElement(): InlineElement { + public getNextInlineElement(): InlineElement | null { return this.getPreviousNextInlineElement(true /*isNext*/); } /** * Get previous inline element */ - public getPreviousInlineElement(): InlineElement { + public getPreviousInlineElement(): InlineElement | null { return this.getPreviousNextInlineElement(false /*isNext*/); } - private getPreviousNextInlineElement(isNext: boolean): InlineElement { + private getPreviousNextInlineElement(isNext: boolean): InlineElement | null { let current = this.currentInlineElement || this.currentInline; - let newInline: InlineElement; + let newInline: InlineElement | null; if (!current) { return null; @@ -207,7 +208,7 @@ function getNextPreviousInlineElement( rootNode: Node, current: InlineElement, isNext: boolean -): InlineElement { +): InlineElement | null { if (!current) { return null; } @@ -221,7 +222,7 @@ function getNextPreviousInlineElement( } // Get a leaf node after startNode and use that base to find next inline - let startNode = current.getContainerNode(); + let startNode: Node | null = current.getContainerNode(); startNode = getLeafSibling(rootNode, startNode, isNext); return getInlineElementAtNode(rootNode, startNode); } diff --git a/packages/roosterjs-editor-dom/lib/contentTraverser/PositionContentSearcher.ts b/packages/roosterjs-editor-dom/lib/contentTraverser/PositionContentSearcher.ts index 0b0ac7f247bc..a857bd336085 100644 --- a/packages/roosterjs-editor-dom/lib/contentTraverser/PositionContentSearcher.ts +++ b/packages/roosterjs-editor-dom/lib/contentTraverser/PositionContentSearcher.ts @@ -22,25 +22,25 @@ export default class PositionContentSearcher implements IPositionContentSearcher private text = ''; // The cached word before position - private word: string; + private word: string = ''; // The inline element before position - private inlineBefore: InlineElement; + private inlineBefore: InlineElement | null = null; // The inline element after position - private inlineAfter: InlineElement; + private inlineAfter: InlineElement | null = null; // The content traverser used to traverse backwards - private traverser: IContentTraverser; + private traverser: IContentTraverser | null = null; // Backward parsing has completed - private traversingComplete: boolean; + private traversingComplete: boolean = false; // All inline elements before position that have been read so far private inlineElements: InlineElement[] = []; // First non-text inline before position - private nearestNonTextInlineElement: InlineElement; + private nearestNonTextInlineElement: InlineElement | null = null; /** * Create a new CursorData instance @@ -59,14 +59,14 @@ export default class PositionContentSearcher implements IPositionContentSearcher this.traverse(() => this.word); } - return this.word; + return this.word || ''; } /** * Get the inline element before position * @returns The inlineElement before position */ - public getInlineElementBefore(): InlineElement { + public getInlineElementBefore(): InlineElement | null { if (!this.inlineBefore) { this.traverse(null); } @@ -78,7 +78,7 @@ export default class PositionContentSearcher implements IPositionContentSearcher * Get the inline element after position * @returns The inline element after position */ - public getInlineElementAfter(): InlineElement { + public getInlineElementAfter(): InlineElement | null { if (!this.inlineAfter) { this.inlineAfter = ContentTraverser.createBlockTraverser( this.rootNode, @@ -111,13 +111,13 @@ export default class PositionContentSearcher implements IPositionContentSearcher * @param exactMatch Whether it is an exact match * @returns The range for the matched text, null if unable to find a match */ - public getRangeFromText(text: string, exactMatch: boolean): Range { + public getRangeFromText(text: string, exactMatch: boolean): Range | null { if (!text) { return null; } - let startPosition: NodePosition; - let endPosition: NodePosition; + let startPosition: NodePosition | null = null; + let endPosition: NodePosition | null = null; let textIndex = text.length - 1; this.forEachTextInlineElement(textInline => { @@ -170,7 +170,7 @@ export default class PositionContentSearcher implements IPositionContentSearcher * Get first non textual inline element before position * @returns First non textual inline element before position or null if no such element exists */ - public getNearestNonTextInlineElement(): InlineElement { + public getNearestNonTextInlineElement(): InlineElement | null { if (!this.nearestNonTextInlineElement) { this.traverse(() => this.nearestNonTextInlineElement); } @@ -181,7 +181,7 @@ export default class PositionContentSearcher implements IPositionContentSearcher /** * Continue traversing backward till stop condition is met or begin of block is reached */ - private traverse(callback: (inlineElement: InlineElement) => any) { + private traverse(callback: null | ((inlineElement: InlineElement) => any)) { this.traverser = this.traverser || ContentTraverser.createBlockTraverser(this.rootNode, this.position); diff --git a/packages/roosterjs-editor-dom/lib/contentTraverser/SelectionBlockScoper.ts b/packages/roosterjs-editor-dom/lib/contentTraverser/SelectionBlockScoper.ts index bf6d0276032b..f9bd685228f4 100644 --- a/packages/roosterjs-editor-dom/lib/contentTraverser/SelectionBlockScoper.ts +++ b/packages/roosterjs-editor-dom/lib/contentTraverser/SelectionBlockScoper.ts @@ -11,6 +11,7 @@ import { getFirstInlineElement, getLastInlineElement, } from '../inlineElements/getFirstLastInlineElement'; +import type { CompatibleContentPosition } from 'roosterjs-editor-types/lib/compatibleTypes'; /** * @internal @@ -20,7 +21,7 @@ import { * This provides a scope for parsing from cursor position up to begin of the selection block */ export default class SelectionBlockScoper implements TraversingScoper { - private block: BlockElement; + private block: BlockElement | null; private position: NodePosition; /** @@ -32,9 +33,12 @@ export default class SelectionBlockScoper implements TraversingScoper { constructor( public rootNode: Node, position: NodePosition | Range, - private startFrom: ContentPosition + private startFrom: ContentPosition | CompatibleContentPosition ) { - position = safeInstanceOf(position, 'Range') ? Position.getStart(position) : position; + if (safeInstanceOf(position, 'Range')) { + position = Position.getStart(position); + } + this.position = position.normalize(); this.block = getBlockElementAtNode(this.rootNode, this.position.node); } @@ -42,7 +46,7 @@ export default class SelectionBlockScoper implements TraversingScoper { /** * Get the start block element */ - public getStartBlockElement(): BlockElement { + public getStartBlockElement(): BlockElement | null { return this.block; } @@ -52,7 +56,7 @@ export default class SelectionBlockScoper implements TraversingScoper { * The reason why we choose the one before rather after is, when cursor is at the end of a paragraph, * the one after likely will point to inline in next paragraph which may be null if the cursor is at bottom of editor */ - public getStartInlineElement(): InlineElement { + public getStartInlineElement(): InlineElement | null { if (this.block) { switch (this.startFrom) { case ContentPosition.Begin: @@ -88,7 +92,7 @@ export default class SelectionBlockScoper implements TraversingScoper { * This is a block scoper, which is not like selection scoper where it may cut an inline element in half * A block scoper does not cut an inline in half */ - public trimInlineElement(inlineElement: InlineElement): InlineElement { + public trimInlineElement(inlineElement: InlineElement): InlineElement | null { return this.block && inlineElement && this.block.contains(inlineElement.getContainerNode()) ? inlineElement : null; @@ -103,7 +107,7 @@ export default class SelectionBlockScoper implements TraversingScoper { function getFirstLastInlineElementFromBlockElement( block: BlockElement, isFirst: boolean -): InlineElement { +): InlineElement | null { if (block instanceof NodeBlockElement) { let blockNode = block.getStartNode(); return isFirst ? getFirstInlineElement(blockNode) : getLastInlineElement(blockNode); diff --git a/packages/roosterjs-editor-dom/lib/contentTraverser/SelectionScoper.ts b/packages/roosterjs-editor-dom/lib/contentTraverser/SelectionScoper.ts index cd6fc1f4c572..62474b716154 100644 --- a/packages/roosterjs-editor-dom/lib/contentTraverser/SelectionScoper.ts +++ b/packages/roosterjs-editor-dom/lib/contentTraverser/SelectionScoper.ts @@ -14,8 +14,8 @@ import { getInlineElementAfter } from '../inlineElements/getInlineElementBeforeA export default class SelectionScoper implements TraversingScoper { private start: NodePosition; private end: NodePosition; - private startBlock: BlockElement; - private startInline: InlineElement; + private startBlock: BlockElement | null = null; + private startInline: InlineElement | null = null; /** * Create a new instance of SelectionScoper class @@ -30,7 +30,7 @@ export default class SelectionScoper implements TraversingScoper { /** * Provide a start block as the first block after the cursor */ - public getStartBlockElement(): BlockElement { + public getStartBlockElement(): BlockElement | null { if (!this.startBlock) { this.startBlock = getBlockElementAtNode(this.rootNode, this.start.node); } @@ -41,7 +41,7 @@ export default class SelectionScoper implements TraversingScoper { /** * Provide a start inline as the first inline after the cursor */ - public getStartInlineElement(): InlineElement { + public getStartInlineElement(): InlineElement | null { if (!this.startInline) { this.startInline = this.trimInlineElement( getInlineElementAfter(this.rootNode, this.start) @@ -62,7 +62,7 @@ export default class SelectionScoper implements TraversingScoper { let inScope = false; let selStartBlock = this.getStartBlockElement(); if (this.start.equalTo(this.end)) { - inScope = selStartBlock && selStartBlock.equals(block); + inScope = !!selStartBlock && selStartBlock.equals(block); } else { let selEndBlock = getBlockElementAtNode(this.rootNode, this.end.node); @@ -71,8 +71,8 @@ export default class SelectionScoper implements TraversingScoper { // 2) The end of selection falls on the block // 3) the block falls in-between selection start and end inScope = - selStartBlock && - selEndBlock && + !!selStartBlock && + !!selEndBlock && (block.equals(selStartBlock) || block.equals(selEndBlock) || (block.isAfter(selStartBlock) && selEndBlock.isAfter(block))); @@ -86,7 +86,7 @@ export default class SelectionScoper implements TraversingScoper { * otherwise return a partial that represents the portion that falls in the selection * @param inline The InlineElement to check */ - public trimInlineElement(inline: InlineElement): InlineElement { + public trimInlineElement(inline: InlineElement | null): InlineElement | null { if (!inline || this.start.equalTo(this.end)) { return null; } @@ -115,7 +115,11 @@ export default class SelectionScoper implements TraversingScoper { return start.isAfter(end) || start.equalTo(end) ? null : startPartial || endPartial - ? new PartialInlineElement(inline, startPartial && start, endPartial && end) + ? new PartialInlineElement( + inline, + startPartial ? start : undefined, + endPartial ? end : undefined + ) : inline; } } diff --git a/packages/roosterjs-editor-dom/lib/contentTraverser/TraversingScoper.ts b/packages/roosterjs-editor-dom/lib/contentTraverser/TraversingScoper.ts index 30164e9fca55..f4e7e2ef5bad 100644 --- a/packages/roosterjs-editor-dom/lib/contentTraverser/TraversingScoper.ts +++ b/packages/roosterjs-editor-dom/lib/contentTraverser/TraversingScoper.ts @@ -19,12 +19,12 @@ export default interface TraversingScoper { /** * Get the start block element */ - getStartBlockElement: () => BlockElement; + getStartBlockElement: () => BlockElement | null; /** * Get the start inline element */ - getStartInlineElement: () => InlineElement; + getStartInlineElement: () => InlineElement | null; /** * Check if the given block element is in this scope @@ -34,5 +34,5 @@ export default interface TraversingScoper { /** * Trim the given inline element to match this scope */ - trimInlineElement: (inlineElement: InlineElement) => InlineElement; + trimInlineElement: (inlineElement: InlineElement) => InlineElement | null; } diff --git a/packages/roosterjs-editor-dom/lib/contentTraverser/tsconfig.child.json b/packages/roosterjs-editor-dom/lib/contentTraverser/tsconfig.child.json deleted file mode 100644 index 5454b0ed5861..000000000000 --- a/packages/roosterjs-editor-dom/lib/contentTraverser/tsconfig.child.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "strict": false - }, - "extends": "../../../tsconfig.json", - "include": ["./**/*.ts"], - "references": [ - { "path": "../../../roosterjs-editor-types/tsconfig.child.json" }, - { "path": "../blockElements/tsconfig.child.json" }, - { "path": "../inlineElements/tsconfig.child.json" }, - { "path": "../selection/tsconfig.child.json" }, - { "path": "../utils/tsconfig.child.json" } - ] -} diff --git a/packages/roosterjs-editor-dom/lib/delimiter/addDelimiters.ts b/packages/roosterjs-editor-dom/lib/delimiter/addDelimiters.ts new file mode 100644 index 000000000000..8fd5d885c0d9 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/delimiter/addDelimiters.ts @@ -0,0 +1,70 @@ +import createElement from '../utils/createElement'; +import getDelimiterFromElement from './getDelimiterFromElement'; +import { DelimiterClasses } from 'roosterjs-editor-types'; + +const ZERO_WIDTH_SPACE = '\u200B'; + +/** + * Adds delimiters to the element provided. If the delimiters already exists, will not be added + * @param node the node to add the delimiters + */ +export default function addDelimiters(node: Element): Element[] { + let [delimiterAfter, delimiterBefore] = getDelimiters(node); + + if (!delimiterAfter) { + delimiterAfter = addDelimiterAfter(node); + } + if (!delimiterBefore) { + delimiterBefore = addDelimiterBefore(node); + } + return [delimiterAfter, delimiterBefore]; +} + +/** + * Adds delimiter after the element provided. + * @param element element to use + */ +export function addDelimiterAfter(element: Element) { + return insertDelimiter(element, DelimiterClasses.DELIMITER_AFTER); +} + +/** + * Adds delimiter before the element provided. + * @param element element to use + */ +export function addDelimiterBefore(element: Element) { + return insertDelimiter(element, DelimiterClasses.DELIMITER_BEFORE); +} + +function getDelimiters(entityWrapper: Element): (Element | undefined)[] { + const result: (Element | undefined)[] = []; + const { nextElementSibling, previousElementSibling } = entityWrapper; + result.push( + isDelimiter(nextElementSibling, DelimiterClasses.DELIMITER_AFTER), + isDelimiter(previousElementSibling, DelimiterClasses.DELIMITER_BEFORE) + ); + + return result; +} + +function isDelimiter(el: Element | null, className: string): Element | undefined { + return el && getDelimiterFromElement(el) && el.classList.contains(className) ? el : undefined; +} + +function insertDelimiter(element: Element, delimiterClass: DelimiterClasses) { + const span = createElement( + { + tag: 'span', + className: delimiterClass, + children: [ZERO_WIDTH_SPACE], + }, + element.ownerDocument + ) as HTMLElement; + if (span) { + const insertPosition: InsertPosition = + delimiterClass == DelimiterClasses.DELIMITER_AFTER ? 'afterend' : 'beforebegin'; + element.insertAdjacentElement(insertPosition, span); + } + + return span; +} diff --git a/packages/roosterjs-editor-dom/lib/delimiter/getDelimiterFromElement.ts b/packages/roosterjs-editor-dom/lib/delimiter/getDelimiterFromElement.ts new file mode 100644 index 000000000000..4de94cf677be --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/delimiter/getDelimiterFromElement.ts @@ -0,0 +1,25 @@ +import safeInstanceOf from '../utils/safeInstanceOf'; +import { DelimiterClasses } from 'roosterjs-editor-types'; + +const ZERO_WIDTH_SPACE = '\u200B'; + +/** + * Retrieves Delimiter information from a provided element. + * @param element element to try to retrieve a delimiter + * @returns delimiter info if it is a Delimiter, else null + */ +export default function getDelimiterFromElement(element: Node | null | undefined): Element | null { + if (!element) { + return null; + } + if ( + safeInstanceOf(element, 'HTMLSpanElement') && + (element.classList.contains(DelimiterClasses.DELIMITER_AFTER) || + element.classList.contains(DelimiterClasses.DELIMITER_BEFORE)) && + element.textContent === ZERO_WIDTH_SPACE + ) { + return element; + } + + return null; +} diff --git a/packages/roosterjs-editor-dom/lib/edit/adjustInsertPosition.ts b/packages/roosterjs-editor-dom/lib/edit/adjustInsertPosition.ts index c757f55c62a3..641052037807 100644 --- a/packages/roosterjs-editor-dom/lib/edit/adjustInsertPosition.ts +++ b/packages/roosterjs-editor-dom/lib/edit/adjustInsertPosition.ts @@ -1,5 +1,6 @@ import changeElementTag from '../utils/changeElementTag'; import contains from '../utils/contains'; +import ContentTraverser from '../contentTraverser/ContentTraverser'; import createRange from '../selection/createRange'; import findClosestElementAncestor from '../utils/findClosestElementAncestor'; import getBlockElementAtNode from '../blockElements/getBlockElementAtNode'; @@ -8,16 +9,25 @@ import isNodeEmpty from '../utils/isNodeEmpty'; import isPositionAtBeginningOf from '../selection/isPositionAtBeginningOf'; import isVoidHtmlElement from '../utils/isVoidHtmlElement'; import LinkInlineElement from '../inlineElements/LinkInlineElement'; +import moveChildNodes from '../utils/moveChildNodes'; +import pasteTable from '../table/pasteTable'; import Position from '../selection/Position'; import PositionContentSearcher from '../contentTraverser/PositionContentSearcher'; import queryElements from '../utils/queryElements'; import splitTextNode from '../utils/splitTextNode'; -import toArray from '../utils/toArray'; +import toArray from '../jsUtils/toArray'; import unwrap from '../utils/unwrap'; -import VTable from '../table/VTable'; import wrap from '../utils/wrap'; -import { NodePosition, NodeType, PositionType, QueryScope } from 'roosterjs-editor-types'; import { splitBalancedNodeRange } from '../utils/splitParentNode'; +import { + BlockElement, + NodePosition, + NodeType, + PositionType, + QueryScope, +} from 'roosterjs-editor-types'; + +const NOT_EDITABLE_SELECTOR = '[contenteditable=false]'; const adjustSteps: (( root: HTMLElement, @@ -30,6 +40,8 @@ const adjustSteps: (( adjustInsertPositionForParagraph, adjustInsertPositionForVoidElement, adjustInsertPositionForMoveCursorOutOfALink, + adjustInsertPositionForNotEditableNode, + adjustInsertPositionForTable, ]; /** @@ -46,13 +58,13 @@ function adjustInsertPositionForHyperLink( if (blockElement) { // Find the first
tag within current block which covers current selection // If there are more than one nested, let's handle the first one only since that is not a common scenario. - let anchor = queryElements( + let anchor: HTMLElement | null = queryElements( root, 'a[href]', null /*forEachCallback*/, QueryScope.OnSelection, createRange(position) - ).filter((a: HTMLElement) => blockElement.contains(a))[0]; + ).filter((a: HTMLElement) => blockElement!.contains(a))[0]; // If this is about to insert node to an empty A tag, clear the A tag and reset position if (anchor && isNodeEmpty(anchor)) { @@ -69,7 +81,7 @@ function adjustInsertPositionForHyperLink( ((nodeToInsert as HTMLElement))?.querySelector('a[href]') ) { let normalizedPosition = position.normalize(); - let parentNode = normalizedPosition.node.parentNode; + let parentNode = normalizedPosition.node.parentNode!; let nextNode = normalizedPosition.node.nodeType == NodeType.Text ? splitTextNode( @@ -80,15 +92,17 @@ function adjustInsertPositionForHyperLink( : normalizedPosition.isAtEnd ? normalizedPosition.node.nextSibling : normalizedPosition.node; - let splitter: Node = root.ownerDocument.createTextNode(''); + let splitter: Node | null = root.ownerDocument.createTextNode(''); parentNode.insertBefore(splitter, nextNode); - while (contains(anchor, splitter)) { + while (splitter && contains(anchor, splitter)) { splitter = splitBalancedNodeRange(splitter); } - position = new Position(splitter, PositionType.Before); - safeRemove(splitter); + if (splitter) { + position = new Position(splitter, PositionType.Before); + safeRemove(splitter); + } } } @@ -104,9 +118,11 @@ function adjustInsertPositionForStructuredNode( position: NodePosition, range: Range ): NodePosition { - let rootNodeToInsert = nodeToInsert; + let rootNodeToInsert: Node | null = nodeToInsert; + let isFragment: boolean = false; if (rootNodeToInsert.nodeType == NodeType.DocumentFragment) { + isFragment = true; let rootNodes = toArray(rootNodeToInsert.childNodes).filter( (n: ChildNode) => getTagOfNode(n) != 'BR' ); @@ -114,57 +130,51 @@ function adjustInsertPositionForStructuredNode( } let tag = getTagOfNode(rootNodeToInsert); - let hasBrNextToRoot = tag && getTagOfNode(rootNodeToInsert.nextSibling) == 'BR'; + let hasBrNextToRoot = + tag && rootNodeToInsert && getTagOfNode(rootNodeToInsert.nextSibling) == 'BR'; let listItem = findClosestElementAncestor(position.node, root, 'LI'); let listNode = listItem && findClosestElementAncestor(listItem, root, 'OL,UL'); let tdNode = findClosestElementAncestor(position.node, root, 'TD,TH'); - let trNode = tdNode && findClosestElementAncestor(tdNode, root, 'TR'); if (tag == 'LI') { tag = listNode ? getTagOfNode(listNode) : 'UL'; - rootNodeToInsert = wrap(rootNodeToInsert, tag); + rootNodeToInsert = wrap(rootNodeToInsert!, tag); } - if ((tag == 'OL' || tag == 'UL') && getTagOfNode(rootNodeToInsert.firstChild) == 'LI') { - let shouldInsertListAsText = !rootNodeToInsert.firstChild.nextSibling && !hasBrNextToRoot; + if ( + (tag == 'OL' || tag == 'UL') && + rootNodeToInsert && + getTagOfNode(rootNodeToInsert.firstChild) == 'LI' + ) { + let shouldInsertListAsText = !rootNodeToInsert.firstChild!.nextSibling && !hasBrNextToRoot; if (hasBrNextToRoot && rootNodeToInsert.parentNode) { - safeRemove(rootNodeToInsert.nextSibling); + safeRemove(rootNodeToInsert.nextSibling!); } if (shouldInsertListAsText) { - unwrap(rootNodeToInsert.firstChild); + unwrap(rootNodeToInsert.firstChild!); unwrap(rootNodeToInsert); } else if (getTagOfNode(listNode) == tag) { unwrap(rootNodeToInsert); position = new Position( - listItem, - isPositionAtBeginningOf(position, listItem) + listItem!, + isPositionAtBeginningOf(position, listItem!) ? PositionType.Before : PositionType.After ); } - } else if (tag == 'TABLE' && trNode) { - // When inserting a table into a table, if these tables have the same column count, and - // current position is at beginning of a row, then merge these two tables - let newTable = new VTable(rootNodeToInsert); - let currentTable = new VTable(tdNode); - if ( - currentTable.col == 0 && - tdNode == currentTable.getCell(currentTable.row, 0).td && - newTable.cells[0] && - newTable.cells[0].length == currentTable.cells[0].length && - isPositionAtBeginningOf(position, tdNode) - ) { - if ( - getTagOfNode(rootNodeToInsert.firstChild) == 'TBODY' && - !rootNodeToInsert.firstChild.nextSibling - ) { - unwrap(rootNodeToInsert.firstChild); - } - unwrap(rootNodeToInsert); - position = new Position(trNode, PositionType.After); - } + } + + if (isFragment && tag == 'TABLE' && tdNode) { + pasteTable( + tdNode, + rootNodeToInsert, + position, + range + ); + position = new Position(rootNodeToInsert!, 0); + moveChildNodes(nodeToInsert); } return position; @@ -235,6 +245,91 @@ function adjustInsertPositionForMoveCursorOutOfALink( return position; } +/** + * Adjust the position cursor out of a not contenteditable element. + */ +function adjustInsertPositionForNotEditableNode( + root: HTMLElement, + nodeToInsert: Node, + position: NodePosition, + range: Range +): NodePosition { + if (!position.element?.isContentEditable) { + let nonEditableElement: HTMLElement | undefined; + let lastNonEditableElement: HTMLElement | null = findClosestElementAncestor( + position.node, + root, + NOT_EDITABLE_SELECTOR + ); + + while (lastNonEditableElement) { + nonEditableElement = lastNonEditableElement; + lastNonEditableElement = nonEditableElement?.parentElement + ? findClosestElementAncestor( + nonEditableElement.parentElement, + root, + NOT_EDITABLE_SELECTOR + ) + : null; + } + + if (nonEditableElement) { + position = new Position(nonEditableElement, PositionType.After); + return adjustInsertPositionForNotEditableNode(root, nodeToInsert, position, range); + } + } + + return position; +} + +/** + * Adjust the position of a table to be one line after another table. + */ +function adjustInsertPositionForTable( + root: HTMLElement, + nodeToInsert: Node, + position: NodePosition, + range: Range +): NodePosition { + if ( + (nodeToInsert.childNodes.length == 1 && + getTagOfNode(nodeToInsert.childNodes[0]) == 'TABLE') || + getTagOfNode(nodeToInsert) == 'TABLE' + ) { + const { element } = position; + + const posBefore = new Position(element, PositionType.Before); + const rangeToTraverse = createRange(posBefore, position); + const contentTraverser = ContentTraverser.createSelectionTraverser(root, rangeToTraverse); + + let blockElement = contentTraverser && contentTraverser.currentBlockElement; + + if (blockElement) { + let nextBlockElement: BlockElement | null = blockElement; + + while (!nextBlockElement) { + nextBlockElement = contentTraverser.getNextBlockElement(); + if (nextBlockElement) { + blockElement = nextBlockElement; + } + } + + const prevElement = blockElement?.getEndNode(); + + if (prevElement && findClosestElementAncestor(prevElement, root, 'TABLE')) { + let tempRange = createRange(position); + tempRange.collapse(false /* toStart */); + const br = root.ownerDocument.createElement('br'); + tempRange.insertNode(br); + + tempRange = createRange(br); + position = Position.getEnd(tempRange); + } + } + } + return position; +} + /** * * @param root the contentDiv of the ditor diff --git a/packages/roosterjs-editor-dom/lib/edit/deleteSelectedContent.ts b/packages/roosterjs-editor-dom/lib/edit/deleteSelectedContent.ts index 129485071123..40c9b665c972 100644 --- a/packages/roosterjs-editor-dom/lib/edit/deleteSelectedContent.ts +++ b/packages/roosterjs-editor-dom/lib/edit/deleteSelectedContent.ts @@ -1,4 +1,4 @@ -import arrayPush from '../utils/arrayPush'; +import arrayPush from '../jsUtils/arrayPush'; import collapseNodesInRegion from '../region/collapseNodesInRegion'; import getRegionsFromRange from '../region/getRegionsFromRange'; import getSelectionRangeInRegion from '../region/getSelectionRangeInRegion'; @@ -7,15 +7,18 @@ import Position from '../selection/Position'; import queryElements from '../utils/queryElements'; import safeInstanceOf from '../utils/safeInstanceOf'; import splitTextNode from '../utils/splitTextNode'; -import { PositionType, QueryScope, RegionType } from 'roosterjs-editor-types'; +import { NodePosition, PositionType, QueryScope, RegionType } from 'roosterjs-editor-types'; /** * Delete selected content, and return the new position to select * @param core The EditorCore object. * @param range The range to delete */ -export default function deleteSelectedContent(root: HTMLElement, range: Range) { - let nodeBefore: Node = null; +export default function deleteSelectedContent( + root: HTMLElement, + range: Range +): NodePosition | null { + let nodeBefore: Node | null = null; // 1. TABLE and TR node in selected should be deleted. It is possible we don't detect them from step 2 // since table cells will fall in to different regions @@ -38,7 +41,21 @@ export default function deleteSelectedContent(root: HTMLElement, range: Range) { return null; } - const { startContainer, endContainer, startOffset, endOffset } = regionRange; + const { + startContainer, + endContainer, + startOffset, + endOffset, + commonAncestorContainer, + } = regionRange; + + // Disallow merging of readonly elements + if ( + safeInstanceOf(commonAncestorContainer, 'HTMLElement') && + !commonAncestorContainer.isContentEditable + ) { + return null; + } // Make sure there are node before and after the merging point. // This is required by mergeBlocksInRegion API. @@ -62,13 +79,17 @@ export default function deleteSelectedContent(root: HTMLElement, range: Range) { }) .filter(x => !!x); - // 3. Delete all nodes that we found - nodesToDelete.forEach(node => node.parentNode?.removeChild(node)); + // 3. Delete all nodes that we found, whose parent is editable + nodesToDelete.forEach( + node => node.parentElement?.isContentEditable && node.parentElement.removeChild(node) + ); // 4. Merge lines for each region, so that after we don't see extra line breaks - nodesPairToMerge.forEach(nodes => - mergeBlocksInRegion(nodes.region, nodes.beforeStart, nodes.afterEnd) - ); + nodesPairToMerge.forEach(nodes => { + if (nodes) { + mergeBlocksInRegion(nodes.region, nodes.beforeStart, nodes.afterEnd); + } + }); return nodeBefore && new Position(nodeBefore, PositionType.End); } @@ -78,8 +99,8 @@ function ensureBeforeAndAfter(node: Node, offset: number, isStart: boolean) { const newNode = splitTextNode(node, offset, isStart); return isStart ? [newNode, node] : [node, newNode]; } else { - let nodeBefore: Node = node.childNodes[offset - 1]; - let nodeAfter: Node = node.childNodes[offset]; + let nodeBefore: Node | null = node.childNodes[offset - 1]; + let nodeAfter: Node | null = node.childNodes[offset]; // Condition 1: node child nodes // ("I" means cursor; "o" means a DOM node, "[ ]" means a parent node) @@ -99,8 +120,8 @@ function ensureBeforeAndAfter(node: Node, offset: number, isStart: boolean) { // [ o I ] or [ I o] // need to add empty text node to convert to condition 3 if ((nodeBefore || nodeAfter) && (!nodeBefore || !nodeAfter)) { - const emptyNode = node.ownerDocument.createTextNode(''); - (nodeBefore || nodeAfter).parentNode?.insertBefore(emptyNode, nodeAfter); + const emptyNode = node.ownerDocument!.createTextNode(''); + (nodeBefore || nodeAfter)?.parentNode?.insertBefore(emptyNode, nodeAfter); if (nodeBefore) { nodeAfter = emptyNode; } else { @@ -111,6 +132,6 @@ function ensureBeforeAndAfter(node: Node, offset: number, isStart: boolean) { // Condition 3: Both nodeBefore and nodeAfter are not null // [o I o] // return the nodes - return [nodeBefore, nodeAfter]; + return [nodeBefore!, nodeAfter!]; } } diff --git a/packages/roosterjs-editor-dom/lib/edit/tsconfig.child.json b/packages/roosterjs-editor-dom/lib/edit/tsconfig.child.json deleted file mode 100644 index c053425dd6f7..000000000000 --- a/packages/roosterjs-editor-dom/lib/edit/tsconfig.child.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "strict": false - }, - "extends": "../../../tsconfig.json", - "include": ["./**/*.ts"], - "references": [ - { "path": "../../../roosterjs-editor-types/tsconfig.child.json" }, - { "path": "../selection/tsconfig.child.json" }, - { "path": "../utils/tsconfig.child.json" }, - { "path": "../blockElements/tsconfig.child.json" }, - { "path": "../inlineElements/tsconfig.child.json" }, - { "path": "../contentTraverser/tsconfig.child.json" }, - { "path": "../table/tsconfig.child.json" }, - { "path": "../region/tsconfig.child.json" } - ] -} diff --git a/packages/roosterjs-editor-dom/lib/entity/entityPlaceholderUtils.ts b/packages/roosterjs-editor-dom/lib/entity/entityPlaceholderUtils.ts new file mode 100644 index 000000000000..b41b14cea7ce --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/entity/entityPlaceholderUtils.ts @@ -0,0 +1,150 @@ +import getEntityFromElement from './getEntityFromElement'; +import getEntitySelector from './getEntitySelector'; +import safeInstanceOf from '../utils/safeInstanceOf'; +import { Entity, EntityClasses, KnownEntityItem } from 'roosterjs-editor-types'; + +const EntityPlaceHolderTagName = 'ENTITY-PLACEHOLDER'; + +/** + * @deprecated + * Create a placeholder comment node for entity + * @param entity The entity to create placeholder from + * @returns A placeholder comment node as + */ +export function createEntityPlaceholder(entity: Entity): HTMLElement { + const placeholder = entity.wrapper.ownerDocument.createElement(EntityPlaceHolderTagName); + placeholder.id = entity.id; + + return placeholder; +} + +/** + * Move content from a container into a new Document fragment, and try keep entities to be reusable by creating placeholder + * for them in the document fragment. + * If an entity is directly under root container, the whole entity can be reused and no need to move it at all. + * If an entity is not directly under root container, it is still reusable, but it may need some movement. + * In any case, entities will be replaced with a placeholder in the target document fragment. + * We will use an entity map (the "entities" parameter) to save the map from entity id to its wrapper element. + * @param root The root element + * @param entities A map from entity id to entity wrapper element + * @returns A new document fragment contains all the content and entity placeholders + */ +export function moveContentWithEntityPlaceholders( + root: HTMLDivElement, + entities: Record +) { + const entitySelector = getEntitySelector(); + const fragment = root.ownerDocument.createDocumentFragment(); + let next: Node | null = null; + + for (let child: Node | null = root.firstChild; child; child = next) { + let entity: Entity | null; + let nodeToAppend = child; + + next = child.nextSibling; + + if (safeInstanceOf(child, 'HTMLElement')) { + if ((entity = getEntityFromElement(child))) { + nodeToAppend = getPlaceholder(entity, entities); + } else { + child.querySelectorAll(entitySelector).forEach(wrapper => { + if ((entity = getEntityFromElement(wrapper))) { + const placeholder = getPlaceholder(entity, entities); + + wrapper.parentNode?.replaceChild(placeholder, wrapper); + } + }); + } + } + + fragment.appendChild(nodeToAppend); + } + + fragment.normalize(); + + return fragment; +} + +/** + * Restore HTML content from a document fragment that may contain entity placeholders. + * @param source Source document fragment that contains HTML content and entity placeholders + * @param target Target container, usually to be editor root container + * @param entities A map from entity id to entity wrapper, used for reusing existing DOM structure for entity + * @param insertClonedNode When pass true, merge with a cloned copy of the nodes from source fragment rather than the nodes themselves @default false + */ +export function restoreContentWithEntityPlaceholder( + source: ParentNode, + target: HTMLElement, + entities: Record | null, + insertClonedNode?: boolean +) { + let anchor = target.firstChild; + + const entitySelector = getEntitySelector(); + + for (let current = source.firstChild; current; ) { + const next = current.nextSibling; + const wrapper = tryGetWrapperFromEntityPlaceholder(entities, current); + + if (wrapper) { + anchor = removeUntil(anchor, wrapper); + + if (anchor) { + anchor = anchor.nextSibling; + } else { + target.appendChild(wrapper); + } + } else { + const nodeToInsert = insertClonedNode ? current.cloneNode(true /*deep*/) : current; + target.insertBefore(nodeToInsert, anchor); + + if (safeInstanceOf(nodeToInsert, 'HTMLElement')) { + nodeToInsert.querySelectorAll(entitySelector).forEach(placeholder => { + const wrapper = tryGetWrapperFromEntityPlaceholder(entities, placeholder); + + if (wrapper) { + placeholder.parentNode?.replaceChild(wrapper, placeholder); + } + }); + } + } + + current = next; + } + + removeUntil(anchor); +} + +function removeUntil(anchor: ChildNode | null, nodeToStop?: HTMLElement) { + while (anchor && (!nodeToStop || anchor != nodeToStop)) { + const nodeToRemove = anchor; + anchor = anchor.nextSibling; + nodeToRemove.parentNode?.removeChild(nodeToRemove); + } + return anchor; +} + +function tryGetWrapperFromEntityPlaceholder( + entities: Record | null, + node: Node +): HTMLElement | null { + const id = + safeInstanceOf(node, 'HTMLElement') && + node.classList.contains(EntityClasses.ENTITY_INFO_NAME) && + getEntityFromElement(node as HTMLElement)?.id; + const item = id ? entities?.[id] : null; + + return !item + ? null + : safeInstanceOf(item, 'HTMLElement') + ? item + : item?.canPersist + ? item.element + : null; +} + +function getPlaceholder(entity: Entity, entities: Record) { + entities[entity.id] = entity.wrapper; + + return entity.wrapper.cloneNode(true /*deep*/); +} diff --git a/packages/roosterjs-editor-dom/lib/entity/tsconfig.child.json b/packages/roosterjs-editor-dom/lib/entity/tsconfig.child.json deleted file mode 100644 index a9db8b7f7e90..000000000000 --- a/packages/roosterjs-editor-dom/lib/entity/tsconfig.child.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "strict": true - }, - "extends": "../../../tsconfig.json", - "include": ["./**/*.ts"], - "references": [{ "path": "../../../roosterjs-editor-types/tsconfig.child.json" }] -} diff --git a/packages/roosterjs-editor-dom/lib/event/cacheGetEventData.ts b/packages/roosterjs-editor-dom/lib/event/cacheGetEventData.ts index fc673880b3b0..2db6df1fc4d6 100644 --- a/packages/roosterjs-editor-dom/lib/event/cacheGetEventData.ts +++ b/packages/roosterjs-editor-dom/lib/event/cacheGetEventData.ts @@ -7,7 +7,11 @@ import { PluginEvent } from 'roosterjs-editor-types'; * @param key Cache key string, need to be unique * @param getter Getter function to get the object when it is not in cache yet */ -export default function cacheGetEventData(event: PluginEvent, key: string, getter: () => T): T { +export default function cacheGetEventData( + event: PluginEvent | null, + key: string, + getter: () => T +): T { let result = event && event.eventDataCache && event.eventDataCache.hasOwnProperty(key) ? event.eventDataCache[key] diff --git a/packages/roosterjs-editor-dom/lib/event/isCharacterValue.ts b/packages/roosterjs-editor-dom/lib/event/isCharacterValue.ts index 6a77d227e6db..eddc8180a73d 100644 --- a/packages/roosterjs-editor-dom/lib/event/isCharacterValue.ts +++ b/packages/roosterjs-editor-dom/lib/event/isCharacterValue.ts @@ -8,5 +8,5 @@ import isModifierKey from './isModifierKey'; * @param event The keyboard event object */ export default function isCharacterValue(event: KeyboardEvent): boolean { - return !isModifierKey(event) && event.key && event.key.length == 1; + return !isModifierKey(event) && !!event.key && event.key.length == 1; } diff --git a/packages/roosterjs-editor-dom/lib/event/tsconfig.child.json b/packages/roosterjs-editor-dom/lib/event/tsconfig.child.json deleted file mode 100644 index fa643fa641b2..000000000000 --- a/packages/roosterjs-editor-dom/lib/event/tsconfig.child.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "strict": false - }, - "extends": "../../../tsconfig.json", - "include": ["./**/*.ts"], - "references": [ - { "path": "../../../roosterjs-editor-types/tsconfig.child.json" }, - { "path": "../utils/tsconfig.child.json" } - ] -} diff --git a/packages/roosterjs-editor-dom/lib/htmlSanitizer/HtmlSanitizer.ts b/packages/roosterjs-editor-dom/lib/htmlSanitizer/HtmlSanitizer.ts index 93904b8c1700..d652b2577fbc 100644 --- a/packages/roosterjs-editor-dom/lib/htmlSanitizer/HtmlSanitizer.ts +++ b/packages/roosterjs-editor-dom/lib/htmlSanitizer/HtmlSanitizer.ts @@ -1,12 +1,14 @@ import changeElementTag from '../utils/changeElementTag'; import getInheritableStyles from './getInheritableStyles'; +import getObjectKeys from '../jsUtils/getObjectKeys'; import getPredefinedCssForElement from './getPredefinedCssForElement'; import getStyles from '../style/getStyles'; import getTagOfNode from '../utils/getTagOfNode'; import safeInstanceOf from '../utils/safeInstanceOf'; import setStyles from '../style/setStyles'; -import toArray from '../utils/toArray'; +import toArray from '../jsUtils/toArray'; import { cloneObject } from './cloneObject'; +import { isCssVariable, processCssVariable } from './processCssVariable'; import { getAllowedAttributes, getAllowedCssClassesRegex, @@ -62,14 +64,14 @@ export default class HtmlSanitizer { private elementCallbacks: ElementCallbackMap; private styleCallbacks: CssStyleCallbackMap; private attributeCallbacks: AttributeCallbackMap; - private tagReplacements: Record; + private tagReplacements: Record; private allowedAttributes: string[]; - private allowedCssClassesRegex: RegExp; + private allowedCssClassesRegex: RegExp | null; private defaultStyleValues: StringMap; - private additionalPredefinedCssForElement: PredefinedCssMap; + private additionalPredefinedCssForElement: PredefinedCssMap | null; private additionalGlobalStyleNodes: HTMLStyleElement[]; private preserveHtmlComments: boolean; - private unknownTagReplacement: string; + private unknownTagReplacement: string | null; /** * Construct a new instance of HtmlSanitizer @@ -86,10 +88,10 @@ export default class HtmlSanitizer { options.additionalAllowedCssClasses ); this.defaultStyleValues = getDefaultStyleValues(options.additionalDefaultStyleValues); - this.additionalPredefinedCssForElement = options.additionalPredefinedCssForElement; + this.additionalPredefinedCssForElement = options.additionalPredefinedCssForElement || null; this.additionalGlobalStyleNodes = options.additionalGlobalStyleNodes || []; - this.preserveHtmlComments = options.preserveHtmlComments; - this.unknownTagReplacement = options.unknownTagReplacement; + this.preserveHtmlComments = options.preserveHtmlComments || false; + this.unknownTagReplacement = options.unknownTagReplacement || null; } /** @@ -184,7 +186,7 @@ export default class HtmlSanitizer { if (isElement) { const tag = getTagOfNode(node); const callback = this.elementCallbacks[tag]; - let replacement = this.tagReplacements[tag.toLowerCase()]; + let replacement: string | null | undefined = this.tagReplacements[tag.toLowerCase()]; if (replacement === undefined) { replacement = this.unknownTagReplacement; @@ -197,7 +199,7 @@ export default class HtmlSanitizer { } else if (tag == replacement || replacement == '*') { shouldKeep = true; } else if (replacement && /^[a-zA-Z][\w\-]*$/.test(replacement)) { - node = changeElementTag(node as HTMLElement, replacement); + node = changeElementTag(node as HTMLElement, replacement)!; shouldKeep = true; } } else if (isText) { @@ -206,7 +208,7 @@ export default class HtmlSanitizer { whiteSpace == 'pre' || whiteSpace == 'pre-line' || whiteSpace == 'pre-wrap' || - !/^[\r\n]*$/g.test(node.nodeValue); + !/^[\r\n]*$/g.test(node.nodeValue || ''); } else if (isFragment) { shouldKeep = true; } else if (isComment) { @@ -216,12 +218,14 @@ export default class HtmlSanitizer { } if (!shouldKeep) { - node.parentNode.removeChild(node); + node.parentNode?.removeChild(node); } else if ( isText && (currentStyle['white-space'] == 'pre' || currentStyle['white-space'] == 'pre-wrap') ) { - node.nodeValue = node.nodeValue.replace(/^ /gm, '\u00A0').replace(/ {2}/g, ' \u00A0'); + node.nodeValue = (node.nodeValue || '') + .replace(/^ /gm, '\u00A0') + .replace(/ {2}/g, ' \u00A0'); } else if (isElement || isFragment) { let thisStyle = cloneObject(currentStyle); let element = node; @@ -231,8 +235,8 @@ export default class HtmlSanitizer { this.processCss(element, thisStyle, context); } - let child: Node = element.firstChild; - let next: Node; + let child: Node | null = element.firstChild; + let next: Node | null; for (; child; child = next) { next = child.nextSibling; this.processNode(child, thisStyle, context); @@ -246,7 +250,7 @@ export default class HtmlSanitizer { this.additionalPredefinedCssForElement ); if (predefinedStyles) { - Object.keys(predefinedStyles).forEach(name => { + getObjectKeys(predefinedStyles).forEach(name => { thisStyle[name] = predefinedStyles[name]; }); } @@ -254,12 +258,23 @@ export default class HtmlSanitizer { private processCss(element: HTMLElement, thisStyle: StringMap, context: Object) { const styles = getStyles(element); - Object.keys(styles).forEach(name => { - const value = styles[name]; + getObjectKeys(styles).forEach(name => { + let value = styles[name]; let callback = this.styleCallbacks[name]; let isInheritable = thisStyle[name] != undefined; - let keep = - (!callback || callback(value, element, thisStyle, context)) && + let keep = true; + + if (keep && !!callback) { + keep = callback(value, element, thisStyle, context); + } + + if (keep && isCssVariable(value)) { + value = processCssVariable(value); + keep = !!value; + } + + keep = + keep && value != 'inherit' && value.indexOf('expression') < 0 && name.substr(0, 1) != '-' && @@ -270,7 +285,9 @@ export default class HtmlSanitizer { thisStyle[name] = value; } - if (!keep) { + if (keep) { + styles[name] = value; + } else { delete styles[name]; } }); @@ -307,19 +324,19 @@ export default class HtmlSanitizer { } } - private processCssClass(originalValue: string, calculatedValue: string): string { + private processCssClass(originalValue: string, calculatedValue: string | null): string | null { const originalClasses = originalValue ? originalValue.split(' ') : []; const calculatedClasses = calculatedValue ? calculatedValue.split(' ') : []; originalClasses.forEach(className => { if ( - this.allowedCssClassesRegex.test(className) && + this.allowedCssClassesRegex?.test(className) && calculatedClasses.indexOf(className) < 0 ) { calculatedClasses.push(className); } }); - return calculatedClasses.length > 0 ? calculatedClasses.join(' ') : null; + return calculatedClasses?.length > 0 ? calculatedClasses.join(' ') : null; } } diff --git a/packages/roosterjs-editor-dom/lib/htmlSanitizer/cloneObject.ts b/packages/roosterjs-editor-dom/lib/htmlSanitizer/cloneObject.ts index 99663f0d7ae9..4c76310c97b5 100644 --- a/packages/roosterjs-editor-dom/lib/htmlSanitizer/cloneObject.ts +++ b/packages/roosterjs-editor-dom/lib/htmlSanitizer/cloneObject.ts @@ -1,30 +1,33 @@ +import getObjectKeys from '../jsUtils/getObjectKeys'; + function nativeClone( - source: Record, + source: Record | null | undefined, existingObj?: Record ): Record { return Object.assign(existingObj || {}, source); } function customClone( - source: Record, + source: Record | null | undefined, existingObj?: Record ): Record { let result: Record = existingObj || {}; if (source) { - for (let key of Object.keys(source)) { + for (let key of getObjectKeys(source)) { result[key] = source[key]; } } return result; } +// @ts-ignore Ignore this error for IE compatibility const cloneObjectImpl = Object.assign ? nativeClone : customClone; /** * @internal */ export function cloneObject( - source: Record, + source: Record | null | undefined, existingObj?: Record ): Record { return cloneObjectImpl(source, existingObj); diff --git a/packages/roosterjs-editor-dom/lib/htmlSanitizer/getAllowedValues.ts b/packages/roosterjs-editor-dom/lib/htmlSanitizer/getAllowedValues.ts index 8beb8a87911a..8812e852de8d 100644 --- a/packages/roosterjs-editor-dom/lib/htmlSanitizer/getAllowedValues.ts +++ b/packages/roosterjs-editor-dom/lib/htmlSanitizer/getAllowedValues.ts @@ -1,7 +1,8 @@ +import getObjectKeys from '../jsUtils/getObjectKeys'; import { cloneObject } from './cloneObject'; import { CssStyleCallbackMap, StringMap } from 'roosterjs-editor-types'; -const HTML_TAG_REPLACEMENT: Record = { +const HTML_TAG_REPLACEMENT: Record = { // Allowed tags a: '*', abbr: '*', @@ -92,7 +93,6 @@ const HTML_TAG_REPLACEMENT: Record = { table: '*', tbody: '*', td: '*', - template: '*', textarea: '*', tfoot: '*', th: '*', @@ -127,6 +127,7 @@ const HTML_TAG_REPLACEMENT: Record = { slot: null, source: null, style: null, + template: null, title: null, track: null, video: null, @@ -164,7 +165,6 @@ const DEFAULT_STYLE_VALUES: { [name: string]: string } = { 'outline-style': 'none', 'outline-width': '0px', overflow: 'visible', - 'text-decoration': 'none', '-webkit-text-stroke-width': '0px', 'word-wrap': 'break-word', 'margin-left': '0px', @@ -190,11 +190,11 @@ const ALLOWED_CSS_CLASSES: string[] = []; * @internal */ export function getTagReplacement( - additionalReplacements: Record -): Record { + additionalReplacements: Record | undefined +): Record { const result = { ...HTML_TAG_REPLACEMENT }; const replacements = additionalReplacements || {}; - Object.keys(replacements).forEach(key => { + getObjectKeys(replacements).forEach(key => { if (key) { result[key.toLowerCase()] = replacements[key]; } @@ -206,7 +206,7 @@ export function getTagReplacement( /** * @internal */ -export function getAllowedAttributes(additionalAttributes: string[]): string[] { +export function getAllowedAttributes(additionalAttributes: string[] | undefined): string[] { return unique(ALLOWED_HTML_ATTRIBUTES.concat(additionalAttributes || [])).map(attr => attr.toLocaleLowerCase() ); @@ -215,7 +215,9 @@ export function getAllowedAttributes(additionalAttributes: string[]): string[] { /** * @internal */ -export function getAllowedCssClassesRegex(additionalCssClasses: string[]): RegExp { +export function getAllowedCssClassesRegex( + additionalCssClasses: string[] | undefined +): RegExp | null { const patterns = ALLOWED_CSS_CLASSES.concat(additionalCssClasses || []); return patterns.length > 0 ? new RegExp(patterns.join('|')) : null; } @@ -223,7 +225,7 @@ export function getAllowedCssClassesRegex(additionalCssClasses: string[]): RegEx /** * @internal */ -export function getDefaultStyleValues(additionalDefaultStyles: StringMap): StringMap { +export function getDefaultStyleValues(additionalDefaultStyles: StringMap | undefined): StringMap { let result = cloneObject(DEFAULT_STYLE_VALUES); if (additionalDefaultStyles) { Object.keys(additionalDefaultStyles).forEach(name => { @@ -242,7 +244,9 @@ export function getDefaultStyleValues(additionalDefaultStyles: StringMap): Strin /** * @internal */ -export function getStyleCallbacks(callbacks: CssStyleCallbackMap): CssStyleCallbackMap { +export function getStyleCallbacks( + callbacks: CssStyleCallbackMap | null | undefined +): CssStyleCallbackMap { let result = cloneObject(callbacks); result.position = result.position || removeValue; result.width = result.width || removeWidthForLiAndDiv; diff --git a/packages/roosterjs-editor-dom/lib/htmlSanitizer/getInheritableStyles.ts b/packages/roosterjs-editor-dom/lib/htmlSanitizer/getInheritableStyles.ts index 89a1df05ada5..aa7e05ab96cc 100644 --- a/packages/roosterjs-editor-dom/lib/htmlSanitizer/getInheritableStyles.ts +++ b/packages/roosterjs-editor-dom/lib/htmlSanitizer/getInheritableStyles.ts @@ -14,9 +14,9 @@ const INHERITABLE_PROPERTIES = ( * Get inheritable CSS style values from the given element * @param element The element to get style from */ -export default function getInheritableStyles(element: HTMLElement): StringMap { +export default function getInheritableStyles(element: HTMLElement | null): StringMap { let win = element && element.ownerDocument && element.ownerDocument.defaultView; - let styles = win && win.getComputedStyle(element); + let styles = win && element && win.getComputedStyle(element); let result: StringMap = {}; INHERITABLE_PROPERTIES.forEach( name => (result[name] = (styles && styles.getPropertyValue(name)) || '') diff --git a/packages/roosterjs-editor-dom/lib/htmlSanitizer/getPredefinedCssForElement.ts b/packages/roosterjs-editor-dom/lib/htmlSanitizer/getPredefinedCssForElement.ts index d6b75c474436..959c0b348702 100644 --- a/packages/roosterjs-editor-dom/lib/htmlSanitizer/getPredefinedCssForElement.ts +++ b/packages/roosterjs-editor-dom/lib/htmlSanitizer/getPredefinedCssForElement.ts @@ -43,7 +43,7 @@ const PREDEFINED_CSS_FOR_ELEMENT: PredefinedCssMap = { */ export default function getPredefinedCssForElement( element: HTMLElement, - additionalPredefinedCssForElement?: PredefinedCssMap + additionalPredefinedCssForElement?: PredefinedCssMap | null ): StringMap { const tag = getTagOfNode(element); return PREDEFINED_CSS_FOR_ELEMENT[tag] || (additionalPredefinedCssForElement || {})[tag]; diff --git a/packages/roosterjs-editor-dom/lib/htmlSanitizer/processCssVariable.ts b/packages/roosterjs-editor-dom/lib/htmlSanitizer/processCssVariable.ts new file mode 100644 index 000000000000..fc8b5bc33303 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/htmlSanitizer/processCssVariable.ts @@ -0,0 +1,18 @@ +const VARIABLE_REGEX = /^\s*var\(\s*[a-zA-Z0-9-_]+\s*(,\s*(.*))?\)\s*$/; +const VARIABLE_PREFIX = 'var('; + +/** + * @internal + * Handle CSS variable format. e.g.: var(--name, fallbackValue) + */ +export function processCssVariable(value: string): string { + const match = VARIABLE_REGEX.exec(value); + return match?.[2] || ''; // Without fallback value, we don't know what does the original value mean, so ignore it +} + +/** + * @internal + */ +export function isCssVariable(value: string): boolean { + return value.indexOf(VARIABLE_PREFIX) == 0; +} diff --git a/packages/roosterjs-editor-dom/lib/htmlSanitizer/tsconfig.child.json b/packages/roosterjs-editor-dom/lib/htmlSanitizer/tsconfig.child.json deleted file mode 100644 index 22bd4b00b6d3..000000000000 --- a/packages/roosterjs-editor-dom/lib/htmlSanitizer/tsconfig.child.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "strict": false - }, - "extends": "../../../tsconfig.json", - "include": ["./**/*.ts"], - "references": [ - { "path": "../../../roosterjs-editor-types/tsconfig.child.json" }, - { "path": "../utils/tsconfig.child.json" }, - { "path": "../style/tsconfig.child.json" } - ] -} diff --git a/packages/roosterjs-editor-dom/lib/index.ts b/packages/roosterjs-editor-dom/lib/index.ts index a5ba19152626..8f9a426d557b 100644 --- a/packages/roosterjs-editor-dom/lib/index.ts +++ b/packages/roosterjs-editor-dom/lib/index.ts @@ -4,6 +4,13 @@ export { default as getFirstLastBlockElement } from './blockElements/getFirstLas export { default as ContentTraverser } from './contentTraverser/ContentTraverser'; export { default as PositionContentSearcher } from './contentTraverser/PositionContentSearcher'; +export { + default as addDelimiters, + addDelimiterAfter, + addDelimiterBefore, +} from './delimiter/addDelimiters'; +export { default as getDelimiterFromElement } from './delimiter/getDelimiterFromElement'; + export { default as getInlineElementAtNode } from './inlineElements/getInlineElementAtNode'; export { default as ImageInlineElement } from './inlineElements/ImageInlineElement'; export { default as LinkInlineElement } from './inlineElements/LinkInlineElement'; @@ -14,8 +21,13 @@ export { default as applyTextStyle } from './inlineElements/applyTextStyle'; export { default as extractClipboardEvent } from './clipboard/extractClipboardEvent'; export { default as extractClipboardItems } from './clipboard/extractClipboardItems'; export { default as extractClipboardItemsForIE } from './clipboard/extractClipboardItemsForIE'; +export { default as createFragmentFromClipboardData } from './clipboard/createFragmentFromClipboardData'; +export { default as handleImagePaste } from './clipboard/handleImagePaste'; +export { default as handleTextPaste } from './clipboard/handleTextPaste'; +export { default as retrieveMetadataFromClipboard } from './clipboard/retrieveMetadataFromClipboard'; +export { default as sanitizePasteContent } from './clipboard/sanitizePasteContent'; +export { default as getPasteType } from './clipboard/getPasteType'; -export { default as arrayPush } from './utils/arrayPush'; export { Browser, getBrowserInfo } from './utils/Browser'; export { default as applyFormat } from './utils/applyFormat'; export { default as changeElementTag } from './utils/changeElementTag'; @@ -42,7 +54,6 @@ export { getNextLeafSibling, getPreviousLeafSibling } from './utils/getLeafSibli export { getFirstLeafNode, getLastLeafNode } from './utils/getLeafNode'; export { default as splitTextNode } from './utils/splitTextNode'; export { default as normalizeRect } from './utils/normalizeRect'; -export { default as toArray } from './utils/toArray'; export { default as safeInstanceOf } from './utils/safeInstanceOf'; export { default as readFile } from './utils/readFile'; export { default as getInnerHTML } from './utils/getInnerHTML'; @@ -50,13 +61,20 @@ export { default as setColor } from './utils/setColor'; export { default as matchesSelector } from './utils/matchesSelector'; export { default as createElement, KnownCreateElementData } from './utils/createElement'; export { default as moveChildNodes } from './utils/moveChildNodes'; +export { default as getIntersectedRect } from './utils/getIntersectedRect'; +export { default as isNodeAfter } from './utils/isNodeAfter'; +export { default as parseColor } from './utils/parseColor'; export { default as VTable } from './table/VTable'; +export { default as isWholeTableSelected } from './table/isWholeTableSelected'; + export { default as VList } from './list/VList'; export { default as VListItem } from './list/VListItem'; export { default as createVListFromRegion } from './list/createVListFromRegion'; export { default as VListChain } from './list/VListChain'; export { default as setListItemStyle } from './list/setListItemStyle'; +export { getTableFormatInfo } from './table/tableFormatInfo'; +export { saveTableCellMetadata } from './table/tableCellInfo'; export { default as getRegionsFromRange } from './region/getRegionsFromRange'; export { default as getSelectedBlockElementsInRegion } from './region/getSelectedBlockElementsInRegion'; @@ -71,12 +89,19 @@ export { default as getPositionRect } from './selection/getPositionRect'; export { default as isPositionAtBeginningOf } from './selection/isPositionAtBeginningOf'; export { default as getSelectionPath } from './selection/getSelectionPath'; export { default as getHtmlWithSelectionPath } from './selection/getHtmlWithSelectionPath'; -export { default as setHtmlWithSelectionPath } from './selection/setHtmlWithSelectionPath'; +export { + default as setHtmlWithSelectionPath, + setHtmlWithMetadata, + extractContentMetadata, +} from './selection/setHtmlWithSelectionPath'; export { default as addRangeToSelection } from './selection/addRangeToSelection'; -export { default as addSnapshot } from './snapshots/addSnapshot'; +export { default as addSnapshot, addSnapshotV2 } from './snapshots/addSnapshot'; export { default as canMoveCurrentSnapshot } from './snapshots/canMoveCurrentSnapshot'; -export { default as clearProceedingSnapshots } from './snapshots/clearProceedingSnapshots'; +export { + default as clearProceedingSnapshots, + clearProceedingSnapshotsV2, +} from './snapshots/clearProceedingSnapshots'; export { default as moveCurrentSnapshot, moveCurrentSnapsnot, @@ -92,6 +117,11 @@ export { default as chainSanitizerCallback } from './htmlSanitizer/chainSanitize export { default as commitEntity } from './entity/commitEntity'; export { default as getEntityFromElement } from './entity/getEntityFromElement'; export { default as getEntitySelector } from './entity/getEntitySelector'; +export { + createEntityPlaceholder, + moveContentWithEntityPlaceholders, + restoreContentWithEntityPlaceholder, +} from './entity/entityPlaceholderUtils'; export { default as cacheGetEventData } from './event/cacheGetEventData'; export { default as clearEventDataCache } from './event/clearEventDataCache'; @@ -101,7 +131,26 @@ export { default as isCtrlOrMetaPressed } from './event/isCtrlOrMetaPressed'; export { default as getStyles } from './style/getStyles'; export { default as setStyles } from './style/setStyles'; +export { default as removeImportantStyleRule } from './style/removeImportantStyleRule'; +export { default as setGlobalCssStyles } from './style/setGlobalCssStyles'; +export { default as removeGlobalCssStyle } from './style/removeGlobalCssStyle'; export { default as adjustInsertPosition } from './edit/adjustInsertPosition'; export { default as deleteSelectedContent } from './edit/deleteSelectedContent'; export { default as getTextContent } from './edit/getTextContent'; + +export { default as validate } from './metadata/validate'; +export { + createNumberDefinition, + createBooleanDefinition, + createStringDefinition, + createArrayDefinition, + createObjectDefinition, +} from './metadata/definitionCreators'; +export { getMetadata, setMetadata, removeMetadata } from './metadata/metadata'; + +export { default as arrayPush } from './jsUtils/arrayPush'; +export { default as getObjectKeys } from './jsUtils/getObjectKeys'; +export { default as toArray } from './jsUtils/toArray'; + +export { default as getPasteSource } from './pasteSourceValidations/getPasteSource'; diff --git a/packages/roosterjs-editor-dom/lib/inlineElements/NodeInlineElement.ts b/packages/roosterjs-editor-dom/lib/inlineElements/NodeInlineElement.ts index 838d9a559c5b..7cee5071f638 100644 --- a/packages/roosterjs-editor-dom/lib/inlineElements/NodeInlineElement.ts +++ b/packages/roosterjs-editor-dom/lib/inlineElements/NodeInlineElement.ts @@ -23,9 +23,11 @@ export default class NodeInlineElement implements InlineElement { */ public getTextContent(): string { // nodeValue is better way to retrieve content for a text. Others, just use textContent - return this.containerNode.nodeType == NodeType.Text - ? this.containerNode.nodeValue - : this.containerNode.textContent; + return ( + (this.containerNode.nodeType == NodeType.Text + ? this.containerNode.nodeValue + : this.containerNode.textContent) || '' + ); } /** diff --git a/packages/roosterjs-editor-dom/lib/inlineElements/PartialInlineElement.ts b/packages/roosterjs-editor-dom/lib/inlineElements/PartialInlineElement.ts index 552ee459e8b8..dea695fac4d4 100644 --- a/packages/roosterjs-editor-dom/lib/inlineElements/PartialInlineElement.ts +++ b/packages/roosterjs-editor-dom/lib/inlineElements/PartialInlineElement.ts @@ -14,8 +14,8 @@ import { getNextLeafSibling, getPreviousLeafSibling } from '../utils/getLeafSibl export default class PartialInlineElement implements InlineElement { constructor( private inlineElement: InlineElement, - private start?: NodePosition, - private end?: NodePosition + private start: NodePosition | null = null, + private end: NodePosition | null = null ) {} /** @@ -65,15 +65,17 @@ export default class PartialInlineElement implements InlineElement { /** * Get next partial inline element if it is not at the end boundary yet */ - public get nextInlineElement(): PartialInlineElement { - return this.end && new PartialInlineElement(this.inlineElement, this.end, null); + public get nextInlineElement(): PartialInlineElement | null { + return this.end ? new PartialInlineElement(this.inlineElement, this.end) : null; } /** * Get previous partial inline element if it is not at the begin boundary yet */ - public get previousInlineElement(): PartialInlineElement { - return this.start && new PartialInlineElement(this.inlineElement, null, this.start); + public get previousInlineElement(): PartialInlineElement | null { + return this.start + ? new PartialInlineElement(this.inlineElement, undefined, this.start) + : null; } /** @@ -103,8 +105,8 @@ export default class PartialInlineElement implements InlineElement { * apply style */ public applyStyle(styler: (element: HTMLElement, isInnerNode?: boolean) => any) { - let from = this.getStartPosition().normalize(); - let to = this.getEndPosition().normalize(); + let from: NodePosition | null = this.getStartPosition().normalize(); + let to: NodePosition | null = this.getEndPosition().normalize(); let container = this.getContainerNode(); if (from.isAtEnd) { @@ -116,6 +118,6 @@ export default class PartialInlineElement implements InlineElement { to = previousNode ? new Position(previousNode, PositionType.End) : null; } - applyTextStyle(container, styler, from, to); + applyTextStyle(container, styler, from || undefined, to || undefined); } } diff --git a/packages/roosterjs-editor-dom/lib/inlineElements/applyTextStyle.ts b/packages/roosterjs-editor-dom/lib/inlineElements/applyTextStyle.ts index 5d5b90abfe4f..74e34b0f6e30 100644 --- a/packages/roosterjs-editor-dom/lib/inlineElements/applyTextStyle.ts +++ b/packages/roosterjs-editor-dom/lib/inlineElements/applyTextStyle.ts @@ -5,15 +5,16 @@ import wrap from '../utils/wrap'; import { getNextLeafSibling } from '../utils/getLeafSibling'; import { NodePosition, NodeType, PositionType } from 'roosterjs-editor-types'; import { splitBalancedNodeRange } from '../utils/splitParentNode'; +import safeInstanceOf from '../utils/safeInstanceOf'; -const STYLET_AGS = 'SPAN,B,I,U,EM,STRONG,STRIKE,S,SMALL'.split(','); +const STYLET_AGS = 'SPAN,B,I,U,EM,STRONG,STRIKE,S,SMALL,SUP,SUB'.split(','); /** * Apply style using a styler function to the given container node in the given range * @param container The container node to apply style to * @param styler The styler function - * @param from From position - * @param to To position + * @param fromPosition From position + * @param toPosition To position */ export default function applyTextStyle( container: Node, @@ -22,23 +23,29 @@ export default function applyTextStyle( to: NodePosition = new Position(container, PositionType.End).normalize() ) { let formatNodes: Node[] = []; + let fromPosition: NodePosition | null = from; + let toPosition: NodePosition | null = to; - while (from && to && to.isAfter(from)) { - let formatNode = from.node; + while (fromPosition && toPosition && toPosition.isAfter(fromPosition)) { + let formatNode = fromPosition.node; let parentTag = getTagOfNode(formatNode.parentNode); // The code below modifies DOM. Need to get the next sibling first otherwise you won't be able to reliably get a good next sibling node let nextNode = getNextLeafSibling(container, formatNode); if (formatNode.nodeType == NodeType.Text && ['TR', 'TABLE'].indexOf(parentTag) < 0) { - if (formatNode == to.node && !to.isAtEnd) { - formatNode = splitTextNode(formatNode, to.offset, true /*returnFirstPart*/); + if (formatNode == toPosition.node && !toPosition.isAtEnd) { + formatNode = splitTextNode( + formatNode, + toPosition.offset, + true /*returnFirstPart*/ + ); } - if (from.offset > 0) { + if (fromPosition.offset > 0) { formatNode = splitTextNode( formatNode, - from.offset, + fromPosition.offset, false /*returnFirstPart*/ ); } @@ -46,23 +53,26 @@ export default function applyTextStyle( formatNodes.push(formatNode); } - from = nextNode && new Position(nextNode, PositionType.Begin); + fromPosition = nextNode && new Position(nextNode, PositionType.Begin); } if (formatNodes.length > 0) { if (formatNodes.every(node => node.parentNode == formatNodes[0].parentNode)) { - let newNode = formatNodes.shift(); + let newNode = formatNodes.shift()!; formatNodes.forEach(node => { - newNode.nodeValue += node.nodeValue; - node.parentNode.removeChild(node); + const newNodeValue = (newNode.nodeValue || '') + (node.nodeValue || ''); + newNode.nodeValue = newNodeValue; + node.parentNode?.removeChild(node); }); formatNodes = [newNode]; } - formatNodes.forEach(node => { + formatNodes.forEach(startingNode => { // When apply style within style tags like B/I/U/..., we split the tag and apply outside them // So that the inner style tag such as U, STRIKE can inherit the style we added + let node: Node | null = startingNode; while ( + node && getTagOfNode(node) != 'SPAN' && STYLET_AGS.indexOf(getTagOfNode(node.parentNode)) >= 0 ) { @@ -70,11 +80,14 @@ export default function applyTextStyle( node = splitBalancedNodeRange(node); } - if (getTagOfNode(node) != 'SPAN') { + if (node && getTagOfNode(node) != 'SPAN') { callStylerWithInnerNode(node, styler); node = wrap(node, 'SPAN'); } - styler(node); + + if (safeInstanceOf(node, 'HTMLElement')) { + styler(node); + } }); } } diff --git a/packages/roosterjs-editor-dom/lib/inlineElements/getFirstLastInlineElement.ts b/packages/roosterjs-editor-dom/lib/inlineElements/getFirstLastInlineElement.ts index cb8fc20baec7..9bc401e50fe9 100644 --- a/packages/roosterjs-editor-dom/lib/inlineElements/getFirstLastInlineElement.ts +++ b/packages/roosterjs-editor-dom/lib/inlineElements/getFirstLastInlineElement.ts @@ -6,7 +6,7 @@ import { InlineElement } from 'roosterjs-editor-types'; * @internal * Get the first inline element inside the given node */ -export function getFirstInlineElement(rootNode: Node): InlineElement { +export function getFirstInlineElement(rootNode: Node): InlineElement | null { // getFirstLeafNode can return null for empty container // do check null before passing on to get inline from the node let node = getFirstLeafNode(rootNode); @@ -17,7 +17,7 @@ export function getFirstInlineElement(rootNode: Node): InlineElement { * @internal * Get the last inline element inside the given node */ -export function getLastInlineElement(rootNode: Node): InlineElement { +export function getLastInlineElement(rootNode: Node): InlineElement | null { // getLastLeafNode can return null for empty container // do check null before passing on to get inline from the node let node = getLastLeafNode(rootNode); diff --git a/packages/roosterjs-editor-dom/lib/inlineElements/getInlineElementAtNode.ts b/packages/roosterjs-editor-dom/lib/inlineElements/getInlineElementAtNode.ts index 465a81789c76..5c33a1167303 100644 --- a/packages/roosterjs-editor-dom/lib/inlineElements/getInlineElementAtNode.ts +++ b/packages/roosterjs-editor-dom/lib/inlineElements/getInlineElementAtNode.ts @@ -11,7 +11,7 @@ import { BlockElement, InlineElement } from 'roosterjs-editor-types'; * @param rootNode The root node of current scope * @param node The node to get InlineElement from */ -export default function getInlineElementAtNode(rootNode: Node, node: Node): InlineElement; +export default function getInlineElementAtNode(rootNode: Node, node: Node | null): InlineElement; /** * Get the inline element at a node @@ -20,13 +20,13 @@ export default function getInlineElementAtNode(rootNode: Node, node: Node): Inli */ export default function getInlineElementAtNode( parentBlock: BlockElement, - node: Node + node: Node | null ): InlineElement; export default function getInlineElementAtNode( parent: Node | BlockElement, - node: Node -): InlineElement { + node: Node | null +): InlineElement | null { // An inline element has to be in a block element, get the block first and then resolve through the factory let parentBlock = safeInstanceOf(parent, 'Node') ? getBlockElementAtNode(parent, node) : parent; return node && parentBlock && resolveInlineElement(node, parentBlock); @@ -47,7 +47,7 @@ function resolveInlineElement(node: Node, parentBlock: BlockElement): InlineElem nodeChain.push(parent); } - let inlineElement: InlineElement; + let inlineElement: InlineElement | undefined; for (let i = nodeChain.length - 1; i >= 0 && !inlineElement; i--) { let currentNode = nodeChain[i]; diff --git a/packages/roosterjs-editor-dom/lib/inlineElements/getInlineElementBeforeAfter.ts b/packages/roosterjs-editor-dom/lib/inlineElements/getInlineElementBeforeAfter.ts index 705e6faaad2d..a4c58ad1e432 100644 --- a/packages/roosterjs-editor-dom/lib/inlineElements/getInlineElementBeforeAfter.ts +++ b/packages/roosterjs-editor-dom/lib/inlineElements/getInlineElementBeforeAfter.ts @@ -14,7 +14,7 @@ import { InlineElement, NodePosition, NodeType } from 'roosterjs-editor-types'; * @param root Root node of current scope, use for create InlineElement * @param position The position to get InlineElement before */ -export function getInlineElementBefore(root: Node, position: NodePosition): InlineElement { +export function getInlineElementBefore(root: Node, position: NodePosition): InlineElement | null { return getInlineElementBeforeAfter(root, position, false /*isAfter*/); } @@ -28,7 +28,7 @@ export function getInlineElementBefore(root: Node, position: NodePosition): Inli * @param root Root node of current scope, use for create InlineElement * @param position The position to get InlineElement after */ -export function getInlineElementAfter(root: Node, position: NodePosition): InlineElement { +export function getInlineElementAfter(root: Node, position: NodePosition): InlineElement | null { return getInlineElementBeforeAfter(root, position, true /*isAfter*/); } @@ -41,7 +41,8 @@ export function getInlineElementBeforeAfter(root: Node, position: NodePosition, } position = position.normalize(); - let { node, offset, isAtEnd } = position; + let { offset, isAtEnd } = position; + let node: Node | null = position.node; let isPartial = false; if ((!isAfter && offset == 0 && !isAtEnd) || (isAfter && isAtEnd)) { @@ -61,8 +62,8 @@ export function getInlineElementBeforeAfter(root: Node, position: NodePosition, if (inlineElement && (isPartial || inlineElement.contains(position))) { inlineElement = isAfter - ? new PartialInlineElement(inlineElement, position, null) - : new PartialInlineElement(inlineElement, null, position); + ? new PartialInlineElement(inlineElement, position, undefined) + : new PartialInlineElement(inlineElement, undefined, position); } return inlineElement; diff --git a/packages/roosterjs-editor-dom/lib/inlineElements/tsconfig.child.json b/packages/roosterjs-editor-dom/lib/inlineElements/tsconfig.child.json deleted file mode 100644 index fce11b74ff6d..000000000000 --- a/packages/roosterjs-editor-dom/lib/inlineElements/tsconfig.child.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "strict": false - }, - "extends": "../../../tsconfig.json", - "include": ["./**/*.ts"], - "references": [ - { "path": "../../../roosterjs-editor-types/tsconfig.child.json" }, - { "path": "../utils/tsconfig.child.json" }, - { "path": "../selection/tsconfig.child.json" }, - { "path": "../blockElements/tsconfig.child.json" } - ] -} diff --git a/packages/roosterjs-editor-dom/lib/utils/arrayPush.ts b/packages/roosterjs-editor-dom/lib/jsUtils/arrayPush.ts similarity index 100% rename from packages/roosterjs-editor-dom/lib/utils/arrayPush.ts rename to packages/roosterjs-editor-dom/lib/jsUtils/arrayPush.ts diff --git a/packages/roosterjs-editor-dom/lib/jsUtils/getObjectKeys.ts b/packages/roosterjs-editor-dom/lib/jsUtils/getObjectKeys.ts new file mode 100644 index 000000000000..72e0f58ce7eb --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/jsUtils/getObjectKeys.ts @@ -0,0 +1,10 @@ +/** + * Provide a strong-typed version of Object.keys() + * @param obj The source object + * @returns Array of keys + */ +export default function getObjectKeys( + obj: Record | Partial> +): T[] { + return Object.keys(obj) as T[]; +} diff --git a/packages/roosterjs-editor-dom/lib/utils/toArray.ts b/packages/roosterjs-editor-dom/lib/jsUtils/toArray.ts similarity index 100% rename from packages/roosterjs-editor-dom/lib/utils/toArray.ts rename to packages/roosterjs-editor-dom/lib/jsUtils/toArray.ts diff --git a/packages/roosterjs-editor-dom/lib/list/VList.ts b/packages/roosterjs-editor-dom/lib/list/VList.ts index bb5670448b31..330c76be7300 100644 --- a/packages/roosterjs-editor-dom/lib/list/VList.ts +++ b/packages/roosterjs-editor-dom/lib/list/VList.ts @@ -7,17 +7,28 @@ import Position from '../selection/Position'; import queryElements from '../utils/queryElements'; import safeInstanceOf from '../utils/safeInstanceOf'; import splitParentNode from '../utils/splitParentNode'; -import toArray from '../utils/toArray'; +import toArray from '../jsUtils/toArray'; import unwrap from '../utils/unwrap'; -import VListItem from './VListItem'; +import VListItem, { ListStyleDefinitionMetadata, ListStyleMetadata } from './VListItem'; import wrap from '../utils/wrap'; +import { getMetadata, setMetadata } from '../metadata/metadata'; import { Indentation, ListType, NodePosition, PositionType, NodeType, + Alignment, + NumberingListType, + BulletListType, } from 'roosterjs-editor-types'; +import type { + CompatibleAlignment, + CompatibleBulletListType, + CompatibleIndentation, + CompatibleListType, + CompatibleNumberingListType, +} from 'roosterjs-editor-types/lib/compatibleTypes'; /** * Represent a bullet or a numbering list @@ -67,7 +78,7 @@ export default class VList { * Create a new instance of VList class * @param rootList The root list element, can be either OL or UL tag */ - constructor(private rootList: HTMLOListElement | HTMLUListElement) { + constructor(public rootList: HTMLOListElement | HTMLUListElement) { if (!rootList) { throw new Error('rootList must not be null'); } @@ -149,7 +160,7 @@ export default class VList { * If there is no order list item, result will be undefined */ getLastItemNumber(): number | undefined { - const start = getStart(this.rootList); + const start = this.getStart(); return start === undefined ? start @@ -166,8 +177,10 @@ export default class VList { /** * Write the result back into DOM tree * After that, this VList becomes unavailable because we set this.rootList to null + * + * @param shouldReuseAllAncestorListElements Optional - defaults to false. */ - writeBack() { + writeBack(shouldReuseAllAncestorListElements?: boolean) { if (!this.rootList) { throw new Error('rootList must not be null'); } @@ -175,20 +188,25 @@ export default class VList { const doc = this.rootList.ownerDocument; const listStack: Node[] = [doc.createDocumentFragment()]; const placeholder = doc.createTextNode(''); - let start = getStart(this.rootList) || 1; + let start = this.getStart() || 1; let lastList: Node; // Use a placeholder to hold the position since the root list may be moved into document fragment later - this.rootList.parentNode.replaceChild(placeholder, this.rootList); + this.rootList.parentNode!.replaceChild(placeholder, this.rootList); this.items.forEach(item => { - if (item.getNewListStart() && item.getNewListStart() != start) { + const newListStart = item.getNewListStart(); + + if (newListStart && newListStart != start) { listStack.splice(1, listStack.length - 1); - start = item.getNewListStart(); + start = newListStart; } - item.writeBack(listStack, this.rootList); + + item.writeBack(listStack, this.rootList, shouldReuseAllAncestorListElements); const topList = listStack[1]; + item.applyListStyle(this.rootList, start); + if (safeInstanceOf(topList, 'HTMLOListElement')) { if (lastList != topList) { if (start == 1) { @@ -198,7 +216,7 @@ export default class VList { } } - if (item.getLevel() == 1) { + if (item.getLevel() == 1 && !item.isDummy()) { start++; } } @@ -207,11 +225,7 @@ export default class VList { }); // Restore the content to the position of placeholder - placeholder.parentNode.replaceChild(listStack[0], placeholder); - - // Set rootList to null to avoid this to be called again for the same VList, because - // after change the rootList may not be available any more (e.g. outdent all items). - this.rootList = null; + placeholder.parentNode!.replaceChild(listStack[0], placeholder); } /** @@ -239,7 +253,11 @@ export default class VList { * @param end End position to operate to * @param indentation Indent or outdent */ - setIndentation(start: NodePosition, end: NodePosition, indentation: Indentation): void; + setIndentation( + start: NodePosition, + end: NodePosition, + indentation: Indentation | CompatibleIndentation + ): void; /** * Outdent the give range of this list @@ -248,27 +266,64 @@ export default class VList { * @param indentation Specify to outdent * @param softOutdent (Optional) True to make the item to by dummy (no bullet or number) if the item is not dummy, * otherwise outdent the item + * @param preventItemRemoval (Optional) True to prevent the indentation to remove the bullet when outdenting a first + * level list item, by default is false */ setIndentation( start: NodePosition, end: NodePosition, - indentation: Indentation.Decrease, - softOutdent?: boolean + indentation: Indentation.Decrease | CompatibleIndentation.Decrease, + softOutdent?: boolean, + preventItemRemoval?: boolean ): void; setIndentation( start: NodePosition, end: NodePosition, - indentation: Indentation, - softOutdent?: boolean + indentation: Indentation | CompatibleIndentation, + softOutdent?: boolean, + preventItemRemoval: boolean = false ) { - this.findListItems(start, end, item => + let shouldAddMargin = false; + this.findListItems(start, end, item => { + shouldAddMargin = shouldAddMargin || this.items.indexOf(item) == 0; indentation == Indentation.Decrease ? softOutdent && !item.isDummy() ? item.setIsDummy(true /*isDummy*/) - : item.outdent() - : item.indent() - ); + : item.outdent(preventItemRemoval) + : item.indent(); + }); + + if (shouldAddMargin && preventItemRemoval) { + for (let index = 0; index < this.items.length; index++) { + this.items[index].addNegativeMargins(); + } + } + } + + /** + * Set alignment of the given range of this list + * @param start Start position to operate from + * @param end End position to operate to + * @param alignment Align items left, center or right + */ + + setAlignment( + start: NodePosition, + end: NodePosition, + alignment: Alignment | CompatibleAlignment + ) { + this.rootList.style.display = 'flex'; + this.rootList.style.flexDirection = 'column'; + this.findListItems(start, end, item => { + let align = 'start'; + if (alignment == Alignment.Center) { + align = 'center'; + } else if (alignment == Alignment.Right) { + align = 'end'; + } + item.getNode().style.alignSelf = align; + }); } /** @@ -279,7 +334,11 @@ export default class VList { * @param end End position to operate to * @param targetType Target list type */ - changeListType(start: NodePosition, end: NodePosition, targetType: ListType) { + changeListType( + start: NodePosition, + end: NodePosition, + targetType: ListType | CompatibleListType + ) { let needChangeType = false; this.findListItems(start, end, item => { @@ -290,22 +349,45 @@ export default class VList { ); } + /** + * Change list style of the given range of this list. + * If some of the items are not real list item yet, this will make them to be list item with given style + * @param orderedStyle The style of ordered list + * @param unorderedStyle The style of unordered list + */ + setListStyleType( + orderedStyle?: NumberingListType | CompatibleNumberingListType, + unorderedStyle?: BulletListType | CompatibleBulletListType + ) { + const style = getMetadata(this.rootList, ListStyleDefinitionMetadata); + const styleMetadata = createListStyleMetadata( + style, + orderedStyle as NumberingListType, + unorderedStyle as BulletListType + ); + setMetadata(this.rootList, styleMetadata, ListStyleDefinitionMetadata); + } + /** * Append a new item to this VList * @param node node of the item to append. If it is not wrapped with LI tag, it will be wrapped * @param type Type of this list item, can be ListType.None */ - appendItem(node: Node, type: ListType) { + appendItem(node: Node, type: ListType | CompatibleListType) { const nodeTag = getTagOfNode(node); // Change DIV tag to SPAN. Otherwise we cannot create new list item by Enter key in Safari if (nodeTag == 'DIV') { - node = changeElementTag(node, 'LI'); + node = changeElementTag(node, 'LI')!; } else if (nodeTag != 'LI') { node = wrap(node, 'LI'); } - this.items.push(type == ListType.None ? new VListItem(node) : new VListItem(node, type)); + this.items.push( + type == ListType.None + ? new VListItem(node) + : new VListItem(node, (type)) + ); } /** @@ -324,6 +406,55 @@ export default class VList { } } + /** + * Get the index of the List Item in the current List + * If the root list is: + * Ordered list, the listIndex start count is going to be the start property of the OL - 1, + * @example For example if we want to find the index of Item 2 in the list below, the returned index is going to be 6 + * * ```html + *
    + *
  1. item 1
  2. + *
  3. item 2
  4. + *
  5. item 3
  6. + *
+ * ``` + * Unordered list, the listIndex start count starts from 0 + * @example For example if we want to find the index of Item 2 in the list below, the returned index is going to be 2 + * ```html + *
    + *
  • item 1
  • + *
  • item 2
  • + *
  • item 3
  • + *
+ * ``` + * @param input List item to find in the root list + */ + getListItemIndex(input: Node) { + if (this.items) { + let listIndex = (this.getStart() || 1) - 1; + + for (let index = 0; index < this.items.length; index++) { + const child = this.items[index]; + if (child.getLevel() == 1 && !child.isDummy()) { + listIndex++; + } + + if (child.getNode() == input) { + return listIndex; + } + } + } + return -1; + } + + /** + * Get the Start property of the root list of this VList + * @returns Start number of the list + */ + getStart(): number | undefined { + return safeInstanceOf(this.rootList, 'HTMLOListElement') ? this.rootList.start : undefined; + } + private findListItems( start: NodePosition, end: NodePosition, @@ -361,7 +492,12 @@ export default class VList { private populateItems( list: HTMLOListElement | HTMLUListElement, - listTypes: (ListType.Ordered | ListType.Unordered)[] = [] + listTypes: ( + | ListType.Ordered + | ListType.Unordered + | CompatibleListType.Ordered + | CompatibleListType.Unordered + )[] = [] ) { const type = getListTypeFromNode(list); const items = toArray(list.childNodes); @@ -371,7 +507,7 @@ export default class VList { if (isListElement(item)) { this.populateItems(item, newListTypes); - } else if (item.nodeType != NodeType.Text || item.nodeValue.trim() != '') { + } else if (item.nodeType != NodeType.Text || (item.nodeValue || '').trim() != '') { this.items.push(new VListItem(item, ...newListTypes)); } }); @@ -384,8 +520,8 @@ export default class VList { // e.g. // From:
  • line 1
  • line 2
// To:
  • line 1
    line 2
-function moveChildNodesToLi(list: HTMLOListElement | HTMLUListElement) { - let currentItem: HTMLLIElement = null; +function moveChildNodesToLi(list: HTMLElement) { + let currentItem: HTMLLIElement | null = null; toArray(list.childNodes).forEach(child => { if (getTagOfNode(child) == 'LI') { @@ -402,10 +538,10 @@ function moveChildNodesToLi(list: HTMLOListElement | HTMLUListElement) { // e.g. // From:
  • line 1
  • line 2
  • line 3
// To:
  • line 1
  • line 2
    line 3
-function moveLiToList(li: HTMLLIElement) { +function moveLiToList(li: HTMLElement) { while (!isListElement(li.parentNode)) { splitParentNode(li, true /*splitBefore*/); - let furtherNodes: Node[] = toArray(li.parentNode.childNodes).slice(1); + let furtherNodes: Node[] = toArray(li.parentNode!.childNodes).slice(1); if (furtherNodes.length > 0) { if (!isBlockElement(furtherNodes[0])) { @@ -414,10 +550,29 @@ function moveLiToList(li: HTMLLIElement) { furtherNodes.forEach(node => li.appendChild(node)); } - unwrap(li.parentNode); + unwrap(li.parentNode!); } } -function getStart(list: HTMLOListElement | HTMLUListElement): number | undefined { - return safeInstanceOf(list, 'HTMLOListElement') ? list.start : undefined; +function getValidValue(...values: (T | undefined)[]): T | undefined { + return values.filter(x => x !== undefined)[0]; +} + +function createListStyleMetadata( + style: ListStyleMetadata | null, + orderedStyle?: NumberingListType | CompatibleNumberingListType, + unorderedStyle?: BulletListType | CompatibleBulletListType +): ListStyleMetadata { + return { + orderedStyleType: getValidValue( + orderedStyle, + style?.orderedStyleType, + NumberingListType.Decimal + ), + unorderedStyleType: getValidValue( + unorderedStyle, + style?.unorderedStyleType, + BulletListType.Disc + ), + }; } diff --git a/packages/roosterjs-editor-dom/lib/list/VListChain.ts b/packages/roosterjs-editor-dom/lib/list/VListChain.ts index de001efb442d..add107156550 100644 --- a/packages/roosterjs-editor-dom/lib/list/VListChain.ts +++ b/packages/roosterjs-editor-dom/lib/list/VListChain.ts @@ -1,4 +1,4 @@ -import arrayPush from '../utils/arrayPush'; +import arrayPush from '../jsUtils/arrayPush'; import getRootListNode from './getRootListNode'; import isNodeAfter from '../utils/isNodeAfter'; import isNodeInRegion from '../region/isNodeInRegion'; @@ -45,7 +45,7 @@ export default class VListChain { chains.filter(c => c.canAppendToTail(list))[0] || new VListChain(region, (nameGenerator || createListChainName)()); const index = chains.indexOf(chain); - const afterCurrentNode = currentNode && isNodeAfter(list, currentNode); + const afterCurrentNode = !!currentNode && isNodeAfter(list, currentNode); if (!afterCurrentNode) { // Make sure current one is at the front if current block has not been met, so that @@ -83,9 +83,9 @@ export default class VListChain { * @param container The container node to create list at * @param startNumber Start number of the new list */ - createVListAtBlock(container: Node, startNumber: number): VList { - if (container) { - const list = container.ownerDocument.createElement('ol'); + createVListAtBlock(container: Node, startNumber: number): VList | null { + if (container && container.parentNode) { + const list = container.ownerDocument!.createElement('ol'); list.start = startNumber; this.applyChainName(list); @@ -104,22 +104,27 @@ export default class VListChain { * After change the lists, commit the change to all lists in this chain to update the list number, * and clear the temporary dataset values added to list node */ - commit() { + commit(shouldReuseAllAncestorListElements?: boolean) { const lists = this.getLists(); let lastNumber = 0; for (let i = 0; i < lists.length; i++) { const list = lists[i]; - list.start = lastNumber + 1; - const vlist = new VList(list); + //If there is a list chain sequence, ensure the list chain keep increasing correctly + if (list.start > 1) { + list.start = list.start === lastNumber ? lastNumber + 1 : list.start; + } else { + list.start = lastNumber + 1; + } - lastNumber = vlist.getLastItemNumber(); + const vlist = new VList(list); + lastNumber = vlist.getLastItemNumber() || 0; delete list.dataset[CHAIN_DATASET_NAME]; delete list.dataset[AFTER_CURSOR_DATASET_NAME]; - vlist.writeBack(); + vlist.writeBack(shouldReuseAllAncestorListElements); } } @@ -144,7 +149,7 @@ export default class VListChain { */ private append(list: HTMLOListElement, isAfterCurrentNode: boolean) { this.applyChainName(list); - this.lastNumber = new VList(list).getLastItemNumber(); + this.lastNumber = new VList(list).getLastItemNumber() || 0; if (isAfterCurrentNode) { list.dataset[AFTER_CURSOR_DATASET_NAME] = 'true'; diff --git a/packages/roosterjs-editor-dom/lib/list/VListItem.ts b/packages/roosterjs-editor-dom/lib/list/VListItem.ts index 12e62814f985..38bd1b241f45 100644 --- a/packages/roosterjs-editor-dom/lib/list/VListItem.ts +++ b/packages/roosterjs-editor-dom/lib/list/VListItem.ts @@ -1,16 +1,71 @@ import contains from '../utils/contains'; import getListTypeFromNode from './getListTypeFromNode'; +import getStyles from '../style/getStyles'; import getTagOfNode from '../utils/getTagOfNode'; import isBlockElement from '../utils/isBlockElement'; import moveChildNodes from '../utils/moveChildNodes'; import safeInstanceOf from '../utils/safeInstanceOf'; +import setBulletListMarkers from './setBulletListMarkers'; import setListItemStyle from './setListItemStyle'; -import toArray from '../utils/toArray'; +import setNumberingListMarkers from './setNumberingListMarkers'; +import setStyles from '../style/setStyles'; +import toArray from '../jsUtils/toArray'; import unwrap from '../utils/unwrap'; import wrap from '../utils/wrap'; -import { KnownCreateElementDataIndex, ListType } from 'roosterjs-editor-types'; +import { createNumberDefinition, createObjectDefinition } from '../metadata/definitionCreators'; +import { getMetadata, setMetadata } from '../metadata/metadata'; +import { + BulletListType, + KnownCreateElementDataIndex, + ListType, + NumberingListType, +} from 'roosterjs-editor-types'; +import type { + CompatibleBulletListType, + CompatibleListType, + CompatibleNumberingListType, +} from 'roosterjs-editor-types/lib/compatibleTypes'; const orderListStyles = [null, 'lower-alpha', 'lower-roman']; +const unorderedListStyles = ['disc', 'circle', 'square']; + +const MARGIN_BASE = '0in 0in 0in 0.5in'; +const NEGATIVE_MARGIN = '-.25in'; + +const stylesToInherit = ['font-size', 'font-family', 'color']; +const attrsToInherit = ['data-ogsc', 'data-ogsb', 'data-ogac', 'data-ogab']; + +/** + * @internal + * The definition for the number of BulletListType or NumberingListType + */ +export const ListStyleDefinitionMetadata = createObjectDefinition( + { + orderedStyleType: createNumberDefinition( + true /** isOptional */, + undefined /** value **/, + NumberingListType.Min, + NumberingListType.Max + ), + unorderedStyleType: createNumberDefinition( + true /** isOptional */, + undefined /** value **/, + BulletListType.Min, + BulletListType.Max + ), + }, + true /** isOptional */, + true /** allowNull */ +); + +/** + * @internal + * Represents the metadata of the style of a list element + */ +export interface ListStyleMetadata { + orderedStyleType?: NumberingListType | CompatibleNumberingListType; + unorderedStyleType?: BulletListType | CompatibleBulletListType; +} /** * !!! Never directly create instance of this class. It should be created within VList class !!! @@ -22,10 +77,10 @@ const orderListStyles = [null, 'lower-alpha', 'lower-roman']; * That can happen after we do "outdent" on a 1-level list item, then it becomes not a list item. */ export default class VListItem { - private listTypes: ListType[]; + private listTypes: (ListType | CompatibleListType)[]; private node: HTMLLIElement; private dummy: boolean; - private newListStart: number = undefined; + private newListStart: number | undefined = undefined; /** * Construct a new instance of VListItem class @@ -33,7 +88,15 @@ export default class VListItem { * @param listTypes An array represents list types of all parent and current level. * Skip this parameter for a non-list item. */ - constructor(node: Node, ...listTypes: (ListType.Ordered | ListType.Unordered)[]) { + constructor( + node: Node, + ...listTypes: ( + | ListType.Ordered + | ListType.Unordered + | CompatibleListType.Ordered + | CompatibleListType.Unordered + )[] + ) { if (!node) { throw new Error('node must not be null'); } @@ -52,7 +115,7 @@ export default class VListItem { /** * Get type of current list item */ - getListType(): ListType { + getListType(): ListType | CompatibleListType { return this.listTypes[this.listTypes.length - 1]; } @@ -130,6 +193,12 @@ export default class VListItem { * If this is not an list item, it will be no op */ indent() { + if (this.node.style.marginLeft == NEGATIVE_MARGIN) { + this.node.style.margin = ''; + this.node.style.marginLeft = ''; + return; + } + const listType = this.getListType(); if (listType != ListType.None) { this.listTypes.push(listType); @@ -139,18 +208,28 @@ export default class VListItem { /** * Outdent this item * If this item is already not an list item, it will be no op + * @param preventItemRemoval Whether prevent the list item to be removed for the listItem by default false */ - outdent() { - if (this.listTypes.length > 1) { + outdent(preventItemRemoval: boolean = false) { + const expectedLength = preventItemRemoval ? 2 : 1; + if (this.listTypes.length > expectedLength) { this.listTypes.pop(); } } + /** + * Add negative margin to the List item + */ + addNegativeMargins() { + this.node.style.margin = MARGIN_BASE; + this.node.style.marginLeft = NEGATIVE_MARGIN; + } + /** * Change list type of this item * @param targetType The target list type to change to */ - changeListType(targetType: ListType) { + changeListType(targetType: ListType | CompatibleListType) { if (targetType == ListType.None) { this.listTypes = [targetType]; } else { @@ -175,23 +254,79 @@ export default class VListItem { this.newListStart = startNumber; } + /** + * Apply the list style type + * @param rootList the vList that receives the style + * @param index the list item index + */ + applyListStyle(rootList: HTMLOListElement | HTMLUListElement, index: number) { + const style = getMetadata(rootList, ListStyleDefinitionMetadata); + // The list just need to be styled if it is at top level, so the listType length for this Vlist must be 2. + const isFirstLevel = this.listTypes.length < 3; + if (style) { + if ( + isFirstLevel && + this.listTypes[1] === ListType.Unordered && + style.unorderedStyleType + ) { + setBulletListMarkers(this.node, style.unorderedStyleType); + } else if ( + isFirstLevel && + this.listTypes[1] === ListType.Ordered && + style.orderedStyleType + ) { + setNumberingListMarkers(this.node, style.orderedStyleType, index); + } else { + this.node.style.removeProperty('list-style-type'); + } + } + } + /** * Write the change result back into DOM * @param listStack current stack of list elements * @param originalRoot Original list root element. It will be reused when write back if possible + * @param shouldReuseAllAncestorListElements Optional - defaults to false. If true, only make + * sure the direct parent of this list matches the list types when writing back. */ - writeBack(listStack: Node[], originalRoot?: HTMLOListElement | HTMLUListElement) { + writeBack( + listStack: Node[], + originalRoot?: HTMLOListElement | HTMLUListElement, + shouldReuseAllAncestorListElements: boolean = false + ) { let nextLevel = 1; - // 1. Determine list elements that we can reuse - // e.g.: - // passed in listStack: Fragment > OL > UL > OL - // local listTypes: null > OL > UL > UL > OL - // then Fragment > OL > UL can be reused - for (; nextLevel < listStack.length; nextLevel++) { - if (getListTypeFromNode(listStack[nextLevel]) !== this.listTypes[nextLevel]) { - listStack.splice(nextLevel); - break; + if (shouldReuseAllAncestorListElements) { + // Remove any un-needed lists from the stack. + if (listStack.length > this.listTypes.length) { + listStack.splice(this.listTypes.length); + } + + // 1. If the listStack is the same length as the listTypes for this item, check + // if the last item needs to change, and remove it if needed. We can always re-use + // the other lists even if the type doesn't match - since the display is the same + // as long as the list immediately surrounding the item is correct. + const listStackEndIndex = listStack.length - 1; + if ( + listStackEndIndex === this.listTypes.length - 1 && // they are the same length + getListTypeFromNode(listStack[listStackEndIndex]) !== + this.listTypes[listStackEndIndex] + ) { + listStack.splice(listStackEndIndex); + } + + nextLevel = listStack.length; + } else { + // 1. Determine list elements that we can reuse + // e.g.: + // passed in listStack: Fragment > OL > UL > OL + // local listTypes: null > OL > UL > UL > OL + // then Fragment > OL > UL can be reused + for (; nextLevel < listStack.length; nextLevel++) { + if (getListTypeFromNode(listStack[nextLevel]) !== this.listTypes[nextLevel]) { + listStack.splice(nextLevel); + break; + } } } @@ -201,6 +336,7 @@ export default class VListItem { // local listTypes: null > OL > UL > UL > OL // then we need to create a UL and a OL tag for (; nextLevel < this.listTypes.length; nextLevel++) { + const stackLength = listStack.length - 1; const newList = createListElement( listStack[0], this.listTypes[nextLevel], @@ -208,42 +344,112 @@ export default class VListItem { originalRoot ); - listStack[listStack.length - 1].appendChild(newList); + listStack[stackLength].appendChild(newList); listStack.push(newList); - } + //If the current node parent is in the same deep child index, + //apply the styles of the current parent list to the new list + if (this.getDeepChildIndex(originalRoot) == stackLength) { + const listStyleType = this.node.parentElement?.style.listStyleType; + if ( + listStyleType && + getTagOfNode(this.node.parentElement) === getTagOfNode(newList) + ) { + newList.style.listStyleType = listStyleType; + } + } + } // 3. Add current node into deepest list element listStack[listStack.length - 1].appendChild(this.node); - this.node.style.display = this.dummy ? 'block' : null; + this.node.style.setProperty('display', this.dummy ? 'block' : null); // 4. Inherit styles of the child element to the li, so we are able to apply the styles to the ::marker if (this.listTypes.length > 1) { - if ( - !(this.node.style.fontSize || this.node.style.color || this.node.style.fontFamily) - ) { - const stylesToInherit = ['font-size', 'font-family', 'color']; - setListItemStyle(this.node, stylesToInherit); - } + setListItemStyle(this.node, stylesToInherit, true /*isCssStyle*/); + setListItemStyle(this.node, attrsToInherit, false /*isCssStyle*/); } // 5. If this is not a list item now, need to unwrap the LI node and do proper handling if (this.listTypes.length <= 1) { - wrapIfNotBlockNode( - getTagOfNode(this.node) == 'LI' ? getChildrenAndUnwrap(this.node) : [this.node], - true /*checkFirst*/, - true /*checkLast*/ - ); + // If original
  • node has styles for font and color, we need to apply it to new parent + const isLi = getTagOfNode(this.node) == 'LI'; + const stylesToApply = isLi + ? { + 'font-family': this.node.style.fontFamily, + 'font-size': this.node.style.fontSize, + color: this.node.style.color, + } + : undefined; + + const childNodes = isLi ? getChildrenAndUnwrap(this.node) : [this.node]; + + if (stylesToApply) { + for (let i = 0; i < childNodes.length; i++) { + if (safeInstanceOf(childNodes[i], 'Text')) { + childNodes[i] = wrap(childNodes[i], 'span'); + } + + const node = childNodes[i]; + + if (safeInstanceOf(node, 'HTMLElement')) { + const styles = { + ...stylesToApply, + ...getStyles(node), + }; + setStyles(node, styles); + + attrsToInherit.forEach(attr => { + const attrValue = this.node.getAttribute(attr); + + if (attrValue) { + node.setAttribute(attr, attrValue); + } + }); + } + } + } + + wrapIfNotBlockNode(childNodes, true /*checkFirst*/, true /*checkLast*/); + } + } + + /** + * Get the index of how deep is the current node parent list inside of the original root list. + * @example In the following structure this function would return 2 + * ```html + *
      + *
        + *
          + *
        1. + *
        + *
      + *
    + * ``` + * @param originalRoot The root list + * @returns -1 if the node does not have parent element or if original root was not provided, + * else, how deep is the parent element inside of the original root. + */ + private getDeepChildIndex(originalRoot: HTMLOListElement | HTMLUListElement | undefined) { + let parentElement = this.node.parentElement; + if (originalRoot && parentElement) { + let deepIndex = 0; + while (parentElement && parentElement != originalRoot) { + deepIndex++; + parentElement = parentElement?.parentElement || null; + } + return deepIndex; } + return -1; } } function createListElement( newRoot: Node, - listType: ListType, + listType: ListType | CompatibleListType, nextLevel: number, originalRoot?: HTMLOListElement | HTMLUListElement ): HTMLOListElement | HTMLUListElement { - const doc = newRoot.ownerDocument; + const doc = newRoot.ownerDocument!; let result: HTMLOListElement | HTMLUListElement; // Try to reuse the existing root element @@ -267,8 +473,26 @@ function createListElement( result = doc.createElement(listType == ListType.Ordered ? 'ol' : 'ul'); } + // Always maintain the metadata saved in the list + if (originalRoot && nextLevel == 1 && listType != getListTypeFromNode(originalRoot)) { + const style = getMetadata(originalRoot, ListStyleDefinitionMetadata); + if (style) { + setMetadata(result, style, ListStyleDefinitionMetadata); + } + } + if (listType == ListType.Ordered && nextLevel > 1) { - result.style.listStyleType = orderListStyles[(nextLevel - 1) % orderListStyles.length]; + result.style.setProperty( + 'list-style-type', + orderListStyles[(nextLevel - 1) % orderListStyles.length] + ); + } + + if (listType == ListType.Unordered && nextLevel > 1) { + result.style.setProperty( + 'list-style-type', + unorderedListStyles[(nextLevel - 1) % unorderedListStyles.length] + ); } return result; diff --git a/packages/roosterjs-editor-dom/lib/list/convertDecimalsToAlpha.ts b/packages/roosterjs-editor-dom/lib/list/convertDecimalsToAlpha.ts new file mode 100644 index 000000000000..567ad55d15c6 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/list/convertDecimalsToAlpha.ts @@ -0,0 +1,44 @@ +const ALPHABET: Record = { + 0: 'A', + 1: 'B', + 2: 'C', + 3: 'D', + 4: 'E', + 5: 'F', + 6: 'G', + 7: 'H', + 8: 'I', + 9: 'J', + 10: 'K', + 11: 'L', + 12: 'M', + 13: 'N', + 14: 'O', + 15: 'P', + 16: 'Q', + 17: 'R', + 18: 'S', + 19: 'T', + 20: 'U', + 21: 'V', + 22: 'W', + 23: 'X', + 24: 'Y', + 25: 'Z', +}; + +/** + * @internal + * Convert decimal numbers into english alphabet letters + * @param decimal The decimal number that needs to be converted + * @param isLowerCase if true the roman value will appear in lower case + * @returns + */ +export default function convertDecimalsToAlpha(decimal: number, isLowerCase?: boolean): string { + let alpha = ''; + while (decimal >= 0) { + alpha = ALPHABET[decimal % 26] + alpha; + decimal = Math.floor(decimal / 26) - 1; + } + return isLowerCase ? alpha.toLowerCase() : alpha; +} diff --git a/packages/roosterjs-editor-dom/lib/list/convertDecimalsToRomans.ts b/packages/roosterjs-editor-dom/lib/list/convertDecimalsToRomans.ts new file mode 100644 index 000000000000..c2d4ff8f08df --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/list/convertDecimalsToRomans.ts @@ -0,0 +1,34 @@ +import getObjectKeys from '../jsUtils/getObjectKeys'; + +const RomanValues: Record = { + M: 1000, + CM: 900, + D: 500, + CD: 400, + C: 100, + XC: 90, + L: 50, + XL: 40, + X: 10, + IX: 9, + V: 5, + IV: 4, + I: 1, +}; + +/** + * @internal + * Convert decimal numbers into roman numbers + * @param decimal The decimal number that needs to be converted + * @param isLowerCase if true the roman value will appear in lower case + * @returns + */ +export default function convertDecimalsToRoman(decimal: number, isLowerCase?: boolean) { + let romanValue = ''; + for (let i of getObjectKeys(RomanValues)) { + let timesRomanCharAppear = Math.floor(decimal / RomanValues[i]); + decimal = decimal - timesRomanCharAppear * RomanValues[i]; + romanValue = romanValue + i.repeat(timesRomanCharAppear); + } + return isLowerCase ? romanValue.toLocaleLowerCase() : romanValue; +} diff --git a/packages/roosterjs-editor-dom/lib/list/createVListFromRegion.ts b/packages/roosterjs-editor-dom/lib/list/createVListFromRegion.ts index 2d24d099c6c8..c8b61c320e69 100644 --- a/packages/roosterjs-editor-dom/lib/list/createVListFromRegion.ts +++ b/packages/roosterjs-editor-dom/lib/list/createVListFromRegion.ts @@ -5,7 +5,7 @@ import isNodeInRegion from '../region/isNodeInRegion'; import Position from '../selection/Position'; import safeInstanceOf from '../utils/safeInstanceOf'; import shouldSkipNode from '../utils/shouldSkipNode'; -import toArray from '../utils/toArray'; +import toArray from '../jsUtils/toArray'; import VList from './VList'; import wrap from '../utils/wrap'; import { getLeafSibling } from '../utils/getLeafSibling'; @@ -31,7 +31,7 @@ export default function createVListFromRegion( region: Region, includeSiblingLists?: boolean, startNode?: Node -): VList { +): VList | null { if (!region) { return null; } @@ -44,7 +44,11 @@ export default function createVListFromRegion( nodes.push(list); } } else { - const blocks = getSelectedBlockElementsInRegion(region); + const blocks = getSelectedBlockElementsInRegion( + region, + undefined, + true /* shouldApplyFormatToSpan */ + ); blocks.forEach(block => { const list = getRootListNode(region, ListSelector, block.getStartNode()); @@ -69,7 +73,7 @@ export default function createVListFromRegion( const newNode = createElement( KnownCreateElementDataIndex.EmptyLine, region.rootNode.ownerDocument - ); + )!; region.rootNode.appendChild(newNode); nodes.push(newNode); region.fullSelectionStart = new Position(newNode, PositionType.Begin); @@ -84,28 +88,32 @@ export default function createVListFromRegion( nodes = nodes.filter(node => !shouldSkipNode(node, true /*ignoreSpace*/)); } - let vList: VList = null; + let vList: VList | null = null; if (nodes.length > 0) { - const firstNode = nodes.shift(); + const firstNode = nodes.shift() || null; vList = isListElement(firstNode) ? new VList(firstNode) - : createVListFromItemNode(firstNode); - - nodes.forEach(node => { - if (isListElement(node)) { - vList.mergeVList(new VList(node)); - } else { - vList.appendItem(node, ListType.None); - } - }); + : firstNode + ? createVListFromItemNode(firstNode) + : null; + + if (vList) { + nodes.forEach(node => { + if (isListElement(node)) { + vList!.mergeVList(new VList(node)); + } else { + vList!.appendItem(node, ListType.None); + } + }); + } } return vList; } function tryIncludeSiblingNode(region: Region, nodes: Node[], isNext: boolean) { - let node = nodes[isNext ? nodes.length - 1 : 0]; + let node: Node | null = nodes[isNext ? nodes.length - 1 : 0]; node = getLeafSibling(region.rootNode, node, isNext, region.skipTags, true /*ignoreSpace*/); node = getRootListNode(region, ListSelector, node); if (isNodeInRegion(region, node) && isListElement(node)) { @@ -129,7 +137,7 @@ function createVListFromItemNode(node: Node): VList { const nodeForItem = childNodes.length == 1 ? childNodes[0] : wrap(childNodes, 'SPAN'); // Create a temporary OL root element for this list. - const listNode = node.ownerDocument.createElement('ol'); // Either OL or UL is ok here + const listNode = node.ownerDocument!.createElement('ol'); // Either OL or UL is ok here node.appendChild(listNode); // Create the VList and append items diff --git a/packages/roosterjs-editor-dom/lib/list/getListTypeFromNode.ts b/packages/roosterjs-editor-dom/lib/list/getListTypeFromNode.ts index 0476923b246b..88385136c2fb 100644 --- a/packages/roosterjs-editor-dom/lib/list/getListTypeFromNode.ts +++ b/packages/roosterjs-editor-dom/lib/list/getListTypeFromNode.ts @@ -1,5 +1,6 @@ import getTagOfNode from '../utils/getTagOfNode'; import { ListType } from 'roosterjs-editor-types'; +import type { CompatibleListType } from 'roosterjs-editor-types/lib/compatibleTypes'; /** * @internal @@ -8,16 +9,20 @@ import { ListType } from 'roosterjs-editor-types'; */ export default function getListTypeFromNode( listElement: HTMLOListElement | HTMLUListElement -): ListType.Ordered | ListType.Unordered; +): + | ListType.Ordered + | ListType.Unordered + | CompatibleListType.Ordered + | CompatibleListType.Unordered; /** * @internal * Get list type from a DOM node. It is possible to return ListType.None * @param node the node to get list type from */ -export default function getListTypeFromNode(node: Node): ListType; +export default function getListTypeFromNode(node: Node | null): ListType | CompatibleListType; -export default function getListTypeFromNode(node: Node): ListType { +export default function getListTypeFromNode(node: Node | null): ListType | CompatibleListType { switch (getTagOfNode(node)) { case 'OL': return ListType.Ordered; @@ -33,6 +38,6 @@ export default function getListTypeFromNode(node: Node): ListType { * Check if the given DOM node is a list element (OL or UL) * @param node The node to check */ -export function isListElement(node: Node): node is HTMLUListElement | HTMLOListElement { +export function isListElement(node: Node | null): node is HTMLUListElement | HTMLOListElement { return getListTypeFromNode(node) != ListType.None; } diff --git a/packages/roosterjs-editor-dom/lib/list/getRootListNode.ts b/packages/roosterjs-editor-dom/lib/list/getRootListNode.ts index 8debd0ab2f8e..374e885ce7a8 100644 --- a/packages/roosterjs-editor-dom/lib/list/getRootListNode.ts +++ b/packages/roosterjs-editor-dom/lib/list/getRootListNode.ts @@ -21,7 +21,7 @@ export interface SelectorToTypeMap { export default function getRootListNode( region: RegionBase, selector: TSelector, - node: Node + node: Node | null ): SelectorToTypeMap[TSelector] { let list = region && diff --git a/packages/roosterjs-editor-dom/lib/list/setBulletListMarkers.ts b/packages/roosterjs-editor-dom/lib/list/setBulletListMarkers.ts new file mode 100644 index 000000000000..eba8f88291a0 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/list/setBulletListMarkers.ts @@ -0,0 +1,28 @@ +import { BulletListType } from 'roosterjs-editor-types'; +import type { CompatibleBulletListType } from 'roosterjs-editor-types/lib/compatibleTypes'; + +/** + * @internal + * Set the marker of a bullet list + * @param li + * @param listStyleType + */ +export default function setBulletListMarkers( + li: HTMLLIElement, + listStyleType: BulletListType | CompatibleBulletListType +) { + const marker = bulletListStyle[listStyleType]; + const isDisc = listStyleType === BulletListType.Disc; + li.style.listStyleType = isDisc ? marker : `"${marker}"`; +} + +const bulletListStyle: Record = { + [BulletListType.Disc]: 'disc', + [BulletListType.Square]: '∎ ', + [BulletListType.Dash]: '- ', + [BulletListType.LongArrow]: '➔ ', + [BulletListType.DoubleLongArrow]: '➔ ', + [BulletListType.ShortArrow]: '➢ ', + [BulletListType.UnfilledArrow]: '➪ ', + [BulletListType.Hyphen]: '— ', +}; diff --git a/packages/roosterjs-editor-dom/lib/list/setListItemStyle.ts b/packages/roosterjs-editor-dom/lib/list/setListItemStyle.ts index 5284d50f8905..699ea1d6c45d 100644 --- a/packages/roosterjs-editor-dom/lib/list/setListItemStyle.ts +++ b/packages/roosterjs-editor-dom/lib/list/setListItemStyle.ts @@ -1,8 +1,6 @@ import ContentTraverser from '../contentTraverser/ContentTraverser'; import findClosestElementAncestor from '../utils/findClosestElementAncestor'; -import getStyles from '../style/getStyles'; import safeInstanceOf from '../utils/safeInstanceOf'; -import setStyles from '../style/setStyles'; import { InlineElement } from 'roosterjs-editor-types'; /** @@ -10,10 +8,14 @@ import { InlineElement } from 'roosterjs-editor-types'; * If the child inline elements have different styles, it will not modify the styles of the list item * @param element the LI Element to set the styles * @param styles The styles that should be applied to the element. + * @param isCssStyle True means the given styles are CSS style names, false means they are HTML attributes @default true */ -export default function setListItemStyle(element: HTMLLIElement, styles: string[]) { - const elementsStyles = getInlineChildElementsStyle(element); - let stylesToApply: Record = getStyles(element); +export default function setListItemStyle( + element: HTMLLIElement, + styles: string[], + isCssStyle: boolean = true +) { + const elementsStyles = getInlineChildElementsStyle(element, styles, isCssStyle); styles.forEach(styleName => { const styleValues = elementsStyles.map(style => @@ -25,28 +27,62 @@ export default function setListItemStyle(element: HTMLLIElement, styles: string[ (styleValues.length == 1 || new Set(styleValues).size == 1) && styleValues[0] ) { - stylesToApply[styleName] = styleValues[0]; + if (isCssStyle) { + element.style.setProperty(styleName, styleValues[0]); + } else { + element.setAttribute(styleName, styleValues[0]); + } } }); - setStyles(element, stylesToApply); } -function getInlineChildElementsStyle(element: HTMLElement) { +function getInlineChildElementsStyle(element: HTMLElement, styles: string[], isCssStyle: boolean) { const result: Record[] = []; const contentTraverser = ContentTraverser.createBodyTraverser(element); - let currentInlineElement: InlineElement; + let currentInlineElement: InlineElement | null = null; while (contentTraverser.currentInlineElement != currentInlineElement) { currentInlineElement = contentTraverser.currentInlineElement; - let currentNode = currentInlineElement.getContainerNode(); + let currentNode = currentInlineElement?.getContainerNode() || null; + let currentStyle: Record | null = null; + + currentNode = currentNode ? findClosestElementAncestor(currentNode) : null; + + // we should consider of when it is the single child node of element, the parentNode's style should add + // such as the "i", "b", "span" node in
  • aa
  • + while ( + currentNode && + currentNode !== element && + safeInstanceOf(currentNode, 'HTMLElement') && + (result.length == 0 || (currentNode.textContent?.trim().length || 0) > 0) + ) { + const element: HTMLElement = currentNode; + + styles.forEach(styleName => { + const styleValue = isCssStyle + ? element.style.getPropertyValue(styleName) + : element.getAttribute(styleName); - currentNode = findClosestElementAncestor(currentNode); - if (safeInstanceOf(currentNode, 'HTMLElement')) { - let childStyle = getStyles(currentNode); - if (childStyle) { - result.push(childStyle); + if (!currentStyle) { + currentStyle = {}; + } + + if (styleValue && !currentStyle[styleName]) { + currentStyle[styleName] = styleValue; + } + }); + + if (currentNode?.parentNode?.childNodes.length === 1) { + currentNode = currentNode.parentNode; + } else { + currentNode = null; } } + + if (currentStyle) { + result.push(currentStyle); + } + contentTraverser.getNextInlineElement(); } diff --git a/packages/roosterjs-editor-dom/lib/list/setNumberingListMarkers.ts b/packages/roosterjs-editor-dom/lib/list/setNumberingListMarkers.ts new file mode 100644 index 000000000000..b502c98203b1 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/list/setNumberingListMarkers.ts @@ -0,0 +1,142 @@ +import convertDecimalsToAlpha from './convertDecimalsToAlpha'; +import convertDecimalsToRoman from './convertDecimalsToRomans'; +import { NumberingListType } from 'roosterjs-editor-types'; +import type { CompatibleNumberingListType } from 'roosterjs-editor-types/lib/compatibleTypes'; + +interface MarkerStyle { + markerType: number; + markerSeparator: string; + markerSecondSeparator?: string; + lowerCase?: boolean; +} + +enum MarkerTypes { + Decimal, + Roman, + Alpha, +} + +/** + * @internal + * Set marker style of a numbering list + * @param listStyleType + * @param li + */ +export default function setNumberingListMarkers( + li: HTMLLIElement, + listStyleType: NumberingListType | CompatibleNumberingListType, + level: number +) { + const { markerSeparator, markerSecondSeparator, markerType, lowerCase } = numberingListStyle[ + listStyleType + ]; + + let markerNumber = level.toString(); + if (markerType === MarkerTypes.Roman) { + markerNumber = convertDecimalsToRoman(level, lowerCase); + } else if (markerType === MarkerTypes.Alpha) { + markerNumber = convertDecimalsToAlpha(level - 1, lowerCase); + } + + const marker = markerSecondSeparator + ? markerSecondSeparator + markerNumber + markerSeparator + : markerNumber + markerSeparator; + + li.style.listStyleType = `"${marker}"`; +} + +const numberingListStyle: Record = { + [NumberingListType.Decimal]: { + markerType: MarkerTypes.Decimal, + markerSeparator: '. ', + }, + [NumberingListType.DecimalDash]: { + markerType: MarkerTypes.Decimal, + markerSeparator: '- ', + }, + [NumberingListType.DecimalParenthesis]: { + markerType: MarkerTypes.Decimal, + markerSeparator: ') ', + }, + [NumberingListType.DecimalDoubleParenthesis]: { + markerType: MarkerTypes.Decimal, + markerSeparator: ') ', + markerSecondSeparator: '(', + }, + [NumberingListType.LowerAlpha]: { + markerType: MarkerTypes.Alpha, + markerSeparator: '. ', + lowerCase: true, + }, + [NumberingListType.LowerAlphaDash]: { + markerType: MarkerTypes.Alpha, + markerSeparator: '- ', + lowerCase: true, + }, + [NumberingListType.LowerAlphaParenthesis]: { + markerType: MarkerTypes.Alpha, + markerSeparator: ') ', + lowerCase: true, + }, + [NumberingListType.LowerAlphaDoubleParenthesis]: { + markerType: MarkerTypes.Alpha, + markerSeparator: ') ', + markerSecondSeparator: '(', + lowerCase: true, + }, + [NumberingListType.UpperAlpha]: { + markerType: MarkerTypes.Alpha, + markerSeparator: '. ', + }, + [NumberingListType.UpperAlphaDash]: { + markerType: MarkerTypes.Alpha, + markerSeparator: '- ', + }, + [NumberingListType.UpperAlphaParenthesis]: { + markerType: MarkerTypes.Alpha, + markerSeparator: ') ', + }, + [NumberingListType.UpperAlphaDoubleParenthesis]: { + markerType: MarkerTypes.Alpha, + markerSeparator: ') ', + markerSecondSeparator: '(', + }, + [NumberingListType.LowerRoman]: { + markerType: MarkerTypes.Roman, + markerSeparator: '. ', + lowerCase: true, + }, + [NumberingListType.LowerRomanDash]: { + markerType: MarkerTypes.Roman, + markerSeparator: '- ', + lowerCase: true, + }, + [NumberingListType.LowerRomanParenthesis]: { + markerType: MarkerTypes.Roman, + markerSeparator: ') ', + lowerCase: true, + }, + [NumberingListType.LowerRomanDoubleParenthesis]: { + markerType: MarkerTypes.Roman, + markerSeparator: ') ', + markerSecondSeparator: '(', + lowerCase: true, + }, + [NumberingListType.UpperRoman]: { + markerType: MarkerTypes.Roman, + markerSeparator: '. ', + }, + [NumberingListType.UpperRomanDash]: { + markerType: MarkerTypes.Roman, + markerSeparator: '- ', + }, + [NumberingListType.UpperRomanParenthesis]: { + markerType: MarkerTypes.Roman, + markerSeparator: ') ', + }, + [NumberingListType.UpperRomanDoubleParenthesis]: { + markerType: MarkerTypes.Roman, + markerSeparator: ') ', + markerSecondSeparator: '(', + }, +}; diff --git a/packages/roosterjs-editor-dom/lib/list/tsconfig.child.json b/packages/roosterjs-editor-dom/lib/list/tsconfig.child.json deleted file mode 100644 index 94f53e966aa2..000000000000 --- a/packages/roosterjs-editor-dom/lib/list/tsconfig.child.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "strict": false - }, - "extends": "../../../tsconfig.json", - "include": ["./**/*.ts"], - "references": [ - { "path": "../../../roosterjs-editor-types/tsconfig.child.json" }, - { "path": "../utils/tsconfig.child.json" }, - { "path": "../region/tsconfig.child.json" }, - { "path": "../selection/tsconfig.child.json" }, - { "path": "../contentTraverser/tsconfig.child.json" }, - { "path": "../style/tsconfig.child.json" } - ] -} diff --git a/packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts b/packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts new file mode 100644 index 000000000000..670ddc8586f3 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/metadata/definitionCreators.ts @@ -0,0 +1,120 @@ +import { + Definition, + DefinitionType, + NumberDefinition, + ArrayDefinition, + BooleanDefinition, + StringDefinition, + ObjectDefinition, + ObjectPropertyDefinition, +} from 'roosterjs-editor-types'; + +/** + * Create a number definition + * @param isOptional Whether this property is optional + * @param value Optional value of the number + * @param minValue Optional minimum value + * @param maxValue Optional maximum value + * @param allowNull Allow the property to be null + * @returns The number definition object + */ +export function createNumberDefinition( + isOptional?: boolean, + value?: number, + minValue?: number, + maxValue?: number, + allowNull?: boolean +): NumberDefinition { + return { + type: DefinitionType.Number, + isOptional, + value, + maxValue, + minValue, + allowNull, + }; +} + +/** + * Create a boolean definition + * @param isOptional Whether this property is optional + * @param value Optional expected boolean value + * @param allowNull Allow the property to be null + * @returns The boolean definition object + */ +export function createBooleanDefinition( + isOptional?: boolean, + value?: boolean, + allowNull?: boolean +): BooleanDefinition { + return { + type: DefinitionType.Boolean, + isOptional, + value, + allowNull, + }; +} + +/** + * Create a string definition + * @param isOptional Whether this property is optional + * @param value Optional expected string value + * @param allowNull Allow the property to be null + * @returns The string definition object + */ +export function createStringDefinition( + isOptional?: boolean, + value?: string, + allowNull?: boolean +): StringDefinition { + return { + type: DefinitionType.String, + isOptional, + value, + allowNull, + }; +} + +/** + * Create an array definition + * @param itemDef Definition of each item of the related array + * @param isOptional Whether this property is optional + * @param allowNull Allow the property to be null + * @returns The array definition object + */ +export function createArrayDefinition( + itemDef: Definition, + isOptional?: boolean, + minLength?: number, + maxLength?: number, + allowNull?: boolean +): ArrayDefinition { + return { + type: DefinitionType.Array, + isOptional, + itemDef, + minLength, + maxLength, + allowNull, + }; +} + +/** + * Create an object definition + * @param propertyDef Definition of each property of the related object + * @param isOptional Whether this property is optional + * @param allowNull Allow the property to be null + * @returns The object definition object + */ +export function createObjectDefinition( + propertyDef: ObjectPropertyDefinition, + isOptional?: boolean, + allowNull?: boolean +): ObjectDefinition { + return { + type: DefinitionType.Object, + isOptional, + propertyDef, + allowNull, + }; +} diff --git a/packages/roosterjs-editor-dom/lib/metadata/metadata.ts b/packages/roosterjs-editor-dom/lib/metadata/metadata.ts new file mode 100644 index 000000000000..fcb097fba158 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/metadata/metadata.ts @@ -0,0 +1,65 @@ +import validate from './validate'; +import { Definition } from 'roosterjs-editor-types'; + +const MetadataDataSetName = 'editingInfo'; + +/** + * Get metadata object from an HTML element + * @param element The HTML element to get metadata object from + * @param definition The type definition of this metadata used for validate this metadata object. + * If not specified, no validation will be performed and always return whatever we get from the element + * @param defaultValue The default value to return if the retrieved object cannot pass the validation, + * or there is no metadata object at all + * @returns The strong-type metadata object if it can be validated, or null + */ +export function getMetadata( + element: HTMLElement, + definition?: Definition, + defaultValue?: T +): T | null { + const str = element.dataset[MetadataDataSetName]; + let obj: any; + + try { + obj = str ? JSON.parse(str) : null; + } catch {} + + if (typeof obj !== 'undefined') { + if (!definition) { + return obj as T; + } else if (validate(obj, definition)) { + return obj; + } + } + + if (defaultValue) { + return defaultValue; + } else { + return null; + } +} + +/** + * Set metadata object into an HTML element + * @param element The HTML element to set metadata object to + * @param metadata The metadata object to set + * @param def An optional type definition object used for validate this metadata object. + * If not specified, metadata will be set without validation + * @returns True if metadata is set, otherwise false + */ +export function setMetadata(element: HTMLElement, metadata: T, def?: Definition): boolean { + if (!def || validate(metadata, def)) { + element.dataset[MetadataDataSetName] = JSON.stringify(metadata); + return true; + } else { + return false; + } +} + +/** + * Remove metadata from the given element if any + * @param element The element to remove metadata from + */ +export function removeMetadata(element: HTMLElement) { + delete element.dataset[MetadataDataSetName]; +} diff --git a/packages/roosterjs-editor-dom/lib/metadata/validate.ts b/packages/roosterjs-editor-dom/lib/metadata/validate.ts new file mode 100644 index 000000000000..cabec16e1302 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/metadata/validate.ts @@ -0,0 +1,68 @@ +import getObjectKeys from '../jsUtils/getObjectKeys'; +import { Definition, DefinitionType } from 'roosterjs-editor-types'; + +/** + * Validate the given object with a type definition object + * @param input The object to validate + * @param def The type definition object used for validation + * @returns True if the object passed the validation, otherwise false + */ +export default function validate(input: any, def: Definition): input is T { + let result = false; + if ((def.isOptional && typeof input === 'undefined') || (def.allowNull && input === null)) { + result = true; + } else if ( + (!def.isOptional && typeof input === 'undefined') || + (!def.allowNull && input === null) + ) { + return false; + } else { + switch (def.type) { + case DefinitionType.String: + result = + typeof input === 'string' && + (typeof def.value === 'undefined' || input === def.value); + break; + + case DefinitionType.Number: + result = + typeof input === 'number' && + (typeof def.value === 'undefined' || areSameNumbers(def.value, input)) && + (typeof def.minValue === 'undefined' || input >= def.minValue) && + (typeof def.maxValue === 'undefined' || input <= def.maxValue); + break; + + case DefinitionType.Boolean: + result = + typeof input === 'boolean' && + (typeof def.value === 'undefined' || input === def.value); + break; + + case DefinitionType.Array: + result = + Array.isArray(input) && + (typeof def.minLength === 'undefined' || input.length >= def.minLength) && + (typeof def.maxLength === 'undefined' || input.length <= def.maxLength) && + input.every(x => validate(x, def.itemDef)); + break; + + case DefinitionType.Object: + result = + typeof input === 'object' && + getObjectKeys(def.propertyDef).every(x => + validate(input[x], def.propertyDef[x]) + ); + break; + + case DefinitionType.Customize: + result = def.validator(input); + break; + } + } + + return result; +} + +function areSameNumbers(n1: number, n2: number) { + return Math.abs(n1 - n2) < 1e-3; +} diff --git a/packages/roosterjs-editor-dom/lib/pasteSourceValidations/constants.ts b/packages/roosterjs-editor-dom/lib/pasteSourceValidations/constants.ts new file mode 100644 index 000000000000..75e562d0a910 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/pasteSourceValidations/constants.ts @@ -0,0 +1,15 @@ +/** + * @internal + * Node attribute used to identify if the content is from Google Sheets. + */ +export const GOOGLE_SHEET_NODE_NAME = 'google-sheets-html-origin'; +/** + * @internal + * Name of the HTMLMeta Property that provides the Office App Source of the pasted content + */ +export const PROG_ID_NAME = 'ProgId'; +/** + * @internal + * Name of the HTMLMeta Property that identifies pated content as from Excel Desktop + */ +export const EXCEL_DESKTOP_ATTRIBUTE_NAME = 'xmlns:x'; diff --git a/packages/roosterjs-editor-dom/lib/pasteSourceValidations/documentContainWacElements.ts b/packages/roosterjs-editor-dom/lib/pasteSourceValidations/documentContainWacElements.ts new file mode 100644 index 000000000000..04bdc411d1f1 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/pasteSourceValidations/documentContainWacElements.ts @@ -0,0 +1,16 @@ +import type { getSourceFunction, getSourceInputParams } from './getPasteSource'; + +const WAC_IDENTIFY_SELECTOR = + 'ul[class^="BulletListStyle"]>.OutlineElement,ol[class^="NumberListStyle"]>.OutlineElement,span.WACImageContainer'; + +/** + * @internal + * Check whether the fragment provided contain Wac Elements + * @param props Properties related to the PasteEvent + * @returns + */ +const documentContainWacElements: getSourceFunction = (props: getSourceInputParams) => { + const { fragment } = props; + return !!fragment.querySelector(WAC_IDENTIFY_SELECTOR); +}; +export default documentContainWacElements; diff --git a/packages/roosterjs-editor-dom/lib/pasteSourceValidations/getPasteSource.ts b/packages/roosterjs-editor-dom/lib/pasteSourceValidations/getPasteSource.ts new file mode 100644 index 000000000000..3fd0a18ee05d --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/pasteSourceValidations/getPasteSource.ts @@ -0,0 +1,62 @@ +import documentContainWacElements from './documentContainWacElements'; +import isExcelDesktopDocument from './isExcelDesktopDocument'; +import isExcelOnlineDocument from './isExcelOnlineDocument'; +import isGoogleSheetDocument from './isGoogleSheetDocument'; +import isPowerPointDesktopDocument from './isPowerPointDesktopDocument'; +import isWordDesktopDocument from './isWordDesktopDocument'; +import shouldConvertToSingleImage from './shouldConvertToSingleImage'; +import { BeforePasteEvent, ClipboardData, KnownPasteSourceType } from 'roosterjs-editor-types'; + +/** + * @internal + */ +export type getSourceInputParams = { + htmlAttributes: Record; + fragment: DocumentFragment; + shouldConvertSingleImage: boolean; + clipboardData: ClipboardData; +}; + +/** + * @internal + */ +export type getSourceFunction = (props: getSourceInputParams) => boolean; + +const getSourceFunctions = new Map([ + [KnownPasteSourceType.WordDesktop, isWordDesktopDocument], + [KnownPasteSourceType.ExcelDesktop, isExcelDesktopDocument], + [KnownPasteSourceType.ExcelOnline, isExcelOnlineDocument], + [KnownPasteSourceType.PowerPointDesktop, isPowerPointDesktopDocument], + [KnownPasteSourceType.WacComponents, documentContainWacElements], + [KnownPasteSourceType.GoogleSheets, isGoogleSheetDocument], + [KnownPasteSourceType.SingleImage, shouldConvertToSingleImage], +]); + +/** + * This function tries to get the source of the Pasted content + * @param event the before paste event + * @param shouldConvertSingleImage Whether convert single image is enabled. + * @returns The Type of pasted content, if no type found will return {KnownSourceType.Default} + */ +export default function getPasteSource( + event: BeforePasteEvent, + shouldConvertSingleImage: boolean +): KnownPasteSourceType { + const { htmlAttributes, clipboardData, fragment } = event; + + let result: KnownPasteSourceType | null = null; + const param: getSourceInputParams = { + htmlAttributes, + fragment, + shouldConvertSingleImage, + clipboardData, + }; + + getSourceFunctions.forEach((func, key) => { + if (!result && func(param)) { + result = key; + } + }); + + return result ?? KnownPasteSourceType.Default; +} diff --git a/packages/roosterjs-editor-dom/lib/pasteSourceValidations/isExcelDesktopDocument.ts b/packages/roosterjs-editor-dom/lib/pasteSourceValidations/isExcelDesktopDocument.ts new file mode 100644 index 000000000000..6c30d67e9b43 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/pasteSourceValidations/isExcelDesktopDocument.ts @@ -0,0 +1,17 @@ +import { EXCEL_DESKTOP_ATTRIBUTE_NAME } from './constants'; +import type { getSourceFunction, getSourceInputParams } from './getPasteSource'; + +const EXCEL_ATTRIBUTE_VALUE = 'urn:schemas-microsoft-com:office:excel'; + +/** + * @internal + * Checks whether the Array provided contains strings that identify Excel Desktop documents + * @param props Properties related to the PasteEvent + * @returns + */ +const isExcelDesktopDocument: getSourceFunction = (props: getSourceInputParams) => { + const { htmlAttributes } = props; + // The presence of this attribute confirms its origin from Excel Desktop + return htmlAttributes[EXCEL_DESKTOP_ATTRIBUTE_NAME] == EXCEL_ATTRIBUTE_VALUE; +}; +export default isExcelDesktopDocument; diff --git a/packages/roosterjs-editor-dom/lib/pasteSourceValidations/isExcelOnlineDocument.ts b/packages/roosterjs-editor-dom/lib/pasteSourceValidations/isExcelOnlineDocument.ts new file mode 100644 index 000000000000..35fc9711e381 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/pasteSourceValidations/isExcelOnlineDocument.ts @@ -0,0 +1,21 @@ +import { EXCEL_DESKTOP_ATTRIBUTE_NAME, PROG_ID_NAME } from './constants'; +import type { getSourceFunction, getSourceInputParams } from './getPasteSource'; + +// Excel Desktop also has this attribute +const EXCEL_ONLINE_ATTRIBUTE_VALUE = 'Excel.Sheet'; + +/** + * @internal + * Checks whether the Array provided contains strings that identify Excel Online documents + * @param props Properties related to the PasteEvent + * @returns + */ +const isExcelOnlineDocument: getSourceFunction = (props: getSourceInputParams) => { + const { htmlAttributes } = props; + // The presence of Excel.Sheet confirms its origin from Excel, the absence of EXCEL_DESKTOP_ATTRIBUTE_NAME confirms it is from the Online version + return ( + htmlAttributes[PROG_ID_NAME] == EXCEL_ONLINE_ATTRIBUTE_VALUE && + htmlAttributes[EXCEL_DESKTOP_ATTRIBUTE_NAME] == undefined + ); +}; +export default isExcelOnlineDocument; diff --git a/packages/roosterjs-editor-dom/lib/pasteSourceValidations/isGoogleSheetDocument.ts b/packages/roosterjs-editor-dom/lib/pasteSourceValidations/isGoogleSheetDocument.ts new file mode 100644 index 000000000000..cfe76ebf3138 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/pasteSourceValidations/isGoogleSheetDocument.ts @@ -0,0 +1,15 @@ +import { GOOGLE_SHEET_NODE_NAME } from './constants'; +import type { getSourceFunction, getSourceInputParams } from './getPasteSource'; + +/** + * @internal + * Checks whether the fragment provided contain elements from Google sheets + * @param props Properties related to the PasteEvent + * @returns + */ +const isGoogleSheetDocument: getSourceFunction = (props: getSourceInputParams) => { + const { fragment } = props; + return !!fragment.querySelector(GOOGLE_SHEET_NODE_NAME); +}; + +export default isGoogleSheetDocument; diff --git a/packages/roosterjs-editor-dom/lib/pasteSourceValidations/isPowerPointDesktopDocument.ts b/packages/roosterjs-editor-dom/lib/pasteSourceValidations/isPowerPointDesktopDocument.ts new file mode 100644 index 000000000000..116eade11e7a --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/pasteSourceValidations/isPowerPointDesktopDocument.ts @@ -0,0 +1,15 @@ +import { PROG_ID_NAME } from './constants'; +import type { getSourceFunction, getSourceInputParams } from './getPasteSource'; + +const POWERPOINT_ATTRIBUTE_VALUE = 'PowerPoint.Slide'; + +/** + * @internal + * Checks whether the Array provided contains strings that identify Power Point Desktop documents + * @param props Properties related to the PasteEvent + * @returns + */ +const isPowerPointDesktopDocument: getSourceFunction = (props: getSourceInputParams) => { + return props.htmlAttributes[PROG_ID_NAME] == POWERPOINT_ATTRIBUTE_VALUE; +}; +export default isPowerPointDesktopDocument; diff --git a/packages/roosterjs-editor-dom/lib/pasteSourceValidations/isWordDesktopDocument.ts b/packages/roosterjs-editor-dom/lib/pasteSourceValidations/isWordDesktopDocument.ts new file mode 100644 index 000000000000..6e6af33ac811 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/pasteSourceValidations/isWordDesktopDocument.ts @@ -0,0 +1,22 @@ +import { PROG_ID_NAME } from './constants'; +import type { getSourceFunction, getSourceInputParams } from './getPasteSource'; + +const WORD_ATTRIBUTE_NAME = 'xmlns:w'; +const WORD_ATTRIBUTE_VALUE = 'urn:schemas-microsoft-com:office:word'; +const WORD_PROG_ID = 'Word.Document'; + +/** + * @internal + * Checks whether the Array provided contains strings that identify Word Desktop documents + * @param props Properties related to the PasteEvent + * @returns + */ +const isWordDesktopDocument: getSourceFunction = (props: getSourceInputParams) => { + const { htmlAttributes } = props; + return ( + htmlAttributes[WORD_ATTRIBUTE_NAME] == WORD_ATTRIBUTE_VALUE || + htmlAttributes[PROG_ID_NAME] == WORD_PROG_ID + ); +}; + +export default isWordDesktopDocument; diff --git a/packages/roosterjs-editor-dom/lib/pasteSourceValidations/shouldConvertToSingleImage.ts b/packages/roosterjs-editor-dom/lib/pasteSourceValidations/shouldConvertToSingleImage.ts new file mode 100644 index 000000000000..f608bce7532b --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/pasteSourceValidations/shouldConvertToSingleImage.ts @@ -0,0 +1,19 @@ +import type { getSourceFunction, getSourceInputParams } from './getPasteSource'; + +/** + * @internal + * Checks whether the fragment only contains a single image to paste + * and the editor have the ConvertSingleImageBody Experimental feature + * @param props Properties related to the PasteEvent + * @returns + */ +const shouldConvertToSingleImage: getSourceFunction = (props: getSourceInputParams) => { + const { shouldConvertSingleImage, clipboardData } = props; + return ( + shouldConvertSingleImage && + clipboardData.htmlFirstLevelChildTags?.length == 1 && + clipboardData.htmlFirstLevelChildTags[0] == 'IMG' + ); +}; + +export default shouldConvertToSingleImage; diff --git a/packages/roosterjs-editor-dom/lib/region/getRegionsFromRange.ts b/packages/roosterjs-editor-dom/lib/region/getRegionsFromRange.ts index 8b64c9da4886..51316a7614a5 100644 --- a/packages/roosterjs-editor-dom/lib/region/getRegionsFromRange.ts +++ b/packages/roosterjs-editor-dom/lib/region/getRegionsFromRange.ts @@ -4,6 +4,7 @@ import Position from '../selection/Position'; import queryElements from '../utils/queryElements'; import { getNextLeafSibling, getPreviousLeafSibling } from '../utils/getLeafSibling'; import { QueryScope, Region, RegionType } from 'roosterjs-editor-types'; +import type { CompatibleRegionType } from 'roosterjs-editor-types/lib/compatibleTypes'; interface RegionTypeData { /** @@ -40,7 +41,7 @@ const regionTypeData: Record = { export default function getRegionsFromRange( root: HTMLElement, range: Range, - type: RegionType + type: RegionType | CompatibleRegionType ): Region[] { let regions: Region[] = []; if (root && range) { @@ -61,7 +62,7 @@ export default function getRegionsFromRange( export function getRegionCreator( fullRange: Range, skipTags: string[] -): (rootNode: HTMLElement, nodeBefore?: Node, nodeAfter?: Node) => Region { +): (rootNode: HTMLElement, nodeBefore?: Node, nodeAfter?: Node) => Region | null { const fullSelectionStart = Position.getStart(fullRange).normalize(); const fullSelectionEnd = Position.getEnd(fullRange).normalize(); return (rootNode: HTMLElement, nodeBefore?: Node, nodeAfter?: Node) => { @@ -110,7 +111,11 @@ interface Boundary { * @param range Existing selected full range * @param type Type of region to create */ -function buildBoundaryTree(root: HTMLElement, range: Range, type: RegionType): Boundary { +function buildBoundaryTree( + root: HTMLElement, + range: Range, + type: RegionType | CompatibleRegionType +): Boundary { const allBoundaries: Boundary[] = [{ innerNode: root, children: [] }]; const { outerSelector, innerSelector } = regionTypeData[type]; const inSelectionOuterNode = queryElements( @@ -166,7 +171,7 @@ function buildBoundaryTree(root: HTMLElement, range: Range, type: RegionType): B * @param started Whether we have already hit the start node */ function iterateNodes( - creator: (rootNode: HTMLElement, nodeBefore?: Node, nodeAfter?: Node) => Region, + creator: (rootNode: HTMLElement, nodeBefore?: Node, nodeAfter?: Node) => Region | null, boundary: Boundary, start: Node, end: Node, @@ -178,14 +183,20 @@ function iterateNodes( let regions: Region[] = []; if (children.length == 0) { - regions.push(creator(innerNode)); + const region = creator(innerNode); + if (region) { + regions.push(region); + } } else { // Need to run one more time to add region after all children for (let i = 0; i <= children.length && !ended; i++) { const { outerNode, boundaries } = children[i] || {}; const previousOuterNode = children[i - 1]?.outerNode; if (started) { - regions.push(creator(innerNode, previousOuterNode, outerNode)); + const region = creator(innerNode, previousOuterNode, outerNode); + if (region) { + regions.push(region); + } } boundaries?.forEach(child => { @@ -211,7 +222,12 @@ function iterateNodes( * @param nodeAfter The boundary node after the region under root * @param skipTags Tags to skip */ -function areNodesValid(root: Node, nodeBefore: Node, nodeAfter: Node, skipTags: string[]) { +function areNodesValid( + root: Node, + nodeBefore: Node | undefined, + nodeAfter: Node | undefined, + skipTags: string[] +) { if (!root) { return false; } else { diff --git a/packages/roosterjs-editor-dom/lib/region/getSelectedBlockElementsInRegion.ts b/packages/roosterjs-editor-dom/lib/region/getSelectedBlockElementsInRegion.ts index d980624ed851..3ddd778e1fa9 100644 --- a/packages/roosterjs-editor-dom/lib/region/getSelectedBlockElementsInRegion.ts +++ b/packages/roosterjs-editor-dom/lib/region/getSelectedBlockElementsInRegion.ts @@ -10,10 +10,12 @@ import { BlockElement, KnownCreateElementDataIndex, RegionBase } from 'roosterjs * @param regionBase The region to get block elements from * @param createBlockIfEmpty When set to true, a new empty block element will be created if there is not * any blocks in the region. Default value is false + * @param deprecated Deprecated parameter, not used */ export default function getSelectedBlockElementsInRegion( regionBase: RegionBase, - createBlockIfEmpty?: boolean + createBlockIfEmpty?: boolean, + deprecated?: boolean ): BlockElement[] { const range = getSelectionRangeInRegion(regionBase); let blocks: BlockElement[] = []; @@ -49,8 +51,13 @@ export default function getSelectedBlockElementsInRegion( KnownCreateElementDataIndex.EmptyLine, regionBase.rootNode.ownerDocument ); - regionBase.rootNode.appendChild(newNode); - blocks.push(getBlockElementAtNode(regionBase.rootNode, newNode)); + regionBase.rootNode.appendChild(newNode!); + + const block = getBlockElementAtNode(regionBase.rootNode, newNode); + + if (block) { + blocks.push(block); + } } return blocks; diff --git a/packages/roosterjs-editor-dom/lib/region/getSelectionRangeInRegion.ts b/packages/roosterjs-editor-dom/lib/region/getSelectionRangeInRegion.ts index ac54a2716cd0..f7e4db47a40a 100644 --- a/packages/roosterjs-editor-dom/lib/region/getSelectionRangeInRegion.ts +++ b/packages/roosterjs-editor-dom/lib/region/getSelectionRangeInRegion.ts @@ -37,10 +37,10 @@ export default function getSelectionRangeInRegion(regionBase: RegionBase): Range const end = fullSelectionEnd.isAfter(regionEnd) ? regionEnd : fullSelectionEnd; return createRange(start, end); - } else { - return null; } } + + return null; } function isRegion(regionBase: RegionBase): regionBase is Region { diff --git a/packages/roosterjs-editor-dom/lib/region/mergeBlocksInRegion.ts b/packages/roosterjs-editor-dom/lib/region/mergeBlocksInRegion.ts index c066ab67c025..fcfca254ce0f 100644 --- a/packages/roosterjs-editor-dom/lib/region/mergeBlocksInRegion.ts +++ b/packages/roosterjs-editor-dom/lib/region/mergeBlocksInRegion.ts @@ -16,7 +16,7 @@ import { collapse } from '../utils/collapseNodes'; * @param targetNode The node of target block element */ export default function mergeBlocksInRegion(region: RegionBase, refNode: Node, targetNode: Node) { - let block: BlockElement; + let block: BlockElement | null; if ( !isNodeInRegion(region, refNode) || @@ -37,8 +37,8 @@ export default function mergeBlocksInRegion(region: RegionBase, refNode: Node, t ); // Copy styles of parent nodes into blockRoot - for (let node: Node = blockRoot; contains(commonContainer, node); ) { - const parent = node.parentNode; + for (let node: Node | null = blockRoot; contains(commonContainer, node); ) { + const parent: Node | null = node!.parentNode; if (safeInstanceOf(parent, 'HTMLElement')) { const styles = { ...(getPredefinedCssForElement(parent) || {}), @@ -50,17 +50,17 @@ export default function mergeBlocksInRegion(region: RegionBase, refNode: Node, t node = parent; } - let nodeToRemove: Node = null; + let nodeToRemove: Node | null = null; let nodeToMerge = blockRoot.childNodes.length == 1 && blockRoot.attributes.length == 0 - ? blockRoot.firstChild - : changeElementTag(blockRoot, 'SPAN'); + ? blockRoot.firstChild! + : changeElementTag(blockRoot, 'SPAN')!; // Remove empty node for ( - let node: Node = nodeToMerge; - contains(commonContainer, node) && node.parentNode.childNodes.length == 1; - node = node.parentNode + let node: Node | null = nodeToMerge; + contains(commonContainer, node) && node.parentNode?.childNodes.length == 1; + node = node!.parentNode ) { // If the only child is the one which is about to be removed, this node should also be removed nodeToRemove = node.parentNode; diff --git a/packages/roosterjs-editor-dom/lib/region/tsconfig.child.json b/packages/roosterjs-editor-dom/lib/region/tsconfig.child.json deleted file mode 100644 index a4de41f96de3..000000000000 --- a/packages/roosterjs-editor-dom/lib/region/tsconfig.child.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "strict": false - }, - "extends": "../../../tsconfig.json", - "include": ["./**/*.ts"], - "references": [ - { "path": "../../../roosterjs-editor-types/tsconfig.child.json" }, - { "path": "../utils/tsconfig.child.json" }, - { "path": "../selection/tsconfig.child.json" }, - { "path": "../contentTraverser/tsconfig.child.json" }, - { "path": "../blockElements/tsconfig.child.json" }, - { "path": "../htmlSanitizer/tsconfig.child.json" }, - { "path": "../style/tsconfig.child.json" } - ] -} diff --git a/packages/roosterjs-editor-dom/lib/selection/Position.ts b/packages/roosterjs-editor-dom/lib/selection/Position.ts index c6826bf97e91..3a436fb20918 100644 --- a/packages/roosterjs-editor-dom/lib/selection/Position.ts +++ b/packages/roosterjs-editor-dom/lib/selection/Position.ts @@ -1,6 +1,7 @@ import findClosestElementAncestor from '../utils/findClosestElementAncestor'; import isNodeAfter from '../utils/isNodeAfter'; import { NodePosition, NodeType, PositionType } from 'roosterjs-editor-types'; +import type { CompatiblePositionType } from 'roosterjs-editor-types/lib/compatibleTypes'; /** * Represent a position in DOM tree by the node and its offset index @@ -33,7 +34,7 @@ export default class Position implements NodePosition { * @param node The node of this position * @param positionType Type of the position, can be Begin, End, Before, After */ - constructor(node: Node, positionType: PositionType); + constructor(node: Node, positionType: PositionType | CompatiblePositionType); constructor( nodeOrPosition: Node | NodePosition, @@ -173,7 +174,7 @@ function getIndexOfNode(node: Node | null): number { function getEndOffset(node: Node): number { if (node.nodeType == NodeType.Text) { return node.nodeValue?.length || 0; - } else if (node.nodeType == NodeType.Element) { + } else if (node.nodeType == NodeType.Element || node.nodeType == NodeType.DocumentFragment) { return node.childNodes.length; } else { return 1; diff --git a/packages/roosterjs-editor-dom/lib/selection/getHtmlWithSelectionPath.ts b/packages/roosterjs-editor-dom/lib/selection/getHtmlWithSelectionPath.ts index ad0a27e3a3dd..3dc49406317f 100644 --- a/packages/roosterjs-editor-dom/lib/selection/getHtmlWithSelectionPath.ts +++ b/packages/roosterjs-editor-dom/lib/selection/getHtmlWithSelectionPath.ts @@ -1,7 +1,5 @@ import getInnerHTML from '../utils/getInnerHTML'; import getSelectionPath from './getSelectionPath'; -import getTagOfNode from '../utils/getTagOfNode'; -import queryElements from '../utils/queryElements'; /** * Get inner Html of a root node with a selection path which can be used for restore selection. @@ -12,42 +10,12 @@ import queryElements from '../utils/queryElements'; */ export default function getHtmlWithSelectionPath( rootNode: HTMLElement | DocumentFragment, - range: Range + range: Range | null ): string { if (!rootNode) { return ''; } - const { startContainer, endContainer, startOffset, endOffset } = range || {}; - let isDOMChanged = false; - - queryElements(rootNode, 'table', table => { - let tbody: HTMLTableSectionElement | null = null; - - for (let child = table.firstChild; child; child = child.nextSibling) { - if (getTagOfNode(child) == 'TR') { - if (!tbody) { - tbody = table.ownerDocument.createElement('tbody'); - table.insertBefore(tbody, child); - } - - tbody.appendChild(child); - child = tbody; - - isDOMChanged = true; - } else { - tbody = null; - } - } - }); - - if (range && isDOMChanged) { - try { - range.setStart(startContainer, startOffset); - range.setEnd(endContainer, endOffset); - } catch {} - } - const content = getInnerHTML(rootNode); const selectionPath = range && getSelectionPath(rootNode, range); diff --git a/packages/roosterjs-editor-dom/lib/selection/isPositionAtBeginningOf.ts b/packages/roosterjs-editor-dom/lib/selection/isPositionAtBeginningOf.ts index 290f001bd0b5..c9779984e07f 100644 --- a/packages/roosterjs-editor-dom/lib/selection/isPositionAtBeginningOf.ts +++ b/packages/roosterjs-editor-dom/lib/selection/isPositionAtBeginningOf.ts @@ -10,7 +10,7 @@ import { NodePosition } from 'roosterjs-editor-types'; * @param targetNode The node to check * @returns True if position is at beginning of the node, otherwise false */ -export default function isPositionAtBeginningOf(position: NodePosition, targetNode: Node) { +export default function isPositionAtBeginningOf(position: NodePosition, targetNode: Node | null) { if (position) { position = position.normalize(); let node: Node | null = position.node; diff --git a/packages/roosterjs-editor-dom/lib/selection/setHtmlWithSelectionPath.ts b/packages/roosterjs-editor-dom/lib/selection/setHtmlWithSelectionPath.ts index e1b14bb5aff8..3d37ceedabc1 100644 --- a/packages/roosterjs-editor-dom/lib/selection/setHtmlWithSelectionPath.ts +++ b/packages/roosterjs-editor-dom/lib/selection/setHtmlWithSelectionPath.ts @@ -1,13 +1,59 @@ import createRange from './createRange'; -import { NodeType, SelectionPath, TrustedHTMLHandler } from 'roosterjs-editor-types'; +import safeInstanceOf from '../utils/safeInstanceOf'; +import validate from '../metadata/validate'; +import { + createArrayDefinition, + createBooleanDefinition, + createNumberDefinition, + createObjectDefinition, + createStringDefinition, +} from '../metadata/definitionCreators'; +import { + ContentMetadata, + SelectionRangeTypes, + TrustedHTMLHandler, + ImageContentMetadata, + NormalContentMetadata, + TableContentMetadata, + Coordinates, +} from 'roosterjs-editor-types'; +const NumberArrayDefinition = createArrayDefinition(createNumberDefinition()); -const LastCommentRegex = /$/; +const CoordinatesDefinition = createObjectDefinition({ + x: createNumberDefinition(), + y: createNumberDefinition(), +}); + +const IsDarkModeDefinition = createBooleanDefinition(true /*isOptional*/); + +const NormalContentMetadataDefinition = createObjectDefinition({ + type: createNumberDefinition(true /*isOptional*/, SelectionRangeTypes.Normal), + isDarkMode: IsDarkModeDefinition, + start: NumberArrayDefinition, + end: NumberArrayDefinition, +}); + +const TableContentMetadataDefinition = createObjectDefinition({ + type: createNumberDefinition(false /*isOptional*/, SelectionRangeTypes.TableSelection), + isDarkMode: IsDarkModeDefinition, + tableId: createStringDefinition(), + firstCell: CoordinatesDefinition, + lastCell: CoordinatesDefinition, +}); + +const ImageContentMetadataDefinition = createObjectDefinition({ + type: createNumberDefinition(false /*isOptional*/, SelectionRangeTypes.ImageSelection), + isDarkMode: IsDarkModeDefinition, + imageId: createStringDefinition(), +}); /** - * Restore inner Html of a root element from given html string. If the string contains selection path, + * @deprecated Use setHtmlWithMetadata instead + * Restore inner HTML of a root element from given html string. If the string contains selection path, * remove the selection path and return a range represented by the path * @param root The root element - * @param html The html to restore + * @param html The HTML to restore + * @param trustedHTMLHandler An optional trusted HTML handler to convert HTML string to security string * @returns A selection range if the html contains a valid selection path, otherwise null */ export default function setHtmlWithSelectionPath( @@ -15,41 +61,60 @@ export default function setHtmlWithSelectionPath( html: string, trustedHTMLHandler?: TrustedHTMLHandler ): Range | null { + const metadata = setHtmlWithMetadata(rootNode, html, trustedHTMLHandler); + return metadata?.type == SelectionRangeTypes.Normal + ? createRange(rootNode, metadata.start, metadata.end) + : null; +} + +/** + * Restore inner HTML of a root element from given html string. If the string contains metadata, + * remove it from DOM tree and return the metadata + * @param root The root element + * @param html The HTML to restore + * @param trustedHTMLHandler An optional trusted HTML handler to convert HTML string to security string + * @returns Content metadata if any, or undefined + */ +export function setHtmlWithMetadata( + rootNode: HTMLElement, + html: string, + trustedHTMLHandler?: TrustedHTMLHandler +): ContentMetadata | undefined { if (!rootNode) { - return null; + return undefined; } html = html || ''; - const lastComment = LastCommentRegex.exec(html); rootNode.innerHTML = trustedHTMLHandler?.(html) || html; - const path = getSelectionPath(rootNode, lastComment?.[1] || ''); - return path && createRange(rootNode, path.start, path.end); + return extractContentMetadata(rootNode); } -function getSelectionPath(root: HTMLElement, alternativeComment: string): SelectionPath | null { - let pathCommentValue: string = ''; - let pathCommentNode: Node | null = null; - let path: SelectionPath | null = null; - if (root.lastChild?.nodeType == NodeType.Comment) { - pathCommentNode = root.lastChild; - pathCommentValue = pathCommentNode.nodeValue || ''; - } else { - pathCommentValue = alternativeComment; - } +/** + * Extract content metadata from DOM tree + * @param rootNode Root of the DOM tree + * @returns If there is a valid content metadata node in the give DOM tree, return this metadata object, otherwise undefined + */ +export function extractContentMetadata(rootNode: HTMLElement): ContentMetadata | undefined { + const potentialMetadataComment = rootNode.lastChild; - if (pathCommentValue) { + if (safeInstanceOf(potentialMetadataComment, 'Comment')) { try { - path = JSON.parse(pathCommentValue) as SelectionPath; - if (path && path.start?.length > 0 && path.end?.length > 0) { - if (pathCommentNode) { - root.removeChild(pathCommentNode); - } - } else { - path = null; + const obj = JSON.parse(potentialMetadataComment.nodeValue || ''); + + if ( + validate(obj, NormalContentMetadataDefinition) || + validate(obj, TableContentMetadataDefinition) || + validate(obj, ImageContentMetadataDefinition) + ) { + rootNode.removeChild(potentialMetadataComment); + obj.type = typeof obj.type === 'undefined' ? SelectionRangeTypes.Normal : obj.type; + obj.isDarkMode = obj.isDarkMode || false; + + return obj; } } catch {} } - return path; + return undefined; } diff --git a/packages/roosterjs-editor-dom/lib/selection/tsconfig.child.json b/packages/roosterjs-editor-dom/lib/selection/tsconfig.child.json deleted file mode 100644 index 02536a6dd3d1..000000000000 --- a/packages/roosterjs-editor-dom/lib/selection/tsconfig.child.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "strict": true - }, - "extends": "../../../tsconfig.json", - "include": ["./**/*.ts"], - "references": [ - { "path": "../../../roosterjs-editor-types/tsconfig.child.json" }, - { "path": "../utils/tsconfig.child.json" } - ] -} diff --git a/packages/roosterjs-editor-dom/lib/snapshots/addSnapshot.ts b/packages/roosterjs-editor-dom/lib/snapshots/addSnapshot.ts index 0712c9846812..98e1b2a30407 100644 --- a/packages/roosterjs-editor-dom/lib/snapshots/addSnapshot.ts +++ b/packages/roosterjs-editor-dom/lib/snapshots/addSnapshot.ts @@ -1,29 +1,59 @@ import clearProceedingSnapshots from './clearProceedingSnapshots'; -import { Snapshots } from 'roosterjs-editor-types'; +import { Snapshot, Snapshots } from 'roosterjs-editor-types'; /** * Add a new snapshot to the given snapshots data structure * @param snapshots The snapshots data structure to add new snapshot into - * @param snapshot The snapshot to add + * @param html The snapshot HTML to add * @param isAutoCompleteSnapshot Whether this is a snapshot before auto complete action */ export default function addSnapshot( - snapshots: Snapshots, - snapshot: string, + snapshots: Snapshots, + html: string, isAutoCompleteSnapshot: boolean +): void; + +/** + * Add a new snapshot to the given snapshots data structure + * @param snapshots The snapshots data structure to add new snapshot into + * @param snapshot The generic snapshot object to add + * @param isAutoCompleteSnapshot Whether this is a snapshot before auto complete action + * @param getLength A callback function to calculate length of the snapshot + * @param isSame A callback function to check if the given snapshots are the same + */ +export default function addSnapshot( + snapshots: Snapshots, + snapshot: T, + isAutoCompleteSnapshot: boolean, + getLength: (snapshot: T) => number, + isSame: (snapshot1: T, snapshot2: T) => boolean +): void; + +export default function addSnapshot( + snapshots: Snapshots, + snapshot: T, + isAutoCompleteSnapshot: boolean, + getLength?: (snapshot: T) => number, + compare?: (snapshot1: T, snapshot2: T) => boolean ) { - if (snapshots.currentIndex < 0 || snapshot != snapshots.snapshots[snapshots.currentIndex]) { - clearProceedingSnapshots(snapshots); + getLength = getLength || (str => ((str))?.length || 0); + compare = compare || defaultCompare; + + const currentSnapshot = snapshots.snapshots[snapshots.currentIndex]; + const isSameSnapshot = currentSnapshot && compare(currentSnapshot, snapshot); + + if (snapshots.currentIndex < 0 || !currentSnapshot || !isSameSnapshot) { + clearProceedingSnapshots(snapshots, getLength); snapshots.snapshots.push(snapshot); snapshots.currentIndex++; - snapshots.totalSize += snapshot.length; + snapshots.totalSize += getLength(snapshot); let removeCount = 0; while ( removeCount < snapshots.snapshots.length && snapshots.totalSize > snapshots.maxSize ) { - snapshots.totalSize -= snapshots.snapshots[removeCount].length; + snapshots.totalSize -= getLength(snapshots.snapshots[removeCount]); removeCount++; } @@ -36,5 +66,36 @@ export default function addSnapshot( if (isAutoCompleteSnapshot) { snapshots.autoCompleteIndex = snapshots.currentIndex; } + } else if (isSameSnapshot) { + // replace the currentSnapshot's metadata so the selection is updated + snapshots.snapshots.splice(snapshots.currentIndex, 1, snapshot); } } + +/** + * Add a new snapshot to the given snapshots data structure + * @param snapshots The snapshots data structure to add new snapshot into + * @param snapshot The snapshot object to add + * @param isAutoCompleteSnapshot Whether this is a snapshot before auto complete action + */ +export function addSnapshotV2( + snapshots: Snapshots, + snapshot: Snapshot, + isAutoCompleteSnapshot: boolean +) { + addSnapshot( + snapshots, + snapshot, + isAutoCompleteSnapshot, + s => s.html?.length || 0, + compareSnapshots + ); +} + +function compareSnapshots(s1: Snapshot, s2: Snapshot) { + return s1.html == s2.html && !s1.entityStates && !s2.entityStates; +} + +function defaultCompare(s1: T, s2: T) { + return s1 == s2; +} diff --git a/packages/roosterjs-editor-dom/lib/snapshots/canMoveCurrentSnapshot.ts b/packages/roosterjs-editor-dom/lib/snapshots/canMoveCurrentSnapshot.ts index 3e3e70d65f10..66ad785f4a83 100644 --- a/packages/roosterjs-editor-dom/lib/snapshots/canMoveCurrentSnapshot.ts +++ b/packages/roosterjs-editor-dom/lib/snapshots/canMoveCurrentSnapshot.ts @@ -6,7 +6,10 @@ import { Snapshots } from 'roosterjs-editor-types'; * @param step The step to check, can be positive, negative or 0 * @returns True if can move current snapshot with the given step, otherwise false */ -export default function canMoveCurrentSnapshot(snapshots: Snapshots, step: number): boolean { +export default function canMoveCurrentSnapshot( + snapshots: Snapshots, + step: number +): boolean { let newIndex = snapshots.currentIndex + step; return newIndex >= 0 && newIndex < snapshots.snapshots.length; } diff --git a/packages/roosterjs-editor-dom/lib/snapshots/canUndoAutoComplete.ts b/packages/roosterjs-editor-dom/lib/snapshots/canUndoAutoComplete.ts index e11c925c10d3..30857c90d4d8 100644 --- a/packages/roosterjs-editor-dom/lib/snapshots/canUndoAutoComplete.ts +++ b/packages/roosterjs-editor-dom/lib/snapshots/canUndoAutoComplete.ts @@ -3,7 +3,7 @@ import { Snapshots } from 'roosterjs-editor-types'; /** * Whether there is a snapshot added before auto complete and it can be undone now */ -export default function canUndoAutoComplete(snapshots: Snapshots): boolean { +export default function canUndoAutoComplete(snapshots: Snapshots): boolean { return ( snapshots.autoCompleteIndex >= 0 && snapshots.currentIndex - snapshots.autoCompleteIndex == 1 diff --git a/packages/roosterjs-editor-dom/lib/snapshots/clearProceedingSnapshots.ts b/packages/roosterjs-editor-dom/lib/snapshots/clearProceedingSnapshots.ts index ec02a7e83d6b..c54d790694d1 100644 --- a/packages/roosterjs-editor-dom/lib/snapshots/clearProceedingSnapshots.ts +++ b/packages/roosterjs-editor-dom/lib/snapshots/clearProceedingSnapshots.ts @@ -1,18 +1,45 @@ import canMoveCurrentSnapshot from './canMoveCurrentSnapshot'; -import { Snapshots } from 'roosterjs-editor-types'; +import { Snapshot, Snapshots } from 'roosterjs-editor-types'; /** * Clear all snapshots after the current one * @param snapshots The snapshots data structure to clear */ -export default function clearProceedingSnapshots(snapshots: Snapshots) { +export default function clearProceedingSnapshots(snapshots: Snapshots): void; + +/** + * Clear all snapshots after the current one + * @param snapshots The snapshots data structure to clear + */ +export default function clearProceedingSnapshots( + snapshots: Snapshots, + getLength: (snapshot: T) => number +): void; + +/** + * Clear all snapshots after the current one + * @param snapshots The snapshots data structure to clear + */ +export default function clearProceedingSnapshots( + snapshots: Snapshots, + getLength?: (snapshot: T) => number +) { + getLength = getLength || (str => ((str))?.length || 0); if (canMoveCurrentSnapshot(snapshots, 1)) { let removedSize = 0; for (let i = snapshots.currentIndex + 1; i < snapshots.snapshots.length; i++) { - removedSize += snapshots.snapshots[i].length; + removedSize += getLength(snapshots.snapshots[i]); } snapshots.snapshots.splice(snapshots.currentIndex + 1); snapshots.totalSize -= removedSize; snapshots.autoCompleteIndex = -1; } } + +/** + * Clear all snapshots after the current one + * @param snapshots The snapshots data structure to clear + */ +export function clearProceedingSnapshotsV2(snapshots: Snapshots) { + clearProceedingSnapshots(snapshots, s => s.html?.length || 0); +} diff --git a/packages/roosterjs-editor-dom/lib/snapshots/createSnapshots.ts b/packages/roosterjs-editor-dom/lib/snapshots/createSnapshots.ts index bb7f1fa403bf..54bde30d5c01 100644 --- a/packages/roosterjs-editor-dom/lib/snapshots/createSnapshots.ts +++ b/packages/roosterjs-editor-dom/lib/snapshots/createSnapshots.ts @@ -4,7 +4,7 @@ import { Snapshots } from 'roosterjs-editor-types'; * Create initial snapshots * @param maxSize max size of all snapshots */ -export default function createSnapshots(maxSize: number): Snapshots { +export default function createSnapshots(maxSize: number): Snapshots { return { snapshots: [], totalSize: 0, diff --git a/packages/roosterjs-editor-dom/lib/snapshots/moveCurrentSnapshot.ts b/packages/roosterjs-editor-dom/lib/snapshots/moveCurrentSnapshot.ts index 30ce776faacc..d5c5c57e5370 100644 --- a/packages/roosterjs-editor-dom/lib/snapshots/moveCurrentSnapshot.ts +++ b/packages/roosterjs-editor-dom/lib/snapshots/moveCurrentSnapshot.ts @@ -7,7 +7,10 @@ import { Snapshots } from 'roosterjs-editor-types'; * @param step The step to move * @returns If can move with the given step, returns the snapshot after move, otherwise null */ -export default function moveCurrentSnapshot(snapshots: Snapshots, step: number): string | null { +export default function moveCurrentSnapshot( + snapshots: Snapshots, + step: number +): T | null { if (canMoveCurrentSnapshot(snapshots, step)) { snapshots.currentIndex += step; snapshots.autoCompleteIndex = -1; diff --git a/packages/roosterjs-editor-dom/lib/snapshots/tsconfig.child.json b/packages/roosterjs-editor-dom/lib/snapshots/tsconfig.child.json deleted file mode 100644 index a9db8b7f7e90..000000000000 --- a/packages/roosterjs-editor-dom/lib/snapshots/tsconfig.child.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "strict": true - }, - "extends": "../../../tsconfig.json", - "include": ["./**/*.ts"], - "references": [{ "path": "../../../roosterjs-editor-types/tsconfig.child.json" }] -} diff --git a/packages/roosterjs-editor-dom/lib/style/removeGlobalCssStyle.ts b/packages/roosterjs-editor-dom/lib/style/removeGlobalCssStyle.ts new file mode 100644 index 000000000000..a2f76eb5dcc3 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/style/removeGlobalCssStyle.ts @@ -0,0 +1,12 @@ +/** + * Remove a css rule style from a style sheet + * @param doc The document object + * @param styleId the ID of the style tag + */ + +export default function removeGlobalCssStyle(doc: Document, styleId: string) { + const styleTag = doc.getElementById(styleId) as HTMLStyleElement; + if (styleTag) { + styleTag.parentNode?.removeChild(styleTag); + } +} diff --git a/packages/roosterjs-editor-dom/lib/style/removeImportantStyleRule.ts b/packages/roosterjs-editor-dom/lib/style/removeImportantStyleRule.ts new file mode 100644 index 000000000000..a0a3ede367a0 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/style/removeImportantStyleRule.ts @@ -0,0 +1,23 @@ +import getStyles from './getStyles'; +import setStyles from './setStyles'; + +/** + * Removes the css important rule from some css properties + * @param element The HTMLElement + * @param styleProperties The css properties that important must be removed. Ex: ['background-color', 'background'] + */ + +export default function removeImportantStyleRule(element: HTMLElement, styleProperties: string[]) { + const styles = getStyles(element); + let modifiedStyles = 0; + styleProperties.forEach(style => { + if (styles[style]?.indexOf('!important') > -1) { + const index = styles[style].indexOf('!'); + styles[style] = styles[style].substring(0, index); + modifiedStyles++; + } + }); + if (modifiedStyles > 0) { + setStyles(element, styles); + } +} diff --git a/packages/roosterjs-editor-dom/lib/style/setGlobalCssStyles.ts b/packages/roosterjs-editor-dom/lib/style/setGlobalCssStyles.ts new file mode 100644 index 000000000000..cd13b281be5e --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/style/setGlobalCssStyles.ts @@ -0,0 +1,18 @@ +/** + * Add global css styles + * @param doc The document object + * @param cssRule The css rule that must added to the selection + * @param styleId The id of the style tag + */ + +export default function setGlobalCssStyles(doc: Document, cssRule: string, styleId: string) { + if (cssRule) { + let styleTag = doc.getElementById(styleId) as HTMLStyleElement; + if (!styleTag) { + styleTag = doc.createElement('style'); + styleTag.id = styleId; + doc.head.appendChild(styleTag); + } + styleTag.sheet?.insertRule(cssRule); + } +} diff --git a/packages/roosterjs-editor-dom/lib/style/setStyles.ts b/packages/roosterjs-editor-dom/lib/style/setStyles.ts index 43c04f2bf42d..b45b5d04ecf7 100644 --- a/packages/roosterjs-editor-dom/lib/style/setStyles.ts +++ b/packages/roosterjs-editor-dom/lib/style/setStyles.ts @@ -1,3 +1,5 @@ +import getObjectKeys from '../jsUtils/getObjectKeys'; + /** * Set styles to an HTML element. If styles are empty, remove 'style' attribute * @param element The element to set styles @@ -5,7 +7,7 @@ */ export default function setStyles(element: HTMLElement, styles: Record) { if (element) { - const style = Object.keys(styles || {}) + const style = getObjectKeys(styles || {}) .map(name => { const value: string | null = styles[name]; const trimmedName = name ? name.trim() : null; diff --git a/packages/roosterjs-editor-dom/lib/style/tsconfig.child.json b/packages/roosterjs-editor-dom/lib/style/tsconfig.child.json deleted file mode 100644 index a9db8b7f7e90..000000000000 --- a/packages/roosterjs-editor-dom/lib/style/tsconfig.child.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "strict": true - }, - "extends": "../../../tsconfig.json", - "include": ["./**/*.ts"], - "references": [{ "path": "../../../roosterjs-editor-types/tsconfig.child.json" }] -} diff --git a/packages/roosterjs-editor-dom/lib/table/VTable.ts b/packages/roosterjs-editor-dom/lib/table/VTable.ts index 7f7f6bcd0f91..fbfc8861cd70 100644 --- a/packages/roosterjs-editor-dom/lib/table/VTable.ts +++ b/packages/roosterjs-editor-dom/lib/table/VTable.ts @@ -1,10 +1,11 @@ -import applyTableFormat from '../utils/applyTableFormat'; +import applyTableFormat from './applyTableFormat'; +import getTagOfNode from '../utils/getTagOfNode'; import moveChildNodes from '../utils/moveChildNodes'; import normalizeRect from '../utils/normalizeRect'; import safeInstanceOf from '../utils/safeInstanceOf'; -import toArray from '../utils/toArray'; +import toArray from '../jsUtils/toArray'; import { getTableFormatInfo, saveTableInfo } from './tableFormatInfo'; - +import { removeMetadata } from '../metadata/metadata'; import { SizeTransformer, TableBorderFormat, @@ -12,10 +13,11 @@ import { TableOperation, TableSelection, VCell, + DarkColorHandler, } from 'roosterjs-editor-types'; +import type { CompatibleTableOperation } from 'roosterjs-editor-types/lib/compatibleTypes'; -const CELL_SHADE = 'cellShade'; -const DEFAULT_FORMAT: TableFormat = { +const DEFAULT_FORMAT: Required = { topBorderColor: '#ABABAB', bottomBorderColor: '#ABABAB', verticalBorderColor: '#ABABAB', @@ -27,6 +29,7 @@ const DEFAULT_FORMAT: TableFormat = { bgColorOdd: '#ABABAB20', headerRowColor: '#ABABAB', tableBorderFormat: TableBorderFormat.DEFAULT, + keepCellShade: false, }; /** @@ -41,40 +44,37 @@ export default class VTable { /** * Virtual cells */ - cells: VCell[][]; + cells: VCell[][] | null = null; /** * Current row index */ - row: number; + row: number | undefined; /** * Current column index */ - col: number; - - /** - * Selected range of cells with the coordinates of the first and last cell selected. - */ - selection: TableSelection; + col: number | undefined; /** * Current format of the table */ - formatInfo: TableFormat; + formatInfo: Required | null = null; private trs: HTMLTableRowElement[] = []; + private tableSelection: TableSelection | null = null; + /** * Create a new instance of VTable object using HTML TABLE or TD node * @param node The HTML Table or TD node * @param normalizeSize Whether table size needs to be normalized - * @param sizeTransformer A size transformer function used for normalize table size + * @param zoomScale When the table is under a zoomed container, pass in the zoom scale here */ constructor( node: HTMLTableElement | HTMLTableCellElement, normalizeSize?: boolean, - sizeTransformer?: SizeTransformer + zoomScale?: number | SizeTransformer ) { this.table = safeInstanceOf(node, 'HTMLTableElement') ? node : getTableFromTd(node); if (this.table) { @@ -85,7 +85,7 @@ export default class VTable { this.trs[rowIndex % 2] = tr; for (let sourceCol = 0, targetCol = 0; sourceCol < tr.cells.length; sourceCol++) { // Skip the cells which already initialized - for (; this.cells[rowIndex][targetCol]; targetCol++) {} + for (; this.cells![rowIndex][targetCol]; targetCol++) {} let td = tr.cells[sourceCol]; if (td == currentTd) { @@ -97,46 +97,79 @@ export default class VTable { for (let rowSpan = 0; rowSpan < td.rowSpan; rowSpan++) { const hasTd: boolean = colSpan + rowSpan == 0; const rect = td.getBoundingClientRect(); - this.cells[rowIndex + rowSpan][targetCol] = { - td: hasTd ? td : null, - spanLeft: colSpan > 0, - spanAbove: rowSpan > 0, - width: hasTd ? rect.width : undefined, - height: hasTd ? rect.height : undefined, - }; + if (this.cells?.[rowIndex + rowSpan]) { + this.cells[rowIndex + rowSpan][targetCol] = { + td: hasTd ? td : null, + spanLeft: colSpan > 0, + spanAbove: rowSpan > 0, + width: hasTd ? rect.width : undefined, + height: hasTd ? rect.height : undefined, + }; + } } } } }); this.formatInfo = getTableFormatInfo(this.table); if (normalizeSize) { - this.normalizeSize(sizeTransformer); + this.normalizeSize(typeof zoomScale == 'number' ? n => n / zoomScale : zoomScale); } } } + /** + * Selected range of cells with the coordinates of the first and last cell selected. + */ + public get selection(): TableSelection | null { + return this.tableSelection || null; + } + + public set selection(value: TableSelection | null) { + if (value) { + const { firstCell } = value; + this.row = firstCell?.y; + this.col = firstCell?.x; + } + this.tableSelection = value; + } + /** * Write the virtual table back to DOM tree to represent the change of VTable + * @param skipApplyFormat Do not reapply table format when write back. Only use this parameter when you are pretty sure there is no format or table structure change during the process. + * @param darkColorHandler An object to handle dark background colors, if not passed the cell background color will not be set */ - writeBack() { + writeBack(skipApplyFormat?: boolean, darkColorHandler?: DarkColorHandler | null) { if (this.cells) { moveChildNodes(this.table); this.cells.forEach((row, r) => { let tr = cloneNode(this.trs[r % 2] || this.trs[0]); - this.table.appendChild(tr); - row.forEach((cell, c) => { - if (cell.td) { - this.recalculateSpans(r, c); - tr.appendChild(cell.td); - } - }); + + if (tr) { + this.table.appendChild(tr); + row.forEach((cell, c) => { + if (cell.td) { + this.recalculateSpans(r, c); + this.recalculateCellHeight(cell.td); + tr!.appendChild(cell.td); + } + }); + } }); - if (this.formatInfo) { + if (this.formatInfo && !skipApplyFormat) { saveTableInfo(this.table, this.formatInfo); - applyTableFormat(this.table, this.cells, this.formatInfo); + applyTableFormat(this.table, this.cells, this.formatInfo, darkColorHandler); } } else if (this.table) { - this.table.parentNode.removeChild(this.table); + this.table.parentNode?.removeChild(this.table); + } + } + + private recalculateCellHeight(td: HTMLTableCellElement) { + if (this.isEmptyCell(td) && td.rowSpan > 1) { + for (let i = 1; i < td.rowSpan; i++) { + const br = document.createElement('br'); + td.appendChild(br); + } } } @@ -148,19 +181,25 @@ export default class VTable { if (!this.table) { return; } - this.formatInfo = { ...DEFAULT_FORMAT, ...(this.formatInfo || {}), ...(format || {}) }; - this.deleteCellShadeDataset(this.cells); + this.formatInfo = { + ...DEFAULT_FORMAT, + ...(this.formatInfo || {}), + ...(format || {}), + }; + if (!this.formatInfo.keepCellShade) { + this.deleteCellShadeDataset(this.cells); + } } /** - * Remove the cellshade dataset to apply a new style format at the cell. + * Remove the cellShade dataset to apply a new style format at the cell. * @param cells */ - private deleteCellShadeDataset(cells: VCell[][]) { - cells.forEach(row => { + private deleteCellShadeDataset(cells: VCell[][] | null) { + cells?.forEach(row => { row.forEach(cell => { - if (cell.td && cell.td.dataset[CELL_SHADE]) { - delete cell.td.dataset[CELL_SHADE]; + if (cell.td) { + removeMetadata(cell.td); } }); }); @@ -170,83 +209,116 @@ export default class VTable { * Edit table with given operation. * @param operation Table operation */ - edit(operation: TableOperation) { - if (!this.table) { + edit(operation: TableOperation | CompatibleTableOperation) { + if (!this.table || !this.cells || this.row === undefined || this.col == undefined) { return; } let currentRow = this.cells[this.row]; let currentCell = currentRow[this.col]; - let { style } = currentCell.td; + const firstRow = this.selection ? this.selection.firstCell.y : this.row; + const lastRow = this.selection ? this.selection.lastCell.y : this.row; + const firstColumn = this.selection ? this.selection.firstCell.x : this.col; + const lastColumn = this.selection ? this.selection.lastCell.x : this.col; switch (operation) { case TableOperation.InsertAbove: - this.cells.splice(this.row, 0, currentRow.map(cloneCell)); + for (let i = firstRow; i <= lastRow; i++) { + this.cells.splice(firstRow, 0, currentRow.map(cloneCell)); + } break; case TableOperation.InsertBelow: - let newRow = this.row + this.countSpanAbove(this.row, this.col); - this.cells.splice( - newRow, - 0, - this.cells[newRow - 1].map((cell, colIndex) => { - let nextCell = this.getCell(newRow, colIndex); - if (nextCell.spanAbove) { - return cloneCell(nextCell); - } else if (cell.spanLeft) { - let newCell = cloneCell(cell); - newCell.spanAbove = false; - return newCell; - } else { - return { - td: cloneNode(this.getTd(this.row, colIndex)), - }; - } - }) - ); + for (let i = firstRow; i <= lastRow; i++) { + let newRow = lastRow + this.countSpanAbove(lastRow, this.col); + this.cells.splice( + newRow, + 0, + this.cells[newRow - 1].map((cell, colIndex) => { + let nextCell = this.getCell(newRow, colIndex); + + if (nextCell.spanAbove) { + return cloneCell(nextCell); + } else if (cell.spanLeft) { + let newCell = cloneCell(cell); + newCell.spanAbove = false; + return newCell; + } else { + return { + td: cloneNode(this.getTd(this.row!, colIndex)), + }; + } + }) + ); + } + break; case TableOperation.InsertLeft: - this.forEachCellOfCurrentColumn((cell, row) => { - row.splice(this.col, 0, cloneCell(cell)); - }); + for (let i = firstColumn; i <= lastColumn; i++) { + this.forEachCellOfCurrentColumn((cell, row) => { + row.splice(i, 0, cloneCell(cell)); + }); + } + break; case TableOperation.InsertRight: - let newCol = this.col + this.countSpanLeft(this.row, this.col); - this.forEachCellOfColumn(newCol - 1, (cell, row, i) => { - let nextCell = this.getCell(i, newCol); - let newCell: VCell; - if (nextCell.spanLeft) { - newCell = cloneCell(nextCell); - } else if (cell.spanAbove) { - newCell = cloneCell(cell); - newCell.spanLeft = false; - } else { - newCell = { - td: cloneNode(this.getTd(i, this.col)), - }; - } + for (let i = firstColumn; i <= lastColumn; i++) { + let newCol = lastColumn + this.countSpanLeft(this.row, lastColumn); + this.forEachCellOfColumn(newCol - 1, (cell, row, i) => { + let nextCell = this.getCell(i, newCol); + let newCell: VCell; + if (nextCell.spanLeft) { + newCell = cloneCell(nextCell); + } else if (cell.spanAbove) { + newCell = cloneCell(cell); + newCell.spanLeft = false; + } else { + newCell = { + td: cloneNode(this.getTd(i, this.col!)), + }; + } + + row.splice(newCol, 0, newCell); + }); + } - row.splice(newCol, 0, newCell); - }); break; case TableOperation.DeleteRow: - this.forEachCellOfCurrentRow((cell, i) => { - let nextCell = this.getCell(this.row + 1, i); - if (cell.td && cell.td.rowSpan > 1 && nextCell.spanAbove) { - nextCell.td = cell.td; - } - }); - this.cells.splice(this.row, 1); - break; + for (let rowIndex = firstRow; rowIndex <= lastRow; rowIndex++) { + this.forEachCellOfRow(rowIndex, (cell: VCell, i: number) => { + let nextCell = this.getCell(rowIndex + 1, i); + if (cell.td && cell.td.rowSpan > 1 && nextCell.spanAbove) { + nextCell.td = cell.td; + } + }); + } + const removedRows = this.selection + ? this.selection.lastCell.y - this.selection.firstCell.y + : 0; + this.cells.splice(firstRow, removedRows + 1); + if (this.cells.length === 0) { + this.cells = null; + } + break; case TableOperation.DeleteColumn: - this.forEachCellOfCurrentColumn((cell, row, i) => { - let nextCell = this.getCell(i, this.col + 1); - if (cell.td && cell.td.colSpan > 1 && nextCell.spanLeft) { - nextCell.td = cell.td; - } - row.splice(this.col, 1); - }); + let deletedColumns = 0; + for (let colIndex = firstColumn; colIndex <= lastColumn; colIndex++) { + this.forEachCellOfColumn(colIndex, (cell, row, i) => { + let nextCell = this.getCell(i, colIndex + 1); + if (cell.td && cell.td.colSpan > 1 && nextCell.spanLeft) { + nextCell.td = cell.td; + } + const removedColumns = this.selection + ? colIndex - deletedColumns + : this.col!; + row.splice(removedColumns, 1); + }); + deletedColumns++; + } + if (this.cells?.length === 0 || this.cells?.every(row => row.length === 0)) { + this.cells = null; + } break; case TableOperation.MergeAbove: @@ -261,15 +333,7 @@ export default class VTable { if (cell.td && !cell.spanAbove) { let aboveCell = rowIndex < this.row ? cell : currentCell; let belowCell = rowIndex < this.row ? currentCell : cell; - if (aboveCell.td.colSpan == belowCell.td.colSpan) { - moveChildNodes( - aboveCell.td, - belowCell.td, - true /*keepExistingChildren*/ - ); - belowCell.td = null; - belowCell.spanAbove = true; - } + this.mergeCells(aboveCell, belowCell); break; } } @@ -287,26 +351,33 @@ export default class VTable { if (cell.td && !cell.spanLeft) { let leftCell = colIndex < this.col ? cell : currentCell; let rightCell = colIndex < this.col ? currentCell : cell; - if (leftCell.td.rowSpan == rightCell.td.rowSpan) { - moveChildNodes( - leftCell.td, - rightCell.td, - true /*keepExistingChildren*/ - ); - rightCell.td = null; - rightCell.spanLeft = true; - } + this.mergeCells(leftCell, rightCell, true /** horizontally */); break; } } break; + case TableOperation.MergeCells: + for (let colIndex = firstColumn; colIndex <= lastColumn; colIndex++) { + for (let rowIndex = firstRow + 1; rowIndex <= lastRow; rowIndex++) { + let cell = this.getCell(firstRow, colIndex); + let nextCellBelow = this.getCell(rowIndex, colIndex); + this.mergeCells(cell, nextCellBelow); + } + } + for (let colIndex = firstColumn + 1; colIndex <= lastColumn; colIndex++) { + let cell = this.getCell(firstRow, firstColumn); + let nextCellRight = this.getCell(firstRow, colIndex); + this.mergeCells(cell, nextCellRight, true /** horizontally */); + } + + break; case TableOperation.DeleteTable: this.cells = null; break; case TableOperation.SplitVertically: - if (currentCell.td.rowSpan > 1) { + if (currentCell.td && currentCell.td.rowSpan > 1) { this.getCell(this.row + 1, this.col).td = cloneNode(currentCell.td); } else { let splitRow = currentRow.map(cell => { @@ -321,11 +392,11 @@ export default class VTable { break; case TableOperation.SplitHorizontally: - if (currentCell.td.colSpan > 1) { + if (currentCell.td && currentCell.td.colSpan > 1) { this.getCell(this.row, this.col + 1).td = cloneNode(currentCell.td); } else { this.forEachCellOfCurrentColumn((cell, row) => { - row.splice(this.col + 1, 0, { + row.splice(this.col! + 1, 0, { td: row == currentRow ? cloneNode(cell.td) : null, spanAbove: cell.spanAbove, spanLeft: row != currentRow, @@ -333,7 +404,6 @@ export default class VTable { }); } break; - case TableOperation.AlignCenter: this.table.style.marginLeft = 'auto'; this.table.style.marginRight = 'auto'; @@ -347,26 +417,116 @@ export default class VTable { this.table.style.marginRight = ''; break; case TableOperation.AlignCellCenter: - style.textAlign = 'center'; + this.setAlignmentToSelectedCells( + firstRow, + lastRow, + firstColumn, + lastColumn, + 'center' + ); break; case TableOperation.AlignCellLeft: - style.textAlign = 'left'; + this.setAlignmentToSelectedCells( + firstRow, + lastRow, + firstColumn, + lastColumn, + 'left' + ); break; case TableOperation.AlignCellRight: - style.textAlign = 'right'; + this.setAlignmentToSelectedCells( + firstRow, + lastRow, + firstColumn, + lastColumn, + 'right' + ); break; case TableOperation.AlignCellTop: - style.verticalAlign = 'top'; + this.setAlignmentToSelectedCells( + firstRow, + lastRow, + firstColumn, + lastColumn, + 'top', + true /** isVertical */ + ); break; case TableOperation.AlignCellMiddle: - style.verticalAlign = 'middle'; + this.setAlignmentToSelectedCells( + firstRow, + lastRow, + firstColumn, + lastColumn, + 'middle', + true /** isVertical */ + ); break; case TableOperation.AlignCellBottom: - style.verticalAlign = 'bottom'; + this.setAlignmentToSelectedCells( + firstRow, + lastRow, + firstColumn, + lastColumn, + 'bottom', + true /** isVertical */ + ); break; } } + setAlignmentToSelectedCells( + firstRow: number, + lastRow: number, + firstColumn: number, + lastColumn: number, + alignmentType: string, + isVertical?: boolean + ) { + for (let i = firstRow; i <= lastRow; i++) { + for (let j = firstColumn; j <= lastColumn; j++) { + if (this.cells) { + const cell = this.cells[i][j].td; + if (isVertical && cell) { + cell.style?.setProperty('vertical-align', alignmentType); + } else if (cell) { + cell.style?.setProperty('text-align', alignmentType); + } + } + } + } + } + + private mergeCells(cell: VCell, nextCell: VCell, horizontally?: boolean) { + const checkSpans = horizontally + ? cell.td?.rowSpan === nextCell.td?.rowSpan && !cell.spanLeft + : cell.td?.colSpan === nextCell.td?.colSpan && !cell.spanAbove; + if (cell.td && nextCell.td && checkSpans) { + this.mergeCellContents(cell.td, nextCell.td); + nextCell.td = null; + if (horizontally) { + nextCell.spanLeft = true; + } else { + nextCell.spanAbove = true; + } + } + } + + private isEmptyCell(td: HTMLTableCellElement) { + return td.childElementCount === 1 && getTagOfNode(td.firstChild) === 'BR'; + } + + private mergeCellContents(cellTd: HTMLTableCellElement, nextCellTd: HTMLTableCellElement) { + if (this.isEmptyCell(nextCellTd)) { + moveChildNodes(cellTd, nextCellTd, false /*keepExistingChildren*/); + } else { + const br = document.createElement('br'); + cellTd.appendChild(br); + moveChildNodes(cellTd, nextCellTd, true /*keepExistingChildren*/); + } + } + /** * Loop each cell of current column and invoke a callback function * @param callback The callback function to invoke @@ -401,7 +561,7 @@ export default class VTable { */ getCellsWithBorder(borderPos: number, getLeftCells: boolean): HTMLTableCellElement[] { const cells: HTMLTableCellElement[] = []; - for (let i = 0; i < this.cells.length; i++) { + for (let i = 0; this.cells && i < this.cells.length; i++) { for (let j = 0; j < this.cells[i].length; j++) { const cell = this.getCell(i, j); if (cell.td) { @@ -452,7 +612,7 @@ export default class VTable { /** * Get current HTML table cell object. If the current table cell is a virtual expanded cell, return its root cell */ - getCurrentTd(): HTMLTableCellElement { + getCurrentTd(): HTMLTableCellElement | null { return this.getTd(this.row, this.col); } @@ -461,8 +621,8 @@ export default class VTable { * @param row row of the cell * @param col column of the cell */ - getTd(row: number, col: number) { - if (this.cells) { + getTd(row: number | undefined, col: number | undefined) { + if (this.cells && row !== undefined && col !== undefined) { row = Math.min(this.cells.length - 1, row); col = this.cells[row] ? Math.min(this.cells[row].length - 1, col) : col; if (!isNaN(row) && !isNaN(col)) { @@ -484,17 +644,21 @@ export default class VTable { } private forEachCellOfColumn( - col: number, + col: number | undefined, callback: (cell: VCell, row: VCell[], i: number) => any ) { - for (let i = 0; i < this.cells.length; i++) { - callback(this.getCell(i, col), this.cells[i], i); + if (col !== undefined) { + for (let i = 0; this.cells && i < this.cells.length; i++) { + callback(this.getCell(i, col), this.cells[i], i); + } } } - private forEachCellOfRow(row: number, callback: (cell: VCell, i: number) => any) { - for (let i = 0; i < this.cells[row].length; i++) { - callback(this.getCell(row, i), i); + private forEachCellOfRow(row: number | undefined, callback: (cell: VCell, i: number) => any) { + if (row !== undefined) { + for (let i = 0; this.cells && i < this.cells[row].length; i++) { + callback(this.getCell(row, i), i); + } } } @@ -514,7 +678,7 @@ export default class VTable { private countSpanLeft(row: number, col: number) { let result = 1; - for (let i = col + 1; i < this.cells[row].length; i++) { + for (let i = col + 1; this.cells && i < this.cells[row].length; i++) { let cell = this.getCell(row, i); if (cell.td || !cell.spanLeft) { break; @@ -526,7 +690,7 @@ export default class VTable { private countSpanAbove(row: number, col: number) { let result = 1; - for (let i = row + 1; i < this.cells.length; i++) { + for (let i = row + 1; this.cells && i < this.cells.length; i++) { let cell = this.getCell(i, col); if (cell.td || !cell.spanAbove) { break; @@ -549,31 +713,36 @@ export default class VTable { } /* normalize width/height for each cell in the table */ - public normalizeTableCellSize(sizeTransformer?: SizeTransformer) { + public normalizeTableCellSize(zoomScale?: number | SizeTransformer) { // remove width/height for each row for (let i = 0, row; (row = this.table.rows[i]); i++) { row.removeAttribute('width'); - row.style.width = null; + row.style.setProperty('width', null); row.removeAttribute('height'); - row.style.height = null; + row.style.setProperty('height', null); } // set width/height for each cell - for (let i = 0; i < this.cells.length; i++) { + for (let i = 0; this.cells && i < this.cells.length; i++) { for (let j = 0; j < this.cells[i].length; j++) { const cell = this.cells[i][j]; if (cell) { + const func = + typeof zoomScale == 'number' ? (n: number) => n / zoomScale : zoomScale; + const width = cell.width || 0; + const height = cell.height || 0; + setHTMLElementSizeInPx( cell.td, - sizeTransformer?.(cell.width) || cell.width, - sizeTransformer?.(cell.height) || cell.height + func?.(width) || width, + func?.(height) || height ); } } } } - private normalizeSize(sizeTransformer: SizeTransformer) { + private normalizeSize(sizeTransformer: SizeTransformer | undefined) { this.normalizeEmptyTableCells(); this.normalizeTableCellSize(sizeTransformer); @@ -588,7 +757,11 @@ export default class VTable { } } -function setHTMLElementSizeInPx(element: HTMLElement, newWidth: number, newHeight: number) { +function setHTMLElementSizeInPx( + element: HTMLElement | null | undefined, + newWidth: number, + newHeight: number +) { if (!!element) { element.removeAttribute('width'); element.removeAttribute('height'); @@ -599,7 +772,7 @@ function setHTMLElementSizeInPx(element: HTMLElement, newWidth: number, newHeigh } function getTableFromTd(td: HTMLTableCellElement) { - let result = td; + let result: Element | null = td; for (; result && result.tagName != 'TABLE'; result = result.parentElement) {} return result; } @@ -620,12 +793,12 @@ function cloneCell(cell: VCell): VCell { * Clone a node without its children. * @param node The node to clone */ -function cloneNode(node: T): T { +function cloneNode(node: T | null | undefined): T | null { let newNode = node ? node.cloneNode(false /*deep*/) : null; if (safeInstanceOf(newNode, 'HTMLTableCellElement')) { newNode.removeAttribute('id'); if (!newNode.firstChild) { - newNode.appendChild(node.ownerDocument.createElement('br')); + newNode.appendChild(node!.ownerDocument!.createElement('br')); } } return newNode; diff --git a/packages/roosterjs-editor-dom/lib/utils/applyTableFormat.ts b/packages/roosterjs-editor-dom/lib/table/applyTableFormat.ts similarity index 73% rename from packages/roosterjs-editor-dom/lib/utils/applyTableFormat.ts rename to packages/roosterjs-editor-dom/lib/table/applyTableFormat.ts index 17f861b020e0..c1fdcf6b6bb2 100644 --- a/packages/roosterjs-editor-dom/lib/utils/applyTableFormat.ts +++ b/packages/roosterjs-editor-dom/lib/table/applyTableFormat.ts @@ -1,28 +1,31 @@ -import changeElementTag from './changeElementTag'; -import { TableBorderFormat, TableFormat, VCell } from 'roosterjs-editor-types'; +import changeElementTag from '../utils/changeElementTag'; +import setColor from '../utils/setColor'; +import { DarkColorHandler, TableBorderFormat, TableFormat, VCell } from 'roosterjs-editor-types'; +import { getTableCellMetadata } from './tableCellInfo'; const TRANSPARENT = 'transparent'; const TABLE_CELL_TAG_NAME = 'TD'; const TABLE_HEADER_TAG_NAME = 'TH'; -const CELL_SHADE = 'cellShade'; /** * @internal * Apply the given table format to this virtual table * @param format Table format to apply + * @param darkColorHandler An object to handle dark background colors, if not passed the cell background color will not be set */ export default function applyTableFormat( table: HTMLTableElement, cells: VCell[][], - format: Partial + format: Required, + darkColorHandler?: DarkColorHandler | null ) { if (!format) { return; } table.style.borderCollapse = 'collapse'; setBordersType(cells, format); - setColor(cells, format); + setCellColor(cells, format, darkColorHandler); setFirstColumnFormat(cells, format); - setHeaderRowFormat(cells, format); + setHeaderRowFormat(cells, format, darkColorHandler); } /** @@ -34,15 +37,20 @@ function hasCellShade(cell: VCell) { if (!cell.td) { return false; } - const colorShade = cell.td.dataset[CELL_SHADE]; - return colorShade ? true : false; + + return !!getTableCellMetadata(cell.td)?.bgColorOverride; } /** * Set color to the table * @param format the format that must be applied + * @param darkColorHandler An object to handle dark background colors, if not passed the cell background color will not be set */ -function setColor(cells: VCell[][], format: TableFormat) { +function setCellColor( + cells: VCell[][], + format: TableFormat, + darkColorHandler?: DarkColorHandler | null +) { const color = (index: number) => (index % 2 === 0 ? format.bgColorEven : format.bgColorOdd); const { hasBandedRows, hasBandedColumns, bgColorOdd, bgColorEven } = format; const shouldColorWholeTable = !hasBandedRows && bgColorOdd === bgColorEven ? true : false; @@ -51,11 +59,32 @@ function setColor(cells: VCell[][], format: TableFormat) { if (cell.td && !hasCellShade(cell)) { if (hasBandedRows) { const backgroundColor = color(index); - cell.td.style.backgroundColor = backgroundColor || TRANSPARENT; + setColor( + cell.td, + backgroundColor || TRANSPARENT, + true /** isBackgroundColor*/, + undefined /** isDarkMode **/, + true /** shouldAdaptFontColor */, + darkColorHandler + ); } else if (shouldColorWholeTable) { - cell.td.style.backgroundColor = format.bgColorOdd || TRANSPARENT; + setColor( + cell.td, + format.bgColorOdd || TRANSPARENT, + true /** isBackgroundColor*/, + undefined /** isDarkMode **/, + true /** shouldAdaptFontColor */, + darkColorHandler + ); } else { - cell.td.style.backgroundColor = TRANSPARENT; + setColor( + cell.td, + TRANSPARENT, + true /** isBackgroundColor*/, + undefined /** isDarkMode **/, + true /** shouldAdaptFontColor */, + darkColorHandler + ); } } }); @@ -65,7 +94,14 @@ function setColor(cells: VCell[][], format: TableFormat) { row.forEach((cell, index) => { const backgroundColor = color(index); if (cell.td && backgroundColor && !hasCellShade(cell)) { - cell.td.style.backgroundColor = backgroundColor; + setColor( + cell.td, + backgroundColor, + true /** isBackgroundColor*/, + undefined /** isDarkMode **/, + true /** shouldAdaptFontColor */, + darkColorHandler + ); } }); }); @@ -255,9 +291,15 @@ function setFirstColumnFormat(cells: VCell[][], format: Partial) { cells.forEach((row, rowIndex) => { row.forEach((cell, cellIndex) => { if (cell.td && cellIndex === 0) { - if (rowIndex !== 0) { + if (rowIndex !== 0 && !hasCellShade(cell)) { cell.td.style.borderTopColor = TRANSPARENT; - cell.td.style.backgroundColor = TRANSPARENT; + setColor( + cell.td, + TRANSPARENT, + true /** isBackgroundColor*/, + undefined /** isDarkMode **/, + true /** shouldAdaptFontColor */ + ); } if (rowIndex !== cells.length - 1 && rowIndex !== 0) { cell.td.style.borderBottomColor = TRANSPARENT; @@ -272,11 +314,16 @@ function setFirstColumnFormat(cells: VCell[][], format: Partial) { /** * Apply custom design to the Header Row * @param format + * @param darkColorHandler An object to handle dark background colors, if not passed the cell background color will not be set * @returns */ -function setHeaderRowFormat(cells: VCell[][], format: TableFormat) { +function setHeaderRowFormat( + cells: VCell[][], + format: TableFormat, + darkColorHandler?: DarkColorHandler | null +) { if (!format.hasHeaderRow) { - cells[0].forEach(cell => { + cells[0]?.forEach(cell => { if (cell.td) { cell.td = changeElementTag(cell.td, TABLE_CELL_TAG_NAME) as HTMLTableCellElement; cell.td.scope = ''; @@ -284,9 +331,18 @@ function setHeaderRowFormat(cells: VCell[][], format: TableFormat) { }); return; } - cells[0].forEach(cell => { + cells[0]?.forEach(cell => { if (cell.td && format.headerRowColor) { - cell.td.style.backgroundColor = format.headerRowColor; + if (!hasCellShade(cell)) { + setColor( + cell.td, + format.headerRowColor, + true /** isBackgroundColor*/, + undefined /** isDarkMode **/, + true /** shouldAdaptFontColor */, + darkColorHandler + ); + } cell.td.style.borderRightColor = format.headerRowColor; cell.td.style.borderLeftColor = format.headerRowColor; cell.td.style.borderTopColor = format.headerRowColor; diff --git a/packages/roosterjs-editor-dom/lib/table/cloneCellStyles.ts b/packages/roosterjs-editor-dom/lib/table/cloneCellStyles.ts new file mode 100644 index 000000000000..5b0b37c66759 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/table/cloneCellStyles.ts @@ -0,0 +1,19 @@ +import { saveTableCellMetadata } from './tableCellInfo'; +/** + * Clone css styles from a element an set to another. + * @param cell cell that will receive the styles + * @param styledCell cell where the styles will be clone + */ + +export default function cloneCellStyles( + cell: HTMLTableCellElement, + styledCell: HTMLTableCellElement +) { + const styles = styledCell.getAttribute('style'); + if (styles) { + cell.setAttribute('style', styles); + saveTableCellMetadata(cell, { + bgColorOverride: true, + }); + } +} diff --git a/packages/roosterjs-editor-dom/lib/table/isWholeTableSelected.ts b/packages/roosterjs-editor-dom/lib/table/isWholeTableSelected.ts new file mode 100644 index 000000000000..e99a601e4317 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/table/isWholeTableSelected.ts @@ -0,0 +1,22 @@ +import VTable from './VTable'; +import { TableSelection } from 'roosterjs-editor-types'; + +/** + * Check if the whole table is selected + * @param vTable VTable to check whether all cells are selected + * @param selection Table selection with first cell selected and last cell selected coordinates. + * @returns + */ +export default function isWholeTableSelected(vTable: VTable, selection: TableSelection) { + if (!selection || !vTable.cells) { + return false; + } + const { firstCell, lastCell } = selection; + const rowsLength = vTable.cells.length - 1; + const colIndex = vTable.cells[rowsLength].length - 1; + const firstX = firstCell.x; + const firstY = firstCell.y; + const lastX = lastCell.x; + const lastY = lastCell.y; + return firstX == 0 && firstY == 0 && lastX == colIndex && lastY == rowsLength; +} diff --git a/packages/roosterjs-editor-dom/lib/table/pasteTable.ts b/packages/roosterjs-editor-dom/lib/table/pasteTable.ts new file mode 100644 index 000000000000..684ffe05da02 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/table/pasteTable.ts @@ -0,0 +1,62 @@ +import cloneCellStyles from './cloneCellStyles'; +import moveChildNodes from '../utils/moveChildNodes'; +import VTable from './VTable'; +import { NodePosition, TableOperation } from 'roosterjs-editor-types'; + +/** + * + * Pastes a table inside another, modifying the original to create a merged one + * @param currentTd The cell where the cursor is in the table to paste into + * @param rootNodeToInsert A Node containing the table to be inserted + * @param position The position to paste the table + * @param range The selected range of the table + * + * Position and range are here for when table selection allows to move pivot point + */ +export default function pasteTable( + currentTd: HTMLTableCellElement, + rootNodeToInsert: HTMLTableElement, + position?: NodePosition, + range?: Range +) { + // This is the table on the clipboard + let newTable = new VTable(rootNodeToInsert); + // This table is already on the editor + let currentTable = new VTable(currentTd); + + // Which cell in the currentTable is the cursor placed + let cursorRow = currentTable.row!; + let cursorCol = currentTable.col!; + + // Total rows and columns of the final table + let rows = cursorRow + newTable.cells?.length! ?? 0; + let columns = cursorCol + newTable.cells?.[0].length! ?? 0; + + // Add new rows + currentTable.row = currentTable.cells!.length! - 1; + while (currentTable.cells!.length! < rows) { + currentTable.edit(TableOperation.InsertBelow); + } + + // Add new columns + currentTable.col = currentTable.cells![0].length! - 1; + while (currentTable.cells![0].length! < columns) { + currentTable.edit(TableOperation.InsertRight); + } + + // Create final table + for (let i = cursorRow; i < rows; i++) { + for (let j = cursorCol; j < columns; j++) { + let cell = currentTable.getCell(i, j); + let newCell = newTable.getTd(i - cursorRow, j - cursorCol); + if (cell.td && newCell) { + moveChildNodes(cell.td, newCell); + cloneCellStyles(cell.td, newCell); + } else { + cell.td = document.createElement('td'); + } + } + } + + currentTable.writeBack(); +} diff --git a/packages/roosterjs-editor-dom/lib/table/tableCellInfo.ts b/packages/roosterjs-editor-dom/lib/table/tableCellInfo.ts new file mode 100644 index 000000000000..ff284262c1b4 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/table/tableCellInfo.ts @@ -0,0 +1,37 @@ +import { createBooleanDefinition, createObjectDefinition } from '../metadata/definitionCreators'; +import { getMetadata, setMetadata } from '../metadata/metadata'; +import { TableCellMetadataFormat } from 'roosterjs-editor-types'; + +const BooleanDefinition = createBooleanDefinition( + false /** isOptional */, + undefined /** value */, + true /** allowNull */ +); + +const TableCellFormatMetadata = createObjectDefinition>( + { + bgColorOverride: BooleanDefinition, + }, + false /* isOptional */, + true /** allowNull */ +); + +/** + * @internal + * Get the format info of a table cell + * @param cell The table cell to use + */ +export function getTableCellMetadata(cell: HTMLTableCellElement) { + return getMetadata(cell, TableCellFormatMetadata); +} + +/** + * Add metadata to a cell + * @param cell The table cell to add the metadata + * @param format The format of the table + */ +export function saveTableCellMetadata(cell: HTMLTableCellElement, format: TableCellMetadataFormat) { + if (cell && format) { + setMetadata(cell, format, TableCellFormatMetadata); + } +} diff --git a/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts b/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts index be8781fff4ec..f24c34f1df7c 100644 --- a/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts +++ b/packages/roosterjs-editor-dom/lib/table/tableFormatInfo.ts @@ -1,16 +1,51 @@ -import { TableFormat } from 'roosterjs-editor-types'; - -const TABLE_STYLE_INFO = 'roosterTableInfo'; +import { getMetadata, setMetadata } from '../metadata/metadata'; +import { TableBorderFormat, TableFormat } from 'roosterjs-editor-types'; +import { + createBooleanDefinition, + createNumberDefinition, + createObjectDefinition, + createStringDefinition, +} from '../metadata/definitionCreators'; + +const NullStringDefinition = createStringDefinition( + false /** isOptional */, + undefined /** value */, + true /** allowNull */ +); + +const BooleanDefinition = createBooleanDefinition(false /** isOptional */); + +const TableFormatMetadata = createObjectDefinition>( + { + topBorderColor: NullStringDefinition, + bottomBorderColor: NullStringDefinition, + verticalBorderColor: NullStringDefinition, + hasHeaderRow: BooleanDefinition, + headerRowColor: NullStringDefinition, + hasFirstColumn: BooleanDefinition, + hasBandedColumns: BooleanDefinition, + hasBandedRows: BooleanDefinition, + bgColorEven: NullStringDefinition, + bgColorOdd: NullStringDefinition, + tableBorderFormat: createNumberDefinition( + false /** isOptional */, + undefined /* value */, + TableBorderFormat.DEFAULT /* first table border format, TODO: Use Min/Max to specify valid values */, + TableBorderFormat.CLEAR /* last table border format, , TODO: Use Min/Max to specify valid values */ + ), + keepCellShade: createBooleanDefinition(true /** isOptional */), + }, + false /* isOptional */, + true /** allowNull */ +); /** - * @internal * Get the format info of a table * If the table does not have a info saved, it will be retrieved from the css styles * @param table The table that has the info */ export function getTableFormatInfo(table: HTMLTableElement) { - const obj = safeParseJSON(table?.dataset[TABLE_STYLE_INFO]) as TableFormat; - return checkIfTableFormatIsValid(obj) ? obj : null; + return getMetadata(table, TableFormatMetadata); } /** @@ -21,74 +56,6 @@ export function getTableFormatInfo(table: HTMLTableElement) { */ export function saveTableInfo(table: HTMLTableElement, format: TableFormat) { if (table && format) { - table.dataset[TABLE_STYLE_INFO] = JSON.stringify(format); - } -} - -function checkIfTableFormatIsValid(format: TableFormat) { - if (!format) { - return false; - } - const { - topBorderColor, - verticalBorderColor, - bottomBorderColor, - bgColorOdd, - bgColorEven, - hasBandedColumns, - hasBandedRows, - hasFirstColumn, - hasHeaderRow, - tableBorderFormat, - } = format; - const colorsValues = [ - topBorderColor, - verticalBorderColor, - bottomBorderColor, - bgColorOdd, - bgColorEven, - ]; - const stateValues = [hasBandedColumns, hasBandedRows, hasFirstColumn, hasHeaderRow]; - - if ( - colorsValues.some(key => !isAValidColor(key)) || - stateValues.some(key => !isBoolean(key)) || - !isAValidTableBorderType(tableBorderFormat) - ) { - return false; - } - - return true; -} - -function isAValidColor(color: any) { - if (color === null || color === undefined || typeof color === 'string') { - return true; - } - return false; -} - -function isBoolean(a: any) { - if (typeof a === 'boolean') { - return true; - } - return false; -} - -function isAValidTableBorderType(border: any) { - if (-1 < border && border < 8) { - return true; - } - return false; -} - -function safeParseJSON(json: string | undefined): any { - if (!json) { - return null; - } - try { - return JSON.parse(json); - } catch { - return null; + setMetadata(table, format, TableFormatMetadata); } } diff --git a/packages/roosterjs-editor-dom/lib/table/tsconfig.child.json b/packages/roosterjs-editor-dom/lib/table/tsconfig.child.json deleted file mode 100644 index fa643fa641b2..000000000000 --- a/packages/roosterjs-editor-dom/lib/table/tsconfig.child.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "strict": false - }, - "extends": "../../../tsconfig.json", - "include": ["./**/*.ts"], - "references": [ - { "path": "../../../roosterjs-editor-types/tsconfig.child.json" }, - { "path": "../utils/tsconfig.child.json" } - ] -} diff --git a/packages/roosterjs-editor-dom/lib/utils/Browser.ts b/packages/roosterjs-editor-dom/lib/utils/Browser.ts index f4b5dead74be..fa779db80d26 100644 --- a/packages/roosterjs-editor-dom/lib/utils/Browser.ts +++ b/packages/roosterjs-editor-dom/lib/utils/Browser.ts @@ -6,9 +6,14 @@ const isAndroidRegex = /android/i; * Get current browser information from user agent string * @param userAgent The userAgent string of a browser * @param appVersion The appVersion string of a browser + * @param vendor The vendor string of a browser * @returns The BrowserInfo object calculated from the given userAgent and appVersion */ -export function getBrowserInfo(userAgent: string, appVersion: string): BrowserInfo { +export function getBrowserInfo( + userAgent: string, + appVersion: string, + vendor?: string +): BrowserInfo { // checks whether the browser is running in IE // IE11 will use rv in UA instead of MSIE. Unfortunately Firefox also uses this. We should also look for "Trident" to confirm this. // There have been cases where companies using older version of IE and custom UserAgents have broken this logic (e.g. IE 10 and KellyServices) @@ -22,6 +27,23 @@ export function getBrowserInfo(userAgent: string, appVersion: string): BrowserIn let isSafari = false; let isEdge = false; let isWebKit = userAgent.indexOf('WebKit') != -1; + let isMobileOrTablet = false; + + // Reference: http://detectmobilebrowsers.com/ + // The default regex on the website doesn't consider tablet. + // To support tablet, add |android|ipad|playbook|silk to the first regex according to the info in /about page + ((userAgentOrVendor: string) => { + if ( + /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test( + userAgentOrVendor + ) || + /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( + userAgentOrVendor.substr(0, 4) + ) + ) { + isMobileOrTablet = true; + } + })(userAgent || vendor || ''); if (!isIE) { isChrome = userAgent.indexOf('Chrome') != -1; @@ -56,12 +78,18 @@ export function getBrowserInfo(userAgent: string, appVersion: string): BrowserIn isEdge, isIEOrEdge: isIE || isEdge, isAndroid, + isMobileOrTablet, }; } /** * Browser object contains browser and operating system information of current environment */ -export const Browser = window - ? getBrowserInfo(window.navigator.userAgent, window.navigator.appVersion) - : {}; +export const Browser = + typeof window !== 'undefined' && window + ? getBrowserInfo( + window.navigator.userAgent, + window.navigator.appVersion, + window.navigator.vendor + ) + : {}; diff --git a/packages/roosterjs-editor-dom/lib/utils/applyFormat.ts b/packages/roosterjs-editor-dom/lib/utils/applyFormat.ts index 5d3adaed396b..a5828419de6f 100644 --- a/packages/roosterjs-editor-dom/lib/utils/applyFormat.ts +++ b/packages/roosterjs-editor-dom/lib/utils/applyFormat.ts @@ -1,15 +1,18 @@ import setColor from './setColor'; -import { DefaultFormat } from 'roosterjs-editor-types'; +import { DarkColorHandler, DefaultFormat } from 'roosterjs-editor-types'; /** * Apply format to an HTML element * @param element The HTML element to apply format to * @param format The format to apply + * @param isDarkMode Whether the content should be formatted in dark mode + * @param darkColorHandler An optional dark color handler object. When it is passed, we will use this handler to do variable-based dark color instead of original dataset base dark color */ export default function applyFormat( element: HTMLElement, format: DefaultFormat, - isDarkMode?: boolean + isDarkMode?: boolean, + darkColorHandler?: DarkColorHandler | null ) { if (format) { let elementStyle = element.style; @@ -33,15 +36,43 @@ export default function applyFormat( } if (textColors) { - setColor(element, textColors, false /*isBackground*/, isDarkMode); + setColor( + element, + textColors, + false /*isBackground*/, + isDarkMode, + false /*shouldAdaptFontColor*/, + darkColorHandler + ); } else if (textColor) { - setColor(element, textColor, false /*isBackground*/, isDarkMode); + setColor( + element, + textColor, + false /*isBackground*/, + isDarkMode, + false /*shouldAdaptFontColor*/, + darkColorHandler + ); } if (backgroundColors) { - setColor(element, backgroundColors, true /*isBackground*/, isDarkMode); + setColor( + element, + backgroundColors, + true /*isBackground*/, + isDarkMode, + false /*shouldAdaptFontColor*/, + darkColorHandler + ); } else if (backgroundColor) { - setColor(element, backgroundColor, true /*isBackground*/, isDarkMode); + setColor( + element, + backgroundColor, + true /*isBackground*/, + isDarkMode, + false /*shouldAdaptFontColor*/, + darkColorHandler + ); } if (bold) { diff --git a/packages/roosterjs-editor-dom/lib/utils/collapseNodes.ts b/packages/roosterjs-editor-dom/lib/utils/collapseNodes.ts index 6157ec0dbe7a..33f98096a3dc 100644 --- a/packages/roosterjs-editor-dom/lib/utils/collapseNodes.ts +++ b/packages/roosterjs-editor-dom/lib/utils/collapseNodes.ts @@ -1,6 +1,6 @@ import contains from './contains'; import splitParentNode from './splitParentNode'; -import toArray from './toArray'; +import toArray from '../jsUtils/toArray'; /** * Collapse nodes within the given start and end nodes to their common ancestor node, diff --git a/packages/roosterjs-editor-dom/lib/utils/contains.ts b/packages/roosterjs-editor-dom/lib/utils/contains.ts index 8d766a8a1866..7f12c472c557 100644 --- a/packages/roosterjs-editor-dom/lib/utils/contains.ts +++ b/packages/roosterjs-editor-dom/lib/utils/contains.ts @@ -11,8 +11,8 @@ import { NodeType } from 'roosterjs-editor-types'; * Otherwise false. */ export default function contains( - container: Node | null, - contained: Node | null, + container: Node | null | undefined, + contained: Node | null | undefined, treatSameNodeAsContain?: boolean ): boolean; @@ -22,11 +22,14 @@ export default function contains( * @param contained The range to check if it is inside container * @returns True if contained is inside container, otherwise false */ -export default function contains(container: Node | null, contained: Range | null): boolean; +export default function contains( + container: Node | null | undefined, + contained: Range | null | undefined +): boolean; export default function contains( - container: Node | null, - contained: Node | Range | null, + container: Node | null | undefined, + contained: Node | Range | null | undefined, treatSameNodeAsContain?: boolean ): boolean { if (!container || !contained) { diff --git a/packages/roosterjs-editor-dom/lib/utils/createElement.ts b/packages/roosterjs-editor-dom/lib/utils/createElement.ts index c745b1bf4d21..b0c878c787dd 100644 --- a/packages/roosterjs-editor-dom/lib/utils/createElement.ts +++ b/packages/roosterjs-editor-dom/lib/utils/createElement.ts @@ -1,6 +1,8 @@ +import getObjectKeys from '../jsUtils/getObjectKeys'; import safeInstanceOf from './safeInstanceOf'; import { Browser } from './Browser'; import { CreateElementData, KnownCreateElementDataIndex } from 'roosterjs-editor-types'; +import type { CompatibleKnownCreateElementDataIndex } from 'roosterjs-editor-types/lib/compatibleTypes'; /** * All known CreateElementData used by roosterjs to create elements @@ -20,7 +22,7 @@ export const KnownCreateElementData: Record { + getObjectKeys(dataset).forEach(datasetName => { result.dataset[datasetName] = dataset[datasetName]; }); } if (attributes) { - Object.keys(attributes).forEach(attrName => { + getObjectKeys(attributes).forEach(attrName => { result.setAttribute(attrName, attributes[attrName]); }); } diff --git a/packages/roosterjs-editor-dom/lib/utils/fromHtml.ts b/packages/roosterjs-editor-dom/lib/utils/fromHtml.ts index 87133040ad38..13cb89bf291a 100644 --- a/packages/roosterjs-editor-dom/lib/utils/fromHtml.ts +++ b/packages/roosterjs-editor-dom/lib/utils/fromHtml.ts @@ -1,4 +1,4 @@ -import toArray from './toArray'; +import toArray from '../jsUtils/toArray'; /** * @deprecated diff --git a/packages/roosterjs-editor-dom/lib/utils/getIntersectedRect.ts b/packages/roosterjs-editor-dom/lib/utils/getIntersectedRect.ts new file mode 100644 index 000000000000..7134c974259f --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/utils/getIntersectedRect.ts @@ -0,0 +1,46 @@ +import normalizeRect from './normalizeRect'; +import { Rect } from 'roosterjs-editor-types'; + +/** + * Get the intersected Rect of elements provided + * + * @example + * The result of the following Elements Rects would be: + { + top: Element2.top, + bottom: Element1.bottom, + left: Element2.left, + right: Element2.right + } + +-------------------------+ + | Element 1 | + | +-----------------+ | + | | Element2 | | + | | | | + | | | | + +-------------------------+ + | | + +-----------------+ + + * @param elements Elements to use. + * @param additionalRects additional rects to use + * @returns If the Rect is valid return the rect, if not, return null. + */ +export default function getIntersectedRect( + elements: HTMLElement[], + additionalRects: Rect[] = [] +): Rect | null { + const rects = elements + .map(element => normalizeRect(element.getBoundingClientRect())) + .concat(additionalRects) + .filter(element => !!element) as Rect[]; + + const result: Rect = { + top: Math.max(...rects.map(r => r.top)), + bottom: Math.min(...rects.map(r => r.bottom)), + left: Math.max(...rects.map(r => r.left)), + right: Math.min(...rects.map(r => r.right)), + }; + + return result.top < result.bottom && result.left < result.right ? result : null; +} diff --git a/packages/roosterjs-editor-dom/lib/utils/getPendableFormatState.ts b/packages/roosterjs-editor-dom/lib/utils/getPendableFormatState.ts index 18d023165252..526a72d55008 100644 --- a/packages/roosterjs-editor-dom/lib/utils/getPendableFormatState.ts +++ b/packages/roosterjs-editor-dom/lib/utils/getPendableFormatState.ts @@ -1,3 +1,4 @@ +import getObjectKeys from '../jsUtils/getObjectKeys'; import { DocumentCommand, PendableFormatState } from 'roosterjs-editor-types'; /** @@ -46,7 +47,7 @@ export const PendableFormatCommandMap: { [key in PendableFormatNames]: DocumentC * @returns A PendableFormatState object which contains the values of pendable format states */ export default function getPendableFormatState(document: Document): PendableFormatState { - let keys = Object.keys(PendableFormatCommandMap) as PendableFormatNames[]; + let keys = getObjectKeys(PendableFormatCommandMap); return keys.reduce((state, key) => { state[key] = document.queryCommandState(PendableFormatCommandMap[key]); diff --git a/packages/roosterjs-editor-dom/lib/utils/getTagOfNode.ts b/packages/roosterjs-editor-dom/lib/utils/getTagOfNode.ts index a486a3b66c32..a8729da7aa4f 100644 --- a/packages/roosterjs-editor-dom/lib/utils/getTagOfNode.ts +++ b/packages/roosterjs-editor-dom/lib/utils/getTagOfNode.ts @@ -5,6 +5,6 @@ import { NodeType } from 'roosterjs-editor-types'; * @param node The node to get tag of * @returns Tag name in upper case if the given node is an Element, or empty string otherwise */ -export default function getTagOfNode(node: Node): string { +export default function getTagOfNode(node: Node | null): string { return node && node.nodeType == NodeType.Element ? (node).tagName.toUpperCase() : ''; } diff --git a/packages/roosterjs-editor-dom/lib/utils/isNodeAfter.ts b/packages/roosterjs-editor-dom/lib/utils/isNodeAfter.ts index b76d9860cc6f..a8a01a32ba17 100644 --- a/packages/roosterjs-editor-dom/lib/utils/isNodeAfter.ts +++ b/packages/roosterjs-editor-dom/lib/utils/isNodeAfter.ts @@ -1,7 +1,6 @@ import { DocumentPosition } from 'roosterjs-editor-types'; /** - * @internal * Checks if node1 is after node2 * @param node1 The node to check if it is after another node * @param node2 The node to check if another node is after this one diff --git a/packages/roosterjs-editor-dom/lib/utils/isNodeEmpty.ts b/packages/roosterjs-editor-dom/lib/utils/isNodeEmpty.ts index 06193de00389..294fac9b23b6 100644 --- a/packages/roosterjs-editor-dom/lib/utils/isNodeEmpty.ts +++ b/packages/roosterjs-editor-dom/lib/utils/isNodeEmpty.ts @@ -12,7 +12,11 @@ const ZERO_WIDTH_SPACE = /\u200b/g; * Default value is false * @returns True if there isn't any visible element inside node, otherwise false */ -export default function isNodeEmpty(node: Node, trimContent?: boolean) { +export default function isNodeEmpty( + node: Node, + trimContent?: boolean, + shouldCountBrAsVisible?: boolean +) { if (!node) { return false; } else if (node.nodeType == NodeType.Text) { @@ -20,10 +24,13 @@ export default function isNodeEmpty(node: Node, trimContent?: boolean) { } else if (node.nodeType == NodeType.Element) { let element = node as Element; let textContent = trim(element.textContent || '', trimContent); + const visibleSelector = shouldCountBrAsVisible + ? `${VISIBLE_CHILD_ELEMENT_SELECTOR},BR` + : VISIBLE_CHILD_ELEMENT_SELECTOR; if ( textContent != '' || VISIBLE_ELEMENT_TAGS.indexOf(getTagOfNode(element)) >= 0 || - element.querySelectorAll(VISIBLE_CHILD_ELEMENT_SELECTOR)[0] + element.querySelectorAll(visibleSelector)[0] ) { return false; } diff --git a/packages/roosterjs-editor-dom/lib/utils/matchLink.ts b/packages/roosterjs-editor-dom/lib/utils/matchLink.ts index a45f33ca9bbf..68481df4810a 100644 --- a/packages/roosterjs-editor-dom/lib/utils/matchLink.ts +++ b/packages/roosterjs-editor-dom/lib/utils/matchLink.ts @@ -1,3 +1,4 @@ +import getObjectKeys from '../jsUtils/getObjectKeys'; import { LinkData } from 'roosterjs-editor-types'; interface LinkMatchRule { @@ -33,7 +34,7 @@ const domainNameRegEx = `(?:${labelRegEx}\\.)*${labelRegEx}`; const domainPortRegEx = `${domainNameRegEx}(?:\\:[0-9]+)?`; const domainPortWithUrlRegEx = `${domainPortRegEx}(?:[\\/\\?]\\S*)?`; -const linkMatchRules: { [schema: string]: LinkMatchRule } = { +const linkMatchRules: Record = { http: { match: new RegExp( `^(?:microsoft-edge:)?http:\\/\\/${domainPortWithUrlRegEx}|www\\.${domainPortWithUrlRegEx}`, @@ -76,7 +77,7 @@ const linkMatchRules: { [schema: string]: LinkMatchRule } = { */ export default function matchLink(url: string): LinkData | null { if (url) { - for (let schema of Object.keys(linkMatchRules)) { + for (let schema of getObjectKeys(linkMatchRules)) { let rule = linkMatchRules[schema]; let matches = url.match(rule.match); if (matches && matches[0] == url && (!rule.except || !rule.except.test(url))) { diff --git a/packages/roosterjs-editor-dom/lib/utils/normalizeRect.ts b/packages/roosterjs-editor-dom/lib/utils/normalizeRect.ts index d5678e2bee92..1635db5646c5 100644 --- a/packages/roosterjs-editor-dom/lib/utils/normalizeRect.ts +++ b/packages/roosterjs-editor-dom/lib/utils/normalizeRect.ts @@ -7,12 +7,12 @@ import { Rect } from 'roosterjs-editor-types'; export default function normalizeRect(clientRect: DOMRect): Rect | null { let { left, right, top, bottom } = clientRect || { left: 0, right: 0, top: 0, bottom: 0 }; - return left + right + top + bottom > 0 - ? { + return left === 0 && right === 0 && top === 0 && bottom === 0 + ? null + : { left: Math.round(left), right: Math.round(right), top: Math.round(top), bottom: Math.round(bottom), - } - : null; + }; } diff --git a/packages/roosterjs-editor-dom/lib/utils/parseColor.ts b/packages/roosterjs-editor-dom/lib/utils/parseColor.ts new file mode 100644 index 000000000000..84dc9688c4c5 --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/utils/parseColor.ts @@ -0,0 +1,29 @@ +const HEX3_REGEX = /^#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])$/; +const HEX6_REGEX = /^#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})$/; +const RGB_REGEX = /^rgb\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*\)$/; +const RGBA_REGEX = /^rgba\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*\)$/; + +/** + * Parse color string to r/g/b value. + * If the given color is not in a recognized format, return null + */ +export default function parseColor(color: string): [number, number, number] | null { + color = (color || '').trim(); + + let match: RegExpMatchArray | null; + if ((match = color.match(HEX3_REGEX))) { + return [ + parseInt(match[1] + match[1], 16), + parseInt(match[2] + match[2], 16), + parseInt(match[3] + match[3], 16), + ]; + } else if ((match = color.match(HEX6_REGEX))) { + return [parseInt(match[1], 16), parseInt(match[2], 16), parseInt(match[3], 16)]; + } else if ((match = color.match(RGB_REGEX) || color.match(RGBA_REGEX))) { + return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])]; + } else { + // CSS color names such as red, green is not included for now. + // If need, we can add those colors from https://www.w3.org/wiki/CSS/Properties/color/keywords + return null; + } +} diff --git a/packages/roosterjs-editor-dom/lib/utils/queryElements.ts b/packages/roosterjs-editor-dom/lib/utils/queryElements.ts index c1abc9a16a45..adf099719a2b 100644 --- a/packages/roosterjs-editor-dom/lib/utils/queryElements.ts +++ b/packages/roosterjs-editor-dom/lib/utils/queryElements.ts @@ -1,5 +1,6 @@ -import toArray from './toArray'; +import toArray from '../jsUtils/toArray'; import { DocumentPosition, NodeType, QueryScope } from 'roosterjs-editor-types'; +import type { CompatibleQueryScope } from 'roosterjs-editor-types/lib/compatibleTypes'; /** * Query HTML elements in the container by a selector string @@ -13,8 +14,8 @@ import { DocumentPosition, NodeType, QueryScope } from 'roosterjs-editor-types'; export default function queryElements( container: ParentNode, selector: string, - forEachCallback?: (node: HTMLElement) => any, - scope: QueryScope = QueryScope.Body, + forEachCallback?: ((node: HTMLElement) => any) | null, + scope: QueryScope | CompatibleQueryScope = QueryScope.Body, range?: Range ): HTMLElement[] { if (!container || !selector) { diff --git a/packages/roosterjs-editor-dom/lib/utils/safeInstanceOf.ts b/packages/roosterjs-editor-dom/lib/utils/safeInstanceOf.ts index 702277aef4a6..111baa9d7a0c 100644 --- a/packages/roosterjs-editor-dom/lib/utils/safeInstanceOf.ts +++ b/packages/roosterjs-editor-dom/lib/utils/safeInstanceOf.ts @@ -6,10 +6,9 @@ import { TargetWindow } from 'roosterjs-editor-types'; /** * @internal Export for test only * Try get window from the given node or range - * @param source Source node or range + * @param node Source node to get window from */ -export function getTargetWindow(source: Node | Range): T { - const node = source && ((source).commonAncestorContainer || source); +export function getTargetWindow(node: Node): T { const document = node && (node.ownerDocument || @@ -31,6 +30,13 @@ export default function safeInstanceOfobj)?.commonAncestorContainer + ); + } + const targetWindow = getTargetWindow(obj); const targetType = targetWindow && (targetWindow[typeName] as any); const mainWindow = (window as any) as W; diff --git a/packages/roosterjs-editor-dom/lib/utils/setColor.ts b/packages/roosterjs-editor-dom/lib/utils/setColor.ts index e44069e95e40..93301c87305b 100644 --- a/packages/roosterjs-editor-dom/lib/utils/setColor.ts +++ b/packages/roosterjs-editor-dom/lib/utils/setColor.ts @@ -1,4 +1,20 @@ -import { DarkModeDatasetNames, ModeIndependentColor } from 'roosterjs-editor-types'; +import parseColor from './parseColor'; +import { DarkColorHandler, ModeIndependentColor } from 'roosterjs-editor-types'; + +const WHITE = '#ffffff'; +const GRAY = '#333333'; +const BLACK = '#000000'; +const TRANSPARENT = 'transparent'; +const enum ColorTones { + BRIGHT, + DARK, + NONE, +} + +//Using the HSL (hue, saturation and lightness) representation for RGB color values, if the value of the lightness is less than 20, the color is dark +const DARK_COLORS_LIGHTNESS = 20; +//If the value of the lightness is more than 80, the color is bright +const BRIGHT_COLORS_LIGHTNESS = 80; /** * Set text color or background color to the given element @@ -6,33 +22,125 @@ import { DarkModeDatasetNames, ModeIndependentColor } from 'roosterjs-editor-typ * @param color The color to set, it can be a string of color name/value or a ModeIndependentColor object * @param isBackgroundColor Whether set background color or text color * @param isDarkMode Whether current mode is dark mode. @default false + * @param shouldAdaptTheFontColor Whether the font color needs to be adapted to be visible in a dark or bright background color. @default false + * @param darkColorHandler A dark color handler object. This is now required. + * We keep it optional only for backward compatibility. If it is not passed, color will not be set. */ export default function setColor( element: HTMLElement, color: string | ModeIndependentColor, isBackgroundColor: boolean, - isDarkMode?: boolean + isDarkMode?: boolean, + shouldAdaptTheFontColor?: boolean, + darkColorHandler?: DarkColorHandler | null ) { const colorString = typeof color === 'string' ? color.trim() : ''; const modeIndependentColor = typeof color === 'string' ? null : color; + const cssName = isBackgroundColor ? 'background-color' : 'color'; if (colorString || modeIndependentColor) { - element.style.setProperty( - isBackgroundColor ? 'background-color' : 'color', - (isDarkMode - ? modeIndependentColor?.darkModeColor - : modeIndependentColor?.lightModeColor) || colorString - ); - - if (element.dataset) { - const dataSetName = isBackgroundColor - ? DarkModeDatasetNames.OriginalStyleBackgroundColor - : DarkModeDatasetNames.OriginalStyleColor; - if (!isDarkMode) { - delete element.dataset[dataSetName]; - } else if (modeIndependentColor) { - element.dataset[dataSetName] = modeIndependentColor.lightModeColor; - } + if (darkColorHandler) { + const colorValue = darkColorHandler.registerColor( + modeIndependentColor?.lightModeColor || colorString, + !!isDarkMode, + modeIndependentColor?.darkModeColor + ); + + element.style.setProperty(cssName, colorValue); } + + if (isBackgroundColor && shouldAdaptTheFontColor) { + adaptFontColorToBackgroundColor( + element, + modeIndependentColor?.lightModeColor || colorString, + isDarkMode, + darkColorHandler + ); + } + } +} + +/** + * Change the font color to white or some other color, so the text can be visible with a darker background + * @param element The element that contains text. + * @param lightModeBackgroundColor Existing background color in light mode + * @param isDarkMode Whether the content is in dark mode + * @param darkColorHandler A dark color handler object. This is now required. + * We keep it optional only for backward compatibility. If it is not passed, color will not be set. + */ +function adaptFontColorToBackgroundColor( + element: HTMLElement, + lightModeBackgroundColor: string, + isDarkMode?: boolean, + darkColorHandler?: DarkColorHandler | null +) { + if (!lightModeBackgroundColor || lightModeBackgroundColor === TRANSPARENT) { + return; + } + + const isADarkOrBrightOrNone = isADarkOrBrightColor(lightModeBackgroundColor!); + + switch (isADarkOrBrightOrNone) { + case ColorTones.DARK: + const fontForDark: ModeIndependentColor = { + lightModeColor: WHITE, + darkModeColor: GRAY, + }; + setColor( + element, + fontForDark, + false /*isBackground*/, + isDarkMode, + false /*shouldAdaptFontColor*/, + darkColorHandler + ); + break; + case ColorTones.BRIGHT: + const fontForLight: ModeIndependentColor = { + lightModeColor: BLACK, + darkModeColor: WHITE, + }; + setColor( + element, + fontForLight, + false /*isBackground*/, + isDarkMode, + false /*shouldAdaptFontColor*/, + darkColorHandler + ); + break; + } +} + +function isADarkOrBrightColor(color: string): ColorTones { + let lightness = calculateLightness(color); + if (lightness < DARK_COLORS_LIGHTNESS) { + return ColorTones.DARK; + } else if (lightness > BRIGHT_COLORS_LIGHTNESS) { + return ColorTones.BRIGHT; + } + + return ColorTones.NONE; +} + +/** + * Calculate the lightness of HSL (hue, saturation and lightness) representation + * @param color a RBG or RGBA COLOR + * @returns + */ +function calculateLightness(color: string) { + const colorValues = parseColor(color); + + // Use the values of r,g,b to calculate the lightness in the HSl representation + //First calculate the fraction of the light in each color, since in css the value of r,g,b is in the interval of [0,255], we have + if (colorValues) { + const red = colorValues[0] / 255; + const green = colorValues[1] / 255; + const blue = colorValues[2] / 255; + + //Then the lightness in the HSL representation is the average between maximum fraction of r,g,b and the minimum fraction + return (Math.max(red, green, blue) + Math.min(red, green, blue)) * 50; + } else { + return 255; } } diff --git a/packages/roosterjs-editor-dom/lib/utils/shouldSkipNode.ts b/packages/roosterjs-editor-dom/lib/utils/shouldSkipNode.ts index d4421048a241..4e3dfd4ecd40 100644 --- a/packages/roosterjs-editor-dom/lib/utils/shouldSkipNode.ts +++ b/packages/roosterjs-editor-dom/lib/utils/shouldSkipNode.ts @@ -2,7 +2,7 @@ import getTagOfNode from './getTagOfNode'; import { getComputedStyle } from './getComputedStyles'; import { NodeType } from 'roosterjs-editor-types'; -const CRLF = /^[\r\n]+$/gm; +const CRLF = /^[\r\n]+$/g; const CRLF_SPACE = /[\t\r\n\u0020\u200B]/gm; // We should only find new line, real space or ZeroWidthSpace (TAB, %20, but not  ) /** diff --git a/packages/roosterjs-editor-dom/lib/utils/splitParentNode.ts b/packages/roosterjs-editor-dom/lib/utils/splitParentNode.ts index 9bd52fe549bf..6440f7918822 100644 --- a/packages/roosterjs-editor-dom/lib/utils/splitParentNode.ts +++ b/packages/roosterjs-editor-dom/lib/utils/splitParentNode.ts @@ -48,10 +48,10 @@ export default function splitParentNode(node: Node, splitBefore: boolean): Node * If two or nodes are passed, will split before the first one and after the last one, all other nodes will be ignored * @returns The parent node of the given node range if the given nodes are balanced, otherwise null */ -export function splitBalancedNodeRange(nodes: Node | Node[]): HTMLElement { +export function splitBalancedNodeRange(nodes: Node | Node[]): Node | null { let start = Array.isArray(nodes) ? nodes[0] : nodes; let end = Array.isArray(nodes) ? nodes[nodes.length - 1] : nodes; - let parentNode = start && end && start.parentNode == end.parentNode ? start.parentNode : null; + const parentNode = start && end && start.parentNode == end.parentNode ? start.parentNode : null; if (parentNode) { if (isNodeAfter(start, end)) { let temp = end; @@ -62,5 +62,5 @@ export function splitBalancedNodeRange(nodes: Node | Node[]): HTMLElement { splitParentNode(end, false /*splitBefore*/); } - return parentNode as HTMLElement; + return parentNode; } diff --git a/packages/roosterjs-editor-dom/lib/utils/tsconfig.child.json b/packages/roosterjs-editor-dom/lib/utils/tsconfig.child.json deleted file mode 100644 index a9db8b7f7e90..000000000000 --- a/packages/roosterjs-editor-dom/lib/utils/tsconfig.child.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "strict": true - }, - "extends": "../../../tsconfig.json", - "include": ["./**/*.ts"], - "references": [{ "path": "../../../roosterjs-editor-types/tsconfig.child.json" }] -} diff --git a/packages/roosterjs-editor-dom/lib/utils/wrap.ts b/packages/roosterjs-editor-dom/lib/utils/wrap.ts index b2cf6d284cc1..c57efc48c689 100644 --- a/packages/roosterjs-editor-dom/lib/utils/wrap.ts +++ b/packages/roosterjs-editor-dom/lib/utils/wrap.ts @@ -2,6 +2,7 @@ import createElement from './createElement'; import fromHtml from './fromHtml'; import safeInstanceOf from './safeInstanceOf'; import { CreateElementData, KnownCreateElementDataIndex } from 'roosterjs-editor-types'; +import type { CompatibleKnownCreateElementDataIndex } from 'roosterjs-editor-types/lib/compatibleTypes'; /** * Wrap all the node with html and return the wrapped node, and put the wrapper node under the parent of the first node @@ -38,12 +39,20 @@ export default function wrap(nodes: Node | Node[], wrapper?: HTMLElement): HTMLE */ export default function wrap( nodes: Node | Node[], - wrapper?: CreateElementData | KnownCreateElementDataIndex + wrapper?: + | CreateElementData + | KnownCreateElementDataIndex + | CompatibleKnownCreateElementDataIndex ): HTMLElement; export default function wrap( nodes: Node | Node[], - wrapper?: string | HTMLElement | CreateElementData | KnownCreateElementDataIndex + wrapper?: + | string + | HTMLElement + | CreateElementData + | KnownCreateElementDataIndex + | CompatibleKnownCreateElementDataIndex ): HTMLElement | null { nodes = !nodes ? [] : safeInstanceOf(nodes, 'Node') ? [nodes] : nodes; if (nodes.length == 0 || !nodes[0] || !nodes[0].ownerDocument) { diff --git a/packages/roosterjs-editor-dom/package.json b/packages/roosterjs-editor-dom/package.json index 4032d57c14b0..970a59c83a8e 100644 --- a/packages/roosterjs-editor-dom/package.json +++ b/packages/roosterjs-editor-dom/package.json @@ -2,6 +2,7 @@ "name": "roosterjs-editor-dom", "description": "Object Model for roosterjs", "dependencies": { + "tslib": "^2.3.1", "roosterjs-editor-types": "" }, "main": "./lib/index.ts" diff --git a/packages/roosterjs-editor-dom/test/DomTestHelper.ts b/packages/roosterjs-editor-dom/test/DomTestHelper.ts index 58036448a7a1..6ab05327dc0f 100644 --- a/packages/roosterjs-editor-dom/test/DomTestHelper.ts +++ b/packages/roosterjs-editor-dom/test/DomTestHelper.ts @@ -2,6 +2,7 @@ import createRange from '../lib/selection/createRange'; import getInlineElementAtNode from '../lib/inlineElements/getInlineElementAtNode'; import NodeBlockElement from '../lib/blockElements/NodeBlockElement'; import StartEndBlockElement from '../lib/blockElements/StartEndBlockElement'; +import { Browser } from '../lib/utils/Browser'; import { InlineElement, NodePosition } from 'roosterjs-editor-types'; // Create element with content and id and insert the element in the DOM @@ -103,14 +104,12 @@ export function htmlToDom(html: string): Node[] { return [].slice.call(element.childNodes); } -declare var __karma__: any; - export function itFirefoxOnly( expectation: string, assertion?: jasmine.ImplementationCallback, timeout?: number ) { - const func = __karma__.config.browser == 'Chrome' ? xit : it; + const func = Browser.isFirefox ? it : xit; return func(expectation, assertion, timeout); } @@ -119,6 +118,6 @@ export function itChromeOnly( assertion?: jasmine.ImplementationCallback, timeout?: number ) { - const func = __karma__.config.browser == 'Chrome' ? it : xit; + const func = Browser.isChrome ? it : xit; return func(expectation, assertion, timeout); } diff --git a/packages/roosterjs-editor-dom/test/clipboard/extractClipboardItemsTest.ts b/packages/roosterjs-editor-dom/test/clipboard/extractClipboardItemsTest.ts index 92fc8c726c7b..68037d4308f4 100644 --- a/packages/roosterjs-editor-dom/test/clipboard/extractClipboardItemsTest.ts +++ b/packages/roosterjs-editor-dom/test/clipboard/extractClipboardItemsTest.ts @@ -1,233 +1,316 @@ -import extractClipboardItems from '../../lib/clipboard/extractClipboardItems'; -import { EdgeLinkPreview } from 'roosterjs-editor-types'; - -describe('extractClipboardItems', () => { - function throwError(): any { - throw new Error('Should never call'); - } - - function createStringItem(type: string, textValue: string): DataTransferItem { - return { - kind: 'string', - type, - getAsFile: throwError, - getAsString: (callback: FunctionStringCallback) => { - callback(textValue); - }, - webkitGetAsEntry: throwError, - }; - } - - function createFile(type: string, stringValue: string) { - const byteString = atob(stringValue); - const arrayBuilder = new ArrayBuffer(byteString.length); - const unit8Array = new Uint8Array(arrayBuilder); - - for (let i = 0; i < byteString.length; i++) { - unit8Array[i] = byteString.charCodeAt(i); - } - - return new File([arrayBuilder], 'image.png', { type: type }); - } - - function createFileItem(type: string, file: File): DataTransferItem { - return { - kind: 'file', - type, - getAsFile: () => file, - getAsString: throwError, - webkitGetAsEntry: throwError, - }; - } - - it('null input', async () => { - const clipboardData = await extractClipboardItems(null); - expect(clipboardData).toEqual({ - types: [], - text: '', - image: null, - rawHtml: null, - customValues: {}, - }); - }); - - it('empty array input', async () => { - const clipboardData = await extractClipboardItems([]); - expect(clipboardData).toEqual({ - types: [], - text: '', - image: null, - rawHtml: null, - customValues: {}, - }); - }); - - it('input with HTML', async () => { - const html = '
    html
    '; - const clipboardData = await extractClipboardItems([createStringItem('text/html', html)]); - expect(clipboardData).toEqual({ - types: ['text/html'], - text: '', - image: null, - rawHtml: html, - customValues: {}, - }); - }); - - it('input with text', async () => { - const text = 'This is a test'; - const clipboardData = await extractClipboardItems([createStringItem('text/plain', text)]); - expect(clipboardData).toEqual({ - types: ['text/plain'], - text: text, - image: null, - rawHtml: null, - customValues: {}, - }); - }); - - it('input with image', async () => { - const stringValue = 'AAAABBBBCCCC'; - const type = 'image/png'; - const file = createFile(type, stringValue); - const clipboardData = await extractClipboardItems([createFileItem(type, file)]); - expect(clipboardData).toEqual({ - types: [type], - text: '', - image: file, - imageDataUri: `data:${type};base64,${stringValue}`, - rawHtml: null, - customValues: {}, - }); - }); - - it('input with text,html,image', async () => { - const text = 'This is a text'; - const html = '
    html
    '; - const stringValue = 'AAAABBBBCCCC'; - const type = 'image/png'; - const file = createFile(type, stringValue); - const clipboardData = await extractClipboardItems([ - createStringItem('text/html', html), - createStringItem('text/plain', text), - createFileItem(type, file), - ]); - expect(clipboardData).toEqual({ - types: ['text/html', 'text/plain', type], - text: text, - image: file, - imageDataUri: `data:${type};base64,${stringValue}`, - rawHtml: html, - customValues: {}, - }); - }); - - it('input with text,html, and multiple images', async () => { - const text = 'This is a text'; - const html = '
    html
    '; - const stringValue1 = 'AAAABBBBCCCC'; - const stringValue2 = 'DDDDEEEEFFFF'; - const type = 'image/png'; - const file1 = createFile(type, stringValue1); - const file2 = createFile(type, stringValue2); - const clipboardData = await extractClipboardItems([ - createStringItem('text/html', html), - createStringItem('text/plain', text), - createFileItem(type, file1), - createFileItem(type, file2), - ]); - expect(clipboardData).toEqual({ - types: ['text/html', 'text/plain', type], - text: text, - image: file1, - imageDataUri: `data:${type};base64,${stringValue1}`, - rawHtml: html, - customValues: {}, - }); - }); - - it('input with text,html, and unrecognized type', async () => { - const text = 'This is a text'; - const html = '
    html
    '; - const clipboardData = await extractClipboardItems([ - createStringItem('text/html', html), - createStringItem('text/plain', text), - createStringItem('text/unknown', 'test'), - ]); - expect(clipboardData).toEqual({ - types: ['text/html', 'text/plain'], - text: text, - image: null, - rawHtml: html, - customValues: {}, - }); - }); - - it('input with text,html,and known custom type', async () => { - const text = 'This is a text'; - const html = '
    html
    '; - const customInput = 'This is a known custom type'; - const customType = 'text/known'; - const clipboardData = await extractClipboardItems( - [ - createStringItem('text/html', html), - createStringItem('text/plain', text), - createStringItem(customType, customInput), - ], - { allowedCustomPasteType: ['known'] } - ); - expect(clipboardData).toEqual({ - types: ['text/html', 'text/plain', customType], - text: text, - image: null, - rawHtml: html, - customValues: { - known: customInput, - }, - }); - }); - - it('input with text,html,and edge link preview', async () => { - const text = 'This is a text'; - const html = '
    html
    '; - const linkPreview: EdgeLinkPreview = { - domain: 'test.com', - preferred_format: 'text/html', - title: 'Test', - type: 'website', - url: 'test url', - }; - const customType = 'text/link-preview'; - const customValue = JSON.stringify(linkPreview); - const clipboardData = await extractClipboardItems( - [ - createStringItem('text/html', html), - createStringItem('text/plain', text), - createStringItem(customType, customValue), - ], - { allowLinkPreview: true } - ); - expect(clipboardData).toEqual({ - types: ['text/html', 'text/plain', customType], - text: text, - image: null, - rawHtml: html, - customValues: { - ['link-preview']: customValue, - }, - linkPreview: linkPreview, - }); - }); - - it('input with svg text', async () => { - const svg = 'test'; - const clipboardData = await extractClipboardItems([createStringItem('image/svg+xml', svg)]); - expect(clipboardData).toEqual({ - types: [], - text: '', - image: null, - rawHtml: null, - customValues: {}, - }); - }); -}); +import extractClipboardItems from '../../lib/clipboard/extractClipboardItems'; +import { EdgeLinkPreview } from 'roosterjs-editor-types'; + +describe('extractClipboardItems', () => { + function throwError(): any { + throw new Error('Should never call'); + } + + function createStringItem(type: string, textValue: string): DataTransferItem { + return { + kind: 'string', + type, + getAsFile: throwError, + getAsString: (callback: FunctionStringCallback) => { + callback(textValue); + }, + webkitGetAsEntry: throwError, + }; + } + + function createFile(type: string, fileNameAndExtension: string, stringValue: string) { + const byteString = atob(stringValue); + const arrayBuilder = new ArrayBuffer(byteString.length); + const unit8Array = new Uint8Array(arrayBuilder); + + for (let i = 0; i < byteString.length; i++) { + unit8Array[i] = byteString.charCodeAt(i); + } + + return new File([arrayBuilder], fileNameAndExtension, { type: type }); + } + + function createFileItem(type: string, file: File): DataTransferItem { + return { + kind: 'file', + type, + getAsFile: () => file, + getAsString: throwError, + webkitGetAsEntry: throwError, + }; + } + + it('null input', async () => { + const clipboardData = await extractClipboardItems(null); + expect(clipboardData).toEqual({ + types: [], + text: '', + image: null, + files: [], + rawHtml: null, + customValues: {}, + }); + }); + + it('empty array input', async () => { + const clipboardData = await extractClipboardItems([]); + expect(clipboardData).toEqual({ + types: [], + text: '', + image: null, + files: [], + rawHtml: null, + customValues: {}, + }); + }); + + it('input with HTML', async () => { + const html = '
    html
    '; + const clipboardData = await extractClipboardItems([createStringItem('text/html', html)]); + expect(clipboardData).toEqual({ + types: ['text/html'], + text: '', + image: null, + files: [], + rawHtml: html, + customValues: {}, + }); + }); + + it('input with text', async () => { + const text = 'This is a test'; + const clipboardData = await extractClipboardItems([createStringItem('text/plain', text)]); + expect(clipboardData).toEqual({ + types: ['text/plain'], + text: text, + image: null, + files: [], + rawHtml: null, + customValues: {}, + }); + }); + + it('input with image', async () => { + const stringValue = 'AAAABBBBCCCC'; + const type = 'image/png'; + const file = createFile(type, 'image.png', stringValue); + const clipboardData = await extractClipboardItems([createFileItem(type, file)]); + expect(clipboardData).toEqual({ + types: [type], + text: '', + image: file, + files: [], + imageDataUri: `data:${type};base64,${stringValue}`, + rawHtml: null, + customValues: {}, + }); + }); + + it('input with file', async () => { + const stringValue = 'AAAABBBBCCCC'; + const type = 'application/pdf'; + const file = createFile(type, 'document.pdf', stringValue); + const clipboardData = await extractClipboardItems([createFileItem(type, file)]); + expect(clipboardData).toEqual({ + types: [type], + text: '', + image: null, + files: [file], + rawHtml: null, + customValues: {}, + }); + }); + + it('input with null file', async () => { + const type = 'application/pdf'; + const transferItem: DataTransferItem = { + kind: 'file', + type, + getAsFile: () => null, + getAsString: throwError, + webkitGetAsEntry: throwError, + }; + const clipboardData = await extractClipboardItems([transferItem]); + expect(clipboardData).toEqual({ + types: [], + text: '', + image: null, + files: [], + rawHtml: null, + customValues: {}, + }); + }); + + it('input with multiple files', async () => { + const stringValue1 = 'AAAABBBBCCCC'; + const stringValue2 = 'DDDDEEEEFFFF'; + const pdfType = 'application/pdf'; + const textType = 'text/plain'; + const pdfFile = createFile(pdfType, 'document.pdf', stringValue1); + const textFile = createFile(textType, 'hello.txt', stringValue2); + const clipboardData = await extractClipboardItems([ + createFileItem(pdfType, pdfFile), + createFileItem(textType, textFile), + ]); + expect(clipboardData).toEqual({ + types: [pdfType, textType], + text: '', + image: null, + files: [pdfFile, textFile], + rawHtml: null, + customValues: {}, + }); + }); + + it('input with text,html,image,file', async () => { + const text = 'This is a text'; + const html = '
    html
    '; + const stringValue1 = 'AAAABBBBCCCC'; + const stringValue2 = 'DDDDEEEEFFFF'; + const imageType = 'image/png'; + const imageFile = createFile(imageType, 'image.png', stringValue1); + const pdfType = 'application/pdf'; + const pdfFile = createFile(pdfType, 'document.pdf', stringValue2); + const clipboardData = await extractClipboardItems([ + createStringItem('text/html', html), + createStringItem('text/plain', text), + createFileItem(imageType, imageFile), + createFileItem(pdfType, pdfFile), + ]); + expect(clipboardData).toEqual({ + types: ['text/html', 'text/plain', imageType, pdfType], + text: text, + image: imageFile, + files: [pdfFile], + imageDataUri: `data:${imageType};base64,${stringValue1}`, + rawHtml: html, + customValues: {}, + }); + }); + + it('input with text,html, multiple images and multiple files', async () => { + const text = 'This is a text'; + const html = '
    html
    '; + const stringValue1 = 'AAAABBBBCCCC'; + const stringValue2 = 'DDDDEEEEFFFF'; + const stringValue3 = 'GGGGHHHHIIII'; + const stringValue4 = 'JJJJKKKKLLLL'; + + const imageType = 'image/png'; + const file1 = createFile(imageType, 'image.png', stringValue1); + const file2 = createFile(imageType, 'image.png', stringValue2); + + const textType = 'text/plain'; + const file3 = createFile(textType, 'hello.txt', stringValue3); + + const pdfType = 'application/pdf'; + const file4 = createFile(pdfType, 'document.pdf', stringValue4); + + const clipboardData = await extractClipboardItems([ + createStringItem('text/html', html), + createStringItem('text/plain', text), + createFileItem(imageType, file1), + createFileItem(imageType, file2), + createFileItem(textType, file3), + createFileItem(pdfType, file4), + ]); + expect(clipboardData).toEqual({ + types: ['text/html', 'text/plain', imageType, imageType, textType, pdfType], + text: text, + image: file1, + files: [file2, file3, file4], + imageDataUri: `data:${imageType};base64,${stringValue1}`, + rawHtml: html, + customValues: {}, + }); + }); + + it('input with text,html, and unrecognized type', async () => { + const text = 'This is a text'; + const html = '
    html
    '; + const clipboardData = await extractClipboardItems([ + createStringItem('text/html', html), + createStringItem('text/plain', text), + createStringItem('text/unknown', 'test'), + ]); + expect(clipboardData).toEqual({ + types: ['text/html', 'text/plain'], + text: text, + image: null, + files: [], + rawHtml: html, + customValues: {}, + }); + }); + + it('input with text,html,and known custom type', async () => { + const text = 'This is a text'; + const html = '
    html
    '; + const customInput = 'This is a known custom type'; + const customType = 'text/known'; + const clipboardData = await extractClipboardItems( + [ + createStringItem('text/html', html), + createStringItem('text/plain', text), + createStringItem(customType, customInput), + ], + { allowedCustomPasteType: ['known'] } + ); + expect(clipboardData).toEqual({ + types: ['text/html', 'text/plain', customType], + text: text, + image: null, + files: [], + rawHtml: html, + customValues: { + known: customInput, + }, + }); + }); + + it('input with text,html,and edge link preview', async () => { + const text = 'This is a text'; + const html = '
    html
    '; + const linkPreview: EdgeLinkPreview = { + domain: 'test.com', + preferred_format: 'text/html', + title: 'Test', + type: 'website', + url: 'test url', + }; + const customType = 'text/link-preview'; + const customValue = JSON.stringify(linkPreview); + const clipboardData = await extractClipboardItems( + [ + createStringItem('text/html', html), + createStringItem('text/plain', text), + createStringItem(customType, customValue), + ], + { allowLinkPreview: true } + ); + expect(clipboardData).toEqual({ + types: ['text/html', 'text/plain', customType], + text: text, + image: null, + files: [], + rawHtml: html, + customValues: { + ['link-preview']: customValue, + }, + linkPreview: linkPreview, + }); + }); + + it('input with svg text', async () => { + const svg = 'test'; + const clipboardData = await extractClipboardItems([createStringItem('image/svg+xml', svg)]); + expect(clipboardData).toEqual({ + types: [], + text: '', + image: null, + files: [], + rawHtml: null, + customValues: {}, + }); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/clipboard/transformTabCharactersTest.ts b/packages/roosterjs-editor-dom/test/clipboard/transformTabCharactersTest.ts new file mode 100644 index 000000000000..b13b079ac5e6 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/clipboard/transformTabCharactersTest.ts @@ -0,0 +1,18 @@ +import { transformTabCharacters } from '../../lib/clipboard/handleTextPaste'; + +describe('transformTabCharacters', () => { + it('no \t', () => { + const input = 'hello world'; + expect(transformTabCharacters(input)).toBe(input); + }); + + it('1 \t', () => { + const input = '\tHello'; + expect(transformTabCharacters(input)).toBe('      Hello'); + }); + + it('complex', () => { + const input = '1\t234\t5'; + expect(transformTabCharacters(input)).toBe('1     234   5'); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/delimiter/addDelimitersTest.ts b/packages/roosterjs-editor-dom/test/delimiter/addDelimitersTest.ts new file mode 100644 index 000000000000..fb65af55189f --- /dev/null +++ b/packages/roosterjs-editor-dom/test/delimiter/addDelimitersTest.ts @@ -0,0 +1,48 @@ +import addDelimiters from '../../lib/delimiter/addDelimiters'; +import { DelimiterClasses } from 'roosterjs-editor-types'; + +describe('addDelimitersTest', () => { + afterAll(() => { + document.body.childNodes.forEach(node => { + document.body.removeChild(node); + }); + }); + + it('Add', () => { + const element = document.createElement('span'); + document.body.append(element); + + const [after, before] = addDelimiters(element); + + expect(element.nextElementSibling).toBeDefined(); + expect(element.nextElementSibling?.className).toEqual(DelimiterClasses.DELIMITER_AFTER); + expect(element.previousElementSibling).toBeDefined(); + expect(element.previousElementSibling?.className).toEqual( + DelimiterClasses.DELIMITER_BEFORE + ); + expect(after.outerHTML).toBe(''); + expect(before.outerHTML).toBe(''); + }); + + it('Add between other Entity with delimiters', () => { + const element1 = document.createElement('span'); + const element2 = document.createElement('span'); + const element3 = document.createElement('span'); + document.body.append(element1); + document.body.append(element2); + document.body.append(element3); + + addDelimiters(element1); + addDelimiters(element3); + addDelimiters(element2); + + [element1, element2, element3].forEach(element => { + expect(element.nextElementSibling).toBeDefined(); + expect(element.nextElementSibling?.className).toEqual(DelimiterClasses.DELIMITER_AFTER); + expect(element.previousElementSibling).toBeDefined(); + expect(element.previousElementSibling?.className).toEqual( + DelimiterClasses.DELIMITER_BEFORE + ); + }); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/delimiter/getDelimiterFromElementTest.ts b/packages/roosterjs-editor-dom/test/delimiter/getDelimiterFromElementTest.ts new file mode 100644 index 000000000000..974a8db01412 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/delimiter/getDelimiterFromElementTest.ts @@ -0,0 +1,88 @@ +import createElement from '../../lib/utils/createElement'; +import getDelimiterFromElement from '../../lib/delimiter/getDelimiterFromElement'; +import { DelimiterClasses } from 'roosterjs-editor-types'; + +const ZERO_WIDTH_SPACE = '\u200B'; + +describe('getDelimiterFromElementTest', () => { + afterEach(() => { + document.body.childNodes.forEach(node => { + document.body.removeChild(node); + }); + }); + + it('Is Delimiter', () => { + const result = getDelimiterFromElement(createEl()); + + expect(result).toBeTruthy(); + }); + + it('No ZWS', () => { + const result = getDelimiterFromElement( + createEl(false /* changeTag */, false /* changeClass */, true /* changeChildren */) + ); + + expect(result).toBeNull(); + }); + + it('No Class', () => { + const result = getDelimiterFromElement( + createEl(false /* changeTag */, true /* changeClass */, false /* changeChildren */) + ); + + expect(result).toBeNull(); + }); + + it('No Span', () => { + const result = getDelimiterFromElement( + createEl(true /* changeTag */, false /* changeClass */, false /* changeChildren */) + ); + + expect(result).toBeNull(); + }); + + it('No Span & class', () => { + const result = getDelimiterFromElement( + createEl(true /* changeTag */, true /* changeClass */, false /* changeChildren */) + ); + + expect(result).toBeNull(); + }); + + it('No ZWS & class', () => { + const result = getDelimiterFromElement( + createEl(false /* changeTag */, true /* changeClass */, true /* changeChildren */) + ); + + expect(result).toBeNull(); + }); + + it('No ZWS & SPAN', () => { + const result = getDelimiterFromElement( + createEl(true /* changeTag */, false /* changeClass */, true /* changeChildren */) + ); + + expect(result).toBeNull(); + }); + + it('Null', () => { + const result = getDelimiterFromElement(null); + + expect(result).toBeNull(); + }); +}); + +function createEl( + changeTag: boolean = false, + changeClass: boolean = false, + changeChildren: boolean = false +) { + return createElement( + { + tag: changeTag ? 'div' : 'span', + className: changeClass ? '' : DelimiterClasses.DELIMITER_AFTER, + children: changeChildren ? [] : [ZERO_WIDTH_SPACE], + }, + document + ); +} diff --git a/packages/roosterjs-editor-dom/test/entity/entityPlaceholderUtilsTest.ts b/packages/roosterjs-editor-dom/test/entity/entityPlaceholderUtilsTest.ts new file mode 100644 index 000000000000..9a85233775f7 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/entity/entityPlaceholderUtilsTest.ts @@ -0,0 +1,502 @@ +import { Entity } from 'roosterjs-editor-types'; +import { + createEntityPlaceholder, + moveContentWithEntityPlaceholders, + restoreContentWithEntityPlaceholder, +} from '../../lib/entity/entityPlaceholderUtils'; + +describe('createEntityPlaceholder', () => { + it('', () => { + const div = document.createElement('div'); + const entity: Entity = { + type: 'a', + id: 'b', + wrapper: div, + isReadonly: false, + }; + const placeholder = createEntityPlaceholder(entity); + + expect(placeholder.outerHTML).toBe(''); + }); +}); + +describe('moveContentWithEntityPlaceholders', () => { + it('empty dom', () => { + const div = document.createElement('div'); + const entities: Record = {}; + + const fragment = moveContentWithEntityPlaceholders(div, entities); + + const resultDiv = document.createElement('div'); + resultDiv.appendChild(fragment); + + expect(div.innerHTML).toBe(''); + expect(resultDiv.innerHTML).toBe(''); + expect(entities).toEqual({}); + }); + + it('no entity', () => { + const div = document.createElement('div'); + const entities: Record = {}; + + div.innerHTML = 'test1test2test3'; + + const fragment = moveContentWithEntityPlaceholders(div, entities); + + const resultDiv = document.createElement('div'); + resultDiv.appendChild(fragment); + + expect(div.innerHTML).toBe(''); + expect(resultDiv.innerHTML).toBe('test1test2test3'); + expect(entities).toEqual({}); + }); + + it('single entity', () => { + const div = document.createElement('div'); + const entities: Record = {}; + + div.innerHTML = '
    '; + + const fragment = moveContentWithEntityPlaceholders(div, entities); + + const resultDiv = document.createElement('div'); + resultDiv.appendChild(fragment); + + expect(div.innerHTML).toBe('
    '); + expect(resultDiv.innerHTML).toBe('
    '); + expect(entities).toEqual({ + a: div.firstChild as HTMLElement, + }); + }); + + it('two entities with other nodes', () => { + const div = document.createElement('div'); + const entities: Record = {}; + + const node1 = document.createTextNode('test1'); + const node2 = document.createElement('div'); + const node3 = document.createElement('div'); + const node4 = document.createElement('div'); + const node5 = document.createElement('div'); + + node2.className = '_Entity _EType_a _EId_a'; + node3.id = 'node3'; + node4.className = '_Entity _EType_b _EId_b'; + node5.textContent = 'test5'; + + div.appendChild(node1); + div.appendChild(node2); + div.appendChild(node3); + div.appendChild(node4); + div.appendChild(node5); + + const fragment = moveContentWithEntityPlaceholders(div, entities); + + const resultDiv = document.createElement('div'); + resultDiv.appendChild(fragment); + + expect(div.innerHTML).toBe( + '
    ' + ); + expect(resultDiv.innerHTML).toBe( + 'test1
    test5
    ' + ); + expect(entities).toEqual({ + a: node2, + b: node4, + }); + }); + + it('with inner entities', () => { + const div = document.createElement('div'); + const entities: Record = {}; + + const node1 = document.createTextNode('test1'); + const node2 = document.createElement('div'); + const node3 = document.createElement('div'); + const node4 = document.createElement('div'); + const node5 = document.createElement('div'); + + node2.className = '_Entity _EType_a _EId_a'; + node3.id = 'node3'; + node4.className = '_Entity _EType_b _EId_b'; + node5.textContent = 'test5'; + + node3.appendChild(node4); + + div.appendChild(node1); + div.appendChild(node2); + div.appendChild(node3); + div.appendChild(node5); + + const fragment = moveContentWithEntityPlaceholders(div, entities); + + const resultDiv = document.createElement('div'); + resultDiv.appendChild(fragment); + + expect(div.innerHTML).toBe('
    '); + expect(resultDiv.innerHTML).toBe( + 'test1
    test5
    ' + ); + expect(entities).toEqual({ + a: node2, + b: node4, + }); + }); +}); + +describe('restoreContentWithEntityPlaceholder', () => { + it('empty fragment', () => { + const target = document.createElement('div'); + const source = document.createDocumentFragment(); + + target.innerHTML = 'test'; + + restoreContentWithEntityPlaceholder(source, target, {}); + + expect(target.innerHTML).toBe(''); + expect(source.firstChild).toBeNull(); + expect(source.lastChild).toBeNull(); + }); + + it('fragment without entity', () => { + const target = document.createElement('div'); + const source = document.createDocumentFragment(); + + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + + div1.id = 'id1'; + div2.id = 'id2'; + + source.appendChild(div1); + source.appendChild(div2); + + target.innerHTML = 'test'; + + restoreContentWithEntityPlaceholder(source, target, {}); + + expect(target.innerHTML).toBe('
    '); + expect(source.firstChild).toBeNull(); + expect(source.lastChild).toBeNull(); + }); + + it('fragment with entity, no reuse', () => { + const target = document.createElement('div'); + const source = document.createDocumentFragment(); + + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + const placeholder = document.createElement('span'); + + placeholder.className = '_Entity _EType_Test _EId_entity1'; + + div1.id = 'id1'; + div2.id = 'id2'; + div1.appendChild(placeholder); + + source.appendChild(div1); + source.appendChild(div2); + + const wrapper = document.createElement('div'); + wrapper.id = 'entity1'; + + target.innerHTML = 'test'; + + restoreContentWithEntityPlaceholder(source, target, { + entity1: wrapper, + }); + + expect(target.innerHTML).toBe( + '
    ' + ); + expect(source.firstChild).toBeNull(); + expect(source.lastChild).toBeNull(); + }); + + it('1 reusable entity', () => { + const target = document.createElement('div'); + const source = document.createDocumentFragment(); + + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + const div3 = document.createElement('div'); + const div4 = document.createElement('div'); + const placeholder = document.createElement('span'); + + placeholder.className = '_Entity _EType_Test _EId_entity1'; + + div1.id = 'id1'; + div2.id = 'id2'; + div3.id = 'id3'; + div4.id = 'id4'; + + source.appendChild(div1); + source.appendChild(placeholder); + source.appendChild(div2); + + const wrapper = document.createElement('div'); + wrapper.id = 'entity1'; + + target.appendChild(div3); + target.appendChild(wrapper); + target.appendChild(div4); + + restoreContentWithEntityPlaceholder(source, target, { + entity1: wrapper, + }); + + expect(target.innerHTML).toBe( + '
    ' + ); + }); + + it('2 reusable entity side by side in source', () => { + const target = document.createElement('div'); + const source = document.createDocumentFragment(); + + const div1 = document.createElement('div'); + const div3 = document.createElement('div'); + const div4 = document.createElement('div'); + const div5 = document.createElement('div'); + const div6 = document.createElement('div'); + const placeholder1 = document.createElement('span'); + const placeholder2 = document.createElement('span'); + + placeholder1.className = '_Entity _EType_Test _EId_entity1'; + placeholder2.className = '_Entity _EType_Test _EId_entity2'; + + div1.id = 'id1'; + div3.id = 'id3'; + div4.id = 'id4'; + div5.id = 'id5'; + div6.id = 'id6'; + + source.appendChild(div1); + source.appendChild(placeholder1); + source.appendChild(placeholder2); + source.appendChild(div3); + + const wrapper1 = document.createElement('div'); + const wrapper2 = document.createElement('div'); + wrapper1.id = 'entity1'; + wrapper2.id = 'entity2'; + + target.appendChild(div4); + target.appendChild(wrapper1); + target.appendChild(div5); + target.appendChild(wrapper2); + target.appendChild(div6); + + restoreContentWithEntityPlaceholder(source, target, { + entity1: wrapper1, + entity2: wrapper2, + }); + + expect(target.innerHTML).toBe( + '
    ' + ); + }); + + it('2 reusable entity side by side in target', () => { + const target = document.createElement('div'); + const source = document.createDocumentFragment(); + + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + const div3 = document.createElement('div'); + const div4 = document.createElement('div'); + const div6 = document.createElement('div'); + const placeholder1 = document.createElement('span'); + const placeholder2 = document.createElement('span'); + + placeholder1.className = '_Entity _EType_Test _EId_entity1'; + placeholder2.className = '_Entity _EType_Test _EId_entity2'; + + div1.id = 'id1'; + div2.id = 'id2'; + div3.id = 'id3'; + div4.id = 'id4'; + div6.id = 'id6'; + + source.appendChild(div1); + source.appendChild(placeholder1); + source.appendChild(div2); + source.appendChild(placeholder2); + source.appendChild(div3); + + const wrapper1 = document.createElement('div'); + const wrapper2 = document.createElement('div'); + wrapper1.id = 'entity1'; + wrapper2.id = 'entity2'; + + target.appendChild(div4); + target.appendChild(wrapper1); + target.appendChild(wrapper2); + target.appendChild(div6); + + restoreContentWithEntityPlaceholder(source, target, { + entity1: wrapper1, + entity2: wrapper2, + }); + + expect(target.innerHTML).toBe( + '
    ' + ); + }); + + it('2 reusable entity in right order', () => { + const target = document.createElement('div'); + const source = document.createDocumentFragment(); + + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + const div3 = document.createElement('div'); + const div4 = document.createElement('div'); + const div5 = document.createElement('div'); + const div6 = document.createElement('div'); + const placeholder1 = document.createElement('span'); + const placeholder2 = document.createElement('span'); + + placeholder1.className = '_Entity _EType_Test _EId_entity1'; + placeholder2.className = '_Entity _EType_Test _EId_entity2'; + + div1.id = 'id1'; + div2.id = 'id2'; + div3.id = 'id3'; + div4.id = 'id4'; + div5.id = 'id5'; + div6.id = 'id6'; + + source.appendChild(div1); + source.appendChild(placeholder1); + source.appendChild(div2); + source.appendChild(placeholder2); + source.appendChild(div3); + + const wrapper1 = document.createElement('div'); + const wrapper2 = document.createElement('div'); + wrapper1.id = 'entity1'; + wrapper2.id = 'entity2'; + + target.appendChild(div4); + target.appendChild(wrapper1); + target.appendChild(div5); + target.appendChild(wrapper2); + target.appendChild(div6); + + restoreContentWithEntityPlaceholder(source, target, { + entity1: wrapper1, + entity2: wrapper2, + }); + + expect(target.innerHTML).toBe( + '
    ' + ); + }); + + it('2 reusable entity in wrong order', () => { + const target = document.createElement('div'); + const source = document.createDocumentFragment(); + + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + const div3 = document.createElement('div'); + const div4 = document.createElement('div'); + const div5 = document.createElement('div'); + const div6 = document.createElement('div'); + const placeholder1 = document.createElement('span'); + const placeholder2 = document.createElement('span'); + + placeholder1.className = '_Entity _EType_Test _EId_entity1'; + placeholder2.className = '_Entity _EType_Test _EId_entity2'; + + div1.id = 'id1'; + div2.id = 'id2'; + div3.id = 'id3'; + div4.id = 'id4'; + div5.id = 'id5'; + div6.id = 'id6'; + + source.appendChild(div1); + source.appendChild(placeholder2); + source.appendChild(div2); + source.appendChild(placeholder1); + source.appendChild(div3); + + const wrapper1 = document.createElement('div'); + const wrapper2 = document.createElement('div'); + wrapper1.id = 'entity1'; + wrapper2.id = 'entity2'; + + target.appendChild(div4); + target.appendChild(wrapper1); + target.appendChild(div5); + target.appendChild(wrapper2); + target.appendChild(div6); + + restoreContentWithEntityPlaceholder(source, target, { + entity1: wrapper1, + entity2: wrapper2, + }); + + expect(target.innerHTML).toBe( + '
    ' + ); + }); + + it('restoreContentWithEntityPlaceholder and entity map', () => { + const target = document.createElement('div'); + const source = document.createDocumentFragment(); + + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + const div3 = document.createElement('div'); + const div4 = document.createElement('div'); + const div5 = document.createElement('div'); + const div6 = document.createElement('div'); + const placeholder1 = document.createElement('span'); + const placeholder2 = document.createElement('span'); + + placeholder1.className = '_Entity _EType_Test _EId_entity1'; + placeholder2.className = '_Entity _EType_Test _EId_entity2'; + + div1.id = 'id1'; + div2.id = 'id2'; + div3.id = 'id3'; + div4.id = 'id4'; + div5.id = 'id5'; + div6.id = 'id6'; + + source.appendChild(div1); + source.appendChild(placeholder2); + source.appendChild(div2); + source.appendChild(placeholder1); + source.appendChild(div3); + + const wrapper1 = document.createElement('div'); + const wrapper2 = document.createElement('div'); + wrapper1.id = 'entity1'; + wrapper2.id = 'entity2'; + + target.appendChild(div4); + target.appendChild(wrapper1); + target.appendChild(div5); + target.appendChild(wrapper2); + target.appendChild(div6); + + restoreContentWithEntityPlaceholder(source, target, { + entity1: { element: wrapper1 }, + entity2: { element: wrapper2, canPersist: true }, + }); + + expect(target.innerHTML).toBe( + '
    ' + ); + expect(target.childNodes[0]).toBe(div1); + expect(target.childNodes[1]).toBe(wrapper2); + expect(target.childNodes[2]).toBe(div2); + expect(target.childNodes[3]).toBe(placeholder1); + expect(target.childNodes[4]).toBe(div3); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/htmlSanitizer/processCssVariableTest.ts b/packages/roosterjs-editor-dom/test/htmlSanitizer/processCssVariableTest.ts new file mode 100644 index 000000000000..f28e1f2aad1c --- /dev/null +++ b/packages/roosterjs-editor-dom/test/htmlSanitizer/processCssVariableTest.ts @@ -0,0 +1,40 @@ +import { isCssVariable, processCssVariable } from '../../lib/htmlSanitizer/processCssVariable'; + +describe('processCssVariable', () => { + it('no var', () => { + const result = processCssVariable('test'); + expect(result).toBe(''); + }); + + it('var without fallback', () => { + const result = processCssVariable('var(--test)'); + expect(result).toBe(''); + }); + + it('var with fallback', () => { + const result = processCssVariable('var(--test, fallback)'); + expect(result).toBe('fallback'); + }); + + it('var with fallback that has complex value', () => { + const result = processCssVariable('var(--test, rgb(1, 2, 3))'); + expect(result).toBe('rgb(1, 2, 3)'); + }); + + it('var with fallback and more spaces', () => { + const result = processCssVariable('var( --test , aa bb cc )'); + expect(result).toBe('aa bb cc '); + }); +}); + +describe('isCssVariable', () => { + it('no var', () => { + const result = isCssVariable('test'); + expect(result).toBeFalse(); + }); + + it('var', () => { + const result = isCssVariable('var('); + expect(result).toBeTrue(); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/htmlSanitizer/sanitizeHtmlTest.ts b/packages/roosterjs-editor-dom/test/htmlSanitizer/sanitizeHtmlTest.ts index 061f210c6f92..1be08d6ab3d4 100644 --- a/packages/roosterjs-editor-dom/test/htmlSanitizer/sanitizeHtmlTest.ts +++ b/packages/roosterjs-editor-dom/test/htmlSanitizer/sanitizeHtmlTest.ts @@ -492,7 +492,7 @@ describe('sanitizeHtml with unknown/disabled tags, replace with SPAN', () => { }); it('Make sure all allowed tags are really allowed', () => { - const allowTags = 'H1,H2,H3,H4,H5,H6,P,ABBR,ADDRESS,B,BDI,BDO,BLOCKQUOTE,CITE,CODE,DEL,DFN,EM,FONT,I,INS,KBD,MARK,METER,PRE,PROGRESS,Q,RP,RT,RUBY,S,SAMP,SMALL,STRIKE,STRONG,SUB,SUP,TEMPLATE,TIME,TT,U,VAR,XMP,TEXTAREA,BUTTON,SELECT,OPTGROUP,OPTION,LABEL,FIELDSET,LEGEND,DATALIST,OUTPUT,MAP,CANVAS,FIGCAPTION,FIGURE,PICTURE,A,NAV,UL,OL,LI,DIR,UL,DL,DT,DD,MENU,MENUITEM,DIV,SPAN,HEADER,FOOTER,MAIN,SECTION,ARTICLE,ASIDE,DETAILS,DIALOG,SUMMARY,DATA' + const allowTags = 'H1,H2,H3,H4,H5,H6,P,ABBR,ADDRESS,B,BDI,BDO,BLOCKQUOTE,CITE,CODE,DEL,DFN,EM,FONT,I,INS,KBD,MARK,METER,PRE,PROGRESS,Q,RP,RT,RUBY,S,SAMP,SMALL,STRIKE,STRONG,SUB,SUP,TIME,TT,U,VAR,XMP,TEXTAREA,BUTTON,SELECT,OPTGROUP,OPTION,LABEL,FIELDSET,LEGEND,DATALIST,OUTPUT,MAP,CANVAS,FIGCAPTION,FIGURE,PICTURE,A,NAV,UL,OL,LI,DIR,UL,DL,DT,DD,MENU,MENUITEM,DIV,SPAN,HEADER,FOOTER,MAIN,SECTION,ARTICLE,ASIDE,DETAILS,DIALOG,SUMMARY,DATA' .toLowerCase() .split(','); @@ -519,7 +519,7 @@ describe('sanitizeHtml with unknown/disabled tags, replace with SPAN', () => { }); it('Make sure disallowed tags are really removed', () => { - const disallowedTags = 'applet,audio,iframe,noscript,object,script,slot,style,title,video'.split( + const disallowedTags = 'applet,audio,iframe,noscript,object,script,slot,style,template,title,video'.split( ',' ); disallowedTags.forEach(tag => { diff --git a/packages/roosterjs-editor-dom/test/inlineElements/applyTextStyleTest.ts b/packages/roosterjs-editor-dom/test/inlineElements/applyTextStyleTest.ts index b39e0efec960..4aea114d6181 100644 --- a/packages/roosterjs-editor-dom/test/inlineElements/applyTextStyleTest.ts +++ b/packages/roosterjs-editor-dom/test/inlineElements/applyTextStyleTest.ts @@ -186,6 +186,22 @@ describe('applyTextStyle()', () => { ); }); + it('applyTextStyle() text node with SUP/SUB', () => { + let div = document.createElement('DIV'); + div.innerHTML = 'test1test2test3test4test5'; + let start = new Position(div, PositionType.Begin).normalize().move(2); + let end = new Position(div, PositionType.End).normalize().move(-2); + applyTextStyle( + div, + (node, isInnerNode) => (node.style.color = isInnerNode ? '' : 'red'), + start, + end + ); + expect(div.innerHTML).toBe( + 'test1test2test3test4test5' + ); + }); + it('applyTextStyle() text node with double span', () => { let div = document.createElement('DIV'); div.innerHTML = 'text'; diff --git a/packages/roosterjs-editor-dom/test/list/VListChainTest.ts b/packages/roosterjs-editor-dom/test/list/VListChainTest.ts index 61385650e2f0..d164c7dbe614 100644 --- a/packages/roosterjs-editor-dom/test/list/VListChainTest.ts +++ b/packages/roosterjs-editor-dom/test/list/VListChainTest.ts @@ -351,7 +351,7 @@ describe('VListChain.commit', () => { it('Add a new list item', () => { runTest( - '
    1. item1
    1. item3
    ', + '
    1. item1
    1. item3
    ', chains => { const ol1 = document.getElementById('ol1'); const li = document.createElement('li'); @@ -369,7 +369,7 @@ describe('VListChain.commit', () => { const li = document.getElementById('li2'); li.parentNode.removeChild(li); }, - '
    1. item1
    1. item3
    ' + '
    1. item1
    1. item3
    ' ); }); @@ -380,7 +380,7 @@ describe('VListChain.commit', () => { const ol = document.getElementById('ol2'); ol.parentNode.removeChild(ol); }, - '
    1. item1
    2. item2
    1. item4
    ' + '
    1. item1
    2. item2
    1. item4
    ' ); }); @@ -391,7 +391,7 @@ describe('VListChain.commit', () => { const li = document.getElementById('li1'); li.parentNode.removeChild(li); }, - '
    1. item2
    1. item3
    ' + '
    1. item2
    1. item3
    ' ); }); @@ -402,7 +402,7 @@ describe('VListChain.commit', () => { const ol = document.getElementById('ol1'); ol.parentNode.removeChild(ol); }, - '
    1. item3
    1. item4
    ' + '
    1. item3
    1. item4
    ' ); }); }); diff --git a/packages/roosterjs-editor-dom/test/list/VListItemTest.ts b/packages/roosterjs-editor-dom/test/list/VListItemTest.ts index 52684f4b502f..adec4750514c 100644 --- a/packages/roosterjs-editor-dom/test/list/VListItemTest.ts +++ b/packages/roosterjs-editor-dom/test/list/VListItemTest.ts @@ -1,6 +1,7 @@ +import VList from '../../lib/list/VList'; import VListItem from '../../lib/list/VListItem'; +import { BulletListType, ListType, NumberingListType } from 'roosterjs-editor-types'; import { itChromeOnly, itFirefoxOnly } from 'roosterjs-editor-api/test/TestHelper'; -import { ListType } from 'roosterjs-editor-types'; describe('VListItem.getListType', () => { it('set ListType to None', () => { @@ -279,7 +280,7 @@ describe('VListItem.writeBack', () => { runTest( ['div', 'ol'], [ListType.Ordered, ListType.Unordered], - '
    ' + '
    ' ); }); @@ -287,7 +288,7 @@ describe('VListItem.writeBack', () => { runTest( ['div', 'ol', 'ol'], [ListType.Ordered, ListType.Unordered], - '
      ' + '
        ' ); }); @@ -377,7 +378,7 @@ describe('VListItem.writeBack', () => { runTest( ['div'], [ListType.Ordered, ListType.Unordered], - '
        ', + '
        ', ol ); }); @@ -404,7 +405,7 @@ describe('VListItem.writeBack', () => { // Assert expect(listStack[0].innerHTML).toBe( - '
        1. test
        ' + '
        1. test
        ' ); }); @@ -434,3 +435,77 @@ describe('VListItem.writeBack', () => { ); }); }); + +describe('VListItem.applyListStyle', () => { + function runTest( + listType: ListType, + orderedStyle: NumberingListType | undefined, + unorderedStyle: BulletListType | undefined, + marker: string + ) { + const list = + listType === ListType.Unordered + ? document.createElement('ul') + : document.createElement('ol'); + document.body.appendChild(list); + const li = document.createElement('li'); + list.appendChild(li); + const vList = new VList(list); + vList.setListStyleType(orderedStyle, unorderedStyle); + vList.items.forEach(item => { + const index = vList.getListItemIndex(item.getNode()); + item.applyListStyle(list, index); + }); + expect(li.style.listStyleType).toBe(marker); + document.body.removeChild(list); + } + + it('DecimalParenthesis Numbering List', () => { + runTest(ListType.Ordered, NumberingListType.DecimalParenthesis, undefined, '"1) "'); + }); + + it('LowerRoman Numbering List', () => { + runTest(ListType.Ordered, NumberingListType.LowerRoman, undefined, '"i. "'); + }); + + it('UpperRomanDoubleParenthesis Numbering List', () => { + runTest( + ListType.Ordered, + NumberingListType.UpperRomanDoubleParenthesis, + undefined, + '"(I) "' + ); + }); + + it('LowerAlphaDash Numbering List', () => { + runTest(ListType.Ordered, NumberingListType.LowerAlphaDash, undefined, '"a- "'); + }); + + it('UpperAlphaParenthesis Numbering List', () => { + runTest(ListType.Ordered, NumberingListType.UpperAlphaParenthesis, undefined, '"A) "'); + }); + + it('LongArrow Bullet List', () => { + runTest(ListType.Unordered, undefined, BulletListType.LongArrow, '"➔ "'); + }); + + it('ShortArrow Bullet List', () => { + runTest(ListType.Unordered, undefined, BulletListType.ShortArrow, '"➢ "'); + }); + + it('UnfilledArrow Bullet List', () => { + runTest(ListType.Unordered, undefined, BulletListType.UnfilledArrow, '"➪ "'); + }); + + it('Dash Bullet List', () => { + runTest(ListType.Unordered, undefined, BulletListType.Dash, '"- "'); + }); + + it('Square Bullet List', () => { + runTest(ListType.Unordered, undefined, BulletListType.Square, '"∎ "'); + }); + + it('Square Bullet List', () => { + runTest(ListType.Unordered, undefined, BulletListType.Hyphen, '"— "'); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/list/VListTest.ts b/packages/roosterjs-editor-dom/test/list/VListTest.ts index 671e2c58665a..dd3af36b065e 100644 --- a/packages/roosterjs-editor-dom/test/list/VListTest.ts +++ b/packages/roosterjs-editor-dom/test/list/VListTest.ts @@ -1,8 +1,16 @@ import * as DomTestHelper from '../DomTestHelper'; import Position from '../../lib/selection/Position'; import VList from '../../lib/list/VList'; -import VListItem from '../../lib/list/VListItem'; -import { Indentation, ListType, PositionType } from 'roosterjs-editor-types'; +import VListItem, { ListStyleMetadata } from '../../lib/list/VListItem'; +import { + Indentation, + ListType, + PositionType, + NumberingListType, + BulletListType, +} from 'roosterjs-editor-types'; + +const editingInfo = 'editingInfo'; describe('VList.ctor', () => { const testId = 'VList_ctor'; @@ -276,9 +284,6 @@ describe('VList.writeBack', () => { vList.writeBack(); expect(div.innerHTML).toBe(expectedHtml); - - // Write again on the same VList should throw - expect(() => vList.writeBack()).toThrow(); } it('simple list, write back directly', () => { @@ -421,7 +426,7 @@ describe('VList.writeBack', () => { listTypes: [ListType.Ordered], }, ], - '
        • item1
        item2
        1. item3
        ' + '
        • item1
        item2
        1. item3
        ' ); }); @@ -493,10 +498,24 @@ describe('VList.writeBack', () => { listTypes: [ListType.Ordered], }, ], - '
        text
        1. item3
        2. item4
        text
        1. item5
        ', + '
        text
        1. item3
        2. item4
        text
        1. item5
        ', ol ); }); + + it('Write back with Lists with list item types', () => { + const styledList = + '
        1. 123
          1. 123
            1. 123

        '; + const div = document.createElement('div'); + document.body.append(div); + div.innerHTML = styledList; + + const list = div.querySelector('ol'); + const vList = new VList(list); + vList.writeBack(); + + expect(div.innerHTML).toEqual(styledList); + }); }); describe('VList.setIndentation', () => { @@ -1234,9 +1253,6 @@ describe('VList.split', () => { vList.split(separatorElement, startNumber); vList.writeBack(); expect(div.innerHTML).toBe(expectedHtml); - - // Write again on the same VList should throw - expect(() => vList.writeBack()).toThrow(); } it('split List', () => { @@ -1270,3 +1286,231 @@ describe('VList.split', () => { ); }); }); + +describe('VList.setListStyleType', () => { + const testId = 'VList_changeListType'; + const ListRoot = 'listRoot'; + const FocusNode = 'focus'; + const FocusNode1 = 'focus1'; + const FocusNode2 = 'focus2'; + + afterEach(() => { + DomTestHelper.removeElement(testId); + }); + + function runTest( + source: string, + orderedStyle: NumberingListType | undefined, + unorderedStyle: BulletListType | undefined, + style: ListStyleMetadata + ) { + DomTestHelper.createElementFromContent(testId, source); + const list = document.getElementById(ListRoot) as HTMLOListElement; + const focus = document.getElementById(FocusNode); + const focus1 = document.getElementById(FocusNode1); + const focus2 = document.getElementById(FocusNode2); + + if (!list) { + throw new Error('No root node'); + } + if (!focus && (!focus1 || !focus2)) { + throw new Error('No focus node'); + } + + const vList = new VList(list); + + // Act + vList.setListStyleType(orderedStyle, unorderedStyle); + expect(list.dataset[editingInfo]).toEqual(JSON.stringify(style)); + DomTestHelper.removeElement(testId); + } + + it('empty list', () => { + runTest( + `
          `, + NumberingListType.Decimal, + undefined, + { orderedStyleType: 1, unorderedStyleType: 1 } + ); + }); + + it('Decimal', () => { + runTest( + `
          1. test
          `, + NumberingListType.Decimal, + undefined, + { orderedStyleType: 1, unorderedStyleType: 1 } + ); + }); + + it('DecimalDash', () => { + runTest( + `
          1. test
          `, + NumberingListType.DecimalDash, + undefined, + { orderedStyleType: 2, unorderedStyleType: 1 } + ); + }); + + it('DecimalParenthesis', () => { + runTest( + `
          1. test
          `, + NumberingListType.DecimalParenthesis, + undefined, + { orderedStyleType: 3, unorderedStyleType: 1 } + ); + }); + + it('DecimalDoubleParenthesis', () => { + runTest( + `
          1. test
          `, + NumberingListType.DecimalDoubleParenthesis, + undefined, + { orderedStyleType: 4, unorderedStyleType: 1 } + ); + }); + + it('LowerAlpha', () => { + runTest( + `
          1. test
          `, + NumberingListType.LowerAlpha, + undefined, + { orderedStyleType: 5, unorderedStyleType: 1 } + ); + }); + + it('LowerAlphaDash', () => { + runTest( + `
          1. test
          `, + NumberingListType.LowerAlphaDash, + undefined, + { orderedStyleType: 8, unorderedStyleType: 1 } + ); + }); + + it('LowerAlphaParenthesis', () => { + runTest( + `
          1. test
          `, + NumberingListType.LowerAlphaParenthesis, + undefined, + { orderedStyleType: 6, unorderedStyleType: 1 } + ); + }); + + it('LowerAlphaDoubleParenthesis', () => { + runTest( + `
          1. test
          `, + NumberingListType.LowerAlphaDoubleParenthesis, + undefined, + { orderedStyleType: 7, unorderedStyleType: 1 } + ); + }); + + it('UpperAlpha', () => { + runTest( + `
          1. test
          `, + NumberingListType.UpperAlpha, + undefined, + { orderedStyleType: 9, unorderedStyleType: 1 } + ); + }); + + it('UpperAlphaDash', () => { + runTest( + `
          1. test
          `, + NumberingListType.UpperAlphaDash, + undefined, + { orderedStyleType: 12, unorderedStyleType: 1 } + ); + }); + + it('UpperAlphaParenthesis', () => { + runTest( + `
          1. test
          `, + NumberingListType.UpperAlphaParenthesis, + undefined, + { orderedStyleType: 10, unorderedStyleType: 1 } + ); + }); + + it('UpperAlphaDoubleParenthesis', () => { + runTest( + `
          1. test
          `, + NumberingListType.UpperAlphaDoubleParenthesis, + undefined, + { orderedStyleType: 11, unorderedStyleType: 1 } + ); + }); + + it('LowerRoman', () => { + runTest( + `
          1. test
          `, + NumberingListType.LowerRoman, + undefined, + { orderedStyleType: 13, unorderedStyleType: 1 } + ); + }); + + it('LowerRomanDash', () => { + runTest( + `
          1. test
          `, + NumberingListType.LowerRomanDash, + undefined, + { orderedStyleType: 16, unorderedStyleType: 1 } + ); + }); + + it('LowerRomanParenthesis', () => { + runTest( + `
          1. test
          `, + NumberingListType.LowerRomanParenthesis, + undefined, + { orderedStyleType: 14, unorderedStyleType: 1 } + ); + }); + + it('LowerRomanDoubleParenthesis', () => { + runTest( + `
          1. test
          `, + NumberingListType.LowerRomanDoubleParenthesis, + undefined, + { orderedStyleType: 15, unorderedStyleType: 1 } + ); + }); + + it('UpperRoman', () => { + runTest( + `
          1. test
          `, + NumberingListType.UpperRoman, + undefined, + { orderedStyleType: 17, unorderedStyleType: 1 } + ); + }); + + it('UpperRomanDash', () => { + runTest( + `
          1. test
          `, + NumberingListType.UpperRomanDash, + undefined, + { orderedStyleType: 20, unorderedStyleType: 1 } + ); + }); + + it('UpperRomanParenthesis', () => { + runTest( + `
          1. test
          `, + NumberingListType.UpperRomanParenthesis, + undefined, + { orderedStyleType: 18, unorderedStyleType: 1 } + ); + }); + + it('UpperRomanDoubleParenthesis', () => { + runTest( + `
          1. test
          `, + NumberingListType.UpperRomanDoubleParenthesis, + undefined, + { orderedStyleType: 19, unorderedStyleType: 1 } + ); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/list/convertDecimalsToAlphaTest.ts b/packages/roosterjs-editor-dom/test/list/convertDecimalsToAlphaTest.ts new file mode 100644 index 000000000000..8b2791cc080c --- /dev/null +++ b/packages/roosterjs-editor-dom/test/list/convertDecimalsToAlphaTest.ts @@ -0,0 +1,24 @@ +import convertDecimalsToAlpha from '../../lib/list/convertDecimalsToAlpha'; + +describe('convertDecimalsToAlpha', () => { + function runTest(decimals: number, expectedResult: string, isLowerCase?: boolean) { + const alpha = convertDecimalsToAlpha(decimals, isLowerCase); + expect(alpha).toBe(expectedResult); + } + + it('should convert 5 to f', () => { + runTest(5, 'f', true); + }); + + it('should convert 6 to G', () => { + runTest(6, 'G'); + }); + + it('should convert 27 to AA', () => { + runTest(27, 'AB'); + }); + + it('should convert 52 to ba', () => { + runTest(52, 'ba', true); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/list/convertDecimalsToRomanTest.ts b/packages/roosterjs-editor-dom/test/list/convertDecimalsToRomanTest.ts new file mode 100644 index 000000000000..e0209a0f336d --- /dev/null +++ b/packages/roosterjs-editor-dom/test/list/convertDecimalsToRomanTest.ts @@ -0,0 +1,24 @@ +import convertDecimalsToRoman from '../../lib/list/convertDecimalsToRomans'; + +describe('convertDecimalsToRoman', () => { + function runTest(decimals: number, expectedResult: string, isLowerCase?: boolean) { + const romanNumber = convertDecimalsToRoman(decimals, isLowerCase); + expect(romanNumber).toBe(expectedResult); + } + + it('should convert 5 to v', () => { + runTest(5, 'v', true); + }); + + it('should convert 6 to VI', () => { + runTest(6, 'VI'); + }); + + it('should convert 20 to XX', () => { + runTest(20, 'XX'); + }); + + it('should convert 16 to xvi', () => { + runTest(16, 'xvi', true); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/list/setBulletListMarkersTest.ts b/packages/roosterjs-editor-dom/test/list/setBulletListMarkersTest.ts new file mode 100644 index 000000000000..990ece53de2b --- /dev/null +++ b/packages/roosterjs-editor-dom/test/list/setBulletListMarkersTest.ts @@ -0,0 +1,44 @@ +import setBulletListMarkers from '../../lib/list/setBulletListMarkers'; +import { BulletListType } from 'roosterjs-editor-types'; + +describe('setBulletListMarkers', () => { + function runTest(bulletType: BulletListType, expectedStyle: string) { + const li = document.createElement('li'); + document.body.appendChild(li); + setBulletListMarkers(li, bulletType); + expect(li.style.listStyleType).toBe(expectedStyle); + document.body.removeChild(li); + } + + it('disc', () => { + runTest(BulletListType.Disc, 'disc'); + }); + + it('square', () => { + runTest(BulletListType.Square, '"∎ "'); + }); + + it('dash', () => { + runTest(BulletListType.Dash, '"- "'); + }); + + it('long arrow', () => { + runTest(BulletListType.LongArrow, '"➔ "'); + }); + + it('double long arrow', () => { + runTest(BulletListType.DoubleLongArrow, '"➔ "'); + }); + + it('short arrow', () => { + runTest(BulletListType.ShortArrow, '"➢ "'); + }); + + it('Unfilled arrow', () => { + runTest(BulletListType.UnfilledArrow, '"➪ "'); + }); + + it('Hyphen', () => { + runTest(BulletListType.Hyphen, '"— "'); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/list/setListItemStyleTest.ts b/packages/roosterjs-editor-dom/test/list/setListItemStyleTest.ts index afde5686a2ef..e626f20a9b5c 100644 --- a/packages/roosterjs-editor-dom/test/list/setListItemStyleTest.ts +++ b/packages/roosterjs-editor-dom/test/list/setListItemStyleTest.ts @@ -18,7 +18,7 @@ describe('setListItemStyle', () => { textContent: 'test', }, ], - 'font-size:72pt;color:blue' + 'font-size: 72pt; color: blue;' ); }); @@ -36,7 +36,7 @@ describe('setListItemStyle', () => { textContent: 'test', }, ], - 'font-size:72pt;color:blue' + 'font-size: 72pt; color: blue;' ); }); @@ -59,7 +59,7 @@ describe('setListItemStyle', () => { textContent: 'test', }, ], - 'font-size:72pt;font-family:Tahoma;color:blue' + 'font-size: 72pt; font-family: Tahoma; color: blue;' ); }); @@ -77,7 +77,7 @@ describe('setListItemStyle', () => { textContent: 'test', }, ], - 'font-size:72pt' + 'font-size: 72pt;' ); }); @@ -100,7 +100,7 @@ describe('setListItemStyle', () => { textContent: 'test', }, ], - 'font-size:72pt' + 'font-size: 72pt;' ); }); @@ -169,7 +169,7 @@ describe('setListItemStyle', () => { textContent: 'test', }, ], - 'font-size:72pt;font-family:Tahoma;color:blue' + 'font-size: 72pt; font-family: Tahoma; color: blue;' ); }); @@ -225,11 +225,11 @@ describe('setListItemStyle', () => { listItemElement.appendChild(divElement); // Act - setListItemStyle(listItemElement, stylesToInherit); + setListItemStyle(listItemElement, stylesToInherit, true /*isCssStyle*/); // Assert expect(listItemElement.getAttribute('style')).toBe( - 'font-size:72pt;font-family:Tahoma;color:blue' + 'font-size: 72pt; font-family: Tahoma; color: blue;' ); }); @@ -269,11 +269,11 @@ describe('setListItemStyle', () => { listItemElement.appendChild(spanElement); // Act - setListItemStyle(listItemElement, stylesToInherit); + setListItemStyle(listItemElement, stylesToInherit, true /*isCssStyle*/); // Assert expect(listItemElement.getAttribute('style')).toBe( - 'font-size:72pt;font-family:Tahoma;color:blue' + 'font-size: 72pt; font-family: Tahoma; color: blue;' ); }); @@ -293,11 +293,64 @@ describe('setListItemStyle', () => { listItemElement.appendChild(spanElement); // Act; - setListItemStyle(listItemElement, stylesToInherit); + setListItemStyle(listItemElement, stylesToInherit, true /*isCssStyle*/); // Assert; expect(listItemElement.getAttribute('style')).toBe( - 'font-size:72pt;font-family:Tahoma;color:blue' + 'font-size: 72pt; font-family: Tahoma; color: blue;' + ); + }); + + it('List Item element, with
          1. aaa
            1. ', () => { + // Arrange; + const listItemElement = document.createElement('li'); + const divElement = document.createElement('div'); + + const spanElement = createElement({ + elementTag: 'span', + styles: 'font-size: 72pt;font-family: Tahoma;color:blue', + textContent: 'test', + }); + + const b = document.createElement('b'); + b.appendChild(spanElement); + divElement.appendChild(b); + listItemElement.appendChild(divElement); + + // Act; + setListItemStyle(listItemElement, stylesToInherit, true /*isCssStyle*/); + + // Assert; + expect(listItemElement.getAttribute('style')).toBe( + 'font-size: 72pt; font-family: Tahoma; color: blue;' + ); + }); + + it('Set HTML attribute', () => { + // Arrange; + const listItemElement = document.createElement('li'); + const divElement = document.createElement('div'); + + const spanElement = createElement({ + elementTag: 'span', + styles: '', + textContent: 'test', + }); + + spanElement.dataset.ogsc = 'red'; + spanElement.dataset.ogsb = 'blue'; + + const b = document.createElement('b'); + b.appendChild(spanElement); + divElement.appendChild(b); + listItemElement.appendChild(divElement); + + // Act; + setListItemStyle(listItemElement, ['data-ogsb', 'data-ogsc'], false /*isCssStyle*/); + + // Assert; + expect(listItemElement.outerHTML).toBe( + '
            2. test
            3. ' ); }); @@ -310,15 +363,20 @@ describe('setListItemStyle', () => { }); // Act - setListItemStyle(listItemElement, stylesToInherit); + setListItemStyle(listItemElement, stylesToInherit, true /*isCssStyle*/); // Assert expect(listItemElement.getAttribute('style')).toBe(result); } + function createElement(input: TestChildElement): HTMLElement { const { elementTag, styles, textContent } = input; const element = document.createElement(elementTag); - element.setAttribute('style', styles); + + if (styles) { + element.setAttribute('style', styles); + } + element.textContent = textContent; return element; } diff --git a/packages/roosterjs-editor-dom/test/list/setNumberingListMarkersTest.ts b/packages/roosterjs-editor-dom/test/list/setNumberingListMarkersTest.ts new file mode 100644 index 000000000000..fa1f3421893f --- /dev/null +++ b/packages/roosterjs-editor-dom/test/list/setNumberingListMarkersTest.ts @@ -0,0 +1,93 @@ +import setNumberingListMarkers from '../../lib/list/setNumberingListMarkers'; +import { NumberingListType } from 'roosterjs-editor-types'; + +describe('setNumberingListMarkers', () => { + function runTest(bulletType: NumberingListType, level: number, expectedStyle: string) { + const li = document.createElement('li'); + document.body.appendChild(li); + li.style.removeProperty('list-style-type'); + setNumberingListMarkers(li, bulletType, level); + expect(li.style.listStyleType).toBe(expectedStyle); + document.body.removeChild(li); + } + + it('1.', () => { + runTest(NumberingListType.Decimal, 1, '"1. "'); + }); + + it('1-', () => { + runTest(NumberingListType.DecimalDash, 1, '"1- "'); + }); + + it('1)', () => { + runTest(NumberingListType.DecimalParenthesis, 1, '"1) "'); + }); + + it('(1)', () => { + runTest(NumberingListType.DecimalDoubleParenthesis, 1, '"(1) "'); + }); + + it('b.', () => { + runTest(NumberingListType.LowerAlpha, 2, '"b. "'); + }); + + it('b-', () => { + runTest(NumberingListType.LowerAlphaDash, 2, '"b- "'); + }); + + it('b)', () => { + runTest(NumberingListType.LowerAlphaParenthesis, 2, '"b) "'); + }); + + it('(b)', () => { + runTest(NumberingListType.LowerAlphaDoubleParenthesis, 2, '"(b) "'); + }); + + it('B.', () => { + runTest(NumberingListType.UpperAlpha, 2, '"B. "'); + }); + + it('B-', () => { + runTest(NumberingListType.UpperAlphaDash, 2, '"B- "'); + }); + + it('B)', () => { + runTest(NumberingListType.UpperAlphaParenthesis, 2, '"B) "'); + }); + + it('(B)', () => { + runTest(NumberingListType.UpperAlphaDoubleParenthesis, 2, '"(B) "'); + }); + + it('iii.', () => { + runTest(NumberingListType.LowerRoman, 3, '"iii. "'); + }); + + it('iii-', () => { + runTest(NumberingListType.LowerRomanDash, 3, '"iii- "'); + }); + + it('iii)', () => { + runTest(NumberingListType.LowerRomanParenthesis, 3, '"iii) "'); + }); + + it('(iii)', () => { + runTest(NumberingListType.LowerRomanDoubleParenthesis, 3, '"(iii) "'); + }); + + it('IV.', () => { + runTest(NumberingListType.UpperRoman, 4, '"IV. "'); + }); + + it('IV-', () => { + runTest(NumberingListType.UpperRomanDash, 4, '"IV- "'); + }); + + it('IV)', () => { + runTest(NumberingListType.UpperRomanParenthesis, 4, '"IV) "'); + }); + + it('(IV)', () => { + runTest(NumberingListType.UpperRomanDoubleParenthesis, 4, '"(IV) "'); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/metadata/definitionCreatorsTest.ts b/packages/roosterjs-editor-dom/test/metadata/definitionCreatorsTest.ts new file mode 100644 index 000000000000..3a3b6fb451f7 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/metadata/definitionCreatorsTest.ts @@ -0,0 +1,160 @@ +import { Definition, DefinitionType, ObjectPropertyDefinition } from 'roosterjs-editor-types'; +import { + createNumberDefinition, + createBooleanDefinition, + createStringDefinition, + createArrayDefinition, + createObjectDefinition, +} from '../../lib/metadata/definitionCreators'; + +describe('createNumberDefinition', () => { + it('normal case', () => { + const def = createNumberDefinition(); + expect(def).toEqual({ + type: DefinitionType.Number, + isOptional: undefined, + value: undefined, + maxValue: undefined, + minValue: undefined, + allowNull: undefined, + }); + }); + + it('full case', () => { + const def = createNumberDefinition(true, 2, 1, 3); + expect(def).toEqual({ + type: DefinitionType.Number, + isOptional: true, + value: 2, + minValue: 1, + maxValue: 3, + allowNull: undefined, + }); + }); +}); + +describe('createBooleanDefinition', () => { + it('normal case', () => { + const def = createBooleanDefinition(); + expect(def).toEqual({ + type: DefinitionType.Boolean, + isOptional: undefined, + value: undefined, + allowNull: undefined, + }); + }); + + it('full case', () => { + const def = createBooleanDefinition(true, false); + expect(def).toEqual({ + type: DefinitionType.Boolean, + isOptional: true, + value: false, + allowNull: undefined, + }); + }); +}); + +describe('createStringDefinition', () => { + it('normal case', () => { + const def = createStringDefinition(); + expect(def).toEqual({ + type: DefinitionType.String, + isOptional: undefined, + value: undefined, + allowNull: undefined, + }); + }); + + it('optional case', () => { + const def = createStringDefinition(true, 'test'); + expect(def).toEqual({ + type: DefinitionType.String, + isOptional: true, + value: 'test', + allowNull: undefined, + }); + }); + + it('full case', () => { + const def = createStringDefinition(true, 'test', true); + expect(def).toEqual({ + type: DefinitionType.String, + isOptional: true, + value: 'test', + allowNull: true, + }); + }); +}); + +describe('createArrayDefinition', () => { + const itemDef: Definition = { + type: DefinitionType.Number, + }; + + it('normal case', () => { + const def = createArrayDefinition(itemDef); + expect(def).toEqual({ + type: DefinitionType.Array, + itemDef, + isOptional: undefined, + minLength: undefined, + maxLength: undefined, + allowNull: undefined, + }); + }); + + it('full case', () => { + const def = createArrayDefinition(itemDef, true, 1, 3); + expect(def).toEqual({ + type: DefinitionType.Array, + isOptional: true, + itemDef, + minLength: 1, + maxLength: 3, + allowNull: undefined, + }); + }); +}); + +interface TestType { + x: number; + y: string; +} + +describe('createObjectDefinition', () => { + const propertyDef: ObjectPropertyDefinition = { + x: { type: DefinitionType.Number }, + y: { type: DefinitionType.String }, + }; + + it('normal case', () => { + const def = createObjectDefinition(propertyDef); + expect(def).toEqual({ + type: DefinitionType.Object, + propertyDef, + isOptional: undefined, + allowNull: undefined, + }); + }); + + it('isOptional case', () => { + const def = createObjectDefinition(propertyDef, true); + expect(def).toEqual({ + type: DefinitionType.Object, + isOptional: true, + propertyDef, + allowNull: undefined, + }); + }); + + it('full case', () => { + const def = createObjectDefinition(propertyDef, true, true); + expect(def).toEqual({ + type: DefinitionType.Object, + isOptional: true, + propertyDef, + allowNull: true, + }); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/metadata/metadataTest.ts b/packages/roosterjs-editor-dom/test/metadata/metadataTest.ts new file mode 100644 index 000000000000..3dc99b78b8a9 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/metadata/metadataTest.ts @@ -0,0 +1,124 @@ +import { CustomizeDefinition, DefinitionType } from 'roosterjs-editor-types'; +import { getMetadata, removeMetadata, setMetadata } from '../../lib/metadata/metadata'; + +describe('metadata', () => { + it('getMetadata gets a valid metadata', () => { + const validators = { + trueValidator: (input: any) => true, + falseValidator: (input: any) => false, + }; + const obj = { x: 1, y: 'test' }; + const validatorSpy = spyOn(validators, 'trueValidator').and.callThrough(); + const div = document.createElement('div'); + div.innerHTML = 'test'; + const node = div.firstChild as HTMLElement; + node.setAttribute('data-editing-info', JSON.stringify(obj)); + const def: CustomizeDefinition = { + type: DefinitionType.Customize, + validator: validators.trueValidator, + }; + + const metadata = getMetadata(node, def); + + expect(validatorSpy).toHaveBeenCalled(); + expect(metadata).toEqual(obj); + }); + + it('getMetadata gets an invalid metadata', () => { + const validators = { + trueValidator: (input: any) => true, + falseValidator: (input: any) => false, + }; + const obj = { x: 1, y: 'test' }; + const validatorSpy = spyOn(validators, 'falseValidator').and.callThrough(); + const div = document.createElement('div'); + div.innerHTML = 'test'; + const node = div.firstChild as HTMLElement; + + node.setAttribute('data-editing-info', JSON.stringify(obj)); + + const def: CustomizeDefinition = { + type: DefinitionType.Customize, + validator: validators.falseValidator, + }; + + const metadata = getMetadata(node, def); + + expect(validatorSpy).toHaveBeenCalled(); + expect(metadata).toBeNull(); + }); + + it('getMetadata gets an invalid metadata and return default value', () => { + const validators = { + trueValidator: (input: any) => true, + falseValidator: (input: any) => false, + }; + const obj = { x: 1, y: 'test' }; + const validatorSpy = spyOn(validators, 'falseValidator').and.callThrough(); + const div = document.createElement('div'); + div.innerHTML = 'test'; + const node = div.firstChild as HTMLElement; + + node.setAttribute('data-editing-info', JSON.stringify(obj)); + + const def: CustomizeDefinition = { + type: DefinitionType.Customize, + validator: validators.falseValidator, + }; + + const metadata = getMetadata(node, def, obj); + + expect(validatorSpy).toHaveBeenCalled(); + expect(metadata).toBe(obj); + }); + + it('setMetadata sets a valid metadata', () => { + const validators = { + trueValidator: (input: any) => true, + falseValidator: (input: any) => false, + }; + const obj = { x: 1, y: 'test' }; + const validatorSpy = spyOn(validators, 'trueValidator').and.callThrough(); + const node = document.createElement('div'); + const def: CustomizeDefinition = { + type: DefinitionType.Customize, + validator: validators.trueValidator, + }; + const result = setMetadata(node, obj, def); + + expect(validatorSpy).toHaveBeenCalled(); + expect(result).toBeTrue(); + expect(node.outerHTML).toBe( + '
              ' + ); + }); + + it('setMetadata sets an invalid metadata', () => { + const validators = { + trueValidator: (input: any) => true, + falseValidator: (input: any) => false, + }; + const validatorSpy = spyOn(validators, 'falseValidator').and.callThrough(); + const node = document.createElement('div'); + const def: CustomizeDefinition = { + type: DefinitionType.Customize, + validator: validators.falseValidator, + }; + const obj = { x: 1, y: 'test' }; + const result = setMetadata(node, obj, def); + + expect(validatorSpy).toHaveBeenCalled(); + expect(result).toBeFalse(); + expect(node.outerHTML).toBe('
              '); + }); +}); + +describe('removeMetadata', () => { + it('removeElement', () => { + const obj = { x: 1, y: 'test' }; + const div = document.createElement('div'); + div.setAttribute('data-editing-info', JSON.stringify(obj)); + removeMetadata(div); + expect(div.outerHTML).toBe('
              '); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/metadata/validateTest.ts b/packages/roosterjs-editor-dom/test/metadata/validateTest.ts new file mode 100644 index 000000000000..32689ec700b0 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/metadata/validateTest.ts @@ -0,0 +1,295 @@ +import validate from '../../lib/metadata/validate'; +import { + Definition, + ObjectPropertyDefinition, + PluginEventType, + DefinitionType, +} from 'roosterjs-editor-types'; +import { + createArrayDefinition, + createBooleanDefinition, + createNumberDefinition, + createObjectDefinition, + createStringDefinition, +} from '../../lib/metadata/definitionCreators'; + +describe('validate', () => { + function runTestInternal(input: any, def: Definition, result: boolean) { + expect(validate(input, def)).toBe(result); + } + + function runNumberTest( + input: any, + resultForRequired: boolean, + resultForOptional: boolean, + value?: number + ) { + const requiredDef = createNumberDefinition(false, value); + const optionalDef = createNumberDefinition(true, value); + runTestInternal(input, requiredDef, resultForRequired); + runTestInternal(input, optionalDef, resultForOptional); + } + + function runStringTest( + input: any, + resultForRequired: boolean, + resultForOptional: boolean, + value?: string + ) { + const requiredDef = createStringDefinition(false, value); + const optionalDef = createStringDefinition(true, value); + runTestInternal(input, requiredDef, resultForRequired); + runTestInternal(input, optionalDef, resultForOptional); + } + + function runBooleanTest( + input: any, + resultForRequired: boolean, + resultForOptional: boolean, + value?: boolean + ) { + const requiredDef = createBooleanDefinition(false, value); + const optionalDef = createBooleanDefinition(true, value); + runTestInternal(input, requiredDef, resultForRequired); + runTestInternal(input, optionalDef, resultForOptional); + } + + function runArrayTest( + input: any, + resultForRequired: boolean, + resultForOptional: boolean, + minLength?: number, + maxLength?: number + ) { + const itemDef = createNumberDefinition(); + const requiredDef = createArrayDefinition(itemDef, false, minLength, maxLength); + const optionalDef = createArrayDefinition(itemDef, true, minLength, maxLength); + runTestInternal(input, requiredDef, resultForRequired); + runTestInternal(input, optionalDef, resultForOptional); + } + + interface TestObj { + x: number; + y: string; + } + + function runObjectTest(input: any, resultForRequired: boolean, resultForOptional: boolean) { + const propertyDef: ObjectPropertyDefinition = { + x: createNumberDefinition(), + y: createStringDefinition(), + }; + const requiredDef = createObjectDefinition(propertyDef, false); + const optionalDef = createObjectDefinition(propertyDef, true); + runTestInternal(input, requiredDef, resultForRequired); + runTestInternal(input, optionalDef, resultForOptional); + } + + it('Validate number', () => { + runNumberTest(0, true, true); + runNumberTest(0, true, true, 0); + runNumberTest(0, true, true, 0.000001); + runNumberTest(0, false, false, 1); + runNumberTest(undefined, false, true); + runNumberTest(null, false, false); + runNumberTest('test', false, false); + runNumberTest(PluginEventType.EditorReady, true, true); + runNumberTest(true, false, false); + runNumberTest({}, false, false); + runNumberTest([], false, false); + runNumberTest({ x: 1 }, false, false); + runNumberTest([1], false, false); + }); + + it('Validate string', () => { + runStringTest('test', true, true); + runStringTest('test', true, true, 'test'); + runStringTest('test', false, false, 'test1'); + runStringTest(undefined, false, true); + runStringTest(null, false, false); + runStringTest(1, false, false); + runStringTest(PluginEventType.EditorReady, false, false); + runStringTest(true, false, false); + runStringTest({}, false, false); + runStringTest([], false, false); + runStringTest({ x: 1 }, false, false); + runStringTest([1], false, false); + }); + + it('Validate boolean', () => { + runBooleanTest(true, true, true); + runBooleanTest(true, true, true, true); + runBooleanTest(true, false, false, false); + runBooleanTest(undefined, false, true); + runBooleanTest(null, false, false); + runBooleanTest(1, false, false); + runBooleanTest(PluginEventType.EditorReady, false, false); + runBooleanTest('test', false, false); + runBooleanTest({}, false, false); + runBooleanTest([], false, false); + runBooleanTest({ x: 1 }, false, false); + runBooleanTest([1], false, false); + }); + + it('Validate array', () => { + runArrayTest([], true, true); + runArrayTest(undefined, false, true); + runArrayTest([1, 2, 3], true, true); + runArrayTest([1, 2, 'test'], false, false); + runArrayTest([null], false, false); + runArrayTest([1, 2], true, true, 0, 3); + runArrayTest([1, 2], false, false, 3); + runArrayTest([1, 2], false, false, undefined, 1); + runArrayTest(true, false, false); + runArrayTest(null, false, false); + runArrayTest(1, false, false); + runArrayTest(PluginEventType.EditorReady, false, false); + runArrayTest('test', false, false); + runArrayTest({}, false, false); + runArrayTest({ x: 1 }, false, false); + }); + + it('Validate object', () => { + runObjectTest({ x: 1, y: 'test' }, true, true); + runObjectTest(undefined, false, true); + runObjectTest({ x: 1, y: 2 }, false, false); + runObjectTest({ x: 1 }, false, false); + runObjectTest([], false, false); + runObjectTest(true, false, false); + runObjectTest(1, false, false); + runObjectTest('test', false, false); + }); + + interface TestObj2 { + a: number[]; + b?: TestObj; + } + + it('Validate object 2', () => { + const def: Definition = createObjectDefinition({ + a: createArrayDefinition(createNumberDefinition()), + b: createObjectDefinition( + { + x: createNumberDefinition(), + y: createStringDefinition(), + }, + true + ), + }); + + expect( + validate( + { + a: [1, 2, 3], + b: { + x: 1, + y: 'test', + }, + }, + def + ) + ).toBeTrue(); + expect( + validate( + { + a: [1, 2, 3], + }, + def + ) + ).toBeTrue(); + expect( + validate( + { + a: [1, 2, 3, 'test'], + b: { + x: 1, + y: 'test', + }, + }, + def + ) + ).toBeFalse(); + expect( + validate( + { + a: null, + b: { + x: 1, + y: 'test', + }, + }, + def + ) + ).toBeFalse(); + expect( + validate( + { + a: [1, 2, 3], + b: { + x: 1, + y: 'test', + }, + c: 0, + }, + def + ) + ).toBeTrue(); + }); +}); + +describe('Validate customize', () => { + it('Validate true', () => { + const validators = { + trueValidator: (input: any) => true, + falseValidator: (input: any) => false, + }; + const trueSpy = spyOn(validators, 'trueValidator').and.callThrough(); + const input = {}; + const result = validate( + {}, + { type: DefinitionType.Customize, validator: validators.trueValidator } + ); + + expect(result).toBe(true); + expect(trueSpy).toHaveBeenCalledWith(input); + }); + + it('Validate false', () => { + const validators = { + trueValidator: (input: any) => true, + falseValidator: (input: any) => false, + }; + const falseSpy = spyOn(validators, 'falseValidator').and.callThrough(); + const input = {}; + const result = validate( + {}, + { type: DefinitionType.Customize, validator: validators.falseValidator } + ); + + expect(result).toBe(false); + expect(falseSpy).toHaveBeenCalledWith(input); + }); + + it('Validate object', () => { + interface TestObj { + name: string; + value: number; + } + const validators = { + trueValidator: (input: any) => true, + falseValidator: (input: any) => false, + }; + + const trueSpy = spyOn(validators, 'trueValidator').and.callThrough(); + const def = createObjectDefinition({ + name: createStringDefinition(), + value: { + type: DefinitionType.Customize, + validator: validators.trueValidator, + }, + }); + const result = validate({ name: 'test', value: 1 }, def); + + expect(result).toBeTrue(); + expect(trueSpy).toHaveBeenCalledWith(1); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/pasteSourceValidations/documentContainWacElementsTest.ts b/packages/roosterjs-editor-dom/test/pasteSourceValidations/documentContainWacElementsTest.ts new file mode 100644 index 000000000000..1eb4c58f89d7 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/pasteSourceValidations/documentContainWacElementsTest.ts @@ -0,0 +1,22 @@ +import documentContainWacElements from '../../lib/pasteSourceValidations/documentContainWacElements'; +import { getSourceInputParams } from '../../lib/pasteSourceValidations/getPasteSource'; +import { getWacElement } from './pasteTestUtils'; + +describe('documentContainWacElements |', () => { + it('Fragment contain Wac elements', () => { + const fragment = document.createDocumentFragment(); + fragment.appendChild(getWacElement()); + + const result = documentContainWacElements({ fragment }); + + expect(result).toBeTrue(); + }); + + it('Fragment does not contain Wac elements', () => { + const fragment = document.createDocumentFragment(); + + const result = documentContainWacElements({ fragment }); + + expect(result).toBeFalse(); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/pasteSourceValidations/getPasteSourceTest.ts b/packages/roosterjs-editor-dom/test/pasteSourceValidations/getPasteSourceTest.ts new file mode 100644 index 000000000000..a22c5a252976 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/pasteSourceValidations/getPasteSourceTest.ts @@ -0,0 +1,112 @@ +import getPasteSource from '../../lib/pasteSourceValidations/getPasteSource'; +import { BeforePasteEvent, ClipboardData } from 'roosterjs-editor-types'; +import { GOOGLE_SHEET_NODE_NAME } from '../../lib/pasteSourceValidations/constants'; +import { KnownPasteSourceType } from 'roosterjs-editor-types'; +import { + EXCEL_ATTRIBUTE_VALUE, + getWacElement, + POWERPOINT_ATTRIBUTE_VALUE, + WORD_ATTRIBUTE_VALUE, +} from '../../../roosterjs-editor-plugins/test/paste/pasteTestUtils'; + +describe('getPasteSourceTest | ', () => { + it('Is Word', () => { + const result = getPasteSource(wordParam(), false /* shouldConvertSingleImage */); + expect(result).toBe(KnownPasteSourceType.WordDesktop); + }); + it('Is Wac Doc', () => { + const result = getPasteSource(wacParam(), false /* shouldConvertSingleImage */); + expect(result).toBe(KnownPasteSourceType.WacComponents); + }); + it('Is Excel Doc', () => { + const result = getPasteSource(excelParam(), false /* shouldConvertSingleImage */); + expect(result).toBe(KnownPasteSourceType.ExcelDesktop); + }); + it('Is GoogleSheet Doc', () => { + const result = getPasteSource(googleSheetParam(), false /* shouldConvertSingleImage */); + expect(result).toBe(KnownPasteSourceType.GoogleSheets); + }); + it('Is PowerPoint Doc', () => { + const result = getPasteSource(powerPointParam(), false /* shouldConvertSingleImage */); + expect(result).toBe(KnownPasteSourceType.PowerPointDesktop); + }); + it('Is SingleImage', () => { + const result = getPasteSource( + converSingleImageParam(), + true /* shouldConvertSingleImage */ + ); + expect(result).toBe(KnownPasteSourceType.SingleImage); + }); + it('Is SingleImage, but should not convert single image', () => { + const result = getPasteSource( + converSingleImageParam(), + false /* shouldConvertSingleImage */ + ); + expect(result).toBe(KnownPasteSourceType.Default); + }); + it('Is Default', () => { + const result = getPasteSource(defaultParam(), false /* shouldConvertSingleImage */); + expect(result).toBe(KnownPasteSourceType.Default); + }); +}); + +function wacParam(): BeforePasteEvent { + const fragment = document.createDocumentFragment(); + fragment.appendChild(getWacElement()); + + return { fragment, htmlAttributes: {}, clipboardData: {} }; +} + +function excelParam(): BeforePasteEvent { + const fragment = document.createDocumentFragment(); + const htmlAttributes: Record = { + 'xmlns:x': EXCEL_ATTRIBUTE_VALUE, + }; + + return { htmlAttributes, fragment, clipboardData: {} }; +} + +function googleSheetParam(): BeforePasteEvent { + const fragment = document.createDocumentFragment(); + fragment.appendChild(document.createElement(GOOGLE_SHEET_NODE_NAME)); + + return { fragment, htmlAttributes: {}, clipboardData: {} }; +} + +function converSingleImageParam(): BeforePasteEvent { + const fragment = document.createDocumentFragment(); + const clipboardData = { + htmlFirstLevelChildTags: ['IMG'], + }; + + return { + fragment, + clipboardData, + htmlAttributes: {}, + }; +} + +function powerPointParam(): BeforePasteEvent { + const fragment = document.createDocumentFragment(); + + const htmlAttributes: Record = { + ProgId: POWERPOINT_ATTRIBUTE_VALUE, + }; + + return { htmlAttributes, fragment, clipboardData: {} }; +} + +function wordParam(): BeforePasteEvent { + const fragment = document.createDocumentFragment(); + const htmlAttributes: Record = { + 'xmlns:w': WORD_ATTRIBUTE_VALUE, + }; + + return { htmlAttributes, fragment, clipboardData: {} }; +} + +function defaultParam(): BeforePasteEvent { + const fragment = document.createDocumentFragment(); + + return { htmlAttributes: {}, fragment, clipboardData: {} }; +} diff --git a/packages/roosterjs-editor-dom/test/pasteSourceValidations/isExcelDesktopDocumentTest.ts b/packages/roosterjs-editor-dom/test/pasteSourceValidations/isExcelDesktopDocumentTest.ts new file mode 100644 index 000000000000..8f986fe40d70 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/pasteSourceValidations/isExcelDesktopDocumentTest.ts @@ -0,0 +1,43 @@ +import isExcelDesktopDocument from '../../lib/pasteSourceValidations/isExcelDesktopDocument'; +import { EXCEL_ATTRIBUTE_VALUE } from './pasteTestUtils'; +import { getSourceInputParams } from '../../lib/pasteSourceValidations/getPasteSource'; + +const EXCEL_ONLINE_ATTRIBUTE_VALUE = 'Excel.Sheet'; + +describe('isExcelDesktopDocument |', () => { + it('Is an ambiguous Excel document, unconfirmed if Desktop', () => { + const htmlAttributes: Record = { + ProgId: EXCEL_ONLINE_ATTRIBUTE_VALUE, + }; + + const result = isExcelDesktopDocument({ htmlAttributes }); + + expect(result).toBeFalse(); + }); + + it('Is an Excel Desktop document 1', () => { + const htmlAttributes: Record = { + 'xmlns:x': EXCEL_ATTRIBUTE_VALUE, + ProgId: EXCEL_ONLINE_ATTRIBUTE_VALUE, + }; + + const result = isExcelDesktopDocument({ htmlAttributes }); + + expect(result).toBeTrue(); + }); + + it('Is an Excel Desktop document 2', () => { + const htmlAttributes: Record = { + 'xmlns:x': EXCEL_ATTRIBUTE_VALUE, + }; + const result = isExcelDesktopDocument({ htmlAttributes }); + + expect(result).toBeTrue(); + }); + + it('Is not a Excel Document', () => { + const result = isExcelDesktopDocument({ htmlAttributes: {} }); + + expect(result).toBeFalse(); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/pasteSourceValidations/isExcelOnlineDocumentTest.ts b/packages/roosterjs-editor-dom/test/pasteSourceValidations/isExcelOnlineDocumentTest.ts new file mode 100644 index 000000000000..0479eac7ed2c --- /dev/null +++ b/packages/roosterjs-editor-dom/test/pasteSourceValidations/isExcelOnlineDocumentTest.ts @@ -0,0 +1,34 @@ +import isExcelOnlineDocument from '../../lib/pasteSourceValidations/isExcelOnlineDocument'; +import { EXCEL_ATTRIBUTE_VALUE } from './pasteTestUtils'; +import { getSourceInputParams } from '../../lib/pasteSourceValidations/getPasteSource'; + +const EXCEL_ONLINE_ATTRIBUTE_VALUE = 'Excel.Sheet'; + +describe('isExcelOnlineDocument |', () => { + it('Is not an Excel Online document', () => { + const htmlAttributes: Record = { + 'xmlns:x': EXCEL_ATTRIBUTE_VALUE, + ProgId: EXCEL_ONLINE_ATTRIBUTE_VALUE, + }; + + const result = isExcelOnlineDocument({ htmlAttributes }); + + expect(result).toBeFalse(); + }); + + it('Is an Excel Online document', () => { + const htmlAttributes: Record = { + ProgId: EXCEL_ONLINE_ATTRIBUTE_VALUE, + }; + + const result = isExcelOnlineDocument({ htmlAttributes }); + + expect(result).toBeTrue(); + }); + + it('Is not a Excel Document', () => { + const result = isExcelOnlineDocument({ htmlAttributes: {} }); + + expect(result).toBeFalse(); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/pasteSourceValidations/isGoogleSheetDocumentTest.ts b/packages/roosterjs-editor-dom/test/pasteSourceValidations/isGoogleSheetDocumentTest.ts new file mode 100644 index 000000000000..b19e071dc107 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/pasteSourceValidations/isGoogleSheetDocumentTest.ts @@ -0,0 +1,32 @@ +import isGoogleSheetDocument from '../../lib/pasteSourceValidations/isGoogleSheetDocument'; +import { getSourceInputParams } from '../../lib/pasteSourceValidations/getPasteSource'; +import { getWacElement } from './pasteTestUtils'; +import { GOOGLE_SHEET_NODE_NAME } from '../../lib/pasteSourceValidations/constants'; + +describe('isGoogleSheetDocument |', () => { + it('Is from Google Sheets', () => { + const fragment = document.createDocumentFragment(); + fragment.appendChild(document.createElement(GOOGLE_SHEET_NODE_NAME)); + + const result = isGoogleSheetDocument({ fragment }); + + expect(result).toBeTrue(); + }); + + it('Is not from Google Sheets', () => { + const fragment = document.createDocumentFragment(); + + const result = isGoogleSheetDocument({ fragment }); + + expect(result).toBeFalse(); + }); + + it('Is not from Google Sheets 2', () => { + const fragment = document.createDocumentFragment(); + fragment.appendChild(getWacElement()); + + const result = isGoogleSheetDocument({ fragment }); + + expect(result).toBeFalse(); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/pasteSourceValidations/isPowerPointDesktopDocumentTest.ts b/packages/roosterjs-editor-dom/test/pasteSourceValidations/isPowerPointDesktopDocumentTest.ts new file mode 100644 index 000000000000..7c0cfcab83d8 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/pasteSourceValidations/isPowerPointDesktopDocumentTest.ts @@ -0,0 +1,21 @@ +import isPowerPointDesktopDocument from '../../lib/pasteSourceValidations/isPowerPointDesktopDocument'; +import { getSourceInputParams } from '../../lib/pasteSourceValidations/getPasteSource'; +import { POWERPOINT_ATTRIBUTE_VALUE } from './pasteTestUtils'; + +describe('isPowerPointDesktopDocument |', () => { + it('Is a PPT document 1', () => { + const htmlAttributes: Record = { + ProgId: POWERPOINT_ATTRIBUTE_VALUE, + }; + + const result = isPowerPointDesktopDocument({ htmlAttributes }); + + expect(result).toBeTrue(); + }); + + it('Is not a PPT Document', () => { + const result = isPowerPointDesktopDocument({ htmlAttributes: {} }); + + expect(result).toBeFalse(); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/pasteSourceValidations/isWordDesktopDocumentTest.ts b/packages/roosterjs-editor-dom/test/pasteSourceValidations/isWordDesktopDocumentTest.ts new file mode 100644 index 000000000000..61501cf7be02 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/pasteSourceValidations/isWordDesktopDocumentTest.ts @@ -0,0 +1,44 @@ +import isWordDesktopDocument from '../../lib/pasteSourceValidations/isWordDesktopDocument'; +import { getSourceInputParams } from '../../lib/pasteSourceValidations/getPasteSource'; +import { WORD_ATTRIBUTE_VALUE } from './pasteTestUtils'; + +const WORD_PROG_ID = 'Word.Document'; + +describe('isWordDesktopDocument |', () => { + it('Is a Word document 1', () => { + const htmlAttributes: Record = { + ProgId: WORD_PROG_ID, + }; + + const result = isWordDesktopDocument({ htmlAttributes }); + + expect(result).toBeTrue(); + }); + + it('Is a Word document 2', () => { + const htmlAttributes: Record = { + 'xmlns:w': WORD_ATTRIBUTE_VALUE, + ProgId: WORD_PROG_ID, + }; + + const result = isWordDesktopDocument({ htmlAttributes }); + + expect(result).toBeTrue(); + }); + + it('Is a Word document 3', () => { + const htmlAttributes: Record = { + 'xmlns:w': WORD_ATTRIBUTE_VALUE, + }; + + const result = isWordDesktopDocument({ htmlAttributes }); + + expect(result).toBeTrue(); + }); + + it('Is not a Word Document', () => { + const result = isWordDesktopDocument({ htmlAttributes: {} }); + + expect(result).toBeFalse(); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/pasteSourceValidations/pasteTestUtils.ts b/packages/roosterjs-editor-dom/test/pasteSourceValidations/pasteTestUtils.ts new file mode 100644 index 000000000000..d41c195825d8 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/pasteSourceValidations/pasteTestUtils.ts @@ -0,0 +1,9 @@ +export const EXCEL_ATTRIBUTE_VALUE = 'urn:schemas-microsoft-com:office:excel'; +export const POWERPOINT_ATTRIBUTE_VALUE = 'PowerPoint.Slide'; +export const WORD_ATTRIBUTE_VALUE = 'urn:schemas-microsoft-com:office:word'; + +export const getWacElement = (): HTMLElement => { + const element = document.createElement('span'); + element.classList.add('WACImageContainer'); + return element; +}; diff --git a/packages/roosterjs-editor-dom/test/pasteSourceValidations/shouldConvertToSingleImageTest.ts b/packages/roosterjs-editor-dom/test/pasteSourceValidations/shouldConvertToSingleImageTest.ts new file mode 100644 index 000000000000..158ec8021df1 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/pasteSourceValidations/shouldConvertToSingleImageTest.ts @@ -0,0 +1,40 @@ +import shouldConvertToSingleImage from '../../lib/pasteSourceValidations/shouldConvertToSingleImage'; +import { ClipboardData } from 'roosterjs-editor-types'; +import { getSourceInputParams } from '../../lib/pasteSourceValidations/getPasteSource'; + +describe('shouldConvertToSingleImage |', () => { + it('Is Single Image', () => { + runTest(['IMG'], true, true /* shouldConvertToSingleImage */); + }); + + it('Is Single Image, feature is not enabled', () => { + runTest(['IMG'], false, false /* shouldConvertToSingleImage */); + }); + + it('Is Not single Image, feature is not enabled', () => { + runTest(['IMG', 'DIV'], false, false /* shouldConvertToSingleImage */); + }); + + it('Is Not single Image, feature is enabled', () => { + runTest(['IMG', 'DIV'], false, true /* shouldConvertToSingleImage */); + }); + + function runTest( + htmlFirstLevelChildTags: string[], + resultExpected: boolean, + shouldConvertToSingleImageInput: boolean + ) { + const fragment = document.createDocumentFragment(); + const clipboardData = { + htmlFirstLevelChildTags, + }; + + const result = shouldConvertToSingleImage({ + fragment, + shouldConvertSingleImage: shouldConvertToSingleImageInput, + clipboardData, + }); + + expect(result).toEqual(resultExpected); + } +}); diff --git a/packages/roosterjs-editor-dom/test/selections/deleteSelectedContentTest.ts b/packages/roosterjs-editor-dom/test/selections/deleteSelectedContentTest.ts index 6a394b48c23c..b40f168a0425 100644 --- a/packages/roosterjs-editor-dom/test/selections/deleteSelectedContentTest.ts +++ b/packages/roosterjs-editor-dom/test/selections/deleteSelectedContentTest.ts @@ -81,14 +81,14 @@ describe('deleteSelectedContent', () => { ); }); - it('Whole talbe 1', () => { + it('Whole table 1', () => { runTest( 'aa
              line1line2
              line3line4
              bb', 'aabb' ); }); - it('Whole talbe 2', () => { + it('Whole table 2', () => { // TODO: the result contains separated continuous text object at root // Selection path gives wrong result. Need to revisit here runTest( @@ -138,4 +138,11 @@ describe('deleteSelectedContent', () => { '
              line3
              line4
              ' ); }); + + it('Readonly entities', () => { + runTest( + '
              hello there
              ', + '
              hello there
              ' + ); + }); }); diff --git a/packages/roosterjs-editor-dom/test/selections/getHtmlWithSelectionPathTest.ts b/packages/roosterjs-editor-dom/test/selections/getHtmlWithSelectionPathTest.ts index 4aeb393442e2..75b65096cef3 100644 --- a/packages/roosterjs-editor-dom/test/selections/getHtmlWithSelectionPathTest.ts +++ b/packages/roosterjs-editor-dom/test/selections/getHtmlWithSelectionPathTest.ts @@ -22,30 +22,4 @@ describe('getHtmlWithSelectionPath', () => { 'test1
              text 2test3test 4
              test 5' ); }); - - it('TABLE content', () => { - const div = document.createElement('div'); - const range = document.createRange(); - const table = document.createElement('table'); - - div.appendChild(document.createTextNode('test1')); - div.appendChild(table); - div.appendChild(document.createTextNode('test2')); - - const tr = document.createElement('tr'); - table.appendChild(tr); - - const td = document.createElement('td'); - tr.appendChild(td); - - const text = document.createTextNode('test'); - td.appendChild(text); - - range.setStart(text, 2); - range.setEnd(text, 3); - const html = getHtmlWithSelectionPath(div, range); - expect(html).toBe( - 'test1
              test
              test2' - ); - }); }); diff --git a/packages/roosterjs-editor-dom/test/selections/setHtmlWithMetadataTest.ts b/packages/roosterjs-editor-dom/test/selections/setHtmlWithMetadataTest.ts new file mode 100644 index 000000000000..15a7019c0628 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/selections/setHtmlWithMetadataTest.ts @@ -0,0 +1,520 @@ +import { SelectionRangeTypes } from 'roosterjs-editor-types'; +import { + extractContentMetadata, + setHtmlWithMetadata, +} from '../../lib/selection/setHtmlWithSelectionPath'; + +describe('setHtmlWithMetadata', () => { + let div: HTMLDivElement; + + beforeEach(() => { + div = document.createElement('div'); + }); + + it('pure HTML', () => { + const html = '
              test
              '; + const metadata = setHtmlWithMetadata(div, html); + + expect(div.innerHTML).toBe(html); + expect(metadata).toBeUndefined(); + }); + + it('HTML with empty comment', () => { + const html = '
              test
              '; + const metadata = setHtmlWithMetadata(div, html); + + expect(div.innerHTML).toBe(html); + expect(metadata).toBeUndefined(); + }); + + it('HTML with comment', () => { + const html = '
              test
              '; + const metadata = setHtmlWithMetadata(div, html); + + expect(div.innerHTML).toBe(html); + expect(metadata).toBeUndefined(); + }); + + it('HTML with comment and invalid JSON', () => { + const html = '
              test
              '; + const metadata = setHtmlWithMetadata(div, html); + + expect(div.innerHTML).toBe(html); + expect(metadata).toBeUndefined(); + }); + + it('HTML with half selection path', () => { + const html = '
              test
              '; + const metadata = setHtmlWithMetadata(div, html); + + expect(div.innerHTML).toBe(html); + expect(metadata).toBeUndefined(); + }); + + it('HTML with full selection path', () => { + const pureHtml = '
              test
              '; + const comment = { start: [], end: [] }; + const html = metadataToString(pureHtml, comment); + const metadata = setHtmlWithMetadata(div, html); + + expect(div.innerHTML).toBe(pureHtml); + expect(metadata).toEqual({ + start: [], + end: [], + type: SelectionRangeTypes.Normal, + isDarkMode: false, + }); + }); + + it('HTML with full normal content metadata', () => { + const pureHtml = '
              test
              '; + const comment = { + start: [1], + end: [2], + isDarkMode: true, + type: SelectionRangeTypes.Normal, + }; + const html = metadataToString(pureHtml, comment); + const metadata = setHtmlWithMetadata(div, html); + + expect(div.innerHTML).toBe(pureHtml); + expect(metadata).toEqual(comment); + }); + + it('HTML with full normal content metadata but wrong type', () => { + const pureHtml = '
              test
              '; + const comment = { + start: [1], + end: [2], + isDarkMode: true, + type: SelectionRangeTypes.TableSelection, + }; + const html = metadataToString(pureHtml, comment); + const metadata = setHtmlWithMetadata(div, html); + + expect(div.innerHTML).toBe(html); + expect(metadata).toBeUndefined(); + }); + + it('HTML with full table selection metadata', () => { + const pureHtml = '
              test
              '; + const comment = { + type: SelectionRangeTypes.TableSelection, + isDarkMode: true, + tableId: 'table', + firstCell: { + x: 1, + y: 2, + }, + lastCell: { + x: 3, + y: 4, + }, + }; + const html = metadataToString(pureHtml, comment); + const metadata = setHtmlWithMetadata(div, html); + + expect(div.innerHTML).toBe(pureHtml); + expect(metadata).toEqual(comment); + }); + + it('HTML with full table selection metadata but wrong type', () => { + const pureHtml = '
              test
              '; + const comment = { + type: SelectionRangeTypes.Normal, + isDarkMode: true, + tableId: 'table', + firstCell: { + x: 1, + y: 2, + }, + lastCell: { + x: 3, + y: 4, + }, + }; + const html = metadataToString(pureHtml, comment); + const metadata = setHtmlWithMetadata(div, html); + + expect(div.innerHTML).toBe(html); + expect(metadata).toBeUndefined(); + }); + + it('HTML with incomplete table selection metadata 1', () => { + const pureHtml = '
              test
              '; + const comment = { + type: SelectionRangeTypes.TableSelection, + tableId: 'table', + firstCell: { + x: 1, + y: 2, + }, + lastCell: { + x: 3, + y: 4, + }, + }; + const html = metadataToString(pureHtml, comment); + const metadata = setHtmlWithMetadata(div, html); + + expect(div.innerHTML).toBe(pureHtml); + expect(metadata).toEqual({ + type: SelectionRangeTypes.TableSelection, + tableId: 'table', + firstCell: { + x: 1, + y: 2, + }, + lastCell: { + x: 3, + y: 4, + }, + isDarkMode: false, + }); + }); + + it('HTML with incomplete table selection metadata 2', () => { + const pureHtml = '
              test
              '; + const comment = { + type: SelectionRangeTypes.TableSelection, + firstCell: { + x: 1, + y: 2, + }, + lastCell: { + x: 3, + y: 4, + }, + }; + const html = metadataToString(pureHtml, comment); + const metadata = setHtmlWithMetadata(div, html); + + expect(div.innerHTML).toBe(html); + expect(metadata).toBeUndefined(); + }); + + it('HTML with incomplete table selection metadata 3', () => { + const pureHtml = '
              test
              '; + const comment = { + type: SelectionRangeTypes.TableSelection, + tableId: 'table', + firstCell: { + y: 2, + }, + lastCell: { + x: 3, + y: 4, + }, + }; + const html = metadataToString(pureHtml, comment); + const metadata = setHtmlWithMetadata(div, html); + + expect(div.innerHTML).toBe(html); + expect(metadata).toBeUndefined(); + }); + + it('HTML with incomplete table selection metadata 4', () => { + const pureHtml = '
              test
              '; + const comment = { + type: SelectionRangeTypes.TableSelection, + tableId: 'table', + firstCell: { + x: 1, + y: 2, + }, + }; + const html = metadataToString(pureHtml, comment); + const metadata = setHtmlWithMetadata(div, html); + + expect(div.innerHTML).toBe(html); + expect(metadata).toBeUndefined(); + }); + + it('HTML with incomplete table selection metadata 5', () => { + const pureHtml = '
              test
              '; + const comment = { + type: SelectionRangeTypes.TableSelection, + tableId: 'table', + firstCell: { + x: 'test', + y: 2, + }, + lastCell: { + x: 3, + y: 4, + }, + }; + const html = metadataToString(pureHtml, comment); + const metadata = setHtmlWithMetadata(div, html); + + expect(div.innerHTML).toBe(html); + expect(metadata).toBeUndefined(); + }); +}); + +describe('extractContentMetadata', () => { + let div: HTMLDivElement; + + beforeEach(() => { + div = document.createElement('div'); + }); + + it('pure HTML', () => { + div.innerHTML = '
              test
              '; + const metadata = extractContentMetadata(div); + + expect(metadata).toBeUndefined(); + }); + + it('HTML with empty comment', () => { + div.innerHTML = '
              test
              '; + const metadata = extractContentMetadata(div); + + expect(metadata).toBeUndefined(); + }); + + it('HTML with comment', () => { + div.innerHTML = '
              test
              '; + const metadata = extractContentMetadata(div); + + expect(metadata).toBeUndefined(); + }); + + it('HTML with comment and invalid JSON', () => { + div.innerHTML = '
              test
              '; + const metadata = extractContentMetadata(div); + + expect(metadata).toBeUndefined(); + }); + + it('HTML with half selection path', () => { + div.innerHTML = '
              test
              '; + const metadata = extractContentMetadata(div); + + expect(metadata).toBeUndefined(); + }); + + it('HTML with full selection path', () => { + const pureHtml = '
              test
              '; + const comment = { start: [], end: [] }; + const html = metadataToString(pureHtml, comment); + + div.innerHTML = html; + + const metadata = extractContentMetadata(div); + + expect(div.innerHTML).toBe(pureHtml); + expect(metadata).toEqual({ + start: [], + end: [], + type: SelectionRangeTypes.Normal, + isDarkMode: false, + }); + }); + + it('HTML with full normal content metadata', () => { + const pureHtml = '
              test
              '; + const comment = { + start: [1], + end: [2], + isDarkMode: true, + type: SelectionRangeTypes.Normal, + }; + const html = metadataToString(pureHtml, comment); + div.innerHTML = html; + + const metadata = extractContentMetadata(div); + + expect(div.innerHTML).toBe(pureHtml); + expect(metadata).toEqual(comment); + }); + + it('HTML with full normal content metadata but wrong type', () => { + const pureHtml = '
              test
              '; + const comment = { + start: [1], + end: [2], + isDarkMode: true, + type: SelectionRangeTypes.TableSelection, + }; + const html = metadataToString(pureHtml, comment); + div.innerHTML = html; + const metadata = extractContentMetadata(div); + + expect(div.innerHTML).toBe(html); + expect(metadata).toBeUndefined(); + }); + + it('HTML with full table selection metadata', () => { + const pureHtml = '
              test
              '; + const comment = { + type: SelectionRangeTypes.TableSelection, + isDarkMode: true, + tableId: 'table', + firstCell: { + x: 1, + y: 2, + }, + lastCell: { + x: 3, + y: 4, + }, + }; + const html = metadataToString(pureHtml, comment); + div.innerHTML = html; + + const metadata = extractContentMetadata(div); + + expect(div.innerHTML).toBe(pureHtml); + expect(metadata).toEqual(comment); + }); + + it('HTML with full table selection metadata but wrong type', () => { + const pureHtml = '
              test
              '; + const comment = { + type: SelectionRangeTypes.Normal, + isDarkMode: true, + tableId: 'table', + firstCell: { + x: 1, + y: 2, + }, + lastCell: { + x: 3, + y: 4, + }, + }; + const html = metadataToString(pureHtml, comment); + div.innerHTML = html; + + const metadata = extractContentMetadata(div); + + expect(div.innerHTML).toBe(html); + expect(metadata).toBeUndefined(); + }); + + it('HTML with incomplete table selection metadata 1', () => { + const pureHtml = '
              test
              '; + const comment = { + type: SelectionRangeTypes.TableSelection, + tableId: 'table', + firstCell: { + x: 1, + y: 2, + }, + lastCell: { + x: 3, + y: 4, + }, + }; + const html = metadataToString(pureHtml, comment); + div.innerHTML = html; + + const metadata = extractContentMetadata(div); + + expect(div.innerHTML).toBe(pureHtml); + expect(metadata).toEqual({ + type: SelectionRangeTypes.TableSelection, + tableId: 'table', + firstCell: { + x: 1, + y: 2, + }, + lastCell: { + x: 3, + y: 4, + }, + isDarkMode: false, + }); + }); + + it('HTML with incomplete table selection metadata 2', () => { + const pureHtml = '
              test
              '; + const comment = { + type: SelectionRangeTypes.TableSelection, + firstCell: { + x: 1, + y: 2, + }, + lastCell: { + x: 3, + y: 4, + }, + }; + const html = metadataToString(pureHtml, comment); + div.innerHTML = html; + + const metadata = extractContentMetadata(div); + + expect(div.innerHTML).toBe(html); + expect(metadata).toBeUndefined(); + }); + + it('HTML with incomplete table selection metadata 3', () => { + const pureHtml = '
              test
              '; + const comment = { + type: SelectionRangeTypes.TableSelection, + tableId: 'table', + firstCell: { + y: 2, + }, + lastCell: { + x: 3, + y: 4, + }, + }; + const html = metadataToString(pureHtml, comment); + div.innerHTML = html; + + const metadata = extractContentMetadata(div); + + expect(div.innerHTML).toBe(html); + expect(metadata).toBeUndefined(); + }); + + it('HTML with incomplete table selection metadata 4', () => { + const pureHtml = '
              test
              '; + const comment = { + type: SelectionRangeTypes.TableSelection, + tableId: 'table', + firstCell: { + x: 1, + y: 2, + }, + }; + const html = metadataToString(pureHtml, comment); + div.innerHTML = html; + + const metadata = extractContentMetadata(div); + + expect(div.innerHTML).toBe(html); + expect(metadata).toBeUndefined(); + }); + + it('HTML with incomplete table selection metadata 5', () => { + const pureHtml = '
              test
              '; + const comment = { + type: SelectionRangeTypes.TableSelection, + tableId: 'table', + firstCell: { + x: 'test', + y: 2, + }, + lastCell: { + x: 3, + y: 4, + }, + }; + const html = metadataToString(pureHtml, comment); + div.innerHTML = html; + + const metadata = extractContentMetadata(div); + + expect(div.innerHTML).toBe(html); + expect(metadata).toBeUndefined(); + }); +}); + +function metadataToString(html: string, metadata: object): string { + return html + (metadata ? `` : ''); +} diff --git a/packages/roosterjs-editor-dom/test/snapshots/addSnapshotTest.ts b/packages/roosterjs-editor-dom/test/snapshots/addSnapshotTest.ts index 9b79a9e6cf7c..8890d8b27ca8 100644 --- a/packages/roosterjs-editor-dom/test/snapshots/addSnapshotTest.ts +++ b/packages/roosterjs-editor-dom/test/snapshots/addSnapshotTest.ts @@ -1,6 +1,6 @@ -import addSnapshot from '../../lib/snapshots/addSnapshot'; +import addSnapshot, { addSnapshotV2 } from '../../lib/snapshots/addSnapshot'; import createSnapshots from '../../lib/snapshots/createSnapshots'; -import { Snapshots } from 'roosterjs-editor-types'; +import { Snapshot, Snapshots } from 'roosterjs-editor-types'; describe('addSnapshot', () => { function runTest( @@ -95,3 +95,72 @@ describe('addSnapshot', () => { ); }); }); + +describe('addSnapshotV2', () => { + it('Add snapshot with entity state', () => { + const snapshots: Snapshots = createSnapshots(100000); + const mockedMetadata = 'METADATA' as any; + const mockedEntityStates = 'ENTITYSTATES' as any; + + addSnapshotV2( + snapshots, + { + html: 'test', + metadata: null, + knownColors: [], + }, + false + ); + + expect(snapshots.snapshots).toEqual([ + { + html: 'test', + metadata: null, + knownColors: [], + }, + ]); + + addSnapshotV2( + snapshots, + { + html: 'test', + metadata: mockedMetadata, + knownColors: [], + }, + false + ); + + expect(snapshots.snapshots).toEqual([ + { + html: 'test', + metadata: mockedMetadata, + knownColors: [], + }, + ]); + + addSnapshotV2( + snapshots, + { + html: 'test', + metadata: null, + knownColors: [], + entityStates: mockedEntityStates, + }, + false + ); + + expect(snapshots.snapshots).toEqual([ + { + html: 'test', + metadata: mockedMetadata, + knownColors: [], + }, + { + html: 'test', + metadata: null, + knownColors: [], + entityStates: mockedEntityStates, + }, + ]); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/style/removeGlobalCssStylesTest.ts b/packages/roosterjs-editor-dom/test/style/removeGlobalCssStylesTest.ts new file mode 100644 index 000000000000..dc9f78753db7 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/style/removeGlobalCssStylesTest.ts @@ -0,0 +1,35 @@ +import removeGlobalCssStyle from '../../lib/style/removeGlobalCssStyle'; +import setGlobalCssStyles from '../../lib/style/setGlobalCssStyles'; + +describe('removeGlobalCssStyle', () => { + let div: HTMLDivElement; + let span: HTMLSpanElement; + beforeEach(() => { + div = document.createElement('div'); + div.id = 'editorTest'; + span = document.createElement('span'); + span.id = 'test'; + div.appendChild(span); + document.body.appendChild(div); + }); + + afterEach(() => { + document.body.removeChild(div); + div = null; + }); + + it('should add an style ', () => { + const css = + '#' + + 'editorTest' + + ' #' + + 'test' + + ' { margin: -2px; border: 2px solid' + + '#DB626C' + + ' !important; }'; + setGlobalCssStyles(document, css, div.id + span.id); + removeGlobalCssStyle(document, div.id + span.id); + const styleTag = document.getElementById('editorTesttest'); + expect(styleTag).toBe(null); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/style/removeImportantStyleTest.ts b/packages/roosterjs-editor-dom/test/style/removeImportantStyleTest.ts new file mode 100644 index 000000000000..a925dfd62b97 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/style/removeImportantStyleTest.ts @@ -0,0 +1,29 @@ +import removeImportantStyleRule from '../../lib/style/removeImportantStyleRule'; + +describe('removeImportantStyleRule', () => { + function runTest(styles: string[], expected: string) { + const div = document.createElement('div'); + div.setAttribute( + 'style', + 'border:1px solid black !important; background-color: green !important;' + ); + removeImportantStyleRule(div, styles); + const style = div.getAttribute('style'); + expect(style).toEqual(expected); + } + + it('should remove important from all', () => { + runTest(['border', 'background-color'], 'border:1px solid black;background-color:green'); + }); + + it('should remove important from border', () => { + runTest(['border'], 'border:1px solid black;background-color:green !important'); + }); + + it('should not remove important', () => { + runTest( + ['margin'], + 'border:1px solid black !important; background-color: green !important;' + ); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/style/setGlobalCssStylesTest.ts b/packages/roosterjs-editor-dom/test/style/setGlobalCssStylesTest.ts new file mode 100644 index 000000000000..4b7cfc0577d4 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/style/setGlobalCssStylesTest.ts @@ -0,0 +1,60 @@ +import setGlobalCssStyles from '../../lib/style/setGlobalCssStyles'; + +describe('setGlobalCssStyles', () => { + let div: HTMLDivElement; + let span: HTMLSpanElement; + beforeEach(() => { + div = document.createElement('div'); + div.id = 'editorTest'; + span = document.createElement('span'); + span.id = 'test'; + div.appendChild(span); + document.body.appendChild(div); + }); + + afterEach(() => { + document.body.removeChild(div); + div.remove(); + }); + + it('Should modify existing style ', () => { + const styleid = 'oldStyle'; + const css1 = '#TestClass { color: red; }'; + const css2 = '#TestClass { color: blue; }'; + const oldStyle = document.createElement('style'); + oldStyle.id = styleid; + document.head.appendChild(oldStyle); + oldStyle.sheet?.insertRule(css1); + expect(oldStyle.sheet?.cssRules.length).toBe(1); + // Should add a new rule to existing style + setGlobalCssStyles(document, css2, styleid); + const styleTag = document.getElementById(styleid) as HTMLStyleElement; + expect(styleTag?.tagName).toBe('STYLE'); + expect(styleTag.sheet?.cssRules.length).toBe(2); + }); + + it('Should add a new style ', () => { + const styleid = 'newStyle'; + const css = + '#' + + 'editorTest' + + ' #' + + 'test' + + ' { margin: -2px; border: 2px solid' + + '#DB626C' + + ' !important; }'; + // Should create a style tag with id newStyle, and the above rule + setGlobalCssStyles(document, css, styleid); + const styleTag = document.getElementById(styleid) as HTMLStyleElement; + expect(styleTag?.tagName).toBe('STYLE'); + }); + + it('Should not add a new style ', () => { + const styleid = 'noStyle'; + const css = ''; + // Should no create a style tag with id noStyle + setGlobalCssStyles(document, css, styleid); + const styleTag = document.getElementById(styleid) as HTMLStyleElement; + expect(styleTag).toBeNull(); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/table/VTableTest.ts b/packages/roosterjs-editor-dom/test/table/VTableTest.ts index cdd1576b11bf..5128bf802574 100644 --- a/packages/roosterjs-editor-dom/test/table/VTableTest.ts +++ b/packages/roosterjs-editor-dom/test/table/VTableTest.ts @@ -1,6 +1,6 @@ import VTable from '../../lib/table/VTable'; import { itFirefoxOnly } from '../DomTestHelper'; -import { TableFormat, TableOperation } from 'roosterjs-editor-types'; +import { TableFormat, TableOperation, TableSelection } from 'roosterjs-editor-types'; describe('VTable.ctor', () => { function runTest( @@ -348,12 +348,19 @@ describe('VTable.edit', () => { let complexTable = '
              12
              34
              5
              '; - function runTest(input: string, id: string, operation: TableOperation, expectedHtml: string) { + function runTest( + input: string, + id: string, + operation: TableOperation, + expectedHtml: string, + selection?: TableSelection + ) { let div = document.createElement('div'); document.body.appendChild(div); div.innerHTML = input; let node = document.getElementById(id) as HTMLTableElement; let vTable = new VTable(node); + vTable.selection = selection; vTable.edit(operation); vTable.writeBack(); const expectedDiv = document.createElement('div'); @@ -364,17 +371,25 @@ describe('VTable.edit', () => { document.body.removeChild(div); } - function runSimpleTableTestOnId1(operation: TableOperation, expectedHtml: string) { - runTest(simpleTable, 'id1', operation, expectedHtml); + function runSimpleTableTestOnId1( + operation: TableOperation, + expectedHtml: string, + selection?: TableSelection + ) { + runTest(simpleTable, 'id1', operation, expectedHtml, selection); } function runSimpleTableTestOnId2(operation: TableOperation, expectedHtml: string) { runTest(simpleTable, 'id2', operation, expectedHtml); } - function runComplexTableTest(operation: TableOperation, expectedResults: string[]) { + function runComplexTableTest( + operation: TableOperation, + expectedResults: string[], + selection?: TableSelection + ) { for (let i = 1; i <= 5; i++) { - runTest(complexTable, 'id' + i, operation, expectedResults[i - 1]); + runTest(complexTable, 'id' + i, operation, expectedResults[i - 1], selection); } } @@ -393,7 +408,15 @@ describe('VTable.edit', () => { ); }); - it('Simple table, DeleteRow', () => { + it('Simple table, DeleteColumn with selection', () => { + runSimpleTableTestOnId1( + TableOperation.DeleteColumn, + '
              2
              4
              ', + { firstCell: { x: 0, y: 0 }, lastCell: { x: 0, y: 1 } } + ); + }); + + it('Simple table, DeleteRow ', () => { runSimpleTableTestOnId1( TableOperation.DeleteRow, '
              34
              ' @@ -404,6 +427,14 @@ describe('VTable.edit', () => { ); }); + it('Simple table, DeleteRow with selection', () => { + runSimpleTableTestOnId1( + TableOperation.DeleteRow, + '
              34
              ', + { firstCell: { x: 0, y: 0 }, lastCell: { x: 1, y: 0 } } + ); + }); + it('Simple table, DeleteTable', () => { runSimpleTableTestOnId1(TableOperation.DeleteTable, ''); runSimpleTableTestOnId2(TableOperation.DeleteTable, ''); @@ -420,6 +451,14 @@ describe('VTable.edit', () => { ); }); + it('Simple table, InsertAbove with selection', () => { + runSimpleTableTestOnId1( + TableOperation.InsertAbove, + '




              12
              34
              ', + { firstCell: { x: 0, y: 0 }, lastCell: { x: 0, y: 1 } } + ); + }); + it('Simple table, InsertBelow', () => { runSimpleTableTestOnId1( TableOperation.InsertBelow, @@ -431,6 +470,14 @@ describe('VTable.edit', () => { ); }); + it('Simple table, InsertBelow with selection', () => { + runSimpleTableTestOnId1( + TableOperation.InsertBelow, + '
              12
              34




              ', + { firstCell: { x: 0, y: 0 }, lastCell: { x: 0, y: 1 } } + ); + }); + it('Simple table, InsertLeft', () => { runSimpleTableTestOnId1( TableOperation.InsertLeft, @@ -442,6 +489,14 @@ describe('VTable.edit', () => { ); }); + it('Simple table, InsertLeft with selection ', () => { + runSimpleTableTestOnId1( + TableOperation.InsertLeft, + '


              12


              34
              ', + { firstCell: { x: 0, y: 0 }, lastCell: { x: 1, y: 0 } } + ); + }); + it('Simple table, InsertRight', () => { runSimpleTableTestOnId1( TableOperation.InsertRight, @@ -453,6 +508,14 @@ describe('VTable.edit', () => { ); }); + it('Simple table, InsertRight with selection', () => { + runSimpleTableTestOnId1( + TableOperation.InsertRight, + '
              12

              34

              ', + { firstCell: { x: 0, y: 0 }, lastCell: { x: 1, y: 0 } } + ); + }); + it('Simple table, MergeAbove', () => { runSimpleTableTestOnId1( TableOperation.MergeAbove, @@ -460,14 +523,14 @@ describe('VTable.edit', () => { ); runSimpleTableTestOnId2( TableOperation.MergeAbove, - '
              124
              3
              ' + '
              12
              4
              3
              ' ); }); it('Simple table, MergeBelow', () => { runSimpleTableTestOnId1( TableOperation.MergeBelow, - '
              132
              4
              ' + '
              1
              3
              2
              4
              ' ); runSimpleTableTestOnId2( TableOperation.MergeBelow, @@ -482,14 +545,14 @@ describe('VTable.edit', () => { ); runSimpleTableTestOnId2( TableOperation.MergeLeft, - '
              12
              34
              ' + '
              12
              3
              4
              ' ); }); it('Simple table, MergeRight', () => { runSimpleTableTestOnId1( TableOperation.MergeRight, - '
              12
              34
              ' + '
              1
              2
              34
              ' ); runSimpleTableTestOnId2( TableOperation.MergeRight, @@ -497,6 +560,14 @@ describe('VTable.edit', () => { ); }); + it('Simple table, MergeCells', () => { + runSimpleTableTestOnId1( + TableOperation.MergeCells, + '
              1
              2
              34
              ', + { firstCell: { x: 0, y: 0 }, lastCell: { x: 1, y: 0 } } + ); + }); + it('Simple table, SplitHorizontally', () => { runSimpleTableTestOnId1( TableOperation.SplitHorizontally, @@ -552,6 +623,78 @@ describe('VTable.edit', () => { ); }); + itFirefoxOnly('Simple table, AlignCellCenter', () => { + runSimpleTableTestOnId1( + TableOperation.AlignCellCenter, + '
              12
              34
              ' + ); + runSimpleTableTestOnId1( + TableOperation.AlignCellCenter, + '
              12
              34
              ', + { firstCell: { x: 0, y: 0 }, lastCell: { x: 0, y: 1 } } + ); + }); + + itFirefoxOnly('Simple table, AlignCellRight', () => { + runSimpleTableTestOnId1( + TableOperation.AlignCellRight, + '
              12
              34
              ' + ); + runSimpleTableTestOnId1( + TableOperation.AlignCellRight, + '
              12
              34
              ', + { firstCell: { x: 0, y: 0 }, lastCell: { x: 0, y: 1 } } + ); + }); + + itFirefoxOnly('Simple table, AlignCellLeft', () => { + runSimpleTableTestOnId1( + TableOperation.AlignCellLeft, + '
              12
              34
              ' + ); + runSimpleTableTestOnId1( + TableOperation.AlignCellLeft, + '
              12
              34
              ', + { firstCell: { x: 0, y: 0 }, lastCell: { x: 0, y: 1 } } + ); + }); + + itFirefoxOnly('Simple table, AlignCellTop', () => { + runSimpleTableTestOnId1( + TableOperation.AlignCellTop, + '
              12
              34
              ' + ); + runSimpleTableTestOnId1( + TableOperation.AlignCellTop, + '
              12
              34
              ', + { firstCell: { x: 0, y: 0 }, lastCell: { x: 0, y: 1 } } + ); + }); + + itFirefoxOnly('Simple table, AlignCellMiddle', () => { + runSimpleTableTestOnId1( + TableOperation.AlignCellMiddle, + '
              12
              34
              ' + ); + runSimpleTableTestOnId1( + TableOperation.AlignCellMiddle, + '
              12
              34
              ', + { firstCell: { x: 0, y: 0 }, lastCell: { x: 0, y: 1 } } + ); + }); + + itFirefoxOnly('Simple table, AlignCellBottom', () => { + runSimpleTableTestOnId1( + TableOperation.AlignCellBottom, + '
              12
              34
              ' + ); + runSimpleTableTestOnId1( + TableOperation.AlignCellBottom, + '
              12
              34
              ', + { firstCell: { x: 0, y: 0 }, lastCell: { x: 0, y: 1 } } + ); + }); + it('Complex table, DeleteColumn', () => { runComplexTableTest(TableOperation.DeleteColumn, [ '
              2
              34
              5
              ', @@ -562,6 +705,20 @@ describe('VTable.edit', () => { ]); }); + it('Complex table, DeleteColumn with selection', () => { + runComplexTableTest( + TableOperation.DeleteColumn, + [ + '
              2
              34
              5
              ', + '
              2
              34
              5
              ', + '
              2
              34
              5
              ', + '
              2
              34
              5
              ', + '
              2
              34
              5
              ', + ], + { firstCell: { x: 0, y: 0 }, lastCell: { x: 0, y: 1 } } + ); + }); + it('Complex table, DeleteRow', () => { runComplexTableTest(TableOperation.DeleteRow, [ '
              134
              5
              ', @@ -572,6 +729,20 @@ describe('VTable.edit', () => { ]); }); + it('Complex table, DeleteRow with selection', () => { + runComplexTableTest( + TableOperation.DeleteRow, + [ + '
              134
              5
              ', + '
              134
              5
              ', + '
              134
              5
              ', + '
              134
              5
              ', + '
              134
              5
              ', + ], + { firstCell: { x: 0, y: 0 }, lastCell: { x: 1, y: 0 } } + ); + }); + it('Complex table, DeleteTable', () => { runComplexTableTest(TableOperation.DeleteTable, ['', '', '', '', '']); }); @@ -586,6 +757,20 @@ describe('VTable.edit', () => { ]); }); + it('Complex table, InsertAbove with selection', () => { + runComplexTableTest( + TableOperation.InsertAbove, + [ + '




              12
              34
              5
              ', + '




              12
              34
              5
              ', + '




              12
              34
              5
              ', + '




              12
              34
              5
              ', + '




              12
              34
              5
              ', + ], + { firstCell: { x: 0, y: 0 }, lastCell: { x: 0, y: 1 } } + ); + }); + it('Complex table, InsertBelow', () => { runComplexTableTest(TableOperation.InsertBelow, [ '
              12
              34


              5
              ', @@ -596,6 +781,20 @@ describe('VTable.edit', () => { ]); }); + it('Complex table, InsertBelow with selection', () => { + runComplexTableTest( + TableOperation.InsertBelow, + [ + '
              12
              34




              5
              ', + '
              12
              34




              5
              ', + '
              12
              34




              5
              ', + '
              12
              34




              5
              ', + '
              12
              34




              5
              ', + ], + { firstCell: { x: 0, y: 0 }, lastCell: { x: 0, y: 1 } } + ); + }); + it('Complex table, InsertLeft', () => { runComplexTableTest(TableOperation.InsertLeft, [ '

              12
              34

              5
              ', @@ -606,6 +805,20 @@ describe('VTable.edit', () => { ]); }); + it('Complex table, InsertLeft with selection', () => { + runComplexTableTest( + TableOperation.InsertLeft, + [ + '


              12
              34


              5
              ', + '


              12
              34


              5
              ', + '


              12
              34


              5
              ', + '


              12
              34


              5
              ', + '


              12
              34


              5
              ', + ], + { firstCell: { x: 0, y: 0 }, lastCell: { x: 1, y: 0 } } + ); + }); + it('Complex table, InsertRight', () => { runComplexTableTest(TableOperation.InsertRight, [ '
              1
              2
              34
              5
              ', @@ -616,6 +829,20 @@ describe('VTable.edit', () => { ]); }); + it('Complex table, InsertRight with selection', () => { + runComplexTableTest( + TableOperation.InsertRight, + [ + '
              12

              34

              5
              ', + '
              12

              34

              5
              ', + '
              12

              34

              5
              ', + '
              12

              34

              5
              ', + '
              12

              34

              5
              ', + ], + { firstCell: { x: 0, y: 0 }, lastCell: { x: 1, y: 0 } } + ); + }); + it('Complex table, MergeAbove', () => { runComplexTableTest(TableOperation.MergeAbove, [ '
              12
              34
              5
              ', @@ -656,6 +883,20 @@ describe('VTable.edit', () => { ]); }); + it('Complex table, MergeCells', () => { + runComplexTableTest( + TableOperation.MergeCells, + [ + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + ], + { firstCell: { x: 0, y: 0 }, lastCell: { x: 1, y: 0 } } + ); + }); + it('Complex table, SplitHorizontally', () => { runComplexTableTest(TableOperation.SplitHorizontally, [ '
              1
              2
              34
              5
              ', @@ -702,6 +943,80 @@ describe('VTable.edit', () => { '
              12
              34
              5
              ', ]); }); + + itFirefoxOnly('Complex table, AlignCellCenter', () => { + runComplexTableTest( + TableOperation.AlignCellCenter, + [ + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + ], + { firstCell: { x: 0, y: 0 }, lastCell: { x: 0, y: 1 } } + ); + }); + itFirefoxOnly('Complex table, AlignCellRight', () => { + runComplexTableTest(TableOperation.AlignCellRight, [ + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + ]); + }); + itFirefoxOnly('Complex table, AlignCellLeft', () => { + runComplexTableTest(TableOperation.AlignCellLeft, [ + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + ]); + }); + + itFirefoxOnly('Complex table, AlignCellTop', () => { + runComplexTableTest( + TableOperation.AlignCellTop, + [ + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + ], + { firstCell: { x: 0, y: 0 }, lastCell: { x: 0, y: 1 } } + ); + }); + + itFirefoxOnly('Complex table, AlignCellMiddle', () => { + runComplexTableTest( + TableOperation.AlignCellMiddle, + [ + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + ], + { firstCell: { x: 0, y: 0 }, lastCell: { x: 0, y: 1 } } + ); + }); + + itFirefoxOnly('Complex table, AlignCellBottom', () => { + runComplexTableTest( + TableOperation.AlignCellBottom, + [ + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + '
              12
              34
              5
              ', + ], + { firstCell: { x: 0, y: 0 }, lastCell: { x: 0, y: 1 } } + ); + }); }); describe('VTable.getCell', () => { diff --git a/packages/roosterjs-editor-dom/test/table/applyTableFormatTest.ts b/packages/roosterjs-editor-dom/test/table/applyTableFormatTest.ts new file mode 100644 index 000000000000..b47e227c3f55 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/table/applyTableFormatTest.ts @@ -0,0 +1,40 @@ +import applyTableFormat from '../../lib/table/applyTableFormat'; +import VTable from '../../lib/table/VTable'; +import { itChromeOnly } from '../DomTestHelper'; +import { TableFormat } from 'roosterjs-editor-types'; + +const format: Required = { + topBorderColor: '#0C64C0', + bottomBorderColor: '#0C64C0', + verticalBorderColor: '#0C64C0', + bgColorEven: '#0C64C020', + bgColorOdd: null, + headerRowColor: null, + tableBorderFormat: 0, + hasHeaderRow: false, + hasFirstColumn: false, + hasBandedRows: false, + hasBandedColumns: false, + keepCellShade: false, +}; + +describe('applyTableFormat', () => { + let table = + '









              '; + let expectedTableChrome = + '









              '; + + let div = document.createElement('div'); + document.body.appendChild(div); + const id = 'id1'; + div.innerHTML = table; + let node = document.getElementById(id) as HTMLTableElement; + let vTable = new VTable(node); + vTable.applyFormat(format); + applyTableFormat(node, vTable.cells, format); + vTable.writeBack(); + itChromeOnly('should return a styled table CHROME', () => { + expect(div.innerHTML).toBe(expectedTableChrome); + }); + document.body.removeChild(div); +}); diff --git a/packages/roosterjs-editor-dom/test/table/cloneCellStylesTest.ts b/packages/roosterjs-editor-dom/test/table/cloneCellStylesTest.ts new file mode 100644 index 000000000000..1e2aecb10d32 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/table/cloneCellStylesTest.ts @@ -0,0 +1,16 @@ +import cloneCellStyles from '../../lib/table/cloneCellStyles'; + +describe('cloneCellStyles', () => { + function runTest(style: string) { + const cell = document.createElement('td'); + const styledCell = document.createElement('td'); + styledCell.setAttribute('style', style); + cloneCellStyles(cell, styledCell); + expect(cell.getAttribute('style')).toEqual(style); + expect(cell.getAttribute('data-editing-info')).toBe('{"bgColorOverride":true}'); + } + + it('cloneCellStyles | should clone style and add metadata', () => { + runTest('color: red'); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/table/pasteTableTest.ts b/packages/roosterjs-editor-dom/test/table/pasteTableTest.ts new file mode 100644 index 000000000000..4186a8d7a5bd --- /dev/null +++ b/packages/roosterjs-editor-dom/test/table/pasteTableTest.ts @@ -0,0 +1,70 @@ +import pasteTable from '../../lib/table/pasteTable'; +import { NodePosition } from 'roosterjs-editor-types'; + +const ID = 'id1'; +const TABLE123 = + '1
              23
              456
              789
              "; +/* + |1|2|3| + |4|5|6| + |7|8|9| + */ +const TABLEABC = + "
              abc
              def
              ghi
              "; +/* + |a|b|c| + |d|e|f| + |g|h|i| + */ + +describe('PasteTable', () => { + let div = document.createElement('div'); + let copyBase = document.createElement('div'); + let node: HTMLElement; + + beforeEach(() => {}); + + afterEach(() => { + document.body.removeChild(div); + }); + + function runTest( + editorTable: string, + clipboardTable: string, + pivotRow: number, + pivotCol: number + ) { + div.innerHTML = editorTable; + copyBase.innerHTML = clipboardTable; + document.body.appendChild(div); + node = document.getElementById(ID) as HTMLElement; + pasteTable( + node.firstChild?.childNodes[pivotRow].childNodes[pivotCol] as HTMLTableCellElement, + copyBase.firstChild! as HTMLTableElement, + ({ + node: node.firstChild?.childNodes[pivotRow].childNodes[pivotCol], + } as unknown) as NodePosition, + new Range() + ); + } + + it('Paste table | Same size', () => { + runTest(TABLE123, TABLEABC, 0, 0); + expect(node.childNodes.length).toEqual(3); + expect(node.childNodes[0].childNodes.length).toEqual(3); + }); + + it('Paste table | 3X3 to 5X5', () => { + runTest(TABLE123, TABLEABC, 2, 2); + expect(node.childNodes.length).toEqual(5); + expect(node.childNodes[0].childNodes.length).toEqual(5); + }); + + it('Paste table | copy styles', () => { + runTest(TABLE123, TABLEABC, 0, 0); + const pivotCell = document.getElementById('pivotCell') as HTMLTableCellElement; + expect(pivotCell.style.color).toEqual('red'); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/table/tableFormatInfoTest.ts b/packages/roosterjs-editor-dom/test/table/tableFormatInfoTest.ts index a2119a634d50..a17314cb93b6 100644 --- a/packages/roosterjs-editor-dom/test/table/tableFormatInfoTest.ts +++ b/packages/roosterjs-editor-dom/test/table/tableFormatInfoTest.ts @@ -2,7 +2,7 @@ import VTable from '../../lib/table/VTable'; import { getTableFormatInfo, saveTableInfo } from '../../lib/table/tableFormatInfo'; import { TableFormat } from 'roosterjs-editor-types'; -const TABLE_STYLE_INFO = 'roosterTableInfo'; +const TABLE_STYLE_INFO = 'editingInfo'; const format: TableFormat = { topBorderColor: '#0C64C0', bottomBorderColor: '#0C64C0', @@ -15,10 +15,11 @@ const format: TableFormat = { hasFirstColumn: false, hasBandedRows: false, hasBandedColumns: false, + keepCellShade: false, }; const expectedTableInfo = - '{"topBorderColor":"#0C64C0","bottomBorderColor":"#0C64C0","verticalBorderColor":"#0C64C0","bgColorEven":"#0C64C020","bgColorOdd":null,"headerRowColor":null,"tableBorderFormat":0, "hasHeaderRow": false, "hasFirstColumn": false, "hasBandedRows": false, "hasBandedColumns": false}'; + '{"topBorderColor":"#0C64C0","bottomBorderColor":"#0C64C0","verticalBorderColor":"#0C64C0","bgColorEven":"#0C64C020","bgColorOdd":null,"headerRowColor":null,"tableBorderFormat":0, "hasHeaderRow": false, "hasFirstColumn": false, "hasBandedRows": false, "hasBandedColumns": false, "keepCellShade": false}'; function createTable(format: TableFormat) { let div = document.createElement('div'); @@ -41,7 +42,7 @@ describe('getTableFormatInfo', () => { it('should return the info of a table ', () => { const table = createTable(format); const tableInfo = getTableFormatInfo(table); - expect(tableInfo).toEqual(JSON.parse(expectedTableInfo) as TableFormat); + expect(tableInfo).toEqual(JSON.parse(expectedTableInfo) as Required); removeTable(); }); }); diff --git a/packages/roosterjs-editor-dom/test/typeUtils/typeUtilsTest.ts b/packages/roosterjs-editor-dom/test/typeUtils/typeUtilsTest.ts index c526df54d034..a0140d27a78a 100644 --- a/packages/roosterjs-editor-dom/test/typeUtils/typeUtilsTest.ts +++ b/packages/roosterjs-editor-dom/test/typeUtils/typeUtilsTest.ts @@ -32,20 +32,6 @@ describe('safeInstanceOf', () => { expect(remoteWindow).toBe(iframeDocument.defaultView, 'remoteNode => getTargetWindow'); }); - it('getTargetWindow for Range', () => { - const localRange = document.createRange(); - const remoteRange = iframeDocument.createRange(); - const localWindow = getTargetWindow(localRange); - const remoteWindow = getTargetWindow(remoteRange); - expect(localWindow).toBe(window, 'localNode => getTargetWindow'); - expect(localWindow).not.toBe( - iframeDocument.defaultView, - 'localNode => getTargetWindow' - ); - expect(remoteWindow).not.toBe(window, 'remoteNode => getTargetWindow'); - expect(remoteWindow).toBe(iframeDocument.defaultView, 'remoteNode => getTargetWindow'); - }); - it('Node', () => { const localNode = document.createTextNode('test 1'); const remoteNode = iframeDocument.createTextNode('test 2'); diff --git a/packages/roosterjs-editor-dom/test/utils/BrowserTest.ts b/packages/roosterjs-editor-dom/test/utils/BrowserTest.ts index 1c34c65e0263..e61467d9050f 100644 --- a/packages/roosterjs-editor-dom/test/utils/BrowserTest.ts +++ b/packages/roosterjs-editor-dom/test/utils/BrowserTest.ts @@ -2,7 +2,7 @@ import { BrowserInfo } from 'roosterjs-editor-types'; import { getBrowserInfo } from '../../lib/utils/Browser'; function runBrowserDataTest(userAgent: string, appVersion: string, expected: BrowserInfo): void { - let b = getBrowserInfo(userAgent, appVersion); + let b = getBrowserInfo(userAgent, appVersion, ''); expect(b.isChrome).toBe(expected.isChrome); expect(b.isEdge).toBe(expected.isEdge); expect(b.isFirefox).toBe(expected.isFirefox); @@ -28,6 +28,7 @@ describe('getBrowserData', () => { isSafari: true, isWebKit: true, isWin: false, + isMobileOrTablet: false, } ); }); @@ -46,6 +47,7 @@ describe('getBrowserData', () => { isSafari: false, isWebKit: false, isWin: true, + isMobileOrTablet: false, } ); }); @@ -64,6 +66,7 @@ describe('getBrowserData', () => { isSafari: false, isWebKit: true, isWin: true, + isMobileOrTablet: false, } ); }); @@ -82,6 +85,7 @@ describe('getBrowserData', () => { isSafari: false, isWebKit: false, isWin: true, + isMobileOrTablet: false, } ); }); @@ -100,6 +104,7 @@ describe('getBrowserData', () => { isSafari: false, isWebKit: false, isWin: true, + isMobileOrTablet: false, } ); }); @@ -118,6 +123,7 @@ describe('getBrowserData', () => { isSafari: false, isWebKit: false, isWin: true, + isMobileOrTablet: false, } ); }); diff --git a/packages/roosterjs-editor-dom/test/utils/applyTableFormatTest.ts b/packages/roosterjs-editor-dom/test/utils/applyTableFormatTest.ts deleted file mode 100644 index 483ead6347be..000000000000 --- a/packages/roosterjs-editor-dom/test/utils/applyTableFormatTest.ts +++ /dev/null @@ -1,38 +0,0 @@ -import applyTableFormat from '../../lib/utils/applyTableFormat'; -import VTable from '../../lib/table/VTable'; -import { itChromeOnly } from '../DomTestHelper'; -import { TableFormat } from 'roosterjs-editor-types'; - -const format: TableFormat = { - topBorderColor: '#0C64C0', - bottomBorderColor: '#0C64C0', - verticalBorderColor: '#0C64C0', - bgColorEven: '#0C64C020', - bgColorOdd: null, - headerRowColor: null, - tableBorderFormat: 0, - hasHeaderRow: false, - hasFirstColumn: false, - hasBandedRows: false, - hasBandedColumns: false, -}; - -describe('applyTableFormat', () => { - let table = - '









              '; - let expectedTableChrome = - '









              '; - let div = document.createElement('div'); - document.body.appendChild(div); - const id = 'id1'; - div.innerHTML = table; - let node = document.getElementById(id) as HTMLTableElement; - let vTable = new VTable(node); - vTable.applyFormat(format); - applyTableFormat(node, vTable.cells, format); - vTable.writeBack(); - itChromeOnly('should return a styled table CHROME', () => { - expect(div.innerHTML).toBe(expectedTableChrome); - }); - document.body.removeChild(div); -}); diff --git a/packages/roosterjs-editor-dom/test/utils/createElementTest.ts b/packages/roosterjs-editor-dom/test/utils/createElementTest.ts index 1150592368e9..9b80a49602e4 100644 --- a/packages/roosterjs-editor-dom/test/utils/createElementTest.ts +++ b/packages/roosterjs-editor-dom/test/utils/createElementTest.ts @@ -16,6 +16,10 @@ describe('createElement', () => { runTest(KnownCreateElementDataIndex.EmptyLine, '

              '); }); + it('create by index with span', () => { + runTest(KnownCreateElementDataIndex.EmptyLineFormatInSpan, '

              '); + }); + it('create by tag', () => { runTest({ tag: 'div' }, '
              '); }); diff --git a/packages/roosterjs-editor-dom/test/utils/parseColorTest.ts b/packages/roosterjs-editor-dom/test/utils/parseColorTest.ts new file mode 100644 index 000000000000..aef1b3d1b615 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/utils/parseColorTest.ts @@ -0,0 +1,73 @@ +import parseColor from '../../lib/utils/parseColor'; + +describe('parseColor', () => { + it('empty string', () => { + const result = parseColor(''); + expect(result).toBe(null); + }); + + it('unrecognized color', () => { + const result = parseColor('aaa'); + expect(result).toBe(null); + }); + + it('short hex 1', () => { + const result = parseColor('#aaa'); + expect(result).toEqual([170, 170, 170]); + }); + + it('short hex 2', () => { + const result = parseColor('#aaab'); + expect(result).toEqual(null); + }); + + it('short hex 3', () => { + const result = parseColor(' #aaa '); + expect(result).toEqual([170, 170, 170]); + }); + + it('long hex 1', () => { + const result = parseColor('#ababab'); + expect(result).toEqual([171, 171, 171]); + }); + + it('long hex 2', () => { + const result = parseColor('#abababc'); + expect(result).toEqual(null); + }); + + it('long hex 3', () => { + const result = parseColor(' #ababab '); + expect(result).toEqual([171, 171, 171]); + }); + + it('rgb 1', () => { + const result = parseColor('rgb(1,2,3)'); + expect(result).toEqual([1, 2, 3]); + }); + + it('rgb 2', () => { + const result = parseColor(' rgb( 1 , 2 , 3 ) '); + expect(result).toEqual([1, 2, 3]); + }); + + it('rgb 3', () => { + const result = parseColor('rgb(1.1, 2.2, 3.3)'); + expect(result).toEqual([1, 2, 3]); + }); + + it('rgba 1', () => { + const result = parseColor('rgba(1, 2, 3, 4)'); + expect(result).toEqual([1, 2, 3]); + }); + + it('rgba 2', () => { + const result = parseColor(' rgba( 1.1 , 2.2 , 3.3 , 4.4 ) '); + expect(result).toEqual([1, 2, 3]); + }); + + it('rgba 3', () => { + const result = parseColor('rgba(1.1, 2.2, 3.3, 4.4)'); + expect(result).toEqual([1, 2, 3]); + }); +}); diff --git a/packages/roosterjs-editor-dom/test/utils/shouldSkipNodeTest.ts b/packages/roosterjs-editor-dom/test/utils/shouldSkipNodeTest.ts index 1f0f534f8f36..63d7169a661d 100644 --- a/packages/roosterjs-editor-dom/test/utils/shouldSkipNodeTest.ts +++ b/packages/roosterjs-editor-dom/test/utils/shouldSkipNodeTest.ts @@ -30,6 +30,17 @@ describe('shouldSkipNode, shouldSkipNode()', () => { expect(shouldSkip).toBe(true); }); + it('CRLF+text textNode', () => { + // Arrange + let node = document.createTextNode('\r\ntest'); + + // Act + let shouldSkip = shouldSkipNode(node); + + // Assert + expect(shouldSkip).toBe(false); + }); + it('DisplayNone', () => { // Arrange let node = DomTestHelper.createElementFromContent( diff --git a/packages/roosterjs-editor-dom/tsconfig.child.json b/packages/roosterjs-editor-dom/tsconfig.child.json deleted file mode 100644 index 2ad13169f900..000000000000 --- a/packages/roosterjs-editor-dom/tsconfig.child.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "strict": false - }, - "extends": "../tsconfig.json", - "references": [ - { "path": "../roosterjs-editor-types/tsconfig.child.json" }, - { "path": "./lib/blockElements/tsconfig.child.json" }, - { "path": "./lib/clipboard/tsconfig.child.json" }, - { "path": "./lib/contentTraverser/tsconfig.child.json" }, - { "path": "./lib/edit/tsconfig.child.json" }, - { "path": "./lib/entity/tsconfig.child.json" }, - { "path": "./lib/event/tsconfig.child.json" }, - { "path": "./lib/htmlSanitizer/tsconfig.child.json" }, - { "path": "./lib/inlineElements/tsconfig.child.json" }, - { "path": "./lib/list/tsconfig.child.json" }, - { "path": "./lib/region/tsconfig.child.json" }, - { "path": "./lib/selection/tsconfig.child.json" }, - { "path": "./lib/snapshots/tsconfig.child.json" }, - { "path": "./lib/style/tsconfig.child.json" }, - { "path": "./lib/table/tsconfig.child.json" }, - { "path": "./lib/utils/tsconfig.child.json" } - ], - "include": ["./lib/index.ts"] -} diff --git a/packages/roosterjs-editor-plugins/lib/AutoFormat.ts b/packages/roosterjs-editor-plugins/lib/AutoFormat.ts new file mode 100644 index 000000000000..7593cfa60054 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/AutoFormat.ts @@ -0,0 +1 @@ +export * from './plugins/AutoFormat/index'; diff --git a/packages/roosterjs-editor-plugins/lib/index.ts b/packages/roosterjs-editor-plugins/lib/index.ts index 3795af978016..5cd296430b23 100644 --- a/packages/roosterjs-editor-plugins/lib/index.ts +++ b/packages/roosterjs-editor-plugins/lib/index.ts @@ -10,3 +10,4 @@ export * from './Picker'; export * from './TableResize'; export * from './Watermark'; export * from './TableCellSelection'; +export * from './AutoFormat'; diff --git a/packages/roosterjs-editor-plugins/lib/pluginUtils/DragAndDropHandler.ts b/packages/roosterjs-editor-plugins/lib/pluginUtils/DragAndDropHandler.ts index 174c9dc7c73b..ff1eed6756e3 100644 --- a/packages/roosterjs-editor-plugins/lib/pluginUtils/DragAndDropHandler.ts +++ b/packages/roosterjs-editor-plugins/lib/pluginUtils/DragAndDropHandler.ts @@ -49,5 +49,9 @@ export default interface DragAndDropHandler { * Returns true will invoke the onSubmit callback, it means this is a meaningful dragging action, something (mostly * under context object) has been changed, and caller should handle this change. Otherwise, return false. */ - onDragEnd?: (context: TContext, event: MouseEvent, initValue: TInitValue) => boolean; + onDragEnd?: ( + context: TContext, + event: MouseEvent, + initValue: TInitValue | undefined + ) => boolean; } diff --git a/packages/roosterjs-editor-plugins/lib/pluginUtils/DragAndDropHelper.ts b/packages/roosterjs-editor-plugins/lib/pluginUtils/DragAndDropHelper.ts index cd12de630b09..eaf228822f16 100644 --- a/packages/roosterjs-editor-plugins/lib/pluginUtils/DragAndDropHelper.ts +++ b/packages/roosterjs-editor-plugins/lib/pluginUtils/DragAndDropHelper.ts @@ -1,15 +1,76 @@ import Disposable from './Disposable'; import DragAndDropHandler from './DragAndDropHandler'; -import { SizeTransformer } from 'roosterjs-editor-types'; +import { Browser } from 'roosterjs-editor-dom'; + +/** + * @internal + */ +interface MouseEventMoves { + MOUSEDOWN: string; + MOUSEMOVE: string; + MOUSEUP: string; +} + +/** + * @internal + */ +interface MouseEventInfo extends MouseEventMoves { + getPageXY: (e: MouseEvent) => number[]; +} + +/** + * @internal + * Compatible mouse event names for different platform + */ +interface TouchEventInfo extends MouseEventMoves { + getPageXY: (e: TouchEvent) => number[]; +} + +/** + * Generate event names and getXY function based on different platforms to be compatible with desktop and mobile browsers + */ +const MOUSE_EVENT_INFO_DESKTOP: MouseEventInfo = (() => { + return { + MOUSEDOWN: 'mousedown', + MOUSEMOVE: 'mousemove', + MOUSEUP: 'mouseup', + getPageXY: getMouseEventPageXY, + }; +})(); + +const MOUSE_EVENT_INFO_MOBILE: TouchEventInfo = (() => { + return { + MOUSEDOWN: 'touchstart', + MOUSEMOVE: 'touchmove', + MOUSEUP: 'touchend', + getPageXY: getTouchEventPageXY, + }; +})(); + +function getMouseEventPageXY(e: MouseEvent): [number, number] { + return [e.pageX, e.pageY]; +} + +function getTouchEventPageXY(e: TouchEvent): [number, number] { + let pageX = 0; + let pageY = 0; + if (e.targetTouches && e.targetTouches.length > 0) { + const touch = e.targetTouches[0]; + pageX = touch.pageX; + pageY = touch.pageY; + } + return [pageX, pageY]; +} /** * @internal * A helper class to help manage drag and drop to an HTML element */ export default class DragAndDropHelper implements Disposable { - private initX: number; - private initY: number; - private initValue: TInitValue; + private initX: number = 0; + private initY: number = 0; + private initValue: TInitValue | undefined = undefined; + private dndMouse: MouseEventInfo | TouchEventInfo; /** * Create a new instance of DragAndDropHelper class @@ -19,63 +80,73 @@ export default class DragAndDropHelper implements Disposab * so that the handler object knows which element it is triggered from. * @param onSubmit A callback that will be invoked when event handler in handler object returns true * @param handler The event handler object, see DragAndDropHandler interface for more information + * @param zoomScale The zoom scale of the editor + * @param forceMobile A boolean to force the use of touch controls for the helper */ constructor( private trigger: HTMLElement, private context: TContext, private onSubmit: (context: TContext, trigger: HTMLElement) => void, private handler: DragAndDropHandler, - private sizeTransformer: SizeTransformer + private zoomScale: number, + forceMobile?: boolean ) { - trigger.addEventListener('mousedown', this.onMouseDown); - this.sizeTransformer = sizeTransformer; + this.dndMouse = + forceMobile || Browser.isMobileOrTablet + ? MOUSE_EVENT_INFO_MOBILE + : MOUSE_EVENT_INFO_DESKTOP; + trigger.addEventListener(this.dndMouse.MOUSEDOWN, this.onMouseDown); } /** * Dispose this object, remove all event listeners that has been attached */ dispose() { - this.trigger.removeEventListener('mousedown', this.onMouseDown); + this.trigger.removeEventListener(this.dndMouse.MOUSEDOWN, this.onMouseDown); this.removeDocumentEvents(); } + public get mouseType(): string { + return this.dndMouse == MOUSE_EVENT_INFO_MOBILE ? 'touch' : 'mouse'; + } + private addDocumentEvents() { const doc = this.trigger.ownerDocument; - doc.addEventListener('mousemove', this.onMouseMove, true /*useCapture*/); - doc.addEventListener('mouseup', this.onMouseUp, true /*useCapture*/); + doc.addEventListener(this.dndMouse.MOUSEMOVE, this.onMouseMove, true /*useCapture*/); + doc.addEventListener(this.dndMouse.MOUSEUP, this.onMouseUp, true /*useCapture*/); } private removeDocumentEvents() { const doc = this.trigger.ownerDocument; - doc.removeEventListener('mousemove', this.onMouseMove, true /*useCapture*/); - doc.removeEventListener('mouseup', this.onMouseUp, true /*useCapture*/); + doc.removeEventListener(this.dndMouse.MOUSEMOVE, this.onMouseMove, true /*useCapture*/); + doc.removeEventListener(this.dndMouse.MOUSEUP, this.onMouseUp, true /*useCapture*/); } - private onMouseDown = (e: MouseEvent) => { + private onMouseDown = (e: Event) => { e.preventDefault(); e.stopPropagation(); this.addDocumentEvents(); - - this.initX = e.pageX; - this.initY = e.pageY; - this.initValue = this.handler.onDragStart?.(this.context, e); + [this.initX, this.initY] = this.dndMouse.getPageXY(e as MouseEvent & TouchEvent); + this.initValue = this.handler.onDragStart?.(this.context, e as MouseEvent); }; - private onMouseMove = (e: MouseEvent) => { + private onMouseMove = (e: Event) => { e.preventDefault(); - const sizeTransformer = this.sizeTransformer; - const deltaX = sizeTransformer(e.pageX - this.initX); - const deltaY = sizeTransformer(e.pageY - this.initY); - if (this.handler.onDragging?.(this.context, e, this.initValue, deltaX, deltaY)) { + const [pageX, pageY] = this.dndMouse.getPageXY(e as MouseEvent & TouchEvent); + const deltaX = (pageX - this.initX) / this.zoomScale; + const deltaY = (pageY - this.initY) / this.zoomScale; + if ( + this.initValue && + this.handler.onDragging?.(this.context, e as MouseEvent, this.initValue, deltaX, deltaY) + ) { this.onSubmit?.(this.context, this.trigger); } }; - private onMouseUp = (e: MouseEvent) => { + private onMouseUp = (e: Event) => { e.preventDefault(); this.removeDocumentEvents(); - - if (this.handler.onDragEnd?.(this.context, e, this.initValue)) { + if (this.handler.onDragEnd?.(this.context, e as MouseEvent, this.initValue)) { this.onSubmit?.(this.context, this.trigger); } }; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/AutoFormat/AutoFormat.ts b/packages/roosterjs-editor-plugins/lib/plugins/AutoFormat/AutoFormat.ts new file mode 100644 index 000000000000..88c3ccd3b326 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/AutoFormat/AutoFormat.ts @@ -0,0 +1,108 @@ +import { + ChangeSource, + EditorPlugin, + IEditor, + PluginEvent, + PluginEventType, + PositionType, +} from 'roosterjs-editor-types'; + +const specialCharacters = /[`!@#$%^&*()_+\=\[\]{};':"\\|,.<>\/?~]/; + +/** + * Automatically transform -- into hyphen, if typed between two words. + */ +export default class AutoFormat implements EditorPlugin { + private editor: IEditor | null = null; + private lastKeyTyped: string | null = null; + + /** + * Get a friendly name of this plugin + */ + getName() { + return 'AutoFormat'; + } + + /** + * Initialize this plugin + * @param editor The editor instance + */ + initialize(editor: IEditor) { + this.editor = editor; + } + + /** + * Dispose this plugin + */ + dispose() { + this.editor = null; + this.lastKeyTyped = null; + } + + /** + * Handle events triggered from editor + * @param event PluginEvent object + */ + onPluginEvent(event: PluginEvent) { + if (!this.editor) { + return; + } + if ( + event.eventType === PluginEventType.ContentChanged || + event.eventType === PluginEventType.MouseDown || + event.eventType === PluginEventType.MouseUp + ) { + this.lastKeyTyped = ''; + } + + if (event.eventType === PluginEventType.KeyPress) { + const keyTyped = event.rawEvent.key; + + if (keyTyped && keyTyped.length > 1) { + this.lastKeyTyped = ''; + } + + if ( + this.lastKeyTyped === '-' && + !specialCharacters.test(keyTyped) && + keyTyped !== ' ' && + keyTyped !== '-' + ) { + const searcher = this.editor.getContentSearcherOfCursor(event); + const textBeforeCursor = searcher?.getSubStringBefore(3); + const dashes = searcher?.getSubStringBefore(2); + const isPrecededByADash = textBeforeCursor?.[0] === '-'; + const isPrecededByASpace = textBeforeCursor?.[0] === ' '; + if ( + isPrecededByADash || + isPrecededByASpace || + (typeof textBeforeCursor === 'string' && + specialCharacters.test(textBeforeCursor[0])) || + dashes !== '--' + ) { + return; + } + + const textRange = searcher?.getRangeFromText(dashes, true /* exactMatch */); + const nodeHyphen = document.createTextNode('—'); + this.editor.addUndoSnapshot( + () => { + if (textRange) { + textRange.deleteContents(); + textRange.insertNode(nodeHyphen); + this.editor!.select(nodeHyphen, PositionType.End); + } + }, + ChangeSource.Format /*changeSource*/, + true /*canUndoByBackspace*/, + { formatApiName: 'autoHyphen' } + ); + + //After the substitution the last key typed needs to be cleaned + this.lastKeyTyped = null; + } else { + this.lastKeyTyped = keyTyped; + } + } + } +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/AutoFormat/index.ts b/packages/roosterjs-editor-plugins/lib/plugins/AutoFormat/index.ts new file mode 100644 index 000000000000..572df30aee2d --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/AutoFormat/index.ts @@ -0,0 +1 @@ +export { default as AutoFormat } from './AutoFormat'; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/ContentEdit.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/ContentEdit.ts index 6e4cd4d54abe..c76a5297644d 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/ContentEdit.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/ContentEdit.ts @@ -1,4 +1,5 @@ import getAllFeatures from './getAllFeatures'; +import { getObjectKeys } from 'roosterjs-editor-dom'; import { ContentEditFeatureSettings, EditorPlugin, @@ -20,6 +21,8 @@ import { * 8. Manage list style */ export default class ContentEdit implements EditorPlugin { + private editor: IEditor | undefined = undefined; + private features: GenericContentEditFeature[] = []; /** * Create instance of ContentEdit plugin * @param settingsOverride An optional feature set to override default feature settings @@ -42,29 +45,36 @@ export default class ContentEdit implements EditorPlugin { * @param editor The editor instance */ initialize(editor: IEditor): void { - const features: GenericContentEditFeature[] = []; + this.editor = editor; const allFeatures = getAllFeatures(); - - Object.keys(allFeatures).forEach((key: keyof typeof allFeatures) => { + getObjectKeys(allFeatures).forEach(key => { const feature = allFeatures[key]; const hasSettingForKey = this.settingsOverride && this.settingsOverride[key] !== undefined; if ( - (hasSettingForKey && this.settingsOverride[key]) || + (hasSettingForKey && this.settingsOverride?.[key]) || (!hasSettingForKey && !feature.defaultDisabled) ) { - features.push(feature); + this.features.push(feature); } }); + this.features = this.features.concat(this.additionalFeatures || []); + this.features.forEach(feature => this.editor?.addContentEditFeature(feature)); + } - features - .concat(this.additionalFeatures || []) - .forEach(feature => editor.addContentEditFeature(feature)); + private disposeFeatures() { + if (this.editor) { + this.features.forEach(feature => this.editor!.removeContentEditFeature(feature)); + } + this.features = []; } /** * Dispose this plugin */ - dispose(): void {} + dispose(): void { + this.disposeFeatures(); + this.editor = undefined; + } } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/autoLinkFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/autoLinkFeatures.ts index 52d30a831440..c70a0564c729 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/autoLinkFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/autoLinkFeatures.ts @@ -49,7 +49,7 @@ const UnlinkWhenBackspaceAfterLink: BuildInEditFeature = { defaultDisabled: true, }; -function cacheGetLinkData(event: PluginEvent, editor: IEditor): LinkData { +function cacheGetLinkData(event: PluginEvent, editor: IEditor): LinkData | null { return event.eventType == PluginEventType.KeyDown || (event.eventType == PluginEventType.ContentChanged && event.source == ChangeSource.Paste) ? cacheGetEventData(event, 'LINK_DATA', () => { @@ -58,15 +58,16 @@ function cacheGetLinkData(event: PluginEvent, editor: IEditor): LinkData { // from clipboard will only contain what we pasted, any existing characters will not // be included. let clipboardData = - event.eventType == PluginEventType.ContentChanged && - event.source == ChangeSource.Paste && - (event.data as ClipboardData); - let link = matchLink((clipboardData.text || '').trim()); + (event.eventType == PluginEventType.ContentChanged && + event.source == ChangeSource.Paste && + (event.data as ClipboardData)) || + null; + let link = matchLink((clipboardData?.text || '').trim()); let searcher = editor.getContentSearcherOfCursor(event); // In case the matched link is already inside a
              tag, we do a range search. // getRangeFromText will return null if the given text is already in a LinkInlineElement - if (link && searcher.getRangeFromText(link.originalUrl, false /*exactMatch*/)) { + if (link && searcher?.getRangeFromText(link.originalUrl, false /*exactMatch*/)) { return link; } @@ -97,13 +98,16 @@ function cacheGetLinkData(event: PluginEvent, editor: IEditor): LinkData { function hasLinkBeforeCursor(event: PluginKeyboardEvent, editor: IEditor): boolean { let contentSearcher = editor.getContentSearcherOfCursor(event); - let inline = contentSearcher.getInlineElementBefore(); + let inline = contentSearcher?.getInlineElementBefore(); return inline instanceof LinkInlineElement; } function autoLink(event: PluginEvent, editor: IEditor) { + const linkData = cacheGetLinkData(event, editor); + if (!linkData) { + return; + } let anchor = editor.getDocument().createElement('a'); - let linkData = cacheGetLinkData(event, editor); // Need to get searcher before we enter the async callback since the callback can happen when cursor is moved to next line // and at that time a new searcher won't be able to find the link text to replace let searcher = editor.getContentSearcherOfCursor(); @@ -118,7 +122,7 @@ function autoLink(event: PluginEvent, editor: IEditor) { linkData.originalUrl, anchor, false /* exactMatch */, - searcher + searcher ?? undefined ); // The content at cursor has changed. Should also clear the cursor data cache @@ -139,5 +143,5 @@ export const AutoLinkFeatures: Record< BuildInEditFeature > = { autoLink: AutoLink, - unlinkWhenBackspaceAfterLink: UnlinkWhenBackspaceAfterLink, + unlinkWhenBackspaceAfterLink: UnlinkWhenBackspaceAfterLink as BuildInEditFeature, }; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/codeFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/codeFeatures.ts new file mode 100644 index 000000000000..0a35641a2aab --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/codeFeatures.ts @@ -0,0 +1,99 @@ +import { + isNodeEmpty, + cacheGetEventData, + safeInstanceOf, + splitBalancedNodeRange, + unwrap, +} from 'roosterjs-editor-dom'; +import { + BuildInEditFeature, + PluginKeyboardEvent, + Keys, + IEditor, + PositionType, + CodeFeatureSettings, + QueryScope, +} from 'roosterjs-editor-types'; + +const RemoveCodeWhenEnterOnEmptyLine: BuildInEditFeature = { + keys: [Keys.ENTER], + shouldHandleEvent: (event, editor) => { + const childOfCode = cacheGetCodeChild(event, editor); + return childOfCode && isNodeEmpty(childOfCode); + }, + handleEvent: (event, editor) => { + event.rawEvent.preventDefault(); + editor.addUndoSnapshot( + () => { + splitCode(event, editor); + }, + undefined /* changeSource */, + true /* canUndoByBackspace */ + ); + }, +}; + +const RemoveCodeWhenBackspaceOnEmptyFirstLine: BuildInEditFeature = { + keys: [Keys.BACKSPACE], + shouldHandleEvent: (event, editor) => { + const childOfCode = cacheGetCodeChild(event, editor); + return childOfCode && isNodeEmpty(childOfCode) && !childOfCode.previousSibling; + }, + handleEvent: (event, editor) => { + event.rawEvent.preventDefault(); + editor.addUndoSnapshot(() => splitCode(event, editor)); + }, +}; + +function cacheGetCodeChild(event: PluginKeyboardEvent, editor: IEditor): Node | null { + return cacheGetEventData(event, 'CODE_CHILD', () => { + const codeElement = + editor.getElementAtCursor('code') ?? + editor.queryElements('code', QueryScope.OnSelection)[0]; + if (codeElement) { + const pos = editor.getFocusedPosition(); + const block = pos && editor.getBlockElementAtNode(pos.normalize().node); + if (block) { + const node = + block.getStartNode() == codeElement.parentNode + ? block.getStartNode() + : block.collapseToSingleElement(); + return isNodeEmpty(node) ? node : null; + } + } + + return null; + }); +} + +function splitCode(event: PluginKeyboardEvent, editor: IEditor) { + const currentContainer = cacheGetCodeChild(event, editor); + if (!safeInstanceOf(currentContainer, 'HTMLElement')) { + return; + } + const codeChild = currentContainer.querySelector('code'); + if (!codeChild) { + const codeParent = splitBalancedNodeRange(currentContainer); + if (codeParent) { + unwrap(codeParent); + } + if (safeInstanceOf(currentContainer.parentElement, 'HTMLPreElement')) { + const preParent = splitBalancedNodeRange(currentContainer); + if (preParent) { + unwrap(preParent); + } + } + } else { + //Content model + unwrap(codeChild); + } + editor.select(currentContainer, PositionType.Begin); +} + +export const CodeFeatures: Record< + keyof CodeFeatureSettings, + BuildInEditFeature +> = { + removeCodeWhenEnterOnEmptyLine: RemoveCodeWhenEnterOnEmptyLine, + removeCodeWhenBackspaceOnEmptyFirstLine: RemoveCodeWhenBackspaceOnEmptyFirstLine, +}; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/cursorFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/cursorFeatures.ts index 4a1b4df9bdca..9b34ee661dc6 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/cursorFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/cursorFeatures.ts @@ -10,7 +10,7 @@ const NoCycleCursorMove: BuildInEditFeature = { keys: [Keys.LEFT, Keys.RIGHT], allowFunctionKeys: true, shouldHandleEvent: (event, editor, ctrlOrMeta) => { - let range: Range; + let range: Range | null = null; let position: Position; if ( diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/entityFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/entityFeatures.ts index d1ea65310550..1406d9ff8ccd 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/entityFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/entityFeatures.ts @@ -1,7 +1,14 @@ +import { ContentTraverser } from 'roosterjs-editor-dom'; import { + addDelimiters, cacheGetEventData, + createRange, + getComputedStyle, + getDelimiterFromElement, getEntityFromElement, getEntitySelector, + isBlockElement, + matchesSelector, Position, } from 'roosterjs-editor-dom'; import { @@ -13,6 +20,13 @@ import { PluginKeyboardEvent, PositionType, PluginEventType, + DelimiterClasses, + PluginEvent, + NodeType, + ExperimentalFeatures, + Entity, + IContentTraverser, + InlineElement, } from 'roosterjs-editor-types'; /** @@ -51,11 +65,14 @@ function cacheGetReadonlyEntityElement( }); if (element && operation !== undefined) { - editor.triggerPluginEvent(PluginEventType.EntityOperation, { - operation, - rawEvent: event.rawEvent, - entity: getEntityFromElement(element), - }); + const entity = getEntityFromElement(element); + if (entity) { + editor.triggerPluginEvent(PluginEventType.EntityOperation, { + operation, + rawEvent: event.rawEvent, + entity, + }); + } } return element; @@ -75,21 +92,25 @@ const EnterBeforeReadonlyEntityFeature: BuildInEditFeature event.rawEvent.preventDefault(); const range = editor.getSelectionRange(); + if (!range) { + return; + } + const node = Position.getEnd(range).normalize().node; const br = editor.getDocument().createElement('BR'); - node.parentNode.insertBefore(br, node.nextSibling); + node.parentNode?.insertBefore(br, node.nextSibling); const block = editor.getBlockElementAtNode(node); - let newContainer: HTMLElement; + let newContainer: HTMLElement | undefined; if (block) { newContainer = block.collapseToSingleElement(); br.parentNode?.removeChild(br); } - editor.getSelectionRange().deleteContents(); + editor.getSelectionRange()?.deleteContents(); - if (newContainer.nextSibling) { + if (newContainer?.nextSibling) { editor.select(newContainer.nextSibling, PositionType.Begin); } }, @@ -139,21 +160,21 @@ function cacheGetNeighborEntityElement( isNext: boolean, collapseOnly: boolean, operation?: EntityOperation -): HTMLElement { +): HTMLElement | null { const element = cacheGetEventData( event, 'NEIGHBOR_ENTITY_ELEMENT_' + isNext + '_' + collapseOnly, () => { const range = editor.getSelectionRange(); - if (collapseOnly && !range.collapsed) { + if (!range || (collapseOnly && !range.collapsed)) { return null; } range.commonAncestorContainer.normalize(); const pos = Position.getEnd(range).normalize(); const isAtBeginOrEnd = pos.offset == 0 || pos.isAtEnd; - let entityNode: HTMLElement = null; + let entityNode: HTMLElement | null = null; if (isAtBeginOrEnd) { const traverser = editor.getBodyTraverser(pos.node); @@ -168,7 +189,7 @@ function cacheGetNeighborEntityElement( if (!collapseOnly) { const block = editor.getBlockElementAtNode(pos.node); - if (!block || !block.contains(node)) { + if (!block || (node && !block.contains(node))) { node = null; } } @@ -181,16 +202,310 @@ function cacheGetNeighborEntityElement( ); if (element && operation !== undefined) { - editor.triggerPluginEvent(PluginEventType.EntityOperation, { - operation, - rawEvent: event.rawEvent, - entity: getEntityFromElement(element), - }); + const entity = getEntityFromElement(element); + if (entity) { + triggerOperation(entity, editor, operation, event); + } } return element; } +/** + * @requires ExperimentalFeatures.InlineEntityReadOnlyDelimiters to be enabled + * Content edit feature to move the cursor from Delimiters around Entities when using Right or Left Arrow Keys + */ +const MoveBetweenDelimitersFeature: BuildInEditFeature = { + keys: [Keys.RIGHT, Keys.LEFT], + allowFunctionKeys: true, + shouldHandleEvent: (event: PluginKeyboardEvent, editor: IEditor) => { + if ( + event.rawEvent.altKey || + !editor.isFeatureEnabled(ExperimentalFeatures.InlineEntityReadOnlyDelimiters) + ) { + return false; + } + + const element = editor.getElementAtCursor(); + if (!element) { + return false; + } + + const isRTL = getComputedStyle(element, 'direction') === 'rtl'; + const shouldCheckBefore = isRTL == (event.rawEvent.which === Keys.LEFT); + + return getIsDelimiterAtCursor(event, editor, shouldCheckBefore); + }, + handleEvent(event: PluginKeyboardEvent, editor: IEditor) { + const checkBefore = cacheGetCheckBefore(event); + const delimiter = cacheDelimiter(event, checkBefore); + + if (!delimiter) { + return; + } + + const { delimiterPair, entity } = getRelatedElements(delimiter, checkBefore, editor); + + if (delimiterPair && entity && matchesSelector(entity, getEntitySelector())) { + event.rawEvent.preventDefault(); + editor.runAsync(() => { + const positionType = checkBefore + ? event.rawEvent.shiftKey + ? PositionType.After + : PositionType.End + : PositionType.Before; + const position = new Position(delimiterPair, positionType); + if (event.rawEvent.shiftKey) { + const selection = delimiterPair.ownerDocument.getSelection(); + selection?.extend(position.node, position.offset); + } else { + editor.select(position); + } + }); + } + }, +}; + +/** + * @requires ExperimentalFeatures.InlineEntityReadOnlyDelimiters to be enabled + * Content edit Feature to trigger a Delete Entity Operation when one of the Delimiter is about to be removed with DELETE or Backspace + */ +const RemoveEntityBetweenDelimitersFeature: BuildInEditFeature = { + keys: [Keys.BACKSPACE, Keys.DELETE], + shouldHandleEvent(event: PluginKeyboardEvent, editor: IEditor) { + if (!editor.isFeatureEnabled(ExperimentalFeatures.InlineEntityReadOnlyDelimiters)) { + return false; + } + + const range = editor.getSelectionRange(); + if (!range?.collapsed) { + return false; + } + const checkBefore = event.rawEvent.which === Keys.DELETE; + const isDelimiter = getIsDelimiterAtCursor(event, editor, checkBefore); + + if (isDelimiter) { + const delimiter = cacheDelimiter(event, checkBefore); + const entityElement = checkBefore + ? delimiter?.nextElementSibling + : delimiter?.previousElementSibling; + + return !!cacheEntityBetweenDelimiter(event, editor, checkBefore, entityElement); + } + + return false; + }, + handleEvent(event: PluginKeyboardEvent, editor: IEditor) { + const checkBefore = event.rawEvent.which === Keys.DELETE; + cacheEntityBetweenDelimiter( + event, + editor, + checkBefore, + null, + checkBefore ? EntityOperation.RemoveFromStart : EntityOperation.RemoveFromEnd + ); + }, +}; + +function getIsDelimiterAtCursor(event: PluginKeyboardEvent, editor: IEditor, checkBefore: boolean) { + const position = editor.getFocusedPosition()?.normalize(); + cacheGetCheckBefore(event, checkBefore); + + if (!position) { + return false; + } + + const focusedElement = + position.node.nodeType == NodeType.Text + ? position.node + : position.node == position.element + ? position.element.childNodes.item(position.offset) + : position.element; + + const data = checkBefore + ? { + class: DelimiterClasses.DELIMITER_BEFORE, + pairClass: DelimiterClasses.DELIMITER_AFTER, + isAtEndOrBeginning: position.isAtEnd, + } + : { + class: DelimiterClasses.DELIMITER_AFTER, + pairClass: DelimiterClasses.DELIMITER_BEFORE, + isAtEndOrBeginning: position.offset == 0, + }; + + const sibling = getNextSibling(editor, focusedElement, checkBefore); + if (data.isAtEndOrBeginning && sibling) { + const elAtCursor = editor.getElementAtCursor('.' + data.class, sibling); + + if (elAtCursor && !!shouldHandle(elAtCursor)) { + return true; + } + } + + const entityAtCursor = + focusedElement && editor.getElementAtCursor('.' + data.class, focusedElement); + return !!shouldHandle(entityAtCursor); + + function shouldHandle(element: HTMLElement | null | undefined) { + if (!element) { + return false; + } + + const { delimiterPair } = getRelatedElements(element, checkBefore, editor); + + return ( + delimiterPair && + (delimiterPair.className || '').indexOf(data.pairClass) > -1 && + cacheDelimiter(event, checkBefore, element) + ); + } +} + +function getNextSibling(editor: IEditor, element: Node, checkBefore: boolean) { + const traverser = getBlockTraverser(editor, element); + if (!traverser) { + return undefined; + } + + const traverseFn = (t: IContentTraverser) => + checkBefore ? t.getNextInlineElement() : t.getPreviousInlineElement(); + + let currentInline = traverser.currentInlineElement; + while (currentInline && currentInline.getContainerNode() === element) { + currentInline = traverseFn(traverser); + } + return currentInline?.getContainerNode(); +} + +function getBlockTraverser(editor: IEditor, element: Node | null | undefined) { + if (!element) { + return undefined; + } + const blockElement = editor.getBlockElementAtNode(element)?.getStartNode(); + if (!blockElement || !isBlockElement(blockElement)) { + return undefined; + } + return ContentTraverser.createBodyTraverser(blockElement, element); +} + +function cacheDelimiter(event: PluginEvent, checkBefore: boolean, delimiter?: HTMLElement | null) { + return cacheGetEventData(event, 'delimiter_cache_key_' + checkBefore, () => delimiter); +} + +function cacheEntityBetweenDelimiter( + event: PluginKeyboardEvent, + editor: IEditor, + checkBefore: boolean, + entity?: Element | null, + operation?: EntityOperation +) { + const element = cacheGetEventData( + event, + 'entity_delimiter_cache_key_' + checkBefore, + () => entity && editor.getElementAtCursor(getEntitySelector(), entity) + ); + + if (element && operation !== undefined) { + const entity = getEntityFromElement(element); + + if (entity) { + triggerOperation(entity, editor, operation, event); + } + } + + return element; +} + +function triggerOperation( + entity: Entity, + editor: IEditor, + operation: EntityOperation, + event: PluginKeyboardEvent +) { + const { nextElementSibling, previousElementSibling } = entity.wrapper; + editor.triggerPluginEvent(PluginEventType.EntityOperation, { + operation, + rawEvent: event.rawEvent, + entity, + }); + + if ( + entity.isReadonly && + !isBlockElement(entity.wrapper) && + editor.isFeatureEnabled(ExperimentalFeatures.InlineEntityReadOnlyDelimiters) + ) { + if (event.rawEvent.defaultPrevented) { + editor.runAsync(() => { + if (!editor.contains(entity.wrapper)) { + removeDelimiters(nextElementSibling, previousElementSibling); + } else { + const [delimiterAfter] = addDelimiters(entity.wrapper); + if (delimiterAfter) { + editor.select(delimiterAfter, PositionType.After); + } + } + }); + } else if ( + getDelimiterFromElement(nextElementSibling) && + getDelimiterFromElement(previousElementSibling) + ) { + editor.select(createRange(previousElementSibling, nextElementSibling)); + } + } +} + +function removeDelimiters( + nextElementSibling: Element | null, + previousElementSibling: Element | null +) { + [nextElementSibling, previousElementSibling].forEach(sibling => { + if (getDelimiterFromElement(sibling)) { + sibling?.parentElement?.removeChild(sibling); + } + }); +} + +function cacheGetCheckBefore(event: PluginKeyboardEvent, checkBefore?: boolean): boolean { + return !!cacheGetEventData(event, 'Check_Before', () => checkBefore); +} + +function getRelatedElements(delimiter: HTMLElement, checkBefore: boolean, editor: IEditor) { + let entity: Element | null = null; + let delimiterPair: Element | null = null; + const traverser = getBlockTraverser(editor, delimiter); + if (!traverser) { + return { delimiterPair, entity }; + } + + const selector = `.${ + checkBefore ? DelimiterClasses.DELIMITER_AFTER : DelimiterClasses.DELIMITER_BEFORE + }`; + const traverseFn = (t: IContentTraverser) => + checkBefore ? t.getNextInlineElement() : t.getPreviousInlineElement(); + const getElementFromInline = (element: InlineElement, selector: string) => { + const node = element?.getContainerNode(); + return (node && editor.getElementAtCursor(selector, node)) ?? null; + }; + const entitySelector = getEntitySelector(); + + let current = traverser.currentInlineElement; + while (current && (!entity || !delimiterPair)) { + entity = entity || getElementFromInline(current, entitySelector); + delimiterPair = delimiterPair || getElementFromInline(current, selector); + + // If we found the entity but the next inline after the entity is not a delimiter, + // it means that the delimiter pair got removed or is invalid, return null instead. + if (entity && !delimiterPair && !getElementFromInline(current, entitySelector)) { + delimiterPair = null; + break; + } + current = traverseFn(traverser); + } + + return { entity, delimiterPair }; +} + /** * @internal */ @@ -203,4 +518,6 @@ export const EntityFeatures: Record< enterBeforeReadonlyEntity: EnterBeforeReadonlyEntityFeature, backspaceAfterEntity: BackspaceAfterEntityFeature, deleteBeforeEntity: DeleteBeforeEntityFeature, + moveBetweenDelimitersFeature: MoveBetweenDelimitersFeature, + removeEntityBetweenDelimiters: RemoveEntityBetweenDelimitersFeature, }; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts index c612369b0edc..642b49a136b9 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts @@ -1,6 +1,8 @@ +import getAutoBulletListStyle from '../utils/getAutoBulletListStyle'; +import getAutoNumberingListStyle from '../utils/getAutoNumberingListStyle'; import { blockFormat, - experimentCommitListChains, + commitListChains, setIndentation, toggleBullet, toggleNumbering, @@ -16,6 +18,13 @@ import { createVListFromRegion, isBlockElement, cacheGetEventData, + safeInstanceOf, + VList, + createObjectDefinition, + createNumberDefinition, + getMetadata, + findClosestElementAncestor, + getComputedStyle, } from 'roosterjs-editor-dom'; import { BuildInEditFeature, @@ -27,19 +36,75 @@ import { QueryScope, RegionBase, ListType, + ExperimentalFeatures, + PositionType, + NumberingListType, + BulletListType, + IPositionContentSearcher, } from 'roosterjs-editor-types'; +const PREVIOUS_BLOCK_CACHE_KEY = 'previousBlock'; +const NEXT_BLOCK_CACHE_KEY = 'nextBlock'; + +interface ListStyleMetadata { + orderedStyleType?: NumberingListType; + unorderedStyleType?: BulletListType; +} + +const ListStyleDefinitionMetadata = createObjectDefinition( + { + orderedStyleType: createNumberDefinition( + true /** isOptional */, + undefined /** value **/, + NumberingListType.Min, + NumberingListType.Max + ), + unorderedStyleType: createNumberDefinition( + true /** isOptional */, + undefined /** value **/, + BulletListType.Min, + BulletListType.Max + ), + }, + true /** isOptional */, + true /** allowNull */ +); + +const shouldHandleIndentationEvent = (indenting: boolean) => ( + event: PluginKeyboardEvent, + editor: IEditor +) => { + const { keyCode, altKey, shiftKey, ctrlKey, metaKey } = event.rawEvent; + return ( + !ctrlKey && + !metaKey && + (keyCode === Keys.TAB + ? !altKey && shiftKey === !indenting + : shiftKey && altKey && keyCode === (indenting ? Keys.RIGHT : Keys.LEFT)) && + cacheGetListElement(event, editor) + ); +}; + +const handleIndentationEvent = (indenting: boolean) => ( + event: PluginKeyboardEvent, + editor: IEditor +) => { + let currentElement: Node | null = null; + const isRTL = + event.rawEvent.keyCode !== Keys.TAB && + (currentElement = editor.getElementAtCursor()) && + getComputedStyle(currentElement, 'direction') == 'rtl'; + setIndentation(editor, isRTL == indenting ? Indentation.Decrease : Indentation.Increase); + event.rawEvent.preventDefault(); +}; + /** * IndentWhenTab edit feature, provides the ability to indent current list when user press TAB */ const IndentWhenTab: BuildInEditFeature = { keys: [Keys.TAB], - shouldHandleEvent: (event, editor) => - !event.rawEvent.shiftKey && cacheGetListElement(event, editor), - handleEvent: (event, editor) => { - setIndentation(editor, Indentation.Increase); - event.rawEvent.preventDefault(); - }, + shouldHandleEvent: shouldHandleIndentationEvent(true), + handleEvent: handleIndentationEvent(true), }; /** @@ -47,12 +112,31 @@ const IndentWhenTab: BuildInEditFeature = { */ const OutdentWhenShiftTab: BuildInEditFeature = { keys: [Keys.TAB], - shouldHandleEvent: (event, editor) => - event.rawEvent.shiftKey && cacheGetListElement(event, editor), - handleEvent: (event, editor) => { - setIndentation(editor, Indentation.Decrease); - event.rawEvent.preventDefault(); - }, + shouldHandleEvent: shouldHandleIndentationEvent(false), + handleEvent: handleIndentationEvent(false), + allowFunctionKeys: true, +}; + +/** + * indentWhenAltShiftRight edit feature, provides the ability to indent or outdent current list when user press Alt+shift+Right + */ +const IndentWhenAltShiftRight: BuildInEditFeature = { + keys: [Keys.RIGHT], + shouldHandleEvent: shouldHandleIndentationEvent(true), + handleEvent: handleIndentationEvent(true), + allowFunctionKeys: true, + defaultDisabled: Browser.isMac, +}; + +/** + * outdentWhenAltShiftLeft edit feature, provides the ability to indent or outdent current list when user press Alt+shift+Left + */ +const OutdentWhenAltShiftLeft: BuildInEditFeature = { + keys: [Keys.LEFT], + shouldHandleEvent: shouldHandleIndentationEvent(false), + handleEvent: handleIndentationEvent(false), + allowFunctionKeys: true, + defaultDisabled: Browser.isMac, }; /** @@ -62,18 +146,26 @@ const OutdentWhenShiftTab: BuildInEditFeature = { const MergeInNewLine: BuildInEditFeature = { keys: [Keys.BACKSPACE], shouldHandleEvent: (event, editor) => { - let li = editor.getElementAtCursor('LI', null /*startFrom*/, event); + let li = editor.getElementAtCursor('LI', undefined /*startFrom*/, event); let range = editor.getSelectionRange(); return li && range?.collapsed && isPositionAtBeginningOf(Position.getStart(range), li); }, handleEvent: (event, editor) => { - let li = editor.getElementAtCursor('LI', null /*startFrom*/, event); - if (li.previousSibling) { + let li = editor.getElementAtCursor('LI', undefined /*startFrom*/, event); + if (li?.previousSibling) { blockFormat(editor, (region, start, end) => { - const vList = createVListFromRegion(region, false /*includeSiblingList*/, li); - vList.setIndentation(start, end, Indentation.Decrease, true /*softOutdent*/); - vList.writeBack(); - event.rawEvent.preventDefault(); + const vList = createVListFromRegion( + region, + false /*includeSiblingList*/, + li ?? undefined + ); + if (vList && start && end) { + vList.setIndentation(start, end, Indentation.Decrease, true /*softOutdent*/); + vList.writeBack( + editor.isFeatureEnabled(ExperimentalFeatures.ReuseAllAncestorListElements) + ); + event.rawEvent.preventDefault(); + } }); } else { toggleListAndPreventDefault(event, editor); @@ -89,8 +181,13 @@ const MergeInNewLine: BuildInEditFeature = { const OutdentWhenBackOn1stEmptyLine: BuildInEditFeature = { keys: [Keys.BACKSPACE], shouldHandleEvent: (event, editor) => { - let li = editor.getElementAtCursor('LI', null /*startFrom*/, event); - return li && isNodeEmpty(li) && !li.previousSibling; + let li = editor.getElementAtCursor('LI', undefined /*startFrom*/, event); + return ( + li && + isNodeEmpty(li) && + !li.previousSibling && + !li.getElementsByTagName('blockquote').length + ); }, handleEvent: toggleListAndPreventDefault, }; @@ -102,18 +199,19 @@ const OutdentWhenBackOn1stEmptyLine: BuildInEditFeature = { const MaintainListChainWhenDelete: BuildInEditFeature = { keys: [Keys.DELETE], shouldHandleEvent: (event, editor) => { - const li = editor.getElementAtCursor('LI', null /*startFrom*/, event); - if (li) { + const li = editor.getElementAtCursor('LI', undefined /*startFrom*/, event); + const range = editor.getSelectionRange(); + if (li || !range) { return false; } - const isAtEnd = Position.getEnd(editor.getSelectionRange()).isAtEnd; - const nextSibling = isAtEnd ? getCacheNextSibling(event, editor) : null; + const isAtEnd = Position.getEnd(range).isAtEnd; + const nextSibling = isAtEnd ? getCacheNextSibling(event, editor) : undefined; const isAtEndAndBeforeLI = editor.getElementAtCursor('LI', nextSibling, event); return isAtEndAndBeforeLI; }, handleEvent: (event, editor) => { const chains = getListChains(editor); - editor.runAsync(editor => experimentCommitListChains(editor, chains)); + editor.runAsync(editor => commitListChains(editor, chains)); }, }; @@ -124,19 +222,30 @@ const MaintainListChainWhenDelete: BuildInEditFeature = { const OutdentWhenEnterOnEmptyLine: BuildInEditFeature = { keys: [Keys.ENTER], shouldHandleEvent: (event, editor) => { - let li = editor.getElementAtCursor('LI', null /*startFrom*/, event); + let li = editor.getElementAtCursor('LI', undefined /*startFrom*/, event); return !event.rawEvent.shiftKey && li && isNodeEmpty(li); }, handleEvent: (event, editor) => { editor.addUndoSnapshot( () => toggleListAndPreventDefault(event, editor, false /* includeSiblingLists */), - null /*changeSource*/, + undefined /*changeSource*/, true /*canUndoByBackspace*/ ); }, defaultDisabled: !Browser.isIE && !Browser.isChrome, }; +/** + * Validate if a block of text is considered a list pattern + * The regex expression will look for patterns of the form: + * 1. 1> 1) 1- (1) + * @returns if a text is considered a list pattern + */ +function isAListPattern(textBeforeCursor: string) { + const REGEX: RegExp = /^(\*|-|[0-9]{1,2}\.|[0-9]{1,2}\>|[0-9]{1,2}\)|[0-9]{1,2}\-|\([0-9]{1,2}\))$/; + return REGEX.test(textBeforeCursor); +} + /** * AutoBullet edit feature, provides the ability to automatically convert current line into a list. * When user input "1. ", convert into a numbering list @@ -145,8 +254,12 @@ const OutdentWhenEnterOnEmptyLine: BuildInEditFeature = { const AutoBullet: BuildInEditFeature = { keys: [Keys.SPACE], shouldHandleEvent: (event, editor) => { - if (!cacheGetListElement(event, editor)) { - let searcher = editor.getContentSearcherOfCursor(event); + let searcher: IPositionContentSearcher | null; + if ( + !cacheGetListElement(event, editor) && + !editor.isFeatureEnabled(ExperimentalFeatures.AutoFormatList) && + (searcher = editor.getContentSearcherOfCursor(event)) + ) { let textBeforeCursor = searcher.getSubStringBefore(4); // Auto list is triggered if: @@ -163,6 +276,9 @@ const AutoBullet: BuildInEditFeature = { () => { let regions: RegionBase[]; let searcher = editor.getContentSearcherOfCursor(); + if (!searcher) { + return; + } let textBeforeCursor = searcher.getSubStringBefore(4); let textRange = searcher.getRangeFromText(textBeforeCursor, true /*exactMatch*/); @@ -184,12 +300,138 @@ const AutoBullet: BuildInEditFeature = { } searcher.getRangeFromText(textBeforeCursor, true /*exactMatch*/)?.deleteContents(); }, - null /*changeSource*/, + undefined /*changeSource*/, + true /*canUndoByBackspace*/ + ); + }, +}; + +/** + * Requires @see ExperimentalFeatures.AutoFormatList to be enabled + * AutoBulletList edit feature, provides the ability to automatically convert current line into a bullet list. + */ +const AutoBulletList: BuildInEditFeature = { + keys: [Keys.SPACE], + shouldHandleEvent: (event, editor) => { + if ( + !cacheGetListElement(event, editor) && + editor.isFeatureEnabled(ExperimentalFeatures.AutoFormatList) + ) { + return shouldTriggerList(event, editor, getAutoBulletListStyle, ListType.Unordered); + } + return false; + }, + handleEvent: (event, editor) => { + editor.insertContent(' '); + event.rawEvent.preventDefault(); + editor.addUndoSnapshot( + () => { + let searcher = editor.getContentSearcherOfCursor(); + if (!searcher) { + return; + } + let textBeforeCursor = searcher.getSubStringBefore(5); + let textRange = searcher.getRangeFromText(textBeforeCursor, true /*exactMatch*/); + const listStyle = getAutoBulletListStyle(textBeforeCursor); + + if (textRange) { + prepareAutoBullet(editor, textRange); + toggleBullet( + editor, + listStyle ?? undefined, + 'autoToggleList' /** apiNameOverride */ + ); + } + searcher.getRangeFromText(textBeforeCursor, true /*exactMatch*/)?.deleteContents(); + }, + undefined /*changeSource*/, true /*canUndoByBackspace*/ ); }, }; +/** + * Requires @see ExperimentalFeatures.AutoFormatList to be enabled + * AutoNumberingList edit feature, provides the ability to automatically convert current line into a numbering list. + */ +const AutoNumberingList: BuildInEditFeature = { + keys: [Keys.SPACE], + shouldHandleEvent: (event, editor) => { + if ( + !cacheGetListElement(event, editor) && + editor.isFeatureEnabled(ExperimentalFeatures.AutoFormatList) + ) { + return shouldTriggerList(event, editor, getAutoNumberingListStyle, ListType.Ordered); + } + return false; + }, + handleEvent: (event, editor) => { + editor.insertContent(' '); + event.rawEvent.preventDefault(); + editor.addUndoSnapshot( + () => { + const searcher = editor.getContentSearcherOfCursor(); + if (!searcher) { + return; + } + const textBeforeCursor = searcher.getSubStringBefore(5); + const textRange = searcher.getRangeFromText(textBeforeCursor, true /*exactMatch*/); + + if (textRange) { + const number = isFirstItemOfAList(textBeforeCursor) + ? 1 + : parseInt(textBeforeCursor); + + const isLi = getPreviousListItem(editor, textRange); + const listStyle = getAutoNumberingListStyle(textBeforeCursor) ?? undefined; + prepareAutoBullet(editor, textRange); + toggleNumbering( + editor, + isLi && number !== 1 ? undefined : number /** startNumber */, + listStyle, + 'autoToggleList' /** apiNameOverride */ + ); + } + searcher.getRangeFromText(textBeforeCursor, true /*exactMatch*/)?.deleteContents(); + }, + undefined /*changeSource*/, + true /*canUndoByBackspace*/ + ); + }, +}; + +const getPreviousListItem = (editor: IEditor, textRange: Range) => { + const blockElement = editor + .getBodyTraverser(textRange?.startContainer) + .getPreviousBlockElement(); + const previousNode = blockElement?.getEndNode() ?? null; + return getTagOfNode(previousNode) === 'LI' ? previousNode : undefined; +}; + +const getPreviousListType = (editor: IEditor, textRange: Range, listType: ListType) => { + const type = listType === ListType.Ordered ? 'orderedStyleType' : 'unorderedStyleType'; + const listItem = getPreviousListItem(editor, textRange); + const list = listItem + ? findClosestElementAncestor( + listItem, + undefined /** root*/, + listType === ListType.Ordered ? 'ol' : 'ul' + ) + : null; + const metadata = list ? getMetadata(list, ListStyleDefinitionMetadata) : null; + return metadata ? metadata[type] : null; +}; + +const isFirstItemOfAList = (item: string) => { + const number = parseInt(item); + if (number && number === 1) { + return 1; + } else { + const letter = item.replace(/\(|\)|\-|\./g, '').trim(); + return letter.length === 1 && ['i', 'a', 'I', 'A'].indexOf(letter) > -1 ? 1 : undefined; + } +}; + /** * Maintain the list numbers in list chain * e.g. we have two lists: @@ -200,24 +442,15 @@ const AutoBullet: BuildInEditFeature = { const MaintainListChain: BuildInEditFeature = { keys: [Keys.ENTER, Keys.TAB, Keys.DELETE, Keys.BACKSPACE, Keys.RANGE], shouldHandleEvent: (event, editor) => - editor.queryElements('li', QueryScope.OnSelection).length > 0, + editor + .queryElements('li', QueryScope.OnSelection) + .filter(li => !li.getElementsByTagName('blockquote').length).length > 0, handleEvent: (event, editor) => { const chains = getListChains(editor); - editor.runAsync(editor => experimentCommitListChains(editor, chains)); + editor.runAsync(editor => commitListChains(editor, chains)); }, }; -/** - * Validate if a block of text is considered a list pattern - * The regex expression will look for patterns of the form: - * 1. 1> 1) 1- (1) - * @returns if a text is considered a list pattern - */ -function isAListPattern(textBeforeCursor: string) { - const REGEX: RegExp = /^(\*|-|[0-9]{1,2}\.|[0-9]{1,2}\>|[0-9]{1,2}\)|[0-9]{1,2}\-|\([0-9]{1,2}\))$/; - return REGEX.test(textBeforeCursor); -} - function getListChains(editor: IEditor) { return VListChain.createListChains(editor.getSelectedRegions()); } @@ -225,8 +458,8 @@ function getListChains(editor: IEditor) { function getCacheNextSibling(event: PluginKeyboardEvent, editor: IEditor): Node | undefined { const element = cacheGetEventData(event, 'nextSibling', () => { const range = editor.getSelectionRange(); - const pos = Position.getEnd(range).normalize(); - const traverser = editor.getBodyTraverser(pos.node); + const pos = range && Position.getEnd(range).normalize(); + const traverser = pos && editor.getBodyTraverser(pos.node); return traverser?.getNextBlockElement()?.getStartNode(); }); return element; @@ -240,7 +473,7 @@ function prepareAutoBullet(editor: IEditor, range: Range) { if (isBlockElement(endNode)) { endNode.appendChild(br); } else { - endNode.parentNode.insertBefore(br, endNode.nextSibling); + endNode.parentNode?.insertBefore(br, endNode.nextSibling); } editor.select(range.startContainer, range.startOffset); } @@ -260,7 +493,7 @@ function toggleListAndPreventDefault( toggleListType( editor, tag == 'UL' ? ListType.Unordered : ListType.Ordered, - null /* startNumber */, + undefined /* startNumber */, includeSiblingLists ); } @@ -271,11 +504,135 @@ function toggleListAndPreventDefault( } function cacheGetListElement(event: PluginKeyboardEvent, editor: IEditor) { - let li = editor.getElementAtCursor('LI,TABLE', null /*startFrom*/, event); + let li = editor.getElementAtCursor('LI,TABLE', undefined /*startFrom*/, event); let listElement = li && getTagOfNode(li) == 'LI' && editor.getElementAtCursor('UL,OL', li); return listElement ? [listElement, li] : null; } +function shouldTriggerList< + T extends ListType, + K extends T extends ListType.Ordered ? NumberingListType : BulletListType +>( + event: PluginKeyboardEvent, + editor: IEditor, + getListStyle: ( + text: string, + previousListChain?: VListChain[], + previousListStyle?: K + ) => K | null, + listType: T +) { + const searcher = editor.getContentSearcherOfCursor(event); + if (!searcher) { + return false; + } + const textBeforeCursor = searcher.getSubStringBefore(4); + const traverser = editor.getBlockTraverser(); + const text = + traverser && traverser.currentBlockElement + ? traverser.currentBlockElement.getTextContent().slice(0, textBeforeCursor.length) + : null; + const isATheBeginning = text && text === textBeforeCursor; + const listChains = getListChains(editor); + const textRange = searcher.getRangeFromText(textBeforeCursor, true /*exactMatch*/); + const previousListType = + textRange && getPreviousListType(editor, textRange, listType); + const isFirstItem = isFirstItemOfAList(textBeforeCursor); + const listStyle = getListStyle(textBeforeCursor, listChains, previousListType ?? undefined); + const shouldTriggerNewListStyle = + isFirstItem || + !previousListType || + previousListType === listStyle || + listType === ListType.Unordered; + + return ( + isATheBeginning && + !searcher.getNearestNonTextInlineElement() && + listStyle && + shouldTriggerNewListStyle + ); +} + +/** + * MergeListOnBackspaceAfterList edit feature, provides the ability to merge list on backspace on block after a list. + */ +const MergeListOnBackspaceAfterList: BuildInEditFeature = { + keys: [Keys.BACKSPACE], + shouldHandleEvent: (event, editor) => { + const target = editor.getElementAtCursor(); + if (target) { + const cursorBlock = editor.getBlockElementAtNode(target)?.getStartNode() as HTMLElement; + const previousBlock = cursorBlock?.previousElementSibling ?? null; + + if (isList(previousBlock)) { + const range = editor.getSelectionRange(); + const searcher = editor.getContentSearcherOfCursor(event); + const textBeforeCursor = searcher?.getSubStringBefore(4); + const nearestInline = searcher?.getNearestNonTextInlineElement(); + + if (range && range.collapsed && textBeforeCursor === '' && !nearestInline) { + const tempBlock = cursorBlock?.nextElementSibling; + const nextBlock = isList(tempBlock) ? tempBlock : tempBlock?.firstChild; + + if ( + isList(nextBlock) && + getTagOfNode(previousBlock) == getTagOfNode(nextBlock) + ) { + const element = cacheGetEventData( + event, + PREVIOUS_BLOCK_CACHE_KEY, + () => previousBlock + ); + const nextElement = cacheGetEventData( + event, + NEXT_BLOCK_CACHE_KEY, + () => nextBlock + ); + + return !!element && !!nextElement; + } + } + } + } + + return false; + }, + handleEvent: (event, editor) => { + editor.runAsync(editor => { + const previousList = cacheGetEventData( + event, + PREVIOUS_BLOCK_CACHE_KEY, + () => null + ); + const targetBlock = cacheGetEventData( + event, + NEXT_BLOCK_CACHE_KEY, + () => null + ); + + const rangeBeforeWriteBack = editor.getSelectionRange(); + + if (previousList && targetBlock && rangeBeforeWriteBack) { + const fvList = new VList(previousList); + fvList.mergeVList(new VList(targetBlock)); + + let span = editor.getDocument().createElement('span'); + span.id = 'restoreRange'; + rangeBeforeWriteBack.insertNode(span); + + fvList.writeBack(); + + span = editor.queryElements('#restoreRange')[0]; + + if (span.parentElement) { + editor.select(new Position(span, PositionType.After)); + span.parentElement.removeChild(span); + } + } + }); + }, +}; + /** * @internal */ @@ -291,4 +648,16 @@ export const ListFeatures: Record< mergeInNewLineWhenBackspaceOnFirstChar: MergeInNewLine, maintainListChain: MaintainListChain, maintainListChainWhenDelete: MaintainListChainWhenDelete, + autoNumberingList: AutoNumberingList, + autoBulletList: AutoBulletList, + mergeListOnBackspaceAfterList: MergeListOnBackspaceAfterList, + outdentWhenAltShiftLeft: OutdentWhenAltShiftLeft, + indentWhenAltShiftRight: IndentWhenAltShiftRight, }; + +function isList(element: Node | null | undefined): element is HTMLOListElement | HTMLOListElement { + return ( + !!element && + (safeInstanceOf(element, 'HTMLOListElement') || safeInstanceOf(element, 'HTMLUListElement')) + ); +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/markdownFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/markdownFeatures.ts index 9f92b0145022..b8adae8ace8d 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/markdownFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/markdownFeatures.ts @@ -1,4 +1,5 @@ -import { cacheGetEventData, createRange } from 'roosterjs-editor-dom'; +import { cacheGetEventData, createRange, Position, wrap } from 'roosterjs-editor-dom'; +import type { CompatibleKeys } from 'roosterjs-editor-types/lib/compatibleTypes'; import { BuildInEditFeature, ChangeSource, @@ -13,7 +14,7 @@ import { const ZERO_WIDTH_SPACE = '\u200B'; function generateBasicMarkdownFeature( - key: Keys, + key: Keys | CompatibleKeys, triggerCharacter: string, elementTag: string, useShiftKey: boolean @@ -36,13 +37,13 @@ function cacheGetRangeForMarkdownOperation( event: PluginKeyboardEvent, editor: IEditor, triggerCharacter: string -): Range { - return cacheGetEventData(event, 'MARKDOWN_RANGE', () => { +): Range | null { + return cacheGetEventData(event, 'MARKDOWN_RANGE', (): Range | null => { const searcher = editor.getContentSearcherOfCursor(event); - let startPosition: NodePosition; - let endPosition: NodePosition; - searcher.forEachTextInlineElement(textInlineElement => { + let startPosition: NodePosition | null = null; + let endPosition: NodePosition | null = null; + searcher?.forEachTextInlineElement(textInlineElement => { if (endPosition && startPosition) { return true; } @@ -53,8 +54,13 @@ function cacheGetRangeForMarkdownOperation( return false; } + //if the text is pasted, it might create a inner element inside the text element, + // then is necessary to check the parent block to get whole text + const parentBlockText = textInlineElement.getParentBlock().getTextContent(); + // special case for consecutive trigger characters - if (inlineTextContent[inlineTextContent.length - 1] === triggerCharacter) { + // check parent block in case of pasted text + if (parentBlockText[parentBlockText.length - 1].trim() === triggerCharacter) { return false; } @@ -79,7 +85,7 @@ function cacheGetRangeForMarkdownOperation( } } }); - return !!startPosition && !!endPosition && createRange(startPosition, endPosition); + return startPosition && endPosition && createRange(startPosition, endPosition); }); } @@ -92,7 +98,12 @@ function handleMarkdownEvent( editor.addUndoSnapshot( () => { const range = cacheGetRangeForMarkdownOperation(event, editor, triggerCharacter); - if (!!range) { + if (!range) { + return; + } + const lastTypedTriggerPosition = new Position(range.endContainer, PositionType.End); + const hasLastTypedTrigger = range.endOffset + 1 <= lastTypedTriggerPosition.offset; + if (!!range && hasLastTypedTrigger) { // get the text content range const textContentRange = range.cloneRange(); textContentRange.setStart( @@ -100,12 +111,13 @@ function handleMarkdownEvent( textContentRange.startOffset + 1 ); - // set the removal range to include the typed last character. - range.setEnd(range.endContainer, range.endOffset + 1); + const text = textContentRange.extractContents().textContent; + const textNode = editor.getDocument().createTextNode(text ?? ''); // extract content and put it into a new element. - const elementToWrap = editor.getDocument().createElement(elementTag); - elementToWrap.appendChild(textContentRange.extractContents()); + const elementToWrap = wrap(textNode, elementTag); + //include last typed character + range.setEnd(range.endContainer, range.endOffset + 1); range.deleteContents(); // ZWS here ensures we don't end up inside the newly created node. @@ -114,6 +126,7 @@ function handleMarkdownEvent( .createTextNode(ZERO_WIDTH_SPACE); range.insertNode(nonPrintedSpaceTextNode); range.insertNode(elementToWrap); + editor.select(nonPrintedSpaceTextNode, PositionType.End); } }, @@ -129,7 +142,7 @@ const MarkdownBold: BuildInEditFeature = generateBasicMarkd Keys.EIGHT_ASTERISK, '*', 'b', - true + true /* useShiftKey */ ); /** @@ -139,7 +152,7 @@ const MarkdownItalic: BuildInEditFeature = generateBasicMar Keys.DASH_UNDERSCORE, '_', 'i', - true + true /* useShiftKey */ ); /** @@ -149,7 +162,7 @@ const MarkdownStrikethrough: BuildInEditFeature = generateB Keys.GRAVE_TILDE, '~', 's', - true + true /* useShiftKey */ ); /** @@ -159,7 +172,7 @@ const MarkdownInlineCode: BuildInEditFeature = generateBasi Keys.GRAVE_TILDE, '`', 'code', - false + false /* useShiftKey */ ); /** diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/quoteFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/quoteFeatures.ts index f1d891ac35c1..983e201cdbd4 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/quoteFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/quoteFeatures.ts @@ -48,12 +48,12 @@ const UnquoteWhenEnterOnEmptyLine: BuildInEditFeature = { handleEvent: (event, editor) => editor.addUndoSnapshot( () => splitQuote(event, editor), - null /*changeSource*/, + undefined /*changeSource*/, true /*canUndoByBackspace*/ ), }; -function cacheGetQuoteChild(event: PluginKeyboardEvent, editor: IEditor): Node { +function cacheGetQuoteChild(event: PluginKeyboardEvent, editor: IEditor): Node | null { return cacheGetEventData(event, 'QUOTE_CHILD', () => { let quote = editor.getElementAtCursor(STRUCTURED_TAGS); if (quote && getTagOfNode(quote) == QUOTE_TAG) { @@ -75,17 +75,21 @@ function cacheGetQuoteChild(event: PluginKeyboardEvent, editor: IEditor): Node { function splitQuote(event: PluginKeyboardEvent, editor: IEditor) { editor.addUndoSnapshot(() => { let childOfQuote = cacheGetQuoteChild(event, editor); - let parent: Node; - let shouldClearFormat: boolean; + if (!childOfQuote) { + return; + } if (getTagOfNode(childOfQuote) == QUOTE_TAG) { childOfQuote = wrap(toArray(childOfQuote.childNodes)); } - parent = splitBalancedNodeRange(childOfQuote); - shouldClearFormat = isStyledBlockquote(parent); - unwrap(parent); + const parent = splitBalancedNodeRange(childOfQuote); + const shouldClearFormat = !!parent && isStyledBlockquote(parent); + const newParent = parent && unwrap(parent); editor.select(childOfQuote, PositionType.Begin); if (shouldClearFormat) { + if (safeInstanceOf(newParent, 'HTMLLIElement')) { + newParent.style.removeProperty('color'); + } clearFormat(editor); } }); diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/shortcutFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/shortcutFeatures.ts index fe02420f1afb..1488274e5715 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/shortcutFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/shortcutFeatures.ts @@ -15,6 +15,7 @@ import { toggleUnderline, toggleBullet, toggleNumbering, + clearFormat, } from 'roosterjs-editor-api'; interface ShortcutCommand { @@ -23,7 +24,15 @@ interface ShortcutCommand { action: (editor: IEditor) => any; } -function createCommand(winKey: number, macKey: number, action: (editor: IEditor) => any) { +function createCommand( + winKey: number, + macKey: number, + action: (editor: IEditor) => any, + disabled: boolean = false +) { + if (disabled) { + return null; + } return { winKey, macKey, @@ -35,7 +44,14 @@ const commands: ShortcutCommand[] = [ createCommand(Keys.Ctrl | Keys.B, Keys.Meta | Keys.B, toggleBold), createCommand(Keys.Ctrl | Keys.I, Keys.Meta | Keys.I, toggleItalic), createCommand(Keys.Ctrl | Keys.U, Keys.Meta | Keys.U, toggleUnderline), + createCommand(Keys.Ctrl | Keys.SPACE, Keys.Meta | Keys.SPACE, clearFormat), createCommand(Keys.Ctrl | Keys.Z, Keys.Meta | Keys.Z, editor => editor.undo()), + createCommand( + Keys.ALT | Keys.BACKSPACE, + Keys.ALT | Keys.BACKSPACE, + editor => editor.undo(), + Browser.isMac /* Option+Backspace to be handled by browsers on Mac */ + ), createCommand(Keys.Ctrl | Keys.Y, Keys.Meta | Keys.Shift | Keys.Z, editor => editor.redo()), createCommand(Keys.Ctrl | Keys.PERIOD, Keys.Meta | Keys.PERIOD, toggleBullet), createCommand(Keys.Ctrl | Keys.FORWARD_SLASH, Keys.Meta | Keys.FORWARD_SLASH, toggleNumbering), @@ -49,13 +65,15 @@ const commands: ShortcutCommand[] = [ Keys.Meta | Keys.Shift | Keys.COMMA, editor => changeFontSize(editor, FontSizeChange.Decrease) ), -]; +].filter((command): command is ShortcutCommand => !!command); /** * DefaultShortcut edit feature, provides shortcuts for the following features: * Ctrl/Meta+B: toggle bold style * Ctrl/Meta+I: toggle italic style * Ctrl/Meta+U: toggle underline style + * Ctrl/Meta+Space: clear formatting + * Alt+Backspace: undo * Ctrl/Meta+Z: undo * Ctrl+Y/Meta+Shift+Z: redo * Ctrl/Meta+PERIOD: toggle bullet list @@ -65,7 +83,18 @@ const commands: ShortcutCommand[] = [ */ const DefaultShortcut: BuildInEditFeature = { allowFunctionKeys: true, - keys: [Keys.B, Keys.I, Keys.U, Keys.Y, Keys.Z, Keys.COMMA, Keys.PERIOD, Keys.FORWARD_SLASH], + keys: [ + Keys.B, + Keys.I, + Keys.U, + Keys.Y, + Keys.Z, + Keys.COMMA, + Keys.PERIOD, + Keys.FORWARD_SLASH, + Keys.SPACE, + Keys.BACKSPACE, + ], shouldHandleEvent: cacheGetCommand, handleEvent: (event, editor) => { let command = cacheGetCommand(event); @@ -81,13 +110,16 @@ function cacheGetCommand(event: PluginKeyboardEvent) { return cacheGetEventData(event, 'DEFAULT_SHORT_COMMAND', () => { let e = event.rawEvent; let key = - // Need to check ALT key to be false since in some language (e.g. Polski) uses AltGr to input some special characters - // In that case, ctrlKey and altKey are both true in Edge, but we should not trigger any shortcut function here - event.eventType == PluginEventType.KeyDown && !e.altKey + // Need to check AltGraph isn't being pressed since some languages (e.g. Polski) use AltGr + // to input some special characters. In that case, ctrlKey and altKey are both true in Edge, + // but we should not trigger any shortcut function here. However, we still want to capture + // the ALT+BACKSPACE combination. + event.eventType == PluginEventType.KeyDown && !e.getModifierState('AltGraph') ? e.which | - (e.metaKey && Keys.Meta) | - (e.shiftKey && Keys.Shift) | - (e.ctrlKey && Keys.Ctrl) + ((e.metaKey && Keys.Meta)) | + ((e.shiftKey && Keys.Shift)) | + ((e.ctrlKey && Keys.Ctrl)) | + ((e.altKey && Keys.ALT)) : 0; return key && commands.filter(cmd => (Browser.isMac ? cmd.macKey : cmd.winKey) == key)[0]; }); diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/structuredNodeFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/structuredNodeFeatures.ts index 137fcc68f70c..7f980d5791ff 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/structuredNodeFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/structuredNodeFeatures.ts @@ -13,6 +13,7 @@ import { Position, getTagOfNode, createElement, + getObjectKeys, } from 'roosterjs-editor-dom'; const CHILD_PARENT_TAG_MAP: { [childTag: string]: string } = { @@ -20,7 +21,7 @@ const CHILD_PARENT_TAG_MAP: { [childTag: string]: string } = { TH: 'TABLE', LI: 'OL,UL', }; -const CHILD_SELECTOR = Object.keys(CHILD_PARENT_TAG_MAP).join(','); +const CHILD_SELECTOR = getObjectKeys(CHILD_PARENT_TAG_MAP).join(','); /** * InsertLineBeforeStructuredNode edit feature, provides the ability to insert an empty line before @@ -37,7 +38,7 @@ const InsertLineBeforeStructuredNodeFeature: BuildInEditFeature { - element.parentNode.insertBefore(div, element); + element?.parentNode?.insertBefore(div, element); // Select the new line when we are in table. This is the same behavior with Word if (getTagOfNode(element) == 'TABLE') { editor.select(new Position(div, PositionType.Begin).normalize()); diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/tableFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/tableFeatures.ts index bcdc005f9533..06697fac4d00 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/tableFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/tableFeatures.ts @@ -1,4 +1,4 @@ -import { editTable } from 'roosterjs-editor-api'; +import { editTable, setIndentation } from 'roosterjs-editor-api'; import { BuildInEditFeature, IEditor, @@ -9,6 +9,10 @@ import { TableFeatureSettings, TableOperation, PluginKeyboardEvent, + SelectionRangeTypes, + TableSelectionRange, + Indentation, + ExperimentalFeatures, } from 'roosterjs-editor-types'; import { Browser, @@ -16,6 +20,7 @@ import { contains, getTagOfNode, isVoidHtmlElement, + isWholeTableSelected, Position, VTable, } from 'roosterjs-editor-dom'; @@ -25,32 +30,37 @@ import { */ const TabInTable: BuildInEditFeature = { keys: [Keys.TAB], - shouldHandleEvent: cacheGetTableCell, + shouldHandleEvent: (event: PluginKeyboardEvent, editor: IEditor) => + cacheGetTableCell(event, editor) && !cacheIsWholeTableSelected(event, editor), handleEvent: (event, editor) => { let shift = event.rawEvent.shiftKey; let td = cacheGetTableCell(event, editor); + if (!td) { + return; + } + let vtable = cacheVTable(event, td); + for ( - let vtable = new VTable(td), - step = shift ? -1 : 1, - row = vtable.row, - col = vtable.col + step; + let step = shift ? -1 : 1, row = vtable.row ?? 0, col = (vtable.col ?? 0) + step; ; col += step ) { - if (col < 0 || col >= vtable.cells[row].length) { + const tableCells = vtable.cells ?? []; + if (col < 0 || col >= tableCells[row].length) { row += step; if (row < 0) { editor.select(vtable.table, PositionType.Before); break; - } else if (row >= vtable.cells.length) { + } else if (row >= tableCells.length) { editTable(editor, TableOperation.InsertBelow); break; } - col = shift ? vtable.cells[row].length - 1 : 0; + col = shift ? tableCells[row].length - 1 : 0; } let cell = vtable.getCell(row, col); if (cell.td) { - editor.select(cell.td, PositionType.Begin); + const newPos = new Position(cell.td, PositionType.Begin).normalize(); + editor.select(newPos); break; } } @@ -58,27 +68,67 @@ const TabInTable: BuildInEditFeature = { }, }; +/** + * IndentTableOnTab edit feature, provides the ability to indent the table if it is all cells are selected. + */ +const IndentTableOnTab: BuildInEditFeature = { + keys: [Keys.TAB], + shouldHandleEvent: (event: PluginKeyboardEvent, editor: IEditor) => + cacheGetTableCell(event, editor) && cacheIsWholeTableSelected(event, editor), + handleEvent: (event, editor) => { + event.rawEvent.preventDefault(); + + editor.addUndoSnapshot(() => { + let shift = event.rawEvent.shiftKey; + let selection = editor.getSelectionRangeEx() as TableSelectionRange; + let td = cacheGetTableCell(event, editor); + if (!td) { + return; + } + let vtable = cacheVTable(event, td); + + if (shift && editor.getElementAtCursor('blockquote', vtable.table, event)) { + setIndentation(editor, Indentation.Decrease); + } else if (!shift) { + setIndentation(editor, Indentation.Increase); + } + + if (selection.coordinates) { + editor.select(selection.table, selection.coordinates); + } + }); + }, +}; + /** * UpDownInTable edit feature, provides the ability to jump to cell above/below when user press UP/DOWN * in table */ const UpDownInTable: BuildInEditFeature = { keys: [Keys.UP, Keys.DOWN], - shouldHandleEvent: cacheGetTableCell, + shouldHandleEvent: (event: PluginKeyboardEvent, editor: IEditor) => + cacheGetTableCell(event, editor) && !cacheIsWholeTableSelected(event, editor), handleEvent: (event, editor) => { const td = cacheGetTableCell(event, editor); + if (!td) { + return; + } const vtable = new VTable(td); const isUp = event.rawEvent.which == Keys.UP; const step = isUp ? -1 : 1; const hasShiftKey = event.rawEvent.shiftKey; const selection = editor.getDocument().defaultView?.getSelection(); - let targetTd: HTMLTableCellElement = null; + let targetTd: HTMLTableCellElement | null = null; if (selection) { let { anchorNode, anchorOffset } = selection; - for (let row = vtable.row; row >= 0 && row < vtable.cells.length; row += step) { - let cell = vtable.getCell(row, vtable.col); + for ( + let row = vtable.row ?? 0; + row >= 0 && vtable.cells && row < vtable.cells.length; + row += step + ) { + let cell = vtable.getCell(row, vtable.col ?? 0); if (cell.td && cell.td != td) { targetTd = cell.td; break; @@ -107,14 +157,16 @@ const UpDownInTable: BuildInEditFeature = { ) : newPos; const selection = editor.getDocument().defaultView?.getSelection(); - selection?.setBaseAndExtent( - anchorNode, - anchorOffset, - newPos.node, - newPos.offset - ); + if (anchorNode) { + selection?.setBaseAndExtent( + anchorNode, + anchorOffset, + newPos.node, + newPos.offset + ); + } } else { - editor.select(newPos); + editor.select(newPos.normalize()); } } }); @@ -123,7 +175,27 @@ const UpDownInTable: BuildInEditFeature = { defaultDisabled: !Browser.isChrome && !Browser.isSafari, }; -function cacheGetTableCell(event: PluginEvent, editor: IEditor): HTMLTableCellElement { +/** + * Requires @see ExperimentalFeatures.DeleteTableWithBackspace + * Delete a table selected with the table selector pressing Backspace key + */ +const DeleteTableWithBackspace: BuildInEditFeature = { + keys: [Keys.BACKSPACE], + shouldHandleEvent: (event: PluginKeyboardEvent, editor: IEditor) => + editor.isFeatureEnabled(ExperimentalFeatures.DeleteTableWithBackspace) && + cacheIsWholeTableSelected(event, editor), + handleEvent: (event, editor) => { + const td = cacheGetTableCell(event, editor); + if (!td) { + return; + } + const vtable = new VTable(td); + vtable.edit(TableOperation.DeleteTable); + vtable.writeBack(); + }, +}; + +function cacheGetTableCell(event: PluginEvent, editor: IEditor): HTMLTableCellElement | null { return cacheGetEventData(event, 'TABLE_CELL_FOR_TABLE_FEATURES', () => { let pos = editor.getFocusedPosition(); let firstTd = pos && editor.getElementAtCursor('TD,TH,LI', pos.node); @@ -133,6 +205,28 @@ function cacheGetTableCell(event: PluginEvent, editor: IEditor): HTMLTableCellEl }); } +function cacheIsWholeTableSelected(event: PluginEvent, editor: IEditor) { + return cacheGetEventData(event, 'WHOLE_TABLE_SELECTED_FOR_FEATURES', () => { + const td = cacheGetTableCell(event, editor); + if (!td) { + return false; + } + let vtable = cacheVTable(event, td); + let selection = editor.getSelectionRangeEx(); + return ( + selection.type == SelectionRangeTypes.TableSelection && + selection.coordinates && + isWholeTableSelected(vtable, selection.coordinates) + ); + }); +} + +function cacheVTable(event: PluginEvent, td: HTMLTableCellElement) { + return cacheGetEventData(event, 'VTABLE_FOR_TABLE_FEATURES', () => { + return new VTable(td); + }); +} + /** * @internal */ @@ -142,4 +236,6 @@ export const TableFeatures: Record< > = { tabInTable: TabInTable, upDownInTable: UpDownInTable, + indentTableOnTab: IndentTableOnTab, + deleteTableWithBackspace: DeleteTableWithBackspace, }; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/textFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/textFeatures.ts new file mode 100644 index 000000000000..2a8e379fe8a2 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/textFeatures.ts @@ -0,0 +1,218 @@ +import { setIndentation } from 'roosterjs-editor-api'; +import { + createRange, + getEntitySelector, + getTagOfNode, + Position, + queryElements, +} from 'roosterjs-editor-dom'; +import { + BuildInEditFeature, + IEditor, + Indentation, + TextFeatureSettings, + Keys, + PluginKeyboardEvent, + SelectionRangeTypes, + ContentPosition, + PositionType, + ExperimentalFeatures, + NodePosition, + QueryScope, +} from 'roosterjs-editor-types'; + +const TAB_SPACES = 6; + +/** + * Requires @see ExperimentalFeatures.TabKeyTextFeatures to be enabled + * Provides additional functionality when press Tab: + * If Whole Paragraph selected, indent paragraph, + * If range is collapsed, add tab spaces + * If range is not collapsed but not all the paragraph is selected, replace selection with Tab spaces + * If there are more than one block in the selection, indent all selection + */ +const IndentWhenTabText: BuildInEditFeature = { + keys: [Keys.TAB], + shouldHandleEvent: (event, editor) => { + if ( + editor.isFeatureEnabled(ExperimentalFeatures.TabKeyTextFeatures) && + !event.rawEvent.shiftKey + ) { + let activeElement = editor.getDocument().activeElement as HTMLElement; + const listOrTable = editor.getElementAtCursor( + 'LI,TABLE', + undefined /*startFrom*/, + event + ); + const entity = editor.getElementAtCursor( + getEntitySelector(), + undefined /*startFrom*/, + event + ); + + return ( + !listOrTable && + (entity ? entity.isContentEditable : activeElement.isContentEditable) + ); + } + + return false; + }, + handleEvent: (event, editor) => { + const selection = editor.getSelectionRangeEx(); + if (selection.type == SelectionRangeTypes.Normal) { + editor.addUndoSnapshot(() => { + if (selection.areAllCollapsed) { + insertTab(editor, event); + } else { + const { ranges } = selection; + const range = ranges[0]; + if (shouldSetIndentation(editor, range)) { + setIndentation(editor, Indentation.Increase); + } else { + const tempRange = createRange(range.startContainer, range.startOffset); + ranges.forEach(range => range.deleteContents()); + editor.select(tempRange); + insertTab(editor, event); + } + } + }); + + event.rawEvent.preventDefault(); + } + }, +}; + +/** + * Requires @see ExperimentalFeatures.TabKeyTextFeatures to be enabled + * If Whole Paragraph selected, outdent paragraph on Tab press + */ +const OutdentWhenTabText: BuildInEditFeature = { + keys: [Keys.TAB], + shouldHandleEvent: (event, editor) => { + if ( + event.rawEvent.shiftKey && + editor.isFeatureEnabled(ExperimentalFeatures.TabKeyTextFeatures) + ) { + const selection = editor.getSelectionRangeEx(); + + return ( + selection.type == SelectionRangeTypes.Normal && + !selection.areAllCollapsed && + editor.getElementAtCursor('blockquote', undefined, event) && + !editor.getElementAtCursor('LI,TABLE', undefined /*startFrom*/, event) && + shouldSetIndentation(editor, selection.ranges[0]) + ); + } + + return false; + }, + handleEvent: (event, editor) => { + editor.addUndoSnapshot(() => setIndentation(editor, Indentation.Decrease)); + + event.rawEvent.preventDefault(); + }, +}; + +/** + * @deprecated + * Automatically transform -- into hyphen, if typed between two words. + */ +const AutoHyphen: BuildInEditFeature = { + keys: [], + shouldHandleEvent: (event, editor) => { + return false; + }, + handleEvent: (event, editor) => { + return false; + }, + defaultDisabled: true, +}; + +/** + * @internal + */ +export const TextFeatures: Record< + keyof TextFeatureSettings, + BuildInEditFeature +> = { + indentWhenTabText: IndentWhenTabText, + outdentWhenTabText: OutdentWhenTabText, + autoHyphen: AutoHyphen, +}; + +function shouldSetIndentation(editor: IEditor, range: Range): boolean { + let result: boolean = false; + + const startPosition: NodePosition = Position.getStart(range); + const endPosition: NodePosition = Position.getEnd(range); + const firstBlock = editor.getBlockElementAtNode(startPosition.node); + const lastBlock = editor.getBlockElementAtNode(endPosition.node); + + if (!firstBlock || !lastBlock) { + return false; + } + + if (!firstBlock.equals(lastBlock)) { + //If the selections has more than one block, we indent all the blocks in the selection + return true; + } else { + //We only indent a single block if all the block is selected. + const blockStart = new Position(firstBlock.getStartNode(), PositionType.Begin); + const blockEnd = new Position(firstBlock.getEndNode(), PositionType.End); + + const rangeBefore = createRange(blockStart, Position.getStart(range)); + const rangeAfter = createRange(Position.getEnd(range), blockEnd); + + if (!result && isRangeEmpty(rangeBefore) && isRangeEmpty(rangeAfter)) { + result = true; + } + + return result; + } +} + +function isRangeEmpty(range: Range) { + return ( + range.toString() == '' && + queryElements( + range.commonAncestorContainer as ParentNode, + 'img,table,ul,ol', + null, + QueryScope.InSelection, + range + ).length == 0 + ); +} + +function insertTab(editor: IEditor, event: PluginKeyboardEvent) { + const span = editor.getDocument().createElement('span'); + let searcher = editor.getContentSearcherOfCursor(event); + if (!searcher) { + return; + } + const charsBefore = searcher.getSubStringBefore(Number.MAX_SAFE_INTEGER); + const numberOfChars = TAB_SPACES - (charsBefore.length % TAB_SPACES); + let span2: HTMLSpanElement | null = null; + + let textContent = ''; + for (let index = 0; index < numberOfChars; index++) { + textContent += ' '; + } + editor.insertNode(span); + if (span.nextElementSibling && getTagOfNode(span.nextElementSibling) == 'A') { + span2 = editor.getDocument().createElement('span'); + span2.textContent = ' '; + editor.insertNode(span2); + editor.select(createRange(span2, PositionType.Before)); + } + editor.insertContent(textContent, { + position: ContentPosition.Range, + range: createRange(span, PositionType.Begin), + updateCursor: false, + }); + editor.select(createRange(span, PositionType.After)); + if (span2) { + editor.deleteNode(span2); + } +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/getAllFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/getAllFeatures.ts index bd9e87bb2b9e..32b438245f33 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/getAllFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/getAllFeatures.ts @@ -7,6 +7,8 @@ import { QuoteFeatures } from './features/quoteFeatures'; import { ShortcutFeatures } from './features/shortcutFeatures'; import { StructuredNodeFeatures } from './features/structuredNodeFeatures'; import { TableFeatures } from './features/tableFeatures'; +import { TextFeatures } from './features/textFeatures'; +import { CodeFeatures } from './features/codeFeatures'; import { BuildInEditFeature, ContentEditFeatureSettings, @@ -23,14 +25,13 @@ const allFeatures = { ...CursorFeatures, ...MarkdownFeatures, ...EntityFeatures, + ...TextFeatures, + ...CodeFeatures, }; /** * Get all content edit features provided by roosterjs */ -export default function getAllFeatures(): Record< - keyof ContentEditFeatureSettings, - BuildInEditFeature -> { - return allFeatures; +export default function getAllFeatures() { + return allFeatures as Record>; } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/utils/convertAlphaToDecimals.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/utils/convertAlphaToDecimals.ts new file mode 100644 index 000000000000..e5b8c222902b --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/utils/convertAlphaToDecimals.ts @@ -0,0 +1,15 @@ +/** + * @internal + * Convert english alphabet numbers into decimal numbers + * @param letter The letter that needs to be converted + * @returns + */ +export default function convertAlphaToDecimals(letter: string): number | null { + const alpha = letter.toLocaleLowerCase(); + if (alpha) { + const size = alpha.length - 1; + const number = 26 * size + alpha.charCodeAt(size) - 96; + return number; + } + return null; +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/utils/getAutoBulletListStyle.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/utils/getAutoBulletListStyle.ts new file mode 100644 index 000000000000..dc6eea2b709b --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/utils/getAutoBulletListStyle.ts @@ -0,0 +1,27 @@ +import { BulletListType } from 'roosterjs-editor-types'; + +const bulletListType: Record = { + '*': BulletListType.Disc, + '-': BulletListType.Dash, + '--': BulletListType.Square, + '->': BulletListType.LongArrow, + '-->': BulletListType.DoubleLongArrow, + '=>': BulletListType.UnfilledArrow, + '>': BulletListType.ShortArrow, + '—': BulletListType.Hyphen, +}; + +const identifyBulletListType = (bullet: string): BulletListType | null => { + return bulletListType[bullet] || null; +}; + +/** + * @internal + * @param textBeforeCursor The trigger character + * @returns The style of a bullet list triggered by a string + */ +export default function getAutoBulletListStyle(textBeforeCursor: string): BulletListType | null { + const trigger = textBeforeCursor.trim(); + const bulletType = identifyBulletListType(trigger); + return bulletType; +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/utils/getAutoNumberingListStyle.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/utils/getAutoNumberingListStyle.ts new file mode 100644 index 000000000000..44a5295abf3e --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/utils/getAutoNumberingListStyle.ts @@ -0,0 +1,178 @@ +import convertAlphaToDecimals from './convertAlphaToDecimals'; +import { NumberingListType } from 'roosterjs-editor-types'; +import { VListChain } from 'roosterjs-editor-dom'; + +const enum NumberingTypes { + Decimal = 1, + LowerAlpha = 2, + UpperAlpha = 3, + LowerRoman = 4, + UpperRoman = 5, +} + +const enum Character { + Dot = 1, + Dash = 2, + Parenthesis = 3, + DoubleParenthesis = 4, +} + +const characters: Record = { + '.': Character.Dot, + '-': Character.Dash, + ')': Character.Parenthesis, +}; + +const lowerRomanTypes = [ + NumberingListType.LowerRoman, + NumberingListType.LowerRomanDash, + NumberingListType.LowerRomanDoubleParenthesis, + NumberingListType.LowerRomanParenthesis, +]; +const upperRomanTypes = [ + NumberingListType.UpperRoman, + NumberingListType.UpperRomanDash, + NumberingListType.UpperRomanDoubleParenthesis, + NumberingListType.UpperRomanParenthesis, +]; +const numberingTriggers = ['1', 'a', 'A', 'I', 'i']; +const lowerRomanNumbers = ['i', 'v', 'x', 'l', 'c', 'd', 'm']; +const upperRomanNumbers = ['I', 'V', 'X', 'L', 'C', 'D', 'M']; + +const identifyNumberingType = (text: string, previousListStyle?: NumberingListType) => { + if (!isNaN(parseInt(text))) { + return NumberingTypes.Decimal; + } else if (/[a-z]+/g.test(text)) { + if ( + (previousListStyle != undefined && + lowerRomanTypes.indexOf(previousListStyle) > -1 && + lowerRomanNumbers.indexOf(text[0]) > -1) || + (!previousListStyle && text === 'i') + ) { + return NumberingTypes.LowerRoman; + } else if (previousListStyle || (!previousListStyle && text === 'a')) { + return NumberingTypes.LowerAlpha; + } + } else if (/[A-Z]+/g.test(text)) { + if ( + (previousListStyle != undefined && + upperRomanTypes.indexOf(previousListStyle) > -1 && + upperRomanNumbers.indexOf(text[0]) > -1) || + (!previousListStyle && text === 'I') + ) { + return NumberingTypes.UpperRoman; + } else if (previousListStyle || (!previousListStyle && text === 'A')) { + return NumberingTypes.UpperAlpha; + } + } +}; + +const numberingListTypes: Record number | null> = { + [NumberingTypes.Decimal]: char => DecimalsTypes[char] || null, + [NumberingTypes.LowerAlpha]: char => LowerAlphaTypes[char] || null, + [NumberingTypes.UpperAlpha]: char => UpperAlphaTypes[char] || null, + [NumberingTypes.LowerRoman]: char => LowerRomanTypes[char] || null, + [NumberingTypes.UpperRoman]: char => UpperRomanTypes[char] || null, +}; + +const UpperRomanTypes: Record = { + [Character.Dot]: NumberingListType.UpperRoman, + [Character.Dash]: NumberingListType.UpperRomanDash, + [Character.Parenthesis]: NumberingListType.UpperRomanParenthesis, + [Character.DoubleParenthesis]: NumberingListType.UpperRomanDoubleParenthesis, +}; + +const LowerRomanTypes: Record = { + [Character.Dot]: NumberingListType.LowerRoman, + [Character.Dash]: NumberingListType.LowerRomanDash, + [Character.Parenthesis]: NumberingListType.LowerRomanParenthesis, + [Character.DoubleParenthesis]: NumberingListType.LowerRomanDoubleParenthesis, +}; + +const UpperAlphaTypes: Record = { + [Character.Dot]: NumberingListType.UpperAlpha, + [Character.Dash]: NumberingListType.UpperAlphaDash, + [Character.Parenthesis]: NumberingListType.UpperAlphaParenthesis, + [Character.DoubleParenthesis]: NumberingListType.UpperAlphaDoubleParenthesis, +}; + +const LowerAlphaTypes: Record = { + [Character.Dot]: NumberingListType.LowerAlpha, + [Character.Dash]: NumberingListType.LowerAlphaDash, + [Character.Parenthesis]: NumberingListType.LowerAlphaParenthesis, + [Character.DoubleParenthesis]: NumberingListType.LowerAlphaDoubleParenthesis, +}; + +const DecimalsTypes: Record = { + [Character.Dot]: NumberingListType.Decimal, + [Character.Dash]: NumberingListType.DecimalDash, + [Character.Parenthesis]: NumberingListType.DecimalParenthesis, + [Character.DoubleParenthesis]: NumberingListType.DecimalDoubleParenthesis, +}; + +const identifyNumberingListType = ( + numbering: string, + isDoubleParenthesis: boolean, + previousListStyle?: NumberingListType +): NumberingListType | null => { + const separatorCharacter = isDoubleParenthesis + ? Character.DoubleParenthesis + : characters[numbering[numbering.length - 1]]; + // if separator is not valid, no need to check if the number is valid. + if (separatorCharacter) { + const number = isDoubleParenthesis ? numbering.slice(1, -1) : numbering.slice(0, -1); + const numberingType = identifyNumberingType(number, previousListStyle); + return numberingType ? numberingListTypes[numberingType](separatorCharacter) : null; + } + return null; +}; + +/** + * @internal + * @param textBeforeCursor The trigger character + * @param previousListChain @optional This parameters is used to keep the list chain, if the is not a new list + * @param previousListStyle @optional The list style of the previous list + * @returns The style of a numbering list triggered by a string + */ +export default function getAutoNumberingListStyle( + textBeforeCursor: string, + previousListChain?: VListChain[], + previousListStyle?: NumberingListType +): NumberingListType | null { + const trigger = textBeforeCursor.trim(); + const isDoubleParenthesis = trigger[0] === '(' && trigger[trigger.length - 1] === ')'; + //Only the staring items ['1', 'a', 'A', 'I', 'i'] must trigger a new list. All the other triggers is used to keep the list chain. + //The index is always the characters before the last character + const listIndex = isDoubleParenthesis ? trigger.slice(1, -1) : trigger.slice(0, -1); + + const indexNumber = parseInt(listIndex); + let index = !isNaN(indexNumber) ? indexNumber : convertAlphaToDecimals(listIndex); + + if (!index || index < 1) { + return null; + } + + if (previousListChain && index > 1) { + if ( + (previousListChain.length < 1 && numberingTriggers.indexOf(listIndex) < 0) || + (previousListChain?.length > 0 && + !previousListChain[previousListChain.length - 1]?.canAppendAtCursor(index)) + ) { + return null; + } + } + + const numberingType = isValidNumbering(listIndex) + ? identifyNumberingListType(trigger, isDoubleParenthesis, previousListStyle) + : null; + return numberingType; +} + +/** + * Check if index has only numbers or only letters to avoid sequence of character such 1:1. trigger a list. + * @param index + * @returns + */ +function isValidNumbering(index: string) { + return Number(index) || /^[A-Za-z\s]*$/.test(index); +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContextMenu/ContextMenu.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContextMenu/ContextMenu.ts index 83873348820f..d86aa3f17922 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContextMenu/ContextMenu.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContextMenu/ContextMenu.ts @@ -37,9 +37,9 @@ export interface ContextMenuOptions { * An editor plugin that support showing a context menu using render() function from options parameter */ export default class ContextMenu implements EditorPlugin { - private container: HTMLElement; - private editor: IEditor; - private isMenuShowing: boolean; + private container: HTMLElement | null = null; + private editor: IEditor | null = null; + private isMenuShowing: boolean = false; /** * Create a new instance of ContextMenu class @@ -68,7 +68,7 @@ export default class ContextMenu implements EditorPlugin { dispose() { this.onDismiss(); - if (this.container) { + if (this.container?.parentNode) { this.container.parentNode.removeChild(this.container); this.container = null; } @@ -89,22 +89,24 @@ export default class ContextMenu implements EditorPlugin { rawEvent.preventDefault(); } - this.initContainer(rawEvent.pageX, rawEvent.pageY); - this.options.render(this.container, items as T[], this.onDismiss); - this.isMenuShowing = true; + if (this.initContainer(rawEvent.pageX, rawEvent.pageY)) { + this.options.render(this.container!, items as T[], this.onDismiss); + this.isMenuShowing = true; + } } } private initContainer(x: number, y: number) { - if (!this.container) { + if (!this.container && this.editor) { this.container = createElement( KnownCreateElementDataIndex.ContextMenuWrapper, this.editor.getDocument() ) as HTMLElement; this.editor.getDocument().body.appendChild(this.container); } - this.container.style.left = x + 'px'; - this.container.style.top = y + 'px'; + this.container?.style.setProperty('left', x + 'px'); + this.container?.style.setProperty('top', y + 'px'); + return !!this.container; } private onDismiss = () => { diff --git a/packages/roosterjs-editor-plugins/lib/plugins/CustomReplace/CustomReplace.ts b/packages/roosterjs-editor-plugins/lib/plugins/CustomReplace/CustomReplace.ts index 878b7ce0753d..9c930c577312 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/CustomReplace/CustomReplace.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/CustomReplace/CustomReplace.ts @@ -11,7 +11,11 @@ const makeReplacement = ( sourceString: string, replacementHTML: string, matchSourceCaseSensitive: boolean, - shouldReplace?: (replacement: CustomReplacement, content: string) => boolean + shouldReplace?: ( + replacement: CustomReplacement, + content: string, + sourceEditor?: IEditor + ) => boolean ): CustomReplacement => ({ sourceString, replacementHTML, @@ -32,10 +36,10 @@ const defaultReplacements: CustomReplacement[] = [ * content edit feature */ export default class CustomReplacePlugin implements EditorPlugin { - private longestReplacementLength: number; - private editor: IEditor; - private replacements: CustomReplacement[]; - private replacementEndCharacters: Set; + private longestReplacementLength: number | null = null; + private editor: IEditor | null = null; + private replacements: CustomReplacement[] | null = null; + private replacementEndCharacters: Set | null = null; /** * Create instance of CustomReplace plugin @@ -82,32 +86,29 @@ export default class CustomReplacePlugin implements EditorPlugin { * @param event PluginEvent object */ public onPluginEvent(event: PluginEvent) { - if (event.eventType != PluginEventType.Input || this.editor.isInIME()) { + if (event.eventType != PluginEventType.Input || !this.editor || this.editor.isInIME()) { return; } // Exit early on input events that do not insert a replacement's final character. - if (!event.rawEvent.data || !this.replacementEndCharacters.has(event.rawEvent.data)) { + if (!event.rawEvent.data || !this.replacementEndCharacters?.has(event.rawEvent.data)) { return; } // Get the matching replacement - const range = this.editor.getSelectionRange(); - if (range == null) { + const searcher = this.editor.getContentSearcherOfCursor(event); + if (!searcher || this.longestReplacementLength == null) { return; } - - const searcher = this.editor.getContentSearcherOfCursor(event); const stringToSearch = searcher.getSubStringBefore(this.longestReplacementLength); const replacement = this.getMatchingReplacement(stringToSearch); - if (replacement == null) { - return; - } if ( - replacement.shouldReplace && - !replacement.shouldReplace(replacement, searcher.getWordBefore()) + !replacement || + (replacement.shouldReplace && + searcher && + !replacement.shouldReplace(replacement, searcher.getWordBefore(), this.editor)) ) { return; } @@ -125,19 +126,21 @@ export default class CustomReplacePlugin implements EditorPlugin { parsingSpan.childNodes.length == 1 ? parsingSpan.childNodes[0] : parsingSpan; // Switch the node for the selection range - this.editor.addUndoSnapshot( - () => { - matchingRange.deleteContents(); - matchingRange.insertNode(nodeToInsert); - this.editor.select(nodeToInsert, PositionType.End); - }, - null /*changeSource*/, - true /*canUndoByBackspace*/ - ); + if (matchingRange) { + this.editor.addUndoSnapshot( + () => { + matchingRange.deleteContents(); + matchingRange.insertNode(nodeToInsert); + this.editor?.select(nodeToInsert, PositionType.End); + }, + undefined /*changeSource*/, + true /*canUndoByBackspace*/ + ); + } } private getMatchingReplacement(stringToSearch: string): CustomReplacement | null { - if (stringToSearch.length == 0) { + if (stringToSearch.length == 0 || !this.replacements) { return null; } const originalStringToSearch = stringToSearch.replace(/\s/g, ' '); diff --git a/packages/roosterjs-editor-plugins/lib/plugins/CutPasteListChain/CutPasteListChain.ts b/packages/roosterjs-editor-plugins/lib/plugins/CutPasteListChain/CutPasteListChain.ts index ef26d069d918..a32d493b1eec 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/CutPasteListChain/CutPasteListChain.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/CutPasteListChain/CutPasteListChain.ts @@ -1,4 +1,4 @@ -import { experimentCommitListChains } from 'roosterjs-editor-api'; +import { commitListChains } from 'roosterjs-editor-api'; import { VListChain } from 'roosterjs-editor-dom'; import { ChangeSource, @@ -7,15 +7,16 @@ import { PluginEvent, PluginEventType, } from 'roosterjs-editor-types'; +import type { CompatibleChangeSource } from 'roosterjs-editor-types/lib/compatibleTypes'; /** * Maintain list numbers of list chain when content is modified by cut/paste/drag&drop */ export default class CutPasteListChain implements EditorPlugin { - private chains: VListChain[]; - private expectedChangeSource: ChangeSource; - private editor: IEditor; - private disposer: () => void; + private chains: VListChain[] | null = null; + private expectedChangeSource: ChangeSource | CompatibleChangeSource | null = null; + private editor: IEditor | null = null; + private disposer: (() => void) | null = null; /** * Get a friendly name of this plugin @@ -59,8 +60,13 @@ export default class CutPasteListChain implements EditorPlugin { break; case PluginEventType.ContentChanged: - if (this.chains?.length > 0 && this.expectedChangeSource == event.source) { - experimentCommitListChains(this.editor, this.chains); + if ( + this.chains && + this.chains.length > 0 && + this.expectedChangeSource == event.source && + this.editor + ) { + commitListChains(this.editor, this.chains); this.chains = null; this.expectedChangeSource = null; } @@ -73,7 +79,10 @@ export default class CutPasteListChain implements EditorPlugin { }; private cacheListChains(source: ChangeSource) { - this.chains = VListChain.createListChains(this.editor.getSelectedRegions()); - this.expectedChangeSource = source; + const selectedRegions = this.editor?.getSelectedRegions(); + if (selectedRegions) { + this.chains = VListChain.createListChains(selectedRegions); + this.expectedChangeSource = source; + } } } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/HyperLink/HyperLink.ts b/packages/roosterjs-editor-plugins/lib/plugins/HyperLink/HyperLink.ts index 9ce37b1fd1aa..77aad119c11b 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/HyperLink/HyperLink.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/HyperLink/HyperLink.ts @@ -1,14 +1,22 @@ -import { Browser, isCharacterValue, isCtrlOrMetaPressed, matchLink } from 'roosterjs-editor-dom'; -import { EditorPlugin, IEditor, Keys, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; +import { isCharacterValue, isCtrlOrMetaPressed, matchLink } from 'roosterjs-editor-dom'; +import { + ChangeSource, + DOMEventHandler, + EditorPlugin, + IEditor, + Keys, + PluginEvent, + PluginEventType, +} from 'roosterjs-editor-types'; /** * An editor plugin that show a tooltip for existing link */ export default class HyperLink implements EditorPlugin { - private originalHref: string; - private trackedLink: HTMLAnchorElement = null; - private editor: IEditor; - private disposer: () => void; + private originalHref: string | null = null; + private trackedLink: HTMLAnchorElement | null = null; + private editor: IEditor | null = null; + private disposer: (() => void) | null = null; /** * Create a new instance of HyperLink class @@ -36,21 +44,22 @@ export default class HyperLink implements EditorPlugin { */ public initialize(editor: IEditor): void { this.editor = editor; - this.disposer = - this.getTooltipCallback && - editor.addDomEventHandler({ - mouseover: this.onMouse, - mouseout: this.onMouse, - blur: this.onBlur, - }); + this.disposer = editor.addDomEventHandler({ + mouseover: this.onMouse, + mouseout: this.onMouse, + blur: this.onBlur, + }); } protected onMouse = (e: MouseEvent) => { - const a = this.editor.getElementAtCursor('a[href]', e.target) as HTMLAnchorElement; - const href = this.tryGetHref(a); + const a = this.editor?.getElementAtCursor( + 'a[href]', + e.target + ) as HTMLAnchorElement | null; + const href = a && this.tryGetHref(a); if (href) { - this.editor.setEditorDomAttribute( + this.editor?.setEditorDomAttribute( 'title', e.type == 'mouseover' ? this.getTooltipCallback(href, a) : null ); @@ -87,17 +96,29 @@ export default class HyperLink implements EditorPlugin { (!this.isContentEditValue(event.rawEvent) || event.rawEvent.which == Keys.SPACE)) || event.eventType == PluginEventType.ContentChanged ) { - const anchor = this.editor.getElementAtCursor( + const anchor = this.editor?.getElementAtCursor( 'A[href]', - null /*startFrom*/, + undefined /*startFrom*/, event - ) as HTMLAnchorElement; + ) as HTMLAnchorElement | null; const shouldCheckUpdateLink = - anchor !== this.trackedLink || + (anchor && anchor !== this.trackedLink) || event.eventType == PluginEventType.KeyUp || event.eventType == PluginEventType.ContentChanged; + if ( + event.eventType == PluginEventType.ContentChanged && + event.source == ChangeSource.Keyboard && + this.trackedLink != anchor && + anchor + ) { + // For Keyboard event that causes content change (mostly come from Content Model), this tracked list may be staled. + // So we need to get an up-to-date link element + // TODO: This is a temporary solution. Later when Content Model can fully take over this behavior, we can remove this code. + this.trackedLink = anchor; + } + if ( this.trackedLink && (shouldCheckUpdateLink || this.tryGetHref(this.trackedLink) !== this.originalHref) @@ -114,34 +135,34 @@ export default class HyperLink implements EditorPlugin { } // Cache link and href value if its href attribute currently matches its display text - if (!this.trackedLink && this.doesLinkDisplayMatchHref(anchor)) { + if (!this.trackedLink && anchor && this.doesLinkDisplayMatchHref(anchor)) { this.trackedLink = anchor; this.originalHref = this.tryGetHref(anchor); } } if (event.eventType == PluginEventType.MouseUp) { - const anchor = this.editor.getElementAtCursor( + const anchor = this.editor?.getElementAtCursor( 'A', event.rawEvent.srcElement - ) as HTMLAnchorElement; + ) as HTMLAnchorElement | null; if (anchor) { if (this.onLinkClick && this.onLinkClick(anchor, event.rawEvent) !== false) { return; } - let href: string; + let href: string | null; if ( - !Browser.isFirefox && (href = this.tryGetHref(anchor)) && isCtrlOrMetaPressed(event.rawEvent) && event.rawEvent.button === 0 ) { + event.rawEvent.preventDefault(); try { const target = this.target || '_blank'; - const window = this.editor.getDocument().defaultView; - window.open(href, target); + const window = this.editor?.getDocument().defaultView; + window?.open(href, target); } catch {} } } @@ -153,10 +174,12 @@ export default class HyperLink implements EditorPlugin { * The reason this is put in a try-catch is that * it has been seen that accessing href may throw an exception, in particular on IE/Edge */ - private tryGetHref(anchor: HTMLAnchorElement): string { + private tryGetHref(anchor: HTMLAnchorElement): string | null { try { return anchor ? anchor.href : null; - } catch {} + } catch { + return null; + } } /** @@ -172,7 +195,7 @@ export default class HyperLink implements EditorPlugin { * Updates the href of the tracked link if the display text doesn't match href anymore */ private updateLinkHrefIfShouldUpdate() { - if (!this.doesLinkDisplayMatchHref(this.trackedLink)) { + if (this.trackedLink && !this.doesLinkDisplayMatchHref(this.trackedLink)) { this.updateLinkHref(); } } @@ -212,8 +235,8 @@ export default class HyperLink implements EditorPlugin { if (this.trackedLink) { let linkData = matchLink(this.trackedLink.innerText.trim()); if (linkData !== null) { - this.editor.addUndoSnapshot(() => { - this.trackedLink.href = linkData.normalizedUrl; + this.editor?.addUndoSnapshot(() => { + this.trackedLink!.href = linkData!.normalizedUrl; }); } } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts index 33b00a4900f9..26e4f2bb91ff 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts @@ -1,6 +1,6 @@ import applyChange from './editInfoUtils/applyChange'; import canRegenerateImage from './api/canRegenerateImage'; -import DragAndDropContext, { X, Y } from './types/DragAndDropContext'; +import DragAndDropContext, { DNDDirectionX, DnDDirectionY } from './types/DragAndDropContext'; import DragAndDropHandler from '../../pluginUtils/DragAndDropHandler'; import DragAndDropHelper from '../../pluginUtils/DragAndDropHelper'; import getGeneratedImageSize from './editInfoUtils/getGeneratedImageSize'; @@ -10,17 +10,15 @@ import { Cropper, getCropHTML } from './imageEditors/Cropper'; import { deleteEditInfo, getEditInfoFromImage } from './editInfoUtils/editInfo'; import { getRotateHTML, Rotator, updateRotateHandlePosition } from './imageEditors/Rotator'; import { ImageEditElementClass } from './types/ImageEditElementClass'; -import { insertEntity } from 'roosterjs-editor-api'; import { arrayPush, Browser, createElement, getComputedStyle, - getEntityFromElement, - getEntitySelector, - matchesSelector, + getObjectKeys, safeInstanceOf, toArray, + unwrap, wrap, } from 'roosterjs-editor-dom'; import { @@ -28,41 +26,29 @@ import { doubleCheckResize, getSideResizeHTML, getCornerResizeHTML, + OnShowResizeHandle, + getResizeBordersHTML, } from './imageEditors/Resizer'; import { - ExperimentalFeatures, ImageEditOperation, ImageEditOptions, - ChangeSource, EditorPlugin, IEditor, PluginEvent, PluginEventType, - EntityOperation, - Entity, - Keys, - PositionType, CreateElementData, KnownCreateElementDataIndex, + ModeIndependentColor, + SelectionRangeTypes, + ChangeSource, } from 'roosterjs-editor-types'; +import type { CompatibleImageEditOperation } from 'roosterjs-editor-types/lib/compatibleTypes'; -const SHIFT_KEYCODE = 16; -const CTRL_KEYCODE = 17; -const ALT_KEYCODE = 18; - +const PI = Math.PI; const DIRECTIONS = 8; -const DirectionRad = (Math.PI * 2) / DIRECTIONS; +const DirectionRad = (PI * 2) / DIRECTIONS; const DirectionOrder = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w']; -/** - * Map the experimental features to image edit operations to help determine which operation is allowed - */ -const FeatureToOperationMap = { - [ExperimentalFeatures.SingleDirectionResize]: ImageEditOperation.SideResize, - [ExperimentalFeatures.ImageRotate]: ImageEditOperation.Rotate, - [ExperimentalFeatures.ImageCrop]: ImageEditOperation.Crop, -}; - /** * Default image edit options */ @@ -73,7 +59,11 @@ const DefaultOptions: Required = { preserveRatio: false, minRotateDeg: 5, imageSelector: 'img', - rotateIconHTML: null, + rotateIconHTML: '', + disableCrop: false, + disableRotate: false, + disableSideResize: false, + onSelectState: ImageEditOperation.ResizeAndRotate, }; /** @@ -87,56 +77,81 @@ const ImageEditHTMLMap = { [ImageEditOperation.Crop]: getCropHTML, }; -/** - * Image edit entity name - */ -const IMAGE_EDIT_WRAPPER_ENTITY_TYPE = 'IMAGE_EDIT_WRAPPER'; - /** * Default background colors for rotate handle */ const LIGHT_MODE_BGCOLOR = 'white'; const DARK_MODE_BGCOLOR = '#333'; +/** + * The biggest area of image with 4 handles + */ +const MAX_SMALL_SIZE_IMAGE = 10000; + /** * ImageEdit plugin provides the ability to edit an inline image in editor, including image resizing, rotation and cropping */ export default class ImageEdit implements EditorPlugin { - protected editor: IEditor; + protected editor: IEditor | null = null; protected options: ImageEditOptions; - private disposer: () => void; + private disposer: (() => void) | null = null; // Allowed editing operations - private allowedOperations: ImageEditOperation = ImageEditOperation.CornerResize; + private allowedOperations: ImageEditOperation; // Current editing image - private image: HTMLImageElement; + private image: HTMLImageElement | null = null; + + // Image cloned from the current editing image + private clonedImage: HTMLImageElement | null = null; + + // The image wrapper + private wrapper: HTMLSpanElement | null = null; // Current edit info of the image. All changes user made will be stored in this object. // We use this object to update the editing UI, and finally we will use this object to generate // the new image if necessary - private editInfo: ImageEditInfo; + private editInfo: ImageEditInfo | null = null; // Src of the image before current editing - private lastSrc: string; + private lastSrc: string | null = null; // Drag and drop helper objects - private dndHelpers: DragAndDropHelper[]; + private dndHelpers: DragAndDropHelper[] = []; /** * Identify if the image was resized by the user. */ - private wasResized: boolean; + private wasResized: boolean = false; + + /** + * The span element that wraps the image and opens shadow dom + */ + private shadowSpan: HTMLSpanElement | null = null; + + /** + * The span element that wraps the image and opens shadow dom + */ + private isCropping: boolean = false; /** * Create a new instance of ImageEdit * @param options Image editing options + * @param onShowResizeHandle An optional callback to allow customize resize handle element of image resizing. + * To customize the resize handle element, add this callback and change the attributes of elementData then it + * will be picked up by ImageEdit code */ - constructor(options?: ImageEditOptions) { + constructor(options?: ImageEditOptions, private onShowResizeHandle?: OnShowResizeHandle) { this.options = { ...DefaultOptions, ...(options || {}), }; + + this.allowedOperations = + ImageEditOperation.CornerResize | + (this.options.disableCrop ? 0 : ImageEditOperation.Crop) | + (this.options.disableRotate ? 0 : ImageEditOperation.Rotate) | + (this.options.disableSideResize ? 0 : ImageEditOperation.SideResize); } /** @@ -152,13 +167,13 @@ export default class ImageEdit implements EditorPlugin { */ initialize(editor: IEditor) { this.editor = editor; - this.disposer = editor.addDomEventHandler('blur', this.onBlur); - - // Read current enabled features from editor to determine which editing operations are allowed - Object.keys(FeatureToOperationMap).forEach((key: keyof typeof FeatureToOperationMap) => { - this.allowedOperations |= this.editor.isFeatureEnabled(key) - ? FeatureToOperationMap[key] - : 0; + this.disposer = editor.addDomEventHandler({ + blur: () => this.onBlur(), + dragstart: e => { + if (this.image) { + e.preventDefault(); + } + }, }); } @@ -167,92 +182,83 @@ export default class ImageEdit implements EditorPlugin { */ dispose() { this.clearDndHelpers(); - this.disposer(); + this.disposer?.(); this.disposer = null; this.editor = null; } /** * Handle events triggered from editor - * @param event PluginEvent object + * @param e PluginEvent object */ onPluginEvent(e: PluginEvent) { switch (e.eventType) { - case PluginEventType.MouseDown: - this.setEditingImage(null); - break; - - case PluginEventType.MouseUp: - const target = e.rawEvent.target; + case PluginEventType.SelectionChanged: if ( - e.isClicking && - e.rawEvent.button == 0 && - safeInstanceOf(target, 'HTMLImageElement') && - target.isContentEditable && - matchesSelector(target, this.options.imageSelector) + e.selectionRangeEx && + e.selectionRangeEx.type === SelectionRangeTypes.ImageSelection && + this.options && + this.options.onSelectState !== undefined ) { - this.setEditingImage(target, ImageEditOperation.ResizeAndRotate); + this.setEditingImage(e.selectionRangeEx.image, this.options.onSelectState); } break; - - case PluginEventType.KeyDown: - const key = e.rawEvent.which; - if (key == Keys.DELETE || key == Keys.BACKSPACE) { - // Set current editing image to null and select the image if any, and do not prevent default of the event - // so that browser will delete the selected image for us - this.setEditingImage(null, true /*selectImage*/); - } else if (key == Keys.ESCAPE && this.image) { - // Press ESC should cancel current editing operation, resume back to original edit info - this.editInfo = getEditInfoFromImage(this.image); - this.setEditingImage(null); - e.rawEvent.preventDefault(); - } else if (key != SHIFT_KEYCODE && key != CTRL_KEYCODE && key != ALT_KEYCODE) { - // For other key, just unselect current image and select it. If this is an input key, the image will be replaced - this.setEditingImage(null, true /*selectImage*/); - } - break; - - case PluginEventType.ContentChanged: + case PluginEventType.MouseDown: + // When left click in a image that already in editing mode, do not quit edit mode + const mouseTarget = e.rawEvent.target; + const button = e.rawEvent.button; if ( - e.source != ChangeSource.InsertEntity || - (e.data).type != IMAGE_EDIT_WRAPPER_ENTITY_TYPE + this.shadowSpan !== mouseTarget || + (this.shadowSpan === mouseTarget && button !== 0) || + this.isCropping ) { - // After contentChanged event, the current image wrapper may not be valid any more, remove all of them if any - this.editor.queryElements( - getEntitySelector(IMAGE_EDIT_WRAPPER_ENTITY_TYPE), - this.removeWrapper - ); + this.setEditingImage(null); } - break; - - case PluginEventType.EntityOperation: - if (e.entity.type == IMAGE_EDIT_WRAPPER_ENTITY_TYPE) { - if (e.operation == EntityOperation.ReplaceTemporaryContent) { - this.removeWrapper(e.entity.wrapper); - } else if (e.operation == EntityOperation.Click) { - e.rawEvent.preventDefault(); - } - } + case PluginEventType.KeyDown: + this.setEditingImage(null); + break; + case PluginEventType.ContentChanged: + //After contentChanged event, the current image wrapper may not be valid any more, remove all of them if any + this.removeWrapper(); break; case PluginEventType.ExtractContentWithDom: // When extract content, remove all image info since they may not be valid when load the content again - toArray(e.clonedRoot.querySelectorAll(this.options.imageSelector)).forEach(img => { - deleteEditInfo(img as HTMLImageElement); - }); + if (this.options?.imageSelector) { + toArray(e.clonedRoot.querySelectorAll(this.options.imageSelector)).forEach( + img => { + deleteEditInfo(img as HTMLImageElement); + } + ); + } + break; + case PluginEventType.BeforeDispose: + this.removeWrapper(); break; } } + /** + * Check if the given image edit operation is allowed by this plugin + * @param operation The image edit operation to check + * @returns True means it is allowed, otherwise false + */ + isOperationAllowed(operation: ImageEditOperation): boolean { + return !!(this.allowedOperations & operation); + } + /** * Set current image for edit. If there is already image in editing, it will quit editing mode and any pending editing * operation will be submitted * @param image The image to edit * @param operation The editing operation */ - setEditingImage(image: HTMLImageElement, operation: ImageEditOperation): void; + setEditingImage( + image: HTMLImageElement, + operation: ImageEditOperation | CompatibleImageEditOperation + ): void; /** * Stop editing image. If there is already image in editing, it will quit editing mode and any pending editing @@ -264,28 +270,37 @@ export default class ImageEdit implements EditorPlugin { setEditingImage( image: HTMLImageElement | null, - operationOrSelect?: ImageEditOperation | boolean + operationOrSelect?: ImageEditOperation | CompatibleImageEditOperation | boolean ) { let operation = typeof operationOrSelect === 'number' ? operationOrSelect : ImageEditOperation.None; const selectImage = typeof operationOrSelect === 'number' ? false : !!operationOrSelect; - if (this.image) { + if ( + !image && + this.image && + this.editor && + this.editInfo && + this.lastSrc && + this.clonedImage + ) { // When there is image in editing, clean up any cached objects and elements this.clearDndHelpers(); // Apply the changes, and add undo snapshot if necessary - if ( - applyChange(this.editor, this.image, this.editInfo, this.lastSrc, this.wasResized) - ) { - this.editor.addUndoSnapshot(() => this.image, ChangeSource.ImageResize); - } + applyChange( + this.editor, + this.image, + this.editInfo, + this.lastSrc, + this.wasResized, + this.clonedImage + ); // Remove editing wrapper - const wrapper = this.getImageWrapper(this.image); - if (wrapper) { - this.removeWrapper(wrapper); - } + this.removeWrapper(); + + this.editor.addUndoSnapshot(() => this.image, ChangeSource.ImageResize); if (selectImage) { this.editor.select(this.image); @@ -294,9 +309,11 @@ export default class ImageEdit implements EditorPlugin { this.image = null; this.editInfo = null; this.lastSrc = null; + this.clonedImage = null; + this.isCropping = false; } - if (!this.image && image?.isContentEditable) { + if (!this.image && image?.isContentEditable && this.editor) { // If there is new image to edit, enter editing mode for this image this.editor.addUndoSnapshot(); this.image = image; @@ -312,7 +329,7 @@ export default class ImageEdit implements EditorPlugin { this.allowedOperations; // Create and update editing wrapper and elements - const wrapper = this.createWrapper(operation); + this.createWrapper(operation); this.updateWrapper(); // Init drag and drop @@ -323,98 +340,142 @@ export default class ImageEdit implements EditorPlugin { ...this.createDndHelpers(ImageEditElementClass.CropContainer, Cropper), ]; - // Put cursor next to the image - this.editor.select(wrapper, PositionType.After); + this.editor.select(this.image); + } + } + + /** + * Flip the image. + * @param image The image to be flipped + * @param direction + */ + public flipImage(image: HTMLImageElement, direction: 'vertical' | 'horizontal') { + this.image = image; + this.editInfo = getEditInfoFromImage(image); + const { angleRad } = this.editInfo; + const isInVerticalPostion = + (angleRad >= Math.PI / 2 && angleRad < (3 * Math.PI) / 4) || + (angleRad <= -Math.PI / 2 && angleRad > (-3 * Math.PI) / 4); + if (isInVerticalPostion) { + if (direction === 'horizontal') { + this.editInfo.flippedVertical = !this.editInfo.flippedVertical; + } else { + this.editInfo.flippedHorizontal = !this.editInfo.flippedHorizontal; + } + } else { + if (direction === 'vertical') { + this.editInfo.flippedVertical = !this.editInfo.flippedVertical; + } else { + this.editInfo.flippedHorizontal = !this.editInfo.flippedHorizontal; + } } + this.createWrapper(ImageEditOperation.Rotate); + this.updateWrapper(); + this.setEditingImage(null); + this.editor?.select(image); + } + + /** + * Rotate the image in radian angle. + * @param image The image to be rotated + * @param angleRad The angle in radian that the image must be rotated. + */ + public rotateImage(image: HTMLImageElement, angleRad: number) { + this.image = image; + this.editInfo = getEditInfoFromImage(image); + this.editInfo.angleRad = this.editInfo.angleRad + angleRad; + this.createWrapper(ImageEditOperation.Rotate); + this.updateWrapper(); + this.setEditingImage(null); + this.editor?.select(image); } /** * quit editing mode when editor lose focus */ private onBlur = () => { - this.setEditingImage(null); + this.setEditingImage(null, false /* selectImage */); }; - /** * Create editing wrapper for the image */ - private createWrapper(operation: ImageEditOperation) { - // Wrap the image with an entity so that we can easily retrieve it later - const { wrapper } = insertEntity( - this.editor, - IMAGE_EDIT_WRAPPER_ENTITY_TYPE, - wrap(this.image, KnownCreateElementDataIndex.ImageEditWrapper), - false /*isBlock*/, - true /*isReadonly*/ - ); - - wrapper.style.position = 'relative'; - wrapper.style.maxWidth = '100%'; - wrapper.style.verticalAlign = 'bottom'; - wrapper.style.display = Browser.isSafari ? 'inline-block' : 'inline-flex'; - - // Cache current src so that we can compare it after edit see if src is changed - this.lastSrc = this.image.getAttribute('src'); - - // Set image src to original src to help show editing UI, also it will be used when regenerate image dataURL after editing - this.image.src = this.editInfo.src; - this.image.style.position = 'absolute'; - this.image.style.maxWidth = null; - - // Get HTML for all edit elements (resize handle, rotate handle, crop handle and overlay, ...) and create HTML element - const options: ImageHtmlOptions = { - borderColor: this.options.borderColor, - rotateIconHTML: this.options.rotateIconHTML, - rotateHandleBackColor: this.editor.isDarkMode() - ? DARK_MODE_BGCOLOR - : LIGHT_MODE_BGCOLOR, - }; - const htmlData: CreateElementData[] = []; + private createWrapper(operation: ImageEditOperation | CompatibleImageEditOperation) { + if (this.image && this.editor && this.options && this.editInfo) { + //Clone the image and insert the clone in a entity + this.clonedImage = this.image.cloneNode(true) as HTMLImageElement; + this.clonedImage.removeAttribute('id'); + this.clonedImage.style.removeProperty('max-width'); + this.wrapper = createElement( + KnownCreateElementDataIndex.ImageEditWrapper, + this.image.ownerDocument + ) as HTMLSpanElement; + this.wrapper?.firstChild?.appendChild(this.clonedImage); + this.wrapper.style.display = Browser.isSafari ? 'inline-block' : 'inline-flex'; + + // Cache current src so that we can compare it after edit see if src is changed + this.lastSrc = this.image.getAttribute('src'); + + // Set image src to original src to help show editing UI, also it will be used when regenerate image dataURL after editing + if (this.clonedImage) { + this.clonedImage.src = this.editInfo.src; + setFlipped( + this.clonedImage, + this.editInfo.flippedHorizontal, + this.editInfo.flippedVertical + ); + this.clonedImage.style.position = 'absolute'; + } - ((Object.keys(ImageEditHTMLMap) as any[]) as (keyof typeof ImageEditHTMLMap)[]).forEach( - thisOperation => { - if ((operation & thisOperation) == thisOperation) { - arrayPush(htmlData, ImageEditHTMLMap[thisOperation](options)); + // Get HTML for all edit elements (resize handle, rotate handle, crop handle and overlay, ...) and create HTML element + const options: ImageHtmlOptions = { + borderColor: getColorString(this.options.borderColor!, this.editor.isDarkMode()), + rotateIconHTML: this.options.rotateIconHTML!, + rotateHandleBackColor: this.editor.isDarkMode() + ? DARK_MODE_BGCOLOR + : LIGHT_MODE_BGCOLOR, + isSmallImage: isASmallImage(this.editInfo!), + }; + const htmlData: CreateElementData[] = [getResizeBordersHTML(options)]; + + getObjectKeys(ImageEditHTMLMap).forEach(thisOperation => { + const element = ImageEditHTMLMap[thisOperation](options, this.onShowResizeHandle); + if ((operation & thisOperation) == thisOperation && element) { + arrayPush(htmlData, element); } - } - ); + }); - htmlData.forEach(data => { - const element = createElement(data, this.image.ownerDocument); - if (element) { - wrapper.appendChild(element); - } - }); - return wrapper; + htmlData.forEach(data => { + const element = createElement(data, this.image!.ownerDocument); + if (element && this.wrapper) { + this.wrapper.appendChild(element); + } + }); + this.insertImageWrapper(this.wrapper); + } } - /** - * Get image wrapper from image - * @param image The image to get wrapper from - */ - private getImageWrapper(image: HTMLImageElement): HTMLElement { - // Get the image wrapper from image using Entity API - const entity = getEntityFromElement(image?.parentNode?.parentNode as HTMLElement); + private insertImageWrapper(wrapper: HTMLSpanElement) { + if (this.image) { + this.shadowSpan = wrap(this.image, 'span'); + const shadowRoot = this.shadowSpan.attachShadow({ + mode: 'open', + }); - return entity?.type == IMAGE_EDIT_WRAPPER_ENTITY_TYPE ? entity.wrapper : null; + this.shadowSpan.style.verticalAlign = 'bottom'; + + shadowRoot.appendChild(wrapper); + } } /** * Remove the temp wrapper of the image - * @param wrapper The wrapper object to remove. If not specified, remove all existing wrappers. */ - private removeWrapper = (wrapper: HTMLElement) => { - const parent = wrapper?.parentNode; - const img = wrapper?.querySelector('img'); - - if (img && parent) { - img.style.position = ''; - img.style.margin = null; - img.style.textAlign = null; - - parent.insertBefore(img, wrapper); - parent.removeChild(wrapper); + private removeWrapper = () => { + if (this.shadowSpan) { + unwrap(this.shadowSpan); } + this.wrapper = null; + this.shadowSpan = null; }; /** @@ -422,18 +483,25 @@ export default class ImageEdit implements EditorPlugin { * @param context */ private updateWrapper = (context?: DragAndDropContext) => { - const wrapper = this.getImageWrapper(this.image); - if (wrapper) { + const wrapper = this.wrapper; + if ( + wrapper && + this.editInfo && + this.image && + this.clonedImage && + this.options && + this.shadowSpan?.parentElement + ) { // Prepare: get related editing elements const cropContainers = getEditElements(wrapper, ImageEditElementClass.CropContainer); const cropOverlays = getEditElements(wrapper, ImageEditElementClass.CropOverlay); + const resizeHandles = getEditElements(wrapper, ImageEditElementClass.ResizeHandle); const rotateCenter = getEditElements(wrapper, ImageEditElementClass.RotateCenter)[0]; const rotateHandle = getEditElements(wrapper, ImageEditElementClass.RotateHandle)[0]; - const resizeHandles = getEditElements(wrapper, ImageEditElementClass.ResizeHandle); const cropHandles = getEditElements(wrapper, ImageEditElementClass.CropHandle); // Cropping and resizing will show different UI, so check if it is cropping here first - const isCropping = cropContainers.length == 1 && cropOverlays.length == 4; + this.isCropping = cropContainers.length == 1 && cropOverlays.length == 4; const { angleRad, bottomPercent, @@ -450,7 +518,8 @@ export default class ImageEdit implements EditorPlugin { originalHeight, visibleWidth, visibleHeight, - } = getGeneratedImageSize(this.editInfo, isCropping); + } = getGeneratedImageSize(this.editInfo, this.isCropping); + const marginHorizontal = (targetWidth - visibleWidth) / 2; const marginVertical = (targetHeight - visibleHeight) / 2; const cropLeftPx = originalWidth * leftPercent; @@ -459,20 +528,19 @@ export default class ImageEdit implements EditorPlugin { const cropBottomPx = originalHeight * bottomPercent; // Update size and margin of the wrapper - wrapper.style.width = getPx(visibleWidth); - wrapper.style.height = getPx(visibleHeight); wrapper.style.margin = `${marginVertical}px ${marginHorizontal}px`; wrapper.style.transform = `rotate(${angleRad}rad)`; + setWrapperSizeDimensions(wrapper, this.image, visibleWidth, visibleHeight); // Update the text-alignment to avoid the image to overflow if the parent element have align center or right // or if the direction is Right To Left - wrapper.style.textAlign = isRtl(wrapper.parentNode) ? 'right' : 'left'; + wrapper.style.textAlign = isRtl(this.shadowSpan.parentElement) ? 'right' : 'left'; // Update size of the image - this.image.style.width = getPx(originalWidth); - this.image.style.height = getPx(originalHeight); + this.clonedImage.style.width = getPx(originalWidth); + this.clonedImage.style.height = getPx(originalHeight); - if (isCropping) { + if (this.isCropping) { // For crop, we also need to set position of the overlays setSize( cropContainers[0], @@ -487,10 +555,11 @@ export default class ImageEdit implements EditorPlugin { setSize(cropOverlays[1], undefined, 0, 0, cropBottomPx, cropRightPx, undefined); setSize(cropOverlays[2], cropLeftPx, undefined, 0, 0, undefined, cropBottomPx); setSize(cropOverlays[3], 0, cropTopPx, undefined, 0, cropLeftPx, undefined); + updateHandleCursor(cropHandles, angleRad); } else { // For rotate/resize, set the margin of the image so that cropped part won't be visible - this.image.style.margin = `${-cropTopPx}px 0 0 ${-cropLeftPx}px`; + this.clonedImage.style.margin = `${-cropTopPx}px 0 0 ${-cropLeftPx}px`; // Double check resize if (context?.elementClass == ImageEditElementClass.ResizeHandle) { @@ -499,7 +568,7 @@ export default class ImageEdit implements EditorPlugin { this.wasResized = true; doubleCheckResize( this.editInfo, - this.options.preserveRatio, + this.options.preserveRatio || false, clientWidth, clientHeight ); @@ -507,13 +576,10 @@ export default class ImageEdit implements EditorPlugin { this.updateWrapper(); } - updateRotateHandlePosition( - this.editInfo, - this.editor.getRelativeDistanceToEditor(wrapper, true /*addScroll*/), - marginVertical, - rotateCenter, - rotateHandle - ); + const viewport = this.editor?.getVisibleViewport(); + if (rotateHandle && rotateCenter && viewport) { + updateRotateHandlePosition(viewport, rotateCenter, rotateHandle); + } updateHandleCursor(resizeHandles, angleRad); } @@ -530,25 +596,22 @@ export default class ImageEdit implements EditorPlugin { elementClass: ImageEditElementClass, dragAndDrop: DragAndDropHandler ): DragAndDropHelper[] { - const commonContext = { - editInfo: this.editInfo, - options: this.options, - elementClass, - }; - const wrapper = this.getImageWrapper(this.image); - return wrapper + const wrapper = this.wrapper; + return wrapper && this.editInfo ? getEditElements(wrapper, elementClass).map( element => new DragAndDropHelper( element, { - ...commonContext, - x: element.dataset.x as X, - y: element.dataset.y as Y, + editInfo: this.editInfo!, + options: this.options, + elementClass, + x: element.dataset.x as DNDDirectionX, + y: element.dataset.y as DnDDirectionY, }, this.updateWrapper, dragAndDrop, - this.editor.getSizeTransformer() + this.editor ? this.editor.getZoomScale() : 1 ) ) : []; @@ -559,29 +622,46 @@ export default class ImageEdit implements EditorPlugin { */ private clearDndHelpers() { this.dndHelpers?.forEach(helper => helper.dispose()); - this.dndHelpers = null; + this.dndHelpers = []; } } function setSize( element: HTMLElement, - left: number, - top: number, - right: number, - bottom: number, + left: number | undefined, + top: number | undefined, + right: number | undefined, + bottom: number | undefined, + width: number | undefined, + height: number | undefined +) { + element.style.left = left !== undefined ? getPx(left) : element.style.left; + element.style.top = top !== undefined ? getPx(top) : element.style.top; + element.style.right = right !== undefined ? getPx(right) : element.style.right; + element.style.bottom = bottom !== undefined ? getPx(bottom) : element.style.bottom; + element.style.width = width !== undefined ? getPx(width) : element.style.width; + element.style.height = height !== undefined ? getPx(height) : element.style.height; +} + +function setWrapperSizeDimensions( + wrapper: HTMLElement, + image: HTMLImageElement, width: number, height: number ) { - element.style.left = getPx(left); - element.style.top = getPx(top); - element.style.right = getPx(right); - element.style.bottom = getPx(bottom); - element.style.width = getPx(width); - element.style.height = getPx(height); + const hasBorder = image.style.borderStyle; + if (hasBorder) { + const borderWidth = image.style.borderWidth ? 2 * parseInt(image.style.borderWidth) : 2; + wrapper.style.width = getPx(width + borderWidth); + wrapper.style.height = getPx(height + borderWidth); + return; + } + wrapper.style.width = getPx(width); + wrapper.style.height = getPx(height); } function getPx(value: number): string { - return value === undefined ? null : value + 'px'; + return value + 'px'; } function getEditElements(wrapper: HTMLElement, elementClass: ImageEditElementClass): HTMLElement[] { @@ -599,12 +679,12 @@ function handleRadIndexCalculator(angleRad: number): number { return idx < 0 ? idx + DIRECTIONS : idx; } -function rotateHandles(element: HTMLElement, angleRad: number): string { +function rotateHandles(angleRad: number, y: string = '', x: string = ''): string { const radIndex = handleRadIndexCalculator(angleRad); - const originalDirection = element.dataset.y + element.dataset.x; + const originalDirection = y + x; const originalIndex = DirectionOrder.indexOf(originalDirection); const rotatedIndex = originalIndex >= 0 && originalIndex + radIndex; - return DirectionOrder[rotatedIndex % DIRECTIONS]; + return rotatedIndex ? DirectionOrder[rotatedIndex % DIRECTIONS] : ''; } /** @@ -614,7 +694,9 @@ function rotateHandles(element: HTMLElement, angleRad: number): string { */ function updateHandleCursor(handles: HTMLElement[], angleRad: number) { handles.map(handle => { - handle.style.cursor = `${rotateHandles(handle, angleRad)}-resize`; + const y = handle.dataset.y; + const x = handle.dataset.x; + handle.style.cursor = `${rotateHandles(angleRad, y, x)}-resize`; }); } @@ -644,3 +726,25 @@ function isFixedNumberValue(value: string | number) { const numberValue = typeof value === 'string' ? parseInt(value) : value; return !isNaN(numberValue); } + +function isASmallImage(editInfo: ImageEditInfo): boolean { + const { widthPx, heightPx } = editInfo; + return widthPx && heightPx && widthPx * widthPx < MAX_SMALL_SIZE_IMAGE ? true : false; +} + +function getColorString(color: string | ModeIndependentColor, isDarkMode: boolean): string { + if (typeof color === 'string') { + return color.trim(); + } + return isDarkMode ? color.darkModeColor.trim() : color.lightModeColor.trim(); +} + +function setFlipped( + element: HTMLImageElement, + flipppedHorizontally?: boolean, + flipppedVertically?: boolean +) { + element.style.transform = `scale(${flipppedHorizontally ? '-1' : '1'}, ${ + flipppedVertically ? '-1' : '1' + })`; +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/api/canRegenerateImage.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/api/canRegenerateImage.ts index 38aec6bbd10b..3a21939e2389 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/api/canRegenerateImage.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/api/canRegenerateImage.ts @@ -14,9 +14,13 @@ export default function canRegenerateImage(img: HTMLImageElement): boolean { canvas.width = 10; canvas.height = 10; const context = canvas.getContext('2d'); - context.drawImage(img, 0, 0); - context.getImageData(0, 0, 1, 1); - return true; + if (context) { + context.drawImage(img, 0, 0); + context.getImageData(0, 0, 1, 1); + return true; + } + + return false; } catch { return false; } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/api/isResizedTo.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/api/isResizedTo.ts index f0e8b363f0f0..3d4cf2ec9631 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/api/isResizedTo.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/api/isResizedTo.ts @@ -8,9 +8,12 @@ import { getEditInfoFromImage } from '../editInfoUtils/editInfo'; */ export default function isResizedTo(image: HTMLImageElement, percentage: number): boolean { const editInfo = getEditInfoFromImage(image); - const { width, height } = getTargetSizeByPercentage(editInfo, percentage); - return ( - Math.round(width) == Math.round(editInfo.widthPx) && - Math.round(height) == Math.round(editInfo.heightPx) - ); + if (editInfo) { + const { width, height } = getTargetSizeByPercentage(editInfo, percentage); + return ( + Math.round(width) == Math.round(editInfo.widthPx) && + Math.round(height) == Math.round(editInfo.heightPx) + ); + } + return false; } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/api/resizeByPercentage.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/api/resizeByPercentage.ts index 454da138b28b..b8927dff7624 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/api/resizeByPercentage.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/api/resizeByPercentage.ts @@ -23,16 +23,17 @@ export default function resizeByPercentage( const editInfo = getEditInfoFromImage(image); if (!isResizedTo(image, percentage)) { - loadImage(image, editInfo.src, () => { - if (!editor.isDisposed() && editor.contains(image)) { + loadImage(image, image.src, () => { + if (!editor.isDisposed() && editor.contains(image) && editInfo) { const lastSrc = image.getAttribute('src'); const { width, height } = getTargetSizeByPercentage(editInfo, percentage); editInfo.widthPx = Math.max(width, minWidth); editInfo.heightPx = Math.max(height, minHeight); editor.addUndoSnapshot(() => { - applyChange(editor, image, editInfo, lastSrc, true /*wasResized*/); + applyChange(editor, image, editInfo, lastSrc || '', true /*wasResized*/); }, ChangeSource.ImageResize); + editor.select(image); } }); } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/applyChange.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/applyChange.ts index 9b9a4ca1927d..1829c60677c5 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/applyChange.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/applyChange.ts @@ -12,18 +12,19 @@ import { IEditor, PluginEventType } from 'roosterjs-editor-types'; * @param image The image to apply the change * @param editInfo Edit info that contains the changed information of the image * @param previousSrc Last src value of the image before the change was made - * @returns True if the image is changed, otherwise false + * @param editingImage (optional) Image in editing state */ export default function applyChange( editor: IEditor, image: HTMLImageElement, editInfo: ImageEditInfo, previousSrc: string, - wasResized: boolean -): boolean { + wasResized: boolean, + editingImage?: HTMLImageElement +) { let newSrc = ''; - const initEditInfo = getEditInfoFromImage(image); + const initEditInfo = getEditInfoFromImage(editingImage ?? image); const state = checkEditInfoState(editInfo, initEditInfo); switch (state) { @@ -38,7 +39,7 @@ export default function applyChange( break; case ImageEditInfoState.FullyChanged: // For other cases (cropped, rotated, ...) we need to create a new image to reflect the change - newSrc = generateDataURL(image, editInfo); + newSrc = generateDataURL(editingImage ?? image, editInfo); break; } @@ -69,36 +70,11 @@ export default function applyChange( // Write back the change to image, and set its new size const { targetWidth, targetHeight } = getGeneratedImageSize(editInfo); image.src = newSrc; - setImageSize(image, wasResized, targetWidth, targetHeight); - return ( - srcChanged || - editInfo.widthPx != initEditInfo.widthPx || - editInfo.heightPx != initEditInfo.heightPx - ); -} - -/** - * @param img The current image. - * @param wasResized the current resize state of the image - */ -function setImageSize( - image: HTMLImageElement, - wasResized: boolean, - targetWidth: number, - targetHeight: number -) { - if (wasResized) { - image.style.maxWidth = 'initial'; + if (wasResized || state == ImageEditInfoState.FullyChanged) { image.width = targetWidth; image.height = targetHeight; image.style.width = targetWidth + 'px'; image.style.height = targetHeight + 'px'; - } else { - image.style.maxWidth = '100%'; - image.style.height = 'initial'; - image.style.width = 'initial'; - image.removeAttribute('height'); - image.removeAttribute('width'); } } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/checkEditInfoState.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/checkEditInfoState.ts index f0eade5859e5..68bf055b964d 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/checkEditInfoState.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/checkEditInfoState.ts @@ -61,13 +61,20 @@ export default function checkEditInfoState( ): ImageEditInfoState { if (!editInfo || !editInfo.src || ALL_KEYS.some(key => !isNumber(editInfo[key]))) { return ImageEditInfoState.Invalid; - } else if (ROTATE_CROP_KEYS.every(key => areSameNumber(editInfo[key], 0))) { + } else if ( + ROTATE_CROP_KEYS.every(key => areSameNumber(editInfo[key], 0)) && + !editInfo.flippedHorizontal && + !editInfo.flippedVertical && + (!compareTo || (compareTo && editInfo.angleRad === compareTo.angleRad)) + ) { return ImageEditInfoState.ResizeOnly; } else if ( compareTo && ROTATE_KEYS.every(key => areSameNumber(editInfo[key], 0)) && ROTATE_KEYS.every(key => areSameNumber(compareTo[key], 0)) && - CROP_KEYS.every(key => areSameNumber(editInfo[key], compareTo[key])) + CROP_KEYS.every(key => areSameNumber(editInfo[key], compareTo[key])) && + compareTo.flippedHorizontal === editInfo.flippedHorizontal && + compareTo.flippedVertical === editInfo.flippedVertical ) { return ImageEditInfoState.SameWithLast; } else { diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/editInfo.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/editInfo.ts index a194895b847b..f82fe54abcbb 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/editInfo.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/editInfo.ts @@ -1,7 +1,6 @@ import checkEditInfoState, { ImageEditInfoState } from './checkEditInfoState'; import ImageEditInfo from '../types/ImageEditInfo'; - -const IMAGE_EDIT_INFO_NAME = 'roosterEditInfo'; +import { getMetadata, removeMetadata, setMetadata } from 'roosterjs-editor-dom'; /** * @internal @@ -11,7 +10,7 @@ const IMAGE_EDIT_INFO_NAME = 'roosterEditInfo'; */ export function saveEditInfo(image: HTMLImageElement, editInfo: ImageEditInfo) { if (image) { - image.dataset[IMAGE_EDIT_INFO_NAME] = JSON.stringify(editInfo); + setMetadata(image, editInfo); } } @@ -22,7 +21,7 @@ export function saveEditInfo(image: HTMLImageElement, editInfo: ImageEditInfo) { */ export function deleteEditInfo(image: HTMLImageElement) { if (image) { - delete image.dataset[IMAGE_EDIT_INFO_NAME]; + removeMetadata(image); } } @@ -35,8 +34,10 @@ export function deleteEditInfo(image: HTMLImageElement) { * @param image The image to get edit info from */ export function getEditInfoFromImage(image: HTMLImageElement): ImageEditInfo { - const obj = safeParseJSON(image?.dataset[IMAGE_EDIT_INFO_NAME]) as ImageEditInfo; - return checkEditInfoState(obj) == ImageEditInfoState.Invalid ? getInitialEditInfo(image) : obj; + const obj = getMetadata(image); + return !obj || checkEditInfoState(obj) == ImageEditInfoState.Invalid + ? getInitialEditInfo(image) + : obj; } function getInitialEditInfo(image: HTMLImageElement): ImageEditInfo { @@ -53,11 +54,3 @@ function getInitialEditInfo(image: HTMLImageElement): ImageEditInfo { angleRad: 0, }; } - -function safeParseJSON(json: string): any { - try { - return JSON.parse(json); - } catch { - return null; - } -} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/generateDataURL.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/generateDataURL.ts index dc22a5a875c3..f48fdf95bab3 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/generateDataURL.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/generateDataURL.ts @@ -30,21 +30,23 @@ export default function generateDataURL(image: HTMLImageElement, editInfo: Image const { targetWidth, targetHeight } = getGeneratedImageSize(editInfo); canvas.width = targetWidth; canvas.height = targetHeight; - const context = canvas.getContext('2d'); - context.translate(targetWidth / 2, targetHeight / 2); - context.rotate(angle); - context.drawImage( - image, - naturalWidth * left, - naturalHeight * top, - imageWidth, - imageHeight, - -width / 2, - -height / 2, - width, - height - ); + if (context) { + context.translate(targetWidth / 2, targetHeight / 2); + context.rotate(angle); + context.scale(editInfo.flippedHorizontal ? -1 : 1, editInfo.flippedVertical ? -1 : 1); + context.drawImage( + image, + naturalWidth * left, + naturalHeight * top, + imageWidth, + imageHeight, + -width / 2, + -height / 2, + width, + height + ); + } return canvas.toDataURL('image/png', 1.0); } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/getLastZIndex.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/getLastZIndex.ts new file mode 100644 index 000000000000..2891c9116d8d --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/getLastZIndex.ts @@ -0,0 +1,20 @@ +import { getTagOfNode } from 'roosterjs-editor-dom'; + +/** + * @internal + * Search through from editor div to it's root for the latest z-index value + * @param editorDiv the editor div element + * @returns the z index value + */ +export default function getLatestZIndex(editorDiv: HTMLElement) { + let child: HTMLElement | null = editorDiv; + let zIndex = 0; + while (child && getTagOfNode(child) !== 'BODY') { + const childZIndex = parseInt(child.style.zIndex || getComputedStyle(child).zIndex, 10); + if (childZIndex) { + zIndex = Math.max(zIndex, childZIndex); + } + child = child.parentElement; + } + return zIndex; +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/imageEditors/Cropper.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/imageEditors/Cropper.ts index e4fc25c88ae5..0a951570827b 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/imageEditors/Cropper.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/imageEditors/Cropper.ts @@ -1,4 +1,4 @@ -import DragAndDropContext, { X, Y } from '../types/DragAndDropContext'; +import DragAndDropContext, { DNDDirectionX, DnDDirectionY } from '../types/DragAndDropContext'; import DragAndDropHandler from '../../../pluginUtils/DragAndDropHandler'; import { CreateElementData } from 'roosterjs-editor-types'; import { CropInfo } from '../types/ImageEditInfo'; @@ -7,8 +7,8 @@ import { rotateCoordinate } from './Resizer'; const CROP_HANDLE_SIZE = 22; const CROP_HANDLE_WIDTH = 7; -const Xs: X[] = ['w', 'e']; -const Ys: Y[] = ['s', 'n']; +const Xs: DNDDirectionX[] = ['w', 'e']; +const Ys: DnDDirectionY[] = ['s', 'n']; const ROTATION: Record = { sw: 0, nw: 90, @@ -37,7 +37,12 @@ export const Cropper: DragAndDropHandler = { const widthPercent = 1 - leftPercent - rightPercent; const heightPercent = 1 - topPercent - bottomPercent; - if (widthPercent > 0 && heightPercent > 0) { + if ( + widthPercent > 0 && + heightPercent > 0 && + minWidth !== undefined && + minHeight !== undefined + ) { const fullWidth = widthPx / widthPercent; const fullHeight = heightPx / heightPercent; const newLeft = @@ -100,13 +105,13 @@ export function getCropHTML(): CreateElementData[] { className: ImageEditElementClass.CropContainer, children: [], }; - - Xs.forEach(x => Ys.forEach(y => containerHTML.children.push(getCropHTMLInternal(x, y)))); - + if (containerHTML) { + Xs.forEach(x => Ys.forEach(y => containerHTML.children?.push(getCropHTMLInternal(x, y)))); + } return [containerHTML, overlayHTML, overlayHTML, overlayHTML, overlayHTML]; } -function getCropHTMLInternal(x: X, y: Y): CreateElementData { +function getCropHTMLInternal(x: DNDDirectionX, y: DnDDirectionY): CreateElementData { const leftOrRight = x == 'w' ? 'left' : 'right'; const topOrBottom = y == 'n' ? 'top' : 'bottom'; const rotation = ROTATION[y + x]; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/imageEditors/Resizer.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/imageEditors/Resizer.ts index 11876d0f17ee..c9fa0dba628c 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/imageEditors/Resizer.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/imageEditors/Resizer.ts @@ -1,14 +1,27 @@ -import DragAndDropContext, { X, Y } from '../types/DragAndDropContext'; +import DragAndDropContext, { DNDDirectionX, DnDDirectionY } from '../types/DragAndDropContext'; import DragAndDropHandler from '../../../pluginUtils/DragAndDropHandler'; import ImageEditInfo, { ResizeInfo } from '../types/ImageEditInfo'; import ImageHtmlOptions from '../types/ImageHtmlOptions'; import { CreateElementData } from 'roosterjs-editor-types'; import { ImageEditElementClass } from '../types/ImageEditElementClass'; -const RESIZE_HANDLE_SIZE = 7; +/** + * An optional callback to allow customize resize handle element of image resizing. + * To customize the resize handle element, add this callback and change the attributes of elementData then it + * will be picked up by ImageEdit code + */ +export interface OnShowResizeHandle { + (elementData: CreateElementData, x: DNDDirectionX, y: DnDDirectionY): void; +} + +const enum HandleTypes { + SquareHandles, + CircularHandlesCorner, +} +const RESIZE_HANDLE_SIZE = 10; const RESIZE_HANDLE_MARGIN = 3; -const Xs: X[] = ['w', '', 'e']; -const Ys: Y[] = ['s', '', 'n']; +const Xs: DNDDirectionX[] = ['w', '', 'e']; +const Ys: DnDDirectionY[] = ['s', '', 'n']; /** * @internal @@ -21,33 +34,41 @@ export const Resizer: DragAndDropHandler = { base.widthPx > 0 && base.heightPx > 0 ? (base.widthPx * 1.0) / base.heightPx : 0; [deltaX, deltaY] = rotateCoordinate(deltaX, deltaY, editInfo.angleRad); - - const horizontalOnly = x == ''; - const verticalOnly = y == ''; - const shouldPreserveRatio = - !(horizontalOnly || verticalOnly) && (options.preserveRatio || e.shiftKey); - let newWidth = horizontalOnly - ? base.widthPx - : Math.max(base.widthPx + deltaX * (x == 'w' ? -1 : 1), options.minWidth); - let newHeight = verticalOnly - ? base.heightPx - : Math.max(base.heightPx + deltaY * (y == 'n' ? -1 : 1), options.minHeight); - - if (shouldPreserveRatio && ratio > 0) { - newHeight = Math.min(newHeight, newWidth / ratio); - newWidth = Math.min(newWidth, newHeight * ratio); - - if (newWidth < newHeight * ratio) { - newWidth = newHeight * ratio; - } else { - newHeight = newWidth / ratio; + if (options.minWidth !== undefined && options.minHeight !== undefined) { + const horizontalOnly = x == ''; + const verticalOnly = y == ''; + const shouldPreserveRatio = + !(horizontalOnly || verticalOnly) && (options.preserveRatio || e.shiftKey); + let newWidth = horizontalOnly + ? base.widthPx + : Math.max(base.widthPx + deltaX * (x == 'w' ? -1 : 1), options.minWidth); + let newHeight = verticalOnly + ? base.heightPx + : Math.max(base.heightPx + deltaY * (y == 'n' ? -1 : 1), options.minHeight); + + if (shouldPreserveRatio && ratio > 0) { + if (ratio > 1) { + // first sure newHeight is right,calculate newWidth + newWidth = newHeight * ratio; + if (newWidth < options.minWidth) { + newWidth = options.minWidth; + newHeight = newWidth / ratio; + } + } else { + // first sure newWidth is right,calculate newHeight + newHeight = newWidth / ratio; + if (newHeight < options.minHeight) { + newHeight = options.minHeight; + newWidth = newHeight * ratio; + } + } } + editInfo.widthPx = newWidth; + editInfo.heightPx = newHeight; + return true; + } else { + return false; } - - editInfo.widthPx = newWidth; - editInfo.heightPx = newHeight; - - return true; }, }; @@ -107,16 +128,30 @@ export function doubleCheckResize( * @internal * Get HTML for resize handles at the corners */ -export function getCornerResizeHTML({ - borderColor: resizeBorderColor, -}: ImageHtmlOptions): CreateElementData[] { +export function getCornerResizeHTML( + { borderColor: resizeBorderColor }: ImageHtmlOptions, + onShowResizeHandle?: OnShowResizeHandle +): CreateElementData[] { const result: CreateElementData[] = []; + Xs.forEach(x => - Ys.forEach(y => - result.push( - (x == '') == (y == '') ? getResizeHandleHTML(x, y, resizeBorderColor) : null - ) - ) + Ys.forEach(y => { + let elementData = + (x == '') == (y == '') + ? getResizeHandleHTML( + x, + y, + resizeBorderColor, + HandleTypes.CircularHandlesCorner + ) + : null; + if (onShowResizeHandle && elementData) { + onShowResizeHandle(elementData, x, y); + } + if (elementData) { + result.push(elementData); + } + }) ); return result; } @@ -125,42 +160,87 @@ export function getCornerResizeHTML({ * @internal * Get HTML for resize handles on the sides */ -export function getSideResizeHTML({ - borderColor: resizeBorderColor, -}: ImageHtmlOptions): CreateElementData[] { +export function getSideResizeHTML( + { borderColor: resizeBorderColor, isSmallImage: isSmallImage }: ImageHtmlOptions, + onShowResizeHandle?: OnShowResizeHandle +): CreateElementData[] | null { + if (isSmallImage) { + return null; + } const result: CreateElementData[] = []; Xs.forEach(x => - Ys.forEach(y => - result.push( - (x == '') != (y == '') ? getResizeHandleHTML(x, y, resizeBorderColor) : null - ) - ) + Ys.forEach(y => { + let elementData = + (x == '') != (y == '') + ? getResizeHandleHTML( + x, + y, + resizeBorderColor, + HandleTypes.CircularHandlesCorner + ) + : null; + if (onShowResizeHandle && elementData) { + onShowResizeHandle(elementData, x, y); + } + if (elementData) { + result.push(elementData); + } + }) ); return result; } -function getResizeHandleHTML(x: X, y: Y, borderColor: string): CreateElementData { +/** + * @internal + * Get HTML for resize borders + */ +export function getResizeBordersHTML({ + borderColor: resizeBorderColor, +}: ImageHtmlOptions): CreateElementData { + return { + tag: 'div', + style: `position:absolute;left:0;right:0;top:0;bottom:0;border:solid 2px ${resizeBorderColor};pointer-events:none;`, + }; +} + +function getResizeHandleHTML( + x: DNDDirectionX, + y: DnDDirectionY, + borderColor: string, + handleTypes: HandleTypes +): CreateElementData | null { const leftOrRight = x == 'w' ? 'left' : 'right'; const topOrBottom = y == 'n' ? 'top' : 'bottom'; const leftOrRightValue = x == '' ? '50%' : '0px'; const topOrBottomValue = y == '' ? '50%' : '0px'; const direction = y + x; - return x == '' && y == '' - ? { - tag: 'div', - style: `position:absolute;left:0;right:0;top:0;bottom:0;border:solid 1px ${borderColor};pointer-events:none`, - } + ? null : { tag: 'div', style: `position:absolute;${leftOrRight}:${leftOrRightValue};${topOrBottom}:${topOrBottomValue}`, children: [ { tag: 'div', - style: `position:relative;width:${RESIZE_HANDLE_SIZE}px;height:${RESIZE_HANDLE_SIZE}px;background-color: ${borderColor};cursor:${direction}-resize;${topOrBottom}:-${RESIZE_HANDLE_MARGIN}px;${leftOrRight}:-${RESIZE_HANDLE_MARGIN}px`, + style: setHandleStyle[handleTypes]( + direction, + topOrBottom, + leftOrRight, + borderColor + ), className: ImageEditElementClass.ResizeHandle, dataset: { x, y }, }, ], }; } + +const setHandleStyle: Record< + HandleTypes, + (direction: string, topOrBottom: string, leftOrRight: string, borderColor: string) => string +> = { + 0: (direction, leftOrRight, topOrBottom, borderColor) => + `position:relative;width:${RESIZE_HANDLE_SIZE}px;height:${RESIZE_HANDLE_SIZE}px;background-color: ${borderColor};cursor:${direction}-resize;${topOrBottom}:-${RESIZE_HANDLE_MARGIN}px;${leftOrRight}:-${RESIZE_HANDLE_MARGIN}px;`, + 1: (direction, leftOrRight, topOrBottom) => + `position:relative;width:${RESIZE_HANDLE_SIZE}px;height:${RESIZE_HANDLE_SIZE}px;background-color: #FFFFFF;cursor:${direction}-resize;${topOrBottom}:-${RESIZE_HANDLE_MARGIN}px;${leftOrRight}:-${RESIZE_HANDLE_MARGIN}px;border-radius:100%;border: 2px solid #bfbfbf;box-shadow: 0px 0.36316px 1.36185px rgba(100, 100, 100, 0.25);`, +}; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/imageEditors/Rotator.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/imageEditors/Rotator.ts index bf02998dddaa..772588ecc07d 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/imageEditors/Rotator.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/imageEditors/Rotator.ts @@ -1,9 +1,9 @@ import DragAndDropContext from '../types/DragAndDropContext'; import DragAndDropHandler from '../../../pluginUtils/DragAndDropHandler'; -import ImageEditInfo, { RotateInfo } from '../types/ImageEditInfo'; import ImageHtmlOptions from '../types/ImageHtmlOptions'; -import { CreateElementData } from 'roosterjs-editor-types'; +import { CreateElementData, Rect } from 'roosterjs-editor-types'; import { ImageEditElementClass } from '../types/ImageEditElementClass'; +import { RotateInfo } from '../types/ImageEditInfo'; const ROTATE_SIZE = 32; const ROTATE_GAP = 15; @@ -23,7 +23,7 @@ export const Rotator: DragAndDropHandler = { const newY = distance * Math.cos(base.angleRad) - deltaY; let angleInRad = Math.atan2(newX, newY); - if (!e.altKey) { + if (!e.altKey && options && options.minRotateDeg !== undefined) { const angleInDeg = angleInRad * DEG_PER_RAD; const adjustedAngleInDeg = Math.round(angleInDeg / options.minRotateDeg) * options.minRotateDeg; @@ -45,19 +45,27 @@ export const Rotator: DragAndDropHandler = { * Fix it by reduce the distance from image to rotate handle */ export function updateRotateHandlePosition( - editInfo: ImageEditInfo, - distance: number[], - marginVertical: number, + editorRect: Rect, rotateCenter: HTMLElement, rotateHandle: HTMLElement ) { - if (rotateCenter && rotateHandle && distance) { - const { angleRad, heightPx } = editInfo; - const cosAngle = Math.cos(angleRad); - const adjustedDistance = - cosAngle <= 0 - ? Number.MAX_SAFE_INTEGER - : (distance[1] + heightPx / 2 + marginVertical) / cosAngle - heightPx / 2; + const rotateHandleRect = rotateHandle.getBoundingClientRect(); + + if (rotateHandleRect) { + const top = rotateHandleRect.top - editorRect.top; + const left = rotateHandleRect.left - editorRect.left; + const right = rotateHandleRect.right - editorRect.right; + const bottom = rotateHandleRect.bottom - editorRect.bottom; + let adjustedDistance = Number.MAX_SAFE_INTEGER; + if (top <= 0) { + adjustedDistance = top; + } else if (left <= 0) { + adjustedDistance = left; + } else if (right >= 0) { + adjustedDistance = right; + } else if (bottom >= 0) { + adjustedDistance = bottom; + } const rotateGap = Math.max(Math.min(ROTATE_GAP, adjustedDistance), 0); const rotateTop = Math.max(Math.min(ROTATE_SIZE, adjustedDistance - rotateGap), 0); @@ -80,12 +88,12 @@ export function getRotateHTML({ { tag: 'div', className: ImageEditElementClass.RotateCenter, - style: `position:absolute;left:50%;width:1px;background-color:${borderColor}`, + style: `position:absolute;left:50%;width:1px;background-color:${borderColor};top:${-ROTATE_GAP}px;height:${ROTATE_GAP}px;`, children: [ { tag: 'div', className: ImageEditElementClass.RotateHandle, - style: `position:absolute;background-color:${rotateHandleBackColor};border:solid 1px ${borderColor};border-radius:50%;width:${ROTATE_SIZE}px;height:${ROTATE_SIZE}px;left:-${handleLeft}px;cursor:move`, + style: `position:absolute;background-color:${rotateHandleBackColor};border:solid 1px ${borderColor};border-radius:50%;width:${ROTATE_SIZE}px;height:${ROTATE_SIZE}px;left:-${handleLeft}px;cursor:move;top:${-ROTATE_SIZE}px;`, children: [getRotateIconHTML(borderColor)], }, ], diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/index.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/index.ts index 9f97e556ddbc..614981d0f653 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/index.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/index.ts @@ -3,3 +3,5 @@ export { default as canRegenerateImage } from './api/canRegenerateImage'; export { default as resizeByPercentage } from './api/resizeByPercentage'; export { default as isResizedTo } from './api/isResizedTo'; export { default as resetImage } from './api/resetImage'; +export { OnShowResizeHandle } from './imageEditors/Resizer'; +export { DNDDirectionX, DnDDirectionY } from './types/DragAndDropContext'; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/types/DragAndDropContext.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/types/DragAndDropContext.ts index d52f1c2dc31d..607a526ed5cd 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/types/DragAndDropContext.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/types/DragAndDropContext.ts @@ -3,16 +3,14 @@ import { ImageEditElementClass } from './ImageEditElementClass'; import { ImageEditOptions } from 'roosterjs-editor-types'; /** - * @internal * Horizontal direction types for image edit */ -export type X = 'w' | '' | 'e'; +export type DNDDirectionX = 'w' | '' | 'e'; /** - * @internal * Vertical direction types for image edit */ -export type Y = 'n' | '' | 's'; +export type DnDDirectionY = 'n' | '' | 's'; /** * @internal @@ -32,12 +30,12 @@ export default interface DragAndDropContext { /** * Horizontal direction */ - x: X; + x: DNDDirectionX; /** * Vertical direction */ - y: Y; + y: DnDDirectionY; /** * Edit options diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/types/ImageEditInfo.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/types/ImageEditInfo.ts index 2f9276b122a2..856e847fc31d 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/types/ImageEditInfo.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/types/ImageEditInfo.ts @@ -62,11 +62,26 @@ export interface RotateInfo { angleRad: number; } +/** + * @internal + * Flip info for inline image rotate + */ +export interface FlipInfo { + /** + * If true, the image was flipped. + */ + flippedVertical?: boolean; + /** + * If true, the image was flipped. + */ + flippedHorizontal?: boolean; +} + /** * @internal * Edit info for inline image editing */ -export default interface ImageEditInfo extends ResizeInfo, CropInfo, RotateInfo { +export default interface ImageEditInfo extends ResizeInfo, CropInfo, RotateInfo, FlipInfo { /** * Original src of the image. This value will not be changed when edit image. We can always use it * to get the original image so that all editing operation will be on top of the original image. diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/types/ImageHtmlOptions.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/types/ImageHtmlOptions.ts index b9c56acdafab..9082e3455f57 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/types/ImageHtmlOptions.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/types/ImageHtmlOptions.ts @@ -19,4 +19,15 @@ export default interface ImageHtmlOptions { * Background color of the rotate handle */ rotateHandleBackColor: string; + + /** + * Verify if the area of the image is less than 10000px, if yes, don't insert the side handles + */ + isSmallImage: boolean; + + /** + * @deprecated this handles are always enabled + * Enable resize handles experimental feature + */ + handlesExperimentalFeatures?: boolean; } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts index db6d49e31089..4e14e79a39d4 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts @@ -1,31 +1,23 @@ import convertPasteContentForSingleImage from './imageConverter/convertPasteContentForSingleImage'; import convertPastedContentForLI from './commonConverter/convertPastedContentForLI'; import convertPastedContentFromExcel from './excelConverter/convertPastedContentFromExcel'; +import convertPastedContentFromOfficeOnline from './officeOnlineConverter/convertPastedContentFromOfficeOnline'; import convertPastedContentFromPowerPoint from './pptConverter/convertPastedContentFromPowerPoint'; import convertPastedContentFromWord from './wordConverter/convertPastedContentFromWord'; import handleLineMerge from './lineMerge/handleLineMerge'; -import { toArray } from 'roosterjs-editor-dom'; +import sanitizeHtmlColorsFromPastedContent from './sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent'; +import sanitizeLinks from './sanitizeLinks/sanitizeLinks'; +import { getPasteSource } from 'roosterjs-editor-dom'; +import { KnownPasteSourceType } from 'roosterjs-editor-types'; import { EditorPlugin, - ExperimentalFeatures, IEditor, + PasteType, PluginEvent, PluginEventType, } from 'roosterjs-editor-types'; -import convertPastedContentFromWordOnline, { - isWordOnlineWithList, -} from './officeOnlineConverter/convertPastedContentFromWordOnline'; -const WORD_ATTRIBUTE_NAME = 'xmlns:w'; -const WORD_ATTRIBUTE_VALUE = 'urn:schemas-microsoft-com:office:word'; -const EXCEL_ATTRIBUTE_NAME = 'xmlns:x'; -const EXCEL_ATTRIBUTE_VALUE = 'urn:schemas-microsoft-com:office:excel'; -const PROG_ID_NAME = 'ProgId'; -const EXCEL_ONLINE_ATTRIBUTE_VALUE = 'Excel.Sheet'; -const POWERPOINT_ATTRIBUTE_VALUE = 'PowerPoint.Slide'; const GOOGLE_SHEET_NODE_NAME = 'google-sheets-html-origin'; -const WAC_IDENTIFY_SELECTOR = - 'ul[class^="BulletListStyle"]>.OutlineElement,ol[class^="NumberListStyle"]>.OutlineElement,span.WACImageContainer'; /** * Paste plugin, handles BeforePaste event and reformat some special content, including: @@ -34,13 +26,18 @@ const WAC_IDENTIFY_SELECTOR = * 3. Content copied from Word Online or OneNote Online */ export default class Paste implements EditorPlugin { - private editor: IEditor; + private editor: IEditor | null = null; /** * Construct a new instance of Paste class * @param unknownTagReplacement Replace solution of unknown tags, default behavior is to replace with SPAN + * @param convertSingleImageBody When enabled, if clipboard HTML contains a single image, we reuse the image without modifying the src attribute. + * When disabled, pasted image src attribute will use the dataUri from clipboard data -- By Default disabled. */ - constructor(private unknownTagReplacement: string = 'SPAN') {} + constructor( + private unknownTagReplacement: string = 'SPAN', + private convertSingleImageBody: boolean = false + ) {} /** * Get a friendly name of this plugin @@ -69,49 +66,44 @@ export default class Paste implements EditorPlugin { * @param event PluginEvent object */ onPluginEvent(event: PluginEvent) { - if (event.eventType == PluginEventType.BeforePaste) { - const { htmlAttributes, fragment, sanitizingOption, clipboardData } = event; + if (this.editor && event.eventType == PluginEventType.BeforePaste) { + const { fragment, sanitizingOption } = event; const trustedHTMLHandler = this.editor.getTrustedHTMLHandler(); - let wacListElements: Node[]; - if (htmlAttributes[WORD_ATTRIBUTE_NAME] == WORD_ATTRIBUTE_VALUE) { - // Handle HTML copied from Word - convertPastedContentFromWord(event); - } else if ( - htmlAttributes[EXCEL_ATTRIBUTE_NAME] == EXCEL_ATTRIBUTE_VALUE || - htmlAttributes[PROG_ID_NAME] == EXCEL_ONLINE_ATTRIBUTE_VALUE - ) { - // Handle HTML copied from Excel - convertPastedContentFromExcel(event, trustedHTMLHandler); - } else if (htmlAttributes[PROG_ID_NAME] == POWERPOINT_ATTRIBUTE_VALUE) { - convertPastedContentFromPowerPoint(event, trustedHTMLHandler); - } else if ( - (wacListElements = toArray(fragment.querySelectorAll(WAC_IDENTIFY_SELECTOR))) && - wacListElements.length > 0 - ) { - // Once it is known that the document is from WAC - // We need to remove the display property and margin from all the list item - wacListElements.forEach((el: HTMLElement) => { - el.style.display = null; - el.style.margin = null; - }); - // call conversion function if the pasted content is from word online and - // has list element in the pasted content. - if (isWordOnlineWithList(fragment)) { - convertPastedContentFromWordOnline(fragment); - } - } else if (fragment.querySelector(GOOGLE_SHEET_NODE_NAME)) { - sanitizingOption.additionalTagReplacements[GOOGLE_SHEET_NODE_NAME] = '*'; - } else if ( - this.editor.isFeatureEnabled(ExperimentalFeatures.ConvertSingleImageBody) && - clipboardData.htmlFirstLevelChildTags?.length == 1 && - clipboardData.htmlFirstLevelChildTags[0] == 'IMG' - ) { - convertPasteContentForSingleImage(event, trustedHTMLHandler); - } else { - convertPastedContentForLI(fragment); - handleLineMerge(fragment); + switch (getPasteSource(event, this.convertSingleImageBody)) { + case KnownPasteSourceType.WordDesktop: + // Handle HTML copied from Word + convertPastedContentFromWord(event); + break; + case KnownPasteSourceType.ExcelDesktop: + case KnownPasteSourceType.ExcelOnline: + if ( + event.pasteType === PasteType.Normal || + event.pasteType === PasteType.MergeFormat + ) { + // Handle HTML copied from Excel + convertPastedContentFromExcel(event, trustedHTMLHandler); + } + break; + case KnownPasteSourceType.PowerPointDesktop: + convertPastedContentFromPowerPoint(event, trustedHTMLHandler); + break; + case KnownPasteSourceType.WacComponents: + convertPastedContentFromOfficeOnline(fragment, sanitizingOption); + break; + case KnownPasteSourceType.GoogleSheets: + sanitizingOption.additionalTagReplacements[GOOGLE_SHEET_NODE_NAME] = '*'; + break; + case KnownPasteSourceType.SingleImage: + convertPasteContentForSingleImage(event, trustedHTMLHandler); + break; + case KnownPasteSourceType.Default: + convertPastedContentForLI(fragment); + handleLineMerge(fragment); + break; } + sanitizeLinks(sanitizingOption); + sanitizeHtmlColorsFromPastedContent(sanitizingOption); // Replace unknown tags with SPAN sanitizingOption.unknownTagReplacement = this.unknownTagReplacement; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/excelConverter/convertPastedContentFromExcel.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/excelConverter/convertPastedContentFromExcel.ts index 5729e88db24e..a5a0d47a7da6 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Paste/excelConverter/convertPastedContentFromExcel.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/excelConverter/convertPastedContentFromExcel.ts @@ -1,5 +1,5 @@ import { BeforePasteEvent, TrustedHTMLHandler } from 'roosterjs-editor-types'; -import { chainSanitizerCallback, moveChildNodes } from 'roosterjs-editor-dom'; +import { chainSanitizerCallback, getTagOfNode, moveChildNodes } from 'roosterjs-editor-dom'; const LAST_TD_END_REGEX = /<\/\s*td\s*>((?!<\/\s*tr\s*>)[\s\S])*$/i; const LAST_TR_END_REGEX = /<\/\s*tr\s*>((?!<\/\s*table\s*>)[\s\S])*$/i; @@ -17,13 +17,29 @@ export default function convertPastedContentFromExcel( trustedHTMLHandler: TrustedHTMLHandler ) { const { fragment, sanitizingOption, htmlBefore, clipboardData } = event; - const html = excelHandler(clipboardData.html, htmlBefore); + const html = clipboardData.html ? excelHandler(clipboardData.html, htmlBefore) : undefined; - if (clipboardData.html != html) { + if (html && clipboardData.html != html) { const doc = new DOMParser().parseFromString(trustedHTMLHandler(html), 'text/html'); moveChildNodes(fragment, doc?.body); } + // For Excel Online + const firstChild = fragment.firstChild; + if (firstChild && firstChild.childNodes.length > 0 && getTagOfNode(firstChild) == 'DIV') { + const tableFound = Array.from(firstChild.childNodes).every((child: Node) => { + // Tables pasted from Excel Online should be of the format: 0 to N META tags and 1 TABLE tag + return getTagOfNode(child) == 'META' + ? true + : getTagOfNode(child) == 'TABLE' && child == firstChild.lastChild; + }); + + // Extract Table from Div + if (tableFound && firstChild.lastChild) { + event.fragment.replaceChildren(firstChild.lastChild); + } + } + chainSanitizerCallback(sanitizingOption.elementCallbacks, 'TD', element => { if (element.style.borderStyle == 'none') { element.style.border = DEFAULT_BORDER_STYLE; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/lineMerge/handleLineMerge.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/lineMerge/handleLineMerge.ts index b8cb337fbf19..a0c6b392e7da 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Paste/lineMerge/handleLineMerge.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/lineMerge/handleLineMerge.ts @@ -1,6 +1,7 @@ import { changeElementTag, ContentTraverser, + findClosestElementAncestor, getBlockElementAtNode, getNextLeafSibling, getPreviousLeafSibling, @@ -29,10 +30,11 @@ export default function handleLineMerge(root: Node) { } if (blocks.length > 0) { + const blocksLength = blocks.length - 1; processBlock(blocks[0]); - processBlock(blocks[blocks.length - 1]); + processBlock(blocks[blocksLength]); checkAndAddBr(root, blocks[0], true /*isFirst*/); - checkAndAddBr(root, blocks[blocks.length - 1], false /*isFirst*/); + checkAndAddBr(root, blocks[blocksLength], false /*isFirst*/, blocks[0]); } } @@ -40,31 +42,60 @@ function processBlock(block: { start: Node; end: Node }) { const { start, end } = block; if (start == end && getTagOfNode(start) == 'DIV') { - const node = changeElementTag(start as HTMLElement, 'SPAN'); + const node = changeElementTag(start as HTMLElement, 'SPAN') as Node; block.start = node; block.end = node; - if (getTagOfNode(node.lastChild) == 'BR') { + if (node && node.lastChild && getTagOfNode(node.lastChild) == 'BR') { node.removeChild(node.lastChild); } } else if (getTagOfNode(end) == 'BR') { - const node = end.ownerDocument.createTextNode(''); - end.parentNode?.insertBefore(node, end); - block.end = node; - end.parentNode?.removeChild(end); + const node = end.ownerDocument?.createTextNode(''); + if (node) { + end.parentNode?.insertBefore(node, end); + block.end = node; + end.parentNode?.removeChild(end); + } } } -function checkAndAddBr(root: Node, block: { start: Node; end: Node }, isFirst: boolean) { +function checkAndAddBr( + root: Node, + block: { start: Node; end: Node }, + isFirst: boolean, + firstBlock?: { start: Node; end: Node } +) { const blockElement = getBlockElementAtNode(root, block.start); const sibling = isFirst ? getNextLeafSibling(root, block.end) : getPreviousLeafSibling(root, block.start); + if (!sibling) { + return; + } + if (blockElement?.contains(sibling)) { - (isFirst ? block.end : block.start).parentNode?.insertBefore( - block.start.ownerDocument.createElement('br'), - isFirst ? block.end.nextSibling : block.start - ); + const br = block.start.ownerDocument?.createElement('br'); + if (br) { + const blockToUse = isFirst ? block.end : block.start; + blockToUse.parentNode?.insertBefore(br, isFirst ? block.end.nextSibling : block.start); + } + } else if ( + firstBlock && + firstBlock.end == firstBlock.start && + getTagOfNode(firstBlock.end) == 'SPAN' + ) { + // If the first block and the last block are Siblings, add a BR before so the only two + // lines that are being pasted are not merged. + const previousSibling = getPreviousLeafSibling(root, block.start); + if ( + firstBlock.end.contains(previousSibling) && + !findClosestElementAncestor(block.start, root, 'li') + ) { + const br = block.start.ownerDocument?.createElement('br'); + if (br) { + block.start.parentNode?.insertBefore(br, block.start); + } + } } } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/officeOnlineConverter/ListItemBlock.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/officeOnlineConverter/ListItemBlock.ts index bdf912a91a70..d1588fd10461 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Paste/officeOnlineConverter/ListItemBlock.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/officeOnlineConverter/ListItemBlock.ts @@ -6,17 +6,17 @@ export default interface ListItemBlock { /** * The first element in block of list item from pasted word online document. */ - startElement: Element; + startElement: Element | null; /** * The last element in block of list item from pasted word online document. */ - endElement: Element; + endElement: Element | null; /** * The position where the processed bulleted list should be inserted. */ - insertPositionNode: Node; + insertPositionNode: Node | null; /** * The list of containers that wraps each list item. @@ -28,7 +28,7 @@ export default interface ListItemBlock { * @internal * Initialize an empty ListItemBlock */ -export function createListItemBlock(listItem: Element = null): ListItemBlock { +export function createListItemBlock(listItem: Element | null = null): ListItemBlock { return { startElement: listItem, endElement: listItem, diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/officeOnlineConverter/convertPastedContentFromOfficeOnline.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/officeOnlineConverter/convertPastedContentFromOfficeOnline.ts new file mode 100644 index 000000000000..8a69427b7d15 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/officeOnlineConverter/convertPastedContentFromOfficeOnline.ts @@ -0,0 +1,53 @@ +import { chainSanitizerCallback } from 'roosterjs-editor-dom'; +import { HtmlSanitizerOptions } from 'roosterjs-editor-types'; +import convertPastedContentFromWordOnline, { + isWordOnlineWithList, +} from './convertPastedContentFromWordOnline'; + +const WAC_IDENTIFY_SELECTOR = + 'ul[class^="BulletListStyle"]>.OutlineElement,ol[class^="NumberListStyle"]>.OutlineElement,span.WACImageContainer'; +const TABLE_TEMP_ELEMENTS_QUERY = [ + 'TableInsertRowGapBlank', + 'TableColumnResizeHandle', + 'TableCellTopBorderHandle', + 'TableCellLeftBorderHandle', + 'TableHoverColumnHandle', + 'TableHoverRowHandle', +] + .map(className => `.${className}`) + .join(','); +/** + * @internal + * Convert pasted content from Office Online + * Once it is known that the document is from WAC + * We need to remove the display property and margin from all the list item + * @param event The BeforePaste event + */ +export default function convertPastedContentFromOfficeOnline( + fragment: DocumentFragment, + sanitizingOption: Required +) { + fragment.querySelectorAll(WAC_IDENTIFY_SELECTOR).forEach((el: Element) => { + const element = el as HTMLElement; + element.style.removeProperty('display'); + element.style.removeProperty('margin'); + }); + // call conversion function if the pasted content is from word online and + // has list element in the pasted content. + if (isWordOnlineWithList(fragment)) { + convertPastedContentFromWordOnline(fragment); + } + + // Remove "border:none" for image to fix image resize behavior + // We found a problem that when paste an image with "border:none" then the resize border will be + // displayed incorrectly when resize it. So we need to drop this style + chainSanitizerCallback( + sanitizingOption.cssStyleCallbacks, + 'border', + (value, element) => element.tagName != 'IMG' || value != 'none' + ); + + fragment + .querySelectorAll(TABLE_TEMP_ELEMENTS_QUERY) + .forEach(node => node.parentElement?.removeChild(node)); +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/officeOnlineConverter/convertPastedContentFromWordOnline.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/officeOnlineConverter/convertPastedContentFromWordOnline.ts index 0d6e90057c68..ca3e5645a3d2 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Paste/officeOnlineConverter/convertPastedContentFromWordOnline.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/officeOnlineConverter/convertPastedContentFromWordOnline.ts @@ -16,6 +16,13 @@ const WORD_ONLINE_IDENTIFYING_SELECTOR = const LIST_CONTAINER_ELEMENT_CLASS_NAME = 'ListContainerWrapper'; const IMAGE_CONTAINER_ELEMENT_CLASS_NAME = 'WACImageContainer'; +//When the list style is a symbol and the value is not in the clipboard, WordOnline +const VALID_LIST_STYLE_CHAR_CODES = [ + '111', //'o' + '9643', //'▫' + '9830', //'♦' +]; + /** * @internal */ @@ -93,42 +100,52 @@ export default function convertPastedContentFromWordOnline(fragment: DocumentFra flattenListBlock(fragment, itemBlock); // Find the node to insertBefore, which is next sibling node of the end of a listItemBlock. - itemBlock.insertPositionNode = itemBlock.endElement.nextSibling; + itemBlock.insertPositionNode = itemBlock.endElement?.nextSibling ?? null; - let convertedListElement: Element; + let convertedListElement: Element | undefined = undefined; const doc = fragment.ownerDocument; itemBlock.listItemContainers.forEach(listItemContainer => { - let listType: 'OL' | 'UL' = getContainerListType(listItemContainer); // list type that is contained by iterator. - // Initialize processed element with proper listType if this is the first element - if (!convertedListElement) { - convertedListElement = doc.createElement(listType); - } - - // Get all list items(
            4. ) in the current iterator element. - const currentListItems = toArray(listItemContainer.querySelectorAll('li')); - currentListItems.forEach(item => { - // If item is in root level and the type of list changes then - // insert the current list into body and then reinitialize the convertedListElement - // Word Online is using data-aria-level to determine the the depth of the list item. - const itemLevel = parseInt(item.getAttribute('data-aria-level')); - // In first level list, there are cases where a consecutive list item DIV may have different list type - // When that happens we need to insert the processed elements into the document, then change the list type - // and keep the processing going. - if (getTagOfNode(convertedListElement) != listType && itemLevel == 1) { - insertConvertedListToDoc(convertedListElement, fragment, itemBlock); - convertedListElement = doc.createElement(listType); + let listType: 'OL' | 'UL' | null = getContainerListType(listItemContainer); // list type that is contained by iterator. + if (listType) { + // Initialize processed element with proper listType if this is the first element + if (!convertedListElement) { + convertedListElement = createNewList(listItemContainer, doc, listType); } - insertListItem(convertedListElement, item, listType, doc); - }); - }); - insertConvertedListToDoc(convertedListElement, fragment, itemBlock); + // Get all list items(
            5. ) in the current iterator element. + const currentListItems = toArray(listItemContainer.querySelectorAll('li')); + currentListItems.forEach(item => { + // If item is in root level and the type of list changes then + // insert the current list into body and then reinitialize the convertedListElement + // Word Online is using data-aria-level to determine the the depth of the list item. + const itemLevel = parseInt(item.getAttribute('data-aria-level') ?? ''); + // In first level list, there are cases where a consecutive list item DIV may have different list type + // When that happens we need to insert the processed elements into the document, then change the list type + // and keep the processing going. + if ( + convertedListElement && + getTagOfNode(convertedListElement) != listType && + itemLevel == 1 && + listType + ) { + insertConvertedListToDoc(convertedListElement, fragment, itemBlock); + convertedListElement = createNewList(listItemContainer, doc, listType); + } + if (convertedListElement && listType) { + insertListItem(convertedListElement, item, listType, doc); + } + }); + } + }); + if (convertedListElement) { + insertConvertedListToDoc(convertedListElement, fragment, itemBlock); + } // Once we finish the process the list items and put them into a list. // After inserting the processed element, // we need to remove all the non processed node from the parent node. - const parentContainer = itemBlock.startElement.parentNode; + const parentContainer = itemBlock.startElement?.parentNode; if (parentContainer) { itemBlock.listItemContainers.forEach(listItemContainer => { parentContainer.removeChild(listItemContainer); @@ -150,13 +167,22 @@ export default function convertPastedContentFromWordOnline(fragment: DocumentFra if (safeInstanceOf(node, 'HTMLSpanElement')) { node.childNodes.forEach(childNode => { if (getTagOfNode(childNode) != 'IMG') { - childNode.parentElement.removeChild(childNode); + childNode.parentElement?.removeChild(childNode); } }); } }); } +function createNewList(listItemContainer: Element, doc: Document, tag: 'OL' | 'UL') { + const newList = doc.createElement(tag); + const startAttribute = listItemContainer.firstElementChild?.getAttribute('start'); + if (startAttribute) { + newList.setAttribute('start', startAttribute); + } + return newList; +} + /** * The node processing is based on the premise of only ol/ul is in ListContainerWrapper class * However the html might be malformed, this function is to split all the other elements out of ListContainerWrapper @@ -186,7 +212,7 @@ function sanitizeListItemContainer(fragment: DocumentFragment) { function getListItemBlocks(fragment: DocumentFragment): ListItemBlock[] { const listElements = fragment.querySelectorAll('.' + LIST_CONTAINER_ELEMENT_CLASS_NAME); const result: ListItemBlock[] = []; - let curListItemBlock: ListItemBlock; + let curListItemBlock: ListItemBlock | null = null; for (let i = 0; i < listElements.length; i++) { let curItem = listElements[i]; if (!curListItemBlock) { @@ -196,8 +222,9 @@ function getListItemBlocks(fragment: DocumentFragment): ListItemBlock[] { const lastItemInCurBlock = listItemContainers[listItemContainers.length - 1]; if ( curItem == lastItemInCurBlock.nextSibling || - getFirstLeafNode(curItem) == - getNextLeafSibling(lastItemInCurBlock.parentNode, lastItemInCurBlock) + (lastItemInCurBlock.parentNode && + getFirstLeafNode(curItem) == + getNextLeafSibling(lastItemInCurBlock.parentNode, lastItemInCurBlock)) ) { listItemContainers.push(curItem); curListItemBlock.endElement = curItem; @@ -209,7 +236,7 @@ function getListItemBlocks(fragment: DocumentFragment): ListItemBlock[] { } } - if (curListItemBlock?.listItemContainers.length > 0) { + if (curListItemBlock && curListItemBlock.listItemContainers.length > 0) { result.push(curListItemBlock); } @@ -222,17 +249,19 @@ function getListItemBlocks(fragment: DocumentFragment): ListItemBlock[] { * @param listItemBlock The list item block needed to be flattened. */ function flattenListBlock(fragment: DocumentFragment, listItemBlock: ListItemBlock) { - const collapsedListItemSections = collapseNodes( - fragment, - listItemBlock.startElement, - listItemBlock.endElement, - true - ); - collapsedListItemSections.forEach(section => { - if (getTagOfNode(section.firstChild) == 'DIV') { - unwrap(section); - } - }); + if (listItemBlock.startElement && listItemBlock.endElement) { + const collapsedListItemSections = collapseNodes( + fragment, + listItemBlock.startElement, + listItemBlock.endElement, + true + ); + collapsedListItemSections.forEach(section => { + if (getTagOfNode(section.firstChild) == 'DIV') { + unwrap(section); + } + }); + } } /** @@ -254,14 +283,25 @@ function getContainerListType(listItemContainer: Element): 'OL' | 'UL' | null { function insertListItem( listRootElement: Element, itemToInsert: HTMLElement, - listType: string, + listType: 'UL' | 'OL', doc: HTMLDocument ): void { if (!listType) { return; } // Get item level from 'data-aria-level' attribute - let itemLevel = parseInt(itemToInsert.getAttribute('data-aria-level')); + let itemLevel = parseInt(itemToInsert.getAttribute('data-aria-level') ?? ''); + + // Try to reuse the List Marker + let style = itemToInsert.getAttribute('data-leveltext'); + if ( + listType == 'UL' && + style && + VALID_LIST_STYLE_CHAR_CODES.indexOf(style.charCodeAt(0).toString()) > -1 + ) { + itemToInsert.style.listStyleType = `"${style} "`; + } + let curListLevel = listRootElement; // Level iterator to find the correct place for the current element. // if the itemLevel is 1 it means the level iterator is at the correct place. while (itemLevel > 1) { @@ -269,20 +309,24 @@ function insertListItem( // If the current level is empty, create empty list within the current level // then move the level iterator into the next level. curListLevel.appendChild(doc.createElement(listType)); - curListLevel = curListLevel.firstElementChild; + if (curListLevel.firstElementChild) { + curListLevel = curListLevel.firstElementChild; + } } else { // If the current level is not empty, the last item in the needs to be a UL or OL // and the level iterator should move to the UL/OL at the last position. let lastChild = curListLevel.lastElementChild; let lastChildTag = getTagOfNode(lastChild); - if (lastChildTag == 'UL' || lastChildTag == 'OL') { + if (lastChild && (lastChildTag == 'UL' || lastChildTag == 'OL')) { // If the last child is a list(UL/OL), then move the level iterator to last child. curListLevel = lastChild; } else { // If the last child is not a list, then append a new list to the level // and move the level iterator to the new level. curListLevel.appendChild(doc.createElement(listType)); - curListLevel = curListLevel.lastElementChild; + if (curListLevel.lastElementChild) { + curListLevel = curListLevel.lastElementChild; + } } } itemLevel--; @@ -314,7 +358,7 @@ function insertConvertedListToDoc( parentNode.insertBefore(convertedListElement, insertPositionNode); } } else { - const parentNode = listItemBlock.startElement.parentNode; + const parentNode = listItemBlock.startElement?.parentNode; if (parentNode) { parentNode.appendChild(convertedListElement); } else { diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/deprecatedColorList.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/deprecatedColorList.ts new file mode 100644 index 000000000000..05588a27f195 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/deprecatedColorList.ts @@ -0,0 +1,30 @@ +/** + * @internal + * List of deprecated colors that should be removed + */ + +export const DeprecatedColorList: string[] = [ + 'activeborder', + 'activecaption', + 'appworkspace', + 'background', + 'buttonhighlight', + 'buttonshadow', + 'captiontext', + 'inactiveborder', + 'inactivecaption', + 'inactivecaptiontext', + 'infobackground', + 'infotext', + 'menu', + 'menutext', + 'scrollbar', + 'threeddarkshadow', + 'threedface', + 'threedhighlight', + 'threedlightshadow', + 'threedfhadow', + 'window', + 'windowframe', + 'windowtext', +]; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts new file mode 100644 index 000000000000..e3da37455a67 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent.ts @@ -0,0 +1,20 @@ +import { chainSanitizerCallback } from 'roosterjs-editor-dom'; +import { DeprecatedColorList } from './deprecatedColorList'; +import { HtmlSanitizerOptions } from 'roosterjs-editor-types'; + +/** + * @internal + * Remove the deprecated colors from pasted content + * @param sanitizingOption the sanitizingOption of BeforePasteEvent + * */ +export default function sanitizeHtmlColorsFromPastedContent( + sanitizingOption: Required +) { + ['color', 'background-color'].forEach(property => { + chainSanitizerCallback( + sanitizingOption.cssStyleCallbacks, + property, + (value: string) => DeprecatedColorList.indexOf(value) < 0 + ); + }); +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeLinks/sanitizeLinks.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeLinks/sanitizeLinks.ts new file mode 100644 index 000000000000..0d63b7d7f2d0 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeLinks/sanitizeLinks.ts @@ -0,0 +1,33 @@ +import { chainSanitizerCallback } from 'roosterjs-editor-dom'; +import { HtmlSanitizerOptions } from 'roosterjs-editor-types'; + +const SUPPORTED_PROTOCOLS = ['http:', 'https:', 'notes:', 'mailto:', 'onenote:']; + +/** + * @internal + * Clear local paths and remove link + * @param sanitizingOption the sanitizingOption of BeforePasteEvent + * */ +export default function sanitizeLinks(sanitizingOption: Required) { + chainSanitizerCallback( + sanitizingOption.attributeCallbacks, + 'href', + (value: string, element: HTMLElement) => validateLink(value, element) + ); +} + +function validateLink(link: string, htmlElement: HTMLElement) { + let url; + try { + url = new URL(link); + } catch { + url = undefined; + } + + /* whitelist supported protocols */ + if (url && SUPPORTED_PROTOCOLS.indexOf(url.protocol) > -1) { + return link; + } + htmlElement.removeAttribute('href'); + return ''; +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/WordConverterArguments.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/WordConverterArguments.ts index e0a1e8fcac72..15f7db40f87b 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/WordConverterArguments.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/WordConverterArguments.ts @@ -35,7 +35,7 @@ export default interface WordConverterArguments { currentListIdsByLevels: LevelLists[]; /** Remembers the item that was last processed */ - lastProcessedItem: HTMLElement; + lastProcessedItem: HTMLElement | null; } /** diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/commentsRemoval.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/commentsRemoval.ts new file mode 100644 index 000000000000..48ca2627439a --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/commentsRemoval.ts @@ -0,0 +1,97 @@ +import { CssStyleCallbackMap, ElementCallbackMap } from 'roosterjs-editor-types'; +import { + chainSanitizerCallback, + getStyles, + moveChildNodes, + safeInstanceOf, +} from 'roosterjs-editor-dom'; + +const MSO_COMMENT_PARENT = 'mso-comment-parent'; +const MSO_COMMENT_REFERENCE = 'mso-comment-reference'; +const MSO_COMMENT_DATE = 'mso-comment-date'; +const MSO_COMMENT_ANCHOR_HREF_REGEX = /#_msocom_/; +const MSO_SPECIAL_CHARACTER = 'mso-special-character'; +const MSO_SPECIAL_CHARACTER_COMMENT = 'comment'; +const MSO_COMMENT_CONTINUATION = 'mso-comment-continuation'; +const MSO_ELEMENT = 'mso-element'; +const MSO_ELEMENT_COMMENT_LIST = 'comment-list'; +const MSO_COMMENT_DONE = 'mso-comment-done'; + +/** + * @internal + * Removes comments when pasting Word content. + */ +export default function commentsRemoval( + elementCallbacks: ElementCallbackMap, + styleCallbacks: CssStyleCallbackMap +) { + // 1st Step, Remove SPAN elements added after each comment. + // Word adds multiple elements for comments as SPAN elements. + // In this step we remove these elements: + // Structure as of 4/18/2022 + // 1.   + // 2. + // + // + // [RS2] + //   + // + // + // + chainSanitizerCallback(elementCallbacks, 'SPAN', element => { + const styles = getStyles(element); + if (styles[MSO_SPECIAL_CHARACTER] == MSO_SPECIAL_CHARACTER_COMMENT) { + element.parentElement?.removeChild(element); + } + return true; + }); + + // 2nd Step, Modify Anchor elements. + // 1. When the element was selected to add a comment in Word, the selection is converted to + // an anchor element, so we change the tag to span. + // 2. Word also adds some Anchor elements with the following structure: + // Structure as of 4/18/2022 + // [SS3] + // In this step we remove this Anchor elements. + chainSanitizerCallback(elementCallbacks, 'A', element => { + if ( + safeInstanceOf(element, 'HTMLAnchorElement') && + MSO_COMMENT_ANCHOR_HREF_REGEX.test(element.href) + ) { + element.parentElement?.removeChild(element); + } + return true; + }); + + // 3rd Step, remove List of comments. + // When the document have a long thread of comments, these comments are appended + // at the end of the copied fragment, we also need to remove it. + // Structure as of 4/18/2022 + // + //
              + //
              + //
              ...
              + //
              ...
              + //
              ...
              + //
              + //
          + chainSanitizerCallback(elementCallbacks, 'DIV', element => { + const styles = getStyles(element); + if (styles[MSO_ELEMENT] == MSO_ELEMENT_COMMENT_LIST) { + moveChildNodes(element); + } + return true; + }); + + /** + * Remove styles related to Office Comments that can cause unwanted behaviors + * depending on the user client + */ + [ + MSO_COMMENT_REFERENCE, + MSO_COMMENT_DATE, + MSO_COMMENT_PARENT, + MSO_COMMENT_CONTINUATION, + MSO_COMMENT_DONE, + ].forEach(style => chainSanitizerCallback(styleCallbacks, style, () => false)); +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/convertPastedContentFromWord.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/convertPastedContentFromWord.ts index ed5cbdd6c287..b9fa0a45e8cd 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/convertPastedContentFromWord.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/convertPastedContentFromWord.ts @@ -1,9 +1,14 @@ +import commentsRemoval from './commentsRemoval'; import { BeforePasteEvent } from 'roosterjs-editor-types'; import { chainSanitizerCallback, moveChildNodes } from 'roosterjs-editor-dom'; import { createWordConverter } from './wordConverter'; import { createWordConverterArguments } from './WordConverterArguments'; import { processNodeConvert, processNodesDiscovery } from './converterUtils'; +const PERCENTAGE_REGEX = /%/; +const DEFAULT_BROWSER_LINE_HEIGHT_PERCENTAGE = 120; +const LIST_ELEMENTS_SELECTOR = 'p,h1,h2,h3,h4,h5,h6'; + /** * @internal * Converts all the Word generated list items in the specified node into standard HTML UL and OL tags @@ -23,11 +28,38 @@ export default function convertPastedContentFromWord(event: BeforePasteEvent) { // First find all the nodes that we need to check for list item information // This call will return all the p and header elements under the root node.. These are the elements that // Word uses a list items, so we'll only process them and avoid walking the whole tree. - let elements = fragment.querySelectorAll('p'); + let elements = fragment.querySelectorAll(LIST_ELEMENTS_SELECTOR) as NodeListOf; if (elements.length > 0) { wordConverter.wordConverterArgs = createWordConverterArguments(elements); if (processNodesDiscovery(wordConverter)) { processNodeConvert(wordConverter); } } + + // If the List style contains marginBottom = 0in, the space after the list is going to be too narrow. + // Remove this style so the list displays correctly. + ['OL', 'UL'].forEach(tag => { + chainSanitizerCallback(sanitizingOption.elementCallbacks, tag, element => { + if (element.style.marginBottom == '0in') { + element.style.marginBottom = ''; + } + + return true; + }); + }); + + //If the line height is less than the browser default line height, line between the text is going to be too narrow + chainSanitizerCallback(sanitizingOption.cssStyleCallbacks, 'line-height', (value: string) => { + let parsedLineHeight: number; + if ( + PERCENTAGE_REGEX.test(value) && + !isNaN((parsedLineHeight = parseInt(value))) && + parsedLineHeight < DEFAULT_BROWSER_LINE_HEIGHT_PERCENTAGE + ) { + return false; + } + return true; + }); + + commentsRemoval(sanitizingOption.elementCallbacks, sanitizingOption.cssStyleCallbacks); } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/converterUtils.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/converterUtils.ts index 522a00322b08..45ba6a7ab650 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/converterUtils.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/converterUtils.ts @@ -28,6 +28,9 @@ const LINE_BREAKS = /[\n|\r]/gi; */ export function processNodesDiscovery(wordConverter: WordConverter): boolean { let args = wordConverter.wordConverterArgs; + if (!args) { + return false; + } while (args.currentIndex < args.nodes.length) { let node = args.nodes.item(args.currentIndex); @@ -133,14 +136,13 @@ export function processNodesDiscovery(wordConverter: WordConverter): boolean { moveChildNodes(last, node, true /*keepExistingChildren*/); // Remove the item that we don't need anymore - node.parentNode.removeChild(node); + node.parentNode?.removeChild(node); } } // Move to the next element are return true if more elements need to be processed args.currentIndex++; } - return args.listItems.length > 0; } @@ -152,41 +154,49 @@ export function processNodesDiscovery(wordConverter: WordConverter): boolean { */ export function processNodeConvert(wordConverter: WordConverter): boolean { let args = wordConverter.wordConverterArgs; - args.currentIndex = 0; - - while (args.currentIndex < args.listItems.length) { - let metadata = args.listItems[args.currentIndex]; - let node = metadata.originalNode; - let listMetadata = args.lists[metadata.uniqueListId.toString()]; - if (!listMetadata.ignore) { - // We have a list item that we need to convert, get or create the list - // that hold this item out - let list = getOrCreateListForNode(wordConverter, node, metadata, listMetadata); - if (list) { - // Clean the element out.. this call gets rid of the fake bullet and unneeded nodes - cleanupListIgnore(node, LOOKUP_DEPTH); - - // Create a new list item and transfer the children - let li = node.ownerDocument.createElement('LI'); - moveChildNodes(li, node); - - // Append the list item into the list - list.appendChild(li); - - // Remove the node we just converted - node.parentNode.removeChild(node); - - if (listMetadata.tagName == 'UL') { - wordConverter.numBulletsConverted++; - } else { - wordConverter.numNumberedConverted++; + if (args) { + args.currentIndex = 0; + + while (args.currentIndex < args.listItems.length) { + let metadata = args.listItems[args.currentIndex]; + let node = metadata.originalNode; + let listMetadata = args.lists[metadata.uniqueListId.toString()]; + if (!listMetadata.ignore) { + // We have a list item that we need to convert, get or create the list + // that hold this item out + let list = getOrCreateListForNode(wordConverter, node, metadata, listMetadata); + if (list) { + // Clean the element out.. this call gets rid of the fake bullet and unneeded nodes + cleanupListIgnore(node, LOOKUP_DEPTH); + + // Create a new list item and transfer the children + let li = node.ownerDocument.createElement('LI'); + if (getTagOfNode(node).startsWith('H')) { + const clone = node.cloneNode(true /* deep */) as HTMLHeadingElement; + clone.style.textIndent = ''; + clone.style.marginLeft = ''; + clone.style.marginRight = ''; + li.appendChild(clone); + } else { + moveChildNodes(li, node); + } + + // Append the list item into the list + list.appendChild(li); + + // Remove the node we just converted + node.parentNode?.removeChild(node); + + if (listMetadata.tagName == 'UL') { + wordConverter.numBulletsConverted++; + } else { + wordConverter.numNumberedConverted++; + } } } + args.currentIndex++; } - - args.currentIndex++; } - return wordConverter.numBulletsConverted > 0 || wordConverter.numNumberedConverted > 0; } @@ -213,7 +223,7 @@ function getOrCreateListForNode( // is a completely new list, so we'll append a new list for that if ((listId && listId != metadata.uniqueListId) || (!listId && list.firstChild)) { let newList = node.ownerDocument.createElement(listMetadata.tagName); - list.parentNode.insertBefore(newList, list.nextSibling); + list.parentNode?.insertBefore(newList, list.nextSibling); list = newList; } @@ -243,17 +253,20 @@ function convertListIfNeeded( // Check if we need to convert the list out if (listMetadata.tagName != getTagOfNode(list)) { // We have the wrong list type.. convert it, set the id again and transfer all the children - let newList = list.ownerDocument.createElement(listMetadata.tagName); - setObject( - wordConverter.wordCustomData, - newList, - UNIQUE_LIST_ID_CUSTOM_DATA, - getObject(wordConverter.wordCustomData, list, UNIQUE_LIST_ID_CUSTOM_DATA) - ); - moveChildNodes(newList, list); - list.parentNode.insertBefore(newList, list); - list.parentNode.removeChild(list); - list = newList; + let newList = list.ownerDocument?.createElement(listMetadata.tagName); + if (newList) { + setObject( + wordConverter.wordCustomData, + newList, + UNIQUE_LIST_ID_CUSTOM_DATA, + getObject(wordConverter.wordCustomData, list, UNIQUE_LIST_ID_CUSTOM_DATA) + ); + moveChildNodes(newList, list); + + list.parentNode?.insertBefore(newList, list); + list.parentNode?.removeChild(list); + list = newList; + } } return list; @@ -265,10 +278,10 @@ function convertListIfNeeded( function recurringGetOrCreateListAtNode( node: HTMLElement, level: number, - listMetadata: ListMetadata + listMetadata: ListMetadata | null ): Node { - let parent: Node = null; - let possibleList: Node; + let parent: Node | null = null; + let possibleList: Node | null = null; if (level == 1) { // Root case, we'll check if the list is the previous sibling of the node possibleList = getRealPreviousSibling(node); @@ -276,7 +289,9 @@ function recurringGetOrCreateListAtNode( // If we get here, we are looking for level 2 or deeper... get the upper list // and check if the last element is a list parent = recurringGetOrCreateListAtNode(node, level - 1, null); - possibleList = parent.lastChild; + if (parent.lastChild) { + possibleList = parent.lastChild; + } } // Check the element that we got and verify that it is a list @@ -290,14 +305,14 @@ function recurringGetOrCreateListAtNode( // If we get here, it means we don't have a list and we need to create one // this code path will always create new lists as UL lists - let newList = node.ownerDocument.createElement(listMetadata ? listMetadata.tagName : 'UL'); + let newList = node.ownerDocument?.createElement(listMetadata ? listMetadata.tagName : 'UL'); if (level == 1) { // For level 1, we'll insert the list before the node - node.parentNode.insertBefore(newList, node); + node.parentNode?.insertBefore(newList, node); } else { // Any level 2 or above, we insert the list as the last // child of the upper level list - parent.appendChild(newList); + parent?.appendChild(newList); } return newList; @@ -311,18 +326,20 @@ function recurringGetOrCreateListAtNode( function cleanupListIgnore(node: Node, levels: number) { let nodesToRemove: Node[] = []; - for (let child: Node = node.firstChild; child; child = child.nextSibling) { - // Clean up the item internally first if we need to based on the number of levels - if (child.nodeType == NodeType.Element && levels > 1) { - cleanupListIgnore(child, levels - 1); - } + for (let child: Node | null = node.firstChild; child; child = child.nextSibling) { + if (child) { + // Clean up the item internally first if we need to based on the number of levels + if (child && child.nodeType == NodeType.Element && levels > 1) { + cleanupListIgnore(child, levels - 1); + } - // Try to convert word comments into ignore elements if we haven't done so for this element - child = fixWordListComments(child, true /*removeComments*/); + // Try to convert word comments into ignore elements if we haven't done so for this element + child = fixWordListComments(child, true /*removeComments*/); - // Check if we can remove this item out - if (isEmptySpan(child) || isIgnoreNode(child)) { - nodesToRemove.push(child); + // Check if we can remove this item out + if (isEmptySpan(child) || isIgnoreNode(child)) { + nodesToRemove.push(child); + } } } @@ -333,7 +350,7 @@ function cleanupListIgnore(node: Node, levels: number) { * Reads the word list meta dada out of the specified node. If the node * is not a Word list item, it returns null. */ -function getListItemMetadata(node: HTMLElement): ListItemMetadata { +function getListItemMetadata(node: HTMLElement): ListItemMetadata | null { if (node.nodeType == NodeType.Element) { let listAttribute = getStyleValue(node, MSO_LIST_STYLE_NAME); if (listAttribute && listAttribute.length > 0) { @@ -385,8 +402,8 @@ function getFakeBulletText(node: Node, levels: number): string { // // Basically, we need to locate the mso-list:Ignore SPAN, which holds either one text or image node. That // text or image node will be the fake bullet we are looking for - let result: string = null; - let child: Node = node.firstChild; + let result: string = ''; + let child: Node | null = node.firstChild; while (!result && child) { // First, check if we need to convert the Word list comments into real elements child = fixWordListComments(child, true /*removeComments*/); @@ -394,7 +411,7 @@ function getFakeBulletText(node: Node, levels: number): string { // Check if this is the node that holds the fake bullets (mso-list: Ignore) if (isIgnoreNode(child)) { // Yes... this is the node that holds either the text or image data - result = child.textContent.trim(); + result = child.textContent?.trim() ?? ''; // This is the case for image case if (result.length == 0) { @@ -426,8 +443,8 @@ function fixWordListComments(child: Node, removeComments: boolean): Node { if (value && value.trim().toLowerCase() == '[if !supportlists]') { // We have a list ignore start, find the end.. We know is not more than // 3 nodes away, so we'll optimize our checks - let nextElement = child; - let endComment: Node = null; + let nextElement: Node | null = child; + let endComment: Node | null = null; for (let j = 0; j < 4; j++) { nextElement = getRealNextSibling(nextElement); if (!nextElement) { @@ -444,25 +461,32 @@ function fixWordListComments(child: Node, removeComments: boolean): Node { // if we found the end node, wrap everything out if (endComment) { - let newSpan = child.ownerDocument.createElement('span'); - newSpan.setAttribute('style', 'mso-list: ignore'); + let newSpan = child.ownerDocument?.createElement('span'); + newSpan?.setAttribute('style', 'mso-list: ignore'); + nextElement = getRealNextSibling(child); while (nextElement != endComment) { - nextElement = nextElement.nextSibling as HTMLElement; - newSpan.appendChild(nextElement.previousSibling); + nextElement = nextElement?.nextSibling as HTMLElement; + if (nextElement.previousSibling) { + newSpan?.appendChild(nextElement.previousSibling); + } } // Insert the element out and use that one as the current child - endComment.parentNode.insertBefore(newSpan, endComment); + if (newSpan) { + endComment.parentNode?.insertBefore(newSpan, endComment); + } // Remove the comments out if the call specified it out if (removeComments) { - child.parentNode.removeChild(child); - endComment.parentNode.removeChild(endComment); + child.parentNode?.removeChild(child); + endComment.parentNode?.removeChild(endComment); } // Last, make sure we return the new element out instead of the comment - child = newSpan; + if (newSpan) { + child = newSpan; + } } } } @@ -471,8 +495,8 @@ function fixWordListComments(child: Node, removeComments: boolean): Node { } /** Finds the real previous sibling, ignoring empty text nodes */ -function getRealPreviousSibling(node: Node): Node { - let prevSibling = node; +function getRealPreviousSibling(node: Node): Node | null { + let prevSibling: Node | null = node; do { prevSibling = prevSibling.previousSibling; } while (prevSibling && isEmptyTextNode(prevSibling)); @@ -480,8 +504,8 @@ function getRealPreviousSibling(node: Node): Node { } /** Finds the real next sibling, ignoring empty text nodes */ -function getRealNextSibling(node: Node): Node { - let nextSibling = node; +function getRealNextSibling(node: Node): Node | null { + let nextSibling: Node | null = node; do { nextSibling = nextSibling.nextSibling; } while (nextSibling && isEmptyTextNode(nextSibling)); @@ -515,7 +539,7 @@ function isEmptySpan(node: Node): boolean { } /** Reads the specified style value from the node */ -function getStyleValue(node: HTMLElement, styleName: string): string { +function getStyleValue(node: HTMLElement, styleName: string): string | null { // Word uses non-standard names for the metadata that puts in the style of the element... // Most browsers will not provide the information for those nonstandard values through the node.style // property, so the only reliable way to read them is to get the attribute directly and do @@ -533,13 +557,17 @@ function isEmptyTextNode(node: Node): boolean { // Empty text node is empty if (node.nodeType == NodeType.Text) { let value = node.nodeValue; - value = value.replace(LINE_BREAKS, ''); - return value.trim().length == 0; + value = value?.replace(LINE_BREAKS, '') ?? ''; + return value?.trim().length == 0; } // Span or Font with an empty child node is empty let tagName = getTagOfNode(node); - if (node.firstChild == node.lastChild && (tagName == 'SPAN' || tagName == 'FONT')) { + if ( + node.firstChild && + node.firstChild == node.lastChild && + (tagName == 'SPAN' || tagName == 'FONT') + ) { return isEmptyTextNode(node.firstChild); } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/wordConverter.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/wordConverter.ts index cb0acce3df13..457aaa8e174e 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/wordConverter.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/wordConverter/wordConverter.ts @@ -16,7 +16,7 @@ export default interface WordConverter { numNumberedConverted: number; /** The structure that records the status of the conversion */ - wordConverterArgs: WordConverterArguments; + wordConverterArgs: WordConverterArguments | null; /** Custom data storage for list items */ wordCustomData: WordCustomData; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts b/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts index f258861eeb4f..b54ee6452c6c 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts @@ -5,6 +5,7 @@ import { isCharacterValue, isModifierKey, PartialInlineElement, + safeInstanceOf, } from 'roosterjs-editor-dom'; import { ChangeSource, @@ -50,21 +51,19 @@ const UNIDENTIFIED_CODE = [0, 229]; * - Apply selected item in picker * * PickerPlugin doesn't provide any UI, it just wraps related DOM events and invoke callback functions. - * To show a picker UI, you need to build your own UI component. Please reference to - * https://github.com/microsoft/roosterjs/tree/master/demo/scripts/controls/samplepicker */ export default class PickerPlugin implements EditorPlugin { - private editor: IEditor; - private eventHandledOnKeyDown: boolean; - private blockSuggestions: boolean; - private isSuggesting: boolean; - private lastKnownRange: Range; + private editor: IEditor | null = null; + private eventHandledOnKeyDown: boolean = false; + private blockSuggestions: boolean = false; + private isSuggesting: boolean = false; + private lastKnownRange: Range | null = null; // For detecting backspace in Android private isPendingInputEventHandling: boolean = false; - private currentInputLength: number; - private newInputLength: number; + private currentInputLength: number = 0; + private newInputLength: number = 0; constructor(public readonly dataProvider: T, private pickerOptions: PickerPluginOptions) {} @@ -83,36 +82,38 @@ export default class PickerPlugin { - this.editor.focus(); + if (this.editor) { + this.editor.focus(); - let wordToReplace = this.getWord(null); + let wordToReplace = this.getWord(null); - // Safari drops our focus out so we get an empty word to replace when we call getWord. - // We fall back to using the lastKnownRange to try to get around this. - if ((!wordToReplace || wordToReplace.length == 0) && this.lastKnownRange) { - this.editor.select(this.lastKnownRange); - wordToReplace = this.getWord(null); - } - - let insertNode = () => { - if (wordToReplace) { - replaceWithNode( - this.editor, - wordToReplace, - htmlNode, - true /* exactMatch */ - ); - } else { - this.editor.insertNode(htmlNode); + // Safari drops our focus out so we get an empty word to replace when we call getWord. + // We fall back to using the lastKnownRange to try to get around this. + if ((!wordToReplace || wordToReplace.length == 0) && this.lastKnownRange) { + this.editor.select(this.lastKnownRange); + wordToReplace = this.getWord(null); } - this.setIsSuggesting(false); - }; - this.editor.addUndoSnapshot( - insertNode, - this.pickerOptions.changeSource, - this.pickerOptions.handleAutoComplete - ); + let insertNode = () => { + if (wordToReplace && this.editor) { + replaceWithNode( + this.editor, + wordToReplace, + htmlNode, + true /* exactMatch */ + ); + } else { + this.editor?.insertNode(htmlNode); + } + this.setIsSuggesting(false); + }; + + this.editor.addUndoSnapshot( + insertNode, + this.pickerOptions.changeSource, + this.pickerOptions.handleAutoComplete + ); + } }, (isSuggesting: boolean) => { this.setIsSuggesting(isSuggesting); @@ -126,6 +127,13 @@ export default class PickerPlugin { if (element.id) { @@ -217,7 +225,7 @@ export default class PickerPlugin { + private getRangeUntilAt(event: PluginKeyboardEvent | null): Range | null { + let positionContentSearcher = this.editor?.getContentSearcherOfCursor(event); + let startPos: NodePosition | undefined = undefined; + let endPos: NodePosition | undefined = undefined; + positionContentSearcher?.forEachTextInlineElement(textInline => { let hasMatched = false; let nodeContent = textInline.getTextContent(); let nodeIndex = nodeContent ? nodeContent.length : -1; @@ -283,7 +293,9 @@ export default class PickerPlugin 0 && - trimmedWordBeforeCursor.split(' ').length <= 4) - ) { - this.dataProvider.queryStringUpdated( - trimmedWordBeforeCursor, - wordBeforeCursorWithoutTriggerChar == trimmedWordBeforeCursor - ); - this.setLastKnownRange(this.editor.getSelectionRange()); - } else { - this.setIsSuggesting(false); - } - } else { - let wordBeforeCursor = this.getWordBeforeCursor(event); - if (!this.blockSuggestions) { - if ( - wordBeforeCursor != null && - wordBeforeCursor.split(' ').length <= 4 && - wordBeforeCursor[0] == this.pickerOptions.triggerCharacter - ) { - this.setIsSuggesting(true); + if (this.editor) { + if (this.isSuggesting) { + // Word before cursor represents the text prior to the cursor, up to and including the trigger symbol. + const wordBeforeCursor = this.getWord(event); + if (wordBeforeCursor !== null) { const wordBeforeCursorWithoutTriggerChar = wordBeforeCursor.substring(1); - let trimmedWordBeforeCursor = wordBeforeCursorWithoutTriggerChar.trim(); - this.dataProvider.queryStringUpdated( - trimmedWordBeforeCursor, - wordBeforeCursorWithoutTriggerChar == trimmedWordBeforeCursor - ); - this.setLastKnownRange(this.editor.getSelectionRange()); - if (this.dataProvider.setCursorPoint) { - // Determine the bounding rectangle for the @mention - let searcher = this.editor.getContentSearcherOfCursor(event); - let rangeNode = this.editor.getDocument().createRange(); - let nodeBeforeCursor = searcher.getInlineElementBefore().getContainerNode(); - let rangeStartSuccessfullySet = this.setRangeStart( - rangeNode, - nodeBeforeCursor, - wordBeforeCursor + const trimmedWordBeforeCursor = wordBeforeCursorWithoutTriggerChar.trim(); + + // If we hit a case where wordBeforeCursor is just the trigger character, + // that means we've gotten a onKeyUp event right after it's been typed. + // Otherwise, update the query string when: + // 1. There's an actual value + // 2. That actual value isn't just pure whitespace + // 3. That actual value isn't more than 4 words long (at which point we assume the person kept typing) + // Otherwise, we want to dismiss the picker plugin's UX. + if ( + wordBeforeCursor == this.pickerOptions.triggerCharacter || + (trimmedWordBeforeCursor && + trimmedWordBeforeCursor.length > 0 && + trimmedWordBeforeCursor.split(' ').length <= 4) + ) { + this.dataProvider.queryStringUpdated( + trimmedWordBeforeCursor, + wordBeforeCursorWithoutTriggerChar == trimmedWordBeforeCursor ); - if (!rangeStartSuccessfullySet) { - // VSO 24891: Out of range error is occurring because nodeBeforeCursor - // is not including the trigger character. In this case, the node before - // the node before cursor is the trigger character, and this is where the range should start. - let nodeBeforeNodeBeforeCursor = nodeBeforeCursor.previousSibling; - this.setRangeStart( - rangeNode, - nodeBeforeNodeBeforeCursor, - this.pickerOptions.triggerCharacter - ); - } - let rect = rangeNode.getBoundingClientRect(); - - // Safari's support for range.getBoundingClientRect is incomplete. - // We perform this check to fall back to getClientRects in case it's at the page origin. - if (rect.left == 0 && rect.bottom == 0 && rect.top == 0) { - rect = rangeNode.getClientRects()[0]; - } - - if (rect) { - rangeNode.detach(); - - // Display the @mention popup in the correct place - let targetPoint = { x: rect.left, y: (rect.bottom + rect.top) / 2 }; - let bufferZone = (rect.bottom - rect.top) / 2; - this.dataProvider.setCursorPoint(targetPoint, bufferZone); - } + this.setLastKnownRange(this.editor.getSelectionRange() ?? null); + } else { + this.setIsSuggesting(false); } } } else { - if ( - wordBeforeCursor != null && - wordBeforeCursor[0] != this.pickerOptions.triggerCharacter - ) { - this.blockSuggestions = false; + let wordBeforeCursor = this.getWordBeforeCursor(event); + if (!this.blockSuggestions) { + if ( + wordBeforeCursor != null && + wordBeforeCursor.split(' ').length <= 4 && + wordBeforeCursor[0] == this.pickerOptions.triggerCharacter + ) { + this.setIsSuggesting(true); + const wordBeforeCursorWithoutTriggerChar = wordBeforeCursor.substring(1); + let trimmedWordBeforeCursor = wordBeforeCursorWithoutTriggerChar.trim(); + this.dataProvider.queryStringUpdated( + trimmedWordBeforeCursor, + wordBeforeCursorWithoutTriggerChar == trimmedWordBeforeCursor + ); + this.setLastKnownRange(this.editor.getSelectionRange() ?? null); + if (this.dataProvider.setCursorPoint) { + // Determine the bounding rectangle for the @mention + let searcher = this.editor.getContentSearcherOfCursor(event); + let rangeNode = this.editor.getDocument().createRange(); + + if (rangeNode) { + let nodeBeforeCursor = + searcher?.getInlineElementBefore()?.getContainerNode() ?? null; + + let rangeStartSuccessfullySet = this.setRangeStart( + rangeNode, + nodeBeforeCursor, + wordBeforeCursor + ); + if (!rangeStartSuccessfullySet) { + // VSO 24891: Out of range error is occurring because nodeBeforeCursor + // is not including the trigger character. In this case, the node before + // the node before cursor is the trigger character, and this is where the range should start. + let nodeBeforeNodeBeforeCursor = + nodeBeforeCursor?.previousSibling ?? null; + this.setRangeStart( + rangeNode, + nodeBeforeNodeBeforeCursor, + this.pickerOptions.triggerCharacter + ); + } + let rect = rangeNode.getBoundingClientRect(); + + // Safari's support for range.getBoundingClientRect is incomplete. + // We perform this check to fall back to getClientRects in case it's at the page origin. + if (rect.left == 0 && rect.bottom == 0 && rect.top == 0) { + rect = rangeNode.getClientRects()[0]; + } + + if (rect) { + rangeNode.detach(); + + // Display the @mention popup in the correct place + let targetPoint = { + x: rect.left, + y: (rect.bottom + rect.top) / 2, + }; + let bufferZone = (rect.bottom - rect.top) / 2; + this.dataProvider.setCursorPoint(targetPoint, bufferZone); + } + } + } + } + } else { + if ( + wordBeforeCursor != null && + wordBeforeCursor[0] != this.pickerOptions.triggerCharacter + ) { + this.blockSuggestions = false; + } } } } @@ -440,15 +465,21 @@ export default class PickerPlugin -1) { + private setRangeStart(rangeNode: Range, node: Node | null, target: string) { + let nodeOffset = node?.textContent ? node.textContent.lastIndexOf(target) : -1; + if (node && nodeOffset > -1) { rangeNode.setStart(node, nodeOffset); return true; } @@ -530,7 +573,7 @@ export default class PickerPlugin { - if (cell?.td) { - deleteNodeContents(cell.td, this.editor); - } - }); - } - clearSelectedTables(event.clonedRoot); - } - } - - //#region Key events - /** - * Handles the on key event. - * @param event the plugin event - */ - private handleKeyDownEvent(event: PluginKeyDownEvent) { - const { shiftKey, ctrlKey, metaKey, which } = event.rawEvent; - if ((shiftKey && (ctrlKey || metaKey)) || which == Keys.SHIFT) { - return; - } - - if (shiftKey) { - if (!this.firstTarget) { - const pos = this.editor.getFocusedPosition(); - - const cell = getCellAtCursor(this.editor, pos.node); - - this.firstTarget = this.firstTarget || cell; - } - - //If first target is not a table cell, we should ignore this plugin - if (!safeInstanceOf(this.firstTarget, 'HTMLTableCellElement')) { - return; - } - this.editor.runAsync(editor => { - const pos = editor.getFocusedPosition(); - this.setData(this.tableSelection ? this.lastTarget : pos.node); - - if (this.firstTable! == this.targetTable!) { - if (!this.shouldConvertToTableSelection() && !this.tableSelection) { - return; - } - //When selection start and end is inside of the same table - this.handleKeySelectionInsideTable(event); - } else if (this.tableSelection) { - clearSelectedTableCells(this.editor); - this.tableSelection = false; - } - }); - } - } - - private handleKeyUpEvent(event: PluginKeyUpEvent) { - const { shiftKey, which } = event.rawEvent; - if (!shiftKey && which != Keys.SHIFT && this.firstTarget) { - this.clearState(); - } - } - - private handleKeySelectionInsideTable(event: PluginKeyDownEvent) { - this.firstTarget = getCellAtCursor(this.editor, this.firstTarget); - this.lastTarget = getCellAtCursor(this.editor, this.lastTarget); - - updateSelection(this.editor, this.firstTarget, 0); - this.vTable = this.vTable || new VTable(this.firstTable as HTMLTableElement); - this.tableRange.firstCell = getCellCoordinates(this.vTable, this.firstTarget as Element); - this.tableRange.lastCell = this.getNextTD(event); - - if ( - !this.tableRange.lastCell || - this.tableRange.lastCell.y > this.vTable.cells.length - 1 || - this.tableRange.lastCell.y == -1 - ) { - //When selection is moving from inside of a table to outside - this.lastTarget = this.editor.getElementAtCursor( - TABLE_CELL_SELECTOR + ',div', - this.firstTable - ); - if (safeInstanceOf(this.lastTarget, 'HTMLTableCellElement')) { - this.prepareSelection(); - } else { - const position = new Position( - this.targetTable, - this.tableRange.lastCell.y == null || this.tableRange.lastCell.y == -1 - ? PositionType.Before - : PositionType.After - ); - - const sel = this.editor.getDocument().defaultView.getSelection(); - const { anchorNode, anchorOffset } = sel; - sel.setBaseAndExtent(anchorNode, anchorOffset, position.node, position.offset); - this.lastTarget = position.node; - event.rawEvent.preventDefault(); - return; - } - } - - this.vTable.selection = this.tableRange; - highlight(this.vTable); - - const isBeginAboveEnd = this.isAfter(this.firstTarget, this.lastTarget); - const targetPosition = new Position( - this.lastTarget, - isBeginAboveEnd ? PositionType.Begin : PositionType.End - ); - updateSelection(this.editor, targetPosition.node, targetPosition.offset); - - this.tableSelection = true; - event.rawEvent.preventDefault(); - } - //#endregion - - //#region Mouse events - private handleMouseDownEvent(event: PluginMouseDownEvent) { - const { which, shiftKey } = event.rawEvent; - - if (which == RIGHT_CLICK && this.tableSelection) { - //If the user is right clicking To open context menu - const td = this.editor.getElementAtCursor(TABLE_CELL_SELECTOR); - if (td?.classList.contains(TABLE_CELL_SELECTED)) { - this.firstTarget = null; - this.lastTarget = null; - - this.editor.queryElements('td.' + TABLE_CELL_SELECTED, node => { - this.firstTarget = this.firstTarget || node; - this.lastTarget = node; - }); - const selection = this.editor.getDocument().defaultView.getSelection(); - selection.setBaseAndExtent(this.firstTarget, 0, this.lastTarget, 0); - highlight(this.vTable); - return; - } - } - this.editor.getDocument().addEventListener('mouseup', this.onMouseUp, true /*setCapture*/); - if (which == LEFT_CLICK && !shiftKey) { - this.clearState(); - this.editor - .getDocument() - .addEventListener('mousemove', this.onMouseMove, true /*setCapture*/); - this.startedSelection = true; - } - - if (which == LEFT_CLICK && shiftKey) { - this.editor.runAsync(editor => { - const sel = editor.getDocument().defaultView.getSelection(); - const first = getCellAtCursor(editor, sel.anchorNode); - const last = getCellAtCursor(editor, sel.focusNode); - const firstTable = getTableAtCursor(editor, first); - const targetTable = getTableAtCursor(editor, first); - if ( - firstTable! == targetTable! && - safeInstanceOf(first, 'HTMLTableCellElement') && - safeInstanceOf(last, 'HTMLTableCellElement') - ) { - this.vTable = new VTable(first); - const firstCord = getCellCoordinates(this.vTable, first); - const lastCord = getCellCoordinates(this.vTable, last); - - this.vTable.selection = { - firstCell: firstCord, - lastCell: lastCord, - }; - - this.firstTarget = first; - this.lastTarget = last; - highlight(this.vTable); - this.tableRange = this.vTable.selection; - this.tableSelection = true; - this.firstTable = firstTable as HTMLTableElement; - this.targetTable = targetTable; - updateSelection(editor, first, 0); - } - }); - } - } - - private onMouseMove = (event: MouseEvent) => { - if (!this.editor.contains(event.target as Node)) { - return; - } - - //If already in table selection and the new target is contained in the last target cell, no need to - //Apply selection styles again. - if (this.tableSelection && contains(this.lastTarget, event.target as Node, true)) { - updateSelection(this.editor, this.firstTarget, 0); - event.preventDefault(); - return; - } - - if (getTagOfNode(event.target as Node) == 'TABLE') { - event.preventDefault(); - return; - } - - this.setData(event.target as Node); - - // If there is a first target, but is not inside a table, no more actions to perform. - if (this.firstTarget && !this.firstTable) { - return; - } - - //Ignore if - // Is a DIV that only contains a Table - // If the event target is not contained in the editor. - if ( - (this.lastTarget.lastChild == this.lastTarget.firstChild && - getTagOfNode(this.lastTarget.lastChild) == 'TABLE' && - getTagOfNode(this.lastTarget) == 'DIV') || - !this.editor.contains(this.lastTarget) - ) { - event.preventDefault(); - return; - } - - this.prepareSelection(); - const isNewTDContainingFirstTable = safeInstanceOf(this.lastTarget, 'HTMLTableCellElement') - ? contains(this.lastTarget, this.firstTable) - : false; - - if ( - (this.firstTable && this.firstTable == this.targetTable) || - isNewTDContainingFirstTable - ) { - //When starting selection inside of a table and ends inside of the same table. - this.selectionInsideTableMouseMove(event); - } else if (this.tableSelection) { - this.restoreSelection(); - } - - if (this.tableSelection) { - updateSelection(this.editor, this.firstTarget, 0); - event.preventDefault(); - } - }; - - private onMouseUp = () => { - if (this.editor) { - this.removeMouseUpEventListener(); - } - }; - - private restoreSelection() { - clearSelectedTableCells(this.editor); - this.tableSelection = false; - const isBeginAboveEnd = this.isAfter(this.firstTarget, this.lastTarget); - const targetPosition = new Position( - this.lastTarget, - isBeginAboveEnd ? PositionType.End : PositionType.Begin - ); - - const firstTargetRange = new Range(); - if (this.firstTarget) { - firstTargetRange.selectNodeContents(this.firstTarget); - } - updateSelection( - this.editor, - this.firstTarget, - isBeginAboveEnd - ? Position.getEnd(firstTargetRange).offset - : Position.getStart(firstTargetRange).offset, - targetPosition.element, - targetPosition.offset - ); - } - - /** - * @internal - * Public only for unit testing - * @param event mouse event - */ - selectionInsideTableMouseMove(event: MouseEvent) { - if (this.lastTarget != this.firstTarget) { - updateSelection(this.editor, this.firstTarget, 0); - if ( - this.firstTable != this.targetTable && - this.targetTable?.contains(this.firstTable) - ) { - //If selection started in a table that is inside of another table and moves to parent table - //Make the firstTarget the TD of the parent table. - this.firstTarget = this.editor.getElementAtCursor( - TABLE_CELL_SELECTOR, - this.lastTarget - ); - (this.firstTarget as HTMLElement).querySelectorAll('table').forEach(table => { - const vTable = new VTable(table); - highlightAll(vTable); - }); - } - - if (this.firstTable) { - this.tableSelection = true; - if ( - this.vTable?.table != this.firstTable && - safeInstanceOf(this.firstTarget, 'HTMLTableCellElement') - ) { - this.vTable = new VTable(this.firstTarget); - } - - this.tableRange.firstCell = getCellCoordinates(this.vTable, this.firstTarget); - this.tableRange.lastCell = getCellCoordinates(this.vTable, this.lastTarget); - this.vTable.selection = this.tableRange; - highlight(this.vTable); - } - - event.preventDefault(); - } else if (this.lastTarget == this.firstTarget && this.tableSelection) { - this.vTable = new VTable(this.firstTable); - this.tableRange.firstCell = getCellCoordinates(this.vTable, this.firstTarget); - this.tableRange.lastCell = this.tableRange.firstCell; - - this.vTable.selection = this.tableRange; - highlight(this.vTable); - - this.tableRange = this.vTable.selection; - } - } - - private removeMouseUpEventListener(): void { - if (this.startedSelection) { - this.startedSelection = false; - this.editor.getDocument().removeEventListener('mouseup', this.onMouseUp, true); - this.editor.getDocument().removeEventListener('mousemove', this.onMouseMove, true); - } - } - //#endregion - - //#region Content Edit Features - - /** - * When press Backspace, delete the contents inside of the selection, if it is Table Selection - */ - DeleteTableContents: BuildInEditFeature = { - keys: [Keys.DELETE, Keys.BACKSPACE], - shouldHandleEvent: (_, editor) => { - const selection = editor.getSelectionRangeEx(); - return selection.type == SelectionRangeTypes.TableSelection; - }, - handleEvent: (_, editor) => { - const selection = editor.getSelectionRangeEx(); - if (selection.type == SelectionRangeTypes.TableSelection) { - editor.addUndoSnapshot(() => { - editor.getSelectedRegions().forEach(region => { - if (safeInstanceOf(region.rootNode, 'HTMLTableCellElement')) { - deleteNodeContents(region.rootNode, editor); - } - }); - }); - } - }, - }; - //#endregion - - //#region utils - private clearTableCellSelection() { - if (this.editor?.hasFocus()) { - clearSelectedTableCells(this.editor); - } - } - - private clearState() { - this.clearTableCellSelection(); - this.vTable = null; - this.firstTarget = null; - this.lastTarget = null; - this.tableRange = { - firstCell: null, - lastCell: null, - }; - this.tableSelection = false; - this.firstTable = null; - this.targetTable = null; - } - - private getNextTD(event: PluginKeyDownEvent): Coordinates { - this.lastTarget = this.editor.getElementAtCursor(TABLE_CELL_SELECTOR, this.lastTarget); - - if (safeInstanceOf(this.lastTarget, 'HTMLTableCellElement')) { - let coordinates = getCellCoordinates(this.vTable, this.lastTarget); - - if (this.tableSelection) { - switch (event.rawEvent.which) { - case Keys.RIGHT: - coordinates.x += this.lastTarget.colSpan; - if (this.vTable.cells[coordinates.y][coordinates.x] == null) { - coordinates.x = this.vTable.cells[coordinates.y].length - 1; - coordinates.y++; - } - break; - case Keys.LEFT: - if (coordinates.x == 0) { - coordinates.y--; - } else { - coordinates.x--; - } - break; - case Keys.UP: - coordinates.y--; - break; - case Keys.DOWN: - coordinates.y++; - break; - } - } - - if (coordinates.y >= 0 && coordinates.x >= 0) { - this.lastTarget = this.vTable.getTd(coordinates.y, coordinates.x); - } - return coordinates; - } - return null; - } - - //Check if the selection started in a inner table. - private prepareSelection() { - let isNewTargetTableContained = - this.lastTarget != this.firstTarget && - this.firstTable?.contains( - findClosestElementAncestor(this.targetTable, this.firstTable, TABLE_CELL_SELECTOR) - ); - - if (isNewTargetTableContained && this.tableSelection) { - while (isNewTargetTableContained) { - this.lastTarget = findClosestElementAncestor( - this.targetTable, - this.firstTable, - TABLE_CELL_SELECTOR - ); - this.targetTable = getTableAtCursor(this.editor, this.lastTarget); - isNewTargetTableContained = - this.lastTarget != this.firstTarget && - this.firstTable?.contains( - findClosestElementAncestor( - this.targetTable, - this.firstTable, - TABLE_CELL_SELECTOR - ) - ); - } - } - - let isFirstTargetTableContained = - this.lastTarget != this.firstTarget && - this.targetTable?.contains( - findClosestElementAncestor(this.firstTable, this.targetTable, TABLE_CELL_SELECTOR) - ); - - if (isFirstTargetTableContained && this.tableSelection) { - while (isFirstTargetTableContained) { - this.firstTarget = findClosestElementAncestor( - this.firstTable, - this.targetTable, - TABLE_CELL_SELECTOR - ); - this.firstTable = this.editor.getElementAtCursor( - 'table', - this.firstTarget - ) as HTMLTableElement; - isFirstTargetTableContained = - this.lastTarget != this.firstTarget && - this.targetTable?.contains( - findClosestElementAncestor( - this.firstTable, - this.targetTable, - TABLE_CELL_SELECTOR - ) - ); - } - } - } - - private setData(eventTarget: Node) { - const pos = this.editor.getFocusedPosition(); - if (pos) { - this.firstTarget = this.firstTarget || getCellAtCursor(this.editor, pos.node); - - if (this.firstTarget.nodeType == Node.TEXT_NODE) { - this.firstTarget = this.editor.getElementAtCursor( - TABLE_CELL_SELECTOR, - this.firstTarget - ); - } - if (!this.editor.contains(this.firstTarget) && this.lastTarget) { - this.firstTarget = this.lastTarget; - } - } - - this.firstTable = getTableAtCursor(this.editor, this.firstTarget) as HTMLTableElement; - this.lastTarget = getCellAtCursor(this.editor, eventTarget as Node); - this.targetTable = getTableAtCursor(this.editor, this.lastTarget); - } - - private isAfter(node1: Node, node2: Node) { - if (node1 && node2) { - if (node2.contains(node1)) { - const r1 = (node1 as Element).getBoundingClientRect?.(); - const r2 = (node2 as Element).getBoundingClientRect?.(); - if (r1 && r2) { - return r1.top > r2.top && r1.bottom < r2.bottom; - } + private handleLeavingShadowEdit(state: TableCellSelectionState, editor: IEditor) { + if (state.firstTable && state.tableSelection && state.firstTable) { + const table = editor.queryElements('#' + state.firstTable.id); + if (table.length == 1) { + state.firstTable = table[0] as HTMLTableElement; + editor.select(state.firstTable, this.shadowEditCoordinatesBackup); + this.shadowEditCoordinatesBackup = null; } - - const position = new Position(node1, PositionType.End); - return position.isAfter(new Position(node2, PositionType.End)); } - return false; } - // if the user selected all the text in a cell and started selecting another TD, we should convert to vSelection - private shouldConvertToTableSelection() { - if (!this.firstTable || !this.editor) { - return false; - } - const regions = this.editor.getSelectedRegions(); - if (regions.length == 1) { - return false; + private handleEnteredShadowEdit(state: TableCellSelectionState, editor: IEditor) { + const selection = editor.getSelectionRangeEx(); + if (selection.type == SelectionRangeTypes.TableSelection) { + this.shadowEditCoordinatesBackup = selection.coordinates ?? null; + state.firstTable = selection.table; + state.tableSelection = true; + editor.select(selection.table, null); } - - let result = true; - - regions.forEach(value => { - if (!contains(this.firstTable, value.rootNode)) { - result = false; - } - }); - - return result; - } - //#endregion -} - -function deleteNodeContents(element: HTMLElement, editor: IEditor) { - const range = new Range(); - range.selectNodeContents(element); - range.deleteContents(); - element.appendChild(editor.getDocument().createElement('br')); -} - -function updateSelection( - editor: IEditor, - start: Node, - offset: number, - end?: Node, - endOffset?: number -) { - const selection = editor.getDocument().defaultView.getSelection(); - end = end || start; - endOffset = endOffset || offset; - selection.setBaseAndExtent(start, offset, end, endOffset); -} - -function getCellAtCursor(editor: IEditor, node: Node) { - if (editor) { - return editor.getElementAtCursor(TABLE_CELL_SELECTOR, node) || (node as HTMLElement); - } - return node as HTMLElement; -} - -function getTableAtCursor(editor: IEditor, node: Node) { - if (editor) { - return editor.getElementAtCursor('table', node); - } - return null; -} - -function clearSelectedTableCells(input: IEditor) { - input.queryElements('table.' + TABLE_SELECTED, deselectTable); -} - -function clearSelectedTables(element: HTMLElement) { - element.querySelectorAll('table.' + TABLE_SELECTED).forEach(deselectTable); -} - -function deselectTable(element: HTMLElement) { - if (safeInstanceOf(element, 'HTMLTableElement')) { - const vTable = new VTable(element); - deSelectAll(vTable); } } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/TableCellSelectionState.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/TableCellSelectionState.ts new file mode 100644 index 000000000000..500dd18fb306 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/TableCellSelectionState.ts @@ -0,0 +1,21 @@ +import { VTable } from 'roosterjs-editor-dom'; + +/** + * @internal + */ +export type Nullable = T | null | undefined; + +/** + * @internal + */ +export interface TableCellSelectionState { + lastTarget: Nullable; + firstTarget: Nullable; + tableSelection: Nullable; + startedSelection: Nullable; + vTable: Nullable; + firstTable: Nullable; + targetTable: Nullable; + preventKeyUp: boolean; + mouseMoveDisposer: (() => void) | null; +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/constants.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/constants.ts new file mode 100644 index 000000000000..51a07c3c5b1b --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/constants.ts @@ -0,0 +1,5 @@ +/** + * @internal + * Table cell query selector + */ +export const TABLE_CELL_SELECTOR = 'td,th'; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/features/DeleteTableContents.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/features/DeleteTableContents.ts new file mode 100644 index 000000000000..0d275e9e8d84 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/features/DeleteTableContents.ts @@ -0,0 +1,39 @@ +import { safeInstanceOf } from 'roosterjs-editor-dom'; +import { + GenericContentEditFeature, + IEditor, + Keys, + PluginEvent, + SelectionRangeTypes, +} from 'roosterjs-editor-types'; + +/** + * @internal + * Feature that when Backspace is pressed and there is Table Selection, delete the contents inside of the selection + */ +export const DeleteTableContents: GenericContentEditFeature = { + keys: [Keys.DELETE, Keys.BACKSPACE], + shouldHandleEvent: (_, editor: IEditor) => { + const selection = editor.getSelectionRangeEx(); + return selection.type == SelectionRangeTypes.TableSelection; + }, + handleEvent: (_, editor) => { + const selection = editor.getSelectionRangeEx(); + if (selection.type == SelectionRangeTypes.TableSelection) { + editor.addUndoSnapshot(() => { + editor.getSelectedRegions().forEach(region => { + if (safeInstanceOf(region.rootNode, 'HTMLTableCellElement')) { + deleteNodeContents(region.rootNode, editor); + } + }); + }); + } + }, +}; + +function deleteNodeContents(element: HTMLElement, editor: IEditor) { + const range = new Range(); + range.selectNodeContents(element); + range.deleteContents(); + element.appendChild(editor.getDocument().createElement('br')); +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/keyUtils/handleKeyDownEvent.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/keyUtils/handleKeyDownEvent.ts new file mode 100644 index 000000000000..e6e1354c0d23 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/keyUtils/handleKeyDownEvent.ts @@ -0,0 +1,224 @@ +import { getCellAtCursor } from '../utils/getCellAtCursor'; +import { getCellCoordinates } from '../utils/getCellCoordinates'; +import { isAfter } from '../utils/isAfter'; +import { prepareSelection } from '../utils/prepareSelection'; +import { selectTable } from '../utils/selectTable'; +import { setData } from '../utils/setData'; +import { TABLE_CELL_SELECTOR } from '../constants'; +import { TableCellSelectionState } from '../TableCellSelectionState'; +import { updateSelection } from '../utils/updateSelection'; +import { + contains, + isCtrlOrMetaPressed, + Position, + safeInstanceOf, + VTable, +} from 'roosterjs-editor-dom'; +import { + Coordinates, + IEditor, + Keys, + PluginKeyDownEvent, + PositionType, + SelectionRangeTypes, +} from 'roosterjs-editor-types'; + +/** + * @internal + */ +export function handleKeyDownEvent( + event: PluginKeyDownEvent, + state: TableCellSelectionState, + editor: IEditor +) { + const { shiftKey, ctrlKey, metaKey, which, defaultPrevented } = event.rawEvent; + if ((shiftKey && (ctrlKey || metaKey)) || which == Keys.SHIFT || defaultPrevented) { + state.preventKeyUp = defaultPrevented; + return; + } + + if (shiftKey) { + if (!state.firstTarget) { + const pos = editor.getFocusedPosition(); + const cell = pos && getCellAtCursor(editor, pos.node); + + state.firstTarget = cell; + } + + //If first target is not a table cell, we should ignore this plugin + if (!safeInstanceOf(state.firstTarget, 'HTMLTableCellElement')) { + return; + } + editor.runAsync(editor => { + const pos = editor.getFocusedPosition(); + const newTarget = state.tableSelection ? state.lastTarget : pos?.node; + if (newTarget) { + setData(newTarget, state, editor); + } + + if (state.firstTable! == state.targetTable!) { + if (!shouldConvertToTableSelection(state, editor) && !state.tableSelection) { + return; + } + //When selection start and end is inside of the same table + handleKeySelectionInsideTable(event, state, editor); + } else if (state.tableSelection) { + if (state.firstTable) { + editor.select(state.firstTable, null /* coordinates */); + } + state.tableSelection = false; + } + }); + } else if ( + editor.getSelectionRangeEx()?.type == SelectionRangeTypes.TableSelection && + (!isCtrlOrMetaPressed(event.rawEvent) || which == Keys.HOME || which == Keys.END) + ) { + editor.select(null); + } +} + +/** + * @internal + */ +function handleKeySelectionInsideTable( + event: PluginKeyDownEvent, + state: TableCellSelectionState, + editor: IEditor +) { + state.firstTarget = getCellAtCursor(editor, state.firstTarget); + state.lastTarget = getCellAtCursor(editor, state.lastTarget); + + updateSelection(editor, state.firstTarget, 0); + state.vTable = state.vTable || new VTable(state.firstTable as HTMLTableElement); + + const firstCell = getCellCoordinates(state.vTable, state.firstTarget as Element); + const lastCell = getNextTD(event, editor, state); + + if (!firstCell || !lastCell) { + return; + } + state.vTable.selection = { + firstCell, + lastCell, + }; + + const { selection } = state.vTable; + + if ( + !selection.lastCell || + (state.vTable.cells && selection.lastCell.y > state.vTable.cells.length - 1) || + selection.lastCell.y == -1 + ) { + //When selection is moving from inside of a table to outside + state.lastTarget = editor.getElementAtCursor( + TABLE_CELL_SELECTOR + ',div', + state.firstTable ?? undefined + ); + if (safeInstanceOf(state.lastTarget, 'HTMLTableCellElement')) { + prepareSelection(state, editor); + } else { + const position = + state.targetTable && + new Position( + state.targetTable, + selection.lastCell.y == null || selection.lastCell.y == -1 + ? PositionType.Before + : PositionType.After + ); + + const sel = editor.getDocument().defaultView?.getSelection(); + const { anchorNode, anchorOffset } = sel || {}; + if ( + sel && + anchorNode && + anchorOffset != undefined && + anchorOffset != null && + position + ) { + editor.select(sel.getRangeAt(0)); + sel.setBaseAndExtent(anchorNode, anchorOffset, position.node, position.offset); + state.lastTarget = position.node; + event.rawEvent.preventDefault(); + return; + } + } + } + + selectTable(editor, state); + + const isBeginAboveEnd = isAfter(state.firstTarget, state.lastTarget); + if (state.lastTarget) { + const targetPosition = new Position( + state.lastTarget, + isBeginAboveEnd ? PositionType.Begin : PositionType.End + ); + updateSelection(editor, targetPosition.node, targetPosition.offset); + } + + state.tableSelection = true; + event.rawEvent.preventDefault(); +} + +function getNextTD( + event: PluginKeyDownEvent, + editor: IEditor, + state: TableCellSelectionState +): Coordinates | undefined { + state.lastTarget = + state.lastTarget && editor.getElementAtCursor(TABLE_CELL_SELECTOR, state.lastTarget); + + if (safeInstanceOf(state.lastTarget, 'HTMLTableCellElement') && state.vTable?.cells) { + let coordinates = getCellCoordinates(state.vTable, state.lastTarget); + + if (state.tableSelection && coordinates) { + switch (event.rawEvent.which) { + case Keys.RIGHT: + coordinates.x += state.lastTarget.colSpan; + if (state.vTable.cells[coordinates.y][coordinates.x] == null) { + coordinates.x = state.vTable.cells[coordinates.y].length - 1; + coordinates.y++; + } + break; + case Keys.LEFT: + if (coordinates.x == 0) { + coordinates.y--; + } else { + coordinates.x--; + } + break; + case Keys.UP: + coordinates.y--; + break; + case Keys.DOWN: + coordinates.y++; + break; + } + } + + if (coordinates && coordinates.y >= 0 && coordinates.x >= 0) { + state.lastTarget = state.vTable.getTd(coordinates.y, coordinates.x); + } + return coordinates; + } + return undefined; +} + +function shouldConvertToTableSelection(state: TableCellSelectionState, editor: IEditor) { + if (!state.firstTable || !editor) { + return false; + } + const regions = editor.getSelectedRegions(); + if (regions.length == 1) { + return false; + } + + let result = true; + + regions.forEach(value => { + if (!contains(state.firstTable, value.rootNode)) { + result = false; + } + }); + + return result; +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/keyUtils/handleKeyUpEvent.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/keyUtils/handleKeyUpEvent.ts new file mode 100644 index 000000000000..617c86b4ca50 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/keyUtils/handleKeyUpEvent.ts @@ -0,0 +1,32 @@ +import { clearState } from '../utils/clearState'; +import { IEditor, Keys, PluginKeyUpEvent } from 'roosterjs-editor-types'; +import { TableCellSelectionState } from '../TableCellSelectionState'; + +const IGNORE_KEY_UP_KEYS = [ + Keys.SHIFT, + Keys.ALT, + Keys.META_LEFT, + Keys.CTRL_LEFT, + Keys.PRINT_SCREEN, +]; + +/** + * @internal + */ +export function handleKeyUpEvent( + event: PluginKeyUpEvent, + state: TableCellSelectionState, + editor: IEditor +) { + const { shiftKey, which, ctrlKey } = event.rawEvent; + if ( + !shiftKey && + !ctrlKey && + state.firstTarget && + !state.preventKeyUp && + IGNORE_KEY_UP_KEYS.indexOf(which) == -1 + ) { + clearState(state, editor); + } + state.preventKeyUp = false; +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/mouseUtils/handleMouseDownEvent.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/mouseUtils/handleMouseDownEvent.ts new file mode 100644 index 000000000000..91e9f6f3b7bd --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/mouseUtils/handleMouseDownEvent.ts @@ -0,0 +1,253 @@ +import normalizeTableSelection from '../utils/normalizeTableSelection'; +import { clearState } from '../utils/clearState'; +import { contains, getTagOfNode, safeInstanceOf, VTable } from 'roosterjs-editor-dom'; +import { getCellAtCursor } from '../utils/getCellAtCursor'; +import { getCellCoordinates } from '../utils/getCellCoordinates'; +import { getTableAtCursor } from '../utils/getTableAtCursor'; +import { IEditor, PluginMouseDownEvent } from 'roosterjs-editor-types'; +import { prepareSelection } from '../utils/prepareSelection'; +import { restoreSelection } from '../utils/restoreSelection'; +import { selectTable } from '../utils/selectTable'; +import { setData } from '../utils/setData'; +import { TABLE_CELL_SELECTOR } from '../constants'; +import { TableCellSelectionState } from '../TableCellSelectionState'; +import { updateSelection } from '../utils/updateSelection'; + +const LEFT_CLICK = 1; +const RIGHT_CLICK = 3; + +/** + * @internal + */ +export function handleMouseDownEvent( + event: PluginMouseDownEvent, + state: TableCellSelectionState, + editor: IEditor +) { + const { which, shiftKey } = event.rawEvent; + + const td = editor.getElementAtCursor(TABLE_CELL_SELECTOR); + if (which == RIGHT_CLICK && state.tableSelection && state.vTable && td) { + //If the user is right clicking To open context menu + const coord = getCellCoordinates(state.vTable, td); + if (coord) { + const { firstCell, lastCell } = normalizeTableSelection(state.vTable) || {}; + if ( + firstCell && + lastCell && + coord.y >= firstCell.y && + coord.y <= lastCell.y && + coord.x >= firstCell.x && + coord.x <= lastCell.x + ) { + state.firstTarget = state.vTable.getCell(firstCell.y, firstCell.x).td; + state.lastTarget = state.vTable.getCell(lastCell.y, lastCell.x).td; + + if (state.firstTarget && state.lastTarget) { + const selection = editor.getDocument().defaultView?.getSelection(); + selection?.setBaseAndExtent(state.firstTarget, 0, state.lastTarget, 0); + selectTable(editor, state); + } + + return; + } + } + } + if (which == LEFT_CLICK && !shiftKey) { + clearState(state, editor); + + if (getTableAtCursor(editor, event.rawEvent.target)) { + const doc = editor.getDocument() || document; + + const mouseUpListener = getOnMouseUp(state); + const mouseMoveListener = onMouseMove(state, editor); + doc.addEventListener('mouseup', mouseUpListener, true /*setCapture*/); + doc.addEventListener('mousemove', mouseMoveListener, true /*setCapture*/); + + state.mouseMoveDisposer = () => { + doc.removeEventListener('mouseup', mouseUpListener, true /*setCapture*/); + doc.removeEventListener('mousemove', mouseMoveListener, true /*setCapture*/); + }; + + state.startedSelection = true; + } + } + + if (which == LEFT_CLICK && shiftKey) { + editor.runAsync(editor => { + const sel = editor.getDocument().defaultView?.getSelection(); + const first = getCellAtCursor(editor, sel?.anchorNode); + const last = getCellAtCursor(editor, sel?.focusNode); + const firstTable = getTableAtCursor(editor, first); + const targetTable = getTableAtCursor(editor, first); + if ( + firstTable! == targetTable! && + safeInstanceOf(first, 'HTMLTableCellElement') && + safeInstanceOf(last, 'HTMLTableCellElement') + ) { + state.vTable = new VTable(first); + const firstCord = getCellCoordinates(state.vTable, first); + const lastCord = getCellCoordinates(state.vTable, last); + + if (!firstCord || !lastCord) { + return; + } + state.vTable.selection = { + firstCell: firstCord, + lastCell: lastCord, + }; + + state.firstTarget = first; + state.lastTarget = last; + selectTable(editor, state); + + state.tableSelection = true; + state.firstTable = firstTable as HTMLTableElement; + state.targetTable = targetTable; + updateSelection(editor, first, 0); + } + }); + } +} + +function getOnMouseUp(state: TableCellSelectionState) { + return () => { + removeMouseUpEventListener(state); + }; +} + +function onMouseMove(state: TableCellSelectionState, editor: IEditor) { + return (event: MouseEvent) => { + if (!editor.contains(event.target as Node)) { + return; + } + + //If already in table selection and the new target is contained in the last target cell, no need to + //Apply selection styles again. + if ( + state.tableSelection && + state.firstTarget && + contains(state.lastTarget, event.target as Node, true) + ) { + updateSelection(editor, state.firstTarget, 0); + event.preventDefault(); + return; + } + + if (getTagOfNode(event.target as Node) == 'TABLE') { + event.preventDefault(); + return; + } + + setData(event.target as Node, state, editor); + + // If there is a first target, but is not inside a table, no more actions to perform. + if (state.firstTarget && !state.firstTable) { + return; + } + + //Ignore if + // Is a DIV that only contains a Table + // If the event target is not contained in the editor. + if ( + state.lastTarget && + ((state.lastTarget.lastChild == state.lastTarget.firstChild && + getTagOfNode(state.lastTarget.lastChild) == 'TABLE' && + getTagOfNode(state.lastTarget) == 'DIV') || + !editor.contains(state.lastTarget)) + ) { + event.preventDefault(); + return; + } + + prepareSelection(state, editor); + const isNewTDContainingFirstTable = safeInstanceOf(state.lastTarget, 'HTMLTableCellElement') + ? contains(state.lastTarget, state.firstTable) + : false; + + if ( + (state.firstTable && state.firstTable == state.targetTable) || + isNewTDContainingFirstTable + ) { + //When starting selection inside of a table and ends inside of the same table. + selectionInsideTableMouseMove(event, state, editor); + } else if (state.tableSelection) { + restoreSelection(state, editor); + } + + if (state.tableSelection && state.firstTarget) { + updateSelection(editor, state.firstTarget, 0); + event.preventDefault(); + } + }; +} + +/** + * @internal + */ +export function selectionInsideTableMouseMove( + event: MouseEvent, + state: TableCellSelectionState, + editor: IEditor +) { + if ( + state.firstTarget && + state.firstTable && + state.lastTarget != state.firstTarget && + state.lastTarget + ) { + updateSelection(editor, state.firstTarget, 0); + if ( + state.firstTable != state.targetTable && + state.targetTable?.contains(state.firstTable) + ) { + //If selection started in a table that is inside of another table and moves to parent table + //Make the firstTarget the TD of the parent table. + state.firstTarget = editor.getElementAtCursor(TABLE_CELL_SELECTOR, state.lastTarget); + } + + if (state.firstTable && state.firstTarget) { + state.tableSelection = true; + + state.vTable = state.vTable || new VTable(state.firstTable); + + const firstCell = getCellCoordinates(state.vTable, state.firstTarget); + const lastCell = getCellCoordinates(state.vTable, state.lastTarget); + + if (!firstCell || !lastCell) { + return; + } + + state.vTable.selection = { + firstCell, + lastCell, + }; + selectTable(editor, state); + } + + event.preventDefault(); + } else if ( + state.lastTarget == state.firstTarget && + state.tableSelection && + state.firstTable && + state.firstTarget + ) { + state.vTable = new VTable(state.firstTable); + const cell = getCellCoordinates(state.vTable, state.firstTarget); + if (cell) { + state.vTable.selection = { + firstCell: cell, + lastCell: cell, + }; + } + + selectTable(editor, state); + } +} + +function removeMouseUpEventListener(state: TableCellSelectionState): void { + if (state.startedSelection) { + state.startedSelection = false; + state.mouseMoveDisposer?.(); + } +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/mouseUtils/handleScrollEvent.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/mouseUtils/handleScrollEvent.ts new file mode 100644 index 000000000000..7c61ce3767e2 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/mouseUtils/handleScrollEvent.ts @@ -0,0 +1,36 @@ +import { getCellCoordinates } from '../utils/getCellCoordinates'; +import { IEditor } from 'roosterjs-editor-types'; +import { restoreSelection } from '../utils/restoreSelection'; +import { selectTable } from '../utils/selectTable'; +import { setData } from '../utils/setData'; +import { TableCellSelectionState } from '../TableCellSelectionState'; +import { updateSelection } from '../utils/updateSelection'; + +/** + * Handle Scroll Event and mantains the selection range, + * Since when we scroll the cursor does not trigger the on Mouse Move event + * The table selection gets removed. + */ +export function handleScrollEvent(state: TableCellSelectionState, editor: IEditor) { + const eventTarget = editor.getElementAtCursor(); + if (!eventTarget) { + return; + } + setData(eventTarget, state, editor); + if ( + state.firstTable == state.targetTable && + state.firstTarget && + state.vTable?.selection && + state.lastTarget && + state.tableSelection + ) { + const newCell = getCellCoordinates(state.vTable, state.lastTarget); + if (newCell) { + state.vTable.selection.lastCell = newCell; + selectTable(editor, state); + updateSelection(editor, state.firstTarget, 0); + } + } else if (state.tableSelection) { + restoreSelection(state, editor); + } +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/clearState.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/clearState.ts new file mode 100644 index 000000000000..92079c9b9ce3 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/clearState.ts @@ -0,0 +1,19 @@ +import { IEditor } from 'roosterjs-editor-types'; +import { TableCellSelectionState } from '../TableCellSelectionState'; + +/** + * @internal + */ +export function clearState(state: TableCellSelectionState | null, editor: IEditor | null): void { + editor?.select(null); + if (state) { + state.vTable = null; + state.firstTarget = null; + state.lastTarget = null; + state.tableSelection = false; + state.firstTable = null; + state.targetTable = null; + state.mouseMoveDisposer?.(); + state.mouseMoveDisposer = null; + } +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/deSelectAll.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/deSelectAll.ts deleted file mode 100644 index c098cb63872b..000000000000 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/deSelectAll.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { deselectCellHandler } from './deselectCellHandler'; -import { forEachCell } from './forEachCell'; -import { tableCellSelectionCommon } from './tableCellSelectionCommon'; -import { VTable } from 'roosterjs-editor-dom'; - -/** - * @internal - * Removes the selection of all the tables - */ -export function deSelectAll(vTable: VTable): void { - forEachCell(vTable, cell => { - if (cell.td) { - deselectCellHandler(cell.td); - } - }); - if (vTable.table?.classList.contains(tableCellSelectionCommon.TABLE_SELECTED)) { - vTable.table.classList.remove(tableCellSelectionCommon.TABLE_SELECTED); - } -} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/deselectCellHandler.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/deselectCellHandler.ts deleted file mode 100644 index fd5baea48a52..000000000000 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/deselectCellHandler.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { forEachCell } from './forEachCell'; -import { safeInstanceOf, VTable } from 'roosterjs-editor-dom'; -import { tableCellSelectionCommon } from './tableCellSelectionCommon'; - -/** - * @internal - * Handler to remove the selected style - * @param cell element to apply the style - */ -export function deselectCellHandler(cell: HTMLElement) { - if ( - cell && - safeInstanceOf(cell, 'HTMLTableCellElement') && - cell.classList.contains(tableCellSelectionCommon.TABLE_CELL_SELECTED) - ) { - cell.classList.remove(tableCellSelectionCommon.TABLE_CELL_SELECTED); - cell.style.backgroundColor = - cell.dataset[tableCellSelectionCommon.TEMP_BACKGROUND_COLOR] ?? ''; - delete cell.dataset[tableCellSelectionCommon.TEMP_BACKGROUND_COLOR]; - cell.querySelectorAll('table').forEach(table => { - const vTable2 = new VTable(table); - forEachCell(vTable2, cell => deselectCellHandler(cell.td)); - }); - } -} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/forEachCell.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/forEachCell.ts deleted file mode 100644 index 8efcbaa7a5d2..000000000000 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/forEachCell.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { VCell } from 'roosterjs-editor-types'; -import { VTable } from 'roosterjs-editor-dom'; - -/** - * @internal - * Execute an action on all the cells - * @param callback action to apply on all the cells. - */ -export function forEachCell( - vTable: VTable, - callback: (cell: VCell, x?: number, y?: number) => void -): void { - for (let indexY = 0; indexY < vTable.cells.length; indexY++) { - for (let indexX = 0; indexX < vTable.cells[indexY].length; indexX++) { - callback(vTable.cells[indexY][indexX], indexX, indexY); - } - } -} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/forEachSelectedCell.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/forEachSelectedCell.ts deleted file mode 100644 index 5d6cc1f39f5f..000000000000 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/forEachSelectedCell.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { tableCellSelectionCommon } from './tableCellSelectionCommon'; -import { VCell } from 'roosterjs-editor-types'; -import { VTable } from 'roosterjs-editor-dom'; - -/** - * @internal - * Executes an action to all the cells within the selection range. - * @param callback action to apply on each selected cell - * @returns the amount of cells modified - */ -export function forEachSelectedCell(vTable: VTable, callback: (cell: VCell) => void): number { - let selectedCells = 0; - - const { lastCell, firstCell } = vTable.selection; - - for (let y = 0; y < vTable.cells.length; y++) { - for (let x = 0; x < vTable.cells[y].length; x++) { - let element = vTable.cells[y][x].td as HTMLElement; - if ( - element?.classList.contains(tableCellSelectionCommon.TABLE_CELL_SELECTED) || - (((y >= firstCell.y && y <= lastCell.y) || (y <= firstCell.y && y >= lastCell.y)) && - ((x >= firstCell.x && x <= lastCell.x) || - (x <= firstCell.x && x >= lastCell.x))) - ) { - selectedCells += 1; - callback(vTable.cells[y][x]); - } - } - } - - return selectedCells; -} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/getCellAtCursor.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/getCellAtCursor.ts new file mode 100644 index 000000000000..304421dcf22c --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/getCellAtCursor.ts @@ -0,0 +1,16 @@ +import { IEditor } from 'roosterjs-editor-types'; +import { Nullable } from '../TableCellSelectionState'; +import { TABLE_CELL_SELECTOR } from '../constants'; + +/** + * @internal + */ +export function getCellAtCursor(editor: IEditor, node: Nullable): HTMLElement { + if (editor) { + return ( + editor.getElementAtCursor(TABLE_CELL_SELECTOR, node ?? undefined) || + (node as HTMLElement) + ); + } + return node as HTMLElement; +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/getCellCoordinates.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/getCellCoordinates.ts index a67ea507e3eb..fd6b5e30c670 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/getCellCoordinates.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/getCellCoordinates.ts @@ -7,9 +7,9 @@ import { VTable } from 'roosterjs-editor-dom'; * @param cellInput The cell the to find the coordinates * @returns Coordinates of the cell, null if not found */ -export function getCellCoordinates(vTable: VTable, cellInput: Node): Coordinates { - let result: Coordinates; - if (vTable.cells) { +export function getCellCoordinates(vTable: VTable, cellInput: Node): Coordinates | undefined { + let result: Coordinates | undefined; + if (vTable?.cells) { for (let indexY = 0; indexY < vTable.cells.length; indexY++) { for (let indexX = 0; indexX < vTable.cells[indexY].length; indexX++) { if (cellInput == vTable.cells[indexY][indexX].td) { diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/getTableAtCursor.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/getTableAtCursor.ts new file mode 100644 index 000000000000..1acd79b8b8f5 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/getTableAtCursor.ts @@ -0,0 +1,15 @@ +import { IEditor } from 'roosterjs-editor-types'; +import { Nullable } from '../TableCellSelectionState'; + +/** + * @internal + */ +export function getTableAtCursor( + editor: IEditor, + node: Nullable +): HTMLTableElement | null { + if (editor) { + return editor.getElementAtCursor('table', node as Node) as HTMLTableElement; + } + return null; +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/highlight.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/highlight.ts deleted file mode 100644 index e5b618b6577f..000000000000 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/highlight.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { deselectCellHandler } from './deselectCellHandler'; -import { highlightCellHandler } from './highlightCellHandler'; -import { normalizeTableSelection } from './normalizeTableSelection'; -import { tableCellSelectionCommon } from './tableCellSelectionCommon'; -import { VTable } from 'roosterjs-editor-dom'; - -/** - * @internal - * Highlights a range of cells, used in the TableSelection Plugin - */ -export function highlight(vTable: VTable): void { - if (vTable.selection && vTable.cells && vTable) { - if (!vTable.table.classList.contains(tableCellSelectionCommon.TABLE_SELECTED)) { - vTable.table.classList.add(tableCellSelectionCommon.TABLE_SELECTED); - } - const { firstCell, lastCell } = normalizeTableSelection(vTable.selection); - - let colIndex = vTable.cells[vTable.cells.length - 1].length - 1; - const selectedAllTable = - firstCell.x == 0 && - firstCell.y == 0 && - lastCell.x == colIndex && - lastCell.y == vTable.cells.length - 1; - - for (let indexY = 0; indexY < vTable.cells.length; indexY++) { - for (let indexX = 0; indexX < vTable.cells[indexY].length; indexX++) { - let element = getMergedCell(vTable, indexX, indexY); - if (element) { - if ( - selectedAllTable || - (((indexY >= firstCell.y && indexY <= lastCell.y) || - (indexY <= firstCell.y && indexY >= lastCell.y)) && - ((indexX >= firstCell.x && indexX <= lastCell.x) || - (indexX <= firstCell.x && indexX >= lastCell.x))) - ) { - highlightCellHandler(element); - } else { - deselectCellHandler(element); - } - } - } - } - } -} - -function getMergedCell(vTable: VTable, x: number, y: number) { - let element = vTable.cells[y][x].td as HTMLElement; - if (vTable.cells[y][x].spanLeft) { - for (let cellX = x; cellX > 0; cellX--) { - const cell = vTable.cells[y][cellX]; - if (cell.spanAbove) { - element = null; - break; - } - if (cell.td) { - element = cell.td; - x = cellX; - break; - } - } - } - - return element; -} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/highlightAll.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/highlightAll.ts deleted file mode 100644 index 056760e99e99..000000000000 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/highlightAll.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { forEachCell } from './forEachCell'; -import { highlightCellHandler } from './highlightCellHandler'; -import { tableCellSelectionCommon } from './tableCellSelectionCommon'; -import { VTable } from 'roosterjs-editor-dom'; - -/** - * @internal - * Highlights all the cells in the table. - */ -export function highlightAll(vTable: VTable): void { - let firstCol: number = null; - let firstRow: number = null; - let lastCol: number; - let lastRow: number; - if (!vTable.table.classList.contains(tableCellSelectionCommon.TABLE_SELECTED)) { - vTable.table.classList.add(tableCellSelectionCommon.TABLE_SELECTED); - } - forEachCell(vTable, (cell, x, y) => { - if (cell.td) { - highlightCellHandler(cell.td); - - firstCol = firstCol ?? x; - firstRow = firstRow ?? y; - lastCol = x; - lastRow = y; - } - }); - - vTable.selection = { - firstCell: { - x: firstCol, - y: firstRow, - }, - lastCell: { - x: lastCol, - y: lastRow, - }, - }; -} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/highlightCellHandler.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/highlightCellHandler.ts deleted file mode 100644 index c1ccc0bfef79..000000000000 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/highlightCellHandler.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { safeInstanceOf } from 'roosterjs-editor-dom'; -import { tableCellSelectionCommon } from './tableCellSelectionCommon'; - -/** - * @internal - * Handler to apply te selected styles on the cell - * @param element element to apply the style - */ -export function highlightCellHandler(element: HTMLElement) { - if ( - !element.classList.contains(tableCellSelectionCommon.TABLE_CELL_SELECTED) && - element.style.backgroundColor != tableCellSelectionCommon.HIGHLIGHT_COLOR && - (!element.dataset[tableCellSelectionCommon.TEMP_BACKGROUND_COLOR] || - element.dataset[tableCellSelectionCommon.TEMP_BACKGROUND_COLOR] == '') - ) { - element.dataset[tableCellSelectionCommon.TEMP_BACKGROUND_COLOR] = - element.style.backgroundColor ?? element.style.background ?? ''; - } - element.style.backgroundColor = tableCellSelectionCommon.HIGHLIGHT_COLOR; - element.classList.add(tableCellSelectionCommon.TABLE_CELL_SELECTED); - - element.querySelectorAll('td, th').forEach(cell => { - if (safeInstanceOf(cell, 'HTMLTableCellElement')) { - highlightCellHandler(cell); - } - }); -} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/isAfter.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/isAfter.ts new file mode 100644 index 000000000000..0c785e2404c8 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/isAfter.ts @@ -0,0 +1,22 @@ +import { Nullable } from '../TableCellSelectionState'; +import { Position } from 'roosterjs-editor-dom'; +import { PositionType } from 'roosterjs-editor-types'; + +/** + * @internal + */ +export function isAfter(node1: Nullable, node2: Nullable) { + if (node1 && node2) { + if (node2.contains(node1)) { + const r1 = (node1 as Element).getBoundingClientRect?.(); + const r2 = (node2 as Element).getBoundingClientRect?.(); + if (r1 && r2) { + return r1.top > r2.top && r1.bottom < r2.bottom; + } + } + + const position = new Position(node1, PositionType.End); + return position.isAfter(new Position(node2, PositionType.End)); + } + return false; +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/normalizeTableSelection.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/normalizeTableSelection.ts index 48f12297cd06..f2ae80c044f1 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/normalizeTableSelection.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/normalizeTableSelection.ts @@ -1,4 +1,6 @@ -import { TableSelection } from 'roosterjs-editor-types'; +import { Coordinates, TableSelection } from 'roosterjs-editor-types'; +import { VTable } from 'roosterjs-editor-dom'; + /** * @internal * Make the first Cell of a table selection always be on top of the last cell. @@ -6,25 +8,43 @@ import { TableSelection } from 'roosterjs-editor-types'; * @returns Table Selection where the first cell is always going to be first selected in the table * and the last cell always going to be last selected in the table. */ -export function normalizeTableSelection(input: TableSelection): TableSelection { - const { firstCell, lastCell } = input; +export default function normalizeTableSelection(vTable: VTable): TableSelection | null { + const { firstCell, lastCell } = vTable?.selection || {}; + if (!vTable?.cells || !vTable.selection || !firstCell || !lastCell) { + return null; + } + + const cells = vTable.cells; let newFirst = { - x: min(firstCell.x, lastCell.x), - y: min(firstCell.y, lastCell.y), + x: Math.min(firstCell.x, lastCell.x), + y: Math.min(firstCell.y, lastCell.y), }; let newLast = { - x: max(firstCell.x, lastCell.x), - y: max(firstCell.y, lastCell.y), + x: Math.max(firstCell.x, lastCell.x), + y: Math.max(firstCell.y, lastCell.y), }; - return { firstCell: newFirst, lastCell: newLast }; -} + const fixCoordinates = (coord: Coordinates) => { + if (coord.x < 0) { + coord.x = 0; + } + if (coord.y < 0) { + coord.y = 0; + } -function min(input1: number, input2: number) { - return input1 > input2 ? input2 : input1; -} + if (coord.y >= cells.length) { + coord.y = cells.length - 1; + } -function max(input1: number, input2: number) { - return input1 < input2 ? input2 : input1; + const rowsCells = cells[coord.y].length; + if (coord.x >= rowsCells) { + coord.x = rowsCells - 1; + } + }; + + fixCoordinates(newFirst); + fixCoordinates(newLast); + + return { firstCell: newFirst, lastCell: newLast }; } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/prepareSelection.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/prepareSelection.ts new file mode 100644 index 000000000000..05ce7845e749 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/prepareSelection.ts @@ -0,0 +1,69 @@ +import { findClosestElementAncestor } from 'roosterjs-editor-dom'; +import { getTableAtCursor } from './getTableAtCursor'; +import { IEditor } from 'roosterjs-editor-types'; +import { TABLE_CELL_SELECTOR } from '../constants'; +import { TableCellSelectionState } from '../TableCellSelectionState'; + +/** + * @internal + * Check if the selection started in a inner table. + */ +export function prepareSelection(state: TableCellSelectionState, editor: IEditor) { + if (!state.firstTable || !state.targetTable) { + return; + } + let isNewTargetTableContained = + state.lastTarget != state.firstTarget && + state.firstTable?.contains( + findClosestElementAncestor(state.targetTable, state.firstTable, TABLE_CELL_SELECTOR) + ); + + if (isNewTargetTableContained && state.tableSelection) { + while (isNewTargetTableContained) { + state.lastTarget = findClosestElementAncestor( + state.targetTable, + state.firstTable, + TABLE_CELL_SELECTOR + ); + state.targetTable = getTableAtCursor(editor, state.lastTarget); + isNewTargetTableContained = + state.lastTarget != state.firstTarget && + state.firstTable?.contains( + findClosestElementAncestor( + state.targetTable, + state.firstTable, + TABLE_CELL_SELECTOR + ) + ); + } + } + + let isFirstTargetTableContained = + state.lastTarget != state.firstTarget && + state.targetTable?.contains( + findClosestElementAncestor(state.firstTable, state.targetTable, TABLE_CELL_SELECTOR) + ); + + if (isFirstTargetTableContained && state.tableSelection && state.targetTable) { + while (isFirstTargetTableContained) { + state.firstTarget = findClosestElementAncestor( + state.firstTable, + state.targetTable, + TABLE_CELL_SELECTOR + ); + if (!state.firstTarget) { + return; + } + state.firstTable = getTableAtCursor(editor, state.firstTarget); + isFirstTargetTableContained = + state.lastTarget != state.firstTarget && + state.targetTable?.contains( + findClosestElementAncestor( + state.firstTable, + state.targetTable, + TABLE_CELL_SELECTOR + ) + ); + } + } +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/removeCellsOutsideSelection.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/removeCellsOutsideSelection.ts deleted file mode 100644 index 5f9c4cfe4678..000000000000 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/removeCellsOutsideSelection.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { normalizeTableSelection } from './normalizeTableSelection'; -import { VCell } from 'roosterjs-editor-types'; -import { VTable } from 'roosterjs-editor-dom'; - -/** - * @internal - * Remove the cells outside of the selection. - */ -export function removeCellsOutsideSelection(vTable: VTable) { - const { firstCell, lastCell } = normalizeTableSelection(vTable.selection); - const rowsLength = vTable.cells.length - 1; - const colIndex = vTable.cells[rowsLength].length - 1; - const resultCells: VCell[][] = []; - - const firstX = firstCell.x; - const firstY = firstCell.y; - const lastX = lastCell.x; - const lastY = lastCell.y; - - const selectedAllTable = firstX == 0 && firstY == 0 && lastX == colIndex && lastY == rowsLength; - - if (selectedAllTable) { - return; - } - - vTable.cells.forEach((row, y) => { - row = row.filter( - (_, x) => - ((y >= firstY && y <= lastY) || (y <= firstY && y >= lastY)) && - ((x >= firstX && x <= lastX) || (x <= firstX && x >= lastX)) - ); - if (row.length > 0) { - resultCells.push(row); - } - }); - vTable.cells = resultCells; -} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/restoreSelection.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/restoreSelection.ts new file mode 100644 index 000000000000..42788e81d0da --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/restoreSelection.ts @@ -0,0 +1,38 @@ +import { IEditor, PositionType } from 'roosterjs-editor-types'; +import { isAfter } from './isAfter'; +import { Position } from 'roosterjs-editor-dom'; +import { TableCellSelectionState } from '../TableCellSelectionState'; +import { updateSelection } from './updateSelection'; + +/** + * @internal + */ +export function restoreSelection(state: TableCellSelectionState, editor: IEditor) { + if (!state.lastTarget || !state.firstTarget) { + return; + } + + if (state.firstTable) { + editor.select(state.firstTable, null /* coordinates */); + } + state.tableSelection = false; + const isBeginAboveEnd = isAfter(state.firstTarget, state.lastTarget); + const targetPosition = new Position( + state.lastTarget, + isBeginAboveEnd ? PositionType.End : PositionType.Begin + ); + + const firstTargetRange = new Range(); + if (state.firstTarget) { + firstTargetRange.selectNodeContents(state.firstTarget); + } + updateSelection( + editor, + state.firstTarget, + isBeginAboveEnd + ? Position.getEnd(firstTargetRange).offset + : Position.getStart(firstTargetRange).offset, + targetPosition.element, + targetPosition.offset + ); +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/selectTable.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/selectTable.ts new file mode 100644 index 000000000000..b476652c8c51 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/selectTable.ts @@ -0,0 +1,12 @@ +import normalizeTableSelection from './normalizeTableSelection'; +import { IEditor } from 'roosterjs-editor-types'; +import { TableCellSelectionState } from '../TableCellSelectionState'; + +/** + * @internal + */ +export function selectTable(editor: IEditor, state: TableCellSelectionState) { + if (editor && state.vTable) { + editor?.select(state.vTable.table, normalizeTableSelection(state.vTable)); + } +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/setData.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/setData.ts new file mode 100644 index 000000000000..ca43845bcefd --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/setData.ts @@ -0,0 +1,26 @@ +import { getCellAtCursor } from './getCellAtCursor'; +import { getTableAtCursor } from './getTableAtCursor'; +import { IEditor } from 'roosterjs-editor-types'; +import { TABLE_CELL_SELECTOR } from '../constants'; +import { TableCellSelectionState } from '../TableCellSelectionState'; + +/** + * @internal + */ +export function setData(eventTarget: Node, state: TableCellSelectionState, editor: IEditor) { + const pos = editor.getFocusedPosition(); + if (pos) { + state.firstTarget = state.firstTarget || getCellAtCursor(editor, pos.node); + + if (state.firstTarget.nodeType == Node.TEXT_NODE) { + state.firstTarget = editor.getElementAtCursor(TABLE_CELL_SELECTOR, state.firstTarget); + } + if (!editor.contains(state.firstTarget) && state.lastTarget) { + state.firstTarget = state.lastTarget; + } + } + + state.firstTable = getTableAtCursor(editor, state.firstTarget) as HTMLTableElement; + state.lastTarget = getCellAtCursor(editor, eventTarget as Node); + state.targetTable = getTableAtCursor(editor, state.lastTarget); +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/tableCellSelectionCommon.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/tableCellSelectionCommon.ts deleted file mode 100644 index a5f9f498c797..000000000000 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/tableCellSelectionCommon.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @internal - * Common data used in the Table Cell Selection Plugin - */ -export const enum tableCellSelectionCommon { - /** - * Class applied when to the parent table when table selection - */ - TABLE_SELECTED = '_tableSelected', - /** - * Class applied to each cell selected - */ - TABLE_CELL_SELECTED = '_tableCellSelected', - /** - * Dataset used to store the current color of the cell when is selected - */ - TEMP_BACKGROUND_COLOR = 'originalBackgroundColor', - /** - * highlight color to apply to the selected cells - */ - HIGHLIGHT_COLOR = 'rgba(198,198,198,0.7)', -} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/updateSelection.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/updateSelection.ts new file mode 100644 index 000000000000..49d364fb8844 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableCellSelection/utils/updateSelection.ts @@ -0,0 +1,22 @@ +import { IEditor } from 'roosterjs-editor-types'; + +/** + * @internal + * Use SetBaseAndExtend to update the selection without losing the order that was used in the selection. + * Using editor.select may lose the order of the selection if the start of the selection is After + * the end container of the selection. + */ +export function updateSelection( + editor: IEditor, + start: Node, + offset: number, + end?: Node, + endOffset?: number +) { + const selection = editor.getDocument().defaultView?.getSelection(); + if (selection) { + end = end || start; + endOffset = endOffset || offset; + selection.setBaseAndExtent(start, offset, end, endOffset); + } +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/TableResize.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/TableResize.ts index fc3f1e4f7096..b845a717f6fe 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/TableResize.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/TableResize.ts @@ -1,6 +1,13 @@ import TableEditor from './editors/TableEditor'; -import { EditorPlugin, IEditor, PluginEvent, PluginEventType, Rect } from 'roosterjs-editor-types'; -import { normalizeRect } from 'roosterjs-editor-dom'; +import { normalizeRect, safeInstanceOf } from 'roosterjs-editor-dom'; +import { + CreateElementData, + EditorPlugin, + IEditor, + PluginEvent, + PluginEventType, + Rect, +} from 'roosterjs-editor-types'; const TABLE_RESIZER_LENGTH = 12; @@ -8,10 +15,23 @@ const TABLE_RESIZER_LENGTH = 12; * TableResize plugin, provides the ability to resize a table by drag-and-drop */ export default class TableResize implements EditorPlugin { - private editor: IEditor; - private onMouseMoveDisposer: () => void; - private tableRectMap: { table: HTMLTableElement; rect: Rect }[] = null; - private tableEditor: TableEditor; + private editor: IEditor | null = null; + private onMouseMoveDisposer: (() => void) | null = null; + private tableRectMap: { table: HTMLTableElement; rect: Rect }[] | null = null; + private tableEditor: TableEditor | null = null; + + /** + * Construct a new instance of TableResize plugin + * @param onShowHelperElement An optional callback to allow customize helper element of table resizing. + * To customize the helper element, add this callback and change the attributes of elementData then it + * will be picked up by TableResize code + */ + constructor( + private onShowHelperElement?: ( + elementData: CreateElementData, + helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector' + ) => void + ) {} /** * Get a friendly name of this plugin @@ -26,17 +46,33 @@ export default class TableResize implements EditorPlugin { */ initialize(editor: IEditor) { this.editor = editor; - this.onMouseMoveDisposer = this.editor.addDomEventHandler({ mousemove: this.onMouseMove }); + this.onMouseMoveDisposer = this.editor.addDomEventHandler({ + mousemove: this.onMouseMove, + mouseout: e => this.onMouseOut(e), + }); } + private onMouseOut = (ev: Event) => { + if ( + isMouseEvent(ev) && + safeInstanceOf(ev.relatedTarget, 'HTMLElement') && + this.tableEditor && + !this.tableEditor.isOwnedElement(ev.relatedTarget) && + !this.editor?.contains(ev.relatedTarget) + ) { + this.setTableEditor(null); + } + }; + /** * Dispose this plugin */ dispose() { - this.onMouseMoveDisposer(); + this.onMouseMoveDisposer?.(); this.invalidateTableRects(); - this.setTableEditor(null); + this.disposeTableEditor(); this.editor = null; + this.onMouseMoveDisposer = null; } /** @@ -48,47 +84,60 @@ export default class TableResize implements EditorPlugin { case PluginEventType.Input: case PluginEventType.ContentChanged: case PluginEventType.Scroll: + case PluginEventType.ZoomChanged: this.setTableEditor(null); this.invalidateTableRects(); break; } } - private onMouseMove = (e: MouseEvent) => { - if (e.buttons > 0) { + private onMouseMove = (event: Event) => { + const e = event as MouseEvent; + + if (e.buttons > 0 || !this.editor) { return; } this.ensureTableRects(); - const { pageX: x, pageY: y } = e; + + const editorWindow = this.editor.getDocument().defaultView || window; + const x = e.pageX - editorWindow.scrollX; + const y = e.pageY - editorWindow.scrollY; let currentTable: HTMLTableElement | null = null; - for (let i = this.tableRectMap.length - 1; i >= 0; i--) { - const { table, rect } = this.tableRectMap[i]; + if (this.tableRectMap) { + for (let i = this.tableRectMap.length - 1; i >= 0; i--) { + const { table, rect } = this.tableRectMap[i]; - if ( - x >= rect.left - TABLE_RESIZER_LENGTH && - x <= rect.right + TABLE_RESIZER_LENGTH && - y >= rect.top - TABLE_RESIZER_LENGTH && - y <= rect.bottom + TABLE_RESIZER_LENGTH - ) { - currentTable = table; - break; + if ( + x >= rect.left - TABLE_RESIZER_LENGTH && + x <= rect.right + TABLE_RESIZER_LENGTH && + y >= rect.top - TABLE_RESIZER_LENGTH && + y <= rect.bottom + TABLE_RESIZER_LENGTH + ) { + currentTable = table; + break; + } } } - this.setTableEditor(currentTable); + this.setTableEditor(currentTable, e); this.tableEditor?.onMouseMove(x, y); }; - private setTableEditor(table: HTMLTableElement) { - if (this.tableEditor && table != this.tableEditor.table) { - this.tableEditor.dispose(); - this.tableEditor = null; + private setTableEditor(table: HTMLTableElement | null, e?: MouseEvent) { + if (this.tableEditor && !this.tableEditor.isEditing() && table != this.tableEditor.table) { + this.disposeTableEditor(); } - if (!this.tableEditor && table) { - this.tableEditor = new TableEditor(this.editor, table, this.invalidateTableRects); + if (!this.tableEditor && table && this.editor) { + this.tableEditor = new TableEditor( + this.editor, + table, + this.invalidateTableRects, + this.onShowHelperElement, + e?.currentTarget + ); } } @@ -96,13 +145,18 @@ export default class TableResize implements EditorPlugin { this.tableRectMap = null; }; + private disposeTableEditor() { + this.tableEditor?.dispose(); + this.tableEditor = null; + } + private ensureTableRects() { - if (!this.tableRectMap) { + if (!this.tableRectMap && this.editor) { this.tableRectMap = []; this.editor.queryElements('table', table => { if (table.isContentEditable) { const rect = normalizeRect(table.getBoundingClientRect()); - if (rect) { + if (rect && this.tableRectMap) { this.tableRectMap.push({ table, rect, @@ -113,3 +167,7 @@ export default class TableResize implements EditorPlugin { } } } + +function isMouseEvent(e: Event): e is MouseEvent { + return !!(e as MouseEvent).pageX; +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/CellResizer.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/CellResizer.ts index 9d0785eb73c5..9de03c29b8ed 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/CellResizer.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/CellResizer.ts @@ -2,7 +2,7 @@ import DragAndDropHandler from '../../../pluginUtils/DragAndDropHandler'; import DragAndDropHelper from '../../../pluginUtils/DragAndDropHelper'; import TableEditFeature from './TableEditorFeature'; import { createElement, normalizeRect, VTable } from 'roosterjs-editor-dom'; -import { KnownCreateElementDataIndex, Rect, SizeTransformer } from 'roosterjs-editor-types'; +import { CreateElementData, Rect } from 'roosterjs-editor-types'; const CELL_RESIZER_WIDTH = 4; const MIN_CELL_WIDTH = 30; @@ -12,23 +12,29 @@ const MIN_CELL_WIDTH = 30; */ export default function createCellResizer( td: HTMLTableCellElement, - sizeTransformer: SizeTransformer, + zoomScale: number, isRTL: boolean, isHorizontal: boolean, onStart: () => void, - onEnd: () => false -): TableEditFeature { + onEnd: () => false, + onShowHelperElement?: ( + elementData: CreateElementData, + helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector' + ) => void +): TableEditFeature | null { const document = td.ownerDocument; - const div = createElement( - isHorizontal - ? KnownCreateElementDataIndex.TableHorizontalResizer - : KnownCreateElementDataIndex.TableVerticalResizer, - document - ) as HTMLDivElement; + const createElementData = { + tag: 'div', + style: `position: fixed; cursor: ${isHorizontal ? 'row' : 'col'}-resize; user-select: none`, + }; + + onShowHelperElement?.(createElementData, 'CellResizer'); + + const div = createElement(createElementData, document) as HTMLDivElement; document.body.appendChild(div); - const context: DragAndDropContext = { td, isRTL, sizeTransformer, onStart }; + const context: DragAndDropContext = { td, isRTL, zoomScale, onStart }; const setPosition = isHorizontal ? setHorizontalPosition : setVerticalPosition; setPosition(context, div); @@ -43,7 +49,7 @@ export default function createCellResizer( context, setPosition, handler, - sizeTransformer + zoomScale ); return { node: td, div, featureHandler }; @@ -52,7 +58,7 @@ export default function createCellResizer( interface DragAndDropContext { td: HTMLTableCellElement; isRTL: boolean; - sizeTransformer: SizeTransformer; + zoomScale: number; onStart: () => void; } @@ -60,11 +66,12 @@ interface DragAndDropInitValue { vTable: VTable; currentCells: HTMLTableCellElement[]; nextCells: HTMLTableCellElement[]; + initialX: number; } -function onDragStart(context: DragAndDropContext, event: MouseEvent) { - const { td, isRTL, sizeTransformer, onStart } = context; - const vTable = new VTable(td, true /*normalizeSize*/, sizeTransformer); +function onDragStart(context: DragAndDropContext, event: MouseEvent): DragAndDropInitValue { + const { td, isRTL, zoomScale, onStart } = context; + const vTable = new VTable(td, true /*normalizeSize*/, zoomScale); const rect = normalizeRect(td.getBoundingClientRect()); if (rect) { @@ -78,9 +85,10 @@ function onDragStart(context: DragAndDropContext, event: MouseEvent) { vTable, currentCells, nextCells, + initialX: event.pageX, }; } else { - return { vTable, currentCells: [], nextCells: [] }; // Just a fallback + return { vTable, currentCells: [], nextCells: [], initialX: 0 }; // Just a fallback } } @@ -91,19 +99,22 @@ function onDraggingHorizontal( deltaX: number, deltaY: number ) { - const { td, sizeTransformer } = context; + const { td, zoomScale } = context; const { vTable } = initValue; vTable.table.removeAttribute('height'); - vTable.table.style.height = null; + vTable.table.style.setProperty('height', null); vTable.forEachCellOfCurrentRow(cell => { if (cell.td) { - cell.td.style.height = - cell.td == td ? `${sizeTransformer(cell.height) + deltaY}px` : null; + cell.td.style.setProperty( + 'height', + cell.td == td ? `${(cell.height ?? 0) / zoomScale + deltaY}px` : null + ); } }); - vTable.writeBack(); + // To avoid apply format styles when the table is being resizing, the skipApplyFormat is set to true. + vTable.writeBack(true /**skipApplyFormat*/); return true; } @@ -111,13 +122,12 @@ function onDraggingVertical( context: DragAndDropContext, event: MouseEvent, initValue: DragAndDropInitValue, - deltaX: number, - deltaY: number + deltaX: number ) { - const { isRTL, sizeTransformer } = context; - const { vTable, nextCells, currentCells } = initValue; + const { isRTL, zoomScale } = context; + const { vTable, nextCells, currentCells, initialX } = initValue; - if (!canResizeColumns(event.pageX, currentCells, nextCells, isRTL, sizeTransformer)) { + if (!canResizeColumns(event.pageX, currentCells, nextCells, isRTL, zoomScale)) { return false; } @@ -128,7 +138,7 @@ function onDraggingVertical( const isShiftPressed = event.shiftKey; if (isLastCell || isShiftPressed) { - vTable.table.style.width = null; + vTable.table.style.setProperty('width', null); } const newWidthList = new Map(); @@ -139,7 +149,7 @@ function onDraggingVertical( td.style.wordBreak = 'break-word'; td.style.whiteSpace = 'normal'; td.style.boxSizing = 'border-box'; - const newWidth = sizeTransformer(getHorizontalDistance(rect, event.pageX, !isRTL)); + const newWidth = getHorizontalDistance(rect, event.pageX, !isRTL) / zoomScale; newWidthList.set(td, newWidth); } }); @@ -148,14 +158,16 @@ function onDraggingVertical( }); if (!isShiftPressed) { nextCells.forEach(td => { + const width = td.rowSpan > 1 ? 0 : td.getBoundingClientRect().right - initialX; td.style.wordBreak = 'break-word'; td.style.whiteSpace = 'normal'; td.style.boxSizing = 'border-box'; - td.style.width = null; + td.style.width = td.rowSpan > 1 ? '' : width / zoomScale - deltaX + 'px'; }); } - vTable.writeBack(); + // To avoid apply format styles when the table is being resizing, the skipApplyFormat is set to true. + vTable.writeBack(true /**skipApplyFormat*/); return true; } @@ -196,13 +208,13 @@ function canResizeColumns( currentCells: HTMLTableCellElement[], nextCells: HTMLTableCellElement[], isRTL: boolean, - sizeTransformer: SizeTransformer + zoomScale: number ) { for (let i = 0; i < currentCells.length; i++) { const td = currentCells[i]; const rect = normalizeRect(td.getBoundingClientRect()); if (rect) { - const width = sizeTransformer(getHorizontalDistance(rect, newPos, !isRTL)); + const width = getHorizontalDistance(rect, newPos, !isRTL) / zoomScale; if (width < MIN_CELL_WIDTH) { return false; } @@ -216,7 +228,7 @@ function canResizeColumns( const rect = normalizeRect(td.getBoundingClientRect()); if (rect) { - width = sizeTransformer(getHorizontalDistance(rect, newPos, isRTL)); + width = getHorizontalDistance(rect, newPos, isRTL) / zoomScale; } } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditor.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditor.ts index bc391b2b8bf3..4767637d4fab 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditor.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditor.ts @@ -1,113 +1,187 @@ import createCellResizer from './CellResizer'; import createTableInserter from './TableInserter'; import createTableResizer from './TableResizer'; +import createTableSelector from './TableSelector'; import TableEditFeature, { disposeTableEditFeature } from './TableEditorFeature'; -import { ChangeSource, IEditor, NodePosition } from 'roosterjs-editor-types'; -import { getComputedStyle, normalizeRect } from 'roosterjs-editor-dom'; - -const INSERTER_HOVER_OFFSET = 5; +import { + contains, + getComputedStyle, + normalizeRect, + Position, + safeInstanceOf, + VTable, +} from 'roosterjs-editor-dom'; +import { + ChangeSource, + IEditor, + NodePosition, + TableSelection, + CreateElementData, +} from 'roosterjs-editor-types'; +const INSERTER_HOVER_OFFSET = 6; +const enum TOP_OR_SIDE { + top = 0, + side = 1, +} /** * @internal * - * A table has 5 hot areas to be resized/edited (take LTR example): - * [ ] - * +[ 1 ]+--------------------+ - * |[ ]| | - * [ ] [ ] | - * [ ] [ ] | - * [2] [3] | - * [ ] [ ] | - * [ ][ 4 ]| | - * +------------------+--------------------+ - * | | | - * | | | - * | | | - * +------------------+--------------------+ - * [5] + * A table has 6 hot areas to be resized/edited (take LTR example): + * + * [6] [ ] + * +[ 1 ]+--------------------+ + * |[ ]| | + * [ ] [ ] | + * [ ] [ ] | + * [2] [3] | + * [ ] [ ] | + * [ ][ 4 ]| | + * +------------------+--------------------+ + * | | | + * | | | + * | | | + * +------------------+--------------------+ + * [5] * * 1 - Hover area to show insert column button * 2 - Hover area to show insert row button * 3 - Hover area to show vertical resizing bar * 4 - Hover area to show horizontal resizing bar * 5 - Hover area to show whole table resize button + * 6 - Hover area to show whole table selector button * * When set a different current table or change current TD, we need to update these areas */ export default class TableEditor { // 1, 2 - Insert a column or a row - private horizontalInserter: TableEditFeature; - private verticalInserter: TableEditFeature; + private horizontalInserter: TableEditFeature | null = null; + private verticalInserter: TableEditFeature | null = null; // 3, 4 - Resize a column or a row from a cell - private horizontalResizer: TableEditFeature; - private verticalResizer: TableEditFeature; + private horizontalResizer: TableEditFeature | null = null; + private verticalResizer: TableEditFeature | null = null; // 5 - Resize whole table - private tableResizer: TableEditFeature; + private tableResizer: TableEditFeature | null = null; + + // 6 - Select whole table + private tableSelector: TableEditFeature | null = null; private isRTL: boolean; - private start: NodePosition; - private end: NodePosition; + private start: NodePosition | null = null; + private end: NodePosition | null = null; + private isCurrentlyEditing: boolean; constructor( private editor: IEditor, public readonly table: HTMLTableElement, - private onChanged: () => void + private onChanged: () => void, + private onShowHelperElement?: ( + elementData: CreateElementData, + helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector' + ) => void, + private contentDiv?: EventTarget | null ) { - const sizeTransformer = editor.getSizeTransformer(); this.isRTL = getComputedStyle(table, 'direction') == 'rtl'; - this.tableResizer = createTableResizer( - table, - sizeTransformer, - this.isRTL, - this.onFinishEditing - ); - this.editor.addUndoSnapshot((start, end) => { - this.start = start; - this.end = end; - }); + this.setEditorFeatures(); + this.isCurrentlyEditing = false; } dispose() { this.disposeTableResizer(); this.disposeCellResizers(); this.disposeTableInserter(); + this.disposeTableSelector(); + } + + isEditing(): boolean { + return this.isCurrentlyEditing; + } + + isOwnedElement(node: Node) { + return [ + this.tableResizer, + this.tableSelector, + this.horizontalInserter, + this.verticalInserter, + this.horizontalResizer, + this.verticalResizer, + ] + .filter(feature => !!feature?.div) + .some(feature => contains(feature?.div, node, true /* treatSameNodeAsContain */)); } onMouseMove(x: number, y: number) { + //Get Cell [0,0] + const firstCell = this.table.rows[0].cells[0]; + const firstCellRect = normalizeRect(firstCell.getBoundingClientRect()); + + if (!firstCellRect) { + return; + } + + //Determine if cursor is on top or side + const topOrSide = + y <= firstCellRect.top + INSERTER_HOVER_OFFSET + ? TOP_OR_SIDE.top + : this.isRTL + ? x >= firstCellRect.right - INSERTER_HOVER_OFFSET + ? TOP_OR_SIDE.side + : undefined + : x <= firstCellRect.left + INSERTER_HOVER_OFFSET + ? TOP_OR_SIDE.side + : undefined; + + // i is row index, j is column index for (let i = 0; i < this.table.rows.length; i++) { const tr = this.table.rows[i]; let j = 0; for (; j < tr.cells.length; j++) { const td = tr.cells[j]; + const tableRect = normalizeRect(this.table.getBoundingClientRect()); const tdRect = normalizeRect(td.getBoundingClientRect()); - if (!tdRect) { + if (!tdRect || !tableRect) { continue; } + // Determine the cell the cursor is in range of const lessThanBottom = y <= tdRect.bottom; - const lessThanRight = this.isRTL ? x >= tdRect.right : x <= tdRect.right; + const lessThanRight = this.isRTL + ? x <= tdRect.right + INSERTER_HOVER_OFFSET + : x <= tdRect.right; + const moreThanLeft = this.isRTL + ? x >= tdRect.left + : x >= tdRect.left - INSERTER_HOVER_OFFSET; - if (lessThanRight && lessThanBottom) { - if (i == 0 && y <= tdRect.top + INSERTER_HOVER_OFFSET) { + if (lessThanBottom && lessThanRight && moreThanLeft) { + const isOnLeftOrRight = this.isRTL + ? tdRect.right <= tableRect.right && tdRect.right >= tableRect.right - 1 + : tdRect.left >= tableRect.left && tdRect.left <= tableRect.left + 1; + if (i === 0 && topOrSide == TOP_OR_SIDE.top) { const center = (tdRect.left + tdRect.right) / 2; const isOnRightHalf = this.isRTL ? x < center : x > center; this.setInserterTd( isOnRightHalf ? td : tr.cells[j - 1], false /*isHorizontal*/ ); - } else if ( - j == 0 && - (this.isRTL - ? x >= tdRect.right - INSERTER_HOVER_OFFSET - : x <= tdRect.left + INSERTER_HOVER_OFFSET) - ) { + } else if (j === 0 && topOrSide == TOP_OR_SIDE.side && isOnLeftOrRight) { + const tdAbove = this.table.rows[i - 1]?.cells[0]; + const tdAboveRect = tdAbove + ? normalizeRect(tdAbove.getBoundingClientRect()) + : null; + + const isTdNotAboveMerged = !tdAboveRect + ? null + : this.isRTL + ? tdAboveRect.right === tdRect.right + : tdAboveRect.left === tdRect.left; + this.setInserterTd( - y > (tdRect.top + tdRect.bottom) / 2 - ? td - : this.table.rows[i - 1]?.cells[0], + y < (tdRect.top + tdRect.bottom) / 2 && isTdNotAboveMerged + ? tdAbove + : td, true /*isHorizontal*/ ); } else { @@ -124,6 +198,33 @@ export default class TableEditor { break; } } + + this.setEditorFeatures(); + } + + private setEditorFeatures() { + if (!this.tableSelector) { + this.tableSelector = createTableSelector( + this.table, + this.editor.getZoomScale(), + this.editor, + this.onSelect, + this.getOnMouseOut, + this.onShowHelperElement, + this.contentDiv + ); + } + + if (!this.tableResizer) { + this.tableResizer = createTableResizer( + this.table, + this.editor.getZoomScale(), + this.isRTL, + this.onStartTableResize, + this.onFinishEditing, + this.onShowHelperElement + ); + } } private setResizingTd(td: HTMLTableCellElement) { @@ -132,29 +233,35 @@ export default class TableEditor { } if (!this.horizontalResizer && td) { - const sizeTransformer = this.editor.getSizeTransformer(); + const zoomScale = this.editor.getZoomScale(); this.horizontalResizer = createCellResizer( td, - sizeTransformer, + zoomScale, this.isRTL, true /*isHorizontal*/, this.onStartCellResize, - this.onFinishEditing + this.onFinishEditing, + this.onShowHelperElement ); this.verticalResizer = createCellResizer( td, - sizeTransformer, + zoomScale, this.isRTL, false /*isHorizontal*/, this.onStartCellResize, - this.onFinishEditing + this.onFinishEditing, + this.onShowHelperElement ); } } - private setInserterTd(td: HTMLTableCellElement, isHorizontal?: boolean) { + /** + * create or remove TableInserter + * @param td td to attach to, set this to null to remove inserters (both horizontal and vertical) + */ + private setInserterTd(td: HTMLTableCellElement | null, isHorizontal?: boolean) { const inserter = isHorizontal ? this.horizontalInserter : this.verticalInserter; - if (inserter && inserter.node != td) { + if (td === null || (inserter && inserter.node != td)) { this.disposeTableInserter(); } @@ -163,8 +270,10 @@ export default class TableEditor { this.editor, td, this.isRTL, - isHorizontal, - this.onInserted + !!isHorizontal, + this.onInserted, + this.getOnMouseOut, + this.onShowHelperElement ); if (isHorizontal) { this.horizontalInserter = newInserter; @@ -203,20 +312,97 @@ export default class TableEditor { } } + private disposeTableSelector() { + if (this.tableSelector) { + disposeTableEditFeature(this.tableSelector); + this.tableSelector = null; + } + } + private onFinishEditing = (): false => { - this.editor.select(this.start, this.end); - this.editor.addUndoSnapshot(null /*callback*/, ChangeSource.Format); this.editor.focus(); + + if (this.start && this.end) { + this.editor.select(this.start, this.end); + } + + this.editor.addUndoSnapshot(undefined /*callback*/, ChangeSource.Format); this.onChanged(); + this.isCurrentlyEditing = false; + return false; }; + private onStartTableResize = () => { + this.isCurrentlyEditing = true; + this.onStartResize(); + }; + private onStartCellResize = () => { + this.isCurrentlyEditing = true; this.disposeTableResizer(); + this.onStartResize(); }; - private onInserted = () => { + private onStartResize() { + this.isCurrentlyEditing = true; + const range = this.editor.getSelectionRange(); + + if (range) { + this.start = Position.getStart(range); + this.end = Position.getEnd(range); + } + + this.editor.addUndoSnapshot(); + } + + private onInserted = (table: HTMLTableElement) => { + this.editor.transformToDarkColor(table); this.disposeTableResizer(); this.onFinishEditing(); }; + + /** + * Public only for testing purposes + * @param table the table to select + */ + public onSelect = (table: HTMLTableElement) => { + this.editor.focus(); + if (table) { + const vTable = new VTable(table); + if (vTable.cells) { + const rows = vTable.cells.length - 1; + let lastCellIndex: number = 0; + vTable.cells[rows].forEach((cell, index) => { + lastCellIndex = index; + }); + + const selection: TableSelection = { + firstCell: { + x: 0, + y: 0, + }, + lastCell: { + y: rows, + x: lastCellIndex, + }, + }; + this.editor.select(table, selection); + } + } + }; + + private getOnMouseOut = (feature: HTMLElement) => { + return (ev: MouseEvent) => { + if ( + feature && + ev.relatedTarget != feature && + safeInstanceOf(this.contentDiv, 'HTMLElement') && + safeInstanceOf(ev.relatedTarget, 'HTMLElement') && + !contains(this.contentDiv, ev.relatedTarget, true /* treatSameNodeAsContain */) + ) { + this.dispose(); + } + }; + }; } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditorFeature.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditorFeature.ts index 52a8fa0e2e11..cb65000ed175 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditorFeature.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditorFeature.ts @@ -5,16 +5,18 @@ import Disposable from '../../../pluginUtils/Disposable'; */ export default interface TableEditFeature { node: Node; - div: HTMLDivElement; - featureHandler: Disposable; + div: HTMLDivElement | null; + featureHandler: Disposable | null; } /** * @internal */ -export function disposeTableEditFeature(resizer: TableEditFeature) { - resizer.div.parentNode?.removeChild(resizer.div); - resizer.div = null; - resizer.featureHandler.dispose(); - resizer.featureHandler = null; +export function disposeTableEditFeature(resizer: TableEditFeature | null) { + if (resizer) { + resizer.div?.parentNode?.removeChild(resizer.div); + resizer.div = null; + resizer.featureHandler?.dispose(); + resizer.featureHandler = null; + } } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableInserter.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableInserter.ts index 3ccd810889d2..94519b87a767 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableInserter.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableInserter.ts @@ -1,12 +1,7 @@ import Disposable from '../../../pluginUtils/Disposable'; import TableEditFeature from './TableEditorFeature'; -import { createElement, normalizeRect, VTable } from 'roosterjs-editor-dom'; -import { - CreateElementData, - IEditor, - SizeTransformer, - TableOperation, -} from 'roosterjs-editor-types'; +import { createElement, getIntersectedRect, normalizeRect, VTable } from 'roosterjs-editor-dom'; +import { CreateElementData, IEditor, TableOperation } from 'roosterjs-editor-types'; const INSERTER_COLOR = '#4A4A4A'; const INSERTER_COLOR_DARK_MODE = 'white'; @@ -21,37 +16,47 @@ export default function createTableInserter( td: HTMLTableCellElement, isRTL: boolean, isHorizontal: boolean, - onInsert: () => void -): TableEditFeature { + onInsert: (table: HTMLTableElement) => void, + getOnMouseOut: (feature: HTMLElement) => (ev: MouseEvent) => void, + onShowHelperElement?: ( + elementData: CreateElementData, + helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector' + ) => void +): TableEditFeature | null { const table = editor.getElementAtCursor('table', td); + const tdRect = normalizeRect(td.getBoundingClientRect()); - const tableRect = table ? normalizeRect(table.getBoundingClientRect()) : null; + const viewPort = editor.getVisibleViewport(); + const tableRect = table && viewPort ? getIntersectedRect([table], [viewPort]) : null; // set inserter position if (tdRect && tableRect) { const document = td.ownerDocument; - const div = createElement( - getInsertElementData( - isHorizontal, - editor.isDarkMode(), - isRTL, - editor.getDefaultFormat().backgroundColor || 'white' - ), - document - ) as HTMLDivElement; + const createElementData = getInsertElementData( + isHorizontal, + editor.isDarkMode(), + isRTL, + editor.getDefaultFormat().backgroundColor || 'white' + ); + + onShowHelperElement?.(createElementData, 'TableInserter'); + + const div = createElement(createElementData, document) as HTMLDivElement; if (isHorizontal) { + // tableRect.left/right is used because the Inserter is always intended to be on the side div.style.left = `${ isRTL - ? tdRect.right - : tdRect.left - (INSERTER_SIDE_LENGTH - 1 + 2 * INSERTER_BORDER_SIZE) + ? tableRect.right + : tableRect.left - (INSERTER_SIDE_LENGTH - 1 + 2 * INSERTER_BORDER_SIZE) }px`; div.style.top = `${tdRect.bottom - 8}px`; (div.firstChild as HTMLElement).style.width = `${tableRect.right - tableRect.left}px`; } else { - div.style.left = `${isRTL ? tdRect.left : tdRect.right - 8}px`; + div.style.left = `${isRTL ? tdRect.left - 8 : tdRect.right - 8}px`; + // tableRect.top is used because the Inserter is always intended to be on top div.style.top = `${ - tdRect.top - (INSERTER_SIDE_LENGTH - 1 + 2 * INSERTER_BORDER_SIZE) + tableRect.top - (INSERTER_SIDE_LENGTH - 1 + 2 * INSERTER_BORDER_SIZE) }px`; (div.firstChild as HTMLElement).style.height = `${tableRect.bottom - tableRect.top}px`; } @@ -62,44 +67,56 @@ export default function createTableInserter( div, td, isHorizontal, - editor.getSizeTransformer(), - onInsert + editor, + onInsert, + getOnMouseOut ); return { div, featureHandler: handler, node: td }; } + + return null; } class TableInsertHandler implements Disposable { + private onMouseOutEvent: null | ((ev: MouseEvent) => void); constructor( private div: HTMLDivElement, private td: HTMLTableCellElement, private isHorizontal: boolean, - private sizeTransformer: SizeTransformer, - private onInsert: () => void + private editor: IEditor, + private onInsert: (table: HTMLTableElement) => void, + getOnMouseOut: (feature: HTMLElement) => (ev: MouseEvent) => void ) { this.div.addEventListener('click', this.insertTd); + this.onMouseOutEvent = getOnMouseOut(div); + this.div.addEventListener('mouseout', this.onMouseOutEvent); } dispose() { this.div.removeEventListener('click', this.insertTd); - this.div = null; + + if (this.onMouseOutEvent) { + this.div.removeEventListener('mouseout', this.onMouseOutEvent); + } + + this.onMouseOutEvent = null; } private insertTd = () => { let vtable = new VTable(this.td); if (!this.isHorizontal) { - vtable.normalizeTableCellSize(this.sizeTransformer); + vtable.normalizeTableCellSize(this.editor.getZoomScale()); // Since adding new column will cause table width to change, we need to remove width properties vtable.table.removeAttribute('width'); - vtable.table.style.width = null; + vtable.table.style.setProperty('width', null); } vtable.edit(this.isHorizontal ? TableOperation.InsertBelow : TableOperation.InsertRight); vtable.writeBack(); - this.onInsert(); + this.onInsert(vtable.table); }; } @@ -110,7 +127,7 @@ function getInsertElementData( backgroundColor: string ): CreateElementData { const inserterColor = isDark ? INSERTER_COLOR_DARK_MODE : INSERTER_COLOR; - const outerDivStyle = `position: fixed; width: ${INSERTER_SIDE_LENGTH}px; height: ${INSERTER_SIDE_LENGTH}px; font-size: 16px; color: ${inserterColor}; line-height: 10px; vertical-align: middle; text-align: center; cursor: pointer; border: solid ${INSERTER_BORDER_SIZE}px ${inserterColor}; border-radius: 50%; background-color: ${backgroundColor}`; + const outerDivStyle = `position: fixed; width: ${INSERTER_SIDE_LENGTH}px; height: ${INSERTER_SIDE_LENGTH}px; font-size: 16px; color: black; line-height: 8px; vertical-align: middle; text-align: center; cursor: pointer; border: solid ${INSERTER_BORDER_SIZE}px ${inserterColor}; border-radius: 50%; background-color: ${backgroundColor}`; const leftOrRight = isRTL ? 'right' : 'left'; const childBaseStyles = `position: absolute; box-sizing: border-box; background-color: ${backgroundColor};`; const childInfo: CreateElementData = { diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableResizer.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableResizer.ts index b24c095af1df..f383ffcb5b4d 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableResizer.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableResizer.ts @@ -1,7 +1,7 @@ import DragAndDropHelper from '../../../pluginUtils/DragAndDropHelper'; import TableEditFeature from './TableEditorFeature'; import { createElement, normalizeRect, VTable } from 'roosterjs-editor-dom'; -import { KnownCreateElementDataIndex, SizeTransformer } from 'roosterjs-editor-types'; +import { CreateElementData } from 'roosterjs-editor-types'; const TABLE_RESIZER_LENGTH = 12; const MIN_CELL_WIDTH = 30; @@ -12,17 +12,26 @@ const MIN_CELL_HEIGHT = 20; */ export default function createTableResizer( table: HTMLTableElement, - sizeTransformer: SizeTransformer, + zoomScale: number, isRTL: boolean, - onDragEnd: () => false -): TableEditFeature { + onStart: () => void, + onDragEnd: () => false, + onShowHelperElement?: ( + elementData: CreateElementData, + helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector' + ) => void +): TableEditFeature | null { const document = table.ownerDocument; - const div = createElement( - isRTL - ? KnownCreateElementDataIndex.TableResizerRTL - : KnownCreateElementDataIndex.TableResizerLTR, - document - ) as HTMLDivElement; + const createElementData = { + tag: 'div', + style: `position: fixed; cursor: ${ + isRTL ? 'ne' : 'nw' + }-resize; user-select: none; border: 1px solid #808080`, + }; + + onShowHelperElement?.(createElementData, 'TableResizer'); + + const div = createElement(createElementData, document) as HTMLDivElement; div.style.width = `${TABLE_RESIZER_LENGTH}px`; div.style.height = `${TABLE_RESIZER_LENGTH}px`; @@ -31,7 +40,8 @@ export default function createTableResizer( const context: DragAndDropContext = { isRTL, table, - sizeTransformer, + zoomScale, + onStart, }; setResizeDivPosition(context, div); @@ -45,7 +55,7 @@ export default function createTableResizer( onDragging, onDragEnd, }, - sizeTransformer + zoomScale ); return { node: table, div, featureHandler }; @@ -54,7 +64,8 @@ export default function createTableResizer( interface DragAndDropContext { table: HTMLTableElement; isRTL: boolean; - sizeTransformer: SizeTransformer; + zoomScale: number; + onStart: () => void; } interface DragAndDropInitValue { @@ -62,10 +73,12 @@ interface DragAndDropInitValue { vTable: VTable; } -function onDragStart(context: DragAndDropContext, event: MouseEvent) { +function onDragStart(context: DragAndDropContext) { + context.onStart(); + return { originalRect: context.table.getBoundingClientRect(), - vTable: new VTable(context.table, true /*normalizeTable*/, context.sizeTransformer), + vTable: new VTable(context.table, true /*normalizeTable*/, context.zoomScale), }; } @@ -76,22 +89,22 @@ function onDragging( deltaX: number, deltaY: number ) { - const { isRTL, sizeTransformer } = context; + const { isRTL, zoomScale } = context; const { originalRect, vTable } = initValue; - const ratioX = 1.0 + (deltaX / sizeTransformer(originalRect.width)) * (isRTL ? -1 : 1); - const ratioY = 1.0 + deltaY / sizeTransformer(originalRect.height); + const ratioX = 1.0 + (deltaX / originalRect.width) * zoomScale * (isRTL ? -1 : 1); + const ratioY = 1.0 + (deltaY / originalRect.height) * zoomScale; const shouldResizeX = Math.abs(ratioX - 1.0) > 1e-3; const shouldResizeY = Math.abs(ratioY - 1.0) > 1e-3; - if (shouldResizeX || shouldResizeY) { + if (vTable.cells && (shouldResizeX || shouldResizeY)) { for (let i = 0; i < vTable.cells.length; i++) { for (let j = 0; j < vTable.cells[i].length; j++) { const cell = vTable.cells[i][j]; if (cell.td) { if (shouldResizeX) { // the width of some external table is fixed, we need to make it resizable - vTable.table.style.width = null; - const newWidth = sizeTransformer(cell.width * ratioX); + vTable.table.style.setProperty('width', null); + const newWidth = ((cell.width ?? 0) * ratioX) / zoomScale; cell.td.style.boxSizing = 'border-box'; if (newWidth >= MIN_CELL_WIDTH) { cell.td.style.wordBreak = 'break-word'; @@ -102,21 +115,22 @@ function onDragging( if (shouldResizeY) { // the height of some external table is fixed, we need to make it resizable - vTable.table.style.height = null; + vTable.table.style.setProperty('height', null); if (j == 0) { - const newHeight = sizeTransformer(cell.height * ratioY); + const newHeight = ((cell.height ?? 0) * ratioY) / zoomScale; if (newHeight >= MIN_CELL_HEIGHT) { cell.td.style.height = `${newHeight}px`; } } else { - cell.td.style.height = null; + cell.td.style.setProperty('height', null); } } } } } - vTable.writeBack(); + // To avoid apply format styles when the table is being resizing, the skipApplyFormat is set to true. + vTable.writeBack(true /**skipApplyFormat*/); return true; } else { return false; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableSelector.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableSelector.ts new file mode 100644 index 000000000000..35b507828a5c --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableSelector.ts @@ -0,0 +1,132 @@ +import DragAndDropHandler from '../../../pluginUtils/DragAndDropHandler'; +import DragAndDropHelper from '../../../pluginUtils/DragAndDropHelper'; +import TableEditorFeature from './TableEditorFeature'; +import { createElement, normalizeRect, safeInstanceOf } from 'roosterjs-editor-dom'; +import { CreateElementData, IEditor, Rect } from 'roosterjs-editor-types'; + +const TABLE_SELECTOR_LENGTH = 12; +const TABLE_SELECTOR_ID = '_Table_Selector'; + +/** + * @internal + */ +export default function createTableSelector( + table: HTMLTableElement, + zoomScale: number, + editor: IEditor, + onFinishDragging: (table: HTMLTableElement) => void, + getOnMouseOut: (feature: HTMLElement) => (ev: MouseEvent) => void, + onShowHelperElement?: ( + elementData: CreateElementData, + helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector' + ) => void, + contentDiv?: EventTarget | null +): TableEditorFeature | null { + const rect = normalizeRect(table.getBoundingClientRect()); + + if (!isTableTopVisible(editor, rect, contentDiv)) { + return null; + } + + const document = table.ownerDocument; + const createElementData = { + tag: 'div', + style: 'position: fixed; cursor: all-scroll; user-select: none; border: 1px solid #808080', + }; + + onShowHelperElement?.(createElementData, 'TableSelector'); + + const div = createElement(createElementData, document) as HTMLDivElement; + + div.id = TABLE_SELECTOR_ID; + div.style.width = `${TABLE_SELECTOR_LENGTH}px`; + div.style.height = `${TABLE_SELECTOR_LENGTH}px`; + document.body.appendChild(div); + + const context: TableSelectorContext = { + table, + zoomScale, + rect, + }; + + setSelectorDivPosition(context, div); + + const onDragEnd = (context: TableSelectorContext, event: MouseEvent): false => { + if (event.target == div) { + onFinishDragging(context.table); + } + return false; + }; + + const featureHandler = new TableSelectorFeature( + div, + context, + setSelectorDivPosition, + { + onDragEnd, + }, + zoomScale, + getOnMouseOut + ); + + return { div, featureHandler, node: table }; +} + +interface TableSelectorContext { + table: HTMLTableElement; + zoomScale: number; + rect: Rect | null; +} + +interface TableSelectorInitValue { + event: MouseEvent; +} + +class TableSelectorFeature extends DragAndDropHelper { + private onMouseOut: ((ev: MouseEvent) => void) | null; + + constructor( + private div: HTMLElement, + context: TableSelectorContext, + onSubmit: (context: TableSelectorContext, trigger: HTMLElement) => void, + handler: DragAndDropHandler, + zoomScale: number, + getOnMouseOut: (feature: HTMLElement) => (ev: MouseEvent) => void, + forceMobile?: boolean + ) { + super(div, context, onSubmit, handler, zoomScale, forceMobile); + this.onMouseOut = getOnMouseOut(div); + div.addEventListener('mouseout', this.onMouseOut); + } + + dispose(): void { + super.dispose(); + if (this.onMouseOut) { + this.div.removeEventListener('mouseout', this.onMouseOut); + } + this.onMouseOut = null; + } +} + +function setSelectorDivPosition(context: TableSelectorContext, trigger: HTMLElement) { + const { rect } = context; + if (rect) { + trigger.style.top = `${rect.top - TABLE_SELECTOR_LENGTH}px`; + trigger.style.left = `${rect.left - TABLE_SELECTOR_LENGTH - 2}px`; + } +} + +function isTableTopVisible( + editor: IEditor, + rect: Rect | null, + contentDiv?: EventTarget | null +): boolean { + const visibleViewport = editor.getVisibleViewport(); + if (contentDiv && safeInstanceOf(contentDiv, 'HTMLElement') && visibleViewport && rect) { + const containerRect = normalizeRect(contentDiv.getBoundingClientRect()); + + return !!containerRect && containerRect.top <= rect.top && visibleViewport.top <= rect.top; + } + + return true; +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Watermark/Watermark.ts b/packages/roosterjs-editor-plugins/lib/plugins/Watermark/Watermark.ts index 1becd05761c8..742788d5894f 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Watermark/Watermark.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Watermark/Watermark.ts @@ -17,15 +17,16 @@ const ENTITY_TYPE = 'WATERMARK_WRAPPER'; * A watermark plugin to manage watermark string for roosterjs */ export default class Watermark implements EditorPlugin { - private editor: IEditor; - private disposer: () => void; + private editor: IEditor | null = null; + private disposer: (() => void) | null = null; + private format: DefaultFormat; /** * Create an instance of Watermark plugin * @param watermark The watermark string */ - constructor(private watermark: string, private format?: DefaultFormat) { - this.format = this.format || { + constructor(private watermark: string, format?: DefaultFormat, private customClass?: string) { + this.format = format || { fontSize: '14px', textColors: { lightModeColor: '#AAAAAA', @@ -57,7 +58,7 @@ export default class Watermark implements EditorPlugin { * Dispose this plugin */ dispose() { - this.disposer(); + this.disposer?.(); this.disposer = null; this.editor = null; } @@ -75,7 +76,8 @@ export default class Watermark implements EditorPlugin { this.showHideWatermark(); } else if ( event.eventType == PluginEventType.EntityOperation && - event.entity.type == ENTITY_TYPE + event.entity.type == ENTITY_TYPE && + this.editor ) { const { operation, @@ -84,13 +86,21 @@ export default class Watermark implements EditorPlugin { if (operation == EntityOperation.ReplaceTemporaryContent) { this.removeWatermark(wrapper); } else if (event.operation == EntityOperation.NewEntity) { - applyFormat(wrapper, this.format, this.editor.isDarkMode()); + applyFormat( + wrapper, + this.format, + this.editor.isDarkMode(), + this.editor.getDarkColorHandler() + ); wrapper.spellcheck = false; } } } private showHideWatermark = () => { + if (!this.editor) { + return; + } const hasFocus = this.editor.hasFocus(); const watermarks = this.editor.queryElements(getEntitySelector(ENTITY_TYPE)); const isShowing = watermarks.length > 0; @@ -99,7 +109,7 @@ export default class Watermark implements EditorPlugin { watermarks.forEach(this.removeWatermark); this.editor.focus(); } else if (!hasFocus && !isShowing && this.editor.isEmpty()) { - insertEntity( + const newEntity = insertEntity( this.editor, ENTITY_TYPE, this.editor.getDocument().createTextNode(this.watermark), @@ -107,6 +117,9 @@ export default class Watermark implements EditorPlugin { false /*isReadonly*/, ContentPosition.Begin ); + if (this.customClass) { + newEntity.wrapper.classList.add(this.customClass); + } } }; @@ -116,7 +129,8 @@ export default class Watermark implements EditorPlugin { // After remove watermark node, if it leaves an empty DIV, append a BR node into it to make it a regular empty line if ( - this.editor.contains(parentNode) && + parentNode && + this.editor?.contains(parentNode) && getTagOfNode(parentNode) == 'DIV' && !parentNode.firstChild ) { diff --git a/packages/roosterjs-editor-plugins/package.json b/packages/roosterjs-editor-plugins/package.json index 8ba2a5c6c8c2..b955d4e4ec03 100644 --- a/packages/roosterjs-editor-plugins/package.json +++ b/packages/roosterjs-editor-plugins/package.json @@ -2,6 +2,7 @@ "name": "roosterjs-editor-plugins", "description": "Plugins for roosterjs", "dependencies": { + "tslib": "^2.3.1", "roosterjs-editor-types": "", "roosterjs-editor-dom": "", "roosterjs-editor-api": "" diff --git a/packages/roosterjs-editor-plugins/test/AutoFormat/autoFormatTest.ts b/packages/roosterjs-editor-plugins/test/AutoFormat/autoFormatTest.ts new file mode 100644 index 000000000000..6218ffc8e339 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/AutoFormat/autoFormatTest.ts @@ -0,0 +1,71 @@ +import * as TestHelper from '../TestHelper'; +import { AutoFormat } from '../../lib/AutoFormat'; +import { EditorPlugin, IEditor, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; + +describe('AutoHyphen |', () => { + let editor: IEditor; + const TEST_ID = 'autoHyphenTest'; + let plugin: EditorPlugin; + beforeEach(() => { + plugin = new AutoFormat(); + editor = TestHelper.initEditor(TEST_ID, [plugin]); + }); + + afterEach(() => { + editor.dispose(); + }); + + const keyDown = (keysTyped: string): PluginEvent => { + return { + eventType: PluginEventType.KeyPress, + rawEvent: { + key: keysTyped, + }, + }; + }; + + function runTestShouldHandleAutoHyphen( + content: string, + keysTyped: string[], + expectedResult: string + ) { + editor.setContent(content); + plugin.onPluginEvent(keyDown(keysTyped[0])); + plugin.onPluginEvent(keyDown(keysTyped[1])); + plugin.onPluginEvent(keyDown(keysTyped[2])); + plugin.onPluginEvent(keyDown(keysTyped[3])); + expect(editor.getContent()).toBe(expectedResult); + } + + it('Should format ', () => { + runTestShouldHandleAutoHyphen( + '
          t--
          ', + ['t', '-', '-', 'b'], + '
          t—
          ' + ); + }); + + it('Should not format| - ', () => { + runTestShouldHandleAutoHyphen( + '
          t—-
          ', + ['t', '-', '-', '-'], + '
          t—-
          ' + ); + }); + + it('Should not format | " "', () => { + runTestShouldHandleAutoHyphen( + '
          t—-
          ', + ['t', '-', '-', ' '], + '
          t—-
          ' + ); + }); + + it('Should not format | ! ', () => { + runTestShouldHandleAutoHyphen( + '
          t—-
          ', + ['t', '-', '-', '!'], + '
          t—-
          ' + ); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/autoLinkFeatureTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/autoLinkFeatureTest.ts index 106a7bf6c756..876ab1265a85 100644 --- a/packages/roosterjs-editor-plugins/test/ContentEdit/features/autoLinkFeatureTest.ts +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/autoLinkFeatureTest.ts @@ -1,407 +1,407 @@ -import * as TestHelper from '../../../../roosterjs-editor-api/test/TestHelper'; -import { AutoLinkFeatures } from '../../../lib/plugins/ContentEdit/features/autoLinkFeatures'; -import { - ImageInlineElement, - LinkInlineElement, - Position, - PositionContentSearcher, -} from 'roosterjs-editor-dom'; -import { - ChangeSource, - ClipboardData, - ContentChangedEvent, - IEditor, - PluginEventType, - PluginKeyboardEvent, - InlineElement, - PositionType, -} from 'roosterjs-editor-types'; -describe('AutoLinkFeature ShouldHandle Tests: ', () => { - let editor: IEditor; - const TEST_ID = 'AutoLinkFeatureShouldHandleTests'; - const TEST_ELEMENT_ID = 'test'; - const rawKeyboardEvent: KeyboardEvent = new KeyboardEvent('keydown', { - shiftKey: false, - altKey: false, - ctrlKey: false, - }); - - beforeEach(() => { - editor = TestHelper.initEditor(TEST_ID); - }); - - afterEach(() => { - let element = document.getElementById(TEST_ID); - if (element) { - element.parentElement.removeChild(element); - } - editor.dispose(); - }); - - function runAutoLinkShouldHandleEvent(content: string, shouldHandleExpect: boolean) { - const keyboardEvent: PluginKeyboardEvent = { - eventType: PluginEventType.KeyDown, - rawEvent: rawKeyboardEvent, - }; - const autoLinkFeature = AutoLinkFeatures.autoLink; - editor.setContent(content); - const element = document.getElementById(TEST_ELEMENT_ID); - editor.select(element, PositionType.End); - const result = autoLinkFeature.shouldHandleEvent(keyboardEvent, editor, false); - - expect(!!result).toBe(shouldHandleExpect); - } - - it('AutoLink | Should Handle Event', () => { - runAutoLinkShouldHandleEvent(`
          site.com
          `, false); - runAutoLinkShouldHandleEvent(`
          asd
          `, false); - runAutoLinkShouldHandleEvent( - `
          site.com
          `, - false - ); - runAutoLinkShouldHandleEvent(`
          `, false); - runAutoLinkShouldHandleEvent(`
          nolink
          `, false); - - runAutoLinkShouldHandleEvent(`
          www.site.com
          `, true); - runAutoLinkShouldHandleEvent(`
          www.site
          `, true); - runAutoLinkShouldHandleEvent( - `
          https://www.site.com
          `, - true - ); - runAutoLinkShouldHandleEvent(`
          https://site.com
          `, true); - runAutoLinkShouldHandleEvent(`
          https://www.site
          `, true); - runAutoLinkShouldHandleEvent( - `
          telnet://192.168.0.0
          `, - true - ); - }); -}); - -describe('UnlinkFeature ShouldHandle Tests: ', () => { - let editor: IEditor; - const TEST_ID = 'UnlinkFeatureShouldHandleTestsTests'; - let contentSearcherOfCursorSpy: jasmine.Spy; - const rawKeyboardEvent: KeyboardEvent = new KeyboardEvent('keydown', { - shiftKey: false, - }); - - beforeEach(() => { - let element = document.getElementById(TEST_ID); - if (element) { - element.parentElement.removeChild(element); - } - editor = TestHelper.initEditor(TEST_ID); - contentSearcherOfCursorSpy = spyOn(editor, 'getContentSearcherOfCursor'); - }); - - afterEach(() => { - editor.dispose(); - }); - - function runUnLinkShouldHandleClipboardTest(element: InlineElement, expected: boolean) { - const aulinkFeature = AutoLinkFeatures.unlinkWhenBackspaceAfterLink; - const keyboardEvent: PluginKeyboardEvent = { - eventType: PluginEventType.KeyDown, - rawEvent: rawKeyboardEvent, - }; - let mockElement = document.createElement('div'); - const mockedPositionContentSearcher = new PositionContentSearcher( - mockElement, - new Position(mockElement, 0) - ); - spyOn(mockedPositionContentSearcher, 'getInlineElementBefore').and.returnValue(element); - contentSearcherOfCursorSpy.and.returnValue(mockedPositionContentSearcher); - const result = aulinkFeature.shouldHandleEvent(keyboardEvent, editor, false); - expect(!!result).toBe(expected); - } - - it('UnlinkWhenBackspaceAfterLink | Should Handle Event ', () => { - runUnLinkShouldHandleClipboardTest( - new ImageInlineElement(null, null) as InlineElement, - false - ); - runUnLinkShouldHandleClipboardTest( - new LinkInlineElement(null, null) as InlineElement, - true - ); - }); -}); - -describe('Auto Link ShouldHandle On Paste', () => { - let editor: IEditor; - const TEST_ID = 'AutoLinkShouldHandleOnPaste'; - const TEST_ELEMENT_ID = 'AutoLinkShouldHandleOnPastetest'; - const autoLinkFeature = AutoLinkFeatures.autoLink; - let editorSearchCursorSpy: jasmine.Spy; - - beforeEach(() => { - let element = document.getElementById(TEST_ID); - if (element) { - element.parentElement.removeChild(element); - } - editor = TestHelper.initEditor(TEST_ID); - editorSearchCursorSpy = spyOn(editor, 'getContentSearcherOfCursor'); - }); - - afterEach(() => { - editor.dispose(); - }); - - function runShouldHandleClipboardTest( - clipboardText: string, - inlineTextHTML: string, - expected: boolean, - trailingTest: boolean = false - ) { - const clipboard: ClipboardData = { - customValues: null, - image: null, - rawHtml: null, - text: clipboardText, - types: [], - }; - const event: ContentChangedEvent = { - eventType: PluginEventType.ContentChanged, - source: ChangeSource.Paste, - data: clipboard, - }; - editor.setContent( - `
          ${inlineTextHTML}
          ` - ); - - const element = document.getElementById(TEST_ID); - editor.select(element, PositionType.End); - const range = editor.getSelectionRange(); - - const mockedPositionContentSearcher = new PositionContentSearcher( - element, - new Position(element, range.endOffset) - ); - - spyOn(mockedPositionContentSearcher, 'getRangeFromText').and.returnValue( - trailingTest ? null : range - ); - if (trailingTest) { - spyOn(mockedPositionContentSearcher, 'getWordBefore').and.returnValue(inlineTextHTML); - } - editorSearchCursorSpy.and.returnValue(mockedPositionContentSearcher); - - editor.focus(); - - const result = autoLinkFeature.shouldHandleEvent(event, editor, false); - - expect(!!result).toBe(expected); - } - - it('AutoLink | Should Handle Event | Clipboard Link', () => { - runShouldHandleClipboardTest('www.site.com', 'www.site.com', true); - runShouldHandleClipboardTest('', 'www.site.com', false); - - runShouldHandleClipboardTest('www.site.com(', '', false, true); - runShouldHandleClipboardTest('www.site.com)', '', false, true); - runShouldHandleClipboardTest('www.site.com{', '', false, true); - runShouldHandleClipboardTest('www.site.com}', '', false, true); - runShouldHandleClipboardTest('www.site.com[', '', false, true); - runShouldHandleClipboardTest('www.site.com]', '', false, true); - - runShouldHandleClipboardTest('www.site.com(', 'www.site.com(', false, true); - runShouldHandleClipboardTest('www.site.com)', 'www.site.com)', true, true); - - runShouldHandleClipboardTest('www.site.com{', 'www.site.com{', false, true); - runShouldHandleClipboardTest('www.site.com}', 'www.site.com}', true, true); - - runShouldHandleClipboardTest('www.site.com[', 'www.site.com[', false, true); - runShouldHandleClipboardTest('www.site.com]', 'www.site.com]', true, true); - }); -}); - -describe('AutoLinkFeature HandleEvent Tests: ', () => { - const TEST_ID = 'AutoLinkFeatureHandleEvent'; - const TEST_ELEMENT_ID = 'test'; - const rawKeyboardEvent: KeyboardEvent = new KeyboardEvent('keydown', { - shiftKey: false, - altKey: false, - ctrlKey: false, - }); - - let editor: IEditor; - let editorContent: string; - - beforeEach(done => { - editor = TestHelper.initEditor(TEST_ID); - editor.runAsync = (callback: (editor: IEditor) => void) => { - callback(editor); - return () => {}; - }; - done(); - }); - - afterEach(done => { - editor.dispose(); - let deleteElement = document.getElementById(TEST_ID); - deleteElement.parentElement.removeChild(deleteElement); - done(); - }); - - function runAutoLinkhandleEventTest(content: string) { - const keyboardEvent: PluginKeyboardEvent = { - eventType: PluginEventType.KeyDown, - rawEvent: rawKeyboardEvent, - }; - - const autoLinkFeature = AutoLinkFeatures.autoLink; - editor.setContent(content); - const element = document.getElementById(TEST_ELEMENT_ID); - editor.select(element.firstChild, PositionType.End); - - autoLinkFeature.handleEvent(keyboardEvent, editor); - } - - function runWrap(content: string, expected: string) { - runAutoLinkhandleEventTest(content); - editorContent = editor.getContent(); - expect(editorContent).toBe(expected); - } - - it('AutoLink | Handle Event 1', () => { - runWrap( - `
          www.site.com
          `, - `` - ); - }); - - it('AutoLink | Handle Event 2', () => { - runWrap( - `
          www.site
          `, - '' - ); - }); - - it('AutoLink | Handle Event 3', () => { - runWrap( - `
          https://www.site.com
          `, - '' - ); - }); - - it('AutoLink | Handle Event 4', () => { - runWrap( - `
          www.site
          `, - '' - ); - }); - - it('AutoLink | Handle Event 5', () => { - runWrap( - `
          https://site.com
          `, - '' - ); - }); - - it('AutoLink | Handle Event 6', () => { - runWrap( - `
          https://www.site
          `, - '' - ); - }); - - it('AutoLink | Handle Event 7', () => { - runWrap( - `
          telnet://192.168.0.0
          `, - '' - ); - }); -}); - -describe('UnlinkFeature HandleEvent Tests: ', () => { - const TEST_ID = 'AutoLinkFeatureHandleEvent'; - const TEST_ELEMENT_ID = 'test'; - const rawKeyboardEvent: KeyboardEvent = new KeyboardEvent('keydown', { - shiftKey: false, - altKey: false, - ctrlKey: false, - }); - - let editor: IEditor; - let editorContent: string; - - beforeEach(done => { - editor = TestHelper.initEditor(TEST_ID); - done(); - }); - - afterEach(done => { - editor.dispose(); - let deleteElement = document.getElementById(TEST_ID); - deleteElement.parentElement.removeChild(deleteElement); - done(); - }); - - function runAutoLinkhandleEventTest(content: string) { - const keyboardEvent: PluginKeyboardEvent = { - eventType: PluginEventType.KeyDown, - rawEvent: rawKeyboardEvent, - }; - - editor.setContent(content); - const element = document.getElementById(TEST_ELEMENT_ID); - editor.select(element.firstChild, PositionType.End); - - AutoLinkFeatures.unlinkWhenBackspaceAfterLink.handleEvent(keyboardEvent, editor); - } - - function runWrap(expected: string, content: string) { - runAutoLinkhandleEventTest(content); - editorContent = editor.getContent(); - expect(editorContent).toBe(expected); - } - - it('Unlink | Handle Event 1', () => { - runWrap( - `
          www.site.com
          `, - `` - ); - }); - - it('Unlink | Handle Event 2', () => { - runWrap( - `
          www.site
          `, - '' - ); - }); - - it('Unlink | Handle Event 3', () => { - runWrap( - `
          https://www.site.com
          `, - '' - ); - }); - - it('Unlink | Handle Event 4', () => { - runWrap( - `
          www.site
          `, - '' - ); - }); - - it('Unlink | Handle Event 5', () => { - runWrap( - `
          https://site.com
          `, - '' - ); - }); - - it('Unlink | Handle Event 6', () => { - runWrap( - `
          https://www.site
          `, - '' - ); - }); - - it('Unlink | Handle Event 7', () => { - runWrap( - `
          telnet://192.168.0.0
          `, - '' - ); - }); -}); +import * as TestHelper from '../../../../roosterjs-editor-api/test/TestHelper'; +import { AutoLinkFeatures } from '../../../lib/plugins/ContentEdit/features/autoLinkFeatures'; +import { + ImageInlineElement, + LinkInlineElement, + Position, + PositionContentSearcher, +} from 'roosterjs-editor-dom'; +import { + ChangeSource, + ClipboardData, + ContentChangedEvent, + IEditor, + PluginEventType, + PluginKeyboardEvent, + InlineElement, + PositionType, +} from 'roosterjs-editor-types'; +describe('AutoLinkFeature ShouldHandle Tests: ', () => { + let editor: IEditor; + const TEST_ID = 'AutoLinkFeatureShouldHandleTests'; + const TEST_ELEMENT_ID = 'test'; + const rawKeyboardEvent: KeyboardEvent = new KeyboardEvent('keydown', { + shiftKey: false, + altKey: false, + ctrlKey: false, + }); + + beforeEach(() => { + editor = TestHelper.initEditor(TEST_ID); + }); + + afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement.removeChild(element); + } + editor.dispose(); + }); + + function runAutoLinkShouldHandleEvent(content: string, shouldHandleExpect: boolean) { + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: rawKeyboardEvent, + }; + const autoLinkFeature = AutoLinkFeatures.autoLink; + editor.setContent(content); + const element = document.getElementById(TEST_ELEMENT_ID); + editor.select(element, PositionType.End); + const result = autoLinkFeature.shouldHandleEvent(keyboardEvent, editor, false); + + expect(!!result).toBe(shouldHandleExpect); + } + + it('AutoLink | Should Handle Event', () => { + runAutoLinkShouldHandleEvent(`
          site.com
          `, false); + runAutoLinkShouldHandleEvent(`
          asd
          `, false); + runAutoLinkShouldHandleEvent( + `
          site.com
          `, + false + ); + runAutoLinkShouldHandleEvent(`
          `, false); + runAutoLinkShouldHandleEvent(`
          nolink
          `, false); + + runAutoLinkShouldHandleEvent(`
          www.site.com
          `, true); + runAutoLinkShouldHandleEvent(`
          www.site
          `, true); + runAutoLinkShouldHandleEvent( + `
          https://www.site.com
          `, + true + ); + runAutoLinkShouldHandleEvent(`
          https://site.com
          `, true); + runAutoLinkShouldHandleEvent(`
          https://www.site
          `, true); + runAutoLinkShouldHandleEvent( + `
          telnet://192.168.0.0
          `, + true + ); + }); +}); + +describe('UnlinkFeature ShouldHandle Tests: ', () => { + let editor: IEditor; + const TEST_ID = 'UnlinkFeatureShouldHandleTestsTests'; + let contentSearcherOfCursorSpy: jasmine.Spy; + const rawKeyboardEvent: KeyboardEvent = new KeyboardEvent('keydown', { + shiftKey: false, + }); + + beforeEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement.removeChild(element); + } + editor = TestHelper.initEditor(TEST_ID); + contentSearcherOfCursorSpy = spyOn(editor, 'getContentSearcherOfCursor'); + }); + + afterEach(() => { + editor.dispose(); + }); + + function runUnLinkShouldHandleClipboardTest(element: InlineElement, expected: boolean) { + const aulinkFeature = AutoLinkFeatures.unlinkWhenBackspaceAfterLink; + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: rawKeyboardEvent, + }; + let mockElement = document.createElement('div'); + const mockedPositionContentSearcher = new PositionContentSearcher( + mockElement, + new Position(mockElement, 0) + ); + spyOn(mockedPositionContentSearcher, 'getInlineElementBefore').and.returnValue(element); + contentSearcherOfCursorSpy.and.returnValue(mockedPositionContentSearcher); + const result = aulinkFeature.shouldHandleEvent(keyboardEvent, editor, false); + expect(!!result).toBe(expected); + } + + it('UnlinkWhenBackspaceAfterLink | Should Handle Event ', () => { + runUnLinkShouldHandleClipboardTest( + new ImageInlineElement(null, null) as InlineElement, + false + ); + runUnLinkShouldHandleClipboardTest( + new LinkInlineElement(null, null) as InlineElement, + true + ); + }); +}); + +describe('Auto Link ShouldHandle On Paste', () => { + let editor: IEditor; + const TEST_ID = 'AutoLinkShouldHandleOnPaste'; + const TEST_ELEMENT_ID = 'AutoLinkShouldHandleOnPastetest'; + const autoLinkFeature = AutoLinkFeatures.autoLink; + let editorSearchCursorSpy: jasmine.Spy; + + beforeEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement.removeChild(element); + } + editor = TestHelper.initEditor(TEST_ID); + editorSearchCursorSpy = spyOn(editor, 'getContentSearcherOfCursor'); + }); + + afterEach(() => { + editor.dispose(); + }); + + function runShouldHandleClipboardTest( + clipboardText: string, + inlineTextHTML: string, + expected: boolean, + trailingTest: boolean = false + ) { + const clipboard: ClipboardData = { + customValues: null, + image: null, + rawHtml: null, + text: clipboardText, + types: [], + }; + const event: ContentChangedEvent = { + eventType: PluginEventType.ContentChanged, + source: ChangeSource.Paste, + data: clipboard, + }; + editor.setContent( + `
          ${inlineTextHTML}
          ` + ); + + const element = document.getElementById(TEST_ID); + editor.select(element, PositionType.End); + const range = editor.getSelectionRange(); + + const mockedPositionContentSearcher = new PositionContentSearcher( + element, + new Position(element, range.endOffset) + ); + + spyOn(mockedPositionContentSearcher, 'getRangeFromText').and.returnValue( + trailingTest ? null : range + ); + if (trailingTest) { + spyOn(mockedPositionContentSearcher, 'getWordBefore').and.returnValue(inlineTextHTML); + } + editorSearchCursorSpy.and.returnValue(mockedPositionContentSearcher); + + editor.focus(); + + const result = autoLinkFeature.shouldHandleEvent(event, editor, false); + + expect(!!result).toBe(expected); + } + + it('AutoLink | Should Handle Event | Clipboard Link', () => { + runShouldHandleClipboardTest('www.site.com', 'www.site.com', true); + runShouldHandleClipboardTest('', 'www.site.com', false); + + runShouldHandleClipboardTest('www.site.com(', '', false, true); + runShouldHandleClipboardTest('www.site.com)', '', false, true); + runShouldHandleClipboardTest('www.site.com{', '', false, true); + runShouldHandleClipboardTest('www.site.com}', '', false, true); + runShouldHandleClipboardTest('www.site.com[', '', false, true); + runShouldHandleClipboardTest('www.site.com]', '', false, true); + + runShouldHandleClipboardTest('www.site.com(', 'www.site.com(', false, true); + runShouldHandleClipboardTest('www.site.com)', 'www.site.com)', true, true); + + runShouldHandleClipboardTest('www.site.com{', 'www.site.com{', false, true); + runShouldHandleClipboardTest('www.site.com}', 'www.site.com}', true, true); + + runShouldHandleClipboardTest('www.site.com[', 'www.site.com[', false, true); + runShouldHandleClipboardTest('www.site.com]', 'www.site.com]', true, true); + }); +}); + +describe('AutoLinkFeature HandleEvent Tests: ', () => { + const TEST_ID = 'AutoLinkFeatureHandleEvent'; + const TEST_ELEMENT_ID = 'test'; + const rawKeyboardEvent: KeyboardEvent = new KeyboardEvent('keydown', { + shiftKey: false, + altKey: false, + ctrlKey: false, + }); + + let editor: IEditor; + let editorContent: string; + + beforeEach(done => { + editor = TestHelper.initEditor(TEST_ID); + editor.runAsync = (callback: (editor: IEditor) => void) => { + callback(editor); + return () => {}; + }; + done(); + }); + + afterEach(done => { + editor.dispose(); + let deleteElement = document.getElementById(TEST_ID); + deleteElement.parentElement.removeChild(deleteElement); + done(); + }); + + function runAutoLinkhandleEventTest(content: string) { + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: rawKeyboardEvent, + }; + + const autoLinkFeature = AutoLinkFeatures.autoLink; + editor.setContent(content); + const element = document.getElementById(TEST_ELEMENT_ID); + editor.select(element.firstChild, PositionType.End); + + autoLinkFeature.handleEvent(keyboardEvent, editor); + } + + function runWrap(content: string, expected: string) { + runAutoLinkhandleEventTest(content); + editorContent = editor.getContent(); + expect(editorContent).toBe(expected); + } + + it('AutoLink | Handle Event 1', () => { + runWrap( + `
          www.site.com
          `, + `` + ); + }); + + it('AutoLink | Handle Event 2', () => { + runWrap( + `
          www.site
          `, + '' + ); + }); + + it('AutoLink | Handle Event 3', () => { + runWrap( + `
          https://www.site.com
          `, + '' + ); + }); + + it('AutoLink | Handle Event 4', () => { + runWrap( + `
          www.site
          `, + '' + ); + }); + + it('AutoLink | Handle Event 5', () => { + runWrap( + `
          https://site.com
          `, + '' + ); + }); + + it('AutoLink | Handle Event 6', () => { + runWrap( + `
          https://www.site
          `, + '' + ); + }); + + it('AutoLink | Handle Event 7', () => { + runWrap( + `
          telnet://192.168.0.0
          `, + '' + ); + }); +}); + +describe('UnlinkFeature HandleEvent Tests: ', () => { + const TEST_ID = 'AutoLinkFeatureHandleEvent'; + const TEST_ELEMENT_ID = 'test'; + const rawKeyboardEvent: KeyboardEvent = new KeyboardEvent('keydown', { + shiftKey: false, + altKey: false, + ctrlKey: false, + }); + + let editor: IEditor; + let editorContent: string; + + beforeEach(done => { + editor = TestHelper.initEditor(TEST_ID); + done(); + }); + + afterEach(done => { + editor.dispose(); + let deleteElement = document.getElementById(TEST_ID); + deleteElement.parentElement.removeChild(deleteElement); + done(); + }); + + function runAutoLinkhandleEventTest(content: string) { + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: rawKeyboardEvent, + }; + + editor.setContent(content); + const element = document.getElementById(TEST_ELEMENT_ID); + editor.select(element.firstChild, PositionType.End); + + AutoLinkFeatures.unlinkWhenBackspaceAfterLink.handleEvent(keyboardEvent, editor); + } + + function runWrap(expected: string, content: string) { + runAutoLinkhandleEventTest(content); + editorContent = editor.getContent(); + expect(editorContent).toBe(expected); + } + + it('Unlink | Handle Event 1', () => { + runWrap( + `
          www.site.com
          `, + `` + ); + }); + + it('Unlink | Handle Event 2', () => { + runWrap( + `
          www.site
          `, + '' + ); + }); + + it('Unlink | Handle Event 3', () => { + runWrap( + `
          https://www.site.com
          `, + '' + ); + }); + + it('Unlink | Handle Event 4', () => { + runWrap( + `
          www.site
          `, + '' + ); + }); + + it('Unlink | Handle Event 5', () => { + runWrap( + `
          https://site.com
          `, + '' + ); + }); + + it('Unlink | Handle Event 6', () => { + runWrap( + `
          https://www.site
          `, + '' + ); + }); + + it('Unlink | Handle Event 7', () => { + runWrap( + `
          telnet://192.168.0.0
          `, + '' + ); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/codeFeaturesTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/codeFeaturesTest.ts new file mode 100644 index 000000000000..32379a982b0b --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/codeFeaturesTest.ts @@ -0,0 +1,164 @@ +import { IEditor, PluginEventType, PluginKeyboardEvent, Keys } from 'roosterjs-editor-types'; +import * as TestHelper from '../../../../roosterjs-editor-api/test/TestHelper'; +import { CodeFeatures } from '../../../lib/plugins/ContentEdit/features/codeFeatures'; + +const TEST_ELEMENT_ID = 'test_codeFeatures'; +const TEST_EDITOR_ID = 'testEditor_codeFeatures'; + +describe('CodeFeatures', () => { + let editor: IEditor; + + beforeEach(() => { + editor = TestHelper.initEditor(TEST_EDITOR_ID); + }); + + afterEach(() => { + document.getElementById(TEST_EDITOR_ID)?.remove(); + editor.dispose(); + editor = null; + }); + + function runShouldHandleEvent( + shouldHandleEventFunc: Function, + content: string, + whichKey: number, + shouldHandle: boolean + ) { + editor.setContent(content); + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID); + const range = new Range(); + range.setStart(element, 0); + editor.select(range); + const keyboardEvent: PluginKeyboardEvent = { + rawEvent: new KeyboardEvent('keydown', { + shiftKey: false, + altKey: false, + ctrlKey: false, + which: whichKey, + }), + eventType: PluginEventType.KeyDown, + }; + expect(!!shouldHandleEventFunc(keyboardEvent, editor, false)).toBe(shouldHandle); + } + + function runHandleEvent( + handleEventFunc: Function, + content: string, + whichKey: number, + expectedContent: string + ) { + editor.setContent(content); + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID); + const range = new Range(); + range.setStart(element, 0); + editor.select(range); + const keyboardEvent: PluginKeyboardEvent = { + rawEvent: new KeyboardEvent('keydown', { + shiftKey: false, + altKey: false, + ctrlKey: false, + which: whichKey, + }), + eventType: PluginEventType.KeyDown, + }; + handleEventFunc(keyboardEvent, editor, false); + expect(editor.getContent()).toBe(expectedContent); + } + + describe('RemoveCodeWhenEnterOnEmptyLine - shouldHandle', () => { + const { shouldHandleEvent } = CodeFeatures.removeCodeWhenEnterOnEmptyLine; + + it('should handle when enter is pressed and cursor is in empty code block', () => { + runShouldHandleEvent( + shouldHandleEvent, + `

          `, + Keys.ENTER, + true + ); + }); + + it('should not handle when enter is pressed and cursor is in an empty block which is not code', () => { + runShouldHandleEvent( + shouldHandleEvent, + `

          `, + Keys.ENTER, + false + ); + }); + + it('should not handle when enter is pressed and cursor is in a non-empty code block', () => { + runShouldHandleEvent( + shouldHandleEvent, + `
          test
          `, + Keys.ENTER, + false + ); + }); + }); + + describe('RemoveCodeWhenEnterOnEmptyLine - handleEvent', () => { + const { handleEvent } = CodeFeatures.removeCodeWhenEnterOnEmptyLine; + + it('should remove code block when enter is pressed and cursor is in empty code block', () => { + runHandleEvent( + handleEvent, + `

          `, + Keys.ENTER, + `

          ` + ); + }); + + it('should not disturb other lines when selecting an empty line between two non-empty lines', () => { + runHandleEvent( + handleEvent, + `
          hello

          hello
          `, + Keys.ENTER, + `
          hello

          hello
          ` + ); + }); + }); + + describe('RemoveCodeWhenBackspaceOnEmptyLine - shouldHandle', () => { + const { shouldHandleEvent } = CodeFeatures.removeCodeWhenBackspaceOnEmptyFirstLine; + + it('should handle when backspace is pressed and cursor is in empty code block', () => { + runShouldHandleEvent( + shouldHandleEvent, + `

          `, + Keys.BACKSPACE, + true + ); + }); + + it('should not handle when backspace is pressed and cursor is in an empty block which is not code', () => { + runShouldHandleEvent( + shouldHandleEvent, + `

          `, + Keys.BACKSPACE, + false + ); + }); + + it('should not handle when backspace is pressed and cursor is in a non-empty code block', () => { + runShouldHandleEvent( + shouldHandleEvent, + `
          test
          `, + Keys.BACKSPACE, + false + ); + }); + }); + + describe('RemoveCodeWhenBackspaceOnEmptyLine - handleEvent', () => { + const { handleEvent } = CodeFeatures.removeCodeWhenBackspaceOnEmptyFirstLine; + + it('should remove code block when backspace is pressed and cursor is in empty code block', () => { + runHandleEvent( + handleEvent, + `

          `, + Keys.BACKSPACE, + `

          ` + ); + }); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/inlineEntityFeatureTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/inlineEntityFeatureTest.ts new file mode 100644 index 000000000000..02ac0a89ca78 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/inlineEntityFeatureTest.ts @@ -0,0 +1,852 @@ +import * as addDelimiters from 'roosterjs-editor-dom/lib/delimiter/addDelimiters'; +import * as getComputedStyles from 'roosterjs-editor-dom/lib/utils/getComputedStyles'; +import { EntityFeatures } from '../../../lib/plugins/ContentEdit/features/entityFeatures'; +import { + commitEntity, + ContentTraverser, + findClosestElementAncestor, + getBlockElementAtNode, + Position, + PositionContentSearcher, +} from 'roosterjs-editor-dom'; +import { + Entity, + ExperimentalFeatures, + IEditor, + Keys, + PluginKeyDownEvent, + BlockElement, +} from 'roosterjs-editor-types'; + +describe('Content Edit Features |', () => { + const { moveBetweenDelimitersFeature, removeEntityBetweenDelimiters } = EntityFeatures; + let entity: Entity; + let delimiterAfter: Element | null; + let delimiterBefore: Element | null; + let wrapper: HTMLElement; + let editor: IEditor; + let select: jasmine.Spy; + let triggerContentChangedEvent: jasmine.Spy; + let testContainer: HTMLDivElement; + + let defaultEvent = {}; + let extendSpy: jasmine.Spy; + let event: PluginKeyDownEvent; + let preventDefaultSpy: jasmine.Spy; + + beforeAll(() => { + cleanUp(); + }); + + beforeEach(() => { + preventDefaultSpy = jasmine.createSpy('preventDefault'); + extendSpy = jasmine.createSpy('expand'); + cleanUp(); + defaultEvent = {}; + testContainer = document.createElement('div'); + document.body.appendChild(testContainer); + + wrapper = document.createElement('span'); + wrapper.innerHTML = 'Test'; + + testContainer.appendChild(wrapper); + select = jasmine.createSpy('select'); + triggerContentChangedEvent = jasmine.createSpy('triggerContentChangedEvent'); + + editor = ({ + getDocument: () => document, + getElementAtCursor: (selector: string, node: Node) => + selector && node + ? findClosestElementAncestor(node, document.body, selector) + : testContainer, + addContentEditFeature: () => {}, + queryElements: (selector: string) => document.querySelectorAll(selector), + triggerPluginEvent: (arg0: any, arg1: any) => triggerContentChangedEvent(arg0, arg1), + runAsync: (callback: () => void) => callback(), + getSelectionRange: () => + { + collapsed: true, + }, + select, + isFeatureEnabled: (feature: ExperimentalFeatures) => + feature === ExperimentalFeatures.InlineEntityReadOnlyDelimiters, + getBodyTraverser: (startNode?: Node) => + ContentTraverser.createBodyTraverser(testContainer, startNode), + getBlockElementAtNode: (node: Node) => getBlockElementAtNode(document.body, node), + }); + + ({ entity, delimiterAfter, delimiterBefore } = addEntityBeforeEach(entity, wrapper)); + spyOn(addDelimiters, 'default').and.callThrough(); + }); + + afterAll(() => { + cleanUp(); + }); + + describe('Move Before |', () => { + function runTest( + element: Element | Position | null, + expected: boolean, + event: PluginKeyDownEvent + ) { + setEditorFuncs(editor, element, testContainer); + + const result = moveBetweenDelimitersFeature.shouldHandleEvent( + event, + editor, + false /* ctrlOrMeta */ + ); + + expect(result).toBe(expected); + return event; + } + function runTests() { + it('DelimiterAfter, shouldHandle and Handle, no shiftKey', () => { + event = runTest(delimiterAfter, true /* expected */, event); + + spyOnSelection(); + + moveBetweenDelimitersFeature.handleEvent(event, editor); + + expect(select).toHaveBeenCalledWith(new Position(testContainer, 0)); + expect(preventDefaultSpy).toHaveBeenCalledTimes(1); + expect(extendSpy).toHaveBeenCalledTimes(0); + + restoreSelection(); + }); + + it('DelimiterAfter, shouldHandle and Handle, with shiftKey', () => { + event = { + ...event, + rawEvent: { + ...event.rawEvent, + shiftKey: true, + }, + }; + + event = runTest(delimiterAfter, true /* expected */, event); + + spyOnSelection(); + + moveBetweenDelimitersFeature.handleEvent(event, editor); + + expect(extendSpy).toHaveBeenCalledWith(testContainer, 0); + expect(preventDefaultSpy).toHaveBeenCalledTimes(1); + + restoreSelection(); + }); + + it('DelimiterAfter, shouldHandle and Handle, no shiftKey, elements wrapped in B', () => { + wrapElementInB(delimiterBefore); + wrapElementInB(entity.wrapper); + wrapElementInB(delimiterAfter); + event = runTest(delimiterAfter, true /* expected */, event); + + spyOnSelection(); + + moveBetweenDelimitersFeature.handleEvent(event, editor); + + expect(select).toHaveBeenCalledWith( + new Position(delimiterBefore!.parentElement!, 0) + ); + expect(preventDefaultSpy).toHaveBeenCalledTimes(1); + expect(extendSpy).toHaveBeenCalledTimes(0); + + restoreSelection(); + }); + + it('DelimiterAfter, shouldHandle and Handle, with shiftKey, elements wrapped in B', () => { + event = { + ...event, + rawEvent: { + ...event.rawEvent, + shiftKey: true, + }, + }; + + wrapElementInB(delimiterBefore); + wrapElementInB(entity.wrapper); + wrapElementInB(delimiterAfter); + event = runTest(delimiterAfter, true /* expected */, event); + + spyOnSelection(); + + moveBetweenDelimitersFeature.handleEvent(event, editor); + + expect(extendSpy).toHaveBeenCalledWith(delimiterBefore?.parentElement, 0); + expect(preventDefaultSpy).toHaveBeenCalledTimes(1); + + restoreSelection(); + }); + + it('DelimiterAfter, should not Handle, no shiftKey', () => { + event = { + ...event, + rawEvent: { + ...event.rawEvent, + shiftKey: true, + which: event.rawEvent.which === Keys.RIGHT ? Keys.LEFT : Keys.RIGHT, + }, + }; + + event = runTest(delimiterAfter, false /* expected */, event); + }); + + it('DelimiterAfter, should not Handle, with shiftKey', () => { + event = { + ...event, + rawEvent: { + ...event.rawEvent, + shiftKey: true, + which: event.rawEvent.which === Keys.RIGHT ? Keys.LEFT : Keys.RIGHT, + }, + }; + + event = runTest(delimiterAfter, false /* expected */, event); + }); + + it('DelimiterAfter, shouldHandle and Handle, with shiftKey', () => { + event = { + ...event, + rawEvent: { + ...event.rawEvent, + shiftKey: true, + }, + }; + + event = runTest(delimiterAfter, true /* expected */, event); + + spyOnSelection(); + + moveBetweenDelimitersFeature.handleEvent(event, editor); + + expect(extendSpy).toHaveBeenCalledWith(testContainer, 0); + expect(preventDefaultSpy).toHaveBeenCalledTimes(1); + + restoreSelection(); + }); + + it('Element not an delimiter', () => { + delimiterAfter!.setAttribute('class', ''); + + runTest(delimiterAfter, false /* expected */, event); + }); + + it('Handle Event without cache', () => { + moveBetweenDelimitersFeature.handleEvent(event, editor); + + expect(preventDefaultSpy).toHaveBeenCalledTimes(0); + expect(extendSpy).toHaveBeenCalledTimes(0); + }); + + it('Null', () => { + runTest(null, false /* expected */, event); + }); + + it('Feature not enabled, do not handle', () => { + editor.isFeatureEnabled = () => false; + runTest(null, false /* expected */, event); + }); + + it('Selection not collapsed. do not handle', () => { + (editor.getSelectionRange = () => + { + collapsed: false, + }), + runTest(null, false /* expected */, event); + }); + + it('DelimiterAfter, shouldHandle and Handle, cursor at start of element after delimiter after', () => { + const bold = document.createElement('b'); + bold.append(document.createTextNode('Bold')); + testContainer.insertBefore(bold, null); + + event = runTest(new Position(bold.firstChild!, 0), true /* expected */, event); + + spyOnSelection(); + + moveBetweenDelimitersFeature.handleEvent(event, editor); + + expect(select).toHaveBeenCalledWith(new Position(testContainer, 0)); + expect(preventDefaultSpy).toHaveBeenCalledTimes(1); + expect(extendSpy).toHaveBeenCalledTimes(0); + + restoreSelection(); + }); + + it('DelimiterAfter, should not Handle, cursor is not not at the start of the element after delimiter after', () => { + const bold = document.createElement('b'); + bold.append(document.createTextNode('Bold')); + testContainer.insertBefore(bold, null); + + event = runTest(new Position(bold.firstChild!, 1), false /* expected */, event); + }); + + it('DelimiterAfter, should not Handle, cursor is at start of next block', () => { + const div = document.createElement('div'); + div.appendChild(document.createTextNode('New block')); + testContainer.insertAdjacentElement('afterend', div); + + runTest(new Position(div.firstChild!, 0), false /* expected */, event); + }); + + it('Delimiter After, Inline Readonly Entity with multiple Inline Elements', () => { + const b = document.createElement('b'); + b.appendChild(document.createTextNode('Bold')); + + entity.wrapper.appendChild(b); + entity.wrapper.appendChild(b.cloneNode(true)); + + wrapElementInB(delimiterBefore); + wrapElementInB(entity.wrapper); + wrapElementInB(delimiterAfter); + + runTest(delimiterAfter, true /* expected */, event); + }); + + it('DelimiterAfter, should not Handle, getBlockElementAtCursor returned inline', () => { + const div = document.createElement('div'); + div.appendChild(document.createTextNode('New block')); + testContainer.insertAdjacentElement('afterend', div); + + const pos = new Position(div.firstChild!, 0); + + setEditorFuncs(editor, pos, testContainer); + editor.getBlockElementAtNode = node => { + return { + getStartNode: () => node, + }; + }; + + const result = moveBetweenDelimitersFeature.shouldHandleEvent( + event, + editor, + false /* ctrlOrMeta */ + ); + + expect(result).toBe(false); + }); + } + + describe('LTR |', () => { + beforeEach(() => { + event = { + rawEvent: { + preventDefault() { + preventDefaultSpy(); + }, + which: Keys.LEFT, + }, + }; + }); + runTests(); + }); + + describe('RTL |', () => { + beforeEach(() => { + event = { + rawEvent: { + preventDefault() { + preventDefaultSpy(); + }, + which: Keys.RIGHT, + }, + }; + spyOn(getComputedStyles, 'getComputedStyle').and.returnValue('rtl'); + }); + runTests(); + }); + }); + + describe('Move After |', () => { + let event: PluginKeyDownEvent; + + function runTest( + element: Element | Position | null, + expected: boolean, + event: PluginKeyDownEvent + ) { + setEditorFuncs(editor, element, testContainer); + + const result = moveBetweenDelimitersFeature.shouldHandleEvent( + event, + editor, + false /* ctrlOrMeta */ + ); + + expect(result).toBe(expected); + return event; + } + + function runTests() { + it('DelimiterAfter', () => { + runTest(delimiterAfter, false /* expected */, event); + }); + + it('DelimiterBefore, should handle and handle, no shiftKey', () => { + event = runTest(delimiterBefore, true /* expected */, event); + + spyOnSelection(); + moveBetweenDelimitersFeature.handleEvent(event, editor); + + expect(select).toHaveBeenCalledWith(new Position(delimiterAfter!, 1)); + expect(preventDefaultSpy).toHaveBeenCalledTimes(1); + expect(extendSpy).toHaveBeenCalledTimes(0); + + restoreSelection(); + }); + + it('DelimiterBefore, should handle and handle, no shiftKey elements wrapped in B', () => { + wrapElementInB(delimiterBefore); + wrapElementInB(entity.wrapper); + wrapElementInB(delimiterAfter); + event = runTest(delimiterBefore, true /* expected */, event); + + spyOnSelection(); + moveBetweenDelimitersFeature.handleEvent(event, editor); + + expect(select).toHaveBeenCalledWith(new Position(delimiterAfter!, 1)); + expect(preventDefaultSpy).toHaveBeenCalledTimes(1); + expect(extendSpy).toHaveBeenCalledTimes(0); + + restoreSelection(); + }); + + it('DelimiterBefore, should handle and handle, with shiftKey', () => { + event = { + ...event, + rawEvent: { + ...event.rawEvent, + shiftKey: true, + }, + }; + + event = runTest(delimiterBefore, true /* expected */, event); + + spyOnSelection(); + + moveBetweenDelimitersFeature.handleEvent(event, editor); + + expect(extendSpy).toHaveBeenCalledWith(testContainer, 3); + expect(preventDefaultSpy).toHaveBeenCalledTimes(1); + + restoreSelection(); + }); + + it('DelimiterBefore, should handle and handle, with shiftKey, elements wrapped in B', () => { + event = { + ...event, + rawEvent: { + ...event.rawEvent, + shiftKey: true, + }, + }; + + wrapElementInB(delimiterBefore); + wrapElementInB(entity.wrapper); + wrapElementInB(delimiterAfter); + event = runTest(delimiterBefore, true /* expected */, event); + + spyOnSelection(); + + moveBetweenDelimitersFeature.handleEvent(event, editor); + + expect(extendSpy).toHaveBeenCalledWith(delimiterAfter?.parentElement, 1); + expect(preventDefaultSpy).toHaveBeenCalledTimes(1); + + restoreSelection(); + }); + + it('DelimiterBefore, should not handle, no shiftKey', () => { + event = { + ...event, + rawEvent: { + ...event.rawEvent, + which: event.rawEvent.which === Keys.RIGHT ? Keys.LEFT : Keys.RIGHT, + }, + }; + event = runTest(delimiterBefore, false /* expected */, event); + }); + + it('DelimiterBefore, should not handle, with shiftKey', () => { + event = { + ...event, + rawEvent: { + ...event.rawEvent, + shiftKey: true, + which: event.rawEvent.which === Keys.RIGHT ? Keys.LEFT : Keys.RIGHT, + }, + }; + event = runTest(delimiterBefore, false /* expected */, event); + }); + + it('Element not an delimiter', () => { + delimiterAfter!.setAttribute('class', ''); + runTest(delimiterAfter, false /* expected */, event); + }); + + it('Handle Event without cache', () => { + moveBetweenDelimitersFeature.handleEvent(defaultEvent, editor); + + expect(preventDefaultSpy).toHaveBeenCalledTimes(0); + expect(extendSpy).toHaveBeenCalledTimes(0); + }); + + it('Null', () => { + runTest(null, false /* expected */, event); + }); + + it('Feature not enabled, do not handle', () => { + editor.isFeatureEnabled = () => false; + runTest(null, false /* expected */, event); + }); + + it('Selection not collapsed. do not handle', () => { + (editor.getSelectionRange = () => + { + collapsed: false, + }), + runTest(null, false /* expected */, event); + }); + + it('DelimiterBefore, shouldHandle and Handle, cursor at end of element before delimiter before', () => { + const bold = document.createElement('b'); + bold.append(document.createTextNode('Bold')); + testContainer.insertBefore(bold, delimiterBefore); + + event = runTest(new Position(bold.firstChild!, 4), true /* expected */, event); + + spyOnSelection(); + + moveBetweenDelimitersFeature.handleEvent(event, editor); + + expect(select).toHaveBeenCalledWith(new Position(delimiterAfter!, 1)); + expect(preventDefaultSpy).toHaveBeenCalledTimes(1); + expect(extendSpy).toHaveBeenCalledTimes(0); + + restoreSelection(); + }); + + it('DelimiterBefore, should not Handle, cursor is not not at the start of the element after delimiter after', () => { + const bold = document.createElement('b'); + bold.append(document.createTextNode('Bold')); + testContainer.insertBefore(bold, null); + + event = runTest(new Position(bold.firstChild!, 1), false /* expected */, event); + }); + + it('DelimiterBefore, should not Handle, cursor is at end of previous block', () => { + const div = document.createElement('div'); + testContainer.insertAdjacentElement('beforeend', div); + + runTest(new Position(div, 0), false /* expected */, event); + }); + + it('DelimiterBefore, Inline Readonly Entity with multiple Inline Elements', () => { + const b = document.createElement('b'); + b.appendChild(document.createTextNode('Bold')); + + entity.wrapper.appendChild(b); + entity.wrapper.appendChild(b.cloneNode(true)); + + wrapElementInB(delimiterBefore); + wrapElementInB(entity.wrapper); + wrapElementInB(delimiterAfter); + + runTest(delimiterBefore, true /* expected */, event); + }); + + it('DelimiterBefore, should not Handle, getBlockElementAtCursor returned inline', () => { + const div = document.createElement('div'); + div.appendChild(document.createTextNode('New block')); + testContainer.insertAdjacentElement('afterend', div); + + const pos = new Position(div.firstChild!, 0); + + setEditorFuncs(editor, pos, testContainer); + editor.getBlockElementAtNode = node => { + return { + getStartNode: () => node, + }; + }; + + const result = moveBetweenDelimitersFeature.shouldHandleEvent( + event, + editor, + false /* ctrlOrMeta */ + ); + + expect(result).toBe(false); + }); + } + + describe('LTR |', () => { + beforeEach(() => { + preventDefaultSpy = jasmine.createSpy('preventDefault'); + event = { + rawEvent: { + preventDefault() { + preventDefaultSpy(); + }, + which: Keys.RIGHT, + }, + }; + }); + runTests(); + }); + + describe('RTL |', () => { + beforeEach(() => { + preventDefaultSpy = jasmine.createSpy('preventDefault'); + event = { + rawEvent: { + preventDefault() { + preventDefaultSpy(); + }, + which: Keys.LEFT, + }, + }; + spyOn(getComputedStyles, 'getComputedStyle').and.returnValue('rtl'); + }); + runTests(); + }); + }); + + describe('Remove Entity Between delimiters', () => { + function runTest(element: Element | null, expected: boolean, event: PluginKeyDownEvent) { + setEditorFuncs(editor, element, testContainer); + + const result = removeEntityBetweenDelimiters.shouldHandleEvent( + event, + editor, + false /* ctrlOrMeta */ + ); + + expect(result).toBe(expected); + return event; + } + + it('removeEntityBetweenDelimiters, should not Handle, getBlockElementAtCursor returned inline', () => { + const div = document.createElement('div'); + div.appendChild(document.createTextNode('New block')); + testContainer.insertAdjacentElement('afterend', div); + + const pos = new Position(div.firstChild!, 0); + + setEditorFuncs(editor, pos, testContainer); + editor.getBlockElementAtNode = node => { + return { + getStartNode: () => node, + }; + }; + + const result = removeEntityBetweenDelimiters.shouldHandleEvent( + { + rawEvent: { + which: Keys.BACKSPACE, + defaultPrevented: false, + }, + }, + editor, + false /* ctrlOrMeta */ + ); + + expect(result).toBe(false); + }); + + it('DelimiterAfter, Backspace, default not prevented', () => { + let event = { + rawEvent: { + which: Keys.BACKSPACE, + defaultPrevented: false, + }, + }; + + runTest(delimiterAfter, true /* expected */, event); + + removeEntityBetweenDelimiters.handleEvent(event, editor); + + expect(triggerContentChangedEvent).toHaveBeenCalledTimes(1); + expect(select).toHaveBeenCalledTimes(1); + }); + + it('DelimiterAfter, Backspace, default prevented and entity is still in editor', () => { + let event = { + rawEvent: { + which: Keys.BACKSPACE, + defaultPrevented: true, + }, + }; + editor.contains = () => true; + runTest(delimiterAfter, true /* expected */, event); + + removeEntityBetweenDelimiters.handleEvent(event, editor); + + expect(triggerContentChangedEvent).toHaveBeenCalledTimes(1); + expect(select).toHaveBeenCalledTimes(1); + expect(testContainer.contains(delimiterAfter)).toBe(true); + expect(testContainer.contains(delimiterBefore)).toBe(true); + expect(addDelimiters.default).toHaveBeenCalledWith(entity.wrapper); + }); + + it('DelimiterAfter, Backspace, default prevented and entity is removed', () => { + let event = { + rawEvent: { + which: Keys.BACKSPACE, + defaultPrevented: true, + }, + }; + editor.contains = () => false; + runTest(delimiterAfter, true /* expected */, event); + + removeEntityBetweenDelimiters.handleEvent(event, editor); + + expect(triggerContentChangedEvent).toHaveBeenCalledTimes(1); + expect(select).toHaveBeenCalledTimes(0); + expect(testContainer.contains(delimiterAfter)).toBe(false); + expect(testContainer.contains(delimiterBefore)).toBe(false); + expect(addDelimiters.default).not.toHaveBeenCalled(); + }); + + it('DelimiterAfter, DELETE', () => { + let event = { + rawEvent: { + which: Keys.DELETE, + }, + }; + + runTest(delimiterAfter, false /* expected */, event); + }); + + it('DelimiterBefore, BACKSPACE', () => { + let event = { + rawEvent: { + which: Keys.BACKSPACE, + }, + }; + + runTest(delimiterBefore, false /* expected */, event); + }); + + it('DelimiterBefore, Backspace, default not prevented', () => { + let event = { + rawEvent: { + which: Keys.DELETE, + defaultPrevented: false, + }, + }; + + runTest(delimiterBefore, true /* expected */, event); + + removeEntityBetweenDelimiters.handleEvent(event, editor); + + expect(triggerContentChangedEvent).toHaveBeenCalledTimes(1); + expect(select).toHaveBeenCalledTimes(1); + }); + + it('DelimiterBefore, Backspace, default prevented and entity is still in editor', () => { + let event = { + rawEvent: { + which: Keys.DELETE, + defaultPrevented: true, + }, + }; + editor.contains = () => true; + runTest(delimiterBefore, true /* expected */, event); + + removeEntityBetweenDelimiters.handleEvent(event, editor); + + expect(triggerContentChangedEvent).toHaveBeenCalledTimes(1); + expect(select).toHaveBeenCalledTimes(1); + expect(testContainer.contains(delimiterAfter)).toBe(true); + expect(testContainer.contains(delimiterBefore)).toBe(true); + expect(addDelimiters.default).toHaveBeenCalledWith(entity.wrapper); + }); + + it('DelimiterBefore, Backspace, default prevented and entity is removed', () => { + let event = { + rawEvent: { + which: Keys.DELETE, + defaultPrevented: true, + }, + }; + editor.contains = () => false; + runTest(delimiterBefore, true /* expected */, event); + + removeEntityBetweenDelimiters.handleEvent(event, editor); + + expect(triggerContentChangedEvent).toHaveBeenCalledTimes(1); + expect(select).toHaveBeenCalledTimes(0); + expect(testContainer.contains(delimiterAfter)).toBe(false); + expect(testContainer.contains(delimiterBefore)).toBe(false); + expect(addDelimiters.default).not.toHaveBeenCalled(); + }); + }); + + let selectionTemp: any; + function spyOnSelection() { + selectionTemp = document.getSelection; + document.getSelection = () => + { + extend(node: Node, offset: number) { + extendSpy(node, offset); + }, + }; + } + + function restoreSelection() { + document.getSelection = selectionTemp; + } +}); + +function wrapElementInB(delimiterBefore: Element | null) { + const element = delimiterBefore?.insertAdjacentElement( + 'beforebegin', + document.createElement('b') + ); + element?.appendChild(delimiterBefore!); +} + +function setEditorFuncs( + editor: IEditor, + element: Element | Position | null, + testContainer: HTMLDivElement +) { + editor.getFocusedPosition = () => getPos(element); + editor.getContentSearcherOfCursor = () => { + const pos = getPos(element); + return pos ? new PositionContentSearcher(testContainer, pos) : null!; + }; +} + +function cleanUp() { + document.body.childNodes.forEach(cn => { + document.body.removeChild(cn); + }); +} + +function addEntityBeforeEach(entity: Entity, wrapper: HTMLElement) { + entity = { + id: 'test', + isReadonly: true, + type: 'Test', + wrapper, + }; + + commitEntity(wrapper, 'test', true, 'test'); + addDelimiters.default(wrapper); + + return { + entity, + delimiterAfter: wrapper.nextElementSibling, + delimiterBefore: wrapper.previousElementSibling, + }; +} + +const getPos = (element: Element | Position | null) => { + return (element + ? (element as Position).element + ? (element as Position) + : new Position(element as Element, 0) + : null)!; +}; diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts index 427cc512b5c1..cab0d0fbf2f2 100644 --- a/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts @@ -1,19 +1,39 @@ +import * as blockFormat from 'roosterjs-editor-api/lib/utils/blockFormat'; +import * as setIndentation from 'roosterjs-editor-api/lib/format/setIndentation'; import * as TestHelper from '../../../../roosterjs-editor-api/test/TestHelper'; -import { IEditor } from 'roosterjs-editor-types'; +import * as toggleListType from 'roosterjs-editor-api/lib/utils/toggleListType'; +import getBlockElementAtNode from '../../../../roosterjs-editor-dom/lib/blockElements/getBlockElementAtNode'; import { ListFeatures } from '../../../lib/plugins/ContentEdit/features/listFeatures'; import { Position, PositionContentSearcher } from 'roosterjs-editor-dom'; +import { + IEditor, + Indentation, + PluginEventType, + PluginKeyboardEvent, + Keys, + BlockElement, + IContentTraverser, +} from 'roosterjs-editor-types'; -describe('listFeatures', () => { +describe('listFeatures | AutoBullet', () => { let editor: IEditor; const TEST_ID = 'listFeatureTests'; let editorSearchCursorSpy: any; + let editorIsFeatureEnabled: any; + let editorBlockTraverserSpy: any; beforeEach(() => { editor = TestHelper.initEditor(TEST_ID); spyOn(editor, 'getElementAtCursor').and.returnValue(null); editorSearchCursorSpy = spyOn(editor, 'getContentSearcherOfCursor'); + editorBlockTraverserSpy = spyOn(editor, 'getBlockTraverser'); + editorIsFeatureEnabled = spyOn(editor, 'isFeatureEnabled'); }); afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement?.removeChild(element); + } editor.dispose(); }); @@ -22,16 +42,67 @@ describe('listFeatures', () => { const mockedPosition = new PositionContentSearcher(root, new Position(root, 4)); spyOn(mockedPosition, 'getSubStringBefore').and.returnValue(text); editorSearchCursorSpy.and.returnValue(mockedPosition); - expect(ListFeatures.autoBullet.shouldHandleEvent(null, editor, false)).toBe(expectedResult); + editorIsFeatureEnabled.and.returnValue(false); + const isAutoBulletTriggered = ListFeatures.autoBullet.shouldHandleEvent(null, editor, false) + ? true + : false; + expect(isAutoBulletTriggered).toBe(expectedResult); + } + + function runTestWithNumberingStyles(text: string, expectedResult: boolean) { + const wrapper = document.createElement('div'); + const root = document.createElement('div'); + root.innerText = text; + wrapper.appendChild(root); + const mockedPosition = new PositionContentSearcher(root, new Position(root, 4)); + spyOn(mockedPosition, 'getSubStringBefore').and.returnValue(text); + const block = getBlockElementAtNode(wrapper, root) as BlockElement; + const traverser = { + currentBlockElement: block, + } as IContentTraverser; + spyOn(traverser.currentBlockElement, 'getTextContent').and.returnValue(text); + editorIsFeatureEnabled.and.returnValue(true); + editorSearchCursorSpy.and.returnValue(mockedPosition); + editorBlockTraverserSpy.and.returnValue(traverser); + + const isAutoBulletTriggered = ListFeatures.autoNumberingList.shouldHandleEvent( + null, + editor, + false + ) + ? true + : false; + expect(isAutoBulletTriggered).toBe(expectedResult); + } + + function runTestWithBulletStyles(text: string, expectedResult: boolean) { + const wrapper = document.createElement('div'); + const root = document.createElement('div'); + root.innerText = text; + wrapper.appendChild(root); + const mockedPosition = new PositionContentSearcher(root, new Position(root, 4)); + spyOn(mockedPosition, 'getSubStringBefore').and.returnValue(text); + const block = getBlockElementAtNode(wrapper, root) as BlockElement; + const traverser = { + currentBlockElement: block, + } as IContentTraverser; + spyOn(traverser.currentBlockElement, 'getTextContent').and.returnValue(text); + editorIsFeatureEnabled.and.returnValue(true); + editorSearchCursorSpy.and.returnValue(mockedPosition); + editorBlockTraverserSpy.and.returnValue(traverser); + const isAutoBulletTriggered = ListFeatures.autoBulletList.shouldHandleEvent( + null, + editor, + false + ) + ? true + : false; + expect(isAutoBulletTriggered).toBe(expectedResult); } it('AutoBullet detects the correct patterns', () => { runListPatternTest('1.', true); runListPatternTest('2.', true); - runListPatternTest('90.', true); - runListPatternTest('1>', true); - runListPatternTest('2>', true); - runListPatternTest('90>', true); runListPatternTest('1)', true); runListPatternTest('2)', true); runListPatternTest('90)', true); @@ -43,11 +114,703 @@ describe('listFeatures', () => { runListPatternTest('(90)', true); }); - it('AutoBullet ignores incorrect not valid patterns', () => { + it('AutoBulletList detects the correct patterns', () => { + runTestWithBulletStyles('*', true); + runTestWithBulletStyles('-', true); + runTestWithBulletStyles('--', true); + runTestWithBulletStyles('->', true); + runTestWithBulletStyles('-->', true); + runTestWithBulletStyles('>', true); + runTestWithBulletStyles('=>', true); + runTestWithBulletStyles('—', true); + }); + + it('AutoNumberingList with styles detects the correct patterns', () => { + runTestWithNumberingStyles('1.', true); + runTestWithNumberingStyles('1-', true); + runTestWithNumberingStyles('1)', true); + runTestWithNumberingStyles('(1)', true); + runTestWithNumberingStyles('i.', true); + runTestWithNumberingStyles('i-', true); + runTestWithNumberingStyles('i)', true); + runTestWithNumberingStyles('(i)', true); + runTestWithNumberingStyles('I.', true); + runTestWithNumberingStyles('I-', true); + runTestWithNumberingStyles('I)', true); + runTestWithNumberingStyles('(I)', true); + runTestWithNumberingStyles('A.', true); + runTestWithNumberingStyles('A-', true); + runTestWithNumberingStyles('A)', true); + runTestWithNumberingStyles('(A)', true); + runTestWithNumberingStyles('a.', true); + runTestWithNumberingStyles('a-', true); + runTestWithNumberingStyles('a)', true); + runTestWithNumberingStyles('(a)', true); + }); + + it('AutoBullet with ignores incorrect not valid patterns', () => { runListPatternTest('1=', false); runListPatternTest('1/', false); runListPatternTest('1#', false); runListPatternTest(' ', false); runListPatternTest('', false); }); + + it('AutoBulletList with ignores incorrect not valid patterns', () => { + runTestWithBulletStyles('1=', false); + runTestWithBulletStyles('1/', false); + runTestWithBulletStyles('1#', false); + runTestWithBulletStyles(' ', false); + runTestWithBulletStyles('', false); + runTestWithBulletStyles('long text a.', false); + runTestWithBulletStyles('long text 1)', false); + }); + + it('AutoNumberingList with ignores incorrect not valid patterns', () => { + runTestWithNumberingStyles('1=', false); + runTestWithNumberingStyles('1/', false); + runTestWithNumberingStyles('1#', false); + }); +}); + +describe('listFeatures | IndentWhenTab | OutdentWhenShiftTab', () => { + let editor: IEditor; + const TEST_ID = 'listFeatureTests'; + let setIndentationFn: jasmine.Spy; + const getKeyboardEvent = (keysPressed: (keyof KeyboardEventInit)[], keyCode: number) => + new KeyboardEvent('keydown', { + altKey: false, + ctrlKey: false, + keyCode, + ...keysPressed.reduce( + (obj, cv) => ({ + ...obj, + [cv]: true, + }), + {} + ), + }); + let list: HTMLOListElement; + + beforeEach(() => { + editor = TestHelper.initEditor(TEST_ID); + list = editor.getDocument().getElementById(TEST_ID) as HTMLOListElement; + editor.setContent(`
          1. 1
          2. 2
          3. 3
          `); + editor.focus(); + setIndentationFn = spyOn(setIndentation, 'default'); + }); + + afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement?.removeChild(element); + } + editor.dispose(); + }); + + function runTestShouldHandleEvent( + indent: boolean, + keysPressed: (keyof KeyboardEventInit)[], + keyCode: number, + shouldHandle: boolean, + runAltShiftTest: boolean = false + ) { + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardEvent(keysPressed, keyCode), + }; + + const feature = indent + ? !runAltShiftTest + ? ListFeatures.indentWhenTab + : ListFeatures.indentWhenAltShiftRight + : !runAltShiftTest + ? ListFeatures.outdentWhenShiftTab + : ListFeatures.outdentWhenAltShiftLeft; + + const triggered: boolean = !!feature.shouldHandleEvent(keyboardEvent, editor, false); + expect(triggered).toBe(shouldHandle); + } + + function runTestHandleEvent( + keysPressed: (keyof KeyboardEventInit)[], + keyCode: number, + indent: boolean, + runAltShiftTest: boolean = false + ) { + const range = document.createRange(); + range.setStart(list, 0); + range.setEnd(list, 1); + editor.select(range); + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardEvent(keysPressed, keyCode), + }; + + const feature = indent + ? !runAltShiftTest + ? ListFeatures.indentWhenTab + : ListFeatures.indentWhenAltShiftRight + : !runAltShiftTest + ? ListFeatures.outdentWhenShiftTab + : ListFeatures.outdentWhenAltShiftLeft; + + feature.handleEvent(keyboardEvent, editor); + + expect(setIndentationFn).toHaveBeenCalled(); + expect(setIndentationFn).toHaveBeenCalledWith( + editor, + indent ? Indentation.Increase : Indentation.Decrease + ); + } + + it('should not handle event | indent with tab', () => { + runTestShouldHandleEvent(true, ['shiftKey'], Keys.TAB, false); + }); + + it('should handle event | indent with tab', () => { + runTestShouldHandleEvent(true, [], Keys.TAB, true); + }); + + it('should not handle event | outdent with shift + tab', () => { + runTestShouldHandleEvent(false, [], Keys.TAB, false); + }); + + it('should handle event | outdent with shift + tab', () => { + runTestShouldHandleEvent(false, ['shiftKey'], Keys.TAB, true); + }); + + it('handle indent | indent with tab', () => { + runTestHandleEvent([], Keys.TAB, true); + }); + + it('should not handle event | indent with Alt + Shift + Arrow', () => { + runTestShouldHandleEvent(true, ['shiftKey'], Keys.TAB, false, true); + }); + + it('should handle event | indent with Alt + Shift + Arrow', () => { + runTestShouldHandleEvent(true, ['shiftKey', 'altKey'], Keys.RIGHT, true, true); + }); + + it('should not handle event | outdent with Alt + Shift + Arrow', () => { + runTestShouldHandleEvent(false, ['shiftKey', 'altKey'], Keys.RIGHT, false, true); + runTestShouldHandleEvent(false, ['shiftKey'], Keys.LEFT, false, true); + runTestShouldHandleEvent(false, ['altKey'], Keys.LEFT, false, true); + }); + + it('should handle event | outdent with Alt + Shift + Arrow', () => { + runTestShouldHandleEvent(false, ['shiftKey', 'altKey'], Keys.LEFT, true, true); + }); + + it('handle indent | indent with Alt + Shift + Arrow', () => { + runTestHandleEvent(['shiftKey', 'altKey'], Keys.RIGHT, true, true); + }); + + it('handle indent | outdent with Alt + Shift + Arrow', () => { + runTestHandleEvent(['shiftKey', 'altKey'], Keys.LEFT, true, true); + }); +}); + +describe('listFeatures | MergeInNewLine', () => { + let editor: IEditor; + const TEST_ID = 'listFeatureTests'; + const ITEM_1 = 'ITEM_1'; + const ITEM_2 = 'ITEM_2'; + let blockFormatFn: jasmine.Spy; + let toggleListTypeFn: jasmine.Spy; + const getKeyboardEvent = (shiftKey: boolean) => + new KeyboardEvent('keydown', { + shiftKey, + altKey: false, + ctrlKey: false, + }); + + beforeEach(() => { + editor = TestHelper.initEditor(TEST_ID); + editor.setContent( + `
          1. 1
          2. 2
          3. 3
          ` + ); + editor.focus(); + blockFormatFn = spyOn(blockFormat, 'default'); + toggleListTypeFn = spyOn(toggleListType, 'default'); + }); + + afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement?.removeChild(element); + } + editor.dispose(); + }); + + function runTestShouldHandleEvent(isAtBeginning: boolean, shouldHandle: boolean) { + const item = editor.getDocument().getElementById(ITEM_2) as HTMLLIElement; + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardEvent(false), + }; + const range = document.createRange(); + range.setStart(item, 0); + if (isAtBeginning) { + range.collapse(); + } else { + range.setEnd(item, 1); + } + editor.select(range); + + const triggered = ListFeatures.mergeInNewLineWhenBackspaceOnFirstChar.shouldHandleEvent( + keyboardEvent, + editor, + false + ) + ? true + : false; + expect(triggered).toBe(shouldHandle); + } + + function runTestHandleEvent(isFirstElement: boolean) { + const item = editor + .getDocument() + .getElementById(isFirstElement ? ITEM_1 : ITEM_2) as HTMLLIElement; + const range = document.createRange(); + range.setStart(item, 0); + range.setEnd(item, 0); + editor.select(range); + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardEvent(false), + }; + ListFeatures.mergeInNewLineWhenBackspaceOnFirstChar.handleEvent(keyboardEvent, editor); + if (isFirstElement) { + expect(toggleListTypeFn).toHaveBeenCalled(); + expect(toggleListTypeFn).toHaveBeenCalledWith(editor, 1, undefined, true); + } else { + expect(blockFormatFn).toHaveBeenCalled(); + } + } + + it('should handle event', () => { + runTestShouldHandleEvent(true, true); + }); + + it('should not handle event', () => { + runTestShouldHandleEvent(false, false); + }); + + it('should handle block format', () => { + runTestHandleEvent(false); + }); + + it('should handle toggle List', () => { + runTestHandleEvent(true); + }); +}); + +describe('listFeatures | OutdentWhenBackOn1stEmptyLine', () => { + let editor: IEditor; + const TEST_ID = 'listFeatureTests'; + const ITEM_1 = 'ITEM_1'; + let toggleListTypeFn: jasmine.Spy; + const getKeyboardEvent = (shiftKey: boolean) => + new KeyboardEvent('keydown', { + shiftKey, + altKey: false, + ctrlKey: false, + }); + + beforeEach(() => { + editor = TestHelper.initEditor(TEST_ID); + toggleListTypeFn = spyOn(toggleListType, 'default'); + }); + + afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement?.removeChild(element); + } + editor.dispose(); + }); + + function runTestShouldHandleEvent(content: string, shouldHandle: boolean) { + editor.setContent(content); + editor.focus(); + const item = editor.getDocument().getElementById(ITEM_1) as HTMLLIElement; + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardEvent(false), + }; + const range = document.createRange(); + range.setStart(item, 0); + range.collapse(); + editor.select(range); + const triggered = ListFeatures.outdentWhenBackspaceOnEmptyFirstLine.shouldHandleEvent( + keyboardEvent, + editor, + false + ) + ? true + : false; + expect(triggered).toBe(shouldHandle); + } + + function runTestHandleEvent(content: string) { + editor.setContent(content); + editor.focus(); + const item = editor.getDocument().getElementById(ITEM_1) as HTMLLIElement; + const range = document.createRange(); + range.setStart(item, 0); + range.collapse(); + editor.select(range); + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardEvent(false), + }; + ListFeatures.outdentWhenBackspaceOnEmptyFirstLine.handleEvent(keyboardEvent, editor); + expect(toggleListTypeFn).toHaveBeenCalled(); + expect(toggleListTypeFn).toHaveBeenCalledWith(editor, 1, undefined, true); + } + + it('should not handle event', () => { + runTestShouldHandleEvent( + `
          1. 1
          2. 2
          3. 3
          `, + false + ); + }); + + it('should handle event', () => { + runTestShouldHandleEvent(`
          `, true); + }); + + it('should handle toggle List', () => { + runTestHandleEvent(`
          `); + }); +}); + +describe('listFeatures | MaintainListChainWhenDelete', () => { + let editor: IEditor; + const TEST_ID = 'listFeatureTests'; + const ITEM_1 = 'ITEM_1'; + const ITEM_2 = 'ITEM_2'; + const getKeyboardEvent = (shiftKey: boolean) => + new KeyboardEvent('keydown', { + shiftKey, + altKey: false, + ctrlKey: false, + }); + + beforeEach(() => { + editor = TestHelper.initEditor(TEST_ID); + }); + + afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement?.removeChild(element); + } + editor.dispose(); + }); + + function runTestHandleEvent(content: string) { + editor.setContent(content); + editor.focus(); + const item = editor.getDocument().getElementById(ITEM_1); + const range = document.createRange(); + range.setStart(item, 0); + range.collapse(); + editor.select(range); + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardEvent(false), + }; + + const runAsync = spyOn(editor, 'runAsync'); + ListFeatures.maintainListChainWhenDelete.handleEvent(keyboardEvent, editor); + expect(runAsync).toHaveBeenCalled(); + } + + it('should handle toggle List', () => { + runTestHandleEvent( + `
          1. 1
          2. 1
          ` + ); + }); +}); + +describe('listFeatures | OutdentWhenEnterOnEmptyLine', () => { + let editor: IEditor; + const TEST_ID = 'listFeatureTests'; + const ITEM_1 = 'ITEM_1'; + let toggleListTypeFn: jasmine.Spy; + const getKeyboardEvent = (shiftKey: boolean) => + new KeyboardEvent('keydown', { + shiftKey, + altKey: false, + ctrlKey: false, + }); + + beforeEach(() => { + editor = TestHelper.initEditor(TEST_ID); + toggleListTypeFn = spyOn(toggleListType, 'default'); + }); + + afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement?.removeChild(element); + } + editor.dispose(); + }); + + function runTestShouldHandleEvent(content: string, shiftKey: boolean, shouldHandle: boolean) { + editor.setContent(content); + editor.focus(); + const item = editor.getDocument().getElementById(ITEM_1) as HTMLLIElement; + if (item) { + const range = document.createRange(); + range.setStart(item, 0); + range.collapse(); + editor.select(range); + } + + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardEvent(shiftKey), + }; + + const triggered = ListFeatures.outdentWhenEnterOnEmptyLine.shouldHandleEvent( + keyboardEvent, + editor, + false + ) + ? true + : false; + expect(triggered).toBe(shouldHandle); + } + + function runTestHandleEvent(content: string) { + editor.setContent(content); + editor.focus(); + const item = editor.getDocument().getElementById(ITEM_1) as HTMLLIElement; + const range = document.createRange(); + range.setStart(item, 0); + range.collapse(); + editor.select(range); + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardEvent(false), + }; + ListFeatures.outdentWhenBackspaceOnEmptyFirstLine.handleEvent(keyboardEvent, editor); + expect(toggleListTypeFn).toHaveBeenCalled(); + expect(toggleListTypeFn).toHaveBeenCalledWith(editor, 1, undefined, true); + } + + it('should handle event', () => { + runTestShouldHandleEvent(`
          `, false, true); + }); + + it('should not handle event | node not empty', () => { + runTestShouldHandleEvent( + `
          1. 1
          2. 2
          3. 3
          `, + true, + false + ); + }); + + it('should not handle event | shift key', () => { + runTestShouldHandleEvent( + `
          1. 1
          2. 2
          3. 3
          `, + true, + false + ); + }); + + it('should handle toggle List', () => { + runTestHandleEvent(`
          `); + }); +}); + +describe('listFeatures | MaintainListChain', () => { + let editor: IEditor; + const TEST_ID = 'listFeatureTests'; + const ITEM_1 = 'ITEM_1'; + const getKeyboardEvent = (shiftKey: boolean) => + new KeyboardEvent('keydown', { + shiftKey, + altKey: false, + ctrlKey: false, + }); + + beforeEach(() => { + editor = TestHelper.initEditor(TEST_ID); + }); + + afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement?.removeChild(element); + } + editor.dispose(); + }); + + function runTestShouldHandleEvent(content: string, shouldHandle: boolean) { + editor.setContent(content); + editor.focus(); + const item = editor.getDocument().getElementById(TEST_ID); + if (item) { + const range = document.createRange(); + range.setStart(item, 0); + range.setEnd(item, 1); + editor.select(range); + } + + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardEvent(false), + }; + + const triggered = ListFeatures.maintainListChain.shouldHandleEvent( + keyboardEvent, + editor, + false + ) + ? true + : false; + expect(triggered).toBe(shouldHandle); + } + + function runTestHandleEvent(content: string) { + editor.setContent(content); + editor.focus(); + const item = editor.getDocument().getElementById(ITEM_1); + if (item) { + const range = document.createRange(); + range.setStart(item, 1); + range.collapse(); + editor.select(range); + } + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardEvent(false), + }; + + const runAsync = spyOn(editor, 'runAsync'); + ListFeatures.maintainListChain.handleEvent(keyboardEvent, editor); + expect(runAsync).toHaveBeenCalled(); + } + + it('should handle event', () => { + runTestShouldHandleEvent( + `
          1. 1
          2. 2
          3. 3
          `, + true + ); + }); + + it('should not handle event', () => { + runTestShouldHandleEvent(`1`, false); + }); + + it('should handle editor async', () => { + runTestHandleEvent( + `
          1. 1
          2. 2
          3. 3
          ` + ); + }); +}); + +describe('listFeatures | mergeListOnBackspaceAfterList', () => { + let editor: IEditor; + const TEST_ID = 'listFeatureTests'; + const ITEM_1 = 'ITEM_1'; + const getKeyboardEvent = (shiftKey: boolean) => + new KeyboardEvent('keydown', { + shiftKey, + altKey: false, + ctrlKey: false, + }); + + beforeEach(() => { + editor = TestHelper.initEditor(TEST_ID); + + editor.runAsync = callback => { + callback(editor); + return () => {}; + }; + }); + + afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement?.removeChild(element); + } + editor.dispose(); + }); + + function runTestShouldHandleEvent(content: string, shouldHandle: boolean) { + editor.setContent(content); + editor.focus(); + const item = editor.getDocument().getElementById(ITEM_1); + if (item) { + const range = document.createRange(); + range.setStart(item, 0); + editor.select(range); + } + + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardEvent(false), + }; + + const triggered = ListFeatures.mergeListOnBackspaceAfterList.shouldHandleEvent( + keyboardEvent, + editor, + false + ) + ? true + : false; + expect(triggered).toBe(shouldHandle); + } + + function runTestHandleEvent(content: string) { + editor.setContent(content); + editor.focus(); + const item = editor.getDocument().getElementById(ITEM_1); + if (item) { + const range = document.createRange(); + range.setStart(item, 0); + range.collapse(); + editor.select(range); + } + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardEvent(false), + }; + + ListFeatures.mergeListOnBackspaceAfterList.shouldHandleEvent(keyboardEvent, editor, false); + item?.parentElement?.removeChild(item); + ListFeatures.mergeListOnBackspaceAfterList.handleEvent(keyboardEvent, editor); + + expect(editor.queryElements('ol,ul').length).toEqual(1); + } + + it('should handle event', () => { + runTestShouldHandleEvent( + `
          1. 123

          1. 213

          `, + true + ); + }); + + it('Should not handle event, lists have different TagName', () => { + runTestShouldHandleEvent( + `
          • 123

          1. 213

          `, + false + ); + }); + + it('should not handle event', () => { + runTestShouldHandleEvent(`1`, false); + }); + + it('should handle editor async', () => { + runTestHandleEvent( + `
          1. 123

          1. 213

          ` + ); + }); }); diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts new file mode 100644 index 000000000000..a5ec5127722a --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts @@ -0,0 +1,138 @@ +import * as TestHelper from '../../../../roosterjs-editor-api/test/TestHelper'; +import { MarkdownFeatures } from '../../../lib/plugins/ContentEdit/features/markdownFeatures'; +import { + BuildInEditFeature, + IEditor, + PluginEventType, + PluginKeyboardEvent, + PositionType, +} from 'roosterjs-editor-types'; + +describe('MarkdownFeatures | ', () => { + let editor: IEditor; + const TEST_ID = 'MarkDownFeatureTest'; + const TEST_ELEMENT_ID = 'MarkDownFeatureTestElementId'; + // Here we only test bolding as the logic for other styling is exactly the same + const markdownBold = MarkdownFeatures.markdownBold; + + beforeEach(done => { + editor = TestHelper.initEditor(TEST_ID); + editor.runAsync = (callback: (editor: IEditor) => void) => { + callback(editor); + return () => {}; + }; + done(); + }); + + afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement?.removeChild(element); + } + editor.dispose(); + }); + + const keyboardEvent = (whichInput?: number) => { + return new KeyboardEvent('keydown', { + shiftKey: true, + altKey: false, + ctrlKey: false, + cancelable: true, + which: whichInput, + }); + }; + + function runShouldHandleEvent( + content: string, + shouldHandleExpect: boolean, + markdownFeature: BuildInEditFeature + ) { + editor.setContent(`
          ${content}
          `); + const element = document.getElementById(TEST_ELEMENT_ID); + editor.select(element, PositionType.End); + const keyboardPluginEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: keyboardEvent(), + }; + const shouldHandleEvent = markdownFeature.shouldHandleEvent( + keyboardPluginEvent, + editor, + false /* ctrlOrMeta */ + ); + expect(!!shouldHandleEvent).toBe(shouldHandleExpect); + } + + function runHandleEvent( + markdownFeature: BuildInEditFeature, + testContent: string, + expectedContent: string + ) { + editor.setContent(`
          ${testContent} other text
          `); + const element = document.getElementById(TEST_ELEMENT_ID); + const range = document.createRange(); + range.setStart(element?.firstChild!, testContent.length); + range.setEnd(element?.firstChild!, testContent.length); + editor.select(range); + const keyboardPluginEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: new KeyboardEvent('keydown', { + shiftKey: true, + altKey: false, + ctrlKey: false, + cancelable: false, + }), + }; + markdownFeature.handleEvent(keyboardPluginEvent, editor); + const styledContent: string = element!.innerHTML; + + expect(styledContent).toContain(expectedContent); + } + + describe('Should Handle Event | ', () => { + it('Should handle in normal scenario 1', () => { + runShouldHandleEvent('*abcd', true /* shouldHandleExpect */, markdownBold); + }); + + it('Should handle in normal scenario 2', () => { + runShouldHandleEvent('*abcd~', true /* shouldHandleExpect */, markdownBold); + }); + + it('Should handle in normal scenario 3', () => { + runShouldHandleEvent('*abcd defi 1234', true /* shouldHandleExpect */, markdownBold); + }); + + it('Should handle in normal scenario 4', () => { + runShouldHandleEvent('abcd *1234', true /* shouldHandleExpect */, markdownBold); + }); + + it('Should not handle because of preceding whitespace', () => { + runShouldHandleEvent('*abcd ', false /* shouldHandleExpect */, markdownBold); + }); + + it('Should not handle because of preceding trigger character', () => { + runShouldHandleEvent('*abcd*', false /* shouldHandleExpect */, markdownBold); + }); + + it('Should not handle because of multiple whitespace', () => { + runShouldHandleEvent('*abcd ', false /* shouldHandleExpect */, markdownBold); + }); + + it('Should not handle because of wrong starting trigger character', () => { + runShouldHandleEvent('-abcd', false /* shouldHandleExpect */, markdownBold); + }); + }); + + describe('Handle Event | ', () => { + it('markdownBold normal scenario 1', () => { + runHandleEvent(markdownBold, '*abcd', 'abcd​other text'); + }); + + it('markdownBold normal scenario 2', () => { + runHandleEvent(markdownBold, '*abcd 123', 'abcd 123​other text'); + }); + + it('markdownBold normal scenario 3', () => { + runHandleEvent(markdownBold, 'abcd *123', 'abcd 123​other text'); + }); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/shortcutFeatureTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/shortcutFeatureTest.ts index a6f2d7762c7c..d05c5da51a1b 100644 --- a/packages/roosterjs-editor-plugins/test/ContentEdit/features/shortcutFeatureTest.ts +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/shortcutFeatureTest.ts @@ -68,6 +68,12 @@ const parameters = [ key: 85, command: DocumentCommand.Underline, }, + { + description: + 'default shortcut calls the clearFormat command on the editor when typing CTLR+Space', + key: 32, + command: DocumentCommand.RemoveFormat, + }, ]; parameters.forEach(({ description, key, command }) => { @@ -85,26 +91,31 @@ parameters.forEach(({ description, key, command }) => { const shortCutFeature = ShortcutFeatures.defaultShortcut; const spya = spyOn(editor.getDocument(), 'execCommand'); shortCutFeature.handleEvent(event, editor); - expect(spya).toHaveBeenCalledWith(command, false, null); + expect(spya).toHaveBeenCalledWith(command, false, undefined); }); }); -it('default shortcut calls the undo command on the editor when typing CTRL+Z', () => { - const rawEvent = new KeyboardEvent('keydown', { - ctrlKey: true, - }); - Object.defineProperty(rawEvent, 'which', { - get: () => 90, - }); - const event = { - rawEvent, - eventType: 0, - }; +[ + { key: 90, mod: 'ctrl', keyCombo: 'Ctrl+Z' }, + { key: 8, mod: 'alt', keyCombo: 'Alt+Backspace' }, +].forEach(({ key, mod, keyCombo }) => { + it(`default shortcut calls the undo command on the editor when typing ${keyCombo}`, () => { + const rawEvent = new KeyboardEvent('keydown', { + [`${mod}Key`]: true, + }); + Object.defineProperty(rawEvent, 'which', { + get: () => key, + }); + const event = { + rawEvent, + eventType: 0, + }; - const shortCutFeature = ShortcutFeatures.defaultShortcut; - const spyUndo = spyOn(editor, 'undo'); - shortCutFeature.handleEvent(event, editor); - expect(spyUndo).toHaveBeenCalled(); + const shortCutFeature = ShortcutFeatures.defaultShortcut; + const spyUndo = spyOn(editor, 'undo'); + shortCutFeature.handleEvent(event, editor); + expect(spyUndo).toHaveBeenCalled(); + }); }); it('default shortcut calls the redo command on the editor when typing CTRL+Y', () => { diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/tableFeaturesTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/tableFeaturesTest.ts new file mode 100644 index 000000000000..e4a17460641c --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/tableFeaturesTest.ts @@ -0,0 +1,386 @@ +import * as setIndentation from 'roosterjs-editor-api/lib/format/setIndentation'; +import * as TestHelper from '../../../../roosterjs-editor-api/test/TestHelper'; +import { Browser, cacheGetEventData } from 'roosterjs-editor-dom'; +import { TableFeatures } from '../../../lib/plugins/ContentEdit/features/tableFeatures'; +import { + IEditor, + PluginEventType, + PluginKeyboardEvent, + Indentation, + TableSelection, +} from 'roosterjs-editor-types'; + +describe('TableFeature', () => { + let editor: IEditor; + let table: HTMLTableElement | null; + const TEST_ID = 'TableFeatureTests'; + const TEST_ELEMENT_ID = 'test'; + const getKeyboardEvent = (shiftKey: boolean) => + new KeyboardEvent('keydown', { + shiftKey, + altKey: false, + ctrlKey: false, + }); + let keyboardEvent: PluginKeyboardEvent; + let shiftKeyboardEvent: PluginKeyboardEvent; + + beforeEach(() => { + editor = TestHelper.initEditor(TEST_ID); + editor.setContent( + `
          Text1Text2
          Text3Text4
          ` + ); + editor.focus(); + table = editor.getDocument().getElementById(TEST_ELEMENT_ID) as HTMLTableElement; + const td = table.querySelector('td,th'); + editor.select(td!, 0); + keyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardEvent(false), + }; + shiftKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardEvent(true), + }; + }); + + afterEach(() => { + TestHelper.removeElement(TEST_ID); + let element = document.getElementById(TEST_ELEMENT_ID); + if (element) { + element.parentElement?.removeChild(element); + } + editor.dispose(); + }); + + describe('tabInTable |', () => { + const feature = TableFeatures.tabInTable; + + describe('ShouldHandle |', () => { + it('Should not handle, is not in a table', () => { + editor.setContent(``); + editor.focus(); + editor.select(document.getElementById('TEST_ELEMENT_ID')!, 0); + const shouldHandleEvent = feature.shouldHandleEvent(keyboardEvent, editor, false); + expect(!!shouldHandleEvent).toBeFalsy(); + }); + it('Should not handle, table is fully selected', () => { + editor.select(table!, { + firstCell: { x: 0, y: 0 }, + lastCell: { y: 1, x: 1 }, + }); + const shouldHandleEvent = feature.shouldHandleEvent(keyboardEvent, editor, false); + expect(!!shouldHandleEvent).toBeFalsy(); + }); + it('Should handle, table is not fully selected', () => { + editor.select(table!, { + firstCell: { x: 0, y: 0 }, + lastCell: { y: 0, x: 1 }, + }); + const shouldHandleEvent = feature.shouldHandleEvent(keyboardEvent, editor, false); + expect(!!shouldHandleEvent).toBeTruthy(); + }); + }); + describe('HandleEvent |', () => { + function runTest(shift: boolean, cellSelected: number, expectedTargetCell: number) { + const event = shift ? shiftKeyboardEvent : keyboardEvent; + let cells = table?.querySelectorAll('td,th')!; + const target = cells[cellSelected]!; + let expectedCell = cells[expectedTargetCell] || undefined; + + editor.select(target, 0); + + feature.handleEvent(event, editor); + + const focusedPos = editor.getFocusedPosition(); + if (!expectedCell) { + cells = table?.querySelectorAll('td,th')!; + expectedCell = cells[expectedTargetCell]; + } + expect(focusedPos.element).toBe(expectedCell! as HTMLElement); + } + it('1st cell to 2nd cell', () => { + runTest(false, 0, 1); + }); + it('2nd cell to 3rd cell', () => { + runTest(false, 1, 2); + }); + it('3rd cell to 2nd cell', () => { + runTest(true, 2, 1); + }); + it('2nd cell to 1st cell', () => { + runTest(true, 1, 0); + }); + it('Shift + Tab in 1st cell', () => { + const event = shiftKeyboardEvent; + let cells = table?.querySelectorAll('td,th')!; + const target = cells[0]!; + + editor.select(target, 0); + feature.handleEvent(event, editor); + + const focusedPos = editor.getFocusedPosition(); + expect(focusedPos.node).toBe(table?.parentNode); + }); + it('Tab in last cell to create new row', () => { + runTest(false, 3, 4); + }); + }); + }); + + describe('indentTableOnTab |', () => { + const feature = TableFeatures.indentTableOnTab; + + describe('ShouldHandle', () => { + it('Should not handle, is not in a table', () => { + editor.setContent(``); + editor.focus(); + editor.select(document.getElementById('TEST_ELEMENT_ID')!, 0); + const shouldHandleEvent = feature.shouldHandleEvent(keyboardEvent, editor, false); + expect(!!shouldHandleEvent).toBeFalsy(); + }); + it('Should handle, table is fully selected', () => { + editor.select(table!, { + firstCell: { x: 0, y: 0 }, + lastCell: { y: 1, x: 1 }, + }); + const shouldHandleEvent = feature.shouldHandleEvent(keyboardEvent, editor, false); + expect(!!shouldHandleEvent).toBeTruthy(); + }); + it('Should not handle, table is not fully selected', () => { + editor.select(table!, { + firstCell: { x: 0, y: 0 }, + lastCell: { y: 0, x: 1 }, + }); + const shouldHandleEvent = feature.shouldHandleEvent(keyboardEvent, editor, false); + expect(!!shouldHandleEvent).toBeFalsy(); + }); + }); + + describe('HandleEvent', () => { + let setIndentationFn: jasmine.Spy; + + beforeEach(() => { + setIndentationFn = spyOn(setIndentation, 'default'); + }); + + function runTest(shift: boolean, setIndentationExpect?: () => void) { + const event = shift ? shiftKeyboardEvent : keyboardEvent; + + editor.select(table!, { + firstCell: { x: 0, y: 0 }, + lastCell: { y: 1, x: 1 }, + }); + + feature.handleEvent(event, editor); + + if (!setIndentationExpect) { + expect(setIndentationFn).toHaveBeenCalled(); + expect(setIndentationFn).toHaveBeenCalledWith( + editor, + shift ? Indentation.Decrease : Indentation.Increase + ); + } else { + setIndentationExpect(); + } + } + it('Indent Whole Table selected', () => { + runTest(false); + }); + + it('Outdent Whole Table selected', () => { + editor.setContent( + `
          TextText
          TextText
          ` + ); + editor.focus(); + table = editor.getDocument().getElementById(TEST_ELEMENT_ID) as HTMLTableElement; + const td = table.querySelector('td,th'); + editor.select(td!, 0); + runTest(true); + }); + + it('Outdent Whole Table selected, but is not going to be executed because table is not wrapped in a blockquote', () => { + runTest(true, () => { + expect(setIndentationFn).not.toHaveBeenCalled(); + }); + }); + }); + }); + + describe('UpDownInTable |', () => { + const feature = TableFeatures.upDownInTable; + + describe('ShouldHandle |', () => { + it('Should not handle, is not in a table', () => { + editor.setContent(``); + editor.focus(); + editor.select(document.getElementById('TEST_ELEMENT_ID')!, 0); + const shouldHandleEvent = feature.shouldHandleEvent(keyboardEvent, editor, false); + expect(!!shouldHandleEvent).toBeFalsy(); + }); + it('Should not handle, table is fully selected', () => { + editor.select(table!, { + firstCell: { x: 0, y: 0 }, + lastCell: { y: 1, x: 1 }, + }); + const shouldHandleEvent = feature.shouldHandleEvent(keyboardEvent, editor, false); + expect(!!shouldHandleEvent).toBeFalsy(); + }); + it('Should handle, table is not fully selected', () => { + editor.select(table!, { + firstCell: { x: 0, y: 0 }, + lastCell: { y: 0, x: 1 }, + }); + const shouldHandleEvent = feature.shouldHandleEvent(keyboardEvent, editor, false); + expect(!!shouldHandleEvent).toBeTruthy(); + }); + }); + + describe('HandleEvent', () => { + beforeEach(() => { + editor.runAsync = callback => { + callback(editor); + return () => {}; + }; + }); + + function getKeyboardUpDownEvent(isUp: boolean, shiftKey: boolean = false) { + const which = isUp ? 38 /* UP */ : 40; /* Down */ + const evt = new KeyboardEvent('keydown', { + shiftKey, + altKey: false, + ctrlKey: false, + cancelable: true, + which, + }); + + if (!Browser.isFirefox) { + //Chromium hack to add which to the event as there is a bug in Webkit + //https://stackoverflow.com/questions/10455626/keydown-simulation-in-chrome-fires-normally-but-not-the-correct-key/10520017#10520017 + Object.defineProperty(evt, 'which', { + get: function () { + return which; + }, + }); + } + return evt; + } + + function runTest(isUp: boolean, cellSelected: number, expectedTargetCell: number) { + const pluginEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardUpDownEvent(isUp), + }; + + const cells = table?.querySelectorAll('td,th')!; + const target = cells[cellSelected]!; + const expectedCell = cells[expectedTargetCell] || undefined; + + spyOn(editor, 'getElementAtCursor').and.returnValue(expectedCell as HTMLElement); + const range = new Range(); + range.setStart(target, 0); + + window.getSelection()?.removeAllRanges(); + window.getSelection()?.addRange(range); + + feature.handleEvent(pluginEvent, editor); + + expect(editor.getElementAtCursor()).toBe(expectedCell as HTMLElement); + } + + function runTestWithShift( + isUp: boolean, + cellSelected: number, + expectedTargetCell: number + ) { + const pluginEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardUpDownEvent(isUp, true), + }; + + const cells = table?.querySelectorAll('td,th')!; + const target = cells[cellSelected]!; + const expectedCell = cells[expectedTargetCell] || undefined; + + spyOn(editor, 'getElementAtCursor').and.returnValue(expectedCell as HTMLElement); + cacheGetEventData(pluginEvent, 'TABLE_CELL_FOR_TABLE_FEATURES', () => { + return target; + }); + + editor.select(target, 0); + + feature.handleEvent(pluginEvent, editor); + + const selection = window.getSelection(); + expect(selection!.getRangeAt(0).collapsed).toBe(false); + } + it('Use DOWN on first cell', () => { + runTest(false, 0, 2); + }); + it('Use DOWN on second cell', () => { + runTest(false, 1, 3); + }); + it('Use UP on third cell', () => { + runTest(true, 2, 0); + }); + it('Use UP on fourth cell', () => { + runTest(true, 3, 1); + }); + it('Use UP on fourth cell', () => { + runTest(true, 3, 1); + }); + it('Shift Use UP on fourth cell', () => { + runTestWithShift(true, 3, 1); + }); + it('Shift Use DOWN on fourth cell', () => { + runTestWithShift(false, 3, 1); + }); + it('Shift Use DOWN on fourth cell', () => { + runTestWithShift(false, 2, 0); + }); + }); + }); + + describe('deleteTable | ', () => { + const feature = TableFeatures.deleteTableWithBackspace; + + describe('ShouldHandle', () => { + it('Should not handle, is not in a table', () => { + editor.setContent(``); + editor.focus(); + editor.select(document.getElementById('TEST_ELEMENT_ID')!, 0); + const shouldHandleEvent = feature.shouldHandleEvent(keyboardEvent, editor, false); + expect(!!shouldHandleEvent).toBeFalsy(); + }); + it('Should handle, table is fully selected', () => { + editor.select(table!, { + firstCell: { x: 0, y: 0 }, + lastCell: { y: 1, x: 1 }, + }); + spyOn(editor, 'isFeatureEnabled').and.returnValue(true); + const shouldHandleEvent = feature.shouldHandleEvent(keyboardEvent, editor, false); + expect(!!shouldHandleEvent).toBeTruthy(); + }); + it('Should not handle, table is not fully selected', () => { + editor.select(table!, { + firstCell: { x: 0, y: 0 }, + lastCell: { y: 0, x: 1 }, + }); + const shouldHandleEvent = feature.shouldHandleEvent(keyboardEvent, editor, false); + expect(!!shouldHandleEvent).toBeFalsy(); + }); + }); + + describe('HandleEvent', () => { + it('Should delete table', () => { + editor.select(table!, { + firstCell: { x: 0, y: 0 }, + lastCell: { y: 1, x: 1 }, + }); + + feature.handleEvent(keyboardEvent, editor); + const deletedTable = document.getElementById('TEST_ELEMENT_ID'); + expect(deletedTable).toBe(null); + }); + }); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/textFeaturesTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/textFeaturesTest.ts new file mode 100644 index 000000000000..05c75849f0f5 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/textFeaturesTest.ts @@ -0,0 +1,553 @@ +import * as TestHelper from '../../../../roosterjs-editor-api/test/TestHelper'; +import { Browser } from 'roosterjs-editor-dom'; +import { TextFeatures } from '../../../lib/plugins/ContentEdit/features/textFeatures'; + +import { + BuildInEditFeature, + ExperimentalFeatures, + IEditor, + Keys, + PluginEventType, + PluginKeyboardEvent, +} from 'roosterjs-editor-types'; + +describe('Text Features |', () => { + let editor: IEditor; + const TEST_ID = 'Test_ID'; + const TEST_ELEMENT_ID = 'test'; + + beforeEach(() => { + editor = TestHelper.initEditor(TEST_ID, null, [ExperimentalFeatures.TabKeyTextFeatures]); + }); + + afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement.removeChild(element); + } + editor.dispose(); + }); + + function runShouldHandleTest( + feature: BuildInEditFeature, + content: string, + selectCallback: () => void, + shouldHandleExpect: boolean, + focusEditorOnStart: boolean = true + ) { + //Arrange + if (focusEditorOnStart) { + editor.focus(); + } + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: simulateKeyDownEvent(Keys.TAB, feature == TextFeatures.outdentWhenTabText), + }; + editor.setContent(content); + selectCallback(); + + //Act + const result = feature.shouldHandleEvent(keyboardEvent, editor, false); + + //Assert + expect(!!result).toBe(shouldHandleExpect); + } + + function runHandleTest( + feature: BuildInEditFeature, + content: string, + selectCallback: () => void, + contentExpected: string + ) { + //Arrange + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: simulateKeyDownEvent(Keys.TAB, feature == TextFeatures.outdentWhenTabText), + }; + editor.setContent(content); + selectCallback(); + + //Act + feature.handleEvent(keyboardEvent, editor); + + //Assert + expect(editor.getContent()).toBe(contentExpected); + } + + describe('indentWhenTabText |', () => { + describe('Should handle event |', () => { + it('Should handle, text collapsed', () => { + runShouldHandleTest( + TextFeatures.indentWhenTabText, + `
          `, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID); + const range = new Range(); + range.setStart(element, 0); + editor.select(range); + }, + true + ); + }); + + it('Should handle, range not collapsed', () => { + runShouldHandleTest( + TextFeatures.indentWhenTabText, + `
          Test
          `, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID); + const range = new Range(); + range.setStart(element, 0); + range.setEnd(element, 1); + editor.select(range); + }, + true + ); + }); + + it('Should not handle, in a list', () => { + runShouldHandleTest( + TextFeatures.indentWhenTabText, + `
          1. sad
          `, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID); + const range = new Range(); + range.setStart(element, 0); + editor.select(range); + }, + false + ); + }); + + it('Should not handle, in a not contenteditable entity', () => { + runShouldHandleTest( + TextFeatures.indentWhenTabText, + `

          Not Editable
          `, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID); + const range = new Range(); + range.setStart(element, 0); + editor.select(range); + }, + false + ); + }); + + it('Should not handle, Link in a not content editable entity is focused', () => { + runShouldHandleTest( + TextFeatures.indentWhenTabText, + `


          `, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID); + element.focus(); + }, + false, + false + ); + }); + }); + + describe('Handle event |', () => { + function runHandleTest( + feature: BuildInEditFeature, + content: string, + selectCallback: () => void, + contentExpected: string, + additionalExpect?: () => void + ) { + //Arrange + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: simulateKeyDownEvent(Keys.TAB), + }; + editor.setContent(content); + selectCallback(); + + //Act + feature.handleEvent(keyboardEvent, editor); + + //Assert + expect(editor.getContent()).toBe(contentExpected); + additionalExpect?.(); + } + TestHelper.itFirefoxOnly('Handle event, text collapsed', () => { + runHandleTest( + TextFeatures.indentWhenTabText, + `
          `, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID); + const range = new Range(); + range.setStart(element, 0); + editor.select(range); + }, + '
                
          ' + ); + }); + + TestHelper.itFirefoxOnly( + 'Handle, range not collapsed and is not selected from start to end 2', + () => { + runHandleTest( + TextFeatures.indentWhenTabText, + `
          Test
          `, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID) + .firstChild; + const range = new Range(); + range.setStart(element, 0); + range.setEnd(element, 1); + editor.select(range); + }, + '
                est
          ' + ); + } + ); + + TestHelper.itFirefoxOnly( + 'Handle, range not collapsed and is selected from start to end', + () => { + runHandleTest( + TextFeatures.indentWhenTabText, + `
          Test
          `, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID) + .firstChild; + const range = new Range(); + range.setStart(element, 0); + range.setEnd(element, 4); + editor.select(range); + }, + '
          Test
          ' + ); + } + ); + + TestHelper.itFirefoxOnly( + 'Handle, range not collapsed and is selected from start to end, with empty elemets at start', + () => { + runHandleTest( + TextFeatures.indentWhenTabText, + `
          Test
          `, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID) + .lastChild; + const range = new Range(); + range.setStart(element, 0); + range.setEnd(element, 4); + editor.select(range); + }, + '
          Test
          ' + ); + } + ); + + TestHelper.itFirefoxOnly( + 'Handle, range not collapsed and is selected from start to end, with empty elemets at start 2', + () => { + runHandleTest( + TextFeatures.indentWhenTabText, + `
          Test
          `, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID) + .lastChild; + const range = new Range(); + range.setStart(element, 0); + range.setEnd(element, 4); + editor.select(range); + }, + '
          Test
          ' + ); + } + ); + + TestHelper.itFirefoxOnly( + 'Handle, range not collapsed and is selected from start to end, with empty elemets at start 3', + () => { + runHandleTest( + TextFeatures.indentWhenTabText, + `
          Test
          `, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID) + .lastChild; + const range = new Range(); + range.setStart(element, 0); + range.setEnd(element, 4); + editor.select(range); + }, + '
          Test
          ' + ); + } + ); + + TestHelper.itFirefoxOnly( + 'Handle, range not collapsed and is selected from start to end, with empty elemets at start 4', + () => { + runHandleTest( + TextFeatures.indentWhenTabText, + `
          Test
          `, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID) + .lastChild; + const range = new Range(); + range.setStart(element, 0); + range.setEnd(element, 4); + editor.select(range); + }, + '
          Test
          ' + ); + } + ); + + TestHelper.itFirefoxOnly( + 'Handle, range not collapsed and is selected from start to end, with empty elemets at end', + () => { + runHandleTest( + TextFeatures.indentWhenTabText, + `
          Test
          `, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID) + .firstChild; + const range = new Range(); + range.setStart(element, 0); + range.setEnd(element, 4); + editor.select(range); + }, + '
          Test
          ' + ); + } + ); + + TestHelper.itFirefoxOnly( + 'Handle, range not collapsed and is selected from start to end, with empty elemets at end 2', + () => { + runHandleTest( + TextFeatures.indentWhenTabText, + `
          Test
          `, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID) + .firstChild; + const range = new Range(); + range.setStart(element, 0); + range.setEnd(element, 4); + editor.select(range); + }, + '
          Test
          ' + ); + } + ); + + TestHelper.itFirefoxOnly( + 'Handle, range not collapsed and is selected from start to end, with empty elemets at end 3', + () => { + runHandleTest( + TextFeatures.indentWhenTabText, + `
          Test
          `, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID) + .firstChild; + const range = new Range(); + range.setStart(element, 0); + range.setEnd(element, 4); + editor.select(range); + }, + '
          Test
          ' + ); + } + ); + + TestHelper.itFirefoxOnly( + 'Handle, range not collapsed and is selected from start to end, with empty elemets at end 4', + () => { + runHandleTest( + TextFeatures.indentWhenTabText, + `
          Test
          `, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID) + .firstChild; + const range = new Range(); + range.setStart(element, 0); + range.setEnd(element, 4); + editor.select(range); + }, + '
          Test
          ' + ); + } + ); + + TestHelper.itFirefoxOnly( + 'Handle, range not collapsed and more than one block in selection', + () => { + runHandleTest( + TextFeatures.indentWhenTabText, + `
          Test
          Test
          `, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID); + const element2 = editor + .getDocument() + .getElementById(TEST_ELEMENT_ID + '2'); + const range = new Range(); + range.setStart(element2.firstChild, 1); + range.setEnd(element.firstChild, 3); + editor.select(range); + }, + '
          Test
          Test
          ' + ); + } + ); + + TestHelper.itFirefoxOnly( + 'Handle, range not collapsed and is not selected from start to end 1', + () => { + runHandleTest( + TextFeatures.indentWhenTabText, + `
          Test
          `, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID); + const range = new Range(); + range.setStart(element.firstChild, 1); + range.setEnd(element.firstChild, 3); + editor.select(range); + }, + '
          T     t
          ' + ); + } + ); + + TestHelper.itFirefoxOnly('Handle, Insert Tab before a Anchor Element', () => { + runHandleTest( + TextFeatures.indentWhenTabText, + ``, + () => { + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID); + const range = new Range(); + range.setStart(element, 1); + range.setEnd(element, 1); + editor.select(range); + }, + '
          Test  TestAnchor
          ', + () => { + const range = editor.getSelectionRange(); + + const element = editor.getDocument().getElementById(TEST_ELEMENT_ID); + const expectedRange = new Range(); + expectedRange.setStart(element, 2); + expect(range).toEqual(expectedRange); + } + ); + }); + }); + }); + + describe('OutdentWhenTabText |', () => { + describe('Should Handle Event |', () => { + it('Should handle event, all paragraph selected', () => { + runShouldHandleTest( + TextFeatures.outdentWhenTabText, + '

          Lorem ipsum dolort.

          Nullam molestie iaculis .


          ', + () => { + const p1 = editor.getDocument().getElementById('p1'); + const p2 = editor.getDocument().getElementById('p2'); + const range = new Range(); + range.setStart(p1.firstChild, 0); + range.setEnd(p2.firstChild, 25); + + editor.select(range); + }, + true + ); + }); + + it('Should not handle event, all paragraph selected but not under blockquote', () => { + runShouldHandleTest( + TextFeatures.outdentWhenTabText, + '

          Lorem ipsum dolort.

          Nullam molestie iaculis .


          ', + () => { + const p1 = editor.getDocument().getElementById('p1'); + const p2 = editor.getDocument().getElementById('p2'); + const range = new Range(); + range.setStart(p1.firstChild, 0); + range.setEnd(p2.firstChild, 25); + + editor.select(range); + }, + false + ); + }); + + it('Should not handle, Feature is not enabled', () => { + editor = TestHelper.initEditor(TEST_ID, null); + runShouldHandleTest( + TextFeatures.outdentWhenTabText, + '

          Lorem ipsum dolort.

          Nullam molestie iaculis .


          ', + () => { + const element = editor.getDocument().getElementById('p1'); + const range = new Range(); + range.setStart(element, 0); + editor.select(range); + }, + false + ); + }); + + it('Should handle event, not all paragraph selected', () => { + runShouldHandleTest( + TextFeatures.outdentWhenTabText, + '

          Lorem ipsum dolort.

          Nullam molestie iaculis .


          ', + () => { + const p1 = editor.getDocument().getElementById('p1'); + const p2 = editor.getDocument().getElementById('p2'); + const range = new Range(); + range.setStart(p1.firstChild, 0); + range.setEnd(p2.firstChild, 24); + + editor.select(range); + }, + false + ); + }); + }); + describe('Handle Event |', () => { + it('Handle Event when all paragraph selected', () => { + runHandleTest( + TextFeatures.outdentWhenTabText, + '

          Lorem ipsum dolort.

          Nullam molestie iaculis .


          ', + () => { + const p1 = editor.getDocument().getElementById('p1'); + const p2 = editor.getDocument().getElementById('p2'); + const range = new Range(); + range.setStart(p1.firstChild, 0); + range.setEnd(p2.firstChild, 25); + + editor.select(range); + }, + '

          Lorem ipsum dolort.

          Nullam molestie iaculis .


          ' + ); + }); + }); + }); +}); + +function simulateKeyDownEvent( + whichInput: number, + shiftKey: boolean = false, + ctrlKey: boolean = false +) { + const evt = new KeyboardEvent('keydown', { + shiftKey, + altKey: false, + ctrlKey, + cancelable: true, + which: whichInput, + }); + + if (!Browser.isFirefox) { + //Chromium hack to add which to the event as there is a bug in Webkit + //https://stackoverflow.com/questions/10455626/keydown-simulation-in-chrome-fires-normally-but-not-the-correct-key/10520017#10520017 + Object.defineProperty(evt, 'which', { + get: function () { + return whichInput; + }, + }); + } + return evt; +} diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/utils/covertAlphaToDecimals.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/utils/covertAlphaToDecimals.ts new file mode 100644 index 000000000000..1cc0d66b4f64 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/utils/covertAlphaToDecimals.ts @@ -0,0 +1,24 @@ +import convertAlphaToDecimals from '../../../../lib/plugins/ContentEdit/utils/convertAlphaToDecimals'; + +describe('convertAlphaToDecimals ', () => { + function runTest(letter: string, expectedNumber: number) { + const decimal = convertAlphaToDecimals(letter); + expect(decimal).toEqual(expectedNumber); + } + + it('A', () => { + runTest('A', 1); + }); + + it('AA', () => { + runTest('AA', 27); + }); + + it('Z', () => { + runTest('Z', 26); + }); + + it('AB', () => { + runTest('AB', 28); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/utils/getAutoBulletListStyleTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/utils/getAutoBulletListStyleTest.ts new file mode 100644 index 000000000000..3638d6d7851f --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/utils/getAutoBulletListStyleTest.ts @@ -0,0 +1,37 @@ +import getAutoBulletListStyle from '../../../../lib/plugins/ContentEdit/utils/getAutoBulletListStyle'; +import { BulletListType } from 'roosterjs-editor-types'; + +describe('getAutoListStyle ', () => { + function runTest(textBeforeCursor: string, listStyle: BulletListType) { + const style = getAutoBulletListStyle(textBeforeCursor); + expect(style).toEqual(listStyle); + } + + it('=> ', () => { + runTest('=> ', BulletListType.UnfilledArrow); + }); + + it('--> ', () => { + runTest('--> ', BulletListType.DoubleLongArrow); + }); + + it('-> ', () => { + runTest('-> ', BulletListType.LongArrow); + }); + + it('> ', () => { + runTest('> ', BulletListType.ShortArrow); + }); + + it('-- ', () => { + runTest('-- ', BulletListType.Square); + }); + + it('- ', () => { + runTest('- ', BulletListType.Dash); + }); + + it('* ', () => { + runTest('* ', BulletListType.Disc); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/utils/getAutoNumberingListStyleTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/utils/getAutoNumberingListStyleTest.ts new file mode 100644 index 000000000000..ff15d37050af --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/utils/getAutoNumberingListStyleTest.ts @@ -0,0 +1,105 @@ +import getAutoNumberingListStyle from '../../../../lib/plugins/ContentEdit/utils/getAutoNumberingListStyle'; +import { NumberingListType } from 'roosterjs-editor-types'; + +describe('getAutoListStyle ', () => { + function runTest(textBeforeCursor: string, listStyle: NumberingListType) { + const style = getAutoNumberingListStyle(textBeforeCursor); + expect(style).toEqual(listStyle); + } + + it('1. ', () => { + runTest('1.', NumberingListType.Decimal); + }); + + it('1- ', () => { + runTest('1- ', NumberingListType.DecimalDash); + }); + + it('1) ', () => { + runTest('1) ', NumberingListType.DecimalParenthesis); + }); + + it('(1) ', () => { + runTest('(1) ', NumberingListType.DecimalDoubleParenthesis); + }); + + it('A.', () => { + runTest('A. ', NumberingListType.UpperAlpha); + }); + + it('A- ', () => { + runTest('A- ', NumberingListType.UpperAlphaDash); + }); + + it('A) ', () => { + runTest('A) ', NumberingListType.UpperAlphaParenthesis); + }); + + it('(A) ', () => { + runTest('(A) ', NumberingListType.UpperAlphaDoubleParenthesis); + }); + + it('a. ', () => { + runTest('a. ', NumberingListType.LowerAlpha); + }); + + it('a- ', () => { + runTest('a- ', NumberingListType.LowerAlphaDash); + }); + + it('a) ', () => { + runTest('a) ', NumberingListType.LowerAlphaParenthesis); + }); + + it('(a) ', () => { + runTest('(a) ', NumberingListType.LowerAlphaDoubleParenthesis); + }); + + it('i. ', () => { + runTest('i. ', NumberingListType.LowerRoman); + }); + + it('i- ', () => { + runTest('i- ', NumberingListType.LowerRomanDash); + }); + + it('i) ', () => { + runTest('i) ', NumberingListType.LowerRomanParenthesis); + }); + + it('(i) ', () => { + runTest('(i) ', NumberingListType.LowerRomanDoubleParenthesis); + }); + + it('I. ', () => { + runTest('I. ', NumberingListType.UpperRoman); + }); + + it('I- ', () => { + runTest('I- ', NumberingListType.UpperRomanDash); + }); + + it('I) ', () => { + runTest('I) ', NumberingListType.UpperRomanParenthesis); + }); + + it('(I) ', () => { + runTest('(I) ', NumberingListType.UpperRomanDoubleParenthesis); + }); + + it('1:1. ', () => { + runTest('1:1. ', null); + }); + + it('30%). ', () => { + runTest('30%). ', null); + }); + + it('4th. ', () => { + runTest('4th. ', null); + }); + + it('30%) ', () => { + runTest('30%) ', null); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/ContextMenu/ContextMenuTest.ts b/packages/roosterjs-editor-plugins/test/ContextMenu/ContextMenuTest.ts new file mode 100644 index 000000000000..897e32cd8cfb --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/ContextMenu/ContextMenuTest.ts @@ -0,0 +1,62 @@ +import { ContextMenu } from '../../lib/ContextMenu'; +import { IEditor, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; +import * as TestHelper from '../TestHelper'; + +describe('ContextMenu plugin', () => { + let plugin: ContextMenu; + let editor: IEditor; + let renderer: jasmine.Spy; + let dismisser: jasmine.Spy; + + beforeEach(() => { + plugin = new ContextMenu({ + render: renderer = jasmine.createSpy('renderer'), + dismiss: dismisser = jasmine.createSpy('dismisser'), + }); + editor = TestHelper.initEditor('ContextMenuEditor', [plugin]); + }); + + afterEach(() => { + editor.dispose(); + plugin.dispose(); + }); + + function triggerWithItems(items: any[]) { + let event: PluginEvent = { + eventType: PluginEventType.ContextMenu, + rawEvent: new MouseEvent('mousedown'), + items, + }; + + editor.triggerPluginEvent(PluginEventType.ContextMenu, event); + } + + it('correctly invokes the renderer', () => { + const items = [{}]; + triggerWithItems(items); + + expect(renderer).toHaveBeenCalledWith( + (plugin).container, + items, + (plugin).onDismiss + ); + }); + + it('doesnt invoke the renderer if no items were provided', () => { + triggerWithItems([]); + + expect(renderer).not.toHaveBeenCalled(); + }); + + it('calls dismissal function when dismissed', () => { + let onDismiss: Function | undefined = undefined; + renderer = renderer.and.callFake((_, _1, onDismissFn) => void (onDismiss = onDismissFn)); + triggerWithItems([{}]); + + expect(onDismiss).toBeDefined(); + + onDismiss!?.(); + + expect(dismisser).toHaveBeenCalledWith((plugin).container); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/CutPasteListChain/cutPasteListChainTest.ts b/packages/roosterjs-editor-plugins/test/CutPasteListChain/cutPasteListChainTest.ts new file mode 100644 index 000000000000..97793c966ec1 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/CutPasteListChain/cutPasteListChainTest.ts @@ -0,0 +1,205 @@ +import * as commitListChains from 'roosterjs-editor-api/lib/utils/commitListChains'; +import * as DomTestHelper from 'roosterjs-editor-dom/test/DomTestHelper'; +import { + ClipboardData, + IEditor, + Keys, + PasteType, + PluginEvent, + PluginEventType, + SelectionRangeTypes, +} from 'roosterjs-editor-types'; +import { CutPasteListChain } from '../../lib/CutPasteListChain'; +import { Position, VListChain } from 'roosterjs-editor-dom'; + +describe('cutPasteListChain tests', () => { + let editor: IEditor; + let plugin: CutPasteListChain; + let addDomEventHandler: jasmine.Spy; + + beforeEach(() => { + spyOn(VListChain, 'createListChains').and.callThrough(); + spyOn(commitListChains, 'default').and.callFake(() => {}); + + plugin = new CutPasteListChain(); + + addDomEventHandler = jasmine + .createSpy('addDomEventHandler') + .and.returnValue(jasmine.createSpy('disposer')); + editor = ({ + addDomEventHandler, + getSelectionRange: () => { collapsed: false }, + getSelectionRangeEx: () => { + return { + type: SelectionRangeTypes.Normal, + ranges: [{ collapsed: false }], + areAllCollapsed: false, + }; + }, + }); + + plugin.initialize(editor); + }); + + afterEach(() => { + plugin.dispose(); + editor = null; + }); + + it('returns the actual plugin name', () => { + const expectedName = 'CutPasteListChain'; + const pluginName = plugin.getName(); + expect(pluginName).toBe(expectedName); + }); + + function createPluginEventBeforeCutCopy(root: HTMLDivElement) { + const range = DomTestHelper.createRangeFromChildNodes(root); + + const pluginEvent: PluginEvent = { + eventType: PluginEventType.BeforeCutCopy, + rawEvent: { + which: Keys.NULL, + }, + clonedRoot: root, + range, + isCut: true, + }; + return pluginEvent; + } + + function createPluginEventBeforePaste(testString: string) { + const clipboardData: ClipboardData = { + types: [], + text: testString, + rawHtml: null, + image: null, + snapshotBeforePaste: null, + imageDataUri: null, + customValues: {}, + }; + + const pluginEvent: PluginEvent = { + eventType: PluginEventType.BeforePaste, + clipboardData, + fragment: null, + sanitizingOption: { + elementCallbacks: {}, + attributeCallbacks: {}, + cssStyleCallbacks: {}, + additionalTagReplacements: {}, + additionalAllowedAttributes: [], + additionalAllowedCssClasses: [], + additionalDefaultStyleValues: {}, + additionalGlobalStyleNodes: [], + additionalPredefinedCssForElement: {}, + preserveHtmlComments: false, + unknownTagReplacement: null, + }, + htmlBefore: null, + htmlAfter: null, + htmlAttributes: {}, + pasteType: PasteType.Normal, + }; + + return pluginEvent; + } + + function createStringElement(text: string) { + const contentDiv = document.createElement('div'); + contentDiv.innerHTML = text; + return contentDiv; + } + + function createListElement() { + const contentDiv = document.createElement('div'); + const ol = document.createElement('ol'); + + const li1 = document.createElement('li'); + li1.appendChild(document.createTextNode('123')); + const li2 = document.createElement('li'); + li2.appendChild(document.createTextNode('456')); + + ol.appendChild(li1); + ol.appendChild(li2); + + contentDiv.appendChild(ol); + return contentDiv; + } + + function setGetSelectedRegions(contentDiv: HTMLElement) { + editor.getSelectedRegions = () => [ + { + rootNode: contentDiv, + nodeBefore: null, + nodeAfter: null, + skipTags: [], + fullSelectionStart: new Position(contentDiv.firstChild, 0), + fullSelectionEnd: new Position(contentDiv.firstChild, 3), + }, + ]; + } + + it('caches the list chain with cut', () => { + const testString: string = 'this is a test'; + const contentDiv = createStringElement(testString); + const expectedText: string = testString; + + const pluginEvent = createPluginEventBeforeCutCopy(contentDiv); + + setGetSelectedRegions(contentDiv); + + plugin.onPluginEvent(pluginEvent); + expect(VListChain.createListChains).toHaveBeenCalled(); + expect(contentDiv.innerHTML).toBe(expectedText); + }); + + it('caches the list chain with paste', () => { + const testString: string = 'this is a test'; + const contentDiv = createStringElement(testString); + const expectedText: string = testString; + + const pluginEvent = createPluginEventBeforePaste(testString); + + setGetSelectedRegions(contentDiv); + + plugin.onPluginEvent(pluginEvent); + expect(VListChain.createListChains).toHaveBeenCalled(); + expect(contentDiv.innerHTML).toBe(expectedText); + }); + + it('not call commitListChains with non-list element', () => { + const testString: string = 'this is a test'; + const contentDiv = createStringElement(testString); + const pasteEvent = createPluginEventBeforePaste(testString); + + setGetSelectedRegions(contentDiv); + + plugin.onPluginEvent(pasteEvent); + + const contentChangedEvent: PluginEvent = { + eventType: PluginEventType.ContentChanged, + source: 'Paste', + }; + plugin.onPluginEvent(contentChangedEvent); + + expect(commitListChains.default).not.toHaveBeenCalled(); + }); + + it('calls commitListChains with list element', () => { + const contentDiv = createListElement(); + + setGetSelectedRegions(contentDiv); + + const testString: string = 'this is a test'; + const pasteEvent = createPluginEventBeforePaste(testString); + plugin.onPluginEvent(pasteEvent); + + const contentChangedEvent: PluginEvent = { + eventType: PluginEventType.ContentChanged, + source: 'Paste', + }; + plugin.onPluginEvent(contentChangedEvent); + + expect(commitListChains.default).toHaveBeenCalled(); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/HyperLink/HyperLinkTest.ts b/packages/roosterjs-editor-plugins/test/HyperLink/HyperLinkTest.ts new file mode 100644 index 000000000000..5239f79130a0 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/HyperLink/HyperLinkTest.ts @@ -0,0 +1,91 @@ +import { IEditor, PluginEventType } from 'roosterjs-editor-types'; +import { HyperLink } from '../../lib/HyperLink'; +import { initEditor } from '../TestHelper'; + +const HREF = 'https://microsoft.github.io/roosterjs'; +const editorId = 'editorId'; + +describe('HyperLink plugin', () => { + let editor: IEditor; + let plugin: HyperLink; + let anchor: HTMLAnchorElement; + let div: HTMLDivElement; + let eventHandlers: Record; + + beforeEach(() => { + anchor = document.createElement('a'); + anchor.href = HREF; + plugin = new HyperLink(); + editor = initEditor(editorId, [plugin]); + spyOn(editor, 'addDomEventHandler').and.callFake(x => void (eventHandlers = x)); + plugin.initialize(editor); + const editorDiv = (editor).core.contentDiv; + if (!editorDiv) { + throw 'Unable to find editor div'; + } + div = editorDiv; + }); + + afterEach(() => { + editor.dispose(); + div.remove(); + eventHandlers = {}; + }); + + it('Removes title attribute if nothing is being hovered', () => { + div.appendChild(anchor); + + eventHandlers.mouseover({ + type: 'mouseover', + target: anchor, + }); + + eventHandlers.mouseout({ + type: 'mouseout', + target: anchor, + }); + + expect(div.getAttribute('title')).toEqual(null); + }); + + it('Displays the link as default for links', () => { + div.appendChild(anchor); + eventHandlers.mouseover({ + type: 'mouseover', + target: anchor, + }); + expect(div.getAttribute('title')).toEqual(HREF); + }); + + it('Opens the link whenever Ctrl+Click', () => { + const openSpy = spyOn(window, 'open'); + div.appendChild(anchor); + plugin.onPluginEvent({ + eventType: PluginEventType.MouseUp, + rawEvent: ({ + target: anchor, + srcElement: anchor, + ctrlKey: true, + button: 0, + preventDefault: () => {}, + } as unknown) as MouseEvent, + }); + expect(openSpy).toHaveBeenCalledTimes(1); + expect(openSpy).toHaveBeenCalledWith(HREF, '_blank'); + }); + + it('Does not open the link if its only clicked', () => { + const openSpy = spyOn(window, 'open'); + div.appendChild(anchor); + plugin.onPluginEvent({ + eventType: PluginEventType.MouseUp, + rawEvent: ({ + target: anchor, + srcElement: anchor, + ctrlKey: false, + button: 0, + } as unknown) as MouseEvent, + }); + expect(openSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/Picker/pickerPluginTest.ts b/packages/roosterjs-editor-plugins/test/Picker/pickerPluginTest.ts new file mode 100644 index 000000000000..07645d81d52d --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/Picker/pickerPluginTest.ts @@ -0,0 +1,185 @@ +import * as TestHelper from '../TestHelper'; +import { ChangeSource, IEditor, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; +import { PickerPlugin } from '../../lib/Picker'; + +const BACKSPACE_CHAR_CODE = 'Backspace'; +const ESC_CHAR_CODE = 'Escape'; + +const dataProvider = { + onInitalize: ( + insertNodeCallback: (nodeToInsert: HTMLElement) => void, + setIsSuggestingCallback: (isSuggesting: boolean) => void, + editor?: IEditor + ) => {}, + onDispose: () => { + return; + }, + + onIsSuggestingChanged: (isSuggesting: boolean) => { + return; + }, + + queryStringUpdated: (queryString: string, isExactMatch: boolean) => { + return; + }, + + onRemove: (nodeRemoved: Node, isBackwards: boolean) => { + return document.createTextNode(''); + }, + + onScroll: (scrollContainer: HTMLElement) => { + return; + }, + + onContentChanged: (elementIds: string[]) => { + return; + }, +}; + +const pickerOptions = { + elementIdPrefix: 'picker_test', + changeSource: ChangeSource.SetContent, + triggerCharacter: ')', +}; + +describe('PickerPlugin |', () => { + let editor: IEditor; + const TEST_ID = 'pickerTest'; + const plugin = new PickerPlugin(dataProvider, pickerOptions); + let spyOnQueryStringUpdated: any; + let spyOnIsSuggestingChanged: any; + let spyOnScroll: any; + let spyOnContentChange: any; + let spyOnInitialize: any; + let spyOnDispose: any; + beforeEach(() => { + editor = TestHelper.initEditor(TEST_ID, [plugin]); + spyOnQueryStringUpdated = spyOn(plugin.dataProvider, 'queryStringUpdated'); + spyOnIsSuggestingChanged = spyOn(plugin.dataProvider, 'onIsSuggestingChanged'); + spyOnScroll = spyOn(plugin.dataProvider, 'onScroll'); + spyOnContentChange = spyOn(plugin.dataProvider, 'onContentChanged'); + spyOnInitialize = spyOn(plugin.dataProvider, 'onInitalize'); + spyOnDispose = spyOn(plugin.dataProvider, 'onDispose'); + }); + + afterEach(() => { + editor.dispose(); + }); + + const keyDown = (keysTyped: string): PluginEvent => { + return { + eventType: PluginEventType.KeyDown, + rawEvent: { + key: keysTyped, + preventDefault: () => { + return; + }, + stopImmediatePropagation: () => { + return; + }, + }, + }; + }; + + const keyUp = (keysTyped: string): PluginEvent => { + return { + eventType: PluginEventType.KeyUp, + rawEvent: { + key: keysTyped, + }, + }; + }; + + const mouseUp = (): PluginEvent => { + return { + eventType: PluginEventType.MouseUp, + rawEvent: {}, + }; + }; + + const scroll = (): PluginEvent => { + return { + eventType: PluginEventType.Scroll, + rawEvent: {}, + scrollContainer: undefined, + }; + }; + + const contentChanged = (): PluginEvent => { + return { + eventType: PluginEventType.ContentChanged, + source: ChangeSource.SetContent, + }; + }; + + function runTestKeyDown(content: string, keyTyped: string) { + editor.setContent(content); + plugin.onPluginEvent(keyUp(')')); + plugin.onPluginEvent(keyDown(keyTyped)); + expect(spyOnIsSuggestingChanged).toHaveBeenCalled(); + expect(spyOnIsSuggestingChanged).toHaveBeenCalledWith(false); + } + + function runTestMouseUp(content: string) { + editor.setContent(content); + plugin.onPluginEvent(keyUp(')')); + plugin.onPluginEvent(mouseUp()); + expect(spyOnIsSuggestingChanged).toHaveBeenCalled(); + expect(spyOnIsSuggestingChanged).toHaveBeenCalledWith(false); + } + + function runTestScroll() { + const scrollEvent = scroll(); + plugin.onPluginEvent(scrollEvent); + expect(spyOnScroll).toHaveBeenCalled(); + expect(spyOnScroll).toHaveBeenCalledWith(spyOnScroll.scrollContainer); + } + + function runTestKeyUp(content: string, keyTyped: string, shouldSuggest: boolean) { + editor.setContent(content); + plugin.onPluginEvent(keyUp(keyTyped)); + expect(spyOnQueryStringUpdated).toHaveBeenCalled(); + expect(spyOnIsSuggestingChanged).toHaveBeenCalled(); + expect(spyOnIsSuggestingChanged).toHaveBeenCalledWith(shouldSuggest); + } + + function runTestContentChange() { + plugin.onPluginEvent(contentChanged()); + expect(spyOnContentChange).toHaveBeenCalled(); + expect(spyOnContentChange).toHaveBeenCalledWith([]); + } + + it('should show picker', () => { + runTestKeyUp('
          )
          ', ')', true); + }); + + it('should hide picker | ESC', () => { + runTestKeyDown('
          )
          ', ESC_CHAR_CODE); + }); + + it('should hide picker | backspace', () => { + runTestKeyDown('
          )
          ', BACKSPACE_CHAR_CODE); + }); + + it('should hide picker | mouseEvent', () => { + runTestMouseUp('
          )
          '); + }); + + it('should execute scroll function', () => { + runTestScroll(); + }); + + it('should execute onContentChange function', () => { + runTestContentChange(); + }); + + it('should execute initialize function', () => { + plugin.initialize(editor); + expect(spyOnInitialize).toHaveBeenCalled(); + }); + + it('should execute dispose function', () => { + plugin.dispose(); + expect(spyOnDispose).toHaveBeenCalled(); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/TableCellSelection/tableCellSelectionTest.ts b/packages/roosterjs-editor-plugins/test/TableCellSelection/tableCellSelectionTest.ts new file mode 100644 index 000000000000..ea1bd2b5447c --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/TableCellSelection/tableCellSelectionTest.ts @@ -0,0 +1,710 @@ +import * as TableCellSelectionFile from '../../lib/plugins/TableCellSelection/mouseUtils/handleMouseDownEvent'; +import { Browser } from 'roosterjs-editor-dom'; +import { DeleteTableContents } from '../../lib/plugins/TableCellSelection/features/DeleteTableContents'; +import { Editor } from 'roosterjs-editor-core'; +import { IEditor } from 'roosterjs-editor-types'; +import { TableCellSelection } from '../../lib/TableCellSelection'; +import { + Coordinates, + EditorOptions, + Keys, + PluginEventType, + PluginKeyboardEvent, + SelectionRangeTypes, + TableSelection, + TableSelectionRange, +} from 'roosterjs-editor-types'; +export * from 'roosterjs-editor-dom/test/DomTestHelper'; + +describe('TableCellSelectionPlugin |', () => { + let editor: IEditor; + let id = 'tableSelectionContainerId'; + let targetId = 'tableSelectionTestId'; + let targetId2 = 'tableSelectionTestId2'; + let tableCellSelection: TableCellSelection; + + beforeEach(() => { + let node = document.createElement('div'); + node.id = id; + document.body.insertBefore(node, document.body.childNodes[0]); + tableCellSelection = new TableCellSelection(); + + let options: EditorOptions = { + plugins: [tableCellSelection], + defaultFormat: { + fontFamily: 'Calibri,Arial,Helvetica,sans-serif', + fontSize: '11pt', + textColor: '#000000', + }, + corePluginOverride: {}, + }; + + editor = new Editor(node as HTMLDivElement, options); + + editor.runAsync = callback => { + callback(editor); + return null; + }; + }); + + afterEach(() => { + editor.dispose(); + editor = null; + const div = document.getElementById(id); + div.parentNode.removeChild(div); + }); + + function initTableSelection(target: HTMLElement) { + let target2 = target.nextElementSibling as HTMLElement; + const newRange = new Range(); + newRange.setStart(target, 0); + newRange.setEnd(target, 0); + + simulateMouseEvent('mousedown', target); + + editor.select(newRange); + + simulateMouseEvent('mousemove', target); + + newRange.setStart(target, 0); + newRange.setEnd(target2, 0); + editor.select(newRange); + + simulateMouseEvent('mousemove', target2); + } + + function runTest( + content: string, + expectRangeCallback?: () => Range[] | undefined, + expectedSelectionType?: SelectionRangeTypes + ) { + //Arrange + editor.setContent(content); + const target = document.getElementById(targetId); + const target2 = document.getElementById(targetId2); + + //Act + editor.focus(); + initTableSelection(target); + simulateMouseEvent('mousemove', target2); + + //Assert + simulateMouseEvent('mouseup', target2); + const selection = editor.getSelectionRangeEx(); + if (expectRangeCallback) { + expect(selection.ranges).toEqual(expectRangeCallback()); + } + expect(selection.type).toBe(expectedSelectionType); + expect(selection.areAllCollapsed).toBe(false); + } + + it('getName', () => { + expect(tableCellSelection.getName()).toBe('TableCellSelection'); + }); + + describe('Mouse Events |', () => { + it('Should not convert to Table Selection', () => { + //Arrange + editor.setContent( + `
          aw
          ` + ); + const target = document.getElementById(targetId); + + //Act + editor.focus(); + const newRange = new Range(); + newRange.setStart(target, 0); + newRange.setEnd(target, 1); + simulateMouseEvent('mousedown', target); + editor.select(newRange); + simulateMouseEvent('mousemove', target); + newRange.setStart(target, 0); + newRange.setEnd(target, 1); + editor.select(newRange); + simulateMouseEvent('mousemove', target); + + //Assert + const selection = editor.getSelectionRangeEx(); + expect(selection.type).toBe(SelectionRangeTypes.Normal); + expect(selection.areAllCollapsed).toBe(false); + }); + + it('Should convert to Table Selection', () => { + //Arrange + editor.setContent( + `
          aw
          ` + ); + const target = document.getElementById(targetId); + + //Act + editor.focus(); + initTableSelection(target); + + //Assert + const selection = editor.getSelectionRangeEx(); + const target2 = document.getElementById(targetId2); + const expectRange = new Range(); + expectRange.setStart(target, 0); + expectRange.setEndAfter(target2); + + expect(selection.ranges).toEqual([expectRange]); + expect(selection.type).toBe(SelectionRangeTypes.TableSelection); + expect(selection.areAllCollapsed).toBe(false); + }); + + it('Selection inside of table 2', () => { + runTest( + `


          fsad fasd














          `, + () => { + const table = editor.queryElements('table')[0]; + const result: Range[] = []; + Array.from(table.rows).forEach(row => { + const tempRange = new Range(); + tempRange.setStart(row, 1); + tempRange.setEnd(row, 3); + result.push(tempRange); + }); + return result; + }, + SelectionRangeTypes.TableSelection + ); + }); + + it('Selection inside of table 3', () => { + runTest( + `


          fsad fasd














          `, + () => { + const table = editor.queryElements('table')[0]; + const result: Range[] = []; + Array.from(table.rows).forEach(row => { + const tempRange = new Range(); + tempRange.setStart(row, 1); + tempRange.setEnd(row, 4); + result.push(tempRange); + }); + return result; + }, + SelectionRangeTypes.TableSelection + ); + }); + + it('Selection inside of table with table with color 1', () => { + runTest( + `
          aw
          `, + () => { + const table = editor.queryElements('table')[0]; + const result: Range[] = []; + Array.from(table.rows).forEach(row => { + const tempRange = new Range(); + tempRange.setStart(row, 0); + tempRange.setEnd(row, 2); + result.push(tempRange); + }); + return result; + }, + SelectionRangeTypes.TableSelection + ); + }); + + it('Selection inside of table with table with color 2', () => { + runTest( + `















          `, + () => { + const table = editor.queryElements('table')[0]; + const result: Range[] = []; + Array.from(table.rows) + .filter((t, i) => i < 2) + .forEach(row => { + const tempRange = new Range(); + tempRange.setStart(row, 0); + tempRange.setEnd(row, 4); + result.push(tempRange); + }); + return result; + }, + SelectionRangeTypes.TableSelection + ); + }); + + it('Selection starts inside of table and ends outside of table', () => { + runTest( + `























          asdsad
          `, + undefined, + SelectionRangeTypes.Normal + ); + }); + + it('Selection starts outside of table and ends inside of table', () => { + //Arrange + editor.setContent( + `























          asdsad
          ` + ); + const target = document.getElementById(targetId); + const target2 = document.getElementById(targetId2); + + //Act + editor.focus(); + const tempRange = new Range(); + tempRange.selectNode(target); + editor.select(tempRange); + simulateMouseEvent('mousedown', target); + simulateMouseEvent('mousemove', target); + simulateMouseEvent('mousemove', target2); + + //Assert + simulateMouseEvent('mouseup', target2); + const selection = editor.getSelectionRangeEx(); + expect(selection.type).toBe(SelectionRangeTypes.Normal); + expect(selection.areAllCollapsed).toBe(false); + }); + + it('Table Selection from inner table to parent table', () => { + //Arrange + editor.setContent( + `





















          ` + ); + const target = document.getElementById('init'); + const targetParent = document.getElementById(targetId); + const target2 = document.getElementById(targetId2); + + //Act + editor.focus(); + initTableSelection(target); + simulateMouseEvent('mousemove', targetParent); + simulateMouseEvent('mousemove', target2); + + //Assert + + const selection = editor.getSelectionRangeEx(); + expect(selection.type).toBe(SelectionRangeTypes.TableSelection); + expect((selection).ranges.length).toBe(1); + expect((selection).coordinates.firstCell).toEqual({ x: 0, y: 0 }); + expect((selection).coordinates.lastCell).toEqual({ x: 2, y: 0 }); + }); + + it('should not handle selectionInsideTableMouseMove on selecting text', () => { + editor.setContent( + '

          What is Lorem Ipsum?

          Lorem Ipsum is simply dummy text of the printing and typesetting industry. .

          Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, 

          when an unknown printer took a galley of type and scrambled it to make a type 

          specimen book. It has survived not only five centuries, but also the leap into electronic

           typesetting, remaining essentially unchanged. It was popularised in the 1960s with the

           release of Letraset sheets containing Lorem Ipsum passages, and more recently with 

          desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.


          ' + ); + spyOn(TableCellSelectionFile, 'selectionInsideTableMouseMove').and.callThrough(); + + const container = editor.getDocument().getElementById('container'); + simulateMouseEvent('mousedown', container); + container.querySelectorAll('p').forEach(p => { + simulateMouseEvent('mousemove', p); + }); + + expect(TableCellSelectionFile.selectionInsideTableMouseMove).toHaveBeenCalledTimes(0); + }); + + it('Shift + Mouse Move scenario', () => { + //Arrange + editor.setContent( + `


          fsad fasd














          ` + ); + const target = document.getElementById(targetId); + const target2 = document.getElementById(targetId2); + + //Act + editor.focus(); + simulateMouseEvent('mousedown', target); + simulateMouseEvent('mousemove', target); + simulateMouseEvent('mouseup', target); + + editor.runAsync = callback => { + const tRange = new Range(); + tRange.setStart(target, 0); + tRange.setEnd(target2, 0); + editor.select(tRange); + callback(editor); + return null; + }; + + simulateMouseEvent('mousedown', target2, true); + simulateMouseEvent('mouseup', target2, true); + + //Assert + const selection = editor.getSelectionRangeEx(); + expect(selection.type).toBe(SelectionRangeTypes.TableSelection); + expect((selection).coordinates).toEqual({ + firstCell: { x: 1, y: 0 }, + lastCell: { x: 2, y: 3 }, + }); + expect(selection.areAllCollapsed).toBe(false); + }); + }); + + describe('Key Events |', () => { + function runKeyDownTest( + which: { whichInput: number; shiftKey?: boolean; ctrlKey?: boolean }, + expectInput: TableSelection, + startCoordinates?: Coordinates, + expectType?: SelectionRangeTypes + ) { + const { whichInput, ctrlKey, shiftKey } = which; + //Arrange + setup(startCoordinates); + + //Assert + editor.triggerPluginEvent(PluginEventType.KeyDown, { + rawEvent: simulateKeyDownEvent(whichInput, shiftKey, ctrlKey), + eventDataCache: {}, + }); + + const selection = editor.getSelectionRangeEx(); + + expect((selection).coordinates).toEqual(expectInput); + expect(selection.type).toEqual(expectType ?? SelectionRangeTypes.TableSelection); + expect(selection.areAllCollapsed).toBe(false); + } + + function runKeyUpTest( + which: { whichInput: number; shiftKey?: boolean; ctrlKey?: boolean }, + expectInput: TableSelection, + startCoordinates?: Coordinates, + expectType?: SelectionRangeTypes, + areAllCollapsed?: boolean + ) { + const { whichInput, ctrlKey, shiftKey } = which; + //Arrange + setup(startCoordinates); + + //Assert + editor.triggerPluginEvent(PluginEventType.KeyUp, { + rawEvent: simulateKeyDownEvent(whichInput, shiftKey, ctrlKey), + eventDataCache: {}, + }); + + const selection = editor.getSelectionRangeEx(); + + expect((selection)?.coordinates ?? undefined).toEqual(expectInput); + expect(selection.type).toEqual(expectType ?? SelectionRangeTypes.TableSelection); + expect(selection.areAllCollapsed).toBe(areAllCollapsed); + } + + function setup(startCoordinates: Coordinates) { + editor.setContent( + `















          ` + ); + + const target = document.getElementById(targetId); + const target2 = startCoordinates + ? (document.querySelector( + `table tr:nth-child(${startCoordinates.y + 1}) td:nth-child(${ + startCoordinates.x + 1 + })` + ) as HTMLElement) + : document.getElementById(targetId2); + + //Act + editor.focus(); + initTableSelection(target); + simulateMouseEvent('mousemove', target2); + simulateMouseEvent('mouseup', target2); + } + + it('Should not convert to Table Selection', () => { + //Arrange + editor.setContent( + `
          aw
          ` + ); + const target = document.getElementById(targetId); + + //Act + editor.focus(); + const newRange = new Range(); + newRange.setStart(target, 0); + newRange.setEnd(target, 1); + + editor.triggerPluginEvent(PluginEventType.KeyDown, { + rawEvent: simulateKeyDownEvent(Keys.RIGHT), + eventDataCache: {}, + }); + editor.select(newRange); + editor.triggerPluginEvent(PluginEventType.KeyDown, { + rawEvent: simulateKeyDownEvent(Keys.RIGHT), + eventDataCache: {}, + }); + newRange.setStart(target, 0); + newRange.setEnd(target, 1); + editor.select(newRange); + editor.triggerPluginEvent(PluginEventType.KeyDown, { + rawEvent: simulateKeyDownEvent(Keys.RIGHT), + eventDataCache: {}, + }); + + //Assert + const selection = editor.getSelectionRangeEx(); + expect(selection.type).toBe(SelectionRangeTypes.Normal); + expect(selection.areAllCollapsed).toBe(false); + }); + + it('Selection using Keyboard RIGHT', () => { + runKeyDownTest({ whichInput: Keys.RIGHT }, { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 3, y: 2 }, + } as TableSelection); + }); + + it('Selection using Keyboard RIGHT at last cell of row', () => { + runKeyDownTest( + { whichInput: Keys.RIGHT }, + { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 3, y: 3 }, + } as TableSelection, + { + x: 3, + y: 2, + } + ); + }); + + it('Selection using Keyboard LEFT', () => { + runKeyDownTest({ whichInput: Keys.LEFT }, { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 1, y: 2 }, + } as TableSelection); + }); + + it('Selection using Keyboard LEFT at first cell of row', () => { + runKeyDownTest( + { whichInput: Keys.LEFT }, + { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 0, y: 2 }, + } as TableSelection, + { + x: 0, + y: 3, + } + ); + }); + + it('Selection using Keyboard UP', () => { + runKeyDownTest({ whichInput: Keys.UP }, { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 2, y: 1 }, + } as TableSelection); + }); + + it('Selection using Keyboard UP on first Row', () => { + runKeyDownTest( + { whichInput: Keys.UP }, + undefined, + { + x: 0, + y: 0, + }, + SelectionRangeTypes.Normal + ); + }); + + it('Selection using Keyboard DOWN', () => { + runKeyDownTest({ whichInput: Keys.DOWN }, { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 2, y: 3 }, + } as TableSelection); + }); + + it('Selection using Keyboard DOWN on last Row', () => { + runKeyDownTest( + { whichInput: Keys.DOWN }, + undefined, + { + x: 0, + y: 3, + }, + SelectionRangeTypes.Normal + ); + }); + + it('Selection using Keyboard DOWN on last Row and use DOWN Key again', () => { + runKeyDownTest( + { whichInput: Keys.DOWN }, + undefined, + { + x: 0, + y: 3, + }, + SelectionRangeTypes.Normal + ); + + editor.triggerPluginEvent(PluginEventType.KeyDown, { + rawEvent: simulateKeyDownEvent(Keys.DOWN), + eventDataCache: {}, + }); + + const selection = editor.getSelectionRangeEx(); + expect(selection.type).toEqual(SelectionRangeTypes.Normal); + }); + + it('Selection using Keyboard SHIFT', () => { + runKeyDownTest( + { whichInput: Keys.SHIFT, shiftKey: true }, + { firstCell: { x: 0, y: 0 }, lastCell: { x: 2, y: 2 } } + ); + }); + + it('preventDefault when still selecting', () => { + //Arrange + spyOn(TableCellSelectionFile, 'selectionInsideTableMouseMove').and.callThrough(); + editor.setContent( + `
          aw
          ` + ); + const target = document.getElementById(targetId); + const target2 = document.getElementById(targetId2); + + //Act + editor.focus(); + initTableSelection(target); + const rawEvent = simulateKeyDownEvent(38); + + //Assert + editor.triggerPluginEvent(PluginEventType.KeyDown, { + rawEvent, + eventDataCache: {}, + }); + + simulateMouseEvent('mouseup', target2); + + //Assert + expect(rawEvent.defaultPrevented).toEqual(true); + }); + + it('Handle key up should clear state', () => { + runKeyUpTest( + { whichInput: 38, shiftKey: false }, + undefined, + null, + SelectionRangeTypes.Normal, + true + ); + }); + + it('Handle key up should not clear state', () => { + runKeyUpTest( + { whichInput: Keys.DOWN, shiftKey: true }, + { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 2, y: 2 }, + } as TableSelection, + null, + SelectionRangeTypes.TableSelection, + false + ); + }); + }); + + describe('ShadowEdit Event |', () => { + it('Shadow Edit on Table Selection', () => { + //Arrange + editor.setContent( + `















          ` + ); + const table = editor.queryElements('table')[0]; + + editor.focus(); + editor.select(table, { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 3, y: 2 }, + } as TableSelection); + + editor.startShadowEdit(); + + let selection = editor.getSelectionRangeEx(); + expect(selection.type).toEqual(SelectionRangeTypes.TableSelection); + expect(selection.areAllCollapsed).toBe(false); + expect(selection.ranges.length).toBe(3); + + editor.stopShadowEdit(); + + selection = editor.getSelectionRangeEx(); + expect(selection.type).toEqual(SelectionRangeTypes.TableSelection); + expect(selection.areAllCollapsed).toBe(false); + expect(selection.ranges.length).toBe(3); + }); + + it('Shadow Edit after performing a selection that starts inside of a table and end outside of a table', () => { + runTest( + `























          asdsad
          `, + undefined, + SelectionRangeTypes.Normal + ); + editor.startShadowEdit(); + let selection = editor.getSelectionRangeEx(); + expect(selection.type).toBe(SelectionRangeTypes.Normal); + + editor.stopShadowEdit(); + selection = editor.getSelectionRangeEx(); + expect(selection.type).toBe(SelectionRangeTypes.Normal); + }); + }); + + it('DeleteTableContents Feature', () => { + //Arrange + editor.setContent( + `
          Test string
          Test string
          Test string
          Test string
          Test string
          Test string
          Test string
          Test string
          Test string
          Test string
          Test string
          Test string
          Test string
          Test string
          Test string
          ` + ); + + const table = editor.getDocument().getElementById('table1') as HTMLTableElement; + + editor.select(table, { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 3, y: 3 }, + } as TableSelection); + + const shouldHandle = DeleteTableContents.shouldHandleEvent( + {}, + editor, + false + ); + + DeleteTableContents.handleEvent({}, editor); + + expect(shouldHandle).toBeTrue(); + + table.querySelectorAll('td').forEach(cell => { + expect(cell.childElementCount).toEqual(1); + expect(cell.firstElementChild?.tagName).toEqual('BR'); + }); + }); +}); + +function simulateMouseEvent(type: string, target: HTMLElement, shiftKey: boolean = false) { + const rect = target.getBoundingClientRect(); + var event = new MouseEvent(type, { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left, + clientY: rect.top, + shiftKey, + }); + target.dispatchEvent(event); +} + +function simulateKeyDownEvent( + whichInput: number, + shiftKey: boolean = true, + ctrlKey: boolean = false +) { + const evt = new KeyboardEvent('keydown', { + shiftKey, + altKey: false, + ctrlKey, + cancelable: true, + which: whichInput, + }); + + if (!Browser.isFirefox) { + //Chromium hack to add which to the event as there is a bug in Webkit + //https://stackoverflow.com/questions/10455626/keydown-simulation-in-chrome-fires-normally-but-not-the-correct-key/10520017#10520017 + Object.defineProperty(evt, 'which', { + get: function () { + return whichInput; + }, + }); + } + return evt; +} diff --git a/packages/roosterjs-editor-plugins/test/TableCellSelection/utils/normalizeTableSelectionTest.ts b/packages/roosterjs-editor-plugins/test/TableCellSelection/utils/normalizeTableSelectionTest.ts new file mode 100644 index 000000000000..8d45057fe8c3 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/TableCellSelection/utils/normalizeTableSelectionTest.ts @@ -0,0 +1,108 @@ +import normalizeTableSelection from '../../../lib/plugins/TableCellSelection/utils/normalizeTableSelection'; +import { TableSelection } from 'roosterjs-editor-types'; +import { VTable } from 'roosterjs-editor-dom'; + +describe('normalize table selection |', () => { + function runTest(selection: TableSelection, expectResult: TableSelection) { + let div = document.createElement('div'); + document.body.appendChild(div); + div.innerHTML = + '
          '; + let node = document.getElementById('testTable') as HTMLTableElement; + const vTable = new VTable(node); + vTable.selection = selection; + expect(normalizeTableSelection(vTable)).toEqual(expectResult); + document.body.removeChild(div); + } + + it('Expect same input', () => { + runTest( + { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 1, y: 1 }, + }, + { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 1, y: 1 }, + } + ); + }); + + it('Expect null, firstCell null', () => { + runTest( + { + firstCell: null, + lastCell: { x: 1, y: 1 }, + }, + null + ); + }); + + it('Expect null, lastCell null', () => { + runTest( + { + firstCell: { x: 1, y: 1 }, + lastCell: null, + }, + null + ); + }); + + it('Expect null, lastCell & firstCell null', () => { + runTest( + { + firstCell: null, + lastCell: null, + }, + null + ); + }); + + it('Expect null, input null', () => { + runTest(null, null); + }); + + it('Normalize 1', () => { + runTest( + { + firstCell: { x: 1, y: 1 }, + lastCell: { x: 0, y: 0 }, + }, + { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 1, y: 1 }, + } + ); + }); + + it('Normalize 2', () => { + runTest( + { + firstCell: { x: 1, y: 0 }, + lastCell: { x: 0, y: 0 }, + }, + { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 1, y: 0 }, + } + ); + }); + + it('Normalize 2', () => { + runTest( + { + firstCell: { x: null, y: null }, + lastCell: { x: 0, y: 0 }, + }, + { + firstCell: { x: 0, y: 0 }, + lastCell: { x: 0, y: 0 }, + } + ); + }); + + it('VTable is null', () => { + const vTable = null; + expect(normalizeTableSelection(vTable)).toEqual(null); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/TableResize/tableSelectorTest.ts b/packages/roosterjs-editor-plugins/test/TableResize/tableSelectorTest.ts new file mode 100644 index 000000000000..eae00ecb692f --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/TableResize/tableSelectorTest.ts @@ -0,0 +1,138 @@ +import createTableSelector from '../../lib/plugins/TableResize/editors/TableSelector'; +import TableEditor from '../../lib/plugins/TableResize/editors/TableEditor'; +import { Editor } from 'roosterjs-editor-core'; +import { EditorOptions, IEditor, SelectionRangeTypes } from 'roosterjs-editor-types'; +import { TableResize } from '../../lib/TableResize'; +export * from 'roosterjs-editor-dom/test/DomTestHelper'; + +describe('Table Selector Tests', () => { + let editor: IEditor; + let id = 'tableSelectionContainerId'; + let targetId = 'tableSelectionTestId'; + let tableResize: TableResize; + let node: HTMLDivElement; + + beforeEach(() => { + document.body.innerHTML = ''; + node = document.createElement('div'); + node.id = id; + document.body.insertBefore(node, document.body.childNodes[0]); + tableResize = new TableResize(); + + let options: EditorOptions = { + plugins: [tableResize], + defaultFormat: { + fontFamily: 'Calibri,Arial,Helvetica,sans-serif', + fontSize: '11pt', + textColor: '#000000', + }, + }; + + editor = new Editor(node as HTMLDivElement, options); + editor.runAsync = callback => { + callback(editor); + return null; + }; + }); + + afterEach(() => { + editor.dispose(); + editor = null; + const div = document.getElementById(id); + div?.parentNode?.removeChild(div); + node.parentElement?.removeChild(node); + }); + + it('Display component on mouse move inside table', () => { + runTest(0, true); + }); + + it('Do not display component, top of table is no visible in the container.', () => { + //Arrange + runTest(15, false); + }); + + it('Do not display component, Top of table is no visible in the scroll container.', () => { + //Arrange + const scrollContainer = document.createElement('div'); + document.body.insertBefore(scrollContainer, document.body.childNodes[0]); + scrollContainer.append(node); + spyOn(editor, 'getScrollContainer').and.returnValue(scrollContainer); + + runTest(15, false); + }); + + it('Display component, Top of table is visible in the scroll container scrolled down.', () => { + //Arrange + const scrollContainer = document.createElement('div'); + scrollContainer.innerHTML = '
          '; + document.body.insertBefore(scrollContainer, document.body.childNodes[0]); + scrollContainer.append(node); + spyOn(editor, 'getScrollContainer').and.returnValue(scrollContainer); + + runTest(0, true); + }); + + it('On click event', () => { + editor.setContent( + `

































































          ` + ); + const table = document.getElementById(targetId) as HTMLTableElement; + + const tableEditor = new TableEditor( + editor, + table, + () => {}, + () => true + ); + + tableEditor.onSelect(table); + + const selection = editor.getSelectionRangeEx(); + expect(selection.type).toBe(SelectionRangeTypes.TableSelection); + if (selection.type == SelectionRangeTypes.TableSelection) { + expect(selection.coordinates).toEqual({ + firstCell: { + x: 0, + y: 0, + }, + lastCell: { + y: 7, + x: 7, + }, + }); + expect(selection.ranges.length).toBe(8); + } + }); + + function runTest(scrollTop: number, isNotNull: boolean | null) { + //Arrange + editor.setContent( + '
          aw
          aw
          ' + ); + + node.style.height = '100px'; + node.style.overflowX = 'auto'; + node.scrollTop = scrollTop; + const target = document.getElementById('table1'); + editor.focus(); + + //Act + const result = createTableSelector( + target as HTMLTableElement, + 1, + editor, + () => {}, + () => () => {}, + () => {}, + node + ); + + //Assert + if (!isNotNull) { + expect(result).toBeNull(); + } else { + expect(result).toBeDefined(); + } + } +}); diff --git a/packages/roosterjs-editor-plugins/test/TableSelection/tableSelectionTest.ts b/packages/roosterjs-editor-plugins/test/TableSelection/tableSelectionTest.ts deleted file mode 100644 index 6facf0cd761e..000000000000 --- a/packages/roosterjs-editor-plugins/test/TableSelection/tableSelectionTest.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { Browser } from 'roosterjs-editor-dom/lib/utils/Browser'; -import { Editor } from 'roosterjs-editor-core'; -import { EditorOptions } from 'roosterjs-editor-types'; -import { IEditor } from 'roosterjs-editor-types'; -import { TableCellSelection } from '../../lib/TableCellSelection'; -export * from 'roosterjs-editor-dom/test/DomTestHelper'; - -describe('TableCellSelectionPlugin', () => { - let editor: IEditor; - let id = 'tableSelectionContainerId'; - let targetId = 'tableSelectionTestId'; - let targetId2 = 'tableSelectionTestId2'; - let tableCellSelection: TableCellSelection; - - beforeEach(() => { - let node = document.createElement('div'); - node.id = id; - document.body.insertBefore(node, document.body.childNodes[0]); - tableCellSelection = new TableCellSelection(); - - let options: EditorOptions = { - plugins: [tableCellSelection], - defaultFormat: { - fontFamily: 'Calibri,Arial,Helvetica,sans-serif', - fontSize: '11pt', - textColor: '#000000', - }, - }; - - editor = new Editor(node as HTMLDivElement, options); - }); - - afterEach(() => { - editor.dispose(); - editor = null; - const div = document.getElementById(id); - div.parentNode.removeChild(div); - }); - - function runTest(content: string, result: string) { - editor.setContent(content); - const target = document.getElementById(targetId); - const target2 = document.getElementById(targetId2); - editor.focus(); - - initTableSelection(target); - simulateMouseEvent('mousemove', target2); - expect(editor.getScrollContainer().innerHTML).toBe(result); - } - - function initTableSelection(target: HTMLElement) { - let target2 = target.nextElementSibling as HTMLElement; - const newRange = new Range(); - newRange.setStart(target, 0); - newRange.setEnd(target, 0); - - simulateMouseEvent('mousedown', target); - - editor.select(newRange); - - simulateMouseEvent('mousemove', target); - - newRange.setStart(target, 0); - newRange.setEnd(target2, 0); - editor.select(newRange); - - simulateMouseEvent('mousemove', target2); - } - - it('Should not convert to Table Selection', () => { - const expected = Browser.isFirefox - ? '
          aw
          ' - : '
          aw
          '; - editor.setContent( - `
          aw
          ` - ); - const target = document.getElementById(targetId); - editor.focus(); - - const newRange = new Range(); - newRange.setStart(target, 0); - newRange.setEnd(target, 1); - - simulateMouseEvent('mousedown', target); - - editor.select(newRange); - - simulateMouseEvent('mousemove', target); - - newRange.setStart(target, 0); - newRange.setEnd(target, 1); - editor.select(newRange); - - simulateMouseEvent('mousemove', target); - expect(editor.getScrollContainer().innerHTML).toBe(expected); - }); - - it('Should convert to Table Selection', () => { - const expected = Browser.isFirefox - ? '
          aw
          ' - : '
          aw
          '; - - spyOn(tableCellSelection, 'selectionInsideTableMouseMove').and.callThrough(); - editor.setContent( - `
          aw
          ` - ); - const target = document.getElementById(targetId); - editor.focus(); - initTableSelection(target); - - expect(editor.getScrollContainer().innerHTML).toBe(expected); - expect(tableCellSelection.selectionInsideTableMouseMove).toHaveBeenCalledTimes(2); - }); - - it('Selection inside of table 2', () => { - const expected = Browser.isFirefox - ? '


          fsad fasd














          ' - : '


          fsad fasd














          '; - runTest( - `


          fsad fasd














          `, - expected - ); - }); - - it('Selection inside of table 3', () => { - const expected = Browser.isFirefox - ? '


          fsad fasd














          ' - : '


          fsad fasd














          '; - runTest( - `


          fsad fasd














          `, - expected - ); - }); - - it('Selection inside of table with table with color 1', () => { - const expected = Browser.isFirefox - ? '
          aw
          ' - : '
          aw
          '; - runTest( - `
          aw
          `, - expected - ); - }); - - it('Selection inside of table with table with color 2', () => { - const expected = Browser.isFirefox - ? '















          ' - : '















          '; - runTest( - `















          `, - expected - ); - }); - - it('Selection starts inside of table and ends outside of table', () => { - const expected = Browser.isFirefox - ? '























          asdsad
          ' - : '























          asdsad
          '; - - runTest( - `























          asdsad
          `, - expected - ); - }); - - it('Table Selection from inner table to parent table', () => { - const result = Browser.isFirefox - ? '





















          ' - : '





















          '; - - editor.setContent( - `





















          ` - ); - const target = document.getElementById('init'); - const targetParent = document.getElementById(targetId); - const target2 = document.getElementById(targetId2); - editor.focus(); - initTableSelection(target); - simulateMouseEvent('mousemove', targetParent); - simulateMouseEvent('mousemove', target2); - expect(editor.getScrollContainer().innerHTML).toBe(result); - }); - - it('handle ExtractContent', () => { - editor.setContent( - '






          ' - ); - - expect(editor.getContent()).toBe( - Browser.isFirefox - ? '






          ' - : '






          ' - ); - }); - - it('should not handle selectionInsideTableMouseMove on selecting text', () => { - editor.setContent( - '

          What is Lorem Ipsum?

          Lorem Ipsum is simply dummy text of the printing and typesetting industry. .

          Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, 

          when an unknown printer took a galley of type and scrambled it to make a type 

          specimen book. It has survived not only five centuries, but also the leap into electronic

           typesetting, remaining essentially unchanged. It was popularised in the 1960s with the

           release of Letraset sheets containing Lorem Ipsum passages, and more recently with 

          desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.


          ' - ); - spyOn(tableCellSelection, 'selectionInsideTableMouseMove').and.callThrough(); - - const container = editor.getDocument().getElementById('container'); - simulateMouseEvent('mousedown', container); - container.querySelectorAll('p').forEach(p => { - simulateMouseEvent('mousemove', p); - }); - - expect(tableCellSelection.selectionInsideTableMouseMove).toHaveBeenCalledTimes(0); - }); -}); - -function simulateMouseEvent(type: string, target: HTMLElement, point?: { x: number; y: number }) { - const rect = target.getBoundingClientRect(); - var event = new MouseEvent(type, { - view: window, - bubbles: true, - cancelable: true, - clientX: rect.left + (point != undefined ? point?.x : 0), - clientY: rect.top + (point != undefined ? point?.y : 0), - }); - target.dispatchEvent(event); -} diff --git a/packages/roosterjs-editor-plugins/test/TestHelper.ts b/packages/roosterjs-editor-plugins/test/TestHelper.ts index db5df155cf66..9abb794540ed 100644 --- a/packages/roosterjs-editor-plugins/test/TestHelper.ts +++ b/packages/roosterjs-editor-plugins/test/TestHelper.ts @@ -1,8 +1,12 @@ -export * from 'roosterjs-editor-dom/test/DomTestHelper'; import { Editor } from 'roosterjs-editor-core'; -import { EditorOptions, EditorPlugin } from 'roosterjs-editor-types'; +import { EditorOptions, EditorPlugin, ExperimentalFeatures } from 'roosterjs-editor-types'; +export * from 'roosterjs-editor-dom/test/DomTestHelper'; -export function initEditor(id: string, plugins?: EditorPlugin[]) { +export function initEditor( + id: string, + plugins?: EditorPlugin[], + experimentalFeatures?: ExperimentalFeatures[] +) { let node = document.createElement('div'); node.id = id; document.body.insertBefore(node, document.body.childNodes[0]); @@ -14,6 +18,7 @@ export function initEditor(id: string, plugins?: EditorPlugin[]) { fontSize: '11pt', textColor: '#000000', }, + experimentalFeatures, }; let editor = new Editor(node as HTMLDivElement, options); diff --git a/packages/roosterjs-editor-plugins/test/imageEdit/ResizerTest.ts b/packages/roosterjs-editor-plugins/test/imageEdit/ResizerTest.ts index 508d118c9f79..00ed4a24b49a 100644 --- a/packages/roosterjs-editor-plugins/test/imageEdit/ResizerTest.ts +++ b/packages/roosterjs-editor-plugins/test/imageEdit/ResizerTest.ts @@ -1,4 +1,4 @@ -import DragAndDropContext, { X, Y } from '../../lib/plugins/ImageEdit/types/DragAndDropContext'; +import DragAndDropContext, { DNDDirectionX, DnDDirectionY } from '../../lib/plugins/ImageEdit/types/DragAndDropContext'; import ImageEditInfo, { ResizeInfo } from '../../lib/plugins/ImageEdit/types/ImageEditInfo'; import { ImageEditOptions } from 'roosterjs-editor-types'; import { Resizer } from '../../lib/plugins/ImageEdit/imageEditors/Resizer'; @@ -12,8 +12,8 @@ describe('Resizer: resize only', () => { const initValue: ResizeInfo = { widthPx: 100, heightPx: 200 }; const mouseEvent: MouseEvent = {} as any; const mouseEventShift: MouseEvent = { shiftKey: true } as any; - const Xs: X[] = ['w', '', 'e']; - const Ys: Y[] = ['n', '', 's']; + const Xs: DNDDirectionX[] = ['w', '', 'e']; + const Ys: DnDDirectionY[] = ['n', '', 's']; function getInitEditInfo(): ImageEditInfo { return { @@ -33,7 +33,7 @@ describe('Resizer: resize only', () => { function runTest( e: MouseEvent, getEditInfo: () => ImageEditInfo, - expectedResult: Record> + expectedResult: Record> ) { const actualResult: { [key: string]: { [key: string]: [number, number] } } = {}; Xs.forEach(x => { @@ -89,9 +89,9 @@ describe('Resizer: resize only', () => { s: [100, 220], }, e: { - n: [90, 180], + n: [120, 240], '': [120, 200], - s: [110, 220], + s: [120, 240], }, }); }); @@ -144,9 +144,9 @@ describe('Resizer: resize only', () => { s: [100, 207], }, e: { - n: [96, 192], + n: [127, 254], '': [127, 200], - s: [103, 207], + s: [127, 254], }, } ); diff --git a/packages/roosterjs-editor-plugins/test/imageEdit/applyChangeTest.ts b/packages/roosterjs-editor-plugins/test/imageEdit/applyChangeTest.ts index c5d8c64620f3..e6a415cc2da9 100644 --- a/packages/roosterjs-editor-plugins/test/imageEdit/applyChangeTest.ts +++ b/packages/roosterjs-editor-plugins/test/imageEdit/applyChangeTest.ts @@ -6,7 +6,7 @@ const IMG_SRC = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAALUlEQVQ4EWNgYGD4T2U8lAz8TyZACzKEl8k0Dz0OhpKBaGGB7hVi+QgvD0oDATe/bqDDw39VAAAAAElFTkSuQmCC'; const WIDTH = 20; const HEIGHT = 10; -const IMAGE_EDIT_EDITINFO_NAME = 'roosterEditInfo'; +const IMAGE_EDIT_EDITINFO_NAME = 'editingInfo'; // Temporarily disable these tests since they fail on Linux/MacOS xdescribe('applyChange', () => { diff --git a/packages/roosterjs-editor-plugins/test/imageEdit/canRegenerateImageTest.ts b/packages/roosterjs-editor-plugins/test/imageEdit/canRegenerateImageTest.ts new file mode 100644 index 000000000000..2b123e6358de --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/imageEdit/canRegenerateImageTest.ts @@ -0,0 +1,36 @@ +import canRegenerateImage from '../../lib/plugins/ImageEdit/api/canRegenerateImage'; + +const IMG_SRC = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAALUlEQVQ4EWNgYGD4T2U8lAz8TyZACzKEl8k0Dz0OhpKBaGGB7hVi+QgvD0oDATe/bqDDw39VAAAAAElFTkSuQmCC'; + +describe('canRegenerateImage', () => { + function runTest(element: HTMLImageElement, canRegenerate: boolean) { + const result = canRegenerateImage(element); + expect(result).toBe(canRegenerate); + } + + it('should not regenerate', () => { + runTest(null!, false); + }); + + it('should regenerate', async () => { + const img = await loadImage(IMG_SRC); + img.width = 100; + img.height = 100; + runTest(img, true); + }); +}); + +function loadImage(src: string): Promise { + return new Promise(resolve => { + const img = document.createElement('img'); + const result = () => { + img.onload = null; + img.onerror = null; + resolve(img); + }; + img.onload = result; + img.onerror = result; + img.src = src; + }); +} diff --git a/packages/roosterjs-editor-plugins/test/imageEdit/cropperTest.ts b/packages/roosterjs-editor-plugins/test/imageEdit/cropperTest.ts new file mode 100644 index 000000000000..d0bf82b848f1 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/imageEdit/cropperTest.ts @@ -0,0 +1,133 @@ +import DragAndDropContext, { + DNDDirectionX, + DnDDirectionY, +} from '../../lib/plugins/ImageEdit/types/DragAndDropContext'; +import ImageEditInfo, { CropInfo } from '../../lib/plugins/ImageEdit/types/ImageEditInfo'; +import { Cropper } from '../../lib/plugins/ImageEdit/imageEditors/Cropper'; +import { ImageEditOptions } from 'roosterjs-editor-types'; + +describe('Cropper: crop only', () => { + const options: ImageEditOptions = { + minWidth: 10, + minHeight: 10, + }; + + const initValue: CropInfo = { + leftPercent: 0, + rightPercent: 0, + topPercent: 0, + bottomPercent: 0, + }; + const mouseEvent: MouseEvent = {} as any; + const Xs: DNDDirectionX[] = ['w', '', 'e']; + const Ys: DnDDirectionY[] = ['n', '', 's']; + + function getInitEditInfo(): ImageEditInfo { + return { + src: '', + naturalWidth: 100, + naturalHeight: 200, + leftPercent: 0, + topPercent: 0, + rightPercent: 0, + bottomPercent: 0, + widthPx: 100, + heightPx: 200, + angleRad: 0, + }; + } + + function runTest( + e: MouseEvent, + getEditInfo: () => ImageEditInfo, + expectedResult: { width: number; height: number } + ) { + let actualResult: { width: number; height: number } = { width: 0, height: 0 }; + Xs.forEach(x => { + Ys.forEach(y => { + const editInfo = getEditInfo(); + const context: DragAndDropContext = { + elementClass: '', + x, + y, + editInfo, + options, + }; + + Cropper.onDragging(context, e, initValue, 20, 20); + actualResult = { + width: Math.floor(editInfo.widthPx), + height: Math.floor(editInfo.heightPx), + }; + }); + }); + + expect(actualResult).toEqual(expectedResult); + } + + it('Crop right', () => { + runTest( + mouseEvent, + () => { + const editInfo = getInitEditInfo(); + editInfo.rightPercent = -0.1; + return editInfo; + }, + { width: 90, height: 200 } + ); + }); + + it('Crop top', () => { + runTest( + mouseEvent, + () => { + const editInfo = getInitEditInfo(); + editInfo.topPercent = 0.5; + return editInfo; + }, + { width: 100, height: 200 } + ); + }); + + it('Crop top and bottom', () => { + runTest( + mouseEvent, + () => { + const editInfo = getInitEditInfo(); + editInfo.topPercent = 0.1; + editInfo.bottomPercent = -0.1; + return editInfo; + }, + { width: 100, height: 180 } + ); + }); + + it('Crop left and right', () => { + runTest( + mouseEvent, + () => { + const editInfo = getInitEditInfo(); + editInfo.leftPercent = 0.1; + editInfo.rightPercent = -0.1; + return editInfo; + }, + { width: 90, height: 200 } + ); + }); + + it('Crop all', () => { + runTest( + mouseEvent, + () => { + const editInfo = getInitEditInfo(); + + editInfo.leftPercent = 0.1; + editInfo.rightPercent = -0.1; + editInfo.topPercent = 0.1; + editInfo.bottomPercent = -0.1; + return editInfo; + }, + { width: 90, height: 180 } + ); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/imageEdit/getEditInfoFromImageTest.ts b/packages/roosterjs-editor-plugins/test/imageEdit/getEditInfoFromImageTest.ts index 5c52ee156042..31dfbbacd6a6 100644 --- a/packages/roosterjs-editor-plugins/test/imageEdit/getEditInfoFromImageTest.ts +++ b/packages/roosterjs-editor-plugins/test/imageEdit/getEditInfoFromImageTest.ts @@ -5,7 +5,7 @@ const IMG_SRC = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAALUlEQVQ4EWNgYGD4T2U8lAz8TyZACzKEl8k0Dz0OhpKBaGGB7hVi+QgvD0oDATe/bqDDw39VAAAAAElFTkSuQmCC'; const WIDTH = 20; const HEIGHT = 10; -const IMAGE_EDIT_EDITINFO_NAME = 'roosterEditInfo'; +const IMAGE_EDIT_EDITINFO_NAME = 'editingInfo'; describe('getEditInfoFromImage', () => { it('getEditInfoFromImage() returns the edit info of the empty image', () => { diff --git a/packages/roosterjs-editor-plugins/test/imageEdit/getLastZIndexTest.ts b/packages/roosterjs-editor-plugins/test/imageEdit/getLastZIndexTest.ts new file mode 100644 index 000000000000..a477a4815c1f --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/imageEdit/getLastZIndexTest.ts @@ -0,0 +1,36 @@ +import getLatestZIndex from '../../lib/plugins/ImageEdit/editInfoUtils/getLastZIndex'; + +describe('getLatestZIndex', () => { + function runTest(element: HTMLElement, expected: number) { + const result = getLatestZIndex(element); + expect(result).toBe(expected); + } + + it('should return parentNode zIndex', () => { + const div = document.createElement('div'); + div.style.zIndex = '20'; + const span = document.createElement('span'); + span.style.zIndex = '10'; + div.appendChild(span); + runTest(span, 20); + }); + + it('should return child zIndex', () => { + const div = document.createElement('div'); + const span = document.createElement('span'); + span.style.zIndex = '30'; + div.appendChild(span); + runTest(span, 30); + }); + + it('should return middle element zIndex', () => { + const div = document.createElement('div'); + const spanParent = document.createElement('div'); + spanParent.style.zIndex = '30'; + div.appendChild(spanParent); + const span = document.createElement('span'); + span.style.zIndex = '20'; + spanParent.appendChild(span); + runTest(span, 30); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/imageEdit/getTargetByPercentageTest.ts b/packages/roosterjs-editor-plugins/test/imageEdit/getTargetByPercentageTest.ts new file mode 100644 index 000000000000..b2b71e5b8e42 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/imageEdit/getTargetByPercentageTest.ts @@ -0,0 +1,47 @@ +import getTargetSizeByPercentage from '../../lib/plugins/ImageEdit/editInfoUtils/getTargetSizeByPercentage'; +import ImageEditInfo from '../../lib/plugins/ImageEdit/types/ImageEditInfo'; + +describe('getTargetSizeByPercentage', () => { + function runTest( + editInfo: ImageEditInfo, + percentage: number, + expectWidth: number, + expectHeight: number + ) { + const result = getTargetSizeByPercentage(editInfo, percentage); + expect(result.width).toBe(expectWidth); + expect(result.height).toBe(expectHeight); + } + + it('should return 50%', () => { + const editInfo: ImageEditInfo = { + naturalWidth: 100, + naturalHeight: 100, + leftPercent: 0, + topPercent: 0, + rightPercent: 0, + bottomPercent: 0, + src: 'test', + widthPx: 100, + heightPx: 100, + angleRad: 0, + }; + runTest(editInfo, 0.5, 50, 50); + }); + + it('should return 50% of image that was cropped in half;', () => { + const editInfo: ImageEditInfo = { + naturalWidth: 100, + naturalHeight: 100, + leftPercent: 0, + topPercent: 0, + rightPercent: 0.5, + bottomPercent: 0.5, + src: 'test', + widthPx: 100, + heightPx: 100, + angleRad: 0, + }; + runTest(editInfo, 0.5, 25, 25); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/imageEdit/imageEditTest.ts b/packages/roosterjs-editor-plugins/test/imageEdit/imageEditTest.ts new file mode 100644 index 000000000000..1baef7da348c --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/imageEdit/imageEditTest.ts @@ -0,0 +1,346 @@ +import * as TestHelper from '../TestHelper'; +import ImageEditInfo from '../../lib/plugins/ImageEdit/types/ImageEditInfo'; +import { IEditor, ImageEditOperation, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; +import { ImageEdit } from '../../lib/ImageEdit'; +import { + getEditInfoFromImage, + saveEditInfo, +} from '../../lib/plugins/ImageEdit/editInfoUtils/editInfo'; + +describe('ImageEdit | rotate and flip', () => { + let editor: IEditor; + const TEST_ID = 'imageEditTest'; + let plugin: ImageEdit; + beforeEach(() => { + plugin = new ImageEdit(); + editor = TestHelper.initEditor(TEST_ID, [plugin]); + }); + + afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement.removeChild(element); + } + editor.dispose(); + }); + + function runRotateTest(angle: number, editInfo?: ImageEditInfo) { + const IMG_ID = 'IMAGE_ID'; + const content = ``; + editor.setContent(content); + const image = document.getElementById(IMG_ID) as HTMLImageElement; + if (editInfo) { + saveEditInfo(image, editInfo); + } + plugin.rotateImage(image, angle); + const metadata = getEditInfoFromImage(image); + if (metadata?.angleRad !== undefined) { + expect(metadata.angleRad).toBe((editInfo?.angleRad || 0) + angle); + } + editor.setContent(''); + } + + function runFlipTest( + direction: 'horizontal' | 'vertical', + flippedHorizontal?: boolean, + flippedVertical?: boolean, + editInfo?: ImageEditInfo + ) { + const IMG_ID = 'IMAGE_ID'; + const content = ``; + editor.setContent(content); + const image = document.getElementById(IMG_ID) as HTMLImageElement; + if (editInfo) { + saveEditInfo(image, editInfo); + } + plugin.flipImage(image, direction); + const metadata = getEditInfoFromImage(image); + expect(metadata.flippedHorizontal).toBe(flippedHorizontal); + expect(metadata.flippedVertical).toBe(flippedVertical); + editor.setContent(''); + } + it('rotateImage', () => { + runRotateTest(30); + }); + + it('rotateImage a image that was rotated', () => { + const editInfo = { + src: 'test', + widthPx: 10, + heightPx: 10, + naturalWidth: 10, + naturalHeight: 10, + leftPercent: 0, + rightPercent: 0, + topPercent: 0, + bottomPercent: 0, + angleRad: 20, + }; + runRotateTest(50, editInfo); + }); + + it('flipImage | horizontal', () => { + const editInfo = { + src: 'test', + widthPx: 10, + heightPx: 10, + naturalWidth: 10, + naturalHeight: 10, + leftPercent: 0, + rightPercent: 0, + topPercent: 0, + bottomPercent: 0, + angleRad: 20, + }; + runFlipTest('horizontal', true, undefined, editInfo); + }); + + it('flipImage a vertical Image | horizontal', () => { + const editInfo = { + src: 'test', + widthPx: 10, + heightPx: 10, + naturalWidth: 10, + naturalHeight: 10, + leftPercent: 0, + rightPercent: 0, + topPercent: 0, + bottomPercent: 0, + angleRad: Math.PI / 2, + }; + runFlipTest('horizontal', undefined, true, editInfo); + }); + + it('unflipImage | horizontal', () => { + const editInfo = { + src: 'test', + widthPx: 10, + heightPx: 10, + naturalWidth: 10, + naturalHeight: 10, + leftPercent: 0, + rightPercent: 0, + topPercent: 0, + bottomPercent: 0, + angleRad: 20, + flippedHorizontal: true, + }; + runFlipTest('horizontal', false, undefined, editInfo); + }); + + it('flipImage | vertical', () => { + const editInfo = { + src: 'test', + widthPx: 10, + heightPx: 10, + naturalWidth: 10, + naturalHeight: 10, + leftPercent: 0, + rightPercent: 0, + topPercent: 0, + bottomPercent: 0, + angleRad: 20, + }; + runFlipTest('vertical', undefined, true, editInfo); + }); + + it('flipImage a vertical Image | vertical', () => { + const editInfo = { + src: 'test', + widthPx: 10, + heightPx: 10, + naturalWidth: 10, + naturalHeight: 10, + leftPercent: 0, + rightPercent: 0, + topPercent: 0, + bottomPercent: 0, + angleRad: Math.PI / 2, + }; + runFlipTest('vertical', true, undefined, editInfo); + }); + + it('unflipVertical | vertical', () => { + const editInfo = { + src: 'test', + widthPx: 10, + heightPx: 10, + naturalWidth: 10, + naturalHeight: 10, + leftPercent: 0, + rightPercent: 0, + topPercent: 0, + bottomPercent: 0, + angleRad: 20, + flippedVertical: true, + }; + runFlipTest('vertical', undefined, false, editInfo); + }); + + it('flipVertical a flipped Image', () => { + const editInfo = { + src: 'test', + widthPx: 10, + heightPx: 10, + naturalWidth: 10, + naturalHeight: 10, + leftPercent: 0, + rightPercent: 0, + topPercent: 0, + bottomPercent: 0, + angleRad: 20, + flippedHorizontal: true, + }; + runFlipTest('vertical', true, true, editInfo); + }); + + it('flipHorizontal a flipped Image', () => { + const editInfo = { + src: 'test', + widthPx: 10, + heightPx: 10, + naturalWidth: 10, + naturalHeight: 10, + leftPercent: 0, + rightPercent: 0, + topPercent: 0, + bottomPercent: 0, + angleRad: 20, + flippedVertical: true, + }; + runFlipTest('horizontal', true, true, editInfo); + }); + + it('start image editing', () => { + const IMG_ID = 'IMAGE_ID'; + const content = ``; + editor.setContent(content); + const image = document.getElementById(IMG_ID) as HTMLImageElement; + editor.focus(); + editor.select(image); + plugin.setEditingImage(image, ImageEditOperation.Resize); + expect(editor.getContent()).toBe( + '' + ); + }); +}); + +describe('ImageEdit | plugin events | quitting', () => { + let editor: IEditor; + const TEST_ID = 'imageEditTest'; + let plugin: ImageEdit; + let setEditingImageSpy: jasmine.Spy; + + beforeEach(() => { + plugin = new ImageEdit(); + editor = TestHelper.initEditor(TEST_ID, [plugin]); + setEditingImageSpy = spyOn(plugin, 'setEditingImage'); + }); + + afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement.removeChild(element); + } + editor.dispose(); + }); + + const keyDown = (key: string): PluginEvent => { + return { + eventType: PluginEventType.KeyDown, + rawEvent: { + key: key, + preventDefault: () => {}, + stopPropagation: () => {}, + }, + }; + }; + + const mouseDown = (target: HTMLElement, keyNumber: number) => { + const rect = target.getBoundingClientRect(); + const event = new MouseEvent('mousedown', { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left, + clientY: rect.top, + shiftKey: false, + button: keyNumber, + }); + target.dispatchEvent(event); + }; + + it('image selection quit editing', () => { + const IMG_ID = 'IMAGE_ID'; + const SPAN_ID = 'SPAN_ID'; + const content = ``; + editor.setContent(content); + const image = document.getElementById(IMG_ID) as HTMLImageElement; + editor.focus(); + editor.select(image); + expect(setEditingImageSpy).toHaveBeenCalled(); + expect(setEditingImageSpy).toHaveBeenCalledWith( + image as any, + ImageEditOperation.ResizeAndRotate as any + ); + }); + + it('mousedown quit editing', () => { + const IMG_ID = 'IMAGE_ID'; + const SPAN_ID = 'SPAN_ID'; + const content = ``; + editor.setContent(content); + const image = document.getElementById(IMG_ID) as HTMLImageElement; + const span = document.getElementById(SPAN_ID) as HTMLImageElement; + editor.focus(); + editor.select(image); + mouseDown(span, 0); + expect(setEditingImageSpy).toHaveBeenCalled(); + expect(setEditingImageSpy).toHaveBeenCalledWith(null); + }); + + it('keydown quit editing', () => { + const IMG_ID = 'IMAGE_ID'; + const content = ``; + editor.setContent(content); + const image = document.getElementById(IMG_ID) as HTMLImageElement; + editor.focus(); + editor.select(image); + plugin.onPluginEvent(keyDown('A')); + expect(setEditingImageSpy).toHaveBeenCalled(); + expect(setEditingImageSpy).toHaveBeenCalledWith(null); + }); +}); + +describe('ImageEdit | wrapper', () => { + let editor: IEditor; + const TEST_ID = 'imageEditTest'; + let plugin: ImageEdit; + + beforeEach(() => { + plugin = new ImageEdit(); + editor = TestHelper.initEditor(TEST_ID, [plugin]); + }); + + afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement.removeChild(element); + } + editor.dispose(); + }); + + it('image selection, remove max-width', () => { + const IMG_ID = 'IMAGE_ID'; + const content = ``; + editor.setContent(content); + const image = document.getElementById(IMG_ID) as HTMLImageElement; + image.style.maxWidth = '100%'; + editor.focus(); + editor.select(image); + const imageParent = image.parentElement; + const shadowRoot = imageParent?.shadowRoot; + const imageShadow = shadowRoot?.querySelector('img'); + expect(imageShadow?.style.maxWidth).toBe(''); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/imageEdit/isResizedToTest.ts b/packages/roosterjs-editor-plugins/test/imageEdit/isResizedToTest.ts new file mode 100644 index 000000000000..a878b9d71bb2 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/imageEdit/isResizedToTest.ts @@ -0,0 +1,71 @@ +import ImageEditInfo from '../../lib/plugins/ImageEdit/types/ImageEditInfo'; +import isResizedTo from '../../lib/plugins/ImageEdit/api/isResizedTo'; + +const EDIT_INFO = 'editingInfo'; + +describe('isResizedTo', () => { + function runTest(element: HTMLImageElement, percentage: number, isResized: boolean) { + const result = isResizedTo(element, percentage); + expect(result).toBe(isResized); + } + + it('is Not Resized', () => { + const image = document.createElement('img'); + image.style.width = '100px'; + image.style.height = '100px'; + const editInfo: ImageEditInfo = { + naturalWidth: 100, + naturalHeight: 100, + leftPercent: 0, + topPercent: 0, + rightPercent: 0, + bottomPercent: 0, + src: 'test', + widthPx: 100, + heightPx: 100, + angleRad: 0, + }; + image.dataset[EDIT_INFO] = JSON.stringify(editInfo); + runTest(image, 0.5, false); + }); + + it('is Resized, but not 40%', () => { + const image = document.createElement('img'); + image.style.width = '100px'; + image.style.height = '100px'; + const editInfo: ImageEditInfo = { + naturalWidth: 100, + naturalHeight: 100, + leftPercent: 0, + topPercent: 0, + rightPercent: 0, + bottomPercent: 0, + src: 'test', + widthPx: 50, + heightPx: 50, + angleRad: 0, + }; + image.dataset[EDIT_INFO] = JSON.stringify(editInfo); + runTest(image, 0.4, false); + }); + + it('is Resized to 50%', () => { + const image = document.createElement('img'); + image.style.width = '100px'; + image.style.height = '100px'; + const editInfo: ImageEditInfo = { + naturalWidth: 100, + naturalHeight: 100, + leftPercent: 0, + topPercent: 0, + rightPercent: 0, + bottomPercent: 0, + src: 'test', + widthPx: 50, + heightPx: 50, + angleRad: 0, + }; + image.dataset[EDIT_INFO] = JSON.stringify(editInfo); + runTest(image, 0.5, true); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/imageEdit/resetImageTest.ts b/packages/roosterjs-editor-plugins/test/imageEdit/resetImageTest.ts new file mode 100644 index 000000000000..d68ac4bb72be --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/imageEdit/resetImageTest.ts @@ -0,0 +1,57 @@ +import * as TestHelper from '../TestHelper'; +import ImageEditInfo from '../../lib/plugins/ImageEdit/types/ImageEditInfo'; +import resetImage from '../../lib/plugins/ImageEdit/api/resetImage'; +import { IEditor } from 'roosterjs-editor-types'; +import { ImageEdit } from '../../lib/ImageEdit'; + +const EDIT_INFO = 'editingInfo'; + +describe('resetImage', () => { + let editor: IEditor; + const TEST_ID = 'imageEditTest'; + let plugin: ImageEdit; + + beforeEach(() => { + plugin = new ImageEdit(); + editor = TestHelper.initEditor(TEST_ID, [plugin]); + }); + + afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement.removeChild(element); + } + editor.dispose(); + }); + + function runTest(element: HTMLImageElement) { + resetImage(editor, element); + expect(element.style.width).toBe(''); + expect(element.style.height).toBe(''); + expect(element.style.maxWidth).toBe('100%'); + expect(element.width).toBe(0); + expect(element.height).toBe(0); + expect(element.dataset[EDIT_INFO]).toBeUndefined(); + } + + it('reset Image', () => { + const image = document.createElement('img'); + image.style.width = '100px'; + image.style.height = '100px'; + const editInfo: ImageEditInfo = { + naturalWidth: 100, + naturalHeight: 100, + leftPercent: 0, + topPercent: 0, + rightPercent: 0, + bottomPercent: 0, + src: 'test', + widthPx: 100, + heightPx: 100, + angleRad: 0, + }; + image.dataset[EDIT_INFO] = JSON.stringify(editInfo); + editor.insertNode(image); + runTest(image); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/imageEdit/resizeByPercentageTest.ts b/packages/roosterjs-editor-plugins/test/imageEdit/resizeByPercentageTest.ts new file mode 100644 index 000000000000..a466c5f48f38 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/imageEdit/resizeByPercentageTest.ts @@ -0,0 +1,111 @@ +import * as TestHelper from '../TestHelper'; +import applyChange from '../../lib/plugins/ImageEdit/editInfoUtils/applyChange'; +import getTargetSizeByPercentage from '../../lib/plugins/ImageEdit/editInfoUtils/getTargetSizeByPercentage'; +import ImageEditInfo from '../../lib/plugins/ImageEdit/types/ImageEditInfo'; +import resizeByPercentage from '../../lib/plugins/ImageEdit/api/resizeByPercentage'; +import { IEditor } from 'roosterjs-editor-types'; +import { ImageEdit } from '../../lib/ImageEdit'; + +const IMG_SRC = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAALUlEQVQ4EWNgYGD4T2U8lAz8TyZACzKEl8k0Dz0OhpKBaGGB7hVi+QgvD0oDATe/bqDDw39VAAAAAElFTkSuQmCC'; +const IMG_ID = 'IMAGE_TEST'; +const IMAGE_EDIT_EDITINFO_NAME = 'editingInfo'; +const expectedEditInfo: ImageEditInfo = { + src: IMG_SRC, + widthPx: 100, + heightPx: 100, + naturalWidth: 100, + naturalHeight: 100, + leftPercent: 0, + rightPercent: 0, + topPercent: 0, + bottomPercent: 0, + angleRad: 0, +}; + +describe('resizeByPercentage', () => { + let editor: IEditor; + const TEST_ID = 'imageEditTest'; + let plugin: ImageEdit; + let spyOnIsDisposed: jasmine.Spy; + let spyContains: jasmine.Spy; + beforeEach(() => { + plugin = new ImageEdit(); + editor = TestHelper.initEditor(TEST_ID, [plugin]); + spyOnIsDisposed = spyOn(editor, 'isDisposed'); + spyContains = spyOn(editor, 'contains'); + }); + + afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement.removeChild(element); + } + editor.dispose(); + }); + + function runTest( + image: HTMLImageElement, + percentage: number, + expectedWidth: number, + expectedHeight: number + ) { + spyOnIsDisposed.and.returnValue(false); + spyContains.and.returnValue(true); + spyOn(editor, 'addUndoSnapshot').and.callFake(() => { + const targetSize = getTargetSizeByPercentage(expectedEditInfo, percentage); + expectedEditInfo.widthPx = Math.max(10, targetSize.width); + expectedEditInfo.heightPx = Math.max(10, targetSize.height); + applyChange(editor, image, expectedEditInfo, '', true); + }); + resizeByPercentage(editor, image, percentage, 10, 10); + const img = document.getElementById(IMG_ID) as HTMLImageElement; + editor.select(img); + expect(img.width).toBe(expectedWidth); + expect(img.height).toBe(expectedHeight); + } + + it('resize image by 1', () => { + const imgString = createImage(); + editor.setContent(imgString); + const img = document.getElementById(IMG_ID) as HTMLImageElement; + img.width = 100; + img.height = 100; + img.dataset[IMAGE_EDIT_EDITINFO_NAME] = JSON.stringify(expectedEditInfo); + runTest(img, 1, 100, 100); + }); + + it('resize image by 0.5', () => { + const imgString = createImage(); + editor.setContent(imgString); + const img = document.getElementById(IMG_ID) as HTMLImageElement; + img.width = 100; + img.height = 100; + img.dataset[IMAGE_EDIT_EDITINFO_NAME] = JSON.stringify(expectedEditInfo); + runTest(img, 0.5, 50, 50); + }); + + it('resize image by 0.25', () => { + const imgString = createImage(); + editor.setContent(imgString); + const img = document.getElementById(IMG_ID) as HTMLImageElement; + img.width = 100; + img.height = 100; + img.dataset[IMAGE_EDIT_EDITINFO_NAME] = JSON.stringify(expectedEditInfo); + runTest(img, 0.25, 25, 25); + }); + + it('resize image by 0.05 - test minimum width', () => { + const imgString = createImage(); + editor.setContent(imgString); + const img = document.getElementById(IMG_ID) as HTMLImageElement; + img.width = 100; + img.height = 100; + img.dataset[IMAGE_EDIT_EDITINFO_NAME] = JSON.stringify(expectedEditInfo); + runTest(img, 0.05, 10, 10); + }); +}); + +function createImage() { + return ``; +} diff --git a/packages/roosterjs-editor-plugins/test/imageEdit/rotatorTest.ts b/packages/roosterjs-editor-plugins/test/imageEdit/rotatorTest.ts new file mode 100644 index 000000000000..9f61831d8458 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/imageEdit/rotatorTest.ts @@ -0,0 +1,259 @@ +import * as TestHelper from '../../../roosterjs-editor-api/test/TestHelper'; +import createElement from '../../../roosterjs-editor-dom/lib/utils/createElement'; +import DragAndDropContext, { + DNDDirectionX, + DnDDirectionY, +} from '../../lib/plugins/ImageEdit/types/DragAndDropContext'; +import ImageEditInfo, { RotateInfo } from '../../lib/plugins/ImageEdit/types/ImageEditInfo'; +import ImageHtmlOptions from '../../lib/plugins/ImageEdit/types/ImageHtmlOptions'; +import { + getRotateHTML, + Rotator, + updateRotateHandlePosition, +} from '../../lib/plugins/ImageEdit/imageEditors/Rotator'; +import { IEditor, ImageEditOperation, ImageEditOptions, Rect } from 'roosterjs-editor-types'; +import { ImageEdit } from '../../lib/ImageEdit'; + +const ROTATE_SIZE = 32; +const ROTATE_GAP = 15; +const DEG_PER_RAD = 180 / Math.PI; +const DEFAULT_ROTATE_HANDLE_HEIGHT = ROTATE_SIZE / 2 + ROTATE_GAP; + +describe('Rotate: rotate only', () => { + const options: ImageEditOptions = { + minRotateDeg: 10, + }; + + const initValue: RotateInfo = { angleRad: 0 }; + const mouseEvent: MouseEvent = {} as any; + const mouseEventAltKey: MouseEvent = { altkey: true } as any; + const Xs: DNDDirectionX[] = ['w', '', 'e']; + const Ys: DnDDirectionY[] = ['n', '', 's']; + + function getInitEditInfo(): ImageEditInfo { + return { + src: '', + naturalWidth: 100, + naturalHeight: 200, + leftPercent: 0, + topPercent: 0, + rightPercent: 0, + bottomPercent: 0, + widthPx: 100, + heightPx: 200, + angleRad: 0, + }; + } + + function runTest(e: MouseEvent, getEditInfo: () => ImageEditInfo, expectedResult: number) { + let angle = 0; + Xs.forEach(x => { + Ys.forEach(y => { + const editInfo = getEditInfo(); + const context: DragAndDropContext = { + elementClass: '', + x, + y, + editInfo, + options, + }; + Rotator.onDragging(context, e, initValue, 20, 20); + angle = editInfo.angleRad; + }); + }); + + expect(angle).toEqual(expectedResult); + } + + it('Rotate alt key', () => { + runTest( + mouseEventAltKey, + () => { + const editInfo = getInitEditInfo(); + editInfo.heightPx = 100; + return editInfo; + }, + calculateAngle(100, mouseEventAltKey) + ); + }); + + it('Rotate no alt key', () => { + runTest( + mouseEvent, + () => { + const editInfo = getInitEditInfo(); + editInfo.heightPx = 180; + return editInfo; + }, + calculateAngle(180, mouseEvent) + ); + }); +}); + +describe('updateRotateHandlePosition', () => { + let editor: IEditor; + const TEST_ID = 'imageEditTest'; + let plugin: ImageEdit; + let editorGetVisibleViewport: any; + beforeEach(() => { + plugin = new ImageEdit(); + editor = TestHelper.initEditor(TEST_ID, [plugin]); + editorGetVisibleViewport = spyOn(editor, 'getVisibleViewport'); + }); + + afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement.removeChild(element); + } + editor.dispose(); + }); + const options: ImageHtmlOptions = { + borderColor: 'blue', + rotateHandleBackColor: 'blue', + rotateIconHTML: '', + isSmallImage: false, + }; + + function runTest( + rotatePosition: DOMRect, + rotateCenterTop: string, + rotateCenterHeight: string, + rotateHandleTop: string + ) { + const IMG_ID = 'IMAGE_ID'; + const content = ``; + editor.setContent(content); + const image = document.getElementById(IMG_ID) as HTMLImageElement; + plugin.setEditingImage(image, ImageEditOperation.Rotate); + const rotate = getRotateHTML(options)[0]; + const rotateHTML = createElement(rotate, document); + image.parentElement!.appendChild(rotateHTML!); + const rotateCenter = document.getElementsByClassName('r_rotateC')[0] as HTMLElement; + const rotateHandle = document.getElementsByClassName('r_rotateH')[0] as HTMLElement; + spyOn(rotateHandle, 'getBoundingClientRect').and.returnValues(rotatePosition); + const viewport: Rect = { + top: 1, + bottom: 200, + left: 1, + right: 200, + }; + editorGetVisibleViewport.and.returnValue(viewport); + + updateRotateHandlePosition(viewport, rotateCenter, rotateHandle); + + expect(rotateCenter.style.top).toBe(rotateCenterTop); + expect(rotateCenter.style.height).toBe(rotateCenterHeight); + expect(rotateHandle.style.top).toBe(rotateHandleTop); + } + + it('adjust rotate handle - ROTATOR HIDDEN ON TOP', () => { + runTest( + { + top: 1, + bottom: 3, + left: 3, + right: 5, + height: 2, + width: 2, + x: 1, + y: 3, + toJSON: () => {}, + }, + '0px', + '0px', + '0px' + ); + }); + + it('adjust rotate handle - ROTATOR NOT HIDDEN', () => { + runTest( + { + top: 2, + bottom: 3, + left: 3, + right: 5, + height: 2, + width: 2, + x: 1, + y: 3, + toJSON: () => {}, + }, + '-15px', + '15px', + '-32px' + ); + }); + + it('adjust rotate handle - ROTATOR HIDDEN ON LEFT', () => { + runTest( + { + top: 2, + bottom: 3, + left: 1, + right: 5, + height: 2, + width: 2, + x: 1, + y: 3, + toJSON: () => {}, + }, + '0px', + '0px', + '0px' + ); + }); + + it('adjust rotate handle - ROTATOR HIDDEN ON BOTTOM', () => { + runTest( + { + top: 2, + bottom: 201, + left: 1, + right: 5, + height: 2, + width: 2, + x: 1, + y: 3, + toJSON: () => {}, + }, + '0px', + '0px', + '0px' + ); + }); + + it('adjust rotate handle - ROTATOR HIDDEN ON RIGHT', () => { + runTest( + { + top: 2, + bottom: 3, + left: 1, + right: 201, + height: 2, + width: 2, + x: 1, + y: 3, + toJSON: () => {}, + }, + '0px', + '0px', + '0px' + ); + }); +}); + +function calculateAngle(heightPx: number, mouseInfo: MouseEvent) { + const distance = heightPx / 2 + DEFAULT_ROTATE_HANDLE_HEIGHT; + const newX = distance * Math.sin(0) + 20; + const newY = distance * Math.cos(0) - 20; + let angleInRad = Math.atan2(newX, newY); + + if (!mouseInfo.altKey) { + const angleInDeg = angleInRad * DEG_PER_RAD; + const adjustedAngleInDeg = Math.round(angleInDeg / 10) * 10; + angleInRad = adjustedAngleInDeg / DEG_PER_RAD; + } + + return angleInRad; +} diff --git a/packages/roosterjs-editor-plugins/test/paste/convertPasteContentFromPowerPoint.ts b/packages/roosterjs-editor-plugins/test/paste/convertPasteContentFromPowerPoint.ts new file mode 100644 index 000000000000..9b7b1476898d --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/paste/convertPasteContentFromPowerPoint.ts @@ -0,0 +1,107 @@ +import * as moveChildNodes from 'roosterjs-editor-dom/lib/utils/moveChildNodes'; +import convertPastedContentFromPowerPoint from '../../lib/plugins/Paste/pptConverter/convertPastedContentFromPowerPoint'; +import { BeforePasteEvent, TrustedHTMLHandler } from 'roosterjs-editor-types'; +import { getPasteEvent } from './pasteTestUtils'; + +describe('convertPastedContentFromPowerPoint |', () => { + let ev: BeforePasteEvent; + let trustedHTMLHandlerMock: TrustedHTMLHandler = (html: string) => html; + let image: HTMLImageElement; + let doc: Document; + + beforeEach(() => { + ev = getPasteEvent(); + image = document.createElement('img'); + spyOn(moveChildNodes, 'default'); + spyOn(window, 'DOMParser').and.returnValue({ + parseFromString(string: string, type: DOMParserSupportedType) { + doc = (document.createDocumentFragment()); + doc.append(image); + return doc; + }, + }); + }); + + afterEach(() => { + if (image) { + image.parentElement?.removeChild(image); + } + }); + + it('Execute, Html✅, Text❎, Image✅', () => { + ev.clipboardData.html = ''; + ev.clipboardData.text = ''; + ev.clipboardData.image = {}; + + convertPastedContentFromPowerPoint(ev, trustedHTMLHandlerMock); + + expect(window.DOMParser).toHaveBeenCalled(); + expect(moveChildNodes.default).toHaveBeenCalledWith(ev.fragment, doc.body); + }); + + it('Dont Execute, Html✅, Text✅, Image✅', () => { + ev.clipboardData.html = 'img'; + ev.clipboardData.text = 'text'; + ev.clipboardData.image = {}; + + convertPastedContentFromPowerPoint(ev, trustedHTMLHandlerMock); + + expect(window.DOMParser).not.toHaveBeenCalled(); + expect(moveChildNodes.default).not.toHaveBeenCalled(); + }); + + it('Dont Execute, Html❎, Text❎, Image✅', () => { + ev.clipboardData.html = undefined; + ev.clipboardData.text = ''; + ev.clipboardData.image = {}; + + convertPastedContentFromPowerPoint(ev, trustedHTMLHandlerMock); + + expect(window.DOMParser).not.toHaveBeenCalled(); + expect(moveChildNodes.default).not.toHaveBeenCalled(); + }); + + it('Dont Execute, Html❎, Text✅, Image✅', () => { + ev.clipboardData.html = undefined; + ev.clipboardData.text = 'Test'; + ev.clipboardData.image = {}; + + convertPastedContentFromPowerPoint(ev, trustedHTMLHandlerMock); + + expect(window.DOMParser).not.toHaveBeenCalled(); + expect(moveChildNodes.default).not.toHaveBeenCalled(); + }); + + it('Dont Execute, Html✅, Text❎, Image❎', () => { + ev.clipboardData.html = 'Test'; + ev.clipboardData.text = ''; + ev.clipboardData.image = (null); + + convertPastedContentFromPowerPoint(ev, trustedHTMLHandlerMock); + + expect(window.DOMParser).not.toHaveBeenCalled(); + expect(moveChildNodes.default).not.toHaveBeenCalled(); + }); + + it('Dont Execute, Html❎, Text❎, Image❎', () => { + ev.clipboardData.html = undefined; + ev.clipboardData.text = ''; + ev.clipboardData.image = (null); + + convertPastedContentFromPowerPoint(ev, trustedHTMLHandlerMock); + + expect(window.DOMParser).not.toHaveBeenCalled(); + expect(moveChildNodes.default).not.toHaveBeenCalled(); + }); + + it('Dont Execute, Html❎, Text✅, Image❎', () => { + ev.clipboardData.html = undefined; + ev.clipboardData.text = 'text'; + ev.clipboardData.image = (null); + + convertPastedContentFromPowerPoint(ev, trustedHTMLHandlerMock); + + expect(window.DOMParser).not.toHaveBeenCalled(); + expect(moveChildNodes.default).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/paste/convertSingleImageTests.ts b/packages/roosterjs-editor-plugins/test/paste/convertSingleImageTests.ts new file mode 100644 index 000000000000..ca8debcf4bff --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/paste/convertSingleImageTests.ts @@ -0,0 +1,70 @@ +import * as moveChildNodes from 'roosterjs-editor-dom/lib/utils/moveChildNodes'; +import convertPasteContentForSingleImage from '../../lib/plugins/Paste/imageConverter/convertPasteContentForSingleImage'; +import { BeforePasteEvent, TrustedHTMLHandler } from 'roosterjs-editor-types'; +import { getPasteEvent } from './pasteTestUtils'; + +describe('convertPasteContentForSingleImage |', () => { + let ev: BeforePasteEvent; + let trustedHTMLHandlerMock: TrustedHTMLHandler = (html: string) => html; + let image: HTMLImageElement; + let doc: Document; + + beforeEach(() => { + ev = getPasteEvent(); + image = document.createElement('img'); + spyOn(moveChildNodes, 'default'); + spyOn(window, 'DOMParser').and.returnValue({ + parseFromString(string: string, type: DOMParserSupportedType) { + doc = (document.createDocumentFragment()); + doc.append(image); + return doc; + }, + }); + }); + + afterEach(() => { + if (image) { + image.parentElement?.removeChild(image); + } + }); + + it('Execute, HTML✅, File✅', () => { + ev.clipboardData.html = ''; + ev.clipboardData.image = {}; + + convertPasteContentForSingleImage(ev, trustedHTMLHandlerMock); + + expect(window.DOMParser).toHaveBeenCalled(); + expect(moveChildNodes.default).toHaveBeenCalledWith(ev.fragment, doc.body); + }); + + it('Dont execute, HTML❎, File✅', () => { + ev.clipboardData.html = undefined; + ev.clipboardData.image = {}; + + convertPasteContentForSingleImage(ev, trustedHTMLHandlerMock); + + expect(window.DOMParser).not.toHaveBeenCalled(); + expect(moveChildNodes.default).not.toHaveBeenCalled(); + }); + + it('Dont execute, HTML❎, File❎', () => { + ev.clipboardData.html = undefined; + ev.clipboardData.image = (null); + + convertPasteContentForSingleImage(ev, trustedHTMLHandlerMock); + + expect(window.DOMParser).not.toHaveBeenCalled(); + expect(moveChildNodes.default).not.toHaveBeenCalled(); + }); + + it('Dont execute, HTML✅, Image❎', () => { + ev.clipboardData.html = ''; + ev.clipboardData.image = (null); + + convertPasteContentForSingleImage(ev, trustedHTMLHandlerMock); + + expect(window.DOMParser).not.toHaveBeenCalled(); + expect(moveChildNodes.default).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/paste/handleLineMergeTest.ts b/packages/roosterjs-editor-plugins/test/paste/handleLineMergeTest.ts index d5b002ae8536..8217e2277c0e 100644 --- a/packages/roosterjs-editor-plugins/test/paste/handleLineMergeTest.ts +++ b/packages/roosterjs-editor-plugins/test/paste/handleLineMergeTest.ts @@ -128,4 +128,18 @@ describe('handleLineMerge', () => { '
          line1
          line2
          line3
          line4
          ' ); }); + + it('Avoid merge when two pasted lines are going to be merged', () => { + runTest( + '
          asdsadasdsadsa
          asdsad
          ', + 'asdsadasdsadsa
          asdsad' + ); + }); + + it('Do not had BR when pasting two list items.', () => { + runTest( + '
          • asdf
          • asdf 
          ', + '
          • asdf
          • asdf 
          ' + ); + }); }); diff --git a/packages/roosterjs-editor-plugins/test/paste/pasteTest.ts b/packages/roosterjs-editor-plugins/test/paste/pasteTest.ts new file mode 100644 index 000000000000..786470de2683 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/paste/pasteTest.ts @@ -0,0 +1,179 @@ +import * as convertPasteContentForSingleImage from '../../lib/plugins/Paste/imageConverter/convertPasteContentForSingleImage'; +import * as convertPastedContentForLI from '../../lib/plugins/Paste/commonConverter/convertPastedContentForLI'; +import * as convertPastedContentFromExcel from '../../lib/plugins/Paste/excelConverter/convertPastedContentFromExcel'; +import * as convertPastedContentFromPowerPoint from '../../lib/plugins/Paste/pptConverter/convertPastedContentFromPowerPoint'; +import * as convertPastedContentFromWord from '../../lib/plugins/Paste/wordConverter/convertPastedContentFromWord'; +import * as getPasteSource from 'roosterjs-editor-dom/lib/pasteSourceValidations/getPasteSource'; +import * as handleLineMerge from '../../lib/plugins/Paste/lineMerge/handleLineMerge'; +import * as sanitizeHtmlColorsFromPastedContent from '../../lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent'; +import * as wordOnlineFile from '../../lib/plugins/Paste/officeOnlineConverter/convertPastedContentFromWordOnline'; +import { Editor } from 'roosterjs-editor-core'; +import { getPasteEvent, getWacElement } from './pasteTestUtils'; +import { KnownPasteSourceType } from 'roosterjs-editor-types'; +import { Paste } from '../../lib/Paste'; +import { + BeforePasteEvent, + EditorOptions, + IEditor, + TrustedHTMLHandler, +} from 'roosterjs-editor-types'; + +const GOOGLE_SHEET_NODE_NAME = 'google-sheets-html-origin'; + +describe('Paste Plugin Test', () => { + let editor: IEditor; + let id = 'tableSelectionContainerId'; + let paste: Paste; + let ev: BeforePasteEvent; + + beforeEach(() => { + ev = getPasteEvent(); + let node = document.createElement('div'); + node.id = id; + document.body.insertBefore(node, document.body.childNodes[0]); + paste = new Paste('Test'); + + let options: EditorOptions = { + plugins: [paste], + defaultFormat: { + fontFamily: 'Calibri,Arial,Helvetica,sans-serif', + fontSize: '11pt', + textColor: '#000000', + }, + corePluginOverride: {}, + }; + + editor = new Editor(node as HTMLDivElement, options); + spyOn(sanitizeHtmlColorsFromPastedContent, 'default'); + }); + + it('getName', () => { + expect(paste.getName()).toEqual('Paste'); + }); + + describe('OnPluginEvent |', () => { + afterEach(() => { + const evt = ev; + expect(sanitizeHtmlColorsFromPastedContent.default).toHaveBeenCalledWith( + evt.sanitizingOption + ); + expect(evt.sanitizingOption.unknownTagReplacement).toBe('Test'); + + editor.dispose(); + const div = document.getElementById(id); + if (div) { + div.parentNode?.removeChild(div); + } + }); + + it('Word Content', () => { + spyOn(getPasteSource, 'default').and.returnValue(KnownPasteSourceType.WordDesktop); + spyOn(convertPastedContentFromWord, 'default').and.callFake(() => {}); + + paste.onPluginEvent(ev); + + expect(convertPastedContentFromWord.default).toHaveBeenCalled(); + expect(convertPastedContentFromWord.default).toHaveBeenCalledWith(ev); + }); + + it('Excel Content', () => { + const mockHandler = {}; + + spyOn(getPasteSource, 'default').and.returnValue(KnownPasteSourceType.ExcelDesktop); + spyOn(convertPastedContentFromExcel, 'default').and.callFake(() => {}); + spyOn(editor, 'getTrustedHTMLHandler').and.returnValue(mockHandler); + + paste.onPluginEvent(ev); + + expect(convertPastedContentFromExcel.default).toHaveBeenCalled(); + expect(convertPastedContentFromExcel.default).toHaveBeenCalledWith( + ev, + mockHandler + ); + }); + + it('PowerPoint Content', () => { + const mockHandler = {}; + + spyOn(getPasteSource, 'default').and.returnValue( + KnownPasteSourceType.PowerPointDesktop + ); + spyOn(editor, 'getTrustedHTMLHandler').and.returnValue(mockHandler); + spyOn(convertPastedContentFromPowerPoint, 'default'); + + paste.onPluginEvent(ev); + + expect(convertPastedContentFromPowerPoint.default).toHaveBeenCalled(); + expect(convertPastedContentFromPowerPoint.default).toHaveBeenCalledWith( + ev, + mockHandler + ); + }); + + it('Document with WAC elements no list', () => { + const tempEl = getWacElement(); + tempEl.style.display = 'inherit'; + tempEl.style.margin = '0px'; + ev.fragment.append(tempEl); + + spyOn(getPasteSource, 'default').and.returnValue(KnownPasteSourceType.WacComponents); + spyOn(wordOnlineFile, 'isWordOnlineWithList').and.returnValue(false); + + paste.onPluginEvent(ev); + + expect(tempEl?.style.display).toBe(''); + expect(tempEl?.style.margin).toBe(''); + }); + + it('Document with WAC elements with list', () => { + spyOn(getPasteSource, 'default').and.returnValue(KnownPasteSourceType.WacComponents); + spyOn(wordOnlineFile, 'isWordOnlineWithList').and.returnValue(true); + spyOn(wordOnlineFile, 'default').and.callFake(() => {}); + + paste.onPluginEvent(ev); + + expect(wordOnlineFile.default).toHaveBeenCalled(); + expect(wordOnlineFile.default).toHaveBeenCalledWith((ev).fragment); + }); + + it('Document from google sheets', () => { + spyOn(getPasteSource, 'default').and.returnValue(KnownPasteSourceType.GoogleSheets); + + paste.onPluginEvent(ev); + + expect( + (ev).sanitizingOption.additionalTagReplacements[ + GOOGLE_SHEET_NODE_NAME + ] + ).toBe('*'); + }); + + it('Convert SIngle Image', () => { + const mockHandler = {}; + spyOn(editor, 'getTrustedHTMLHandler').and.returnValue(mockHandler); + spyOn(getPasteSource, 'default').and.returnValue(KnownPasteSourceType.SingleImage); + spyOn(convertPasteContentForSingleImage, 'default'); + + paste.onPluginEvent(ev); + + expect(convertPasteContentForSingleImage.default).toHaveBeenCalled(); + expect(convertPasteContentForSingleImage.default).toHaveBeenCalledWith( + ev, + mockHandler + ); + }); + + it('Any doc', () => { + spyOn(getPasteSource, 'default').and.returnValue(KnownPasteSourceType.Default); + spyOn(convertPastedContentForLI, 'default'); + spyOn(handleLineMerge, 'default'); + + paste.onPluginEvent(ev); + + expect(convertPastedContentForLI.default).toHaveBeenCalledWith( + (ev).fragment + ); + expect(handleLineMerge.default).toHaveBeenCalledWith((ev).fragment); + }); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/paste/pasteTestUtils.ts b/packages/roosterjs-editor-plugins/test/paste/pasteTestUtils.ts new file mode 100644 index 000000000000..711792a52b03 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/paste/pasteTestUtils.ts @@ -0,0 +1,30 @@ +import { createDefaultHtmlSanitizerOptions } from 'roosterjs-editor-dom'; +import { + BeforePasteEvent, + ClipboardData, + PasteType, + PluginEventType, +} from 'roosterjs-editor-types'; + +export const EXCEL_ATTRIBUTE_VALUE = 'urn:schemas-microsoft-com:office:excel'; +export const POWERPOINT_ATTRIBUTE_VALUE = 'PowerPoint.Slide'; +export const WORD_ATTRIBUTE_VALUE = 'urn:schemas-microsoft-com:office:word'; + +export const getPasteEvent = (): BeforePasteEvent => { + return { + eventType: PluginEventType.BeforePaste, + clipboardData: {}, + fragment: document.createDocumentFragment(), + sanitizingOption: createDefaultHtmlSanitizerOptions(), + htmlBefore: '', + htmlAfter: '', + htmlAttributes: {}, + pasteType: PasteType.Normal, + }; +}; + +export const getWacElement = (): HTMLElement => { + const element = document.createElement('span'); + element.classList.add('WACImageContainer'); + return element; +}; diff --git a/packages/roosterjs-editor-plugins/test/paste/sanitizeHtmlColorsFromPastedContentTest.ts b/packages/roosterjs-editor-plugins/test/paste/sanitizeHtmlColorsFromPastedContentTest.ts new file mode 100644 index 000000000000..b123c36566b5 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/paste/sanitizeHtmlColorsFromPastedContentTest.ts @@ -0,0 +1,90 @@ +import sanitizeHtmlColorsFromPastedContent from '../../lib/plugins/Paste/sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent'; +import { HtmlSanitizer } from 'roosterjs-editor-dom'; +import { + BeforePasteEvent, + SanitizeHtmlOptions, + PluginEventType, + ClipboardData, +} from 'roosterjs-editor-types'; + +describe('sanitizeHtmlColorsFromPastedContent', () => { + function callSanitizer(fragment: DocumentFragment, sanitizingOption: SanitizeHtmlOptions) { + const sanitizer = new HtmlSanitizer(sanitizingOption); + sanitizer.convertGlobalCssToInlineCss(fragment); + sanitizer.sanitize(fragment); + } + + function runTest(source: string, expected: string) { + const doc = new DOMParser().parseFromString(source, 'text/html'); + const fragment = doc.createDocumentFragment(); + while (doc.body.firstChild) { + fragment.appendChild(doc.body.firstChild); + } + + const event = createBeforePasteEventMock(fragment); + sanitizeHtmlColorsFromPastedContent(event.sanitizingOption); + callSanitizer(fragment, event.sanitizingOption); + + while (fragment.firstChild) { + doc.body.appendChild(fragment.firstChild); + } + + expect(doc.body.innerHTML).toBe(expected); + } + + it('sanitize on a div', () => { + runTest('
          ', '
          '); + }); + + it('sanitize on a div', () => { + runTest( + '
          ', + '
          ' + ); + }); + + it('sanitize on a p', () => { + runTest( + '

          ', + '

          ' + ); + }); + + it('sanitize on nested elements', () => { + runTest( + '

          ', + '

          ' + ); + }); + + it('sanitize on nested elements with background color', () => { + runTest( + '

          ', + '

          ' + ); + }); +}); + +function createBeforePasteEventMock(fragment: DocumentFragment) { + return ({ + eventType: PluginEventType.BeforePaste, + clipboardData: {}, + fragment: fragment, + sanitizingOption: { + elementCallbacks: {}, + attributeCallbacks: {}, + cssStyleCallbacks: {}, + additionalTagReplacements: {}, + additionalAllowedAttributes: [], + additionalAllowedCssClasses: [], + additionalDefaultStyleValues: {}, + additionalGlobalStyleNodes: [], + additionalPredefinedCssForElement: {}, + preserveHtmlComments: false, + unknownTagReplacement: null, + }, + htmlBefore: '', + htmlAfter: '', + htmlAttributes: {}, + } as unknown) as BeforePasteEvent; +} diff --git a/packages/roosterjs-editor-plugins/test/paste/sanitizeLinksTest.ts b/packages/roosterjs-editor-plugins/test/paste/sanitizeLinksTest.ts new file mode 100644 index 000000000000..64685badf270 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/paste/sanitizeLinksTest.ts @@ -0,0 +1,94 @@ +import sanitizeLinks from '../../lib/plugins/Paste/sanitizeLinks/sanitizeLinks'; +import { HtmlSanitizer } from 'roosterjs-editor-dom'; +import { + BeforePasteEvent, + SanitizeHtmlOptions, + PluginEventType, + ClipboardData, +} from 'roosterjs-editor-types'; + +describe('sanitizeLinks', () => { + function callSanitizer(fragment: DocumentFragment, sanitizingOption: SanitizeHtmlOptions) { + const sanitizer = new HtmlSanitizer(sanitizingOption); + sanitizer.convertGlobalCssToInlineCss(fragment); + sanitizer.sanitize(fragment); + } + + function runTest(source: string, expected: string) { + const doc = new DOMParser().parseFromString(source, 'text/html'); + const fragment = doc.createDocumentFragment(); + while (doc.body.firstChild) { + fragment.appendChild(doc.body.firstChild); + } + + const event = createBeforePasteEventMock(fragment); + sanitizeLinks(event.sanitizingOption); + callSanitizer(fragment, event.sanitizingOption); + + while (fragment.firstChild) { + doc.body.appendChild(fragment.firstChild); + } + + expect(doc.body.innerHTML).toBe(expected); + } + + it('sanitize anchor', () => { + runTest('', ''); + }); + + it('not sanitize anchor', () => { + runTest( + '', + '' + ); + }); + + it('sanitize div', () => { + runTest('
          ', '
          '); + }); + + it('not sanitize div', () => { + runTest( + '
          ', + '
          ' + ); + }); + + it('not sanitize onenote link', () => { + runTest( + '
          ', + '
          ' + ); + }); + + it('not sanitize mailto link', () => { + runTest( + '
          ', + '
          ' + ); + }); +}); + +function createBeforePasteEventMock(fragment: DocumentFragment) { + return ({ + eventType: PluginEventType.BeforePaste, + clipboardData: {}, + fragment: fragment, + sanitizingOption: { + elementCallbacks: {}, + attributeCallbacks: {}, + cssStyleCallbacks: {}, + additionalTagReplacements: {}, + additionalAllowedAttributes: [], + additionalAllowedCssClasses: [], + additionalDefaultStyleValues: {}, + additionalGlobalStyleNodes: [], + additionalPredefinedCssForElement: {}, + preserveHtmlComments: false, + unknownTagReplacement: null, + }, + htmlBefore: '', + htmlAfter: '', + htmlAttributes: {}, + } as unknown) as BeforePasteEvent; +} diff --git a/packages/roosterjs-editor-plugins/test/paste/word/CustomDataTests.ts b/packages/roosterjs-editor-plugins/test/paste/word/CustomDataTests.ts new file mode 100644 index 000000000000..2202b0274dec --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/paste/word/CustomDataTests.ts @@ -0,0 +1,41 @@ +import WordCustomData, { + createCustomData, + getObject, + setObject, +} from '../../../lib/plugins/Paste/wordConverter/WordCustomData'; + +const TEST_KEY = 'Test'; +const TEST_VALUE = 'VALUE'; +const NODE_ID_ATTRIBUTE_NAME = 'NodeId'; + +describe('Word Custom Data | ', () => { + let customData: WordCustomData; + beforeEach(() => { + customData = createCustomData(); + }); + + it('set And get Object', () => { + const el = document.createElement('div'); + + setObject(customData, el, TEST_KEY, TEST_VALUE); + const val = getObject(customData, el, TEST_KEY); + + expect(customData.nextNodeId).toEqual(2); + expect(el.getAttribute(NODE_ID_ATTRIBUTE_NAME)).toEqual('1'); + expect(val).toEqual(TEST_VALUE); + }); + + it('getObject === undefined', () => { + const el = document.createElement('div'); + const val = getObject(customData, el, TEST_KEY); + + expect(val).toBeUndefined(); + }); + + it('getObject === null', () => { + const el = document.createTextNode('div'); + const val = getObject(customData, el, TEST_KEY); + + expect(val).toBeNull(); + }); +}); diff --git a/packages/roosterjs-editor-plugins/test/paste/convertPastedContentForLITest.ts b/packages/roosterjs-editor-plugins/test/paste/word/convertPastedContentForLITest.ts similarity index 95% rename from packages/roosterjs-editor-plugins/test/paste/convertPastedContentForLITest.ts rename to packages/roosterjs-editor-plugins/test/paste/word/convertPastedContentForLITest.ts index d9dfdeda172a..d74d7ed9cd24 100644 --- a/packages/roosterjs-editor-plugins/test/paste/convertPastedContentForLITest.ts +++ b/packages/roosterjs-editor-plugins/test/paste/word/convertPastedContentForLITest.ts @@ -1,5 +1,5 @@ import * as DomTestHelper from 'roosterjs-editor-dom/test/DomTestHelper'; -import convertPastedContentForLI from '../../lib/plugins/Paste/commonConverter/convertPastedContentForLI'; +import convertPastedContentForLI from '../../../lib/plugins/Paste/commonConverter/convertPastedContentForLI'; describe('convertPastedContentForLi', () => { function runTest(source: string, expected: string) { diff --git a/packages/roosterjs-editor-plugins/test/paste/word/convertPastedContentFromOfficeOnlineTest.ts b/packages/roosterjs-editor-plugins/test/paste/word/convertPastedContentFromOfficeOnlineTest.ts new file mode 100644 index 000000000000..ddd96def614b --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/paste/word/convertPastedContentFromOfficeOnlineTest.ts @@ -0,0 +1,35 @@ +import convertPastedContentFromOfficeOnline from '../../../lib/plugins/Paste/officeOnlineConverter/convertPastedContentFromOfficeOnline'; +import { createDefaultHtmlSanitizerOptions, toArray } from 'roosterjs-editor-dom'; + +describe('convertPastedContentFromOfficeOnlineTest', () => { + function runTest(html: string, expectedInnerHtml: string) { + const doc = sanitizeContent(html); + + expect(doc.body.innerHTML).toBe(expectedInnerHtml); + } + + it('remove table temp elements', () => { + runTest( + '

          a

          asd

          ', + '

          a

          asd

          ' + ); + }); +}); + +function sanitizeContent(html: string) { + const doc = new DOMParser().parseFromString(html, 'text/html'); + const fragment = doc.createDocumentFragment(); + while (doc.body.firstChild) { + fragment.appendChild(doc.body.firstChild); + } + const opts = createDefaultHtmlSanitizerOptions(); + convertPastedContentFromOfficeOnline(fragment, opts); + + fragment.querySelectorAll('*').forEach(n => { + toArray(n.attributes).forEach(a => n.removeAttribute(a.name)); + }); + while (fragment.firstChild) { + doc.body.appendChild(fragment.firstChild); + } + return doc; +} diff --git a/packages/roosterjs-editor-plugins/test/paste/word/convertPastedContentFromWordTest.ts b/packages/roosterjs-editor-plugins/test/paste/word/convertPastedContentFromWordTest.ts new file mode 100644 index 000000000000..b8831bfb2cae --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/paste/word/convertPastedContentFromWordTest.ts @@ -0,0 +1,183 @@ +import convertPastedContentFromWord from '../../../lib/plugins/Paste/wordConverter/convertPastedContentFromWord'; +import { HtmlSanitizer, moveChildNodes } from 'roosterjs-editor-dom'; +import { + BeforePasteEvent, + SanitizeHtmlOptions, + PluginEventType, + ClipboardData, +} from 'roosterjs-editor-types'; + +describe('convertPastedContentFromWord', () => { + function callSanitizer(fragment: DocumentFragment, sanitizingOption: SanitizeHtmlOptions) { + const sanitizer = new HtmlSanitizer(sanitizingOption); + + sanitizer.convertGlobalCssToInlineCss(fragment); + sanitizer.sanitize(fragment); + } + + function runTest(source: string, expected: string) { + //Arrange + const div = document.createElement('div'); + + //Act + div.innerHTML = source; + const fragment = document.createDocumentFragment(); + moveChildNodes(fragment, div); + const event = createBeforePasteEventMock(fragment); + convertPastedContentFromWord(event); + callSanitizer(fragment, event.sanitizingOption); + moveChildNodes(div, fragment); + document.body.append(div); + + //Assert + expect(div.innerHTML).toBe(expected); + div.parentElement?.removeChild(div); + } + + it('Remove Comment | mso-element:comment-list', () => { + let source = + '
          '; + runTest(source, '
          '); + }); + + it('Remove Comment | #_msocom_', () => { + let source = + '

          [BV11]

          '; + runTest(source, '

          '); + }); + + it('Remove Comment | mso-comment-reference', () => { + let source = + '

          '; + + runTest(source, '

          '); + }); + + it('Remove Comment | mso-comment-continuation, remove style 1', () => { + let source = ''; + runTest(source, ''); + }); + + it('Remove Comment | mso-comment-continuation, remove style 2', () => { + let source = ''; + runTest(source, ''); + }); + + it('Remove Comment | mso-comment-done, remove style', () => { + let source = ''; + runTest(source, ''); + }); + + it('Remove Comment | mso-special-character:comment', () => { + let source = ' '; + runTest(source, ''); + }); + + it('Remove Bottom Margin = 0in | UL', () => { + let source = '
          '; + runTest(source, '
          '); + }); + + it('Do Not Remove Bottom Margin = 1in | UL', () => { + let source = '
          '; + runTest(source, '
          '); + }); + + it('Remove Bottom Margin = 0in | OL', () => { + let source = '
          '; + runTest(source, '
          '); + }); + + it('Remove Margin bottom from List', () => { + let source = + '
          1. 1
            1. 2

          123


          '; + runTest( + source, + '
          1. 1
            1. 2

          123


          ' + ); + }); + + it('Remove Line height less than default', () => { + let source = '

          '; + runTest(source, '

          '); + }); + + it('Remove Line height, not percentage', () => { + let source = '

          '; + runTest(source, source); + }); + + it('Remove Line height, not percentage 2', () => { + let source = '

          '; + runTest(source, source); + }); + it('Remove Line height, percentage greater than default', () => { + let source = '

          '; + runTest(source, source); + }); + + describe('List Convertion Tests | ', () => { + it('List with Headings', () => { + const html = + createListElementFromWord('p', 'test1') + createListElementFromWord('h1', 'test2'); + runTest(html, '
          • test1
          • test2

          '); + }); + + it('List with Headings in sub level 1', () => { + const html = + createListElementFromWord('p', 'test1') + + createListElementFromWord('h1', 'test2', 'l0 level2 lfo1'); + runTest(html, '
          • test1
            • test2

          '); + }); + + it('List with Headings in sub level 2', () => { + const html = + createListElementFromWord('p', 'test1') + + createListElementFromWord('h1', 'test2', 'l0 level3 lfo1'); + runTest(html, '
          • test1
              • test2

          '); + }); + + it('List with Headings in sub level 3', () => { + const html = + createListElementFromWord('p', 'test1') + + createListElementFromWord('h1', 'test2', 'l1 level3 lfo2'); + runTest(html, '
          • test1
              • test2

          '); + }); + }); +}); + +function createBeforePasteEventMock(fragment: DocumentFragment) { + return ({ + eventType: PluginEventType.BeforePaste, + clipboardData: {}, + fragment: fragment, + sanitizingOption: { + elementCallbacks: {}, + attributeCallbacks: {}, + cssStyleCallbacks: {}, + additionalTagReplacements: {}, + additionalAllowedAttributes: [], + additionalAllowedCssClasses: [], + additionalDefaultStyleValues: {}, + additionalGlobalStyleNodes: [], + additionalPredefinedCssForElement: {}, + preserveHtmlComments: false, + unknownTagReplacement: null, + }, + htmlBefore: '', + htmlAfter: '', + htmlAttributes: {}, + } as unknown) as BeforePasteEvent; +} + +function createListElementFromWord( + tag: string, + content: string, + msoList: string = 'l0 level1 lfo1' +) { + return ( + `<${tag} style="text-indent:-.25in;mso-list: ${msoList}" class="MsoListParagraph">·       ' + + `${content}` + ); +} diff --git a/packages/roosterjs-editor-plugins/test/paste/wordOnlineHandlerTest.ts b/packages/roosterjs-editor-plugins/test/paste/word/wordOnlineHandlerTest.ts similarity index 91% rename from packages/roosterjs-editor-plugins/test/paste/wordOnlineHandlerTest.ts rename to packages/roosterjs-editor-plugins/test/paste/word/wordOnlineHandlerTest.ts index d1cccdd8d4db..533faadd61fd 100644 --- a/packages/roosterjs-editor-plugins/test/paste/wordOnlineHandlerTest.ts +++ b/packages/roosterjs-editor-plugins/test/paste/word/wordOnlineHandlerTest.ts @@ -1,18 +1,8 @@ -import convertPastedContentFromWordOnline from '../../lib/plugins/Paste/officeOnlineConverter/convertPastedContentFromWordOnline'; +import convertPastedContentFromWordOnline from '../../../lib/plugins/Paste/officeOnlineConverter/convertPastedContentFromWordOnline'; describe('wordOnlineHandler', () => { function runTest(html: string, expectedInnerHtml: string) { - const doc = new DOMParser().parseFromString(html, 'text/html'); - const fragment = doc.createDocumentFragment(); - while (doc.body.firstChild) { - fragment.appendChild(doc.body.firstChild); - } - - convertPastedContentFromWordOnline(fragment); - - while (fragment.firstChild) { - doc.body.appendChild(fragment.firstChild); - } + const doc = sanitizeContent(html); expect(doc.body.innerHTML).toBe(expectedInnerHtml); } @@ -384,4 +374,56 @@ describe('wordOnlineHandler', () => { ); }); }); + + it('Keep the start property on lists and try to reuse the Word provided marker style', () => { + const doc = sanitizeContent( + '

          Test 

          1. Test 

          • Test 

          Test 

          1. Test 

          • Test 

          ' + ); + + doc.querySelectorAll('ul li').forEach(el => { + const dataLevelText = el.getAttribute('data-leveltext'); + if (dataLevelText) { + expect((el as HTMLElement).style.listStyleType).toContain(dataLevelText); + } + }); + + const orderedLists = doc.querySelectorAll('ol'); + expect(orderedLists.length).toBe(2); + expect(orderedLists[0].start).toBe(1); + expect(orderedLists[1].start).toBe(5); + }); + + it('Keep the start property on lists and remove marker style that is not reusable', () => { + const notUsableMarker = String.fromCharCode(10); + const doc = sanitizeContent( + `

          Test 

          1. Test 

          • Test 

          Test 

          1. Test 

          • Test 

          ` + ); + + doc.querySelectorAll('ul li').forEach(el => { + const dataLevelText = el.getAttribute('data-leveltext'); + if (dataLevelText) { + expect((el as HTMLElement).style.listStyleType).not.toContain(dataLevelText); + } + }); + + const orderedLists = doc.querySelectorAll('ol'); + expect(orderedLists.length).toBe(2); + expect(orderedLists[0].start).toBe(1); + expect(orderedLists[1].start).toBe(2); + }); }); + +function sanitizeContent(html: string) { + const doc = new DOMParser().parseFromString(html, 'text/html'); + const fragment = doc.createDocumentFragment(); + while (doc.body.firstChild) { + fragment.appendChild(doc.body.firstChild); + } + + convertPastedContentFromWordOnline(fragment); + + while (fragment.firstChild) { + doc.body.appendChild(fragment.firstChild); + } + return doc; +} diff --git a/packages/roosterjs-editor-plugins/test/pluginUtils/DragAndDropHelperTest.ts b/packages/roosterjs-editor-plugins/test/pluginUtils/DragAndDropHelperTest.ts new file mode 100644 index 000000000000..3efbeec2ff74 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/pluginUtils/DragAndDropHelperTest.ts @@ -0,0 +1,141 @@ +import DragAndDropHelper from '../../lib/pluginUtils/DragAndDropHelper'; + +interface DragAndDropContext { + node: HTMLElement; +} + +interface DragAndDropInitValue { + originalRect: DOMRect; +} + +describe('DragAndDropHelper |', () => { + let id = 'DragAndDropHelperId'; + let dndHelper: DragAndDropHelper; + + beforeEach(() => { + //Empty Div for dragging + let node = document.createElement('div'); + node.id = id; + //Start as black square + node.style.width = '50px'; + node.style.height = '50px'; + node.style.backgroundColor = 'black'; + node.style.position = 'fixed'; + node.style.top = '0px'; + node.style.left = '0px'; + + //Put node on top of body + document.body.insertBefore(node, document.body.childNodes[0]); + }); + + //Creates the DragAndDropHelper for testing + function createDnD(node: HTMLElement, mobile: boolean) { + dndHelper = new DragAndDropHelper( + node, + { node }, + () => {}, + { + onDragEnd(context: DragAndDropContext) { + //Red indicates dragging stopped + context.node.style.backgroundColor = 'red'; + return true; + }, + onDragStart(context: DragAndDropContext) { + //Green indicates dragging started + context.node.style.backgroundColor = 'green'; + return { originalRect: context.node.getBoundingClientRect() }; + }, + onDragging(context: DragAndDropContext, event: MouseEvent) { + //Yellow indicates dragging is happening + context.node.style.backgroundColor = 'yellow'; + context.node.style.left = event.pageX + 'px'; + context.node.style.top = event.pageY + 'px'; + return true; + }, + }, + 1, + mobile + ); + } + + afterEach(() => { + dndHelper.dispose(); + }); + + it('mouse movement', () => { + // Arrange + const target = document.getElementById(id); + createDnD(target, false); + let targetEnd = target; + targetEnd.style.top = 50 + 'px'; + + // Assert + expect(dndHelper.mouseType).toBe('mouse'); + + // Act + simulateMouseEvent('mousedown', target); + + // Assert + expect(target?.style.backgroundColor).toBe('green'); + + // Act + simulateMouseEvent('mousemove', targetEnd); + + // Assert + expect(target?.style.backgroundColor).toBe('yellow'); + + // Act + simulateMouseEvent('mouseup', targetEnd); + + // Assert + expect(target?.style.backgroundColor).toBe('red'); + }); + + it('touch movement', () => { + // Arrange + const target = document.getElementById(id); + createDnD(target, true); + let targetEnd = target; + targetEnd.style.left = 50 + 'px'; + + // Assert + expect(dndHelper.mouseType).toBe('touch'); + + // Act + simulateTouchEvent('touchstart', target); + + // Assert + expect(target?.style.backgroundColor).toBe('green'); + + // Act + simulateTouchEvent('touchmove', targetEnd); + + // Assert + expect(target?.style.backgroundColor).toBe('yellow'); + + // Act + simulateTouchEvent('touchend', targetEnd); + + // Assert + expect(target?.style.backgroundColor).toBe('red'); + }); +}); + +function simulateMouseEvent(type: string, target: HTMLElement, shiftKey: boolean = false) { + const rect = target.getBoundingClientRect(); + var event = new MouseEvent(type, { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left, + clientY: rect.top, + shiftKey, + }); + target.dispatchEvent(event); +} + +function simulateTouchEvent(type: string, target: HTMLElement) { + var event = (new Event(type) as any) as TouchEvent; + + target.dispatchEvent(event); +} diff --git a/packages/roosterjs-editor-plugins/tsconfig.child.json b/packages/roosterjs-editor-plugins/tsconfig.child.json deleted file mode 100644 index b82cafe96793..000000000000 --- a/packages/roosterjs-editor-plugins/tsconfig.child.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "strict": false - }, - "extends": "../tsconfig.json", - "include": ["./lib/**/*.ts"], - "references": [ - { "path": "../roosterjs-editor-types/tsconfig.child.json" }, - { "path": "../roosterjs-editor-dom/tsconfig.child.json" }, - { "path": "../roosterjs-editor-api/tsconfig.child.json" } - ] -} diff --git a/packages/roosterjs-editor-types-compatible/lib/index.ts b/packages/roosterjs-editor-types-compatible/lib/index.ts new file mode 100644 index 000000000000..01f121e67db0 --- /dev/null +++ b/packages/roosterjs-editor-types-compatible/lib/index.ts @@ -0,0 +1 @@ +export * from 'roosterjs-editor-types/lib/compatibleTypes'; diff --git a/packages/roosterjs-editor-types-compatible/package.json b/packages/roosterjs-editor-types-compatible/package.json new file mode 100644 index 000000000000..46571705c819 --- /dev/null +++ b/packages/roosterjs-editor-types-compatible/package.json @@ -0,0 +1,8 @@ +{ + "name": "roosterjs-editor-types-compatible", + "description": "Type definition for roosterjs with enums that can be compatible with isolatedModules", + "dependencies": { + "roosterjs-editor-types": "" + }, + "main": "./lib/index.ts" +} diff --git a/packages/roosterjs-editor-types/lib/browser/BrowserInfo.ts b/packages/roosterjs-editor-types/lib/browser/BrowserInfo.ts index 0ed3e54f74ec..ac9840b20f6d 100644 --- a/packages/roosterjs-editor-types/lib/browser/BrowserInfo.ts +++ b/packages/roosterjs-editor-types/lib/browser/BrowserInfo.ts @@ -56,4 +56,9 @@ export default interface BrowserInfo { * Whether current OS is Android */ readonly isAndroid?: boolean; + + /** + * Whether current browser is on mobile or a tablet + */ + readonly isMobileOrTablet?: boolean; } diff --git a/packages/roosterjs-editor-types/lib/browser/index.ts b/packages/roosterjs-editor-types/lib/browser/index.ts new file mode 100644 index 000000000000..982b6c15611a --- /dev/null +++ b/packages/roosterjs-editor-types/lib/browser/index.ts @@ -0,0 +1,2 @@ +export { default as BrowserInfo } from './BrowserInfo'; +export { default as EdgeLinkPreview } from './EdgeLinkPreview'; diff --git a/packages/roosterjs-editor-types/lib/compatibleTypes.ts b/packages/roosterjs-editor-types/lib/compatibleTypes.ts new file mode 100644 index 000000000000..ea7c33f0b7cf --- /dev/null +++ b/packages/roosterjs-editor-types/lib/compatibleTypes.ts @@ -0,0 +1,6 @@ +export * from './browser/index'; +export * from './corePluginState/index'; +export * from './compatibleEnum/index'; +export * from './event/index'; +export * from './interface/index'; +export * from './type/index'; diff --git a/packages/roosterjs-editor-types/lib/corePluginState/DOMEventPluginState.ts b/packages/roosterjs-editor-types/lib/corePluginState/DOMEventPluginState.ts index 725245827a1d..dbc727eeabb4 100644 --- a/packages/roosterjs-editor-types/lib/corePluginState/DOMEventPluginState.ts +++ b/packages/roosterjs-editor-types/lib/corePluginState/DOMEventPluginState.ts @@ -1,5 +1,5 @@ import ContextMenuProvider from '../interface/ContextMenuProvider'; -import { TableSelectionRange } from '../interface/SelectionRangeEx'; +import { ImageSelectionRange, TableSelectionRange } from '../interface/SelectionRangeEx'; /** * The state object for DOMEventPlugin @@ -18,12 +18,12 @@ export default interface DOMEventPluginState { /** * Cached selection range */ - selectionRange: Range; + selectionRange: Range | null; /** * Table selection range */ - tableSelectionRange: TableSelectionRange; + tableSelectionRange: TableSelectionRange | null; /** * stop propagation of a printable keyboard event @@ -34,4 +34,9 @@ export default interface DOMEventPluginState { * Context menu providers, that can provide context menu items */ contextMenuProviders: ContextMenuProvider[]; + + /** + * Image selection range + */ + imageSelectionRange: ImageSelectionRange | null; } diff --git a/packages/roosterjs-editor-types/lib/corePluginState/EditPluginState.ts b/packages/roosterjs-editor-types/lib/corePluginState/EditPluginState.ts index 4b42cddbae5d..6a32e941cc63 100644 --- a/packages/roosterjs-editor-types/lib/corePluginState/EditPluginState.ts +++ b/packages/roosterjs-editor-types/lib/corePluginState/EditPluginState.ts @@ -1,4 +1,4 @@ -import { GenericContentEditFeature } from '../interface/IEditor'; +import { GenericContentEditFeature } from '../interface/ContentEditFeature'; import { PluginEvent } from '../event/PluginEvent'; /** diff --git a/packages/roosterjs-editor-types/lib/corePluginState/EntityPluginState.ts b/packages/roosterjs-editor-types/lib/corePluginState/EntityPluginState.ts index 25e21ebdee22..97de02de3a8e 100644 --- a/packages/roosterjs-editor-types/lib/corePluginState/EntityPluginState.ts +++ b/packages/roosterjs-editor-types/lib/corePluginState/EntityPluginState.ts @@ -1,3 +1,5 @@ +import { KnownEntityItem } from '../interface/KnownEntityItem'; + /** * The state object for EntityPlugin */ @@ -9,14 +11,18 @@ export default interface EntityPluginState { clickingPoint?: { pageX: number; pageY: number }; /** + * @deprecated * All known entity elements */ - knownEntityElements: HTMLElement[]; + knownEntityElements?: HTMLElement[]; + + /** + * @deprecated + */ + shadowEntityCache?: Record; /** - * Cache for the hydrated content of shadow DOM entity. - * When set content to replace the whole editor, we will cache the hydrated content - * before it is gone, then after that we can use the cached content to rehydrate entity + * Entities cached for undo snapshot */ - shadowEntityCache: Record; + entityMap: Record; } diff --git a/packages/roosterjs-editor-types/lib/corePluginState/LifecyclePluginState.ts b/packages/roosterjs-editor-types/lib/corePluginState/LifecyclePluginState.ts index fd8b8f13c045..b5e3da079368 100644 --- a/packages/roosterjs-editor-types/lib/corePluginState/LifecyclePluginState.ts +++ b/packages/roosterjs-editor-types/lib/corePluginState/LifecyclePluginState.ts @@ -2,6 +2,7 @@ import CustomData from '../interface/CustomData'; import DefaultFormat from '../interface/DefaultFormat'; import SelectionPath from '../interface/SelectionPath'; import { ExperimentalFeatures } from '../enum/ExperimentalFeatures'; +import type { CompatibleExperimentalFeatures } from '../compatibleEnum/ExperimentalFeatures'; /** * The state object for LifecyclePlugin @@ -15,7 +16,7 @@ export default interface LifecyclePluginState { /** * Default format of this editor */ - defaultFormat: DefaultFormat; + defaultFormat: DefaultFormat | null; /** * Whether editor is in dark mode @@ -30,25 +31,35 @@ export default interface LifecyclePluginState { /** * External content transform function to help do color transform for existing content */ - onExternalContentTransform: (htmlIn: HTMLElement) => void; + onExternalContentTransform: ((htmlIn: HTMLElement) => void) | null; /** * Enabled experimental features */ - experimentalFeatures: ExperimentalFeatures[]; + experimentalFeatures: (ExperimentalFeatures | CompatibleExperimentalFeatures)[]; /** * Cached document fragment for original content */ - shadowEditFragment: DocumentFragment; + shadowEditFragment: DocumentFragment | null; + + /** + * Cached entity pairs for original content + */ + shadowEditEntities: Record | null; /** * Cached selection path for original content */ - shadowEditSelectionPath: SelectionPath; + shadowEditSelectionPath: SelectionPath | null; /** * Cached table selection path for original content */ - shadowEditTableSelectionPath: SelectionPath[]; + shadowEditTableSelectionPath: SelectionPath[] | null; + + /** + * Cached image selection path for original content + */ + shadowEditImageSelectionPath: SelectionPath[] | null; } diff --git a/packages/roosterjs-editor-types/lib/corePluginState/PendingFormatStatePluginState.ts b/packages/roosterjs-editor-types/lib/corePluginState/PendingFormatStatePluginState.ts index 3d562d56d689..fb227f561344 100644 --- a/packages/roosterjs-editor-types/lib/corePluginState/PendingFormatStatePluginState.ts +++ b/packages/roosterjs-editor-types/lib/corePluginState/PendingFormatStatePluginState.ts @@ -8,10 +8,16 @@ export default interface PendingFormatStatePluginState { /** * Current pending format state */ - pendableFormatState: PendableFormatState; + pendableFormatState: PendableFormatState | null; /** * The position of last pendable format state changing */ - pendableFormatPosition: NodePosition; + pendableFormatPosition: NodePosition | null; + + /** + * A temporary SPAN element to hold pending format change. it will be inserted into content when user type something, + * or discard if focus position is moved + */ + pendableFormatSpan: HTMLElement | null; } diff --git a/packages/roosterjs-editor-types/lib/corePluginState/UndoPluginState.ts b/packages/roosterjs-editor-types/lib/corePluginState/UndoPluginState.ts index 1a32d033532b..8c556059ea52 100644 --- a/packages/roosterjs-editor-types/lib/corePluginState/UndoPluginState.ts +++ b/packages/roosterjs-editor-types/lib/corePluginState/UndoPluginState.ts @@ -1,4 +1,5 @@ import NodePosition from '../interface/NodePosition'; +import Snapshot from '../interface/Snapshot'; import UndoSnapshotsService from '../interface/UndoSnapshotsService'; /** @@ -8,7 +9,7 @@ export default interface UndoPluginState { /** * Snapshot service for undo, it helps handle snapshot add, remove and retrieve */ - snapshotsService: UndoSnapshotsService; + snapshotsService: UndoSnapshotsService; /** * Whether restoring of undo snapshot is in progress. @@ -28,5 +29,5 @@ export default interface UndoPluginState { /** * Position after last auto complete. Undo autoComplete only works if the current position matches this one */ - autoCompletePosition: NodePosition; + autoCompletePosition: NodePosition | null; } diff --git a/packages/roosterjs-editor-types/lib/corePluginState/index.ts b/packages/roosterjs-editor-types/lib/corePluginState/index.ts new file mode 100644 index 000000000000..16e9c783ae13 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/corePluginState/index.ts @@ -0,0 +1,7 @@ +export { default as DOMEventPluginState } from './DOMEventPluginState'; +export { default as EditPluginState } from './EditPluginState'; +export { default as EntityPluginState } from './EntityPluginState'; +export { default as LifecyclePluginState } from './LifecyclePluginState'; +export { default as PendingFormatStatePluginState } from './PendingFormatStatePluginState'; +export { default as UndoPluginState } from './UndoPluginState'; +export { default as CopyPastePluginState } from './CopyPastePluginState'; diff --git a/packages/roosterjs-editor-types/lib/enum/BulletListType.ts b/packages/roosterjs-editor-types/lib/enum/BulletListType.ts new file mode 100644 index 000000000000..c9078df0ddc6 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/enum/BulletListType.ts @@ -0,0 +1,59 @@ +/** + * Enum used to control the different types of bullet list + */ +export const enum BulletListType { + /** + * Minimum value of the enum + */ + Min = 1, + + /** + * Bullet triggered by * + */ + Disc = 1, + + /** + * Bullet triggered by - + */ + Dash = 2, + + /** + * Bullet triggered by -- + */ + Square = 3, + + /** + * Bullet triggered by > + */ + ShortArrow = 4, + + /** + * Bullet triggered by -> + */ + LongArrow = 5, + + /** + * Bullet triggered by => + */ + UnfilledArrow = 6, + + /** + * Bullet triggered by — + */ + Hyphen = 7, + + /** + * Bullet triggered by --> + */ + DoubleLongArrow = 8, + + /** + * Bullet type circle + */ + Circle = 9, + + /** + * Maximum value of the enum + */ + Max = 9, +} diff --git a/packages/roosterjs-editor-types/lib/enum/ChangeSource.ts b/packages/roosterjs-editor-types/lib/enum/ChangeSource.ts index a32fb1fc014d..fb1fc5ebe2d7 100644 --- a/packages/roosterjs-editor-types/lib/enum/ChangeSource.ts +++ b/packages/roosterjs-editor-types/lib/enum/ChangeSource.ts @@ -62,4 +62,10 @@ export const enum ChangeSource { * List chain reorganized numbers of lists */ ListChain = 'ListChain', + + /** + * Keyboard event, used by Content Model. + * Data of this event will be the key code number + */ + Keyboard = 'Keyboard', } diff --git a/packages/roosterjs-editor-types/lib/browser/ContentType.ts b/packages/roosterjs-editor-types/lib/enum/ContentType.ts similarity index 79% rename from packages/roosterjs-editor-types/lib/browser/ContentType.ts rename to packages/roosterjs-editor-types/lib/enum/ContentType.ts index a5dfbfa64545..3088b48176cf 100644 --- a/packages/roosterjs-editor-types/lib/browser/ContentType.ts +++ b/packages/roosterjs-editor-types/lib/enum/ContentType.ts @@ -20,10 +20,10 @@ export const enum ContentType { /** * Plain text content type */ - PlainText = ContentTypePrefix.Text + 'plain', + PlainText = 'text/plain', /** * HTML content type */ - HTML = ContentTypePrefix.Text + 'html', + HTML = 'text/html', } diff --git a/packages/roosterjs-editor-types/lib/enum/DarkModeDatasetNames.ts b/packages/roosterjs-editor-types/lib/enum/DarkModeDatasetNames.ts index 43d5e632b95e..8eba81bee986 100644 --- a/packages/roosterjs-editor-types/lib/enum/DarkModeDatasetNames.ts +++ b/packages/roosterjs-editor-types/lib/enum/DarkModeDatasetNames.ts @@ -1,4 +1,5 @@ /** + * @deprecated * Constants string for dataset names used by dark mode */ export const enum DarkModeDatasetNames { diff --git a/packages/roosterjs-editor-types/lib/enum/DefinitionType.ts b/packages/roosterjs-editor-types/lib/enum/DefinitionType.ts new file mode 100644 index 000000000000..0a808a527323 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/enum/DefinitionType.ts @@ -0,0 +1,34 @@ +/** + * Types of definitions, used by Definition type + */ +export const enum DefinitionType { + /** + * Boolean type definition, represents a boolean type value + */ + Boolean, + + /** + * Number type definition, represents a number type value + */ + Number, + + /** + * String type definition, represents a string type value + */ + String, + + /** + * Array type definition, represents an array with a given item type + */ + Array, + + /** + * Object type definition, represents an object with the given property types + */ + Object, + + /** + * Customize type definition, represents a customized type with a validator function + */ + Customize, +} diff --git a/packages/roosterjs-editor-types/lib/enum/DelimiterClasses.ts b/packages/roosterjs-editor-types/lib/enum/DelimiterClasses.ts new file mode 100644 index 000000000000..a966ff9e2c58 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/enum/DelimiterClasses.ts @@ -0,0 +1,14 @@ +/** + * Class names for Delimiter + */ +export const enum DelimiterClasses { + /** + * Class name to specify this delimiter is before an entity + */ + DELIMITER_BEFORE = 'entityDelimiterBefore', + + /** + * Class name to specify this delimiter is after an entity + */ + DELIMITER_AFTER = 'entityDelimiterAfter', +} diff --git a/packages/roosterjs-editor-types/lib/browser/DocumentCommand.ts b/packages/roosterjs-editor-types/lib/enum/DocumentCommand.ts similarity index 97% rename from packages/roosterjs-editor-types/lib/browser/DocumentCommand.ts rename to packages/roosterjs-editor-types/lib/enum/DocumentCommand.ts index 8de9600c5677..48d14affa065 100644 --- a/packages/roosterjs-editor-types/lib/browser/DocumentCommand.ts +++ b/packages/roosterjs-editor-types/lib/enum/DocumentCommand.ts @@ -1,261 +1,261 @@ -/** - * Command strings for Document.execCommand() API - * https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand - */ -export const enum DocumentCommand { - /** - * Changes the browser auto-link behavior (Internet Explorer only) - */ - AutoUrlDetect = 'AutoUrlDetect', - - /** - * Changes the document background color. In styleWithCss mode, it affects the background color of the containing block instead. - * This requires a <color> value string to be passed in as a value argument. Note that Internet Explorer uses this to set the - * text background color. - */ - BackColor = 'backColor', - - /** - * Toggles bold on/off for the selection or at the insertion point. Internet Explorer uses the <strong> tag instead of <b>. - */ - Bold = 'bold', - - /** - * Clears all authentication credentials from the cache. - */ - ClearAuthenticationCache = 'ClearAuthenticationCache', - - /** - * Makes the content document either read-only or editable. This requires a boolean true/false as the value argument. - * (Not supported by Internet Explorer.) - */ - ContentReadOnly = 'contentReadOnly', - - /** - * Copies the current selection to the clipboard. Conditions of having this behavior enabled vary from one browser to another, - * and have evolved over time. Check the compatibility table to determine if you can use it in your case. - */ - Copy = 'copy', - - /** - * Creates an hyperlink from the selection, but only if there is a selection. Requires a URI string as a value argument for the - * hyperlink's href. The URI must contain at least a single character, which may be whitespace. - * (Internet Explorer will create a link with a null value.) - */ - CreateLink = 'createLink', - - /** - * Removes the current selection and copies it to the clipboard. When this behavior is enabled varies between browsers, - * and its conditions have evolved over time. Check the compatibility table for usage details. - */ - Cut = 'cut', - - /** - * Adds a <small> tag around the selection or at the insertion point. (Not supported by Internet Explorer.) - */ - DecreaseFontSize = 'decreaseFontSize', - - /** - * Changes the paragraph separator used when new paragraphs are created in editable text regions. See Differences in markup - * generation for more details. - */ - DefaultParagraphSeparator = 'defaultParagraphSeparator', - - /** - * Deletes the current selection. - */ - Delete = 'delete', - - /** - * Enables or disables the table row/column insertion and deletion controls. (Not supported by Internet Explorer.) - */ - EnableInlineTableEditing = 'enableInlineTableEditing', - - /** - * Enables or disables the resize handles on images and other resizable objects. (Not supported by Internet Explorer.) - */ - EnableObjectResizing = 'enableObjectResizing', - - /** - * Changes the font name for the selection or at the insertion point. This requires a font name string (like "Arial") - * as a value argument. - */ - FontName = 'fontName', - - /** - * Changes the font size for the selection or at the insertion point. This requires an integer from 1-7 as a value argument. - */ - FontSize = 'fontSize', - - /** - * Changes a font color for the selection or at the insertion point. This requires a hexadecimal color value string - * as a value argument. - */ - ForeColor = 'foreColor', - - /** - * Adds an HTML block-level element around the line containing the current selection, replacing the block element containing - * the line if one exists (in Firefox, <blockquote> is the exception — it will wrap any containing block element). - * Requires a tag-name string as a value argument. Virtually all block-level elements can be used. - * (Internet Explorer supports only heading tags H1–H6, ADDRESS, and PRE, which must be wrapped in angle brackets, such as "<H1>".) - */ - FormatBlock = 'formatBlock', - - /** - * Deletes the character ahead of the cursor's position, identical to hitting the Delete key on a Windows keyboard. - */ - ForwardDelete = 'forwardDelete', - - /** - * Adds a heading element around a selection or insertion point line. Requires the tag-name strings a value argument (i.e. "H1", "H6"). - * (Not supported by Internet Explorer and Safari.) - */ - Heading = 'heading', - - /** - * Changes the background color for the selection or at the insertion point. Requires a color value string as a value argument. - * useCSS must be true for this to function. (Not supported by Internet Explorer.) - */ - HiliteColor = 'hiliteColor', - - /** - * Adds a <big> tag around the selection or at the insertion point. (Not supported by Internet Explorer.) - */ - IncreaseFontSize = 'increaseFontSize', - - /** - * Indents the line containing the selection or insertion point. In Firefox, if the selection spans multiple lines at different - * levels of indentation, only the least indented lines in the selection will be indented. - */ - Indent = 'indent', - - /** - * Controls whether the Enter key inserts a <br> element, or splits the current block element into two. - * (Not supported by Internet Explorer.) - */ - InsertBrOnReturn = 'insertBrOnReturn', - - /** - * Inserts a <hr> element at the insertion point, or replaces the selection with it. - */ - InsertHorizontalRule = 'insertHorizontalRule', - - /** - * Inserts an HTML string at the insertion point (deletes selection). Requires a valid HTML string as a value argument. - * (Not supported by Internet Explorer.) - */ - InsertHTML = 'insertHTML', - - /** - * Inserts an image at the insertion point (deletes selection). Requires a URL string for the image's src as a value argument. - * The requirements for this string are the same as createLink. - */ - InsertImage = 'insertImage', - - /** - * Creates a numbered ordered list for the selection or at the insertion point. - */ - InsertOrderedList = 'insertOrderedList', - - /** - * Creates a bulleted unordered list for the selection or at the insertion point. - */ - InsertUnorderedList = 'insertUnorderedList', - - /** - * Inserts a paragraph around the selection or the current line. - * (Internet Explorer inserts a paragraph at the insertion point and deletes the selection.) - */ - InsertParagraph = 'insertParagraph', - - /** - * Inserts the given plain text at the insertion point (deletes selection). - */ - InsertText = 'insertText', - - /** - * Toggles italics on/off for the selection or at the insertion point. - * (Internet Explorer uses the <em> element instead of <i>.) - */ - Italic = 'italic', - - /** - * Centers the selection or insertion point. - */ - JustifyCenter = 'justifyCenter', - - /** - * Justifies the selection or insertion point. - */ - JustifyFull = 'justifyFull', - - /** - * Justifies the selection or insertion point to the left. - */ - JustifyLeft = 'justifyLeft', - - /** - * Right-justifies the selection or the insertion point. - */ - JustifyRight = 'justifyRight', - - /** - * Outdents the line containing the selection or insertion point. - */ - Outdent = 'outdent', - - /** - * Pastes the clipboard contents at the insertion point (replaces current selection). Disabled for web content. See [1]. - */ - Paste = 'paste', - - /** - * Redoes the previous undo command. - */ - Redo = 'redo', - - /** - * Removes all formatting from the current selection. - */ - RemoveFormat = 'removeFormat', - - /** - * Selects all of the content of the editable region. - */ - SelectAll = 'selectAll', - - /** - * Toggles strikethrough on/off for the selection or at the insertion point. - */ - StrikeThrough = 'strikeThrough', - - /** - * Toggles subscript on/off for the selection or at the insertion point. - */ - Subscript = 'subscript', - - /** - * Toggles superscript on/off for the selection or at the insertion point. - */ - Superscript = 'superscript', - - /** - * Toggles underline on/off for the selection or at the insertion point. - */ - Underline = 'underline', - - /** - * Undoes the last executed command. - */ - Undo = 'undo', - - /** - * Removes the anchor element from a selected hyperlink. - */ - Unlink = 'unlink', - - /** - * Replaces the useCSS command. true modifies/generates style attributes in markup, false generates presentational elements. - */ - StyleWithCSS = 'styleWithCSS', -} +/** + * Command strings for Document.execCommand() API + * https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand + */ +export const enum DocumentCommand { + /** + * Changes the browser auto-link behavior (Internet Explorer only) + */ + AutoUrlDetect = 'AutoUrlDetect', + + /** + * Changes the document background color. In styleWithCss mode, it affects the background color of the containing block instead. + * This requires a <color> value string to be passed in as a value argument. Note that Internet Explorer uses this to set the + * text background color. + */ + BackColor = 'backColor', + + /** + * Toggles bold on/off for the selection or at the insertion point. Internet Explorer uses the <strong> tag instead of <b>. + */ + Bold = 'bold', + + /** + * Clears all authentication credentials from the cache. + */ + ClearAuthenticationCache = 'ClearAuthenticationCache', + + /** + * Makes the content document either read-only or editable. This requires a boolean true/false as the value argument. + * (Not supported by Internet Explorer.) + */ + ContentReadOnly = 'contentReadOnly', + + /** + * Copies the current selection to the clipboard. Conditions of having this behavior enabled vary from one browser to another, + * and have evolved over time. Check the compatibility table to determine if you can use it in your case. + */ + Copy = 'copy', + + /** + * Creates an hyperlink from the selection, but only if there is a selection. Requires a URI string as a value argument for the + * hyperlink's href. The URI must contain at least a single character, which may be whitespace. + * (Internet Explorer will create a link with a null value.) + */ + CreateLink = 'createLink', + + /** + * Removes the current selection and copies it to the clipboard. When this behavior is enabled varies between browsers, + * and its conditions have evolved over time. Check the compatibility table for usage details. + */ + Cut = 'cut', + + /** + * Adds a <small> tag around the selection or at the insertion point. (Not supported by Internet Explorer.) + */ + DecreaseFontSize = 'decreaseFontSize', + + /** + * Changes the paragraph separator used when new paragraphs are created in editable text regions. See Differences in markup + * generation for more details. + */ + DefaultParagraphSeparator = 'defaultParagraphSeparator', + + /** + * Deletes the current selection. + */ + Delete = 'delete', + + /** + * Enables or disables the table row/column insertion and deletion controls. (Not supported by Internet Explorer.) + */ + EnableInlineTableEditing = 'enableInlineTableEditing', + + /** + * Enables or disables the resize handles on images and other resizable objects. (Not supported by Internet Explorer.) + */ + EnableObjectResizing = 'enableObjectResizing', + + /** + * Changes the font name for the selection or at the insertion point. This requires a font name string (like "Arial") + * as a value argument. + */ + FontName = 'fontName', + + /** + * Changes the font size for the selection or at the insertion point. This requires an integer from 1-7 as a value argument. + */ + FontSize = 'fontSize', + + /** + * Changes a font color for the selection or at the insertion point. This requires a hexadecimal color value string + * as a value argument. + */ + ForeColor = 'foreColor', + + /** + * Adds an HTML block-level element around the line containing the current selection, replacing the block element containing + * the line if one exists (in Firefox, <blockquote> is the exception — it will wrap any containing block element). + * Requires a tag-name string as a value argument. Virtually all block-level elements can be used. + * (Internet Explorer supports only heading tags H1–H6, ADDRESS, and PRE, which must be wrapped in angle brackets, such as "<H1>".) + */ + FormatBlock = 'formatBlock', + + /** + * Deletes the character ahead of the cursor's position, identical to hitting the Delete key on a Windows keyboard. + */ + ForwardDelete = 'forwardDelete', + + /** + * Adds a heading element around a selection or insertion point line. Requires the tag-name strings a value argument (i.e. "H1", "H6"). + * (Not supported by Internet Explorer and Safari.) + */ + Heading = 'heading', + + /** + * Changes the background color for the selection or at the insertion point. Requires a color value string as a value argument. + * useCSS must be true for this to function. (Not supported by Internet Explorer.) + */ + HiliteColor = 'hiliteColor', + + /** + * Adds a <big> tag around the selection or at the insertion point. (Not supported by Internet Explorer.) + */ + IncreaseFontSize = 'increaseFontSize', + + /** + * Indents the line containing the selection or insertion point. In Firefox, if the selection spans multiple lines at different + * levels of indentation, only the least indented lines in the selection will be indented. + */ + Indent = 'indent', + + /** + * Controls whether the Enter key inserts a <br> element, or splits the current block element into two. + * (Not supported by Internet Explorer.) + */ + InsertBrOnReturn = 'insertBrOnReturn', + + /** + * Inserts a <hr> element at the insertion point, or replaces the selection with it. + */ + InsertHorizontalRule = 'insertHorizontalRule', + + /** + * Inserts an HTML string at the insertion point (deletes selection). Requires a valid HTML string as a value argument. + * (Not supported by Internet Explorer.) + */ + InsertHTML = 'insertHTML', + + /** + * Inserts an image at the insertion point (deletes selection). Requires a URL string for the image's src as a value argument. + * The requirements for this string are the same as createLink. + */ + InsertImage = 'insertImage', + + /** + * Creates a numbered ordered list for the selection or at the insertion point. + */ + InsertOrderedList = 'insertOrderedList', + + /** + * Creates a bulleted unordered list for the selection or at the insertion point. + */ + InsertUnorderedList = 'insertUnorderedList', + + /** + * Inserts a paragraph around the selection or the current line. + * (Internet Explorer inserts a paragraph at the insertion point and deletes the selection.) + */ + InsertParagraph = 'insertParagraph', + + /** + * Inserts the given plain text at the insertion point (deletes selection). + */ + InsertText = 'insertText', + + /** + * Toggles italics on/off for the selection or at the insertion point. + * (Internet Explorer uses the <em> element instead of <i>.) + */ + Italic = 'italic', + + /** + * Centers the selection or insertion point. + */ + JustifyCenter = 'justifyCenter', + + /** + * Justifies the selection or insertion point. + */ + JustifyFull = 'justifyFull', + + /** + * Justifies the selection or insertion point to the left. + */ + JustifyLeft = 'justifyLeft', + + /** + * Right-justifies the selection or the insertion point. + */ + JustifyRight = 'justifyRight', + + /** + * Outdents the line containing the selection or insertion point. + */ + Outdent = 'outdent', + + /** + * Pastes the clipboard contents at the insertion point (replaces current selection). Disabled for web content. See [1]. + */ + Paste = 'paste', + + /** + * Redoes the previous undo command. + */ + Redo = 'redo', + + /** + * Removes all formatting from the current selection. + */ + RemoveFormat = 'removeFormat', + + /** + * Selects all of the content of the editable region. + */ + SelectAll = 'selectAll', + + /** + * Toggles strikethrough on/off for the selection or at the insertion point. + */ + StrikeThrough = 'strikeThrough', + + /** + * Toggles subscript on/off for the selection or at the insertion point. + */ + Subscript = 'subscript', + + /** + * Toggles superscript on/off for the selection or at the insertion point. + */ + Superscript = 'superscript', + + /** + * Toggles underline on/off for the selection or at the insertion point. + */ + Underline = 'underline', + + /** + * Undoes the last executed command. + */ + Undo = 'undo', + + /** + * Removes the anchor element from a selected hyperlink. + */ + Unlink = 'unlink', + + /** + * Replaces the useCSS command. true modifies/generates style attributes in markup, false generates presentational elements. + */ + StyleWithCSS = 'styleWithCSS', +} diff --git a/packages/roosterjs-editor-types/lib/browser/DocumentPosition.ts b/packages/roosterjs-editor-types/lib/enum/DocumentPosition.ts similarity index 95% rename from packages/roosterjs-editor-types/lib/browser/DocumentPosition.ts rename to packages/roosterjs-editor-types/lib/enum/DocumentPosition.ts index c0b3076786ea..9c88353769d5 100644 --- a/packages/roosterjs-editor-types/lib/browser/DocumentPosition.ts +++ b/packages/roosterjs-editor-types/lib/enum/DocumentPosition.ts @@ -1,35 +1,35 @@ -/** - * The is essentially an enum representing result from browser compareDocumentPosition API - * https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition - */ -export const enum DocumentPosition { - /** - * Same node - */ - Same = 0, - - /** - * Node is disconnected from document - */ - Disconnected = 1, - - /** - * Node is preceding the comparing node - */ - Preceding = 2, - - /** - * Node is following the comparing node - */ - Following = 4, - - /** - * Node contains the comparing node - */ - Contains = 8, - - /** - * Node is contained by the comparing node - */ - ContainedBy = 16, -} +/** + * The is essentially an enum representing result from browser compareDocumentPosition API + * https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition + */ +export const enum DocumentPosition { + /** + * Same node + */ + Same = 0, + + /** + * Node is disconnected from document + */ + Disconnected = 1, + + /** + * Node is preceding the comparing node + */ + Preceding = 2, + + /** + * Node is following the comparing node + */ + Following = 4, + + /** + * Node contains the comparing node + */ + Contains = 8, + + /** + * Node is contained by the comparing node + */ + ContainedBy = 16, +} diff --git a/packages/roosterjs-editor-types/lib/enum/EntityOperation.ts b/packages/roosterjs-editor-types/lib/enum/EntityOperation.ts index 75c0e9f01661..7a2ea7763e18 100644 --- a/packages/roosterjs-editor-types/lib/enum/EntityOperation.ts +++ b/packages/roosterjs-editor-types/lib/enum/EntityOperation.ts @@ -59,14 +59,18 @@ export const enum EntityOperation { ReplaceTemporaryContent, /** - * Notify plugins that editor has attached shadow root for an entity. - * Plugins can handle this event to do extra operations to the shadow root + * @deprecated */ AddShadowRoot, /** - * Notify plugins that editor has removed the shadow root of an entity - * Plugins can handle this event to do any necessary clean up for shadow root + * @deprecated */ RemoveShadowRoot, + + /** + * Notify plugins that a new entity state need to be updated to an entity. + * This is normally happened when user undo/redo the content with an entity snapshot added by a plugin that handles entity + */ + UpdateEntityState, } diff --git a/packages/roosterjs-editor-types/lib/enum/ExperimentalFeatures.ts b/packages/roosterjs-editor-types/lib/enum/ExperimentalFeatures.ts index fed8e72efd79..8183e07c85a1 100644 --- a/packages/roosterjs-editor-types/lib/enum/ExperimentalFeatures.ts +++ b/packages/roosterjs-editor-types/lib/enum/ExperimentalFeatures.ts @@ -2,6 +2,8 @@ * Experimental feature flags */ export const enum ExperimentalFeatures { + // #region Graduated and deprecated features. + // These features will be removed in next major release /** * @deprecated This feature is always enabled */ @@ -28,31 +30,142 @@ export const enum ExperimentalFeatures { MergePastedLine = 'MergePastedLine', /** - * Resize an image horizontally or vertically + * @deprecated This feature is always enabled */ SingleDirectionResize = 'SingleDirectionResize', /** - * Try retrieve link preview information when paste + * @deprecated This feature is always enabled */ PasteWithLinkPreview = 'PasteWithLinkPreview', /** - * Rotate an inline image (requires ImageEdit plugin) + * @deprecated This feature is always enabled */ ImageRotate = 'ImageRotate', /** - * Crop an inline image (requires ImageEdit plugin) + * @deprecated This feature is always enabled */ ImageCrop = 'ImageCrop', + /** + * @deprecated This feature is always enabled * Check if the element has a style attribute, if not, apply the default format */ AlwaysApplyDefaultFormat = 'AlwaysApplyDefaultFormat', /** + * @deprecated This feature can be enabled/disabled using Paste Plugin contructor param * Paste the Html instead of the Img when the Html Body only have one IMG Child node */ ConvertSingleImageBody = 'ConvertSingleImageBody', + + /** + * @deprecated This feature is always enabled + * Align table elements to left, center and right using setAlignment API + */ + TableAlignment = 'TableAlignment', + + /** + * @deprecated this feature is always enabled + * Provide a circular resize handles that adaptive the number od handles to the size of the image + */ + AdaptiveHandlesResizer = 'AdaptiveHandlesResizer', + + /** + * @deprecated this feature is always disabled + * Automatically transform -- into hyphen, if typed between two words. + */ + AutoHyphen = 'AutoHyphen', + + /** + * @deprecated this feature is always disabled + * Use pending format strategy to do style based format, e.g. Font size, Color. + * With this feature enabled, we don't need to insert temp ZeroWidthSpace character to hold pending format + * when selection is collapsed. Instead, we will hold the pending format in memory and only apply it when type something + */ + PendingStyleBasedFormat = 'PendingStyleBasedFormat', + + /** + * @deprecated this feature is always disabled + * Normalize list to make sure it can be displayed correctly in other client + * e.g. We will move list items with "display: block" into previous list item and change tag to be DIV + */ + NormalizeList = 'NormalizeList', + + /** + * @deprecated this feature is always enabled + * When a html image is selected, the selected image data will be stored by editor core. + */ + ImageSelection = 'ImageSelection', + + /** + * @deprecated this feature is always enabled + * Use variable-based dark mode solution rather than dataset-based solution. + * When enable this feature, need to pass in a DarkModelHandler object to each call of setColor and applyFormat + * if you need them work for dark mode + */ + VariableBasedDarkColor = 'VariableBasedDarkColor', + + /** + * @deprecated this feature is always enabled + * Align list elements elements to left, center and right using setAlignment API + */ + ListItemAlignment = 'ListItemAlignment', + + /** + * @deprecated + */ + DefaultFormatInSpan = 'DefaultFormatInSpan', + + /** + * @deprecated + */ + DefaultFormatOnContainer = 'DefaultFormatOnContainer', + + //#endregion + + /** + * Provide additional Tab Key Features. Requires Text Features Content Editable Features + */ + TabKeyTextFeatures = 'TabKeyTextFeatures', + + /** + * Trigger formatting by a especial characters. Ex: (A), 1. i). + */ + AutoFormatList = 'AutoFormatList', + + /** + * With this feature enabled, when writing back a list item we will re-use all + * ancestor list elements, even if they don't match the types currently in the + * listTypes array for that item. The only list that we will ensure is correct + * is the one closest to the item. + */ + ReuseAllAncestorListElements = 'ReuseAllAncestorListElements', + + /** + * Reuse existing DOM structure if possible when convert Content Model back to DOM tree + */ + ReusableContentModel = 'ReusableContentModel', + + /** + * Handle keyboard editing event with Content Model + */ + EditWithContentModel = 'EditWithContentModel', + + /** + * Delete table with Backspace key with the whole was selected with table selector + */ + DeleteTableWithBackspace = 'DeleteTableWithBackspace', + + /** + * Add entities around a Read Only Inline entity to prevent cursor to be hidden when cursor is next of it. + */ + InlineEntityReadOnlyDelimiters = 'InlineEntityReadOnlyDelimiters', + + /** + * Paste with Content model + */ + ContentModelPaste = 'ContentModelPaste', } diff --git a/packages/roosterjs-editor-types/lib/browser/Keys.ts b/packages/roosterjs-editor-types/lib/enum/Keys.ts similarity index 80% rename from packages/roosterjs-editor-types/lib/browser/Keys.ts rename to packages/roosterjs-editor-types/lib/enum/Keys.ts index a5d2e273873a..51dfd0e39bd7 100644 --- a/packages/roosterjs-editor-types/lib/browser/Keys.ts +++ b/packages/roosterjs-editor-types/lib/enum/Keys.ts @@ -7,13 +7,18 @@ export const enum Keys { TAB = 9, ENTER = 13, SHIFT = 16, + CTRL_LEFT = 17, + ALT = 18, ESCAPE = 27, SPACE = 32, PAGEUP = 33, + END = 35, + HOME = 36, LEFT = 37, UP = 38, RIGHT = 39, DOWN = 40, + PRINT_SCREEN = 44, DELETE = 46, /** * @deprecated Just for backward compatibility @@ -25,6 +30,7 @@ export const enum Keys { U = 85, Y = 89, Z = 90, + META_LEFT = 91, COMMA = 188, DASH_UNDERSCORE = 189, PERIOD = 190, @@ -35,7 +41,9 @@ export const enum Keys { FORWARD_SLASH = 191, GRAVE_TILDE = 192, - // Keys below are non-standard, and should be used in ContentEditFeatures only + /** + * Keys below are non-standard, and should be used in ContentEditFeatures only + */ CONTENTCHANGED = 0x101, RANGE = 0x102, diff --git a/packages/roosterjs-editor-types/lib/enum/KnownCreateElementDataIndex.ts b/packages/roosterjs-editor-types/lib/enum/KnownCreateElementDataIndex.ts index e7fb53b2885c..fd028e074f5c 100644 --- a/packages/roosterjs-editor-types/lib/enum/KnownCreateElementDataIndex.ts +++ b/packages/roosterjs-editor-types/lib/enum/KnownCreateElementDataIndex.ts @@ -38,14 +38,32 @@ export const enum KnownCreateElementDataIndex { ImageEditWrapper = 6, /** - * Table resizer elements + * @deprecated */ TableHorizontalResizer = 7, + + /** + * @deprecated + */ TableVerticalResizer = 8, + + /** + * @deprecated + */ TableResizerLTR = 9, + + /** + * @deprecated + */ TableResizerRTL = 10, + /** - * Table Selector element + * @deprecated */ TableSelector = 11, + + /** + * @deprecated + */ + EmptyLineFormatInSpan = 12, } diff --git a/packages/roosterjs-editor-types/lib/enum/KnownPasteSourceType.ts b/packages/roosterjs-editor-types/lib/enum/KnownPasteSourceType.ts new file mode 100644 index 000000000000..5f2cd40edd7d --- /dev/null +++ b/packages/roosterjs-editor-types/lib/enum/KnownPasteSourceType.ts @@ -0,0 +1,13 @@ +/** + * Represent the types of sources to handle in the Paste Plugin + */ +export const enum KnownPasteSourceType { + WordDesktop, + ExcelDesktop, + ExcelOnline, + PowerPointDesktop, + GoogleSheets, + WacComponents, + Default, + SingleImage, +} diff --git a/packages/roosterjs-editor-types/lib/browser/NodeType.ts b/packages/roosterjs-editor-types/lib/enum/NodeType.ts similarity index 90% rename from packages/roosterjs-editor-types/lib/browser/NodeType.ts rename to packages/roosterjs-editor-types/lib/enum/NodeType.ts index ca5879f593b8..79c38fea5443 100644 --- a/packages/roosterjs-editor-types/lib/browser/NodeType.ts +++ b/packages/roosterjs-editor-types/lib/enum/NodeType.ts @@ -1,41 +1,46 @@ -/** - * The is essentially an enum represents the type of the node - * https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType - * Values not listed here are deprecated. - */ -export const enum NodeType { - /** - * An Element node such as <p> or <div>. - */ - Element = 1, - - /** - * The actual Text of Element or Attr. - */ - Text = 3, - - /** - * A ProcessingInstruction of an XML document such as <?xml-stylesheet ... ?> declaration. - */ - ProcessingInstruction = 7, - - /** - * A Comment node. - */ - Comment = 8, - - /** - * A Document node. - */ - Document = 9, - - /** - * A DocumentType node e.g. <!DOCTYPE html> for HTML5 documents. - */ - DocumentType = 10, - - /** - * A DocumentFragment node. - */ - DocumentFragment = 11, -} +/** + * The is essentially an enum represents the type of the node + * https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType + * Values not listed here are deprecated. + */ +export const enum NodeType { + /** + * An Element node such as <p> or <div>. + */ + Element = 1, + + /** + * An Attribute node such as name="value". + */ + Attribute = 2, + + /** + * The actual Text of Element or Attr. + */ + Text = 3, + + /** + * A ProcessingInstruction of an XML document such as <?xml-stylesheet ... ?> declaration. + */ + ProcessingInstruction = 7, + + /** + * A Comment node. + */ + Comment = 8, + + /** + * A Document node. + */ + Document = 9, + + /** + * A DocumentType node e.g. <!DOCTYPE html> for HTML5 documents. + */ + DocumentType = 10, + + /** + * A DocumentFragment node. + */ + DocumentFragment = 11, +} diff --git a/packages/roosterjs-editor-types/lib/enum/NumberingListType.ts b/packages/roosterjs-editor-types/lib/enum/NumberingListType.ts new file mode 100644 index 000000000000..39117b544006 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/enum/NumberingListType.ts @@ -0,0 +1,114 @@ +/** + * Enum used to control the different types of numbering list + */ +export const enum NumberingListType { + /** + * Minimum value of the enum + */ + Min = 1, + + /** + * Numbering triggered by 1. + */ + Decimal = 1, + + /** + * Numbering triggered by 1- + */ + DecimalDash = 2, + + /** + * Numbering triggered by 1) + */ + DecimalParenthesis = 3, + + /** + * Numbering triggered by (1) + */ + DecimalDoubleParenthesis = 4, + + /** + * Numbering triggered by a. + */ + LowerAlpha = 5, + + /** + * Numbering triggered by a) + */ + LowerAlphaParenthesis = 6, + + /** + * Numbering triggered by (a) + */ + LowerAlphaDoubleParenthesis = 7, + + /** + * Numbering triggered by a- + */ + LowerAlphaDash = 8, + + /** + * Numbering triggered by A. + */ + UpperAlpha = 9, + + /** + * Numbering triggered by A) + */ + UpperAlphaParenthesis = 10, + + /** + * Numbering triggered by (A) + */ + UpperAlphaDoubleParenthesis = 11, + + /** + * Numbering triggered by A- + */ + UpperAlphaDash = 12, + + /** + * Numbering triggered by i. + */ + LowerRoman = 13, + + /** + * Numbering triggered by i) + */ + LowerRomanParenthesis = 14, + + /** + * Numbering triggered by (i) + */ + LowerRomanDoubleParenthesis = 15, + + /** + * Numbering triggered by i- + */ + LowerRomanDash = 16, + + /** + * Numbering triggered by I. + */ + UpperRoman = 17, + + /** + * Numbering triggered by I) + */ + UpperRomanParenthesis = 18, + + /** + * Numbering triggered by (I) + */ + UpperRomanDoubleParenthesis = 19, + + /** + * Numbering triggered by I- + */ + UpperRomanDash = 20, + + /** + * Maximum value of the enum + */ + Max = 20, +} diff --git a/packages/roosterjs-editor-types/lib/enum/PasteType.ts b/packages/roosterjs-editor-types/lib/enum/PasteType.ts new file mode 100644 index 000000000000..12187505a7bc --- /dev/null +++ b/packages/roosterjs-editor-types/lib/enum/PasteType.ts @@ -0,0 +1,24 @@ +/** + * Enum for paste options + */ +export const enum PasteType { + /** + * Default paste behavior + */ + Normal, + + /** + * Paste only the plain text + */ + AsPlainText, + + /** + * Apply the current style to pasted content + */ + MergeFormat, + + /** + * If there is a image uri in the clipboard, paste the content as image element + */ + AsImage, +} diff --git a/packages/roosterjs-editor-types/lib/event/PluginEventType.ts b/packages/roosterjs-editor-types/lib/enum/PluginEventType.ts similarity index 83% rename from packages/roosterjs-editor-types/lib/event/PluginEventType.ts rename to packages/roosterjs-editor-types/lib/enum/PluginEventType.ts index a82dc4b66be6..29a518a64b88 100644 --- a/packages/roosterjs-editor-types/lib/event/PluginEventType.ts +++ b/packages/roosterjs-editor-types/lib/enum/PluginEventType.ts @@ -1,113 +1,131 @@ -/** - * Editor plugin event type - */ -export const enum PluginEventType { - /** - * HTML KeyDown event - */ - KeyDown = 0, - - /** - * HTML KeyPress event - */ - KeyPress = 1, - - /** - * HTML KeyUp event - */ - KeyUp = 2, - - /** - * HTML Input / TextInput event - */ - Input = 3, - - /** - * HTML CompositionEnd event - */ - CompositionEnd = 4, - - /** - * HTML MouseDown event - */ - MouseDown = 5, - - /** - * HTML MouseUp event - */ - MouseUp = 6, - - /** - * Content changed event - */ - ContentChanged = 7, - - /** - * Extract Content with a DOM tree event - * This event is triggered when getContent() is called with triggerExtractContentEvent = true - * Plugin can handle this event to remove the UI only markups to return clean HTML - * by operating on a cloned DOM tree - */ - ExtractContentWithDom = 8, - - /** - * Before Paste event, provide a chance to change copied content - */ - BeforeCutCopy = 9, - - /** - * Before Paste event, provide a chance to change paste content - */ - BeforePaste = 10, - - /** - * Let plugin know editor is ready now - */ - EditorReady = 11, - - /** - * Let plugin know editor is about to dispose - */ - BeforeDispose = 12, - - /** - * Pending format state (bold, italic, underline, ... with collapsed selection) is changed - */ - PendingFormatStateChanged = 13, - - /** - * Scroll event triggered by scroll container - */ - Scroll = 14, - - /** - * Operating on an entity. See enum EntityOperation for more details about each operation - */ - EntityOperation = 15, - - /** - * HTML ContextMenu event - */ - ContextMenu = 16, - - /** - * Editor has entered shadow edit mode - */ - EnteredShadowEdit = 17, - - /** - * Editor is about to leave shadow edit mode - */ - LeavingShadowEdit = 18, - - /** - * Content of image is being changed from client side - */ - EditImage = 19, - - /** - * Content of editor is about to be cleared by SetContent API, handle this event to cache anything you need - * before it is gone - */ - BeforeSetContent = 20, -} +/** + * Editor plugin event type + */ +export const enum PluginEventType { + /** + * HTML KeyDown event + */ + KeyDown = 0, + + /** + * HTML KeyPress event + */ + KeyPress = 1, + + /** + * HTML KeyUp event + */ + KeyUp = 2, + + /** + * HTML Input / TextInput event + */ + Input = 3, + + /** + * HTML CompositionEnd event + */ + CompositionEnd = 4, + + /** + * HTML MouseDown event + */ + MouseDown = 5, + + /** + * HTML MouseUp event + */ + MouseUp = 6, + + /** + * Content changed event + */ + ContentChanged = 7, + + /** + * Extract Content with a DOM tree event + * This event is triggered when getContent() is called with triggerExtractContentEvent = true + * Plugin can handle this event to remove the UI only markups to return clean HTML + * by operating on a cloned DOM tree + */ + ExtractContentWithDom = 8, + + /** + * Before Paste event, provide a chance to change copied content + */ + BeforeCutCopy = 9, + + /** + * Before Paste event, provide a chance to change paste content + */ + BeforePaste = 10, + + /** + * Let plugin know editor is ready now + */ + EditorReady = 11, + + /** + * Let plugin know editor is about to dispose + */ + BeforeDispose = 12, + + /** + * Pending format state (bold, italic, underline, ... with collapsed selection) is changed + */ + PendingFormatStateChanged = 13, + + /** + * Scroll event triggered by scroll container + */ + Scroll = 14, + + /** + * Operating on an entity. See enum EntityOperation for more details about each operation + */ + EntityOperation = 15, + + /** + * HTML ContextMenu event + */ + ContextMenu = 16, + + /** + * Editor has entered shadow edit mode + */ + EnteredShadowEdit = 17, + + /** + * Editor is about to leave shadow edit mode + */ + LeavingShadowEdit = 18, + + /** + * Content of image is being changed from client side + */ + EditImage = 19, + + /** + * Content of editor is about to be cleared by SetContent API, handle this event to cache anything you need + * before it is gone + */ + BeforeSetContent = 20, + + /** + * Zoom scale value is changed, triggered by Editor.setZoomScale() when set a different scale number + */ + ZoomChanged = 21, + + /** + * EXPERIMENTAL FEATURE + * Editor changed the selection. + */ + SelectionChanged = 22, + + /** + * EXPERIMENTAL FEATURE + * Editor content is about to be changed by keyboard event. + * This is only used by Content Model editing + */ + BeforeKeyboardEditing = 23, +} diff --git a/packages/roosterjs-editor-types/lib/enum/SelectionRangeTypes.ts b/packages/roosterjs-editor-types/lib/enum/SelectionRangeTypes.ts new file mode 100644 index 000000000000..258f070bdc03 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/enum/SelectionRangeTypes.ts @@ -0,0 +1,17 @@ +/** + * Types of Selection Ranges that the SelectionRangeEx can return + */ +export const enum SelectionRangeTypes { + /** + * Normal selection range provided by browser. + */ + Normal, + /** + * Selection made inside of a single table. + */ + TableSelection, + /** + * Selection made in a image. + */ + ImageSelection, +} diff --git a/packages/roosterjs-editor-types/lib/enum/TableBorderFormat.ts b/packages/roosterjs-editor-types/lib/enum/TableBorderFormat.ts index 03dc288465b1..2dc031e102cd 100644 --- a/packages/roosterjs-editor-types/lib/enum/TableBorderFormat.ts +++ b/packages/roosterjs-editor-types/lib/enum/TableBorderFormat.ts @@ -73,4 +73,9 @@ export const enum TableBorderFormat { * | */ ESPECIAL_TYPE_3, + + /** + * No border + */ + CLEAR, } diff --git a/packages/roosterjs-editor-types/lib/enum/TableOperation.ts b/packages/roosterjs-editor-types/lib/enum/TableOperation.ts index 62d3db9543a5..4a6270d7de7f 100644 --- a/packages/roosterjs-editor-types/lib/enum/TableOperation.ts +++ b/packages/roosterjs-editor-types/lib/enum/TableOperation.ts @@ -57,6 +57,11 @@ export const enum TableOperation { */ MergeRight, + /** + * Merge all selected cells + */ + MergeCells, + /** * Split current table cell horizontally */ diff --git a/packages/roosterjs-editor-types/lib/enum/index.ts b/packages/roosterjs-editor-types/lib/enum/index.ts new file mode 100644 index 000000000000..0db87f996bdb --- /dev/null +++ b/packages/roosterjs-editor-types/lib/enum/index.ts @@ -0,0 +1,35 @@ +export { DocumentCommand } from './DocumentCommand'; +export { DocumentPosition } from './DocumentPosition'; +export { Keys } from './Keys'; +export { NodeType } from './NodeType'; +export { ContentTypePrefix, ContentType } from './ContentType'; +export { Alignment } from './Alignment'; +export { ChangeSource } from './ChangeSource'; +export { ColorTransformDirection } from './ColorTransformDirection'; +export { ContentPosition } from './ContentPosition'; +export { DarkModeDatasetNames } from './DarkModeDatasetNames'; +export { DelimiterClasses } from './DelimiterClasses'; +export { Direction } from './Direction'; +export { EntityClasses } from './EntityClasses'; +export { EntityOperation } from './EntityOperation'; +export { ExperimentalFeatures } from './ExperimentalFeatures'; +export { FontSizeChange } from './FontSizeChange'; +export { GetContentMode } from './GetContentMode'; +export { Indentation } from './Indentation'; +export { Capitalization } from './Capitalization'; +export { ListType } from './ListType'; +export { PositionType } from './PositionType'; +export { QueryScope } from './QueryScope'; +export { RegionType } from './RegionType'; +export { TableOperation } from './TableOperation'; +export { ImageEditOperation } from './ImageEditOperation'; +export { ClearFormatMode } from './ClearFormatMode'; +export { KnownCreateElementDataIndex } from './KnownCreateElementDataIndex'; +export { KnownPasteSourceType } from './KnownPasteSourceType'; +export { TableBorderFormat } from './TableBorderFormat'; +export { PluginEventType } from './PluginEventType'; +export { SelectionRangeTypes } from './SelectionRangeTypes'; +export { NumberingListType } from './NumberingListType'; +export { BulletListType } from './BulletListType'; +export { DefinitionType } from './DefinitionType'; +export { PasteType } from './PasteType'; diff --git a/packages/roosterjs-editor-types/lib/event/BasePluginEvent.ts b/packages/roosterjs-editor-types/lib/event/BasePluginEvent.ts index 08f9e0b46e33..b27bcd33b24d 100644 --- a/packages/roosterjs-editor-types/lib/event/BasePluginEvent.ts +++ b/packages/roosterjs-editor-types/lib/event/BasePluginEvent.ts @@ -1,9 +1,10 @@ -import { PluginEventType } from './PluginEventType'; +import { PluginEventType } from '../enum/PluginEventType'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; /** * Editor plugin event interface */ -export default interface BasePluginEvent { +export default interface BasePluginEvent { /** * Type of this event */ diff --git a/packages/roosterjs-editor-types/lib/event/BeforeCutCopyEvent.ts b/packages/roosterjs-editor-types/lib/event/BeforeCutCopyEvent.ts index 41b2409f944e..443044c0f55c 100644 --- a/packages/roosterjs-editor-types/lib/event/BeforeCutCopyEvent.ts +++ b/packages/roosterjs-editor-types/lib/event/BeforeCutCopyEvent.ts @@ -1,10 +1,11 @@ import BasePluginEvent from './BasePluginEvent'; -import { PluginEventType } from './PluginEventType'; +import { PluginEventType } from '../enum/PluginEventType'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; /** - * Provides a chance for plugin to change the content before it is copied from editor. + * Data of BeforeCutCopyEvent */ -export default interface BeforeCutCopyEvent extends BasePluginEvent { +export interface BeforeCutCopyEventData { /** * Raw DOM event */ @@ -25,3 +26,17 @@ export default interface BeforeCutCopyEvent extends BasePluginEvent {} + +/** + * Provides a chance for plugin to change the content before it is copied from editor. + */ +export interface CompatibleBeforeCutCopyEvent + extends BeforeCutCopyEventData, + BasePluginEvent {} diff --git a/packages/roosterjs-editor-types/lib/event/BeforeDisposeEvent.ts b/packages/roosterjs-editor-types/lib/event/BeforeDisposeEvent.ts index 2b8cb4ed7e4c..f79ac1882b92 100644 --- a/packages/roosterjs-editor-types/lib/event/BeforeDisposeEvent.ts +++ b/packages/roosterjs-editor-types/lib/event/BeforeDisposeEvent.ts @@ -1,8 +1,15 @@ import BasePluginEvent from './BasePluginEvent'; -import { PluginEventType } from './PluginEventType'; +import { PluginEventType } from '../enum/PluginEventType'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; /** * Provides a chance for plugin to change the content before it is pasted into editor. */ export default interface BeforeDisposeEvent extends BasePluginEvent {} + +/** + * Provides a chance for plugin to change the content before it is pasted into editor. + */ +export interface CompatibleBeforeDisposeEvent + extends BasePluginEvent {} diff --git a/packages/roosterjs-editor-types/lib/event/BeforeKeyboardEditingEvent.ts b/packages/roosterjs-editor-types/lib/event/BeforeKeyboardEditingEvent.ts new file mode 100644 index 000000000000..dae86f5949f8 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/event/BeforeKeyboardEditingEvent.ts @@ -0,0 +1,27 @@ +import BasePluginEvent from './BasePluginEvent'; +import { PluginEventType } from '../enum/PluginEventType'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; + +/** + * Data of BeforeKeyboardEditing + */ +export interface BeforeKeyboardEditingData { + /** + * Raw DOM event + */ + rawEvent: KeyboardEvent; +} + +/** + * Provides a chance for plugin to change the content before it is copied from editor. + */ +export default interface BeforeKeyboardEditingEvent + extends BeforeKeyboardEditingData, + BasePluginEvent {} + +/** + * Provides a chance for plugin to change the content before it is copied from editor. + */ +export interface CompatibleBeforeKeyboardEditingEvent + extends BeforeKeyboardEditingData, + BasePluginEvent {} diff --git a/packages/roosterjs-editor-types/lib/event/BeforePasteEvent.ts b/packages/roosterjs-editor-types/lib/event/BeforePasteEvent.ts index 9b49031efe83..18a512d505ca 100644 --- a/packages/roosterjs-editor-types/lib/event/BeforePasteEvent.ts +++ b/packages/roosterjs-editor-types/lib/event/BeforePasteEvent.ts @@ -1,12 +1,15 @@ import BasePluginEvent from './BasePluginEvent'; import ClipboardData from '../interface/ClipboardData'; import HtmlSanitizerOptions from '../interface/HtmlSanitizerOptions'; -import { PluginEventType } from './PluginEventType'; +import { PluginEventType } from '../enum/PluginEventType'; +import type { CompatiblePasteType } from '../compatibleEnum/PasteType'; +import type { PasteType } from '../enum/PasteType'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; /** - * Provides a chance for plugin to change the content before it is pasted into editor. + * Data of BeforePasteEvent */ -export default interface BeforePasteEvent extends BasePluginEvent { +export interface BeforePasteEventData { /** * An object contains all related data for pasting */ @@ -36,4 +39,23 @@ export default interface BeforePasteEvent extends BasePluginEvent; + + /** + * Paste type option (as plain text, merge format, normal, as image) + */ + readonly pasteType: PasteType | CompatiblePasteType; } + +/** + * Provides a chance for plugin to change the content before it is pasted into editor. + */ +export default interface BeforePasteEvent + extends BeforePasteEventData, + BasePluginEvent {} + +/** + * Provides a chance for plugin to change the content before it is pasted into editor. + */ +export interface CompatibleBeforePasteEvent + extends BeforePasteEventData, + BasePluginEvent {} diff --git a/packages/roosterjs-editor-types/lib/event/BeforeSetContentEvent.ts b/packages/roosterjs-editor-types/lib/event/BeforeSetContentEvent.ts index 4c013ca35ff0..b00b672dc77e 100644 --- a/packages/roosterjs-editor-types/lib/event/BeforeSetContentEvent.ts +++ b/packages/roosterjs-editor-types/lib/event/BeforeSetContentEvent.ts @@ -1,14 +1,29 @@ import BasePluginEvent from './BasePluginEvent'; -import { PluginEventType } from './PluginEventType'; +import { PluginEventType } from '../enum/PluginEventType'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; /** - * The event to be triggered before SetContent API is called. - * Handle this event to cache anything you need from editor before it is gone. + * Data of BeforeSetContentEvent */ -export default interface BeforeSetContentEvent - extends BasePluginEvent { +export interface BeforeSetContentEventData { /** * New content HTML that is about to set to editor */ newContent: string; } + +/** + * The event to be triggered before SetContent API is called. + * Handle this event to cache anything you need from editor before it is gone. + */ +export default interface BeforeSetContentEvent + extends BeforeSetContentEventData, + BasePluginEvent {} + +/** + * The event to be triggered before SetContent API is called. + * Handle this event to cache anything you need from editor before it is gone. + */ +export interface CompatibleBeforeSetContentEvent + extends BeforeSetContentEventData, + BasePluginEvent {} diff --git a/packages/roosterjs-editor-types/lib/event/ContentChangedEvent.ts b/packages/roosterjs-editor-types/lib/event/ContentChangedEvent.ts index 427b40c7e7b0..1bec1bae1613 100644 --- a/packages/roosterjs-editor-types/lib/event/ContentChangedEvent.ts +++ b/packages/roosterjs-editor-types/lib/event/ContentChangedEvent.ts @@ -1,19 +1,40 @@ import BasePluginEvent from './BasePluginEvent'; +import ContentChangedData from '../interface/ContentChangedData'; import { ChangeSource } from '../enum/ChangeSource'; -import { PluginEventType } from './PluginEventType'; +import { PluginEventType } from '../enum/PluginEventType'; +import type { CompatibleChangeSource } from '../compatibleEnum/ChangeSource'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; /** - * Represents a change to the editor made by another plugin + * Data of ContentChangedEvent */ -export default interface ContentChangedEvent - extends BasePluginEvent { +export interface ContentChangedEventData { /** * Source of the change */ - source: ChangeSource | string; + source: ChangeSource | CompatibleChangeSource | string; /** * Optional related data */ data?: any; + + /* + * Additional Data Related to the ContentChanged Event + */ + additionalData?: ContentChangedData; } + +/** + * Represents a change to the editor made by another plugin + */ +export default interface ContentChangedEvent + extends ContentChangedEventData, + BasePluginEvent {} + +/** + * Represents a change to the editor made by another plugin + */ +export interface CompatibleContentChangedEvent + extends ContentChangedEventData, + BasePluginEvent {} diff --git a/packages/roosterjs-editor-types/lib/event/EditImageEvent.ts b/packages/roosterjs-editor-types/lib/event/EditImageEvent.ts index bcbefc640b64..1f92840dfec0 100644 --- a/packages/roosterjs-editor-types/lib/event/EditImageEvent.ts +++ b/packages/roosterjs-editor-types/lib/event/EditImageEvent.ts @@ -1,11 +1,11 @@ import BasePluginEvent from './BasePluginEvent'; -import { PluginEventType } from './PluginEventType'; +import { PluginEventType } from '../enum/PluginEventType'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; /** - * Represents an event that will be fired when an inline image is edited by user, and the src - * attribute of the image is about to be changed + * Data of EditImageEvent */ -export default interface EditImageEvent extends BasePluginEvent { +export interface EditImageEventData { /** * The image element that is being changed */ @@ -28,3 +28,19 @@ export default interface EditImageEvent extends BasePluginEvent {} + +/** + * Represents an event that will be fired when an inline image is edited by user, and the src + * attribute of the image is about to be changed + */ +export interface CompatibleEditImageEvent + extends EditImageEventData, + BasePluginEvent {} diff --git a/packages/roosterjs-editor-types/lib/event/EditorReadyEvent.ts b/packages/roosterjs-editor-types/lib/event/EditorReadyEvent.ts index d6f6120dc13b..96e3cbb9116b 100644 --- a/packages/roosterjs-editor-types/lib/event/EditorReadyEvent.ts +++ b/packages/roosterjs-editor-types/lib/event/EditorReadyEvent.ts @@ -1,7 +1,14 @@ import BasePluginEvent from './BasePluginEvent'; -import { PluginEventType } from './PluginEventType'; +import { PluginEventType } from '../enum/PluginEventType'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; /** * Provides a chance for plugin to change the content before it is pasted into editor. */ export default interface EditorReadyEvent extends BasePluginEvent {} + +/** + * Provides a chance for plugin to change the content before it is pasted into editor. + */ +export interface CompatibleEditorReadyEvent + extends BasePluginEvent {} diff --git a/packages/roosterjs-editor-types/lib/event/EntityOperationEvent.ts b/packages/roosterjs-editor-types/lib/event/EntityOperationEvent.ts index 8a53dec24aad..f65e61ed3989 100644 --- a/packages/roosterjs-editor-types/lib/event/EntityOperationEvent.ts +++ b/packages/roosterjs-editor-types/lib/event/EntityOperationEvent.ts @@ -1,18 +1,18 @@ import BasePluginEvent from './BasePluginEvent'; import Entity from '../interface/Entity'; import { EntityOperation } from '../enum/EntityOperation'; -import { PluginEventType } from './PluginEventType'; +import { PluginEventType } from '../enum/PluginEventType'; +import type { CompatibleEntityOperation } from '../compatibleEnum/EntityOperation'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; /** - * Provide a chance for plugins to handle entity related events. - * See enum EntityOperation for more details about each operation + * Data of EntityOperationEvent */ -export default interface EntityOperationEvent - extends BasePluginEvent { +export interface EntityOperationEventData { /** * Operation to this entity */ - operation: EntityOperation; + operation: EntityOperation | CompatibleEntityOperation; /** * The entity that editor is operating on @@ -25,15 +25,36 @@ export default interface EntityOperationEvent rawEvent?: Event; /** - * A document fragment for entity based on Shadow DOM. This property is only available for NewEntity operation. - * Putting DOM nodes under this fragment will cause a shadow root to be attached to the entity wrapper - * with these DOM nodes under it. - * - * If there is already cached DOM nodes, they will also be put under this fragment. - * Plugin need to decide: - * 1. Apply the cache: do nothing and the DOM nodes will be appended as shadow DOM entity content - * 2. Discard the cache and use new content instead: clear the fragment and append new DOM nodes, then new DOM nodes will be used - * 3. Dehydrate this entity: clear the fragment, and leave it empty + * For EntityOperation.UpdateEntityState, we use this object to pass the new entity state to plugin. + * For other operation types, it is not used. + */ + state?: string; + + /** + * For EntityOperation.NewEntity, plugin can set this property to true then the entity will be persisted. + * A persisted entity won't be touched during undo/redo, unless it does not exist after undo/redo. + * For other operation types, this value will be ignored. + */ + shouldPersist?: boolean; + + /** + * @deprecated */ contentForShadowEntity?: DocumentFragment; } + +/** + * Provide a chance for plugins to handle entity related events. + * See enum EntityOperation for more details about each operation + */ +export default interface EntityOperationEvent + extends EntityOperationEventData, + BasePluginEvent {} + +/** + * Provide a chance for plugins to handle entity related events. + * See enum EntityOperation for more details about each operation + */ +export interface CompatibleEntityOperationEvent + extends EntityOperationEventData, + BasePluginEvent {} diff --git a/packages/roosterjs-editor-types/lib/event/ExtractContentWithDomEvent.ts b/packages/roosterjs-editor-types/lib/event/ExtractContentWithDomEvent.ts index 304edb1039eb..7f3110f55a45 100644 --- a/packages/roosterjs-editor-types/lib/event/ExtractContentWithDomEvent.ts +++ b/packages/roosterjs-editor-types/lib/event/ExtractContentWithDomEvent.ts @@ -1,17 +1,34 @@ import BasePluginEvent from './BasePluginEvent'; -import { PluginEventType } from './PluginEventType'; +import { PluginEventType } from '../enum/PluginEventType'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; /** - * Extract Content with a DOM tree event - * This event is triggered when getContent() is called with triggerExtractContentEvent = true - * Plugin can handle this event to remove the UI only markups to return clean HTML - * by operating on a cloned DOM tree + * Data of ExtractContentWithDomEvent */ -export default interface ExtractContentWithDomEvent - extends BasePluginEvent { +export interface ExtractContentWithDomEventData { /** * Cloned root element of editor * Plugin can change this DOM tree to clean up the markups it added before */ clonedRoot: HTMLElement; } + +/** + * Extract Content with a DOM tree event + * This event is triggered when getContent() is called with triggerExtractContentEvent = true + * Plugin can handle this event to remove the UI only markups to return clean HTML + * by operating on a cloned DOM tree + */ +export default interface ExtractContentWithDomEvent + extends ExtractContentWithDomEventData, + BasePluginEvent {} + +/** + * Extract Content with a DOM tree event + * This event is triggered when getContent() is called with triggerExtractContentEvent = true + * Plugin can handle this event to remove the UI only markups to return clean HTML + * by operating on a cloned DOM tree + */ +export interface CompatibleExtractContentWithDomEvent + extends ExtractContentWithDomEventData, + BasePluginEvent {} diff --git a/packages/roosterjs-editor-types/lib/event/PendingFormatStateChangedEvent.ts b/packages/roosterjs-editor-types/lib/event/PendingFormatStateChangedEvent.ts index 9b2fc20c9d14..e73d1b598c05 100644 --- a/packages/roosterjs-editor-types/lib/event/PendingFormatStateChangedEvent.ts +++ b/packages/roosterjs-editor-types/lib/event/PendingFormatStateChangedEvent.ts @@ -1,11 +1,36 @@ import BasePluginEvent from './BasePluginEvent'; import { PendableFormatState } from '../interface/FormatState'; -import { PluginEventType } from './PluginEventType'; +import { PluginEventType } from '../enum/PluginEventType'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; /** * An event fired when pending format state (bold, italic, underline, ... with collapsed selection) is changed */ export default interface PendingFormatStateChangedEvent extends BasePluginEvent { + /** + * The new format state to apply. If null is passed, clear existing pending format state if any + */ formatState: PendableFormatState; + + /** + * A callback to do format change to a temp element. This is used for style-based format such as font and color + */ + formatCallback?: (element: HTMLElement, isInnerNode?: boolean) => any; +} + +/** + * An event fired when pending format state (bold, italic, underline, ... with collapsed selection) is changed + */ +export interface CompatiblePendingFormatStateChangedEvent + extends BasePluginEvent { + /** + * The new format state to apply. If null is passed, clear existing pending format state if any + */ + formatState: PendableFormatState; + + /** + * A callback to do format change to a temp element. This is used for style-based format such as font and color + */ + formatCallback?: (element: HTMLElement, isInnerNode?: boolean) => any; } diff --git a/packages/roosterjs-editor-types/lib/event/PluginDomEvent.ts b/packages/roosterjs-editor-types/lib/event/PluginDomEvent.ts index 1941beb55980..35bf454e26fd 100644 --- a/packages/roosterjs-editor-types/lib/event/PluginDomEvent.ts +++ b/packages/roosterjs-editor-types/lib/event/PluginDomEvent.ts @@ -1,11 +1,46 @@ import BasePluginEvent from './BasePluginEvent'; -import { PluginEventType } from './PluginEventType'; +import { PluginEventType } from '../enum/PluginEventType'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; + +/** + * Data of PluginMouseUpEvent + */ +export interface PluginMouseUpEventData { + /** + * Whether this is a mouse click event (mouse up and down on the same position) + */ + isClicking?: boolean; +} + +/** + * Data of PluginContextMenuEvent + */ +export interface PluginContextMenuEventData { + /** + * A callback array to let editor retrieve context menu item related to this event. + * Plugins can add their own getter callback to this array, + * items from each getter will be separated by a splitter item represented by null + */ + items: any[]; +} + +/** + * Data of PluginScrollEvent + */ +export interface PluginScrollEventData { + /** + * Current scroll container that triggers this scroll event + */ + scrollContainer: HTMLElement; +} /** * A base interface of all DOM events */ -export interface PluginDomEventBase - extends BasePluginEvent { +export interface PluginDomEventBase< + TEventType extends PluginEventType | CompatiblePluginEventType, + TRawEvent extends Event +> extends BasePluginEvent { rawEvent: TRawEvent; } @@ -25,25 +60,15 @@ export interface PluginMouseDownEvent * This interface represents a PluginEvent wrapping native MouseUp event */ export interface PluginMouseUpEvent - extends PluginDomEventBase { - /** - * Whether this is a mouse click event (mouse up and down on the same position) - */ - isClicking?: boolean; -} + extends PluginMouseUpEventData, + PluginDomEventBase {} /** * This interface represents a PluginEvent wrapping native ContextMenu event */ export interface PluginContextMenuEvent - extends PluginDomEventBase { - /** - * A callback array to let editor retrieve context menu item related to this event. - * Plugins can add their own getter callback to this array, - * items from each getter will be separated by a splitter item represented by null - */ - items: any[]; -} + extends PluginContextMenuEventData, + PluginDomEventBase {} /** * This interface represents a PluginEvent wrapping native Mouse event @@ -81,9 +106,82 @@ export interface PluginInputEvent extends PluginDomEventBase { - scrollContainer: HTMLElement; -} +export interface PluginScrollEvent + extends PluginScrollEventData, + PluginDomEventBase {} + +/** + * This interface represents a PluginEvent wrapping native CompositionEnd event + */ +export interface CompatiblePluginCompositionEvent + extends PluginDomEventBase {} + +/** + * This interface represents a PluginEvent wrapping native MouseDown event + */ +export interface CompatiblePluginMouseDownEvent + extends PluginDomEventBase {} + +/** + * This interface represents a PluginEvent wrapping native MouseUp event + */ +export interface CompatiblePluginMouseUpEvent + extends PluginMouseUpEventData, + PluginDomEventBase {} + +/** + * This interface represents a PluginEvent wrapping native ContextMenu event + */ +export interface CompatiblePluginContextMenuEvent + extends PluginContextMenuEventData, + PluginDomEventBase {} + +/** + * This interface represents a PluginEvent wrapping native Mouse event + */ +export type CompatiblePluginMouseEvent = + | CompatiblePluginMouseDownEvent + | CompatiblePluginMouseUpEvent + | CompatiblePluginContextMenuEvent; + +/** + * This interface represents a PluginEvent wrapping native KeyDown event + */ +export interface CompatiblePluginKeyDownEvent + extends PluginDomEventBase {} + +/** + * This interface represents a PluginEvent wrapping native KeyPress event + */ +export interface CompatiblePluginKeyPressEvent + extends PluginDomEventBase {} + +/** + * This interface represents a PluginEvent wrapping native KeyUp event + */ +export interface CompatiblePluginKeyUpEvent + extends PluginDomEventBase {} + +/** + * The interface represents a PluginEvent wrapping native Keyboard event + */ +export type CompatiblePluginKeyboardEvent = + | CompatiblePluginKeyDownEvent + | CompatiblePluginKeyPressEvent + | CompatiblePluginKeyUpEvent; + +/** + * This interface represents a PluginEvent wrapping native input / textinput event + */ +export interface CompatiblePluginInputEvent + extends PluginDomEventBase {} + +/** + * This interface represents a PluginEvent wrapping native scroll event + */ +export interface CompatiblePluginScrollEvent + extends PluginScrollEventData, + PluginDomEventBase {} /** * This represents a PluginEvent wrapping native browser event @@ -94,3 +192,13 @@ export type PluginDomEvent = | PluginKeyboardEvent | PluginInputEvent | PluginScrollEvent; + +/** + * This represents a PluginEvent wrapping native browser event + */ +export type CompatiblePluginDomEvent = + | CompatiblePluginCompositionEvent + | CompatiblePluginMouseEvent + | CompatiblePluginKeyboardEvent + | CompatiblePluginInputEvent + | CompatiblePluginScrollEvent; diff --git a/packages/roosterjs-editor-types/lib/event/PluginEvent.ts b/packages/roosterjs-editor-types/lib/event/PluginEvent.ts index a7f5a71a718c..792655277012 100644 --- a/packages/roosterjs-editor-types/lib/event/PluginEvent.ts +++ b/packages/roosterjs-editor-types/lib/event/PluginEvent.ts @@ -1,15 +1,29 @@ -import BeforeCutCopyEvent from './BeforeCutCopyEvent'; -import BeforeDisposeEvent from './BeforeDisposeEvent'; -import BeforePasteEvent from './BeforePasteEvent'; -import BeforeSetContentEvent from './BeforeSetContentEvent'; -import ContentChangedEvent from './ContentChangedEvent'; -import EditImageEvent from './EditImageEvent'; -import EditorReadyEvent from './EditorReadyEvent'; -import EntityOperationEvent from './EntityOperationEvent'; -import ExtractContentWithDomEvent from './ExtractContentWithDomEvent'; -import PendingFormatStateChangedEvent from './PendingFormatStateChangedEvent'; -import { EnterShadowEditEvent, LeaveShadowEditEvent } from './ShadowEditEvent'; -import { PluginDomEvent } from './PluginDomEvent'; +import BeforeCutCopyEvent, { CompatibleBeforeCutCopyEvent } from './BeforeCutCopyEvent'; +import BeforeDisposeEvent, { CompatibleBeforeDisposeEvent } from './BeforeDisposeEvent'; +import BeforeKeyboardEditingEvent, { + CompatibleBeforeKeyboardEditingEvent, +} from './BeforeKeyboardEditingEvent'; +import BeforePasteEvent, { CompatibleBeforePasteEvent } from './BeforePasteEvent'; +import BeforeSetContentEvent, { CompatibleBeforeSetContentEvent } from './BeforeSetContentEvent'; +import ContentChangedEvent, { CompatibleContentChangedEvent } from './ContentChangedEvent'; +import EditImageEvent, { CompatibleEditImageEvent } from './EditImageEvent'; +import EditorReadyEvent, { CompatibleEditorReadyEvent } from './EditorReadyEvent'; +import EntityOperationEvent, { CompatibleEntityOperationEvent } from './EntityOperationEvent'; +import SelectionChangedEvent, { CompatibleSelectionChangedEvent } from './SelectionChangeEvent'; +import ZoomChangedEvent, { CompatibleZoomChangedEvent } from './ZoomChangedEvent'; +import { CompatiblePluginDomEvent, PluginDomEvent } from './PluginDomEvent'; +import { + CompatibleEnterShadowEditEvent, + CompatibleLeaveShadowEditEvent, + EnterShadowEditEvent, + LeaveShadowEditEvent, +} from './ShadowEditEvent'; +import PendingFormatStateChangedEvent, { + CompatiblePendingFormatStateChangedEvent, +} from './PendingFormatStateChangedEvent'; +import ExtractContentWithDomEvent, { + CompatibleExtractContentWithDomEvent, +} from './ExtractContentWithDomEvent'; /** * Editor plugin event interface @@ -27,4 +41,23 @@ export type PluginEvent = | EnterShadowEditEvent | LeaveShadowEditEvent | EditImageEvent - | BeforeSetContentEvent; + | BeforeSetContentEvent + | ZoomChangedEvent + | SelectionChangedEvent + | BeforeKeyboardEditingEvent + | CompatibleBeforeCutCopyEvent + | CompatibleBeforeDisposeEvent + | CompatibleBeforePasteEvent + | CompatibleBeforeSetContentEvent + | CompatibleContentChangedEvent + | CompatibleEditImageEvent + | CompatibleEditorReadyEvent + | CompatibleEntityOperationEvent + | CompatibleExtractContentWithDomEvent + | CompatiblePendingFormatStateChangedEvent + | CompatiblePluginDomEvent + | CompatibleEnterShadowEditEvent + | CompatibleLeaveShadowEditEvent + | CompatibleZoomChangedEvent + | CompatibleSelectionChangedEvent + | CompatibleBeforeKeyboardEditingEvent; diff --git a/packages/roosterjs-editor-types/lib/event/PluginEventData.ts b/packages/roosterjs-editor-types/lib/event/PluginEventData.ts index caeed07b3c2c..885c5c11a1df 100644 --- a/packages/roosterjs-editor-types/lib/event/PluginEventData.ts +++ b/packages/roosterjs-editor-types/lib/event/PluginEventData.ts @@ -1,6 +1,7 @@ import BasePluginEvent from './BasePluginEvent'; import { PluginEvent } from './PluginEvent'; -import { PluginEventType } from './PluginEventType'; +import { PluginEventType } from '../enum/PluginEventType'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; /** * A type to get specify plugin event type from eventType parameter. @@ -8,16 +9,15 @@ import { PluginEventType } from './PluginEventType'; */ export type PluginEventFromTypeGeneric< E extends PluginEvent, - T extends PluginEventType + T extends PluginEventType | CompatiblePluginEventType > = E extends BasePluginEvent ? E : never; /** * A type to get specify plugin event type from eventType parameter. */ -export type PluginEventFromType = PluginEventFromTypeGeneric< - PluginEvent, - T ->; +export type PluginEventFromType< + T extends PluginEventType | CompatiblePluginEventType +> = PluginEventFromTypeGeneric; /** * A type to extract data part of a plugin event type. Data part is the plugin event without eventType field. @@ -25,10 +25,12 @@ export type PluginEventFromType = PluginEventFromType */ export type PluginEventDataGeneric< E extends PluginEvent, - T extends PluginEventType + T extends PluginEventType | CompatiblePluginEventType > = E extends BasePluginEvent ? Pick> : never; /** * A type to extract data part of a plugin event type. Data part is the plugin event without eventType field. */ -export type PluginEventData = PluginEventDataGeneric; +export type PluginEventData< + T extends PluginEventType | CompatiblePluginEventType +> = PluginEventDataGeneric; diff --git a/packages/roosterjs-editor-types/lib/event/SelectionChangeEvent.ts b/packages/roosterjs-editor-types/lib/event/SelectionChangeEvent.ts new file mode 100644 index 000000000000..c2726446267b --- /dev/null +++ b/packages/roosterjs-editor-types/lib/event/SelectionChangeEvent.ts @@ -0,0 +1,28 @@ +import BasePluginEvent from './BasePluginEvent'; +import { PluginEventType } from '../enum/PluginEventType'; +import { SelectionRangeEx } from '../interface/SelectionRangeEx'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; + +/** + * Data of SelectionChangedEvent + */ +export interface SelectionChangedEventData { + /** + * Information of the selection + */ + selectionRangeEx: SelectionRangeEx | null; +} + +/** + * Represents an event that will be fired when the user changed the selection + */ +export default interface SelectionChangedEvent + extends SelectionChangedEventData, + BasePluginEvent {} + +/** + * Represents an event that will be fired when the user changed the selection + */ +export interface CompatibleSelectionChangedEvent + extends SelectionChangedEventData, + BasePluginEvent {} diff --git a/packages/roosterjs-editor-types/lib/event/ShadowEditEvent.ts b/packages/roosterjs-editor-types/lib/event/ShadowEditEvent.ts index 984aa2046bf1..15d128c87f80 100644 --- a/packages/roosterjs-editor-types/lib/event/ShadowEditEvent.ts +++ b/packages/roosterjs-editor-types/lib/event/ShadowEditEvent.ts @@ -1,11 +1,12 @@ import BasePluginEvent from './BasePluginEvent'; import SelectionPath from '../interface/SelectionPath'; -import { PluginEventType } from './PluginEventType'; +import { PluginEventType } from '../enum/PluginEventType'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; /** - * A plugin triggered right after editor has entered Shadow Edit mode + * Data of EnterShadowEditEvent */ -export interface EnterShadowEditEvent extends BasePluginEvent { +export interface EnterShadowEditEventData { /** * The document fragment of original editor content */ @@ -14,10 +15,30 @@ export interface EnterShadowEditEvent extends BasePluginEvent {} + /** * A plugin triggered right before editor leave Shadow Edit mode */ export interface LeaveShadowEditEvent extends BasePluginEvent {} + +/** + * A plugin triggered right after editor has entered Shadow Edit mode + */ +export interface CompatibleEnterShadowEditEvent + extends EnterShadowEditEventData, + BasePluginEvent {} + +/** + * A plugin triggered right before editor leave Shadow Edit mode + */ +export interface CompatibleLeaveShadowEditEvent + extends BasePluginEvent {} diff --git a/packages/roosterjs-editor-types/lib/event/ZoomChangedEvent.ts b/packages/roosterjs-editor-types/lib/event/ZoomChangedEvent.ts new file mode 100644 index 000000000000..92c1eb8c5c92 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/event/ZoomChangedEvent.ts @@ -0,0 +1,36 @@ +import BasePluginEvent from './BasePluginEvent'; +import { PluginEventType } from '../enum/PluginEventType'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; + +/** + * Data of ZoomChangedEvent + */ +export interface ZoomChangedEventData { + /** + * Zoom scale value before this change + */ + oldZoomScale: number; + + /** + * Zoom scale value after this change + */ + newZoomScale: number; +} + +/** + * Represents an event object triggered from Editor.setZoomScale() API. + * Plugins can handle this event when they need to do something for zoom changing. + * + */ +export default interface ZoomChangedEvent + extends ZoomChangedEventData, + BasePluginEvent {} + +/** + * Represents an event object triggered from Editor.setZoomScale() API. + * Plugins can handle this event when they need to do something for zoom changing. + * + */ +export interface CompatibleZoomChangedEvent + extends ZoomChangedEventData, + BasePluginEvent {} diff --git a/packages/roosterjs-editor-types/lib/event/index.ts b/packages/roosterjs-editor-types/lib/event/index.ts new file mode 100644 index 000000000000..9c7646645477 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/event/index.ts @@ -0,0 +1,101 @@ +export { + default as BeforeCutCopyEvent, + BeforeCutCopyEventData, + CompatibleBeforeCutCopyEvent, +} from './BeforeCutCopyEvent'; +export { default as BasePluginEvent } from './BasePluginEvent'; +export { default as BeforeDisposeEvent, CompatibleBeforeDisposeEvent } from './BeforeDisposeEvent'; +export { + default as BeforePasteEvent, + BeforePasteEventData, + CompatibleBeforePasteEvent, +} from './BeforePasteEvent'; +export { + default as BeforeSetContentEvent, + BeforeSetContentEventData, + CompatibleBeforeSetContentEvent, +} from './BeforeSetContentEvent'; +export { + default as ContentChangedEvent, + ContentChangedEventData, + CompatibleContentChangedEvent, +} from './ContentChangedEvent'; +export { + default as EditImageEvent, + EditImageEventData, + CompatibleEditImageEvent, +} from './EditImageEvent'; +export { default as EditorReadyEvent, CompatibleEditorReadyEvent } from './EditorReadyEvent'; +export { + default as EntityOperationEvent, + EntityOperationEventData, + CompatibleEntityOperationEvent, +} from './EntityOperationEvent'; +export { + default as ExtractContentWithDomEvent, + ExtractContentWithDomEventData, + CompatibleExtractContentWithDomEvent, +} from './ExtractContentWithDomEvent'; +export { + default as PendingFormatStateChangedEvent, + CompatiblePendingFormatStateChangedEvent, +} from './PendingFormatStateChangedEvent'; +export { + PluginDomEvent, + PluginDomEventBase, + PluginCompositionEvent, + PluginContextMenuEvent, + PluginKeyboardEvent, + PluginKeyDownEvent, + PluginKeyPressEvent, + PluginKeyUpEvent, + PluginMouseEvent, + PluginMouseDownEvent, + PluginMouseUpEvent, + PluginInputEvent, + PluginScrollEvent, + CompatiblePluginDomEvent, + CompatiblePluginCompositionEvent, + CompatiblePluginContextMenuEvent, + CompatiblePluginKeyboardEvent, + CompatiblePluginKeyDownEvent, + CompatiblePluginKeyPressEvent, + CompatiblePluginKeyUpEvent, + CompatiblePluginMouseEvent, + CompatiblePluginMouseDownEvent, + CompatiblePluginMouseUpEvent, + CompatiblePluginInputEvent, + CompatiblePluginScrollEvent, + PluginScrollEventData, + PluginMouseUpEventData, + PluginContextMenuEventData, +} from './PluginDomEvent'; +export { PluginEvent } from './PluginEvent'; +export { + PluginEventData, + PluginEventDataGeneric, + PluginEventFromType, + PluginEventFromTypeGeneric, +} from './PluginEventData'; +export { + EnterShadowEditEvent, + LeaveShadowEditEvent, + EnterShadowEditEventData, + CompatibleEnterShadowEditEvent, + CompatibleLeaveShadowEditEvent, +} from './ShadowEditEvent'; +export { + default as ZoomChangedEvent, + ZoomChangedEventData, + CompatibleZoomChangedEvent, +} from './ZoomChangedEvent'; +export { + default as SelectionChangedEvent, + SelectionChangedEventData, + CompatibleSelectionChangedEvent, +} from './SelectionChangeEvent'; +export { + default as BeforeKeyboardEditingEvent, + BeforeKeyboardEditingData, + CompatibleBeforeKeyboardEditingEvent, +} from './BeforeKeyboardEditingEvent'; diff --git a/packages/roosterjs-editor-types/lib/index.ts b/packages/roosterjs-editor-types/lib/index.ts index 5cb21b1f0cfd..d8251d47ec08 100644 --- a/packages/roosterjs-editor-types/lib/index.ts +++ b/packages/roosterjs-editor-types/lib/index.ts @@ -1,208 +1,6 @@ -// Browser -export { default as BrowserInfo } from './browser/BrowserInfo'; -export { DocumentCommand } from './browser/DocumentCommand'; -export { DocumentPosition } from './browser/DocumentPosition'; -export { default as EdgeLinkPreview } from './browser/EdgeLinkPreview'; -export { Keys } from './browser/Keys'; -export { NodeType } from './browser/NodeType'; -export { ContentTypePrefix, ContentType } from './browser/ContentType'; - -// Enum -export { Alignment } from './enum/Alignment'; -export { ChangeSource } from './enum/ChangeSource'; -export { ColorTransformDirection } from './enum/ColorTransformDirection'; -export { ContentPosition } from './enum/ContentPosition'; -export { DarkModeDatasetNames } from './enum/DarkModeDatasetNames'; -export { Direction } from './enum/Direction'; -export { EntityClasses } from './enum/EntityClasses'; -export { EntityOperation } from './enum/EntityOperation'; -export { ExperimentalFeatures } from './enum/ExperimentalFeatures'; -export { FontSizeChange } from './enum/FontSizeChange'; -export { GetContentMode } from './enum/GetContentMode'; -export { Indentation } from './enum/Indentation'; -export { Capitalization } from './enum/Capitalization'; -export { ListType } from './enum/ListType'; -export { PositionType } from './enum/PositionType'; -export { QueryScope } from './enum/QueryScope'; -export { RegionType } from './enum/RegionType'; -export { TableOperation } from './enum/TableOperation'; -export { ImageEditOperation } from './enum/ImageEditOperation'; -export { ClearFormatMode } from './enum/ClearFormatMode'; -export { KnownCreateElementDataIndex } from './enum/KnownCreateElementDataIndex'; -export { TableBorderFormat } from './enum/TableBorderFormat'; - -// Event -export { default as BeforeCutCopyEvent } from './event/BeforeCutCopyEvent'; -export { default as BasePluginEvent } from './event/BasePluginEvent'; -export { default as BeforeDisposeEvent } from './event/BeforeDisposeEvent'; -export { default as BeforePasteEvent } from './event/BeforePasteEvent'; -export { default as BeforeSetContentEvent } from './event/BeforeSetContentEvent'; -export { default as ContentChangedEvent } from './event/ContentChangedEvent'; -export { default as EditImageEvent } from './event/EditImageEvent'; -export { default as EditorReadyEvent } from './event/EditorReadyEvent'; -export { default as EntityOperationEvent } from './event/EntityOperationEvent'; -export { default as ExtractContentWithDomEvent } from './event/ExtractContentWithDomEvent'; -export { default as PendingFormatStateChangedEvent } from './event/PendingFormatStateChangedEvent'; -export { - PluginDomEvent, - PluginDomEventBase, - PluginCompositionEvent, - PluginContextMenuEvent, - PluginKeyboardEvent, - PluginKeyDownEvent, - PluginKeyPressEvent, - PluginKeyUpEvent, - PluginMouseEvent, - PluginMouseDownEvent, - PluginMouseUpEvent, - PluginInputEvent, - PluginScrollEvent, -} from './event/PluginDomEvent'; -export { PluginEvent } from './event/PluginEvent'; -export { PluginEventType } from './event/PluginEventType'; -export { - PluginEventData, - PluginEventDataGeneric, - PluginEventFromType, - PluginEventFromTypeGeneric, -} from './event/PluginEventData'; -export { EnterShadowEditEvent, LeaveShadowEditEvent } from './event/ShadowEditEvent'; - -// Interface -export { default as BlockElement } from './interface/BlockElement'; -export { default as ClipboardData } from './interface/ClipboardData'; -export { default as ContextMenuProvider } from './interface/ContextMenuProvider'; -export { default as CustomData } from './interface/CustomData'; -export { default as DefaultFormat } from './interface/DefaultFormat'; -export { default as Entity } from './interface/Entity'; -export { - default as FormatState, - PendableFormatState, - ElementBasedFormatState, - StyleBasedFormatState, - EditorUndoState, -} from './interface/FormatState'; -export { - default as ExtractClipboardEventOption, - ExtractClipboardItemsOption, - ExtractClipboardItemsForIEOptions, -} from './interface/ExtractClipboardEventOption'; -export { default as IContentTraverser } from './interface/IContentTraverser'; -export { default as InlineElement } from './interface/InlineElement'; -export { - InsertOption, - InsertOptionBase, - InsertOptionBasic, - InsertOptionRange, -} from './interface/InsertOption'; -export { default as IPositionContentSearcher } from './interface/IPositionContentSearcher'; -export { default as LinkData } from './interface/LinkData'; -export { default as ModeIndependentColor } from './interface/ModeIndependentColor'; -export { default as NodePosition } from './interface/NodePosition'; -export { default as Rect } from './interface/Rect'; -export { default as Region } from './interface/Region'; -export { default as RegionBase } from './interface/RegionBase'; -export { default as SelectionPath } from './interface/SelectionPath'; -export { default as Snapshots } from './interface/Snapshots'; -export { default as TableFormat } from './interface/TableFormat'; -export { default as TableSelection } from './interface/TableSelection'; -export { default as Coordinates } from './interface/Coordinates'; -export { default as HtmlSanitizerOptions } from './interface/HtmlSanitizerOptions'; -export { default as SanitizeHtmlOptions } from './interface/SanitizeHtmlOptions'; -export { default as TargetWindowBase } from './interface/TargetWindowBase'; -export { default as TargetWindow } from './interface/TargetWindow'; -export { - default as IEditor, - ContentEditFeature, - GenericContentEditFeature, - BuildInEditFeature, -} from './interface/IEditor'; -export { default as EditorPlugin } from './interface/EditorPlugin'; -export { default as PluginWithState } from './interface/PluginWithState'; -export { - default as CorePlugins, - PluginKey, - KeyOfStatePlugin, - GenericPluginState, - PluginState, - StatePluginKeys, - TypeOfStatePlugin, -} from './interface/CorePlugins'; -export { - default as EditorCore, - AddUndoSnapshot, - AttachDomEvent, - CoreApiMap, - CreatePasteFragment, - EnsureTypeInContainer, - Focus, - GetContent, - GetSelectionRange, - GetSelectionRangeEx, - GetStyleBasedFormatState, - GetPendableFormatState, - HasFocus, - InsertNode, - RestoreUndoSnapshot, - SelectRange, - SetContent, - SwitchShadowEdit, - TransformColor, - TriggerEvent, - SelectTable, -} from './interface/EditorCore'; -export { default as EditorOptions } from './interface/EditorOptions'; -export { - default as ContentEditFeatureSettings, - AutoLinkFeatureSettings, - CursorFeatureSettings, - EntityFeatureSettings, - ListFeatureSettings, - MarkdownFeatureSettings, - QuoteFeatureSettings, - ShortcutFeatureSettings, - StructuredNodeFeatureSettings, - TableFeatureSettings, -} from './interface/ContentEditFeatureSettings'; -export { default as CustomReplacement } from './interface/CustomReplacement'; -export { default as UndoSnapshotsService } from './interface/UndoSnapshotsService'; -export { default as PickerDataProvider } from './interface/PickerDataProvider'; -export { default as PickerPluginOptions } from './interface/PickerPluginOptions'; -export { default as VCell } from './interface/VCell'; -export { default as ImageEditOptions } from './interface/ImageEditOptions'; -export { default as CreateElementData } from './interface/CreateElementData'; -export { - SelectionRangeExBase, - NormalSelectionRange, - TableSelectionRange, - SelectionRangeEx, - SelectionRangeTypes, -} from './interface/SelectionRangeEx'; - -// Core Plugin State -export { default as DOMEventPluginState } from './corePluginState/DOMEventPluginState'; -export { default as EditPluginState } from './corePluginState/EditPluginState'; -export { default as EntityPluginState } from './corePluginState/EntityPluginState'; -export { default as LifecyclePluginState } from './corePluginState/LifecyclePluginState'; -export { default as PendingFormatStatePluginState } from './corePluginState/PendingFormatStatePluginState'; -export { default as UndoPluginState } from './corePluginState/UndoPluginState'; -export { default as CopyPastePluginState } from './corePluginState/CopyPastePluginState'; - -// Other type -export { - AttributeCallback, - AttributeCallbackMap, - CssStyleCallback, - CssStyleCallbackMap, - ElementCallback, - StringMap, - ElementCallbackMap, - PredefinedCssMap, -} from './type/htmlSanitizerCallbackTypes'; -export { - DOMEventHandlerFunction, - DOMEventHandlerObject, - DOMEventHandler, -} from './type/domEventHandler'; -export { TrustedHTMLHandler } from './type/TrustedHTMLHandler'; -export { SizeTransformer } from './type/SizeTransformer'; +export * from './browser/index'; +export * from './corePluginState/index'; +export * from './enum/index'; +export * from './event/index'; +export * from './interface/index'; +export * from './type/index'; diff --git a/packages/roosterjs-editor-types/lib/interface/ClipboardData.ts b/packages/roosterjs-editor-types/lib/interface/ClipboardData.ts index 17a3f138dd33..c43a7dc64ced 100644 --- a/packages/roosterjs-editor-types/lib/interface/ClipboardData.ts +++ b/packages/roosterjs-editor-types/lib/interface/ClipboardData.ts @@ -19,7 +19,7 @@ export default interface ClipboardData { * When set to null, it means there's no HTML from clipboard event. * When set to undefined, it means there may be HTML in clipboard event, but fail to retrieve */ - rawHtml: string; + rawHtml: string | null | undefined; /** * Link Preview information provided by Edge @@ -29,7 +29,12 @@ export default interface ClipboardData { /** * Image file from clipboard event */ - image: File; + image: File | null; + + /** + * General file from clipboard event + */ + files?: File[]; /** * Html extracted from raw html string and remove content before and after fragment tag @@ -44,7 +49,7 @@ export default interface ClipboardData { /** * BASE64 encoded data uri of the image if any */ - imageDataUri?: string; + imageDataUri?: string | null; /** * Array of tag names of the first level child nodes diff --git a/packages/roosterjs-editor-types/lib/interface/ContentChangedData.ts b/packages/roosterjs-editor-types/lib/interface/ContentChangedData.ts new file mode 100644 index 000000000000..9b17977cacdc --- /dev/null +++ b/packages/roosterjs-editor-types/lib/interface/ContentChangedData.ts @@ -0,0 +1,18 @@ +import { EntityState } from './Snapshot'; + +/** + * Property that is going to store additional data related to the Content Changed Event + */ +export default interface ContentChangedData { + /** + * Optional property to store the format api name when using ChangeSource.Format + */ + formatApiName?: string; + + /** + * @optional Get entity states related to the snapshot. If it returns entity states, each state will cause + * an EntityOperation event with operation = EntityOperation.UpdateEntityState when undo/redo to this snapshot + * @returns Related entity state array + */ + getEntityState?: () => EntityState[]; +} diff --git a/packages/roosterjs-editor-types/lib/interface/ContentEditFeature.ts b/packages/roosterjs-editor-types/lib/interface/ContentEditFeature.ts new file mode 100644 index 000000000000..dfa0b58c7e2d --- /dev/null +++ b/packages/roosterjs-editor-types/lib/interface/ContentEditFeature.ts @@ -0,0 +1,52 @@ +import IEditor from './IEditor'; +import { CompatiblePluginKeyboardEvent, PluginKeyboardEvent } from '../event/PluginDomEvent'; +import { PluginEvent } from '../event/PluginEvent'; + +/** + * Generic ContentEditFeature interface + */ +export interface GenericContentEditFeature { + /** + * Keys of this edit feature to handle + */ + keys: number[]; + + /** + * Check if the event should be handled by this edit feature + * @param event The plugin event to check + * @param editor The editor object + * @param ctrlOrMeta If Ctrl key (for Windows) or Meta key (for Mac) is pressed + */ + shouldHandleEvent: (event: TEvent, editor: IEditor, ctrlOrMeta: boolean) => any; + + /** + * Handle this event + * @param event The event to handle + * @param editor The editor object + */ + handleEvent: (event: TEvent, editor: IEditor) => any; + + /** + * Whether function keys (Ctrl/Meta or Alt) is allowed for this edit feature, default value is false. + * When set to false, this edit feature won't be triggered if user has pressed Ctrl/Meta/Alt key + */ + allowFunctionKeys?: boolean; +} + +/** + * ContentEditFeature interface that handles keyboard event + */ +export type ContentEditFeature = GenericContentEditFeature< + PluginKeyboardEvent | CompatiblePluginKeyboardEvent +>; + +/** + * RoosterJs build in content edit feature + */ +export interface BuildInEditFeature + extends GenericContentEditFeature { + /** + * Whether this edit feature is disabled by default + */ + defaultDisabled?: boolean; +} diff --git a/packages/roosterjs-editor-types/lib/interface/ContentEditFeatureSettings.ts b/packages/roosterjs-editor-types/lib/interface/ContentEditFeatureSettings.ts index cab779f9940d..45d50433e081 100644 --- a/packages/roosterjs-editor-types/lib/interface/ContentEditFeatureSettings.ts +++ b/packages/roosterjs-editor-types/lib/interface/ContentEditFeatureSettings.ts @@ -62,6 +62,16 @@ export interface EntityFeatureSettings { * press DELETE right after an entity */ deleteBeforeEntity: boolean; + + /** + * Content edit feature to move the cursor from Delimiters around Entities when using Right or Left Arrow Keys + */ + moveBetweenDelimitersFeature: boolean; + + /** + * Content edit Feature to trigger a Delete Entity Operation when one of the Delimiter is about to be removed with DELETE or Backspace + */ + removeEntityBetweenDelimiters: boolean; } /** @@ -113,6 +123,36 @@ export interface ListFeatureSettings { * When delete key is pressed before the first item, indent the correct list of numbers */ maintainListChainWhenDelete: boolean; + + /** + * When press space after *, -, --, ->, -->, >, => in an empty line, toggle bullet + * @default true + */ + autoBulletList: boolean; + + /** + * When press space after an number, a letter or roman number followed by ), ., -, or between parenthesis in an empty line, toggle numbering + * @default true + */ + autoNumberingList: boolean; + + /** + * MergeListOnBackspaceAfterList edit feature, provides the ability to merge list on backspace on block after a list. + * @default true + */ + mergeListOnBackspaceAfterList: boolean; + + /** + * indentWhenAltShiftRight edit feature, provides the ability to indent or outdent current list when user press Alt+shift+Right + * @default when browser is in Mac it is default disabled, else it is enabled + */ + indentWhenAltShiftRight: boolean; + + /** + * outdentWhenAltShiftLeft edit feature, provides the ability to indent or outdent current list when user press Alt+shift+Left + * @default when browser is in Mac it is default disabled, else it is enabled + */ + outdentWhenAltShiftLeft: boolean; } /** @@ -199,6 +239,63 @@ export interface TableFeatureSettings { * @default true for Chrome and safari, false for other browsers since they already have correct behavior */ upDownInTable: boolean; + + /** + * IndentTableOnTab edit feature, provides the ability to indent the table if it is all cells are selected. + */ + indentTableOnTab: boolean; + + /** + * Requires @see ExperimentalFeatures.DeleteTableWithBackspace + * Delete a table selected with the table selector pressing Backspace key + */ + deleteTableWithBackspace: boolean; +} + +/** + * Settings for text features + */ +export interface TextFeatureSettings { + /** + * Requires @see ExperimentalFeatures.TabKeyTextFeatures to be enabled + * When press Tab: + * If Whole Paragraph selected, indent paragraph, + * If range is collapsed, add spaces + * If range is not collapsed but not all the paragraph is selected, remove selection and add + * spaces + */ + indentWhenTabText: boolean; + + /** + * Requires @see ExperimentalFeatures.TabKeyTextFeatures to be enabled + * When press Tab: + * If Whole Paragraph selected, outdent paragraph + */ + outdentWhenTabText: boolean; + + /** + * @deprecated + * Requires @see ExperimentalFeatures.AutoHyphen to be enabled + * Automatically transform -- into hyphen, if typed between two words. + */ + autoHyphen: boolean; +} + +/** + * Settings for code features + */ +export interface CodeFeatureSettings { + /** + * When inside a code block, exit the code block by pressing Enter twice, or once on an empty line + * @default true + */ + removeCodeWhenEnterOnEmptyLine: boolean; + + /** + * When inside an empty code block (or an empty first line), exit the code block by pressing Backspace + * @default true + */ + removeCodeWhenBackspaceOnEmptyFirstLine: boolean; } /** @@ -213,4 +310,6 @@ export default interface ContentEditFeatureSettings ShortcutFeatureSettings, CursorFeatureSettings, MarkdownFeatureSettings, - EntityFeatureSettings {} + EntityFeatureSettings, + TextFeatureSettings, + CodeFeatureSettings {} diff --git a/packages/roosterjs-editor-types/lib/interface/ContentMetadata.ts b/packages/roosterjs-editor-types/lib/interface/ContentMetadata.ts new file mode 100644 index 000000000000..db7104a98415 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/interface/ContentMetadata.ts @@ -0,0 +1,59 @@ +import SelectionPath from './SelectionPath'; +import TableSelection from './TableSelection'; +import { SelectionRangeTypes } from '../enum/SelectionRangeTypes'; + +/** + * Common part of NormalContentMetadata and TableContentMetadata + */ +export interface ContentMetadataBase { + isDarkMode: boolean; + type: T; +} + +/** + * A content metadata is a data structure storing information other than HTML content, + * such as dark mode info, selection info, ... + * + * NormalContentMetadata is content metadata for normal selection with a start and end selection path. + * + * When do any change to this type, also need to fix function isUndoMetadata to make sure + * the check is correct + */ +export interface NormalContentMetadata + extends SelectionPath, + ContentMetadataBase {} + +/** + * A content metadata is a data structure storing information other than HTML content, + * such as dark mode info, selection info, ... + * + * TableContentMetadata is content metadata for table selection with table id and start and end coordinates + * + * When do any change to this type, also need to fix function isUndoMetadata to make sure + * the check is correct + */ +export interface TableContentMetadata + extends TableSelection, + ContentMetadataBase { + tableId: string; +} + +/** + * A content metadata is a data structure storing information other than HTML content, + * such as dark mode info, selection info, ... + * + * ImageContentMetadata is content metadata for image selection with image id + * + * When do any change to this type, also need to fix function isUndoMetadata to make sure + * the check is correct + */ +export interface ImageContentMetadata + extends ContentMetadataBase { + imageId: string; +} + +/** + * A content metadata is a data structure storing information other than HTML content, + * such as dark mode info, selection info, ... + */ +export type ContentMetadata = NormalContentMetadata | TableContentMetadata | ImageContentMetadata; diff --git a/packages/roosterjs-editor-types/lib/interface/CorePlugins.ts b/packages/roosterjs-editor-types/lib/interface/CorePlugins.ts index d87431a68a66..39ae92f5d97c 100644 --- a/packages/roosterjs-editor-types/lib/interface/CorePlugins.ts +++ b/packages/roosterjs-editor-types/lib/interface/CorePlugins.ts @@ -60,6 +60,16 @@ export default interface CorePlugins { */ readonly entity: PluginWithState; + /** + * Image selection Plugin detects image selection and help highlight the image + */ + readonly imageSelection: EditorPlugin; + + /** + * NormalizeTable plugin makes sure each table in editor has TBODY/THEAD/TFOOT tag around TR tags + */ + readonly normalizeTable: EditorPlugin; + /** * Lifecycle plugin handles editor initialization and disposing */ diff --git a/packages/roosterjs-editor-types/lib/interface/CustomReplacement.ts b/packages/roosterjs-editor-types/lib/interface/CustomReplacement.ts index 11a9fc81dbf2..807717947f2d 100644 --- a/packages/roosterjs-editor-types/lib/interface/CustomReplacement.ts +++ b/packages/roosterjs-editor-types/lib/interface/CustomReplacement.ts @@ -1,3 +1,5 @@ +import IEditor from './IEditor'; + /** * An interface to define a replacement rule for CustomReplace plugin */ @@ -20,8 +22,13 @@ export default interface CustomReplacement { /** * A callback to check if the string should be replaced * @param content the content where the string is - * @param sourceString string to be replaced + * @param replacement string to be replaced + * @param sourceEditor reference to the editor, allows for more complex replacement rules * @return true, if the string should be replaced, else return false */ - shouldReplace?: (replacement: CustomReplacement, content: string) => boolean; + shouldReplace?: ( + replacement: CustomReplacement, + content: string, + sourceEditor?: IEditor + ) => boolean; } diff --git a/packages/roosterjs-editor-types/lib/interface/DarkColorHandler.ts b/packages/roosterjs-editor-types/lib/interface/DarkColorHandler.ts new file mode 100644 index 000000000000..e8b158593a9a --- /dev/null +++ b/packages/roosterjs-editor-types/lib/interface/DarkColorHandler.ts @@ -0,0 +1,60 @@ +import ModeIndependentColor from './ModeIndependentColor'; + +/** + * Represents a combination of color key, light color and dark color, parsed from existing color value + */ +export interface ColorKeyAndValue { + /** + * Key of color, if found, otherwise undefined + */ + key?: string; + + /** + * Light mode color value + */ + lightModeColor: string; + + /** + * Dark mode color value, if found, otherwise undefined + */ + darkModeColor?: string; +} + +/** + * A handler object for dark color, used for variable-based dark color solution + */ +export default interface DarkColorHandler { + /** + * Given a light mode color value and an optional dark mode color value, register this color + * so that editor can handle it, then return the CSS color value for current color mode. + * @param lightModeColor Light mode color value + * @param isDarkMode Whether current color mode is dark mode + * @param darkModeColor Optional dark mode color value. If not passed, we will calculate one. + */ + registerColor(lightModeColor: string, isDarkMode: boolean, darkModeColor?: string): string; + + /** + * Reset known color record, clean up registered color variables. + */ + reset(): void; + + /** + * Parse an existing color value, if it is in variable-based color format, extract color key, + * light color and query related dark color if any + * @param color The color string to parse + * @param isInDarkMode Whether current content is in dark mode. When set to true, if the color value is not in dark var format, + * we will treat is as a dark mode color and try to find a matched dark mode color. + */ + parseColorValue(color: string | null | undefined, isInDarkMode?: boolean): ColorKeyAndValue; + + /** + * Get a copy of known colors + */ + getKnownColorsCopy(): Readonly[]; + + /** + * Find related light mode color from dark mode color. + * @param darkColor The existing dark color + */ + findLightColorFromDarkColor(darkColor: string): string | null; +} diff --git a/packages/roosterjs-editor-types/lib/interface/EditorCore.ts b/packages/roosterjs-editor-types/lib/interface/EditorCore.ts index 48d5367f4614..6c62bf73f6f4 100644 --- a/packages/roosterjs-editor-types/lib/interface/EditorCore.ts +++ b/packages/roosterjs-editor-types/lib/interface/EditorCore.ts @@ -1,20 +1,28 @@ import ClipboardData from './ClipboardData'; +import ContentChangedData from './ContentChangedData'; +import DarkColorHandler from './DarkColorHandler'; import EditorPlugin from './EditorPlugin'; import NodePosition from './NodePosition'; +import Rect from './Rect'; +import SelectionPath from './SelectionPath'; import TableSelection from './TableSelection'; import { ChangeSource } from '../enum/ChangeSource'; import { ColorTransformDirection } from '../enum/ColorTransformDirection'; +import { ContentMetadata } from './ContentMetadata'; import { DOMEventHandler } from '../type/domEventHandler'; import { GetContentMode } from '../enum/GetContentMode'; +import { ImageSelectionRange, SelectionRangeEx } from './SelectionRangeEx'; import { InsertOption } from './InsertOption'; import { PendableFormatState, StyleBasedFormatState } from './FormatState'; import { PluginEvent } from '../event/PluginEvent'; import { PluginState } from './CorePlugins'; -import { SelectionRangeEx } from './SelectionRangeEx'; +import { PositionType } from '../enum/PositionType'; import { SizeTransformer } from '../type/SizeTransformer'; import { TableSelectionRange } from './SelectionRangeEx'; import { TrustedHTMLHandler } from '../type/TrustedHTMLHandler'; - +import type { CompatibleChangeSource } from '../compatibleEnum/ChangeSource'; +import type { CompatibleColorTransformDirection } from '../compatibleEnum/ColorTransformDirection'; +import type { CompatibleGetContentMode } from '../compatibleEnum/GetContentMode'; /** * Represents the core data structure of an editor */ @@ -34,6 +42,11 @@ export default interface EditorCore extends PluginState { */ readonly api: CoreApiMap; + /** + * Original API map of this editor. Overridden core API can use API from this map to call the original version of core API. + */ + readonly originalApi: CoreApiMap; + /** * A handler to convert HTML string to a trust HTML string. * By default it will just return the original HTML string directly. @@ -41,12 +54,33 @@ export default interface EditorCore extends PluginState { */ readonly trustedHTMLHandler: TrustedHTMLHandler; + /* + * Current zoom scale, default value is 1 + * When editor is put under a zoomed container, need to pass the zoom scale number using this property + * to let editor behave correctly especially for those mouse drag/drop behaviors + */ + zoomScale: number; + /** - * A transformer function. It transform the size changes according to current situation. - * A typical scenario to use this function is when editor is located under a scaled container, so we need to - * calculate the scaled size change according to current zoom rate. + * @deprecated Use zoomScale instead */ sizeTransformer: SizeTransformer; + + /** + * Retrieves the Visible Viewport of the editor. + */ + getVisibleViewport: () => Rect | null; + + /** + * Color of the border of a selectedImage. Default color: '#DB626C' + */ + imageSelectionBorderColor?: string; + + /** + * Dark model handler for the editor, used for variable-based solution. + * If keep it null, editor will still use original dataset-based dark mode solution. + */ + darkColorHandler: DarkColorHandler; } /** @@ -56,12 +90,14 @@ export default interface EditorCore extends PluginState { * @param callback The editing callback, accepting current selection start and end position, returns an optional object used as the data field of ContentChangedEvent. * @param changeSource The ChangeSource string of ContentChangedEvent. @default ChangeSource.Format. Set to null to avoid triggering ContentChangedEvent * @param canUndoByBackspace True if this action can be undone when user press Backspace key (aka Auto Complete). + * @param additionalData Optional parameter to provide additional data related to the ContentChanged Event. */ export type AddUndoSnapshot = ( core: EditorCore, - callback: (start: NodePosition, end: NodePosition) => any, - changeSource: ChangeSource | string, - canUndoByBackspace: boolean + callback: ((start: NodePosition | null, end: NodePosition | null) => any) | null, + changeSource: ChangeSource | CompatibleChangeSource | string | null, + canUndoByBackspace: boolean, + additionalData?: ContentChangedData ) => void; /** @@ -86,21 +122,24 @@ export type AttachDomEvent = ( export type CreatePasteFragment = ( core: EditorCore, clipboardData: ClipboardData, - position: NodePosition, + position: NodePosition | null, pasteAsText: boolean, - applyCurrentStyle: boolean -) => DocumentFragment; + applyCurrentStyle: boolean, + pasteAsImage: boolean +) => DocumentFragment | null; /** * Ensure user will type into a container element rather than into the editor content DIV directly * @param core The EditorCore object. * @param position The position that user is about to type to * @param keyboardEvent Optional keyboard event object + * @param deprecated Deprecated parameter, not used */ export type EnsureTypeInContainer = ( core: EditorCore, position: NodePosition, - keyboardEvent?: KeyboardEvent + keyboardEvent?: KeyboardEvent, + deprecated?: boolean ) => void; /** @@ -115,7 +154,10 @@ export type Focus = (core: EditorCore) => void; * @param mode specify what kind of HTML content to retrieve * @returns HTML string representing current editor content */ -export type GetContent = (core: EditorCore, mode: GetContentMode) => string; +export type GetContent = ( + core: EditorCore, + mode: GetContentMode | CompatibleGetContentMode +) => string; /** * Get current or cached selection range @@ -123,7 +165,7 @@ export type GetContent = (core: EditorCore, mode: GetContentMode) => string; * @param tryGetFromCache Set to true to retrieve the selection range from cache if editor doesn't own the focus now * @returns A Range object of the selection range */ -export type GetSelectionRange = (core: EditorCore, tryGetFromCache: boolean) => Range; +export type GetSelectionRange = (core: EditorCore, tryGetFromCache: boolean) => Range | null; /** * Get current selection range @@ -137,7 +179,10 @@ export type GetSelectionRangeEx = (core: EditorCore) => SelectionRangeEx; * @param core The EditorCore objects * @param node The node to get style from */ -export type GetStyleBasedFormatState = (core: EditorCore, node: Node) => StyleBasedFormatState; +export type GetStyleBasedFormatState = ( + core: EditorCore, + node: Node | null +) => StyleBasedFormatState; /** * Get the pendable format such as underline and bold @@ -162,7 +207,7 @@ export type HasFocus = (core: EditorCore) => boolean; * @param core The EditorCore object. No op if null. * @param option An insert option object to specify how to insert the node */ -export type InsertNode = (core: EditorCore, node: Node, option: InsertOption) => boolean; +export type InsertNode = (core: EditorCore, node: Node, option: InsertOption | null) => boolean; /** * Restore an undo snapshot into editor @@ -171,6 +216,23 @@ export type InsertNode = (core: EditorCore, node: Node, option: InsertOption) => */ export type RestoreUndoSnapshot = (core: EditorCore, step: number) => void; +/** + * Select content according to the given information. + * There are a bunch of allowed combination of parameters. See IEditor.select for more details + * @param core The editor core object + * @param arg1 A DOM Range, or SelectionRangeEx, or NodePosition, or Node, or Selection Path + * @param arg2 (optional) A NodePosition, or an offset number, or a PositionType, or a TableSelection, or null + * @param arg3 (optional) A Node + * @param arg4 (optional) An offset number, or a PositionType + */ +export type Select = ( + core: EditorCore, + arg1: Range | SelectionRangeEx | NodePosition | Node | SelectionPath | null, + arg2?: NodePosition | number | PositionType | TableSelection | null, + arg3?: Node, + arg4?: number | PositionType +) => boolean; + /** * Change the editor selection to the given range * @param core The EditorCore object @@ -191,7 +253,8 @@ export type SelectRange = (core: EditorCore, range: Range, skipSameRange?: boole export type SetContent = ( core: EditorCore, content: string, - triggerContentChangedEvent: boolean + triggerContentChangedEvent: boolean, + metadata?: ContentMetadata ) => void; /** @@ -208,13 +271,18 @@ export type SwitchShadowEdit = (core: EditorCore, isOn: boolean) => void; * @param includeSelf True to transform the root node as well, otherwise false * @param callback The callback function to invoke before do color transformation * @param direction To specify the transform direction, light to dark, or dark to light + * @param forceTransform By default this function will only work when editor core is in dark mode. + * Pass true to this value to force do color transformation even editor core is in light mode + * @param fromDarkModel Whether the given content is already in dark mode */ export type TransformColor = ( core: EditorCore, - rootNode: Node, + rootNode: Node | null, includeSelf: boolean, - callback: () => void, - direction: ColorTransformDirection + callback: (() => void) | null, + direction: ColorTransformDirection | CompatibleColorTransformDirection, + forceTransform?: boolean, + fromDarkMode?: boolean ) => void; /** @@ -235,9 +303,20 @@ export type TriggerEvent = (core: EditorCore, pluginEvent: PluginEvent, broadcas */ export type SelectTable = ( core: EditorCore, - table: HTMLTableElement, + table: HTMLTableElement | null, coordinates?: TableSelection -) => TableSelectionRange; +) => TableSelectionRange | null; + +/** + * Select a table and save data of the selected range + * @param core The EditorCore object + * @param image image to select + * @returns true if successful + */ +export type SelectImage = ( + core: EditorCore, + image: HTMLImageElement | null +) => ImageSelectionRange | null; /** * The interface for the map of core API. @@ -279,6 +358,7 @@ export interface CoreApiMap { * @param core The EditorCore object. * @param position The position that user is about to type to * @param keyboardEvent Optional keyboard event object + * @param deprecated Deprecated parameter, not used */ ensureTypeInContainer: EnsureTypeInContainer; @@ -348,6 +428,17 @@ export interface CoreApiMap { */ restoreUndoSnapshot: RestoreUndoSnapshot; + /** + * Select content according to the given information. + * There are a bunch of allowed combination of parameters. See IEditor.select for more details + * @param core The editor core object + * @param arg1 A DOM Range, or SelectionRangeEx, or NodePosition, or Node, or Selection Path + * @param arg2 (optional) A NodePosition, or an offset number, or a PositionType, or a TableSelection, or null + * @param arg3 (optional) A Node + * @param arg4 (optional) An offset number, or a PositionType + */ + select: Select; + /** * Change the editor selection to the given range * @param core The EditorCore object @@ -381,6 +472,9 @@ export interface CoreApiMap { * @param includeSelf True to transform the root node as well, otherwise false * @param callback The callback function to invoke before do color transformation * @param direction To specify the transform direction, light to dark, or dark to light + * @param forceTransform By default this function will only work when editor core is in dark mode. + * Pass true to this value to force do color transformation even editor core is in light mode + * @param fromDarkModel Whether the given content is already in dark mode */ transformColor: TransformColor; @@ -402,4 +496,13 @@ export interface CoreApiMap { * @returns true if successful */ selectTable: SelectTable; + + /** + * Select a image and save data of the selected range + * @param core The EditorCore object + * @param image image to select + * @param imageId the id of the image element + * @returns true if successful + */ + selectImage: SelectImage; } diff --git a/packages/roosterjs-editor-types/lib/interface/EditorOptions.ts b/packages/roosterjs-editor-types/lib/interface/EditorOptions.ts index fbbd45088945..987af5123183 100644 --- a/packages/roosterjs-editor-types/lib/interface/EditorOptions.ts +++ b/packages/roosterjs-editor-types/lib/interface/EditorOptions.ts @@ -1,11 +1,14 @@ import CorePlugins from './CorePlugins'; import DefaultFormat from './DefaultFormat'; import EditorPlugin from './EditorPlugin'; +import Rect from './Rect'; +import Snapshot from './Snapshot'; import UndoSnapshotsService from './UndoSnapshotsService'; import { CoreApiMap } from './EditorCore'; import { ExperimentalFeatures } from '../enum/ExperimentalFeatures'; import { SizeTransformer } from '../type/SizeTransformer'; import { TrustedHTMLHandler } from '../type/TrustedHTMLHandler'; +import type { CompatibleExperimentalFeatures } from '../compatibleEnum/ExperimentalFeatures'; /** * The options to specify parameters customizing an editor, used by ctor of Editor class @@ -27,9 +30,16 @@ export default interface EditorOptions { defaultFormat?: DefaultFormat; /** + * @deprecated Use undoMetadataSnapshotService instead * Undo snapshot service. Use this parameter to customize the undo snapshot service. */ - undoSnapshotService?: UndoSnapshotsService; + undoSnapshotService?: UndoSnapshotsService; + + /** + * Undo snapshot service based on content metadata. Use this parameter to customize the undo snapshot service. + * When this property is set, value of undoSnapshotService will be ignored. + */ + undoMetadataSnapshotService?: UndoSnapshotsService; /** * Initial HTML content @@ -84,7 +94,7 @@ export default interface EditorOptions { /** * Specify the enabled experimental features */ - experimentalFeatures?: ExperimentalFeatures[]; + experimentalFeatures?: (ExperimentalFeatures | CompatibleExperimentalFeatures)[]; /** * By default, we will stop propagation of a printable keyboard event @@ -108,9 +118,24 @@ export default interface EditorOptions { trustedHTMLHandler?: TrustedHTMLHandler; /** - * A transformer function. It transform the size changes according to current situation. - * A typical scenario to use this function is when editor is located under a scaled container, so we need to - * calculate the scaled size change according to current zoom rate. + * Current zoom scale, @default value is 1 + * When editor is put under a zoomed container, need to pass the zoom scale number using this property + * to let editor behave correctly especially for those mouse drag/drop behaviors + */ + zoomScale?: number; + + /** + * @deprecated Use zoomScale instead */ sizeTransformer?: SizeTransformer; + + /** + * Retrieves the visible viewport of the Editor. The default viewport is the Rect of the scrollContainer. + */ + getVisibleViewport?: () => Rect | null; + + /** + * Color of the border of a selectedImage. Default color: '#DB626C' + */ + imageSelectionBorderColor?: string; } diff --git a/packages/roosterjs-editor-types/lib/interface/ExtractClipboardEventOption.ts b/packages/roosterjs-editor-types/lib/interface/ExtractClipboardEventOption.ts index 646d4ff92ff5..4ab252e002c9 100644 --- a/packages/roosterjs-editor-types/lib/interface/ExtractClipboardEventOption.ts +++ b/packages/roosterjs-editor-types/lib/interface/ExtractClipboardEventOption.ts @@ -3,7 +3,7 @@ */ export interface ExtractClipboardItemsOption { /** - * Whether retrieving value of text/link-preview is allowed + * @deprecated This feature is always enabled */ allowLinkPreview?: boolean; diff --git a/packages/roosterjs-editor-types/lib/interface/FormatState.ts b/packages/roosterjs-editor-types/lib/interface/FormatState.ts index 898cfc20adfd..350e1c577d78 100644 --- a/packages/roosterjs-editor-types/lib/interface/FormatState.ts +++ b/packages/roosterjs-editor-types/lib/interface/FormatState.ts @@ -1,4 +1,5 @@ import ModeIndependentColor from './ModeIndependentColor'; +import TableFormat from './TableFormat'; /** * Format states that can have pending state. @@ -58,11 +59,26 @@ export interface ElementBasedFormatState { */ isBlockQuote?: boolean; + /** + * Whether the text is in Code element + */ + isCodeInline?: boolean; + + /** + * Whether the text is in Code block + */ + isCodeBlock?: boolean; + /** * Whether unlink command can be called to the text */ canUnlink?: boolean; + /** + * Whether the selected text is multiline + */ + isMultilineSelection?: boolean; + /** * Whether add image alt text command can be called to the text */ @@ -72,6 +88,26 @@ export interface ElementBasedFormatState { * Header level (0-6, 0 means no header) */ headerLevel?: number; + + /** + * Whether the cursor is in table + */ + isInTable?: boolean; + + /** + * Format of table, if there is table at cursor position + */ + tableFormat?: TableFormat; + + /** + * If there is a table, whether the table has header row + */ + tableHasHeader?: boolean; + + /** + * Whether we can execute table cell merge operation + */ + canMergeTableCell?: boolean; } /** @@ -107,6 +143,31 @@ export interface StyleBasedFormatState { * Mode independent background color for dark mode */ textColors?: ModeIndependentColor; + + /** + * Line height + */ + lineHeight?: string; + + /** + * Margin Top + */ + marginTop?: string; + + /** + * Margin Bottom + */ + marginBottom?: string; + + /** + * Text Align + */ + textAlign?: string; + + /** + * Direction of the element ('ltr' or 'rtl') + */ + direction?: string; } /** @@ -131,4 +192,14 @@ export default interface FormatState extends PendableFormatState, ElementBasedFormatState, StyleBasedFormatState, - EditorUndoState {} + EditorUndoState { + /** + * Whether editor is in dark mode + */ + isDarkMode?: boolean; + + /** + * Current zoom scale of editor + */ + zoomScale?: number; +} diff --git a/packages/roosterjs-editor-types/lib/interface/HtmlSanitizerOptions.ts b/packages/roosterjs-editor-types/lib/interface/HtmlSanitizerOptions.ts index c278d335db6e..7b3c3b25c0ac 100644 --- a/packages/roosterjs-editor-types/lib/interface/HtmlSanitizerOptions.ts +++ b/packages/roosterjs-editor-types/lib/interface/HtmlSanitizerOptions.ts @@ -35,7 +35,7 @@ export default interface HtmlSanitizerOptions { * * For other unknown tags, we will respect the value of unknownTagReplacement with the same meaning */ - additionalTagReplacements?: Record; + additionalTagReplacements?: Record; /** * Allowed HTML attributes in addition to default attributes, in lower case @@ -71,8 +71,8 @@ export default interface HtmlSanitizerOptions { * Define a replacement tag name of unknown tags. * A star "*" means keep as it is, no replacement * Other valid string means replace the tag name with this string. - * Empty string, undefined or null means drop such elements and all its children + * Empty string, undefined means drop such elements and all its children * @default undefined */ - unknownTagReplacement?: string; + unknownTagReplacement?: string | null; } diff --git a/packages/roosterjs-editor-types/lib/interface/IContentTraverser.ts b/packages/roosterjs-editor-types/lib/interface/IContentTraverser.ts index 2e66bf51b4b3..322bfb36c889 100644 --- a/packages/roosterjs-editor-types/lib/interface/IContentTraverser.ts +++ b/packages/roosterjs-editor-types/lib/interface/IContentTraverser.ts @@ -8,30 +8,30 @@ export default interface IContentTraverser { /** * Get current block */ - currentBlockElement: BlockElement; + currentBlockElement: BlockElement | null; /** * Get next block element */ - getNextBlockElement(): BlockElement; + getNextBlockElement(): BlockElement | null; /** * Get previous block element */ - getPreviousBlockElement(): BlockElement; + getPreviousBlockElement(): BlockElement | null; /** * Current inline element getter */ - currentInlineElement: InlineElement; + currentInlineElement: InlineElement | null; /** * Get next inline element */ - getNextInlineElement(): InlineElement; + getNextInlineElement(): InlineElement | null; /** * Get previous inline element */ - getPreviousInlineElement(): InlineElement; + getPreviousInlineElement(): InlineElement | null; } diff --git a/packages/roosterjs-editor-types/lib/interface/IEditor.ts b/packages/roosterjs-editor-types/lib/interface/IEditor.ts index e322d8a0409a..f5ead36f91a9 100644 --- a/packages/roosterjs-editor-types/lib/interface/IEditor.ts +++ b/packages/roosterjs-editor-types/lib/interface/IEditor.ts @@ -1,9 +1,12 @@ import BlockElement from './BlockElement'; import ClipboardData from './ClipboardData'; +import ContentChangedData from './ContentChangedData'; +import DarkColorHandler from './DarkColorHandler'; import DefaultFormat from './DefaultFormat'; import IContentTraverser from './IContentTraverser'; import IPositionContentSearcher from './IPositionContentSearcher'; import NodePosition from './NodePosition'; +import Rect from './Rect'; import Region from './Region'; import SelectionPath from './SelectionPath'; import TableSelection from './TableSelection'; @@ -12,18 +15,25 @@ import { ContentPosition } from '../enum/ContentPosition'; import { DOMEventHandler } from '../type/domEventHandler'; import { EditorUndoState, PendableFormatState, StyleBasedFormatState } from './FormatState'; import { ExperimentalFeatures } from '../enum/ExperimentalFeatures'; +import { GenericContentEditFeature } from './ContentEditFeature'; import { GetContentMode } from '../enum/GetContentMode'; import { InsertOption } from './InsertOption'; import { PluginEvent } from '../event/PluginEvent'; import { PluginEventData, PluginEventFromType } from '../event/PluginEventData'; -import { PluginEventType } from '../event/PluginEventType'; -import { PluginKeyboardEvent } from '../event/PluginDomEvent'; +import { PluginEventType } from '../enum/PluginEventType'; import { PositionType } from '../enum/PositionType'; import { QueryScope } from '../enum/QueryScope'; import { RegionType } from '../enum/RegionType'; import { SelectionRangeEx } from './SelectionRangeEx'; import { SizeTransformer } from '../type/SizeTransformer'; import { TrustedHTMLHandler } from '../type/TrustedHTMLHandler'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; +import type { CompatibleChangeSource } from '../compatibleEnum/ChangeSource'; +import type { CompatibleContentPosition } from '../compatibleEnum/ContentPosition'; +import type { CompatibleExperimentalFeatures } from '../compatibleEnum/ExperimentalFeatures'; +import type { CompatibleGetContentMode } from '../compatibleEnum/GetContentMode'; +import type { CompatibleQueryScope } from '../compatibleEnum/QueryScope'; +import type { CompatibleRegionType } from '../compatibleEnum/RegionType'; /** * Interface of roosterjs editor object @@ -75,14 +85,14 @@ export default interface IEditor { * @param node The node to create InlineElement * @returns The BlockElement result */ - getBlockElementAtNode(node: Node): BlockElement; + getBlockElementAtNode(node: Node): BlockElement | null; /** * Check if the node falls in the editor content * @param node The node to check * @returns True if the given node is in editor content, otherwise false */ - contains(node: Node): boolean; + contains(node: Node | null): boolean; /** * Check if the range falls in the editor content @@ -122,7 +132,7 @@ export default interface IEditor { */ queryElements( tag: T, - scope: QueryScope, + scope: QueryScope | CompatibleQueryScope, forEachCallback?: (node: HTMLElementTagNameMap[T]) => any ): HTMLElementTagNameMap[T][]; @@ -135,7 +145,7 @@ export default interface IEditor { */ queryElements( selector: string, - scope: QueryScope, + scope: QueryScope | CompatibleQueryScope, forEachCallback?: (node: T) => any ): T[]; @@ -168,7 +178,7 @@ export default interface IEditor { * @param mode specify what kind of HTML content to retrieve * @returns HTML string representing current editor content */ - getContent(mode?: GetContentMode): string; + getContent(mode?: GetContentMode | CompatibleGetContentMode): string; /** * Set HTML content to this editor. All existing content will be replaced. A ContentChanged event will be triggered @@ -191,7 +201,7 @@ export default interface IEditor { /** * Delete selected content */ - deleteSelectedContent(): NodePosition; + deleteSelectedContent(): NodePosition | null; /** * Paste into editor using a clipboardData object @@ -199,8 +209,14 @@ export default interface IEditor { * @param pasteAsText Force pasting as plain text. Default value is false * @param applyCurrentStyle True if apply format of current selection to the pasted content, * false to keep original format. Default value is false. When pasteAsText is true, this parameter is ignored + * @param pasteAsImage: When set to true, if the clipboardData contains a imageDataUri will paste the image to the editor */ - paste(clipboardData: ClipboardData, pasteAsText?: boolean, applyCurrentFormat?: boolean): void; + paste( + clipboardData: ClipboardData, + pasteAsText?: boolean, + applyCurrentFormat?: boolean, + pasteAsImage?: boolean + ): void; //#endregion @@ -213,7 +229,7 @@ export default interface IEditor { * Default value is true * @returns current selection range, or null if editor never got focus before */ - getSelectionRange(tryGetFromCache?: boolean): Range; + getSelectionRange(tryGetFromCache?: boolean): Range | null; /** * Get current selection range from Editor. @@ -227,7 +243,7 @@ export default interface IEditor { * It does a live pull on the selection, if nothing retrieved, return whatever we have in cache. * @returns current selection path, or null if editor never got focus before */ - getSelectionPath(): SelectionPath; + getSelectionPath(): SelectionPath | null; /** * Check if focus is in editor now @@ -252,7 +268,7 @@ export default interface IEditor { * @param position The position to select * @returns True if content is selected, otherwise false */ - select(position: NodePosition): boolean; + select(position: NodePosition | null): boolean; /** * Select content by a start and end position @@ -302,14 +318,20 @@ export default interface IEditor { /** * Select content using the Table Selection * @param table to select - * @param coordinates first and last cell of the range + * @param coordinates first and last cell of the range, if null is provided will remove the selection on the table */ - select(table: HTMLTableElement, coordinates: TableSelection): boolean; + select(table: HTMLTableElement, coordinates: TableSelection | null): boolean; + + /** + * Select content SelectionRangeEx + * @param rangeEx SelectionRangeEx object to specify what to select + */ + select(rangeEx: SelectionRangeEx): boolean; /** * Get current focused position. Return null if editor doesn't have focus at this time. */ - getFocusedPosition(): NodePosition; + getFocusedPosition(): NodePosition | null; /** * Get an HTML element from current cursor position. @@ -323,7 +345,11 @@ export default interface IEditor { * @param event Optional, if specified, editor will try to get cached result from the event object first. * If it is not cached before, query from DOM and cache the result into the event object */ - getElementAtCursor(selector?: string, startFrom?: Node, event?: PluginEvent): HTMLElement; + getElementAtCursor( + selector?: string, + startFrom?: Node, + event?: PluginEvent + ): HTMLElement | null; /** * Check if this position is at beginning of the editor. @@ -336,7 +362,7 @@ export default interface IEditor { /** * Get impacted regions from selection */ - getSelectedRegions(type?: RegionType): Region[]; + getSelectedRegions(type?: RegionType | CompatibleRegionType): Region[]; //#endregion @@ -368,7 +394,7 @@ export default interface IEditor { * @returns the event object which is really passed into plugins. Some plugin may modify the event object so * the result of this function provides a chance to read the modified result */ - triggerPluginEvent( + triggerPluginEvent( eventType: T, data: PluginEventData, broadcast?: boolean @@ -379,7 +405,10 @@ export default interface IEditor { * @param source Source of this event, by default is 'SetContent' * @param data additional data for this event */ - triggerContentChangedEvent(source?: ChangeSource | string, data?: any): void; + triggerContentChangedEvent( + source?: ChangeSource | CompatibleChangeSource | string, + data?: any + ): void; //#endregion @@ -404,11 +433,13 @@ export default interface IEditor { * @param changeSource The change source to use when fire ContentChangedEvent. When the value is not null, * a ContentChangedEvent will be fired with change source equal to this value * @param canUndoByBackspace True if this action can be undone when user presses Backspace key (aka Auto Complete). + * @param additionalData Optional parameter to provide additional data related to the ContentChanged Event. */ addUndoSnapshot( - callback?: (start: NodePosition, end: NodePosition) => any, - changeSource?: ChangeSource | string, - canUndoByBackspace?: boolean + callback?: (start: NodePosition | null, end: NodePosition | null) => any, + changeSource?: ChangeSource | CompatibleChangeSource | string, + canUndoByBackspace?: boolean, + additionalData?: ContentChangedData ): void; /** @@ -461,21 +492,26 @@ export default interface IEditor { /** * Get a content traverser for current selection + * @returns A content traverser, or null if editor never got focus before and no range is provided */ - getSelectionTraverser(range?: Range): IContentTraverser; + getSelectionTraverser(range?: Range): IContentTraverser | null; /** * Get a content traverser for current block element start from specified position * @param startFrom Start position of the traverser. Default value is ContentPosition.SelectionStart + * @returns A content traverser, or null if editor never got focus before */ - getBlockTraverser(startFrom?: ContentPosition): IContentTraverser; + getBlockTraverser( + startFrom?: ContentPosition | CompatibleContentPosition + ): IContentTraverser | null; /** * Get a text traverser of current selection * @param event Optional, if specified, editor will try to get cached result from the event object first. * If it is not cached before, query from DOM and cache the result into the event object + * @returns A content traverser, or null if editor never got focus before */ - getContentSearcherOfCursor(event?: PluginEvent): IPositionContentSearcher; + getContentSearcherOfCursor(event?: PluginEvent | null): IPositionContentSearcher | null; /** * Run a callback function asynchronously @@ -489,22 +525,24 @@ export default interface IEditor { * @param name Name of the attribute * @param value Value of the attribute */ - setEditorDomAttribute(name: string, value: string): void; + setEditorDomAttribute(name: string, value: string | null): void; /** - * Get DOM attribute of editor content DIV + * Get DOM attribute of editor content DIV, null if there is no such attribute. * @param name Name of the attribute */ - getEditorDomAttribute(name: string): string; + getEditorDomAttribute(name: string): string | null; /** + * @deprecated Use getVisibleViewport() instead + * * Get current relative distance from top-left corner of the given element to top-left corner of editor content DIV. * @param element The element to calculate from. If the given element is not in editor, return value will be null * @param addScroll When pass true, The return value will also add scrollLeft and scrollTop if any. So the value * may be different than what user is seeing from the view. When pass false, scroll position will be ignored. * @returns An [x, y] array which contains the left and top distances, or null if the given element is not in editor. */ - getRelativeDistanceToEditor(element: HTMLElement, addScroll?: boolean): number[]; + getRelativeDistanceToEditor(element: HTMLElement, addScroll?: boolean): number[] | null; /** * Add a Content Edit feature. @@ -512,6 +550,12 @@ export default interface IEditor { */ addContentEditFeature(feature: GenericContentEditFeature): void; + /** + * Remove a Content Edit feature. + * @param feature The feature to remove + */ + removeContentEditFeature(feature: GenericContentEditFeature): void; + /** * Get style based format state from current selection, including font name/size and colors */ @@ -547,6 +591,17 @@ export default interface IEditor { */ isDarkMode(): boolean; + /** + * Transform the given node and all its child nodes to dark mode color if editor is in dark mode + * @param node The node to transform + */ + transformToDarkColor(node: Node): void; + + /** + * Get a darkColorHandler object for this editor. + */ + getDarkColorHandler(): DarkColorHandler; + /** * Make the editor in "Shadow Edit" mode. * In Shadow Edit mode, all format change will finally be ignored. @@ -571,7 +626,7 @@ export default interface IEditor { * Check if the given experimental feature is enabled * @param feature The feature to check */ - isFeatureEnabled(feature: ExperimentalFeatures): boolean; + isFeatureEnabled(feature: ExperimentalFeatures | CompatibleExperimentalFeatures): boolean; /** * Get a function to convert HTML string to trusted HTML string. @@ -582,59 +637,28 @@ export default interface IEditor { getTrustedHTMLHandler(): TrustedHTMLHandler; /** - * Get a transformer function. It transform the size changes according to current situation. + * Get current zoom scale, default value is 1 + * When editor is put under a zoomed container, need to pass the zoom scale number using EditorOptions.zoomScale + * to let editor behave correctly especially for those mouse drag/drop behaviors + * @returns current zoom scale number */ - getSizeTransformer(): SizeTransformer; - - //#endregion -} - -// Temporarily put these interfaces here to workaround circular dependency issue -// Need to revisit these interfaces later. - -/** - * Generic ContentEditFeature interface - */ -export interface GenericContentEditFeature { - /** - * Keys of this edit feature to handle - */ - keys: number[]; - - /** - * Check if the event should be handled by this edit feature - * @param event The plugin event to check - * @param editor The editor object - * @param ctrlOrMeta If Ctrl key (for Windows) or Meta key (for Mac) is pressed - */ - shouldHandleEvent: (event: TEvent, editor: IEditor, ctrlOrMeta: boolean) => any; + getZoomScale(): number; /** - * Handle this event - * @param event The event to handle - * @param editor The editor object + * Set current zoom scale, default value is 1 + * When editor is put under a zoomed container, need to pass the zoom scale number using EditorOptions.zoomScale + * to let editor behave correctly especially for those mouse drag/drop behaviors */ - handleEvent: (event: TEvent, editor: IEditor) => any; + setZoomScale(scale: number): void; /** - * Whether function keys (Ctrl/Meta or Alt) is allowed for this edit feature, default value is false. - * When set to false, this edit feature won't be triggered if user has pressed Ctrl/Meta/Alt key + * @deprecated Use getZoomScale() instead */ - allowFunctionKeys?: boolean; -} - -/** - * ContentEditFeature interface that handles keyboard event - */ -export type ContentEditFeature = GenericContentEditFeature; + getSizeTransformer(): SizeTransformer; -/** - * RoosterJs build in content edit feature - */ -export interface BuildInEditFeature - extends GenericContentEditFeature { /** - * Whether this edit feature is disabled by default + * Retrieves the rect of the visible viewport of the editor. */ - defaultDisabled?: boolean; + getVisibleViewport(): Rect | null; + //#endregion } diff --git a/packages/roosterjs-editor-types/lib/interface/IPositionContentSearcher.ts b/packages/roosterjs-editor-types/lib/interface/IPositionContentSearcher.ts index 3c5f9c01c339..cdcd6e659b66 100644 --- a/packages/roosterjs-editor-types/lib/interface/IPositionContentSearcher.ts +++ b/packages/roosterjs-editor-types/lib/interface/IPositionContentSearcher.ts @@ -15,13 +15,13 @@ export default interface IPositionContentSearcher { * Get the inline element before position * @returns The inlineElement before position */ - getInlineElementBefore(): InlineElement; + getInlineElementBefore(): InlineElement | null; /** * Get the inline element after position * @returns The inline element after position */ - getInlineElementAfter(): InlineElement; + getInlineElementAfter(): InlineElement | null; /** * Get X number of chars before position @@ -39,7 +39,7 @@ export default interface IPositionContentSearcher { * @param exactMatch Whether it is an exact match * @returns The range for the matched text, null if unable to find a match */ - getRangeFromText(text: string, exactMatch: boolean): Range; + getRangeFromText(text: string, exactMatch: boolean): Range | null; /** * Get text section before position till stop condition is met. @@ -55,5 +55,5 @@ export default interface IPositionContentSearcher { * Get first non textual inline element before position * @returns First non textual inline element before position or null if no such element exists */ - getNearestNonTextInlineElement(): InlineElement; + getNearestNonTextInlineElement(): InlineElement | null; } diff --git a/packages/roosterjs-editor-types/lib/interface/ImageEditOptions.ts b/packages/roosterjs-editor-types/lib/interface/ImageEditOptions.ts index a608664cf6ae..bc1dbb037ece 100644 --- a/packages/roosterjs-editor-types/lib/interface/ImageEditOptions.ts +++ b/packages/roosterjs-editor-types/lib/interface/ImageEditOptions.ts @@ -1,3 +1,7 @@ +import ModeIndependentColor from './ModeIndependentColor'; +import { ImageEditOperation } from '../enum/ImageEditOperation'; +import type { CompatibleImageEditOperation } from '../compatibleEnum/ImageEditOperation'; + /** * Options for ImageEdit plugin */ @@ -6,7 +10,7 @@ export default interface ImageEditOptions { * Color of resize/rotate border, handle and icon * @default #DB626C */ - borderColor?: string; + borderColor?: string | ModeIndependentColor; /** * Minimum resize/crop width @@ -46,4 +50,25 @@ export default interface ImageEditOptions { * @default A predefined SVG icon */ rotateIconHTML?: string; + + /** + * Whether side resizing (single direction resizing) is disabled. @default false + */ + disableSideResize?: boolean; + + /** + * Whether image rotate is disabled. @default false + */ + disableRotate?: boolean; + + /** + * Whether image crop is disabled. @default false + */ + disableCrop?: boolean; + + /** + * Which operations will be executed when image is selected + * @default ImageEditOperation.ResizeAndRotate + */ + onSelectState?: ImageEditOperation | CompatibleImageEditOperation; } diff --git a/packages/roosterjs-editor-types/lib/interface/InsertOption.ts b/packages/roosterjs-editor-types/lib/interface/InsertOption.ts index bd66257fbf3c..f2bb370eef78 100644 --- a/packages/roosterjs-editor-types/lib/interface/InsertOption.ts +++ b/packages/roosterjs-editor-types/lib/interface/InsertOption.ts @@ -1,4 +1,5 @@ import { ContentPosition } from '../enum/ContentPosition'; +import type { CompatibleContentPosition } from '../compatibleEnum/ContentPosition'; /** * Shared options for insertNode related APIs @@ -20,6 +21,13 @@ export interface InsertOptionBase { * No-op for ContentPosition.Begin, End, and Outside */ replaceSelection?: boolean; + + /** + * Boolean flag for inserting the content onto root node of current region. + * If current position is not at root of region, break parent node until insert can happen at root of region. + * This option only takes effect when insertOnNewLine is true, otherwise it will be ignored. + */ + insertToRegionRoot?: boolean; } /** @@ -31,14 +39,19 @@ export interface InsertOptionBasic extends InsertOptionBase { | ContentPosition.End | ContentPosition.DomEnd | ContentPosition.Outside - | ContentPosition.SelectionStart; + | ContentPosition.SelectionStart + | CompatibleContentPosition.Begin + | CompatibleContentPosition.End + | CompatibleContentPosition.DomEnd + | CompatibleContentPosition.Outside + | CompatibleContentPosition.SelectionStart; } /** * The Range variant where insertNode will operate on a range disjointed from the current selection state. */ export interface InsertOptionRange extends InsertOptionBase { - position: ContentPosition.Range; + position: ContentPosition.Range | CompatibleContentPosition.Range; /** * The range to be targeted when insertion happens. diff --git a/packages/roosterjs-editor-types/lib/interface/KnownEntityItem.ts b/packages/roosterjs-editor-types/lib/interface/KnownEntityItem.ts new file mode 100644 index 000000000000..60054291be83 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/interface/KnownEntityItem.ts @@ -0,0 +1,19 @@ +/** + * Represents all info of a known entity, including its DOM element, whether it is deleted and if it can be persisted + */ +export interface KnownEntityItem { + /** + * The HTML element of entity wrapper + */ + element: HTMLElement; + + /** + * Whether this entity is deleted. + */ + isDeleted?: boolean; + + /** + * Whether we want to persist this entity element during undo/redo + */ + canPersist?: boolean; +} diff --git a/packages/roosterjs-editor-types/lib/interface/SelectionRangeEx.ts b/packages/roosterjs-editor-types/lib/interface/SelectionRangeEx.ts index bb319598f03e..4fb23a010cd4 100644 --- a/packages/roosterjs-editor-types/lib/interface/SelectionRangeEx.ts +++ b/packages/roosterjs-editor-types/lib/interface/SelectionRangeEx.ts @@ -1,7 +1,13 @@ +import TableSelection from './TableSelection'; +import { SelectionRangeTypes } from '../enum/SelectionRangeTypes'; +import type { CompatibleSelectionRangeTypes } from '../compatibleEnum/SelectionRangeTypes'; + /** * Represents normal selection */ -export interface SelectionRangeExBase { +export interface SelectionRangeExBase< + T extends SelectionRangeTypes | CompatibleSelectionRangeTypes +> { /** * Selection Type definition */ @@ -22,33 +28,41 @@ export interface SelectionRangeExBase { * Represents the selection made inside of a table. */ export interface TableSelectionRange - extends SelectionRangeExBase { + extends SelectionRangeExBase< + SelectionRangeTypes.TableSelection | CompatibleSelectionRangeTypes.TableSelection + > { /** * Table that has cells selected */ table: HTMLTableElement; + /** + * Coordinates of first and last Cell + */ + coordinates: TableSelection | undefined; } /** - * Represents normal selection - */ -export interface NormalSelectionRange extends SelectionRangeExBase {} - -/** - * Types of Selection Ranges that the SelectionRangeEx can return + * Represents a selected image. */ -export const enum SelectionRangeTypes { +export interface ImageSelectionRange + extends SelectionRangeExBase< + SelectionRangeTypes.ImageSelection | CompatibleSelectionRangeTypes.ImageSelection + > { /** - * Normal selection range provided by browser. + * Selected Image */ - Normal, - /** - * Selection made inside of a single table. - */ - TableSelection, + image: HTMLImageElement; } +/** + * Represents normal selection + */ +export interface NormalSelectionRange + extends SelectionRangeExBase< + SelectionRangeTypes.Normal | CompatibleSelectionRangeTypes.Normal + > {} + /** * Types of ranges used in editor api getSelectionRangeEx */ -export type SelectionRangeEx = NormalSelectionRange | TableSelectionRange; +export type SelectionRangeEx = NormalSelectionRange | TableSelectionRange | ImageSelectionRange; diff --git a/packages/roosterjs-editor-types/lib/interface/Snapshot.ts b/packages/roosterjs-editor-types/lib/interface/Snapshot.ts new file mode 100644 index 000000000000..4586a692d427 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/interface/Snapshot.ts @@ -0,0 +1,50 @@ +import ModeIndependentColor from './ModeIndependentColor'; +import { ContentMetadata } from './ContentMetadata'; + +/** + * State for an entity. This is used for storing entity undo snapshot + */ +export interface EntityState { + /** + * Type of the entity + */ + type: string; + + /** + * Id of the entity + */ + id: string; + + /** + * The state of this entity to store into undo snapshot. + * The state can be any string, or a serialized JSON object. + * We are using string here instead of a JSON object to make sure the whole state is serializable. + */ + state: string; +} + +/** + * A serializable snapshot of editor content, including the html content and metadata + */ +export default interface Snapshot { + /** + * HTML content string + */ + html: string; + + /** + * Metadata of the editor content state + */ + metadata: ContentMetadata | null; + + /** + * Known colors for dark mode + */ + knownColors: Readonly[]; + + /** + * Entity states related to this undo snapshots. When undo/redo to this snapshot, each entity state will trigger + * an EntityOperation event with operation = EntityOperation.UpdateEntityState + */ + entityStates?: EntityState[]; +} diff --git a/packages/roosterjs-editor-types/lib/interface/Snapshots.ts b/packages/roosterjs-editor-types/lib/interface/Snapshots.ts index 30a5120af592..5d08fc911319 100644 --- a/packages/roosterjs-editor-types/lib/interface/Snapshots.ts +++ b/packages/roosterjs-editor-types/lib/interface/Snapshots.ts @@ -1,11 +1,11 @@ /** * Represents a data structure of snapshots, this is usually used for undo snapshots */ -export default interface Snapshots { +export default interface Snapshots { /** * The snapshot array */ - snapshots: string[]; + snapshots: T[]; /** * Size of all snapshots diff --git a/packages/roosterjs-editor-types/lib/interface/TableCellMetadataFormat.ts b/packages/roosterjs-editor-types/lib/interface/TableCellMetadataFormat.ts new file mode 100644 index 000000000000..8ad3db549be0 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/interface/TableCellMetadataFormat.ts @@ -0,0 +1,9 @@ +/** + * Format of table cell that stored as metadata + */ +export type TableCellMetadataFormat = { + /** + * Override default background color + */ + bgColorOverride?: boolean; +}; diff --git a/packages/roosterjs-editor-types/lib/interface/TableFormat.ts b/packages/roosterjs-editor-types/lib/interface/TableFormat.ts index a691f0bf4864..933906f91136 100644 --- a/packages/roosterjs-editor-types/lib/interface/TableFormat.ts +++ b/packages/roosterjs-editor-types/lib/interface/TableFormat.ts @@ -1,4 +1,5 @@ import { TableBorderFormat } from '../enum/TableBorderFormat'; +import type { CompatibleTableBorderFormat } from '../compatibleEnum/TableBorderFormat'; /** * Table format @@ -47,5 +48,9 @@ export default interface TableFormat { /** * Table Borders Type */ - tableBorderFormat?: TableBorderFormat; + tableBorderFormat?: TableBorderFormat | CompatibleTableBorderFormat; + /** + * If true, the new format will not overlay cells that has color applied + */ + keepCellShade?: boolean; } diff --git a/packages/roosterjs-editor-types/lib/interface/UndoSnapshotsService.ts b/packages/roosterjs-editor-types/lib/interface/UndoSnapshotsService.ts index 847dbc78d18e..a215737f50c5 100644 --- a/packages/roosterjs-editor-types/lib/interface/UndoSnapshotsService.ts +++ b/packages/roosterjs-editor-types/lib/interface/UndoSnapshotsService.ts @@ -1,7 +1,7 @@ /** * Represent an interface to provide functionalities for Undo Snapshots */ -export default interface UndoSnapshotsService { +export default interface UndoSnapshotsService { /** * Check whether can move current undo snapshot with the given step * @param step The step to check, can be positive, negative or 0 @@ -14,13 +14,13 @@ export default interface UndoSnapshotsService { * @param step The step to move * @returns If can move with the given step, returns the snapshot after move, otherwise null */ - move(step: number): string; + move(step: number): T | null; /** * Add a new undo snapshot * @param snapshot The snapshot to add */ - addSnapshot(snapshot: string, isAutoCompleteSnapshot: boolean): void; + addSnapshot(snapshot: T, isAutoCompleteSnapshot: boolean): void; /** * Clear all undo snapshots after the current one diff --git a/packages/roosterjs-editor-types/lib/interface/VCell.ts b/packages/roosterjs-editor-types/lib/interface/VCell.ts index 2bfa0accbe6b..66ea98ca58c1 100644 --- a/packages/roosterjs-editor-types/lib/interface/VCell.ts +++ b/packages/roosterjs-editor-types/lib/interface/VCell.ts @@ -5,7 +5,7 @@ export default interface VCell { /** * The table cell object. The value will be null if this is an expanded virtual cell */ - td?: HTMLTableCellElement; + td?: HTMLTableCellElement | null; /** * Whether this cell is spanned from left diff --git a/packages/roosterjs-editor-types/lib/interface/index.ts b/packages/roosterjs-editor-types/lib/interface/index.ts new file mode 100644 index 000000000000..93fd12d35ea3 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/interface/index.ts @@ -0,0 +1,125 @@ +export { default as BlockElement } from './BlockElement'; +export { default as ClipboardData } from './ClipboardData'; +export { default as ContextMenuProvider } from './ContextMenuProvider'; +export { default as CustomData } from './CustomData'; +export { default as ContentChangedData } from './ContentChangedData'; +export { default as DefaultFormat } from './DefaultFormat'; +export { default as Entity } from './Entity'; +export { + default as FormatState, + PendableFormatState, + ElementBasedFormatState, + StyleBasedFormatState, + EditorUndoState, +} from './FormatState'; +export { + default as ExtractClipboardEventOption, + ExtractClipboardItemsOption, + ExtractClipboardItemsForIEOptions, +} from './ExtractClipboardEventOption'; +export { default as IContentTraverser } from './IContentTraverser'; +export { default as InlineElement } from './InlineElement'; +export { + InsertOption, + InsertOptionBase, + InsertOptionBasic, + InsertOptionRange, +} from './InsertOption'; +export { default as IPositionContentSearcher } from './IPositionContentSearcher'; +export { default as LinkData } from './LinkData'; +export { default as ModeIndependentColor } from './ModeIndependentColor'; +export { default as NodePosition } from './NodePosition'; +export { default as Rect } from './Rect'; +export { default as Region } from './Region'; +export { default as RegionBase } from './RegionBase'; +export { default as SelectionPath } from './SelectionPath'; +export { default as Snapshots } from './Snapshots'; +export { + ContentMetadataBase, + NormalContentMetadata, + TableContentMetadata, + ImageContentMetadata, + ContentMetadata, +} from './ContentMetadata'; +export { default as Snapshot, EntityState } from './Snapshot'; +export { default as TableFormat } from './TableFormat'; +export { TableCellMetadataFormat } from './TableCellMetadataFormat'; +export { default as TableSelection } from './TableSelection'; +export { default as Coordinates } from './Coordinates'; +export { default as HtmlSanitizerOptions } from './HtmlSanitizerOptions'; +export { default as SanitizeHtmlOptions } from './SanitizeHtmlOptions'; +export { default as TargetWindowBase } from './TargetWindowBase'; +export { default as TargetWindow } from './TargetWindow'; +export { default as IEditor } from './IEditor'; +export { default as DarkColorHandler, ColorKeyAndValue } from './DarkColorHandler'; +export { + ContentEditFeature, + GenericContentEditFeature, + BuildInEditFeature, +} from './ContentEditFeature'; +export { default as EditorPlugin } from './EditorPlugin'; +export { default as PluginWithState } from './PluginWithState'; +export { + default as CorePlugins, + PluginKey, + KeyOfStatePlugin, + GenericPluginState, + PluginState, + StatePluginKeys, + TypeOfStatePlugin, +} from './CorePlugins'; +export { + default as EditorCore, + AddUndoSnapshot, + AttachDomEvent, + CoreApiMap, + CreatePasteFragment, + EnsureTypeInContainer, + Focus, + GetContent, + GetSelectionRange, + GetSelectionRangeEx, + GetStyleBasedFormatState, + GetPendableFormatState, + HasFocus, + InsertNode, + RestoreUndoSnapshot, + Select, + SelectRange, + SetContent, + SwitchShadowEdit, + TransformColor, + TriggerEvent, + SelectTable, + SelectImage, +} from './EditorCore'; +export { default as EditorOptions } from './EditorOptions'; +export { + default as ContentEditFeatureSettings, + AutoLinkFeatureSettings, + CursorFeatureSettings, + EntityFeatureSettings, + ListFeatureSettings, + MarkdownFeatureSettings, + QuoteFeatureSettings, + ShortcutFeatureSettings, + StructuredNodeFeatureSettings, + TableFeatureSettings, + TextFeatureSettings, + CodeFeatureSettings, +} from './ContentEditFeatureSettings'; +export { default as CustomReplacement } from './CustomReplacement'; +export { default as UndoSnapshotsService } from './UndoSnapshotsService'; +export { default as PickerDataProvider } from './PickerDataProvider'; +export { default as PickerPluginOptions } from './PickerPluginOptions'; +export { default as VCell } from './VCell'; +export { default as ImageEditOptions } from './ImageEditOptions'; +export { default as CreateElementData } from './CreateElementData'; +export { + SelectionRangeExBase, + NormalSelectionRange, + TableSelectionRange, + ImageSelectionRange, + SelectionRangeEx, +} from './SelectionRangeEx'; +export { KnownEntityItem } from './KnownEntityItem'; diff --git a/packages/roosterjs-editor-types/lib/type/CoreCreator.ts b/packages/roosterjs-editor-types/lib/type/CoreCreator.ts new file mode 100644 index 000000000000..98cbab5b77e8 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/type/CoreCreator.ts @@ -0,0 +1,12 @@ +import EditorCore from '../interface/EditorCore'; +import EditorOptions from '../interface/EditorOptions'; + +/** + * Type of Editor Core Creator + * @param contentDiv The DIV HTML element which will be the container element of editor + * @param options An optional options object to customize the editor + */ +export type CoreCreator = ( + contentDiv: HTMLDivElement, + options: TEditorOptions +) => TEditorCore; diff --git a/packages/roosterjs-editor-types/lib/type/Definition.ts b/packages/roosterjs-editor-types/lib/type/Definition.ts new file mode 100644 index 000000000000..20756f7e6ad9 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/type/Definition.ts @@ -0,0 +1,140 @@ +import { DefinitionType } from '../enum/DefinitionType'; +import type { CompatibleDefinitionType } from '../compatibleEnum/DefinitionType'; + +/** + * A type template to get item type of an array + */ +export type ArrayItemType = T extends (infer U)[] ? U : never; + +/** + * Base interface of property definition + */ +export interface DefinitionBase { + /** + * Type of this property + */ + type: T; + + /** + * Whether this property is optional + */ + isOptional?: boolean; + + /** + * Whether this property is allowed to be null + */ + allowNull?: boolean; +} + +/** + * String property definition. This definition can also be used for string based enum property + */ +export interface StringDefinition + extends DefinitionBase { + /** + * An optional value of this property. When specified, the given property must have exactly same value of this value + */ + value?: string; +} + +/** + * Number property definition. This definition can also be used for number based enum property + */ +export interface NumberDefinition + extends DefinitionBase { + /** + * An optional value of this property. When specified, the given property must have same value of this value + */ + value?: number; + + /** + * An optional minimum value of this property. When specified, the given property must be greater or equal to this value + */ + minValue?: number; + + /** + * An optional maximum value of this property. When specified, the given property must be less or equal to this value + */ + maxValue?: number; +} + +/** + * Boolean property definition + */ +export interface BooleanDefinition + extends DefinitionBase { + /** + * An optional value of this property. When specified, the given property must have same value of this value + */ + value?: boolean; +} + +/** + * Array property definition. + */ +export interface ArrayDefinition + extends DefinitionBase { + /** + * Definition of each item of this array. All items of the given array must have the same type. Otherwise, use CustomizeDefinition instead. + */ + itemDef: Definition>; + + /** + * An optional minimum length of this array. When specified, the given array must have at least this value of items + */ + minLength?: number; + + /** + * An optional maximum length of this array. When specified, the given array must have at most this value of items + */ + maxLength?: number; +} + +/** + * Object property definition type used by Object Definition + */ +export type ObjectPropertyDefinition = { + [Key in keyof T]: Definition; +}; + +/** + * Object property definition. + */ +export interface ObjectDefinition + extends DefinitionBase { + /** + * A key-value map to specify the definition of each possible property of this object + */ + propertyDef: ObjectPropertyDefinition; +} + +/** + * Customize property definition. When all other property definition type cannot satisfy your requirement, + * use this definition with a customized validator function to do property validation. + */ +export interface CustomizeDefinition + extends DefinitionBase { + /** + * The customized validator function to do customized validation + * @param input The value to validate + * @returns True means the given value is of the specified type, otherwise false + */ + validator: (input: any) => boolean; +} + +/** + * A combination of all definition types + */ +export type Definition = + | CustomizeDefinition + | (T extends any[] + ? ArrayDefinition + : T extends Record + ? ObjectDefinition + : T extends String + ? StringDefinition + : T extends Number + ? NumberDefinition + : T extends Boolean + ? BooleanDefinition + : never); diff --git a/packages/roosterjs-editor-types/lib/type/domEventHandler.ts b/packages/roosterjs-editor-types/lib/type/domEventHandler.ts index 4471a00b301a..d3e57e9fb914 100644 --- a/packages/roosterjs-editor-types/lib/type/domEventHandler.ts +++ b/packages/roosterjs-editor-types/lib/type/domEventHandler.ts @@ -1,9 +1,10 @@ -import { PluginEventType } from '../event/PluginEventType'; +import { PluginEventType } from '../enum/PluginEventType'; +import type { CompatiblePluginEventType } from '../compatibleEnum/PluginEventType'; /** * Handler function type of DOM event */ -export type DOMEventHandlerFunction = (event: Event) => void; +export type DOMEventHandlerFunction = (event: E) => void; /** * DOM event handler object with mapped plugin event type and handler function @@ -12,16 +13,20 @@ export interface DOMEventHandlerObject { /** * Type of plugin event. The DOM event will be mapped with this plugin event type */ - pluginEventType: PluginEventType; + pluginEventType: PluginEventType | CompatiblePluginEventType | null; /** * Handler function. Besides the mapped plugin event type, this function will also be triggered * when correlated DOM event is fired */ - beforeDispatch: DOMEventHandlerFunction; + beforeDispatch: DOMEventHandlerFunction | null; } /** * Combined event handler type with all 3 possibilities */ -export type DOMEventHandler = PluginEventType | DOMEventHandlerFunction | DOMEventHandlerObject; +export type DOMEventHandler = + | PluginEventType + | CompatiblePluginEventType + | DOMEventHandlerFunction + | DOMEventHandlerObject; diff --git a/packages/roosterjs-editor-types/lib/type/index.ts b/packages/roosterjs-editor-types/lib/type/index.ts new file mode 100644 index 000000000000..303148266c7f --- /dev/null +++ b/packages/roosterjs-editor-types/lib/type/index.ts @@ -0,0 +1,26 @@ +export { + AttributeCallback, + AttributeCallbackMap, + CssStyleCallback, + CssStyleCallbackMap, + ElementCallback, + StringMap, + ElementCallbackMap, + PredefinedCssMap, +} from './htmlSanitizerCallbackTypes'; +export { DOMEventHandlerFunction, DOMEventHandlerObject, DOMEventHandler } from './domEventHandler'; +export { TrustedHTMLHandler } from './TrustedHTMLHandler'; +export { SizeTransformer } from './SizeTransformer'; +export { + ArrayItemType, + DefinitionBase, + StringDefinition, + NumberDefinition, + BooleanDefinition, + ArrayDefinition, + ObjectDefinition, + ObjectPropertyDefinition, + CustomizeDefinition, + Definition, +} from './Definition'; +export { CoreCreator } from './CoreCreator'; diff --git a/packages/roosterjs-editor-types/package.json b/packages/roosterjs-editor-types/package.json index 6ca2beea64d9..fa5cbaf9528e 100644 --- a/packages/roosterjs-editor-types/package.json +++ b/packages/roosterjs-editor-types/package.json @@ -1,8 +1,6 @@ { "name": "roosterjs-editor-types", "description": "Type definition for roosterjs", - "dependencies": { - "@types/dom-inputevent": "^1.0.3" - }, + "dependencies": {}, "main": "./lib/index.ts" } diff --git a/packages/roosterjs-editor-types/tsconfig.child.json b/packages/roosterjs-editor-types/tsconfig.child.json deleted file mode 100644 index fd30cc051bbd..000000000000 --- a/packages/roosterjs-editor-types/tsconfig.child.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "compilerOptions": { - "strict": true - }, - "extends": "../tsconfig.json", - "include": ["./lib/**/*.ts"] -} diff --git a/packages/roosterjs/lib/index.ts b/packages/roosterjs/lib/index.ts index ca0a71f3104c..90fe816fd436 100644 --- a/packages/roosterjs/lib/index.ts +++ b/packages/roosterjs/lib/index.ts @@ -1,5 +1,6 @@ export { default as createEditor } from './createEditor'; export * from 'roosterjs-editor-types'; +export * from 'roosterjs-editor-types-compatible'; export * from 'roosterjs-editor-dom'; export * from 'roosterjs-editor-core'; export * from 'roosterjs-editor-api'; diff --git a/packages/roosterjs/package.json b/packages/roosterjs/package.json index bf4253c854d9..ae2fc36ad59b 100644 --- a/packages/roosterjs/package.json +++ b/packages/roosterjs/package.json @@ -2,7 +2,9 @@ "name": "roosterjs", "description": "A simple facade for all roosterjs code", "dependencies": { + "tslib": "^2.3.1", "roosterjs-editor-types": "", + "roosterjs-editor-types-compatible": "", "roosterjs-editor-dom": "", "roosterjs-editor-core": "", "roosterjs-editor-api": "", diff --git a/packages/roosterjs/tsconfig.child.json b/packages/roosterjs/tsconfig.child.json deleted file mode 100644 index 77150c34d12d..000000000000 --- a/packages/roosterjs/tsconfig.child.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "strict": false - }, - "extends": "../tsconfig.json", - "include": ["./lib/**/*.ts"], - "references": [ - { "path": "../roosterjs-editor-types/tsconfig.child.json" }, - { "path": "../roosterjs-editor-dom/tsconfig.child.json" }, - { "path": "../roosterjs-editor-core/tsconfig.child.json" }, - { "path": "../roosterjs-editor-api/tsconfig.child.json" }, - { "path": "../roosterjs-editor-plugins/tsconfig.child.json" }, - { "path": "../roosterjs-color-utils/tsconfig.child.json" } - ] -} diff --git a/packages/tsconfig.json b/packages/tsconfig.json index 46fe297a2890..8bab19b7bcb3 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "strict": true, "target": "es5", "module": "commonjs", "outDir": "../dist", @@ -10,57 +11,14 @@ "noImplicitAny": true, "preserveConstEnums": true, "noUnusedLocals": true, + "downlevelIteration": true, + "importHelpers": true, "baseUrl": ".", "paths": { "*": ["*"] }, "rootDir": ".", - "composite": true, "lib": ["es6", "dom"] }, - - "references": [ - { - "path": "roosterjs-editor-types/tsconfig.child.json" - }, - { - "path": "roosterjs-editor-dom/tsconfig.child.json" - }, - { - "path": "roosterjs-editor-core/tsconfig.child.json" - }, - { - "path": "roosterjs-editor-api/tsconfig.child.json" - }, - { - "path": "roosterjs-editor-plugins/tsconfig.child.json" - }, - { - "path": "roosterjs-color-utils/tsconfig.child.json" - }, - { - "path": "roosterjs/tsconfig.child.json" - } - ], - "include": [], - "typedocOptions": { - "entryPoints": [ - "roosterjs-editor-types/lib/index.ts", - "roosterjs-editor-dom/lib/index.ts", - "roosterjs-editor-core/lib/index.ts", - "roosterjs-editor-api/lib/index.ts", - "roosterjs-editor-plugins/lib/index.ts", - "roosterjs-color-utils/lib/index.ts", - "roosterjs/lib/index.ts" - ], - "plugin": ["typedoc-plugin-external-module-map"], - "out": "../dist/deploy/docs", - "readme": "../reference.md", - "name": "RoosterJs API Reference", - "excludeExternals": true, - "exclude": "**/*.d.ts", - "excludePrivate": true, - "includeVersion": true, - "external-modulemap": ".*\\/(roosterjs[a-zA-Z0-9\\-]*)\\/lib\\/" - } + "include": ["./*/lib/**/*.ts"] } diff --git a/tools/build.js b/tools/build.js index d53da47d7a11..9c5b14f27067 100644 --- a/tools/build.js +++ b/tools/build.js @@ -2,7 +2,6 @@ // Utilities const ProgressBar = require('progress'); -const { mainPackageJson } = require('./buildTools/common'); // Steps const tslintStep = require('./buildTools/tslint'); @@ -10,6 +9,7 @@ const checkDependencyStep = require('./buildTools/checkDependency'); const cleanStep = require('./buildTools/clean'); const normalizeStep = require('./buildTools/normalize'); const buildAmdStep = require('./buildTools/buildAmd'); +const buildMjsStep = require('./buildTools/buildMjs'); const buildCommonJsStep = require('./buildTools/buildCommonJs'); const pack = require('./buildTools/pack'); const dts = require('./buildTools/dts'); @@ -18,17 +18,24 @@ const buildDocumentStep = require('./buildTools/buildDocument'); const publishStep = require('./buildTools/publish'); const allTasks = [ tslintStep, - checkDependencyStep, cleanStep, normalizeStep, + checkDependencyStep, buildAmdStep, + buildMjsStep, buildCommonJsStep, pack.commonJsDebug, pack.commonJsProduction, pack.amdDebug, pack.amdProduction, + pack.commonJsDebugUi, + pack.commonJsProductionUi, + pack.amdDebugUi, + pack.amdProductionUi, dts.dtsCommonJs, dts.dtsAmd, + dts.dtsCommonJsUi, + dts.dtsAmdUi, buildDemoStep, buildDocumentStep, publishStep, @@ -36,18 +43,19 @@ const allTasks = [ // Commands const commands = [ + 'tslint', // Run tslint to check code style 'checkdep', // Check circular dependency among files 'clean', // Clean target folder - 'dts', // Generate type definition files (.d.ts) - 'tslint', // Run tslint to check code style 'normalize', // Normalize package.json files + 'buildamd', // Build in AMD mode + 'buildmjs', // Build in ESM/MJS mode + 'buildcommonjs', // Build in CommonJs mode 'pack', // Run webpack to generate standalone .js files 'packprod', // Run webpack to generate standalone .js files in production mode + 'dts', // Generate type definition files (.d.ts) 'builddemo', // Build the demo site - 'buildcommonjs', // Build in CommonJs mode - 'buildamd', // Build in AMD mode - 'publish', // Publish roosterjs packages to npm 'builddoc', // Build documents + 'publish', // Publish roosterjs packages to npm ]; class Runner { @@ -78,7 +86,7 @@ class Runner { run() { (async () => { - console.log(`Start building roosterjs version ${mainPackageJson.version}\n`); + console.log(`Start building roosterjs\n`); const bar = this.getUI(); diff --git a/tools/buildTools/buildAmd.js b/tools/buildTools/buildAmd.js index b31e26a61890..edeebf2bfe3e 100644 --- a/tools/buildTools/buildAmd.js +++ b/tools/buildTools/buildAmd.js @@ -3,23 +3,32 @@ const path = require('path'); const fs = require('fs'); const { - rootPath, packagesPath, + packagesUiPath, nodeModulesPath, - packages, + allPackages, distPath, runNode, } = require('./common'); function buildAmd() { const typescriptPath = path.join(nodeModulesPath, 'typescript/lib/tsc.js'); - const tsconfigPath = path.join(packagesPath, 'tsconfig.build.json'); + + runNode( + typescriptPath + + ` -p ${path.join(packagesPath, 'tsconfig.json')} -t es5 --moduleResolution node -m amd`, + packagesPath + ); runNode( - typescriptPath + ` -p ${tsconfigPath} -t es5 --moduleResolution node -m amd`, + typescriptPath + + ` -p ${path.join( + packagesUiPath, + 'tsconfig.json' + )} -t es5 --moduleResolution node -m amd`, packagesPath ); - packages.forEach(packageName => { + allPackages.forEach(packageName => { const packagePath = path.join(distPath, packageName); fs.renameSync(`${packagePath}/lib`, `${packagePath}/lib-amd`); }); diff --git a/tools/buildTools/buildCommonJs.js b/tools/buildTools/buildCommonJs.js index bd7042672157..97122bceceec 100644 --- a/tools/buildTools/buildCommonJs.js +++ b/tools/buildTools/buildCommonJs.js @@ -8,14 +8,23 @@ const { nodeModulesPath, distPath, packagesPath, - packages, + packagesUiPath, + allPackages, } = require('./common'); function buildCommonJs() { const typescriptPath = path.join(nodeModulesPath, 'typescript/lib/tsc.js'); - runNode(typescriptPath + ` --build`, packagesPath); - packages.forEach(packageName => { + runNode( + typescriptPath + + ` -p ${path.join( + packagesPath, + 'tsconfig.json' + )} -t es5 --moduleResolution node -m commonjs` + ); + runNode(typescriptPath, packagesUiPath); + + allPackages.forEach(packageName => { const copy = fileName => { const source = path.join(rootPath, fileName); const target = path.join(distPath, packageName, fileName); diff --git a/tools/buildTools/buildDemo.js b/tools/buildTools/buildDemo.js index 0e329dea6453..9663d0114d6a 100644 --- a/tools/buildTools/buildDemo.js +++ b/tools/buildTools/buildDemo.js @@ -2,43 +2,44 @@ const path = require('path'); const fs = require('fs'); -const webpack = require('webpack'); const { rootPath, nodeModulesPath, packagesPath, deployPath, roosterJsDistPath, - packages, - runNode, - mainPackageJson, + packagesUiPath, + roosterJsUiDistPath, + runWebPack, + getWebpackExternalCallback, } = require('./common'); async function buildDemoSite() { const sourcePathRoot = path.join(rootPath, 'demo'); const sourcePath = path.join(sourcePathRoot, 'scripts'); - const typescriptPath = path.join(nodeModulesPath, 'typescript/lib/tsc.js'); - - runNode(typescriptPath + ' --noEmit ', sourcePath); - - const distPathRoot = path.join(deployPath); const filename = 'demo.js'; const webpackConfig = { entry: path.join(sourcePath, 'index.ts'), devtool: 'source-map', output: { filename, - path: distPathRoot, + path: deployPath, }, resolve: { extensions: ['.ts', '.tsx', '.js', '.svg', '.scss', '.'], - modules: [sourcePath, packagesPath, nodeModulesPath], + modules: [sourcePath, packagesPath, packagesUiPath, nodeModulesPath], }, module: { rules: [ { test: /\.tsx?$/, loader: 'ts-loader', + options: { + compilerOptions: { + downlevelIteration: true, + importHelpers: true, + }, + }, }, { test: /\.svg$/, @@ -63,16 +64,10 @@ async function buildDemoSite() { }, ], }, - externals: packages.reduce( - (externals, packageName) => { - externals[packageName] = 'roosterjs'; - return externals; - }, - { - react: 'React', - 'react-dom': 'ReactDOM', - } - ), + externals: getWebpackExternalCallback([ + [/^roosterjs-editor-plugins\/.*$/, 'roosterjs'], + [/^rosterjs-react\/.*$/, 'roosterjsReact'], + ]), stats: 'minimal', mode: 'production', optimization: { @@ -80,33 +75,28 @@ async function buildDemoSite() { }, }; - await new Promise((resolve, reject) => { - webpack(webpackConfig).run(err => { - if (err) { - reject(err); - } else { - fs.copyFileSync( - path.resolve(roosterJsDistPath, 'rooster-min.js'), - path.resolve(distPathRoot, 'rooster-min.js') - ); - fs.copyFileSync( - path.resolve(roosterJsDistPath, 'rooster-min.js.map'), - path.resolve(distPathRoot, 'rooster-min.js.map') - ); - fs.copyFileSync( - path.resolve(sourcePathRoot, 'index.html'), - path.resolve(distPathRoot, 'index.html') - ); - var outputFilename = path.join(distPathRoot, filename); - fs.writeFileSync( - outputFilename, - `window.roosterJsVer = "v${mainPackageJson.version}";` + - fs.readFileSync(outputFilename).toString() - ); - resolve(); - } - }); - }); + await runWebPack(webpackConfig); + + fs.copyFileSync( + path.resolve(roosterJsDistPath, 'rooster-min.js'), + path.resolve(deployPath, 'rooster-min.js') + ); + fs.copyFileSync( + path.resolve(roosterJsDistPath, 'rooster-min.js.map'), + path.resolve(deployPath, 'rooster-min.js.map') + ); + fs.copyFileSync( + path.resolve(roosterJsUiDistPath, 'rooster-react-min.js'), + path.resolve(deployPath, 'rooster-react-min.js') + ); + fs.copyFileSync( + path.resolve(roosterJsUiDistPath, 'rooster-react-min.js.map'), + path.resolve(deployPath, 'rooster-react-min.js.map') + ); + fs.copyFileSync( + path.resolve(sourcePathRoot, 'index.html'), + path.resolve(deployPath, 'index.html') + ); } module.exports = { diff --git a/tools/buildTools/buildDocument.js b/tools/buildTools/buildDocument.js index 335ba4f69895..6d6845357fec 100644 --- a/tools/buildTools/buildDocument.js +++ b/tools/buildTools/buildDocument.js @@ -1,11 +1,11 @@ 'use strict'; const path = require('path'); -const { rootPath, packagesPath, nodeModulesPath, runNode } = require('./common'); +const { rootPath, nodeModulesPath, runNode } = require('./common'); function buildDocument() { const config = { - tsconfig: path.join(packagesPath, 'tsconfig.json'), + tsconfig: path.join(rootPath, 'tools', 'tsconfig.doc.json'), }; let cmd = path.join(nodeModulesPath, 'typedoc/bin/typedoc'); diff --git a/tools/buildTools/buildMjs.js b/tools/buildTools/buildMjs.js new file mode 100644 index 000000000000..142c6f420395 --- /dev/null +++ b/tools/buildTools/buildMjs.js @@ -0,0 +1,44 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const { + packagesPath, + packagesUiPath, + nodeModulesPath, + allPackages, + distPath, + runNode, +} = require('./common'); + +function buildMjs() { + const typescriptPath = path.join(nodeModulesPath, 'typescript/lib/tsc.js'); + + runNode( + typescriptPath + + ` -p ${path.join( + packagesPath, + 'tsconfig.json' + )} -t es5 --moduleResolution node -m esnext`, + packagesPath + ); + runNode( + typescriptPath + + ` -p ${path.join( + packagesUiPath, + 'tsconfig.json' + )} -t es5 --moduleResolution node -m esnext`, + packagesPath + ); + + allPackages.forEach(packageName => { + const packagePath = path.join(distPath, packageName); + fs.renameSync(`${packagePath}/lib`, `${packagePath}/lib-mjs`); + }); +} + +module.exports = { + message: 'Building packages in ESNEXT mode...', + callback: buildMjs, + enabled: options => options.buildmjs, +}; diff --git a/tools/buildTools/checkDependency.js b/tools/buildTools/checkDependency.js index b7b1257d690a..83a49598a9a0 100644 --- a/tools/buildTools/checkDependency.js +++ b/tools/buildTools/checkDependency.js @@ -2,34 +2,63 @@ const path = require('path'); const fs = require('fs'); -const { packagesPath, packages, readPackageJson, err } = require('./common'); +const { allPackages, readPackageJson, findPackageRoot, err } = require('./common'); -function processFile(dir, filename, files, packageDependencies) { - if (packageDependencies.indexOf(filename) >= 0) { +function getPossibleNames(dir, objectName) { + return [ + path.join(dir, objectName), + path.join(dir, objectName + '.ts'), + path.join(dir, objectName + '.tsx'), + ]; +} + +function processFile(dir, filename, files, externalDependencies) { + if ( + externalDependencies.some(d => (typeof d === 'string' ? d == filename : d.test(filename))) + ) { return; } - const thisFilename = path.resolve(dir, !/\.ts.?$/.test(filename) ? filename + '.ts' : filename); + const thisFilename = getPossibleNames(dir, filename).filter(name => fs.existsSync(name))[0]; + + if (!thisFilename) { + err( + 'Found dependency issue when processing file ' + + filename + + ' under ' + + dir + + ': File not found' + ); + } + const index = files.indexOf(thisFilename); + files.push(thisFilename); + if (index >= 0) { - files = files.slice(index); - files.push(thisFilename); - err(`Circular dependency: \r\n${files.join(' =>\r\n')}`); + const packageNames = files.map(findPackageName).sort(); + + if (packageNames[0] == packageNames[packageNames.length - 1]) { + return; // All packages names are the same, that is allowed + } else { + err(`Cross package circular dependency: \r\n${files.join(' =>\r\n')}`); + } } var match; try { - files.push(thisFilename); var dir = path.dirname(thisFilename); var content = fs.readFileSync(thisFilename).toString(); var reg = /from\s+'([^']+)';$/gm; + while ((match = reg.exec(content))) { var nextFile = match[1]; if (nextFile) { - processFile(dir, nextFile, files.slice(), packageDependencies); + processFile(dir, nextFile, files, externalDependencies); } } + + files.pop(); } catch (e) { err( 'Found dependency issue when processing file ' + @@ -42,16 +71,41 @@ function processFile(dir, filename, files, packageDependencies) { } } +function findPackageName(filename) { + for (let i = 0; i < allPackages.length; i++) { + if (filename.indexOf(allPackages[i])) { + return allPackages[i]; + } + } + + err('Package name not found in file name: ' + filename); +} + +const GlobalAllowedCrossPackageDependency = [ + 'roosterjs-editor-types/lib/compatibleTypes', + /@fluentui\/react(\/.*)?/, +]; + function checkDependency() { - packages.forEach(packageName => { + allPackages.forEach(packageName => { + const packageRoot = findPackageRoot(packageName); + var packageJson = readPackageJson(packageName, true /*readFromSourceFolder*/); var dependencies = Object.keys(packageJson.dependencies); - processFile(packagesPath, path.join(packageName, 'lib/index'), [], dependencies); + var peerDependencies = packageJson.peerDependencies + ? Object.keys(packageJson.peerDependencies) + : []; + processFile( + packageRoot, + path.join(packageName, 'lib/index'), + [], + dependencies.concat(peerDependencies).concat(GlobalAllowedCrossPackageDependency) + ); }); } module.exports = { - message: 'Checking circular dependency...', + message: 'Checking dependency...', callback: checkDependency, enabled: options => options.checkdep, }; diff --git a/tools/buildTools/clean.js b/tools/buildTools/clean.js index 1c8fa0d3ebb1..2eb7e6f94b1e 100644 --- a/tools/buildTools/clean.js +++ b/tools/buildTools/clean.js @@ -1,11 +1,12 @@ 'use strict'; +const path = require('path'); const rimraf = require('rimraf'); -const { distPath } = require('./common'); +const { distPath, compatibleEnumPath } = require('./common'); -async function clean() { +async function cleanDir(dirName) { await new Promise((resolve, reject) => { - rimraf(distPath, err => { + rimraf(dirName, err => { if (err) { reject(err); } else { @@ -15,6 +16,11 @@ async function clean() { }); } +async function clean() { + await cleanDir(distPath); + await cleanDir(compatibleEnumPath); +} + module.exports = { message: 'Clearing destination folder...', callback: clean, diff --git a/tools/buildTools/common.js b/tools/buildTools/common.js index adcb27924d6e..5977b8b51687 100644 --- a/tools/buildTools/common.js +++ b/tools/buildTools/common.js @@ -6,18 +6,27 @@ const glob = require('glob'); const fs = require('fs'); const assign = require('object-assign'); const toposort = require('toposort'); +const webpack = require('webpack'); const rootPath = path.join(__dirname, '../..'); const packagesPath = path.join(rootPath, 'packages'); +const packagesUiPath = path.join(rootPath, 'packages-ui'); const nodeModulesPath = path.join(rootPath, 'node_modules'); const typescriptPath = path.join(nodeModulesPath, 'typescript/lib/tsc.js'); const distPath = path.join(rootPath, 'dist'); const roosterJsDistPath = path.join(distPath, 'roosterjs/dist'); +const roosterJsUiDistPath = path.join(distPath, 'roosterjs-react/dist'); const deployPath = path.join(distPath, 'deploy'); +const compatibleEnumPath = path.join( + packagesPath, + 'roosterjs-editor-types', + 'lib', + 'compatibleEnum' +); -function collectPackages() { +function collectPackages(startPath) { const packagePaths = glob.sync( - path.relative(rootPath, path.join(packagesPath, '**', 'package.json')), + path.relative(rootPath, path.join(startPath, '**', 'package.json')), { nocase: true } ); @@ -54,6 +63,8 @@ function collectPackages() { } const packages = collectPackages(packagesPath); +const packagesUI = collectPackages(packagesUiPath); +const allPackages = packages.concat(packagesUI); function runNode(command, cwd, stdio) { exec('node ' + command, { @@ -68,29 +79,92 @@ function err(message) { throw ex; } +function findPackageRoot(packageName) { + return packages.indexOf(packageName) >= 0 + ? packagesPath + : packagesUI.indexOf(packageName) >= 0 + ? packagesUiPath + : null; +} + function readPackageJson(packageName, readFromSourceFolder) { const packageJsonFilePath = path.join( - readFromSourceFolder ? packagesPath : distPath, + readFromSourceFolder ? findPackageRoot(packageName) : distPath, packageName, 'package.json' ); const content = fs.readFileSync(packageJsonFilePath); + return JSON.parse(content); } const mainPackageJson = JSON.parse(fs.readFileSync(path.join(rootPath, 'package.json'))); +async function runWebPack(config) { + return new Promise((resolve, reject) => { + webpack(config).run((err, result) => { + const compileErrors = result?.compilation?.errors || []; + + if (compileErrors.length > 0) { + reject(compileErrors); + } else if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +const NoneExternalPackageNames = [ + // For now we don't pack ContentModel code into rooster.js file, + // so need to bundle it together with demo site and anywhere it is used. + // Once ContentModel is finished, we will bundle it into rooster.js and remove from this list. + 'roosterjs-content-model', +]; + +function getWebpackExternalCallback(externalLibraryPairs) { + const externalMap = new Map([ + ['react', 'React'], + ['react-dom', 'ReactDOM'], + [/^office-ui-fabric-react(\/.*)?$/, 'FluentUIReact'], + [/^@fluentui(\/.*)?$/, 'FluentUIReact'], + ...packages.filter(x => NoneExternalPackageNames.indexOf(x) < 0).map(p => [p, 'roosterjs']), + ...externalLibraryPairs, + ]); + + return (_, request, callback) => { + for (const [key, value] of externalMap) { + if (key instanceof RegExp && key.test(request)) { + return callback(null, request.replace(key, value)); + } else if (request === key) { + return callback(null, value); + } + } + + callback(); + }; +} + module.exports = { rootPath, packagesPath, + packagesUiPath, nodeModulesPath, typescriptPath, distPath, roosterJsDistPath, + roosterJsUiDistPath, + compatibleEnumPath, deployPath, runNode, err, packages, + packagesUI, + allPackages, readPackageJson, mainPackageJson, + findPackageRoot, + runWebPack, + getWebpackExternalCallback, }; diff --git a/tools/buildTools/dts.js b/tools/buildTools/dts.js index 17faf1b61d25..84823a7cfd0d 100644 --- a/tools/buildTools/dts.js +++ b/tools/buildTools/dts.js @@ -12,11 +12,15 @@ const { nodeModulesPath, runNode, err, + packages, + roosterJsUiDistPath, + packagesUI, + getWebpackExternalCallback, } = require('./common'); const namePlaceholder = '__NAME__'; const regExportFrom = /export([^;]+)from\s+'([^']+)';/gm; -const regImportFrom = /import[^;]+from\s+'([^']+)';/gm; +const regImportFrom = /import\s+(?:type\s+)?([^;]*)\s+from\s+'([^']+)';/gm; const singleLineComment = /\/\/[^\n]*\n/g; const multiLineComment = /(^\/\*(\*(?!\/)|[^*])*\*\/\s*)/m; @@ -33,6 +37,10 @@ const regConst = /(\/\*(\*(?!\/)|[^*])*\*\/\s*)?(export\s+)?(default\s+|declare\ // 6. export[ default] |{NAMES}; const regExport = /(\/\*(\*(?!\/)|[^*])*\*\/\s*)?(export\s+)(default\s+([0-9a-zA-Z_]+)\s*,?)?(\s*{([^}]+)})?\s*;/g; +const AllowedCrossPackageImport = { + 'roosterjs-editor-types/lib/compatibleTypes': 'roosterjs-editor-types/lib/compatibleTypes.d.ts', +}; + function enqueue(queue, filename, exports) { var existingItem = queue.find(v => v.filename == filename); if (existingItem) { @@ -70,21 +78,34 @@ function parseExports(exports) { } } -function parseFrom(from, currentFileName, baseDir, projDir) { - var importFileName; - if (from[0] == '.') { +function defaultExternalHandler(_, __, callback) { + callback(); +} + +function parseFrom(from, currentFileName, baseDir, projDir, externalHandler) { + let importFileName; + let replacedName; + if (from.substr(0, 1) == '.') { var currentPath = path.dirname(currentFileName); importFileName = path.resolve(currentPath, from + '.d.ts'); } else { - importFileName = path.resolve(baseDir, from, 'lib/index.d.ts'); - if (!fs.existsSync(importFileName)) { - importFileName = path.resolve(projDir, 'node_modules', from, 'lib/index.d.ts'); - } - if (!fs.existsSync(importFileName)) { - err(`Can't resolve package name ${from} in file ${currentFileName}`); - } + (externalHandler || defaultExternalHandler)(null, from, (_, replacement) => { + if (replacement) { + replacedName = replacement; + } else if (AllowedCrossPackageImport[from]) { + importFileName = path.resolve(baseDir, AllowedCrossPackageImport[from]); + } else { + importFileName = path.resolve(baseDir, from, 'lib/index.d.ts'); + if (!fs.existsSync(importFileName)) { + importFileName = path.resolve(projDir, 'node_modules', from, 'lib/index.d.ts'); + } + if (!fs.existsSync(importFileName)) { + err(`Can't resolve package name '${from}' in file '${currentFileName}'`); + } + } + }); } - return importFileName; + return [importFileName, replacedName]; } function parsePair(content, startIndex, open, close, startLevel, until) { @@ -216,22 +237,53 @@ function parseExportFrom(content, currentFileName, queue, baseDir, projDir) { var matches; while ((matches = regExportFrom.exec(content))) { var exports = parseExports(matches[1].trim()); - var fromFileName = parseFrom(matches[2].trim(), currentFileName, baseDir, projDir); + var [fromFileName] = parseFrom(matches[2].trim(), currentFileName, baseDir, projDir); enqueue(queue, fromFileName, exports); } return content.replace(regExportFrom, ''); } -function parseImportFrom(content, currentFileName, queue, baseDir, projDir) { +function parseImportFrom(content, currentFileName, queue, baseDir, projDir, externalHandler) { var matches; + let newContent = content; while ((matches = regImportFrom.exec(content))) { - var fromFileName = parseFrom(matches[1].trim(), currentFileName, baseDir, projDir); - enqueue(queue, fromFileName); + var [fromFileName, replacedName] = parseFrom( + matches[2].trim(), + currentFileName, + baseDir, + projDir, + externalHandler + ); + + if (fromFileName) { + enqueue(queue, fromFileName); + } else { + const imports = matches[1] + .split(',') + .map(x => + x + .replace('{', '') + .replace('}', '') + .replace(/[\.\*\(\)\{\}\[\]\\]/g, '\\$&') + .trim() + ) + .filter(x => !!x); + imports.forEach(x => { + newContent = newContent.replace( + new RegExp(`(\\W|^)(${x})(\\W|$)`, 'gm'), + '$1' + replacedName + '.$2$3' + ); + }); + } } - return content.replace(regImportFrom, ''); + return newContent.replace(regImportFrom, ''); +} + +function parseEmptyExport(content) { + return content.replace(/export \{\};/g, ''); } -function process(baseDir, queue, index, projDir) { +function process(baseDir, queue, index, projDir, externalHandler) { var item = queue[index]; var currentFileName = item.filename; var file = fs.readFileSync(currentFileName); @@ -241,13 +293,18 @@ function process(baseDir, queue, index, projDir) { content = parseExportFrom(content, currentFileName, queue, baseDir, projDir); // 2. Remove imports - content = parseImportFrom(content, currentFileName, queue, baseDir, projDir); + content = parseImportFrom(content, currentFileName, queue, baseDir, projDir, externalHandler); // 3. Parse all the public elements - content = [parseClasses, parseFunctions, parseEnum, parseType, parseConst, parseExport].reduce( - (c, func) => func(c, item.elements), - content - ); + content = [ + parseClasses, + parseFunctions, + parseEnum, + parseType, + parseConst, + parseExport, + parseEmptyExport, + ].reduce((c, func) => func(c, item.elements), content); // 4. Remove single line comments content = content.replace(singleLineComment, ''); @@ -264,7 +321,7 @@ function publicElement(element) { }); } -function output(targetDir, library, isAmd, queue) { +function generateDts(library, isAmd, queue) { var version = JSON.stringify(mainPackageJson.version).replace(/"/g, ''); var content = ''; content += `// Type definitions for roosterjs (Version ${version})\r\n`; @@ -340,56 +397,98 @@ function output(targetDir, library, isAmd, queue) { } } - var filename = `${path.resolve(targetDir, 'rooster')}${isAmd ? '-amd' : ''}.d.ts`; - fs.writeFileSync(filename, content); - return filename; + return content; } -function createQueue(rootPath, baseDir, root, additionalFiles) { +function createQueue(rootPath, baseDir, root, additionalFiles, externalHandler) { var queue = []; var i = 0; // First part, process exported members enqueue(queue, path.join(baseDir, root)); for (; i < queue.length; i++) { - process(baseDir, queue, i, rootPath); + process(baseDir, queue, i, rootPath, externalHandler); } // Second part, process "local exported" members (exported from a file, but not exported from index) (additionalFiles || []).forEach(f => enqueue(queue, path.join(baseDir, f))); for (; i < queue.length; i++) { - process(baseDir, queue, i, rootPath); + process(baseDir, queue, i, rootPath, externalHandler); } return queue; } -function dts(isAmd) { - mkdirp.sync(roosterJsDistPath); +function dts(isAmd, isUi) { + const targetPath = isUi ? roosterJsUiDistPath : roosterJsDistPath; + const targetPackages = isUi ? packagesUI : ['roosterjs']; + const startFileName = isUi ? 'roosterjs-react/lib/index.d.ts' : 'roosterjs/lib/index.d.ts'; + const libraryName = isUi ? 'roosterjsReact' : 'roosterjs'; + const targetFileName = isUi ? 'rooster-react' : 'rooster'; + const externalHandler = isUi ? getWebpackExternalCallback([]) : undefined; + + mkdirp.sync(targetPath); + + let tsFiles = []; + + targetPackages.forEach(packageName => { + tsFiles = tsFiles.concat( + glob + .sync( + path.relative(rootPath, path.join(distPath, packageName, 'lib', '**', '*.d.ts')) + ) + .map(x => path.relative(distPath, x)) + ); + }); - const tsFiles = glob - .sync(path.relative(rootPath, path.join(distPath, '**', 'lib', '**', '*.d.ts')), { - nocase: true, - }) - .map(x => path.relative(distPath, x)); - const dtsQueue = createQueue(rootPath, distPath, 'roosterjs/lib/index.d.ts', tsFiles); - const filename = output(roosterJsDistPath, 'roosterjs', isAmd, dtsQueue); + const dtsQueue = createQueue(rootPath, distPath, startFileName, tsFiles, externalHandler); + const dtsContent = generateDts(libraryName, isAmd, dtsQueue); + const fileName = `${targetFileName}${isAmd ? '-amd' : ''}.d.ts`; + const fullFileName = path.join(targetPath, fileName); + + if (isUi) { + const roosterjsDtsFileName = `rooster${isAmd ? '-amd' : ''}.d.ts`; + fs.copyFileSync( + path.join(roosterJsDistPath, roosterjsDtsFileName), + path.join(targetPath, roosterjsDtsFileName) + ); + fs.writeFileSync( + fullFileName, + `/// \n/// \n\n` + + "import * as FluentUIReact from '@fluentui/react/dist/react';\n\n" + + dtsContent + ); + } else { + fs.writeFileSync(fullFileName, dtsContent); + } if (!isAmd) { const typescriptPath = path.join(nodeModulesPath, 'typescript/lib/tsc.js'); - runNode(typescriptPath + ' ' + filename + ' --noEmit', rootPath); + runNode(typescriptPath + ' ' + fullFileName + ' --noEmit', rootPath); } } module.exports = { dtsCommonJs: { - message: `Generating type definition file for CommonJs}...`, - callback: () => dts(false /*isAmd*/), + message: `Generating type definition file (rooster.d.ts) for CommonJs...`, + callback: () => dts(false /*isAmd*/, false /*isUi*/), enabled: options => options.dts, }, dtsAmd: { - message: `Generating type definition file for AMD}...`, - callback: () => dts(true /*isAmd*/), + message: `Generating type definition file (rooster-amd.d.ts) for AMD...`, + callback: () => dts(true /*isAmd*/, false /*isUi*/), + enabled: options => options.dts, + }, + dtsCommonJsUi: { + message: `Generating type definition file (rooster-react.d.ts) for CommonJs...`, + callback: () => dts(false /*isAmd*/, true /*isUi*/), + enabled: options => options.dts, + }, + dtsAmdUi: { + message: `Generating type definition file (rooster-react-amd.d.ts) for AMD...`, + callback: () => dts(true /*isAmd*/, true /*isUi*/), enabled: options => options.dts, }, }; diff --git a/tools/buildTools/generateTargetWindow.js b/tools/buildTools/generateTargetWindow.js index 32cfae5add85..247ebf6d2205 100644 --- a/tools/buildTools/generateTargetWindow.js +++ b/tools/buildTools/generateTargetWindow.js @@ -51,5 +51,5 @@ function generateTargetWindow() { module.exports = { message: 'Generating TargetWindowBase.g.ts...', callback: generateTargetWindow, - enabled: options => options.buildcommonjs || options.buildamd, + enabled: options => options.buildcommonjs || options.buildamd || options.buildmjs, }; diff --git a/tools/buildTools/normalize.js b/tools/buildTools/normalize.js index ac03fc9e4866..f0be2213101d 100644 --- a/tools/buildTools/normalize.js +++ b/tools/buildTools/normalize.js @@ -3,21 +3,29 @@ const path = require('path'); const mkdirp = require('mkdirp'); const fs = require('fs'); -const { packages, distPath, readPackageJson, mainPackageJson, err } = require('./common'); +const processConstEnum = require('./processConstEnum'); +const { + packages, + allPackages, + distPath, + readPackageJson, + mainPackageJson, + err, +} = require('./common'); function normalize() { const knownCustomizedPackages = {}; - packages.forEach(packageName => { + allPackages.forEach(packageName => { const packageJson = readPackageJson(packageName, true /*readFromSourceFolder*/); Object.keys(packageJson.dependencies).forEach(dep => { if (packageJson.dependencies[dep]) { // No op, keep the specified value } else if (knownCustomizedPackages[dep]) { - packageJson.dependencies[dep] = knownCustomizedPackages[dep]; + packageJson.dependencies[dep] = '^' + knownCustomizedPackages[dep]; } else if (packages.indexOf(dep) > -1) { - packageJson.dependencies[dep] = mainPackageJson.version; + packageJson.dependencies[dep] = '^' + mainPackageJson.version; } else if (mainPackageJson.dependencies && mainPackageJson.dependencies[dep]) { packageJson.dependencies[dep] = mainPackageJson.dependencies[dep]; } else if (!packageJson.dependencies[dep]) { @@ -44,6 +52,8 @@ function normalize() { mkdirp.sync(targetPackagePath); fs.writeFileSync(targetFileName, JSON.stringify(packageJson, null, 4)); }); + + processConstEnum(); } module.exports = { diff --git a/tools/buildTools/pack.js b/tools/buildTools/pack.js index 05f816bed92d..8716017b2634 100644 --- a/tools/buildTools/pack.js +++ b/tools/buildTools/pack.js @@ -1,34 +1,51 @@ 'use strict'; const path = require('path'); -const webpack = require('webpack'); -const { packagesPath, roosterJsDistPath, nodeModulesPath } = require('./common'); +const { + packagesPath, + roosterJsDistPath, + roosterJsUiDistPath, + nodeModulesPath, + packagesUiPath, + rootPath, + runWebPack, + getWebpackExternalCallback, +} = require('./common'); -async function pack(isProduction, isAmd, filename) { +async function pack(isProduction, isAmd, isUi, filename) { const webpackConfig = { - entry: path.join(packagesPath, 'roosterjs/lib/index.ts'), + entry: isUi + ? path.join(packagesUiPath, 'roosterjs-react/lib/index.ts') + : path.join(packagesPath, 'roosterjs/lib/index.ts'), devtool: 'source-map', output: { filename, - path: roosterJsDistPath, + path: isUi ? roosterJsUiDistPath : roosterJsDistPath, libraryTarget: isAmd ? 'amd' : undefined, - library: isAmd ? undefined : 'roosterjs', + library: isAmd ? undefined : isUi ? 'roosterjsReact' : 'roosterjs', }, resolve: { - extensions: ['.ts', '.js'], - modules: [packagesPath, nodeModulesPath], + extensions: ['.ts', '.tsx', '.js'], + modules: [packagesPath, packagesUiPath, nodeModulesPath], }, module: { rules: [ { - test: /\.ts$/, + test: /\.tsx?$/, loader: 'ts-loader', options: { - configFile: 'tsconfig.build.json', + configFile: 'tsconfig.json', + compilerOptions: { + rootDir: rootPath, + declaration: false, + downlevelIteration: true, + importHelpers: true, + }, }, }, ], }, + externals: isUi ? getWebpackExternalCallback([]) : undefined, stats: 'minimal', mode: isProduction ? 'production' : 'development', optimization: { @@ -36,29 +53,27 @@ async function pack(isProduction, isAmd, filename) { }, }; - await new Promise((resolve, reject) => { - webpack(webpackConfig).run(err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); + await runWebPack(webpackConfig); } -function createStep(isProduction, isAmd) { - const fileName = `rooster${isAmd ? '-amd' : ''}${isProduction ? '-min' : ''}.js`; +function createStep(isProduction, isAmd, isUi) { + const fileName = `rooster${isUi ? '-react' : ''}${isAmd ? '-amd' : ''}${ + isProduction ? '-min' : '' + }.js`; return { message: `Packing ${fileName}...`, - callback: async () => pack(isProduction, isAmd, fileName), + callback: async () => pack(isProduction, isAmd, isUi, fileName), enabled: options => (isProduction ? options.packprod : options.pack), }; } module.exports = { - commonJsDebug: createStep(false, false), - commonJsProduction: createStep(true, false), - amdDebug: createStep(false, true), - amdProduction: createStep(true, true), + commonJsDebug: createStep(false /*isProduction*/, false /*isAmd*/, false /*isUi*/), + commonJsProduction: createStep(true /*isProduction*/, false /*isAmd*/, false /*isUi*/), + amdDebug: createStep(false /*isProduction*/, true /*isAmd*/, false /*isUi*/), + amdProduction: createStep(true /*isProduction*/, true /*isAmd*/, false /*isUi*/), + commonJsDebugUi: createStep(false /*isProduction*/, false /*isAmd*/, true /*isUi*/), + commonJsProductionUi: createStep(true /*isProduction*/, false /*isAmd*/, true /*isUi*/), + amdDebugUi: createStep(false /*isProduction*/, true /*isAmd*/, true /*isUi*/), + amdProductionUi: createStep(true /*isProduction*/, true /*isAmd*/, true /*isUi*/), }; diff --git a/tools/buildTools/processConstEnum.js b/tools/buildTools/processConstEnum.js new file mode 100644 index 000000000000..b35c4ca55171 --- /dev/null +++ b/tools/buildTools/processConstEnum.js @@ -0,0 +1,72 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const { compatibleEnumPath } = require('./common'); + +const EnumRegex = /(^\s*\/\*(?:\*(?!\/)|[^*])*\*\/)?\W*export const enum ([A-Za-z0-9]+)\s{([^}]+)}/gm; +const CompatibleTypePrefix = 'Compatible'; + +function parseEnum(source) { + const enums = []; + + let enumMatch; + while (!!(enumMatch = EnumRegex.exec(source))) { + const enumComment = enumMatch[1] || ''; + const enumName = enumMatch[2]; + const enumContent = enumMatch[3]; + const currentEnum = { + name: enumName, + comment: enumComment, + content: enumContent, + }; + + enums.push(currentEnum); + } + + return enums; +} + +function generateCompatibleEnum(currentEnum) { + const enumName = currentEnum.name; + return `${currentEnum.comment}\r\nexport enum ${CompatibleTypePrefix}${enumName} {\r\n${currentEnum.content}}\r\n`; +} + +function generateCompatibleEnumScript(enums) { + return enums.map(generateCompatibleEnum).join('\r\n'); +} + +function processConstEnumInternal(targetPath) { + const sourceDir = targetPath.replace('compatibleEnum', 'enum'); + const fileNames = fs.readdirSync(sourceDir); + let indexTs = ''; + + fileNames.forEach(fileName => { + const fullName = path.join(sourceDir, fileName); + const content = fs.readFileSync(fullName).toString(); + const enums = parseEnum(content); + + if (enums.length > 0) { + const newContent = generateCompatibleEnumScript(enums); + + indexTs += `export { ${enums + .map(e => `${CompatibleTypePrefix}${e.name}`) + .join(', ')} } from './${fileName.replace(/\.ts$/, '')}'\r\n`; + + const newFullName = path.join(targetPath, fileName); + + fs.mkdirSync(targetPath, { recursive: true }); + fs.writeFileSync(newFullName, newContent); + } + }); + + if (indexTs) { + fs.writeFileSync(path.join(targetPath, 'index.ts'), indexTs); + } +} + +function processConstEnum() { + processConstEnumInternal(compatibleEnumPath); +} + +module.exports = processConstEnum; diff --git a/tools/buildTools/publish.js b/tools/buildTools/publish.js index 712f8ebf3ff4..24a5016adf70 100644 --- a/tools/buildTools/publish.js +++ b/tools/buildTools/publish.js @@ -3,13 +3,13 @@ const path = require('path'); const fs = require('fs'); const exec = require('child_process').execSync; -const { packages, distPath, readPackageJson } = require('./common'); +const { allPackages, distPath, readPackageJson } = require('./common'); const VersionRegex = /\d+\.\d+\.\d+(-([^\.]+)(\.\d+)?)?/; const NpmrcContent = 'registry=https://registry.npmjs.com/\n//registry.npmjs.com/:_authToken='; function publish(options) { - packages.forEach(packageName => { + allPackages.forEach(packageName => { const json = readPackageJson(packageName, false /*readFromSourceFolder*/); const localVersion = json.version; const versionMatch = VersionRegex.exec(localVersion); @@ -20,7 +20,15 @@ function publish(options) { npmVersion = exec(`npm view ${packageName}@${tagname} version`).toString().trim(); } catch (e) {} - if (localVersion != npmVersion) { + if (localVersion == '0.0.0') { + console.log( + `Skip publishing package ${packageName}, because version (${localVersion}) is not ready to publish` + ); + } else if (localVersion == npmVersion) { + console.log( + `Skip publishing package ${packageName}, because version (${npmVersion}) is not changed` + ); + } else { let npmrcName = path.join(distPath, packageName, '.npmrc'); if (options.token) { const npmrc = `${NpmrcContent}${options.token}\n`; @@ -42,10 +50,6 @@ function publish(options) { fs.unlinkSync(npmrcName); } } - } else { - console.log( - `Skip publishing package ${packageName}, because version (${npmVersion}) is not changed` - ); } }); } diff --git a/tools/karma.test.all.js b/tools/karma.test.all.js new file mode 100644 index 000000000000..236036d9ba0a --- /dev/null +++ b/tools/karma.test.all.js @@ -0,0 +1,4 @@ +var context = require.context('../packages', true, /test\/.+\.ts?$/); +var karmaTest = require('./karma.test'); + +module.exports = karmaTest(context); diff --git a/tools/karma.test.contentmodel.js b/tools/karma.test.contentmodel.js new file mode 100644 index 000000000000..45f21fb56550 --- /dev/null +++ b/tools/karma.test.contentmodel.js @@ -0,0 +1,4 @@ +var context = require.context('../packages/roosterjs-content-model', true, /test\/.+\.ts?$/); +var karmaTest = require('./karma.test'); + +module.exports = karmaTest(context); diff --git a/tools/karma.test.js b/tools/karma.test.js new file mode 100644 index 000000000000..abec3064e379 --- /dev/null +++ b/tools/karma.test.js @@ -0,0 +1,10 @@ +module.exports = function (context) { + if (!!__karma__.config.components) { + const filenameWithoutTest = __karma__.config.components.replace('Test', ''); + const filterRegExpByFilename = new RegExp(filenameWithoutTest); + const specificFiles = context.keys().filter(path => filterRegExpByFilename.test(path)); + return specificFiles.map(context); + } else { + return context.keys().map(key => context(key)); + } +}; diff --git a/tools/karma.test.roosterjs.js b/tools/karma.test.roosterjs.js new file mode 100644 index 000000000000..cebf2698b8bd --- /dev/null +++ b/tools/karma.test.roosterjs.js @@ -0,0 +1,4 @@ +var context = require.context('../packages', true, /roosterjs(-editor-\w+)?\/test\/.+\.ts?$/); +var karmaTest = require('./karma.test'); + +module.exports = karmaTest(context); diff --git a/tools/karma.test.ui.js b/tools/karma.test.ui.js new file mode 100644 index 000000000000..0a1a16c4d064 --- /dev/null +++ b/tools/karma.test.ui.js @@ -0,0 +1,4 @@ +var context = require.context('../packages-ui', true, /test\/.+\.ts?$/); +var karmaTest = require('./karma.test'); + +module.exports = karmaTest(context); diff --git a/reference.md b/tools/reference.md similarity index 62% rename from reference.md rename to tools/reference.md index 0a9257ab97cc..f3c3dc1bda8e 100644 --- a/reference.md +++ b/tools/reference.md @@ -1,35 +1,48 @@ -Welcome to RoosterJs API References! - -## Content - -Rooster contains 6 packages. - -1. [roosterjs](modules/roosterjs.html): - A facade of all Rooster code for those who want a quick start. Use the - `createEditor()` function in roosterjs to create an editor with default - configurations. - -2. [roosterjs-editor-core](modules/roosterjs_editor_core.html): - Defines the core editor and plugin infrastructure. Use `roosterjs-editor-core` - instead of `roosterjs` to build and customize your own editor. - -3. [roosterjs-editor-api](modules/roosterjs_editor_api.html): - Defines APIs for editor operations. Use these APIs to modify content and - formatting in the editor you built using `roosterjs-editor-core`. - -4. [roosterjs-editor-dom](modules/roosterjs_editor_dom.html): - Defines APIs for DOM operations. Use `roosterjs-editor-api` instead unless - you want to access DOM API directly. - -5. [roosterjs-editor-plugins](modules/roosterjs_editor_plugins.html): - Defines basic plugins for common features. Examples: making hyperlinks, - pasting HTML content, inserting inline images. - -6. [roosterjs-editor-types](modules/roosterjs_editor_types.html): - Defines public interfaces and enumerations. - -## See also - -[RoosterJs Demo Site](../index.html). - -[RoosterJs wiki](https://github.com/Microsoft/roosterjs/wiki) +Welcome to RoosterJs API References! + +## Content + +Rooster contains 6 basic packages. + +1. [roosterjs](modules/roosterjs.html): + A facade of all Rooster code for those who want a quick start. Use the + `createEditor()` function in roosterjs to create an editor with default + configurations. + +2. [roosterjs-editor-core](modules/roosterjs_editor_core.html): + Defines the core editor and plugin infrastructure. Use `roosterjs-editor-core` + instead of `roosterjs` to build and customize your own editor. + +3. [roosterjs-editor-api](modules/roosterjs_editor_api.html): + Defines APIs for editor operations. Use these APIs to modify content and + formatting in the editor you built using `roosterjs-editor-core`. + +4. [roosterjs-editor-dom](modules/roosterjs_editor_dom.html): + Defines APIs for DOM operations. Use `roosterjs-editor-api` instead unless + you want to access DOM API directly. + +5. [roosterjs-editor-plugins](modules/roosterjs_editor_plugins.html): + Defines basic plugins for common features. Examples: making hyperlinks, + pasting HTML content, inserting inline images. + +6. [roosterjs-editor-types](modules/roosterjs_editor_types.html): + Defines public interfaces and enumerations. + +There are also some extension packages to provide additional functionalities. + +1. [roosterjs-color-utils](modules/roosterjs_color_utils.html): + Provide color transformation utility to make editor work under dark mode. + +2. [roosterjs-react](modules/roosterjs_react.html): + Provide a React wrapper of roosterjs so it can be easily used with React. + +3. [roosterjs-editor-types-compatible](modules/roosterjs_editor_types_compatible.html): + Provide types that are compatible with isolatedModules mode. When using isolatedModules mode, + "const enum" will not work correctly, this package provides enums with prefix "Compatible" in + their names and they have the same value with const enums in roosterjs-editor-types package + +## See also + +[RoosterJs Demo Site](../index.html). + +[RoosterJs wiki](https://github.com/Microsoft/roosterjs/wiki) diff --git a/tools/start.js b/tools/start.js index 69606e9b0905..1434f0ab5ba7 100644 --- a/tools/start.js +++ b/tools/start.js @@ -2,6 +2,7 @@ var webpack = require('webpack'); var webpackConfig = require('../webpack.config'); var webpackDevServer = require('webpack-dev-server'); var detect = require('detect-port'); +var processConstEnum = require('./buildTools/processConstEnum'); function startWebpackDevServer() { port = webpackConfig.devServer.port; @@ -23,4 +24,5 @@ function startWebpackDevServer() { }); } +processConstEnum(); +startWebpackDevServer(); diff --git a/tools/tsconfig.doc.json b/tools/tsconfig.doc.json new file mode 100644 index 000000000000..41c6dd85b333 --- /dev/null +++ b/tools/tsconfig.doc.json @@ -0,0 +1,44 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "outDir": "dist", + "baseUrl": "..", + "jsx": "react", + "paths": { + "*": ["packages/*", "packages-ui/*"] + }, + "rootDir": "..", + "downlevelIteration": true, + "importHelpers": true, + "lib": ["es6", "dom"] + }, + "include": [ + "../packages/**/*.ts", + "../packages-ui/**/*.ts", + "../packages-ui/**/*.tsx", + "../packages/roosterjs-editor-types-compatible/**/*.ts" + ], + "typedocOptions": { + "entryPoints": [ + "../packages/roosterjs-editor-types/lib/index.ts", + "../packages/roosterjs-editor-types-compatible/lib/index.ts", + "../packages/roosterjs-editor-dom/lib/index.ts", + "../packages/roosterjs-editor-core/lib/index.ts", + "../packages/roosterjs-editor-api/lib/index.ts", + "../packages/roosterjs-editor-plugins/lib/index.ts", + "../packages/roosterjs-color-utils/lib/index.ts", + "../packages-ui/roosterjs-react/lib/index.ts", + "../packages/roosterjs/lib/index.ts" + ], + "plugin": ["typedoc-plugin-remove-references", "typedoc-plugin-external-module-map"], + "out": "../dist/deploy/docs", + "readme": "reference.md", + "name": "RoosterJs API Reference", + "excludeExternals": true, + "exclude": "../**/*.d.ts", + "excludePrivate": true, + "includeVersion": false, + "external-modulemap": ".*\\/(roosterjs[a-zA-Z0-9\\-]*)\\/lib\\/" + } +} diff --git a/tools/tsconfig.tslint.json b/tools/tsconfig.tslint.json index 15b9777beff1..68f6eebd999e 100644 --- a/tools/tsconfig.tslint.json +++ b/tools/tsconfig.tslint.json @@ -1,3 +1,9 @@ { - "include": ["../packages/**/*.ts", "../demo/scripts/**/*.ts", "../demo/scripts/**/*.tsx"] + "include": [ + "../packages/**/*.ts", + "../packages-ui/**/*.ts", + "../packages-ui/**/*.tsx", + "../demo/scripts/**/*.ts", + "../demo/scripts/**/*.tsx" + ] } diff --git a/webpack.config.js b/webpack.config.js index c85fb37944f7..8a9895fe5317 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,6 +1,13 @@ const path = require('path'); const devServerPort = 3000; +const externalMap = new Map([ + ['react', 'React'], + ['react-dom', 'ReactDOM'], + [/^office-ui-fabric-react(\/.*)?$/, 'FluentUIReact'], + [/^@fluentui(\/.*)?$/, 'FluentUIReact'], +]); + module.exports = { entry: path.join(__dirname, './demo/scripts/index.ts'), devtool: 'source-map', @@ -12,7 +19,7 @@ module.exports = { }, resolve: { extensions: ['.ts', '.tsx', '.js', '.svg', '.scss', '.'], - modules: ['./demo/scripts', 'packages', './node_modules'], + modules: ['./demo/scripts', 'packages', 'packages-ui', './node_modules'], }, mode: 'development', module: { @@ -20,6 +27,12 @@ module.exports = { { test: /\.tsx?$/, loader: 'ts-loader', + options: { + compilerOptions: { + downlevelIteration: true, + importHelpers: true, + }, + }, }, { test: /\.svg$/, @@ -44,9 +57,16 @@ module.exports = { }, ], }, - externals: { - react: 'React', - 'react-dom': 'ReactDOM', + externals: function (context, request, callback) { + for (const [key, value] of externalMap) { + if (key instanceof RegExp && key.test(request)) { + return callback(null, request.replace(key, value)); + } else if (request === key) { + return callback(null, value); + } + } + + callback(); }, watch: true, stats: 'minimal', diff --git a/yarn.lock b/yarn.lock index 40e4077033c7..0c6f0a4118f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,6950 +1,5829 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/code-frame@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" - integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA== - dependencies: - "@babel/highlight" "^7.0.0" - -"@babel/code-frame@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" - integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== - dependencies: - "@babel/highlight" "^7.10.4" - -"@babel/core@^7.7.5": - version "7.11.1" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.1.tgz#2c55b604e73a40dc21b0e52650b11c65cf276643" - integrity sha512-XqF7F6FWQdKGGWAzGELL+aCO1p+lRY5Tj5/tbT3St1G8NaH70jhhDIKknIZaDans0OQBG5wRAldROLHSt44BgQ== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.0" - "@babel/helper-module-transforms" "^7.11.0" - "@babel/helpers" "^7.10.4" - "@babel/parser" "^7.11.1" - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.11.0" - "@babel/types" "^7.11.0" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.1" - json5 "^2.1.2" - lodash "^4.17.19" - resolve "^1.3.2" - semver "^5.4.1" - source-map "^0.5.0" - -"@babel/generator@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.0.tgz#4b90c78d8c12825024568cbe83ee6c9af193585c" - integrity sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ== - dependencies: - "@babel/types" "^7.11.0" - jsesc "^2.5.1" - source-map "^0.5.0" - -"@babel/helper-function-name@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" - integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== - dependencies: - "@babel/helper-get-function-arity" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-get-function-arity@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" - integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-member-expression-to-functions@^7.10.4": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" - integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q== - dependencies: - "@babel/types" "^7.11.0" - -"@babel/helper-module-imports@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" - integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-module-transforms@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" - integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg== - dependencies: - "@babel/helper-module-imports" "^7.10.4" - "@babel/helper-replace-supers" "^7.10.4" - "@babel/helper-simple-access" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/template" "^7.10.4" - "@babel/types" "^7.11.0" - lodash "^4.17.19" - -"@babel/helper-optimise-call-expression@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" - integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-replace-supers@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" - integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.10.4" - "@babel/helper-optimise-call-expression" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-simple-access@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" - integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw== - dependencies: - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-split-export-declaration@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" - integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== - dependencies: - "@babel/types" "^7.11.0" - -"@babel/helper-validator-identifier@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" - integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== - -"@babel/helpers@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044" - integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA== - dependencies: - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/highlight@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" - integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw== - dependencies: - chalk "^2.0.0" - esutils "^2.0.2" - js-tokens "^4.0.0" - -"@babel/highlight@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" - integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1": - version "7.11.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.2.tgz#0882ab8a455df3065ea2dcb4c753b2460a24bead" - integrity sha512-Vuj/+7vLo6l1Vi7uuO+1ngCDNeVmNbTngcJFKCR/oEtz8tKz0CJxZEGmPt9KcIloZhOZ3Zit6xbpXT2MDlS9Vw== - -"@babel/runtime@^7.9.2": - version "7.9.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f" - integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/template@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" - integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/parser" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.0.tgz#9b996ce1b98f53f7c3e4175115605d56ed07dd24" - integrity sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.0" - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/parser" "^7.11.0" - "@babel/types" "^7.11.0" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.19" - -"@babel/types@^7.10.4", "@babel/types@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.0.tgz#2ae6bf1ba9ae8c3c43824e5861269871b206e90d" - integrity sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - lodash "^4.17.19" - to-fast-properties "^2.0.0" - -"@istanbuljs/schema@^0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" - integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== - -"@jsdevtools/coverage-istanbul-loader@3.0.5": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz#2a4bc65d0271df8d4435982db4af35d81754ee26" - integrity sha512-EUCPEkaRPvmHjWAAZkWMT7JDzpw7FKB00WTISaiXsbNOd5hCHg77XLA8sLYLFDo1zepYLo2w7GstN8YBqRXZfA== - dependencies: - convert-source-map "^1.7.0" - istanbul-lib-instrument "^4.0.3" - loader-utils "^2.0.0" - merge-source-map "^1.1.0" - schema-utils "^2.7.0" - -"@microsoft/load-themed-styles@1.10.44": - version "1.10.44" - resolved "https://registry.yarnpkg.com/@microsoft/load-themed-styles/-/load-themed-styles-1.10.44.tgz#55ab022a9b7790492215d3fc1b408e597bb689c8" - integrity sha512-OHLj1VT0gwkDDaWJoCsmvIu2WhNHOXudxQQJ58gJnAowR5l9c4GwJsGbqePGZ1w4h68+cEF/1vXsjTpwJiKFvg== - -"@microsoft/loader-load-themed-styles@1.8.11": - version "1.8.11" - resolved "https://registry.yarnpkg.com/@microsoft/loader-load-themed-styles/-/loader-load-themed-styles-1.8.11.tgz#e2f67dd49df10cb2f86b744b1c93cb514203bcdb" - integrity sha512-ynaXU8Mt5javarBsVwBOQCkE9KXwxzkRRpf8LtZiqB27WZwpO4nLPfDcIZxzdfoNIDvy2f7hIxVVQP4hsFecCA== - dependencies: - "@microsoft/load-themed-styles" "1.10.44" - loader-utils "~1.1.0" - -"@types/color-convert@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-2.0.0.tgz#8f5ee6b9e863dcbee5703f5a517ffb13d3ea4e22" - integrity sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ== - dependencies: - "@types/color-name" "*" - -"@types/color-name@*", "@types/color-name@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" - integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== - -"@types/color@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/color/-/color-3.0.0.tgz#40f8a6bf2fd86e969876b339a837d8ff1b0a6e30" - integrity sha512-5qqtNia+m2I0/85+pd2YzAXaTyKO8j+svirO5aN+XaQJ5+eZ8nx0jPtEWZLxCi50xwYsX10xUHetFzfb1WEs4Q== - dependencies: - "@types/color-convert" "*" - -"@types/dom-inputevent@1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@types/dom-inputevent/-/dom-inputevent-1.0.5.tgz#c880fa9b4482b49accc107e4a950117a1af7a61b" - integrity sha512-oL8NzIAn1J8vsIigjEM2qip6PUBRkb1kE+3gbM+NvSCzrScgz+Ixymuv9Z9jmktVjeHWMJc9zhP49YBUBeCTaQ== - -"@types/dompurify@2.2.3": - version "2.2.3" - resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.2.3.tgz#6e89677a07902ac1b6821c345f34bd85da239b08" - integrity sha512-CLtc2mZK8+axmrz1JqtpklO/Kvn38arGc8o1l3UVopZaXXuer9ONdZwJ/9f226GrhRLtUmLr9WrvZsRSNpS8og== - dependencies: - "@types/trusted-types" "*" - -"@types/events@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" - integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== - -"@types/glob@^7.1.1": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" - integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== - dependencies: - "@types/events" "*" - "@types/minimatch" "*" - "@types/node" "*" - -"@types/jasmine@3.5.10": - version "3.5.10" - resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.5.10.tgz#a1a41012012b5da9d4b205ba9eba58f6cce2ab7b" - integrity sha512-3F8qpwBAiVc5+HPJeXJpbrl+XjawGmciN5LgiO7Gv1pl1RHtjoMNqZpqEksaPJW05ViKe8snYInRs6xB25Xdew== - -"@types/json-schema@^7.0.4": - version "7.0.5" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" - integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== - -"@types/minimatch@*", "@types/minimatch@^3.0.3": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" - integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== - -"@types/node@*": - version "12.0.10" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.10.tgz#51babf9c7deadd5343620055fc8aff7995c8b031" - integrity sha512-LcsGbPomWsad6wmMNv7nBLw7YYYyfdYcz6xryKYQhx89c3XXan+8Q6AJ43G5XDIaklaVkK3mE4fCb0SBvMiPSQ== - -"@types/node@13.13.4": - version "13.13.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.4.tgz#1581d6c16e3d4803eb079c87d4ac893ee7501c2c" - integrity sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA== - -"@types/object-assign@4.0.30": - version "4.0.30" - resolved "https://registry.yarnpkg.com/@types/object-assign/-/object-assign-4.0.30.tgz#8949371d5a99f4381ee0f1df0a9b7a187e07e652" - integrity sha1-iUk3HVqZ9Dge4PHfCpt6GH4H5lI= - -"@types/parse-json@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" - integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== - -"@types/prop-types@*": - version "15.7.1" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.1.tgz#f1a11e7babb0c3cad68100be381d1e064c68f1f6" - integrity sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg== - -"@types/react-dom@16.9.7": - version "16.9.7" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.7.tgz#60844d48ce252d7b2dccf0c7bb937130e27c0cd2" - integrity sha512-GHTYhM8/OwUCf254WO5xqR/aqD3gC9kSTLpopWGpQLpnw23jk44RvMHsyUSEplvRJZdHxhJGMMLF0kCPYHPhQA== - dependencies: - "@types/react" "*" - -"@types/react@*": - version "16.8.22" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.22.tgz#7f18bf5ea0c1cad73c46b6b1c804a3ce0eec6d54" - integrity sha512-C3O1yVqk4sUXqWyx0wlys76eQfhrQhiDhDlHBrjER76lR2S2Agiid/KpOU9oCqj1dISStscz7xXz1Cg8+sCQeA== - dependencies: - "@types/prop-types" "*" - csstype "^2.2.0" - -"@types/trusted-types@*": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756" - integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg== - -"@webassemblyjs/ast@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" - integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA== - dependencies: - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/wast-parser" "1.9.0" - -"@webassemblyjs/floating-point-hex-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4" - integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA== - -"@webassemblyjs/helper-api-error@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2" - integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw== - -"@webassemblyjs/helper-buffer@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" - integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA== - -"@webassemblyjs/helper-code-frame@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27" - integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA== - dependencies: - "@webassemblyjs/wast-printer" "1.9.0" - -"@webassemblyjs/helper-fsm@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8" - integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw== - -"@webassemblyjs/helper-module-context@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07" - integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g== - dependencies: - "@webassemblyjs/ast" "1.9.0" - -"@webassemblyjs/helper-wasm-bytecode@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790" - integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw== - -"@webassemblyjs/helper-wasm-section@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346" - integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - -"@webassemblyjs/ieee754@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4" - integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg== - dependencies: - "@xtuc/ieee754" "^1.2.0" - -"@webassemblyjs/leb128@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95" - integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw== - dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab" - integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w== - -"@webassemblyjs/wasm-edit@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf" - integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/helper-wasm-section" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-opt" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - "@webassemblyjs/wast-printer" "1.9.0" - -"@webassemblyjs/wasm-gen@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c" - integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/ieee754" "1.9.0" - "@webassemblyjs/leb128" "1.9.0" - "@webassemblyjs/utf8" "1.9.0" - -"@webassemblyjs/wasm-opt@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" - integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - -"@webassemblyjs/wasm-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e" - integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-api-error" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/ieee754" "1.9.0" - "@webassemblyjs/leb128" "1.9.0" - "@webassemblyjs/utf8" "1.9.0" - -"@webassemblyjs/wast-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914" - integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/floating-point-hex-parser" "1.9.0" - "@webassemblyjs/helper-api-error" "1.9.0" - "@webassemblyjs/helper-code-frame" "1.9.0" - "@webassemblyjs/helper-fsm" "1.9.0" - "@xtuc/long" "4.2.2" - -"@webassemblyjs/wast-printer@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899" - integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/wast-parser" "1.9.0" - "@xtuc/long" "4.2.2" - -"@xtuc/ieee754@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" - integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== - -"@xtuc/long@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" - integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: - version "1.3.7" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== - dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" - -acorn@^6.4.1: - version "6.4.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" - integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== - -address@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" - integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== - -after@0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" - integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= - -ajv-errors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" - integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== - -ajv-keywords@^3.1.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.0.tgz#4b831e7b531415a7cc518cd404e73f6193c6349d" - integrity sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw== - -ajv-keywords@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" - integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== - -ajv@^6.1.0, ajv@^6.5.5: - version "6.10.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" - integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg== - dependencies: - fast-deep-equal "^2.0.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^6.10.2: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^6.12.0: - version "6.12.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" - integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^6.12.2: - version "6.12.3" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" - integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -amdefine@>=0.0.4: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= - -ansi-colors@^3.0.0: - version "3.2.4" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" - integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== - -ansi-html@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" - integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -ansi-styles@^3.2.0, ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" - integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== - dependencies: - "@types/color-name" "^1.1.1" - color-convert "^2.0.1" - -anymatch@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" - integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== - dependencies: - micromatch "^3.1.4" - normalize-path "^2.1.1" - -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -anymatch@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -aproba@^1.0.3, aproba@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-differ@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" - integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== - -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= - -array-flatten@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" - integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== - -array-union@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= - dependencies: - array-uniq "^1.0.1" - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - -arraybuffer.slice@~0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" - integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== - -arrify@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" - integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== - -asn1.js@^5.2.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" - integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" - -asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - -assert@^1.1.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" - integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== - dependencies: - object-assign "^4.1.1" - util "0.10.3" - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - -async-each@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" - integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== - -async-foreach@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" - integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI= - -async-limiter@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" - integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== - -async@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381" - integrity sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg== - dependencies: - lodash "^4.17.11" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= - -atob@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= - -aws4@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" - integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== - -backo2@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" - integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -base64-arraybuffer@0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" - integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= - -base64-js@^1.0.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -base64id@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6" - integrity sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY= - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -batch@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" - integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= - -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= - dependencies: - tweetnacl "^0.14.3" - -better-assert@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" - integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI= - dependencies: - callsite "1.0.0" - -big.js@^3.1.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" - integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -binary-extensions@^1.0.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" - integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== - -binary-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" - integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== - -blob@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" - integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== - -block-stream@*: - version "0.0.9" - resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" - integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= - dependencies: - inherits "~2.0.0" - -bluebird@^3.5.5: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: - version "4.12.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" - integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== - -bn.js@^5.0.0, bn.js@^5.1.1: - version "5.2.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" - integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== - -body-parser@1.19.0, body-parser@^1.16.1: - version "1.19.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" - integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== - dependencies: - bytes "3.1.0" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.2" - http-errors "1.7.2" - iconv-lite "0.4.24" - on-finished "~2.3.0" - qs "6.7.0" - raw-body "2.4.0" - type-is "~1.6.17" - -bonjour@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" - integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= - dependencies: - array-flatten "^2.1.0" - deep-equal "^1.0.1" - dns-equal "^1.0.0" - dns-txt "^2.0.2" - multicast-dns "^6.0.1" - multicast-dns-service-types "^1.1.0" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^2.3.1, braces@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -brorand@^1.0.1, brorand@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= - -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" - integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== - dependencies: - bn.js "^5.0.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" - integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== - dependencies: - bn.js "^5.1.1" - browserify-rsa "^4.0.1" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.3" - inherits "^2.0.4" - parse-asn1 "^5.1.5" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== - dependencies: - pako "~1.0.5" - -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - -buffer-indexof@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" - integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== - -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= - -buffer@^4.3.0: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - -builtin-modules@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" - integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= - -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= - -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== - -cacache@^12.0.2: - version "12.0.4" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" - integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ== - dependencies: - bluebird "^3.5.5" - chownr "^1.1.1" - figgy-pudding "^3.5.1" - glob "^7.1.4" - graceful-fs "^4.1.15" - infer-owner "^1.0.3" - lru-cache "^5.1.1" - mississippi "^3.0.0" - mkdirp "^0.5.1" - move-concurrently "^1.0.1" - promise-inflight "^1.0.1" - rimraf "^2.6.3" - ssri "^6.0.1" - unique-filename "^1.1.1" - y18n "^4.0.0" - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -callsite@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" - integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - -camelcase@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" - integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= - -camelcase@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" - integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= - -camelcase@^5.0.0, camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= - -chalk@2.4.2, chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72" - integrity sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chokidar@^2.1.8: - version "2.1.8" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" - integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== - dependencies: - anymatch "^2.0.0" - async-each "^1.0.1" - braces "^2.3.2" - glob-parent "^3.1.0" - inherits "^2.0.3" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^3.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.2.1" - upath "^1.1.1" - optionalDependencies: - fsevents "^1.2.7" - -chokidar@^3.0.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450" - integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.3.0" - optionalDependencies: - fsevents "~2.1.2" - -chokidar@^3.4.1: - version "3.5.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" - integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chownr@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" - integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g== - -chrome-trace-event@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" - integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== - dependencies: - tslib "^1.9.0" - -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== - -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -cliui@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" - integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi "^2.0.0" - -cliui@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" - integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== - dependencies: - string-width "^2.1.1" - strip-ansi "^4.0.0" - wrap-ansi "^2.0.0" - -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== - dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" - -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" - -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -color-convert@^1.9.0, color-convert@^1.9.1: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -color-name@^1.0.0, color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-string@^1.5.4: - version "1.5.5" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014" - integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color@3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" - integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ== - dependencies: - color-convert "^1.9.1" - color-string "^1.5.4" - -colors@^1.1.0: - version "1.3.3" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d" - integrity sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg== - -combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@^2.12.1: - version "2.20.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" - integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== - -commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= - -compare-versions@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" - integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== - -component-bind@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" - integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E= - -component-emitter@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" - integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= - -component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - -component-inherit@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" - integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM= - -compressible@~2.0.16: - version "2.0.17" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.17.tgz#6e8c108a16ad58384a977f3a482ca20bff2f38c1" - integrity sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw== - dependencies: - mime-db ">= 1.40.0 < 2" - -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== - dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" - debug "2.6.9" - on-headers "~1.0.2" - safe-buffer "5.1.2" - vary "~1.1.2" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -concat-stream@1.6.2, concat-stream@^1.5.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -connect-history-api-fallback@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" - integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== - -connect@^3.6.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" - integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== - dependencies: - debug "2.6.9" - finalhandler "1.1.2" - parseurl "~1.3.3" - utils-merge "1.0.1" - -console-browserify@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= - -constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= - -content-disposition@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== - dependencies: - safe-buffer "5.1.2" - -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== - -convert-source-map@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" - integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== - dependencies: - safe-buffer "~5.1.1" - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= - -cookie@0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" - integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= - -cookie@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" - integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== - -copy-concurrently@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" - integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== - dependencies: - aproba "^1.1.1" - fs-write-stream-atomic "^1.0.8" - iferr "^0.1.5" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.0" - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= - -core-util-is@1.0.2, core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.7.2" - -coverage-istanbul-loader@3.0.5: - version "3.0.5" - resolved "https://registry.yarnpkg.com/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz#bf942efc0f4e3ac27565203c17dca5008eae6637" - integrity sha512-xsw2phF0VNqUPk47V/vHXkdcTyl0tkMSmaZfLrTOhoPhPMXFelNju7utl5s7I93KXzipqDEK0YwofQSSflPz8A== - dependencies: - "@jsdevtools/coverage-istanbul-loader" "3.0.5" - -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -cross-spawn@6.0.5, cross-spawn@^6.0.0: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -cross-spawn@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" - integrity sha1-ElYDfsufDF9549bvE14wdwGEuYI= - dependencies: - lru-cache "^4.0.1" - which "^1.2.9" - -cross-spawn@^7.0.0: - version "7.0.2" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.2.tgz#d0d7dcfa74e89115c7619f4f721a94e1fdb716d6" - integrity sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -crypto-browserify@^3.11.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - -css-loader@3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.5.3.tgz#95ac16468e1adcd95c844729e0bb167639eb0bcf" - integrity sha512-UEr9NH5Lmi7+dguAm+/JSPovNjYbm2k3TK58EiwQHzOHH5Jfq1Y+XoP2bQO6TMn7PptMd0opxxedAWcaSTRKHw== - dependencies: - camelcase "^5.3.1" - cssesc "^3.0.0" - icss-utils "^4.1.1" - loader-utils "^1.2.3" - normalize-path "^3.0.0" - postcss "^7.0.27" - postcss-modules-extract-imports "^2.0.0" - postcss-modules-local-by-default "^3.0.2" - postcss-modules-scope "^2.2.0" - postcss-modules-values "^3.0.0" - postcss-value-parser "^4.0.3" - schema-utils "^2.6.6" - semver "^6.3.0" - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -csstype@^2.2.0: - version "2.6.5" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.5.tgz#1cd1dff742ebf4d7c991470ae71e12bb6751e034" - integrity sha512-JsTaiksRsel5n7XwqPAfB0l3TFKdpjW/kgAELf9vrb5adGA7UCPLajKK5s3nFrcFm3Rkyp/Qkgl73ENc1UY3cA== - -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= - dependencies: - array-find-index "^1.0.1" - -custom-event@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" - integrity sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU= - -cyclist@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" - integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= - dependencies: - assert-plus "^1.0.0" - -date-format@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/date-format/-/date-format-2.0.0.tgz#7cf7b172f1ec564f0003b39ea302c5498fb98c8f" - integrity sha512-M6UqVvZVgFYqZL1SfHsRGIQSz3ZL+qgbsV5Lp1Vj61LZVYuEwcMXYay7DRDtYs2HQQBK5hQtQ0fD9aEJ89V0LA== - -debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - -debug@^4.1.0, debug@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== - dependencies: - ms "^2.1.1" - -debug@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -deep-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" - integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -default-gateway@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" - integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== - dependencies: - execa "^1.0.0" - ip-regex "^2.1.0" - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -del@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" - integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== - dependencies: - "@types/glob" "^7.1.1" - globby "^6.1.0" - is-path-cwd "^2.0.0" - is-path-in-cwd "^2.0.0" - p-map "^2.0.0" - pify "^4.0.1" - rimraf "^2.6.3" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= - -des.js@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" - integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= - -detect-file@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" - integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= - -detect-libc@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= - -detect-node@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" - integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== - -detect-port@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.3.0.tgz#d9c40e9accadd4df5cac6a782aefd014d573d1f1" - integrity sha512-E+B1gzkl2gqxt1IhUzwjrxBKRqx1UzC3WLONHinn8S3T6lwV/agVCyitiFOsGJ/eYuEUBvD71MZHy3Pv1G9doQ== - dependencies: - address "^1.0.1" - debug "^2.6.0" - -di@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" - integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw= - -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - -dns-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" - integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= - -dns-packet@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" - integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg== - dependencies: - ip "^1.1.0" - safe-buffer "^5.0.1" - -dns-txt@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" - integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= - dependencies: - buffer-indexof "^1.0.0" - -doctrine@0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-0.7.2.tgz#7cb860359ba3be90e040b26b729ce4bfa654c523" - integrity sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM= - dependencies: - esutils "^1.1.6" - isarray "0.0.1" - -dom-serialize@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" - integrity sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs= - dependencies: - custom-event "~1.0.0" - ent "~2.2.0" - extend "^3.0.0" - void-elements "^2.0.0" - -domain-browser@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" - integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== - -dompurify@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.0.tgz#07bb39515e491588e5756b1d3e8375b5964814e2" - integrity sha512-VV5C6Kr53YVHGOBKO/F86OYX6/iLTw2yVSI721gKetxpHCK/V5TaLEf9ODjRgl1KLSWRMY6cUhAbv/c+IUnwQw== - -duplexify@^3.4.2, duplexify@^3.6.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" - integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== - dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" - -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= - -elliptic@^6.5.3: - version "6.5.4" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" - integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emojis-list@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" - integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= - -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= - -end-of-stream@^1.0.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -end-of-stream@^1.1.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" - integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== - dependencies: - once "^1.4.0" - -engine.io-client@~3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.2.1.tgz#6f54c0475de487158a1a7c77d10178708b6add36" - integrity sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw== - dependencies: - component-emitter "1.2.1" - component-inherit "0.0.3" - debug "~3.1.0" - engine.io-parser "~2.1.1" - has-cors "1.1.0" - indexof "0.0.1" - parseqs "0.0.5" - parseuri "0.0.5" - ws "~3.3.1" - xmlhttprequest-ssl "~1.5.4" - yeast "0.1.2" - -engine.io-parser@~2.1.0, engine.io-parser@~2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.1.3.tgz#757ab970fbf2dfb32c7b74b033216d5739ef79a6" - integrity sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA== - dependencies: - after "0.8.2" - arraybuffer.slice "~0.0.7" - base64-arraybuffer "0.1.5" - blob "0.0.5" - has-binary2 "~1.0.2" - -engine.io@~3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.2.1.tgz#b60281c35484a70ee0351ea0ebff83ec8c9522a2" - integrity sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w== - dependencies: - accepts "~1.3.4" - base64id "1.0.0" - cookie "0.3.1" - debug "~3.1.0" - engine.io-parser "~2.1.0" - ws "~3.3.1" - -enhanced-resolve@4.1.0, enhanced-resolve@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" - integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng== - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.4.0" - tapable "^1.0.0" - -enhanced-resolve@^4.1.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec" - integrity sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg== - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.5.0" - tapable "^1.0.0" - -ent@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" - integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= - -errno@^0.1.3: - version "0.1.7" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" - integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== - dependencies: - prr "~1.0.1" - -errno@~0.1.7: - version "0.1.8" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" - integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== - dependencies: - prr "~1.0.1" - -error-ex@^1.2.0, error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es6-promise@^4.0.3: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= - -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -eslint-scope@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" - integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esrecurse@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" - integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM= - -estraverse@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" - integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== - -esutils@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-1.1.6.tgz#c01ccaa9ae4b897c6d0c3e210ae52f3c7a844375" - integrity sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U= - -esutils@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" - integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= - -eventemitter3@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" - integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== - -events@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -eventsource@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" - integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== - dependencies: - original "^1.0.0" - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -execa@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-2.1.0.tgz#e5d3ecd837d2a60ec50f3da78fd39767747bbe99" - integrity sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw== - dependencies: - cross-spawn "^7.0.0" - get-stream "^5.0.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^3.0.0" - onetime "^5.1.0" - p-finally "^2.0.0" - signal-exit "^3.0.2" - strip-final-newline "^2.0.0" - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -expand-tilde@^2.0.0, expand-tilde@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" - integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= - dependencies: - homedir-polyfill "^1.0.1" - -express@^4.17.1: - version "4.17.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== - dependencies: - accepts "~1.3.7" - array-flatten "1.1.1" - body-parser "1.19.0" - content-disposition "0.5.3" - content-type "~1.0.4" - cookie "0.4.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.2" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "~1.1.2" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.5" - qs "6.7.0" - range-parser "~1.2.1" - safe-buffer "5.1.2" - send "0.17.1" - serve-static "1.14.1" - setprototypeof "1.1.1" - statuses "~1.5.0" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extend@^3.0.0, extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -extract-zip@^1.6.5: - version "1.6.7" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" - integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k= - dependencies: - concat-stream "1.6.2" - debug "2.6.9" - mkdirp "0.5.1" - yauzl "2.4.1" - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= - -fast-deep-equal@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= - -fast-deep-equal@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" - integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== - -fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= - -faye-websocket@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" - integrity sha1-TkkvjQTftviQA1B/btvy1QHnxvQ= - dependencies: - websocket-driver ">=0.5.1" - -faye-websocket@~0.11.1: - version "0.11.3" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" - integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== - dependencies: - websocket-driver ">=0.5.1" - -fd-slicer@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" - integrity sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU= - dependencies: - pend "~1.2.0" - -figgy-pudding@^3.5.1: - version "3.5.2" - resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" - integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -finalhandler@1.1.2, finalhandler@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" - integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.3" - statuses "~1.5.0" - unpipe "~1.0.0" - -find-cache-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" - integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== - dependencies: - commondir "^1.0.1" - make-dir "^2.0.0" - pkg-dir "^3.0.0" - -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -find-up@^4.0.0, find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -find-versions@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-3.2.0.tgz#10297f98030a786829681690545ef659ed1d254e" - integrity sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww== - dependencies: - semver-regex "^2.0.0" - -findup-sync@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" - integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== - dependencies: - detect-file "^1.0.0" - is-glob "^4.0.0" - micromatch "^3.0.4" - resolve-dir "^1.0.1" - -flatted@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" - integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== - -flush-write-stream@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" - integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== - dependencies: - inherits "^2.0.3" - readable-stream "^2.3.6" - -follow-redirects@^1.0.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" - integrity sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ== - dependencies: - debug "^3.2.6" - -for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= - -from2@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" - integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= - dependencies: - inherits "^2.0.1" - readable-stream "^2.0.0" - -fs-extra@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" - integrity sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA= - dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - klaw "^1.0.0" - -fs-extra@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" - integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== - dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" - -fs-minipass@^1.2.5: - version "1.2.6" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07" - integrity sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ== - dependencies: - minipass "^2.2.1" - -fs-write-stream-atomic@^1.0.8: - version "1.0.10" - resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" - integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk= - dependencies: - graceful-fs "^4.1.2" - iferr "^0.1.5" - imurmurhash "^0.1.4" - readable-stream "1 || 2" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@^1.2.7: - version "1.2.9" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" - integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== - dependencies: - nan "^2.12.1" - node-pre-gyp "^0.12.0" - -fsevents@~2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" - integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA== - -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -fstream@^1.0.0, fstream@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" - integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== - dependencies: - graceful-fs "^4.1.2" - inherits "~2.0.0" - mkdirp ">=0.5 0" - rimraf "2" - -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -gaze@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a" - integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g== - dependencies: - globule "^1.0.0" - -gensync@^1.0.0-beta.1: - version "1.0.0-beta.1" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" - integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== - -get-caller-file@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" - integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== - -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= - -get-stream@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -get-stream@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9" - integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw== - dependencies: - pump "^3.0.0" - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" - -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob-parent@~5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" - integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw== - dependencies: - is-glob "^4.0.1" - -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob@7.1.6: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@~7.1.1: - version "7.1.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" - integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.1.4, glob@^7.1.7: - version "7.2.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -global-modules@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" - integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== - dependencies: - global-prefix "^3.0.0" - -global-modules@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" - integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== - dependencies: - global-prefix "^1.0.1" - is-windows "^1.0.1" - resolve-dir "^1.0.0" - -global-prefix@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" - integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= - dependencies: - expand-tilde "^2.0.2" - homedir-polyfill "^1.0.1" - ini "^1.3.4" - is-windows "^1.0.1" - which "^1.2.14" - -global-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" - integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== - dependencies: - ini "^1.3.5" - kind-of "^6.0.2" - which "^1.3.1" - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globby@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" - integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= - dependencies: - array-union "^1.0.1" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -globule@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.1.tgz#5dffb1b191f22d20797a9369b49eab4e9839696d" - integrity sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ== - dependencies: - glob "~7.1.1" - lodash "~4.17.10" - minimatch "~3.0.2" - -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: - version "4.2.0" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b" - integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg== - -graceful-fs@^4.1.15: - version "4.2.8" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" - integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== - -handle-thing@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" - integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== - -handlebars@^4.7.7: - version "4.7.7" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" - integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== - dependencies: - minimist "^1.2.5" - neo-async "^2.6.0" - source-map "^0.6.1" - wordwrap "^1.0.0" - optionalDependencies: - uglify-js "^3.1.4" - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.0: - version "5.1.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" - integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== - dependencies: - ajv "^6.5.5" - har-schema "^2.0.0" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-binary2@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" - integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw== - dependencies: - isarray "2.0.1" - -has-cors@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" - integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk= - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== - dependencies: - inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -hasha@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1" - integrity sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE= - dependencies: - is-stream "^1.0.1" - pinkie-promise "^2.0.0" - -hmac-drbg@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -homedir-polyfill@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" - integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== - dependencies: - parse-passwd "^1.0.0" - -hosted-git-info@^2.1.4: - version "2.7.1" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" - integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w== - -hpack.js@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" - integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= - dependencies: - inherits "^2.0.1" - obuf "^1.0.0" - readable-stream "^2.0.1" - wbuf "^1.1.0" - -html-entities@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" - integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= - -html-escaper@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" - integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== - -http-deceiver@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" - integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= - -http-errors@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - -http-errors@~1.7.2: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -"http-parser-js@>=0.4.0 <0.4.11": - version "0.4.10" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" - integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q= - -http-proxy-middleware@0.19.1: - version "0.19.1" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" - integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== - dependencies: - http-proxy "^1.17.0" - is-glob "^4.0.0" - lodash "^4.17.11" - micromatch "^3.1.10" - -http-proxy@^1.13.0, http-proxy@^1.17.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" - integrity sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g== - dependencies: - eventemitter3 "^3.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= - -husky@^4.2.5: - version "4.2.5" - resolved "https://registry.yarnpkg.com/husky/-/husky-4.2.5.tgz#2b4f7622673a71579f901d9885ed448394b5fa36" - integrity sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ== - dependencies: - chalk "^4.0.0" - ci-info "^2.0.0" - compare-versions "^3.6.0" - cosmiconfig "^6.0.0" - find-versions "^3.2.0" - opencollective-postinstall "^2.0.2" - pkg-dir "^4.2.0" - please-upgrade-node "^3.2.0" - slash "^3.0.0" - which-pm-runs "^1.0.0" - -iconv-lite@0.4.24, iconv-lite@^0.4.4: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -icss-utils@^4.0.0, icss-utils@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" - integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== - dependencies: - postcss "^7.0.14" - -ieee754@^1.1.4: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -iferr@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" - integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= - -ignore-walk@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" - integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ== - dependencies: - minimatch "^3.0.4" - -ignore@^5.1.4: - version "5.1.4" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" - integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== - -import-fresh@^3.1.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" - integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-local@2.0.0, import-local@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" - integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== - dependencies: - pkg-dir "^3.0.0" - resolve-cwd "^2.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -in-publish@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" - integrity sha1-4g/146KvwmkDILbcVSaCqcf631E= - -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= - dependencies: - repeating "^2.0.0" - -indexes-of@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" - integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= - -indexof@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" - integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= - -infer-owner@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" - integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== - -internal-ip@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" - integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg== - dependencies: - default-gateway "^4.2.0" - ipaddr.js "^1.9.0" - -interpret@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" - integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== - -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" - integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= - -invert-kv@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" - integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== - -ip-regex@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" - integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= - -ip@^1.1.0, ip@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= - -ipaddr.js@1.9.0, ipaddr.js@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" - integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== - -is-absolute-url@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" - integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-arrayish@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" - integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== - -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= - dependencies: - binary-extensions "^1.0.0" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-docker@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.0.0.tgz#2cb0df0e75e2d064fe1864c37cdeacb7b2dcf25b" - integrity sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ== - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^2.1.0, is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-finite@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" - integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= - dependencies: - kind-of "^3.0.2" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-path-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.1.0.tgz#2e0c7e463ff5b7a0eb60852d851a6809347a124c" - integrity sha512-Sc5j3/YnM8tDeyCsVeKlm/0p95075DyLmDEIkSgQ7mXkrOX+uTCtmQFm0CYzVyJwcCCmO3k8qfJt17SxQwB5Zw== - -is-path-in-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" - integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== - dependencies: - is-path-inside "^2.1.0" - -is-path-inside@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" - integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== - dependencies: - path-is-inside "^1.0.2" - -is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-stream@^1.0.1, is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - -is-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" - integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - -is-windows@^1.0.1, is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" - integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= - -is-wsl@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" - -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= - -isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isarray@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" - integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4= - -isbinaryfile@^4.0.2: - version "4.0.6" - resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b" - integrity sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= - -istanbul-lib-coverage@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" - integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== - -istanbul-lib-coverage@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" - integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg== - -istanbul-lib-instrument@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" - integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== - dependencies: - "@babel/core" "^7.7.5" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-coverage "^3.0.0" - semver "^6.3.0" - -istanbul-lib-report@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" - integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== - dependencies: - istanbul-lib-coverage "^3.0.0" - make-dir "^3.0.0" - supports-color "^7.1.0" - -istanbul-lib-source-maps@^3.0.6: - version "3.0.6" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" - integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== - dependencies: - debug "^4.1.1" - istanbul-lib-coverage "^2.0.5" - make-dir "^2.1.0" - rimraf "^2.6.3" - source-map "^0.6.1" - -istanbul-reports@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b" - integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw== - dependencies: - html-escaper "^2.0.0" - istanbul-lib-report "^3.0.0" - -jasmine-core@3.5.0, jasmine-core@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.5.0.tgz#132c23e645af96d85c8bca13c8758b18429fc1e4" - integrity sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA== - -js-base64@^2.1.8: - version "2.5.1" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121" - integrity sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw== - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.13.1: - version "3.13.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= - -json3@^3.3.2: - version "3.3.3" - resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" - integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== - -json5@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= - -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== - dependencies: - minimist "^1.2.0" - -json5@^2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" - integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== - dependencies: - minimist "^1.2.5" - -jsonc-parser@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" - integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== - -jsonfile@^2.1.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" - integrity sha1-NzaitCi4e72gzIO1P6PWM6NcKug= - optionalDependencies: - graceful-fs "^4.1.6" - -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= - optionalDependencies: - graceful-fs "^4.1.6" - -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - -karma-chrome-launcher@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz#805a586799a4d05f4e54f72a204979f3f3066738" - integrity sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg== - dependencies: - which "^1.2.1" - -karma-coverage-istanbul-reporter@3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz#f3b5303553aadc8e681d40d360dfdc19bc7e9fe9" - integrity sha512-wE4VFhG/QZv2Y4CdAYWDbMmcAHeS926ZIji4z+FkB2aF/EposRb6DP6G5ncT/wXhqUfAb/d7kZrNKPonbvsATw== - dependencies: - istanbul-lib-coverage "^3.0.0" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^3.0.6" - istanbul-reports "^3.0.2" - minimatch "^3.0.4" - -karma-firefox-launcher@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/karma-firefox-launcher/-/karma-firefox-launcher-1.3.0.tgz#ebcbb1d1ddfada6be900eb8fae25bcf2dcdc8171" - integrity sha512-Fi7xPhwrRgr+94BnHX0F5dCl1miIW4RHnzjIGxF8GaIEp7rNqX7LSi7ok63VXs3PS/5MQaQMhGxw+bvD+pibBQ== - dependencies: - is-wsl "^2.1.0" - -karma-jasmine@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-3.1.1.tgz#f592b253e7619a8d84559d7daf473a647498ade8" - integrity sha512-pxBmv5K7IkBRLsFSTOpgiK/HzicQT3mfFF+oHAC7nxMfYKhaYFgxOa5qjnHW4sL5rUnmdkSajoudOnnOdPyW4Q== - dependencies: - jasmine-core "^3.5.0" - -karma-phantomjs-launcher@1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.4.tgz#d23ca34801bda9863ad318e3bb4bd4062b13acd2" - integrity sha1-0jyjSAG9qYY60xjju0vUBisTrNI= - dependencies: - lodash "^4.0.1" - phantomjs-prebuilt "^2.1.7" - -karma-sourcemap-loader@0.3.7: - version "0.3.7" - resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz#91322c77f8f13d46fed062b042e1009d4c4505d8" - integrity sha1-kTIsd/jxPUb+0GKwQuEAnUxFBdg= - dependencies: - graceful-fs "^4.1.2" - -karma-webpack@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-4.0.2.tgz#23219bd95bdda853e3073d3874d34447c77bced0" - integrity sha512-970/okAsdUOmiMOCY8sb17A2I8neS25Ad9uhyK3GHgmRSIFJbDcNEFE8dqqUhNe9OHiCC9k3DMrSmtd/0ymP1A== - dependencies: - clone-deep "^4.0.1" - loader-utils "^1.1.0" - neo-async "^2.6.1" - schema-utils "^1.0.0" - source-map "^0.7.3" - webpack-dev-middleware "^3.7.0" - -karma@5.0.4: - version "5.0.4" - resolved "https://registry.yarnpkg.com/karma/-/karma-5.0.4.tgz#b374a1e541ad66da668460b99c6faf20cfffc97a" - integrity sha512-UGqTe2LBiGQBXRN+Fygeiq63tbfOX45639SKSbPkLpARwnxROWJZg+froGkpHxr84FXCe8UGCf+1PITM6frT5w== - dependencies: - body-parser "^1.16.1" - braces "^3.0.2" - chokidar "^3.0.0" - colors "^1.1.0" - connect "^3.6.0" - di "^0.0.1" - dom-serialize "^2.2.0" - flatted "^2.0.0" - glob "^7.1.1" - graceful-fs "^4.1.2" - http-proxy "^1.13.0" - isbinaryfile "^4.0.2" - lodash "^4.17.14" - log4js "^4.0.0" - mime "^2.3.1" - minimatch "^3.0.2" - qjobs "^1.1.4" - range-parser "^1.2.0" - rimraf "^2.6.0" - socket.io "2.1.1" - source-map "^0.6.1" - tmp "0.0.33" - ua-parser-js "0.7.21" - yargs "^15.3.1" - -kew@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b" - integrity sha1-edk9LTM2PW/dKXCzNdkUGtWR15s= - -killable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" - integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" - integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== - -klaw@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" - integrity sha1-QIhDO0azsbolnXh4XY6W9zugJDk= - optionalDependencies: - graceful-fs "^4.1.9" - -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" - integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= - dependencies: - invert-kv "^1.0.0" - -lcid@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" - integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== - dependencies: - invert-kv "^2.0.0" - -lines-and-columns@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" - integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= - -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - -loader-runner@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" - integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== - -loader-utils@1.2.3, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" - integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== - dependencies: - big.js "^5.2.2" - emojis-list "^2.0.0" - json5 "^1.0.1" - -loader-utils@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" - integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^2.1.2" - -loader-utils@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" - integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0= - dependencies: - big.js "^3.1.3" - emojis-list "^2.0.0" - json5 "^0.5.0" - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -lodash@^4.0.0, lodash@^4.0.1, lodash@^4.17.11, lodash@~4.17.10: - version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" - integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== - -lodash@^4.17.14, lodash@^4.17.15: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== - -lodash@^4.17.19: - version "4.17.19" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" - integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== - -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -log4js@^4.0.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/log4js/-/log4js-4.4.0.tgz#3da357d98848596c0ef193f6153ca29d23209217" - integrity sha512-xwRvmxFsq8Hb7YeS+XKfvCrsH114bXex6mIwJ2+KmYVi23pB3+hlzyGq1JPycSFTJWNLhD/7PCtM0RfPy6/2yg== - dependencies: - date-format "^2.0.0" - debug "^4.1.1" - flatted "^2.0.0" - rfdc "^1.1.4" - streamroller "^1.0.5" - -loglevel@^1.6.6: - version "1.6.8" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" - integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA== - -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - -lru-cache@^4.0.1: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -lunr@^2.3.9: - version "2.3.9" - resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" - integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== - -make-dir@^2.0.0, make-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== - dependencies: - pify "^4.0.1" - semver "^5.6.0" - -make-dir@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -map-age-cleaner@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" - integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== - dependencies: - p-defer "^1.0.0" - -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-obj@^1.0.0, map-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= - dependencies: - object-visit "^1.0.0" - -marked@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753" - integrity sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA== - -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= - -mem@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" - integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== - dependencies: - map-age-cleaner "^0.1.1" - mimic-fn "^2.0.0" - p-is-promise "^2.0.0" - -memory-fs@^0.4.0, memory-fs@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" - integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -memory-fs@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" - integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -meow@^3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= - dependencies: - camelcase-keys "^2.0.0" - decamelize "^1.1.2" - loud-rejection "^1.0.0" - map-obj "^1.0.1" - minimist "^1.1.3" - normalize-package-data "^2.3.4" - object-assign "^4.0.1" - read-pkg-up "^1.0.1" - redent "^1.0.0" - trim-newlines "^1.0.0" - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= - -merge-source-map@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" - integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== - dependencies: - source-map "^0.6.1" - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= - -micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -micromatch@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== - dependencies: - braces "^3.0.1" - picomatch "^2.0.5" - -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - -mime-db@1.40.0, "mime-db@>= 1.40.0 < 2": - version "1.40.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" - integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== - -mime-db@1.44.0: - version "1.44.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== - -mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: - version "2.1.24" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" - integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== - dependencies: - mime-db "1.40.0" - -mime-types@^2.1.26: - version "2.1.27" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== - dependencies: - mime-db "1.44.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mime@^2.3.1, mime@^2.4.2, mime@^2.4.4: - version "2.4.4" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" - integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== - -mimic-fn@^2.0.0, mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= - -minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= - -minimist@^1.1.3, minimist@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= - -minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -minipass@^2.2.1, minipass@^2.3.5: - version "2.3.5" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" - integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA== - dependencies: - safe-buffer "^5.1.2" - yallist "^3.0.0" - -minizlib@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" - integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== - dependencies: - minipass "^2.2.1" - -mississippi@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" - integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA== - dependencies: - concat-stream "^1.5.0" - duplexify "^3.4.2" - end-of-stream "^1.1.0" - flush-write-stream "^1.0.0" - from2 "^2.1.0" - parallel-transform "^1.1.0" - pump "^3.0.0" - pumpify "^1.3.3" - stream-each "^1.1.0" - through2 "^2.0.0" - -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= - dependencies: - minimist "0.0.8" - -mkdirp@^0.5.3: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -move-concurrently@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" - integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I= - dependencies: - aproba "^1.1.1" - copy-concurrently "^1.0.0" - fs-write-stream-atomic "^1.0.8" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.3" - -mri@^1.1.4: - version "1.1.5" - resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.5.tgz#ce21dba2c69f74a9b7cf8a1ec62307e089e223e0" - integrity sha512-d2RKzMD4JNyHMbnbWnznPaa8vbdlq/4pNZ3IgdaGrVbBhebBsGUUE/6qorTMYNS6TwuH3ilfOlD2bf4Igh8CKg== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - -ms@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -multicast-dns-service-types@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" - integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= - -multicast-dns@^6.0.1: - version "6.2.3" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" - integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== - dependencies: - dns-packet "^1.3.1" - thunky "^1.0.2" - -multimatch@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-4.0.0.tgz#8c3c0f6e3e8449ada0af3dd29efb491a375191b3" - integrity sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ== - dependencies: - "@types/minimatch" "^3.0.3" - array-differ "^3.0.0" - array-union "^2.1.0" - arrify "^2.0.1" - minimatch "^3.0.4" - -nan@^2.12.1, nan@^2.13.2: - version "2.14.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" - integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -needle@^2.2.1: - version "2.4.0" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" - integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== - dependencies: - debug "^3.2.6" - iconv-lite "^0.4.4" - sax "^1.2.4" - -negotiator@0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== - -neo-async@^2.5.0, neo-async@^2.6.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -neo-async@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" - integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - -node-forge@0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" - integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== - -node-gyp@^3.8.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" - integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA== - dependencies: - fstream "^1.0.0" - glob "^7.0.3" - graceful-fs "^4.1.2" - mkdirp "^0.5.0" - nopt "2 || 3" - npmlog "0 || 1 || 2 || 3 || 4" - osenv "0" - request "^2.87.0" - rimraf "2" - semver "~5.3.0" - tar "^2.0.0" - which "1" - -node-libs-browser@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" - integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== - dependencies: - assert "^1.1.1" - browserify-zlib "^0.2.0" - buffer "^4.3.0" - console-browserify "^1.1.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.11.0" - domain-browser "^1.1.1" - events "^3.0.0" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "0.0.1" - process "^0.11.10" - punycode "^1.2.4" - querystring-es3 "^0.2.0" - readable-stream "^2.3.3" - stream-browserify "^2.0.1" - stream-http "^2.7.2" - string_decoder "^1.0.0" - timers-browserify "^2.0.4" - tty-browserify "0.0.0" - url "^0.11.0" - util "^0.11.0" - vm-browserify "^1.0.1" - -node-pre-gyp@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" - integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4" - -node-sass@4.14.0: - version "4.14.0" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.14.0.tgz#a8e9d7720f8e15b4a1072719dcf04006f5648eeb" - integrity sha512-AxqU+DFpk0lEz95sI6jO0hU0Rwyw7BXVEv6o9OItoXLyeygPeaSpiV4rwQb10JiTghHaa0gZeD21sz+OsQluaw== - dependencies: - async-foreach "^0.1.3" - chalk "^1.1.1" - cross-spawn "^3.0.0" - gaze "^1.0.0" - get-stdin "^4.0.1" - glob "^7.0.3" - in-publish "^2.0.0" - lodash "^4.17.15" - meow "^3.7.0" - mkdirp "^0.5.1" - nan "^2.13.2" - node-gyp "^3.8.0" - npmlog "^4.0.0" - request "^2.88.0" - sass-graph "^2.2.4" - stdout-stream "^1.4.0" - "true-case-path" "^1.0.2" - -"nopt@2 || 3": - version "3.0.6" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" - integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= - dependencies: - abbrev "1" - -nopt@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" - integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= - dependencies: - abbrev "1" - osenv "^0.1.4" - -normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -npm-bundled@^1.0.1: - version "1.0.6" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd" - integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== - -npm-packlist@^1.1.6: - version "1.4.4" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.4.tgz#866224233850ac534b63d1a6e76050092b5d2f44" - integrity sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw== - dependencies: - ignore-walk "^3.0.1" - npm-bundled "^1.0.1" - -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" - -npm-run-path@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-3.1.0.tgz#7f91be317f6a466efed3c9f2980ad8a4ee8b0fa5" - integrity sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg== - dependencies: - path-key "^3.0.0" - -"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-component@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" - integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE= - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= - dependencies: - isobject "^3.0.0" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" - -obuf@^1.0.0, obuf@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" - integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -onetime@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" - integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== - dependencies: - mimic-fn "^2.1.0" - -onigasm@^2.2.5: - version "2.2.5" - resolved "https://registry.yarnpkg.com/onigasm/-/onigasm-2.2.5.tgz#cc4d2a79a0fa0b64caec1f4c7ea367585a676892" - integrity sha512-F+th54mPc0l1lp1ZcFMyL/jTs2Tlq4SqIHKIXGZOR/VkHkF9A7Fr5rRr5+ZG/lWeRsyrClLYRq7s/yFQ/XhWCA== - dependencies: - lru-cache "^5.1.1" - -opencollective-postinstall@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89" - integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw== - -opn@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" - integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== - dependencies: - is-wsl "^1.1.0" - -original@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" - integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== - dependencies: - url-parse "^1.4.3" - -os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= - -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= - -os-locale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" - integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= - dependencies: - lcid "^1.0.0" - -os-locale@^3.0.0, os-locale@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" - integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== - dependencies: - execa "^1.0.0" - lcid "^2.0.0" - mem "^4.0.0" - -os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -osenv@0, osenv@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" - integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - -p-defer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" - integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - -p-finally@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" - integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== - -p-is-promise@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" - integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== - -p-limit@^2.0.0, p-limit@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" - integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ== - dependencies: - p-try "^2.0.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-map@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" - integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== - -p-retry@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" - integrity sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w== - dependencies: - retry "^0.12.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -pako@~1.0.5: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== - -parallel-transform@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc" - integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg== - dependencies: - cyclist "^1.0.1" - inherits "^2.0.3" - readable-stream "^2.1.5" - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-asn1@^5.0.0, parse-asn1@^5.1.5: - version "5.1.6" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" - integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== - dependencies: - asn1.js "^5.2.0" - browserify-aes "^1.0.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - -parse-json@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.0.tgz#73e5114c986d143efa3712d4ea24db9a4266f60f" - integrity sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - lines-and-columns "^1.1.6" - -parse-passwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= - -parseqs@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" - integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0= - dependencies: - better-assert "~1.0.0" - -parseuri@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" - integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo= - dependencies: - better-assert "~1.0.0" - -parseurl@~1.3.2, parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -path-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" - integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= - dependencies: - pinkie-promise "^2.0.0" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-is-inside@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= - -path-key@^2.0.0, path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -path-key@^3.0.0, path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -pbkdf2@^3.0.3: - version "3.1.2" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" - integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - -phantomjs-prebuilt@^2.1.7: - version "2.1.16" - resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz#efd212a4a3966d3647684ea8ba788549be2aefef" - integrity sha1-79ISpKOWbTZHaE6ouniFSb4q7+8= - dependencies: - es6-promise "^4.0.3" - extract-zip "^1.6.5" - fs-extra "^1.0.0" - hasha "^2.2.0" - kew "^0.7.0" - progress "^1.1.8" - request "^2.81.0" - request-progress "^2.0.1" - which "^1.2.10" - -picomatch@^2.0.4, picomatch@^2.0.7: - version "2.2.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a" - integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA== - -picomatch@^2.0.5: - version "2.0.7" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" - integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== - -picomatch@^2.2.1: - version "2.3.0" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" - integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== - -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - -pkg-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" - integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== - dependencies: - find-up "^3.0.0" - -pkg-dir@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - -please-upgrade-node@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" - integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== - dependencies: - semver-compare "^1.0.0" - -portfinder@^1.0.25: - version "1.0.26" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.26.tgz#475658d56ca30bed72ac7f1378ed350bd1b64e70" - integrity sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ== - dependencies: - async "^2.6.2" - debug "^3.1.1" - mkdirp "^0.5.1" - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= - -postcss-modules-extract-imports@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" - integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== - dependencies: - postcss "^7.0.5" - -postcss-modules-local-by-default@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz#e8a6561be914aaf3c052876377524ca90dbb7915" - integrity sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ== - dependencies: - icss-utils "^4.1.1" - postcss "^7.0.16" - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.0.0" - -postcss-modules-scope@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" - integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== - dependencies: - postcss "^7.0.6" - postcss-selector-parser "^6.0.0" - -postcss-modules-values@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" - integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg== - dependencies: - icss-utils "^4.0.0" - postcss "^7.0.6" - -postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c" - integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg== - dependencies: - cssesc "^3.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" - -postcss-value-parser@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.0.tgz#99a983d365f7b2ad8d0f9b8c3094926eab4b936d" - integrity sha512-ESPktioptiSUchCKgggAkzdmkgzKfmp0EU8jXH+5kbIUB+unr0Y4CY9SRMvibuvYUBjNh1ACLbxqYNpdTQOteQ== - -postcss-value-parser@^4.0.3: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" - integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== - -postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.17" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.17.tgz#4da1bdff5322d4a0acaab4d87f3e782436bad31f" - integrity sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -postcss@^7.0.27: - version "7.0.29" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.29.tgz#d3a903872bd52280b83bce38cdc83ce55c06129e" - integrity sha512-ba0ApvR3LxGvRMMiUa9n0WR4HjzcYm7tS+ht4/2Nd0NLtHpPIH77fuB9Xh1/yJVz9O/E/95Y/dn8ygWsyffXtw== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -prettier@2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" - integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== - -pretty-quick@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pretty-quick/-/pretty-quick-2.0.1.tgz#417ee605ade98ecc686e72f63b5d28a2c35b43e9" - integrity sha512-y7bJt77XadjUr+P1uKqZxFWLddvj3SKY6EU4BuQtMxmmEFSMpbN132pUWdSG1g1mtUfO0noBvn7wBf0BVeomHg== - dependencies: - chalk "^2.4.2" - execa "^2.1.0" - find-up "^4.1.0" - ignore "^5.1.4" - mri "^1.1.4" - multimatch "^4.0.0" - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - -progress@2.0.3, progress@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - -progress@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" - integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74= - -promise-inflight@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" - integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= - -proxy-addr@~2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" - integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ== - dependencies: - forwarded "~0.1.2" - ipaddr.js "1.9.0" - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= - -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= - -psl@^1.1.24: - version "1.2.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.2.0.tgz#df12b5b1b3a30f51c329eacbdef98f3a6e136dc6" - integrity sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA== - -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - -pump@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" - integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pumpify@^1.3.3: - version "1.5.1" - resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" - integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== - dependencies: - duplexify "^3.6.0" - inherits "^2.0.3" - pump "^2.0.0" - -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - -punycode@^1.2.4, punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -qjobs@^1.1.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" - integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== - -qs@6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== - -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - -querystring-es3@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -querystringify@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" - integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== - -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - -range-parser@^1.2.0, range-parser@^1.2.1, range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" - integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== - dependencies: - bytes "3.1.0" - http-errors "1.7.2" - iconv-lite "0.4.24" - unpipe "1.0.0" - -rc@^1.2.7: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.1.5, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.2: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.0.6: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" - integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" - integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== - dependencies: - graceful-fs "^4.1.11" - micromatch "^3.1.10" - readable-stream "^2.0.2" - -readdirp@~3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17" - integrity sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ== - dependencies: - picomatch "^2.0.7" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= - dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - -regenerator-runtime@^0.13.4: - version "0.13.5" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" - integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= - -repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== - -repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - -request-progress@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-2.0.1.tgz#5d36bb57961c673aa5b788dbc8141fdf23b44e08" - integrity sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg= - dependencies: - throttleit "^1.0.0" - -request@^2.81.0, request@^2.87.0, request@^2.88.0: - version "2.88.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" - integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.0" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.4.3" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" - integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= - -resolve-cwd@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" - integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= - dependencies: - resolve-from "^3.0.0" - -resolve-dir@^1.0.0, resolve-dir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" - integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M= - dependencies: - expand-tilde "^2.0.0" - global-modules "^1.0.0" - -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -resolve@^1.10.0, resolve@^1.3.2: - version "1.11.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.1.tgz#ea10d8110376982fef578df8fc30b9ac30a07a3e" - integrity sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw== - dependencies: - path-parse "^1.0.6" - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -retry@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" - integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= - -rfdc@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" - integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug== - -rimraf@2, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - -rimraf@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -rimraf@^2.5.4: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -run-queue@^1.0.0, run-queue@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" - integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec= - dependencies: - aproba "^1.1.1" - -safe-buffer@5.1.2, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@^5.1.1, safe-buffer@^5.2.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sass-graph@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" - integrity sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k= - dependencies: - glob "^7.0.0" - lodash "^4.0.0" - scss-tokenizer "^0.2.3" - yargs "^7.0.0" - -sass-loader@8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-8.0.2.tgz#debecd8c3ce243c76454f2e8290482150380090d" - integrity sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ== - dependencies: - clone-deep "^4.0.1" - loader-utils "^1.2.3" - neo-async "^2.6.1" - schema-utils "^2.6.1" - semver "^6.3.0" - -sax@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - -schema-utils@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" - integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== - dependencies: - ajv "^6.1.0" - ajv-errors "^1.0.0" - ajv-keywords "^3.1.0" - -schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.6.6: - version "2.6.6" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.6.tgz#299fe6bd4a3365dc23d99fd446caff8f1d6c330c" - integrity sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA== - dependencies: - ajv "^6.12.0" - ajv-keywords "^3.4.1" - -schema-utils@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" - integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== - dependencies: - "@types/json-schema" "^7.0.4" - ajv "^6.12.2" - ajv-keywords "^3.4.1" - -scss-tokenizer@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" - integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE= - dependencies: - js-base64 "^2.1.8" - source-map "^0.4.2" - -select-hose@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" - integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= - -selfsigned@^1.10.7: - version "1.10.7" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.7.tgz#da5819fd049d5574f28e88a9bcc6dbc6e6f3906b" - integrity sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA== - dependencies: - node-forge "0.9.0" - -semver-compare@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" - integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= - -semver-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" - integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== - -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0: - version "5.7.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" - integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== - -semver@^5.4.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.2.0.tgz#4d813d9590aaf8a9192693d6c85b9344de5901db" - integrity sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A== - -semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" - integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= - -send@0.17.1: - version "0.17.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" - integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== - dependencies: - debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.7.2" - mime "1.6.0" - ms "2.1.1" - on-finished "~2.3.0" - range-parser "~1.2.1" - statuses "~1.5.0" - -serialize-javascript@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" - integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== - dependencies: - randombytes "^2.1.0" - -serve-index@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" - integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= - dependencies: - accepts "~1.3.4" - batch "0.6.1" - debug "2.6.9" - escape-html "~1.0.3" - http-errors "~1.6.2" - mime-types "~2.1.17" - parseurl "~1.3.2" - -serve-static@1.14.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" - integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.17.1" - -set-blocking@^2.0.0, set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -set-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -setimmediate@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== - -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== - -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -shiki@^0.9.3: - version "0.9.12" - resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.9.12.tgz#70cbc8c1bb78ff7b356f84a7eecdb040efddd247" - integrity sha512-VXcROdldv0/Qu0w2XvzU4IrvTeBNs/Kj/FCmtcEXGz7Tic/veQzliJj6tEiAgoKianhQstpYmbPDStHU5Opqcw== - dependencies: - jsonc-parser "^3.0.0" - onigasm "^2.2.5" - vscode-textmate "5.2.0" - -signal-exit@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= - -signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" - integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= - dependencies: - is-arrayish "^0.3.1" - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -socket.io-adapter@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b" - integrity sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs= - -socket.io-client@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.1.1.tgz#dcb38103436ab4578ddb026638ae2f21b623671f" - integrity sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ== - dependencies: - backo2 "1.0.2" - base64-arraybuffer "0.1.5" - component-bind "1.0.0" - component-emitter "1.2.1" - debug "~3.1.0" - engine.io-client "~3.2.0" - has-binary2 "~1.0.2" - has-cors "1.1.0" - indexof "0.0.1" - object-component "0.0.3" - parseqs "0.0.5" - parseuri "0.0.5" - socket.io-parser "~3.2.0" - to-array "0.1.4" - -socket.io-parser@~3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.2.0.tgz#e7c6228b6aa1f814e6148aea325b51aa9499e077" - integrity sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA== - dependencies: - component-emitter "1.2.1" - debug "~3.1.0" - isarray "2.0.1" - -socket.io@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.1.1.tgz#a069c5feabee3e6b214a75b40ce0652e1cfb9980" - integrity sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA== - dependencies: - debug "~3.1.0" - engine.io "~3.2.0" - has-binary2 "~1.0.2" - socket.io-adapter "~1.1.0" - socket.io-client "2.1.1" - socket.io-parser "~3.2.0" - -sockjs-client@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.4.0.tgz#c9f2568e19c8fd8173b4997ea3420e0bb306c7d5" - integrity sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g== - dependencies: - debug "^3.2.5" - eventsource "^1.0.7" - faye-websocket "~0.11.1" - inherits "^2.0.3" - json3 "^3.3.2" - url-parse "^1.4.3" - -sockjs@0.3.19: - version "0.3.19" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.19.tgz#d976bbe800af7bd20ae08598d582393508993c0d" - integrity sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw== - dependencies: - faye-websocket "^0.10.0" - uuid "^3.0.1" - -source-list-map@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" - integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== - -source-map-resolve@^0.5.0: - version "0.5.2" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" - integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA== - dependencies: - atob "^2.1.1" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-support@~0.5.12: - version "0.5.20" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9" - integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= - -source-map@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" - integrity sha1-66T12pwNyZneaAMti092FzZSA2s= - dependencies: - amdefine ">=0.0.4" - -source-map@^0.5.0, source-map@^0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -source-map@^0.7.3: - version "0.7.3" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== - -spdx-correct@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" - integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" - integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== - -spdx-expression-parse@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" - integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz#75ecd1a88de8c184ef015eafb51b5b48bfd11bb1" - integrity sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA== - -spdy-transport@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" - integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== - dependencies: - debug "^4.1.0" - detect-node "^2.0.4" - hpack.js "^2.1.6" - obuf "^1.1.2" - readable-stream "^3.0.6" - wbuf "^1.7.3" - -spdy@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" - integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== - dependencies: - debug "^4.1.0" - handle-thing "^2.0.0" - http-deceiver "^1.2.7" - select-hose "^2.0.0" - spdy-transport "^3.0.0" - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - -ssri@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" - integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q== - dependencies: - figgy-pudding "^3.5.1" - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= - -stdout-stream@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de" - integrity sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA== - dependencies: - readable-stream "^2.0.1" - -stream-browserify@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" - integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== - dependencies: - inherits "~2.0.1" - readable-stream "^2.0.2" - -stream-each@^1.1.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" - integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw== - dependencies: - end-of-stream "^1.1.0" - stream-shift "^1.0.0" - -stream-http@^2.7.2: - version "2.8.3" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" - integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.1" - readable-stream "^2.3.6" - to-arraybuffer "^1.0.0" - xtend "^4.0.0" - -stream-shift@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" - integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== - -streamroller@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-1.0.5.tgz#71660c20b06b1a7b204d46085731ad13c10a562d" - integrity sha512-iGVaMcyF5PcUY0cPbW3xFQUXnr9O4RZXNBBjhuLZgrjLO4XCLLGfx4T2sGqygSeylUjwgWRsnNbT9aV0Zb8AYw== - dependencies: - async "^2.6.2" - date-format "^2.0.0" - debug "^3.2.6" - fs-extra "^7.0.1" - lodash "^4.17.11" - -string-width@^1.0.1, string-width@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string-width@^3.0.0, string-width@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -string_decoder@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" - integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w== - dependencies: - safe-buffer "~5.1.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= - dependencies: - is-utf8 "^0.2.0" - -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= - dependencies: - get-stdin "^4.0.1" - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -supports-color@6.1.0, supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" - integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== - dependencies: - has-flag "^4.0.0" - -tapable@^1.0.0, tapable@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" - integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== - -tar@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" - integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA== - dependencies: - block-stream "*" - fstream "^1.0.12" - inherits "2" - -tar@^4: - version "4.4.10" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.10.tgz#946b2810b9a5e0b26140cf78bea6b0b0d689eba1" - integrity sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA== - dependencies: - chownr "^1.1.1" - fs-minipass "^1.2.5" - minipass "^2.3.5" - minizlib "^1.2.1" - mkdirp "^0.5.0" - safe-buffer "^5.1.2" - yallist "^3.0.3" - -terser-webpack-plugin@^1.4.3: - version "1.4.5" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b" - integrity sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw== - dependencies: - cacache "^12.0.2" - find-cache-dir "^2.1.0" - is-wsl "^1.1.0" - schema-utils "^1.0.0" - serialize-javascript "^4.0.0" - source-map "^0.6.1" - terser "^4.1.2" - webpack-sources "^1.4.0" - worker-farm "^1.7.0" - -terser@^4.1.2: - version "4.8.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" - integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== - dependencies: - commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" - -throttleit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" - integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw= - -through2@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" - integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== - dependencies: - readable-stream "~2.3.6" - xtend "~4.0.1" - -thunky@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.3.tgz#f5df732453407b09191dae73e2a8cc73f381a826" - integrity sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow== - -timers-browserify@^2.0.4: - version "2.0.12" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" - integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ== - dependencies: - setimmediate "^1.0.4" - -tmp@0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - -to-array@0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" - integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA= - -to-arraybuffer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" - integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - -toposort@2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" - integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA= - -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" - integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== - dependencies: - psl "^1.1.24" - punycode "^1.4.1" - -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= - -"true-case-path@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d" - integrity sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew== - dependencies: - glob "^7.1.2" - -ts-loader@7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-7.0.2.tgz#465bc904aea4c331e9550e7c7d75dd17a0b7c24c" - integrity sha512-DwpZFB67RoILQHx42dMjSgv2STpacsQu5X+GD/H9ocd8IhU0m8p3b/ZrIln2KmcucC6xep2PdEMEblpWT71euA== - dependencies: - chalk "^2.3.0" - enhanced-resolve "^4.0.0" - loader-utils "^1.0.2" - micromatch "^4.0.0" - semver "^6.0.0" - -tslib@1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8" - integrity sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ== - -tslib@^1.10.0: - version "1.11.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" - integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== - -tslib@^1.8.1, tslib@^1.9.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" - integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== - -tslint-eslint-rules@5.4.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/tslint-eslint-rules/-/tslint-eslint-rules-5.4.0.tgz#e488cc9181bf193fe5cd7bfca213a7695f1737b5" - integrity sha512-WlSXE+J2vY/VPgIcqQuijMQiel+UtmXS+4nvK4ZzlDiqBfXse8FAvkNnTcYhnQyOTW5KFM+uRRGXxYhFpuBc6w== - dependencies: - doctrine "0.7.2" - tslib "1.9.0" - tsutils "^3.0.0" - -tslint-microsoft-contrib@6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/tslint-microsoft-contrib/-/tslint-microsoft-contrib-6.2.0.tgz#8aa0f40584d066d05e6a5e7988da5163b85f2ad4" - integrity sha512-6tfi/2tHqV/3CL77pULBcK+foty11Rr0idRDxKnteTaKm6gWF9qmaCNU17HVssOuwlYNyOmd9Jsmjd+1t3a3qw== - dependencies: - tsutils "^2.27.2 <2.29.0" - -tslint@6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-6.1.2.tgz#2433c248512cc5a7b2ab88ad44a6b1b34c6911cf" - integrity sha512-UyNrLdK3E0fQG/xWNqAFAC5ugtFyPO4JJR1KyyfQAyzR8W0fTRrC91A8Wej4BntFzcvETdCSDa/4PnNYJQLYiA== - dependencies: - "@babel/code-frame" "^7.0.0" - builtin-modules "^1.1.1" - chalk "^2.3.0" - commander "^2.12.1" - diff "^4.0.1" - glob "^7.1.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" - mkdirp "^0.5.3" - resolve "^1.3.2" - semver "^5.3.0" - tslib "^1.10.0" - tsutils "^2.29.0" - -"tsutils@^2.27.2 <2.29.0": - version "2.28.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.28.0.tgz#6bd71e160828f9d019b6f4e844742228f85169a1" - integrity sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA== - dependencies: - tslib "^1.8.1" - -tsutils@^2.29.0: - version "2.29.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" - integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== - dependencies: - tslib "^1.8.1" - -tsutils@^3.0.0: - version "3.14.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.14.0.tgz#bf8d5a7bae5369331fa0f2b0a5a10bd7f7396c77" - integrity sha512-SmzGbB0l+8I0QwsPgjooFRaRvHLBLNYM8SeQ0k6rtNDru5sCGeLJcZdwilNndN+GysuFjF5EIYgN8GfFG6UeUw== - dependencies: - tslib "^1.8.1" - -tty-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" - integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= - -type-is@~1.6.17, type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= - -typedoc-default-themes@^0.12.10: - version "0.12.10" - resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.12.10.tgz#614c4222fe642657f37693ea62cad4dafeddf843" - integrity sha512-fIS001cAYHkyQPidWXmHuhs8usjP5XVJjWB8oZGqkTowZaz3v7g3KDZeeqE82FBrmkAnIBOY3jgy7lnPnqATbA== - -typedoc-plugin-external-module-map@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/typedoc-plugin-external-module-map/-/typedoc-plugin-external-module-map-1.2.1.tgz#32669a6b81e57962d2dae80d7a6ef8f5d0be65dd" - integrity sha512-ha+he4JFhCufF6wnpMpeH2XwsMgnYR6IrRUBCiMbZoYoudn6zICX7NA40pMjA35A6afxWNhKZU19pXnvysPK7A== - -typedoc@0.21.0: - version "0.21.0" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.21.0.tgz#d35dd69b1566032cd893f4f6f21f37156f5f78d2" - integrity sha512-InmPBVlpOXptIkg/WnsQhbGYhv9cuDh/cRACUSautQ0QwcJPLAK2kHcfP0Pld6z/NiDvHc159fMq2qS+b/ALUw== - dependencies: - glob "^7.1.7" - handlebars "^4.7.7" - lodash "^4.17.21" - lunr "^2.3.9" - marked "^2.1.1" - minimatch "^3.0.0" - progress "^2.0.3" - shiki "^0.9.3" - typedoc-default-themes "^0.12.10" - -typescript@4.4.4: - version "4.4.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c" - integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA== - -ua-parser-js@0.7.21: - version "0.7.21" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777" - integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ== - -uglify-js@^3.1.4: - version "3.14.2" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.2.tgz#d7dd6a46ca57214f54a2d0a43cad0f35db82ac99" - integrity sha512-rtPMlmcO4agTUfz10CbgJ1k6UAoXM2gWb3GoMPPZB/+/Ackf8lNWk11K4rYi2D0apgoFRLtQOZhb+/iGNJq26A== - -ultron@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" - integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og== - -union-value@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" - -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" - integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= - -unique-filename@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" - integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== - dependencies: - unique-slug "^2.0.0" - -unique-slug@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" - integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== - dependencies: - imurmurhash "^0.1.4" - -universalify@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -upath@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" - integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== - -uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== - dependencies: - punycode "^2.1.0" - -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - -url-loader@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.0.tgz#c7d6b0d6b0fccd51ab3ffc58a78d32b8d89a7be2" - integrity sha512-IzgAAIC8wRrg6NYkFIJY09vtktQcsvU8V6HhtQj9PTefbYImzLB1hufqo4m+RyM5N3mLx5BqJKccgxJS+W3kqw== - dependencies: - loader-utils "^2.0.0" - mime-types "^2.1.26" - schema-utils "^2.6.5" - -url-parse@^1.4.3: - version "1.4.7" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" - integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" - integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= - dependencies: - inherits "2.0.1" - -util@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" - integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== - dependencies: - inherits "2.0.3" - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= - -uuid@^3.0.1, uuid@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== - -v8-compile-cache@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" - integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w== - -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -vm-browserify@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== - -void-elements@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" - integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= - -vscode-textmate@5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" - integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== - -watchpack-chokidar2@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957" - integrity sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww== - dependencies: - chokidar "^2.1.8" - -watchpack@^1.6.1: - version "1.7.5" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.5.tgz#1267e6c55e0b9b5be44c2023aed5437a2c26c453" - integrity sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ== - dependencies: - graceful-fs "^4.1.2" - neo-async "^2.5.0" - optionalDependencies: - chokidar "^3.4.1" - watchpack-chokidar2 "^2.0.1" - -wbuf@^1.1.0, wbuf@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" - integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== - dependencies: - minimalistic-assert "^1.0.0" - -webpack-cli@3.3.11: - version "3.3.11" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.11.tgz#3bf21889bf597b5d82c38f215135a411edfdc631" - integrity sha512-dXlfuml7xvAFwYUPsrtQAA9e4DOe58gnzSxhgrO/ZM/gyXTBowrsYeubyN4mqGhYdpXMFNyQ6emjJS9M7OBd4g== - dependencies: - chalk "2.4.2" - cross-spawn "6.0.5" - enhanced-resolve "4.1.0" - findup-sync "3.0.0" - global-modules "2.0.0" - import-local "2.0.0" - interpret "1.2.0" - loader-utils "1.2.3" - supports-color "6.1.0" - v8-compile-cache "2.0.3" - yargs "13.2.4" - -webpack-dev-middleware@^3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.0.tgz#ef751d25f4e9a5c8a35da600c5fda3582b5c6cff" - integrity sha512-qvDesR1QZRIAZHOE3iQ4CXLZZSQ1lAUsSpnQmlB1PBfoN/xdRjmge3Dok0W4IdaVLJOGJy3sGI4sZHwjRU0PCA== - dependencies: - memory-fs "^0.4.1" - mime "^2.4.2" - range-parser "^1.2.1" - webpack-log "^2.0.0" - -webpack-dev-middleware@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3" - integrity sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw== - dependencies: - memory-fs "^0.4.1" - mime "^2.4.4" - mkdirp "^0.5.1" - range-parser "^1.2.1" - webpack-log "^2.0.0" - -webpack-dev-server@3.10.3: - version "3.10.3" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.10.3.tgz#f35945036813e57ef582c2420ef7b470e14d3af0" - integrity sha512-e4nWev8YzEVNdOMcNzNeCN947sWJNd43E5XvsJzbAL08kGc2frm1tQ32hTJslRS+H65LCb/AaUCYU7fjHCpDeQ== - dependencies: - ansi-html "0.0.7" - bonjour "^3.5.0" - chokidar "^2.1.8" - compression "^1.7.4" - connect-history-api-fallback "^1.6.0" - debug "^4.1.1" - del "^4.1.1" - express "^4.17.1" - html-entities "^1.2.1" - http-proxy-middleware "0.19.1" - import-local "^2.0.0" - internal-ip "^4.3.0" - ip "^1.1.5" - is-absolute-url "^3.0.3" - killable "^1.0.1" - loglevel "^1.6.6" - opn "^5.5.0" - p-retry "^3.0.1" - portfinder "^1.0.25" - schema-utils "^1.0.0" - selfsigned "^1.10.7" - semver "^6.3.0" - serve-index "^1.9.1" - sockjs "0.3.19" - sockjs-client "1.4.0" - spdy "^4.0.1" - strip-ansi "^3.0.1" - supports-color "^6.1.0" - url "^0.11.0" - webpack-dev-middleware "^3.7.2" - webpack-log "^2.0.0" - ws "^6.2.1" - yargs "12.0.5" - -webpack-log@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" - integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== - dependencies: - ansi-colors "^3.0.0" - uuid "^3.3.2" - -webpack-sources@^1.4.0, webpack-sources@^1.4.1: - version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" - integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - -webpack@4.43.0: - version "4.43.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.43.0.tgz#c48547b11d563224c561dad1172c8aa0b8a678e6" - integrity sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/wasm-edit" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - acorn "^6.4.1" - ajv "^6.10.2" - ajv-keywords "^3.4.1" - chrome-trace-event "^1.0.2" - enhanced-resolve "^4.1.0" - eslint-scope "^4.0.3" - json-parse-better-errors "^1.0.2" - loader-runner "^2.4.0" - loader-utils "^1.2.3" - memory-fs "^0.4.1" - micromatch "^3.1.10" - mkdirp "^0.5.3" - neo-async "^2.6.1" - node-libs-browser "^2.2.1" - schema-utils "^1.0.0" - tapable "^1.1.3" - terser-webpack-plugin "^1.4.3" - watchpack "^1.6.1" - webpack-sources "^1.4.1" - -websocket-driver@>=0.5.1: - version "0.7.3" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" - integrity sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg== - dependencies: - http-parser-js ">=0.4.0 <0.4.11" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" - integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== - -which-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" - integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8= - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which-pm-runs@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" - integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= - -which@1, which@^1.2.1, which@^1.2.10, which@^1.2.14, which@^1.2.9, which@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - -wordwrap@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= - -worker-farm@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" - integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== - dependencies: - errno "~0.1.7" - -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -ws@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" - integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== - dependencies: - async-limiter "~1.0.0" - -ws@~3.3.1: - version "3.3.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" - integrity sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA== - dependencies: - async-limiter "~1.0.0" - safe-buffer "~5.1.0" - ultron "~1.1.0" - -xmlhttprequest-ssl@~1.5.4: - version "1.5.5" - resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" - integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= - -xtend@^4.0.0, xtend@~4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -y18n@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" - integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= - -"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== - -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= - -yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" - integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== - -yaml@^1.7.2: - version "1.9.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.9.2.tgz#f0cfa865f003ab707663e4f04b3956957ea564ed" - integrity sha512-HPT7cGGI0DuRcsO51qC1j9O16Dh1mZ2bnXwsi0jrSpsLz0WxOLSLXfkABVl6bZO629py3CU+OMJtpNHDLB97kg== - dependencies: - "@babel/runtime" "^7.9.2" - -yargs-parser@^11.1.1: - version "11.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" - integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-parser@^13.1.0: - version "13.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" - integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-parser@^18.1.1: - version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-parser@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" - integrity sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo= - dependencies: - camelcase "^3.0.0" - -yargs@12.0.5: - version "12.0.5" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" - integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== - dependencies: - cliui "^4.0.0" - decamelize "^1.2.0" - find-up "^3.0.0" - get-caller-file "^1.0.1" - os-locale "^3.0.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1 || ^4.0.0" - yargs-parser "^11.1.1" - -yargs@13.2.4: - version "13.2.4" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83" - integrity sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - os-locale "^3.1.0" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.0" - -yargs@^15.3.1: - version "15.3.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.3.1.tgz#9505b472763963e54afe60148ad27a330818e98b" - integrity sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.1" - -yargs@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" - integrity sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg= - dependencies: - camelcase "^3.0.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^1.4.0" - read-pkg-up "^1.0.1" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^1.0.2" - which-module "^1.0.0" - y18n "^3.2.1" - yargs-parser "^5.0.0" - -yauzl@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" - integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU= - dependencies: - fd-slicer "~1.0.1" - -yeast@0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" - integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" + integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA== + dependencies: + "@babel/highlight" "^7.0.0" + +"@babel/code-frame@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/core@^7.7.5": + version "7.11.1" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.1.tgz#2c55b604e73a40dc21b0e52650b11c65cf276643" + integrity sha512-XqF7F6FWQdKGGWAzGELL+aCO1p+lRY5Tj5/tbT3St1G8NaH70jhhDIKknIZaDans0OQBG5wRAldROLHSt44BgQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.11.0" + "@babel/helper-module-transforms" "^7.11.0" + "@babel/helpers" "^7.10.4" + "@babel/parser" "^7.11.1" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.11.0" + "@babel/types" "^7.11.0" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/generator@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.0.tgz#4b90c78d8c12825024568cbe83ee6c9af193585c" + integrity sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ== + dependencies: + "@babel/types" "^7.11.0" + jsesc "^2.5.1" + source-map "^0.5.0" + +"@babel/helper-function-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" + integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== + dependencies: + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-get-function-arity@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" + integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-member-expression-to-functions@^7.10.4": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" + integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q== + dependencies: + "@babel/types" "^7.11.0" + +"@babel/helper-module-imports@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" + integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-module-transforms@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" + integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-simple-access" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/template" "^7.10.4" + "@babel/types" "^7.11.0" + lodash "^4.17.19" + +"@babel/helper-optimise-call-expression@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" + integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-replace-supers@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" + integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.10.4" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-simple-access@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" + integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw== + dependencies: + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-split-export-declaration@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" + integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== + dependencies: + "@babel/types" "^7.11.0" + +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== + +"@babel/helpers@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044" + integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA== + dependencies: + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/highlight@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" + integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" + +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1": + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.2.tgz#0882ab8a455df3065ea2dcb4c753b2460a24bead" + integrity sha512-Vuj/+7vLo6l1Vi7uuO+1ngCDNeVmNbTngcJFKCR/oEtz8tKz0CJxZEGmPt9KcIloZhOZ3Zit6xbpXT2MDlS9Vw== + +"@babel/runtime@^7.9.2": + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f" + integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/template@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" + integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.0.tgz#9b996ce1b98f53f7c3e4175115605d56ed07dd24" + integrity sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.11.0" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/parser" "^7.11.0" + "@babel/types" "^7.11.0" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + +"@babel/types@^7.10.4", "@babel/types@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.0.tgz#2ae6bf1ba9ae8c3c43824e5861269871b206e90d" + integrity sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + +"@fluentui/date-time-utilities@^8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@fluentui/date-time-utilities/-/date-time-utilities-8.3.0.tgz#a51cd59ea327b948bdb76a083d9c42d95a71c98a" + integrity sha512-shSWwarh+ueDF22hIRL1T9bGA4epsP8QAIfYhpoK5ySr0Zg5RjqdNMM5IXzlTiXqE/LGe16CVrvyW2ksqfvQPg== + dependencies: + "@fluentui/set-version" "^8.1.5" + tslib "^2.1.0" + +"@fluentui/dom-utilities@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@fluentui/dom-utilities/-/dom-utilities-2.1.5.tgz#21ba77c8bfe64d15ffc16a8e055255bbb23600b2" + integrity sha512-OLDYV5ZGIiK/JXx/DFFib4vSa7PELvznbdAujDcX2wjt3V3Lt2N5ucv59JsVxk5LlwXjasUHJI2NZadagmnM6A== + dependencies: + "@fluentui/set-version" "^8.1.5" + tslib "^2.1.0" + +"@fluentui/font-icons-mdl2@^8.1.26": + version "8.1.26" + resolved "https://registry.yarnpkg.com/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.1.26.tgz#e74070e8ab91302539ccb23a05e0a6c792333e42" + integrity sha512-CmdyZ3QP5UCdyHrYwwtA2AlFyG2oG2aOav4A7quVZZ60gX/63tPUskcVK+UvB8f+zecOLBvaHFJIkTWL13fvvA== + dependencies: + "@fluentui/set-version" "^8.1.5" + "@fluentui/style-utilities" "^8.5.8" + tslib "^2.1.0" + +"@fluentui/foundation-legacy@^8.1.25": + version "8.1.25" + resolved "https://registry.yarnpkg.com/@fluentui/foundation-legacy/-/foundation-legacy-8.1.25.tgz#2c0c149c2ab037419458f71632504ad87a4b340e" + integrity sha512-keV/jdvUJAPFiMARrruvBGvXgix0kMbo+pfrdsbEFnQHs2o9m7Ha1ChsQCAkVmhv3DYrkb7shDG9R1eerPLJiw== + dependencies: + "@fluentui/merge-styles" "^8.3.0" + "@fluentui/set-version" "^8.1.5" + "@fluentui/style-utilities" "^8.5.8" + "@fluentui/utilities" "^8.5.0" + tslib "^2.1.0" + +"@fluentui/keyboard-key@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@fluentui/keyboard-key/-/keyboard-key-0.3.5.tgz#af1273bbd8db3e7e08bf8ce8a303890d33d4d8b2" + integrity sha512-qPNPnRtkC92b8Zjx3mJ6+vRX+pdmbDYcXP8zXb2NJ/briAQXYmyqdjJLUl2riVBcAC4H3cL6dTKLR9VAyqhdYQ== + dependencies: + tslib "^2.1.0" + +"@fluentui/merge-styles@^8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@fluentui/merge-styles/-/merge-styles-8.3.0.tgz#e27581dd43cbe1b5d54d7325e619924883dd0fd5" + integrity sha512-qI61vWnmSuZYopMfWbEkTuDmnedmOYmsphTwflG6zif1EPV6h5u3dDUfHnmHMahywNOLOv5MOa+zVU0SCEHSAg== + dependencies: + "@fluentui/set-version" "^8.1.5" + tslib "^2.1.0" + +"@fluentui/react-focus@^8.3.21": + version "8.3.21" + resolved "https://registry.yarnpkg.com/@fluentui/react-focus/-/react-focus-8.3.21.tgz#01f3f9398719496fa1158c3f227cf855e4938e4e" + integrity sha512-MCPKvPiyifwAErjXyAAu9NWlhLEJOohcGf4y6GVKWlXi44oslSIfKQatUVrstFW83hbkcXHigDuBZmhdyTThVg== + dependencies: + "@fluentui/keyboard-key" "^0.3.5" + "@fluentui/merge-styles" "^8.3.0" + "@fluentui/set-version" "^8.1.5" + "@fluentui/style-utilities" "^8.5.8" + "@fluentui/utilities" "^8.5.0" + tslib "^2.1.0" + +"@fluentui/react-hooks@^8.4.0": + version "8.4.0" + resolved "https://registry.yarnpkg.com/@fluentui/react-hooks/-/react-hooks-8.4.0.tgz#a4c75f97badc7d758abb36138c4e928d27842229" + integrity sha512-BI0lp+5rlabvXpV6uCEP+cCvCJy5srlrPZ2J63Jr1T+nWnn7CKJ5mgE2Uc+VYW9EoHRLJAFbrD0a9WYkXKZ3xA== + dependencies: + "@fluentui/react-window-provider" "^2.1.6" + "@fluentui/set-version" "^8.1.5" + "@fluentui/utilities" "^8.5.0" + tslib "^2.1.0" + +"@fluentui/react-window-provider@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@fluentui/react-window-provider/-/react-window-provider-2.1.6.tgz#1e3d66842a6b1c724dcc3ee13d2bc0aa0d7028a8" + integrity sha512-Fr1THmtn/Kx95/WVXI1qIELX2VHpuGYVKWSpdgbpcmKukvk30h4JG6kKMGYPz3r7UeaTzpaeIw537A9CJ2hVMg== + dependencies: + "@fluentui/set-version" "^8.1.5" + tslib "^2.1.0" + +"@fluentui/react@^8.0.0": + version "8.56.2" + resolved "https://registry.yarnpkg.com/@fluentui/react/-/react-8.56.2.tgz#6c6246f5c1a9a20cc2121e7db6bcdbf2da5f7d19" + integrity sha512-RMmto4/zExtfQf3r9ooQyqdC2lWSpNWQ9apvK464pW314BXWNMFO6E3mb8vDPjmfBzZ8yDYjrB0L2y6OO4ORDA== + dependencies: + "@fluentui/date-time-utilities" "^8.3.0" + "@fluentui/font-icons-mdl2" "^8.1.26" + "@fluentui/foundation-legacy" "^8.1.25" + "@fluentui/merge-styles" "^8.3.0" + "@fluentui/react-focus" "^8.3.21" + "@fluentui/react-hooks" "^8.4.0" + "@fluentui/react-window-provider" "^2.1.6" + "@fluentui/set-version" "^8.1.5" + "@fluentui/style-utilities" "^8.5.8" + "@fluentui/theme" "^2.4.12" + "@fluentui/utilities" "^8.5.0" + "@microsoft/load-themed-styles" "^1.10.26" + tslib "^2.1.0" + +"@fluentui/set-version@^8.1.5": + version "8.1.5" + resolved "https://registry.yarnpkg.com/@fluentui/set-version/-/set-version-8.1.5.tgz#68d3d8c7fbefba20b3d1aef71fcc730ca46dd353" + integrity sha512-AfaycaduWd/aErqEmrAUWpr2gpZrkaSe6D9noXhtVH3JlreRuFM78Ji1oE4f8cpWxSA/K5qb7BT6x4z4I2Bs+A== + dependencies: + tslib "^2.1.0" + +"@fluentui/style-utilities@^8.5.8": + version "8.5.8" + resolved "https://registry.yarnpkg.com/@fluentui/style-utilities/-/style-utilities-8.5.8.tgz#198512c3a710c71b814c5a0eba1e9f885f91a4e1" + integrity sha512-g6jUptGmkKrh0mDQBjKi+1wUNlJeRjpemavXmSBG+i+ZTn8PT/XjshLB9T9Q7FBriil0ccr6wBkPr11ElZC5gg== + dependencies: + "@fluentui/merge-styles" "^8.3.0" + "@fluentui/set-version" "^8.1.5" + "@fluentui/theme" "^2.4.12" + "@fluentui/utilities" "^8.5.0" + "@microsoft/load-themed-styles" "^1.10.26" + tslib "^2.1.0" + +"@fluentui/theme@^2.4.12": + version "2.4.12" + resolved "https://registry.yarnpkg.com/@fluentui/theme/-/theme-2.4.12.tgz#82181b786922aab142816af300f6e3f54ac83357" + integrity sha512-dlBmGOp3Ib08iP1F2rU8gb/DX/tsbdzNNFT0WqlO+1bjAVwnmn46xuDpsvGgMz0ZRYN4ZXfpvWkWqXpDnsHS9g== + dependencies: + "@fluentui/merge-styles" "^8.3.0" + "@fluentui/set-version" "^8.1.5" + "@fluentui/utilities" "^8.5.0" + tslib "^2.1.0" + +"@fluentui/utilities@^8.5.0": + version "8.5.0" + resolved "https://registry.yarnpkg.com/@fluentui/utilities/-/utilities-8.5.0.tgz#07a2d9c1f41ca294e89a5a4f454479d51f86ff59" + integrity sha512-npuYmwQfCx2WggW0MHwKgpaSPdk62FtQLmw0zoCNdSNDDQ5u3lWKScwy7FR3Nn1jjL0CDaC9PnD8ults2iIeSg== + dependencies: + "@fluentui/dom-utilities" "^2.1.5" + "@fluentui/merge-styles" "^8.3.0" + "@fluentui/set-version" "^8.1.5" + tslib "^2.1.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" + integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.3.tgz#8108265659d4c33e72ffe14e33d6cc5eb59f2fda" + integrity sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@1.4.14": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.18" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" + integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + +"@jsdevtools/coverage-istanbul-loader@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz#2a4bc65d0271df8d4435982db4af35d81754ee26" + integrity sha512-EUCPEkaRPvmHjWAAZkWMT7JDzpw7FKB00WTISaiXsbNOd5hCHg77XLA8sLYLFDo1zepYLo2w7GstN8YBqRXZfA== + dependencies: + convert-source-map "^1.7.0" + istanbul-lib-instrument "^4.0.3" + loader-utils "^2.0.0" + merge-source-map "^1.1.0" + schema-utils "^2.7.0" + +"@microsoft/load-themed-styles@1.10.44": + version "1.10.44" + resolved "https://registry.yarnpkg.com/@microsoft/load-themed-styles/-/load-themed-styles-1.10.44.tgz#55ab022a9b7790492215d3fc1b408e597bb689c8" + integrity sha512-OHLj1VT0gwkDDaWJoCsmvIu2WhNHOXudxQQJ58gJnAowR5l9c4GwJsGbqePGZ1w4h68+cEF/1vXsjTpwJiKFvg== + +"@microsoft/load-themed-styles@^1.10.26": + version "1.10.247" + resolved "https://registry.yarnpkg.com/@microsoft/load-themed-styles/-/load-themed-styles-1.10.247.tgz#964ef32836a050c09486a6e9d092e50c9fb642ec" + integrity sha512-vKbuG3Mcbc4kkNAcIE13aIv5KoI2g+tHFFIZnFhtUilpYHc0VsMd4Fw7Jz81A8AB7L3wWu3OZB2CNiRnr1a3ew== + +"@microsoft/loader-load-themed-styles@1.8.11": + version "1.8.11" + resolved "https://registry.yarnpkg.com/@microsoft/loader-load-themed-styles/-/loader-load-themed-styles-1.8.11.tgz#e2f67dd49df10cb2f86b744b1c93cb514203bcdb" + integrity sha512-ynaXU8Mt5javarBsVwBOQCkE9KXwxzkRRpf8LtZiqB27WZwpO4nLPfDcIZxzdfoNIDvy2f7hIxVVQP4hsFecCA== + dependencies: + "@microsoft/load-themed-styles" "1.10.44" + loader-utils "~1.1.0" + +"@socket.io/base64-arraybuffer@~1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#568d9beae00b0d835f4f8c53fd55714986492e61" + integrity sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ== + +"@types/color-convert@*": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-2.0.0.tgz#8f5ee6b9e863dcbee5703f5a517ffb13d3ea4e22" + integrity sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ== + dependencies: + "@types/color-name" "*" + +"@types/color-name@*", "@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + +"@types/color@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/color/-/color-3.0.0.tgz#40f8a6bf2fd86e969876b339a837d8ff1b0a6e30" + integrity sha512-5qqtNia+m2I0/85+pd2YzAXaTyKO8j+svirO5aN+XaQJ5+eZ8nx0jPtEWZLxCi50xwYsX10xUHetFzfb1WEs4Q== + dependencies: + "@types/color-convert" "*" + +"@types/component-emitter@^1.2.10": + version "1.2.11" + resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.11.tgz#50d47d42b347253817a39709fef03ce66a108506" + integrity sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ== + +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + +"@types/cors@^2.8.12": + version "2.8.12" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" + integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== + +"@types/dompurify@2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.2.3.tgz#6e89677a07902ac1b6821c345f34bd85da239b08" + integrity sha512-CLtc2mZK8+axmrz1JqtpklO/Kvn38arGc8o1l3UVopZaXXuer9ONdZwJ/9f226GrhRLtUmLr9WrvZsRSNpS8og== + dependencies: + "@types/trusted-types" "*" + +"@types/eslint-scope@^3.7.3": + version "3.7.4" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" + integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.40.0" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.40.0.tgz#ae73dc9ec5237f2794c4f79efd6a4c73b13daf23" + integrity sha512-nbq2mvc/tBrK9zQQuItvjJl++GTN5j06DaPtp3hZCpngmG6Q3xoyEmd0TwZI0gAy/G1X0zhGBbr2imsGFdFV0g== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" + integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== + +"@types/events@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" + integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== + +"@types/glob@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" + integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== + dependencies: + "@types/events" "*" + "@types/minimatch" "*" + "@types/node" "*" + +"@types/jasmine@3.5.10": + version "3.5.10" + resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.5.10.tgz#a1a41012012b5da9d4b205ba9eba58f6cce2ab7b" + integrity sha512-3F8qpwBAiVc5+HPJeXJpbrl+XjawGmciN5LgiO7Gv1pl1RHtjoMNqZpqEksaPJW05ViKe8snYInRs6xB25Xdew== + +"@types/json-schema@*", "@types/json-schema@^7.0.8": + version "7.0.12" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" + integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== + +"@types/json-schema@^7.0.4": + version "7.0.5" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" + integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== + +"@types/minimatch@*", "@types/minimatch@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + +"@types/node@*": + version "12.0.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.10.tgz#51babf9c7deadd5343620055fc8aff7995c8b031" + integrity sha512-LcsGbPomWsad6wmMNv7nBLw7YYYyfdYcz6xryKYQhx89c3XXan+8Q6AJ43G5XDIaklaVkK3mE4fCb0SBvMiPSQ== + +"@types/node@13.13.4": + version "13.13.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.4.tgz#1581d6c16e3d4803eb079c87d4ac893ee7501c2c" + integrity sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA== + +"@types/node@>=10.0.0": + version "17.0.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.17.tgz#a8ddf6e0c2341718d74ee3dc413a13a042c45a0c" + integrity sha512-e8PUNQy1HgJGV3iU/Bp2+D/DXh3PYeyli8LgIwsQcs1Ar1LoaWHSIT6Rw+H2rNJmiq6SNWiDytfx8+gYj7wDHw== + +"@types/object-assign@4.0.30": + version "4.0.30" + resolved "https://registry.yarnpkg.com/@types/object-assign/-/object-assign-4.0.30.tgz#8949371d5a99f4381ee0f1df0a9b7a187e07e652" + integrity sha1-iUk3HVqZ9Dge4PHfCpt6GH4H5lI= + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@types/prop-types@*": + version "15.7.1" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.1.tgz#f1a11e7babb0c3cad68100be381d1e064c68f1f6" + integrity sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg== + +"@types/react-dom@17.0.11": + version "17.0.11" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.11.tgz#e1eadc3c5e86bdb5f7684e00274ae228e7bcc466" + integrity sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@16.8.22": + version "16.8.22" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.22.tgz#7f18bf5ea0c1cad73c46b6b1c804a3ce0eec6d54" + integrity sha512-C3O1yVqk4sUXqWyx0wlys76eQfhrQhiDhDlHBrjER76lR2S2Agiid/KpOU9oCqj1dISStscz7xXz1Cg8+sCQeA== + dependencies: + "@types/prop-types" "*" + csstype "^2.2.0" + +"@types/trusted-types@*": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756" + integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg== + +"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" + integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + +"@webassemblyjs/helper-buffer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" + integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== + +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + +"@webassemblyjs/helper-wasm-section@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" + integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" + integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-opt" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/wast-printer" "1.11.6" + +"@webassemblyjs/wasm-gen@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" + integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" + integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + +"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" + integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" + integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@xtuc/long" "4.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-assertions@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" + integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== + +acorn@^8.5.0, acorn@^8.7.1: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + +address@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" + integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== + +ajv-errors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" + integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== + +ajv-keywords@^3.1.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.0.tgz#4b831e7b531415a7cc518cd404e73f6193c6349d" + integrity sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw== + +ajv-keywords@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" + integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@^6.1.0, ajv@^6.12.0, ajv@^6.12.2, ajv@^6.12.5, ajv@^6.5.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-colors@^3.0.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" + integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== + +ansi-html@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" + integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-differ@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" + integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-flatten@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= + dependencies: + array-uniq "^1.0.1" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +arrify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +async-each@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== + +async-limiter@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" + integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== + +async@^2.6.2: + version "2.6.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== + dependencies: + lodash "^4.17.14" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +atob@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +big.js@^3.1.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" + integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^1.0.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" + integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== + +binary-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" + integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== + +body-parser@1.20.1, body-parser@^1.19.0: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^2.3.1, braces@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@^4.14.5: + version "4.21.7" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.7.tgz#e2b420947e5fb0a58e8f4668ae6e23488127e551" + integrity sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA== + dependencies: + caniuse-lite "^1.0.30001489" + electron-to-chromium "^1.4.411" + node-releases "^2.0.12" + update-browserslist-db "^1.0.11" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-indexof@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" + integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== + +builtin-modules@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +caniuse-lite@^1.0.30001489: + version "1.0.30001492" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001492.tgz#4a06861788a52b4c81fd3344573b68cc87fe062b" + integrity sha512-2efF8SAZwgAX1FJr87KWhvuJxnGJKOnctQa8xLOskAXNXq8oiuqgl6u1kk3fFpsp3GgvzlRjiK1sl63hNtFADw== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chalk@2.4.2, chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72" + integrity sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.1: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chokidar@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" + integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + optionalDependencies: + fsevents "^1.2.7" + +chownr@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +chrome-trace-event@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" + integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== + dependencies: + tslib "^1.9.0" + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +cliui@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" + integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + wrap-ansi "^2.0.0" + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0, color-convert@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.5.4: + version "1.5.5" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014" + integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" + integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ== + dependencies: + color-convert "^1.9.1" + color-string "^1.5.4" + +colors@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^2.12.1, commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +compare-versions@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" + integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== + +component-emitter@^1.2.1, component-emitter@~1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +compressible@~2.0.16: + version "2.0.17" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.17.tgz#6e8c108a16ad58384a977f3a482ca20bff2f38c1" + integrity sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw== + dependencies: + mime-db ">= 1.40.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +connect-history-api-fallback@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" + integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== + +connect@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +convert-source-map@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" + integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== + dependencies: + safe-buffer "~5.1.1" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +cookie@~0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cors@~2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cosmiconfig@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" + integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.7.2" + +coverage-istanbul-loader@3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz#bf942efc0f4e3ac27565203c17dca5008eae6637" + integrity sha512-xsw2phF0VNqUPk47V/vHXkdcTyl0tkMSmaZfLrTOhoPhPMXFelNju7utl5s7I93KXzipqDEK0YwofQSSflPz8A== + dependencies: + "@jsdevtools/coverage-istanbul-loader" "3.0.5" + +cross-spawn@6.0.5, cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.2.tgz#d0d7dcfa74e89115c7619f4f721a94e1fdb716d6" + integrity sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +css-loader@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.5.3.tgz#95ac16468e1adcd95c844729e0bb167639eb0bcf" + integrity sha512-UEr9NH5Lmi7+dguAm+/JSPovNjYbm2k3TK58EiwQHzOHH5Jfq1Y+XoP2bQO6TMn7PptMd0opxxedAWcaSTRKHw== + dependencies: + camelcase "^5.3.1" + cssesc "^3.0.0" + icss-utils "^4.1.1" + loader-utils "^1.2.3" + normalize-path "^3.0.0" + postcss "^7.0.27" + postcss-modules-extract-imports "^2.0.0" + postcss-modules-local-by-default "^3.0.2" + postcss-modules-scope "^2.2.0" + postcss-modules-values "^3.0.0" + postcss-value-parser "^4.0.3" + schema-utils "^2.6.6" + semver "^6.3.0" + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +csstype@^2.2.0: + version "2.6.5" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.5.tgz#1cd1dff742ebf4d7c991470ae71e12bb6751e034" + integrity sha512-JsTaiksRsel5n7XwqPAfB0l3TFKdpjW/kgAELf9vrb5adGA7UCPLajKK5s3nFrcFm3Rkyp/Qkgl73ENc1UY3cA== + +custom-event@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" + integrity sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU= + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +date-format@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.3.tgz#f63de5dc08dc02efd8ef32bf2a6918e486f35873" + integrity sha512-7P3FyqDcfeznLZp2b+OMitV9Sz2lUnsT87WaTat9nVwqsBkTzPG3lPLNwW3en6F4pHUiWzr6vb8CLhjdK9bcxQ== + +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.0, debug@^4.1.1, debug@^4.3.3, debug@~4.3.1, debug@~4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decode-uri-component@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + +deep-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +default-gateway@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" + integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== + dependencies: + execa "^1.0.0" + ip-regex "^2.1.0" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +del@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" + integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== + dependencies: + "@types/glob" "^7.1.1" + globby "^6.1.0" + is-path-cwd "^2.0.0" + is-path-in-cwd "^2.0.0" + p-map "^2.0.0" + pify "^4.0.1" + rimraf "^2.6.3" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-file@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" + integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= + +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + +detect-node@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" + integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== + +detect-port@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.3.0.tgz#d9c40e9accadd4df5cac6a782aefd014d573d1f1" + integrity sha512-E+B1gzkl2gqxt1IhUzwjrxBKRqx1UzC3WLONHinn8S3T6lwV/agVCyitiFOsGJ/eYuEUBvD71MZHy3Pv1G9doQ== + dependencies: + address "^1.0.1" + debug "^2.6.0" + +di@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" + integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw= + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= + +dns-packet@^1.3.1: + version "1.3.4" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.4.tgz#e3455065824a2507ba886c55a89963bb107dec6f" + integrity sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA== + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= + dependencies: + buffer-indexof "^1.0.0" + +doctrine@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-0.7.2.tgz#7cb860359ba3be90e040b26b729ce4bfa654c523" + integrity sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM= + dependencies: + esutils "^1.1.6" + isarray "0.0.1" + +dom-serialize@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" + integrity sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs= + dependencies: + custom-event "~1.0.0" + ent "~2.2.0" + extend "^3.0.0" + void-elements "^2.0.0" + +dompurify@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.0.tgz#07bb39515e491588e5756b1d3e8375b5964814e2" + integrity sha512-VV5C6Kr53YVHGOBKO/F86OYX6/iLTw2yVSI721gKetxpHCK/V5TaLEf9ODjRgl1KLSWRMY6cUhAbv/c+IUnwQw== + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.4.411: + version "1.4.414" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.414.tgz#f9eedb6fb01b50439d8228d8ee3a6fa5e0108437" + integrity sha512-RRuCvP6ekngVh2SAJaOKT/hxqc9JAsK+Pe0hP5tGQIfonU2Zy9gMGdJ+mBdyl/vNucMG6gkXYtuM4H/1giws5w== + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +end-of-stream@^1.1.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== + dependencies: + once "^1.4.0" + +engine.io-parser@~5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.3.tgz#ca1f0d7b11e290b4bfda251803baea765ed89c09" + integrity sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg== + dependencies: + "@socket.io/base64-arraybuffer" "~1.0.2" + +engine.io@~6.1.0: + version "6.1.2" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.1.2.tgz#e7b9d546d90c62246ffcba4d88594be980d3855a" + integrity sha512-v/7eGHxPvO2AWsksyx2PUsQvBafuvqs0jJJQ0FdmJG1b9qIvgSbqDRGwNhfk2XHaTTbTXiC4quRE8Q9nRjsrQQ== + dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.4.1" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.0.0" + ws "~8.2.3" + +enhanced-resolve@4.1.0, enhanced-resolve@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" + integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.4.0" + tapable "^1.0.0" + +enhanced-resolve@^5.14.1: + version "5.14.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz#de684b6803724477a4af5d74ccae5de52c25f6b3" + integrity sha512-Vklwq2vDKtl0y/vtwjSesgJ5MYS7Etuk5txS8VdKL4AOS1aUlD96zqIfsOSLQsdv3xgMRbtkWM8eG9XDfKUPow== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +ent@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" + integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= + +errno@^0.1.3: + version "0.1.7" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" + integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== + dependencies: + prr "~1.0.1" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-module-lexer@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.2.1.tgz#ba303831f63e6a394983fde2f97ad77b22324527" + integrity sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg== + +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM= + +estraverse@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + +esutils@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-1.1.6.tgz#c01ccaa9ae4b897c6d0c3e210ae52f3c7a844375" + integrity sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U= + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +eventsource@^1.0.7: + version "1.1.1" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.1.tgz#4544a35a57d7120fba4fa4c86cb4023b2c09df2f" + integrity sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA== + dependencies: + original "^1.0.0" + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +execa@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-2.1.0.tgz#e5d3ecd837d2a60ec50f3da78fd39767747bbe99" + integrity sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^3.0.0" + onetime "^5.1.0" + p-finally "^2.0.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expand-tilde@^2.0.0, expand-tilde@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" + integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= + dependencies: + homedir-polyfill "^1.0.1" + +express@^4.17.1: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@^3.0.0, extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extract-zip@^1.6.5: + version "1.6.7" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" + integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k= + dependencies: + concat-stream "1.6.2" + debug "2.6.9" + mkdirp "0.5.1" + yauzl "2.4.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +faye-websocket@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" + integrity sha1-TkkvjQTftviQA1B/btvy1QHnxvQ= + dependencies: + websocket-driver ">=0.5.1" + +faye-websocket@~0.11.1: + version "0.11.3" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" + integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== + dependencies: + websocket-driver ">=0.5.1" + +fd-slicer@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" + integrity sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU= + dependencies: + pend "~1.2.0" + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-versions@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-3.2.0.tgz#10297f98030a786829681690545ef659ed1d254e" + integrity sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww== + dependencies: + semver-regex "^2.0.0" + +findup-sync@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" + integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== + dependencies: + detect-file "^1.0.0" + is-glob "^4.0.0" + micromatch "^3.0.4" + resolve-dir "^1.0.1" + +flatted@^3.2.4: + version "3.2.5" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" + integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== + +follow-redirects@^1.0.0: + version "1.14.8" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" + integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== + +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" + integrity sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA= + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + +fs-extra@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" + integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-minipass@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" + integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== + dependencies: + minipass "^2.6.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@^1.2.7: + version "1.2.9" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" + integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== + dependencies: + nan "^2.12.1" + node-pre-gyp "^0.12.0" + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gensync@^1.0.0-beta.1: + version "1.0.0-beta.1" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" + integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== + +get-caller-file@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" + integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== + +get-caller-file@^2.0.1, get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" + integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-stream@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9" + integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw== + dependencies: + pump "^3.0.0" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.3, glob@^7.1.1, glob@^7.1.3, glob@^7.1.7: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-modules@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + +global-modules@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" + integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== + dependencies: + global-prefix "^1.0.1" + is-windows "^1.0.1" + resolve-dir "^1.0.0" + +global-prefix@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" + integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= + dependencies: + expand-tilde "^2.0.2" + homedir-polyfill "^1.0.1" + ini "^1.3.4" + is-windows "^1.0.1" + which "^1.2.14" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.6: + version "4.2.9" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" + integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== + +graceful-fs@^4.2.10, graceful-fs@^4.2.4, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +handle-thing@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" + integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== + +handlebars@^4.7.7: + version "4.7.7" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.0" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hasha@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1" + integrity sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE= + dependencies: + is-stream "^1.0.1" + pinkie-promise "^2.0.0" + +homedir-polyfill@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" + integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== + dependencies: + parse-passwd "^1.0.0" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-entities@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" + integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +"http-parser-js@>=0.4.0 <0.4.11": + version "0.4.10" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" + integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q= + +http-proxy-middleware@0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" + integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== + dependencies: + http-proxy "^1.17.0" + is-glob "^4.0.0" + lodash "^4.17.11" + micromatch "^3.1.10" + +http-proxy@^1.17.0, http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +husky@^4.2.5: + version "4.2.5" + resolved "https://registry.yarnpkg.com/husky/-/husky-4.2.5.tgz#2b4f7622673a71579f901d9885ed448394b5fa36" + integrity sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ== + dependencies: + chalk "^4.0.0" + ci-info "^2.0.0" + compare-versions "^3.6.0" + cosmiconfig "^6.0.0" + find-versions "^3.2.0" + opencollective-postinstall "^2.0.2" + pkg-dir "^4.2.0" + please-upgrade-node "^3.2.0" + slash "^3.0.0" + which-pm-runs "^1.0.0" + +iconv-lite@0.4.24, iconv-lite@^0.4.4: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +icss-utils@^4.0.0, icss-utils@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" + integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== + dependencies: + postcss "^7.0.14" + +ignore-walk@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" + integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ== + dependencies: + minimatch "^3.0.4" + +ignore@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" + integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== + +immutable@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23" + integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw== + +import-fresh@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" + integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@2.0.0, import-local@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" + integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== + dependencies: + pkg-dir "^3.0.0" + resolve-cwd "^2.0.0" + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + +internal-ip@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" + integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg== + dependencies: + default-gateway "^4.2.0" + ipaddr.js "^1.9.0" + +interpret@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" + integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== + +invert-kv@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" + integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== + +ip-regex@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" + integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= + +ip@^1.1.0, ip@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" + integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== + +is-absolute-url@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" + integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= + dependencies: + binary-extensions "^1.0.0" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-docker@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.0.0.tgz#2cb0df0e75e2d064fe1864c37cdeacb7b2dcf25b" + integrity sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ== + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.1.0.tgz#2e0c7e463ff5b7a0eb60852d851a6809347a124c" + integrity sha512-Sc5j3/YnM8tDeyCsVeKlm/0p95075DyLmDEIkSgQ7mXkrOX+uTCtmQFm0CYzVyJwcCCmO3k8qfJt17SxQwB5Zw== + +is-path-in-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" + integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== + dependencies: + is-path-inside "^2.1.0" + +is-path-inside@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" + integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== + dependencies: + path-is-inside "^1.0.2" + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-stream@^1.0.1, is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-windows@^1.0.1, is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= + +is-wsl@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isbinaryfile@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.8.tgz#5d34b94865bd4946633ecc78a026fc76c5b11fcf" + integrity sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +istanbul-lib-coverage@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" + integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== + +istanbul-lib-coverage@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" + integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg== + +istanbul-lib-instrument@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" + integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== + dependencies: + "@babel/core" "^7.7.5" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.0.0" + semver "^6.3.0" + +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" + integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + rimraf "^2.6.3" + source-map "^0.6.1" + +istanbul-reports@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b" + integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +jasmine-core@3.5.0, jasmine-core@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.5.0.tgz#132c23e645af96d85c8bca13c8758b18429fc1e4" + integrity sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA== + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +json3@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" + integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== + +json5@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +json5@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" + integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== + dependencies: + minimist "^1.2.5" + +jsonc-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" + integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== + +jsonfile@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + integrity sha1-NzaitCi4e72gzIO1P6PWM6NcKug= + optionalDependencies: + graceful-fs "^4.1.6" + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +karma-chrome-launcher@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz#805a586799a4d05f4e54f72a204979f3f3066738" + integrity sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg== + dependencies: + which "^1.2.1" + +karma-coverage-istanbul-reporter@3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz#f3b5303553aadc8e681d40d360dfdc19bc7e9fe9" + integrity sha512-wE4VFhG/QZv2Y4CdAYWDbMmcAHeS926ZIji4z+FkB2aF/EposRb6DP6G5ncT/wXhqUfAb/d7kZrNKPonbvsATw== + dependencies: + istanbul-lib-coverage "^3.0.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^3.0.6" + istanbul-reports "^3.0.2" + minimatch "^3.0.4" + +karma-firefox-launcher@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/karma-firefox-launcher/-/karma-firefox-launcher-1.3.0.tgz#ebcbb1d1ddfada6be900eb8fae25bcf2dcdc8171" + integrity sha512-Fi7xPhwrRgr+94BnHX0F5dCl1miIW4RHnzjIGxF8GaIEp7rNqX7LSi7ok63VXs3PS/5MQaQMhGxw+bvD+pibBQ== + dependencies: + is-wsl "^2.1.0" + +karma-jasmine@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-3.1.1.tgz#f592b253e7619a8d84559d7daf473a647498ade8" + integrity sha512-pxBmv5K7IkBRLsFSTOpgiK/HzicQT3mfFF+oHAC7nxMfYKhaYFgxOa5qjnHW4sL5rUnmdkSajoudOnnOdPyW4Q== + dependencies: + jasmine-core "^3.5.0" + +karma-phantomjs-launcher@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.4.tgz#d23ca34801bda9863ad318e3bb4bd4062b13acd2" + integrity sha1-0jyjSAG9qYY60xjju0vUBisTrNI= + dependencies: + lodash "^4.0.1" + phantomjs-prebuilt "^2.1.7" + +karma-sourcemap-loader@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz#b01d73f8f688f533bcc8f5d273d43458e13b5488" + integrity sha512-xCRL3/pmhAYF3I6qOrcn0uhbQevitc2DERMPH82FMnG+4WReoGcGFZb1pURf2a5apyrOHRdvD+O6K7NljqKHyA== + dependencies: + graceful-fs "^4.2.10" + +karma-webpack@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-5.0.0.tgz#2a2c7b80163fe7ffd1010f83f5507f95ef39f840" + integrity sha512-+54i/cd3/piZuP3dr54+NcFeKOPnys5QeM1IY+0SPASwrtHsliXUiCL50iW+K9WWA7RvamC4macvvQ86l3KtaA== + dependencies: + glob "^7.1.3" + minimatch "^3.0.4" + webpack-merge "^4.1.5" + +karma@6.3.16: + version "6.3.16" + resolved "https://registry.yarnpkg.com/karma/-/karma-6.3.16.tgz#76d1a705fd1cf864ee5ed85270b572641e0958ef" + integrity sha512-nEU50jLvDe5yvXqkEJRf8IuvddUkOY2x5Xc4WXHz6dxINgGDrgD2uqQWeVrJs4hbfNaotn+HQ1LZJ4yOXrL7xQ== + dependencies: + body-parser "^1.19.0" + braces "^3.0.2" + chokidar "^3.5.1" + colors "1.4.0" + connect "^3.7.0" + di "^0.0.1" + dom-serialize "^2.2.1" + glob "^7.1.7" + graceful-fs "^4.2.6" + http-proxy "^1.18.1" + isbinaryfile "^4.0.8" + lodash "^4.17.21" + log4js "^6.4.1" + mime "^2.5.2" + minimatch "^3.0.4" + mkdirp "^0.5.5" + qjobs "^1.2.0" + range-parser "^1.2.1" + rimraf "^3.0.2" + socket.io "^4.2.0" + source-map "^0.6.1" + tmp "^0.2.1" + ua-parser-js "^0.7.30" + yargs "^16.1.1" + +kew@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b" + integrity sha1-edk9LTM2PW/dKXCzNdkUGtWR15s= + +killable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" + integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== + +klaw@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" + integrity sha1-QIhDO0azsbolnXh4XY6W9zugJDk= + optionalDependencies: + graceful-fs "^4.1.9" + +lcid@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" + integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== + dependencies: + invert-kv "^2.0.0" + +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +loader-utils@1.2.3, loader-utils@^1.0.2, loader-utils@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" + integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== + dependencies: + big.js "^5.2.2" + emojis-list "^2.0.0" + json5 "^1.0.1" + +loader-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" + integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +loader-utils@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" + integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0= + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash@^4.0.1, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log4js@^6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.4.1.tgz#9d3a8bf2c31c1e213fe3fc398a6053f7a2bc53e8" + integrity sha512-iUiYnXqAmNKiIZ1XSAitQ4TmNs8CdZYTAWINARF3LjnsLN8tY5m0vRwd6uuWj/yNY0YHxeZodnbmxKFUOM2rMg== + dependencies: + date-format "^4.0.3" + debug "^4.3.3" + flatted "^3.2.4" + rfdc "^1.3.0" + streamroller "^3.0.2" + +loglevel@^1.6.6: + version "1.6.8" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" + integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lunr@^2.3.9: + version "2.3.9" + resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" + integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== + +make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-dir@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +map-age-cleaner@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" + integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== + dependencies: + p-defer "^1.0.0" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + +marked@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753" + integrity sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +mem@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" + integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== + dependencies: + map-age-cleaner "^0.1.1" + mimic-fn "^2.0.0" + p-is-promise "^2.0.0" + +memory-fs@^0.4.0, memory-fs@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-source-map@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" + integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== + dependencies: + source-map "^0.6.1" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +micromatch@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + +mime-db@1.40.0, "mime-db@>= 1.40.0 < 2": + version "1.40.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" + integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== + +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19: + version "2.1.24" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" + integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== + dependencies: + mime-db "1.40.0" + +mime-types@^2.1.26: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + +mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^2.4.4, mime@^2.5.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +mimic-fn@^2.0.0, mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@^3.0.0, minimatch@^3.0.4: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +minipass@^2.6.0, minipass@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" + integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minizlib@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" + integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== + dependencies: + minipass "^2.9.0" + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +mri@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.5.tgz#ce21dba2c69f74a9b7cf8a1ec62307e089e223e0" + integrity sha512-d2RKzMD4JNyHMbnbWnznPaa8vbdlq/4pNZ3IgdaGrVbBhebBsGUUE/6qorTMYNS6TwuH3ilfOlD2bf4Igh8CKg== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2, ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= + +multicast-dns@^6.0.1: + version "6.2.3" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" + integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== + dependencies: + dns-packet "^1.3.1" + thunky "^1.0.2" + +multimatch@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-4.0.0.tgz#8c3c0f6e3e8449ada0af3dd29efb491a375191b3" + integrity sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ== + dependencies: + "@types/minimatch" "^3.0.3" + array-differ "^3.0.0" + array-union "^2.1.0" + arrify "^2.0.1" + minimatch "^3.0.4" + +nan@^2.12.1: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +needle@^2.2.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" + integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== + dependencies: + debug "^3.2.6" + iconv-lite "^0.4.4" + sax "^1.2.4" + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.0, neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +neo-async@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" + integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-forge@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" + integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== + +node-pre-gyp@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" + integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A== + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + +node-releases@^2.0.12: + version "2.0.12" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.12.tgz#35627cc224a23bfb06fb3380f2b3afaaa7eb1039" + integrity sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ== + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-bundled@^1.0.1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd" + integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== + +npm-packlist@^1.1.6: + version "1.4.4" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.4.tgz#866224233850ac534b63d1a6e76050092b5d2f44" + integrity sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw== + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +npm-run-path@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-3.1.0.tgz#7f91be317f6a466efed3c9f2980ad8a4ee8b0fa5" + integrity sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg== + dependencies: + path-key "^3.0.0" + +npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" + integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== + dependencies: + mimic-fn "^2.1.0" + +onigasm@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/onigasm/-/onigasm-2.2.5.tgz#cc4d2a79a0fa0b64caec1f4c7ea367585a676892" + integrity sha512-F+th54mPc0l1lp1ZcFMyL/jTs2Tlq4SqIHKIXGZOR/VkHkF9A7Fr5rRr5+ZG/lWeRsyrClLYRq7s/yFQ/XhWCA== + dependencies: + lru-cache "^5.1.1" + +opencollective-postinstall@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89" + integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw== + +opn@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" + integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== + dependencies: + is-wsl "^1.1.0" + +original@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" + integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== + dependencies: + url-parse "^1.4.3" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + +os-locale@^3.0.0, os-locale@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" + integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== + dependencies: + execa "^1.0.0" + lcid "^2.0.0" + mem "^4.0.0" + +os-tmpdir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-defer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-finally@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" + integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== + +p-is-promise@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" + integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== + +p-limit@^2.0.0, p-limit@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" + integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ== + dependencies: + p-try "^2.0.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-map@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + +p-retry@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" + integrity sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w== + dependencies: + retry "^0.12.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.0.tgz#73e5114c986d143efa3712d4ea24db9a4266f60f" + integrity sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + lines-and-columns "^1.1.6" + +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +phantomjs-prebuilt@^2.1.7: + version "2.1.16" + resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz#efd212a4a3966d3647684ea8ba788549be2aefef" + integrity sha1-79ISpKOWbTZHaE6ouniFSb4q7+8= + dependencies: + es6-promise "^4.0.3" + extract-zip "^1.6.5" + fs-extra "^1.0.0" + hasha "^2.2.0" + kew "^0.7.0" + progress "^1.1.8" + request "^2.81.0" + request-progress "^2.0.1" + which "^1.2.10" + +picocolors@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" + integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4: + version "2.2.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a" + integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA== + +picomatch@^2.0.5: + version "2.0.7" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" + integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== + +picomatch@^2.2.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" + integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +please-upgrade-node@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" + integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== + dependencies: + semver-compare "^1.0.0" + +portfinder@^1.0.25: + version "1.0.26" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.26.tgz#475658d56ca30bed72ac7f1378ed350bd1b64e70" + integrity sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ== + dependencies: + async "^2.6.2" + debug "^3.1.1" + mkdirp "^0.5.1" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +postcss-modules-extract-imports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" + integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== + dependencies: + postcss "^7.0.5" + +postcss-modules-local-by-default@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz#e8a6561be914aaf3c052876377524ca90dbb7915" + integrity sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ== + dependencies: + icss-utils "^4.1.1" + postcss "^7.0.16" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.0.0" + +postcss-modules-scope@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" + integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== + dependencies: + postcss "^7.0.6" + postcss-selector-parser "^6.0.0" + +postcss-modules-values@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" + integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg== + dependencies: + icss-utils "^4.0.0" + postcss "^7.0.6" + +postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c" + integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg== + dependencies: + cssesc "^3.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-value-parser@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.0.tgz#99a983d365f7b2ad8d0f9b8c3094926eab4b936d" + integrity sha512-ESPktioptiSUchCKgggAkzdmkgzKfmp0EU8jXH+5kbIUB+unr0Y4CY9SRMvibuvYUBjNh1ACLbxqYNpdTQOteQ== + +postcss-value-parser@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" + integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== + +postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.27, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.39" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" + integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== + dependencies: + picocolors "^0.2.1" + source-map "^0.6.1" + +prettier@2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" + integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== + +pretty-quick@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pretty-quick/-/pretty-quick-2.0.1.tgz#417ee605ade98ecc686e72f63b5d28a2c35b43e9" + integrity sha512-y7bJt77XadjUr+P1uKqZxFWLddvj3SKY6EU4BuQtMxmmEFSMpbN132pUWdSG1g1mtUfO0noBvn7wBf0BVeomHg== + dependencies: + chalk "^2.4.2" + execa "^2.1.0" + find-up "^4.1.0" + ignore "^5.1.4" + mri "^1.1.4" + multimatch "^4.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +progress@2.0.3, progress@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +progress@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" + integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74= + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= + +psl@^1.1.24: + version "1.2.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.2.0.tgz#df12b5b1b3a30f51c329eacbdef98f3a6e136dc6" + integrity sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qjobs@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" + integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +qs@~6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.2: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" + integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== + dependencies: + graceful-fs "^4.1.11" + micromatch "^3.1.10" + readable-stream "^2.0.2" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +regenerator-runtime@^0.13.4: + version "0.13.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" + integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +request-progress@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-2.0.1.tgz#5d36bb57961c673aa5b788dbc8141fdf23b44e08" + integrity sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg= + dependencies: + throttleit "^1.0.0" + +request@^2.81.0: + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= + dependencies: + resolve-from "^3.0.0" + +resolve-dir@^1.0.0, resolve-dir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" + integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M= + dependencies: + expand-tilde "^2.0.0" + global-modules "^1.0.0" + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha1-six699nWiBvItuZTM17rywoYh0g= + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +resolve@^1.3.2: + version "1.11.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.1.tgz#ea10d8110376982fef578df8fc30b9ac30a07a3e" + integrity sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw== + dependencies: + path-parse "^1.0.6" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= + +rfdc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + +rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rimraf@^2.6.1, rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sass-loader@8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-8.0.2.tgz#debecd8c3ce243c76454f2e8290482150380090d" + integrity sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ== + dependencies: + clone-deep "^4.0.1" + loader-utils "^1.2.3" + neo-async "^2.6.1" + schema-utils "^2.6.1" + semver "^6.3.0" + +sass@^1.49.8: + version "1.49.8" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.8.tgz#9bbbc5d43d14862db07f1c04b786c9da9b641828" + integrity sha512-NoGOjvDDOU9og9oAxhRnap71QaTjjlzrvLnKecUJ3GxhaQBrV6e7gPuSPF28u1OcVAArVojPAe4ZhOXwwC4tGw== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + +sax@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +schema-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" + integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== + dependencies: + ajv "^6.1.0" + ajv-errors "^1.0.0" + ajv-keywords "^3.1.0" + +schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.6.6: + version "2.6.6" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.6.tgz#299fe6bd4a3365dc23d99fd446caff8f1d6c330c" + integrity sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA== + dependencies: + ajv "^6.12.0" + ajv-keywords "^3.4.1" + +schema-utils@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" + integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== + dependencies: + "@types/json-schema" "^7.0.4" + ajv "^6.12.2" + ajv-keywords "^3.4.1" + +schema-utils@^3.1.1, schema-utils@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.2.tgz#36c10abca6f7577aeae136c804b0c741edeadc99" + integrity sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= + +selfsigned@^1.10.7: + version "1.10.7" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.7.tgz#da5819fd049d5574f28e88a9bcc6dbc6e6f3906b" + integrity sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA== + dependencies: + node-forge "0.9.0" + +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + +semver-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" + integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== + +semver@^5.3.0, semver@^5.5.0, semver@^5.6.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" + integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== + +semver@^5.4.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.2.0.tgz#4d813d9590aaf8a9192693d6c85b9344de5901db" + integrity sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A== + +semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" + integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shiki@^0.9.3: + version "0.9.12" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.9.12.tgz#70cbc8c1bb78ff7b356f84a7eecdb040efddd247" + integrity sha512-VXcROdldv0/Qu0w2XvzU4IrvTeBNs/Kj/FCmtcEXGz7Tic/veQzliJj6tEiAgoKianhQstpYmbPDStHU5Opqcw== + dependencies: + jsonc-parser "^3.0.0" + onigasm "^2.2.5" + vscode-textmate "5.2.0" + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +signal-exit@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +socket.io-adapter@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz#4d6111e4d42e9f7646e365b4f578269821f13486" + integrity sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ== + +socket.io-parser@~4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.5.tgz#cb404382c32324cc962f27f3a44058cf6e0552df" + integrity sha512-sNjbT9dX63nqUFIOv95tTVm6elyIU4RvB1m8dOeZt+IgWwcWklFDOdmGcfo3zSiRsnR/3pJkjY5lfoGqEe4Eig== + dependencies: + "@types/component-emitter" "^1.2.10" + component-emitter "~1.3.0" + debug "~4.3.1" + +socket.io@^4.2.0: + version "4.4.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.4.1.tgz#cd6de29e277a161d176832bb24f64ee045c56ab8" + integrity sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + debug "~4.3.2" + engine.io "~6.1.0" + socket.io-adapter "~2.3.3" + socket.io-parser "~4.0.4" + +sockjs-client@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.4.0.tgz#c9f2568e19c8fd8173b4997ea3420e0bb306c7d5" + integrity sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g== + dependencies: + debug "^3.2.5" + eventsource "^1.0.7" + faye-websocket "~0.11.1" + inherits "^2.0.3" + json3 "^3.3.2" + url-parse "^1.4.3" + +sockjs@0.3.19: + version "0.3.19" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.19.tgz#d976bbe800af7bd20ae08598d582393508993c0d" + integrity sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw== + dependencies: + faye-websocket "^0.10.0" + uuid "^3.0.1" + +"source-map-js@>=0.6.2 <2.0.0": + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map-resolve@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" + integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA== + dependencies: + atob "^2.1.1" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + +source-map@^0.5.0, source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +streamroller@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.0.2.tgz#30418d0eee3d6c93ec897f892ed098e3a81e68b7" + integrity sha512-ur6y5S5dopOaRXBuRIZ1u6GC5bcEXHRZKgfBjfCglMhmIf+roVCECjvkEYzNQOXIN2/JPnkMPW/8B3CZoKaEPA== + dependencies: + date-format "^4.0.3" + debug "^4.1.1" + fs-extra "^10.0.0" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string_decoder@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" + integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w== + dependencies: + safe-buffer "~5.1.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +supports-color@6.1.0, supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +tapable@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +tar@^4: + version "4.4.19" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3" + integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA== + dependencies: + chownr "^1.1.4" + fs-minipass "^1.2.7" + minipass "^2.9.0" + minizlib "^1.3.3" + mkdirp "^0.5.5" + safe-buffer "^5.2.1" + yallist "^3.1.1" + +terser-webpack-plugin@^5.3.7: + version "5.3.9" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" + integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.17" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.16.8" + +terser@^5.16.8: + version "5.17.6" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.6.tgz#d810e75e1bb3350c799cd90ebefe19c9412c12de" + integrity sha512-V8QHcs8YuyLkLHsJO5ucyff1ykrLVsR4dNnS//L5Y3NiSXpbK1J+WMVUs67eI0KTxs9JtHhgEQpXQVHlHI92DQ== + dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" + commander "^2.20.0" + source-map-support "~0.5.20" + +throttleit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" + integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw= + +thunky@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.3.tgz#f5df732453407b09191dae73e2a8cc73f381a826" + integrity sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow== + +tmp@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +toposort@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" + integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA= + +tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +ts-loader@7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-7.0.2.tgz#465bc904aea4c331e9550e7c7d75dd17a0b7c24c" + integrity sha512-DwpZFB67RoILQHx42dMjSgv2STpacsQu5X+GD/H9ocd8IhU0m8p3b/ZrIln2KmcucC6xep2PdEMEblpWT71euA== + dependencies: + chalk "^2.3.0" + enhanced-resolve "^4.0.0" + loader-utils "^1.0.2" + micromatch "^4.0.0" + semver "^6.0.0" + +tslib@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8" + integrity sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ== + +tslib@^1.10.0: + version "1.11.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" + integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== + +tslib@^1.8.1, tslib@^1.9.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + +tslib@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + +tslib@^2.3.1: + version "2.5.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" + integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== + +tslint-eslint-rules@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/tslint-eslint-rules/-/tslint-eslint-rules-5.4.0.tgz#e488cc9181bf193fe5cd7bfca213a7695f1737b5" + integrity sha512-WlSXE+J2vY/VPgIcqQuijMQiel+UtmXS+4nvK4ZzlDiqBfXse8FAvkNnTcYhnQyOTW5KFM+uRRGXxYhFpuBc6w== + dependencies: + doctrine "0.7.2" + tslib "1.9.0" + tsutils "^3.0.0" + +tslint-microsoft-contrib@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/tslint-microsoft-contrib/-/tslint-microsoft-contrib-6.2.0.tgz#8aa0f40584d066d05e6a5e7988da5163b85f2ad4" + integrity sha512-6tfi/2tHqV/3CL77pULBcK+foty11Rr0idRDxKnteTaKm6gWF9qmaCNU17HVssOuwlYNyOmd9Jsmjd+1t3a3qw== + dependencies: + tsutils "^2.27.2 <2.29.0" + +tslint@6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-6.1.2.tgz#2433c248512cc5a7b2ab88ad44a6b1b34c6911cf" + integrity sha512-UyNrLdK3E0fQG/xWNqAFAC5ugtFyPO4JJR1KyyfQAyzR8W0fTRrC91A8Wej4BntFzcvETdCSDa/4PnNYJQLYiA== + dependencies: + "@babel/code-frame" "^7.0.0" + builtin-modules "^1.1.1" + chalk "^2.3.0" + commander "^2.12.1" + diff "^4.0.1" + glob "^7.1.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + mkdirp "^0.5.3" + resolve "^1.3.2" + semver "^5.3.0" + tslib "^1.10.0" + tsutils "^2.29.0" + +"tsutils@^2.27.2 <2.29.0": + version "2.28.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.28.0.tgz#6bd71e160828f9d019b6f4e844742228f85169a1" + integrity sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA== + dependencies: + tslib "^1.8.1" + +tsutils@^2.29.0: + version "2.29.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" + integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== + dependencies: + tslib "^1.8.1" + +tsutils@^3.0.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.14.0.tgz#bf8d5a7bae5369331fa0f2b0a5a10bd7f7396c77" + integrity sha512-SmzGbB0l+8I0QwsPgjooFRaRvHLBLNYM8SeQ0k6rtNDru5sCGeLJcZdwilNndN+GysuFjF5EIYgN8GfFG6UeUw== + dependencies: + tslib "^1.8.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +typedoc-default-themes@^0.12.10: + version "0.12.10" + resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.12.10.tgz#614c4222fe642657f37693ea62cad4dafeddf843" + integrity sha512-fIS001cAYHkyQPidWXmHuhs8usjP5XVJjWB8oZGqkTowZaz3v7g3KDZeeqE82FBrmkAnIBOY3jgy7lnPnqATbA== + +typedoc-plugin-external-module-map@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/typedoc-plugin-external-module-map/-/typedoc-plugin-external-module-map-1.2.1.tgz#32669a6b81e57962d2dae80d7a6ef8f5d0be65dd" + integrity sha512-ha+he4JFhCufF6wnpMpeH2XwsMgnYR6IrRUBCiMbZoYoudn6zICX7NA40pMjA35A6afxWNhKZU19pXnvysPK7A== + +typedoc-plugin-remove-references@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/typedoc-plugin-remove-references/-/typedoc-plugin-remove-references-0.0.5.tgz#08b129d2697e50208c807e06c3662fd2fc86a925" + integrity sha512-DSZ7kM/Y90CgZUKt8MiDsoi4fvrJyleHydj3ncGyqDqMdhuMes2E/4I6mSmXBrVdTjYhVH6BeoOFSbj2pQ821g== + +typedoc@0.21.0: + version "0.21.0" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.21.0.tgz#d35dd69b1566032cd893f4f6f21f37156f5f78d2" + integrity sha512-InmPBVlpOXptIkg/WnsQhbGYhv9cuDh/cRACUSautQ0QwcJPLAK2kHcfP0Pld6z/NiDvHc159fMq2qS+b/ALUw== + dependencies: + glob "^7.1.7" + handlebars "^4.7.7" + lodash "^4.17.21" + lunr "^2.3.9" + marked "^2.1.1" + minimatch "^3.0.0" + progress "^2.0.3" + shiki "^0.9.3" + typedoc-default-themes "^0.12.10" + +typescript@4.4.4: + version "4.4.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c" + integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA== + +ua-parser-js@^0.7.30: + version "0.7.33" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532" + integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw== + +uglify-js@^3.1.4: + version "3.14.2" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.2.tgz#d7dd6a46ca57214f54a2d0a43cad0f35db82ac99" + integrity sha512-rtPMlmcO4agTUfz10CbgJ1k6UAoXM2gWb3GoMPPZB/+/Ackf8lNWk11K4rYi2D0apgoFRLtQOZhb+/iGNJq26A== + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +upath@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" + integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== + +update-browserslist-db@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +url-loader@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.0.tgz#c7d6b0d6b0fccd51ab3ffc58a78d32b8d89a7be2" + integrity sha512-IzgAAIC8wRrg6NYkFIJY09vtktQcsvU8V6HhtQj9PTefbYImzLB1hufqo4m+RyM5N3mLx5BqJKccgxJS+W3kqw== + dependencies: + loader-utils "^2.0.0" + mime-types "^2.1.26" + schema-utils "^2.6.5" + +url-parse@^1.4.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^3.0.1, uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +v8-compile-cache@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" + integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +void-elements@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= + +vscode-textmate@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" + integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== + +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webpack-cli@3.3.11: + version "3.3.11" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.11.tgz#3bf21889bf597b5d82c38f215135a411edfdc631" + integrity sha512-dXlfuml7xvAFwYUPsrtQAA9e4DOe58gnzSxhgrO/ZM/gyXTBowrsYeubyN4mqGhYdpXMFNyQ6emjJS9M7OBd4g== + dependencies: + chalk "2.4.2" + cross-spawn "6.0.5" + enhanced-resolve "4.1.0" + findup-sync "3.0.0" + global-modules "2.0.0" + import-local "2.0.0" + interpret "1.2.0" + loader-utils "1.2.3" + supports-color "6.1.0" + v8-compile-cache "2.0.3" + yargs "13.2.4" + +webpack-dev-middleware@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3" + integrity sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw== + dependencies: + memory-fs "^0.4.1" + mime "^2.4.4" + mkdirp "^0.5.1" + range-parser "^1.2.1" + webpack-log "^2.0.0" + +webpack-dev-server@3.10.3: + version "3.10.3" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.10.3.tgz#f35945036813e57ef582c2420ef7b470e14d3af0" + integrity sha512-e4nWev8YzEVNdOMcNzNeCN947sWJNd43E5XvsJzbAL08kGc2frm1tQ32hTJslRS+H65LCb/AaUCYU7fjHCpDeQ== + dependencies: + ansi-html "0.0.7" + bonjour "^3.5.0" + chokidar "^2.1.8" + compression "^1.7.4" + connect-history-api-fallback "^1.6.0" + debug "^4.1.1" + del "^4.1.1" + express "^4.17.1" + html-entities "^1.2.1" + http-proxy-middleware "0.19.1" + import-local "^2.0.0" + internal-ip "^4.3.0" + ip "^1.1.5" + is-absolute-url "^3.0.3" + killable "^1.0.1" + loglevel "^1.6.6" + opn "^5.5.0" + p-retry "^3.0.1" + portfinder "^1.0.25" + schema-utils "^1.0.0" + selfsigned "^1.10.7" + semver "^6.3.0" + serve-index "^1.9.1" + sockjs "0.3.19" + sockjs-client "1.4.0" + spdy "^4.0.1" + strip-ansi "^3.0.1" + supports-color "^6.1.0" + url "^0.11.0" + webpack-dev-middleware "^3.7.2" + webpack-log "^2.0.0" + ws "^6.2.1" + yargs "12.0.5" + +webpack-log@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" + integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== + dependencies: + ansi-colors "^3.0.0" + uuid "^3.3.2" + +webpack-merge@^4.1.5: + version "4.2.2" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" + integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g== + dependencies: + lodash "^4.17.15" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@5.84.1: + version "5.84.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.84.1.tgz#d4493acdeca46b26ffc99d86d784cabfeb925a15" + integrity sha512-ZP4qaZ7vVn/K8WN/p990SGATmrL1qg4heP/MrVneczYtpDGJWlrgZv55vxaV2ul885Kz+25MP2kSXkPe3LZfmg== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^1.0.0" + "@webassemblyjs/ast" "^1.11.5" + "@webassemblyjs/wasm-edit" "^1.11.5" + "@webassemblyjs/wasm-parser" "^1.11.5" + acorn "^8.7.1" + acorn-import-assertions "^1.9.0" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.14.1" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.2" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.7" + watchpack "^2.4.0" + webpack-sources "^3.2.3" + +websocket-driver@>=0.5.1: + version "0.7.3" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" + integrity sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg== + dependencies: + http-parser-js ">=0.4.0 <0.4.11" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" + integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which-pm-runs@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" + integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= + +which@^1.2.1, which@^1.2.10, which@^1.2.14, which@^1.2.9, which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +ws@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" + integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== + dependencies: + async-limiter "~1.0.0" + +ws@~8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" + integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== + +"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.0, yallist@^3.0.2, yallist@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yaml@^1.7.2: + version "1.9.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.9.2.tgz#f0cfa865f003ab707663e4f04b3956957ea564ed" + integrity sha512-HPT7cGGI0DuRcsO51qC1j9O16Dh1mZ2bnXwsi0jrSpsLz0WxOLSLXfkABVl6bZO629py3CU+OMJtpNHDLB97kg== + dependencies: + "@babel/runtime" "^7.9.2" + +yargs-parser@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" + integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^13.1.0: + version "13.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" + integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs@12.0.5: + version "12.0.5" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" + integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== + dependencies: + cliui "^4.0.0" + decamelize "^1.2.0" + find-up "^3.0.0" + get-caller-file "^1.0.1" + os-locale "^3.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1 || ^4.0.0" + yargs-parser "^11.1.1" + +yargs@13.2.4: + version "13.2.4" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83" + integrity sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + os-locale "^3.1.0" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.0" + +yargs@^16.1.1: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yauzl@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" + integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU= + dependencies: + fd-slicer "~1.0.1"