commit 977ba1c2832767665ae759d752b219eb09175ab2 Author: hanshiyang Date: Tue Sep 23 10:43:28 2025 +0800 init from https://github.com/Hufe921/canvas-editor diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..29cfd96 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = false \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..340cf7f --- /dev/null +++ b/.eslintrc @@ -0,0 +1,46 @@ +{ + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "env": { + "browser": true + }, + "globals": { + "process": true + }, + "rules": { + "linebreak-style": 0, + "no-console": 0, + "no-debugger": 0, + "no-useless-escape": "off", + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-this-alias": 0, + "@typescript-eslint/ban-ts-comment": 0, + "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-non-null-assertion": 0, + "@typescript-eslint/ban-types": [1, { + "types": { + "Function": false, + "{}": false + }, + "extendDefaults": true + }], + "no-constant-condition": ["error", { + "checkLoops": false + }], + "semi": [1, "never"], + "quotes": [1, "single", { + "allowTemplateLiterals": true + }] + }, + "ignorePatterns": ["node_modules", "dist", "index.html"] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db7c07f --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.idea +node_modules +.DS_Store +dist +dist-ssr +*.local +cache +.temp \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..2407e7e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": false, + "singleQuote": true, + "printWidth": 80, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "lf" +} \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d997f15 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,57 @@ +{ + "cSpell.words": [ + "atrule", + "Chainable", + "colspan", + "compositionend", + "compositionstart", + "contenteditable", + "contextmenu", + "CRDT", + "deletable", + "dppx", + "esbenp", + "eventbus", + "inputarea", + "keyof", + "linebreak", + "mousedown", + "mouseup", + "mousemove", + "mouseleave", + "noopener", + "Parens", + "prismjs", + "resizer", + "richtext", + "rowmargin", + "rowspan", + "srcdoc", + "TEXTLIKE", + "trlist", + "updown", + "vite", + "vitepress", + "Yahei" + ], + "cSpell.ignorePaths": [ + ".github", + "dist", + "node_modules", + "yarn.lock", + "src/editor/core/draw/particle/latex/utils" + ], + "typescript.tsdk": "node_modules/typescript/lib", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..19db33d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2326 @@ +## [0.9.116](https://github.com/Hufe921/canvas-editor/compare/v0.9.115...v0.9.116) (2025-09-07) + + +### Bug Fixes + +* adjust the priority of default style settings #1248 ([c2e4413](https://github.com/Hufe921/canvas-editor/commit/c2e4413d237dcc93b079cb6ef6be8201df6d68ea)), closes [#1248](https://github.com/Hufe921/canvas-editor/issues/1248) +* repeated input in firefox browser using input method #1244 ([2fcb595](https://github.com/Hufe921/canvas-editor/commit/2fcb595bcf2e510e2da6540b9764207104642586)), closes [#1244](https://github.com/Hufe921/canvas-editor/issues/1244) +* use executeSetHTML to set the table row height #1251 ([60c530c](https://github.com/Hufe921/canvas-editor/commit/60c530c43c9cc6882f55c88c2fb320d4b51757bb)), closes [#1251](https://github.com/Hufe921/canvas-editor/issues/1251) + + +### Chores + +* update README.md ([1a68612](https://github.com/Hufe921/canvas-editor/commit/1a686125f23c3ba79049abaad8e4e68bff0f5bd6)) +* update tsconfig.json ([f650a32](https://github.com/Hufe921/canvas-editor/commit/f650a3201d33b8dcb74f7e0c6fc0f3c9adb0007a)) + + +### Features + +* add executeSetAreaValue api #1243 ([a1d49a5](https://github.com/Hufe921/canvas-editor/commit/a1d49a5f7d820e7012e29fbb548571089ebb2e2f)), closes [#1243](https://github.com/Hufe921/canvas-editor/issues/1243) +* optimize empty lines using executeSetHTML #1252 ([ae2f4c9](https://github.com/Hufe921/canvas-editor/commit/ae2f4c9213e807c77c9494311c0ba33691859c49)), closes [#1252](https://github.com/Hufe921/canvas-editor/issues/1252) + + + +## [0.9.115](https://github.com/Hufe921/canvas-editor/compare/v0.9.114...v0.9.115) (2025-08-23) + + +### Bug Fixes + +* fix: table width and alignment issue when using getHTML and setHTML #991 ([79941a1](https://github.com/Hufe921/canvas-editor/commit/79941a17a5c0bb6e4df5a8dff6701ec421c643b6)), closes [#991](https://github.com/Hufe921/canvas-editor/issues/991) +* surrounding image rendering error when scaling the page ([aab4615](https://github.com/Hufe921/canvas-editor/commit/aab4615f36ade98fd77b5a1b0b0dd0d7dcf84657)) + + +### Features + +* add element hide option #1194 ([5b8f714](https://github.com/Hufe921/canvas-editor/commit/5b8f7149db9cf7cd3ef8cf95c6ead053fd8102bf)), closes [#1194](https://github.com/Hufe921/canvas-editor/issues/1194) +* allow replacing search results with empty string #1222 ([da687db](https://github.com/Hufe921/canvas-editor/commit/da687db6a17f5f02d902bfb0c5fe0c7f4800f8b5)), closes [#1222](https://github.com/Hufe921/canvas-editor/issues/1222) +* executeInsertArea api support inserting by range #1223 ([8d39bb1](https://github.com/Hufe921/canvas-editor/commit/8d39bb1c0ee650af775aa3072c189bc68dc0bb0f)), closes [#1223](https://github.com/Hufe921/canvas-editor/issues/1223) +* optimization of positioning outside the area #1212 ([7cd7bc7](https://github.com/Hufe921/canvas-editor/commit/7cd7bc7ab2d5249e13a2cccf336e3fefc2fafa93)), closes [#1212](https://github.com/Hufe921/canvas-editor/issues/1212) +* the getCatalog and locationCatalog api support within table #1216 ([ba5884c](https://github.com/Hufe921/canvas-editor/commit/ba5884c5665bea283d9ababdfb12802d3d2e5c8e)), closes [#1216](https://github.com/Hufe921/canvas-editor/issues/1216) + + + +## [0.9.114](https://github.com/Hufe921/canvas-editor/compare/v0.9.113...v0.9.114) (2025-08-08) + + +### Bug Fixes + +* composition error while using input method editor #1193 ([87e7394](https://github.com/Hufe921/canvas-editor/commit/87e739455898d2c99485aa8621af26c02269b6be)), closes [#1193](https://github.com/Hufe921/canvas-editor/issues/1193) +* update position context when selecting table cells #1211 ([602896a](https://github.com/Hufe921/canvas-editor/commit/602896adb8989ca5f0a0f91441a90926307a7bb0)), closes [#1211](https://github.com/Hufe921/canvas-editor/issues/1211) + + +### Features + +* add extension option to executeImage api #1201 ([5845843](https://github.com/Hufe921/canvas-editor/commit/5845843e4bee5214745a5f93dcf392db21cbba20)), closes [#1201](https://github.com/Hufe921/canvas-editor/issues/1201) +* add location option to executeLocationArea api #1180 ([f6eff67](https://github.com/Hufe921/canvas-editor/commit/f6eff67e99e6472e065a60cecc5fb9091e8ac7cf)), closes [#1180](https://github.com/Hufe921/canvas-editor/issues/1180) + + + +## [0.9.113](https://github.com/Hufe921/canvas-editor/compare/v0.9.112...v0.9.113) (2025-07-12) + + +### Bug Fixes + +* block element rendering position ([5e2184e](https://github.com/Hufe921/canvas-editor/commit/5e2184eda2325cd318dfd9f569ff9de68aea6596)) +* checkbox cannot be selected after indentation of the list #1186 ([ab170d8](https://github.com/Hufe921/canvas-editor/commit/ab170d85747f9ee4e362fca8998a7df396165007)), closes [#1186](https://github.com/Hufe921/canvas-editor/issues/1186) + + +### Features + +* add previous/next navigation to image preview #1173 ([cd42f79](https://github.com/Hufe921/canvas-editor/commit/cd42f790a90bbc7880759f0b1e99a4f86476e5da)), closes [#1173](https://github.com/Hufe921/canvas-editor/issues/1173) +* add shortcutDisableKeys option #1167 ([d72f6b2](https://github.com/Hufe921/canvas-editor/commit/d72f6b27550d6b14c11f675fdb78b8a8a7325e9b)), closes [#1167](https://github.com/Hufe921/canvas-editor/issues/1167) +* alpha option when header and footer are inactive #1164 ([1be6a99](https://github.com/Hufe921/canvas-editor/commit/1be6a99495c47c9ac58841a2c3d7efd572bdbdd1)), closes [#1164](https://github.com/Hufe921/canvas-editor/issues/1164) +* not activate control when selection exists #1189 ([5819e28](https://github.com/Hufe921/canvas-editor/commit/5819e2860ff386a382cbbbfbf6c47bf9d187eed3)), closes [#1189](https://github.com/Hufe921/canvas-editor/issues/1189) + + + +## [0.9.112](https://github.com/Hufe921/canvas-editor/compare/v0.9.111...v0.9.112) (2025-06-20) + + +### Chores + +* add mouse event spell check words ([c901437](https://github.com/Hufe921/canvas-editor/commit/c9014371ac2e6e6e9bb3ef66a40c9a3fe7616bc3)) + + +### Features + +* add highlight margin height option #1161 ([7e552a6](https://github.com/Hufe921/canvas-editor/commit/7e552a655e1b4a64d14be5c09b42b4278bff0011)), closes [#1161](https://github.com/Hufe921/canvas-editor/issues/1161) +* add image mousedown event #1160 ([b13f483](https://github.com/Hufe921/canvas-editor/commit/b13f4837b4fc0b6e5ab52493cddf7e8d39608d54)), closes [#1160](https://github.com/Hufe921/canvas-editor/issues/1160) +* add input event listener #1156 ([9b7d13a](https://github.com/Hufe921/canvas-editor/commit/9b7d13af54fc5c7cb2e24162421aa7e60e0d4c7d)), closes [#1156](https://github.com/Hufe921/canvas-editor/issues/1156) +* set locale option and api #1159 ([c1f154a](https://github.com/Hufe921/canvas-editor/commit/c1f154a616bc739724d26d2e3f60d337fe123045)), closes [#1159](https://github.com/Hufe921/canvas-editor/issues/1159) +* watermark supports image format #1043 ([99e982a](https://github.com/Hufe921/canvas-editor/commit/99e982a56f3b9cc3996cf74b48d0de3e127a487e)), closes [#1043](https://github.com/Hufe921/canvas-editor/issues/1043) + + + +## [0.9.111](https://github.com/Hufe921/canvas-editor/compare/v0.9.110...v0.9.111) (2025-06-06) + + +### Bug Fixes + +* control activation and deactivation trigger points #1125 ([ed64e9b](https://github.com/Hufe921/canvas-editor/commit/ed64e9b4574573d8520956ce1838ae85170bc8cb)), closes [#1125](https://github.com/Hufe921/canvas-editor/issues/1125) +* format area element boundary error #1147 ([facf277](https://github.com/Hufe921/canvas-editor/commit/facf277935988b458005a9e776a49ffb937f0d01)), closes [#1147](https://github.com/Hufe921/canvas-editor/issues/1147) + + +### Chores + +* add row and col position display ([ff230df](https://github.com/Hufe921/canvas-editor/commit/ff230df4f2a00c0198e380c5b4f65abde8c065a8)) + + +### Features + +* add col index to getRangeContext api #1150 ([754e87f](https://github.com/Hufe921/canvas-editor/commit/754e87fc76bb53ff7f60808c454aade0b74f6ac0)), closes [#1150](https://github.com/Hufe921/canvas-editor/issues/1150) +* add range option to executeFocus api #1090 ([8b75715](https://github.com/Hufe921/canvas-editor/commit/8b757150c65e7c67bc116f708a8e6087ab1fccb0)), closes [#1090](https://github.com/Hufe921/canvas-editor/issues/1090) +* highlight when control value exists or not #1140 ([cadc530](https://github.com/Hufe921/canvas-editor/commit/cadc530ff9f4d13f3281abfbbc39a4cd5f5da214)), closes [#1140](https://github.com/Hufe921/canvas-editor/issues/1140) + + + +## [0.9.110](https://github.com/Hufe921/canvas-editor/compare/v0.9.109...v0.9.110) (2025-05-23) + + +### Bug Fixes + +* delete pagination table #1130 ([5c1d1da](https://github.com/Hufe921/canvas-editor/commit/5c1d1da58f99f42f620315329e1f2be23ec8c93e)), closes [#1130](https://github.com/Hufe921/canvas-editor/issues/1130) +* floating image position when scaling page #1131 ([0c04243](https://github.com/Hufe921/canvas-editor/commit/0c042436363f261b6f5a1701454de65461d0c165)), closes [#1131](https://github.com/Hufe921/canvas-editor/issues/1131) + + +### Features + +* add area hide option #1139 ([595eb07](https://github.com/Hufe921/canvas-editor/commit/595eb0708d4cf7486d979bfd8b4d4c18482eda1e)), closes [#1139](https://github.com/Hufe921/canvas-editor/issues/1139) +* add form mode rule option #1143 ([99c2838](https://github.com/Hufe921/canvas-editor/commit/99c283835955d43cf71f4f59fb4de392048f347e)), closes [#1143](https://github.com/Hufe921/canvas-editor/issues/1143) +* add group deletable option #1134 ([68da503](https://github.com/Hufe921/canvas-editor/commit/68da503f307c6132c1ba318a370b7a466d80ac19)), closes [#1134](https://github.com/Hufe921/canvas-editor/issues/1134) +* add mode rule option #1132 ([a6c44d4](https://github.com/Hufe921/canvas-editor/commit/a6c44d4265e7dc4aea8dc97e5001f97752cf6c67)), closes [#1132](https://github.com/Hufe921/canvas-editor/issues/1132) +* add rowNo option to executeFocus api #1127 ([603f6fe](https://github.com/Hufe921/canvas-editor/commit/603f6feb9aeb71aeb704fcb3d419457eb6e44ca3)), closes [#1127](https://github.com/Hufe921/canvas-editor/issues/1127) +* set date control value with format #1136 ([2288b66](https://github.com/Hufe921/canvas-editor/commit/2288b663f79ccbe76d6790d8e9f2f5e6b8eea05f)), closes [#1136](https://github.com/Hufe921/canvas-editor/issues/1136) + + + +## [0.9.109](https://github.com/Hufe921/canvas-editor/compare/v0.9.108...v0.9.109) (2025-05-11) + + +### Bug Fixes + +* area mode priority error #1119 ([2abd43a](https://github.com/Hufe921/canvas-editor/commit/2abd43a2a4eb156d154c72a7b709cde6aa433fbd)), closes [#1119](https://github.com/Hufe921/canvas-editor/issues/1119) + + +### Features + +* add controlComponent property to getRangeContext api ([6420421](https://github.com/Hufe921/canvas-editor/commit/6420421e2fd01d79ad5be835eac4f2f49359fec6)) +* add disable selection option outside the page #1120 ([f2c61e5](https://github.com/Hufe921/canvas-editor/commit/f2c61e539747c71a6e051fe9f607bf0098b3b22f)), closes [#1120](https://github.com/Hufe921/canvas-editor/issues/1120) +* add isIgnoreDisabledRule option to richtext style setting apis #1109 ([46d917c](https://github.com/Hufe921/canvas-editor/commit/46d917caa5023c28e692a852f5b036bfbb9a6f0d)), closes [#1109](https://github.com/Hufe921/canvas-editor/issues/1109) +* add isSubmitHistory option to the insert element apis #1124 ([424e01f](https://github.com/Hufe921/canvas-editor/commit/424e01f9d30d88cf8d749cb169c6733309821d71)), closes [#1124](https://github.com/Hufe921/canvas-editor/issues/1124) + + + +## [0.9.108](https://github.com/Hufe921/canvas-editor/compare/v0.9.107...v0.9.108) (2025-04-30) + + +### Bug Fixes + +* delete pickElementAttr method data reference #1107 ([a7a7e47](https://github.com/Hufe921/canvas-editor/commit/a7a7e474b6170d26c2b253c8189720e25e17438b)), closes [#1107](https://github.com/Hufe921/canvas-editor/issues/1107) +* set non deletable control boundary error #1111 ([34fe3e8](https://github.com/Hufe921/canvas-editor/commit/34fe3e87d03b0abf22511d5054ea966847ef853a)), closes [#1111](https://github.com/Hufe921/canvas-editor/issues/1111) + + +### Chores + +* update README.md ([f6828fb](https://github.com/Hufe921/canvas-editor/commit/f6828fb43a176d3098fe3bac1cd0dde9744d630d)) + + +### Features + +* add control disabledBackgroundColor option #1105 ([bb6b5ce](https://github.com/Hufe921/canvas-editor/commit/bb6b5ce711ab784ff4e1f3e2b98629bca2018dbd)), closes [#1105](https://github.com/Hufe921/canvas-editor/issues/1105) +* locate outside the control #1100 ([949421b](https://github.com/Hufe921/canvas-editor/commit/949421baad9ba5854913af0bc17f1b4df7b88f69)), closes [#1100](https://github.com/Hufe921/canvas-editor/issues/1100) + + + +## [0.9.107](https://github.com/Hufe921/canvas-editor/compare/v0.9.106...v0.9.107) (2025-04-18) + + +### Bug Fixes + +* area background position error #1082 ([d708629](https://github.com/Hufe921/canvas-editor/commit/d708629c4883b73c78d722352a6ff5a08642fa9e)), closes [#1082](https://github.com/Hufe921/canvas-editor/issues/1082) +* delete control element boundary error #1079 ([b8132cd](https://github.com/Hufe921/canvas-editor/commit/b8132cdd82822b2840dee95a88fcc8492737f352)), closes [#1079](https://github.com/Hufe921/canvas-editor/issues/1079) +* input boundary error when control is disabled #1092 ([353ada2](https://github.com/Hufe921/canvas-editor/commit/353ada21cd70adb61094fef301462063ddc00a01)), closes [#1092](https://github.com/Hufe921/canvas-editor/issues/1092) +* update area element context #1084 ([a7598d2](https://github.com/Hufe921/canvas-editor/commit/a7598d2a08e87e86ea34e80903f168010362d188)), closes [#1084](https://github.com/Hufe921/canvas-editor/issues/1084) + + +### Features + +* add area deletable option #1014 ([6bb1982](https://github.com/Hufe921/canvas-editor/commit/6bb19824c1f6f31adb99ecac5c8b20d3dfa7b17c)), closes [#1014](https://github.com/Hufe921/canvas-editor/issues/1014) +* add area placeholder option #1076 ([fd9f780](https://github.com/Hufe921/canvas-editor/commit/fd9f780b537c99d9aaa93054e6bbaee8b363ec45)), closes [#1076](https://github.com/Hufe921/canvas-editor/issues/1076) +* add conceptId to executeImage api #1080 ([b05eb06](https://github.com/Hufe921/canvas-editor/commit/b05eb065e25162ebf21a32c160db6f3e74365588)), closes [#1080](https://github.com/Hufe921/canvas-editor/issues/1080) +* remove null values when merging cells #1096 ([f673352](https://github.com/Hufe921/canvas-editor/commit/f673352b095653e85921c02cee7a23a0bd8105b1)), closes [#1096](https://github.com/Hufe921/canvas-editor/issues/1096) + + + +## [0.9.106](https://github.com/Hufe921/canvas-editor/compare/v0.9.105...v0.9.106) (2025-04-04) + + +### Bug Fixes + +* delete elements within the td when dragging #1056 ([dcb7589](https://github.com/Hufe921/canvas-editor/commit/dcb7589afa2b00bcb0775a4d216032339368e695)), closes [#1056](https://github.com/Hufe921/canvas-editor/issues/1056) +* internal shortcut keys ignore capitalization #1061 ([c282366](https://github.com/Hufe921/canvas-editor/commit/c28236699cf7ce2acb55fbb5a8dcbf52f46aae89)), closes [#1061](https://github.com/Hufe921/canvas-editor/issues/1061) + + +### Chores + +* optimize the import interface path ([7256a13](https://github.com/Hufe921/canvas-editor/commit/7256a13d57735a51700af3ba261fe4c684a47250)) + + +### Features + +* add dragFloatImageDisabled to cursor option #1054 ([09a8de2](https://github.com/Hufe921/canvas-editor/commit/09a8de2a88bc3193b5cfee366dcae189e38fc8da)), closes [#1054](https://github.com/Hufe921/canvas-editor/issues/1054) +* add getValueAsync api #1067 ([6f34bce](https://github.com/Hufe921/canvas-editor/commit/6f34bce7db81f429372bba9110df8a2f314c4981)), closes [#1067](https://github.com/Hufe921/canvas-editor/issues/1067) +* add row number to RangeContext ([fa5936e](https://github.com/Hufe921/canvas-editor/commit/fa5936ec004de0273d9b94fa86a08162cce4cad4)) +* batch set control properties #1037 ([94e7b2c](https://github.com/Hufe921/canvas-editor/commit/94e7b2ca7cd43fd3da83612369cb29c074481378)), closes [#1037](https://github.com/Hufe921/canvas-editor/issues/1037) + + + +## [0.9.105](https://github.com/Hufe921/canvas-editor/compare/v0.9.104...v0.9.105) (2025-03-15) + + +### Bug Fixes + +* delete hidden control boundary error #1036 ([960e918](https://github.com/Hufe921/canvas-editor/commit/960e91841f4b8a6c8a176f3f09f8cb2d3f1fccee)), closes [#1036](https://github.com/Hufe921/canvas-editor/issues/1036) +* initial print mode settings invalid #1034 ([6d95284](https://github.com/Hufe921/canvas-editor/commit/6d95284609edf855b8bd049fff15816cb2710cd1)), closes [#1034](https://github.com/Hufe921/canvas-editor/issues/1034) + + +### Features + +* add border width to table option #897 ([17550b9](https://github.com/Hufe921/canvas-editor/commit/17550b98b4798e6887c7320d7125d54287b92bcf)), closes [#897](https://github.com/Hufe921/canvas-editor/issues/897) +* add imageSizeChange event #1035 ([f93f3a7](https://github.com/Hufe921/canvas-editor/commit/f93f3a73be31aaf8c569869928970a4787ca7244)), closes [#1035](https://github.com/Hufe921/canvas-editor/issues/1035) +* add isReplace option to executeInsertElementList api #1031 ([076302a](https://github.com/Hufe921/canvas-editor/commit/076302a9192cff9990a8d5f76672d16e32151f33)), closes [#1031](https://github.com/Hufe921/canvas-editor/issues/1031) +* add table external border width option #897 ([9df856f](https://github.com/Hufe921/canvas-editor/commit/9df856f61272098d5a004bb2df7eb07d1f34d078)), closes [#897](https://github.com/Hufe921/canvas-editor/issues/897) +* update cursor on right-click #1022 ([edd5df3](https://github.com/Hufe921/canvas-editor/commit/edd5df3029c3aaa6763bcbe1dc9152c70cf3b330)), closes [#1022](https://github.com/Hufe921/canvas-editor/issues/1022) + + + +## [0.9.104](https://github.com/Hufe921/canvas-editor/compare/v0.9.103...v0.9.104) (2025-02-22) + + +### Bug Fixes + +* missing some fields when zip elements #1023 ([c41fc9e](https://github.com/Hufe921/canvas-editor/commit/c41fc9eb578d4dc0f59cbb642050cee49158b460)), closes [#1023](https://github.com/Hufe921/canvas-editor/issues/1023) + + +### Features + +* add imgToolDisabled option #1028 ([01b2c16](https://github.com/Hufe921/canvas-editor/commit/01b2c16596e9208bfdc08182b5e329f7a78a6b45)), closes [#1028](https://github.com/Hufe921/canvas-editor/issues/1028) +* increase priority when zip area elements #1020 ([21543cc](https://github.com/Hufe921/canvas-editor/commit/21543cc15f87302cd89d53da56f1a0a95b0a9f54)), closes [#1020](https://github.com/Hufe921/canvas-editor/issues/1020) + + +### Performance Improvements + +* spliceElementList method performance #1021 ([2066d0d](https://github.com/Hufe921/canvas-editor/commit/2066d0de80a2f2aae5f0b8bc4f0bc8378aaaa47d)), closes [#1021](https://github.com/Hufe921/canvas-editor/issues/1021) + + + +## [0.9.103](https://github.com/Hufe921/canvas-editor/compare/v0.9.102...v0.9.103) (2025-02-16) + + +### Bug Fixes + +* checkbox list select error in paper horizontal mode #997 ([5442f5e](https://github.com/Hufe921/canvas-editor/commit/5442f5ea1795cd54869912e887aacb60a3160cc9)), closes [#997](https://github.com/Hufe921/canvas-editor/issues/997) +* control content change event boundary error #996 ([e6c681d](https://github.com/Hufe921/canvas-editor/commit/e6c681d2867c7894491b42842ac180503f0702a3)), closes [#996](https://github.com/Hufe921/canvas-editor/issues/996) +* format error when update element by id #1006 ([15728e8](https://github.com/Hufe921/canvas-editor/commit/15728e8d799cf334bcc355cc7198ebcfc0345843)), closes [#1006](https://github.com/Hufe921/canvas-editor/issues/1006) + + +### Chores + +* update issue template ([fd34310](https://github.com/Hufe921/canvas-editor/commit/fd343106fbd613f395a78d768dbca306d9461338)) +* update README.md ([e355de8](https://github.com/Hufe921/canvas-editor/commit/e355de8a597b58b9b79aba1bfec2ae31e0db4589)) + + +### Features + +* add executeDeleteElementById api #1003 ([089d684](https://github.com/Hufe921/canvas-editor/commit/089d6841988d20efb84efd6556469455207d5a2e)), closes [#1003](https://github.com/Hufe921/canvas-editor/issues/1003) +* convert block elements to html #984 ([b77fd96](https://github.com/Hufe921/canvas-editor/commit/b77fd96dfadc012c6a2a9a4957d59fb8af0abc55)), closes [#984](https://github.com/Hufe921/canvas-editor/issues/984) + + +### Performance Improvements + +* mouse event listener option #1010 ([56f9604](https://github.com/Hufe921/canvas-editor/commit/56f9604e9c8c11e31c7c86f5246373412bcc6625)), closes [#1010](https://github.com/Hufe921/canvas-editor/issues/1010) +* timing of updating range style #985 ([cfb09ce](https://github.com/Hufe921/canvas-editor/commit/cfb09cef78c9e661540b87834495dc5a19839ef8)), closes [#985](https://github.com/Hufe921/canvas-editor/issues/985) + + + +## [0.9.102](https://github.com/Hufe921/canvas-editor/compare/v0.9.101...v0.9.102) (2025-02-07) + + +### Bug Fixes + +* clear control content boundary error #988 ([635f7f0](https://github.com/Hufe921/canvas-editor/commit/635f7f09a4bd76386860bc2c6694e8b07f107897)), closes [#988](https://github.com/Hufe921/canvas-editor/issues/988) +* disable replace when element cannot be deleted #976 ([f0ffe31](https://github.com/Hufe921/canvas-editor/commit/f0ffe317f6b5a956b192b9ad00b39c3a123ae6bd)), closes [#976](https://github.com/Hufe921/canvas-editor/issues/976) + + +### Features + +* add control content change event #940 ([01a3149](https://github.com/Hufe921/canvas-editor/commit/01a31491ff3432ee8a0ed6a1855013a5b3fe5fb1)), closes [#940](https://github.com/Hufe921/canvas-editor/issues/940) +* add executeLocationArea api #940 ([cd42514](https://github.com/Hufe921/canvas-editor/commit/cd42514a01eca2e133c1fd875cfc9b8f85fb97ff)), closes [#940](https://github.com/Hufe921/canvas-editor/issues/940) +* add hide property to control element #979 ([e49fe94](https://github.com/Hufe921/canvas-editor/commit/e49fe94efef5becb54fbc5d9ba3c059ae65cdf35)), closes [#979](https://github.com/Hufe921/canvas-editor/issues/979) +* add id option to executeImage api #989 ([d82237e](https://github.com/Hufe921/canvas-editor/commit/d82237e586d425742dce2e123fc6c459abb8c275)), closes [#989](https://github.com/Hufe921/canvas-editor/issues/989) +* add page number format option to watermark #981 ([b3c6259](https://github.com/Hufe921/canvas-editor/commit/b3c6259fd55be34f0f1d58828aa790427df759f4)), closes [#981](https://github.com/Hufe921/canvas-editor/issues/981) +* copy style when insert tab element #974 ([ae8bbb8](https://github.com/Hufe921/canvas-editor/commit/ae8bbb87a5ac019a982c9b737370ea72ce6c342e)), closes [#974](https://github.com/Hufe921/canvas-editor/issues/974) +* prefer structuredClone api for cloning #980 ([fdda5c7](https://github.com/Hufe921/canvas-editor/commit/fdda5c7e1b16e5d76ac547d04507c0b0f8846208)), closes [#980](https://github.com/Hufe921/canvas-editor/issues/980) + + + +## [0.9.101](https://github.com/Hufe921/canvas-editor/compare/v0.9.100...v0.9.101) (2025-01-18) + + +### Bug Fixes + +* image asynchronous rendering boundary error #959 ([344a6e0](https://github.com/Hufe921/canvas-editor/commit/344a6e038862a290c810ab55b66ae7fcceffecae)), closes [#959](https://github.com/Hufe921/canvas-editor/issues/959) +* verify the range boundary error of control in the table #959 ([6d55698](https://github.com/Hufe921/canvas-editor/commit/6d55698987d7a4a119b0514560625db09884cd94)), closes [#959](https://github.com/Hufe921/canvas-editor/issues/959) + + +### Features + +* add isSubmitHistory option to setControlValue api #960 ([9c0b67f](https://github.com/Hufe921/canvas-editor/commit/9c0b67f184ebb35cc74dcacc10e44efc39c2c95d)), closes [#960](https://github.com/Hufe921/canvas-editor/issues/960) +* add option to executeReplace api #969 ([3fbaa3b](https://github.com/Hufe921/canvas-editor/commit/3fbaa3b7cc513336b8278221c639ea651e27155f)), closes [#969](https://github.com/Hufe921/canvas-editor/issues/969) +* add split table cell api #826 ([04c7194](https://github.com/Hufe921/canvas-editor/commit/04c7194a94845d54456798509846fc2f9a86f807)), closes [#826](https://github.com/Hufe921/canvas-editor/issues/826) +* not update default range style when unchanged #970 ([21a71c0](https://github.com/Hufe921/canvas-editor/commit/21a71c0cfabaf9e6f0a68ca5ddbe1df49d408ee8)), closes [#970](https://github.com/Hufe921/canvas-editor/issues/970) + + +### Performance Improvements + +* deep clone performance for control data #971 ([71d52de](https://github.com/Hufe921/canvas-editor/commit/71d52de30c63beca806c0a7af0c82c1244d59429)), closes [#971](https://github.com/Hufe921/canvas-editor/issues/971) + + + +## [0.9.100](https://github.com/Hufe921/canvas-editor/compare/v0.9.99...v0.9.100) (2025-01-03) + + +### Bug Fixes + +* get or update elements within a table by id #951 ([951de97](https://github.com/Hufe921/canvas-editor/commit/951de9794186cb6225e1f668f1021f6e7f04b1bc)), closes [#951](https://github.com/Hufe921/canvas-editor/issues/951) +* set default style boundary error #942 ([2fd9d10](https://github.com/Hufe921/canvas-editor/commit/2fd9d10c10705df57fbc67455edb8da05df92d0a)), closes [#942](https://github.com/Hufe921/canvas-editor/issues/942) + + +### Features + +* add table tool disabled option #954 ([a6eccc7](https://github.com/Hufe921/canvas-editor/commit/a6eccc7a78569e881d80c745f8a77455b91df0ad)), closes [#954](https://github.com/Hufe921/canvas-editor/issues/954) +* adjust watermark rendering layer #948 ([0f53552](https://github.com/Hufe921/canvas-editor/commit/0f5355216c1c092ea59ecb737f447c0a0ff11558)), closes [#948](https://github.com/Hufe921/canvas-editor/issues/948) +* optimize cursor focus in readonly mode #936 ([2e0ac96](https://github.com/Hufe921/canvas-editor/commit/2e0ac966e839a2dc9df911b04d24577c46d930e4)), closes [#936](https://github.com/Hufe921/canvas-editor/issues/936) +* preserve cell content when merging cells #932 ([167bfa1](https://github.com/Hufe921/canvas-editor/commit/167bfa1e7c44a580f29c06bccd9bf5675e4d166f)), closes [#932](https://github.com/Hufe921/canvas-editor/issues/932) + + + +## [0.9.99](https://github.com/Hufe921/canvas-editor/compare/v0.9.98...v0.9.99) (2024-12-20) + + +### Bug Fixes + +* locate catalog when the context is within the table ([012dc7d](https://github.com/Hufe921/canvas-editor/commit/012dc7debcce4366c8e094cbc2af1a7892227ad2)) +* optimize image caching method #933 ([8516931](https://github.com/Hufe921/canvas-editor/commit/85169313bd8f51feb9f442cb32b774dcbc580e96)), closes [#933](https://github.com/Hufe921/canvas-editor/issues/933) +* verify control integrity boundary error #920 ([77a2550](https://github.com/Hufe921/canvas-editor/commit/77a25504bc40caaf20390944f286748bdf113e39)), closes [#920](https://github.com/Hufe921/canvas-editor/issues/920) + + +### Chores + +* rename some particles ([b5d18d3](https://github.com/Hufe921/canvas-editor/commit/b5d18d3a59f89a701758af160fc889e49ba1f5c6)) + + +### Features + +* add badge option #918 ([190785a](https://github.com/Hufe921/canvas-editor/commit/190785ac69f6187d10abb070eab32a523e394b34)), closes [#918](https://github.com/Hufe921/canvas-editor/issues/918) +* add control flex direction option #652 ([c599d56](https://github.com/Hufe921/canvas-editor/commit/c599d563b631f29a6bea6fe94624afa05c6879ef)), closes [#652](https://github.com/Hufe921/canvas-editor/issues/652) +* add number control #925 ([aff4979](https://github.com/Hufe921/canvas-editor/commit/aff49797da3f7e966b9f692269468ccb792af309)), closes [#925](https://github.com/Hufe921/canvas-editor/issues/925) +* delete control by id or conceptId #905 ([5d434bd](https://github.com/Hufe921/canvas-editor/commit/5d434bd7d3dce31f761a0e1f6c81dfa7a80bb03e)), closes [#905](https://github.com/Hufe921/canvas-editor/issues/905) +* element id support customization ([c79be3a](https://github.com/Hufe921/canvas-editor/commit/c79be3ab72e9e1cda75e7af2b8b8d50e1ba2085a)) +* optimize cursor focus when dragging elements #926 ([3679cbd](https://github.com/Hufe921/canvas-editor/commit/3679cbd4205427a1337e023c318ea98be3c952f8)), closes [#926](https://github.com/Hufe921/canvas-editor/issues/926) + + + +## [0.9.98](https://github.com/Hufe921/canvas-editor/compare/v0.9.97...v0.9.98) (2024-12-01) + + +### Bug Fixes + +* fix: paste error caused by different line breaks #912 ([e45f4e9](https://github.com/Hufe921/canvas-editor/commit/e45f4e96389a5273a13aebacc5f5577721bb2da1)), closes [#912](https://github.com/Hufe921/canvas-editor/issues/912) +* control property modification #914 ([a827138](https://github.com/Hufe921/canvas-editor/commit/a827138978bb9953d351d30d994f586738f080fd)), closes [#914](https://github.com/Hufe921/canvas-editor/issues/914) +* copy elements within the control #901 ([750bf14](https://github.com/Hufe921/canvas-editor/commit/750bf14428c7c7e55ed55f88c0c2b20f31f76e59)), closes [#901](https://github.com/Hufe921/canvas-editor/issues/901) +* insertControl and insertTitle apis add area context #911 ([b6294b2](https://github.com/Hufe921/canvas-editor/commit/b6294b264a93a25c12acdae2041132ba88c79ba0)), closes [#911](https://github.com/Hufe921/canvas-editor/issues/911) +* processing area property is empty ([5112451](https://github.com/Hufe921/canvas-editor/commit/5112451f912e814af8439b907eb87ce5df6ee38c)) +* table row height calculation boundary error #909 ([bbb554f](https://github.com/Hufe921/canvas-editor/commit/bbb554feabec664df89fd7d751be4f89c97b5145)), closes [#909](https://github.com/Hufe921/canvas-editor/issues/909) + + +### Features + +* add executePageScale api #906 ([2430bf7](https://github.com/Hufe921/canvas-editor/commit/2430bf70d69ce97c0cc10d5b99d50f58ff1838ed)), closes [#906](https://github.com/Hufe921/canvas-editor/issues/906) +* add plain text copy option ([34c4401](https://github.com/Hufe921/canvas-editor/commit/34c4401cb0c461cc64e223f0b23fddc679bc88c7)) +* add select control can input option #518 ([1d01576](https://github.com/Hufe921/canvas-editor/commit/1d01576769960fa130f8a16410c74d5a285084ee)), closes [#518](https://github.com/Hufe921/canvas-editor/issues/518) +* add table border color #897 ([7e8af04](https://github.com/Hufe921/canvas-editor/commit/7e8af04977d8c8038e8af262c0cfde2b46704a03)), closes [#897](https://github.com/Hufe921/canvas-editor/issues/897) +* add text before and after the control #902 ([5e15ffe](https://github.com/Hufe921/canvas-editor/commit/5e15ffebc6b34394138b4a6c47fc90033bd82819)), closes [#902](https://github.com/Hufe921/canvas-editor/issues/902) +* the select control support multiple selection #518 ([62ae039](https://github.com/Hufe921/canvas-editor/commit/62ae039d58768ad580696229695ce80af0c1b257)), closes [#518](https://github.com/Hufe921/canvas-editor/issues/518) + + + +## [0.9.97](https://github.com/Hufe921/canvas-editor/compare/v0.9.96...v0.9.97) (2024-11-22) + + +### Bug Fixes + +* add the first character of area ([1a3f062](https://github.com/Hufe921/canvas-editor/commit/1a3f0626514615d3b5c48dfca9d4d5a03b673ad3)) +* cursor position is on the top margin calculation error #882 ([992e4ed](https://github.com/Hufe921/canvas-editor/commit/992e4edb2a7ade02847a0be234d3ca4f429949ff)), closes [#882](https://github.com/Hufe921/canvas-editor/issues/882) +* data retrieval error when control was destroyed #884 ([1c5ef78](https://github.com/Hufe921/canvas-editor/commit/1c5ef7801ac1fb18fe131912cbd2547023e35982)), closes [#884](https://github.com/Hufe921/canvas-editor/issues/884) +* destroy control boundary error #895 ([6bed83b](https://github.com/Hufe921/canvas-editor/commit/6bed83b645f67c5c59ba5c4ee811e985e1dbc60f)), closes [#895](https://github.com/Hufe921/canvas-editor/issues/895) +* disable highlight style in print mode #893 ([49360fd](https://github.com/Hufe921/canvas-editor/commit/49360fda0eff9ae24f5b79b742bd8cdf1f99ca20)), closes [#893](https://github.com/Hufe921/canvas-editor/issues/893) +* insert area error when cursor is within table #891 ([b16aa71](https://github.com/Hufe921/canvas-editor/commit/b16aa71798c440327d49c6c8f4c3166ea5bc5ee7)), closes [#891](https://github.com/Hufe921/canvas-editor/issues/891) +* line break rendering error for control placeholder #883 ([679c4fa](https://github.com/Hufe921/canvas-editor/commit/679c4fac5f3eba524f582cb090390b2e302a1b81)), closes [#883](https://github.com/Hufe921/canvas-editor/issues/883) +* set alignment error when row width is not enough #885 ([c71d99c](https://github.com/Hufe921/canvas-editor/commit/c71d99cbd4d827b8af6788663f46403b141e2797)), closes [#885](https://github.com/Hufe921/canvas-editor/issues/885) +* virtual input location boundary error #878 ([2d43343](https://github.com/Hufe921/canvas-editor/commit/2d43343d83917ea35340a8610958df4e09d67e8b)), closes [#878](https://github.com/Hufe921/canvas-editor/issues/878) + + +### Chores + +* export controlId and titleId ([30b405c](https://github.com/Hufe921/canvas-editor/commit/30b405c270a7ca6a09f5824b3cf4a5bd552e0478)) +* update add control method ([c370df7](https://github.com/Hufe921/canvas-editor/commit/c370df7d3666ab0ed99fc2538b50e3bb5fbd0bf1)) + + +### Features + +* add area element #216 ([204a886](https://github.com/Hufe921/canvas-editor/commit/204a886c1dcd01c3b56695eef74c3c39221bcdb3)), closes [#216](https://github.com/Hufe921/canvas-editor/issues/216) +* add control state property to the ControlChange event #884 ([e5480a3](https://github.com/Hufe921/canvas-editor/commit/e5480a3f00044e901b255c77f6b8ae7fb91ef4f4)), closes [#884](https://github.com/Hufe921/canvas-editor/issues/884) +* handle copy area context boundary ([64b4100](https://github.com/Hufe921/canvas-editor/commit/64b41009c42dbba1600e92301a6db89cd8fb421d)) +* style settings when range is collapsed #870 ([fbdd5f6](https://github.com/Hufe921/canvas-editor/commit/fbdd5f62786398e66c7740da28b1656c4f36f131)), closes [#870](https://github.com/Hufe921/canvas-editor/issues/870) + + + +## [0.9.96](https://github.com/Hufe921/canvas-editor/compare/v0.9.95...v0.9.96) (2024-11-09) + + +### Bug Fixes + +* adjust column width after adding new columns #855 ([2439c24](https://github.com/Hufe921/canvas-editor/commit/2439c24e120bdfb0553d5f32ef9af25517bb6baa)), closes [#855](https://github.com/Hufe921/canvas-editor/issues/855) +* focus editor when selection exists #871 ([85cc2b7](https://github.com/Hufe921/canvas-editor/commit/85cc2b7f68726fbecb1484f420a6d20bda004dfe)), closes [#871](https://github.com/Hufe921/canvas-editor/issues/871) +* margin height in continuous page mode ([c0403b8](https://github.com/Hufe921/canvas-editor/commit/c0403b8f8d069d0c4b3fcb8bb23159cb1fb00fe5)) +* table rendering error after pagination when scaled #867 ([a1472c2](https://github.com/Hufe921/canvas-editor/commit/a1472c23aacef239f00cab9ab365d261599d1bee)), closes [#867](https://github.com/Hufe921/canvas-editor/issues/867) + + +### Chores + +* export EDITOR_CLIPBOARD constant #860 ([7e56297](https://github.com/Hufe921/canvas-editor/commit/7e562970031a4227d3d8ac66f2447cf2c52f25d1)), closes [#860](https://github.com/Hufe921/canvas-editor/issues/860) +* update README.md ([3a81d56](https://github.com/Hufe921/canvas-editor/commit/3a81d56974e1e30bb01985f9a79de7f31785a8fc)) + + +### Features + +* add control paste disabled rule #853 ([2bb84ab](https://github.com/Hufe921/canvas-editor/commit/2bb84abb5f3fb6670ca9ae88e96f5790de4b6a9d)), closes [#853](https://github.com/Hufe921/canvas-editor/issues/853) +* add getElementById api ([8eabd07](https://github.com/Hufe921/canvas-editor/commit/8eabd0719981b16f4ba87005d5759c6710848bde)) +* add table dashed border #858 ([dffac63](https://github.com/Hufe921/canvas-editor/commit/dffac639739d82e50cb122b3c95af12805351bb9)), closes [#858](https://github.com/Hufe921/canvas-editor/issues/858) +* add table internal border #869 ([5cc4f13](https://github.com/Hufe921/canvas-editor/commit/5cc4f1349cacc974918068619dc9b258f6c9cad5)), closes [#869](https://github.com/Hufe921/canvas-editor/issues/869) +* optimize previewer interactive in readonly mode #875 ([09b8bac](https://github.com/Hufe921/canvas-editor/commit/09b8baca888453685eba03e205f91187a101427c)), closes [#875](https://github.com/Hufe921/canvas-editor/issues/875) +* quick select table row and col tool ([c6a1703](https://github.com/Hufe921/canvas-editor/commit/c6a170367fbcb9822bce3040ebd63bed8ea27355)) + + + +## [0.9.95](https://github.com/Hufe921/canvas-editor/compare/v0.9.94...v0.9.95) (2024-10-19) + + +### Bug Fixes + +* date element update data boundary error #835 ([7f69fd9](https://github.com/Hufe921/canvas-editor/commit/7f69fd9ca2ea474167b10b0e122b745cffed2ab8)), closes [#835](https://github.com/Hufe921/canvas-editor/issues/835) +* previewer wheel event stop propagation ([ddf0806](https://github.com/Hufe921/canvas-editor/commit/ddf0806e3751c12c56523b987dc1031a85b17b69)) +* set table context error when using directional keys ([b210ce3](https://github.com/Hufe921/canvas-editor/commit/b210ce39b1a440d8fecf7d222c5398b2025d3d68)) + + +### Chores + +* update watermark option ([d14e720](https://github.com/Hufe921/canvas-editor/commit/d14e72001784202db5eeb7d9bb5ca76cce3cea3c)) + + +### Documentation + +* update api markdown ([8c0f660](https://github.com/Hufe921/canvas-editor/commit/8c0f660bce8b2a579041af073e04de89913aa1a2)) + + +### Features + +* add getKeywordContext api #846 ([abbb62d](https://github.com/Hufe921/canvas-editor/commit/abbb62df3bf45e64c67a7da3f7bbe666f4eb1328)), closes [#846](https://github.com/Hufe921/canvas-editor/issues/846) +* control element support set row flex #839 ([3f265c8](https://github.com/Hufe921/canvas-editor/commit/3f265c835b8f63bcc07b41596ab4dce886e99629)), closes [#839](https://github.com/Hufe921/canvas-editor/issues/839) +* export getElementListByHTML and getTextFromElementList api ([590c97d](https://github.com/Hufe921/canvas-editor/commit/590c97db225931d6bb4e117736a71be83cbb117e)) +* remove row spacing from continuous tables #842 ([a4d8633](https://github.com/Hufe921/canvas-editor/commit/a4d863320bc8f9b2efa0390c09d58566faa62be9)), closes [#842](https://github.com/Hufe921/canvas-editor/issues/842) + + + +## [0.9.94](https://github.com/Hufe921/canvas-editor/compare/v0.9.93...v0.9.94) (2024-10-07) + + +### Bug Fixes + +* pageSizeChange event listener inaccurate #817 ([0308089](https://github.com/Hufe921/canvas-editor/commit/03080896f594afb3044d401fa736bcaabc8153c4)), closes [#817](https://github.com/Hufe921/canvas-editor/issues/817) + + +### Documentation + +* update command markdown ([b2978b2](https://github.com/Hufe921/canvas-editor/commit/b2978b27c279945ea77d2e00f7233e0351975ed9)) + + +### Features + +* add get cursor position api ([3052337](https://github.com/Hufe921/canvas-editor/commit/30523378a8d5f0fac18bd06a2a3fdd259a758be3)) +* add header and footer editable option ([aa667a6](https://github.com/Hufe921/canvas-editor/commit/aa667a6b8d8f12b5df207111b45f17d1e1fc0e8d)) +* add repeat attribute to watermark #665 ([c6e0176](https://github.com/Hufe921/canvas-editor/commit/c6e017631cda9f85301a0f97816f41c1b042a42e)), closes [#665](https://github.com/Hufe921/canvas-editor/issues/665) +* export createDomFromElementList function #819 ([f7b6f42](https://github.com/Hufe921/canvas-editor/commit/f7b6f42c7f4a7daa9e1bad398255b4ca6078103a)), closes [#819](https://github.com/Hufe921/canvas-editor/issues/819) +* quick add table select tool ([6a05052](https://github.com/Hufe921/canvas-editor/commit/6a05052d9e97b35297fbd29f128faca0a486a6a7)) +* the control api support set and get element list #816 ([9482f85](https://github.com/Hufe921/canvas-editor/commit/9482f85f0e860e0449402fdbb80ca80a3bcd800c)), closes [#816](https://github.com/Hufe921/canvas-editor/issues/816) + + + +## [0.9.93](https://github.com/Hufe921/canvas-editor/compare/v0.9.92...v0.9.93) (2024-09-20) + + +### Bug Fixes + +* executeUpdateElementById api format data error #806 ([1f88dca](https://github.com/Hufe921/canvas-editor/commit/1f88dcafae035be1130f0956c51c21fa60c3759e)), closes [#806](https://github.com/Hufe921/canvas-editor/issues/806) +* image floating position boundary error ([a833f85](https://github.com/Hufe921/canvas-editor/commit/a833f85ade8ec7d0d7cc2a27f79f34ef747a9987)) + + +### Chores + +* change the style of the hollow block list #809 ([9880deb](https://github.com/Hufe921/canvas-editor/commit/9880deb116e0b135556592b124dc95861fc2b0b6)), closes [#809](https://github.com/Hufe921/canvas-editor/issues/809) + + +### Documentation + +* update plugin markdown ([e5711b4](https://github.com/Hufe921/canvas-editor/commit/e5711b40189d93a887280a88e5604ee9429ae20e)) + + +### Features + +* add image surround display #554 ([a9f80a4](https://github.com/Hufe921/canvas-editor/commit/a9f80a448f42b09de6ac5f36aac8cd2d01dfae38)), closes [#554](https://github.com/Hufe921/canvas-editor/issues/554) +* add the pageNo property to the getCatalog api ([1e48893](https://github.com/Hufe921/canvas-editor/commit/1e48893e92ee16eb3de463910d6ccebe83aefce0)) +* add title attributes to range context #738 ([1e8a923](https://github.com/Hufe921/canvas-editor/commit/1e8a923da4fe1c4bb6470cf9623818c9c6c66a5f)), closes [#738](https://github.com/Hufe921/canvas-editor/issues/738) +* quick add table row and col tool ([641acda](https://github.com/Hufe921/canvas-editor/commit/641acdabf26ae99c2239e1ffebdb59b6e4a78f75)) +* separate table operation api ([ee2312e](https://github.com/Hufe921/canvas-editor/commit/ee2312e9071473bb296d4515bab60254937e2414)) + + + +## [0.9.92](https://github.com/Hufe921/canvas-editor/compare/v0.9.91...v0.9.92) (2024-09-06) + + +### Bug Fixes + +* update resizer size when scaling the page ([2351855](https://github.com/Hufe921/canvas-editor/commit/2351855e83c87da3b0999799102e6e051b456295)) + + +### Documentation + +* update command markdown #792 ([3c598ba](https://github.com/Hufe921/canvas-editor/commit/3c598bae1a4fb10d74fd330e46f95aa386edc956)), closes [#792](https://github.com/Hufe921/canvas-editor/issues/792) +* update plugin markdown #789 ([91c68e8](https://github.com/Hufe921/canvas-editor/commit/91c68e8dcd117e3189bbb9385fca4c42b2074c23)), closes [#789](https://github.com/Hufe921/canvas-editor/issues/789) + + +### Features + +* add deletable and disabled attributes for table td #724 ([753510b](https://github.com/Hufe921/canvas-editor/commit/753510bac4f763ab2f034657d9efd7d9760f9d4a)), closes [#724](https://github.com/Hufe921/canvas-editor/issues/724) +* add design mode #795 ([55a58cd](https://github.com/Hufe921/canvas-editor/commit/55a58cdfb20ba7b642400314a9ea0d207e2e7dc4)), closes [#795](https://github.com/Hufe921/canvas-editor/issues/795) +* add executeFocus api #796 ([3c17631](https://github.com/Hufe921/canvas-editor/commit/3c176318e079d8f793367df552c1a3c6a6294840)), closes [#796](https://github.com/Hufe921/canvas-editor/issues/796) +* add extension property to Td and Tr element #799 ([0074781](https://github.com/Hufe921/canvas-editor/commit/0074781ef3753cdf9baf4cae77325de5d7b2e9df)), closes [#799](https://github.com/Hufe921/canvas-editor/issues/799) +* add position context change actuator #733 ([66c73e1](https://github.com/Hufe921/canvas-editor/commit/66c73e174c54106cdf35e6d40382bb4a7ceeae43)), closes [#733](https://github.com/Hufe921/canvas-editor/issues/733) +* highlight the background when the control is activated #740 ([b426b13](https://github.com/Hufe921/canvas-editor/commit/b426b13ec6ffbb5aebe6d008ddc3ccc8b5efaa05)), closes [#740](https://github.com/Hufe921/canvas-editor/issues/740) + + + +## [0.9.91](https://github.com/Hufe921/canvas-editor/compare/v0.9.90...v0.9.91) (2024-08-25) + + +### Bug Fixes + +* format different types of line breaks #769 ([f65ff87](https://github.com/Hufe921/canvas-editor/commit/f65ff87e44728322417d235d2347ceca25cc6667)), closes [#769](https://github.com/Hufe921/canvas-editor/issues/769) +* format initial data boundary error #771 #784 ([f62a315](https://github.com/Hufe921/canvas-editor/commit/f62a315a7bd04e49e9e17bc7737ccd1f0e751d69)), closes [#771](https://github.com/Hufe921/canvas-editor/issues/771) [#784](https://github.com/Hufe921/canvas-editor/issues/784) +* set row margin boundary error ([5285170](https://github.com/Hufe921/canvas-editor/commit/528517094373b5fa9ca2145bb259889db865a588)) + + +### Features + +* add page border ([3f6bdf6](https://github.com/Hufe921/canvas-editor/commit/3f6bdf6fcb583ab370cb5abe3eafebf6100f415e)) +* hit checkbox/radio control when click on label #651 ([31b76b6](https://github.com/Hufe921/canvas-editor/commit/31b76b6fb6640ea75383569cb16ad1b5a2ccf25f)), closes [#651](https://github.com/Hufe921/canvas-editor/issues/651) + + + +## [0.9.90](https://github.com/Hufe921/canvas-editor/compare/v0.9.89...v0.9.90) (2024-08-18) + + +### Bug Fixes + +* float image position when scaling the page #766 ([c249b9e](https://github.com/Hufe921/canvas-editor/commit/c249b9eec215fe573b325d1430212f5f01c32099)), closes [#766](https://github.com/Hufe921/canvas-editor/issues/766) +* get range paragraph info boundary error #758 ([4653fe7](https://github.com/Hufe921/canvas-editor/commit/4653fe7427f4a9ec40700f6a86ff284d1d46088a)), closes [#758](https://github.com/Hufe921/canvas-editor/issues/758) +* insert block element row flex error #754 ([136b1ff](https://github.com/Hufe921/canvas-editor/commit/136b1ffa55b7b0b78bf3d114400489e1bbab4f17)), closes [#754](https://github.com/Hufe921/canvas-editor/issues/754) +* paper printing size setting #760 ([7a6dd75](https://github.com/Hufe921/canvas-editor/commit/7a6dd753e59bde4cb581ac215b038dd1cd08c96f)), closes [#760](https://github.com/Hufe921/canvas-editor/issues/760) +* set editor mode option error #755 ([500cec3](https://github.com/Hufe921/canvas-editor/commit/500cec3e0b63e012f6572dcedb071befa114956e)), closes [#755](https://github.com/Hufe921/canvas-editor/issues/755) +* set row flex boundary error when deleting element ([2f272de](https://github.com/Hufe921/canvas-editor/commit/2f272dee58169607783e4cfd1347534f49db0834)) + + +### Features + +* add location property to executeLocationControl api #753 ([d1a1aaa](https://github.com/Hufe921/canvas-editor/commit/d1a1aaa6ae08d8395df1df664d47bfe4fe821869)), closes [#753](https://github.com/Hufe921/canvas-editor/issues/753) +* add radio and checkbox vertical align setting ([c375466](https://github.com/Hufe921/canvas-editor/commit/c3754663cee25003a7021a6079dd46e2a3a0abd8)) +* get context content width ([187498e](https://github.com/Hufe921/canvas-editor/commit/187498ed3dfba4386375d812d41b31c057ac3af8)) + + + +## [0.9.89](https://github.com/Hufe921/canvas-editor/compare/v0.9.88...v0.9.89) (2024-08-09) + + +### Bug Fixes + +* three click selection paragraph boundary error #742 ([9dd192f](https://github.com/Hufe921/canvas-editor/commit/9dd192ffd26f9f271efa06c81b6bd5e32557d872)), closes [#742](https://github.com/Hufe921/canvas-editor/issues/742) + + +### Documentation + +* update plugin markdown ([c2d3e94](https://github.com/Hufe921/canvas-editor/commit/c2d3e941b19a6bc766b0bfa8dac7911578170b6c)) + + +### Features + +* add id property to contextMenu context #737 ([997ecc0](https://github.com/Hufe921/canvas-editor/commit/997ecc03e89afeaaf6b903f9f253c5c5090b3f78)), closes [#737](https://github.com/Hufe921/canvas-editor/issues/737) +* add line number option #734 ([d89218a](https://github.com/Hufe921/canvas-editor/commit/d89218a916fce5a7b7b6bf27fa4663ecc4d15cf6)), closes [#734](https://github.com/Hufe921/canvas-editor/issues/734) +* control related apis support the control id property ([dd1b53e](https://github.com/Hufe921/canvas-editor/commit/dd1b53ee6297bc72b0064c09522b597118919c50)) +* set range using the shift shortcut key #728 ([8878fd7](https://github.com/Hufe921/canvas-editor/commit/8878fd7734ae0a566f16ca2db14e64ded5a05f38)), closes [#728](https://github.com/Hufe921/canvas-editor/issues/728) + + + +## [0.9.88](https://github.com/Hufe921/canvas-editor/compare/v0.9.87...v0.9.88) (2024-08-02) + + +### Bug Fixes + +* float image position boundary error #716 ([f5113f5](https://github.com/Hufe921/canvas-editor/commit/f5113f539c5b224fdb9af8cde8e61f632dc7e49f)), closes [#716](https://github.com/Hufe921/canvas-editor/issues/716) + + +### Chores + +* add touch support to signature component ([c3ef290](https://github.com/Hufe921/canvas-editor/commit/c3ef2907ae31be21cd3bc61a5600ad381230034f)) +* update issue template ([eea301e](https://github.com/Hufe921/canvas-editor/commit/eea301eb11fdc3d5870da2408abccf4d96d8532f)) + + +### Features + +* add applyPageNumbers attribute to background option #729 ([8d112a8](https://github.com/Hufe921/canvas-editor/commit/8d112a8518656edd14201cb9bd16a417752fcdf9)), closes [#729](https://github.com/Hufe921/canvas-editor/issues/729) +* add cursor setting option to executeSetValue api #715 ([3235e5a](https://github.com/Hufe921/canvas-editor/commit/3235e5ae386bd1f48f5f64427e1eb0ed3782838d)), closes [#715](https://github.com/Hufe921/canvas-editor/issues/715) +* add title disabled property #680 ([87a8dbe](https://github.com/Hufe921/canvas-editor/commit/87a8dbea1596bcdee28edbb58b02fbe2a36e6c58)), closes [#680](https://github.com/Hufe921/canvas-editor/issues/680) + + + +## [0.9.87](https://github.com/Hufe921/canvas-editor/compare/v0.9.86...v0.9.87) (2024-07-26) + + +### Bug Fixes + +* format of checkbox and radio control value ([72686fd](https://github.com/Hufe921/canvas-editor/commit/72686fda1bf12353699fd57273493d8a9b4caee4)) +* highlight checkbox and radio control #707 ([d939aa3](https://github.com/Hufe921/canvas-editor/commit/d939aa35ba7b19e5d1b5c4121fb873b2c579cc55)), closes [#707](https://github.com/Hufe921/canvas-editor/issues/707) +* set control highlight limit component type ([e14fbd6](https://github.com/Hufe921/canvas-editor/commit/e14fbd622d1076cba5f34bd4fa29530af4f70261)) +* update punctuation width when scaling the page #712 ([83cb479](https://github.com/Hufe921/canvas-editor/commit/83cb47913195c43cde2b3ae344ff1d89a223e6a6)), closes [#712](https://github.com/Hufe921/canvas-editor/issues/712) +* word breaking when scaling the page #666 ([2bd1f34](https://github.com/Hufe921/canvas-editor/commit/2bd1f34c15196968f69d4711613f1c81f1dade68)), closes [#666](https://github.com/Hufe921/canvas-editor/issues/666) + + +### Features + +* add custom field to getValue api #699 ([67c63f8](https://github.com/Hufe921/canvas-editor/commit/67c63f856f2c0e3e8c0644e39694357639c18d7e)), closes [#699](https://github.com/Hufe921/canvas-editor/issues/699) +* delete cell contents when selecting rows and columns #706 ([ccd0627](https://github.com/Hufe921/canvas-editor/commit/ccd0627a6fad975acb43e9f16dda3fa13972c908)), closes [#706](https://github.com/Hufe921/canvas-editor/issues/706) +* optimize text selection at the beginning of a line #695 ([97ac2da](https://github.com/Hufe921/canvas-editor/commit/97ac2daaf8f0688b181cb8baea9ba74ae1664361)), closes [#695](https://github.com/Hufe921/canvas-editor/issues/695) +* set control properties in read-only mode #679 ([26a3468](https://github.com/Hufe921/canvas-editor/commit/26a3468f66d67bf8249cdc1a679c740d7cf1a9c9)), closes [#679](https://github.com/Hufe921/canvas-editor/issues/679) +* set the container scrollbar to automatically scroll #711 ([b226566](https://github.com/Hufe921/canvas-editor/commit/b226566e2bf6f1b4bc3ae11577c7a96ae3cbf2d0)), closes [#711](https://github.com/Hufe921/canvas-editor/issues/711) + + + +## [0.9.86](https://github.com/Hufe921/canvas-editor/compare/v0.9.85...v0.9.86) (2024-07-13) + + +### Bug Fixes + +* add control placeholder boundary error #686 ([fac5c5c](https://github.com/Hufe921/canvas-editor/commit/fac5c5c045a30cb5e0c46ef027c83f21e07bcaa8)), closes [#686](https://github.com/Hufe921/canvas-editor/issues/686) +* add control placeholder using default style #691 ([eb3ea5e](https://github.com/Hufe921/canvas-editor/commit/eb3ea5ed55cdedea0e281d45177c8661f674a280)), closes [#691](https://github.com/Hufe921/canvas-editor/issues/691) +* delete table col boundary error #688 ([3f0a49f](https://github.com/Hufe921/canvas-editor/commit/3f0a49f56be7060d328a63c994f571b0a35a3521)), closes [#688](https://github.com/Hufe921/canvas-editor/issues/688) +* refocus when cursor is not focused #685 ([0ac8ae7](https://github.com/Hufe921/canvas-editor/commit/0ac8ae7c4b0b47ee84a9c0f8c37ffde612849957)), closes [#685](https://github.com/Hufe921/canvas-editor/issues/685) +* remove title and list properties from getControlList return value #683 ([b024050](https://github.com/Hufe921/canvas-editor/commit/b024050b3ef7f787079a74b062e5d83085be1a5f)), closes [#683](https://github.com/Hufe921/canvas-editor/issues/683) + + +### Features + +* add executeInsertControl api ([e5b3d05](https://github.com/Hufe921/canvas-editor/commit/e5b3d05991a26dc186cdf63962ca3c7b50a32572)) + + + +## [0.9.85](https://github.com/Hufe921/canvas-editor/compare/v0.9.84...v0.9.85) (2024-07-07) + + +### Bug Fixes + +* custom override method removes support for asynchronous #672 ([0e705d6](https://github.com/Hufe921/canvas-editor/commit/0e705d6a0bdb0922efd5c47edd8ca9eba9964199)), closes [#672](https://github.com/Hufe921/canvas-editor/issues/672) +* set control highlight and re render #678 ([24df9d3](https://github.com/Hufe921/canvas-editor/commit/24df9d3b006a8daf795ada677bfd0159e3ccc3f5)), closes [#678](https://github.com/Hufe921/canvas-editor/issues/678) + + +### Chores + +* update build.yml ([a40441d](https://github.com/Hufe921/canvas-editor/commit/a40441dc3994a41ae29c341a542be6e0e7dada2e)) + + +### Features + +* add render mode #667 ([affd191](https://github.com/Hufe921/canvas-editor/commit/affd1911552a73a2b63a13f2c423121637830b99)), closes [#667](https://github.com/Hufe921/canvas-editor/issues/667) +* add title deletable property #670 ([b3d8413](https://github.com/Hufe921/canvas-editor/commit/b3d8413b35050eac626af1c57beaaf1b8d692a0e)), closes [#670](https://github.com/Hufe921/canvas-editor/issues/670) +* insert element boundary optimization #669 ([de44bd6](https://github.com/Hufe921/canvas-editor/commit/de44bd68ab01e5ffa8d6a8dc5d566b0cdb8d08e6)), closes [#669](https://github.com/Hufe921/canvas-editor/issues/669) + + +### Tests + +* update text test case ([c24da73](https://github.com/Hufe921/canvas-editor/commit/c24da737b15200775a7d4a5edf4e2224f6ec5429)) + + + +## [0.9.84](https://github.com/Hufe921/canvas-editor/compare/v0.9.83...v0.9.84) (2024-06-30) + + +### Bug Fixes + +* merge table cell boundary error #661 ([146ac75](https://github.com/Hufe921/canvas-editor/commit/146ac75a002338f13c96900a2849062c29018606)), closes [#661](https://github.com/Hufe921/canvas-editor/issues/661) +* set default style for control using executeSetControlProperties #658 ([7b5079c](https://github.com/Hufe921/canvas-editor/commit/7b5079c9b638730f7ea609239b3cb91b915d4650)), closes [#658](https://github.com/Hufe921/canvas-editor/issues/658) + + +### Features + +* optimization of table operations in form mode #662 ([b740637](https://github.com/Hufe921/canvas-editor/commit/b74063741f413d5bf6f90c748761f64141405ca7)), closes [#662](https://github.com/Hufe921/canvas-editor/issues/662) +* override method with default interception behavior #663 ([9a4b4f9](https://github.com/Hufe921/canvas-editor/commit/9a4b4f9a4a70a344740212507e250d9bdea5dd32)), closes [#663](https://github.com/Hufe921/canvas-editor/issues/663) + + + +## [0.9.83](https://github.com/Hufe921/canvas-editor/compare/v0.9.82...v0.9.83) (2024-06-21) + + +### Bug Fixes + +* executeSetControlProperties api invalid in table #653 ([fdcf639](https://github.com/Hufe921/canvas-editor/commit/fdcf6397e1ce62ef1ed18a526a0642c3f120df3b)), closes [#653](https://github.com/Hufe921/canvas-editor/issues/653) + + +### Features + +* add clear format attributes ([e21533a](https://github.com/Hufe921/canvas-editor/commit/e21533a3d1cfeecde562aaa6c52ee306e5063a01)) +* add conceptId attribute to table td #654 ([959a062](https://github.com/Hufe921/canvas-editor/commit/959a062f830042b78af498e2d6cbc4e5f82fa3d4)), closes [#654](https://github.com/Hufe921/canvas-editor/issues/654) +* add mouse event listener #603 ([a2978bd](https://github.com/Hufe921/canvas-editor/commit/a2978bd1f507e9417b995bbb0b7ff756dbe5d2c4)), closes [#603](https://github.com/Hufe921/canvas-editor/issues/603) +* copy table structure and data #516 ([76c20a6](https://github.com/Hufe921/canvas-editor/commit/76c20a6b44914396ae7dd69a7c97db1b179937c2)), closes [#516](https://github.com/Hufe921/canvas-editor/issues/516) + + + +## [0.9.82](https://github.com/Hufe921/canvas-editor/compare/v0.9.81...v0.9.82) (2024-06-14) + + +### Bug Fixes + +* table cell merge boundary error #645 ([f7da332](https://github.com/Hufe921/canvas-editor/commit/f7da33235419aadb2aea024d3cba4444d6d719fd)), closes [#645](https://github.com/Hufe921/canvas-editor/issues/645) + + +### Features + +* add executeUpdateElementById api #648 ([5c896bf](https://github.com/Hufe921/canvas-editor/commit/5c896bf95814c6ef2956221763e7df560b3fe98a)), closes [#648](https://github.com/Hufe921/canvas-editor/issues/648) +* add override internal drop function api #643 ([ec7e076](https://github.com/Hufe921/canvas-editor/commit/ec7e0760c113711fdafc13374ea521605308f867)), closes [#643](https://github.com/Hufe921/canvas-editor/issues/643) +* move control position by dragging #456 ([cdb0788](https://github.com/Hufe921/canvas-editor/commit/cdb0788dfcecadb70e680eb40a31551e0e096401)), closes [#456](https://github.com/Hufe921/canvas-editor/issues/456) + + + +## [0.9.81](https://github.com/Hufe921/canvas-editor/compare/v0.9.80...v0.9.81) (2024-06-07) + + +### Bug Fixes + +* disable zone tip in continuous page mode #638 ([bf322df](https://github.com/Hufe921/canvas-editor/commit/bf322dfea334ee242a452c44fdb613a1937d963b)), closes [#638](https://github.com/Hufe921/canvas-editor/issues/638) +* some shortcut keys with shift are invalid #629 ([aca9d34](https://github.com/Hufe921/canvas-editor/commit/aca9d34a46004f2da207f7127f69430e4e59ab25)), closes [#629](https://github.com/Hufe921/canvas-editor/issues/629) + + +### Documentation + +* update start.md ([55bbe22](https://github.com/Hufe921/canvas-editor/commit/55bbe22578320319d9ab38b26ebbe8687b21f1c1)) + + +### Features + +* add executeLocationControl api #592 ([53701fc](https://github.com/Hufe921/canvas-editor/commit/53701fc46c347595722801e8bca40647cda74bcb)), closes [#592](https://github.com/Hufe921/canvas-editor/issues/592) +* add maximum page number option #617 ([afce688](https://github.com/Hufe921/canvas-editor/commit/afce6882493c198595a0575640dd313c8cbdb14f)), closes [#617](https://github.com/Hufe921/canvas-editor/issues/617) +* add options to the getValue api ([65acd58](https://github.com/Hufe921/canvas-editor/commit/65acd580d6813bd15b8ee079b16ae8d5f9e77767)) +* add selection info to rangeContext ([2df03ed](https://github.com/Hufe921/canvas-editor/commit/2df03ed3867cf8edf5fb656c017986af6725780c)) +* set title style through executeSetHtml api #626 ([ac795b0](https://github.com/Hufe921/canvas-editor/commit/ac795b0bc45f99e92a5292a676414e1d7347cb7e)), closes [#626](https://github.com/Hufe921/canvas-editor/issues/626) + + +### Tests + +* update watermark test case ([c75482a](https://github.com/Hufe921/canvas-editor/commit/c75482a4f6f18da010963500e790780a63c061c4)) + + + +## [0.9.80](https://github.com/Hufe921/canvas-editor/compare/v0.9.79...v0.9.80) (2024-05-31) + + +### Bug Fixes + +* boundary error when deleting elements backwards #606 ([5006264](https://github.com/Hufe921/canvas-editor/commit/500626498f6c5088d3b998f615c25e8873021d8a)), closes [#606](https://github.com/Hufe921/canvas-editor/issues/606) +* cursor position outside the margin of the page #609 ([d16b0f4](https://github.com/Hufe921/canvas-editor/commit/d16b0f46facc7155e74cd06bf019725c5cac2a82)), closes [#609](https://github.com/Hufe921/canvas-editor/issues/609) +* error using tab key at control postfix ([bb1ee59](https://github.com/Hufe921/canvas-editor/commit/bb1ee5910e7caa0b54ea721038571210950eb59c)) +* get controls within the table #628 ([2af0847](https://github.com/Hufe921/canvas-editor/commit/2af0847f555af5491640b7a8a650ec57fb309a2c)), closes [#628](https://github.com/Hufe921/canvas-editor/issues/628) +* not copy control postfix style #631 ([523183f](https://github.com/Hufe921/canvas-editor/commit/523183f1751be898f5f9e0f84e31b73be46a9430)), closes [#631](https://github.com/Hufe921/canvas-editor/issues/631) + + +### Chores + +* add date control demo ([b08dba0](https://github.com/Hufe921/canvas-editor/commit/b08dba0e967485a8875db993344f7a87571ee080)) + + +### Features + +* add date control #601 ([3989199](https://github.com/Hufe921/canvas-editor/commit/398919938f947aea96008a55af059bf5ae4dc768)), closes [#601](https://github.com/Hufe921/canvas-editor/issues/601) +* add executeInsertTitle api #604 ([2659d6b](https://github.com/Hufe921/canvas-editor/commit/2659d6bde7b2ce7f221f93e5fcb4d4bef0f83d54)), closes [#604](https://github.com/Hufe921/canvas-editor/issues/604) +* delete image using the delete key #614 ([d54a701](https://github.com/Hufe921/canvas-editor/commit/d54a701adb435c617ff7877561fc4f265673a409)), closes [#614](https://github.com/Hufe921/canvas-editor/issues/614) + + + +## [0.9.79](https://github.com/Hufe921/canvas-editor/compare/v0.9.78...v0.9.79) (2024-05-25) + + +### Bug Fixes + +* control setting row flex data error #586 ([8f36d1f](https://github.com/Hufe921/canvas-editor/commit/8f36d1fb4f161bd125d0c8a8ac72590df2d2aefe)), closes [#586](https://github.com/Hufe921/canvas-editor/issues/586) +* drawing size error when browser scale page #594 ([7fa58f8](https://github.com/Hufe921/canvas-editor/commit/7fa58f8290bd368dc70fc5c568bea7c2b7385bd1)), closes [#594](https://github.com/Hufe921/canvas-editor/issues/594) +* export HTML block elements row flex error #598 ([4cd18c8](https://github.com/Hufe921/canvas-editor/commit/4cd18c8c65b6954bf2029b165e2749640feb15c3)), closes [#598](https://github.com/Hufe921/canvas-editor/issues/598) +* resizer position error when crossing pages #591 ([f4dd90b](https://github.com/Hufe921/canvas-editor/commit/f4dd90bbc517af8a89d707973e49032fbf320232)), closes [#591](https://github.com/Hufe921/canvas-editor/issues/591) +* set non deletable control value boundary error #595 ([bcf311f](https://github.com/Hufe921/canvas-editor/commit/bcf311ff19557764eb82294c211d2e0e22c55ff1)), closes [#595](https://github.com/Hufe921/canvas-editor/issues/595) +* update cursor status after setting page mode #588 ([28596c8](https://github.com/Hufe921/canvas-editor/commit/28596c889f1b1d49dfd246d7f7f6c4046483c8eb)), closes [#588](https://github.com/Hufe921/canvas-editor/issues/588) + + +### Features + +* add externalId property to element #552 ([6525522](https://github.com/Hufe921/canvas-editor/commit/65255229c548c2646b70d1de8b25f89a5b6b46f7)), closes [#552](https://github.com/Hufe921/canvas-editor/issues/552) +* move between controls using shortcut keys #548 ([c6c2f98](https://github.com/Hufe921/canvas-editor/commit/c6c2f981497224cc95dbdd50ee9552683bc1df6f)), closes [#548](https://github.com/Hufe921/canvas-editor/issues/548) +* split text support multiple languages #593 ([1cb07af](https://github.com/Hufe921/canvas-editor/commit/1cb07af75ec4cff03f4562ea917175d91d55852e)), closes [#593](https://github.com/Hufe921/canvas-editor/issues/593) + + +### Performance Improvements + +* adaptive size when the image is larger than the page width #599 ([b8323c0](https://github.com/Hufe921/canvas-editor/commit/b8323c0d8af01750e954ab49b7fe462844096d5a)), closes [#599](https://github.com/Hufe921/canvas-editor/issues/599) + + + +## [0.9.78](https://github.com/Hufe921/canvas-editor/compare/v0.9.77...v0.9.78) (2024-05-18) + + +### Bug Fixes + +* disable line break drawing in print and clean mode ([fc55e55](https://github.com/Hufe921/canvas-editor/commit/fc55e557dad94754be56a174eee54a1a76d86a56)) +* drag image resizer position error #567 ([1e669a6](https://github.com/Hufe921/canvas-editor/commit/1e669a6f03b9011c68366348313bfd79d154633d)), closes [#567](https://github.com/Hufe921/canvas-editor/issues/567) +* dragging to adjust td width boundary error #569 ([2738d3a](https://github.com/Hufe921/canvas-editor/commit/2738d3ae4cef76aa9a7d85ab1ab2d925ba0df5f8)), closes [#569](https://github.com/Hufe921/canvas-editor/issues/569) +* first row height boundary error #563 ([6ada65e](https://github.com/Hufe921/canvas-editor/commit/6ada65e7680b8ca71657bc643f0eb807d9b78f5f)), closes [#563](https://github.com/Hufe921/canvas-editor/issues/563) +* justify row flex boundary error #577 ([d49ed5e](https://github.com/Hufe921/canvas-editor/commit/d49ed5ea4337dc03b534b97c774b77769f1b04bc)), closes [#577](https://github.com/Hufe921/canvas-editor/issues/577) +* re render when the page is visible #578 ([24aa33a](https://github.com/Hufe921/canvas-editor/commit/24aa33a4f16282812caa16d5666bbd7f57517c7c)), closes [#578](https://github.com/Hufe921/canvas-editor/issues/578) +* wake up pop-up controls #580 ([28f6bdb](https://github.com/Hufe921/canvas-editor/commit/28f6bdbb4114f0f8b693b7688ae0b175368d491c)), closes [#580](https://github.com/Hufe921/canvas-editor/issues/580) + + +### Features + +* add executeUpdateOptions api #571 ([3175bad](https://github.com/Hufe921/canvas-editor/commit/3175bad5e70e9ac2143a1101c9e3e674cc793b69)), closes [#571](https://github.com/Hufe921/canvas-editor/issues/571) +* add table context to contextmenu and getRangeContext api #576 ([149c95f](https://github.com/Hufe921/canvas-editor/commit/149c95f9522a9832bcf91ec259fd40f146ff80c3)), closes [#576](https://github.com/Hufe921/canvas-editor/issues/576) + + + +## [0.9.77](https://github.com/Hufe921/canvas-editor/compare/v0.9.76...v0.9.77) (2024-05-11) + + +### Bug Fixes + +* delete control placeholder boundary error #553 ([73e47f5](https://github.com/Hufe921/canvas-editor/commit/73e47f56b54dfc2e8dbbe1e167b1cf868684b38c)), closes [#553](https://github.com/Hufe921/canvas-editor/issues/553) +* image resizer position boundary error #538 ([9f37995](https://github.com/Hufe921/canvas-editor/commit/9f37995a23e5655ee9011e19ce7b7aed1f9298eb)), closes [#538](https://github.com/Hufe921/canvas-editor/issues/538) +* move cursor boundary error with up and down keys #556 ([d58b28c](https://github.com/Hufe921/canvas-editor/commit/d58b28c1dbe47a7a4970b4333c0846fafbf511db)), closes [#556](https://github.com/Hufe921/canvas-editor/issues/556) +* subscript and superscript strikeout rendering ([62c94fc](https://github.com/Hufe921/canvas-editor/commit/62c94fce59c206b87dc68d0ed9d74f036cde956f)) +* subscript underline rendering position #537 ([745a098](https://github.com/Hufe921/canvas-editor/commit/745a098a707893cc7ae8ba812ab11f826b961d55)), closes [#537](https://github.com/Hufe921/canvas-editor/issues/537) + + +### Features + +* table header appears repeatedly when paging #541 ([c86e546](https://github.com/Hufe921/canvas-editor/commit/c86e5468e666c64c087a430af8a9a8307b837f0f)), closes [#541](https://github.com/Hufe921/canvas-editor/issues/541) + + +### Performance Improvements + +* control operation history boundary #540 ([24c5b74](https://github.com/Hufe921/canvas-editor/commit/24c5b74bf2196ce3a1c76d0cf5626cd249a2a5fd)), closes [#540](https://github.com/Hufe921/canvas-editor/issues/540) + + + +## [0.9.76](https://github.com/Hufe921/canvas-editor/compare/v0.9.75...v0.9.76) (2024-05-04) + + +### Bug Fixes + +* checkbox custom size rendering error #529 ([5a5fd64](https://github.com/Hufe921/canvas-editor/commit/5a5fd64176de288c7d0e02ab4cbbc2607dc3df20)), closes [#529](https://github.com/Hufe921/canvas-editor/issues/529) +* copy style after title line break #531 ([2e14035](https://github.com/Hufe921/canvas-editor/commit/2e1403507f593d6c827f6be6d0cb75c5a1615e62)), closes [#531](https://github.com/Hufe921/canvas-editor/issues/531) +* paste elements boundary error ([34d59bb](https://github.com/Hufe921/canvas-editor/commit/34d59bbb589b9bb38c74f00b66b734b6342dd440)) + + +### Chores + +* update radio menu icon ([0ae67ec](https://github.com/Hufe921/canvas-editor/commit/0ae67ec602df9c224ad134bff63b51f66d71aff7)) + + +### Features + +* add getTitleValue api #536 ([15f52c5](https://github.com/Hufe921/canvas-editor/commit/15f52c5e43a733657e6079c7112bb9df3a8d5aa6)), closes [#536](https://github.com/Hufe921/canvas-editor/issues/536) +* add justify-all property to row flex #535 ([a1293af](https://github.com/Hufe921/canvas-editor/commit/a1293aff228595cafe2c730d9fb5dbdb1241c3d8)), closes [#535](https://github.com/Hufe921/canvas-editor/issues/535) +* add radio element #494 ([c6d9cff](https://github.com/Hufe921/canvas-editor/commit/c6d9cffc275ab4114a1c9a3ce931dc0843cb5585)), closes [#494](https://github.com/Hufe921/canvas-editor/issues/494) +* add separator option #530 ([7416a88](https://github.com/Hufe921/canvas-editor/commit/7416a881515a443a5736a673cd12732d44d65264)), closes [#530](https://github.com/Hufe921/canvas-editor/issues/530) + + + +## [0.9.75](https://github.com/Hufe921/canvas-editor/compare/v0.9.74...v0.9.75) (2024-04-27) + + +### Bug Fixes + +* control element rendering boundary error in table #527 ([f41cea2](https://github.com/Hufe921/canvas-editor/commit/f41cea244309e98ca880c74aaa4e0f3a2811ad66)), closes [#527](https://github.com/Hufe921/canvas-editor/issues/527) +* list position error when setting row flex #523 ([3fdd4de](https://github.com/Hufe921/canvas-editor/commit/3fdd4dedf434a45ded0c7114cf1cd0c8a1e94a18)), closes [#523](https://github.com/Hufe921/canvas-editor/issues/523) +* search for duplicate keyword boundary error #528 ([d4c6cd2](https://github.com/Hufe921/canvas-editor/commit/d4c6cd25f639ea5d933e2c4a2d006c96e3138219)), closes [#528](https://github.com/Hufe921/canvas-editor/issues/528) +* word break boundary error #521 ([4d1a0b6](https://github.com/Hufe921/canvas-editor/commit/4d1a0b69f876eada2d0c5d866bd25464d2587a79)), closes [#521](https://github.com/Hufe921/canvas-editor/issues/521) + + +### Chores + +* add editor option settings in the demo ([07956ca](https://github.com/Hufe921/canvas-editor/commit/07956caec20eea75c994e968429028ebcfb174f4)) + + +### Features + +* draw line break marker #520 ([4c2b8fc](https://github.com/Hufe921/canvas-editor/commit/4c2b8fc20af98533796d5c4fec0d8d0c3d876116)), closes [#520](https://github.com/Hufe921/canvas-editor/issues/520) + + + +## [0.9.74](https://github.com/Hufe921/canvas-editor/compare/v0.9.73...v0.9.74) (2024-04-19) + + +### Bug Fixes + +* control component disabling segmenter ([868a791](https://github.com/Hufe921/canvas-editor/commit/868a79148d8e68ba1b34ddd8d37941e3e26988d7)) +* delete default control color property #513 ([224ead0](https://github.com/Hufe921/canvas-editor/commit/224ead0dff28f333e04bfb7c7ba4068d32670e52)), closes [#513](https://github.com/Hufe921/canvas-editor/issues/513) +* disable control placeholder selection #511 ([2985d6b](https://github.com/Hufe921/canvas-editor/commit/2985d6b62ee5311a5f8350282f313e9faca95204)), closes [#511](https://github.com/Hufe921/canvas-editor/issues/511) + + +### Features + +* add control border #388 ([de06f6c](https://github.com/Hufe921/canvas-editor/commit/de06f6cc9b2d3f37033647cf159bb9c09a432c1b)), closes [#388](https://github.com/Hufe921/canvas-editor/issues/388) +* add extension property ([5027d73](https://github.com/Hufe921/canvas-editor/commit/5027d730c4522d496ef933830df026960077e660)) +* add security rules to IFrameBlock element ([cdbd1ff](https://github.com/Hufe921/canvas-editor/commit/cdbd1ff4ded52a588d837c3b2cb04fe6168ed51f)) +* add srcdoc property to IFrameBlock element #454 ([6696992](https://github.com/Hufe921/canvas-editor/commit/66969925ac5193b5a2b0e227df247052cf79364f)), closes [#454](https://github.com/Hufe921/canvas-editor/issues/454) +* control default style #340 ([eee2236](https://github.com/Hufe921/canvas-editor/commit/eee22363d3a0de8333a6b6f8815ef178fbfc3c8d)), closes [#340](https://github.com/Hufe921/canvas-editor/issues/340) +* record the first cursor position #517 ([0878506](https://github.com/Hufe921/canvas-editor/commit/087850606224290bc6e1992711416ac0acbfa45b)), closes [#517](https://github.com/Hufe921/canvas-editor/issues/517) + + +### Tests + +* update block test case ([6d358d1](https://github.com/Hufe921/canvas-editor/commit/6d358d16dc2eb1af309c554c369f30a805451acc)) + + + +## [0.9.73](https://github.com/Hufe921/canvas-editor/compare/v0.9.72...v0.9.73) (2024-04-12) + + +### Bug Fixes + +* add context param to the shrinkBoundary function #503 ([6f690a8](https://github.com/Hufe921/canvas-editor/commit/6f690a805ff385ce4e6ed3959285ceeb730567bf)), closes [#503](https://github.com/Hufe921/canvas-editor/issues/503) +* checkbox list cannot be selected within the table ([632f8f5](https://github.com/Hufe921/canvas-editor/commit/632f8f5af8d626d28a306c212419565bf216c997)) +* disable table pagination in continuous page mode ([d0500ac](https://github.com/Hufe921/canvas-editor/commit/d0500ac58345b689bd3f331342e7c08ca1684094)) +* format list elements boundary error ([21807a6](https://github.com/Hufe921/canvas-editor/commit/21807a6f7e7b73adf0696ea848fb0b7ebdbf14ea)) + + +### Chores + +* upgrade typescript version ([7e5a1ac](https://github.com/Hufe921/canvas-editor/commit/7e5a1ac04c1866ceb6b27131d7b2cf7f5fa46fe4)) + + +### Features + +* add checkbox list #385 ([a546262](https://github.com/Hufe921/canvas-editor/commit/a546262b9d7e94011565198e0f18e7671b5439b0)), closes [#385](https://github.com/Hufe921/canvas-editor/issues/385) +* double click the selected text through the segmenter #510 ([3f8399d](https://github.com/Hufe921/canvas-editor/commit/3f8399de6299331c07e3d7c3c8d16dee64528d4a)), closes [#510](https://github.com/Hufe921/canvas-editor/issues/510) +* the getText method add list style conversion ([f80e004](https://github.com/Hufe921/canvas-editor/commit/f80e004917d53cbb3868c940c26d94f85d0d8615)) +* the getText method add tab conversion #507 ([762f10c](https://github.com/Hufe921/canvas-editor/commit/762f10c37729a2d51dd3a79e6f3b8c9e3134926b)), closes [#507](https://github.com/Hufe921/canvas-editor/issues/507) + + + +## [0.9.72](https://github.com/Hufe921/canvas-editor/compare/v0.9.71...v0.9.72) (2024-04-06) + + +### Bug Fixes + +* cannot page when merge cells across columns in the same row #41 ([5851e61](https://github.com/Hufe921/canvas-editor/commit/5851e61cfcbb14fa199b507dc429917075659161)), closes [#41](https://github.com/Hufe921/canvas-editor/issues/41) +* format text class elements boundary error ([95b337d](https://github.com/Hufe921/canvas-editor/commit/95b337de05df74a07d086dbec6c6c52ab95ba43a)) +* strikeout style rendering position #498 ([46e153d](https://github.com/Hufe921/canvas-editor/commit/46e153d588e78302e26827461a06682bbccd75aa)), closes [#498](https://github.com/Hufe921/canvas-editor/issues/498) +* table range drawing boundary error ([1df98b9](https://github.com/Hufe921/canvas-editor/commit/1df98b9359f3ec4184a7045cfcea93e156ef7f9f)) + + +### Features + +* add isTable property to the RangeContext interface ([9ad991a](https://github.com/Hufe921/canvas-editor/commit/9ad991a3934fc284bb829d2652a739cfbec80eee)) + + + +## [0.9.71](https://github.com/Hufe921/canvas-editor/compare/v0.9.70...v0.9.71) (2024-03-29) + + +### Bug Fixes + +* adjust the order of rich text rendering ([7458a9f](https://github.com/Hufe921/canvas-editor/commit/7458a9fd2036819a5646d7cf5563f03d1e7ce48b)) +* cannot deletable elements boundary error #491 ([291ea26](https://github.com/Hufe921/canvas-editor/commit/291ea26b06c39b6649dbc6fff2fdb75748756556)), closes [#491](https://github.com/Hufe921/canvas-editor/issues/491) +* control front and back operation boundary error ([1bb7a58](https://github.com/Hufe921/canvas-editor/commit/1bb7a58f5eaee8d09fadecbd1ee8717dd2763086)) +* punctuation symbols rendered separately ([d91b47c](https://github.com/Hufe921/canvas-editor/commit/d91b47cee540f562647e0d84ec04191a65945123)) + + +### Features + +* move between table cells using up and down keys #465 ([2de1ba7](https://github.com/Hufe921/canvas-editor/commit/2de1ba7b62cc04307abeaec78b61516db41a71aa)), closes [#465](https://github.com/Hufe921/canvas-editor/issues/465) + + + +## [0.9.70](https://github.com/Hufe921/canvas-editor/compare/v0.9.69...v0.9.70) (2024-03-22) + + +### Bug Fixes + +* clear draw side effects when set zone ([169864f](https://github.com/Hufe921/canvas-editor/commit/169864f040924aded054a6ca174ab0e074cb6984)) +* header and footer floating image error #473 ([f14b863](https://github.com/Hufe921/canvas-editor/commit/f14b8635c25ccf176f3dfd23afc082a37b89aca6)), closes [#473](https://github.com/Hufe921/canvas-editor/issues/473) +* paste list element boundary error #487 ([3796cab](https://github.com/Hufe921/canvas-editor/commit/3796cabb377247541b39b24a9f25c5db9c856d64)), closes [#487](https://github.com/Hufe921/canvas-editor/issues/487) +* table border style lost when exporting HTML #480 ([b6758a6](https://github.com/Hufe921/canvas-editor/commit/b6758a63e651690c9a7797cbd7dfbfabf6aa51e6)), closes [#480](https://github.com/Hufe921/canvas-editor/issues/480) + + +### Chores + +* update build.yml ([056648d](https://github.com/Hufe921/canvas-editor/commit/056648dd5ccf6bbf62f845ca66ee05a00cbc9d86)) + + +### Features + +* move between table cells using left and right keys #465 ([83f37ed](https://github.com/Hufe921/canvas-editor/commit/83f37edbca80ca5df5349c9c24639528107846ef)), closes [#465](https://github.com/Hufe921/canvas-editor/issues/465) +* table element paging across multiple pages #41 ([01b1104](https://github.com/Hufe921/canvas-editor/commit/01b1104de47fcdbb61d8e81e25047c2560d8b086)), closes [#41](https://github.com/Hufe921/canvas-editor/issues/41) + + +### Performance Improvements + +* floating image initial position #484 ([9d2ee3a](https://github.com/Hufe921/canvas-editor/commit/9d2ee3a6407052b18e9556a020197509ef1ae5b3)), closes [#484](https://github.com/Hufe921/canvas-editor/issues/484) + + +### Refactor + +* keydown event code structure ([0ff6c2f](https://github.com/Hufe921/canvas-editor/commit/0ff6c2fd1f8c1e58973f557a4621cb36cb8d26c4)) + + + +## [0.9.69](https://github.com/Hufe921/canvas-editor/compare/v0.9.68...v0.9.69) (2024-03-15) + + +### Bug Fixes + +* adjust the style of converting table element to html #458 ([0003686](https://github.com/Hufe921/canvas-editor/commit/000368636cba3547e3b280e1960e72198b41cb01)), closes [#458](https://github.com/Hufe921/canvas-editor/issues/458) +* copy html boundary error #470 ([4e46afa](https://github.com/Hufe921/canvas-editor/commit/4e46afab687c696360a96f45dd3cd97551f951ec)), closes [#470](https://github.com/Hufe921/canvas-editor/issues/470) + + +### Features + +* add getControlList api #455 ([0523fc2](https://github.com/Hufe921/canvas-editor/commit/0523fc257ae6f2c9b2511e912a4272c6b962d41e)), closes [#455](https://github.com/Hufe921/canvas-editor/issues/455) +* add parameter for clearing font color and highlight color #461 ([73f9cfd](https://github.com/Hufe921/canvas-editor/commit/73f9cfdf88afcfab4376bf3c4011b171c0669d2f)), closes [#461](https://github.com/Hufe921/canvas-editor/issues/461) +* cancel painter style setting #453 ([51427c7](https://github.com/Hufe921/canvas-editor/commit/51427c7dc462ea5a33ae6a92836d1e7ded7cf43d)), closes [#453](https://github.com/Hufe921/canvas-editor/issues/453) +* table element can be merged after paging #41 ([33a2dd8](https://github.com/Hufe921/canvas-editor/commit/33a2dd8faa7a46bc9290b744dad93a761ed6e1cf)), closes [#41](https://github.com/Hufe921/canvas-editor/issues/41) + + +### Refactor + +* date element renderer #460 ([788f96a](https://github.com/Hufe921/canvas-editor/commit/788f96aa89766cecf0e835fd0ffe64140bb94e87)), closes [#460](https://github.com/Hufe921/canvas-editor/issues/460) + + +### Tests + +* update painter test case (#459) ([a058c59](https://github.com/Hufe921/canvas-editor/commit/a058c5956cc2c26b347e5528d8164e2e1eff1225)), closes [#459](https://github.com/Hufe921/canvas-editor/issues/459) + + + +## [0.9.68](https://github.com/Hufe921/canvas-editor/compare/v0.9.67...v0.9.68) (2024-03-10) + + +### Bug Fixes + +* dragging element boundary error ([a2d8dd5](https://github.com/Hufe921/canvas-editor/commit/a2d8dd55b36a09b42fae377e06bf667dced3857e)) +* hyperlink word count statistics #449 ([180bd08](https://github.com/Hufe921/canvas-editor/commit/180bd088397159e32dc70da4eefd507721ced432)), closes [#449](https://github.com/Hufe921/canvas-editor/issues/449) + + +### Features + +* set print layout format when printing #448 ([c6534f7](https://github.com/Hufe921/canvas-editor/commit/c6534f766d8640cbea4e441541065d25e0dd8b82)), closes [#448](https://github.com/Hufe921/canvas-editor/issues/448) + + +### Performance Improvements + +* history stack memory ([5044c31](https://github.com/Hufe921/canvas-editor/commit/5044c319211322c0ab2a2db461b029f34b292939)) + + + +## [0.9.67](https://github.com/Hufe921/canvas-editor/compare/v0.9.66...v0.9.67) (2024-03-01) + + +### Bug Fixes + +* dragging image boundary error within the control ([52590f6](https://github.com/Hufe921/canvas-editor/commit/52590f6d92746e30aaf92efe85b0486ddd3cb467)) +* text control clear value range context error #439 (#443) ([c299290](https://github.com/Hufe921/canvas-editor/commit/c2992909b6c7ff94d6685007d09fa9611b5e6d8d)), closes [#439](https://github.com/Hufe921/canvas-editor/issues/439) [#443](https://github.com/Hufe921/canvas-editor/issues/443) + + +### Chores + +* add underline decoration type demo ([aa12296](https://github.com/Hufe921/canvas-editor/commit/aa12296ef67aa66e46d6615e91833199746c8bae)) +* update FUNDING.yml ([dc2804c](https://github.com/Hufe921/canvas-editor/commit/dc2804c492b1d5d745ec58276e4ff8bc1ed825b3)) + + +### Features + +* add text decoration property ([f1570f2](https://github.com/Hufe921/canvas-editor/commit/f1570f2180086c1d4f9bf92e06edf5baecbd436c)) +* add textDecoration property to the rangeStyleChange event ([a7fa847](https://github.com/Hufe921/canvas-editor/commit/a7fa847b198010cc5c7b8af9c860a04fe1c4250d)) + + + +## [0.9.66](https://github.com/Hufe921/canvas-editor/compare/v0.9.65...v0.9.66) (2024-02-24) + + +### Bug Fixes + +* disable automatic selection when double clicking the checkbox ([72a22b5](https://github.com/Hufe921/canvas-editor/commit/72a22b5e619ea0308af8d2c06d9afb5d2c8e81f2)) +* get catalog text filtering element types ([36477d2](https://github.com/Hufe921/canvas-editor/commit/36477d23a3caf234a1165a784ebf7d0451fefa01)) +* latex element preview rendering boundary error ([6f0ab64](https://github.com/Hufe921/canvas-editor/commit/6f0ab649cd49c6f0272da9d12b1ac9ab8b6a262e)) +* richtext elements boundary render error ([956035b](https://github.com/Hufe921/canvas-editor/commit/956035b0e308db5129ea461fae868343723cede7)) + + +### Features + +* dragging image element to adjust position #404 ([9428148](https://github.com/Hufe921/canvas-editor/commit/9428148f42fdd1828af69de9e9a3be30c6191796)), closes [#404](https://github.com/Hufe921/canvas-editor/issues/404) +* image element floating #363 ([b357a57](https://github.com/Hufe921/canvas-editor/commit/b357a57dfd15e20abdc275e2be148977cb73889c)), closes [#363](https://github.com/Hufe921/canvas-editor/issues/363) +* table td with multiple border types #435 ([1d4987e](https://github.com/Hufe921/canvas-editor/commit/1d4987ea670ffbd254d040cf93019c1d3b5f0765)), closes [#435](https://github.com/Hufe921/canvas-editor/issues/435) +* table td with multiple slash types #436 ([5b52bb8](https://github.com/Hufe921/canvas-editor/commit/5b52bb8794b96a9f9460fe65cf4c2467e8790299)), closes [#436](https://github.com/Hufe921/canvas-editor/issues/436) + + + +## [0.9.65](https://github.com/Hufe921/canvas-editor/compare/v0.9.64...v0.9.65) (2024-02-06) + + +### Bug Fixes + +* cursor position error when scaling the page #434 ([e03feb2](https://github.com/Hufe921/canvas-editor/commit/e03feb210282779ecebb7af3d9a3801392b66979)), closes [#434](https://github.com/Hufe921/canvas-editor/issues/434) +* insert image render error when scaling the page #433 ([acb0d3f](https://github.com/Hufe921/canvas-editor/commit/acb0d3fc47953c822b2daa5b1b437780a2c0f67e)), closes [#433](https://github.com/Hufe921/canvas-editor/issues/433) + + +### Features + +* add getRange api #429 ([2a6a41c](https://github.com/Hufe921/canvas-editor/commit/2a6a41c8c9ebe8188de08d43f10db89c77016950)), closes [#429](https://github.com/Hufe921/canvas-editor/issues/429) +* paste original elements by api ([7ab103e](https://github.com/Hufe921/canvas-editor/commit/7ab103edd9c3dbfa3c94b31167fc68487656a4d0)) +* set margin style when printing #431 ([4015707](https://github.com/Hufe921/canvas-editor/commit/4015707025689648be2d3082dfbcbe2e597b55d1)), closes [#431](https://github.com/Hufe921/canvas-editor/issues/431) + + + +## [0.9.64](https://github.com/Hufe921/canvas-editor/compare/v0.9.63...v0.9.64) (2024-01-28) + + +### Bug Fixes + +* error inserting image within control #422 ([ea4ac33](https://github.com/Hufe921/canvas-editor/commit/ea4ac339c7de962845f639a1c5ac24d8e3640485)), closes [#422](https://github.com/Hufe921/canvas-editor/issues/422) +* render error when row element is empty #420 ([8999f28](https://github.com/Hufe921/canvas-editor/commit/8999f283bb87d92fe58b1aa8330bf4d9d75b9064)), closes [#420](https://github.com/Hufe921/canvas-editor/issues/420) +* zone tip position error in firefox browser #423 ([3cf911c](https://github.com/Hufe921/canvas-editor/commit/3cf911c501a0c0af85d994d5b50c657c6cd77692)), closes [#423](https://github.com/Hufe921/canvas-editor/issues/423) + + +### Features + +* add executeSetControlProperties api #391 ([3ffb6b9](https://github.com/Hufe921/canvas-editor/commit/3ffb6b94b57a0d0fe81cc778a26d4e2a234e24ab)), closes [#391](https://github.com/Hufe921/canvas-editor/issues/391) +* copy and paste original elements #397 (#426) ([2fc16de](https://github.com/Hufe921/canvas-editor/commit/2fc16de4e15578cdd181c4186b4cf978924b5207)), closes [#397](https://github.com/Hufe921/canvas-editor/issues/397) [#426](https://github.com/Hufe921/canvas-editor/issues/426) + + + +## [0.9.63](https://github.com/Hufe921/canvas-editor/compare/v0.9.62...v0.9.63) (2024-01-19) + + +### Bug Fixes + +* copy row properties on input #415 ([55a43e6](https://github.com/Hufe921/canvas-editor/commit/55a43e61bf6aded9f50644e86d3a1c276ee7a53a)), closes [#415](https://github.com/Hufe921/canvas-editor/issues/415) +* format list element boundary error ([094af57](https://github.com/Hufe921/canvas-editor/commit/094af57302a7db0c83cb3dd8a5eb9bbe5581b8f8)) +* image render error within the control #406 ([d175f92](https://github.com/Hufe921/canvas-editor/commit/d175f920e8887cc3b1f5132e8ac7443b0d556204)), closes [#406](https://github.com/Hufe921/canvas-editor/issues/406) + + +### Features + +* keep aspect ratio when drag image #414 ([e8684da](https://github.com/Hufe921/canvas-editor/commit/e8684daffd40a8efda0342809846451afa0027a2)), closes [#414](https://github.com/Hufe921/canvas-editor/issues/414) + + + +## [0.9.62](https://github.com/Hufe921/canvas-editor/compare/v0.9.61...v0.9.62) (2024-01-12) + + +### Bug Fixes + +* control minimum width rendering boundary error #401 ([5272c85](https://github.com/Hufe921/canvas-editor/commit/5272c85bbe9a723886506363e3ff4f51c2c6a941)), closes [#401](https://github.com/Hufe921/canvas-editor/issues/401) +* disable zone tips when header and footer disabled #386 ([531750b](https://github.com/Hufe921/canvas-editor/commit/531750bb44844c31f2ea140078e68964a7c50923)), closes [#386](https://github.com/Hufe921/canvas-editor/issues/386) + + +### Features + +* add background image option ([eadf7f6](https://github.com/Hufe921/canvas-editor/commit/eadf7f6e49df4534a49f3e7c263c2caca96b3c3a)) +* add defaultColor option #405 ([a324ecc](https://github.com/Hufe921/canvas-editor/commit/a324ecc417fd2993ecdc22fc6d4299178d27de60)), closes [#405](https://github.com/Hufe921/canvas-editor/issues/405) +* add table cell border type #389 ([3253f37](https://github.com/Hufe921/canvas-editor/commit/3253f3708e50220828cd26dc129ba6bd448a2ad0)), closes [#389](https://github.com/Hufe921/canvas-editor/issues/389) +* copy style information when wrapping #384 ([981e458](https://github.com/Hufe921/canvas-editor/commit/981e4582f91b87a23afde67c0e401ff71dc42b21)), closes [#384](https://github.com/Hufe921/canvas-editor/issues/384) +* support drop images #398 (#402) ([a96d239](https://github.com/Hufe921/canvas-editor/commit/a96d2390365c6fe058e15b654bf5589373214109)), closes [#398](https://github.com/Hufe921/canvas-editor/issues/398) [#402](https://github.com/Hufe921/canvas-editor/issues/402) + + +### Tests + +* update format test case ([f9edf73](https://github.com/Hufe921/canvas-editor/commit/f9edf731a2a4a4a421636a512dfe41071d86b9ba)) + + + +## [0.9.61](https://github.com/Hufe921/canvas-editor/compare/v0.9.60...v0.9.61) (2023-12-29) + + +### Bug Fixes + +* checkbox cannot be selected #382 ([3fb8435](https://github.com/Hufe921/canvas-editor/commit/3fb843570680d1607834b47c5ad86781ea4f5f14)), closes [#382](https://github.com/Hufe921/canvas-editor/issues/382) +* double-click to expand selection boundary error ([0bd4c5c](https://github.com/Hufe921/canvas-editor/commit/0bd4c5cc51eb400e985d77911c37fd02bf574f07)) +* elements in the table cannot be selected #378 ([1477bd0](https://github.com/Hufe921/canvas-editor/commit/1477bd0e3f2685753ec66f868d5664ce5c4c85c2)), closes [#378](https://github.com/Hufe921/canvas-editor/issues/378) +* line break error before separator element #379 ([bdb981d](https://github.com/Hufe921/canvas-editor/commit/bdb981d242821d80d3d45f8569badaaa6f3a8a1d)), closes [#379](https://github.com/Hufe921/canvas-editor/issues/379) +* three click selection paragraph boundary error ([56ea7d8](https://github.com/Hufe921/canvas-editor/commit/56ea7d8bb13d6f57e24e15550a81f6e2c947f653)) + + +### Features + +* enter to delete list #376 ([f542739](https://github.com/Hufe921/canvas-editor/commit/f542739d3eb1be273e1f77c3a908dab329f8c619)), closes [#376](https://github.com/Hufe921/canvas-editor/issues/376) + + + +## [0.9.60](https://github.com/Hufe921/canvas-editor/compare/v0.9.59...v0.9.60) (2023-12-23) + + +### Bug Fixes + +* clone the values set to the editor #369 (#371) ([f73759f](https://github.com/Hufe921/canvas-editor/commit/f73759fdd75119fdff113d4ec914d5cc20de6dae)), closes [#369](https://github.com/Hufe921/canvas-editor/issues/369) [#371](https://github.com/Hufe921/canvas-editor/issues/371) +* format element list boundary error #367 ([7a6f656](https://github.com/Hufe921/canvas-editor/commit/7a6f6566994245da16d3e3f31186565d99bdcf89)), closes [#367](https://github.com/Hufe921/canvas-editor/issues/367) + + +### Chores + +* insert hyperlink with default value #368 ([d83fc0f](https://github.com/Hufe921/canvas-editor/commit/d83fc0f37a368cb14b631ff10a4618789562d570)), closes [#368](https://github.com/Hufe921/canvas-editor/issues/368) + + +### Features + +* add conceptId attribute to table element ([b55471b](https://github.com/Hufe921/canvas-editor/commit/b55471b13446959e6ac1802d086ab10140a8435d)) +* add zone attribute to getRangeContext api ([57fdcb8](https://github.com/Hufe921/canvas-editor/commit/57fdcb8b81079c1fa0836b9092596df986863382)) +* add zone tooltip #367 ([095414f](https://github.com/Hufe921/canvas-editor/commit/095414fbe68bdf8c714dea36d7159a16d6ee9349)), closes [#367](https://github.com/Hufe921/canvas-editor/issues/367) + + +### Performance Improvements + +* compute zone tooltip performance ([28ef4af](https://github.com/Hufe921/canvas-editor/commit/28ef4af15e2a873fac021364449725f78c89348f)) + + + +## [0.9.59](https://github.com/Hufe921/canvas-editor/compare/v0.9.58...v0.9.59) (2023-12-17) + + +### Chores + +* update default font #360 ([8ace079](https://github.com/Hufe921/canvas-editor/commit/8ace07962da4f7e373c361776233330fdd8e4139)), closes [#360](https://github.com/Hufe921/canvas-editor/issues/360) + + +### Features + +* add zone attribute to getControlValue api ([285aeec](https://github.com/Hufe921/canvas-editor/commit/285aeec2f6eeba676361b109e595744c2f1e4641)) +* add resizer size Indicator ([61c110d](https://github.com/Hufe921/canvas-editor/commit/61c110d5eed09197fb2187e6ecec2ee9a10d0d27)) +* set control highlight rule #332 ([b6fe212](https://github.com/Hufe921/canvas-editor/commit/b6fe21230b34afa6ac53eee146e1c291a22da04e)), closes [#332](https://github.com/Hufe921/canvas-editor/issues/332) + + + +## [0.9.58](https://github.com/Hufe921/canvas-editor/compare/v0.9.57...v0.9.58) (2023-12-08) + + +### Bug Fixes + +* empty list don't render placeholder ([910f756](https://github.com/Hufe921/canvas-editor/commit/910f75662f62dc38f068a71feb4cd68150b83341)) +* multiple empty lists render error in first row ([1487033](https://github.com/Hufe921/canvas-editor/commit/1487033d75e7c94c2fdf748217f79642181eaaf1)) +* not render margin indicator in print mode #354 ([3f1babe](https://github.com/Hufe921/canvas-editor/commit/3f1babec68b5d22babade28214ca9a7212d3cf8a)), closes [#354](https://github.com/Hufe921/canvas-editor/issues/354) +* repeated input in firefox browser #357 ([6de3ad8](https://github.com/Hufe921/canvas-editor/commit/6de3ad8b21b557ddc9e218d22b283348dcbd9211)), closes [#357](https://github.com/Hufe921/canvas-editor/issues/357) + + +### Chores + +* export splitText function ([bcbd879](https://github.com/Hufe921/canvas-editor/commit/bcbd879e0f44443f0bcac71402330b37def434b2)) +* update eslint fixAll option ([bba0b09](https://github.com/Hufe921/canvas-editor/commit/bba0b090f33e4f3e391a14e22c2b579f132b5348)) + + +### Features + +* add control indentation option #345 ([5f1cf3a](https://github.com/Hufe921/canvas-editor/commit/5f1cf3ab37aa22180eee45c02486b478dfbc6b99)), closes [#345](https://github.com/Hufe921/canvas-editor/issues/345) + + + +## [0.9.57](https://github.com/Hufe921/canvas-editor/compare/v0.9.56...v0.9.57) (2023-12-03) + + +### Bug Fixes + +* disable focus in readonly mode #326 ([f0823d7](https://github.com/Hufe921/canvas-editor/commit/f0823d7a6b5e975b60affeea5aae626e14162dc7)), closes [#326](https://github.com/Hufe921/canvas-editor/issues/326) +* scaling table and separator elements error #326 ([b3354ac](https://github.com/Hufe921/canvas-editor/commit/b3354ac35ff34031b3dcbeab293b69b7686afdf2)), closes [#326](https://github.com/Hufe921/canvas-editor/issues/326) +* unable to copy elements in control #347 ([6ca1919](https://github.com/Hufe921/canvas-editor/commit/6ca1919498328d3154865469943bde06deb5e465)), closes [#347](https://github.com/Hufe921/canvas-editor/issues/347) +* underline position of superscript and subscript elements is error #268 ([90efe10](https://github.com/Hufe921/canvas-editor/commit/90efe1020fa801c64849b68b15580aa3b505d1cf)), closes [#268](https://github.com/Hufe921/canvas-editor/issues/268) + + +### Chores + +* upgrade cypress version ([ecd4ae9](https://github.com/Hufe921/canvas-editor/commit/ecd4ae9652a9e819bc2a02f2f735d5b5bde9bc71)) + + +### Features + +* add control disabled rule ([1455a2a](https://github.com/Hufe921/canvas-editor/commit/1455a2afb2949b7db10a3cfa30258e0f03bcbf31)) +* add range and position context api ([8acce15](https://github.com/Hufe921/canvas-editor/commit/8acce15e767aa14a80dfe88e596986f3cba9ad63)) +* add set active zone api ([6b30e3c](https://github.com/Hufe921/canvas-editor/commit/6b30e3ca9d48cb45cfe082b55dcf11c8287c36ee)) +* limit the max cursor offsetHeight #348 ([2666bc4](https://github.com/Hufe921/canvas-editor/commit/2666bc43c3e6eca26f51e6317afcb2b02805dad4)), closes [#348](https://github.com/Hufe921/canvas-editor/issues/348) + + + +## [0.9.56](https://github.com/Hufe921/canvas-editor/compare/v0.9.55...v0.9.56) (2023-11-14) + + +### Bug Fixes + +* compute table row and col info boundary error #324 ([455b397](https://github.com/Hufe921/canvas-editor/commit/455b397fbc5de18ffe5a22bc9f9f68e23f9874eb)), closes [#324](https://github.com/Hufe921/canvas-editor/issues/324) +* get and set control property in table #323 ([17cd6cc](https://github.com/Hufe921/canvas-editor/commit/17cd6ccd09e7c368e2ef98c0be2b8de526e8a4c3)), closes [#323](https://github.com/Hufe921/canvas-editor/issues/323) + + + +## [0.9.55](https://github.com/Hufe921/canvas-editor/compare/v0.9.54...v0.9.55) (2023-11-10) + + +### Bug Fixes + +* break after pasting HTML #318 ([80f6531](https://github.com/Hufe921/canvas-editor/commit/80f6531b96e22b434cd10b4441dba86c8944f99b)), closes [#318](https://github.com/Hufe921/canvas-editor/issues/318) +* delete table row boundary error #313 ([8f8bc04](https://github.com/Hufe921/canvas-editor/commit/8f8bc046db60a7c66c3b17e61b1f9f5a5c731f58)), closes [#313](https://github.com/Hufe921/canvas-editor/issues/313) +* reset event ability after delete element #314 ([c6483a4](https://github.com/Hufe921/canvas-editor/commit/c6483a4da68881490cc25c52cafcf96386d9a0a6)), closes [#314](https://github.com/Hufe921/canvas-editor/issues/314) +* shrink control range boundary error #305 ([a9fc226](https://github.com/Hufe921/canvas-editor/commit/a9fc226a39c3ef78d217fe7435b4b463c5879eac)), closes [#305](https://github.com/Hufe921/canvas-editor/issues/305) + + +### Features + +* add pageScaleChange eventbus #321 ([c697586](https://github.com/Hufe921/canvas-editor/commit/c69758686de22328ac84138ed2c2aa9a0668ed78)), closes [#321](https://github.com/Hufe921/canvas-editor/issues/321) +* add scrollContainerSelection option #320 ([192113e](https://github.com/Hufe921/canvas-editor/commit/192113e271b02cf3e4a462343a7b3d5604b90b23)), closes [#320](https://github.com/Hufe921/canvas-editor/issues/320) +* collapsed selection rect information ([7c32f95](https://github.com/Hufe921/canvas-editor/commit/7c32f9572f4d29fbf2f5d6d3f775c5dbe2d0ba8b)) +* support for paste richtext data in contextmenu ([8989831](https://github.com/Hufe921/canvas-editor/commit/8989831474f637fe52133abc85d1ed2dc41f6354)) + + + +## [0.9.54](https://github.com/Hufe921/canvas-editor/compare/v0.9.53...v0.9.54) (2023-11-03) + + +### Bug Fixes + +* clone payload data when call add element api #308 ([aeefca3](https://github.com/Hufe921/canvas-editor/commit/aeefca34caa86f9f52fb742e3f7555cd0364baa4)), closes [#308](https://github.com/Hufe921/canvas-editor/issues/308) +* print error of control assistant components in table #311 ([7fb0150](https://github.com/Hufe921/canvas-editor/commit/7fb0150635f80d62c2c27d2e2c976a3f0fa1deff)), closes [#311](https://github.com/Hufe921/canvas-editor/issues/311) +* set control value error in table #302 ([7fba458](https://github.com/Hufe921/canvas-editor/commit/7fba458d523ae06678557add90dc9d19a3cb02cb)), closes [#302](https://github.com/Hufe921/canvas-editor/issues/302) + + +### Chores + +* update hyperlink spell ([1bde309](https://github.com/Hufe921/canvas-editor/commit/1bde309cb5bcf797416a019f1a1507fd155dbed6)) + + +### Features + +* add copy table cell content option to contextmenu #307 ([a94328f](https://github.com/Hufe921/canvas-editor/commit/a94328fa956c5164111c27773f557b9761cd775e)), closes [#307](https://github.com/Hufe921/canvas-editor/issues/307) +* support for insert more elements into control #306 ([d2d649b](https://github.com/Hufe921/canvas-editor/commit/d2d649b01123a13962b5ba45c2982f80cc701306)), closes [#306](https://github.com/Hufe921/canvas-editor/issues/306) + + + +## [0.9.53](https://github.com/Hufe921/canvas-editor/compare/v0.9.52...v0.9.53) (2023-10-26) + + +### Bug Fixes + +* cannot undo and redo in form mode #301 ([22c69ee](https://github.com/Hufe921/canvas-editor/commit/22c69eec728e392346de179086bd90f81486a8c9)), closes [#301](https://github.com/Hufe921/canvas-editor/issues/301) + + +### Features + +* add control deletable rule #301 ([e5acf6e](https://github.com/Hufe921/canvas-editor/commit/e5acf6efcbb719790770978656f01d8597f6a1e8)), closes [#301](https://github.com/Hufe921/canvas-editor/issues/301) +* add executeBlur api #262 ([d9f7d50](https://github.com/Hufe921/canvas-editor/commit/d9f7d5045f956f0c184457562d7d0cbd8bf6033d)), closes [#262](https://github.com/Hufe921/canvas-editor/issues/262) +* add modify internal context menu interface #300 ([0891f05](https://github.com/Hufe921/canvas-editor/commit/0891f053d2279e0db816118e073be9a1e268460a)), closes [#300](https://github.com/Hufe921/canvas-editor/issues/300) +* add override internal copy function api ([45e7eab](https://github.com/Hufe921/canvas-editor/commit/45e7eab119a303482dde317298478fdc41536294)) +* add set control extension value api #293 ([096778d](https://github.com/Hufe921/canvas-editor/commit/096778d37c8176bb70c2d7eb4534138b4af5a712)), closes [#293](https://github.com/Hufe921/canvas-editor/issues/293) + + +### Performance Improvements + +* set select control value style #298 ([f4d7554](https://github.com/Hufe921/canvas-editor/commit/f4d75544d0a063a05a06bc5dbac74fd4d176eb5f)), closes [#298](https://github.com/Hufe921/canvas-editor/issues/298) + + + +## [0.9.52](https://github.com/Hufe921/canvas-editor/compare/v0.9.51...v0.9.52) (2023-10-12) + + +### Bug Fixes + +* bounding rect error in getRangeContext api ([06c3a33](https://github.com/Hufe921/canvas-editor/commit/06c3a337aeb1eccf6e7182040e6db1acadc9aee9)) +* set range style when on double click ([6f2fb5d](https://github.com/Hufe921/canvas-editor/commit/6f2fb5de0971fc98a585283f5676aed4dcdae1cc)) + + +### Features + +* add extend attribute to control element #293 ([0395a72](https://github.com/Hufe921/canvas-editor/commit/0395a72fe6464a78883630abac659aafac11d723)), closes [#293](https://github.com/Hufe921/canvas-editor/issues/293) +* add getContainer api ([c944872](https://github.com/Hufe921/canvas-editor/commit/c944872ae7ccc273508884f103ca0fe1711c5d2c)) + + + +## [0.9.51](https://github.com/Hufe921/canvas-editor/compare/v0.9.50...v0.9.51) (2023-10-10) + + +### Features + +* add bounding rect to getRangeContext api ([319da3f](https://github.com/Hufe921/canvas-editor/commit/319da3fca16da7cbfd8a4d6ec7f54079a8f0f38e)) +* add table cell slash to contextmenu ([d540195](https://github.com/Hufe921/canvas-editor/commit/d540195dfaa80da05e9d8e2dd4b28c87ab312655)) + + +### Performance Improvements + +* contextmenu boundary position ([1ce2e2f](https://github.com/Hufe921/canvas-editor/commit/1ce2e2f44f99f7e409fba38648bd76806aa7042e)) + + + +## [0.9.50](https://github.com/Hufe921/canvas-editor/compare/v0.9.49...v0.9.50) (2023-09-28) + + +### Bug Fixes + +* remove block element last line break #287 ([0e67395](https://github.com/Hufe921/canvas-editor/commit/0e6739522d022775e39520a3cb9396531a57bf17)), closes [#287](https://github.com/Hufe921/canvas-editor/issues/287) + + +### Chores + +* update README.md ([5ad1414](https://github.com/Hufe921/canvas-editor/commit/5ad1414d82b88bccdb1779c550b75db28dfc18e7)) + + +### Documentation + +* add plugin tips ([acdd107](https://github.com/Hufe921/canvas-editor/commit/acdd1075449a38270e3fe6fd0c9eb75f21c433cb)) +* move the cursor shortcut ([33ebc59](https://github.com/Hufe921/canvas-editor/commit/33ebc59c721cffd54c14c51ed5d6174226c8a33b)) + + +### Features + +* move the cursor the entire word #281 ([b38e4ed](https://github.com/Hufe921/canvas-editor/commit/b38e4ed0ddaa020c0dcbb0336efa32cc605e281c)), closes [#281](https://github.com/Hufe921/canvas-editor/issues/281) +* paper background color option ([46be700](https://github.com/Hufe921/canvas-editor/commit/46be70072852e6cb2c827d960734b54880fc0681)) +* support for table cell slash #290 ([4269628](https://github.com/Hufe921/canvas-editor/commit/4269628cc46e7f9fdf0a9832601b2cf07e429aec)), closes [#290](https://github.com/Hufe921/canvas-editor/issues/290) +* typing on ios devices #286 ([8cf2b19](https://github.com/Hufe921/canvas-editor/commit/8cf2b19d3db2b48b840641c05156d1b3a0297b2b)), closes [#286](https://github.com/Hufe921/canvas-editor/issues/286) + + + +## [0.9.49](https://github.com/Hufe921/canvas-editor/compare/v0.9.48...v0.9.49) (2023-09-16) + + +### Bug Fixes + +* control minimum width when scaling ([05ddc2d](https://github.com/Hufe921/canvas-editor/commit/05ddc2db290e58a4ec0fb1b00ad5d64ce7f4cf3a)) +* draw text element letter space error #282 ([c35f8ab](https://github.com/Hufe921/canvas-editor/commit/c35f8ab82c57849269a09fbad9d54d5085065d22)), closes [#282](https://github.com/Hufe921/canvas-editor/issues/282) +* omitObject function missing reference ([c45317e](https://github.com/Hufe921/canvas-editor/commit/c45317eced93e3d79129aea24776a8629d058050)) + + +### Features + +* add set and get control value api #278 ([f754741](https://github.com/Hufe921/canvas-editor/commit/f754741f32de6c5d5c27d15dfc9fc31e284d29dc)), closes [#278](https://github.com/Hufe921/canvas-editor/issues/278) +* text element width #277 ([bb64626](https://github.com/Hufe921/canvas-editor/commit/bb646266b10897c3097ada5932f9b7cef317aebe)), closes [#277](https://github.com/Hufe921/canvas-editor/issues/277) + + +### Performance Improvements + +* adjusted the tab draw in the list style #283 ([fc0fdb2](https://github.com/Hufe921/canvas-editor/commit/fc0fdb2fe36966e3b8a51107d9929ca137e5681c)), closes [#283](https://github.com/Hufe921/canvas-editor/issues/283) + + + +## [0.9.48](https://github.com/Hufe921/canvas-editor/compare/v0.9.47...v0.9.48) (2023-09-09) + + +### Bug Fixes + +* control minimum width boundary ([05caccc](https://github.com/Hufe921/canvas-editor/commit/05caccc74c7d723f4ac357d161acf45364368f4b)) + + +### Features + +* add control minimum width option ([4b2bbfb](https://github.com/Hufe921/canvas-editor/commit/4b2bbfbe9a9bfe0fcad276dc16eb01b7015e6205)) +* custom letter class #279 ([de76977](https://github.com/Hufe921/canvas-editor/commit/de769778c04266ed08c5721a01670cdbf0992d2b)), closes [#279](https://github.com/Hufe921/canvas-editor/issues/279) + + + +## [0.9.47](https://github.com/Hufe921/canvas-editor/compare/v0.9.46...v0.9.47) (2023-09-02) + + +### Bug Fixes + +* format control element default options ([7b07cf3](https://github.com/Hufe921/canvas-editor/commit/7b07cf3eb4d62d867e779b7a1be3fb72fdb7c71a)) +* insert tab element with context #265 ([b7a0df8](https://github.com/Hufe921/canvas-editor/commit/b7a0df8b1c7af20d53ed0b22d6f33159aa28e33a)), closes [#265](https://github.com/Hufe921/canvas-editor/issues/265) +* table selection boundary error ([7260b64](https://github.com/Hufe921/canvas-editor/commit/7260b6439bd36f961f545c43e1536b3538dd586a)) + + +### Features + +* add forceUpdate api #263 ([bc2f445](https://github.com/Hufe921/canvas-editor/commit/bc2f44547207e74c531ca62d17f26b1496eb9387)), closes [#263](https://github.com/Hufe921/canvas-editor/issues/263) +* add getOptions api ([761fcde](https://github.com/Hufe921/canvas-editor/commit/761fcde91aabe8d90d13ff07e0c1d26798d1edba)) +* add override internal function api #260 ([abcaa9b](https://github.com/Hufe921/canvas-editor/commit/abcaa9b51c7d703b18e3141105555811988eebdd)), closes [#260](https://github.com/Hufe921/canvas-editor/issues/260) +* add page break option ([ec627dc](https://github.com/Hufe921/canvas-editor/commit/ec627dc407f419641d8e4436eb984bef0ede77c9)) +* add shortcut disable option ([640f262](https://github.com/Hufe921/canvas-editor/commit/640f26292f28c309a1b11b51965938d7a5ab40fe)) +* add table td border type ([d8876b1](https://github.com/Hufe921/canvas-editor/commit/d8876b1095f7388700a3155c7af7e7e3ff5da11b)) +* copy entire line when cursor is inside ([3c10be2](https://github.com/Hufe921/canvas-editor/commit/3c10be26fb58aa49a340aa72f17e9ff8d1c15e15)) + + +### Refactor + +* update tdPadding option format ([752ca43](https://github.com/Hufe921/canvas-editor/commit/752ca43077b4092ba15edf4f650b5e35af8bfa8a)) + + + +## [0.9.46](https://github.com/Hufe921/canvas-editor/compare/v0.9.45...v0.9.46) (2023-08-25) + + +### Bug Fixes + +* disable paste in read only mode #260 ([f19077b](https://github.com/Hufe921/canvas-editor/commit/f19077b2ce2ed8450257c29a12c77ec627cdd866)), closes [#260](https://github.com/Hufe921/canvas-editor/issues/260) +* drawing background size error #254 ([01340bb](https://github.com/Hufe921/canvas-editor/commit/01340bb13ce31e51abaac150e5294b7b10e64005)), closes [#254](https://github.com/Hufe921/canvas-editor/issues/254) +* error converting some element types to HTML #257 ([a805590](https://github.com/Hufe921/canvas-editor/commit/a805590696bed12c0b4ececdc7de222f629a8cc9)), closes [#257](https://github.com/Hufe921/canvas-editor/issues/257) + + +### Features + +* add comment demo #238 ([86cdcf3](https://github.com/Hufe921/canvas-editor/commit/86cdcf3481ffcaf4c1acee729d0f7aeb1eaf5dee)), closes [#238](https://github.com/Hufe921/canvas-editor/issues/238) +* add element group ([3e183ae](https://github.com/Hufe921/canvas-editor/commit/3e183aefa574268658760eab202e1188cd5651ed)) +* add zone field to contextmenu context ([2064236](https://github.com/Hufe921/canvas-editor/commit/2064236d94258dc97bb5decd14ac309288e35db9)) +* get range row and paragraph element list #255 ([9495bfe](https://github.com/Hufe921/canvas-editor/commit/9495bfef397a2d9cb39a674d637d85c05bec2382)), closes [#255](https://github.com/Hufe921/canvas-editor/issues/255) + + + +## [0.9.45](https://github.com/Hufe921/canvas-editor/compare/v0.9.44...v0.9.45) (2023-08-18) + + +### Bug Fixes + +* merge table cell error #241 ([1f552a0](https://github.com/Hufe921/canvas-editor/commit/1f552a05d588da1a661cbc000f36253f8894377c)), closes [#241](https://github.com/Hufe921/canvas-editor/issues/241) +* reduce underline distance #247 ([33bafbd](https://github.com/Hufe921/canvas-editor/commit/33bafbd427d5e80db059886c4b116e4ac97d3cf8)), closes [#247](https://github.com/Hufe921/canvas-editor/issues/247) + + +### Features + +* add getLocale api #248 ([fef6ddf](https://github.com/Hufe921/canvas-editor/commit/fef6ddf7a84f3f40ccf5424b37dfb48b805af7bb)), closes [#248](https://github.com/Hufe921/canvas-editor/issues/248) +* support for inserting for surrogate pair #250 ([8f145e2](https://github.com/Hufe921/canvas-editor/commit/8f145e251b2cfb8ffc14aec66cebda29a6368061)), closes [#250](https://github.com/Hufe921/canvas-editor/issues/250) +* support for inserting standard emoji #245 ([913b853](https://github.com/Hufe921/canvas-editor/commit/913b8538b709aa919cb33411dd24924c03effa81)), closes [#245](https://github.com/Hufe921/canvas-editor/issues/245) +* update emoji regex ([a4f5c94](https://github.com/Hufe921/canvas-editor/commit/a4f5c94c96c6edc4f32049081e8fd6bb563ba708)) + + + +## [0.9.44](https://github.com/Hufe921/canvas-editor/compare/v0.9.43...v0.9.44) (2023-08-11) + + +### Features + +* add getText api #240 ([f8fdea6](https://github.com/Hufe921/canvas-editor/commit/f8fdea61fcd45d4d0be143b7c889eba9a72dd35b)), closes [#240](https://github.com/Hufe921/canvas-editor/issues/240) +* add print mode #236 ([fd31b3e](https://github.com/Hufe921/canvas-editor/commit/fd31b3e78c2bbd6701655b060ea50e3c4f070a65)), closes [#236](https://github.com/Hufe921/canvas-editor/issues/236) +* apply style to entire table #232 ([b54b66d](https://github.com/Hufe921/canvas-editor/commit/b54b66d3d4a9e7366cef02add9b897770bac39c5)), closes [#232](https://github.com/Hufe921/canvas-editor/issues/232) + + + +## [0.9.43](https://github.com/Hufe921/canvas-editor/compare/v0.9.42...v0.9.43) (2023-08-04) + + +### Bug Fixes + +* cursor navigation across pages #229 ([a96a77a](https://github.com/Hufe921/canvas-editor/commit/a96a77a237a62f0881a2b106b040f29c840fff58)), closes [#229](https://github.com/Hufe921/canvas-editor/issues/229) + + +### Features + +* add form mode #221 ([94247c3](https://github.com/Hufe921/canvas-editor/commit/94247c324be8bd3688f4098ab9520fc563d20ded)), closes [#221](https://github.com/Hufe921/canvas-editor/issues/221) +* cursor following page scrolling #229 ([3db28cc](https://github.com/Hufe921/canvas-editor/commit/3db28cc04f84af502901da51a14e3f63c8a36964)), closes [#229](https://github.com/Hufe921/canvas-editor/issues/229) + + + +## [0.9.42](https://github.com/Hufe921/canvas-editor/compare/v0.9.41...v0.9.42) (2023-07-31) + + +### Bug Fixes + +* contentChange call error during initialization #224 ([1b25afb](https://github.com/Hufe921/canvas-editor/commit/1b25afb664b6feff8a0e96dda53a0a3b252663c7)), closes [#224](https://github.com/Hufe921/canvas-editor/issues/224) +* control value style not affected by prefix #227 ([cf5dd35](https://github.com/Hufe921/canvas-editor/commit/cf5dd356869ac272dd8d3a1948ff5f4c6536b23d)), closes [#227](https://github.com/Hufe921/canvas-editor/issues/227) +* limit word break element type ([73014dc](https://github.com/Hufe921/canvas-editor/commit/73014dc87be5360ab8f09e211a12b06dfcbb77e2)) +* set header and footer data error #224 ([b22f0b4](https://github.com/Hufe921/canvas-editor/commit/b22f0b45418b76ba008a2f6200bf8210f3c6f0dd)), closes [#224](https://github.com/Hufe921/canvas-editor/issues/224) + + +### Features + +* add SetHTML api ([52f7500](https://github.com/Hufe921/canvas-editor/commit/52f7500ae3e27b1746af423dcbf1faffc9134948)) + + + +## [0.9.41](https://github.com/Hufe921/canvas-editor/compare/v0.9.40...v0.9.41) (2023-07-27) + + +### Chores + +* add id to style element #219 ([166ff7f](https://github.com/Hufe921/canvas-editor/commit/166ff7f3f90c515487d63c06aca5a1e528a5f51a)), closes [#219](https://github.com/Hufe921/canvas-editor/issues/219) + + +### Documentation + +* support document internationalization #222 ([9f80168](https://github.com/Hufe921/canvas-editor/commit/9f8016884f56431fd29a77241bcc69368ffdbb37)), closes [#222](https://github.com/Hufe921/canvas-editor/issues/222) [#213](https://github.com/Hufe921/canvas-editor/issues/213) [#222](https://github.com/Hufe921/canvas-editor/issues/222) + + +### Features + +* add event bus ([0bacc11](https://github.com/Hufe921/canvas-editor/commit/0bacc113cdf8eec3dd01e4dc89937f2538f65e19)) +* add getHTML api #218 ([b12c6cc](https://github.com/Hufe921/canvas-editor/commit/b12c6cc4282d5777118c9d2e177f0198af048989)), closes [#218](https://github.com/Hufe921/canvas-editor/issues/218) +* clear contextmenu side effect in web component #219 ([fc356c7](https://github.com/Hufe921/canvas-editor/commit/fc356c7eb66b92fc418838f218ac45f383525573)), closes [#219](https://github.com/Hufe921/canvas-editor/issues/219) +* clear side effect in web component #219 ([ce70f0d](https://github.com/Hufe921/canvas-editor/commit/ce70f0d7e8cd4f997191c88dd4462b6903f09104)), closes [#219](https://github.com/Hufe921/canvas-editor/issues/219) + + + +## [0.9.40](https://github.com/Hufe921/canvas-editor/compare/v0.9.39...v0.9.40) (2023-07-21) + + +### Bug Fixes + +* disable partial contextmenu in readonly mode ([3f03d88](https://github.com/Hufe921/canvas-editor/commit/3f03d88443c2d5c746ba3c77f6e5fc69464c7c3c)) + + +### Performance Improvements + +* cursor drawing when page visible ([9c2bd33](https://github.com/Hufe921/canvas-editor/commit/9c2bd33b7a3cd7b557723e9c18870dcd8d7bba6b)) +* cursor position at the beginning of a line ([1bd2e45](https://github.com/Hufe921/canvas-editor/commit/1bd2e455a5f5c0bd53cb51d30e8205cd6a148e7c)) +* print quality #185 ([842b4fc](https://github.com/Hufe921/canvas-editor/commit/842b4fca0723bf764c8fb05f04a5511c443169db)), closes [#185](https://github.com/Hufe921/canvas-editor/issues/185) + + +### Refactor + +* add prettier and format ([d464c50](https://github.com/Hufe921/canvas-editor/commit/d464c5043508c63b29174ae05bc37ec66c87d45f)) + + + +## [0.9.39](https://github.com/Hufe921/canvas-editor/compare/v0.9.38...v0.9.39) (2023-07-14) + + +### Documentation + +* add clean mode remark #214 ([abcc241](https://github.com/Hufe921/canvas-editor/commit/abcc2417966dfa6a43246e3b33dfc39fc65595be)), closes [#214](https://github.com/Hufe921/canvas-editor/issues/214) + + +### Features + +* add table row and col size option #214 ([8d1100c](https://github.com/Hufe921/canvas-editor/commit/8d1100cdb1646bf8822112993c00881cf9e39a5e)), closes [#214](https://github.com/Hufe921/canvas-editor/issues/214) +* get range context info ([09c4d53](https://github.com/Hufe921/canvas-editor/commit/09c4d53bca9744b115b5e7c94a42ebe81da487b8)) + + + +## [0.9.38](https://github.com/Hufe921/canvas-editor/compare/v0.9.37...v0.9.38) (2023-07-12) + + +### Bug Fixes + +* limit word break element type #212 ([d7424f8](https://github.com/Hufe921/canvas-editor/commit/d7424f8b8798cc889df3b54748523c1367af2776)), closes [#212](https://github.com/Hufe921/canvas-editor/issues/212) + + +### Chores + +* add plugin tip ([8c6eee1](https://github.com/Hufe921/canvas-editor/commit/8c6eee1a57f5130919d1bdfa6a1067615f3a95e3)) +* update README.md ([ccb1aa7](https://github.com/Hufe921/canvas-editor/commit/ccb1aa70abacd9398f9ff0e5b066ad51cfc27274)) +* update release script ([4d1ad65](https://github.com/Hufe921/canvas-editor/commit/4d1ad650746ff2c4aa58dde2b725704ab9e7a639)) + + +### Features + +* add word break option #212 ([d471165](https://github.com/Hufe921/canvas-editor/commit/d471165430e27e5322160e22da5a5f53fc969b0f)), closes [#212](https://github.com/Hufe921/canvas-editor/issues/212) +* get page value and append element api #211 ([85a9dcb](https://github.com/Hufe921/canvas-editor/commit/85a9dcbcf29a2fc0e1f89293c4f3ecbc976868b7)), closes [#211](https://github.com/Hufe921/canvas-editor/issues/211) + + + +## [0.9.37](https://github.com/Hufe921/canvas-editor/compare/v0.9.36...v0.9.37) (2023-07-02) + + +### Chores + +* add plugin demo ([107c4b8](https://github.com/Hufe921/canvas-editor/commit/107c4b86a8703bd1ab01ab7fc9d3cc8a1078591d)) +* update next features roadmap ([ca28454](https://github.com/Hufe921/canvas-editor/commit/ca284540493b985081c86623b569eeef4bf9dcdc)) + + +### Documentation + +* add plugin ([d0e1c9b](https://github.com/Hufe921/canvas-editor/commit/d0e1c9b5267e15c0afa94bbd400502735846050b)) + + +### Features + +* add fallback placeholder image ([366374e](https://github.com/Hufe921/canvas-editor/commit/366374eb105a49360f7ddecfdb63420e48254698)) +* add plugin interface ([ad0bb32](https://github.com/Hufe921/canvas-editor/commit/ad0bb32b70cd2a7b8cf2c25a83091e92cc13f53b)) +* add setValue command api #210 ([193bd21](https://github.com/Hufe921/canvas-editor/commit/193bd21f7049a565abddd3ae9be761d95d49fea1)), closes [#210](https://github.com/Hufe921/canvas-editor/issues/210) +* smooth signature drawing ([c328778](https://github.com/Hufe921/canvas-editor/commit/c3287783abfb27670e3572db8c431f23cc04c6ce)) + + + +## [0.9.36](https://github.com/Hufe921/canvas-editor/compare/v0.9.35...v0.9.36) (2023-06-16) + + +### Bug Fixes + +* close toolbar menu when click outside #192 ([9c39c54](https://github.com/Hufe921/canvas-editor/commit/9c39c540ff4a2a2f3806f8cea6b053bc6f2e7279)), closes [#192](https://github.com/Hufe921/canvas-editor/issues/192) +* copy highlight element #193 ([88ebfd2](https://github.com/Hufe921/canvas-editor/commit/88ebfd2f74b4cb1f3686478d0bba6a8231020ccb)), closes [#193](https://github.com/Hufe921/canvas-editor/issues/193) +* inability to select list pasted into table #206 ([53dd962](https://github.com/Hufe921/canvas-editor/commit/53dd9628caa8de064f6e87bef905113947273d84)), closes [#206](https://github.com/Hufe921/canvas-editor/issues/206) +* multiple editor instances conflict #205 ([68bea13](https://github.com/Hufe921/canvas-editor/commit/68bea13333a2ea10ec537700a8fd6e9295d963fb)), closes [#205](https://github.com/Hufe921/canvas-editor/issues/205) +* not allow change zone in continuous mode #201 ([16c2e9a](https://github.com/Hufe921/canvas-editor/commit/16c2e9a6d7d325aa33a8e8b5aec518f9e9d3861a)), closes [#201](https://github.com/Hufe921/canvas-editor/issues/201) +* prevent page auto scroll when no selection #204 ([183e644](https://github.com/Hufe921/canvas-editor/commit/183e644089f6ec471a9ad26a037873e4b132911d)), closes [#204](https://github.com/Hufe921/canvas-editor/issues/204) +* remove header and footer in continuous mode ([b92bd40](https://github.com/Hufe921/canvas-editor/commit/b92bd407ddc64e5663c24a63a1fb7dadd15fbf58)) + + +### Chores + +* update mock data ([2120780](https://github.com/Hufe921/canvas-editor/commit/21207808a3987136f18f3a49b55b909facce54b9)) +* update README.md ([7594942](https://github.com/Hufe921/canvas-editor/commit/759494272a64129533f6f74da2ea8760657f0044)) + + +### Documentation + +* add editor placeholder ([3a371e6](https://github.com/Hufe921/canvas-editor/commit/3a371e6ee312b7bff8bf8091a42a7ce5df1623bb)) +* add word tool ([e4ea580](https://github.com/Hufe921/canvas-editor/commit/e4ea580263443cbc9cb978272d11bf35c8423d11)) + + +### Features + +* add editor placeholder ([5ded5c3](https://github.com/Hufe921/canvas-editor/commit/5ded5c3428fc70e6074d9c9101ba34ef7eeb7d19)) +* add history max record option #203 ([c467505](https://github.com/Hufe921/canvas-editor/commit/c46750557ad14ec2625d8aaef8a5ae08eb10ac96)), closes [#203](https://github.com/Hufe921/canvas-editor/issues/203) +* add word tool ([2a3c2e2](https://github.com/Hufe921/canvas-editor/commit/2a3c2e2ec674beed8b20f28764e4d1bb636ce83c)) + + +### Performance Improvements + +* cursor selection at the beginning of a line #202 ([a133585](https://github.com/Hufe921/canvas-editor/commit/a1335850a3f0ce1b90a2bf13ad8532f0ccf06cff)), closes [#202](https://github.com/Hufe921/canvas-editor/issues/202) +* range style callback and inactive cursor style #204 ([7628eee](https://github.com/Hufe921/canvas-editor/commit/7628eee283da7236961f61c48c42625442ae02ed)), closes [#204](https://github.com/Hufe921/canvas-editor/issues/204) + + + +## [0.9.35](https://github.com/Hufe921/canvas-editor/compare/v0.9.34...v0.9.35) (2023-05-31) + + +### Chores + +* add CRDT CSpell word ([ef31552](https://github.com/Hufe921/canvas-editor/commit/ef315526be244c210b2a2a220dd3bed986660d75)) + + +### Documentation + +* starting page number option ([618cb47](https://github.com/Hufe921/canvas-editor/commit/618cb47f77f2aed05d4ca72135ea5312b6d26b00)) +* table cell background color ([9225bef](https://github.com/Hufe921/canvas-editor/commit/9225bef99192d8c4848536d973bb2467ea205f27)) +* update next features ([c6ea0a6](https://github.com/Hufe921/canvas-editor/commit/c6ea0a655c997645cda6df7ced4377707973680f)) + + +### Features + +* copy and paste sub and sup elements ([a500402](https://github.com/Hufe921/canvas-editor/commit/a500402dd401c6eac55222a366f5fc596c27d7c3)) +* copy and paste table cell background color ([c97c6ef](https://github.com/Hufe921/canvas-editor/commit/c97c6eff4047a91e68079cc9b228f41e02bef1ab)) +* optimize paste title ([bf52e25](https://github.com/Hufe921/canvas-editor/commit/bf52e2588b3af426382334d45d16d1a33510fde1)) +* starting page number option ([bfc61a8](https://github.com/Hufe921/canvas-editor/commit/bfc61a8a06b6c2c09fd968767f2a6ccbe280fe55)) +* table cell background color ([dbcab3b](https://github.com/Hufe921/canvas-editor/commit/dbcab3b48557f5a4ba77b9e6ec40b7ac8d36cc40)) +* unordered list default style ([c8b2a7e](https://github.com/Hufe921/canvas-editor/commit/c8b2a7e59fdd97ac268b0a38e80bac98dfd797ce)) + + + +## [0.9.34](https://github.com/Hufe921/canvas-editor/compare/v0.9.33...v0.9.34) (2023-05-16) + + +### Documentation + +* get catalog api ([c2cc8d9](https://github.com/Hufe921/canvas-editor/commit/c2cc8d98d29a472ccbe35aa39893e1dfc8df74e0)) +* location catalog api ([402e448](https://github.com/Hufe921/canvas-editor/commit/402e448559a5e2a1d2dd6399889758172651bcef)) + + +### Features + +* add catalog demo ([9343afe](https://github.com/Hufe921/canvas-editor/commit/9343afe83136b4e1a23bad813adb5f9cf813604a)) +* get catalog api ([237c0f2](https://github.com/Hufe921/canvas-editor/commit/237c0f22cbd26ac737187b8a195bc08665dcc450)) +* location catalog api ([535562e](https://github.com/Hufe921/canvas-editor/commit/535562e396ce150916456a0ed791edc4ff208de4)) + + +### Performance Improvements + +* optimize cursor blink ([7ad4ba3](https://github.com/Hufe921/canvas-editor/commit/7ad4ba3a2664744bbcee200775028a009fecd5b4)) + + + +## [0.9.33](https://github.com/Hufe921/canvas-editor/compare/v0.9.32...v0.9.33) (2023-05-02) + + +### Bug Fixes + +* get range paragraph boundary error ([84b236f](https://github.com/Hufe921/canvas-editor/commit/84b236fe224e48002f7991abefda0c9c646447e2)) +* some IME position error #184 ([c5699bc](https://github.com/Hufe921/canvas-editor/commit/c5699bcb4cad9c9dcd7dad9eba00e0dd6d60cf3a)), closes [#184](https://github.com/Hufe921/canvas-editor/issues/184) + + +### Documentation + +* update list schema and shortcut ([98ea30e](https://github.com/Hufe921/canvas-editor/commit/98ea30e203003f5336911a4cb52d5bce6b8e08e5)) + + +### Features + +* wrap within list item ([69750a1](https://github.com/Hufe921/canvas-editor/commit/69750a115a5adcc5ab61c69846fcde402d1019b3)) + + + +## [0.9.32](https://github.com/Hufe921/canvas-editor/compare/v0.9.31...v0.9.32) (2023-04-26) + + +### Bug Fixes + +* not wrap when exceeding container width #177 ([e8f61d9](https://github.com/Hufe921/canvas-editor/commit/e8f61d9d15c1aa5e3179e3565f956382dca9cace)), closes [#177](https://github.com/Hufe921/canvas-editor/issues/177) +* delete list element boundary error ([9a37179](https://github.com/Hufe921/canvas-editor/commit/9a37179c748af63d652735f0ad9e4c5dd65f7e23)) +* error when selecting table cells #174 ([f0b6014](https://github.com/Hufe921/canvas-editor/commit/f0b6014daa4ac3ac1d88acb941aa92e183eef6fa)), closes [#174](https://github.com/Hufe921/canvas-editor/issues/174) +* header and footer compute position list error ([3b66b26](https://github.com/Hufe921/canvas-editor/commit/3b66b26c85c59468a5a5c0bfeed39d7bef793239)) +* image element row margin error ([3daacc6](https://github.com/Hufe921/canvas-editor/commit/3daacc6cb80506aefd25069f8960766b85e2d88a)) +* inline image ascent value ([59065bb](https://github.com/Hufe921/canvas-editor/commit/59065bb29e9e7e85cca83eb6dd5ccdd99533a183)) +* paste and format element boundary error ([86569f5](https://github.com/Hufe921/canvas-editor/commit/86569f51d3a32a7c148dbd6f06b5734a98836e68)) +* paste list element boundary error ([5935eb7](https://github.com/Hufe921/canvas-editor/commit/5935eb77d04824c60a4e1ffe6149097045188336)) +* set paper size error #181 ([10ada8c](https://github.com/Hufe921/canvas-editor/commit/10ada8cf542e8b8f5ddd9e619637c4d22d66f0b6)), closes [#181](https://github.com/Hufe921/canvas-editor/issues/181) +* tslint error ([8202c1c](https://github.com/Hufe921/canvas-editor/commit/8202c1c67b2ff18feee09223f3f2eea4c8fc7bac)) +* unset list error ([c02a96c](https://github.com/Hufe921/canvas-editor/commit/c02a96cb8a24514f8d3aa8386d915e5e9e42050f)) + + +### Chores + +* add git pre commit hook ([ef9ee07](https://github.com/Hufe921/canvas-editor/commit/ef9ee07a55b17111011c970adc5f67ee02a93eff)) + + +### Documentation + +* add list command ([a5b5f87](https://github.com/Hufe921/canvas-editor/commit/a5b5f8785d598ee632b3fb14ce2bcff93a497ce9)) +* update schema, shortcut, option ([2f64395](https://github.com/Hufe921/canvas-editor/commit/2f64395cbdd6b0d4ab04bf40c2e113af7a0fa4c5)) + + +### Features + +* adaptive list style during page scaling ([e53c0c5](https://github.com/Hufe921/canvas-editor/commit/e53c0c5d3b1f6e83482ed470d09bb6851600e368)) +* add list and title shortcuts ([bb28755](https://github.com/Hufe921/canvas-editor/commit/bb2875516c1339d334068462905e96f28895da6f)) +* add list element ([c2330a8](https://github.com/Hufe921/canvas-editor/commit/c2330a8c11d83568f406cbecc8dbad2396d0df40)) +* enable keyboard event when image resizer (#179) ([fb78f0a](https://github.com/Hufe921/canvas-editor/commit/fb78f0a0aa4e19e156a09dc530e84bf86e5b861a)), closes [#179](https://github.com/Hufe921/canvas-editor/issues/179) +* handle boundary when dragging elements ([8fba929](https://github.com/Hufe921/canvas-editor/commit/8fba929fb8c0567e195ca9ecdd90f9d677c0aa3e)) +* handle list boundary ([406fca3](https://github.com/Hufe921/canvas-editor/commit/406fca359034991fbde568ee3a6f981dac3f73d1)) +* header,footer,page number disabled option #180 ([797b9a1](https://github.com/Hufe921/canvas-editor/commit/797b9a14dffa5e3fcdff8bbc5dd9ebc41cc5c7c8)), closes [#180](https://github.com/Hufe921/canvas-editor/issues/180) +* insert table in list element ([3ec7d71](https://github.com/Hufe921/canvas-editor/commit/3ec7d71f2e597841fe063cdd955c36a9d0187339)) +* recursion format element context ([9f84285](https://github.com/Hufe921/canvas-editor/commit/9f8428576302a00d182aa3c469def0b6d3d0e708)) +* set title at paragraph level ([8a56a49](https://github.com/Hufe921/canvas-editor/commit/8a56a49a0ae23fc59d2edd9ec7894756d36542c7)) + + + +## [0.9.31](https://github.com/Hufe921/canvas-editor/compare/v0.9.30...v0.9.31) (2023-04-07) + + +### Bug Fixes + +* lose line break when set title ([722a910](https://github.com/Hufe921/canvas-editor/commit/722a91014508d9a8d65a30ab7d71b23924fa9b91)) + + +### Performance Improvements + +* range style anchor element ([d9eec5b](https://github.com/Hufe921/canvas-editor/commit/d9eec5b9be6cbb545c57cf8816b197a214c70f3e)) + + + +## [0.9.30](https://github.com/Hufe921/canvas-editor/compare/v0.9.29...v0.9.30) (2023-04-07) + + +### Bug Fixes + +* set defaultTrMinHeight option invalid #168 ([045e2ff](https://github.com/Hufe921/canvas-editor/commit/045e2ffe4ece76d46a8a579fad267f14a6778b1e)), closes [#168](https://github.com/Hufe921/canvas-editor/issues/168) + + +### Chores + +* add image accept values ([189ca73](https://github.com/Hufe921/canvas-editor/commit/189ca73c3213cb9b76fa84df400f9b3624ec0d42)) + + +### Documentation + +* add page number format option ([72e97b7](https://github.com/Hufe921/canvas-editor/commit/72e97b7883f0f84961afd7ed93b740d1915f731c)) +* add title api and option ([a9b4438](https://github.com/Hufe921/canvas-editor/commit/a9b44387bd174cbd1bab8c308bb732f68800ef34)) +* add zone change listener ([3cba30b](https://github.com/Hufe921/canvas-editor/commit/3cba30bcf969a9b777885a4ca6daca3019afd9d0)) +* improve editor options ([51d4a03](https://github.com/Hufe921/canvas-editor/commit/51d4a036a0e1c99e48a02b326ab459319536ab7b)) + + +### Features + +* add page number format option ([4987723](https://github.com/Hufe921/canvas-editor/commit/4987723f27684a3f4ba4e9952b75c16926f4a07b)) +* add title element ([9701b21](https://github.com/Hufe921/canvas-editor/commit/9701b2153e4de71697776765081f39bbfda82eb7)) +* add zone change listener ([86871c3](https://github.com/Hufe921/canvas-editor/commit/86871c310be1402f03831ca182698e7ba78a7912)) +* format title element value ([1fc276f](https://github.com/Hufe921/canvas-editor/commit/1fc276fbae6a0dd7c8cff39a5c5ca6aa7d7f47ed)) + + +### Performance Improvements + +* copy title and table element ([03cd85f](https://github.com/Hufe921/canvas-editor/commit/03cd85f423d207eea7f88c4267552f4af6945030)) + + +### Tests + +* add title test case ([c275216](https://github.com/Hufe921/canvas-editor/commit/c275216fb4626e811d702c7f72f1e987823c4787)) + + + +## [0.9.29](https://github.com/Hufe921/canvas-editor/compare/v0.9.28...v0.9.29) (2023-04-01) + + +### Bug Fixes + +* delete rowFlex when row position change #164 ([5c3ce57](https://github.com/Hufe921/canvas-editor/commit/5c3ce57c24253cfe9b16ddc682f0f75aaab653fd)), closes [#164](https://github.com/Hufe921/canvas-editor/issues/164) +* failed to execute 'toDataURL' #163 ([f11d5c8](https://github.com/Hufe921/canvas-editor/commit/f11d5c806ea5cc308b59df4eaa58bf82998a3c50)), closes [#163](https://github.com/Hufe921/canvas-editor/issues/163) +* render composing text error ([310e0e9](https://github.com/Hufe921/canvas-editor/commit/310e0e91fbe4ef64343b5cc746ec4e7d471df974)) +* table cell text render position error #166 ([266915a](https://github.com/Hufe921/canvas-editor/commit/266915af09be90a1a12092fdff2d266d62f3c90d)), closes [#166](https://github.com/Hufe921/canvas-editor/issues/166) +* table cell vertical align error after page scaled #165 ([1fa1d10](https://github.com/Hufe921/canvas-editor/commit/1fa1d10fc9c279eb1d91b804484019d02fec945d)), closes [#165](https://github.com/Hufe921/canvas-editor/issues/165) + + +### Documentation + +* update snapshot ([e0791cf](https://github.com/Hufe921/canvas-editor/commit/e0791cf5f74341547a86809e80c647a3f0fa5a2a)) + + +### Features + +* avoid punctuation at the beginning of a row ([29a988a](https://github.com/Hufe921/canvas-editor/commit/29a988a1d4d57a98068a299986ddbe38a52d80a4)) + + + +## [0.9.28](https://github.com/Hufe921/canvas-editor/compare/v0.9.27...v0.9.28) (2023-03-27) + + +### Bug Fixes + +* drag table border to change size #160 ([fda18d9](https://github.com/Hufe921/canvas-editor/commit/fda18d968e7f54c11726dcd11a9d3536bca33d9a)), closes [#160](https://github.com/Hufe921/canvas-editor/issues/160) +* extra blank row appear when insert table #162 ([2f8c6b7](https://github.com/Hufe921/canvas-editor/commit/2f8c6b71d2787e85a677073ca41832c4b00f6c41)), closes [#162](https://github.com/Hufe921/canvas-editor/issues/162) +* position header and footer zone ([ca5c4be](https://github.com/Hufe921/canvas-editor/commit/ca5c4be9c39f374ccbb1bb6ea126b06fa978c885)) +* table cell height adaptation #162 ([a2090c8](https://github.com/Hufe921/canvas-editor/commit/a2090c82fc2c09ed28a40064094cbca0b9bd1431)), closes [#162](https://github.com/Hufe921/canvas-editor/issues/162) + + +### Documentation + +* add page footer ([45a17ba](https://github.com/Hufe921/canvas-editor/commit/45a17ba756eb822060aa66370821434d1167be55)) +* update editor options ([6351b95](https://github.com/Hufe921/canvas-editor/commit/6351b95646d80dab45cc517c223561aa0e8fc725)) + + +### Features + +* add page footer ([21626cc](https://github.com/Hufe921/canvas-editor/commit/21626cc41b24f72c28694881a8aba92fddcccf35)) + + + +## [0.9.27](https://github.com/Hufe921/canvas-editor/compare/v0.9.26...v0.9.27) (2023-03-24) + + +### Chores + +* verify release package ([4773c03](https://github.com/Hufe921/canvas-editor/commit/4773c0363ccb15b4ca9b406bb2c888a53c9af777)) + + + +## [0.9.26](https://github.com/Hufe921/canvas-editor/compare/v0.9.25...v0.9.26) (2023-03-24) + + +### Chores + +* add release script ([caa2c34](https://github.com/Hufe921/canvas-editor/commit/caa2c3463d6306b49fbfb893dec6caf6a9f16f0d)) + + + +## [0.9.25](https://github.com/Hufe921/canvas-editor/compare/v0.9.24...v0.9.25) (2023-03-24) + + +### Bug Fixes + +* table elements position when zooming ([3ff0eea](https://github.com/Hufe921/canvas-editor/commit/3ff0eea9482d5ab868b09940d3ebbb3864c71a2b)) +* table tool render option ([4a022a2](https://github.com/Hufe921/canvas-editor/commit/4a022a2724521d842f72f679a3bc8ca2b0e244ea)) + + +### Chores + +* add eslint global variable ([70f3d17](https://github.com/Hufe921/canvas-editor/commit/70f3d17ff0e4a4477e5d83f0b9b422f91aee1226)) +* add verify git commit message script ([0582da5](https://github.com/Hufe921/canvas-editor/commit/0582da56dde70e1f023f8cd3d8aad64b0e163cc8)) +* update .editorConfig ([4c48c79](https://github.com/Hufe921/canvas-editor/commit/4c48c79f71256449a82d50052216c22914b72afe)) + + +### Documentation + +* delete headerTop option ([756c706](https://github.com/Hufe921/canvas-editor/commit/756c706b9f5321b67f47be99ec04232339b6fdb5)) +* table border type #152 ([9521d59](https://github.com/Hufe921/canvas-editor/commit/9521d59fafe8a5b5a2e66db97128720bc0206d48)), closes [#152](https://github.com/Hufe921/canvas-editor/issues/152) + + +### Features + +* table border tool ([5c529b7](https://github.com/Hufe921/canvas-editor/commit/5c529b76ca8184bed118955fdae36b8f1717dbb9)) +* table border type #152 ([48ad18b](https://github.com/Hufe921/canvas-editor/commit/48ad18bccb16e1a7751680103ac52a20320861d8)), closes [#152](https://github.com/Hufe921/canvas-editor/issues/152) + + + +## [0.9.24](https://github.com/Hufe921/canvas-editor/compare/v0.9.23...v0.9.24) (2023-03-22) + + +### Bug Fixes + +* table cell auto height #150 ([e68c0be](https://github.com/Hufe921/canvas-editor/commit/e68c0bebc380f9dd9e870677eac888a7b04c56cb)), closes [#150](https://github.com/Hufe921/canvas-editor/issues/150) +* cannot copy table element when it in the first position ([73457cb](https://github.com/Hufe921/canvas-editor/commit/73457cb2e8cc693138e4785c382ae4c025d8fedd)) +* compute only the main body word count ([4306d44](https://github.com/Hufe921/canvas-editor/commit/4306d44ed62a0233fdb0fe9b3c3f7d00423ba4da)) +* some IME position error #155 ([b6dfcb5](https://github.com/Hufe921/canvas-editor/commit/b6dfcb5f08f42790395059963b16a57d20367978)), closes [#155](https://github.com/Hufe921/canvas-editor/issues/155) + + +### Documentation + +* table cell vertical align ([47918db](https://github.com/Hufe921/canvas-editor/commit/47918db3bbcf78d4af60e43e7ed1183c5525a758)) +* page number options ([c753a9d](https://github.com/Hufe921/canvas-editor/commit/c753a9d00dd3a5bda0b75df7e6237448bd16f21f)) +* font size setting api ([690aa1b](https://github.com/Hufe921/canvas-editor/commit/690aa1b96d185ca7d80b9af884d23a39dac9a2bf)) +* next features road map ([0e8c5fd](https://github.com/Hufe921/canvas-editor/commit/0e8c5fd388a81fabe28a66cf1239348930ae5346)) + + +### Features + +* table cell vertical align contextmenu i18n ([32643a5](https://github.com/Hufe921/canvas-editor/commit/32643a5573660e66fd7084fa33ae599521457ced)) +* table cell vertical align ([665e201](https://github.com/Hufe921/canvas-editor/commit/665e2018aaae38aacd6ec9d32980fba245f12ddd)) +* page number set row flex ([0a9f44e](https://github.com/Hufe921/canvas-editor/commit/0a9f44eed8fd72ff3318a34222ff91d4f140de9b)) +* add fontSize settings API ([d951532](https://github.com/Hufe921/canvas-editor/commit/d95153299880ef7139d3a2d64d7d3dc16fbef615)) +* fontSize setting Example ([3f218f6](https://github.com/Hufe921/canvas-editor/commit/3f218f698bd361ae73eac9043f31585c97058963)) + + +### Performance Improvements + +* font size setting api ([84e2fc9](https://github.com/Hufe921/canvas-editor/commit/84e2fc95699adf81fd361ff7a76b900abfd2b164)) + + +### Tests + +* font size setting ([89012b7](https://github.com/Hufe921/canvas-editor/commit/89012b7183327f3a5e6497ad96741e6f5badc699)) + + + +## [0.9.23](https://github.com/Hufe921/canvas-editor/compare/v0.9.22...v0.9.23) (2023-03-19) + + +### Bug Fixes + +* set editor zone method ([9de29ed](https://github.com/Hufe921/canvas-editor/commit/9de29ed934ffb9556e473911fdb715524f1d2fad)) +* table cursor position in page header ([85a2bbe](https://github.com/Hufe921/canvas-editor/commit/85a2bbea60867d4314b571586785ad03dfd716da)) + + +### Documentation + +* next features road map ([debc6c1](https://github.com/Hufe921/canvas-editor/commit/debc6c128383a45791763b230d6802b6219d6d8d)) +* add page header ([6d47ce0](https://github.com/Hufe921/canvas-editor/commit/6d47ce0a67144a891a3cb16850bfc25396ba3b55)) + + +### Features + +* add header indicator ([86707ca](https://github.com/Hufe921/canvas-editor/commit/86707cad47381e8f807aece3ffc76e34f8cffa24)) +* zoom page header ([c0fee3e](https://github.com/Hufe921/canvas-editor/commit/c0fee3ea143412928650aa44f4954fe8209ab264)) +* export page header type ([23b6cd2](https://github.com/Hufe921/canvas-editor/commit/23b6cd28ad80bf8dc4ecf5968e53909681307c6d)) +* paging cursor position ([7b4b33b](https://github.com/Hufe921/canvas-editor/commit/7b4b33b5c5d4b1c27224f7ee25d78f4ba3fe9619)) +* page header boundary value ([ed79d25](https://github.com/Hufe921/canvas-editor/commit/ed79d2587ca2932e0c6e1229c72ac24d71d953fc)) +* edit page header ([6082ab2](https://github.com/Hufe921/canvas-editor/commit/6082ab26d110d5c90e2742ee51688cdf3e4a41fa)) +* render header element ([da2dfd3](https://github.com/Hufe921/canvas-editor/commit/da2dfd3a16d8e87689ea92aae9907cb4f9e21d50)) + + +### Tests + +* update get editor value ([436d1de](https://github.com/Hufe921/canvas-editor/commit/436d1de2845599e5767833648ffc836647d7e292)) + + + +## [0.9.22](https://github.com/Hufe921/canvas-editor/compare/v0.9.21...v0.9.22) (2023-03-15) + + +### Bug Fixes + +* init page context when paper change ([bb63eeb](https://github.com/Hufe921/canvas-editor/commit/bb63eeb335e45899cf5b6906f26fc1bb7599356e)) + + +### Documentation + +* set paper direction api ([0963fc1](https://github.com/Hufe921/canvas-editor/commit/0963fc1adfeda7e3938bf7fbe2bfab1433f401d5)) +* update command execute api ([82b5256](https://github.com/Hufe921/canvas-editor/commit/82b525649fea0793fc54897e9befba1b91efe782)) +* next features roadmap ([dd7a768](https://github.com/Hufe921/canvas-editor/commit/dd7a7686af40b0a99f388961ff302ea41de4c905)) + + +### Features + +* adjust background when paper direction change ([f076f2b](https://github.com/Hufe921/canvas-editor/commit/f076f2bd00eae6f3adf56559e40807898a8de229)) +* adjust margins when paper direction change ([1eefa57](https://github.com/Hufe921/canvas-editor/commit/1eefa570b23a3330971c595dde8cef0e6ec2f530)) +* add paper direction ([9aeb928](https://github.com/Hufe921/canvas-editor/commit/9aeb928b35c90c0a5a6040b34b77e2d6b91343d5)) +* drag and drop date element ([780a40c](https://github.com/Hufe921/canvas-editor/commit/780a40caae3cabd9ceea347ae796ccfda9e9b3ef)) + + + +## [0.9.21](https://github.com/Hufe921/canvas-editor/compare/v0.9.20...v0.9.21) (2023-03-11) + + +### Bug Fixes + +* reset canvas context properties that can be overwritten by css #144 ([a3664a2](https://github.com/Hufe921/canvas-editor/commit/a3664a2012ea0ef8bcaa58bd41acd7a6bcd17968)), closes [#144](https://github.com/Hufe921/canvas-editor/issues/144) +* hyperlink popup max width ([1cad605](https://github.com/Hufe921/canvas-editor/commit/1cad605d4f672b8dd01a59ccb55526353e611242)) + + +### Documentation + +* next features roadmap ([d86a155](https://github.com/Hufe921/canvas-editor/commit/d86a155159b9f0e768954d3c18a5fccf3c7aba22)) +* fix usage errors ([74447a1](https://github.com/Hufe921/canvas-editor/commit/74447a1c5cadaee9fdb9df0116b97cc5e294e016)) + + +### Features + +* drag and drop element ([9b9a0a0](https://github.com/Hufe921/canvas-editor/commit/9b9a0a09aeb36c1ae50641cdc8585615a06c28e4)) +* render checkbox control with style ([9f64a06](https://github.com/Hufe921/canvas-editor/commit/9f64a068b13b0789dc2974e443ad000655a2c929)) + + + +## [0.9.20](https://github.com/Hufe921/canvas-editor/compare/v0.9.19...v0.9.20) (2023-03-08) + + +### Bug Fixes + +* near highlight elements render error ([17b469b](https://github.com/Hufe921/canvas-editor/commit/17b469be6b95621b635383c9fc20c9c0adcb8d2b)) + + +### Chores + +* add CHANGELOG.md ([367a247](https://github.com/Hufe921/canvas-editor/commit/367a24730a739514d24d7c882524f8ded479fb38)) +* add issue template ([7a26819](https://github.com/Hufe921/canvas-editor/commit/7a268199b3607a8c629fc5bc0242be89abbbac90)) + + +### Features + +* signature adapt to high-resolution screen ([4acf243](https://github.com/Hufe921/canvas-editor/commit/4acf243fed01b0b41ebbcc46c4b6603cabf6c825)) +* open hyperlink shortcut ([3295e37](https://github.com/Hufe921/canvas-editor/commit/3295e3711ae92f5503064691c5558afea99e3f0c)) +* copy and paste highlight element ([0493ae2](https://github.com/Hufe921/canvas-editor/commit/0493ae2d5e10daf917e39a9deb4e29c90e096420)) + + + +## [0.9.19](https://github.com/Hufe921/canvas-editor/compare/v0.9.18...v0.9.19) (2023-03-03) + + +### Bug Fixes + +* continuity page render error in lazy mode ([ff06e50](https://github.com/Hufe921/canvas-editor/commit/ff06e50138697ef61ce308e78a7e873046664e30)) +* format paste table data ([909096b](https://github.com/Hufe921/canvas-editor/commit/909096bd0d9d6e845ccecbee815ddebe35e6f021)) + + +### Performance Improvements + +* improve:control element input ([dc54622](https://github.com/Hufe921/canvas-editor/commit/dc54622258872630ff39309f2b1da3baee1f508f)) + + + +## [0.9.18](https://github.com/Hufe921/canvas-editor/compare/v0.9.17...v0.9.18) (2023-03-02) + + +### Bug Fixes + +* scrollbar scroll automatically ([8b5c41b](https://github.com/Hufe921/canvas-editor/commit/8b5c41bd58008a2945574ea178058638b64c0ffb)) +* paper remove error in lazy render mode ([8aac99d](https://github.com/Hufe921/canvas-editor/commit/8aac99d5c3a984d8c89b251e53dd393e73c66327)) +* cannot paste html at the end of the control #133 ([0694bf0](https://github.com/Hufe921/canvas-editor/commit/0694bf0bec5da94d800affbf60b79a16c7d4d0e1)), closes [#133](https://github.com/Hufe921/canvas-editor/issues/133) +* cannot delete control when it is first element #131 ([45ef8b6](https://github.com/Hufe921/canvas-editor/commit/45ef8b69540ee28f3d4c3b7cada5fbb44c26a023)), closes [#131](https://github.com/Hufe921/canvas-editor/issues/131) + + +### Features + +* add lazy render mode ([f428f56](https://github.com/Hufe921/canvas-editor/commit/f428f566e9e92c7d4cc2affe73b4fc01eaaa56dd)) + + +### Performance Improvements + +* improve:position compute separate from draw row ([8910c7c](https://github.com/Hufe921/canvas-editor/commit/8910c7cf0a5d74f6ec46615bbc106773b3147cdc)) + + + +## [0.9.17](https://github.com/Hufe921/canvas-editor/compare/v0.9.16...v0.9.17) (2023-02-28) + + +### Bug Fixes + +* composing input not save history ([c4f2687](https://github.com/Hufe921/canvas-editor/commit/c4f268772646f91d63c224cf72d5e23278ff2f5e)) +* visible page computing method ([fcb96a6](https://github.com/Hufe921/canvas-editor/commit/fcb96a6f561d315945c1069b4a47d4f788212556)) + + +### Documentation + +* next features road map ([6e99d8a](https://github.com/Hufe921/canvas-editor/commit/6e99d8ad93a7a907fd97a201186b51539980ba55)) +* cursor style option ([92d65da](https://github.com/Hufe921/canvas-editor/commit/92d65da7d80e0e312d053006d0690d1f9a258ef4)) + + +### Features + +* set the cursor style when dragging text ([2977183](https://github.com/Hufe921/canvas-editor/commit/29771838f0bdc5aef1f5714fd9d6110f482f3f64)) + + + +## [0.9.16](https://github.com/Hufe921/canvas-editor/compare/v0.9.15...v0.9.16) (2023-02-21) + + +### Features + +* render composing text ([63487d4](https://github.com/Hufe921/canvas-editor/commit/63487d4f90332be68cb07f3eacfca3a0d04f8eff)) +* redraw when device pixel ratio change ([4c370ae](https://github.com/Hufe921/canvas-editor/commit/4c370aec1adbc4056f394d2faf370327fd544e22)) +* support mac os shortcut remark ([189e88c](https://github.com/Hufe921/canvas-editor/commit/189e88c5601b2a99b032ae966f945986b8acf8b1)) + + +### Tests + +* optimize the method of get editor value ([708d578](https://github.com/Hufe921/canvas-editor/commit/708d57812e6ab145113e3fbab047970d155b515c)) + + + +## [0.9.15](https://github.com/Hufe921/canvas-editor/compare/v0.9.14...v0.9.15) (2023-02-16) + + +### Bug Fixes + +* draw multi-segment richtext element in one row ([c522c22](https://github.com/Hufe921/canvas-editor/commit/c522c225b1c26d16abcccd74c0d2573fbeb88595)) + + +### Documentation + +* mac os shortcut ([df8096e](https://github.com/Hufe921/canvas-editor/commit/df8096ebfb8259d02f19b1926457d96af574bda2)) +* update next features ([338a67c](https://github.com/Hufe921/canvas-editor/commit/338a67c6e7baf38097f1ec801d8292000e94742b)) +* update next features ([4cb8d2a](https://github.com/Hufe921/canvas-editor/commit/4cb8d2adf6aa25c7734f3a544e6bbb8a78fe5892)) +* add i18n ([c912563](https://github.com/Hufe921/canvas-editor/commit/c9125635eae8cecf4bdeda56455b66221c2f1db3)) + + +### Features + +* support mac os shortcut ([ef4bda2](https://github.com/Hufe921/canvas-editor/commit/ef4bda2a46fec7c901fe62abeee4e4315e740643)) +* support mac os shortcut ([0d6e0cf](https://github.com/Hufe921/canvas-editor/commit/0d6e0cf4124ddbbc1333a3955acf6aa7b4e159cc)) +* support partial fields to set i18n lang ([7287b57](https://github.com/Hufe921/canvas-editor/commit/7287b576e86447ebde026117d0e6068a3dfaf8f6)) + + + +## [0.9.14](https://github.com/Hufe921/canvas-editor/compare/v0.9.13...v0.9.14) (2023-02-08) + + +### Bug Fixes + +* get rowFlex when line breaks ([34799d7](https://github.com/Hufe921/canvas-editor/commit/34799d7cb90a5ed2161c219121ca7b4fcd692558)) +* paste table data format judgment ([8ff0d01](https://github.com/Hufe921/canvas-editor/commit/8ff0d01dde6f66a5ecdabb89703618f16b86ac75)) + + +### Features + +* add i18n ([82b8d2c](https://github.com/Hufe921/canvas-editor/commit/82b8d2c5a965386720e029d52689f97b5c62f0bc)) +* paste html with textAlign info ([eb0086a](https://github.com/Hufe921/canvas-editor/commit/eb0086a4b81cd87fdb558e89e92c304f16169cb6)) + + + +## [0.9.13](https://github.com/Hufe921/canvas-editor/compare/v0.9.12...v0.9.13) (2023-02-03) + + +### Bug Fixes + +* remove style sheet when paste html ([5bf7029](https://github.com/Hufe921/canvas-editor/commit/5bf7029f0ada5a370f1dfdf6c09b2f977a08609a)) +* copy table width colspan and rowspan info ([0f46db1](https://github.com/Hufe921/canvas-editor/commit/0f46db1f8addca04510c683855840770abde69a9)) +* adjust selection boundary ([4865eb5](https://github.com/Hufe921/canvas-editor/commit/4865eb5d5a0ded64215a98651ab23dc6404681c2)) + + +### Documentation + +* add algolia search ([8177c11](https://github.com/Hufe921/canvas-editor/commit/8177c11c2b4225196aa2a387dcf814fd5af73007)) + + +### Features + +* paste table element ([db51a27](https://github.com/Hufe921/canvas-editor/commit/db51a27666e21a942ecabc9c6d5f926ce473fedb)) +* paste image element ([0c07db7](https://github.com/Hufe921/canvas-editor/commit/0c07db7a5d53db47117e6fdd11bec995f0a3616b)) +* paste separator element ([77d546f](https://github.com/Hufe921/canvas-editor/commit/77d546f476b0009f6925d660cdcbeefd5150b6c2)) +* paste checkbox element ([e37da11](https://github.com/Hufe921/canvas-editor/commit/e37da11a79dd5e226f954833b102a8c01a19fef9)) +* shrink the contextmenu scope ([64f5ff1](https://github.com/Hufe921/canvas-editor/commit/64f5ff15aa25aaede301277580be0770cec593b0)) + + + +## [0.9.12](https://github.com/Hufe921/canvas-editor/compare/v0.9.11...v0.9.12) (2023-01-20) + + +### Bug Fixes + +* adjust selection by shortcut #111 ([a19a0a1](https://github.com/Hufe921/canvas-editor/commit/a19a0a1126f5d8521cde7d53d1042d4fa67ade89)), closes [#111](https://github.com/Hufe921/canvas-editor/issues/111) +* compatible with browsers that do not support ClipboardItem #108 ([196f638](https://github.com/Hufe921/canvas-editor/commit/196f63831849e555d1c24daa78d69e187e642214)), closes [#108](https://github.com/Hufe921/canvas-editor/issues/108) +* line thickness of rendered margin ([e8f3b2a](https://github.com/Hufe921/canvas-editor/commit/e8f3b2a6da725feb014d83ff96ab3c6a68b50655)) +* cannot cut whole line except the first page ([ca13a3b](https://github.com/Hufe921/canvas-editor/commit/ca13a3b268791e18d7bc6b0b3297ef0ca5c76387)) + + +### Documentation + +* adjust selection by direction key ([01353ad](https://github.com/Hufe921/canvas-editor/commit/01353ad59e208d1db1810f43f868201090273d02)) +* adjust selection by shortcut ([81ac4d8](https://github.com/Hufe921/canvas-editor/commit/81ac4d8dc0177090ef11098c9916d156415b5db9)) +* add global api ([3678b7f](https://github.com/Hufe921/canvas-editor/commit/3678b7f34b692e9e56141101e2c9e8b2d627a677)) +* add docs url to README.md ([a369adb](https://github.com/Hufe921/canvas-editor/commit/a369adbe32c9f0779725a68b3f6b20cc9cfbe5b2)) +* update index page ([38cb302](https://github.com/Hufe921/canvas-editor/commit/38cb302fa3a62d5db600ed2df3e8794497a59c52)) + + +### Features + +* adjust selection by direction key ([1dfdd9a](https://github.com/Hufe921/canvas-editor/commit/1dfdd9a057f20c0fc512f869b3c914317c0fed85)) +* adjust range by shortcut ([4a11bca](https://github.com/Hufe921/canvas-editor/commit/4a11bcacd94a6a0f1340b1741e8ce77d1c6d7e84)) +* update server host ([bf93c29](https://github.com/Hufe921/canvas-editor/commit/bf93c2991ad7e657ea63f2ccb84d0ea803609125)) +* add docs workflow ([f2374a1](https://github.com/Hufe921/canvas-editor/commit/f2374a14f562d25e9498b4ec0c96da9927849ebb)) +* add docs workflow ([7fd0792](https://github.com/Hufe921/canvas-editor/commit/7fd07928b340e3a9795bc4c10d41820d5daffb61)) +* add docs ([db52ab8](https://github.com/Hufe921/canvas-editor/commit/db52ab815d708b5f66df0f6661b17e1227067181)) +* add font selection and font wysiwyg ([72d6174](https://github.com/Hufe921/canvas-editor/commit/72d6174d7b0b61be8db42dcf7a21512fafbc1f2d)) + + + +## [0.9.11](https://github.com/Hufe921/canvas-editor/compare/v0.9.10...v0.9.11) (2022-12-25) + + +### Features + +* optimize event code structure ([f63affc](https://github.com/Hufe921/canvas-editor/commit/f63affc1c4219ee4d65485d11299dbfee47a8be2)) +* add isPointInRange function to Range ([5e9c1ce](https://github.com/Hufe921/canvas-editor/commit/5e9c1ce57774012cb1903c2e97ac87ff42d1e245)) +* drag text to editor ([4cf4ea5](https://github.com/Hufe921/canvas-editor/commit/4cf4ea5e45e0ee94a20b53f5e775bba0ef9bacca)) +* use selection text when searching ([bcdb234](https://github.com/Hufe921/canvas-editor/commit/bcdb2340ae87dd72831d23b5ac83e984d09842e3)) +* add cut row feature to contextmenu ([172cb6d](https://github.com/Hufe921/canvas-editor/commit/172cb6d88cf3069452c012b8a52b18d5dba1ff99)) +* cut a whole line when no selection ([2c38f13](https://github.com/Hufe921/canvas-editor/commit/2c38f13113afb0889a3907a825f397de6a877181)) + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..45b507d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021-present, hufe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d2d267f --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +

canvas-editor

+ +

+Version + + Cypress Passing + + + GitHub Contributors + +License +PRs +

+ +

a rich text editor by canvas/svg

+ +

+ View Demo + · + View Docs + · + Report Bug + · + Request Feature + · + FAQ +

+ +

Love the project? Please consider donating(赞助) to help it improve!

+ +## Tips + +1. Official plugin: [canvas-editor-plugin](https://github.com/Hufe921/canvas-editor-plugin) +2. The render layer by svg is under development, see [feature/svg](https://github.com/Hufe921/canvas-editor/tree/feature/svg) +3. The export pdf feature is available now, see [feature/pdf](https://github.com/Hufe921/canvas-editor/tree/feature/pdf) +4. The AI-powered text processing demo, see [feature/ai](https://github.com/Hufe921/canvas-editor/tree/feature/ai) +5. Table pagination [#41](https://github.com/Hufe921/canvas-editor/issues/41) is under active development, see: [poc/table-paging](https://github.com/Hufe921/canvas-editor/tree/poc/table-paging) · [demo](https://hufe.club/canvas-editor-table/) + +## Basic usage + +```bash +npm i @hufe921/canvas-editor --save +``` + +```html +
+``` + +```javascript +import Editor from '@hufe921/canvas-editor' + +new Editor(document.querySelector('.canvas-editor'), { + main: [ + { + value: 'Hello World' + } + ] +}) +``` + +## Features + +- Rich text operations (Undo, Redo, Font, Size, Bold, Italic, Underline, Strikeout, Superscript, Alignment, Title, List, ...) +- Insert elements (Table, Image, Link, Code Block, Page Break, Math Formula, Date Picker, Block, ...) +- Print (Based on canvas to picture, pdf drawing) +- Controls (Select, Text, Date, Radio, Checkbox) +- Contextmenu (Internal, Custom) +- Shortcut keys (Internal, Custom) +- Drag and Drop(Text, Element, Control) +- Header, Footer, Page Number +- Page Margin +- Watermark +- Pagination +- Comment +- Catalog + +## Roadmap + +1. Table paging +2. Control rules +3. Improve performance +4. [CRDT](https://github.com/Hufe921/canvas-editor/tree/feature/CRDT) + +## Snapshot + +![image](https://github.com/Hufe921/canvas-editor/blob/main/src/assets/snapshots/main_v0.9.35.png) + +## Install + +`yarn` + +## Dev + +`npm run dev` + +## Build + +#### app + +`npm run build` + +#### lib + +`npm run lib` diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 0000000..f6c338c --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'cypress' + +export default defineConfig({ + video: false, + viewportWidth: 1366, + viewportHeight: 720, + e2e: { + experimentalRunAllSpecs: true + } +}) diff --git a/cypress/e2e/control/checkbox.cy.ts b/cypress/e2e/control/checkbox.cy.ts new file mode 100644 index 0000000..b768cb3 --- /dev/null +++ b/cypress/e2e/control/checkbox.cy.ts @@ -0,0 +1,46 @@ +import Editor, { ControlType, ElementType } from '../../../src/editor' + +describe('控件-复选框', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + const elementType: ElementType = 'control' + const controlType: ControlType = 'checkbox' + + it('复选框', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + type: elementType, + value: '', + control: { + code: '98175', + type: controlType, + value: null, + valueSets: [ + { + value: '有', + code: '98175' + }, + { + value: '无', + code: '98176' + } + ] + } + } + ]) + + const data = editor.command.getValue().data.main[0] + + expect(data.control!.code).to.be.eq('98175') + }) + }) +}) diff --git a/cypress/e2e/control/select.cy.ts b/cypress/e2e/control/select.cy.ts new file mode 100644 index 0000000..003e150 --- /dev/null +++ b/cypress/e2e/control/select.cy.ts @@ -0,0 +1,56 @@ +import Editor, { ControlType, ElementType } from '../../../src/editor' + +describe('控件-列举型', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + const text = `有` + const elementType: ElementType = 'control' + const controlType: ControlType = 'select' + + it('列举型', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + type: elementType, + value: '', + control: { + type: controlType, + value: null, + placeholder: '列举型', + valueSets: [ + { + value: '有', + code: '98175' + }, + { + value: '无', + code: '98176' + } + ] + } + } + ]) + + cy.get('@canvas').type(`{leftArrow}`) + + cy.get('.ce-select-control-popup li') + .eq(0) + .click() + .then(() => { + const data = editor.command.getValue().data.main[0] + + expect(data.control!.value![0].value).to.be.eq(text) + + expect(data.control!.code).to.be.eq('98175') + }) + }) + }) +}) diff --git a/cypress/e2e/control/text.cy.ts b/cypress/e2e/control/text.cy.ts new file mode 100644 index 0000000..a030e7b --- /dev/null +++ b/cypress/e2e/control/text.cy.ts @@ -0,0 +1,43 @@ +import Editor, { ControlType, ElementType } from '../../../src/editor' + +describe('控件-文本型', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + const text = `canvas-editor` + const elementType: ElementType = 'control' + const controlType: ControlType = 'text' + + it('文本型', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + type: elementType, + value: '', + control: { + type: controlType, + value: null, + placeholder: '文本型' + } + } + ]) + + cy.get('@canvas').type(`{leftArrow}`) + + cy.get('.ce-inputarea') + .type(text) + .then(() => { + const data = editor.command.getValue().data.main[0] + + expect(data.control!.value![0].value).to.be.eq(text) + }) + }) + }) +}) diff --git a/cypress/e2e/editor.cy.ts b/cypress/e2e/editor.cy.ts new file mode 100644 index 0000000..5033a39 --- /dev/null +++ b/cypress/e2e/editor.cy.ts @@ -0,0 +1,67 @@ +import Editor from '../../src/editor' + +describe('基础功能', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + const text = 'canvas-editor' + + it('编辑保存', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + cy.get('@canvas') + .type(text) + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].value).to.eq(text) + }) + }) + }) + + it('模式切换', () => { + cy.get('@canvas').click() + + cy.get('.ce-cursor').should('have.css', 'display', 'block') + + cy.get('.editor-mode').click().click() + + cy.get('.editor-mode').contains('只读') + + cy.get('@canvas').click() + + cy.get('.ce-cursor').should('have.css', 'display', 'none') + }) + + it('页面缩放', () => { + cy.get('.page-scale-add').click() + + cy.get('.page-scale-percentage').contains('110%') + + cy.get('.page-scale-minus').click().click() + + cy.get('.page-scale-percentage').contains('90%') + }) + + it('字数统计', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: 'canvas-editor 2022 编辑器' + } + ]) + + cy.get('.word-count').contains('7') + }) + }) +}) diff --git a/cypress/e2e/menus/block.cy.ts b/cypress/e2e/menus/block.cy.ts new file mode 100644 index 0000000..063147f --- /dev/null +++ b/cypress/e2e/menus/block.cy.ts @@ -0,0 +1,38 @@ +import Editor from '../../../src/editor' + +describe('菜单-内容块', () => { + const url = 'http://localhost:3000/canvas-editor/' + + beforeEach(() => { + cy.visit(url) + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + it('内容块', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + cy.get('.menu-item__block').click() + + cy.get('.dialog-option__item [name="width"]').type('500') + + cy.get('.dialog-option__item [name="height"]').type('300') + + cy.get('.dialog-option__item [name="src"]').type(url) + + cy.get('.dialog-menu button') + .eq(1) + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].type).to.eq('block') + + expect(data[0].block?.iframeBlock?.src).to.eq(url) + }) + }) + }) +}) diff --git a/cypress/e2e/menus/checkbox.cy.ts b/cypress/e2e/menus/checkbox.cy.ts new file mode 100644 index 0000000..6e65cb6 --- /dev/null +++ b/cypress/e2e/menus/checkbox.cy.ts @@ -0,0 +1,33 @@ +import Editor, { ElementType } from '../../../src/editor' + +describe('菜单-复选框', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + const type: ElementType = 'checkbox' + + it('代码块', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + type, + value: '', + checkbox: { + value: true + } + } + ]) + + const data = editor.command.getValue().data.main[0] + + expect(data.checkbox?.value).to.eq(true) + }) + }) +}) diff --git a/cypress/e2e/menus/codeblock.cy.ts b/cypress/e2e/menus/codeblock.cy.ts new file mode 100644 index 0000000..81d918c --- /dev/null +++ b/cypress/e2e/menus/codeblock.cy.ts @@ -0,0 +1,34 @@ +import Editor from '../../../src/editor' + +describe('菜单-代码块', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + const text = `console.log('canvas-editor')` + + it('代码块', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + cy.get('.menu-item__codeblock').click() + + cy.get('.dialog-option [name="codeblock"]').type(text) + + cy.get('.dialog-menu button') + .eq(1) + .click() + .then(() => { + const data = editor.command.getValue().data.main[2] + + expect(data.value).to.eq('log') + + expect(data.color).to.eq('#b9a40a') + }) + }) + }) +}) diff --git a/cypress/e2e/menus/date.cy.ts b/cypress/e2e/menus/date.cy.ts new file mode 100644 index 0000000..540e241 --- /dev/null +++ b/cypress/e2e/menus/date.cy.ts @@ -0,0 +1,28 @@ +import Editor from '../../../src/editor' + +describe('菜单-日期选择器', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + it('LaTeX', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + cy.get('.menu-item__date').click() + + cy.get('.menu-item__date li') + .first() + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].type).to.eq('date') + }) + }) + }) +}) diff --git a/cypress/e2e/menus/format.cy.ts b/cypress/e2e/menus/format.cy.ts new file mode 100644 index 0000000..7bc4e1a --- /dev/null +++ b/cypress/e2e/menus/format.cy.ts @@ -0,0 +1,40 @@ +import Editor from '../../../src/editor' + +describe('菜单-清除格式', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + const text = 'canvas-editor' + const textLength = text.length + + it('清除格式', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text, + bold: true, + italic: true + } + ]) + + editor.command.executeSetRange(0, textLength) + + cy.get('.menu-item__format') + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].italic).to.eq(undefined) + + expect(data[0].bold).to.eq(undefined) + }) + }) + }) +}) diff --git a/cypress/e2e/menus/hyperlink.cy.ts b/cypress/e2e/menus/hyperlink.cy.ts new file mode 100644 index 0000000..2ffe84e --- /dev/null +++ b/cypress/e2e/menus/hyperlink.cy.ts @@ -0,0 +1,39 @@ +import Editor from '../../../src/editor' + +describe('菜单-超链接', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + const text = 'canvas-editor' + const url = 'https://hufe.club/canvas-editor' + + it('超链接', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + cy.get('.menu-item__hyperlink').click() + + cy.get('.dialog-option__item [name="name"]').type(text) + + cy.get('.dialog-option__item [name="url"]').type(url) + + cy.get('.dialog-menu button') + .eq(1) + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].type).to.eq('hyperlink') + + expect(data[0].url).to.eq(url) + + expect(data[0]?.valueList?.[0].value).to.eq(text) + }) + }) + }) +}) diff --git a/cypress/e2e/menus/image.cy.ts b/cypress/e2e/menus/image.cy.ts new file mode 100644 index 0000000..32462e7 --- /dev/null +++ b/cypress/e2e/menus/image.cy.ts @@ -0,0 +1,25 @@ +import Editor from '../../../src/editor' + +describe('菜单-图片', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + it('图片', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + cy.get('#image').attachFile('test.png') + + cy.wait(200).then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].type).to.eq('image') + }) + }) + }) +}) diff --git a/cypress/e2e/menus/latex.cy.ts b/cypress/e2e/menus/latex.cy.ts new file mode 100644 index 0000000..216aa0f --- /dev/null +++ b/cypress/e2e/menus/latex.cy.ts @@ -0,0 +1,34 @@ +import Editor from '../../../src/editor' + +describe('菜单-LaTeX', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + const text = 'canvas-editor' + + it('LaTeX', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + cy.get('.menu-item__latex').click() + + cy.get('.dialog-option__item [name="value"]').type(text) + + cy.get('.dialog-menu button') + .eq(1) + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].type).to.eq('latex') + + expect(data[0].value).to.eq(text) + }) + }) + }) +}) diff --git a/cypress/e2e/menus/pagebreak.cy.ts b/cypress/e2e/menus/pagebreak.cy.ts new file mode 100644 index 0000000..24d04a1 --- /dev/null +++ b/cypress/e2e/menus/pagebreak.cy.ts @@ -0,0 +1,21 @@ +import Editor from '../../../src/editor' + +describe('菜单-分页符', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + it('分页符', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + cy.get('.menu-item__page-break').click().click() + + cy.get('canvas').should('have.length', 2) + }) + }) +}) diff --git a/cypress/e2e/menus/painter.cy.ts b/cypress/e2e/menus/painter.cy.ts new file mode 100644 index 0000000..4b04ab9 --- /dev/null +++ b/cypress/e2e/menus/painter.cy.ts @@ -0,0 +1,53 @@ +import Editor from '../../../src/editor' + +describe('菜单-格式刷', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + const text = 'canvas-editor' + const textLength = text.length + + it('格式刷', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text, + bold: true, + italic: true + } + ]) + + editor.command.executeInsertElementList([ + { + value: text + } + ]) + + editor.command.executeSetRange(0, textLength) + + cy.get('.menu-item__painter') + .click() + .wait(300) + .then(() => { + editor.command.executeSetRange(textLength, 2 * textLength) + + editor.command.executeApplyPainterStyle() + + const data = editor.command.getValue().data.main + + expect(data.length).to.eq(1) + + expect(data[0].italic).to.eq(true) + + expect(data[0].bold).to.eq(true) + }) + }) + }) +}) diff --git a/cypress/e2e/menus/print.cy.ts b/cypress/e2e/menus/print.cy.ts new file mode 100644 index 0000000..163b572 --- /dev/null +++ b/cypress/e2e/menus/print.cy.ts @@ -0,0 +1,25 @@ +import Editor from '../../../src/editor' + +describe('菜单-打印', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').should('have.length', 2) + }) + + it('打印', () => { + cy.getEditor().then(async (editor: Editor) => { + const imageList2 = await editor.command.getImage() + expect(imageList2.length).to.eq(2) + + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + cy.wait(200).then(async () => { + const imageList1 = await editor.command.getImage() + expect(imageList1.length).to.eq(1) + }) + }) + }) +}) diff --git a/cypress/e2e/menus/row.cy.ts b/cypress/e2e/menus/row.cy.ts new file mode 100644 index 0000000..6496279 --- /dev/null +++ b/cypress/e2e/menus/row.cy.ts @@ -0,0 +1,103 @@ +import Editor from '../../../src/editor' + +describe('菜单-行处理', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + const text = 'canvas-editor' + + it('左对齐', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text + } + ]) + + cy.get('.menu-item__left') + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].rowFlex).to.eq('left') + }) + }) + }) + + it('居中对齐', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text + } + ]) + + cy.get('.menu-item__center') + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].rowFlex).to.eq('center') + }) + }) + }) + + it('靠右对齐', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text + } + ]) + + cy.get('.menu-item__right') + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].rowFlex).to.eq('right') + }) + }) + }) + + it('行间距', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text + } + ]) + + cy.get('.menu-item__row-margin').as('rowMargin').click() + + cy.get('@rowMargin') + .find('li') + .eq(1) + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].rowMargin).to.eq(1.25) + }) + }) + }) +}) diff --git a/cypress/e2e/menus/search.cy.ts b/cypress/e2e/menus/search.cy.ts new file mode 100644 index 0000000..5758f46 --- /dev/null +++ b/cypress/e2e/menus/search.cy.ts @@ -0,0 +1,112 @@ +import Editor, { ElementType } from '../../../src/editor' + +describe('菜单-搜索', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + const searchText = 'canvas-editor' + const replaceText = 'replace' + const type: ElementType = 'table' + + it('搜索', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: searchText + }, + { + value: '\n', + type, + trList: [ + { + height: 42, + tdList: [ + { + colspan: 1, + rowspan: 1, + value: [ + { + value: searchText + } + ] + }, + { + colspan: 1, + rowspan: 1, + value: [] + } + ] + }, + { + height: 42, + tdList: [ + { + colspan: 1, + rowspan: 1, + value: [] + }, + { + colspan: 1, + rowspan: 1, + value: [ + { + value: searchText + } + ] + } + ] + } + ], + colgroup: [ + { + width: 200 + }, + { + width: 200 + } + ] + } + ]) + + cy.get('.menu-item__search').click() + + cy.get('.menu-item__search__collapse input').eq(0).type(searchText) + + // 搜索导航 + cy.get('.menu-item__search__collapse .arrow-right').click() + cy.get('.menu-item__search__collapse__search .search-result').should( + 'have.text', + '1/3' + ) + + cy.get('.menu-item__search__collapse__replace').as('replace') + + cy.get('@replace').find('input').type(replaceText) + + cy.get('@replace') + .find('button') + .click() + .then(() => { + const data = editor.command.getValue().data.main + + // 普通文本 + expect(data[0].value).to.be.eq(replaceText) + + // 表格内文本 + expect(data[1].trList![0].tdList[0].value[0].value).to.be.eq( + replaceText + ) + expect(data[1].trList![1].tdList[1].value[0].value).to.be.eq( + replaceText + ) + }) + }) + }) +}) diff --git a/cypress/e2e/menus/separator.cy.ts b/cypress/e2e/menus/separator.cy.ts new file mode 100644 index 0000000..880ea1f --- /dev/null +++ b/cypress/e2e/menus/separator.cy.ts @@ -0,0 +1,32 @@ +import Editor from '../../../src/editor' + +describe('菜单-分割线', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + it('分割线', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + cy.get('.menu-item__separator').click() + + cy.get('.menu-item__separator li') + .eq(1) + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].type).to.eq('separator') + + expect(data[0]?.dashArray?.[0]).to.eq(1) + + expect(data[0]?.dashArray?.[1]).to.eq(1) + }) + }) + }) +}) diff --git a/cypress/e2e/menus/table.cy.ts b/cypress/e2e/menus/table.cy.ts new file mode 100644 index 0000000..9449d8a --- /dev/null +++ b/cypress/e2e/menus/table.cy.ts @@ -0,0 +1,25 @@ +import Editor from '../../../src/editor' + +describe('菜单-表格', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + it('表格', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertTable(8, 8) + + const data = editor.command.getValue().data.main + + expect(data[0].type).to.eq('table') + + expect(data[0].trList?.length).to.eq(8) + }) + }) +}) diff --git a/cypress/e2e/menus/text.cy.ts b/cypress/e2e/menus/text.cy.ts new file mode 100644 index 0000000..3c8eeb5 --- /dev/null +++ b/cypress/e2e/menus/text.cy.ts @@ -0,0 +1,304 @@ +import Editor from '../../../src/editor' + +describe('菜单-文本处理', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + const text = 'canvas-editor' + const textLength = text.length + + it('字体', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text + } + ]) + + editor.command.executeSetRange(0, textLength) + + cy.get('.menu-item__font').as('font').click() + + cy.get('@font') + .find('li') + .eq(1) + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].font).to.eq('华文宋体') + }) + }) + }) + + it('字号设置', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text + } + ]) + + editor.command.executeSetRange(0, textLength) + + cy.get('.menu-item__size').as('size').click() + + cy.get('@size') + .find('li') + .eq(0) + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].size).to.eq(56) + }) + }) + }) + + it('字体增大', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text + } + ]) + + editor.command.executeSetRange(0, textLength) + + cy.get('.menu-item__size-add') + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].size).to.eq(18) + }) + }) + }) + + it('字体减小', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text + } + ]) + + editor.command.executeSetRange(0, textLength) + + cy.get('.menu-item__size-minus') + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].size).to.eq(14) + }) + }) + }) + + it('加粗', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text + } + ]) + + editor.command.executeSetRange(0, textLength) + + cy.get('.menu-item__bold') + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].bold).to.eq(true) + }) + }) + }) + + it('斜体', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text + } + ]) + + editor.command.executeSetRange(0, textLength) + + cy.get('.menu-item__italic') + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].italic).to.eq(true) + }) + }) + }) + + it('下划线', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text + } + ]) + + editor.command.executeSetRange(0, textLength) + + cy.get('.menu-item__underline') + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].underline).to.eq(true) + }) + }) + }) + + it('删除线', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text + } + ]) + + editor.command.executeSetRange(0, textLength) + + cy.get('.menu-item__strikeout') + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].strikeout).to.eq(true) + }) + }) + }) + + it('上标', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text + } + ]) + + editor.command.executeSetRange(0, textLength) + + cy.get('.menu-item__superscript') + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].type).to.eq('superscript') + }) + }) + }) + + it('下标', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text + } + ]) + + editor.command.executeSetRange(0, textLength) + + cy.get('.menu-item__subscript') + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].type).to.eq('subscript') + }) + }) + }) + + it('字体颜色', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text + } + ]) + + editor.command.executeSetRange(0, textLength) + + editor.command.executeColor('red') + + const data = editor.command.getValue().data.main + + expect(data[0].color).to.eq('red') + }) + }) + + it('高亮', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text + } + ]) + + editor.command.executeSetRange(0, textLength) + + editor.command.executeHighlight('red') + + const data = editor.command.getValue().data.main + + expect(data[0].highlight).to.eq('red') + }) + }) +}) diff --git a/cypress/e2e/menus/title.cy.ts b/cypress/e2e/menus/title.cy.ts new file mode 100644 index 0000000..83314ba --- /dev/null +++ b/cypress/e2e/menus/title.cy.ts @@ -0,0 +1,43 @@ +import Editor, { ElementType, TitleLevel } from '../../../src/editor' + +describe('菜单-标题', () => { + const url = 'http://localhost:3000/canvas-editor/' + + beforeEach(() => { + cy.visit(url) + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + const text = 'canvas-editor' + const elementType = 'title' + const level = 'first' + + it('标题', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + editor.command.executeInsertElementList([ + { + value: text + } + ]) + + cy.get('.menu-item__title').as('title').click() + + cy.get('@title') + .find('li') + .eq(1) + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].type).to.eq(elementType) + + expect(data[0].level).to.eq(level) + }) + }) + }) +}) diff --git a/cypress/e2e/menus/undoRedo.cy.ts b/cypress/e2e/menus/undoRedo.cy.ts new file mode 100644 index 0000000..c2e75f5 --- /dev/null +++ b/cypress/e2e/menus/undoRedo.cy.ts @@ -0,0 +1,49 @@ +import Editor from '../../../src/editor' + +describe('菜单-撤销&重做', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + const text = 'canvas-editor' + + it('撤销', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + cy.get('@canvas').type(`${text}1`) + + cy.get('.menu-item__undo') + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].value).to.eq(text) + }) + }) + }) + + it('重做', () => { + cy.getEditor().then((editor: Editor) => { + editor.command.executeSelectAll() + + editor.command.executeBackspace() + + cy.get('@canvas').type(`${text}1`) + + cy.get('.menu-item__undo').click() + + cy.get('.menu-item__redo') + .click() + .then(() => { + const data = editor.command.getValue().data.main + + expect(data[0].value).to.eq(`${text}1`) + }) + }) + }) +}) diff --git a/cypress/e2e/menus/watermark.cy.ts b/cypress/e2e/menus/watermark.cy.ts new file mode 100644 index 0000000..f810906 --- /dev/null +++ b/cypress/e2e/menus/watermark.cy.ts @@ -0,0 +1,64 @@ +import Editor from '../../../src/editor' + +describe('菜单-水印', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/canvas-editor/') + + cy.get('canvas').first().as('canvas').should('have.length', 1) + }) + + const text = 'canvas-editor' + const size = 80 + + it('添加水印', () => { + cy.getEditor().then((editor: Editor) => { + cy.get('.menu-item__watermark').click() + + cy.get('.menu-item__watermark li').eq(0).click() + + cy.get('.dialog-option [name="data"]').type(text) + + cy.get('.dialog-option [name="size"]').as('size') + + cy.get('@size').clear() + + cy.get('@size').type(`${size}`) + + cy.get('.dialog-menu button') + .eq(1) + .click() + .then(() => { + const payload = editor.command.getValue() + + const { + options: { watermark } + } = payload + + expect(watermark?.data).to.eq(text) + + expect(watermark?.size).to.eq(size) + }) + }) + }) + + it('删除水印', () => { + cy.getEditor().then((editor: Editor) => { + cy.get('.menu-item__watermark').click() + + cy.get('.menu-item__watermark li') + .eq(1) + .click() + .then(() => { + const payload = editor.command.getValue() + + const { + options: { watermark } + } = payload + + expect(watermark?.data).to.eq('') + + expect(watermark?.size).to.eq(200) + }) + }) + }) +}) diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 0000000..bbe48f0 --- /dev/null +++ b/cypress/fixtures/example.json @@ -0,0 +1,3 @@ +{ + "name": "canvas-editor" +} diff --git a/cypress/fixtures/test.png b/cypress/fixtures/test.png new file mode 100644 index 0000000..d05e470 Binary files /dev/null and b/cypress/fixtures/test.png differ diff --git a/cypress/global.d.ts b/cypress/global.d.ts new file mode 100644 index 0000000..fa9bc2c --- /dev/null +++ b/cypress/global.d.ts @@ -0,0 +1,13 @@ +/// + +declare namespace Editor { + import('../src/editor/index') + // eslint-disable-next-line @typescript-eslint/no-unused-vars + import Editor from '../src/editor/index' +} + +declare namespace Cypress { + interface Chainable { + getEditor(): Chainable + } +} diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts new file mode 100644 index 0000000..da232dc --- /dev/null +++ b/cypress/support/commands.ts @@ -0,0 +1,5 @@ +import 'cypress-file-upload' + +Cypress.Commands.add('getEditor', () => { + return cy.window().its('editor') +}) diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts new file mode 100644 index 0000000..43c03b7 --- /dev/null +++ b/cypress/support/e2e.ts @@ -0,0 +1 @@ +import './commands' diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 0000000..3ec51fd --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["es2015", "dom", "esnext"], + "types": ["cypress", "cypress-file-upload"], + "isolatedModules": false, + "allowJs": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + }, + "include": [ + "./**/*.ts" + ] +} \ No newline at end of file diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts new file mode 100644 index 0000000..1ec0e8f --- /dev/null +++ b/docs/.vitepress/config.ts @@ -0,0 +1,191 @@ +import { defineConfig } from 'vitepress' + +export default defineConfig({ + base: '/canvas-editor-docs/', + title: 'canvas-editor', + description: 'rich text editor by canvas/svg', + themeConfig: { + i18nRouting: false, + algolia: { + appId: 'RWSVW6F3S5', + apiKey: 'e462fffb4d2e9ab4a78c29e0b457ab33', + indexName: 'hufe' + }, + logo: '/favicon.png', + nav: [ + { + text: '指南', + link: '/guide/start', + activeMatch: '/guide/' + }, + { + text: 'Demo', + link: 'https://hufe.club/canvas-editor' + }, + { + text: '官方插件', + link: '/guide/plugin-internal.html' + }, + { + text: '赞助', + link: 'https://hufe.club/donate.jpg' + } + ], + sidebar: [ + { + text: '开始', + items: [ + { text: '入门', link: '/guide/start' }, + { text: '配置', link: '/guide/option' }, + { text: '国际化', link: '/guide/i18n' }, + { text: '数据结构', link: '/guide/schema' } + ] + }, + { + text: '命令', + items: [ + { text: '执行动作命令', link: '/guide/command-execute' }, + { text: '获取数据命令', link: '/guide/command-get' } + ] + }, + { + text: '监听', + items: [ + { text: '事件监听(listener)', link: '/guide/listener' }, + { text: '事件监听(eventBus)', link: '/guide/eventbus' } + ] + }, + { + text: '快捷键', + items: [ + { text: '内部快捷键', link: '/guide/shortcut-internal' }, + { text: '自定义快捷键', link: '/guide/shortcut-custom' } + ] + }, + { + text: '右键菜单', + items: [ + { text: '内部右键菜单', link: '/guide/contextmenu-internal' }, + { text: '自定义右键菜单', link: '/guide/contextmenu-custom' } + ] + }, + { + text: '重写方法', + items: [{ text: '重写方法', link: '/guide/override' }] + }, + { + text: 'API', + items: [ + { text: '实例API', link: '/guide/api-instance' }, + { text: '通用API', link: '/guide/api-common' } + ] + }, + { + text: '插件', + items: [ + { text: '自定义插件', link: '/guide/plugin-custom' }, + { text: '官方插件', link: '/guide/plugin-internal' } + ] + } + ], + socialLinks: [ + { + icon: 'github', + link: 'https://github.com/Hufe921/canvas-editor' + } + ], + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright © 2021-present Hufe' + } + }, + locales: { + root: { + label: '简体中文', + lang: 'zh-CN' + }, + en: { + label: 'English', + lang: 'en', + link: '/en/', + themeConfig: { + nav: [ + { + text: 'Guide', + link: '/en/guide/start', + activeMatch: '/en/guide/' + }, + { + text: 'Demo', + link: 'https://hufe.club/canvas-editor' + }, + { + text: 'Official plugin', + link: '/en/guide/plugin-internal.html' + }, + { + text: 'Donate', + link: 'https://hufe.club/donate.jpg' + } + ], + sidebar: [ + { + text: 'Start', + items: [ + { text: 'start', link: '/en/guide/start' }, + { text: 'option', link: '/en/guide/option' }, + { text: 'i18n', link: '/en/guide/i18n' }, + { text: 'schema', link: '/en/guide/schema' } + ] + }, + { + text: 'Command', + items: [ + { text: 'execute', link: '/en/guide/command-execute' }, + { text: 'get', link: '/en/guide/command-get' } + ] + }, + { + text: 'Listener', + items: [ + { text: 'listener', link: '/en/guide/listener' }, + { text: 'eventbus', link: '/en/guide/eventbus' } + ] + }, + { + text: 'Shortcut', + items: [ + { text: 'internal', link: '/en/guide/shortcut-internal' }, + { text: 'custom', link: '/en/guide/shortcut-custom' } + ] + }, + { + text: 'Contextmenu', + items: [ + { text: 'internal', link: '/en/guide/contextmenu-internal' }, + { text: 'custom', link: '/en/guide/contextmenu-custom' } + ] + }, + { + text: 'Override', + items: [{ text: 'override', link: '/en/guide/override' }] + }, + { + text: 'Api', + items: [ + { text: 'instance', link: '/en/guide/api-instance' }, + { text: 'common', link: '/en/guide/api-common' } + ] + }, + { + text: 'Plugin', + items: [ + { text: 'custom', link: '/en/guide/plugin-custom' }, + { text: 'official', link: '/en/guide/plugin-internal' } + ] + } + ] + } + } + } +}) diff --git a/docs/en/guide/api-common.md b/docs/en/guide/api-common.md new file mode 100644 index 0000000..4dc79a1 --- /dev/null +++ b/docs/en/guide/api-common.md @@ -0,0 +1,49 @@ +# Common API + +## splitText + +Feature: split text + +Usage: + +```javascript +import { splitText } from '@hufe921/canvas-editor' + +splitText(text: string): string[] +``` + +## createDomFromElementList + +Feature: Create a DOM tree based on the elementList + +Usage: + +```javascript +import { createDomFromElementList } from '@hufe921/canvas-editor' + +createDomFromElementList(elementList: IElement[], options?: IEditorOption): HTMLDivElement +``` + +## getElementListByHTML + +Feature: Create an elementList based on HTML + +Usage: + +```javascript +import { getElementListByHTML } from '@hufe921/canvas-editor' + +getElementListByHTML(htmlText: string, options: IGetElementListByHTMLOption): IElement[] +``` + +## getTextFromElementList + +Feature: Create text based on elementList + +Usage: + +```javascript +import { getTextFromElementList } from '@hufe921/canvas-editor' + +getTextFromElementList(elementList: IElement[]): string +``` diff --git a/docs/en/guide/api-instance.md b/docs/en/guide/api-instance.md new file mode 100644 index 0000000..2b810f3 --- /dev/null +++ b/docs/en/guide/api-instance.md @@ -0,0 +1,24 @@ +# Instance API + +## How to Use + +```javascript +import Editor from "@hufe921/canvas-editor" + +const instance = new Editor(container, data, options) +instance.apiName() +``` + +## destroy + +Feature: Destroy the editor + +Usage: + +```javascript +instance.destroy() +``` + +::: warning +Only destroy the editor DOM and related events, menu bars, toolbars, external variables, etc. need to be handled by themselves. +::: diff --git a/docs/en/guide/command-execute.md b/docs/en/guide/command-execute.md new file mode 100644 index 0000000..af54cb5 --- /dev/null +++ b/docs/en/guide/command-execute.md @@ -0,0 +1,1115 @@ +# Execute Command + +## How to Use + +```javascript +import Editor from "@hufe921/canvas-editor" + +const instance = new Editor(container, data, options) +instance.command.commandName() +``` + +## executeMode + +Feature: Switch editor mode (Edit, Clean, Read only) + +Usage: + +```javascript +instance.command.executeMode(editorMode: EditorMode) +``` + +## executeCut + +Feature: Cut + +Usage: + +```javascript +instance.command.executeCut() +``` + +## executeCopy + +Feature: Copy + +Usage: + +```javascript +instance.command.executeCopy(payload?: ICopyOption) +``` + +## executePaste + +Feature: Paste + +Usage: + +```javascript +instance.command.executePaste(payload?: IPasteOption) +``` + +## executeSelectAll + +Feature: Select all + +Usage: + +```javascript +instance.command.executeSelectAll() +``` + +## executeBackspace + +Feature: Forward delete + +Usage: + +```javascript +instance.command.executeBackspace() +``` + +## executeSetRange + +Feature: Set range + +Usage: + +```javascript +instance.command.executeSetRange( + startIndex: number, + endIndex: number, + tableId?: string, + startTdIndex?: number, + endTdIndex?: number, + startTrIndex?: number, + endTrIndex?: number +) +``` + +## executeReplaceRange + +Feature: Replace range + +Usage: + +```javascript +instance.command.executeReplaceRange(range: IRange) +``` + +## executeSetPositionContext + +Feature: Set position context + +Usage: + +```javascript +instance.command.executeSetPositionContext(range: IRange) +``` + +## executeForceUpdate + +Feature: force update editor + +Usage: + +```javascript +instance.command.executeForceUpdate(options?: IForceUpdateOption) +``` + +## executeBlur + +Feature: Set editor blur + +Usage: + +```javascript +instance.command.executeBlur() +``` + +## executeUndo + +Feature: Undo + +Usage: + +```javascript +instance.command.executeUndo() +``` + +## executeRedo + +Feature: Redo + +Usage: + +```javascript +instance.command.executeRedo() +``` + +## executePainter + +Feature: Format Brush - Copy style + +Usage: + +```javascript +instance.command.executePainter() +``` + +## executeApplyPainterStyle + +Feature: Format brush - Apply style + +Usage: + +```javascript +instance.command.executeApplyPainterStyle() +``` + +## executeFormat + +Feature: Clear format + +Usage: + +```javascript +instance.command.executeFormat(options?: IRichtextOption) +``` + +## executeFont + +Feature: Set font + +Usage: + +```javascript +instance.command.executeFont(font: string, options?: IRichtextOption) +``` + +## executeSize + +Feature: Set font size + +Usage: + +```javascript +instance.command.executeSize(size: number, options?: IRichtextOption) +``` + +## executeSizeAdd + +Feature: Increase the font size + +Usage: + +```javascript +instance.command.executeSizeAdd(options?: IRichtextOption) +``` + +## executeSizeMinus + +Feature: Reduce the font size + +Usage: + +```javascript +instance.command.executeSizeMinus(options?: IRichtextOption) +``` + +## executeBold + +Feature: Bold + +Usage: + +```javascript +instance.command.executeBold(options?: IRichtextOption) +``` + +## executeItalic + +Feature: Italic + +Usage: + +```javascript +instance.command.executeItalic(options?: IRichtextOption) +``` + +## executeUnderline + +Feature: Underline + +Usage: + +```javascript +instance.command.executeUnderline(textDecoration?: ITextDecoration, options?: IRichtextOption) +``` + +## executeStrikeout + +Feature: Strikeout + +Usage: + +```javascript +instance.command.executeStrikeout(options?: IRichtextOption) +``` + +## executeSuperscript + +Feature: Superscript + +Usage: + +```javascript +instance.command.executeSuperscript(options?: IRichtextOption) +``` + +## executeSubscript + +Feature: Subscript + +Usage: + +```javascript +instance.command.executeSubscript(options?: IRichtextOption) +``` + +## executeColor + +Feature: Font color + +Usage: + +```javascript +instance.command.executeColor(color: string | null, options?: IRichtextOption) +``` + +## executeHighlight + +Feature: Highlight + +Usage: + +```javascript +instance.command.executeHighlight(color: string | null, options?: IRichtextOption) +``` + +## executeTitle + +Feature: Set title + +Usage: + +```javascript +instance.command.executeTitle(TitleLevel | null) +``` + +## executeList + +Feature: Set list + +Usage: + +```javascript +instance.command.executeList(listType: ListType | null, listStyle?: ListStyle) +``` + +## executeRowFlex + +Feature: Line alignment + +Usage: + +```javascript +instance.command.executeRowFlex(rowFlex: RowFlex) +``` + +## executeRowMargin + +Feature: Line spacing + +Usage: + +```javascript +instance.command.executeRowMargin(rowMargin: number) +``` + +## executeInsertTable + +Feature: Insert table + +Usage: + +```javascript +instance.command.executeInsertTable(row: number, col: number) +``` + +## executeInsertTableTopRow + +Feature: Insert a row up + +Usage: + +```javascript +instance.command.executeInsertTableTopRow() +``` + +## executeInsertTableBottomRow + +Feature: Insert a row down + +Usage: + +```javascript +instance.command.executeInsertTableBottomRow() +``` + +## executeInsertTableLeftCol + +Feature: Insert a column to the left + +Usage: + +```javascript +instance.command.executeInsertTableLeftCol() +``` + +## executeInsertTableRightCol + +Feature: Insert a column to the right + +Usage: + +```javascript +instance.command.executeInsertTableRightCol() +``` + +## executeDeleteTableRow + +Feature: Deletes the current row + +Usage: + +```javascript +instance.command.executeDeleteTableRow() +``` + +## executeDeleteTableCol + +Feature: Delete the current column + +Usage: + +```javascript +instance.command.executeDeleteTableCol() +``` + +## executeDeleteTable + +Feature: Delete the table + +Usage: + +```javascript +instance.command.executeDeleteTable() +``` + +## executeMergeTableCell + +Feature: Merge tables + +Usage: + +```javascript +instance.command.executeMergeTableCell() +``` + +## executeCancelMergeTableCell + +Feature: Cancel the merge form + +Usage: + +```javascript +instance.command.executeCancelMergeTableCell() +``` + +## executeSplitVerticalTableCell + +Feature: Split table cell (vertical) + +Usage: + +```javascript +instance.command.executeSplitVerticalTableCell() +``` + +## executeSplitHorizontalTableCell + +Feature: Split table cell (horizontal) + +Usage: + +```javascript +instance.command.executeSplitHorizontalTableCell() +``` + +## executeTableTdVerticalAlign + +Feature: Table cell vertical alignment + +Usage: + +```javascript +instance.command.executeTableTdVerticalAlign(payload: VerticalAlign) +``` + +## executeTableBorderType + +Feature: Table border type + +Usage: + +```javascript +instance.command.executeTableBorderType(payload: TableBorder) +``` + +## executeTableBorderColor + +Feature: Table border color + +Usage: + +```javascript +instance.command.executeTableBorderColor(payload: string) +``` + +## executeTableTdBorderType + +Feature: Table td border type + +Usage: + +```javascript +instance.command.executeTableTdBorderType(payload: TdBorder) +``` + +## executeTableTdSlashType + +Feature: Table td slash type + +Usage: + +```javascript +instance.command.executeTableTdSlashType(payload: TdSlash) +``` + +## executeTableTdBackgroundColor + +Feature: Table cell background color + +Usage: + +```javascript +instance.command.executeTableTdBackgroundColor(payload: string) +``` + +## executeTableSelectAll + +Feature: Select the entire table + +Usage: + +```javascript +instance.command.executeTableSelectAll() +``` + +## executeImage + +Feature: Insert a picture + +Usage: + +```javascript +instance.command.executeImage({ + id?: string; + width: number; + height: number; + value: string; + imgDisplay?: ImageDisplay; +}) +``` + +## executeHyperlink + +Feature: Insert a link + +Usage: + +```javascript +instance.command.executeHyperlink({ + type: ElementType.HYPERLINK, + value: string, + url: string, + valueList: IElement[] +}) +``` + +## executeDeleteHyperlink + +Feature: Delete the link + +Usage: + +```javascript +instance.command.executeDeleteHyperlink() +``` + +## executeCancelHyperlink + +Feature: Unlink + +Usage: + +```javascript +instance.command.executeCancelHyperlink() +``` + +## executeEditHyperlink + +Feature: Edit the link + +Usage: + +```javascript +instance.command.executeEditHyperlink(newUrl: string) +``` + +## executeSeparator + +Feature: Insert a dividing line + +Usage: + +```javascript +instance.command.executeSeparator(dashArray: number[]) +``` + +## executePageBreak + +Feature: Page breaks + +Usage: + +```javascript +instance.command.executePageBreak() +``` + +## executeAddWatermark + +Feature: Add a watermark + +Usage: + +```javascript +instance.command.executeAddWatermark({ + data: string; + color?: string; + opacity?: number; + size?: number; + font?: string; +}) +``` + +## executeDeleteWatermark + +Feature: Remove the watermark + +Usage: + +```javascript +instance.command.executeDeleteWatermark() +``` + +## executeSearch + +Feature: 搜索 + +Usage: + +```javascript +instance.command.executeSearch(keyword: string) +``` + +## executeSearchNavigatePre + +Feature: Search Navigation - Previous + +Usage: + +```javascript +instance.command.executeSearchNavigatePre() +``` + +## executeSearchNavigateNext + +Feature: Search Navigation - Next + +Usage: + +```javascript +instance.command.executeSearchNavigateNext() +``` + +## executeReplace + +Feature: Search for replacement + +Usage: + +```javascript +instance.command.executeReplace(newWord: string, option?: IReplaceOption) +``` + +## executePrint + +Feature: Print + +Usage: + +```javascript +instance.command.executePrint() +``` + +## executeReplaceImageElement + +Feature: Replace the picture + +Usage: + +```javascript +instance.command.executeReplaceImageElement(newUrl: string) +``` + +## executeSaveAsImageElement + +Feature: Save as picture + +Usage: + +```javascript +instance.command.executeSaveAsImageElement() +``` + +## executeChangeImageDisplay + +Feature: Change how image rows are displayed + +Usage: + +```javascript +instance.command.executeChangeImageDisplay(element: IElement, display: ImageDisplay) +``` + +## executePageMode + +Feature: Page mode + +Usage: + +```javascript +instance.command.executePageMode(pageMode: PageMode) +``` + +## executePageScale + +Feature: Set page scale + +Usage: + +```javascript +instance.command.executePageScale(scale: number) +``` + +## executePageScaleRecovery + +Feature: Restore the original zoom factor of the page + +Usage: + +```javascript +instance.command.executePageScaleRecovery() +``` + +## executePageScaleMinus + +Feature: Page zoom out + +Usage: + +```javascript +instance.command.executePageScaleMinus() +``` + +## executePageScaleAdd + +Feature: Page zoom in + +Usage: + +```javascript +instance.command.executePageScaleAdd() +``` + +## executePaperSize + +Feature: Set the paper size + +Usage: + +```javascript +instance.command.executePaperSize(width: number, height: number) +``` + +## executePaperDirection + +Feature: Set the paper orientation + +Usage: + +```javascript +instance.command.executePaperDirection(paperDirection: PaperDirection) +``` + +## executeSetPaperMargin + +Feature: Set the paper margins + +Usage: + +```javascript +instance.command.executeSetPaperMargin([top: number, right: number, bottom: number, left: number]) +``` + +## executeSetMainBadge + +Feature: Set main badge + +Usage: + +```javascript +instance.command.executeSetMainBadge(payload: IBadge | null) +``` + +## executeSetAreaBadge + +Feature: Set area badge + +Usage: + +```javascript +instance.command.executeSetAreaBadge(payload: IAreaBadge[]) +``` + +## executeInsertElementList + +Feature: Insert an element + +Usage: + +```javascript +instance.command.executeInsertElementList(elementList: IElement[], options?: IInsertElementListOption) +``` + +## executeAppendElementList + +Feature: Append elements + +Usage: + +```javascript +instance.command.executeAppendElementList(elementList: IElement[], options?: IAppendElementListOption) +``` + +## executeUpdateElementById + +Feature: Update element by id + +Usage: + +```javascript +instance.command.executeUpdateElementById(payload: IUpdateElementByIdOption) +``` + +## executeDeleteElementById + +Feature: Delete element by id + +Usage: + +```javascript +instance.command.executeDeleteElementById(payload: IDeleteElementByIdOption) +``` + +## executeSetValue + +Feature: Set the editor data + +Usage: + +```javascript +instance.command.executeSetValue(payload: Partial, options?: ISetValueOption) +``` + +## executeRemoveControl + +Feature: Delete the control + +Usage: + +```javascript +instance.command.executeRemoveControl(payload?: IRemoveControlOption) +``` + +## executeSetLocale + +Feature: Set local language + +Usage: + +```javascript +instance.command.executeSetLocale(locale: string) +``` + +## executeLocationCatalog + +Feature: Locate directory location + +Usage: + +```javascript +instance.command.executeLocationCatalog(titleId: string) +``` + +## executeWordTool + +Feature: Word Tool (Delete blank lines and leading Spaces) + +Usage: + +```javascript +instance.command.executeWordTool() +``` + +## executeSetHTML + +Feature: Set the editor HTML data + +Usage: + +```javascript +instance.command.executeSetHTML(payload: Partialdata, options) +const value = instance.command.commandName() +``` + +## getValue + +Feature: Get the current document value + +Usage: + +```javascript +const { + version: string + data: IEditorData + options: IEditorOption +} = instance.command.getValue(options?: IGetValueOption) +``` + +## getValueAsync + +Feature: Get the current document value (async) + +Usage: + +```javascript +const { + version: string + data: IEditorData + options: IEditorOption +} = await instance.command.getValueAsync(options?: IGetValueOption) +``` + + +## getImage + +Feature: Gets the base64 string of the current page image + +Usage: + +```javascript +const base64StringList = await instance.command.getImage(option?: IGetImageOption) +``` + +## getOptions + +Feature: Get editor options + +Usage: + +```javascript +const editorOption = await instance.command.getOptions() +``` + +## getWordCount + +Feature: Get document word count + +Usage: + +```javascript +const wordCount = await instance.command.getWordCount() +``` + +## getCursorPosition + +Feature: Get cursor position with coordinates + +Usage: + +```javascript +const range = instance.command.getCursorPosition() +``` + +## getRange + +Feature: Get range + +Usage: + +```javascript +const range = instance.command.getRange() +``` + +## getRangeText + +Feature: Get range text + +Usage: + +```javascript +const rangeText = instance.command.getRangeText() +``` + +## getRangeContext + +Feature: Get range context + +Usage: + +```javascript +const rangeContext = instance.command.getRangeContext() +``` + +## getRangeRow + +Feature: Get range row element list + +Usage: + +```javascript +const rowElementList = instance.command.getRangeRow() +``` + +## getKeywordRangeList + +Feature: Get range list by keyword + +Usage: + +```javascript +const rangeList = instance.command.getKeywordRangeList() +``` + +## getKeywordContext + +Feature: Get context list by keyword + +Usage: + +```javascript +const keywordContextList = instance.command.getKeywordContext(payload: string) +``` + +## getRangeParagraph + +Feature: Get range paragraph element list + +Usage: + +```javascript +const paragraphElementList = instance.command.getRangeParagraph() +``` + +## getPaperMargin + +Feature: Gets the margins + +Usage: + +```javascript +const [top: number, right: number, bottom: number, left: number] = + instance.command.getPaperMargin() +``` + +## getSearchNavigateInfo + +Feature: Get search navigation information + +Usage: + +```javascript +const { + index: number; + count: number; +} = instance.command.getSearchNavigateInfo() +``` + +## getCatalog + +Feature: Get directory + +Usage: + +```javascript +const catalog = await instance.command.getCatalog() +``` + +## getHTML + +Feature: Get HTML + +Usage: + +```javascript +const { + header: string + main: string + footer: string +} = await instance.command.getHTML() +``` + +## getText + +Feature: Get text + +Usage: + +```javascript +const { + header: string + main: string + footer: string +} = await instance.command.getText() +``` + +## getLocale + +Feature: Get current locale + +Usage: + +```javascript +const locale = await instance.command.getLocale() +``` + +## getGroupIds + +Feature: Get all group ids + +Usage: + +```javascript +const groupIds = await instance.command.getGroupIds() +``` + +## getControlValue + +Feature: Get control value + +Usage: + +```javascript +const { + value: string | null + innerText: string | null + zone: EditorZone + elementList?: IElement[] +} = await instance.command.getControlValue(payload: IGetControlValueOption) +``` + +## getControlList + +Feature: Get control list + +Usage: + +```javascript +const controlList = await instance.command.getControlList() +``` + +## getContainer + +Feature: Get editor container + +Usage: + +```javascript +const container = await instance.command.getContainer() +``` + +## getTitleValue + +Feature: Get title value + +Usage: + +```javascript +const { + value: string | null + elementList: IElement[] + zone: EditorZone +}[] = await instance.command.getTitleValue(payload: IGetTitleValueOption) +``` + +## getPositionContextByEvent + +Feature: Get position context by mouse event + +Usage: + +```javascript +const { + pageNo: number + element: IElement | null + rangeRect: RangeRect | null + tableInfo: ITableInfoByEvent | null +}[] = await instance.command.getPositionContextByEvent(evt: MouseEvent, options?: IPositionContextByEventOption) +``` + +demo: + +```javascript +instance.eventBus.on( + 'mousemove', + debounce(evt => { + const positionContext = instance.command.getPositionContextByEvent(evt) + console.log(positionContext) + }, 200) +)`` +``` + +## getElementById + +Feature: Get element list by id + +Usage: + +```javascript +const elementList = await instance.command.getElementById(payload: IGetElementByIdOption) +``` + +## getAreaValue + +Feature: Get area value + +Usage: + +```javascript +const { + id?: string + area: IArea + value: IElement[] + startPageNo: number + endPageNo: number +} = instance.command.getAreaValue(options: IGetAreaValueOption) +``` diff --git a/docs/en/guide/contextmenu-custom.md b/docs/en/guide/contextmenu-custom.md new file mode 100644 index 0000000..1643254 --- /dev/null +++ b/docs/en/guide/contextmenu-custom.md @@ -0,0 +1,44 @@ +# Customize Contextmenu + +## How to Use + +```javascript +import Editor from "@hufe921/canvas-editor" + +const instance = new Editor(container, data, options) +instance.register.contextMenuList([ + { + key?: string; + isDivider?: boolean; + icon?: string; + name?: string; // Use %s for selection text. Example: Search: %s + shortCut?: string; + disable?: boolean; + when?: (payload: IContextMenuContext) => boolean; + callback?: (command: Command, context: IContextMenuContext) => any; + childMenus?: IRegisterContextMenu[]; + } + ]) +``` + +## getContextMenuList + +Feature: Get context menu list + +Usage: + +```javascript +const contextMenuList = await instance.register.getContextMenuList() +``` + +Remark: + +```javascript +// Example of modifying internal contextmenu +contextmenuList.forEach(menu => { + // Find the menu item through the menu key and modify its properties + if (menu.key === INTERNAL_CONTEXT_MENU_KEY.GLOBAL.PASTE) { + menu.when = () => false + } +}) +``` diff --git a/docs/en/guide/contextmenu-internal.md b/docs/en/guide/contextmenu-internal.md new file mode 100644 index 0000000..0900e86 --- /dev/null +++ b/docs/en/guide/contextmenu-internal.md @@ -0,0 +1,61 @@ +# Internal Contextmenu + +## Global + +- Cut +- Copy +- Paste +- Select all +- Print + +## Hyperlinks + +- Delete the link +- Unlink +- Edit the link + +## Image + +- Change the picture +- Save as picture +- Text wrapping + - Embed + - Up down + - Surround + - Float above text + - Float below text + +## Table + +- Table borders +- All borders +- Borderless +- Dashed border +- Outer border +- Internal border +- Td borders + - Top border + - Right border + - Bottom border + - Left border + - Forward border + - Back border +- Vertical alignment +- Top alignment +- Center vertically +- Bottom end alignment +- Insert rows and columns +- Insert 1 row above +- Insert 1 row below +- Insert 1 column on the left +- Insert 1 column on the right side +- Delete rows and columns +- Delete 1 row +- Remove 1 column +- Delete the entire table +- Merge cells +- Cancel the merge + +## control + +- Delete the control diff --git a/docs/en/guide/eventbus.md b/docs/en/guide/eventbus.md new file mode 100644 index 0000000..c9eaaee --- /dev/null +++ b/docs/en/guide/eventbus.md @@ -0,0 +1,234 @@ +# Event Listening(eventBus) + +## How to Use + +```javascript +import Editor from "@hufe921/canvas-editor" + +const instance = new Editor(container, data, options) + +// register +instance.eventBus.on( + eventName: K, + callback: EventMap[K] +) + +// remove +instance.eventBus.off( + eventName: K, + callback: EventMap[K] +) +``` + +## rangeStyleChange + +Feature: The selection style changes + +Usage: + +```javascript +instance.eventBus.on('rangeStyleChange', (payload: IRangeStyle) => void) +``` + +## visiblePageNoListChange + +Feature: The visible page changes + +Usage: + +```javascript +instance.eventBus.on('visiblePageNoListChange', (payload: number[]) => void) +``` + +## intersectionPageNoChange + +Feature: The current page changes + +Usage: + +```javascript +instance.eventBus.on('intersectionPageNoChange', (payload: number) => void) +``` + +## pageSizeChange + +Feature: The current number of pages has changed + +Usage: + +```javascript +instance.eventBus.on('pageSizeChange', (payload: number) => void) +``` + +## pageScaleChange + +Feature: The current page scaling has changed + +Usage: + +```javascript +instance.eventBus.on('pageScaleChange', (payload: number) => void) +``` + +## contentChange + +Feature: The current content has changed + +Usage: + +```javascript +instance.eventBus.on('contentChange', () => void) +``` + +## controlChange + +Feature: The control where the current cursor is located changes + +Usage: + +```javascript +instance.eventBus.on('controlChange', (payload: IControlChangeResult) => void) +``` + +## controlContentChange + +Feature: The control content changes + +Usage: + +```javascript +instance.eventBus.on('controlContentChange', (payload: IControlContentChangeResult) => void) +``` + +## pageModeChange + +Feature: The page mode changes + +Usage: + +```javascript +instance.eventBus.on('pageModeChange', (payload: PageMode) => void) +``` + +## saved + +Feature: Document saved + +Usage: + +```javascript +instance.eventBus.on('saved', (payload: IEditorResult) => void) +``` + +## zoneChange + +Feature: The zone changes + +Usage: + +```javascript +instance.eventBus.on('zoneChange', (payload: EditorZone) => void) +``` + +## mousemove + +Feature: Editor mousemove event + +Usage: + +```javascript +instance.eventBus.on('mousemove', (evt: MouseEvent) => void) +``` + +## mouseenter + +Feature: Editor mouseenter event + +Usage: + +```javascript +instance.eventBus.on('mouseenter', (evt: MouseEvent) => void) +``` + +## mouseleave + +Feature: Editor mouseleave event + +Usage: + +```javascript +instance.eventBus.on('mouseleave', (evt: MouseEvent) => void) +``` + +## mousedown + +Feature: Editor mousedown event + +Usage: + +```javascript +instance.eventBus.on('mousedown', (evt: MouseEvent) => void) +``` + +## mouseup + +Feature: Editor mouseup event + +Usage: + +```javascript +instance.eventBus.on('mouseup', (evt: MouseEvent) => void) +``` + +## click + +Feature: Editor click event + +Usage: + +```javascript +instance.eventBus.on('click', (evt: MouseEvent) => void) +``` + +## input + +Feature: Editor input event + +Usage: + +```javascript +instance.eventBus.on('input', (evt: Event) => void) +``` + +## positionContextChange + +Feature: The position context change + +Usage: + +```javascript +instance.eventBus.on('positionContextChange', (payload: IPositionContextChangePayload) => void) +``` + +## imageSizeChange + +Feature: The image size change + +Usage: + +```javascript +instance.eventBus.on('imageSizeChange', (payload: { element: IElement }) => void) +``` + +## imageMousedown + +Feature: The image mousedown event + +Usage: + +```javascript +instance.eventBus.on('imageMousedown', (payload: { + evt: MouseEvent + element: IElement +}) => void) +``` diff --git a/docs/en/guide/i18n.md b/docs/en/guide/i18n.md new file mode 100644 index 0000000..f7b3f43 --- /dev/null +++ b/docs/en/guide/i18n.md @@ -0,0 +1,112 @@ +# i18n + +## How to Use + +```javascript +import Editor from "@hufe921/canvas-editor" + +const instance = new Editor(container, data, options) + +// register +instance.register.langMap(locale: string, lang: ILang) + +// set locale +instance.command.executeSetLocale(locale) +``` + +## ILang + +```typescript +interface ILang { + contextmenu: { + global: { + cut: string + copy: string + paste: string + selectAll: string + print: string + } + control: { + delete: string + } + hyperlink: { + delete: string + cancel: string + edit: string + } + image: { + change: string + saveAs: string + textWrap: string + textWrapType: { + embed: string + upDown: string + surround: string + floatTop: string + floatBottom: string + } + } + table: { + insertRowCol: string + insertTopRow: string + insertBottomRow: string + insertLeftCol: string + insertRightCol: string + deleteRowCol: string + deleteRow: string + deleteCol: string + deleteTable: string + mergeCell: string + mergeCancelCell: string + verticalAlign: string + verticalAlignTop: string + verticalAlignMiddle: string + verticalAlignBottom: string + border: string + borderAll: string + borderEmpty: string + borderDash: string + borderExternal: string + borderInternal: string + borderTd: string + borderTdTop: string + borderTdRight: string + borderTdBottom: string + borderTdLeft: string + borderTdForward: string + borderTdBack: string + } + } + datePicker: { + now: string + confirm: string + return: string + timeSelect: string + weeks: { + sun: string + mon: string + tue: string + wed: string + thu: string + fri: string + sat: string + } + year: string + month: string + hour: string + minute: string + second: string + } + frame: { + header: string + footer: string + } + pageBreak: { + displayName: string + } + zone: { + headerTip: string + footerTip: string + } +} +``` diff --git a/docs/en/guide/listener.md b/docs/en/guide/listener.md new file mode 100644 index 0000000..3a76523 --- /dev/null +++ b/docs/en/guide/listener.md @@ -0,0 +1,126 @@ +# Event Listening(listener) + +::: warning +The listener can only respond to one method, and no new listening methods will be added in the future. It is recommended to use eventBus for event listening. +::: + +## How to Use + +```javascript +import Editor from "@hufe921/canvas-editor" + +const instance = new Editor(container, data, options) +instance.listener.eventName = ()=>{} +``` + +## rangeStyleChange + +Feature: The selection style changes + +Usage: + +```javascript +instance.listener.rangeStyleChange = (payload: IRangeStyle) => {} +``` + +## visiblePageNoListChange + +Feature: The visible page changes + +Usage: + +```javascript +instance.listener.visiblePageNoListChange = (payload: number[]) => {} +``` + +## intersectionPageNoChange + +Feature: The current page changes + +Usage: + +```javascript +instance.listener.intersectionPageNoChange = (payload: number) => {} +``` + +## pageSizeChange + +Feature: The current page size has changed + +Usage: + +```javascript +instance.listener.pageSizeChange = (payload: number) => {} +``` + +## pageScaleChange + +Feature: The current page scaling has changed + +Usage: + +```javascript +instance.listener.pageScaleChange = (payload: number) => {} +``` + +## contentChange + +Feature: The current content has changed + +Usage: + +```javascript +instance.listener.contentChange = () => {} +``` + +## controlChange + +Feature: The control where the current cursor is located changes + +Usage: + +```javascript +instance.listener.controlChange = (payload: IControlChangeResult) => {} +``` + +## controlContentChange + +Feature: The control content changes + +Usage: + +```javascript +instance.listener.controlContentChange = ( + payload: IControlContentChangeResult +) => {} +``` + +## pageModeChange + +Feature: The page mode changes + +Usage: + +```javascript +instance.listener.pageModeChange = (payload: PageMode) => {} +``` + +## saved + +Feature: Document saved + +Usage: + +```javascript +instance.listener.saved = (payload: IEditorResult) => {} +``` + +## zoneChange + +Feature: 区域发生改变 + +Usage: + +```javascript +instance.listener.zoneChange = (payload: EditorZone) => {} +``` diff --git a/docs/en/guide/option.md b/docs/en/guide/option.md new file mode 100644 index 0000000..0b86d0f --- /dev/null +++ b/docs/en/guide/option.md @@ -0,0 +1,189 @@ +# Configuration + +## How to Use? + +```javascript +import Editor from "@hufe921/canvas-editor" + +new Editor(container, IEditorData | IElement[], { + // option +}) +``` + +## Complete Configuration + +```typescript +interface IEditorOption { + mode?: EditorMode // Editor mode: Edit, Clean (Visual aids are not displayed, For example: page break), ReadOnly, Form (Only editable within the control), Print (Visual aids are not displayed, Unwritten content control), Design (Do not handle configurations such as non deletable and read-only). default: Edit + locale?: string // Language. default: zhCN + defaultType?: string // Default element type. default: TEXT + defaultColor?: string // Default color. default: #000000 + defaultFont?: string // Default font. default: Microsoft YaHei + defaultSize?: number // Default font size. default: 16 + minSize?: number // Min font size。default: 5 + maxSize?: number // Max font size。default: 72 + defaultBasicRowMarginHeight?: number // Default line height。default: 8 + defaultRowMargin?: number // Default line spacing. default: 1 + defaultTabWidth?: number // Default tab width. default: 32 + width?: number // Paper width. default: 794 + height?: number // Paper height. default: 1123 + scale?: number // scaling. default: 1 + pageGap?: number // Paper spacing. default: 20 + underlineColor?: string // Underline color. default: #000000 + strikeoutColor?: string // Strikeout color. default: #FF0000 + rangeColor?: string // Range color. default: #AECBFA + rangeAlpha?: number // Range transparency. default: 0.6 + rangeMinWidth?: number // Range min width. default: 5 + searchMatchColor?: string // Search for highlight color. default: #FFFF00 + searchNavigateMatchColor?: string // Search navigation highlighted color.default: #AAD280 + searchMatchAlpha?: number // Search for highlight transparency. default: 0.6 + highlightAlpha?: number // Highlight element transparency. default: 0.6 + highlightMarginHeight?: number // Highlight element margin height. default: 8 + resizerColor?: string // Image sizer color. default: #4182D9 + resizerSize?: number // Image sizer size. default: 5 + marginIndicatorSize?: number // The margin indicator length. default: 35 + marginIndicatorColor?: string // The margin indicator color. default: #BABABA + margins?: IMargin // Page margins. default: [100, 120, 100, 120] + pageMode?: PageMode // Paper mode: Linkage, Pagination. default: Pagination + renderMode?: RenderMode // Render mode: speed(multi words combination rendering), compatibility(word by word rendering:avoid environmental differences such as browse,fonts...). default: speed + defaultHyperlinkColor?: string // Default hyperlink color. default: #0000FF + table?: ITableOption // table configuration {tdPadding?:IPadding; defaultTrMinHeight?:number; defaultColMinWidth?:number} + header?: IHeader // Header information.{top?:number; maxHeightRadio?:MaxHeightRatio;} + footer?: IFooter // Footer information. {bottom?:number; maxHeightRadio?:MaxHeightRatio;} + pageNumber?: IPageNumber // Page number information. {bottom:number; size:number; font:string; color:string; rowFlex:RowFlex; format:string; numberType:NumberType;} + paperDirection?: PaperDirection // Paper orientation: portrait, landscape + inactiveAlpha?: number // When the body content is out of focus, transparency. default: 0.6 + historyMaxRecordCount?: number // History (undo redo) maximum number of records. default: 100 + printPixelRatio?: number // Print the pixel ratio (larger values are clearer, but larger sizes). default: 3 + maskMargin?: IMargin // Masking margins above the editor(for example: menu bar, bottom toolbar)。default: [0, 0, 0, 0] + letterClass?: string[] // Alphabet class supported by typesetting. default: a-zA-Z. Built-in alternative alphabet class: LETTER_CLASS + contextMenuDisableKeys?: string[] // Disable context menu keys. default: [] + shortcutDisableKeys?: string[] // Disable shortcut keys. default: [] + scrollContainerSelector?: string // scroll container selector. default: document + pageOuterSelectionDisable?: boolean // Disable selection when the mouse moves out of the page. default: false + wordBreak?: WordBreak // Word and punctuation breaks: No punctuation in the first line of the BREAK_WORD &The word is not split, and the line is folded after BREAK_ALL full according to the width of the character. default: BREAK_WORD + watermark?: IWatermark // Watermark{data:string; color?:string; opacity?:number; size?:number; font?:string; numberType:NumberType;} + control?: IControlOption // Control {placeholderColor?:string; bracketColor?:string; prefix?:string; postfix?:string; borderWidth?: number; borderColor?: string; activeBackgroundColor?: string; disabledBackgroundColor?: string; existValueBackgroundColor?: string; noValueBackgroundColor?: string;} + checkbox?: ICheckboxOption // Checkbox {width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string; verticalAlign?: VerticalAlign;} + radio?: IRadioOption // Radio {width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string; verticalAlign?: VerticalAlign;} + cursor?: ICursorOption // Cursor style. {width?: number; color?: string; dragWidth?: number; dragColor?: string; dragFloatImageDisabled?: boolean;} + title?: ITitleOption // Title configuration.{ defaultFirstSize?: number; defaultSecondSize?: number; defaultThirdSize?: number defaultFourthSize?: number; defaultFifthSize?: number; defaultSixthSize?: number;} + placeholder?: IPlaceholder // Placeholder text + group?: IGroup // Group option. {opacity?:number; backgroundColor?:string; activeOpacity?:number; activeBackgroundColor?:string; disabled?:boolean; deletable?:boolean;} + pageBreak?: IPageBreak // PageBreak option。{font?:string; fontSize?:number; lineDash?:number[];} + zone?: IZoneOption // Zone option。{tipDisabled?:boolean;} + background?: IBackgroundOption // Background option. {color?:string; image?:string; size?:BackgroundSize; repeat?:BackgroundRepeat; applyPageNumbers?:number[]}。default: {color: '#FFFFFF'} + lineBreak?: ILineBreakOption // LineBreak option. {disabled?:boolean; color?:string; lineWidth?:number;} + separator?: ISeparatorOption // Separator option. {lineWidth?:number; strokeStyle?:string;} + lineNumber?: ILineNumberOption // LineNumber option. {size?:number; font?:string; color?:string; disabled?:boolean; right?:number} + pageBorder?: IPageBorderOption // PageBorder option. {color?:string; lineWidth:number; padding?:IPadding; disabled?:boolean;} + badge?: IBadgeOption // Badge option. {top?:number; left?:number} + modeRule?: IModeRule // mode rule option. {print:{imagePreviewerDisabled?: boolean}; readonly:{imagePreviewerDisabled?: boolean}; form:{controlDeletableDisabled?: boolean}} +} +``` + +## Table Configuration + +```typescript +interface ITableOption { + tdPadding?: IPadding // Cell padding. default: [0, 5, 5, 5] + defaultTrMinHeight?: number // Default table row minimum height. default: 42 + defaultColMinWidth?: number // Default minimum width for table columns (applied if the overall width is sufficient, otherwise +} +``` + +## Header Configuration + +```typescript +interface IHeader { + top?: number // Size from the top of the page.default: 30 + inactiveAlpha?: number // Transparency during deactivation. default: 1 + maxHeightRadio?: MaxHeightRatio // Occupies the maximum height ratio of the page.default: HALF + disabled?: boolean // Whether to disable + editable?: boolean // Disable the header content from being edited +} +``` + +## Footer Configuration + +```typescript +interface IFooter { + bottom?: number // The size from the bottom of the page.default: 30 + inactiveAlpha?: number // Transparency during deactivation. default: 1 + maxHeightRadio?: MaxHeightRatio // Occupies the maximum height ratio of the page.default: HALF + disabled?: boolean // Whether to disable + editable?: boolean // Disable the footer content from being edited +} +``` + +## Page Number Configuration + +```typescript +interface IPageNumber { + bottom?: number // The size from the bottom of the page.default: 60 + size?: number // font size. default: 12 + font?: string // font. default: Microsoft YaHei + color?: string // font color. default: #000000 + rowFlex?: RowFlex // Line alignment. default: CENTER + format?: string // Page number format. default: {pageNo}。example:{pageNo}/{pageCount} + numberType?: NumberType // The numeric type. default: ARABIC + disabled?: boolean // Whether to disable + startPageNo?: number // Start page number.default: 1 + fromPageNo?: number // Page numbers appear from page number.default: 0 + maxPageNo?: number | null // Max page number(starting from 0).default: null +} +``` + +## Watermark Configuration + +```typescript +interface IWatermark { + data: string // text. + type?: WatermarkType + width?: number + height?: number + color?: string // color. default: #AEB5C0 + opacity?: number // transparency. default: 0.3 + size?: number // font size. default: 200 + font?: string // font. default: Microsoft YaHei + repeat?: boolean // repeat watermark. default: false + gap?: [horizontal: number, vertical: number] // watermark spacing. default: [10,10] + numberType?: NumberType // The numeric type. default: ARABIC +} +``` + +## Placeholder Text Configuration + +```typescript +interface IPlaceholder { + data: string // text. + color?: string // color. default: #DCDFE6 + opacity?: number // transparency. default: 1 + size?: number // font size. default: 16 + font?: string // font. default: Microsoft YaHei +} +``` + +## LineNumber Configuration + +```typescript +interface ILineNumberOption { + size?: number // font size. default: 12 + font?: string // font. default: Microsoft YaHei + color?: string // color. default: #000000 + disabled?: boolean // Whether to disable. default: false + right?: number // Distance from the main text. default: 20 + type?: LineNumberType // Number type (renumber each page, consecutive numbering). default: continuity +} +``` + +## PageBorder Configuration + +```typescript +interface IPageBorderOption { + color?: string // color. default: #000000 + lineWidth?: number // line width. default: 1 + padding?: IPadding // padding. default: [0, 0, 0, 0] + disabled?: boolean // Whether to disable. default: true +} +``` diff --git a/docs/en/guide/override.md b/docs/en/guide/override.md new file mode 100644 index 0000000..61e653c --- /dev/null +++ b/docs/en/guide/override.md @@ -0,0 +1,47 @@ +# Override + +## How to Use + +```javascript +import Editor from "@hufe921/canvas-editor" + +const instance = new Editor(container, data, options) + +instance.override.overrideFunction = () => unknown | IOverrideResult +``` + +```typescript +interface IOverrideResult { + preventDefault?: boolean // Prevent the execution of internal default method. Default prevent +} +``` + +## paste + +Feature: Override internal paste function + +Usage: + +```javascript +instance.override.paste = (evt?: ClipboardEvent) => unknown | IOverrideResult +``` + +## copy + +Feature: Override internal copy function + +Usage: + +```javascript +instance.override.copy = () => unknown | IOverrideResult +``` + +## drop + +Feature: Override internal drop function + +Usage: + +```javascript +instance.override.drop = (evt: DragEvent) => unknown | IOverrideResult +``` diff --git a/docs/en/guide/plugin-custom.md b/docs/en/guide/plugin-custom.md new file mode 100644 index 0000000..a0df005 --- /dev/null +++ b/docs/en/guide/plugin-custom.md @@ -0,0 +1,25 @@ +# Custom Plugin + +::: tip +Official plugin: https://github.com/Hufe921/canvas-editor-plugin +::: + +## Write a Plugin + +```javascript +export function myPlugin(editor: Editor, options?: Option) { + // 1. update,see more:src/plugins/copy + editor.command.updateFunction = () => {} + + // 2. add,see more:src/plugins/markdown + editor.command.addFunction = () => {} + + // 3. listener, eventbus, shortcut, contextmenu, override... +} +``` + +## Use Plugin + +```javascript +instance.use(myPlugin, options?: Option) +``` diff --git a/docs/en/guide/plugin-internal.md b/docs/en/guide/plugin-internal.md new file mode 100644 index 0000000..0d30344 --- /dev/null +++ b/docs/en/guide/plugin-internal.md @@ -0,0 +1,125 @@ +# Official plugin + +::: tip +Official plugin: https://github.com/Hufe921/canvas-editor-plugin + +Official plugin demo: https://hufe.club/canvas-editor-plugin +::: + +## Barcode1d + +```javascript +import Editor from "@hufe921/canvas-editor" +import barcode1DPlugin from "@hufe921/canvas-editor-plugin-barcode1d" + +const instance = new Editor() +instance.use(barcode1DPlugin) + +instance.executeInsertBarcode1D( + content: string, + width: number, + height: number, + options?: JsBarcode.Options +) +``` + +## Barcode2d + +```javascript +import Editor from "@hufe921/canvas-editor" +import barcode2DPlugin from "@hufe921/canvas-editor-plugin-barcode2d" + +const instance = new Editor() +instance.use(barcode2DPlugin, options?: IBarcode2DOption) + +instance.executeInsertBarcode2D( + content: string, + width: number, + height: number, + hints?: Map +) +``` + +## Code block + +```javascript +import Editor from "@hufe921/canvas-editor" +import codeblockPlugin from "@hufe921/canvas-editor-plugin-codeblock" + +const instance = new Editor() +instance.use(codeblockPlugin) + +instance.executeInsertCodeblock(content: string) +``` + +## Word + +```javascript +import Editor from '@hufe921/canvas-editor' +import docxPlugin from '@hufe921/canvas-editor-plugin-docx' + +const instance = new Editor() +instance.use(docxPlugin) + +command.executeImportDocx({ + arrayBuffer: buffer +}) + +instance.executeExportDocx({ + fileName: string +}) +``` + +## Excel + +```javascript +import Editor from '@hufe921/canvas-editor' +import excelPlugin from '@hufe921/canvas-editor-plugin-excel' + +const instance = new Editor() +instance.use(excelPlugin) + +command.executeImportExcel({ + arrayBuffer: buffer +}) +``` + +## Floating toolbar + +```javascript +import Editor from '@hufe921/canvas-editor' +import floatingToolbarPlugin from '@hufe921/canvas-editor-plugin-floating-toolbar' + +const instance = new Editor() +instance.use(floatingToolbarPlugin) +``` + +## Diagram + +```javascript +import Editor from '@hufe921/canvas-editor' +import diagramPlugin from '@hufe921/canvas-editor-plugin-diagram' + +const instance = new Editor() +instance.use(diagramPlugin) + +command.executeLoadDiagram({ + lang?: Lang + data?: string + onDestroy?: (message?: any) => void +}) +``` + +## Convert uppercase and lowercase + +```javascript +import Editor from '@hufe921/canvas-editor' +import casePlugin from '@hufe921/canvas-editor-plugin-case' + +const instance = new Editor() +instance.use(casePlugin) + +command.executeUpperCase() + +command.executeLowerCase() +``` diff --git a/docs/en/guide/schema.md b/docs/en/guide/schema.md new file mode 100644 index 0000000..7dc3e2a --- /dev/null +++ b/docs/en/guide/schema.md @@ -0,0 +1,209 @@ +# Data Structure + +```typescript +interface IElement { + // basic + id?: string; + type?: { + TEXT = 'text', + IMAGE = 'image', + TABLE = 'table', + HYPERLINK = 'hyperlink', + SUPERSCRIPT = 'superscript', + SUBSCRIPT = 'subscript', + SEPARATOR = 'separator', + PAGE_BREAK = 'pageBreak', + CONTROL = 'control', + CHECKBOX = 'checkbox', + RADIO = 'radio', + LATEX = 'latex', + TAB = 'tab', + DATE = 'date', + BLOCK = 'block' + }; + value: string; + valueList?: IElement[]; // Use of composite elements (hyperlinks, titles, lists, and so on). + extension?: unknown; + externalId?: string; + hide?: boolean; + // style + font?: string; + size?: number; + width?: number; + height?: number; + bold?: boolean; + color?: string; + highlight?: string; + italic?: boolean; + underline?: boolean; + strikeout?: boolean; + rowFlex?: { + LEFT = 'left', + CENTER = 'center', + RIGHT = 'right', + ALIGNMENT = 'alignment', + JUSTIFY = 'justify' + }; + rowMargin?: number; + letterSpacing?: number; + textDecoration?: { + style?: TextDecorationStyle; + }; + // groupIds + groupIds?: string[]; + // table + conceptId?: string; + colgroup?: { + width: number; + }[]; + trList?: { + height: number; + pagingRepeat?: boolean; + extension?: unknown; + externalId?: string; + tdList: { + colspan: number; + rowspan: number; + conceptId?: string; + verticalAlign?: VerticalAlign; + backgroundColor?: string; + borderTypes?: TdBorder[]; + slashTypes?: TdSlash[]; + value: IElement[]; + extension?: unknown; + externalId?: string; + disabled?: boolean; + deletable?: boolean; + }[]; + }[]; + borderType?: TableBorder; + borderColor?: string; + borderWidth?: number; + borderExternalWidth?: number; + tableToolDisabled?: boolean; + // Hyperlinks + url?: string; + // Superscript and subscript + actualSize?: number; + // Dividing line + dashArray?: number[]; + // control + control?: { + type: { + TEXT = 'text', + SELECT = 'select', + CHECKBOX = 'checkbox', + RADIO = 'radio', + DATE = 'date', + NUMBER = 'number' + }; + value: IElement[] | null; + placeholder?: string; + groupId?: string; + conceptId?: string; + prefix?: string; + postfix?: string; + preText?: string; + postText?: string; + minWidth?: number; + underline?: boolean; + border?: boolean; + extension?: unknown; + indentation?: ControlIndentation; + rowFlex?: RowFlex + deletable?: boolean; + disabled?: boolean; + pasteDisabled?: boolean; + hide?: boolean; + code: string | null; + min?: number; + max?: number; + flexDirection: FlexDirection; + valueSets: { + value: string; + code: string; + }[]; + isMultiSelect?: boolean; + multiSelectDelimiter?: string; + dateFormat?: string; + font?: string; + size?: number; + bold?: boolean; + color?: string; + highlight?: string; + italic?: boolean; + strikeout?: boolean; + selectExclusiveOptions?: { + inputAble?: boolean; + } + }; + controlComponent?: { + PREFIX = 'prefix', + POSTFIX = 'postfix', + PLACEHOLDER = 'placeholder', + VALUE = 'value', + CHECKBOX = 'checkbox', + RADIO = 'radio' + }; + // checkbox + checkbox?: { + value: boolean | null; + }; + // radio + radio?: { + value: boolean | null; + }; + // LaTeX + laTexSVG?: string; + // date + dateFormat?: string; + // picture + imgDisplay?: { + INLINE = 'inline', + BLOCK = 'block' + } + imgFloatPosition?: { + x: number; + y: number; + pageNo?: number; + } + imgToolDisabled?: boolean; + // block + block?: { + type: { + IFRAME = 'iframe', + VIDEO = 'video' + }; + iframeBlock?: { + src?: string; + srcdoc?: string; + }; + videoBlock?: { + src: string; + }; + }; + // title + level?: TitleLevel; + title?: { + conceptId?: string; + deletable?: boolean; + disabled?: boolean; + }; + // list + listType?: ListType; + listStyle?: ListStyle; + listWrap?: boolean; + // area + areaId?: string; + area?: { + extension?: unknown; + top?: number; + hide?: boolean; + borderColor?: string; + backgroundColor?: string; + mode?: AreaMode; + deletable?: boolean; + placeholder?: IPlaceholder; + }; +} +``` diff --git a/docs/en/guide/shortcut-custom.md b/docs/en/guide/shortcut-custom.md new file mode 100644 index 0000000..2829cf6 --- /dev/null +++ b/docs/en/guide/shortcut-custom.md @@ -0,0 +1,22 @@ +# Custom shortcut keys + +## How to use? + +```javascript +import Editor from "@hufe921/canvas-editor" + +const instance = new Editor(container, data, options) +instance.register.shortcutList([ + { + key: KeyMap; + ctrl?: boolean; + meta?: boolean; + mod?: boolean; // windows:ctrl || mac:command + shift?: boolean; + alt?: boolean; + isGlobal?: boolean; + callback?: (command: Command) => any; + disable?: boolean; + } + ]) +``` diff --git a/docs/en/guide/shortcut-internal.md b/docs/en/guide/shortcut-internal.md new file mode 100644 index 0000000..340711a --- /dev/null +++ b/docs/en/guide/shortcut-internal.md @@ -0,0 +1,189 @@ +# Internal shortcut keys + +## Backspace + +Feature: Forward delete + +## Delete + +Feature: Backward delete + +## Enter + +Feature: Line break + +## Shift + Enter + +Feature: Line breaks within the list + +## ← + +Feature: Move to the left + +## Shift + ← + +Feature: Zoom the selection to the left + +## Ctrl/Cmd + ← + +Feature: Move to the left(jump word) + +## Ctrl/Cmd + Shift + ← + +Feature: Zoom the selection to the left(jump word) + +## → + +Feature: Move to the right + +## Shift + → + +Feature: Zoom the selection to the right + +## Ctrl/Cmd + → + +Feature: Move to the right(jump word) + +## Ctrl/Cmd + Shift + → + +Feature: Zoom the selection to the right(jump word) + +## ↑ + +Feature: Move up + +## Shift + ↑ + +Feature: Zoom up the selection + +## ↓ + +Feature: Move down + +## Shift + ↓ + +Feature: Zoom down the selection + +## Esc + +Feature: Exit format brush + +## Tab + +Feature: Increase indent/Move next control + +## Shift + Tab + +Feature: Move previous control + +## Ctrl/Cmd + Z + +Feature: Undo + +## Ctrl/Cmd + Y + +Feature: Redo + +## Ctrl/Cmd + C + +Feature: Copy + +## Ctrl/Cmd + X + +Feature: Cut + +## Ctrl/Cmd + A + +Feature: Select all + +## Ctrl/Cmd + S + +Feature: Save + +## Ctrl/Cmd + { + +Feature: Increase the font + +## Ctrl/Cmd + } + +Feature: Reduce the font + +## Ctrl/Cmd + B + +Feature: Bold + +## Ctrl/Cmd + I + +Feature: Italic + +## Ctrl/Cmd + U + +Feature: Underline + +## Ctrl/Cmd + L + +Feature: Line left + +## Ctrl/Cmd + E + +Feature: Line center + +## Ctrl/Cmd + R + +Feature: Line right + +## Ctrl/Cmd + J + +Feature: Both sides are aligned + +## Ctrl/Cmd + Shift + J + +Feature: Line justify + +## Ctrl + Shift + X + +Feature: Strikethrough + +## Ctrl/Cmd + Shift + > + +Feature: Superscript + +## Ctrl/Cmd + Shift + < + +Feature: Subscript + +## Ctrl + Alt/Option + 0 + +Feature: Main body + +## Ctrl + Alt/Option + 1 + +Feature: Header1 + +## Ctrl + Alt/Option + 2 + +Feature: Header2 + +## Ctrl + Alt/Option + 3 + +Feature: Header3 + +## Ctrl + Alt/Option + 4 + +Feature: Header4 + +## Ctrl + Alt/Option + 5 + +Feature: Header5 + +## Ctrl + Alt/Option + 6 + +Feature: Header6 + +## Ctrl/Cmd + Shift + I + +Feature: Unordered list + +## Ctrl/Cmd + Shift + U + +Feature: Ordered list diff --git a/docs/en/guide/start.md b/docs/en/guide/start.md new file mode 100644 index 0000000..a134bb0 --- /dev/null +++ b/docs/en/guide/start.md @@ -0,0 +1,97 @@ +# Getting Started + +> WYSIWYG rich text editor. + +Benefit from the complete self-implementation of cursor and text layout. The underlying rendering can also be rendered by svg, See code:[feature/svg](https://github.com/Hufe921/canvas-editor/tree/feature/svg); Or complete pdf drawing with pdfjs,See code:[feature/pdf](https://github.com/Hufe921/canvas-editor/tree/feature/pdf). + +::: warning +The official only provides the editor core layer npm package, the menu bar or other external tools can refer to the document extension, or directly refer the implementation of [official](https://github.com/Hufe921/canvas-editor), See details [demo](https://hufe.club/canvas-editor/). +::: + +## Features + +- Rich text operations (Undo, Redo, Font, Size, Bold, Italic, Underline, Strikeout, Superscript, Alignment, Title, List, ...) +- Insert elements (Table, Image, Link, Code Block, Page Break, Math Formula, Date Picker, Block, ...) +- Print (Based on canvas to picture, pdf drawing) +- Controls (Select, Text, Date, Radio, Checkbox) +- Right-click menu (Internal, Custom) +- Shortcut keys (Internal, Custom) +- Drag and Drop(Text, Element, Control) +- Header, Footer, Page Number +- Page Margin +- Watermark +- Pagination +- Comment +- Catalog +- [Plugin](https://github.com/Hufe921/canvas-editor-plugin) + +## TODO + +- Computational performance +- Control rule +- Table paging +- Out of the box version for vue, react and other frameworks + +## Step. 1: Download NPM Package + +```sh +npm i @hufe921/canvas-editor --save +``` + +## Step. 2: Prepare Container + +```html +
+``` + +## Step. 3: Instantiate Editor + +- Examples that only the body content is included + +```javascript +import Editor from '@hufe921/canvas-editor' + +new Editor( + document.querySelector('.canvas-editor'), + [ + { + value: 'Hello World' + } + ], + {} +) +``` + +- Examples that contain body, header, footer content + +```javascript +import Editor from '@hufe921/canvas-editor' + +new Editor( + document.querySelector('.canvas-editor'), + { + header: [ + { + value: 'Header', + rowFlex: RowFlex.CENTER + } + ], + main: [ + { + value: 'Hello World' + } + ], + footer: [ + { + value: 'canvas-editor', + size: 12 + } + ] + }, + {} +) +``` + +## Step. 4: Configuration Editor + +See the next section for details diff --git a/docs/en/index.md b/docs/en/index.md new file mode 100644 index 0000000..59a53be --- /dev/null +++ b/docs/en/index.md @@ -0,0 +1,43 @@ +--- +layout: home + +title: canvas-editor +titleTemplate: rich text editor by canvas/svg + +hero: + name: canvas-editor + text: canvas/svg based rich text editor + actions: + - theme: brand + text: Get Started + link: /en/guide/start.html + - theme: alt + text: View on Github + link: https://github.com/Hufe921/canvas-editor + +features: + - icon: 💡 + title: WYSIWYG + details: Similar to word pageable, what you see is what you get + - icon: ⚡️ + title: Lightweight Data Structure + details: A piece of JSON can render complex styles + - icon: 🛠️ + title: Rich Features + details: Supports familiar rich text operations, tables, watermarks, controls, formulas, etc + - icon: 📦 + title: Easy to Use + details: The core npm package is officially released, and the menu bar and toolbar can be maintained by themselves + - icon: 🔩 + title: Flexible Development Mechanism + details: Through the interface, you can obtain the life cycle, event callback, custom right-click menu, and shortcut keys + - icon: 🔑 + title: Full TypeScript Types API + details: Flexible apis and full TypeScript types +--- + + diff --git a/docs/guide/api-common.md b/docs/guide/api-common.md new file mode 100644 index 0000000..86f47ab --- /dev/null +++ b/docs/guide/api-common.md @@ -0,0 +1,49 @@ +# 通用 API + +## splitText + +功能:拆分字符 + +用法: + +```javascript +import { splitText } from '@hufe921/canvas-editor' + +splitText(text: string): string[] +``` + +## createDomFromElementList + +功能:根据 elementList 创建 dom 树 + +用法: + +```javascript +import { createDomFromElementList } from '@hufe921/canvas-editor' + +createDomFromElementList(elementList: IElement[], options?: IEditorOption): HTMLDivElement +``` + +## getElementListByHTML + +功能:根据 HTML 创建 elementList + +用法: + +```javascript +import { getElementListByHTML } from '@hufe921/canvas-editor' + +getElementListByHTML(htmlText: string, options: IGetElementListByHTMLOption): IElement[] +``` + +## getTextFromElementList + +功能:根据 elementList 创建文本 + +用法: + +```javascript +import { getTextFromElementList } from '@hufe921/canvas-editor' + +getTextFromElementList(elementList: IElement[]): string +``` diff --git a/docs/guide/api-instance.md b/docs/guide/api-instance.md new file mode 100644 index 0000000..b16ab59 --- /dev/null +++ b/docs/guide/api-instance.md @@ -0,0 +1,24 @@ +# 实例 API + +## 使用方式 + +```javascript +import Editor from "@hufe921/canvas-editor" + +const instance = new Editor(container, data, options) +instance.apiName() +``` + +## destroy + +功能:销毁编辑器 + +用法: + +```javascript +instance.destroy() +``` + +::: warning +仅销毁编辑器 dom 及相关事件,菜单栏、工具栏、外部变量等需自行处理。 +::: diff --git a/docs/guide/command-execute.md b/docs/guide/command-execute.md new file mode 100644 index 0000000..de4d279 --- /dev/null +++ b/docs/guide/command-execute.md @@ -0,0 +1,1107 @@ +# 执行动作命令 + +## 使用方式 + +```javascript +import Editor from "@hufe921/canvas-editor" + +const instance = new Editor(container, data, options) +instance.command.commandName() +``` + +## executeMode + +功能:切换编辑器模式(编辑、清洁、只读、表单) + +用法: + +```javascript +instance.command.executeMode(editorMode: EditorMode) +``` + +## executeCut + +功能:剪切 + +用法: + +```javascript +instance.command.executeCut() +``` + +## executeCopy + +功能:复制 + +用法: + +```javascript +instance.command.executeCopy(payload?: ICopyOption) +``` + +## executePaste + +功能:粘贴 + +用法: + +```javascript +instance.command.executePaste(payload?: IPasteOption) +``` + +## executeSelectAll + +功能:全选 + +用法: + +```javascript +instance.command.executeSelectAll() +``` + +## executeBackspace + +功能:向前删除 + +用法: + +```javascript +instance.command.executeBackspace() +``` + +## executeSetRange + +功能:设置选区 + +用法: + +```javascript +instance.command.executeSetRange( + startIndex: number, + endIndex: number, + tableId?: string, + startTdIndex?: number, + endTdIndex?: number, + startTrIndex?: number, + endTrIndex?: number +) +``` + +## executeReplaceRange + +功能:替换选区 + +用法: + +```javascript +instance.command.executeReplaceRange(range: IRange) +``` + +## executeSetPositionContext + +功能:设置位置上下文 + +用法: + +```javascript +instance.command.executeSetPositionContext(range: IRange) +``` + +## executeForceUpdate + +功能:强制重新渲染文档 + +用法: + +```javascript +instance.command.executeForceUpdate(options?: IForceUpdateOption) +``` + +## executeBlur + +功能:设置编辑器失焦 + +用法: + +```javascript +instance.command.executeBlur() +``` + +## executeUndo + +功能:撤销 + +用法: + +```javascript +instance.command.executeUndo() +``` + +## executeRedo + +功能:重做 + +用法: + +```javascript +instance.command.executeRedo() +``` + +## executePainter + +功能:格式刷-复制样式 + +用法: + +```javascript +instance.command.executePainter() +``` + +## executeApplyPainterStyle + +功能:格式刷-应用样式 + +用法: + +```javascript +instance.command.executeApplyPainterStyle() +``` + +## executeFormat + +功能:清除样式 + +用法: + +```javascript +instance.command.executeFormat(options?: IRichtextOption) +``` + +## executeFont + +功能:设置字体 + +用法: + +```javascript +instance.command.executeFont(font: string, options?: IRichtextOption) +``` + +## executeSize + +功能:设置字号 + +用法: + +```javascript +instance.command.executeSize(size: number, options?: IRichtextOption) +``` + +## executeSizeAdd + +功能:增大字号 + +用法: + +```javascript +instance.command.executeSizeAdd(options?: IRichtextOption) +``` + +## executeSizeMinus + +功能:减小字号 + +用法: + +```javascript +instance.command.executeSizeMinus(options?: IRichtextOption) +``` + +## executeBold + +功能:字体加粗 + +用法: + +```javascript +instance.command.executeBold(options?: IRichtextOption) +``` + +## executeItalic + +功能:字体斜体 + +用法: + +```javascript +instance.command.executeItalic(options?: IRichtextOption) +``` + +## executeUnderline + +功能:下划线 + +用法: + +```javascript +instance.command.executeUnderline(textDecoration?: ITextDecoration, options?: IRichtextOption) +``` + +## executeStrikeout + +功能:删除线 + +用法: + +```javascript +instance.command.executeStrikeout(options?: IRichtextOption) +``` + +## executeSuperscript + +功能:上标 + +用法: + +```javascript +instance.command.executeSuperscript(options?: IRichtextOption) +``` + +## executeSubscript + +功能:上下标 + +用法: + +```javascript +instance.command.executeSubscript(options?: IRichtextOption) +``` + +## executeColor + +功能:字体颜色 + +用法: + +```javascript +instance.command.executeColor(color: string | null, options?: IRichtextOption) +``` + +## executeHighlight + +功能:高亮 + +用法: + +```javascript +instance.command.executeHighlight(color: string | null, options?: IRichtextOption) +``` + +## executeTitle + +功能:标题设置 + +用法: + +```javascript +instance.command.executeTitle(TitleLevel | null) +``` + +## executeList + +功能:列表设置 + +用法: + +```javascript +instance.command.executeList(listType: ListType | null, listStyle?: ListStyle) +``` + +## executeRowFlex + +功能:行对齐 + +用法: + +```javascript +instance.command.executeRowFlex(rowFlex: RowFlex) +``` + +## executeRowMargin + +功能:行间距 + +用法: + +```javascript +instance.command.executeRowMargin(rowMargin: number) +``` + +## executeInsertTable + +功能:插入表格 + +用法: + +```javascript +instance.command.executeInsertTable(row: number, col: number) +``` + +## executeInsertTableTopRow + +功能:向上插入一行 + +用法: + +```javascript +instance.command.executeInsertTableTopRow() +``` + +## executeInsertTableBottomRow + +功能:向下插入一行 + +用法: + +```javascript +instance.command.executeInsertTableBottomRow() +``` + +## executeInsertTableLeftCol + +功能:向左插入一列 + +用法: + +```javascript +instance.command.executeInsertTableLeftCol() +``` + +## executeInsertTableRightCol + +功能:向右插入一列 + +用法: + +```javascript +instance.command.executeInsertTableRightCol() +``` + +## executeDeleteTableRow + +功能:删除当前行 + +用法: + +```javascript +instance.command.executeDeleteTableRow() +``` + +## executeDeleteTableCol + +功能:删除当前列 + +用法: + +```javascript +instance.command.executeDeleteTableCol() +``` + +## executeDeleteTable + +功能:删除表格 + +用法: + +```javascript +instance.command.executeDeleteTable() +``` + +## executeMergeTableCell + +功能:合并表格 + +用法: + +```javascript +instance.command.executeMergeTableCell() +``` + +## executeCancelMergeTableCell + +功能:取消合并表格 + +用法: + +```javascript +instance.command.executeCancelMergeTableCell() +``` + +## executeSplitVerticalTableCell + +功能:分隔当前单元格(垂直方向) + +用法: + +```javascript +instance.command.executeSplitVerticalTableCell() +``` + +## executeSplitHorizontalTableCell + +功能:分隔当前单元格(水平方向) + +用法: + +```javascript +instance.command.executeSplitHorizontalTableCell() +``` + +## executeTableTdVerticalAlign + +功能:表格单元格垂直对齐方式 + +用法: + +```javascript +instance.command.executeTableTdVerticalAlign(payload: VerticalAlign) +``` + +## executeTableBorderType + +功能:表格边框类型 + +用法: + +```javascript +instance.command.executeTableBorderType(payload: TableBorder) +``` + +## executeTableBorderColor + +功能:表格边框颜色 + +用法: + +```javascript +instance.command.executeTableBorderColor(payload: string) +``` + +## executeTableTdBorderType + +功能:表格单元格边框类型 + +用法: + +```javascript +instance.command.executeTableTdBorderType(payload: TdBorder) +``` + +## executeTableTdSlashType + +功能:表格单元格内斜线 + +用法: + +```javascript +instance.command.executeTableTdSlashType(payload: TdSlash) +``` + +## executeTableTdBackgroundColor + +功能:表格单元格背景色 + +用法: + +```javascript +instance.command.executeTableTdBackgroundColor(payload: string) +``` + +## executeTableSelectAll + +功能:选中整个表格 + +用法: + +```javascript +instance.command.executeTableSelectAll() +``` + +## executeImage + +功能:插入图片 + +用法: + +```javascript +instance.command.executeImage({ + id?: string; + width: number; + height: number; + value: string; + imgDisplay?: ImageDisplay; +}) +``` + +## executeHyperlink + +功能:插入链接 + +用法: + +```javascript +instance.command.executeHyperlink({ + type: ElementType.HYPERLINK, + value: string, + url: string, + valueList: IElement[] +}) +``` + +## executeDeleteHyperlink + +功能:删除链接 + +用法: + +```javascript +instance.command.executeDeleteHyperlink() +``` + +## executeCancelHyperlink + +功能:取消链接 + +用法: + +```javascript +instance.command.executeCancelHyperlink() +``` + +## executeEditHyperlink + +功能:编辑链接 + +用法: + +```javascript +instance.command.executeEditHyperlink(newUrl: string) +``` + +## executeSeparator + +功能:插入分割线 + +用法: + +```javascript +instance.command.executeSeparator(dashArray: number[]) +``` + +## executePageBreak + +功能:分页符 + +用法: + +```javascript +instance.command.executePageBreak() +``` + +## executeAddWatermark + +功能:添加水印 + +用法: + +```javascript +instance.command.executeAddWatermark({ + data: string; + color?: string; + opacity?: number; + size?: number; + font?: string; +}) +``` + +## executeDeleteWatermark + +功能:删除水印 + +用法: + +```javascript +instance.command.executeDeleteWatermark() +``` + +## executeSearch + +功能:搜索 + +用法: + +```javascript +instance.command.executeSearch(keyword: string) +``` + +## executeSearchNavigatePre + +功能:搜索导航-上一个 + +用法: + +```javascript +instance.command.executeSearchNavigatePre() +``` + +## executeSearchNavigateNext + +功能:搜索导航-下一个 + +用法: + +```javascript +instance.command.executeSearchNavigateNext() +``` + +## executeReplace + +功能:搜索替换 + +用法: + +```javascript +instance.command.executeReplace(newWord: string, option?: IReplaceOption) +``` + +## executePrint + +功能:打印 + +用法: + +```javascript +instance.command.executePrint() +``` + +## executeReplaceImageElement + +功能:替换图片 + +用法: + +```javascript +instance.command.executeReplaceImageElement(newUrl: string) +``` + +## executeSaveAsImageElement + +功能:另存为图片 + +用法: + +```javascript +instance.command.executeSaveAsImageElement() +``` + +## executeChangeImageDisplay + +功能:改变图片行显示方式 + +用法: + +```javascript +instance.command.executeChangeImageDisplay(element: IElement, display: ImageDisplay) +``` + +## executePageMode + +功能:页面模式 + +用法: + +```javascript +instance.command.executePageMode(pageMode: PageMode) +``` + +## executePageScale + +功能:设置缩放比例 + +用法: + +```javascript +instance.command.executePageScale(scale: number) +``` + +## executePageScaleRecovery + +功能:恢复页面原始缩放比例 + +用法: + +```javascript +instance.command.executePageScaleRecovery() +``` + +## executePageScaleMinus + +功能:页面缩小 + +用法: + +```javascript +instance.command.executePageScaleMinus() +``` + +## executePageScaleAdd + +功能:页面放大 + +用法: + +```javascript +instance.command.executePageScaleAdd() +``` + +## executePaperSize + +功能:设置纸张大小 + +用法: + +```javascript +instance.command.executePaperSize(width: number, height: number) +``` + +## executePaperDirection + +功能:设置纸张方向 + +用法: + +```javascript +instance.command.executePaperDirection(paperDirection: PaperDirection) +``` + +## executeSetPaperMargin + +功能:设置纸张页边距 + +用法: + +```javascript +instance.command.executeSetPaperMargin([top: number, right: number, bottom: number, left: number]) +``` + +## executeSetMainBadge + +功能:设置正文徽章 + +用法: + +```javascript +instance.command.executeSetMainBadge(payload: IBadge | null) +``` + +## executeSetAreaBadge + +功能:设置区域徽章 + +用法: + +```javascript +instance.command.executeSetAreaBadge(payload: IAreaBadge[]) +``` + +## executeInsertElementList + +功能:插入元素 + +用法: + +```javascript +instance.command.executeInsertElementList(elementList: IElement[], options?: IInsertElementListOption) +``` + +## executeAppendElementList + +功能:追加元素 + +用法: + +```javascript +instance.command.executeAppendElementList(elementList: IElement[], options?: IAppendElementListOption) +``` + +## executeUpdateElementById + +功能:根据 id 修改元素属性 + +用法: + +```javascript +instance.command.executeUpdateElementById(payload: IUpdateElementByIdOption) +``` + +## executeDeleteElementById + +功能:根据 id 删除元素 + +用法: + +```javascript +instance.command.executeDeleteElementById(payload: IDeleteElementByIdOption) +``` + +## executeSetValue + +功能:设置编辑器数据 + +用法: + +```javascript +instance.command.executeSetValue(payload: Partial, options?: ISetValueOption) +``` + +## executeRemoveControl + +功能:删除控件 + +用法: + +```javascript +instance.command.executeRemoveControl(payload?: IRemoveControlOption) +``` + +## executeSetLocale + +功能:设置本地语言 + +用法: + +```javascript +instance.command.executeSetLocale(locale: string) +``` + +## executeLocationCatalog + +功能:定位目录位置 + +用法: + +```javascript +instance.command.executeLocationCatalog(titleId: string) +``` + +## executeWordTool + +功能:文字工具(删除空行、行首空格) + +用法: + +```javascript +instance.command.executeWordTool() +``` + +## executeSetHTML + +功能:设置编辑器 HTML 数据 + +用法: + +```javascript +instance.command.executeSetHTML(payload: Partialdata, options) +const value = instance.command.commandName() +``` + +## getValue + +功能:获取当前文档信息 + +用法: + +```javascript +const { + version: string + data: IEditorData + options: IEditorOption +} = instance.command.getValue(options?: IGetValueOption) +``` + +## getValueAsync + +功能:获取当前文档信息(异步) + +用法: + +```javascript +const { + version: string + data: IEditorData + options: IEditorOption +} = await instance.command.getValueAsync(options?: IGetValueOption) +``` + +## getImage + +功能:获取当前页面图片 base64 字符串 + +用法: + +```javascript +const base64StringList = await instance.command.getImage(option?: IGetImageOption) +``` + +## getOptions + +功能:获取编辑器配置 + +用法: + +```javascript +const editorOption = await instance.command.getOptions() +``` + +## getWordCount + +功能:获取文档字数 + +用法: + +```javascript +const wordCount = await instance.command.getWordCount() +``` + +## getCursorPosition + +功能: 获取光标位置坐标 + +用法: + +```javascript +const range = instance.command.getCursorPosition() +``` + +## getRange + +功能:获取选区 + +用法: + +```javascript +const range = instance.command.getRange() +``` + +## getRangeText + +功能:获取选区文本 + +用法: + +```javascript +const rangeText = instance.command.getRangeText() +``` + +## getRangeContext + +功能:获取选区上下文 + +用法: + +```javascript +const rangeContext = instance.command.getRangeContext() +``` + +## getRangeRow + +功能:获取选区所在行元素列表 + +用法: + +```javascript +const rowElementList = instance.command.getRangeRow() +``` + +## getKeywordRangeList + +功能:获取关键词所在选区列表 + +用法: + +```javascript +const rangeList = instance.command.getKeywordRangeList() +``` + +## getKeywordContext + +功能:获取关键词所在上下文本信息 + +用法: + +```javascript +const keywordContextList = instance.command.getKeywordContext(payload: string) +``` + +## getRangeParagraph + +功能:获取选区所在段落元素列表 + +用法: + +```javascript +const paragraphElementList = instance.command.getRangeParagraph() +``` + +## getPaperMargin + +功能:获取页边距 + +用法: + +```javascript +const [top: number, right: number, bottom: number, left: number] = + instance.command.getPaperMargin() +``` + +## getSearchNavigateInfo + +功能:获取搜索导航信息 + +用法: + +```javascript +const { + index: number; + count: number; +} = instance.command.getSearchNavigateInfo() +``` + +## getCatalog + +功能:获取目录 + +用法: + +```javascript +const catalog = await instance.command.getCatalog() +``` + +## getHTML + +功能:获取 HTML + +用法: + +```javascript +const { + header: string + main: string + footer: string +} = await instance.command.getHTML() +``` + +## getText + +功能:获取文本 + +用法: + +```javascript +const { + header: string + main: string + footer: string +} = await instance.command.getText() +``` + +## getLocale + +功能:获取当前语言 + +用法: + +```javascript +const locale = await instance.command.getLocale() +``` + +## getGroupIds + +功能:获取所有成组 id + +用法: + +```javascript +const groupIds = await instance.command.getGroupIds() +``` + +## getControlValue + +功能:获取控件值 + +用法: + +```javascript +const { + value: string | null + innerText: string | null + zone: EditorZone + elementList?: IElement[] +}[] = await instance.command.getControlValue(payload: IGetControlValueOption) +``` + +## getControlList + +功能:获取所有控件 + +用法: + +```javascript +const controlList = await instance.command.getControlList() +``` + +## getContainer + +功能:获取编辑器容器 + +用法: + +```javascript +const container = await instance.command.getContainer() +``` + +## getTitleValue + +功能:获取标题值 + +用法: + +```javascript +const { + value: string | null + elementList: IElement[] + zone: EditorZone +}[] = await instance.command.getTitleValue(payload: IGetTitleValueOption) +``` + +## getPositionContextByEvent + +功能:获取位置上下文信息通过鼠标事件 + +用法: + +```javascript +const { + pageNo: number + element: IElement | null + rangeRect: RangeRect | null + tableInfo: ITableInfoByEvent | null +}[] = await instance.command.getPositionContextByEvent(evt: MouseEvent, options?: IPositionContextByEventOption) +``` + +示例: + +```javascript +instance.eventBus.on( + 'mousemove', + debounce(evt => { + const positionContext = instance.command.getPositionContextByEvent(evt) + console.log(positionContext) + }, 200) +)`` +``` + +## getElementById + +功能:根据 id 获取元素 + +用法: + +```javascript +const elementList = await instance.command.getElementById(payload: IGetElementByIdOption) +``` + +## getAreaValue + +功能: 获取区域数据 +用法: + +```js +const { + id?: string + area: IArea + value: IElement[] + startPageNo: number + endPageNo: number +} = instance.command.getAreaValue(options: IGetAreaValueOption) +``` diff --git a/docs/guide/contextmenu-custom.md b/docs/guide/contextmenu-custom.md new file mode 100644 index 0000000..518b7bf --- /dev/null +++ b/docs/guide/contextmenu-custom.md @@ -0,0 +1,44 @@ +# 自定义右键菜单 + +## 使用方式 + +```javascript +import Editor from "@hufe921/canvas-editor" + +const instance = new Editor(container, data, options) +instance.register.contextMenuList([ + { + key?: string; + isDivider?: boolean; + icon?: string; + name?: string; // 使用%s代表选区文本。示例:搜索:%s + shortCut?: string; + disable?: boolean; + when?: (payload: IContextMenuContext) => boolean; + callback?: (command: Command, context: IContextMenuContext) => any; + childMenus?: IRegisterContextMenu[]; + } + ]) +``` + +## getContextMenuList + +功能:获取注册的右键菜单列表 + +用法: + +```javascript +const contextMenuList = await instance.register.getContextMenuList() +``` + +备注: + +```javascript +// 修改内部右键菜单示例 +contextmenuList.forEach(menu => { + // 通过菜单key找到菜单项后进行属性修改 + if (menu.key === INTERNAL_CONTEXT_MENU_KEY.GLOBAL.PASTE) { + menu.when = () => false + } +}) +``` diff --git a/docs/guide/contextmenu-internal.md b/docs/guide/contextmenu-internal.md new file mode 100644 index 0000000..495bbd6 --- /dev/null +++ b/docs/guide/contextmenu-internal.md @@ -0,0 +1,61 @@ +# 内部右键菜单 + +## 全局 + +- 剪切 +- 复制 +- 粘贴 +- 全选 +- 打印 + +## 超链接 + +- 删除链接 +- 取消链接 +- 编辑链接 + +## 图片 + +- 更改图片 +- 另存为图片 +- 文字环绕 + - 嵌入型 + - 上下型环绕 + - 四周型环绕 + - 浮于文字上方 + - 衬于文字下方 + +## 表格 + +- 表格边框 + - 所有框线 + - 无框线 + - 虚框线 + - 外侧框线 + - 内侧框线 + - 单元格边框 + - 上边框 + - 右边框 + - 下边框 + - 左边框 + - 正斜线 + - 反斜线 +- 垂直对齐 + - 顶端对齐 + - 垂直居中 + - 底端对齐 +- 插入行列 + - 上方插入 1 行 + - 下方插入 1 行 + - 左侧插入 1 列 + - 右侧插入 1 列 +- 删除行列 + - 删除 1 行 + - 删除 1 列 + - 删除整个表格 +- 合并单元格 +- 取消合并 + +## 控件 + +- 删除控件 diff --git a/docs/guide/eventbus.md b/docs/guide/eventbus.md new file mode 100644 index 0000000..5a7a5f7 --- /dev/null +++ b/docs/guide/eventbus.md @@ -0,0 +1,234 @@ +# 事件监听(eventBus) + +## 使用方式 + +```javascript +import Editor from "@hufe921/canvas-editor" + +const instance = new Editor(container, data, options) + +// 注册 +instance.eventBus.on( + eventName: K, + callback: EventMap[K] +) + +// 移除 +instance.eventBus.off( + eventName: K, + callback: EventMap[K] +) +``` + +## rangeStyleChange + +功能:选区样式发生改变 + +用法: + +```javascript +instance.eventBus.on('rangeStyleChange', (payload: IRangeStyle) => void) +``` + +## visiblePageNoListChange + +功能:可见页发生改变 + +用法: + +```javascript +instance.eventBus.on('visiblePageNoListChange', (payload: number[]) => void) +``` + +## intersectionPageNoChange + +功能:当前页发生改变 + +用法: + +```javascript +instance.eventBus.on('intersectionPageNoChange', (payload: number) => void) +``` + +## pageSizeChange + +功能:当前页数发生改变 + +用法: + +```javascript +instance.eventBus.on('pageSizeChange', (payload: number) => void) +``` + +## pageScaleChange + +功能:当前页面缩放比例发生改变 + +用法: + +```javascript +instance.eventBus.on('pageScaleChange', (payload: number) => void) +``` + +## contentChange + +功能:当前内容发生改变 + +用法: + +```javascript +instance.eventBus.on('contentChange', () => void) +``` + +## controlChange + +功能:当前光标所在控件发生改变 + +用法: + +```javascript +instance.eventBus.on('controlChange', (payload: IControlChangeResult) => void) +``` + +## controlContentChange + +功能:控件内容发生改变 + +用法: + +```javascript +instance.eventBus.on('controlContentChange', (payload: IControlContentChangeResult) => void) +``` + +## pageModeChange + +功能:页面模式发生改变 + +用法: + +```javascript +instance.eventBus.on('pageModeChange', (payload: PageMode) => void) +``` + +## saved + +功能:文档执行保存 + +用法: + +```javascript +instance.eventBus.on('saved', (payload: IEditorResult) => void) +``` + +## zoneChange + +功能:区域发生改变 + +用法: + +```javascript +instance.eventBus.on('zoneChange', (payload: EditorZone) => void) +``` + +## mousemove + +功能:编辑器 mousemove 事件监听 + +用法: + +```javascript +instance.eventBus.on('mousemove', (evt: MouseEvent) => void) +``` + +## mouseenter + +功能:编辑器 mouseenter 事件监听 + +用法: + +```javascript +instance.eventBus.on('mouseenter', (evt: MouseEvent) => void) +``` + +## mouseleave + +功能:编辑器 mouseleave 事件监听 + +用法: + +```javascript +instance.eventBus.on('mouseleave', (evt: MouseEvent) => void) +``` + +## mousedown + +功能:编辑器 mousedown 事件监听 + +用法: + +```javascript +instance.eventBus.on('mousedown', (evt: MouseEvent) => void) +``` + +## mouseup + +功能:编辑器 mouseup 事件监听 + +用法: + +```javascript +instance.eventBus.on('mouseup', (evt: MouseEvent) => void) +``` + +## click + +功能:编辑器 click 事件监听 + +用法: + +```javascript +instance.eventBus.on('click', (evt: MouseEvent) => void) +``` + +## input + +功能:编辑器 input 事件监听 + +用法: + +```javascript +instance.eventBus.on('input', (evt: Event) => void) +``` + +## positionContextChange + +功能:上下文内容发生改变 + +用法: + +```javascript +instance.eventBus.on('positionContextChange', (payload: IPositionContextChangePayload) => void) +``` + +## imageSizeChange + +功能:图片尺寸发生改变事件 + +用法: + +```javascript +instance.eventBus.on('imageSizeChange', (payload: { element: IElement }) => void) +``` + +## imageMousedown + +功能:图片 mousedown 事件 + +用法: + +```javascript +instance.eventBus.on('imageMousedown', (payload: { + evt: MouseEvent + element: IElement +}) => void) +``` diff --git a/docs/guide/i18n.md b/docs/guide/i18n.md new file mode 100644 index 0000000..48cba7f --- /dev/null +++ b/docs/guide/i18n.md @@ -0,0 +1,111 @@ +# 国际化 + +## 使用方式 + +```javascript +import Editor from "@hufe921/canvas-editor" + +const instance = new Editor(container, data, options) + +// 注册 +instance.register.langMap(locale: string, lang: ILang) + +// 设置 +instance.command.executeSetLocale(locale) +``` + +## ILang + +```typescript +interface ILang { + contextmenu: { + global: { + cut: string + copy: string + paste: string + selectAll: string + print: string + } + control: { + delete: string + } + hyperlink: { + delete: string + cancel: string + edit: string + } + image: { + change: string + saveAs: string + textWrap: string + textWrapType: { + embed: string + upDown: string + surround: string + floatTop: string + floatBottom: string + } + table: { + insertRowCol: string + insertTopRow: string + insertBottomRow: string + insertLeftCol: string + insertRightCol: string + deleteRowCol: string + deleteRow: string + deleteCol: string + deleteTable: string + mergeCell: string + mergeCancelCell: string + verticalAlign: string + verticalAlignTop: string + verticalAlignMiddle: string + verticalAlignBottom: string + border: string + borderAll: string + borderEmpty: string + borderDash: string + borderExternal: string + borderInternal: string + borderTd: string + borderTdTop: string + borderTdRight: string + borderTdBottom: string + borderTdLeft: string + borderTdForward: string + borderTdBack: string + } + } + datePicker: { + now: string + confirm: string + return: string + timeSelect: string + weeks: { + sun: string + mon: string + tue: string + wed: string + thu: string + fri: string + sat: string + } + year: string + month: string + hour: string + minute: string + second: string + } + frame: { + header: string + footer: string + } + pageBreak: { + displayName: string + } + zone: { + headerTip: string + footerTip: string + } +} +``` diff --git a/docs/guide/listener.md b/docs/guide/listener.md new file mode 100644 index 0000000..98a9522 --- /dev/null +++ b/docs/guide/listener.md @@ -0,0 +1,126 @@ +# 事件监听(listener) + +::: warning +listener 只能响应一个方法,后续不再添加新监听方法,推荐使用 eventBus 进行事件监听。 +::: + +## 使用方式 + +```javascript +import Editor from "@hufe921/canvas-editor" + +const instance = new Editor(container, data, options) +instance.listener.eventName = ()=>{} +``` + +## rangeStyleChange + +功能:选区样式发生改变 + +用法: + +```javascript +instance.listener.rangeStyleChange = (payload: IRangeStyle) => {} +``` + +## visiblePageNoListChange + +功能:可见页发生改变 + +用法: + +```javascript +instance.listener.visiblePageNoListChange = (payload: number[]) => {} +``` + +## intersectionPageNoChange + +功能:当前页发生改变 + +用法: + +```javascript +instance.listener.intersectionPageNoChange = (payload: number) => {} +``` + +## pageSizeChange + +功能:当前页数发生改变 + +用法: + +```javascript +instance.listener.pageSizeChange = (payload: number) => {} +``` + +## pageScaleChange + +功能:当前页面缩放比例发生改变 + +用法: + +```javascript +instance.listener.pageScaleChange = (payload: number) => {} +``` + +## contentChange + +功能:当前内容发生改变 + +用法: + +```javascript +instance.listener.contentChange = () => {} +``` + +## controlChange + +功能:当前光标所在控件发生改变 + +用法: + +```javascript +instance.listener.controlChange = (payload: IControlChangeResult) => {} +``` + +## controlContentChange + +功能:控件内容发生改变 + +用法: + +```javascript +instance.listener.controlContentChange = ( + payload: IControlContentChangeResult +) => {} +``` + +## pageModeChange + +功能:页面模式发生改变 + +用法: + +```javascript +instance.listener.pageModeChange = (payload: PageMode) => {} +``` + +## saved + +功能:文档执行保存 + +用法: + +```javascript +instance.listener.saved = (payload: IEditorResult) => {} +``` + +## zoneChange + +功能:区域发生改变 + +用法: + +```javascript +instance.listener.zoneChange = (payload: EditorZone) => {} +``` diff --git a/docs/guide/option.md b/docs/guide/option.md new file mode 100644 index 0000000..4ba1a67 --- /dev/null +++ b/docs/guide/option.md @@ -0,0 +1,189 @@ +# 配置 + +## 使用方式 + +```javascript +import Editor from "@hufe921/canvas-editor" + +new Editor(container, IEditorData | IElement[], { + // 配置项 +}) +``` + +## 完整配置 + +```typescript +interface IEditorOption { + mode?: EditorMode // 编辑器模式:编辑、清洁(不显示视觉辅助元素。如:分页符)、只读、表单(仅控件内可编辑)、打印(不显示辅助元素、未书写控件及前后括号)、设计模式(不可删除、只读等配置不控制)。默认:编辑 + locale?: string // 多语言类型。默认:zhCN + defaultType?: string // 默认元素类型。默认:TEXT + defaultColor?: string // 默认字体颜色。默认:#000000 + defaultFont?: string // 默认字体。默认:Microsoft YaHei + defaultSize?: number // 默认字号。默认:16 + minSize?: number // 最小字号。默认:5 + maxSize?: number // 最大字号。默认:72 + defaultBasicRowMarginHeight?: number // 默认行高。默认:8 + defaultRowMargin?: number // 默认行间距。默认:1 + defaultTabWidth?: number // 默认tab宽度。默认:32 + width?: number // 纸张宽度。默认:794 + height?: number // 纸张高度。默认:1123 + scale?: number // 缩放比例。默认:1 + pageGap?: number // 纸张间隔。默认:20 + underlineColor?: string // 下划线颜色。默认:#000000 + strikeoutColor?: string // 删除线颜色。默认:#FF0000 + rangeColor?: string // 选区颜色。默认:#AECBFA + rangeAlpha?: number // 选区透明度。默认:0.6 + rangeMinWidth?: number // 选区最小宽度。默认:5 + searchMatchColor?: string // 搜索高亮颜色。默认:#FFFF00 + searchNavigateMatchColor?: string // 搜索导航高亮颜色。默认:#AAD280 + searchMatchAlpha?: number // 搜索高亮透明度。默认:0.6 + highlightAlpha?: number // 高亮元素透明度。默认:0.6 + highlightMarginHeight?: number // 高亮元素边距高度。默认:8 + resizerColor?: string // 图片尺寸器颜色。默认:#4182D9 + resizerSize?: number // 图片尺寸器大小。默认:5 + marginIndicatorSize?: number // 页边距指示器长度。默认:35 + marginIndicatorColor?: string // 页边距指示器颜色。默认:#BABABA + margins?: IMargin // 页面边距。默认:[100, 120, 100, 120] + pageMode?: PageMode // 纸张模式:连页、分页。默认:分页 + renderMode?: RenderMode // 渲染模式:极速(多个字组合渲染)、兼容(逐字渲染:避免浏览器字体等环境差异)。默认:极速 + defaultHyperlinkColor?: string // 默认超链接颜色。默认:#0000FF + table?: ITableOption // 表格配置。{tdPadding?:IPadding; defaultTrMinHeight?:number; defaultColMinWidth?:number} + header?: IHeader // 页眉信息。{top?:number; maxHeightRadio?:MaxHeightRatio;} + footer?: IFooter // 页脚信息。{bottom?:number; maxHeightRadio?:MaxHeightRatio;} + pageNumber?: IPageNumber // 页码信息。{bottom:number; size:number; font:string; color:string; rowFlex:RowFlex; format:string; numberType:NumberType;} + paperDirection?: PaperDirection // 纸张方向:纵向、横向 + inactiveAlpha?: number // 正文内容失焦时透明度。默认值:0.6 + historyMaxRecordCount?: number // 历史(撤销重做)最大记录次数。默认:100次 + printPixelRatio?: number // 打印像素比率(值越大越清晰,但尺寸越大)。默认:3 + maskMargin?: IMargin // 编辑器上的遮盖边距(如悬浮到编辑器上的菜单栏、底部工具栏)。默认:[0, 0, 0, 0] + letterClass?: string[] // 排版支持的字母类。默认:a-zA-Z。内置可选择的字母表类:LETTER_CLASS + contextMenuDisableKeys?: string[] // 禁用的右键菜单。默认:[] + shortcutDisableKeys?: string[] // 禁用的快捷键。默认:[] + scrollContainerSelector?: string // 滚动区域选择器。默认:document + pageOuterSelectionDisable?: boolean // 鼠标移出页面时选区禁用。默认:false + wordBreak?: WordBreak // 单词与标点断行:BREAK_WORD首行不出现标点&单词不拆分、BREAK_ALL按字符宽度撑满后折行。默认:BREAK_WORD + watermark?: IWatermark // 水印信息。{data:string; color?:string; opacity?:number; size?:number; font?:string; numberType:NumberType;} + control?: IControlOption // 控件信息。 {placeholderColor?:string; bracketColor?:string; prefix?:string; postfix?:string; borderWidth?: number; borderColor?: string; activeBackgroundColor?: string; disabledBackgroundColor?: string; existValueBackgroundColor?: string; noValueBackgroundColor?: string;} + checkbox?: ICheckboxOption // 复选框信息。{width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string; verticalAlign?: VerticalAlign;} + radio?: IRadioOption // 单选框信息。{width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string; verticalAlign?: VerticalAlign;} + cursor?: ICursorOption // 光标样式。{width?: number; color?: string; dragWidth?: number; dragColor?: string; dragFloatImageDisabled?: boolean;} + title?: ITitleOption // 标题配置。{ defaultFirstSize?: number; defaultSecondSize?: number; defaultThirdSize?: number defaultFourthSize?: number; defaultFifthSize?: number; defaultSixthSize?: number;} + placeholder?: IPlaceholder // 编辑器空白占位文本 + group?: IGroup // 成组配置。{opacity?:number; backgroundColor?:string; activeOpacity?:number; activeBackgroundColor?:string; disabled?:boolean; deletable?:boolean;} + pageBreak?: IPageBreak // 分页符配置。{font?:string; fontSize?:number; lineDash?:number[];} + zone?: IZoneOption // 编辑器区域配置。{tipDisabled?:boolean;} + background?: IBackgroundOption // 背景配置。{color?:string; image?:string; size?:BackgroundSize; repeat?:BackgroundRepeat; applyPageNumbers?:number[]}。默认:{color: '#FFFFFF'} + lineBreak?: ILineBreakOption // 换行符配置。{disabled?:boolean; color?:string; lineWidth?:number;} + separator?: ISeparatorOption // 分隔符配置。{lineWidth?:number; strokeStyle?:string;} + lineNumber?: ILineNumberOption // 行号配置。{size?:number; font?:string; color?:string; disabled?:boolean; right?:number} + pageBorder?: IPageBorderOption // 页面边框配置。{color?:string; lineWidth:number; padding?:IPadding; disabled?:boolean;} + badge?: IBadgeOption // 徽章配置。{top?:number; left?:number} + modeRule?: IModeRule // 编辑器模式规则配置。{print:{imagePreviewerDisabled?: boolean}; readonly:{imagePreviewerDisabled?: boolean}; form:{controlDeletableDisabled?: boolean}} +} +``` + +## 表格配置 + +```typescript +interface ITableOption { + tdPadding?: IPadding // 单元格内边距。默认:[0, 5, 5, 5] + defaultTrMinHeight?: number // 默认表格行最小高度。默认:42 + defaultColMinWidth?: number // 默认表格列最小宽度(整体宽度足够时应用,否则会按比例缩小)。默认:40 +} +``` + +## 页眉配置 + +```typescript +interface IHeader { + top?: number // 距离页面顶部大小。默认:30 + inactiveAlpha?: number // 失活时透明度。默认:1 + maxHeightRadio?: MaxHeightRatio // 占页面最大高度比。默认:HALF + disabled?: boolean // 是否禁用 + editable?: boolean // 禁止编辑标题内容 +} +``` + +## 页脚配置 + +```typescript +interface IFooter { + bottom?: number // 距离页面底部大小。默认:30 + inactiveAlpha?: number // 失活时透明度。默认:1 + maxHeightRadio?: MaxHeightRatio // 占页面最大高度比。默认:HALF + disabled?: boolean // 是否禁用 + editable?: boolean // 禁止编辑页脚内容 +} +``` + +## 页码配置 + +```typescript +interface IPageNumber { + bottom?: number // 距离页面底部大小。默认:60 + size?: number // 字体大小。默认:12 + font?: string // 字体。默认:Microsoft YaHei + color?: string // 字体颜色。默认:#000000 + rowFlex?: RowFlex // 行对齐方式。默认:CENTER + format?: string // 页码格式。默认:{pageNo}。示例:第{pageNo}页/共{pageCount}页 + numberType?: NumberType // 数字类型。默认:ARABIC + disabled?: boolean // 是否禁用 + startPageNo?: number // 起始页码。默认:1 + fromPageNo?: number // 从第几页开始出现页码。默认:0 + maxPageNo?: number | null // 最大页码(从0开始)。默认:null +} +``` + +## 水印配置 + +```typescript +interface IWatermark { + data: string // 文本。 + type?: WatermarkType + width?: number + height?: number + color?: string // 颜色。默认:#AEB5C0 + opacity?: number // 透明度。默认:0.3 + size?: number // 字体大小。默认:200 + font?: string // 字体。默认:Microsoft YaHei + repeat?: boolean // 重复水印。默认:false + gap?: [horizontal: number, vertical: number] // 水印间距。默认:[10,10] + numberType: NumberType.ARABIC // 页码格式。默认:{pageNo}。示例:第{pageNo}页/共{pageCount}页 +} +``` + +## 占位文本配置 + +```typescript +interface IPlaceholder { + data: string // 文本。 + color?: string // 颜色。默认:#DCDFE6 + opacity?: number // 透明度。默认:1 + size?: number // 字体大小。默认:16 + font?: string // 字体。默认:Microsoft YaHei +} +``` + +## 行号配置 + +```typescript +interface ILineNumberOption { + size?: number // 字体大小。默认:12 + font?: string // 字体。默认:Microsoft YaHei + color?: string // 颜色。默认:#000000 + disabled?: boolean // 是否禁用。默认:true + right?: number // 距离正文距离。默认:20 + type?: LineNumberType // 编号类型(每页重新编号、连续编号)。默认:连续编号 +} +``` + +## 页面边框配置 + +```typescript +interface IPageBorderOption { + color?: string // 颜色。默认:#000000 + lineWidth?: number // 宽度。默认:1 + padding?: IPadding // 距离正文内边距。默认:[0, 5, 0, 5] + disabled?: boolean // 是否禁用。默认:true +} +``` diff --git a/docs/guide/override.md b/docs/guide/override.md new file mode 100644 index 0000000..b48958b --- /dev/null +++ b/docs/guide/override.md @@ -0,0 +1,47 @@ +# 重写方法 + +## 使用方式 + +```javascript +import Editor from "@hufe921/canvas-editor" + +const instance = new Editor(container, data, options) + +instance.override.overrideFunction = () => unknown | IOverrideResult +``` + +```typescript +interface IOverrideResult { + preventDefault?: boolean // 阻止执行内部默认方法。默认阻止 +} +``` + +## paste + +功能:重写粘贴方法 + +用法: + +```javascript +instance.override.paste = (evt?: ClipboardEvent) => unknown | IOverrideResult +``` + +## copy + +功能:重写复制方法 + +用法: + +```javascript +instance.override.copy = () => unknown | IOverrideResult +``` + +## drop + +功能:重写拖放方法 + +用法: + +```javascript +instance.override.drop = (evt: DragEvent) => unknown | IOverrideResult +``` diff --git a/docs/guide/plugin-custom.md b/docs/guide/plugin-custom.md new file mode 100644 index 0000000..1b104ed --- /dev/null +++ b/docs/guide/plugin-custom.md @@ -0,0 +1,25 @@ +# 自定义插件 + +::: tip +官方维护插件仓库:https://github.com/Hufe921/canvas-editor-plugin +::: + +## 开发插件 + +```javascript +export function myPlugin(editor: Editor, options?: Option) { + // 1. 修改方法,详见:src/plugins/copy + editor.command.updateFunction = () => {} + + // 2. 增加方法,详见:src/plugins/markdown + editor.command.addFunction = () => {} + + // 3. 事件监听、快捷键、右键菜单、重写方法等组合处理 +} +``` + +## 使用插件 + +```javascript +instance.use(myPlugin, options?: Option) +``` diff --git a/docs/guide/plugin-internal.md b/docs/guide/plugin-internal.md new file mode 100644 index 0000000..a369d44 --- /dev/null +++ b/docs/guide/plugin-internal.md @@ -0,0 +1,125 @@ +# 官方插件 + +::: tip +官方维护插件仓库:https://github.com/Hufe921/canvas-editor-plugin + +官方维护插件演示地址:https://hufe.club/canvas-editor-plugin +::: + +## 条形码 + +```javascript +import Editor from "@hufe921/canvas-editor" +import barcode1DPlugin from "@hufe921/canvas-editor-plugin-barcode1d" + +const instance = new Editor() +instance.use(barcode1DPlugin) + +instance.executeInsertBarcode1D( + content: string, + width: number, + height: number, + options?: JsBarcode.Options +) +``` + +## 二维码 + +```javascript +import Editor from "@hufe921/canvas-editor" +import barcode2DPlugin from "@hufe921/canvas-editor-plugin-barcode2d" + +const instance = new Editor() +instance.use(barcode2DPlugin, options?: IBarcode2DOption) + +instance.executeInsertBarcode2D( + content: string, + width: number, + height: number, + hints?: Map +) +``` + +## 代码块 + +```javascript +import Editor from "@hufe921/canvas-editor" +import codeblockPlugin from "@hufe921/canvas-editor-plugin-codeblock" + +const instance = new Editor() +instance.use(codeblockPlugin) + +instance.executeInsertCodeblock(content: string) +``` + +## Word + +```javascript +import Editor from '@hufe921/canvas-editor' +import docxPlugin from '@hufe921/canvas-editor-plugin-docx' + +const instance = new Editor() +instance.use(docxPlugin) + +command.executeImportDocx({ + arrayBuffer: buffer +}) + +instance.executeExportDocx({ + fileName: string +}) +``` + +## Excel + +```javascript +import Editor from '@hufe921/canvas-editor' +import excelPlugin from '@hufe921/canvas-editor-plugin-excel' + +const instance = new Editor() +instance.use(excelPlugin) + +command.executeImportExcel({ + arrayBuffer: buffer +}) +``` + +## 悬浮工具 + +```javascript +import Editor from '@hufe921/canvas-editor' +import floatingToolbarPlugin from '@hufe921/canvas-editor-plugin-floating-toolbar' + +const instance = new Editor() +instance.use(floatingToolbarPlugin) +``` + +## 流程图 + +```javascript +import Editor from '@hufe921/canvas-editor' +import diagramPlugin from '@hufe921/canvas-editor-plugin-diagram' + +const instance = new Editor() +instance.use(diagramPlugin) + +command.executeLoadDiagram({ + lang?: Lang + data?: string + onDestroy?: (message?: any) => void +}) +``` + +## 大小写转换 + +```javascript +import Editor from '@hufe921/canvas-editor' +import casePlugin from '@hufe921/canvas-editor-plugin-case' + +const instance = new Editor() +instance.use(casePlugin) + +command.executeUpperCase() + +command.executeLowerCase() +``` diff --git a/docs/guide/schema.md b/docs/guide/schema.md new file mode 100644 index 0000000..3ad86da --- /dev/null +++ b/docs/guide/schema.md @@ -0,0 +1,209 @@ +# 数据结构 + +```typescript +interface IElement { + // 基础 + id?: string; + type?: { + TEXT = 'text', + IMAGE = 'image', + TABLE = 'table', + HYPERLINK = 'hyperlink', + SUPERSCRIPT = 'superscript', + SUBSCRIPT = 'subscript', + SEPARATOR = 'separator', + PAGE_BREAK = 'pageBreak', + CONTROL = 'control', + CHECKBOX = 'checkbox', + RADIO = 'radio', + LATEX = 'latex', + TAB = 'tab', + DATE = 'date', + BLOCK = 'block' + }; + value: string; + valueList?: IElement[]; // 复合元素(超链接、标题、列表等)使用 + extension?: unknown; + externalId?: string; + hide?: boolean; + // 样式 + font?: string; + size?: number; + width?: number; + height?: number; + bold?: boolean; + color?: string; + highlight?: string; + italic?: boolean; + underline?: boolean; + strikeout?: boolean; + rowFlex?: { + LEFT = 'left', + CENTER = 'center', + RIGHT = 'right', + ALIGNMENT = 'alignment', + JUSTIFY = 'justify' + }; + rowMargin?: number; + letterSpacing?: number; + textDecoration?: { + style?: TextDecorationStyle; + }; + // 组信息-可用于批注等其他成组使用场景 + groupIds?: string[]; + // 表格 + conceptId?: string; + colgroup?: { + width: number; + }[]; + trList?: { + height: number; + pagingRepeat?: boolean; + extension?: unknown; + externalId?: string; + tdList: { + colspan: number; + rowspan: number; + conceptId?: string; + verticalAlign?: VerticalAlign; + backgroundColor?: string; + borderTypes?: TdBorder[]; + slashTypes?: TdSlash[]; + value: IElement[]; + extension?: unknown; + externalId?: string; + disabled?: boolean; + deletable?: boolean; + }[]; + }[]; + borderType?: TableBorder; + borderColor?: string; + borderWidth?: number; + borderExternalWidth?: number; + tableToolDisabled?: boolean; + // 超链接 + url?: string; + // 上下标 + actualSize?: number; + // 分割线 + dashArray?: number[]; + // 控件 + control?: { + type: { + TEXT = 'text', + SELECT = 'select', + CHECKBOX = 'checkbox', + RADIO = 'radio' + DATE = 'date', + NUMBER = 'number' + }; + value: IElement[] | null; + placeholder?: string; + groupId?: string; + conceptId?: string; + prefix?: string; + postfix?: string; + preText?: string; + postText?: string; + minWidth?: number; + underline?: boolean; + border?: boolean; + extension?: unknown; + indentation?: ControlIndentation; + rowFlex?: RowFlex + deletable?: boolean; + disabled?: boolean; + pasteDisabled?: boolean; + hide?: boolean; + code: string | null; + min?: number; + max?: number; + flexDirection: FlexDirection; + valueSets: { + value: string; + code: string; + }[]; + isMultiSelect?: boolean; + multiSelectDelimiter?: string; + dateFormat?: string; + font?: string; + size?: number; + bold?: boolean; + color?: string; + highlight?: string; + italic?: boolean; + strikeout?: boolean; + selectExclusiveOptions?: { + inputAble?: boolean; + } + }; + controlComponent?: { + PREFIX = 'prefix', + POSTFIX = 'postfix', + PLACEHOLDER = 'placeholder', + VALUE = 'value', + CHECKBOX = 'checkbox', + RADIO = 'radio' + }; + // 复选框 + checkbox?: { + value: boolean | null; + }; + // 单选框 + radio?: { + value: boolean | null; + }; + // LaTeX + laTexSVG?: string; + // 日期 + dateFormat?: string; + // 图片 + imgDisplay?: { + INLINE = 'inline', + BLOCK = 'block' + } + imgFloatPosition?: { + x: number; + y: number; + pageNo?: number; + } + imgToolDisabled?: boolean; + // 内容块 + block?: { + type: { + IFRAME = 'iframe', + VIDEO = 'video' + }; + iframeBlock?: { + src?: string; + srcdoc?: string; + }; + videoBlock?: { + src: string; + }; + }; + // 标题 + level?: TitleLevel; + title?: { + conceptId?: string; + deletable?: boolean; + disabled?: boolean; + }; + // 列表 + listType?: ListType; + listStyle?: ListStyle; + listWrap?: boolean; + // 区域 + areaId?: string; + area?: { + extension?: unknown; + top?: number; + hide?: boolean; + borderColor?: string; + backgroundColor?: string; + mode?: AreaMode; + deletable?: boolean; + placeholder?: IPlaceholder; + }; +} +``` diff --git a/docs/guide/shortcut-custom.md b/docs/guide/shortcut-custom.md new file mode 100644 index 0000000..dce51ca --- /dev/null +++ b/docs/guide/shortcut-custom.md @@ -0,0 +1,22 @@ +# 自定义快捷键 + +## 使用方式 + +```javascript +import Editor from "@hufe921/canvas-editor" + +const instance = new Editor(container, data, options) +instance.register.shortcutList([ + { + key: KeyMap; + ctrl?: boolean; + meta?: boolean; + mod?: boolean; // windows:ctrl || mac:command + shift?: boolean; + alt?: boolean; + isGlobal?: boolean; + callback?: (command: Command) => any; + disable?: boolean; + } + ]) +``` diff --git a/docs/guide/shortcut-internal.md b/docs/guide/shortcut-internal.md new file mode 100644 index 0000000..8434e66 --- /dev/null +++ b/docs/guide/shortcut-internal.md @@ -0,0 +1,189 @@ +# 内部快捷键 + +## Backspace + +功能:向前删除 + +## Delete + +功能:向后删除 + +## Enter + +功能:换行 + +## Shift + Enter + +功能:列表内换行 + +## ← + +功能:向左移动 + +## Shift + ← + +功能:向左缩放选区 + +## Ctrl/Cmd + ← + +功能:向左移动(跳单词) + +## Ctrl/Cmd + Shift + ← + +功能:向左缩放选区(跳单词) + +## → + +功能:向右移动 + +## Shift + → + +功能:向右缩放选区 + +## Ctrl/Cmd + → + +功能:向右移动(跳单词) + +## Ctrl/Cmd + Shift + → + +功能:向右缩放选区(跳单词) + +## ↑ + +功能:向上移动 + +## Shift + ↑ + +功能:向上缩放选区 + +## ↓ + +功能:向下移动 + +## Shift + ↓ + +功能:向下缩放选区 + +## Esc + +功能:退出格式刷 + +## Tab + +功能:增加缩进/移动到下一个控件 + +## Shift + Tab + +功能:移动到上一个控件 + +## Ctrl/Cmd + Z + +功能:撤销 + +## Ctrl/Cmd + Y + +功能:重做 + +## Ctrl/Cmd + C + +功能:复制 + +## Ctrl/Cmd + X + +功能:剪切 + +## Ctrl/Cmd + A + +功能:全选 + +## Ctrl/Cmd + S + +功能:保存 + +## Ctrl/Cmd + { + +功能:增大字体 + +## Ctrl/Cmd + } + +功能:减小字体 + +## Ctrl/Cmd + B + +功能:加粗 + +## Ctrl/Cmd + I + +功能:斜体 + +## Ctrl/Cmd + U + +功能:下划线 + +## Ctrl/Cmd + L + +功能:行居左 + +## Ctrl/Cmd + E + +功能:行居中 + +## Ctrl/Cmd + R + +功能:行居右 + +## Ctrl/Cmd + J + +功能:两端对齐 + +## Ctrl/Cmd + Shift + J + +功能:分散对齐 + +## Ctrl + Shift + X + +功能:删除线 + +## Ctrl/Cmd + Shift + > + +功能:上标 + +## Ctrl/Cmd + Shift + < + +功能:下标 + +## Ctrl + Alt/Option + 0 + +功能:正文 + +## Ctrl + Alt/Option + 1 + +功能:标题一 + +## Ctrl + Alt/Option + 2 + +功能:标题二 + +## Ctrl + Alt/Option + 3 + +功能:标题三 + +## Ctrl + Alt/Option + 4 + +功能:标题四 + +## Ctrl + Alt/Option + 5 + +功能:标题五 + +## Ctrl + Alt/Option + 6 + +功能:标题六 + +## Ctrl/Cmd + Shift + I + +功能:无序列表 + +## Ctrl/Cmd + Shift + U + +功能:有序列表 diff --git a/docs/guide/start.md b/docs/guide/start.md new file mode 100644 index 0000000..4968d5d --- /dev/null +++ b/docs/guide/start.md @@ -0,0 +1,97 @@ +# 入门 + +> 所见即所得的富文本编辑器。 + +得益于光标及文字排版的完全自行实现。绘制底层也可由 svg 渲染,详见代码:[feature/svg](https://github.com/Hufe921/canvas-editor/tree/feature/svg);或借助 pdfjs 可以完成 pdf 的绘制,详见代码:[feature/pdf](https://github.com/Hufe921/canvas-editor/tree/feature/pdf)。 + +::: warning +官方仅提供编辑器核心层 npm 包,菜单栏或其他外部工具可自行参考文档扩展,或直接参考[官方](https://github.com/Hufe921/canvas-editor)实现,详见[demo](https://hufe.club/canvas-editor/)。 +::: + +## 功能点 + +- 富文本操作(撤销、重做、字体、字号、加粗、斜体、下划线、删除线、上下标、对齐方式、标题、列表.....) +- 插入元素(表格、图片、链接、代码块、分页符、Math 公式、日期选择器、内容块......) +- 打印(基于 canvas 转图片、pdf 绘制) +- 控件(单选、文本、日期、单选框组、复选框组) +- 右键菜单(内部、自定义) +- 快捷键(内部、自定义) +- 拖拽(文字、元素、控件) +- 页眉、页脚、页码 +- 页边距 +- 分页 +- 水印 +- 批注 +- 目录 +- [插件](https://github.com/Hufe921/canvas-editor-plugin) + +## 待开发 + +- 计算性能 +- 控件规则 +- 表格分页 +- vue、react 等框架开箱即用版 + +## Step. 1: 下载 npm 包 + +```sh +npm i @hufe921/canvas-editor --save +``` + +## Step. 2: 准备一个容器 + +```html +
+``` + +## Step. 3: 实例化编辑器 + +- 仅包含正文内容 + +```javascript +import Editor from '@hufe921/canvas-editor' + +new Editor( + document.querySelector('.canvas-editor'), + [ + { + value: 'Hello World' + } + ], + {} +) +``` + +- 包含正文、页眉、页脚内容 + +```javascript +import Editor from '@hufe921/canvas-editor' + +new Editor( + document.querySelector('.canvas-editor'), + { + header: [ + { + value: 'Header', + rowFlex: RowFlex.CENTER + } + ], + main: [ + { + value: 'Hello World' + } + ], + footer: [ + { + value: 'canvas-editor', + size: 12 + } + ] + }, + {} +) +``` + +## Step. 4: 配置编辑器 + +详见下一节 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..2de4457 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,43 @@ +--- +layout: home + +title: canvas-editor +titleTemplate: rich text editor by canvas/svg + +hero: + name: canvas-editor + text: 基于canvas/svg的富文本编辑器 + actions: + - theme: brand + text: 开始 + link: /guide/start.html + - theme: alt + text: 在 GitHub 上查看 + link: https://github.com/Hufe921/canvas-editor + +features: + - icon: 💡 + title: 所见即所得 + details: 类word可分页,所见即所得 + - icon: ⚡️ + title: 轻量的数据结构 + details: 一段JSON即可呈现复杂样式 + - icon: 🛠️ + title: 丰富的功能 + details: 支持常见富文本操作、表格、水印、控件、公式等 + - icon: 📦 + title: 使用方便 + details: 官方发布核心npm包,菜单栏、工具栏可自行维护 + - icon: 🔩 + title: 灵活的开发机制 + details: 通过接口可获取生命周期、事件回调、自定义右键菜单、快捷键等 + - icon: 🔑 + title: 完全类型化的API + details: 灵活的 API 和完整的 TypeScript 类型 +--- + + diff --git a/docs/public/favicon.png b/docs/public/favicon.png new file mode 100644 index 0000000..d05e470 Binary files /dev/null and b/docs/public/favicon.png differ diff --git a/favicon.png b/favicon.png new file mode 100644 index 0000000..d05e470 Binary files /dev/null and b/favicon.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..cd4f492 --- /dev/null +++ b/index.html @@ -0,0 +1,421 @@ + + + + + + + + canvas-editor + + + +
+ +
+
+ 目录 +
+ +
+
+
+
+
+
+ +
+ + + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..c16e608 --- /dev/null +++ b/package.json @@ -0,0 +1,74 @@ +{ + "name": "@hufe921/canvas-editor", + "author": "Hufe", + "license": "MIT", + "version": "0.9.116", + "description": "rich text editor by canvas/svg", + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + }, + "files": [ + "dist", + "README.md", + "CHANGELOG.md", + "LICENSE", + "package.json" + ], + "typings": "./dist/src/editor/index.d.ts", + "main": "./dist/canvas-editor.umd.js", + "module": "./dist/canvas-editor.es.js", + "homepage": "https://github.com/Hufe921/canvas-editor", + "repository": { + "type": "git", + "url": "https://github.com/Hufe921/canvas-editor.git" + }, + "keywords": [ + "canvas-editor", + "editor", + "wysiwyg", + "emr" + ], + "engines": { + "node": ">=16.9.1" + }, + "type": "module", + "scripts": { + "dev": "vite", + "lib": "npm run lint && tsc && vite build --mode lib", + "build": "npm run lint && tsc && vite build --mode app", + "serve": "vite preview", + "lint": "eslint .", + "cypress:open": "cypress open", + "cypress:run": "cypress run", + "type:check": "tsc --noEmit", + "docs:dev": "vitepress dev docs", + "docs:build": "vitepress build docs", + "docs:preview": "vitepress preview docs", + "postinstall": "simple-git-hooks", + "release": "node scripts/release.js" + }, + "devDependencies": { + "@rollup/plugin-typescript": "^10.0.1", + "@types/node": "16.18.96", + "@types/prismjs": "^1.26.0", + "@typescript-eslint/eslint-plugin": "5.62.0", + "@typescript-eslint/parser": "5.62.0", + "cypress": "13.6.0", + "cypress-file-upload": "^5.0.8", + "eslint": "7.32.0", + "simple-git-hooks": "^2.8.1", + "typescript": "4.9.5", + "vite": "^2.4.2", + "vite-plugin-css-injected-by-js": "^2.1.1", + "vitepress": "1.0.0-beta.6", + "vue": "^3.2.45" + }, + "dependencies": { + "prismjs": "^1.27.0" + }, + "simple-git-hooks": { + "pre-commit": "npm run lint && npm run type:check", + "commit-msg": "node scripts/verifyCommit.js" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..7839a57 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3742 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + prismjs: + specifier: ^1.27.0 + version: 1.30.0 + devDependencies: + '@rollup/plugin-typescript': + specifier: ^10.0.1 + version: 10.0.1(rollup@3.29.5)(tslib@2.8.1)(typescript@4.9.5) + '@types/node': + specifier: 16.18.96 + version: 16.18.96 + '@types/prismjs': + specifier: ^1.26.0 + version: 1.26.5 + '@typescript-eslint/eslint-plugin': + specifier: 5.62.0 + version: 5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5))(eslint@7.32.0)(typescript@4.9.5) + '@typescript-eslint/parser': + specifier: 5.62.0 + version: 5.62.0(eslint@7.32.0)(typescript@4.9.5) + cypress: + specifier: 13.6.0 + version: 13.6.0 + cypress-file-upload: + specifier: ^5.0.8 + version: 5.0.8(cypress@13.6.0) + eslint: + specifier: 7.32.0 + version: 7.32.0 + simple-git-hooks: + specifier: ^2.8.1 + version: 2.13.1 + typescript: + specifier: 4.9.5 + version: 4.9.5 + vite: + specifier: ^2.4.2 + version: 2.9.18 + vite-plugin-css-injected-by-js: + specifier: ^2.1.1 + version: 2.4.0(vite@2.9.18) + vitepress: + specifier: 1.0.0-beta.6 + version: 1.0.0-beta.6(@algolia/client-search@5.37.0)(@types/node@16.18.96)(search-insights@2.17.3)(typescript@4.9.5) + vue: + specifier: ^3.2.45 + version: 3.5.21(typescript@4.9.5) + +packages: + + '@algolia/abtesting@1.3.0': + resolution: {integrity: sha512-KqPVLdVNfoJzX5BKNGM9bsW8saHeyax8kmPFXul5gejrSPN3qss7PgsFH5mMem7oR8tvjvNkia97ljEYPYCN8Q==} + engines: {node: '>= 14.0.0'} + + '@algolia/autocomplete-core@1.17.9': + resolution: {integrity: sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ==} + + '@algolia/autocomplete-plugin-algolia-insights@1.17.9': + resolution: {integrity: sha512-u1fEHkCbWF92DBeB/KHeMacsjsoI0wFhjZtlCq2ddZbAehshbZST6Hs0Avkc0s+4UyBGbMDnSuXHLuvRWK5iDQ==} + peerDependencies: + search-insights: '>= 1 < 3' + + '@algolia/autocomplete-preset-algolia@1.17.9': + resolution: {integrity: sha512-Na1OuceSJeg8j7ZWn5ssMu/Ax3amtOwk76u4h5J4eK2Nx2KB5qt0Z4cOapCsxot9VcEN11ADV5aUSlQF4RhGjQ==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/autocomplete-shared@1.17.9': + resolution: {integrity: sha512-iDf05JDQ7I0b7JEA/9IektxN/80a2MZ1ToohfmNS3rfeuQnIKI3IJlIafD0xu4StbtQTghx9T3Maa97ytkXenQ==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/client-abtesting@5.37.0': + resolution: {integrity: sha512-Dp2Zq+x9qQFnuiQhVe91EeaaPxWBhzwQ6QnznZQnH9C1/ei3dvtmAFfFeaTxM6FzfJXDLvVnaQagTYFTQz3R5g==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-analytics@5.37.0': + resolution: {integrity: sha512-wyXODDOluKogTuZxRII6mtqhAq4+qUR3zIUJEKTiHLe8HMZFxfUEI4NO2qSu04noXZHbv/sRVdQQqzKh12SZuQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-common@5.37.0': + resolution: {integrity: sha512-GylIFlPvLy9OMgFG8JkonIagv3zF+Dx3H401Uo2KpmfMVBBJiGfAb9oYfXtplpRMZnZPxF5FnkWaI/NpVJMC+g==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-insights@5.37.0': + resolution: {integrity: sha512-T63afO2O69XHKw2+F7mfRoIbmXWGzgpZxgOFAdP3fR4laid7pWBt20P4eJ+Zn23wXS5kC9P2K7Bo3+rVjqnYiw==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-personalization@5.37.0': + resolution: {integrity: sha512-1zOIXM98O9zD8bYDCJiUJRC/qNUydGHK/zRK+WbLXrW1SqLFRXECsKZa5KoG166+o5q5upk96qguOtE8FTXDWQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-query-suggestions@5.37.0': + resolution: {integrity: sha512-31Nr2xOLBCYVal+OMZn1rp1H4lPs1914Tfr3a34wU/nsWJ+TB3vWjfkUUuuYhWoWBEArwuRzt3YNLn0F/KRVkg==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-search@5.37.0': + resolution: {integrity: sha512-DAFVUvEg+u7jUs6BZiVz9zdaUebYULPiQ4LM2R4n8Nujzyj7BZzGr2DCd85ip4p/cx7nAZWKM8pLcGtkTRTdsg==} + engines: {node: '>= 14.0.0'} + + '@algolia/ingestion@1.37.0': + resolution: {integrity: sha512-pkCepBRRdcdd7dTLbFddnu886NyyxmhgqiRcHHaDunvX03Ij4WzvouWrQq7B7iYBjkMQrLS8wQqSP0REfA4W8g==} + engines: {node: '>= 14.0.0'} + + '@algolia/monitoring@1.37.0': + resolution: {integrity: sha512-fNw7pVdyZAAQQCJf1cc/ih4fwrRdQSgKwgor4gchsI/Q/ss9inmC6bl/69jvoRSzgZS9BX4elwHKdo0EfTli3w==} + engines: {node: '>= 14.0.0'} + + '@algolia/recommend@5.37.0': + resolution: {integrity: sha512-U+FL5gzN2ldx3TYfQO5OAta2TBuIdabEdFwD5UVfWPsZE5nvOKkc/6BBqP54Z/adW/34c5ZrvvZhlhNTZujJXQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-browser-xhr@5.37.0': + resolution: {integrity: sha512-Ao8GZo8WgWFABrU7iq+JAftXV0t+UcOtCDL4mzHHZ+rQeTTf1TZssr4d0vIuoqkVNnKt9iyZ7T4lQff4ydcTrw==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-fetch@5.37.0': + resolution: {integrity: sha512-H7OJOXrFg5dLcGJ22uxx8eiFId0aB9b0UBhoOi4SMSuDBe6vjJJ/LeZyY25zPaSvkXNBN3vAM+ad6M0h6ha3AA==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-node-http@5.37.0': + resolution: {integrity: sha512-npZ9aeag4SGTx677eqPL3rkSPlQrnzx/8wNrl1P7GpWq9w/eTmRbOq+wKrJ2r78idlY0MMgmY/mld2tq6dc44g==} + engines: {node: '>= 14.0.0'} + + '@babel/code-frame@7.12.11': + resolution: {integrity: sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.25.9': + resolution: {integrity: sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + + '@cypress/request@3.0.9': + resolution: {integrity: sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw==} + engines: {node: '>= 6'} + + '@cypress/xvfb@1.2.4': + resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==} + + '@docsearch/css@3.9.0': + resolution: {integrity: sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA==} + + '@docsearch/js@3.9.0': + resolution: {integrity: sha512-4bKHcye6EkLgRE8ze0vcdshmEqxeiJM77M0JXjef7lrYZfSlMunrDOCqyLjiZyo1+c0BhUqA2QpFartIjuHIjw==} + + '@docsearch/react@3.9.0': + resolution: {integrity: sha512-mb5FOZYZIkRQ6s/NWnM98k879vu5pscWqTLubLFBO87igYYT4VzVazh4h5o/zCvTIZgEt3PvsCOMOswOUo9yHQ==} + peerDependencies: + '@types/react': '>= 16.8.0 < 20.0.0' + react: '>= 16.8.0 < 20.0.0' + react-dom: '>= 16.8.0 < 20.0.0' + search-insights: '>= 1 < 3' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + search-insights: + optional: true + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.14.54': + resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@0.4.3': + resolution: {integrity: sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==} + engines: {node: ^10.12.0 || >=12.0.0} + + '@humanwhocodes/config-array@0.5.0': + resolution: {integrity: sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/object-schema@1.2.1': + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + deprecated: Use @eslint/object-schema instead + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@rollup/plugin-typescript@10.0.1': + resolution: {integrity: sha512-wBykxRLlX7EzL8BmUqMqk5zpx2onnmRMSw/l9M1sVfkJvdwfxogZQVNUM9gVMJbjRLDR5H6U0OMOrlDGmIV45A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.14.0||^3.0.0 + tslib: '*' + typescript: '>=3.7.0' + peerDependenciesMeta: + rollup: + optional: true + tslib: + optional: true + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@16.18.96': + resolution: {integrity: sha512-84iSqGXoO+Ha16j8pRZ/L90vDMKX04QTYMTfYeE1WrjWaZXuchBehGUZEpNgx7JnmlrIHdnABmpjrQjhCnNldQ==} + + '@types/node@18.19.127': + resolution: {integrity: sha512-gSjxjrnKXML/yo0BO099uPixMqfpJU0TKYjpfLU7TrtA2WWDki412Np/RSTPRil1saKBhvVVKzVx/p/6p94nVA==} + + '@types/prismjs@1.26.5': + resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} + + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + + '@types/sinonjs__fake-timers@8.1.1': + resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==} + + '@types/sizzle@2.3.10': + resolution: {integrity: sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==} + + '@types/web-bluetooth@0.0.20': + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@typescript-eslint/eslint-plugin@5.62.0': + resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@5.62.0': + resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@5.62.0': + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/type-utils@5.62.0': + resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@5.62.0': + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/typescript-estree@5.62.0': + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@5.62.0': + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@typescript-eslint/visitor-keys@5.62.0': + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@vitejs/plugin-vue@4.6.2': + resolution: {integrity: sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.0.0 || ^5.0.0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.21': + resolution: {integrity: sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==} + + '@vue/compiler-dom@3.5.21': + resolution: {integrity: sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==} + + '@vue/compiler-sfc@3.5.21': + resolution: {integrity: sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==} + + '@vue/compiler-ssr@3.5.21': + resolution: {integrity: sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/reactivity@3.5.21': + resolution: {integrity: sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==} + + '@vue/runtime-core@3.5.21': + resolution: {integrity: sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==} + + '@vue/runtime-dom@3.5.21': + resolution: {integrity: sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==} + + '@vue/server-renderer@3.5.21': + resolution: {integrity: sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==} + peerDependencies: + vue: 3.5.21 + + '@vue/shared@3.5.21': + resolution: {integrity: sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==} + + '@vueuse/core@10.11.1': + resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==} + + '@vueuse/integrations@10.11.1': + resolution: {integrity: sha512-Y5hCGBguN+vuVYTZmdd/IMXLOdfS60zAmDmFYc4BKBcMUPZH1n4tdyDECCPjXm0bNT3ZRUy1xzTLGaUje8Xyaw==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^4 + drauu: ^0.3 + focus-trap: ^7 + fuse.js: ^6 + idb-keyval: ^6 + jwt-decode: ^3 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^6 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + + '@vueuse/metadata@10.11.1': + resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==} + + '@vueuse/shared@10.11.1': + resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + algoliasearch@5.37.0: + resolution: {integrity: sha512-y7gau/ZOQDqoInTQp0IwTOjkrHc4Aq4R8JgpmCleFwiLl+PbN2DMWoDUWZnrK8AhNJwT++dn28Bt4NZYNLAmuA==} + engines: {node: '>= 14.0.0'} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-sequence-parser@1.1.3: + resolution: {integrity: sha512-+fksAx9eG3Ab6LDnLs3ZqZa8KVJ/jYnX+D4Qe1azX+LFGFAXqynCQLOdLpNYN/l9e7l6hMWwZbrnctqr6eSQSw==} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + arch@2.2.0: + resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + aws-sign2@0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + + blob-util@2.0.2: + resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + body-scroll-lock@4.0.0-beta.0: + resolution: {integrity: sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + cachedir@2.4.0: + resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==} + engines: {node: '>=6'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + check-more-types@2.24.0: + resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} + engines: {node: '>= 0.8.0'} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + cypress-file-upload@5.0.8: + resolution: {integrity: sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g==} + engines: {node: '>=8.2.1'} + peerDependencies: + cypress: '>3.0.0' + + cypress@13.6.0: + resolution: {integrity: sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==} + engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} + hasBin: true + + dashdash@1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + + dayjs@1.11.18: + resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecc-jsbn@0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild-android-64@0.14.54: + resolution: {integrity: sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + esbuild-android-arm64@0.14.54: + resolution: {integrity: sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + esbuild-darwin-64@0.14.54: + resolution: {integrity: sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + esbuild-darwin-arm64@0.14.54: + resolution: {integrity: sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + esbuild-freebsd-64@0.14.54: + resolution: {integrity: sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + esbuild-freebsd-arm64@0.14.54: + resolution: {integrity: sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + esbuild-linux-32@0.14.54: + resolution: {integrity: sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + esbuild-linux-64@0.14.54: + resolution: {integrity: sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + esbuild-linux-arm64@0.14.54: + resolution: {integrity: sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + esbuild-linux-arm@0.14.54: + resolution: {integrity: sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + esbuild-linux-mips64le@0.14.54: + resolution: {integrity: sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + esbuild-linux-ppc64le@0.14.54: + resolution: {integrity: sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + esbuild-linux-riscv64@0.14.54: + resolution: {integrity: sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + esbuild-linux-s390x@0.14.54: + resolution: {integrity: sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + esbuild-netbsd-64@0.14.54: + resolution: {integrity: sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + esbuild-openbsd-64@0.14.54: + resolution: {integrity: sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + esbuild-sunos-64@0.14.54: + resolution: {integrity: sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + esbuild-windows-32@0.14.54: + resolution: {integrity: sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + esbuild-windows-64@0.14.54: + resolution: {integrity: sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + esbuild-windows-arm64@0.14.54: + resolution: {integrity: sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + esbuild@0.14.54: + resolution: {integrity: sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-utils@2.1.0: + resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} + engines: {node: '>=6'} + + eslint-visitor-keys@1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} + + eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@7.32.0: + resolution: {integrity: sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==} + engines: {node: ^10.12.0 || >=12.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@7.3.1: + resolution: {integrity: sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==} + engines: {node: ^10.12.0 || >=12.0.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter2@6.4.7: + resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==} + + execa@4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} + + executable@4.1.1: + resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} + engines: {node: '>=4'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + extsprintf@1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + focus-trap@7.6.5: + resolution: {integrity: sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==} + + forever-agent@0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + functional-red-black-tree@1.0.1: + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + getos@3.2.1: + resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==} + + getpass@0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + global-dirs@3.0.1: + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-signature@1.4.0: + resolution: {integrity: sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==} + engines: {node: '>=0.10'} + + human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@4.0.6: + resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==} + engines: {node: '>= 4'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + + is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-installed-globally@0.4.0: + resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} + engines: {node: '>=10'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + jsbn@0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jsprim@2.0.2: + resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==} + engines: {'0': node >=0.6.0} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + lazy-ass@1.6.0: + resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} + engines: {node: '> 0.8'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + listr2@3.14.0: + resolution: {integrity: sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==} + engines: {node: '>=10.0.0'} + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-update@4.0.0: + resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} + engines: {node: '>=10'} + + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + + mark.js@8.11.1: + resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minisearch@6.3.0: + resolution: {integrity: sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ospath@1.2.2: + resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + preact@10.27.2: + resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + proxy-from-env@1.0.0: + resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + regexpp@3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + + request-progress@3.0.0: + resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@2.77.3: + resolution: {integrity: sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==} + engines: {node: '>=10.0.0'} + hasBin: true + + rollup@3.29.5: + resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + search-insights@2.17.3: + resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shiki@0.14.7: + resolution: {integrity: sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + simple-git-hooks@2.13.1: + resolution: {integrity: sha512-WszCLXwT4h2k1ufIXAgsbiTOazqqevFCIncOuUBZJ91DdvWcC5+OFkluWRQPrcuSYd8fjq+o2y1QfWqYMoAToQ==} + hasBin: true + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + sshpk@1.18.0: + resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + throttleit@1.0.1: + resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsutils@3.21.0: + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + untildify@4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + v8-compile-cache@2.4.0: + resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==} + + verror@1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + + vite-plugin-css-injected-by-js@2.4.0: + resolution: {integrity: sha512-fQkJ5baPEasjjJLxHINLjXuPREO61VIDFUeUqleEBghOLfZZe/k/zrxG5b3kFZXu6JtdI11pnwtj3dh3CN9X4Q==} + peerDependencies: + vite: '>2.0.0-0' + + vite@2.9.18: + resolution: {integrity: sha512-sAOqI5wNM9QvSEE70W3UGMdT8cyEn0+PmJMTFvTB8wB0YbYUWw3gUbY62AOyrXosGieF2htmeLATvNxpv/zNyQ==} + engines: {node: '>=12.2.0'} + hasBin: true + peerDependencies: + less: '*' + sass: '*' + stylus: '*' + peerDependenciesMeta: + less: + optional: true + sass: + optional: true + stylus: + optional: true + + vite@4.5.14: + resolution: {integrity: sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitepress@1.0.0-beta.6: + resolution: {integrity: sha512-xK/ulKgQpKZVbvlL4+/vW49VG7ySi5nmSoKUNH1G4kM+Cj9JwYM+PDJO7jSJROv8zW99G0ise+maDYnaLlbGBQ==} + hasBin: true + + vscode-oniguruma@1.7.0: + resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} + + vscode-textmate@8.0.0: + resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue@3.5.21: + resolution: {integrity: sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + +snapshots: + + '@algolia/abtesting@1.3.0': + dependencies: + '@algolia/client-common': 5.37.0 + '@algolia/requester-browser-xhr': 5.37.0 + '@algolia/requester-fetch': 5.37.0 + '@algolia/requester-node-http': 5.37.0 + + '@algolia/autocomplete-core@1.17.9(@algolia/client-search@5.37.0)(algoliasearch@5.37.0)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-plugin-algolia-insights': 1.17.9(@algolia/client-search@5.37.0)(algoliasearch@5.37.0)(search-insights@2.17.3) + '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.37.0)(algoliasearch@5.37.0) + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + - search-insights + + '@algolia/autocomplete-plugin-algolia-insights@1.17.9(@algolia/client-search@5.37.0)(algoliasearch@5.37.0)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.37.0)(algoliasearch@5.37.0) + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + + '@algolia/autocomplete-preset-algolia@1.17.9(@algolia/client-search@5.37.0)(algoliasearch@5.37.0)': + dependencies: + '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.37.0)(algoliasearch@5.37.0) + '@algolia/client-search': 5.37.0 + algoliasearch: 5.37.0 + + '@algolia/autocomplete-shared@1.17.9(@algolia/client-search@5.37.0)(algoliasearch@5.37.0)': + dependencies: + '@algolia/client-search': 5.37.0 + algoliasearch: 5.37.0 + + '@algolia/client-abtesting@5.37.0': + dependencies: + '@algolia/client-common': 5.37.0 + '@algolia/requester-browser-xhr': 5.37.0 + '@algolia/requester-fetch': 5.37.0 + '@algolia/requester-node-http': 5.37.0 + + '@algolia/client-analytics@5.37.0': + dependencies: + '@algolia/client-common': 5.37.0 + '@algolia/requester-browser-xhr': 5.37.0 + '@algolia/requester-fetch': 5.37.0 + '@algolia/requester-node-http': 5.37.0 + + '@algolia/client-common@5.37.0': {} + + '@algolia/client-insights@5.37.0': + dependencies: + '@algolia/client-common': 5.37.0 + '@algolia/requester-browser-xhr': 5.37.0 + '@algolia/requester-fetch': 5.37.0 + '@algolia/requester-node-http': 5.37.0 + + '@algolia/client-personalization@5.37.0': + dependencies: + '@algolia/client-common': 5.37.0 + '@algolia/requester-browser-xhr': 5.37.0 + '@algolia/requester-fetch': 5.37.0 + '@algolia/requester-node-http': 5.37.0 + + '@algolia/client-query-suggestions@5.37.0': + dependencies: + '@algolia/client-common': 5.37.0 + '@algolia/requester-browser-xhr': 5.37.0 + '@algolia/requester-fetch': 5.37.0 + '@algolia/requester-node-http': 5.37.0 + + '@algolia/client-search@5.37.0': + dependencies: + '@algolia/client-common': 5.37.0 + '@algolia/requester-browser-xhr': 5.37.0 + '@algolia/requester-fetch': 5.37.0 + '@algolia/requester-node-http': 5.37.0 + + '@algolia/ingestion@1.37.0': + dependencies: + '@algolia/client-common': 5.37.0 + '@algolia/requester-browser-xhr': 5.37.0 + '@algolia/requester-fetch': 5.37.0 + '@algolia/requester-node-http': 5.37.0 + + '@algolia/monitoring@1.37.0': + dependencies: + '@algolia/client-common': 5.37.0 + '@algolia/requester-browser-xhr': 5.37.0 + '@algolia/requester-fetch': 5.37.0 + '@algolia/requester-node-http': 5.37.0 + + '@algolia/recommend@5.37.0': + dependencies: + '@algolia/client-common': 5.37.0 + '@algolia/requester-browser-xhr': 5.37.0 + '@algolia/requester-fetch': 5.37.0 + '@algolia/requester-node-http': 5.37.0 + + '@algolia/requester-browser-xhr@5.37.0': + dependencies: + '@algolia/client-common': 5.37.0 + + '@algolia/requester-fetch@5.37.0': + dependencies: + '@algolia/client-common': 5.37.0 + + '@algolia/requester-node-http@5.37.0': + dependencies: + '@algolia/client-common': 5.37.0 + + '@babel/code-frame@7.12.11': + dependencies: + '@babel/highlight': 7.25.9 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/highlight@7.25.9': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@colors/colors@1.5.0': + optional: true + + '@cypress/request@3.0.9': + dependencies: + aws-sign2: 0.7.0 + aws4: 1.13.2 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 4.0.4 + http-signature: 1.4.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + performance-now: 2.1.0 + qs: 6.14.0 + safe-buffer: 5.2.1 + tough-cookie: 5.1.2 + tunnel-agent: 0.6.0 + uuid: 8.3.2 + + '@cypress/xvfb@1.2.4(supports-color@8.1.1)': + dependencies: + debug: 3.2.7(supports-color@8.1.1) + lodash.once: 4.1.1 + transitivePeerDependencies: + - supports-color + + '@docsearch/css@3.9.0': {} + + '@docsearch/js@3.9.0(@algolia/client-search@5.37.0)(search-insights@2.17.3)': + dependencies: + '@docsearch/react': 3.9.0(@algolia/client-search@5.37.0)(search-insights@2.17.3) + preact: 10.27.2 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/react' + - react + - react-dom + - search-insights + + '@docsearch/react@3.9.0(@algolia/client-search@5.37.0)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-core': 1.17.9(@algolia/client-search@5.37.0)(algoliasearch@5.37.0)(search-insights@2.17.3) + '@algolia/autocomplete-preset-algolia': 1.17.9(@algolia/client-search@5.37.0)(algoliasearch@5.37.0) + '@docsearch/css': 3.9.0 + algoliasearch: 5.37.0 + optionalDependencies: + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.14.54': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@7.32.0)': + dependencies: + eslint: 7.32.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/eslintrc@0.4.3': + dependencies: + ajv: 6.12.6 + debug: 4.4.3(supports-color@8.1.1) + espree: 7.3.1 + globals: 13.24.0 + ignore: 4.0.6 + import-fresh: 3.3.1 + js-yaml: 3.14.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/config-array@0.5.0': + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/object-schema@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@rollup/plugin-typescript@10.0.1(rollup@3.29.5)(tslib@2.8.1)(typescript@4.9.5)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@3.29.5) + resolve: 1.22.10 + typescript: 4.9.5 + optionalDependencies: + rollup: 3.29.5 + tslib: 2.8.1 + + '@rollup/pluginutils@5.3.0(rollup@3.29.5)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 3.29.5 + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@16.18.96': {} + + '@types/node@18.19.127': + dependencies: + undici-types: 5.26.5 + + '@types/prismjs@1.26.5': {} + + '@types/semver@7.7.1': {} + + '@types/sinonjs__fake-timers@8.1.1': {} + + '@types/sizzle@2.3.10': {} + + '@types/web-bluetooth@0.0.20': {} + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 16.18.96 + optional: true + + '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5))(eslint@7.32.0)(typescript@4.9.5)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@4.9.5) + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/type-utils': 5.62.0(eslint@7.32.0)(typescript@4.9.5) + '@typescript-eslint/utils': 5.62.0(eslint@7.32.0)(typescript@4.9.5) + debug: 4.4.3(supports-color@8.1.1) + eslint: 7.32.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare-lite: 1.4.0 + semver: 7.7.2 + tsutils: 3.21.0(typescript@4.9.5) + optionalDependencies: + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5)': + dependencies: + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) + debug: 4.4.3(supports-color@8.1.1) + eslint: 7.32.0 + optionalDependencies: + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + + '@typescript-eslint/type-utils@5.62.0(eslint@7.32.0)(typescript@4.9.5)': + dependencies: + '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) + '@typescript-eslint/utils': 5.62.0(eslint@7.32.0)(typescript@4.9.5) + debug: 4.4.3(supports-color@8.1.1) + eslint: 7.32.0 + tsutils: 3.21.0(typescript@4.9.5) + optionalDependencies: + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@5.62.0': {} + + '@typescript-eslint/typescript-estree@5.62.0(typescript@4.9.5)': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.4.3(supports-color@8.1.1) + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.7.2 + tsutils: 3.21.0(typescript@4.9.5) + optionalDependencies: + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@5.62.0(eslint@7.32.0)(typescript@4.9.5)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@7.32.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.7.1 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) + eslint: 7.32.0 + eslint-scope: 5.1.1 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + + '@vitejs/plugin-vue@4.6.2(vite@4.5.14(@types/node@16.18.96))(vue@3.5.21(typescript@4.9.5))': + dependencies: + vite: 4.5.14(@types/node@16.18.96) + vue: 3.5.21(typescript@4.9.5) + + '@vue/compiler-core@3.5.21': + dependencies: + '@babel/parser': 7.28.4 + '@vue/shared': 3.5.21 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.21': + dependencies: + '@vue/compiler-core': 3.5.21 + '@vue/shared': 3.5.21 + + '@vue/compiler-sfc@3.5.21': + dependencies: + '@babel/parser': 7.28.4 + '@vue/compiler-core': 3.5.21 + '@vue/compiler-dom': 3.5.21 + '@vue/compiler-ssr': 3.5.21 + '@vue/shared': 3.5.21 + estree-walker: 2.0.2 + magic-string: 0.30.19 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.21': + dependencies: + '@vue/compiler-dom': 3.5.21 + '@vue/shared': 3.5.21 + + '@vue/devtools-api@6.6.4': {} + + '@vue/reactivity@3.5.21': + dependencies: + '@vue/shared': 3.5.21 + + '@vue/runtime-core@3.5.21': + dependencies: + '@vue/reactivity': 3.5.21 + '@vue/shared': 3.5.21 + + '@vue/runtime-dom@3.5.21': + dependencies: + '@vue/reactivity': 3.5.21 + '@vue/runtime-core': 3.5.21 + '@vue/shared': 3.5.21 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.21(vue@3.5.21(typescript@4.9.5))': + dependencies: + '@vue/compiler-ssr': 3.5.21 + '@vue/shared': 3.5.21 + vue: 3.5.21(typescript@4.9.5) + + '@vue/shared@3.5.21': {} + + '@vueuse/core@10.11.1(vue@3.5.21(typescript@4.9.5))': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 10.11.1 + '@vueuse/shared': 10.11.1(vue@3.5.21(typescript@4.9.5)) + vue-demi: 0.14.10(vue@3.5.21(typescript@4.9.5)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/integrations@10.11.1(focus-trap@7.6.5)(vue@3.5.21(typescript@4.9.5))': + dependencies: + '@vueuse/core': 10.11.1(vue@3.5.21(typescript@4.9.5)) + '@vueuse/shared': 10.11.1(vue@3.5.21(typescript@4.9.5)) + vue-demi: 0.14.10(vue@3.5.21(typescript@4.9.5)) + optionalDependencies: + focus-trap: 7.6.5 + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@10.11.1': {} + + '@vueuse/shared@10.11.1(vue@3.5.21(typescript@4.9.5))': + dependencies: + vue-demi: 0.14.10(vue@3.5.21(typescript@4.9.5)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + acorn-jsx@5.3.2(acorn@7.4.1): + dependencies: + acorn: 7.4.1 + + acorn@7.4.1: {} + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + algoliasearch@5.37.0: + dependencies: + '@algolia/abtesting': 1.3.0 + '@algolia/client-abtesting': 5.37.0 + '@algolia/client-analytics': 5.37.0 + '@algolia/client-common': 5.37.0 + '@algolia/client-insights': 5.37.0 + '@algolia/client-personalization': 5.37.0 + '@algolia/client-query-suggestions': 5.37.0 + '@algolia/client-search': 5.37.0 + '@algolia/ingestion': 1.37.0 + '@algolia/monitoring': 1.37.0 + '@algolia/recommend': 5.37.0 + '@algolia/requester-browser-xhr': 5.37.0 + '@algolia/requester-fetch': 5.37.0 + '@algolia/requester-node-http': 5.37.0 + + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-sequence-parser@1.1.3: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + arch@2.2.0: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + array-union@2.1.0: {} + + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + + assert-plus@1.0.0: {} + + astral-regex@2.0.0: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + aws-sign2@0.7.0: {} + + aws4@1.13.2: {} + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + + blob-util@2.0.2: {} + + bluebird@3.7.2: {} + + body-scroll-lock@4.0.0-beta.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-crc32@0.2.13: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + cachedir@2.4.0: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caseless@0.12.0: {} + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-more-types@2.24.0: {} + + ci-info@3.9.0: {} + + clean-stack@2.2.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + + cli-truncate@2.1.0: + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@6.2.1: {} + + common-tags@1.8.2: {} + + concat-map@0.0.1: {} + + core-util-is@1.0.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.1.3: {} + + cypress-file-upload@5.0.8(cypress@13.6.0): + dependencies: + cypress: 13.6.0 + + cypress@13.6.0: + dependencies: + '@cypress/request': 3.0.9 + '@cypress/xvfb': 1.2.4(supports-color@8.1.1) + '@types/node': 18.19.127 + '@types/sinonjs__fake-timers': 8.1.1 + '@types/sizzle': 2.3.10 + arch: 2.2.0 + blob-util: 2.0.2 + bluebird: 3.7.2 + buffer: 5.7.1 + cachedir: 2.4.0 + chalk: 4.1.2 + check-more-types: 2.24.0 + cli-cursor: 3.1.0 + cli-table3: 0.6.5 + commander: 6.2.1 + common-tags: 1.8.2 + dayjs: 1.11.18 + debug: 4.4.3(supports-color@8.1.1) + enquirer: 2.4.1 + eventemitter2: 6.4.7 + execa: 4.1.0 + executable: 4.1.1 + extract-zip: 2.0.1(supports-color@8.1.1) + figures: 3.2.0 + fs-extra: 9.1.0 + getos: 3.2.1 + is-ci: 3.0.1 + is-installed-globally: 0.4.0 + lazy-ass: 1.6.0 + listr2: 3.14.0(enquirer@2.4.1) + lodash: 4.17.21 + log-symbols: 4.1.0 + minimist: 1.2.8 + ospath: 1.2.2 + pretty-bytes: 5.6.0 + process: 0.11.10 + proxy-from-env: 1.0.0 + request-progress: 3.0.0 + semver: 7.7.2 + supports-color: 8.1.1 + tmp: 0.2.5 + untildify: 4.0.0 + yauzl: 2.10.0 + + dashdash@1.14.1: + dependencies: + assert-plus: 1.0.0 + + dayjs@1.11.18: {} + + debug@3.2.7(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + debug@4.4.3(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + deep-is@0.1.4: {} + + delayed-stream@1.0.0: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecc-jsbn@0.1.2: + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + + emoji-regex@8.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + entities@4.5.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild-android-64@0.14.54: + optional: true + + esbuild-android-arm64@0.14.54: + optional: true + + esbuild-darwin-64@0.14.54: + optional: true + + esbuild-darwin-arm64@0.14.54: + optional: true + + esbuild-freebsd-64@0.14.54: + optional: true + + esbuild-freebsd-arm64@0.14.54: + optional: true + + esbuild-linux-32@0.14.54: + optional: true + + esbuild-linux-64@0.14.54: + optional: true + + esbuild-linux-arm64@0.14.54: + optional: true + + esbuild-linux-arm@0.14.54: + optional: true + + esbuild-linux-mips64le@0.14.54: + optional: true + + esbuild-linux-ppc64le@0.14.54: + optional: true + + esbuild-linux-riscv64@0.14.54: + optional: true + + esbuild-linux-s390x@0.14.54: + optional: true + + esbuild-netbsd-64@0.14.54: + optional: true + + esbuild-openbsd-64@0.14.54: + optional: true + + esbuild-sunos-64@0.14.54: + optional: true + + esbuild-windows-32@0.14.54: + optional: true + + esbuild-windows-64@0.14.54: + optional: true + + esbuild-windows-arm64@0.14.54: + optional: true + + esbuild@0.14.54: + optionalDependencies: + '@esbuild/linux-loong64': 0.14.54 + esbuild-android-64: 0.14.54 + esbuild-android-arm64: 0.14.54 + esbuild-darwin-64: 0.14.54 + esbuild-darwin-arm64: 0.14.54 + esbuild-freebsd-64: 0.14.54 + esbuild-freebsd-arm64: 0.14.54 + esbuild-linux-32: 0.14.54 + esbuild-linux-64: 0.14.54 + esbuild-linux-arm: 0.14.54 + esbuild-linux-arm64: 0.14.54 + esbuild-linux-mips64le: 0.14.54 + esbuild-linux-ppc64le: 0.14.54 + esbuild-linux-riscv64: 0.14.54 + esbuild-linux-s390x: 0.14.54 + esbuild-netbsd-64: 0.14.54 + esbuild-openbsd-64: 0.14.54 + esbuild-sunos-64: 0.14.54 + esbuild-windows-32: 0.14.54 + esbuild-windows-64: 0.14.54 + esbuild-windows-arm64: 0.14.54 + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-utils@2.1.0: + dependencies: + eslint-visitor-keys: 1.3.0 + + eslint-visitor-keys@1.3.0: {} + + eslint-visitor-keys@2.1.0: {} + + eslint-visitor-keys@3.4.3: {} + + eslint@7.32.0: + dependencies: + '@babel/code-frame': 7.12.11 + '@eslint/eslintrc': 0.4.3 + '@humanwhocodes/config-array': 0.5.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@8.1.1) + doctrine: 3.0.0 + enquirer: 2.4.1 + escape-string-regexp: 4.0.0 + eslint-scope: 5.1.1 + eslint-utils: 2.1.0 + eslint-visitor-keys: 2.1.0 + espree: 7.3.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + functional-red-black-tree: 1.0.1 + glob-parent: 5.1.2 + globals: 13.24.0 + ignore: 4.0.6 + import-fresh: 3.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + js-yaml: 3.14.1 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + progress: 2.0.3 + regexpp: 3.2.0 + semver: 7.7.2 + strip-ansi: 6.0.1 + strip-json-comments: 3.1.1 + table: 6.9.0 + text-table: 0.2.0 + v8-compile-cache: 2.4.0 + transitivePeerDependencies: + - supports-color + + espree@7.3.1: + dependencies: + acorn: 7.4.1 + acorn-jsx: 5.3.2(acorn@7.4.1) + eslint-visitor-keys: 1.3.0 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + eventemitter2@6.4.7: {} + + execa@4.1.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + executable@4.1.1: + dependencies: + pify: 2.3.0 + + extend@3.0.2: {} + + extract-zip@2.0.1(supports-color@8.1.1): + dependencies: + debug: 4.4.3(supports-color@8.1.1) + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + extsprintf@1.3.0: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.0: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.3: {} + + focus-trap@7.6.5: + dependencies: + tabbable: 6.2.0 + + forever-agent@0.6.1: {} + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + functional-red-black-tree@1.0.1: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@5.2.0: + dependencies: + pump: 3.0.3 + + getos@3.2.1: + dependencies: + async: 3.2.6 + + getpass@0.1.7: + dependencies: + assert-plus: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-dirs@3.0.1: + dependencies: + ini: 2.0.0 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-signature@1.4.0: + dependencies: + assert-plus: 1.0.0 + jsprim: 2.0.2 + sshpk: 1.18.0 + + human-signals@1.1.1: {} + + ieee754@1.2.1: {} + + ignore@4.0.6: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@2.0.0: {} + + is-ci@3.0.1: + dependencies: + ci-info: 3.9.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-installed-globally@0.4.0: + dependencies: + global-dirs: 3.0.1 + is-path-inside: 3.0.3 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-stream@2.0.1: {} + + is-typedarray@1.0.0: {} + + is-unicode-supported@0.1.0: {} + + isexe@2.0.0: {} + + isstream@0.1.2: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + jsbn@0.1.1: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-schema@0.4.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json-stringify-safe@5.0.1: {} + + jsonc-parser@3.3.1: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsprim@2.0.2: + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + lazy-ass@1.6.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + listr2@3.14.0(enquirer@2.4.1): + dependencies: + cli-truncate: 2.1.0 + colorette: 2.0.20 + log-update: 4.0.0 + p-map: 4.0.0 + rfdc: 1.4.1 + rxjs: 7.8.2 + through: 2.3.8 + wrap-ansi: 7.0.0 + optionalDependencies: + enquirer: 2.4.1 + + lodash.merge@4.6.2: {} + + lodash.once@4.1.1: {} + + lodash.truncate@4.4.2: {} + + lodash@4.17.21: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + log-update@4.0.0: + dependencies: + ansi-escapes: 4.3.2 + cli-cursor: 3.1.0 + slice-ansi: 4.0.0 + wrap-ansi: 6.2.0 + + magic-string@0.30.19: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mark.js@8.11.1: {} + + math-intrinsics@1.1.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-fn@2.1.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimist@1.2.8: {} + + minisearch@6.3.0: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare-lite@1.4.0: {} + + natural-compare@1.4.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + object-inspect@1.13.4: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ospath@1.2.2: {} + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + pend@1.2.0: {} + + performance-now@2.1.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pify@2.3.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + preact@10.27.2: {} + + prelude-ls@1.2.1: {} + + pretty-bytes@5.6.0: {} + + prismjs@1.30.0: {} + + process@0.11.10: {} + + progress@2.0.3: {} + + proxy-from-env@1.0.0: {} + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + queue-microtask@1.2.3: {} + + regexpp@3.2.0: {} + + request-progress@3.0.0: + dependencies: + throttleit: 1.0.1 + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@2.77.3: + optionalDependencies: + fsevents: 2.3.3 + + rollup@3.29.5: + optionalDependencies: + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + search-insights@2.17.3: {} + + semver@7.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shiki@0.14.7: + dependencies: + ansi-sequence-parser: 1.1.3 + jsonc-parser: 3.3.1 + vscode-oniguruma: 1.7.0 + vscode-textmate: 8.0.0 + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + simple-git-hooks@2.13.1: {} + + slash@3.0.0: {} + + slice-ansi@3.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + source-map-js@1.2.1: {} + + sprintf-js@1.0.3: {} + + sshpk@1.18.0: + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-final-newline@2.0.0: {} + + strip-json-comments@3.1.1: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tabbable@6.2.0: {} + + table@6.9.0: + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + text-table@0.2.0: {} + + throttleit@1.0.1: {} + + through@2.3.8: {} + + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tmp@0.2.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tslib@1.14.1: {} + + tslib@2.8.1: {} + + tsutils@3.21.0(typescript@4.9.5): + dependencies: + tslib: 1.14.1 + typescript: 4.9.5 + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + tweetnacl@0.14.5: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + typescript@4.9.5: {} + + undici-types@5.26.5: {} + + universalify@2.0.1: {} + + untildify@4.0.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + uuid@8.3.2: {} + + v8-compile-cache@2.4.0: {} + + verror@1.10.0: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + + vite-plugin-css-injected-by-js@2.4.0(vite@2.9.18): + dependencies: + vite: 2.9.18 + + vite@2.9.18: + dependencies: + esbuild: 0.14.54 + postcss: 8.5.6 + resolve: 1.22.10 + rollup: 2.77.3 + optionalDependencies: + fsevents: 2.3.3 + + vite@4.5.14(@types/node@16.18.96): + dependencies: + esbuild: 0.18.20 + postcss: 8.5.6 + rollup: 3.29.5 + optionalDependencies: + '@types/node': 16.18.96 + fsevents: 2.3.3 + + vitepress@1.0.0-beta.6(@algolia/client-search@5.37.0)(@types/node@16.18.96)(search-insights@2.17.3)(typescript@4.9.5): + dependencies: + '@docsearch/css': 3.9.0 + '@docsearch/js': 3.9.0(@algolia/client-search@5.37.0)(search-insights@2.17.3) + '@vitejs/plugin-vue': 4.6.2(vite@4.5.14(@types/node@16.18.96))(vue@3.5.21(typescript@4.9.5)) + '@vue/devtools-api': 6.6.4 + '@vueuse/core': 10.11.1(vue@3.5.21(typescript@4.9.5)) + '@vueuse/integrations': 10.11.1(focus-trap@7.6.5)(vue@3.5.21(typescript@4.9.5)) + body-scroll-lock: 4.0.0-beta.0 + focus-trap: 7.6.5 + mark.js: 8.11.1 + minisearch: 6.3.0 + shiki: 0.14.7 + vite: 4.5.14(@types/node@16.18.96) + vue: 3.5.21(typescript@4.9.5) + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/node' + - '@types/react' + - '@vue/composition-api' + - async-validator + - axios + - change-case + - drauu + - fuse.js + - idb-keyval + - jwt-decode + - less + - lightningcss + - nprogress + - qrcode + - react + - react-dom + - sass + - search-insights + - sortablejs + - stylus + - sugarss + - terser + - typescript + - universal-cookie + + vscode-oniguruma@1.7.0: {} + + vscode-textmate@8.0.0: {} + + vue-demi@0.14.10(vue@3.5.21(typescript@4.9.5)): + dependencies: + vue: 3.5.21(typescript@4.9.5) + + vue@3.5.21(typescript@4.9.5): + dependencies: + '@vue/compiler-dom': 3.5.21 + '@vue/compiler-sfc': 3.5.21 + '@vue/runtime-dom': 3.5.21 + '@vue/server-renderer': 3.5.21(vue@3.5.21(typescript@4.9.5)) + '@vue/shared': 3.5.21 + optionalDependencies: + typescript: 4.9.5 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..328c3a3 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +onlyBuiltDependencies: + - cypress + - esbuild + - simple-git-hooks + - vue-demi diff --git a/scripts/release.js b/scripts/release.js new file mode 100644 index 0000000..b6c84dc --- /dev/null +++ b/scripts/release.js @@ -0,0 +1,29 @@ +import { execSync } from 'child_process' +import fs from 'fs' +import path from 'path' + +const pkgPath = path.resolve('package.json') + +// 校验包合法性 +fs.accessSync(path.resolve('dist'), fs.constants.F_OK) +fs.accessSync(path.resolve('dist/canvas-editor.es.js'), fs.constants.F_OK) +fs.accessSync(path.resolve('dist/canvas-editor.umd.js'), fs.constants.F_OK) + +// 缓存项目package.json +const sourcePkg = fs.readFileSync(pkgPath, 'utf-8') + +// 删除无用属性 +const targetPkg = JSON.parse(sourcePkg) +Reflect.deleteProperty(targetPkg, 'dependencies') +Reflect.deleteProperty(targetPkg.scripts, 'postinstall') +fs.writeFileSync(pkgPath, JSON.stringify(targetPkg, null, 2)) + +// 发布包 +try { + execSync('npm publish') +} catch (error) { + throw new Error(error) +} finally { + // 还原 + fs.writeFileSync(pkgPath, sourcePkg) +} diff --git a/scripts/verifyCommit.js b/scripts/verifyCommit.js new file mode 100644 index 0000000..215825b --- /dev/null +++ b/scripts/verifyCommit.js @@ -0,0 +1,19 @@ +// @ts-check +import { readFileSync } from 'fs' +import path from 'path' + +const msgPath = path.resolve('.git/COMMIT_EDITMSG') +const msg = readFileSync(msgPath, 'utf-8').trim() + +const commitRE = + /^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release|improve)(\(.+\))?: .{1,50}/ + +if (!commitRE.test(msg)) { + console.error( + `invalid commit message format.\n\n` + + ` Proper commit message format is required for automated changelog generation. Examples:\n\n` + + ` feat: add page header\n` + + ` fix: IME position error #155\n` + ) + process.exit(1) +} \ No newline at end of file diff --git a/src/assets/images/alignment.svg b/src/assets/images/alignment.svg new file mode 100644 index 0000000..08b66e3 --- /dev/null +++ b/src/assets/images/alignment.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/arrow-left.svg b/src/assets/images/arrow-left.svg new file mode 100644 index 0000000..b55538b --- /dev/null +++ b/src/assets/images/arrow-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/arrow-right.svg b/src/assets/images/arrow-right.svg new file mode 100644 index 0000000..1aadb00 --- /dev/null +++ b/src/assets/images/arrow-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/block.svg b/src/assets/images/block.svg new file mode 100644 index 0000000..38b1704 --- /dev/null +++ b/src/assets/images/block.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/bold.svg b/src/assets/images/bold.svg new file mode 100644 index 0000000..80728d0 --- /dev/null +++ b/src/assets/images/bold.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/catalog.svg b/src/assets/images/catalog.svg new file mode 100644 index 0000000..90d45c2 --- /dev/null +++ b/src/assets/images/catalog.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/center.svg b/src/assets/images/center.svg new file mode 100644 index 0000000..28dc13c --- /dev/null +++ b/src/assets/images/center.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/checkbox.svg b/src/assets/images/checkbox.svg new file mode 100644 index 0000000..d522091 --- /dev/null +++ b/src/assets/images/checkbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/close.svg b/src/assets/images/close.svg new file mode 100644 index 0000000..e5b1c23 --- /dev/null +++ b/src/assets/images/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/codeblock.svg b/src/assets/images/codeblock.svg new file mode 100644 index 0000000..6c3163c --- /dev/null +++ b/src/assets/images/codeblock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/color.svg b/src/assets/images/color.svg new file mode 100644 index 0000000..2b84e88 --- /dev/null +++ b/src/assets/images/color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/control.svg b/src/assets/images/control.svg new file mode 100644 index 0000000..3fde9fb --- /dev/null +++ b/src/assets/images/control.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/date.svg b/src/assets/images/date.svg new file mode 100644 index 0000000..065db22 --- /dev/null +++ b/src/assets/images/date.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/exit-fullscreen.svg b/src/assets/images/exit-fullscreen.svg new file mode 100644 index 0000000..7999e25 --- /dev/null +++ b/src/assets/images/exit-fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/format.svg b/src/assets/images/format.svg new file mode 100644 index 0000000..aae6e6b --- /dev/null +++ b/src/assets/images/format.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/highlight.svg b/src/assets/images/highlight.svg new file mode 100644 index 0000000..c4b2e8b --- /dev/null +++ b/src/assets/images/highlight.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/hyperlink.svg b/src/assets/images/hyperlink.svg new file mode 100644 index 0000000..45090f6 --- /dev/null +++ b/src/assets/images/hyperlink.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/image.svg b/src/assets/images/image.svg new file mode 100644 index 0000000..7b43678 --- /dev/null +++ b/src/assets/images/image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/italic.svg b/src/assets/images/italic.svg new file mode 100644 index 0000000..73b2af5 --- /dev/null +++ b/src/assets/images/italic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/justify.svg b/src/assets/images/justify.svg new file mode 100644 index 0000000..e522de5 --- /dev/null +++ b/src/assets/images/justify.svg @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/images/latex.svg b/src/assets/images/latex.svg new file mode 100644 index 0000000..ca5426b --- /dev/null +++ b/src/assets/images/latex.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/left.svg b/src/assets/images/left.svg new file mode 100644 index 0000000..b41d2b2 --- /dev/null +++ b/src/assets/images/left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/line-dash-dot-dot.svg b/src/assets/images/line-dash-dot-dot.svg new file mode 100644 index 0000000..30ab5ac --- /dev/null +++ b/src/assets/images/line-dash-dot-dot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/line-dash-dot.svg b/src/assets/images/line-dash-dot.svg new file mode 100644 index 0000000..1958671 --- /dev/null +++ b/src/assets/images/line-dash-dot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/line-dash-large-gap.svg b/src/assets/images/line-dash-large-gap.svg new file mode 100644 index 0000000..2e38e60 --- /dev/null +++ b/src/assets/images/line-dash-large-gap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/line-dash-small-gap.svg b/src/assets/images/line-dash-small-gap.svg new file mode 100644 index 0000000..88d6082 --- /dev/null +++ b/src/assets/images/line-dash-small-gap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/line-dot.svg b/src/assets/images/line-dot.svg new file mode 100644 index 0000000..c08b564 --- /dev/null +++ b/src/assets/images/line-dot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/line-double.svg b/src/assets/images/line-double.svg new file mode 100644 index 0000000..2efe69f --- /dev/null +++ b/src/assets/images/line-double.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/line-single.svg b/src/assets/images/line-single.svg new file mode 100644 index 0000000..453d4fa --- /dev/null +++ b/src/assets/images/line-single.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/line-wavy.svg b/src/assets/images/line-wavy.svg new file mode 100644 index 0000000..bc0f47c --- /dev/null +++ b/src/assets/images/line-wavy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/list.svg b/src/assets/images/list.svg new file mode 100644 index 0000000..564897c --- /dev/null +++ b/src/assets/images/list.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/option.svg b/src/assets/images/option.svg new file mode 100644 index 0000000..53b6cae --- /dev/null +++ b/src/assets/images/option.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/page-break.svg b/src/assets/images/page-break.svg new file mode 100644 index 0000000..c40ec93 --- /dev/null +++ b/src/assets/images/page-break.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/page-mode.svg b/src/assets/images/page-mode.svg new file mode 100644 index 0000000..516f3a8 --- /dev/null +++ b/src/assets/images/page-mode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/page-scale-add.svg b/src/assets/images/page-scale-add.svg new file mode 100644 index 0000000..bcfa9a3 --- /dev/null +++ b/src/assets/images/page-scale-add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/page-scale-minus.svg b/src/assets/images/page-scale-minus.svg new file mode 100644 index 0000000..f85bf77 --- /dev/null +++ b/src/assets/images/page-scale-minus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/painter.svg b/src/assets/images/painter.svg new file mode 100644 index 0000000..a865d1d --- /dev/null +++ b/src/assets/images/painter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/paper-direction.svg b/src/assets/images/paper-direction.svg new file mode 100644 index 0000000..ee90234 --- /dev/null +++ b/src/assets/images/paper-direction.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/paper-margin.svg b/src/assets/images/paper-margin.svg new file mode 100644 index 0000000..6188f36 --- /dev/null +++ b/src/assets/images/paper-margin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/paper-size.svg b/src/assets/images/paper-size.svg new file mode 100644 index 0000000..205a6aa --- /dev/null +++ b/src/assets/images/paper-size.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/print.svg b/src/assets/images/print.svg new file mode 100644 index 0000000..5ee44a0 --- /dev/null +++ b/src/assets/images/print.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/radio.svg b/src/assets/images/radio.svg new file mode 100644 index 0000000..ecc25ed --- /dev/null +++ b/src/assets/images/radio.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/images/redo.svg b/src/assets/images/redo.svg new file mode 100644 index 0000000..fc88331 --- /dev/null +++ b/src/assets/images/redo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/request-fullscreen.svg b/src/assets/images/request-fullscreen.svg new file mode 100644 index 0000000..cf47c4a --- /dev/null +++ b/src/assets/images/request-fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/right.svg b/src/assets/images/right.svg new file mode 100644 index 0000000..eca4643 --- /dev/null +++ b/src/assets/images/right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/row-margin.svg b/src/assets/images/row-margin.svg new file mode 100644 index 0000000..97f2baa --- /dev/null +++ b/src/assets/images/row-margin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/search.svg b/src/assets/images/search.svg new file mode 100644 index 0000000..9d515dc --- /dev/null +++ b/src/assets/images/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/separator.svg b/src/assets/images/separator.svg new file mode 100644 index 0000000..58225e9 --- /dev/null +++ b/src/assets/images/separator.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/signature-undo.svg b/src/assets/images/signature-undo.svg new file mode 100644 index 0000000..518a37f --- /dev/null +++ b/src/assets/images/signature-undo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/signature.svg b/src/assets/images/signature.svg new file mode 100644 index 0000000..57a007f --- /dev/null +++ b/src/assets/images/signature.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/size-add.svg b/src/assets/images/size-add.svg new file mode 100644 index 0000000..aa1073c --- /dev/null +++ b/src/assets/images/size-add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/size-minus.svg b/src/assets/images/size-minus.svg new file mode 100644 index 0000000..7bfa958 --- /dev/null +++ b/src/assets/images/size-minus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/strikeout.svg b/src/assets/images/strikeout.svg new file mode 100644 index 0000000..c2c83ca --- /dev/null +++ b/src/assets/images/strikeout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/subscript.svg b/src/assets/images/subscript.svg new file mode 100644 index 0000000..9ec06b7 --- /dev/null +++ b/src/assets/images/subscript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/superscript.svg b/src/assets/images/superscript.svg new file mode 100644 index 0000000..053bd3e --- /dev/null +++ b/src/assets/images/superscript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/table.svg b/src/assets/images/table.svg new file mode 100644 index 0000000..0a349fe --- /dev/null +++ b/src/assets/images/table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/title.svg b/src/assets/images/title.svg new file mode 100644 index 0000000..c131320 --- /dev/null +++ b/src/assets/images/title.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/trash.svg b/src/assets/images/trash.svg new file mode 100644 index 0000000..c9852d1 --- /dev/null +++ b/src/assets/images/trash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/underline.svg b/src/assets/images/underline.svg new file mode 100644 index 0000000..dcd81b0 --- /dev/null +++ b/src/assets/images/underline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/undo.svg b/src/assets/images/undo.svg new file mode 100644 index 0000000..820f852 --- /dev/null +++ b/src/assets/images/undo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/watermark.svg b/src/assets/images/watermark.svg new file mode 100644 index 0000000..68de565 --- /dev/null +++ b/src/assets/images/watermark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/word-tool.svg b/src/assets/images/word-tool.svg new file mode 100644 index 0000000..21fbd33 --- /dev/null +++ b/src/assets/images/word-tool.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/snapshots/main_v0.2.1.png b/src/assets/snapshots/main_v0.2.1.png new file mode 100644 index 0000000..5006c26 Binary files /dev/null and b/src/assets/snapshots/main_v0.2.1.png differ diff --git a/src/assets/snapshots/main_v0.2.2.png b/src/assets/snapshots/main_v0.2.2.png new file mode 100644 index 0000000..f85c01f Binary files /dev/null and b/src/assets/snapshots/main_v0.2.2.png differ diff --git a/src/assets/snapshots/main_v0.3.0.png b/src/assets/snapshots/main_v0.3.0.png new file mode 100644 index 0000000..28fce0c Binary files /dev/null and b/src/assets/snapshots/main_v0.3.0.png differ diff --git a/src/assets/snapshots/main_v0.3.1.png b/src/assets/snapshots/main_v0.3.1.png new file mode 100644 index 0000000..1c546b3 Binary files /dev/null and b/src/assets/snapshots/main_v0.3.1.png differ diff --git a/src/assets/snapshots/main_v0.5.0.png b/src/assets/snapshots/main_v0.5.0.png new file mode 100644 index 0000000..bdeb796 Binary files /dev/null and b/src/assets/snapshots/main_v0.5.0.png differ diff --git a/src/assets/snapshots/main_v0.5.1.png b/src/assets/snapshots/main_v0.5.1.png new file mode 100644 index 0000000..3f6ac2c Binary files /dev/null and b/src/assets/snapshots/main_v0.5.1.png differ diff --git a/src/assets/snapshots/main_v0.6.0.png b/src/assets/snapshots/main_v0.6.0.png new file mode 100644 index 0000000..434a4ec Binary files /dev/null and b/src/assets/snapshots/main_v0.6.0.png differ diff --git a/src/assets/snapshots/main_v0.6.1.png b/src/assets/snapshots/main_v0.6.1.png new file mode 100644 index 0000000..cf3676e Binary files /dev/null and b/src/assets/snapshots/main_v0.6.1.png differ diff --git a/src/assets/snapshots/main_v0.7.0.png b/src/assets/snapshots/main_v0.7.0.png new file mode 100644 index 0000000..801edd5 Binary files /dev/null and b/src/assets/snapshots/main_v0.7.0.png differ diff --git a/src/assets/snapshots/main_v0.7.1.png b/src/assets/snapshots/main_v0.7.1.png new file mode 100644 index 0000000..3ff9d96 Binary files /dev/null and b/src/assets/snapshots/main_v0.7.1.png differ diff --git a/src/assets/snapshots/main_v0.7.2.png b/src/assets/snapshots/main_v0.7.2.png new file mode 100644 index 0000000..f68a1fa Binary files /dev/null and b/src/assets/snapshots/main_v0.7.2.png differ diff --git a/src/assets/snapshots/main_v0.7.3.png b/src/assets/snapshots/main_v0.7.3.png new file mode 100644 index 0000000..6e366ba Binary files /dev/null and b/src/assets/snapshots/main_v0.7.3.png differ diff --git a/src/assets/snapshots/main_v0.7.4.png b/src/assets/snapshots/main_v0.7.4.png new file mode 100644 index 0000000..69ef956 Binary files /dev/null and b/src/assets/snapshots/main_v0.7.4.png differ diff --git a/src/assets/snapshots/main_v0.7.6.png b/src/assets/snapshots/main_v0.7.6.png new file mode 100644 index 0000000..8427c5f Binary files /dev/null and b/src/assets/snapshots/main_v0.7.6.png differ diff --git a/src/assets/snapshots/main_v0.7.7.png b/src/assets/snapshots/main_v0.7.7.png new file mode 100644 index 0000000..10a99e9 Binary files /dev/null and b/src/assets/snapshots/main_v0.7.7.png differ diff --git a/src/assets/snapshots/main_v0.8.0.png b/src/assets/snapshots/main_v0.8.0.png new file mode 100644 index 0000000..9f053a9 Binary files /dev/null and b/src/assets/snapshots/main_v0.8.0.png differ diff --git a/src/assets/snapshots/main_v0.8.5.png b/src/assets/snapshots/main_v0.8.5.png new file mode 100644 index 0000000..9a81721 Binary files /dev/null and b/src/assets/snapshots/main_v0.8.5.png differ diff --git a/src/assets/snapshots/main_v0.8.6.png b/src/assets/snapshots/main_v0.8.6.png new file mode 100644 index 0000000..a938076 Binary files /dev/null and b/src/assets/snapshots/main_v0.8.6.png differ diff --git a/src/assets/snapshots/main_v0.8.7.png b/src/assets/snapshots/main_v0.8.7.png new file mode 100644 index 0000000..1ebd64f Binary files /dev/null and b/src/assets/snapshots/main_v0.8.7.png differ diff --git a/src/assets/snapshots/main_v0.8.8.png b/src/assets/snapshots/main_v0.8.8.png new file mode 100644 index 0000000..6a48f77 Binary files /dev/null and b/src/assets/snapshots/main_v0.8.8.png differ diff --git a/src/assets/snapshots/main_v0.9.0.png b/src/assets/snapshots/main_v0.9.0.png new file mode 100644 index 0000000..bd27915 Binary files /dev/null and b/src/assets/snapshots/main_v0.9.0.png differ diff --git a/src/assets/snapshots/main_v0.9.1.png b/src/assets/snapshots/main_v0.9.1.png new file mode 100644 index 0000000..d28a67a Binary files /dev/null and b/src/assets/snapshots/main_v0.9.1.png differ diff --git a/src/assets/snapshots/main_v0.9.2.png b/src/assets/snapshots/main_v0.9.2.png new file mode 100644 index 0000000..b6b44e5 Binary files /dev/null and b/src/assets/snapshots/main_v0.9.2.png differ diff --git a/src/assets/snapshots/main_v0.9.23.png b/src/assets/snapshots/main_v0.9.23.png new file mode 100644 index 0000000..5500acf Binary files /dev/null and b/src/assets/snapshots/main_v0.9.23.png differ diff --git a/src/assets/snapshots/main_v0.9.28.png b/src/assets/snapshots/main_v0.9.28.png new file mode 100644 index 0000000..e116815 Binary files /dev/null and b/src/assets/snapshots/main_v0.9.28.png differ diff --git a/src/assets/snapshots/main_v0.9.29.png b/src/assets/snapshots/main_v0.9.29.png new file mode 100644 index 0000000..27f0362 Binary files /dev/null and b/src/assets/snapshots/main_v0.9.29.png differ diff --git a/src/assets/snapshots/main_v0.9.3.png b/src/assets/snapshots/main_v0.9.3.png new file mode 100644 index 0000000..ad3ee12 Binary files /dev/null and b/src/assets/snapshots/main_v0.9.3.png differ diff --git a/src/assets/snapshots/main_v0.9.30.png b/src/assets/snapshots/main_v0.9.30.png new file mode 100644 index 0000000..9ac2f5a Binary files /dev/null and b/src/assets/snapshots/main_v0.9.30.png differ diff --git a/src/assets/snapshots/main_v0.9.32.png b/src/assets/snapshots/main_v0.9.32.png new file mode 100644 index 0000000..502e7e8 Binary files /dev/null and b/src/assets/snapshots/main_v0.9.32.png differ diff --git a/src/assets/snapshots/main_v0.9.35.png b/src/assets/snapshots/main_v0.9.35.png new file mode 100644 index 0000000..de7443c Binary files /dev/null and b/src/assets/snapshots/main_v0.9.35.png differ diff --git a/src/assets/snapshots/main_v0.9.4.png b/src/assets/snapshots/main_v0.9.4.png new file mode 100644 index 0000000..ac5009b Binary files /dev/null and b/src/assets/snapshots/main_v0.9.4.png differ diff --git a/src/assets/snapshots/main_v0.9.5.png b/src/assets/snapshots/main_v0.9.5.png new file mode 100644 index 0000000..b5de744 Binary files /dev/null and b/src/assets/snapshots/main_v0.9.5.png differ diff --git a/src/assets/snapshots/main_v0.9.6.png b/src/assets/snapshots/main_v0.9.6.png new file mode 100644 index 0000000..c9d353a Binary files /dev/null and b/src/assets/snapshots/main_v0.9.6.png differ diff --git a/src/assets/snapshots/main_v0.9.8.png b/src/assets/snapshots/main_v0.9.8.png new file mode 100644 index 0000000..235b4e3 Binary files /dev/null and b/src/assets/snapshots/main_v0.9.8.png differ diff --git a/src/components/dialog/Dialog.ts b/src/components/dialog/Dialog.ts new file mode 100644 index 0000000..d3f0112 --- /dev/null +++ b/src/components/dialog/Dialog.ts @@ -0,0 +1,171 @@ +import { EditorComponent, EDITOR_COMPONENT } from '../../editor' +import './dialog.css' + +export interface IDialogData { + type: string + label?: string + name: string + value?: string + options?: { label: string; value: string }[] + placeholder?: string + width?: number + height?: number + required?: boolean +} + +export interface IDialogConfirm { + name: string + value: string +} + +export interface IDialogOptions { + onClose?: () => void + onCancel?: () => void + onConfirm?: (payload: IDialogConfirm[]) => void + title: string + data: IDialogData[] +} + +export class Dialog { + private options: IDialogOptions + private mask: HTMLDivElement | null + private container: HTMLDivElement | null + private inputList: ( + | HTMLInputElement + | HTMLTextAreaElement + | HTMLSelectElement + )[] + + constructor(options: IDialogOptions) { + this.options = options + this.mask = null + this.container = null + this.inputList = [] + this._render() + } + + private _render() { + const { title, data, onClose, onCancel, onConfirm } = this.options + // 渲染遮罩层 + const mask = document.createElement('div') + mask.classList.add('dialog-mask') + mask.setAttribute(EDITOR_COMPONENT, EditorComponent.COMPONENT) + document.body.append(mask) + // 渲染容器 + const container = document.createElement('div') + container.classList.add('dialog-container') + container.setAttribute(EDITOR_COMPONENT, EditorComponent.COMPONENT) + // 弹窗 + const dialogContainer = document.createElement('div') + dialogContainer.classList.add('dialog') + container.append(dialogContainer) + // 标题容器 + const titleContainer = document.createElement('div') + titleContainer.classList.add('dialog-title') + // 标题&关闭按钮 + const titleSpan = document.createElement('span') + titleSpan.append(document.createTextNode(title)) + const titleClose = document.createElement('i') + titleClose.onclick = () => { + if (onClose) { + onClose() + } + this._dispose() + } + titleContainer.append(titleSpan) + titleContainer.append(titleClose) + dialogContainer.append(titleContainer) + // 选项容器 + const optionContainer = document.createElement('div') + optionContainer.classList.add('dialog-option') + // 选项 + for (let i = 0; i < data.length; i++) { + const option = data[i] + const optionItemContainer = document.createElement('div') + optionItemContainer.classList.add('dialog-option__item') + // 选项名称 + if (option.label) { + const optionName = document.createElement('span') + optionName.append(document.createTextNode(option.label)) + optionItemContainer.append(optionName) + if (option.required) { + optionName.classList.add('dialog-option__item--require') + } + } + // 选项输入框 + let optionInput: + | HTMLInputElement + | HTMLTextAreaElement + | HTMLSelectElement + if (option.type === 'select') { + optionInput = document.createElement('select') + option.options?.forEach(item => { + const optionItem = document.createElement('option') + optionItem.value = item.value + optionItem.label = item.label + optionInput.append(optionItem) + }) + } else if (option.type === 'textarea') { + optionInput = document.createElement('textarea') + } else { + optionInput = document.createElement('input') + optionInput.type = option.type + } + if (option.width) { + optionInput.style.width = `${option.width}px` + } + if (option.height) { + optionInput.style.height = `${option.height}px` + } + optionInput.name = option.name + optionInput.value = option.value || '' + if (!(optionInput instanceof HTMLSelectElement)) { + optionInput.placeholder = option.placeholder || '' + } + optionItemContainer.append(optionInput) + optionContainer.append(optionItemContainer) + this.inputList.push(optionInput) + } + dialogContainer.append(optionContainer) + // 按钮容器 + const menuContainer = document.createElement('div') + menuContainer.classList.add('dialog-menu') + // 取消按钮 + const cancelBtn = document.createElement('button') + cancelBtn.classList.add('dialog-menu__cancel') + cancelBtn.append(document.createTextNode('取消')) + cancelBtn.type = 'button' + cancelBtn.onclick = () => { + if (onCancel) { + onCancel() + } + this._dispose() + } + menuContainer.append(cancelBtn) + // 确认按钮 + const confirmBtn = document.createElement('button') + confirmBtn.append(document.createTextNode('确定')) + confirmBtn.type = 'submit' + confirmBtn.onclick = () => { + if (onConfirm) { + const payload = this.inputList.map(input => ({ + name: input.name, + value: input.value + })) + onConfirm(payload) + } + this._dispose() + } + menuContainer.append(confirmBtn) + dialogContainer.append(menuContainer) + // 渲染 + document.body.append(container) + this.container = container + this.mask = mask + } + + private _dispose() { + this.mask?.remove() + this.container?.remove() + } +} diff --git a/src/components/dialog/dialog.css b/src/components/dialog/dialog.css new file mode 100644 index 0000000..8a58245 --- /dev/null +++ b/src/components/dialog/dialog.css @@ -0,0 +1,131 @@ +.dialog-mask { + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + opacity: .5; + background: #000000; + z-index: 99; +} + +.dialog-container { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + overflow: auto; + z-index: 999; + margin: 0; + display: flex; + justify-content: center; + align-items: center; +} + +.dialog { + position: absolute; + padding: 0 30px 30px; + background: #ffffff; + box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%); + border: 1px solid #e2e6ed; + border-radius: 2px; +} + +.dialog-title { + position: relative; + border-bottom: 1px solid #e2e6ed; + margin-bottom: 30px; + height: 60px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.dialog-title i { + width: 16px; + height: 16px; + cursor: pointer; + display: inline-block; + background: url(../../assets/images/close.svg); +} + +.dialog-option__item { + margin-bottom: 18px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.dialog-option__item span { + margin-right: 12px; + font-size: 14px; + color: #3d4757; + position: relative; +} + +.dialog-option__item input, +.dialog-option__item textarea, +.dialog-option__item select { + width: 276px; + height: 30px; + border-radius: 2px; + border: 1px solid #d3d3d3; + min-height: 30px; + padding: 5px; + box-sizing: border-box; + outline: none; + appearance: none; + user-select: none; + font-family: inherit; +} + +.dialog-option__item input:focus, +.dialog-option__item textarea:focus { + border-color: #4991f2; +} + +.dialog-option__item--require::before { + content: "*"; + color: #f56c6c; + margin-right: 4px; + position: absolute; + left: -8px; +} + +.dialog-menu { + display: flex; + align-items: center; + justify-content: flex-end; +} + +.dialog-menu button { + position: relative; + display: inline-block; + border: 1px solid #e2e6ed; + border-radius: 2px; + background: #ffffff; + line-height: 22px; + padding: 0 16px; + white-space: nowrap; + cursor: pointer; +} + +.dialog-menu button:hover { + background: rgba(25, 55, 88, .04); +} + +.dialog-menu__cancel { + margin-right: 16px; +} + +.dialog-menu button[type='submit'] { + color: #ffffff; + background: #4991f2; + border-color: #4991f2; +} + +.dialog-menu button[type='submit']:hover { + background: #5b9cf3; + border-color: #5b9cf3; +} \ No newline at end of file diff --git a/src/components/signature/Signature.ts b/src/components/signature/Signature.ts new file mode 100644 index 0000000..f50dc1b --- /dev/null +++ b/src/components/signature/Signature.ts @@ -0,0 +1,340 @@ +import { EditorComponent, EDITOR_COMPONENT } from '../../editor' +import './signature.css' + +export interface ISignatureResult { + value: string + width: number + height: number +} + +export interface ISignatureOptions { + width?: number + height?: number + onClose?: () => void + onCancel?: () => void + onConfirm?: (payload: ISignatureResult | null) => void +} + +export class Signature { + private readonly MAX_RECORD_COUNT = 1000 + private readonly DEFAULT_WIDTH = 390 + private readonly DEFAULT_HEIGHT = 180 + private undoStack: Array = [] + private x = 0 + private y = 0 + private isDrawing = false + private isDrawn = false + private linePoints: [number, number][] = [] + private canvasWidth: number + private canvasHeight: number + private options: ISignatureOptions + private mask: HTMLDivElement + private container: HTMLDivElement + private trashContainer: HTMLDivElement + private undoContainer: HTMLDivElement + private canvas: HTMLCanvasElement + private ctx: CanvasRenderingContext2D + private preTimeStamp: number + private dpr: number + + constructor(options: ISignatureOptions) { + this.options = options + this.preTimeStamp = 0 + this.dpr = window.devicePixelRatio + this.canvasWidth = (options.width || this.DEFAULT_WIDTH) * this.dpr + this.canvasHeight = (options.height || this.DEFAULT_HEIGHT) * this.dpr + const { mask, container, trashContainer, undoContainer, canvas } = + this._render() + this.mask = mask + this.container = container + this.trashContainer = trashContainer + this.undoContainer = undoContainer + this.canvas = canvas + this.ctx = canvas.getContext('2d') + this.ctx.scale(this.dpr, this.dpr) + this.ctx.lineCap = 'round' + this._bindEvent() + this._clearUndoFn() + // this is necessary so that the screen does not move when moving - it is removed when closing the modal + document.documentElement.classList.add('overflow-hidden') + document.body.classList.add('overflow-hidden') + this.container.classList.add('overflow-hidden') + } + + private _render() { + const { onClose, onCancel, onConfirm } = this.options + // 渲染遮罩层 + const mask = document.createElement('div') + mask.classList.add('signature-mask') + mask.setAttribute(EDITOR_COMPONENT, EditorComponent.COMPONENT) + document.body.append(mask) + // 渲染容器 + const container = document.createElement('div') + container.classList.add('signature-container') + container.setAttribute(EDITOR_COMPONENT, EditorComponent.COMPONENT) + // 弹窗 + const signatureContainer = document.createElement('div') + signatureContainer.classList.add('signature') + container.append(signatureContainer) + // 标题容器 + const titleContainer = document.createElement('div') + titleContainer.classList.add('signature-title') + // 标题&关闭按钮 + const titleSpan = document.createElement('span') + titleSpan.append(document.createTextNode('插入签名')) + const titleClose = document.createElement('i') + titleClose.onclick = () => { + if (onClose) { + onClose() + } + this._dispose() + } + titleContainer.append(titleSpan) + titleContainer.append(titleClose) + signatureContainer.append(titleContainer) + // 操作区 + const operationContainer = document.createElement('div') + operationContainer.classList.add('signature-operation') + // 撤销 + const undoContainer = document.createElement('div') + undoContainer.classList.add('signature-operation__undo') + const undoIcon = document.createElement('i') + const undoLabel = document.createElement('span') + undoLabel.innerText = '撤销' + undoContainer.append(undoIcon) + undoContainer.append(undoLabel) + operationContainer.append(undoContainer) + // 清空画布 + const trashContainer = document.createElement('div') + trashContainer.classList.add('signature-operation__trash') + const trashIcon = document.createElement('i') + const trashLabel = document.createElement('span') + trashLabel.innerText = '清空' + trashContainer.append(trashIcon) + trashContainer.append(trashLabel) + operationContainer.append(trashContainer) + signatureContainer.append(operationContainer) + // 绘图区 + const canvasContainer = document.createElement('div') + canvasContainer.classList.add('signature-canvas') + const canvas = document.createElement('canvas') + canvas.width = this.canvasWidth + canvas.height = this.canvasHeight + canvas.style.width = `${this.canvasWidth / this.dpr}px` + canvas.style.height = `${this.canvasHeight / this.dpr}px` + canvasContainer.append(canvas) + signatureContainer.append(canvasContainer) + // 按钮容器 + const menuContainer = document.createElement('div') + menuContainer.classList.add('signature-menu') + // 取消按钮 + const cancelBtn = document.createElement('button') + cancelBtn.classList.add('signature-menu__cancel') + cancelBtn.append(document.createTextNode('取消')) + cancelBtn.type = 'button' + cancelBtn.onclick = () => { + if (onCancel) { + onCancel() + } + this._dispose() + } + menuContainer.append(cancelBtn) + // 确认按钮 + const confirmBtn = document.createElement('button') + confirmBtn.append(document.createTextNode('确定')) + confirmBtn.type = 'submit' + confirmBtn.onclick = () => { + if (onConfirm) { + onConfirm(this._toData()) + } + this._dispose() + } + menuContainer.append(confirmBtn) + signatureContainer.append(menuContainer) + // 渲染 + document.body.append(container) + this.container = container + this.mask = mask + return { + mask, + canvas, + container, + trashContainer, + undoContainer + } + } + + private _bindEvent() { + this.trashContainer.onclick = this._clearCanvas.bind(this) + this.undoContainer.onclick = this._undo.bind(this) + this.canvas.onmousedown = this._startDraw.bind(this) + this.canvas.onmousemove = this._draw.bind(this) + this.container.onmouseup = this._stopDraw.bind(this) + this.container.ontouchmove = this.registerTouchmove.bind(this) + this.container.ontouchstart = this.registerTouchstart.bind(this) + this.container.ontouchend = this.registerTouchend.bind(this) + } + + private _undo() { + if (this.undoStack.length > 1) { + this.undoStack.pop() + if (this.undoStack.length) { + this.undoStack[this.undoStack.length - 1]() + } + } + } + + private _saveUndoFn(fn: Function) { + this.undoStack.push(fn) + while (this.undoStack.length > this.MAX_RECORD_COUNT) { + this.undoStack.shift() + } + } + + private _clearUndoFn() { + const clearFn = () => { + this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight) + } + this.undoStack = [clearFn] + } + + private _clearCanvas() { + this._clearUndoFn() + this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight) + } + + private _startDraw(evt: MouseEvent) { + this.isDrawing = true + this.x = evt.offsetX + this.y = evt.offsetY + this.ctx.lineWidth = 1 + } + + private _draw(evt: MouseEvent) { + if (!this.isDrawing) return + // 计算鼠标移动速度 + const curTimestamp = performance.now() + const distance = Math.sqrt(evt.movementX ** 2 + evt.movementY ** 2) + const speed = distance / (curTimestamp - this.preTimeStamp) + // 目标线宽:最小速度1,最大速度5,系数3 + const SPEED_FACTOR = 3 + const targetLineWidth = Math.min(5, Math.max(1, 5 - speed * SPEED_FACTOR)) + // 平滑过渡算法(20%的变化比例)调整线条粗细:系数0.2 + const SMOOTH_FACTOR = 0.2 + this.ctx.lineWidth = + this.ctx.lineWidth * (1 - SMOOTH_FACTOR) + targetLineWidth * SMOOTH_FACTOR + // 绘制 + const { offsetX, offsetY } = evt + this.ctx.beginPath() + this.ctx.moveTo(this.x, this.y) + this.ctx.lineTo(offsetX, offsetY) + this.ctx.stroke() + this.x = offsetX + this.y = offsetY + this.linePoints.push([offsetX, offsetY]) + this.isDrawn = true + // 缓存之前时间戳 + this.preTimeStamp = curTimestamp + } + + private _stopDraw() { + this.isDrawing = false + if (this.isDrawn) { + const imageData = this.ctx.getImageData( + 0, + 0, + this.canvasWidth, + this.canvasHeight + ) + const self = this + this._saveUndoFn(function () { + self.ctx.clearRect(0, 0, self.canvasWidth, self.canvasHeight) + self.ctx.putImageData(imageData, 0, 0) + }) + this.isDrawn = false + } + } + + private _toData(): ISignatureResult | null { + if (!this.linePoints.length) return null + // 查找矩形四角坐标 + const startX = this.linePoints[0][0] + const startY = this.linePoints[0][1] + let minX = startX + let minY = startY + let maxX = startX + let maxY = startY + for (let p = 0; p < this.linePoints.length; p++) { + const point = this.linePoints[p] + if (minX > point[0]) { + minX = point[0] + } + if (maxX < point[0]) { + maxX = point[0] + } + if (minY > point[1]) { + minY = point[1] + } + if (maxY < point[1]) { + maxY = point[1] + } + } + // 增加边框宽度 + const lineWidth = this.ctx.lineWidth + minX = minX < lineWidth ? 0 : minX - lineWidth + minY = minY < lineWidth ? 0 : minY - lineWidth + maxX = maxX + lineWidth + maxY = maxY + lineWidth + const sw = maxX - minX + const sh = maxY - minY + // 裁剪图像 + const imageData = this.ctx.getImageData( + minX * this.dpr, + minY * this.dpr, + sw * this.dpr, + sh * this.dpr + ) + const canvas = document.createElement('canvas') + canvas.style.width = `${sw}px` + canvas.style.height = `${sh}px` + canvas.width = sw * this.dpr + canvas.height = sh * this.dpr + const ctx = canvas.getContext('2d')! + ctx.putImageData(imageData, 0, 0) + const value = canvas.toDataURL() + return { + value, + width: sw, + height: sh + } + } + + private registerTouchmove(evt: TouchEvent) { + this.registerTouchEvent(evt, 'mousemove') + } + + private registerTouchstart(evt: TouchEvent) { + this.registerTouchEvent(evt, 'mousedown') + } + + private registerTouchend() { + const me = new MouseEvent('mouseup', {}) + this.canvas.dispatchEvent(me) + } + + private registerTouchEvent(evt: TouchEvent, eventName: string) { + const touch = evt.touches[0] + const me = new MouseEvent(eventName, { + clientX: touch.clientX, + clientY: touch.clientY + }) + this.canvas.dispatchEvent(me) + } + + private _dispose() { + this.mask.remove() + this.container.remove() + document.documentElement.classList.remove('overflow-hidden') + document.body.classList.remove('overflow-hidden') + } +} diff --git a/src/components/signature/signature.css b/src/components/signature/signature.css new file mode 100644 index 0000000..e5e4d38 --- /dev/null +++ b/src/components/signature/signature.css @@ -0,0 +1,132 @@ +.signature-mask { + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + opacity: .5; + background: #000000; + z-index: 99; +} + +.signature-container { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + overflow: auto; + z-index: 999; + margin: 0; + display: flex; + justify-content: center; + align-items: center; +} + +.signature { + position: absolute; + padding: 0 30px 30px; + background: #ffffff; + box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%); + border: 1px solid #e2e6ed; + border-radius: 2px; +} + +.signature-title { + position: relative; + border-bottom: 1px solid #e2e6ed; + margin-bottom: 15px; + height: 60px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.signature-title i { + width: 16px; + height: 16px; + cursor: pointer; + display: inline-block; + background: url(../../assets/images/close.svg); +} + +.signature-operation>div { + cursor: pointer; + display: inline-flex; + align-items: center; + color: #3d4757; + user-select: none; +} + +.signature-operation>div:hover { + color: #6e7175; +} + +.signature-operation>div i { + width: 24px; + height: 24px; + display: inline-block; +} + +.signature-operation__undo { + background: url(../../assets/images/signature-undo.svg) no-repeat; +} + +.signature-operation__trash { + background: url(../../assets/images/trash.svg) no-repeat; +} + +.signature-operation>div span { + font-size: 12px; + margin: 0 5px; +} + +.signature-canvas { + margin: 15px 0; + user-select: none; +} + +.signature-canvas canvas { + background: #f3f5f7; +} + +.signature-menu { + display: flex; + align-items: center; + justify-content: flex-end; +} + +.signature-menu button { + position: relative; + display: inline-block; + border: 1px solid #e2e6ed; + border-radius: 2px; + background: #ffffff; + line-height: 22px; + padding: 0 16px; + white-space: nowrap; + cursor: pointer; +} + +.signature-menu button:hover { + background: rgba(25, 55, 88, .04); +} + +.signature-menu__cancel { + margin-right: 16px; +} + +.signature-menu button[type='submit'] { + color: #ffffff; + background: #4991f2; + border-color: #4991f2; +} + +.signature-menu button[type='submit']:hover { + background: #5b9cf3; + border-color: #5b9cf3; +} + +.overflow-hidden { + overflow: hidden !important; +} \ No newline at end of file diff --git a/src/editor/assets/css/block/block.css b/src/editor/assets/css/block/block.css new file mode 100644 index 0000000..5adb171 --- /dev/null +++ b/src/editor/assets/css/block/block.css @@ -0,0 +1,21 @@ +.ce-block-item { + position: absolute; + z-index: 0; + background-color: #ffffff; + border: 1px solid rgb(235 236 240); +} + +.ce-block-item .ce-resizer-selection { + width: 100%; + height: 100%; +} + +.ce-block-item .ce-resizer-mask { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 1; + background-color: transparent; +} \ No newline at end of file diff --git a/src/editor/assets/css/contextmenu/contextmenu.css b/src/editor/assets/css/contextmenu/contextmenu.css new file mode 100644 index 0000000..feb1a42 --- /dev/null +++ b/src/editor/assets/css/contextmenu/contextmenu.css @@ -0,0 +1,196 @@ +.ce-contextmenu-container { + z-index: 9; + position: fixed; + display: none; + padding: 4px; + overflow-x: hidden; + overflow-y: auto; + background: #fff; + box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%); + border: 1px solid #e2e6ed; + border-radius: 2px; +} + +.ce-contextmenu-content { + display: flex; + flex-direction: column; +} + +.ce-contextmenu-content .ce-contextmenu-sub-item::after { + position: absolute; + content: ""; + width: 16px; + height: 16px; + right: 12px; + background: url(../../images/submenu-dropdown.svg); +} + +.ce-contextmenu-content .ce-contextmenu-item { + min-width: 140px; + padding: 0 32px 0 16px; + height: 30px; + display: flex; + align-items: center; + white-space: nowrap; + box-sizing: border-box; + cursor: pointer; +} + +.ce-contextmenu-content .ce-contextmenu-item.hover { + background: rgba(25, 55, 88, .04); +} + +.ce-contextmenu-content .ce-contextmenu-item span { + max-width: 300px; + font-size: 12px; + color: #3d4757; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.ce-contextmenu-content .ce-contextmenu-item span.ce-shortcut { + color: #767c85; + height: 30px; + flex: 1; + text-align: right; + line-height: 30px; + margin-left: 20px; +} + +.ce-contextmenu-content .ce-contextmenu-item i { + width: 16px; + height: 16px; + vertical-align: middle; + display: inline-block; + background-repeat: no-repeat; + background-size: 100% 100%; + flex-shrink: 0; + margin-right: 8px; +} + +.ce-contextmenu-divider { + background-color: #e2e6ed; + margin: 4px 16px; + height: 1px; +} + +.ce-contextmenu-print { + background-image: url(../../../assets/images/print.svg); +} + +.ce-contextmenu-image { + background-image: url(../../../assets/images/image.svg); +} + +.ce-contextmenu-image-change { + background-image: url(../../../assets/images/image-change.svg); +} + +.ce-contextmenu-insert-row-col { + background-image: url(../../../assets/images/insert-row-col.svg); +} + +.ce-contextmenu-insert-top-row { + background-image: url(../../../assets/images/insert-top-row.svg); +} + +.ce-contextmenu-insert-bottom-row { + background-image: url(../../../assets/images/insert-bottom-row.svg); +} + +.ce-contextmenu-insert-left-col { + background-image: url(../../../assets/images/insert-left-col.svg); +} + +.ce-contextmenu-insert-right-col { + background-image: url(../../../assets/images/insert-right-col.svg); +} + +.ce-contextmenu-delete-row-col { + background-image: url(../../../assets/images/delete-row-col.svg); +} + +.ce-contextmenu-delete-row { + background-image: url(../../../assets/images/delete-row.svg); +} + +.ce-contextmenu-delete-col { + background-image: url(../../../assets/images/delete-col.svg); +} + +.ce-contextmenu-delete-table { + background-image: url(../../../assets/images/delete-table.svg); +} + +.ce-contextmenu-merge-cell { + background-image: url(../../../assets/images/merge-cell.svg); +} + +.ce-contextmenu-merge-cancel-cell { + background-image: url(../../../assets/images/merge-cancel-cell.svg); +} + +.ce-contextmenu-vertical-align { + background-image: url(../../../assets/images/vertical-align.svg); +} + +.ce-contextmenu-vertical-align-top { + background-image: url(../../../assets/images/vertical-align-top.svg); +} + +.ce-contextmenu-vertical-align-middle { + background-image: url(../../../assets/images/vertical-align-middle.svg); +} + +.ce-contextmenu-vertical-align-bottom { + background-image: url(../../../assets/images/vertical-align-bottom.svg); +} + +.ce-contextmenu-border-all { + background-image: url(../../../assets/images/table-border-all.svg); +} + +.ce-contextmenu-border-empty { + background-image: url(../../../assets/images/table-border-empty.svg); +} + +.ce-contextmenu-border-dash { + background-image: url(../../../assets/images/table-border-dash.svg); +} + +.ce-contextmenu-border-external { + background-image: url(../../../assets/images/table-border-external.svg); +} + +.ce-contextmenu-border-internal { + background-image: url(../../../assets/images/table-border-internal.svg); +} + +.ce-contextmenu-border-td { + background-image: url(../../../assets/images/table-border-td.svg); +} + +.ce-contextmenu-border-td-top { + background-image: url(../../../assets/images/table-border-td-top.svg); +} + +.ce-contextmenu-border-td-left { + background-image: url(../../../assets/images/table-border-td-left.svg); +} + +.ce-contextmenu-border-td-bottom { + background-image: url(../../../assets/images/table-border-td-bottom.svg); +} + +.ce-contextmenu-border-td-right { + background-image: url(../../../assets/images/table-border-td-right.svg); +} + +.ce-contextmenu-border-td-forward { + background-image: url(../../../assets/images/table-border-td-forward.svg); +} + +.ce-contextmenu-border-td-back { + background-image: url(../../../assets/images/table-border-td-back.svg); +} \ No newline at end of file diff --git a/src/editor/assets/css/control/select.css b/src/editor/assets/css/control/select.css new file mode 100644 index 0000000..af47877 --- /dev/null +++ b/src/editor/assets/css/control/select.css @@ -0,0 +1,44 @@ +.ce-select-control-popup { + max-width: 160px; + min-width: 69px; + max-height: 225px; + position: absolute; + z-index: 1; + border: 1px solid #e4e7ed; + border-radius: 4px; + background-color: #fff; + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1); + box-sizing: border-box; + margin: 5px 0; + overflow-y: auto; +} + +.ce-select-control-popup ul { + list-style: none; + padding: 3px 0; + margin: 0; + box-sizing: border-box; +} + +.ce-select-control-popup ul li { + font-size: 13px; + padding: 0 20px; + position: relative; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: #666; + height: 36px; + line-height: 36px; + box-sizing: border-box; + cursor: pointer; +} + +.ce-select-control-popup ul li:hover { + background-color: #EEF2FD; +} + +.ce-select-control-popup ul li.active { + color: var(--COLOR-HOVER, #5175f4); + font-weight: 700; +} \ No newline at end of file diff --git a/src/editor/assets/css/date/datePicker.css b/src/editor/assets/css/date/datePicker.css new file mode 100644 index 0000000..0c22969 --- /dev/null +++ b/src/editor/assets/css/date/datePicker.css @@ -0,0 +1,233 @@ +.ce-date-container { + display: none; + width: 300px; + overflow: hidden; + left: 0; + right: 0; + position: absolute; + z-index: 1; + color: #606266; + background: #ffffff; + border-radius: 4px; + padding: 10px; + user-select: none; + border: 1px solid #e4e7ed; + box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%); +} + +.ce-date-container.active { + display: block; +} + +.ce-date-wrap { + display: none; +} + +.ce-date-wrap.active { + display: block; +} + +.ce-date-title { + display: flex; + justify-content: center; + align-items: center; + text-align: center; + color: #606266; + font-size: 16px; +} + +.ce-date-title>span { + display: inline-block; +} + +.ce-date-title>span:not(.ce-date-title__now) { + font-family: cursive; + cursor: pointer; +} + +.ce-date-title>span:not(.ce-date-title__now):hover { + color: #5175F4; +} + +.ce-date-title .ce-date-title__pre-year { + width: 15%; +} + +.ce-date-title .ce-date-title__pre-month { + width: 15%; +} + +.ce-date-title .ce-date-title__now { + width: 40%; +} + +.ce-date-title .ce-date-title__next-year { + width: 15%; +} + +.ce-date-title .ce-date-title__next-month { + width: 15%; +} + +.ce-date-week { + width: 100%; + display: flex; + justify-content: center; + margin-top: 15px; + padding-bottom: 5px; + border-bottom: 1px solid #e4e7ed; +} + +.ce-date-week>span { + list-style: none; + width: calc(100%/7); + text-align: center; + color: #606266; + font-size: 14px; +} + +.ce-date-day { + width: 100%; + display: flex; + flex-wrap: wrap; + align-items: center; + margin-top: 5px; +} + +.ce-date-day>div { + width: calc(100%/7); + height: 40px; + text-align: center; + color: #606266; + font-size: 14px; + cursor: pointer; + line-height: 40px; + border-radius: 4px; +} + +.ce-date-day>div:hover { + color: #5175F4; + opacity: .8; +} + +.ce-date-day>div.active { + color: #5175F4; + font-weight: 700; +} + +.ce-date-day>div.disable { + color: #c0c4cc; +} + +.ce-date-day>div.select { + color: #fff; + background-color: #5175F4; +} + +.ce-time-wrap { + display: none; + padding: 10px; + height: 286px; +} + +.ce-time-wrap ::-webkit-scrollbar { + width: 0; +} + +.ce-time-wrap.active { + display: flex; +} + +.ce-time-wrap li { + list-style: none; +} + +.ce-time-wrap>li { + width: 33.3%; + height: 100%; + text-align: center; +} + +.ce-time-wrap>li>span { + transform: translateY(-5px); + display: inline-block; +} + +.ce-time-wrap>li>ol { + height: calc(100% - 20px); + overflow-y: auto; + border: 1px solid #e2e2e2; + position: relative; +} + +.ce-time-wrap>li:first-child>ol { + border-right: 0; +} + +.ce-time-wrap>li:last-child>ol { + border-left: 0; +} + +.ce-time-wrap>li>ol>li { + line-height: 30px; + cursor: pointer; + transition: all .3s; +} + +.ce-time-wrap>li>ol>li:hover { + background-color: #eaeaea; +} + +.ce-time-wrap>li>ol>li.active { + color: #ffffff; + background: #5175F4; +} + +.ce-date-menu { + width: 100%; + height: 28px; + display: flex; + justify-content: flex-end; + align-items: center; + padding-top: 10px; + position: relative; + border-top: 1px solid #e4e7ed; +} + +.ce-date-menu button { + display: inline-block; + line-height: 1; + white-space: nowrap; + cursor: pointer; + background: #fff; + border: 1px solid #dcdfe6; + color: #606266; + appearance: none; + text-align: center; + box-sizing: border-box; + outline: none; + margin: 0; + transition: .1s; + font-weight: 500; + user-select: none; + padding: 7px 15px; + font-size: 12px; + border-radius: 3px; + margin-left: 10px; +} + +.ce-date-menu button:hover { + color: #5175F4; + border-color: #5175F4; +} + +.ce-date-menu button.ce-date-menu__time { + border: 1px solid transparent; + position: absolute; + left: 0; + margin-left: 0; +} + +.ce-date-menu button.ce-date-menu__time:hover { + color: #5175F4; +} \ No newline at end of file diff --git a/src/editor/assets/css/hyperlink/hyperlink.css b/src/editor/assets/css/hyperlink/hyperlink.css new file mode 100644 index 0000000..2b1caf6 --- /dev/null +++ b/src/editor/assets/css/hyperlink/hyperlink.css @@ -0,0 +1,26 @@ +.ce-hyperlink-popup { + background: #fff; + box-shadow: 0 2px 12px 0 rgb(98 107 132 / 20%); + border-radius: 2px; + color: #3d4757; + padding: 12px 16px; + position: absolute; + z-index: 1; + text-align: center; + display: none; +} + +.ce-hyperlink-popup a { + min-width: 100px; + max-width: 300px; + font-size: 12px; + display: inline-block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; + text-decoration: none; + border-bottom-width: 1px; + border-bottom-style: solid; + color: #0000ff; +} \ No newline at end of file diff --git a/src/editor/assets/css/index.css b/src/editor/assets/css/index.css new file mode 100644 index 0000000..206baa8 --- /dev/null +++ b/src/editor/assets/css/index.css @@ -0,0 +1,77 @@ +@import './control/select.css'; +@import './date/datePicker.css'; +@import './block/block.css'; +@import './table/table.css'; +@import './resizer/resizer.css'; +@import './previewer/previewer.css'; +@import './contextmenu/contextmenu.css'; +@import './hyperlink/hyperlink.css'; +@import './zone/zone.css'; + +.ce-inputarea { + width: 100px; + height: 30px; + min-width: 0; + min-height: 0; + margin: 0; + padding: 0; + left: 0; + top: 0; + letter-spacing: 0; + font-size: 12px; + position: absolute; + z-index: -1; + outline: none; + resize: none; + border: none; + overflow: hidden; + color: transparent; + user-select: none; + caret-color: transparent; + background-color: transparent; +} + +.ce-cursor { + width: 1px; + height: 20px; + left: 0; + right: 0; + position: absolute; + outline: none; + background-color: #000000; + pointer-events: none; +} + +.ce-cursor.ce-cursor--animation { + animation-duration: 1s; + animation-iteration-count: infinite; + animation-name: cursorAnimation; +} + +@keyframes cursorAnimation { + from { + opacity: 1 + } + + 13% { + opacity: 0 + } + + 50% { + opacity: 0 + } + + 63% { + opacity: 1 + } + + to { + opacity: 1 + } +} + +.ce-float-image { + position: absolute; + opacity: 0.5; + pointer-events: none; +} \ No newline at end of file diff --git a/src/editor/assets/css/previewer/previewer.css b/src/editor/assets/css/previewer/previewer.css new file mode 100644 index 0000000..d98224f --- /dev/null +++ b/src/editor/assets/css/previewer/previewer.css @@ -0,0 +1,122 @@ +.ce-image-previewer { + position: fixed; + left: 0; + top: 0; + z-index: 1000; + width: 100%; + height: 100%; + overflow: hidden; + background: #f2f4f7; + display: flex; + align-items: center; + justify-content: center; + animation: previewerAnimation .3s; +} + +@keyframes previewerAnimation { + 0% { + opacity: 0.1; + } + + 100% { + opacity: 1; + } +} + +.ce-image-previewer .image-close { + width: 24px; + height: 24px; + display: inline-block; + position: absolute; + right: 50px; + top: 30px; + z-index: 99; + cursor: pointer; + background: url(../../images/close.svg) no-repeat; + background-size: 100% 100%; + transition: all .3s; + border-radius: 50%; +} + +.ce-image-previewer .image-close:hover { + background-color: #e2e6ed; +} + +.ce-image-previewer .ce-image-container { + position: relative; +} + +.ce-image-previewer .ce-image-container img { + cursor: move; + position: relative; +} + +.ce-image-previewer .ce-image-menu { + height: 50px; + position: absolute; + bottom: 50px; + z-index: 99; + display: flex; + align-items: center; + justify-content: center; +} + +.ce-image-previewer .ce-image-menu i { + width: 32px; + height: 32px; + margin: 0 8px; + cursor: pointer; + display: inline-block; + background-repeat: no-repeat; + background-size: 100% 100%; + transition: all .3s; + border-radius: 50%; +} + +.ce-image-previewer .ce-image-menu i:hover { + background-color: #e2e6ed; +} + +.ce-image-previewer .ce-image-menu i.zoom-in { + background-image: url(../../images/zoom-in.svg); +} + +.ce-image-previewer .ce-image-menu i.zoom-out { + background-image: url(../../images/zoom-out.svg); +} + +.ce-image-previewer .ce-image-menu i.rotate { + background-image: url(../../images/rotate.svg); +} + +.ce-image-previewer .ce-image-menu i.original-size { + background-image: url(../../images/original-size.svg); +} + +.ce-image-previewer .ce-image-menu i.image-download { + background-image: url(../../images/image-download.svg); +} + +.ce-image-previewer .ce-image-menu .image-navigate { + display: flex; + align-items: center; + justify-content: center; +} + +.ce-image-previewer .ce-image-menu i.image-pre { + background-image: url(../../images/image-pre.svg); +} + +.ce-image-previewer .ce-image-menu i.image-next { + background-image: url(../../images/image-next.svg); +} + +.ce-image-previewer .ce-image-menu .image-count { + color: #000000; + font-size: 20px; +} + +.ce-image-previewer .ce-image-menu i.disabled { + cursor: not-allowed; + opacity: 0.5; +} \ No newline at end of file diff --git a/src/editor/assets/css/resizer/resizer.css b/src/editor/assets/css/resizer/resizer.css new file mode 100644 index 0000000..924d99c --- /dev/null +++ b/src/editor/assets/css/resizer/resizer.css @@ -0,0 +1,74 @@ +.ce-resizer-selection { + position: absolute; + border: 1px solid; + pointer-events: none; +} + +.ce-resizer-selection .resizer-handle { + position: absolute; + z-index: 9; + width: 10px; + height: 10px; + box-shadow: 0 1px 4px 0 rgb(0 0 0 / 30%); + border-radius: 5px; + border: 2px solid #ffffff; + box-sizing: border-box; + pointer-events: initial; +} + +.ce-resizer-selection .handle-0 { + cursor: nw-resize; +} + +.ce-resizer-selection .handle-1 { + cursor: n-resize; +} + +.ce-resizer-selection .handle-2 { + cursor: ne-resize; +} + +.ce-resizer-selection .handle-3 { + cursor: e-resize; +} + +.ce-resizer-selection .handle-4 { + cursor: se-resize; +} + +.ce-resizer-selection .handle-5 { + cursor: s-resize; +} + +.ce-resizer-selection .handle-6 { + cursor: sw-resize; +} + +.ce-resizer-selection .handle-7 { + cursor: w-resize; +} + +.ce-resizer-size-view { + display: flex; + align-items: center; + height: 20px; + white-space: nowrap; + position: absolute; + z-index: 9; + top: -30px; + left: 0; + opacity: .9; + background-color: #000000; + padding: 0 5px; + border-radius: 4px; +} + +.ce-resizer-size-view span { + color: #ffffff; + font-size: 12px; +} + +.ce-resizer-image { + position: absolute; + opacity: 0.5; +} \ No newline at end of file diff --git a/src/editor/assets/css/table/table.css b/src/editor/assets/css/table/table.css new file mode 100644 index 0000000..5e456ea --- /dev/null +++ b/src/editor/assets/css/table/table.css @@ -0,0 +1,155 @@ +.ce-table-tool__row { + position: absolute; + width: 12px; + border-radius: 6.5px; + overflow: hidden; + background-color: #E2E6ED; +} + +.ce-table-tool__row .ce-table-tool__row__item { + width: 100%; + position: relative; + cursor: pointer; + transition: all .3s; +} + +.ce-table-tool__row .ce-table-tool__row__item::after { + content: ''; + position: absolute; + bottom: 0; + left: 2px; + width: 8px; + height: 1px; + background-color: #C0C6CF; +} + +.ce-table-tool__row .ce-table-tool__row__item:hover { + background-color: #dadce0; +} + +.ce-table-tool__row .ce-table-tool__row__item:last-child:after { + display: none; +} + +.ce-table-tool__quick__add { + width: 16px; + height: 16px; + position: absolute; + border-radius: 50%; + background-color: #E2E6ED; + cursor: pointer; +} + +.ce-table-tool__quick__add::after { + content: '+'; + color: #ffffff; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -55%); +} + +.ce-table-tool__select { + width: 16px; + height: 18px; + position: absolute; + border-radius: 3px; + cursor: pointer; +} + +.ce-table-tool__select:hover { + background-color: #E2E6ED; +} + +.ce-table-tool__select::after { + content: ':::'; + color: #AAAAAB; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-75%, -50%) rotate(-90deg); +} + +.ce-table-tool__col { + position: absolute; + height: 12px; + border-radius: 6.5px; + overflow: hidden; + background-color: #E2E6ED; + display: flex; +} + +.ce-table-tool__col .ce-table-tool__col__item { + height: 100%; + position: relative; + cursor: pointer; + transition: all .3s; +} + +.ce-table-tool__col .ce-table-tool__col__item::after { + content: ''; + position: absolute; + top: 2px; + left: -1px; + width: 1px; + height: 8px; + z-index: 1; + background-color: #C0C6CF; +} + +.ce-table-tool__col .ce-table-tool__col__item:hover { + background-color: #dadce0; +} + +.ce-table-tool__col .ce-table-tool__col__item:first-child:after { + display: none; +} + +.ce-table-tool__row .ce-table-tool__row__item.active, +.ce-table-tool__col .ce-table-tool__col__item.active { + background-color: #C4D7FA; +} + +.ce-table-tool__col .ce-table-tool__anchor { + right: -5px; + width: 10px; + height: 12px; + z-index: 9; + position: absolute; + cursor: col-resize; +} + +.ce-table-tool__row .ce-table-tool__anchor { + bottom: -5px; + left: 0; + width: 12px; + height: 10px; + z-index: 9; + position: absolute; + cursor: row-resize; +} + +.ce-table-anchor__line { + z-index: 9; + position: absolute; + border: 1px dotted #000000; +} + +.ce-table-tool__border { + position: absolute; + z-index: 1; + background: transparent; + pointer-events: none; +} + +.ce-table-tool__border__row { + position: absolute; + cursor: row-resize; + pointer-events: auto; +} + +.ce-table-tool__border__col { + position: absolute; + cursor: col-resize; + pointer-events: auto; +} \ No newline at end of file diff --git a/src/editor/assets/css/zone/zone.css b/src/editor/assets/css/zone/zone.css new file mode 100644 index 0000000..a8c2fd3 --- /dev/null +++ b/src/editor/assets/css/zone/zone.css @@ -0,0 +1,61 @@ +.ce-zone-indicator>div { + padding: 3px 6px; + color: #000000; + font-size: 12px; + background: rgb(218 231 252); + position: absolute; + transform-origin: 0 0; +} + +.ce-zone-indicator-border__top, +.ce-zone-indicator-border__bottom, +.ce-zone-indicator-border__left, +.ce-zone-indicator-border__right { + display: block; + position: absolute; + z-index: 0; +} + +.ce-zone-indicator-border__top { + border-top: 2px dashed rgb(238, 238, 238); +} + +.ce-zone-indicator-border__bottom { + border-top: 2px dashed rgb(238, 238, 238); + width: 100%; +} + +.ce-zone-indicator-border__left { + border-left: 2px dashed rgb(238, 238, 238); +} + +.ce-zone-indicator-border__right { + border-right: 2px dashed rgb(238, 238, 238); +} + +.ce-zone-tip { + display: none; + align-items: center; + height: 30px; + white-space: nowrap; + position: fixed; + opacity: .9; + background-color: #000000; + padding: 0 5px; + border-radius: 4px; + z-index: 9; + transition: all .3s; + outline: none; + user-select: none; + pointer-events: none; + transform: translate(10px, 10px); +} + +.ce-zone-tip.show { + display: flex; +} + +.ce-zone-tip span { + color: #ffffff; + font-size: 12px; +} \ No newline at end of file diff --git a/src/editor/assets/images/close.svg b/src/editor/assets/images/close.svg new file mode 100644 index 0000000..45aaf1f --- /dev/null +++ b/src/editor/assets/images/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/delete-col.svg b/src/editor/assets/images/delete-col.svg new file mode 100644 index 0000000..5e22407 --- /dev/null +++ b/src/editor/assets/images/delete-col.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/delete-row-col.svg b/src/editor/assets/images/delete-row-col.svg new file mode 100644 index 0000000..1a46462 --- /dev/null +++ b/src/editor/assets/images/delete-row-col.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/delete-row.svg b/src/editor/assets/images/delete-row.svg new file mode 100644 index 0000000..98ffc5c --- /dev/null +++ b/src/editor/assets/images/delete-row.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/delete-table.svg b/src/editor/assets/images/delete-table.svg new file mode 100644 index 0000000..73c6b76 --- /dev/null +++ b/src/editor/assets/images/delete-table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/image-change.svg b/src/editor/assets/images/image-change.svg new file mode 100644 index 0000000..04075f9 --- /dev/null +++ b/src/editor/assets/images/image-change.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/image-download.svg b/src/editor/assets/images/image-download.svg new file mode 100644 index 0000000..8df2bd5 --- /dev/null +++ b/src/editor/assets/images/image-download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/image-next.svg b/src/editor/assets/images/image-next.svg new file mode 100644 index 0000000..345859c --- /dev/null +++ b/src/editor/assets/images/image-next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/image-pre.svg b/src/editor/assets/images/image-pre.svg new file mode 100644 index 0000000..6399602 --- /dev/null +++ b/src/editor/assets/images/image-pre.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/image.svg b/src/editor/assets/images/image.svg new file mode 100644 index 0000000..7b43678 --- /dev/null +++ b/src/editor/assets/images/image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/insert-bottom-row.svg b/src/editor/assets/images/insert-bottom-row.svg new file mode 100644 index 0000000..f18d203 --- /dev/null +++ b/src/editor/assets/images/insert-bottom-row.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/insert-left-col.svg b/src/editor/assets/images/insert-left-col.svg new file mode 100644 index 0000000..492cec3 --- /dev/null +++ b/src/editor/assets/images/insert-left-col.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/insert-right-col.svg b/src/editor/assets/images/insert-right-col.svg new file mode 100644 index 0000000..38d8306 --- /dev/null +++ b/src/editor/assets/images/insert-right-col.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/insert-row-col.svg b/src/editor/assets/images/insert-row-col.svg new file mode 100644 index 0000000..cd80e5f --- /dev/null +++ b/src/editor/assets/images/insert-row-col.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/insert-top-row.svg b/src/editor/assets/images/insert-top-row.svg new file mode 100644 index 0000000..1ad48b6 --- /dev/null +++ b/src/editor/assets/images/insert-top-row.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/merge-cancel-cell.svg b/src/editor/assets/images/merge-cancel-cell.svg new file mode 100644 index 0000000..d0f9c4b --- /dev/null +++ b/src/editor/assets/images/merge-cancel-cell.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/merge-cell.svg b/src/editor/assets/images/merge-cell.svg new file mode 100644 index 0000000..d3ea706 --- /dev/null +++ b/src/editor/assets/images/merge-cell.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/original-size.svg b/src/editor/assets/images/original-size.svg new file mode 100644 index 0000000..38c33ef --- /dev/null +++ b/src/editor/assets/images/original-size.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/print.svg b/src/editor/assets/images/print.svg new file mode 100644 index 0000000..5ee44a0 --- /dev/null +++ b/src/editor/assets/images/print.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/rotate.svg b/src/editor/assets/images/rotate.svg new file mode 100644 index 0000000..fcc5239 --- /dev/null +++ b/src/editor/assets/images/rotate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/submenu-dropdown.svg b/src/editor/assets/images/submenu-dropdown.svg new file mode 100644 index 0000000..cbfb42e --- /dev/null +++ b/src/editor/assets/images/submenu-dropdown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/table-border-all.svg b/src/editor/assets/images/table-border-all.svg new file mode 100644 index 0000000..338ae14 --- /dev/null +++ b/src/editor/assets/images/table-border-all.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/table-border-dash.svg b/src/editor/assets/images/table-border-dash.svg new file mode 100644 index 0000000..9aa7cd9 --- /dev/null +++ b/src/editor/assets/images/table-border-dash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/table-border-empty.svg b/src/editor/assets/images/table-border-empty.svg new file mode 100644 index 0000000..4a6baa6 --- /dev/null +++ b/src/editor/assets/images/table-border-empty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/table-border-external.svg b/src/editor/assets/images/table-border-external.svg new file mode 100644 index 0000000..2ad564f --- /dev/null +++ b/src/editor/assets/images/table-border-external.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/table-border-internal.svg b/src/editor/assets/images/table-border-internal.svg new file mode 100644 index 0000000..6ddb7e8 --- /dev/null +++ b/src/editor/assets/images/table-border-internal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/table-border-td-back.svg b/src/editor/assets/images/table-border-td-back.svg new file mode 100644 index 0000000..5bccca4 --- /dev/null +++ b/src/editor/assets/images/table-border-td-back.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/table-border-td-bottom.svg b/src/editor/assets/images/table-border-td-bottom.svg new file mode 100644 index 0000000..220fb72 --- /dev/null +++ b/src/editor/assets/images/table-border-td-bottom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/table-border-td-forward.svg b/src/editor/assets/images/table-border-td-forward.svg new file mode 100644 index 0000000..153bc5b --- /dev/null +++ b/src/editor/assets/images/table-border-td-forward.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/table-border-td-left.svg b/src/editor/assets/images/table-border-td-left.svg new file mode 100644 index 0000000..762804f --- /dev/null +++ b/src/editor/assets/images/table-border-td-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/table-border-td-right.svg b/src/editor/assets/images/table-border-td-right.svg new file mode 100644 index 0000000..b550f61 --- /dev/null +++ b/src/editor/assets/images/table-border-td-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/table-border-td-top.svg b/src/editor/assets/images/table-border-td-top.svg new file mode 100644 index 0000000..3e36a7b --- /dev/null +++ b/src/editor/assets/images/table-border-td-top.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/table-border-td.svg b/src/editor/assets/images/table-border-td.svg new file mode 100644 index 0000000..219d5f6 --- /dev/null +++ b/src/editor/assets/images/table-border-td.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/vertical-align-bottom.svg b/src/editor/assets/images/vertical-align-bottom.svg new file mode 100644 index 0000000..b4b8f01 --- /dev/null +++ b/src/editor/assets/images/vertical-align-bottom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/vertical-align-middle.svg b/src/editor/assets/images/vertical-align-middle.svg new file mode 100644 index 0000000..9fc9f0b --- /dev/null +++ b/src/editor/assets/images/vertical-align-middle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/vertical-align-top.svg b/src/editor/assets/images/vertical-align-top.svg new file mode 100644 index 0000000..447ed06 --- /dev/null +++ b/src/editor/assets/images/vertical-align-top.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/vertical-align.svg b/src/editor/assets/images/vertical-align.svg new file mode 100644 index 0000000..b41d2b2 --- /dev/null +++ b/src/editor/assets/images/vertical-align.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/zoom-in.svg b/src/editor/assets/images/zoom-in.svg new file mode 100644 index 0000000..c26ff88 --- /dev/null +++ b/src/editor/assets/images/zoom-in.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/assets/images/zoom-out.svg b/src/editor/assets/images/zoom-out.svg new file mode 100644 index 0000000..e828778 --- /dev/null +++ b/src/editor/assets/images/zoom-out.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/editor/core/actuator/Actuator.ts b/src/editor/core/actuator/Actuator.ts new file mode 100644 index 0000000..424150e --- /dev/null +++ b/src/editor/core/actuator/Actuator.ts @@ -0,0 +1,21 @@ +import { EventBusMap } from '../../interface/EventBus' +import { Draw } from '../draw/Draw' +import { EventBus } from '../event/eventbus/EventBus' +import { positionContextChange } from './handlers/positionContextChange' + +export class Actuator { + private draw: Draw + private eventBus: EventBus + + constructor(draw: Draw) { + this.draw = draw + this.eventBus = draw.getEventBus() + this.execute() + } + + private execute() { + this.eventBus.on('positionContextChange', payload => { + positionContextChange(this.draw, payload) + }) + } +} diff --git a/src/editor/core/actuator/handlers/positionContextChange.ts b/src/editor/core/actuator/handlers/positionContextChange.ts new file mode 100644 index 0000000..95f214b --- /dev/null +++ b/src/editor/core/actuator/handlers/positionContextChange.ts @@ -0,0 +1,13 @@ +import { IPositionContextChangePayload } from '../../../interface/Listener' +import { Draw } from '../../draw/Draw' + +export function positionContextChange( + draw: Draw, + payload: IPositionContextChangePayload +) { + const { value, oldValue } = payload + // 表格工具移除 + if (oldValue.isTable && !value.isTable) { + draw.getTableTool().dispose() + } +} diff --git a/src/editor/core/command/Command.ts b/src/editor/core/command/Command.ts new file mode 100644 index 0000000..f028442 --- /dev/null +++ b/src/editor/core/command/Command.ts @@ -0,0 +1,296 @@ +import { CommandAdapt } from './CommandAdapt' + +// 通过CommandAdapt中转避免直接暴露编辑器上下文 +export class Command { + public executeMode: CommandAdapt['mode'] + public executeCut: CommandAdapt['cut'] + public executeCopy: CommandAdapt['copy'] + public executePaste: CommandAdapt['paste'] + public executeSelectAll: CommandAdapt['selectAll'] + public executeBackspace: CommandAdapt['backspace'] + public executeSetRange: CommandAdapt['setRange'] + public executeReplaceRange: CommandAdapt['replaceRange'] + public executeSetPositionContext: CommandAdapt['setPositionContext'] + public executeForceUpdate: CommandAdapt['forceUpdate'] + public executeBlur: CommandAdapt['blur'] + public executeUndo: CommandAdapt['undo'] + public executeRedo: CommandAdapt['redo'] + public executePainter: CommandAdapt['painter'] + public executeApplyPainterStyle: CommandAdapt['applyPainterStyle'] + public executeFormat: CommandAdapt['format'] + public executeFont: CommandAdapt['font'] + public executeSize: CommandAdapt['size'] + public executeSizeAdd: CommandAdapt['sizeAdd'] + public executeSizeMinus: CommandAdapt['sizeMinus'] + public executeBold: CommandAdapt['bold'] + public executeItalic: CommandAdapt['italic'] + public executeUnderline: CommandAdapt['underline'] + public executeStrikeout: CommandAdapt['strikeout'] + public executeSuperscript: CommandAdapt['superscript'] + public executeSubscript: CommandAdapt['subscript'] + public executeColor: CommandAdapt['color'] + public executeHighlight: CommandAdapt['highlight'] + public executeTitle: CommandAdapt['title'] + public executeList: CommandAdapt['list'] + public executeRowFlex: CommandAdapt['rowFlex'] + public executeRowMargin: CommandAdapt['rowMargin'] + public executeInsertTable: CommandAdapt['insertTable'] + public executeInsertTableTopRow: CommandAdapt['insertTableTopRow'] + public executeInsertTableBottomRow: CommandAdapt['insertTableBottomRow'] + public executeInsertTableLeftCol: CommandAdapt['insertTableLeftCol'] + public executeInsertTableRightCol: CommandAdapt['insertTableRightCol'] + public executeDeleteTableRow: CommandAdapt['deleteTableRow'] + public executeDeleteTableCol: CommandAdapt['deleteTableCol'] + public executeDeleteTable: CommandAdapt['deleteTable'] + public executeMergeTableCell: CommandAdapt['mergeTableCell'] + public executeCancelMergeTableCell: CommandAdapt['cancelMergeTableCell'] + public executeSplitVerticalTableCell: CommandAdapt['splitVerticalTableCell'] + public executeSplitHorizontalTableCell: CommandAdapt['splitHorizontalTableCell'] + public executeTableTdVerticalAlign: CommandAdapt['tableTdVerticalAlign'] + public executeTableBorderType: CommandAdapt['tableBorderType'] + public executeTableBorderColor: CommandAdapt['tableBorderColor'] + public executeTableTdBorderType: CommandAdapt['tableTdBorderType'] + public executeTableTdSlashType: CommandAdapt['tableTdSlashType'] + public executeTableTdBackgroundColor: CommandAdapt['tableTdBackgroundColor'] + public executeTableSelectAll: CommandAdapt['tableSelectAll'] + public executeImage: CommandAdapt['image'] + public executeHyperlink: CommandAdapt['hyperlink'] + public executeDeleteHyperlink: CommandAdapt['deleteHyperlink'] + public executeCancelHyperlink: CommandAdapt['cancelHyperlink'] + public executeEditHyperlink: CommandAdapt['editHyperlink'] + public executeSeparator: CommandAdapt['separator'] + public executePageBreak: CommandAdapt['pageBreak'] + public executeAddWatermark: CommandAdapt['addWatermark'] + public executeDeleteWatermark: CommandAdapt['deleteWatermark'] + public executeSearch: CommandAdapt['search'] + public executeSearchNavigatePre: CommandAdapt['searchNavigatePre'] + public executeSearchNavigateNext: CommandAdapt['searchNavigateNext'] + public executeReplace: CommandAdapt['replace'] + public executePrint: CommandAdapt['print'] + public executeReplaceImageElement: CommandAdapt['replaceImageElement'] + public executeSaveAsImageElement: CommandAdapt['saveAsImageElement'] + public executeChangeImageDisplay: CommandAdapt['changeImageDisplay'] + public executePageMode: CommandAdapt['pageMode'] + public executePageScale: CommandAdapt['pageScale'] + public executePageScaleRecovery: CommandAdapt['pageScaleRecovery'] + public executePageScaleMinus: CommandAdapt['pageScaleMinus'] + public executePageScaleAdd: CommandAdapt['pageScaleAdd'] + public executePaperSize: CommandAdapt['paperSize'] + public executePaperDirection: CommandAdapt['paperDirection'] + public executeSetPaperMargin: CommandAdapt['setPaperMargin'] + public executeSetMainBadge: CommandAdapt['setMainBadge'] + public executeSetAreaBadge: CommandAdapt['setAreaBadge'] + public executeInsertElementList: CommandAdapt['insertElementList'] + public executeInsertArea: CommandAdapt['insertArea'] + public executeSetAreaValue: CommandAdapt['setAreaValue'] + public executeSetAreaProperties: CommandAdapt['setAreaProperties'] + public executeLocationArea: CommandAdapt['locationArea'] + public executeAppendElementList: CommandAdapt['appendElementList'] + public executeUpdateElementById: CommandAdapt['updateElementById'] + public executeDeleteElementById: CommandAdapt['deleteElementById'] + public executeSetValue: CommandAdapt['setValue'] + public executeRemoveControl: CommandAdapt['removeControl'] + public executeTranslate: CommandAdapt['translate'] + public executeSetLocale: CommandAdapt['setLocale'] + public executeLocationCatalog: CommandAdapt['locationCatalog'] + public executeWordTool: CommandAdapt['wordTool'] + public executeSetHTML: CommandAdapt['setHTML'] + public executeSetGroup: CommandAdapt['setGroup'] + public executeDeleteGroup: CommandAdapt['deleteGroup'] + public executeLocationGroup: CommandAdapt['locationGroup'] + public executeSetZone: CommandAdapt['setZone'] + public executeSetControlValue: CommandAdapt['setControlValue'] + public executeSetControlValueList: CommandAdapt['setControlValueList'] + public executeSetControlExtension: CommandAdapt['setControlExtension'] + public executeSetControlExtensionList: CommandAdapt['setControlExtensionList'] + public executeSetControlProperties: CommandAdapt['setControlProperties'] + public executeSetControlPropertiesList: CommandAdapt['setControlPropertiesList'] + public executeSetControlHighlight: CommandAdapt['setControlHighlight'] + public executeLocationControl: CommandAdapt['locationControl'] + public executeInsertControl: CommandAdapt['insertControl'] + public executeUpdateOptions: CommandAdapt['updateOptions'] + public executeInsertTitle: CommandAdapt['insertTitle'] + public executeFocus: CommandAdapt['focus'] + public getCatalog: CommandAdapt['getCatalog'] + public getImage: CommandAdapt['getImage'] + public getOptions: CommandAdapt['getOptions'] + public getValue: CommandAdapt['getValue'] + public getValueAsync: CommandAdapt['getValueAsync'] + public getAreaValue: CommandAdapt['getAreaValue'] + public getHTML: CommandAdapt['getHTML'] + public getText: CommandAdapt['getText'] + public getWordCount: CommandAdapt['getWordCount'] + public getCursorPosition: CommandAdapt['getCursorPosition'] + public getRange: CommandAdapt['getRange'] + public getRangeText: CommandAdapt['getRangeText'] + public getRangeContext: CommandAdapt['getRangeContext'] + public getRangeRow: CommandAdapt['getRangeRow'] + public getRangeParagraph: CommandAdapt['getRangeParagraph'] + public getKeywordRangeList: CommandAdapt['getKeywordRangeList'] + public getKeywordContext: CommandAdapt['getKeywordContext'] + public getPaperMargin: CommandAdapt['getPaperMargin'] + public getSearchNavigateInfo: CommandAdapt['getSearchNavigateInfo'] + public getLocale: CommandAdapt['getLocale'] + public getGroupIds: CommandAdapt['getGroupIds'] + public getControlValue: CommandAdapt['getControlValue'] + public getControlList: CommandAdapt['getControlList'] + public getContainer: CommandAdapt['getContainer'] + public getTitleValue: CommandAdapt['getTitleValue'] + public getPositionContextByEvent: CommandAdapt['getPositionContextByEvent'] + public getElementById: CommandAdapt['getElementById'] + + constructor(adapt: CommandAdapt) { + // 全局命令 + this.executeMode = adapt.mode.bind(adapt) + this.executeCut = adapt.cut.bind(adapt) + this.executeCopy = adapt.copy.bind(adapt) + this.executePaste = adapt.paste.bind(adapt) + this.executeSelectAll = adapt.selectAll.bind(adapt) + this.executeBackspace = adapt.backspace.bind(adapt) + this.executeSetRange = adapt.setRange.bind(adapt) + this.executeReplaceRange = adapt.replaceRange.bind(adapt) + this.executeSetPositionContext = adapt.setPositionContext.bind(adapt) + this.executeForceUpdate = adapt.forceUpdate.bind(adapt) + this.executeBlur = adapt.blur.bind(adapt) + // 撤销、重做、格式刷、清除格式 + this.executeUndo = adapt.undo.bind(adapt) + this.executeRedo = adapt.redo.bind(adapt) + this.executePainter = adapt.painter.bind(adapt) + this.executeApplyPainterStyle = adapt.applyPainterStyle.bind(adapt) + this.executeFormat = adapt.format.bind(adapt) + // 字体、字体大小、字体变大、字体变小、加粗、斜体、下划线、删除线、字体颜色、背景色 + this.executeFont = adapt.font.bind(adapt) + this.executeSize = adapt.size.bind(adapt) + this.executeSizeAdd = adapt.sizeAdd.bind(adapt) + this.executeSizeMinus = adapt.sizeMinus.bind(adapt) + this.executeBold = adapt.bold.bind(adapt) + this.executeItalic = adapt.italic.bind(adapt) + this.executeUnderline = adapt.underline.bind(adapt) + this.executeStrikeout = adapt.strikeout.bind(adapt) + this.executeSuperscript = adapt.superscript.bind(adapt) + this.executeSubscript = adapt.subscript.bind(adapt) + this.executeColor = adapt.color.bind(adapt) + this.executeHighlight = adapt.highlight.bind(adapt) + // 标题、对齐方式、列表 + this.executeTitle = adapt.title.bind(adapt) + this.executeList = adapt.list.bind(adapt) + this.executeRowFlex = adapt.rowFlex.bind(adapt) + this.executeRowMargin = adapt.rowMargin.bind(adapt) + // 表格、图片上传、超链接、搜索、打印、图片操作 + this.executeInsertTable = adapt.insertTable.bind(adapt) + this.executeInsertTableTopRow = adapt.insertTableTopRow.bind(adapt) + this.executeInsertTableBottomRow = adapt.insertTableBottomRow.bind(adapt) + this.executeInsertTableLeftCol = adapt.insertTableLeftCol.bind(adapt) + this.executeInsertTableRightCol = adapt.insertTableRightCol.bind(adapt) + this.executeDeleteTableRow = adapt.deleteTableRow.bind(adapt) + this.executeDeleteTableCol = adapt.deleteTableCol.bind(adapt) + this.executeDeleteTable = adapt.deleteTable.bind(adapt) + this.executeMergeTableCell = adapt.mergeTableCell.bind(adapt) + this.executeCancelMergeTableCell = adapt.cancelMergeTableCell.bind(adapt) + this.executeSplitVerticalTableCell = + adapt.splitVerticalTableCell.bind(adapt) + this.executeSplitHorizontalTableCell = + adapt.splitHorizontalTableCell.bind(adapt) + this.executeTableTdVerticalAlign = adapt.tableTdVerticalAlign.bind(adapt) + this.executeTableBorderType = adapt.tableBorderType.bind(adapt) + this.executeTableBorderColor = adapt.tableBorderColor.bind(adapt) + this.executeTableTdBorderType = adapt.tableTdBorderType.bind(adapt) + this.executeTableTdSlashType = adapt.tableTdSlashType.bind(adapt) + this.executeTableTdBackgroundColor = + adapt.tableTdBackgroundColor.bind(adapt) + this.executeTableSelectAll = adapt.tableSelectAll.bind(adapt) + this.executeImage = adapt.image.bind(adapt) + this.executeHyperlink = adapt.hyperlink.bind(adapt) + this.executeDeleteHyperlink = adapt.deleteHyperlink.bind(adapt) + this.executeCancelHyperlink = adapt.cancelHyperlink.bind(adapt) + this.executeEditHyperlink = adapt.editHyperlink.bind(adapt) + this.executeSeparator = adapt.separator.bind(adapt) + this.executePageBreak = adapt.pageBreak.bind(adapt) + this.executeAddWatermark = adapt.addWatermark.bind(adapt) + this.executeDeleteWatermark = adapt.deleteWatermark.bind(adapt) + this.executeSearch = adapt.search.bind(adapt) + this.executeSearchNavigatePre = adapt.searchNavigatePre.bind(adapt) + this.executeSearchNavigateNext = adapt.searchNavigateNext.bind(adapt) + this.executeReplace = adapt.replace.bind(adapt) + this.executePrint = adapt.print.bind(adapt) + this.executeReplaceImageElement = adapt.replaceImageElement.bind(adapt) + this.executeSaveAsImageElement = adapt.saveAsImageElement.bind(adapt) + this.executeChangeImageDisplay = adapt.changeImageDisplay.bind(adapt) + // 页面模式、页面缩放、纸张大小、纸张方向、页边距 + this.executePageMode = adapt.pageMode.bind(adapt) + this.executePageScale = adapt.pageScale.bind(adapt) + this.executePageScaleRecovery = adapt.pageScaleRecovery.bind(adapt) + this.executePageScaleMinus = adapt.pageScaleMinus.bind(adapt) + this.executePageScaleAdd = adapt.pageScaleAdd.bind(adapt) + this.executePaperSize = adapt.paperSize.bind(adapt) + this.executePaperDirection = adapt.paperDirection.bind(adapt) + this.executeSetPaperMargin = adapt.setPaperMargin.bind(adapt) + // 签章 + this.executeSetMainBadge = adapt.setMainBadge.bind(adapt) + this.executeSetAreaBadge = adapt.setAreaBadge.bind(adapt) + // 区域 + this.getAreaValue = adapt.getAreaValue.bind(adapt) + this.executeInsertArea = adapt.insertArea.bind(adapt) + this.executeSetAreaValue = adapt.setAreaValue.bind(adapt) + this.executeSetAreaProperties = adapt.setAreaProperties.bind(adapt) + this.executeLocationArea = adapt.locationArea.bind(adapt) + // 通用 + this.executeInsertElementList = adapt.insertElementList.bind(adapt) + this.executeAppendElementList = adapt.appendElementList.bind(adapt) + this.executeUpdateElementById = adapt.updateElementById.bind(adapt) + this.executeDeleteElementById = adapt.deleteElementById.bind(adapt) + this.executeSetValue = adapt.setValue.bind(adapt) + this.executeRemoveControl = adapt.removeControl.bind(adapt) + this.executeTranslate = adapt.translate.bind(adapt) + this.executeSetLocale = adapt.setLocale.bind(adapt) + this.executeLocationCatalog = adapt.locationCatalog.bind(adapt) + this.executeWordTool = adapt.wordTool.bind(adapt) + this.executeSetHTML = adapt.setHTML.bind(adapt) + this.executeSetGroup = adapt.setGroup.bind(adapt) + this.executeDeleteGroup = adapt.deleteGroup.bind(adapt) + this.executeLocationGroup = adapt.locationGroup.bind(adapt) + this.executeSetZone = adapt.setZone.bind(adapt) + this.executeUpdateOptions = adapt.updateOptions.bind(adapt) + this.executeInsertTitle = adapt.insertTitle.bind(adapt) + this.executeFocus = adapt.focus.bind(adapt) + // 获取 + this.getImage = adapt.getImage.bind(adapt) + this.getOptions = adapt.getOptions.bind(adapt) + this.getValue = adapt.getValue.bind(adapt) + this.getValueAsync = adapt.getValueAsync.bind(adapt) + this.getHTML = adapt.getHTML.bind(adapt) + this.getText = adapt.getText.bind(adapt) + this.getWordCount = adapt.getWordCount.bind(adapt) + this.getCursorPosition = adapt.getCursorPosition.bind(adapt) + this.getRange = adapt.getRange.bind(adapt) + this.getRangeText = adapt.getRangeText.bind(adapt) + this.getRangeContext = adapt.getRangeContext.bind(adapt) + this.getRangeRow = adapt.getRangeRow.bind(adapt) + this.getRangeParagraph = adapt.getRangeParagraph.bind(adapt) + this.getKeywordRangeList = adapt.getKeywordRangeList.bind(adapt) + this.getKeywordContext = adapt.getKeywordContext.bind(adapt) + this.getCatalog = adapt.getCatalog.bind(adapt) + this.getPaperMargin = adapt.getPaperMargin.bind(adapt) + this.getSearchNavigateInfo = adapt.getSearchNavigateInfo.bind(adapt) + this.getLocale = adapt.getLocale.bind(adapt) + this.getGroupIds = adapt.getGroupIds.bind(adapt) + this.getContainer = adapt.getContainer.bind(adapt) + this.getTitleValue = adapt.getTitleValue.bind(adapt) + this.getPositionContextByEvent = adapt.getPositionContextByEvent.bind(adapt) + this.getElementById = adapt.getElementById.bind(adapt) + // 控件 + this.executeSetControlValue = adapt.setControlValue.bind(adapt) + this.executeSetControlValueList = adapt.setControlValueList.bind(adapt) + this.executeSetControlExtension = adapt.setControlExtension.bind(adapt) + this.executeSetControlExtensionList = + adapt.setControlExtensionList.bind(adapt) + this.executeSetControlProperties = adapt.setControlProperties.bind(adapt) + this.executeSetControlPropertiesList = + adapt.setControlPropertiesList.bind(adapt) + this.executeSetControlHighlight = adapt.setControlHighlight.bind(adapt) + this.getControlValue = adapt.getControlValue.bind(adapt) + this.getControlList = adapt.getControlList.bind(adapt) + this.executeLocationControl = adapt.locationControl.bind(adapt) + this.executeInsertControl = adapt.insertControl.bind(adapt) + } +} diff --git a/src/editor/core/command/CommandAdapt.ts b/src/editor/core/command/CommandAdapt.ts new file mode 100644 index 0000000..099fc14 --- /dev/null +++ b/src/editor/core/command/CommandAdapt.ts @@ -0,0 +1,2654 @@ +import { NBSP, WRAP, ZERO } from '../../dataset/constant/Common' +import { + AREA_CONTEXT_ATTR, + EDITOR_ELEMENT_STYLE_ATTR, + EDITOR_ROW_ATTR, + LIST_CONTEXT_ATTR, + TABLE_CONTEXT_ATTR +} from '../../dataset/constant/Element' +import { + titleOrderNumberMapping, + titleSizeMapping +} from '../../dataset/constant/Title' +import { defaultWatermarkOption } from '../../dataset/constant/Watermark' +import { ImageDisplay, LocationPosition } from '../../dataset/enum/Common' +import { ControlComponent } from '../../dataset/enum/Control' +import { + EditorMode, + EditorZone, + PageMode, + PaperDirection +} from '../../dataset/enum/Editor' +import { ElementType } from '../../dataset/enum/Element' +import { ElementStyleKey } from '../../dataset/enum/ElementStyle' +import { ListStyle, ListType } from '../../dataset/enum/List' +import { MoveDirection } from '../../dataset/enum/Observer' +import { RowFlex } from '../../dataset/enum/Row' +import { TableBorder, TdBorder, TdSlash } from '../../dataset/enum/table/Table' +import { TitleLevel } from '../../dataset/enum/Title' +import { VerticalAlign } from '../../dataset/enum/VerticalAlign' +import { ICatalog } from '../../interface/Catalog' +import { DeepRequired } from '../../interface/Common' +import { + IGetControlValueOption, + IGetControlValueResult, + ILocationControlOption, + IRemoveControlOption, + ISetControlExtensionOption, + ISetControlHighlightOption, + ISetControlProperties, + ISetControlValueOption +} from '../../interface/Control' +import { + IAppendElementListOption, + IDrawImagePayload, + IDrawOption, + IForceUpdateOption, + IGetImageOption, + IGetValueOption, + IPainterOption +} from '../../interface/Draw' +import { + IEditorData, + IEditorHTML, + IEditorOption, + IEditorResult, + IEditorText, + IFocusOption, + ISetValueOption, + IUpdateOption +} from '../../interface/Editor' +import { + IDeleteElementByIdOption, + IElement, + IElementPosition, + IElementStyle, + IGetElementByIdOption, + IInsertElementListOption, + IUpdateElementByIdOption +} from '../../interface/Element' +import { + ICopyOption, + IPasteOption, + IPositionContextByEventOption, + IPositionContextByEventResult, + ITableInfoByEvent +} from '../../interface/Event' +import { IMargin } from '../../interface/Margin' +import { ILocationPosition, IPositionContext } from '../../interface/Position' +import { IRange, RangeContext, RangeRect } from '../../interface/Range' +import { IReplaceOption, ISearchResultContext } from '../../interface/Search' +import { ITextDecoration } from '../../interface/Text' +import { + IGetTitleValueOption, + IGetTitleValueResult +} from '../../interface/Title' +import { IWatermark } from '../../interface/Watermark' +import { + cloneProperty, + deepClone, + downloadFile, + getUUID, + isNumber, + isObjectEqual +} from '../../utils' +import { + createDomFromElementList, + formatElementContext, + formatElementList, + isTextLikeElement, + pickElementAttr, + getElementListByHTML, + getTextFromElementList, + zipElementList, + getAnchorElement +} from '../../utils/element' +import { mergeOption } from '../../utils/option' +import { printImageBase64 } from '../../utils/print' +import { Control } from '../draw/control/Control' +import { Draw } from '../draw/Draw' +import { INavigateInfo, Search } from '../draw/interactive/Search' +import { TableOperate } from '../draw/particle/table/TableOperate' +import { CanvasEvent } from '../event/CanvasEvent' +import { pasteByApi } from '../event/handlers/paste' +import { HistoryManager } from '../history/HistoryManager' +import { I18n } from '../i18n/I18n' +import { Position } from '../position/Position' +import { RangeManager } from '../range/RangeManager' +import { WorkerManager } from '../worker/WorkerManager' +import { Zone } from '../zone/Zone' +import { + IGetAreaValueOption, + IGetAreaValueResult, + IInsertAreaOption, + ILocationAreaOption, + ISetAreaPropertiesOption, + ISetAreaValueOption +} from '../../interface/Area' +import { IAreaBadge, IBadge } from '../../interface/Badge' +import { IRichtextOption } from '../../interface/Command' +import { WatermarkType } from '../../dataset/enum/Watermark' + +export class CommandAdapt { + private draw: Draw + private range: RangeManager + private position: Position + private historyManager: HistoryManager + private canvasEvent: CanvasEvent + private options: DeepRequired + private control: Control + private workerManager: WorkerManager + private searchManager: Search + private i18n: I18n + private zone: Zone + private tableOperate: TableOperate + + constructor(draw: Draw) { + this.draw = draw + this.range = draw.getRange() + this.position = draw.getPosition() + this.historyManager = draw.getHistoryManager() + this.canvasEvent = draw.getCanvasEvent() + this.options = draw.getOptions() + this.control = draw.getControl() + this.workerManager = draw.getWorkerManager() + this.searchManager = draw.getSearch() + this.i18n = draw.getI18n() + this.zone = draw.getZone() + this.tableOperate = draw.getTableOperate() + } + + public mode(payload: EditorMode) { + this.draw.setMode(payload) + } + + public cut() { + const isDisabled = this.draw.isReadonly() || this.draw.isDisabled() + if (isDisabled) return + this.canvasEvent.cut() + } + + public copy(payload?: ICopyOption) { + this.canvasEvent.copy(payload) + } + + public paste(payload?: IPasteOption) { + const isDisabled = this.draw.isReadonly() || this.draw.isDisabled() + if (isDisabled) return + pasteByApi(this.canvasEvent, payload) + } + + public selectAll() { + this.canvasEvent.selectAll() + } + + public backspace() { + const isDisabled = this.draw.isReadonly() || this.draw.isDisabled() + if (isDisabled) return + const elementList = this.draw.getElementList() + const { startIndex, endIndex } = this.range.getRange() + const isCollapsed = startIndex === endIndex + // 首字符禁止删除 + if ( + isCollapsed && + elementList[startIndex].value === ZERO && + startIndex === 0 + ) { + return + } + if (!isCollapsed) { + this.draw.spliceElementList( + elementList, + startIndex + 1, + endIndex - startIndex + ) + } else { + this.draw.spliceElementList(elementList, startIndex, 1) + } + const curIndex = isCollapsed ? startIndex - 1 : startIndex + this.range.setRange(curIndex, curIndex) + this.draw.render({ curIndex }) + } + + public setRange( + startIndex: number, + endIndex: number, + tableId?: string, + startTdIndex?: number, + endTdIndex?: number, + startTrIndex?: number, + endTrIndex?: number + ) { + if (startIndex < 0 || endIndex < 0 || endIndex < startIndex) return + this.range.setRange( + startIndex, + endIndex, + tableId, + startTdIndex, + endTdIndex, + startTrIndex, + endTrIndex + ) + const isCollapsed = startIndex === endIndex + this.draw.render({ + curIndex: isCollapsed ? startIndex : undefined, + isCompute: false, + isSubmitHistory: false, + isSetCursor: isCollapsed + }) + } + + public replaceRange(range: IRange) { + this.setRange( + range.startIndex, + range.endIndex, + range.tableId, + range.startTdIndex, + range.endTdIndex, + range.startTrIndex, + range.endTrIndex + ) + } + + public setPositionContext(range: IRange) { + const { tableId, startTrIndex, startTdIndex } = range + const elementList = this.draw.getOriginalElementList() + if (tableId) { + const tableElementIndex = elementList.findIndex(el => el.id === tableId) + if (!~tableElementIndex) return + const tableElement = elementList[tableElementIndex] + const tr = tableElement.trList![startTrIndex!] + const td = tr.tdList[startTdIndex!] + this.position.setPositionContext({ + isTable: true, + index: tableElementIndex, + trIndex: startTrIndex, + tdIndex: startTdIndex, + tdId: td.id, + trId: tr.id, + tableId + }) + } else { + this.position.setPositionContext({ + isTable: false + }) + } + } + + public forceUpdate(options?: IForceUpdateOption) { + const { isSubmitHistory = false } = options || {} + this.range.clearRange() + this.draw.render({ + isSubmitHistory, + isSetCursor: false + }) + } + + public blur() { + this.range.clearRange() + this.draw.getCursor().recoveryCursor() + } + + public undo() { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.historyManager.undo() + } + + public redo() { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.historyManager.redo() + } + + public painter(options: IPainterOption) { + // 如果单击且已经有样式设置则取消设置 + if (!options.isDblclick && this.draw.getPainterStyle()) { + this.canvasEvent.clearPainterStyle() + return + } + const selection = this.range.getSelection() + if (!selection) return + const painterStyle: IElementStyle = {} + selection.forEach(s => { + const painterStyleKeys = EDITOR_ELEMENT_STYLE_ATTR + painterStyleKeys.forEach(p => { + const key = p as keyof typeof ElementStyleKey + if (painterStyle[key] === undefined) { + painterStyle[key] = s[key] as any + } + }) + }) + this.draw.setPainterStyle(painterStyle, options) + } + + public applyPainterStyle() { + const isDisabled = this.draw.isReadonly() || this.draw.isDisabled() + if (isDisabled) return + this.canvasEvent.applyPainterStyle() + } + + public format(options?: IRichtextOption) { + const { isIgnoreDisabledRule = false } = options || {} + const isDisabled = + !isIgnoreDisabledRule && + (this.draw.isReadonly() || this.draw.isDisabled()) + if (isDisabled) return + const selection = this.range.getSelectionElementList() + // 选区设置或设置换行处样式 + let renderOption: IDrawOption = {} + let changeElementList: IElement[] = [] + if (selection?.length) { + changeElementList = selection + renderOption = { isSetCursor: false } + } else { + const { endIndex } = this.range.getRange() + const elementList = this.draw.getElementList() + const enterElement = elementList[endIndex] + if (enterElement?.value === ZERO) { + changeElementList.push(enterElement) + renderOption = { curIndex: endIndex } + } + } + if (!changeElementList.length) return + changeElementList.forEach(el => { + EDITOR_ELEMENT_STYLE_ATTR.forEach(attr => { + delete el[attr] + }) + }) + this.draw.render(renderOption) + } + + public font(payload: string, options?: IRichtextOption) { + const { isIgnoreDisabledRule = false } = options || {} + const isDisabled = + !isIgnoreDisabledRule && + (this.draw.isReadonly() || this.draw.isDisabled()) + if (isDisabled) return + const selection = this.range.getSelectionElementList() + if (selection?.length) { + selection.forEach(el => { + el.font = payload + }) + this.draw.render({ isSetCursor: false }) + } else { + let isSubmitHistory = true + const { endIndex } = this.range.getRange() + const elementList = this.draw.getElementList() + const enterElement = elementList[endIndex] + this.range.setDefaultStyle({ + font: payload + }) + if (enterElement?.value === ZERO) { + enterElement.font = payload + } else { + isSubmitHistory = false + } + this.draw.render({ + isSubmitHistory, + curIndex: endIndex, + isCompute: false + }) + } + } + + public size(payload: number, options?: IRichtextOption) { + const { isIgnoreDisabledRule = false } = options || {} + const isDisabled = + !isIgnoreDisabledRule && + (this.draw.isReadonly() || this.draw.isDisabled()) + if (isDisabled) return + const { minSize, maxSize, defaultSize } = this.options + if (payload < minSize || payload > maxSize) return + // 选区设置或设置换行处样式 + let renderOption: IDrawOption = {} + let changeElementList: IElement[] = [] + const selection = this.range.getTextLikeSelectionElementList() + if (selection?.length) { + changeElementList = selection + renderOption = { isSetCursor: false } + } else { + const { endIndex } = this.range.getRange() + const elementList = this.draw.getElementList() + const enterElement = elementList[endIndex] + this.range.setDefaultStyle({ + size: payload + }) + if (enterElement?.value === ZERO) { + changeElementList.push(enterElement) + renderOption = { curIndex: endIndex } + } else { + this.draw.render({ + curIndex: endIndex, + isCompute: false, + isSubmitHistory: false + }) + } + } + if (!changeElementList.length) return + let isExistUpdate = false + changeElementList.forEach(el => { + if ( + (!el.size && payload === defaultSize) || + (el.size && el.size === payload) + ) { + return + } + el.size = payload + isExistUpdate = true + }) + if (isExistUpdate) { + this.draw.render(renderOption) + } + } + + public sizeAdd(options?: IRichtextOption) { + const { isIgnoreDisabledRule = false } = options || {} + const isDisabled = + !isIgnoreDisabledRule && + (this.draw.isReadonly() || this.draw.isDisabled()) + if (isDisabled) return + const { defaultSize, maxSize } = this.options + const selection = this.range.getTextLikeSelectionElementList() + // 选区设置或设置换行处样式 + let renderOption: IDrawOption = {} + let changeElementList: IElement[] = [] + if (selection?.length) { + changeElementList = selection + renderOption = { isSetCursor: false } + } else { + const { endIndex } = this.range.getRange() + const elementList = this.draw.getElementList() + const enterElement = elementList[endIndex] + // 设置默认样式 + const style = this.range.getDefaultStyle() + const anchorSize = style?.size || enterElement.size || defaultSize + this.range.setDefaultStyle({ + size: anchorSize + 2 > maxSize ? maxSize : anchorSize + 2 + }) + if (enterElement?.value === ZERO) { + changeElementList.push(enterElement) + renderOption = { curIndex: endIndex } + } else { + this.draw.render({ + curIndex: endIndex, + isCompute: false, + isSubmitHistory: false + }) + } + } + if (!changeElementList.length) return + let isExistUpdate = false + changeElementList.forEach(el => { + if (!el.size) { + el.size = defaultSize + } + if (el.size >= maxSize) return + if (el.size + 2 > maxSize) { + el.size = maxSize + } else { + el.size += 2 + } + isExistUpdate = true + }) + if (isExistUpdate) { + this.draw.render(renderOption) + } + } + + public sizeMinus(options?: IRichtextOption) { + const { isIgnoreDisabledRule = false } = options || {} + const isDisabled = + !isIgnoreDisabledRule && + (this.draw.isReadonly() || this.draw.isDisabled()) + if (isDisabled) return + const { defaultSize, minSize } = this.options + const selection = this.range.getTextLikeSelectionElementList() + // 选区设置或设置换行处样式 + let renderOption: IDrawOption = {} + let changeElementList: IElement[] = [] + if (selection?.length) { + changeElementList = selection + renderOption = { isSetCursor: false } + } else { + const { endIndex } = this.range.getRange() + const elementList = this.draw.getElementList() + const enterElement = elementList[endIndex] + const style = this.range.getDefaultStyle() + const anchorSize = style?.size || enterElement.size || defaultSize + this.range.setDefaultStyle({ + size: anchorSize - 2 < minSize ? minSize : anchorSize - 2 + }) + if (enterElement?.value === ZERO) { + changeElementList.push(enterElement) + renderOption = { curIndex: endIndex } + } else { + this.draw.render({ + curIndex: endIndex, + isCompute: false, + isSubmitHistory: false + }) + } + } + if (!changeElementList.length) return + let isExistUpdate = false + changeElementList.forEach(el => { + if (!el.size) { + el.size = defaultSize + } + if (el.size <= minSize) return + if (el.size - 2 < minSize) { + el.size = minSize + } else { + el.size -= 2 + } + isExistUpdate = true + }) + if (isExistUpdate) { + this.draw.render(renderOption) + } + } + + public bold(options?: IRichtextOption) { + const { isIgnoreDisabledRule = false } = options || {} + const isDisabled = + !isIgnoreDisabledRule && + (this.draw.isReadonly() || this.draw.isDisabled()) + if (isDisabled) return + const selection = this.range.getSelectionElementList() + if (selection?.length) { + const noBoldIndex = selection.findIndex(s => !s.bold) + selection.forEach(el => { + el.bold = !!~noBoldIndex + }) + this.draw.render({ isSetCursor: false }) + } else { + let isSubmitHistory = true + const { endIndex } = this.range.getRange() + const elementList = this.draw.getElementList() + const enterElement = elementList[endIndex] + this.range.setDefaultStyle({ + bold: enterElement.bold ? false : !this.range.getDefaultStyle()?.bold + }) + if (enterElement?.value === ZERO) { + enterElement.bold = !enterElement.bold + } else { + isSubmitHistory = false + } + this.draw.render({ + isSubmitHistory, + curIndex: endIndex, + isCompute: false + }) + } + } + + public italic(options?: IRichtextOption) { + const { isIgnoreDisabledRule = false } = options || {} + const isDisabled = + !isIgnoreDisabledRule && + (this.draw.isReadonly() || this.draw.isDisabled()) + if (isDisabled) return + const selection = this.range.getSelectionElementList() + if (selection?.length) { + const noItalicIndex = selection.findIndex(s => !s.italic) + selection.forEach(el => { + el.italic = !!~noItalicIndex + }) + this.draw.render({ isSetCursor: false }) + } else { + let isSubmitHistory = true + const { endIndex } = this.range.getRange() + const elementList = this.draw.getElementList() + const enterElement = elementList[endIndex] + this.range.setDefaultStyle({ + italic: enterElement.italic + ? false + : !this.range.getDefaultStyle()?.italic + }) + if (enterElement?.value === ZERO) { + enterElement.italic = !enterElement.italic + } else { + isSubmitHistory = false + } + this.draw.render({ + isSubmitHistory, + curIndex: endIndex, + isCompute: false + }) + } + } + + public underline( + textDecoration?: ITextDecoration, + options?: IRichtextOption + ) { + const { isIgnoreDisabledRule = false } = options || {} + const isDisabled = + !isIgnoreDisabledRule && + (this.draw.isReadonly() || this.draw.isDisabled()) + if (isDisabled) return + const selection = this.range.getSelectionElementList() + if (selection?.length) { + // 没有设置下划线、当前与之前有一个设置不存在、文本装饰不一致时重设下划线 + const isSetUnderline = selection.some( + s => + !s.underline || + (!textDecoration && s.textDecoration) || + (textDecoration && !s.textDecoration) || + (textDecoration && + s.textDecoration && + !isObjectEqual(s.textDecoration, textDecoration)) + ) + selection.forEach(el => { + el.underline = isSetUnderline + if (isSetUnderline && textDecoration) { + el.textDecoration = textDecoration + } else { + delete el.textDecoration + } + }) + this.draw.render({ + isSetCursor: false, + isCompute: false + }) + } else { + let isSubmitHistory = true + const { endIndex } = this.range.getRange() + const elementList = this.draw.getElementList() + const enterElement = elementList[endIndex] + this.range.setDefaultStyle({ + underline: enterElement?.underline + ? false + : !this.range.getDefaultStyle()?.underline + }) + if (enterElement?.value === ZERO) { + enterElement.underline = !enterElement.underline + } else { + isSubmitHistory = false + } + this.draw.render({ + isSubmitHistory, + curIndex: endIndex, + isCompute: false + }) + } + } + + public strikeout(options?: IRichtextOption) { + const { isIgnoreDisabledRule = false } = options || {} + const isDisabled = + !isIgnoreDisabledRule && + (this.draw.isReadonly() || this.draw.isDisabled()) + if (isDisabled) return + const selection = this.range.getSelectionElementList() + if (selection?.length) { + const noStrikeoutIndex = selection.findIndex(s => !s.strikeout) + selection.forEach(el => { + el.strikeout = !!~noStrikeoutIndex + }) + this.draw.render({ + isSetCursor: false, + isCompute: false + }) + } else { + let isSubmitHistory = true + const { endIndex } = this.range.getRange() + const elementList = this.draw.getElementList() + const enterElement = elementList[endIndex] + this.range.setDefaultStyle({ + strikeout: enterElement.strikeout + ? false + : !this.range.getDefaultStyle()?.strikeout + }) + if (enterElement?.value === ZERO) { + enterElement.strikeout = !enterElement.strikeout + } else { + isSubmitHistory = false + } + this.draw.render({ + isSubmitHistory, + curIndex: endIndex, + isCompute: false + }) + } + } + + public superscript(options?: IRichtextOption) { + const { isIgnoreDisabledRule = false } = options || {} + const isDisabled = + !isIgnoreDisabledRule && + (this.draw.isReadonly() || this.draw.isDisabled()) + if (isDisabled) return + const selection = this.range.getSelectionElementList() + if (!selection) return + const superscriptIndex = selection.findIndex( + s => s.type === ElementType.SUPERSCRIPT + ) + selection.forEach(el => { + // 取消上标 + if (~superscriptIndex) { + if (el.type === ElementType.SUPERSCRIPT) { + el.type = ElementType.TEXT + delete el.actualSize + } + } else { + // 设置上标 + if ( + !el.type || + el.type === ElementType.TEXT || + el.type === ElementType.SUBSCRIPT + ) { + el.type = ElementType.SUPERSCRIPT + } + } + }) + this.draw.render({ isSetCursor: false }) + } + + public subscript(options?: IRichtextOption) { + const { isIgnoreDisabledRule = false } = options || {} + const isDisabled = + !isIgnoreDisabledRule && + (this.draw.isReadonly() || this.draw.isDisabled()) + if (isDisabled) return + const selection = this.range.getSelectionElementList() + if (!selection) return + const subscriptIndex = selection.findIndex( + s => s.type === ElementType.SUBSCRIPT + ) + selection.forEach(el => { + // 取消下标 + if (~subscriptIndex) { + if (el.type === ElementType.SUBSCRIPT) { + el.type = ElementType.TEXT + delete el.actualSize + } + } else { + // 设置下标 + if ( + !el.type || + el.type === ElementType.TEXT || + el.type === ElementType.SUPERSCRIPT + ) { + el.type = ElementType.SUBSCRIPT + } + } + }) + this.draw.render({ isSetCursor: false }) + } + + public color(payload: string | null, options?: IRichtextOption) { + const { isIgnoreDisabledRule = false } = options || {} + const isDisabled = + !isIgnoreDisabledRule && + (this.draw.isReadonly() || this.draw.isDisabled()) + if (isDisabled) return + const selection = this.range.getSelectionElementList() + if (selection?.length) { + selection.forEach(el => { + if (payload) { + el.color = payload + } else { + delete el.color + } + }) + this.draw.render({ + isSetCursor: false, + isCompute: false + }) + } else { + let isSubmitHistory = true + const { endIndex } = this.range.getRange() + const elementList = this.draw.getElementList() + const enterElement = elementList[endIndex] + this.range.setDefaultStyle({ + color: payload || undefined + }) + if (enterElement?.value === ZERO) { + if (payload) { + enterElement.color = payload + } else { + delete enterElement.color + } + } else { + isSubmitHistory = false + } + this.draw.render({ + isSubmitHistory, + curIndex: endIndex, + isCompute: false + }) + } + } + + public highlight(payload: string | null, options?: IRichtextOption) { + const { isIgnoreDisabledRule = false } = options || {} + const isDisabled = + !isIgnoreDisabledRule && + (this.draw.isReadonly() || this.draw.isDisabled()) + if (isDisabled) return + const selection = this.range.getSelectionElementList() + if (selection?.length) { + selection.forEach(el => { + if (payload) { + el.highlight = payload + } else { + delete el.highlight + } + }) + this.draw.render({ + isSetCursor: false, + isCompute: false + }) + } else { + let isSubmitHistory = true + const { endIndex } = this.range.getRange() + const elementList = this.draw.getElementList() + const enterElement = elementList[endIndex] + this.range.setDefaultStyle({ + highlight: payload || undefined + }) + if (enterElement?.value === ZERO) { + if (payload) { + enterElement.highlight = payload + } else { + delete enterElement.highlight + } + } else { + isSubmitHistory = false + } + this.draw.render({ + isSubmitHistory, + curIndex: endIndex, + isCompute: false + }) + } + } + + public title(payload: TitleLevel | null) { + const isDisabled = this.draw.isReadonly() || this.draw.isDisabled() + if (isDisabled) return + const { startIndex, endIndex } = this.range.getRange() + if (!~startIndex && !~endIndex) return + const elementList = this.draw.getElementList() + // 需要改变的元素列表 + const changeElementList = + startIndex === endIndex + ? this.range.getRangeParagraphElementList() + : elementList.slice(startIndex + 1, endIndex + 1) + if (!changeElementList || !changeElementList.length) return + // 设置值 + const titleId = getUUID() + const titleOptions = this.draw.getOptions().title + changeElementList.forEach(el => { + if (!el.type && el.value === ZERO) return + if (payload) { + el.level = payload + el.titleId = titleId + if (isTextLikeElement(el)) { + el.size = titleOptions[titleSizeMapping[payload]] + el.bold = true + } + } else { + if (el.titleId) { + delete el.titleId + delete el.title + delete el.level + delete el.size + delete el.bold + } + } + }) + // 光标定位 + const isSetCursor = startIndex === endIndex + const curIndex = isSetCursor ? endIndex : startIndex + this.draw.render({ curIndex, isSetCursor }) + } + + public list(listType: ListType | null, listStyle?: ListStyle) { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.draw.getListParticle().setList(listType, listStyle) + } + + public rowFlex(payload: RowFlex) { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + const { startIndex, endIndex } = this.range.getRange() + if (!~startIndex && !~endIndex) return + const rowElementList = this.range.getRangeRowElementList() + if (!rowElementList) return + rowElementList.forEach(element => { + element.rowFlex = payload + }) + // 光标定位 + const isSetCursor = startIndex === endIndex + const curIndex = isSetCursor ? endIndex : startIndex + this.draw.render({ curIndex, isSetCursor }) + } + + public rowMargin(payload: number) { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + const { startIndex, endIndex } = this.range.getRange() + if (!~startIndex && !~endIndex) return + const rowElementList = this.range.getRangeRowElementList() + if (!rowElementList) return + rowElementList.forEach(element => { + element.rowMargin = payload + }) + // 光标定位 + const isSetCursor = startIndex === endIndex + const curIndex = isSetCursor ? endIndex : startIndex + this.draw.render({ curIndex, isSetCursor }) + } + + public insertTable(row: number, col: number) { + const isDisabled = this.draw.isReadonly() || this.draw.isDisabled() + if (isDisabled) return + const activeControl = this.control.getActiveControl() + if (activeControl) return + this.tableOperate.insertTable(row, col) + } + + public insertTableTopRow() { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.tableOperate.insertTableTopRow() + } + + public insertTableBottomRow() { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.tableOperate.insertTableBottomRow() + } + + public insertTableLeftCol() { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.tableOperate.insertTableLeftCol() + } + + public insertTableRightCol() { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.tableOperate.insertTableRightCol() + } + + public deleteTableRow() { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.tableOperate.deleteTableRow() + } + + public deleteTableCol() { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.tableOperate.deleteTableCol() + } + + public deleteTable() { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.tableOperate.deleteTable() + } + + public mergeTableCell() { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.tableOperate.mergeTableCell() + } + + public cancelMergeTableCell() { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.tableOperate.cancelMergeTableCell() + } + + public splitVerticalTableCell() { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.tableOperate.splitVerticalTableCell() + } + + public splitHorizontalTableCell() { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.tableOperate.splitHorizontalTableCell() + } + + public tableTdVerticalAlign(payload: VerticalAlign) { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.tableOperate.tableTdVerticalAlign(payload) + } + + public tableBorderType(payload: TableBorder) { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.tableOperate.tableBorderType(payload) + } + + public tableBorderColor(payload: string) { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.tableOperate.tableBorderColor(payload) + } + + public tableTdBorderType(payload: TdBorder) { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.tableOperate.tableTdBorderType(payload) + } + + public tableTdSlashType(payload: TdSlash) { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.tableOperate.tableTdSlashType(payload) + } + + public tableTdBackgroundColor(payload: string) { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.tableOperate.tableTdBackgroundColor(payload) + } + + public tableSelectAll() { + this.tableOperate.tableSelectAll() + } + + public hyperlink(payload: IElement) { + const isDisabled = this.draw.isReadonly() || this.draw.isDisabled() + if (isDisabled) return + const activeControl = this.control.getActiveControl() + if (activeControl) return + const { startIndex, endIndex } = this.range.getRange() + if (!~startIndex && !~endIndex) return + const elementList = this.draw.getElementList() + const { valueList, url } = payload + const hyperlinkId = getUUID() + const newElementList = valueList?.map(v => ({ + url, + hyperlinkId, + value: v.value, + type: ElementType.HYPERLINK + })) + if (!newElementList) return + const start = startIndex + 1 + formatElementContext(elementList, newElementList, startIndex, { + editorOptions: this.options + }) + this.draw.spliceElementList( + elementList, + start, + startIndex === endIndex ? 0 : endIndex - startIndex, + newElementList + ) + const curIndex = start + newElementList.length - 1 + this.range.setRange(curIndex, curIndex) + this.draw.render({ curIndex }) + } + + public getHyperlinkRange(): [number, number] | null { + let leftIndex = -1 + let rightIndex = -1 + const { startIndex, endIndex } = this.range.getRange() + if (!~startIndex && !~endIndex) return null + const elementList = this.draw.getElementList() + const startElement = elementList[startIndex] + if (startElement.type !== ElementType.HYPERLINK) return null + // 向左查找 + let preIndex = startIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if (preElement.hyperlinkId !== startElement.hyperlinkId) { + leftIndex = preIndex + 1 + break + } + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if (nextElement.hyperlinkId !== startElement.hyperlinkId) { + rightIndex = nextIndex - 1 + break + } + nextIndex++ + } + // 控件在最后 + if (nextIndex === elementList.length) { + rightIndex = nextIndex - 1 + } + if (!~leftIndex || !~rightIndex) return null + return [leftIndex, rightIndex] + } + + public deleteHyperlink() { + const isDisabled = this.draw.isReadonly() || this.draw.isDisabled() + if (isDisabled) return + // 获取超链接索引 + const hyperRange = this.getHyperlinkRange() + if (!hyperRange) return + const elementList = this.draw.getElementList() + const [leftIndex, rightIndex] = hyperRange + // 删除元素 + this.draw.spliceElementList( + elementList, + leftIndex, + rightIndex - leftIndex + 1 + ) + this.draw.getHyperlinkParticle().clearHyperlinkPopup() + // 重置画布 + const newIndex = leftIndex - 1 + this.range.setRange(newIndex, newIndex) + this.draw.render({ + curIndex: newIndex + }) + } + + public cancelHyperlink() { + const isDisabled = this.draw.isReadonly() || this.draw.isDisabled() + if (isDisabled) return + // 获取超链接索引 + const hyperRange = this.getHyperlinkRange() + if (!hyperRange) return + const elementList = this.draw.getElementList() + const [leftIndex, rightIndex] = hyperRange + // 删除属性 + for (let i = leftIndex; i <= rightIndex; i++) { + const element = elementList[i] + delete element.type + delete element.url + delete element.hyperlinkId + delete element.underline + } + this.draw.getHyperlinkParticle().clearHyperlinkPopup() + // 重置画布 + const { endIndex } = this.range.getRange() + this.draw.render({ + curIndex: endIndex, + isCompute: false + }) + } + + public editHyperlink(payload: string) { + const isDisabled = this.draw.isReadonly() || this.draw.isDisabled() + if (isDisabled) return + // 获取超链接索引 + const hyperRange = this.getHyperlinkRange() + if (!hyperRange) return + const elementList = this.draw.getElementList() + const [leftIndex, rightIndex] = hyperRange + // 替换url + for (let i = leftIndex; i <= rightIndex; i++) { + const element = elementList[i] + element.url = payload + } + this.draw.getHyperlinkParticle().clearHyperlinkPopup() + // 重置画布 + const { endIndex } = this.range.getRange() + this.draw.render({ + curIndex: endIndex, + isCompute: false + }) + } + + public separator(payload: number[]) { + const isDisabled = this.draw.isReadonly() || this.draw.isDisabled() + if (isDisabled) return + const activeControl = this.control.getActiveControl() + if (activeControl) return + const { startIndex, endIndex } = this.range.getRange() + if (!~startIndex && !~endIndex) return + const elementList = this.draw.getElementList() + let curIndex = -1 + // 光标存在分割线,则判断为修改线段逻辑 + const endElement = elementList[endIndex + 1] + if (endElement && endElement.type === ElementType.SEPARATOR) { + if ( + endElement.dashArray && + endElement.dashArray.join() === payload.join() + ) { + return + } + curIndex = endIndex + endElement.dashArray = payload + } else { + const newElement: IElement = { + value: WRAP, + type: ElementType.SEPARATOR, + dashArray: payload + } + // 从行头增加分割线 + formatElementContext(elementList, [newElement], startIndex, { + editorOptions: this.options + }) + if (startIndex !== 0 && elementList[startIndex].value === ZERO) { + this.draw.spliceElementList(elementList, startIndex, 1, [newElement]) + curIndex = startIndex - 1 + } else { + this.draw.spliceElementList(elementList, startIndex + 1, 0, [ + newElement + ]) + curIndex = startIndex + } + } + this.range.setRange(curIndex, curIndex) + this.draw.render({ curIndex }) + } + + public pageBreak() { + const isDisabled = this.draw.isReadonly() || this.draw.isDisabled() + if (isDisabled) return + const activeControl = this.control.getActiveControl() + if (activeControl) return + this.insertElementList([ + { + type: ElementType.PAGE_BREAK, + value: WRAP + } + ]) + } + + public addWatermark(payload: IWatermark) { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + const options = this.draw.getOptions() + const { color, size, opacity, font, gap } = defaultWatermarkOption + options.watermark.data = payload.data + options.watermark.type = payload.type || WatermarkType.TEXT + if (payload.width) { + options.watermark.width = payload.width + } + if (payload.height) { + options.watermark.height = payload.height + } + options.watermark.color = payload.color || color + options.watermark.opacity = payload.opacity || opacity + options.watermark.size = payload.size || size + options.watermark.font = payload.font || font + options.watermark.repeat = !!payload.repeat + if (payload.numberType) { + options.watermark.numberType = payload.numberType + } + options.watermark.gap = payload.gap || gap + this.draw.render({ + isSetCursor: false, + isSubmitHistory: false, + isCompute: false + }) + } + + public deleteWatermark() { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + const options = this.draw.getOptions() + if (options.watermark && options.watermark.data) { + options.watermark = { ...defaultWatermarkOption } + this.draw.render({ + isSetCursor: false, + isSubmitHistory: false, + isCompute: false + }) + } + } + + public image(payload: IDrawImagePayload): string | null { + const isDisabled = this.draw.isReadonly() || this.draw.isDisabled() + if (isDisabled) return null + const { startIndex, endIndex } = this.range.getRange() + if (!~startIndex && !~endIndex) return null + const imageId = payload.id || getUUID() + this.insertElementList([ + { + ...payload, + id: imageId, + type: ElementType.IMAGE + } + ]) + return imageId + } + + public search(payload: string | null) { + this.searchManager.setSearchKeyword(payload) + this.draw.render({ + isSetCursor: false, + isSubmitHistory: false + }) + } + + public searchNavigatePre() { + const index = this.searchManager.searchNavigatePre() + if (index === null) return + this.draw.render({ + isSetCursor: false, + isSubmitHistory: false, + isCompute: false, + isLazy: false + }) + } + + public searchNavigateNext() { + const index = this.searchManager.searchNavigateNext() + if (index === null) return + this.draw.render({ + isSetCursor: false, + isSubmitHistory: false, + isCompute: false, + isLazy: false + }) + } + + public getSearchNavigateInfo(): null | INavigateInfo { + return this.searchManager.getSearchNavigateInfo() + } + + public replace(payload: string, option?: IReplaceOption) { + this.draw.getSearch().replace(payload, option) + } + + public async print() { + const { scale, printPixelRatio, paperDirection, width, height } = + this.options + if (scale !== 1) { + this.draw.setPageScale(1) + } + const base64List = await this.draw.getDataURL({ + pixelRatio: printPixelRatio, + mode: EditorMode.PRINT + }) + printImageBase64(base64List, { + width, + height, + direction: paperDirection + }) + if (scale !== 1) { + this.draw.setPageScale(scale) + } + } + + public replaceImageElement(payload: string) { + const { startIndex } = this.range.getRange() + const elementList = this.draw.getElementList() + const element = elementList[startIndex] + if (!element || element.type !== ElementType.IMAGE) return + element.value = payload + this.draw.render({ + isSetCursor: false + }) + } + + public saveAsImageElement() { + const { startIndex } = this.range.getRange() + const elementList = this.draw.getElementList() + const element = elementList[startIndex] + if (!element || element.type !== ElementType.IMAGE) return + downloadFile(element.value, `${element.id!}.png`) + } + + public changeImageDisplay(element: IElement, display: ImageDisplay) { + if (element.imgDisplay === display) return + element.imgDisplay = display + const { startIndex, endIndex } = this.range.getRange() + if ( + display === ImageDisplay.SURROUND || + display === ImageDisplay.FLOAT_TOP || + display === ImageDisplay.FLOAT_BOTTOM + ) { + const positionList = this.position.getPositionList() + const { + pageNo, + coordinate: { leftTop } + } = positionList[startIndex] + element.imgFloatPosition = { + pageNo, + x: leftTop[0], + y: leftTop[1] + } + } else { + delete element.imgFloatPosition + } + this.draw.getPreviewer().clearResizer() + this.draw.render({ + isSetCursor: true, + curIndex: endIndex + }) + } + + public getImage(payload?: IGetImageOption): Promise { + return this.draw.getDataURL(payload) + } + + public getOptions(): DeepRequired { + return this.options + } + + public getValue(options?: IGetValueOption): IEditorResult { + return this.draw.getValue(options) + } + + public getValueAsync(options?: IGetValueOption): Promise { + return this.draw.getWorkerManager().getValue(options) + } + + public getAreaValue( + options?: IGetAreaValueOption + ): IGetAreaValueResult | null { + return this.draw.getArea().getAreaValue(options) + } + + public getHTML(): IEditorHTML { + const options = this.options + const headerElementList = this.draw.getHeaderElementList() + const mainElementList = this.draw.getOriginalMainElementList() + const footerElementList = this.draw.getFooterElementList() + return { + header: createDomFromElementList(headerElementList, options).innerHTML, + main: createDomFromElementList(mainElementList, options).innerHTML, + footer: createDomFromElementList(footerElementList, options).innerHTML + } + } + + public getText(): IEditorText { + const headerElementList = this.draw.getHeaderElementList() + const mainElementList = this.draw.getOriginalMainElementList() + const footerElementList = this.draw.getFooterElementList() + return { + header: getTextFromElementList(headerElementList), + main: getTextFromElementList(mainElementList), + footer: getTextFromElementList(footerElementList) + } + } + + public getWordCount(): Promise { + return this.workerManager.getWordCount() + } + + public getCursorPosition(): IElementPosition | null { + return this.position.getCursorPosition() + } + + public getRange(): IRange { + return deepClone(this.range.getRange()) + } + + public getRangeText(): string { + return this.range.toString() + } + + public getRangeContext(): RangeContext | null { + const range = this.range.getRange() + const { startIndex, endIndex } = range + if (!~startIndex && !~endIndex) return null + // 选区信息 + const isCollapsed = startIndex === endIndex + const selectionText = this.range.toString() + const selectionElementList = zipElementList( + this.range.getSelectionElementList() || [] + ) + // 元素信息 + const elementList = this.draw.getElementList() + const startElement = pickElementAttr( + elementList[isCollapsed ? startIndex : startIndex + 1], + { + extraPickAttrs: ['id', 'controlComponent'] + } + ) + const endElement = pickElementAttr(elementList[endIndex], { + extraPickAttrs: ['id', 'controlComponent'] + }) + // 页码信息、行信息 + const rowList = this.draw.getRowList() + const positionList = this.position.getPositionList() + const startPosition = positionList[startIndex] + const endPosition = positionList[endIndex] + const startPageNo = startPosition.pageNo + const endPageNo = endPosition.pageNo + const startRowNo = startPosition.rowIndex + const endRowNo = endPosition.rowIndex + // 列信息 + const startRow = rowList[startRowNo] + const endRow = rowList[endRowNo] + let startColNo = 0 + let endColNo = 0 + // 以光标显示位置为准 + if (!this.draw.getCursor().getHitLineStartIndex()) { + // 换行符不计算列数量 + startColNo = + startRow.elementList[0]?.value === ZERO + ? startPosition.index! - startRow.startIndex + : startPosition.index! - startRow.startIndex + 1 + } + // 光标闭合时列位置相同 + if (startPosition === endPosition) { + endColNo = startColNo + } else { + endColNo = + endRow.elementList[0]?.value === ZERO + ? endPosition.index! - endRow.startIndex + : endPosition.index! - endRow.startIndex + 1 + } + + // 坐标信息(相对编辑器书写区) + const rangeRects: RangeRect[] = [] + const height = this.draw.getOriginalHeight() + const pageGap = this.draw.getOriginalPageGap() + const selectionPositionList = this.position.getSelectionPositionList() + if (selectionPositionList) { + // 起始信息及x坐标 + let currentRowNo: number | null = null + let currentX = 0 + let rangeRect: RangeRect | null = null + for (let p = 0; p < selectionPositionList.length; p++) { + const { + rowNo, + pageNo, + coordinate: { leftTop, rightTop }, + lineHeight + } = selectionPositionList[p] + // 起始行变化追加选区信息 + if (currentRowNo === null || currentRowNo !== rowNo) { + if (rangeRect) { + rangeRects.push(rangeRect) + } + rangeRect = { + x: leftTop[0], + y: leftTop[1] + pageNo * (height + pageGap), + width: rightTop[0] - leftTop[0], + height: lineHeight + } + currentRowNo = rowNo + currentX = leftTop[0] + } else { + rangeRect!.width = rightTop[0] - currentX + } + // 最后一个元素结束追加选区信息 + if (p === selectionPositionList.length - 1 && rangeRect) { + rangeRects.push(rangeRect) + } + } + } else { + const positionList = this.position.getPositionList() + const position = positionList[endIndex] + const { + coordinate: { rightTop }, + pageNo, + lineHeight + } = position + rangeRects.push({ + x: rightTop[0], + y: rightTop[1] + pageNo * (height + pageGap), + width: 0, + height: lineHeight + }) + } + // 区域信息 + const zone = this.draw.getZone().getZone() + // 表格信息 + const { isTable, trIndex, tdIndex, index } = + this.position.getPositionContext() + let tableElement: IElement | null = null + if (isTable) { + const originalElementList = this.draw.getOriginalElementList() + const originTableElement = originalElementList[index!] || null + if (originTableElement) { + tableElement = zipElementList([originTableElement])[0] + } + } + // 标题信息 + let titleId: string | null = null + let titleStartPageNo: number | null = null + let start = startIndex - 1 + while (start > 0) { + const curElement = elementList[start] + const preElement = elementList[start - 1] + if (curElement.titleId && curElement.titleId !== preElement?.titleId) { + titleId = curElement.titleId + titleStartPageNo = positionList[start].pageNo + break + } + start-- + } + return deepClone({ + isCollapsed, + startElement, + endElement, + startPageNo, + endPageNo, + startRowNo, + endRowNo, + startColNo, + endColNo, + rangeRects, + zone, + isTable, + trIndex: trIndex ?? null, + tdIndex: tdIndex ?? null, + tableElement, + selectionText, + selectionElementList, + titleId, + titleStartPageNo + }) + } + + public getRangeRow(): IElement[] | null { + const rowElementList = this.range.getRangeRowElementList() + return rowElementList ? zipElementList(rowElementList) : null + } + + public getRangeParagraph(): IElement[] | null { + const paragraphElementList = this.range.getRangeParagraphElementList() + return paragraphElementList ? zipElementList(paragraphElementList) : null + } + + public getKeywordRangeList(payload: string): IRange[] { + return this.range.getKeywordRangeList(payload) + } + + public getKeywordContext(payload: string): ISearchResultContext[] | null { + const rangeList = this.getKeywordRangeList(payload) + if (!rangeList.length) return null + const searchResultContextList: ISearchResultContext[] = [] + const positionList = this.position.getOriginalMainPositionList() + const elementList = this.draw.getOriginalMainElementList() + for (let r = 0; r < rangeList.length; r++) { + const range = rangeList[r] + const { startIndex, endIndex, tableId, startTrIndex, startTdIndex } = + range + let keywordPositionList: IElementPosition[] = positionList + if (range.tableId) { + const tableElement = elementList.find(el => el.id === tableId) + if (tableElement) { + keywordPositionList = + tableElement.trList?.[startTrIndex!]?.tdList?.[startTdIndex!] + ?.positionList || [] + } + } + // 获取关键词始末位置 + const startPosition = deepClone(keywordPositionList[startIndex]) + const endPosition = deepClone(keywordPositionList[endIndex]) + searchResultContextList.push({ + range, + startPosition, + endPosition + }) + } + return searchResultContextList + } + + public pageMode(payload: PageMode) { + this.draw.setPageMode(payload) + } + + public pageScale(scale: number) { + if (scale === this.options.scale) return + this.draw.setPageScale(scale) + } + + public pageScaleRecovery() { + const { scale } = this.options + if (scale !== 1) { + this.draw.setPageScale(1) + } + } + + public pageScaleMinus() { + const { scale } = this.options + const nextScale = scale * 10 - 1 + if (nextScale >= 5) { + this.draw.setPageScale(nextScale / 10) + } + } + + public pageScaleAdd() { + const { scale } = this.options + const nextScale = scale * 10 + 1 + if (nextScale <= 30) { + this.draw.setPageScale(nextScale / 10) + } + } + + public paperSize(width: number, height: number) { + this.draw.setPaperSize(width, height) + } + + public paperDirection(payload: PaperDirection) { + this.draw.setPaperDirection(payload) + } + + public getPaperMargin(): number[] { + return this.options.margins + } + + public setPaperMargin(payload: IMargin) { + return this.draw.setPaperMargin(payload) + } + + public setMainBadge(payload: IBadge | null) { + this.draw.getBadge().setMainBadge(payload) + this.draw.render({ + isCompute: false, + isSubmitHistory: false + }) + } + + public setAreaBadge(payload: IAreaBadge[]) { + this.draw.getBadge().setAreaBadgeMap(payload) + this.draw.render({ + isCompute: false, + isSubmitHistory: false + }) + } + + public insertElementList( + payload: IElement[], + options: IInsertElementListOption = {} + ) { + if (!payload.length) return + const isDisabled = this.draw.isReadonly() || this.draw.isDisabled() + if (isDisabled) return + const { isReplace = true } = options + // 如果配置不替换时,需收缩选区至末尾 + if (!isReplace) { + this.range.shrinkRange() + } + const cloneElementList = deepClone(payload) + // 格式化上下文信息 + const { startIndex } = this.range.getRange() + const elementList = this.draw.getElementList() + formatElementContext(elementList, cloneElementList, startIndex, { + isBreakWhenWrap: true, + editorOptions: this.options + }) + this.draw.insertElementList(cloneElementList, options) + } + + public appendElementList( + elementList: IElement[], + options?: IAppendElementListOption + ) { + if (!elementList.length) return + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.draw.appendElementList(deepClone(elementList), options) + } + + public updateElementById(payload: IUpdateElementByIdOption) { + const { id, conceptId } = payload + if (!id && !conceptId) return + const updateElementInfoList: { + elementList: IElement[] + index: number + }[] = [] + function getElementInfoById(elementList: IElement[]) { + let i = 0 + while (i < elementList.length) { + const element = elementList[i] + i++ + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + getElementInfoById(td.value) + } + } + } + if ( + (id && element.id === id) || + (conceptId && element.conceptId === conceptId) + ) { + updateElementInfoList.push({ + elementList, + index: i - 1 + }) + } + } + } + // 优先正文再页眉页脚 + const data = [ + this.draw.getOriginalMainElementList(), + this.draw.getHeaderElementList(), + this.draw.getFooterElementList() + ] + for (const elementList of data) { + getElementInfoById(elementList) + } + // 更新内容 + if (!updateElementInfoList.length) return + for (let i = 0; i < updateElementInfoList.length; i++) { + const { elementList, index } = updateElementInfoList[i] + // 重新格式化元素 + const oldElement = elementList[index] + const newElement = zipElementList( + [ + { + ...oldElement, + ...payload.properties + } + ], + { + extraPickAttrs: ['id'] + } + ) + // 区域上下文提取 + cloneProperty(AREA_CONTEXT_ATTR, oldElement, newElement[0]) + formatElementList(newElement, { + isHandleFirstElement: false, + editorOptions: this.options + }) + elementList[index] = newElement[0] + } + this.draw.render({ + isSetCursor: false + }) + } + + public deleteElementById(payload: IDeleteElementByIdOption) { + const { id, conceptId } = payload + if (!id && !conceptId) return + let isExistDelete = false + function deleteElement(elementList: IElement[]) { + let i = 0 + while (i < elementList.length) { + const element = elementList[i] + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + deleteElement(td.value) + } + } + } + if ( + (id && element.id === id) || + (conceptId && element.conceptId === conceptId) + ) { + isExistDelete = true + elementList.splice(i, 1) + i-- + } + i++ + } + } + // 优先正文再页眉页脚 + const data = [ + this.draw.getOriginalMainElementList(), + this.draw.getHeaderElementList(), + this.draw.getFooterElementList() + ] + for (const elementList of data) { + deleteElement(elementList) + } + if (!isExistDelete) return + this.draw.render({ + isSetCursor: false + }) + } + + public getElementById(payload: IGetElementByIdOption): IElement[] { + const { id, conceptId } = payload + const result: IElement[] = [] + if (!id && !conceptId) return result + const getElement = (elementList: IElement[]) => { + let i = 0 + while (i < elementList.length) { + const element = elementList[i] + i++ + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + getElement(td.value) + } + } + } + if ( + (id && element.id !== id) || + (conceptId && element.conceptId !== conceptId) + ) { + continue + } + result.push(element) + } + } + const data = [ + this.draw.getHeaderElementList(), + this.draw.getOriginalMainElementList(), + this.draw.getFooterElementList() + ] + for (const elementList of data) { + getElement(elementList) + } + return zipElementList(result, { + extraPickAttrs: ['id'] + }) + } + + public setValue(payload: Partial, options?: ISetValueOption) { + this.draw.setValue(payload, options) + } + + public removeControl(payload?: IRemoveControlOption) { + if (payload?.id || payload?.conceptId) { + const { id, conceptId } = payload + let isExistRemove = false + const remove = (elementList: IElement[]) => { + let i = elementList.length - 1 + while (i >= 0) { + const element = elementList[i] + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + remove(td.value) + } + } + } + i-- + if ( + !element.control || + (id && element.controlId !== id) || + (conceptId && element.control.conceptId !== conceptId) + ) { + continue + } + isExistRemove = true + elementList.splice(i + 1, 1) + } + } + const data = [ + this.draw.getHeaderElementList(), + this.draw.getOriginalMainElementList(), + this.draw.getFooterElementList() + ] + for (const elementList of data) { + remove(elementList) + } + if (isExistRemove) { + this.draw.render({ + isSetCursor: false + }) + } + } else { + const { startIndex, endIndex } = this.range.getRange() + if (startIndex !== endIndex) return + const elementList = this.draw.getElementList() + const element = elementList[startIndex] + if (!element.controlId) return + // 删除控件 + const control = this.draw.getControl() + const newIndex = control.removeControl(startIndex) + if (newIndex === null) return + // 重新渲染 + this.range.setRange(newIndex, newIndex) + this.draw.render({ + curIndex: newIndex + }) + } + } + + public translate(path: string): string { + return this.i18n.t(path) + } + + public setLocale(payload: string) { + this.i18n.setLocale(payload) + } + + public getLocale(): string { + return this.i18n.getLocale() + } + + public getCatalog(): Promise { + return this.workerManager.getCatalog() + } + + public locationCatalog(titleId: string) { + const elementList = this.draw.getOriginalElementList() + + function getPosition( + elementList: IElement[], + titleId: string + ): (IRange & IPositionContext) | null { + for (let e = 0; e < elementList.length; e++) { + const element = elementList[e] + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const range = getPosition(td.value, titleId) + if (range) { + return { + ...range, + isTable: true, + index: e, + trIndex: r, + tdIndex: d, + tdId: td.id, + trId: tr.id, + tableId: element.id + } + } + } + } + } + // 找到标题末尾 + if (element.titleId === titleId) { + let newIndex = e + while (newIndex < elementList.length) { + if (elementList[newIndex + 1]?.titleId !== titleId) { + return { + isTable: false, + startIndex: newIndex, + endIndex: newIndex + } + } + newIndex++ + } + } + } + return null + } + + const context = getPosition(elementList, titleId) + if (!context) return + const { + isTable, + index, + startTdIndex, + endTdIndex, + startTrIndex, + endTrIndex, + trIndex, + tdIndex, + tdId, + trId, + tableId, + endIndex + } = context + this.position.setPositionContext({ + isTable, + index, + trIndex, + tdIndex, + tdId, + trId, + tableId + }) + this.range.setRange( + endIndex, + endIndex, + tableId, + startTdIndex, + endTdIndex, + startTrIndex, + endTrIndex + ) + this.draw.render({ + curIndex: endIndex, + isCompute: false, + isSubmitHistory: false + }) + } + + public wordTool() { + const elementList = this.draw.getMainElementList() + let isApply = false + for (let i = 0; i < elementList.length; i++) { + const element = elementList[i] + // 删除空行、行首空格 + if (element.value === ZERO) { + while (i + 1 < elementList.length) { + const nextElement = elementList[i + 1] + if (nextElement.value !== ZERO && nextElement.value !== NBSP) break + elementList.splice(i + 1, 1) + isApply = true + } + } + } + if (!isApply) { + // 避免输入框光标丢失 + const isCollapsed = this.range.getIsCollapsed() + this.draw.getCursor().drawCursor({ + isShow: isCollapsed + }) + } else { + this.draw.render({ + isSetCursor: false + }) + } + } + + public setHTML(payload: Partial) { + const { header, main, footer } = payload + const innerWidth = this.draw.getOriginalInnerWidth() + // 不设置值时数据为undefined,避免覆盖当前数据 + const getElementList = (htmlText?: string) => + htmlText !== undefined + ? getElementListByHTML(htmlText, { + innerWidth + }) + : undefined + this.setValue({ + header: getElementList(header), + main: getElementList(main), + footer: getElementList(footer) + }) + } + + public setGroup(): string | null { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return null + return this.draw.getGroup().setGroup() + } + + public deleteGroup(groupId: string) { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.draw.getGroup().deleteGroup(groupId) + } + + public getGroupIds(): Promise { + return this.draw.getWorkerManager().getGroupIds() + } + + public locationGroup(groupId: string) { + const elementList = this.draw.getOriginalMainElementList() + const context = this.draw + .getGroup() + .getContextByGroupId(elementList, groupId) + if (!context) return + const { isTable, index, trIndex, tdIndex, tdId, trId, tableId, endIndex } = + context + this.position.setPositionContext({ + isTable, + index, + trIndex, + tdIndex, + tdId, + trId, + tableId + }) + this.range.setRange(endIndex, endIndex) + this.draw.render({ + curIndex: endIndex, + isCompute: false, + isSubmitHistory: false + }) + } + + public setZone(zone: EditorZone) { + this.draw.getZone().setZone(zone) + } + + public getControlValue( + payload: IGetControlValueOption + ): IGetControlValueResult | null { + return this.draw.getControl().getValueById(payload) + } + + public setControlValue(payload: ISetControlValueOption) { + this.draw.getControl().setValueListById([payload]) + } + + public setControlValueList(payload: ISetControlValueOption[]) { + this.draw.getControl().setValueListById(payload) + } + + public setControlExtension(payload: ISetControlExtensionOption) { + this.draw.getControl().setExtensionListById([payload]) + } + + public setControlExtensionList(payload: ISetControlExtensionOption[]) { + this.draw.getControl().setExtensionListById(payload) + } + + public setControlProperties(payload: ISetControlProperties) { + this.draw.getControl().setPropertiesListById([payload]) + } + + public setControlPropertiesList(payload: ISetControlProperties[]) { + this.draw.getControl().setPropertiesListById(payload) + } + + public setControlHighlight(payload: ISetControlHighlightOption) { + this.draw.getControl().setHighlightList(payload) + this.draw.render({ + isSubmitHistory: false + }) + } + + public updateOptions(payload: IUpdateOption) { + const newOption = mergeOption(payload) + Object.entries(newOption).forEach(([key, value]) => { + Reflect.set(this.options, key, value) + }) + this.forceUpdate() + } + + public getControlList(): IElement[] { + return this.draw.getControl().getList() + } + + public locationControl(controlId: string, options?: ILocationControlOption) { + function location( + elementList: IElement[], + zone: EditorZone + ): ILocationPosition | null { + let i = 0 + while (i < elementList.length) { + const element = elementList[i] + i++ + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const locationContext = location(td.value, zone) + if (locationContext) { + return { + ...locationContext, + positionContext: { + isTable: true, + index: i - 1, + trIndex: r, + tdIndex: d, + tdId: element.tdId, + trId: element.trId, + tableId: element.tableId + } + } + } + } + } + } + if (element?.controlId !== controlId) continue + let curIndex = i - 1 + if (options?.position === LocationPosition.OUTER_AFTER) { + // 控件外面最后 + if ( + !( + element.controlComponent === ControlComponent.POSTFIX && + elementList[i + 1]?.controlComponent !== + ControlComponent.POST_TEXT + ) + ) { + continue + } + } else if (options?.position === LocationPosition.OUTER_BEFORE) { + // 控件外面最前 + curIndex -= 1 + } else if (options?.position === LocationPosition.AFTER) { + // 控件内部最后 + curIndex -= 1 + if ( + element.controlComponent !== ControlComponent.PLACEHOLDER && + element.controlComponent !== ControlComponent.POSTFIX && + element.controlComponent !== ControlComponent.POST_TEXT + ) { + continue + } + } else { + // 控件内部最前(默认) + if ( + (element.controlComponent !== ControlComponent.PREFIX && + element.controlComponent !== ControlComponent.PRE_TEXT) || + elementList[i]?.controlComponent === ControlComponent.PREFIX || + elementList[i]?.controlComponent === ControlComponent.PRE_TEXT + ) { + continue + } + } + return { + zone, + range: { + startIndex: curIndex, + endIndex: curIndex + }, + positionContext: { + isTable: false + } + } + } + return null + } + const data = [ + { + zone: EditorZone.HEADER, + elementList: this.draw.getHeaderElementList() + }, + { + zone: EditorZone.MAIN, + elementList: this.draw.getOriginalMainElementList() + }, + { + zone: EditorZone.FOOTER, + elementList: this.draw.getFooterElementList() + } + ] + for (const context of data) { + const locationContext = location(context.elementList, context.zone) + if (locationContext) { + // 设置区域、上下文、光标信息 + this.setZone(locationContext.zone) + this.position.setPositionContext(locationContext.positionContext) + this.range.replaceRange(locationContext.range) + this.draw.render({ + curIndex: locationContext.range.startIndex, + isCompute: false, + isSubmitHistory: false + }) + break + } + } + } + + public insertControl(payload: IElement) { + const isDisabled = this.draw.isReadonly() || this.draw.isDisabled() + if (isDisabled) return + const cloneElement = deepClone(payload) + // 格式化上下文信息 + const { startIndex } = this.range.getRange() + const elementList = this.draw.getElementList() + const copyElement = getAnchorElement(elementList, startIndex) + if (!copyElement) return + const cloneAttr = [ + ...TABLE_CONTEXT_ATTR, + ...EDITOR_ROW_ATTR, + ...LIST_CONTEXT_ATTR, + ...AREA_CONTEXT_ATTR + ] + cloneProperty(cloneAttr, copyElement, cloneElement) + // 插入控件 + this.draw.insertElementList([cloneElement]) + } + + public getContainer(): HTMLDivElement { + return this.draw.getContainer() + } + + public getTitleValue( + payload: IGetTitleValueOption + ): IGetTitleValueResult | null { + const { conceptId } = payload + const result: IGetTitleValueResult = [] + const getValue = (elementList: IElement[], zone: EditorZone) => { + let i = 0 + while (i < elementList.length) { + const element = elementList[i] + i++ + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + getValue(td.value, zone) + } + } + } + if (element?.title?.conceptId !== conceptId) continue + // 先查找到标题,后循环至同级或上级标题处停止 + const valueList: IElement[] = [] + let j = i + while (j < elementList.length) { + const nextElement = elementList[j] + j++ + if (element.titleId === nextElement.titleId) continue + if ( + nextElement.level && + titleOrderNumberMapping[nextElement.level] <= + titleOrderNumberMapping[element.level!] + ) { + break + } + valueList.push(nextElement) + } + result.push({ + ...element.title!, + value: getTextFromElementList(valueList), + elementList: zipElementList(valueList), + zone + }) + i = j + } + } + const data = [ + { + zone: EditorZone.HEADER, + elementList: this.draw.getHeaderElementList() + }, + { + zone: EditorZone.MAIN, + elementList: this.draw.getOriginalMainElementList() + }, + { + zone: EditorZone.FOOTER, + elementList: this.draw.getFooterElementList() + } + ] + for (const { zone, elementList } of data) { + getValue(elementList, zone) + } + return result + } + + public getPositionContextByEvent( + evt: MouseEvent, + options: IPositionContextByEventOption = {} + ): IPositionContextByEventResult | null { + const pageIndex = (evt.target)?.dataset.index + if (!pageIndex) return null + const { isMustDirectHit = true } = options + const pageNo = Number(pageIndex) + const positionContext = this.position.getPositionByXY({ + x: evt.offsetX, + y: evt.offsetY, + pageNo + }) + const { + isDirectHit, + isTable, + index, + trIndex, + tdIndex, + tdValueIndex, + zone + } = positionContext + // 非直接命中或选区不一致时返回空值 + if ( + (isMustDirectHit && !isDirectHit) || + (zone && zone !== this.zone.getZone()) + ) { + return null + } + // 命中元素信息 + let tableInfo: ITableInfoByEvent | null = null + let element: IElement | null = null + const elementList = this.draw.getOriginalElementList() + let position: IElementPosition | null = null + const positionList = this.position.getOriginalPositionList() + if (isTable) { + const td = elementList[index!].trList?.[trIndex!].tdList[tdIndex!] + element = td?.value[tdValueIndex!] || null + position = td?.positionList?.[tdValueIndex!] || null + tableInfo = { + element: elementList[index!], + trIndex: trIndex!, + tdIndex: tdIndex! + } + } else { + element = elementList[index] || null + position = positionList[index] || null + } + // 元素包围信息 + let rangeRect: RangeRect | null = null + if (position) { + const { + pageNo, + coordinate: { leftTop, rightTop }, + lineHeight + } = position + const height = this.draw.getOriginalHeight() + const pageGap = this.draw.getOriginalPageGap() + rangeRect = { + x: leftTop[0], + y: leftTop[1] + pageNo * (height + pageGap), + width: rightTop[0] - leftTop[0], + height: lineHeight + } + } + return { + pageNo, + element, + rangeRect, + tableInfo + } + } + + public insertTitle(payload: IElement) { + const isDisabled = this.draw.isReadonly() || this.draw.isDisabled() + if (isDisabled) return + const cloneElement = deepClone(payload) + // 格式化上下文信息 + const { startIndex } = this.range.getRange() + const elementList = this.draw.getElementList() + const copyElement = getAnchorElement(elementList, startIndex) + if (!copyElement) return + const cloneAttr = [ + ...TABLE_CONTEXT_ATTR, + ...EDITOR_ROW_ATTR, + ...LIST_CONTEXT_ATTR, + ...AREA_CONTEXT_ATTR + ] + cloneElement.valueList?.forEach(valueItem => { + cloneProperty(cloneAttr, copyElement, valueItem) + }) + // 插入标题 + this.draw.insertElementList([cloneElement]) + } + + public focus(payload?: IFocusOption) { + const { + position = LocationPosition.AFTER, + isMoveCursorToVisible = true, + rowNo, + range + } = payload || {} + let curIndex = -1 + if (range) { + // 根据选区定位 + this.range.replaceRange(range) + curIndex = + position === LocationPosition.BEFORE ? range.startIndex : range.endIndex + } else if (isNumber(rowNo)) { + // 根据行号定位 + const rowList = this.draw.getOriginalRowList() + curIndex = + position === LocationPosition.BEFORE + ? rowList[rowNo]?.startIndex + : rowList[rowNo + 1]?.startIndex - 1 + if (!isNumber(curIndex)) return + this.range.setRange(curIndex, curIndex) + } else { + // 默认文档首尾 + curIndex = + position === LocationPosition.BEFORE + ? 0 + : this.draw.getOriginalMainElementList().length - 1 + this.range.setRange(curIndex, curIndex) + } + // 光标存在且闭合时定位 + const renderParams: IDrawOption = { + isCompute: false, + isSetCursor: false, + isSubmitHistory: false + } + if (~curIndex && this.range.getIsCollapsed()) { + renderParams.curIndex = curIndex + renderParams.isSetCursor = true + } + this.draw.render(renderParams) + // 移动滚动条到可见区域 + if (isMoveCursorToVisible) { + const positionList = this.draw.getPosition().getPositionList() + this.draw.getCursor().moveCursorToVisible({ + cursorPosition: positionList[curIndex], + direction: MoveDirection.DOWN + }) + } + } + + public insertArea(payload: IInsertAreaOption) { + return this.draw.getArea().insertArea(payload) + } + + public setAreaValue(payload: ISetAreaValueOption) { + return this.draw.getArea().setAreaValue(payload) + } + + public setAreaProperties(payload: ISetAreaPropertiesOption) { + this.draw.getArea().setAreaProperties(payload) + } + + public locationArea(areaId: string, options?: ILocationAreaOption) { + // 区域在最后时,如果后面没有元素是否追加换行符 + if ( + options?.isAppendLastLineBreak && + options?.position === LocationPosition.OUTER_AFTER + ) { + const elementList = this.draw.getOriginalMainElementList() + if (elementList[elementList.length - 1].areaId === areaId) { + this.draw.appendElementList( + [ + { + value: ZERO + } + ], + { + isSubmitHistory: false + } + ) + } + } + // 获取区域位置 + const context = this.draw.getArea().getContextByAreaId(areaId, options) + if (!context) return + const { + range: { endIndex }, + elementPosition + } = context + this.position.setPositionContext({ + isTable: false + }) + this.range.setRange(endIndex, endIndex) + this.draw.render({ + curIndex: endIndex, + isSetCursor: true, + isCompute: false, + isSubmitHistory: false + }) + // 移动到可见区域 + const cursor = this.draw.getCursor() + this.position.setCursorPosition(elementPosition) + cursor.moveCursorToVisible({ + cursorPosition: elementPosition, + direction: MoveDirection.UP + }) + } +} diff --git a/src/editor/core/contextmenu/ContextMenu.ts b/src/editor/core/contextmenu/ContextMenu.ts new file mode 100644 index 0000000..9d862d6 --- /dev/null +++ b/src/editor/core/contextmenu/ContextMenu.ts @@ -0,0 +1,363 @@ +import { NAME_PLACEHOLDER } from '../../dataset/constant/ContextMenu' +import { EDITOR_COMPONENT, EDITOR_PREFIX } from '../../dataset/constant/Editor' +import { EditorComponent } from '../../dataset/enum/Editor' +import { DeepRequired } from '../../interface/Common' +import { IEditorOption } from '../../interface/Editor' +import { IElement } from '../../interface/Element' +import { + IContextMenuContext, + IRegisterContextMenu +} from '../../interface/contextmenu/ContextMenu' +import { findParent } from '../../utils' +import { zipElementList } from '../../utils/element' +import { Command } from '../command/Command' +import { Draw } from '../draw/Draw' +import { I18n } from '../i18n/I18n' +import { Position } from '../position/Position' +import { RangeManager } from '../range/RangeManager' +import { controlMenus } from './menus/controlMenus' +import { globalMenus } from './menus/globalMenus' +import { hyperlinkMenus } from './menus/hyperlinkMenus' +import { imageMenus } from './menus/imageMenus' +import { tableMenus } from './menus/tableMenus' + +interface IRenderPayload { + contextMenuList: IRegisterContextMenu[] + left: number + top: number + parentMenuContainer?: HTMLDivElement +} + +export class ContextMenu { + private options: DeepRequired + private draw: Draw + private command: Command + private range: RangeManager + private position: Position + private i18n: I18n + private container: HTMLDivElement + private contextMenuList: IRegisterContextMenu[] + private contextMenuContainerList: HTMLDivElement[] + private contextMenuRelationShip: Map + private context: IContextMenuContext | null + + constructor(draw: Draw, command: Command) { + this.options = draw.getOptions() + this.draw = draw + this.command = command + this.range = draw.getRange() + this.position = draw.getPosition() + this.i18n = draw.getI18n() + this.container = draw.getContainer() + this.context = null + // 内部菜单 + this.contextMenuList = [ + ...globalMenus, + ...tableMenus, + ...imageMenus, + ...controlMenus, + ...hyperlinkMenus + ] + this.contextMenuContainerList = [] + this.contextMenuRelationShip = new Map() + this._addEvent() + } + + public getContextMenuList(): IRegisterContextMenu[] { + return this.contextMenuList + } + + private _addEvent() { + // 菜单权限 + this.container.addEventListener('contextmenu', this._proxyContextMenuEvent) + // 副作用处理 + document.addEventListener('mousedown', this._handleSideEffect) + } + + public removeEvent() { + this.container.removeEventListener( + 'contextmenu', + this._proxyContextMenuEvent + ) + document.removeEventListener('mousedown', this._handleSideEffect) + } + + private _filterMenuList( + menuList: IRegisterContextMenu[] + ): IRegisterContextMenu[] { + const { contextMenuDisableKeys } = this.options + const renderList: IRegisterContextMenu[] = [] + for (let m = 0; m < menuList.length; m++) { + const menu = menuList[m] + if ( + menu.disable || + (menu.key && contextMenuDisableKeys.includes(menu.key)) + ) { + continue + } + if (menu.isDivider) { + renderList.push(menu) + } else { + if (menu.when?.(this.context!)) { + renderList.push(menu) + } + } + } + return renderList + } + + private _proxyContextMenuEvent = (evt: MouseEvent) => { + this.context = this._getContext() + const renderList = this._filterMenuList(this.contextMenuList) + const isRegisterContextMenu = renderList.some(menu => !menu.isDivider) + if (isRegisterContextMenu) { + this.dispose() + this._render({ + contextMenuList: renderList, + left: evt.x, + top: evt.y + }) + } + evt.preventDefault() + } + + private _handleSideEffect = (evt: MouseEvent) => { + if (this.contextMenuContainerList.length) { + // 点击非右键菜单内 + const target = (evt?.composedPath()[0] || evt.target) + const contextMenuDom = findParent( + target, + (node: Node & Element) => + !!node && + node.nodeType === 1 && + node.getAttribute(EDITOR_COMPONENT) === EditorComponent.CONTEXTMENU, + true + ) + if (!contextMenuDom) { + this.dispose() + } + } + } + + private _getContext(): IContextMenuContext { + // 是否是只读模式 + const isReadonly = this.draw.isReadonly() + const { + isCrossRowCol: crossRowCol, + startIndex, + endIndex + } = this.range.getRange() + // 是否存在焦点 + const editorTextFocus = !!(~startIndex || ~endIndex) + // 是否存在选区 + const editorHasSelection = editorTextFocus && startIndex !== endIndex + // 是否在表格内 + const { isTable, trIndex, tdIndex, index } = + this.position.getPositionContext() + let tableElement: IElement | null = null + if (isTable) { + const originalElementList = this.draw.getOriginalElementList() + const originTableElement = originalElementList[index!] || null + if (originTableElement) { + tableElement = zipElementList([originTableElement], { + extraPickAttrs: ['id'] + })[0] + } + } + // 是否存在跨行/列 + const isCrossRowCol = isTable && !!crossRowCol + // 当前元素 + const elementList = this.draw.getElementList() + const startElement = elementList[startIndex] || null + const endElement = elementList[endIndex] || null + // 当前区域 + const zone = this.draw.getZone().getZone() + return { + startElement, + endElement, + isReadonly, + editorHasSelection, + editorTextFocus, + isCrossRowCol, + zone, + isInTable: isTable, + trIndex: trIndex ?? null, + tdIndex: tdIndex ?? null, + tableElement, + options: this.options + } + } + + private _createContextMenuContainer(): HTMLDivElement { + const contextMenuContainer = document.createElement('div') + contextMenuContainer.classList.add(`${EDITOR_PREFIX}-contextmenu-container`) + contextMenuContainer.setAttribute( + EDITOR_COMPONENT, + EditorComponent.CONTEXTMENU + ) + this.container.append(contextMenuContainer) + return contextMenuContainer + } + + private _render(payload: IRenderPayload): HTMLDivElement { + const { contextMenuList, left, top, parentMenuContainer } = payload + const contextMenuContainer = this._createContextMenuContainer() + const contextMenuContent = document.createElement('div') + contextMenuContent.classList.add(`${EDITOR_PREFIX}-contextmenu-content`) + // 直接子菜单 + let childMenuContainer: HTMLDivElement | null = null + // 父菜单添加子菜单映射关系 + if (parentMenuContainer) { + this.contextMenuRelationShip.set( + parentMenuContainer, + contextMenuContainer + ) + } + for (let c = 0; c < contextMenuList.length; c++) { + const menu = contextMenuList[c] + if (menu.isDivider) { + // 分割线相邻 || 首尾分隔符时不渲染 + if ( + c !== 0 && + c !== contextMenuList.length - 1 && + !contextMenuList[c - 1]?.isDivider + ) { + const divider = document.createElement('div') + divider.classList.add(`${EDITOR_PREFIX}-contextmenu-divider`) + contextMenuContent.append(divider) + } + } else { + const menuItem = document.createElement('div') + menuItem.classList.add(`${EDITOR_PREFIX}-contextmenu-item`) + // 菜单事件 + if (menu.childMenus) { + const childMenus = this._filterMenuList(menu.childMenus) + const isRegisterContextMenu = childMenus.some(menu => !menu.isDivider) + if (isRegisterContextMenu) { + menuItem.classList.add(`${EDITOR_PREFIX}-contextmenu-sub-item`) + menuItem.onmouseenter = () => { + this._setHoverStatus(menuItem, true) + this._removeSubMenu(contextMenuContainer) + // 子菜单 + const subMenuRect = menuItem.getBoundingClientRect() + const left = subMenuRect.left + subMenuRect.width + const top = subMenuRect.top + childMenuContainer = this._render({ + contextMenuList: childMenus, + left, + top, + parentMenuContainer: contextMenuContainer + }) + } + menuItem.onmouseleave = evt => { + // 移动到子菜单选项选中状态不变化 + if ( + !childMenuContainer || + !childMenuContainer.contains(evt.relatedTarget as Node) + ) { + this._setHoverStatus(menuItem, false) + } + } + } + } else { + menuItem.onmouseenter = () => { + this._setHoverStatus(menuItem, true) + this._removeSubMenu(contextMenuContainer) + } + menuItem.onmouseleave = () => { + this._setHoverStatus(menuItem, false) + } + menuItem.onclick = () => { + if (menu.callback && this.context) { + menu.callback(this.command, this.context) + } + this.dispose() + } + } + // 图标 + const icon = document.createElement('i') + menuItem.append(icon) + if (menu.icon) { + icon.classList.add(`${EDITOR_PREFIX}-contextmenu-${menu.icon}`) + } + // 文本 + const span = document.createElement('span') + const name = menu.i18nPath + ? this._formatName(this.i18n.t(menu.i18nPath)) + : this._formatName(menu.name || '') + span.append(document.createTextNode(name)) + menuItem.append(span) + // 快捷方式提示 + if (menu.shortCut) { + const span = document.createElement('span') + span.classList.add(`${EDITOR_PREFIX}-shortcut`) + span.append(document.createTextNode(menu.shortCut)) + menuItem.append(span) + } + contextMenuContent.append(menuItem) + } + } + contextMenuContainer.append(contextMenuContent) + contextMenuContainer.style.display = 'block' + // 右侧空间不足时,以菜单右上角作为起始点 + const innerWidth = window.innerWidth + const contextmenuRect = contextMenuContainer.getBoundingClientRect() + const contextMenuWidth = contextmenuRect.width + const adjustLeft = + left + contextMenuWidth > innerWidth ? left - contextMenuWidth : left + contextMenuContainer.style.left = `${adjustLeft}px` + // 下侧空间不足时,以菜单底部作为起始点 + const innerHeight = window.innerHeight + const contextMenuHeight = contextmenuRect.height + const adjustTop = + top + contextMenuHeight > innerHeight ? top - contextMenuHeight : top + contextMenuContainer.style.top = `${adjustTop}px` + this.contextMenuContainerList.push(contextMenuContainer) + return contextMenuContainer + } + + private _removeSubMenu(payload: HTMLDivElement) { + const childMenu = this.contextMenuRelationShip.get(payload) + if (childMenu) { + this._removeSubMenu(childMenu) + childMenu.remove() + this.contextMenuRelationShip.delete(payload) + } + } + + private _setHoverStatus(payload: HTMLDivElement, status: boolean) { + if (status) { + payload.parentNode + ?.querySelectorAll(`${EDITOR_PREFIX}-contextmenu-item`) + .forEach(child => child.classList.remove('hover')) + payload.classList.add('hover') + } else { + payload.classList.remove('hover') + } + } + + private _formatName(name: string): string { + const placeholderValues = Object.values(NAME_PLACEHOLDER) + const placeholderReg = new RegExp(`${placeholderValues.join('|')}`) + let formatName = name + if (placeholderReg.test(formatName)) { + // 选区名称 + const selectedReg = new RegExp(NAME_PLACEHOLDER.SELECTED_TEXT, 'g') + if (selectedReg.test(formatName)) { + const selectedText = this.range.toString() + formatName = formatName.replace(selectedReg, selectedText) + } + } + return formatName + } + + public registerContextMenuList(payload: IRegisterContextMenu[]) { + this.contextMenuList.push(...payload) + } + + public dispose() { + this.contextMenuContainerList.forEach(child => child.remove()) + this.contextMenuContainerList = [] + this.contextMenuRelationShip.clear() + } +} diff --git a/src/editor/core/contextmenu/menus/controlMenus.ts b/src/editor/core/contextmenu/menus/controlMenus.ts new file mode 100644 index 0000000..30ef993 --- /dev/null +++ b/src/editor/core/contextmenu/menus/controlMenus.ts @@ -0,0 +1,25 @@ +import { INTERNAL_CONTEXT_MENU_KEY } from '../../../dataset/constant/ContextMenu' +import { EditorMode } from '../../../dataset/enum/Editor' +import { IRegisterContextMenu } from '../../../interface/contextmenu/ContextMenu' +import { Command } from '../../command/Command' +const { + CONTROL: { DELETE } +} = INTERNAL_CONTEXT_MENU_KEY + +export const controlMenus: IRegisterContextMenu[] = [ + { + key: DELETE, + i18nPath: 'contextmenu.control.delete', + when: payload => { + return ( + !payload.isReadonly && + !payload.editorHasSelection && + !!payload.startElement?.controlId && + payload.options.mode !== EditorMode.FORM + ) + }, + callback: (command: Command) => { + command.executeRemoveControl() + } + } +] diff --git a/src/editor/core/contextmenu/menus/globalMenus.ts b/src/editor/core/contextmenu/menus/globalMenus.ts new file mode 100644 index 0000000..c408d14 --- /dev/null +++ b/src/editor/core/contextmenu/menus/globalMenus.ts @@ -0,0 +1,66 @@ +import { INTERNAL_CONTEXT_MENU_KEY } from '../../../dataset/constant/ContextMenu' +import { IRegisterContextMenu } from '../../../interface/contextmenu/ContextMenu' +import { isApple } from '../../../utils/ua' +import { Command } from '../../command/Command' +const { + GLOBAL: { CUT, COPY, PASTE, SELECT_ALL, PRINT } +} = INTERNAL_CONTEXT_MENU_KEY + +export const globalMenus: IRegisterContextMenu[] = [ + { + key: CUT, + i18nPath: 'contextmenu.global.cut', + shortCut: `${isApple ? '⌘' : 'Ctrl'} + X`, + when: payload => { + return !payload.isReadonly + }, + callback: (command: Command) => { + command.executeCut() + } + }, + { + key: COPY, + i18nPath: 'contextmenu.global.copy', + shortCut: `${isApple ? '⌘' : 'Ctrl'} + C`, + when: payload => { + return payload.editorHasSelection || payload.isCrossRowCol + }, + callback: (command: Command) => { + command.executeCopy() + } + }, + { + key: PASTE, + i18nPath: 'contextmenu.global.paste', + shortCut: `${isApple ? '⌘' : 'Ctrl'} + V`, + when: payload => { + return !payload.isReadonly && payload.editorTextFocus + }, + callback: (command: Command) => { + command.executePaste() + } + }, + { + key: SELECT_ALL, + i18nPath: 'contextmenu.global.selectAll', + shortCut: `${isApple ? '⌘' : 'Ctrl'} + A`, + when: payload => { + return payload.editorTextFocus + }, + callback: (command: Command) => { + command.executeSelectAll() + } + }, + { + isDivider: true + }, + { + key: PRINT, + i18nPath: 'contextmenu.global.print', + icon: 'print', + when: () => true, + callback: (command: Command) => { + command.executePrint() + } + } +] diff --git a/src/editor/core/contextmenu/menus/hyperlinkMenus.ts b/src/editor/core/contextmenu/menus/hyperlinkMenus.ts new file mode 100644 index 0000000..80c6dae --- /dev/null +++ b/src/editor/core/contextmenu/menus/hyperlinkMenus.ts @@ -0,0 +1,58 @@ +import { INTERNAL_CONTEXT_MENU_KEY } from '../../../dataset/constant/ContextMenu' +import { ElementType } from '../../../dataset/enum/Element' +import { + IContextMenuContext, + IRegisterContextMenu +} from '../../../interface/contextmenu/ContextMenu' +import { Command } from '../../command/Command' +const { + HYPERLINK: { DELETE, CANCEL, EDIT } +} = INTERNAL_CONTEXT_MENU_KEY + +export const hyperlinkMenus: IRegisterContextMenu[] = [ + { + key: DELETE, + i18nPath: 'contextmenu.hyperlink.delete', + when: payload => { + return ( + !payload.isReadonly && + payload.startElement?.type === ElementType.HYPERLINK + ) + }, + callback: (command: Command) => { + command.executeDeleteHyperlink() + } + }, + { + key: CANCEL, + i18nPath: 'contextmenu.hyperlink.cancel', + when: payload => { + return ( + !payload.isReadonly && + payload.startElement?.type === ElementType.HYPERLINK + ) + }, + callback: (command: Command) => { + command.executeCancelHyperlink() + } + }, + { + key: EDIT, + i18nPath: 'contextmenu.hyperlink.edit', + when: payload => { + return ( + !payload.isReadonly && + payload.startElement?.type === ElementType.HYPERLINK + ) + }, + callback: (command: Command, context: IContextMenuContext) => { + const url = window.prompt( + command.executeTranslate('contextmenu.hyperlink.edit'), + context.startElement?.url + ) + if (url) { + command.executeEditHyperlink(url) + } + } + } +] diff --git a/src/editor/core/contextmenu/menus/imageMenus.ts b/src/editor/core/contextmenu/menus/imageMenus.ts new file mode 100644 index 0000000..fae52ec --- /dev/null +++ b/src/editor/core/contextmenu/menus/imageMenus.ts @@ -0,0 +1,134 @@ +import { INTERNAL_CONTEXT_MENU_KEY } from '../../../dataset/constant/ContextMenu' +import { ImageDisplay } from '../../../dataset/enum/Common' +import { ElementType } from '../../../dataset/enum/Element' +import { + IContextMenuContext, + IRegisterContextMenu +} from '../../../interface/contextmenu/ContextMenu' +import { Command } from '../../command/Command' +const { + IMAGE: { + CHANGE, + SAVE_AS, + TEXT_WRAP, + TEXT_WRAP_EMBED, + TEXT_WRAP_UP_DOWN, + TEXT_WRAP_SURROUND, + TEXT_WRAP_FLOAT_TOP, + TEXT_WRAP_FLOAT_BOTTOM + } +} = INTERNAL_CONTEXT_MENU_KEY + +export const imageMenus: IRegisterContextMenu[] = [ + { + key: CHANGE, + i18nPath: 'contextmenu.image.change', + icon: 'image-change', + when: payload => { + return ( + !payload.isReadonly && + !payload.editorHasSelection && + payload.startElement?.type === ElementType.IMAGE + ) + }, + callback: (command: Command) => { + // 创建代理元素 + const proxyInputFile = document.createElement('input') + proxyInputFile.type = 'file' + proxyInputFile.accept = '.png, .jpg, .jpeg' + // 监听上传 + proxyInputFile.onchange = () => { + const file = proxyInputFile.files![0]! + const fileReader = new FileReader() + fileReader.readAsDataURL(file) + fileReader.onload = () => { + const value = fileReader.result as string + command.executeReplaceImageElement(value) + } + } + proxyInputFile.click() + } + }, + { + key: SAVE_AS, + i18nPath: 'contextmenu.image.saveAs', + icon: 'image', + when: payload => { + return ( + !payload.editorHasSelection && + payload.startElement?.type === ElementType.IMAGE + ) + }, + callback: (command: Command) => { + command.executeSaveAsImageElement() + } + }, + { + key: TEXT_WRAP, + i18nPath: 'contextmenu.image.textWrap', + when: payload => { + return ( + !payload.isReadonly && + !payload.editorHasSelection && + payload.startElement?.type === ElementType.IMAGE + ) + }, + childMenus: [ + { + key: TEXT_WRAP_EMBED, + i18nPath: 'contextmenu.image.textWrapType.embed', + when: () => true, + callback: (command: Command, context: IContextMenuContext) => { + command.executeChangeImageDisplay( + context.startElement!, + ImageDisplay.BLOCK + ) + } + }, + { + key: TEXT_WRAP_UP_DOWN, + i18nPath: 'contextmenu.image.textWrapType.upDown', + when: () => true, + callback: (command: Command, context: IContextMenuContext) => { + command.executeChangeImageDisplay( + context.startElement!, + ImageDisplay.INLINE + ) + } + }, + { + key: TEXT_WRAP_SURROUND, + i18nPath: 'contextmenu.image.textWrapType.surround', + when: () => true, + callback: (command: Command, context: IContextMenuContext) => { + command.executeChangeImageDisplay( + context.startElement!, + ImageDisplay.SURROUND + ) + } + }, + { + key: TEXT_WRAP_FLOAT_TOP, + i18nPath: 'contextmenu.image.textWrapType.floatTop', + when: () => true, + callback: (command: Command, context: IContextMenuContext) => { + command.executeChangeImageDisplay( + context.startElement!, + ImageDisplay.FLOAT_TOP + ) + } + }, + { + key: TEXT_WRAP_FLOAT_BOTTOM, + i18nPath: 'contextmenu.image.textWrapType.floatBottom', + when: () => true, + callback: (command: Command, context: IContextMenuContext) => { + command.executeChangeImageDisplay( + context.startElement!, + ImageDisplay.FLOAT_BOTTOM + ) + } + } + ] + } +] diff --git a/src/editor/core/contextmenu/menus/tableMenus.ts b/src/editor/core/contextmenu/menus/tableMenus.ts new file mode 100644 index 0000000..bcb5a97 --- /dev/null +++ b/src/editor/core/contextmenu/menus/tableMenus.ts @@ -0,0 +1,331 @@ +import { INTERNAL_CONTEXT_MENU_KEY } from '../../../dataset/constant/ContextMenu' +import { EditorMode } from '../../../dataset/enum/Editor' +import { VerticalAlign } from '../../../dataset/enum/VerticalAlign' +import { + TableBorder, + TdBorder, + TdSlash +} from '../../../dataset/enum/table/Table' +import { IRegisterContextMenu } from '../../../interface/contextmenu/ContextMenu' +import { Command } from '../../command/Command' +const { + TABLE: { + BORDER, + BORDER_ALL, + BORDER_EMPTY, + BORDER_DASH, + BORDER_EXTERNAL, + BORDER_INTERNAL, + BORDER_TD, + BORDER_TD_TOP, + BORDER_TD_LEFT, + BORDER_TD_BOTTOM, + BORDER_TD_RIGHT, + BORDER_TD_BACK, + BORDER_TD_FORWARD, + VERTICAL_ALIGN, + VERTICAL_ALIGN_TOP, + VERTICAL_ALIGN_MIDDLE, + VERTICAL_ALIGN_BOTTOM, + INSERT_ROW_COL, + INSERT_TOP_ROW, + INSERT_BOTTOM_ROW, + INSERT_LEFT_COL, + INSERT_RIGHT_COL, + DELETE_ROW_COL, + DELETE_ROW, + DELETE_COL, + DELETE_TABLE, + MERGE_CELL, + CANCEL_MERGE_CELL + } +} = INTERNAL_CONTEXT_MENU_KEY + +export const tableMenus: IRegisterContextMenu[] = [ + { + isDivider: true + }, + { + key: BORDER, + i18nPath: 'contextmenu.table.border', + icon: 'border-all', + when: payload => { + return ( + !payload.isReadonly && + payload.isInTable && + payload.options.mode !== EditorMode.FORM + ) + }, + childMenus: [ + { + key: BORDER_ALL, + i18nPath: 'contextmenu.table.borderAll', + icon: 'border-all', + when: () => true, + callback: (command: Command) => { + command.executeTableBorderType(TableBorder.ALL) + } + }, + { + key: BORDER_EMPTY, + i18nPath: 'contextmenu.table.borderEmpty', + icon: 'border-empty', + when: () => true, + callback: (command: Command) => { + command.executeTableBorderType(TableBorder.EMPTY) + } + }, + { + key: BORDER_DASH, + i18nPath: 'contextmenu.table.borderDash', + icon: 'border-dash', + when: () => true, + callback: (command: Command) => { + command.executeTableBorderType(TableBorder.DASH) + } + }, + { + key: BORDER_EXTERNAL, + i18nPath: 'contextmenu.table.borderExternal', + icon: 'border-external', + when: () => true, + callback: (command: Command) => { + command.executeTableBorderType(TableBorder.EXTERNAL) + } + }, + { + key: BORDER_INTERNAL, + i18nPath: 'contextmenu.table.borderInternal', + icon: 'border-internal', + when: () => true, + callback: (command: Command) => { + command.executeTableBorderType(TableBorder.INTERNAL) + } + }, + { + key: BORDER_TD, + i18nPath: 'contextmenu.table.borderTd', + icon: 'border-td', + when: () => true, + childMenus: [ + { + key: BORDER_TD_TOP, + i18nPath: 'contextmenu.table.borderTdTop', + icon: 'border-td-top', + when: () => true, + callback: (command: Command) => { + command.executeTableTdBorderType(TdBorder.TOP) + } + }, + { + key: BORDER_TD_RIGHT, + i18nPath: 'contextmenu.table.borderTdRight', + icon: 'border-td-right', + when: () => true, + callback: (command: Command) => { + command.executeTableTdBorderType(TdBorder.RIGHT) + } + }, + { + key: BORDER_TD_BOTTOM, + i18nPath: 'contextmenu.table.borderTdBottom', + icon: 'border-td-bottom', + when: () => true, + callback: (command: Command) => { + command.executeTableTdBorderType(TdBorder.BOTTOM) + } + }, + { + key: BORDER_TD_LEFT, + i18nPath: 'contextmenu.table.borderTdLeft', + icon: 'border-td-left', + when: () => true, + callback: (command: Command) => { + command.executeTableTdBorderType(TdBorder.LEFT) + } + }, + { + key: BORDER_TD_FORWARD, + i18nPath: 'contextmenu.table.borderTdForward', + icon: 'border-td-forward', + when: () => true, + callback: (command: Command) => { + command.executeTableTdSlashType(TdSlash.FORWARD) + } + }, + { + key: BORDER_TD_BACK, + i18nPath: 'contextmenu.table.borderTdBack', + icon: 'border-td-back', + when: () => true, + callback: (command: Command) => { + command.executeTableTdSlashType(TdSlash.BACK) + } + } + ] + } + ] + }, + { + key: VERTICAL_ALIGN, + i18nPath: 'contextmenu.table.verticalAlign', + icon: 'vertical-align', + when: payload => { + return ( + !payload.isReadonly && + payload.isInTable && + payload.options.mode !== EditorMode.FORM + ) + }, + childMenus: [ + { + key: VERTICAL_ALIGN_TOP, + i18nPath: 'contextmenu.table.verticalAlignTop', + icon: 'vertical-align-top', + when: () => true, + callback: (command: Command) => { + command.executeTableTdVerticalAlign(VerticalAlign.TOP) + } + }, + { + key: VERTICAL_ALIGN_MIDDLE, + i18nPath: 'contextmenu.table.verticalAlignMiddle', + icon: 'vertical-align-middle', + when: () => true, + callback: (command: Command) => { + command.executeTableTdVerticalAlign(VerticalAlign.MIDDLE) + } + }, + { + key: VERTICAL_ALIGN_BOTTOM, + i18nPath: 'contextmenu.table.verticalAlignBottom', + icon: 'vertical-align-bottom', + when: () => true, + callback: (command: Command) => { + command.executeTableTdVerticalAlign(VerticalAlign.BOTTOM) + } + } + ] + }, + { + key: INSERT_ROW_COL, + i18nPath: 'contextmenu.table.insertRowCol', + icon: 'insert-row-col', + when: payload => { + return ( + !payload.isReadonly && + payload.isInTable && + payload.options.mode !== EditorMode.FORM + ) + }, + childMenus: [ + { + key: INSERT_TOP_ROW, + i18nPath: 'contextmenu.table.insertTopRow', + icon: 'insert-top-row', + when: () => true, + callback: (command: Command) => { + command.executeInsertTableTopRow() + } + }, + { + key: INSERT_BOTTOM_ROW, + i18nPath: 'contextmenu.table.insertBottomRow', + icon: 'insert-bottom-row', + when: () => true, + callback: (command: Command) => { + command.executeInsertTableBottomRow() + } + }, + { + key: INSERT_LEFT_COL, + i18nPath: 'contextmenu.table.insertLeftCol', + icon: 'insert-left-col', + when: () => true, + callback: (command: Command) => { + command.executeInsertTableLeftCol() + } + }, + { + key: INSERT_RIGHT_COL, + i18nPath: 'contextmenu.table.insertRightCol', + icon: 'insert-right-col', + when: () => true, + callback: (command: Command) => { + command.executeInsertTableRightCol() + } + } + ] + }, + { + key: DELETE_ROW_COL, + i18nPath: 'contextmenu.table.deleteRowCol', + icon: 'delete-row-col', + when: payload => { + return ( + !payload.isReadonly && + payload.isInTable && + payload.options.mode !== EditorMode.FORM + ) + }, + childMenus: [ + { + key: DELETE_ROW, + i18nPath: 'contextmenu.table.deleteRow', + icon: 'delete-row', + when: () => true, + callback: (command: Command) => { + command.executeDeleteTableRow() + } + }, + { + key: DELETE_COL, + i18nPath: 'contextmenu.table.deleteCol', + icon: 'delete-col', + when: () => true, + callback: (command: Command) => { + command.executeDeleteTableCol() + } + }, + { + key: DELETE_TABLE, + i18nPath: 'contextmenu.table.deleteTable', + icon: 'delete-table', + when: () => true, + callback: (command: Command) => { + command.executeDeleteTable() + } + } + ] + }, + { + key: MERGE_CELL, + i18nPath: 'contextmenu.table.mergeCell', + icon: 'merge-cell', + when: payload => { + return ( + !payload.isReadonly && + payload.isCrossRowCol && + payload.options.mode !== EditorMode.FORM + ) + }, + callback: (command: Command) => { + command.executeMergeTableCell() + } + }, + { + key: CANCEL_MERGE_CELL, + i18nPath: 'contextmenu.table.mergeCancelCell', + icon: 'merge-cancel-cell', + when: payload => { + return ( + !payload.isReadonly && + payload.isInTable && + payload.options.mode !== EditorMode.FORM + ) + }, + callback: (command: Command) => { + command.executeCancelMergeTableCell() + } + } +] diff --git a/src/editor/core/cursor/Cursor.ts b/src/editor/core/cursor/Cursor.ts new file mode 100644 index 0000000..a8410f8 --- /dev/null +++ b/src/editor/core/cursor/Cursor.ts @@ -0,0 +1,235 @@ +import { CURSOR_AGENT_OFFSET_HEIGHT } from '../../dataset/constant/Cursor' +import { EDITOR_PREFIX } from '../../dataset/constant/Editor' +import { MoveDirection } from '../../dataset/enum/Observer' +import { DeepRequired } from '../../interface/Common' +import { ICursorOption } from '../../interface/Cursor' +import { IEditorOption } from '../../interface/Editor' +import { IElementPosition } from '../../interface/Element' +import { findScrollContainer } from '../../utils' +import { isMobile } from '../../utils/ua' +import { Draw } from '../draw/Draw' +import { CanvasEvent } from '../event/CanvasEvent' +import { Position } from '../position/Position' +import { CursorAgent } from './CursorAgent' + +export type IDrawCursorOption = ICursorOption & { + isShow?: boolean + isBlink?: boolean + isFocus?: boolean + hitLineStartIndex?: number +} + +export interface IMoveCursorToVisibleOption { + direction: MoveDirection + cursorPosition: IElementPosition +} + +export class Cursor { + private readonly ANIMATION_CLASS = `${EDITOR_PREFIX}-cursor--animation` + + private draw: Draw + private container: HTMLDivElement + private options: DeepRequired + private position: Position + private cursorDom: HTMLDivElement + private cursorAgent: CursorAgent + private blinkTimeout: number | null + private hitLineStartIndex: number | undefined + + constructor(draw: Draw, canvasEvent: CanvasEvent) { + this.draw = draw + this.container = draw.getContainer() + this.position = draw.getPosition() + this.options = draw.getOptions() + + this.cursorDom = document.createElement('div') + this.cursorDom.classList.add(`${EDITOR_PREFIX}-cursor`) + this.container.append(this.cursorDom) + this.cursorAgent = new CursorAgent(draw, canvasEvent) + this.blinkTimeout = null + } + + public getCursorDom(): HTMLDivElement { + return this.cursorDom + } + + public getAgentDom(): HTMLTextAreaElement { + return this.cursorAgent.getAgentCursorDom() + } + + public getAgentIsActive(): boolean { + return this.getAgentDom() === document.activeElement + } + + public getAgentDomValue(): string { + return this.getAgentDom().value + } + + public clearAgentDomValue() { + this.getAgentDom().value = '' + } + + public getHitLineStartIndex() { + return this.hitLineStartIndex + } + + private _blinkStart() { + this.cursorDom.classList.add(this.ANIMATION_CLASS) + } + + private _blinkStop() { + this.cursorDom.classList.remove(this.ANIMATION_CLASS) + } + + private _setBlinkTimeout() { + this._clearBlinkTimeout() + this.blinkTimeout = window.setTimeout(() => { + this._blinkStart() + }, 500) + } + + private _clearBlinkTimeout() { + if (this.blinkTimeout) { + this._blinkStop() + window.clearTimeout(this.blinkTimeout) + this.blinkTimeout = null + } + } + + public focus() { + // 移动端只读模式禁用聚焦避免唤起输入法,web端允许聚焦避免事件无法捕获 + if (isMobile && this.draw.isReadonly()) return + const agentCursorDom = this.cursorAgent.getAgentCursorDom() + // 光标不聚焦时重新定位 + if (document.activeElement !== agentCursorDom) { + agentCursorDom.focus() + agentCursorDom.setSelectionRange(0, 0) + } + } + + public drawCursor(payload?: IDrawCursorOption) { + let cursorPosition = this.position.getCursorPosition() + if (!cursorPosition) return + const { scale, cursor } = this.options + const { + color, + width, + isShow = true, + isBlink = true, + isFocus = true, + hitLineStartIndex + } = { ...cursor, ...payload } + // 设置光标代理 + const height = this.draw.getHeight() + const pageGap = this.draw.getPageGap() + // 光标位置 + this.hitLineStartIndex = hitLineStartIndex + if (hitLineStartIndex) { + const positionList = this.position.getPositionList() + cursorPosition = positionList[hitLineStartIndex] + } + const { + metrics, + coordinate: { leftTop, rightTop }, + ascent, + pageNo + } = cursorPosition + const zoneManager = this.draw.getZone() + const curPageNo = zoneManager.isMainActive() + ? pageNo + : this.draw.getPageNo() + const preY = curPageNo * (height + pageGap) + // 默认偏移高度 + const defaultOffsetHeight = CURSOR_AGENT_OFFSET_HEIGHT * scale + // 增加1/4字体大小(最小为defaultOffsetHeight即默认偏移高度) + const increaseHeight = Math.min(metrics.height / 4, defaultOffsetHeight) + const cursorHeight = metrics.height + increaseHeight * 2 + const agentCursorDom = this.cursorAgent.getAgentCursorDom() + if (isFocus) { + setTimeout(() => { + this.focus() + }) + } + // fillText位置 + 文字基线到底部距离 - 模拟光标偏移量 + const descent = + metrics.boundingBoxDescent < 0 ? 0 : metrics.boundingBoxDescent + const cursorTop = + leftTop[1] + ascent + descent - (cursorHeight - increaseHeight) + preY + const cursorLeft = hitLineStartIndex ? leftTop[0] : rightTop[0] + agentCursorDom.style.left = `${cursorLeft}px` + agentCursorDom.style.top = `${ + cursorTop + cursorHeight - defaultOffsetHeight + }px` + // 模拟光标显示 + if (!isShow) { + this.recoveryCursor() + return + } + const isReadonly = this.draw.isReadonly() + this.cursorDom.style.width = `${width * scale}px` + this.cursorDom.style.backgroundColor = color + this.cursorDom.style.left = `${cursorLeft}px` + this.cursorDom.style.top = `${cursorTop}px` + this.cursorDom.style.display = isReadonly ? 'none' : 'block' + this.cursorDom.style.height = `${cursorHeight}px` + if (isBlink) { + this._setBlinkTimeout() + } else { + this._clearBlinkTimeout() + } + } + + public recoveryCursor() { + this.cursorDom.style.display = 'none' + this._clearBlinkTimeout() + } + + public moveCursorToVisible(payload: IMoveCursorToVisibleOption) { + const { cursorPosition, direction } = payload + if (!cursorPosition || !direction) return + const { + pageNo, + coordinate: { leftTop, leftBottom } + } = cursorPosition + // 当前页面距离滚动容器顶部距离 + const prePageY = + pageNo * (this.draw.getHeight() + this.draw.getPageGap()) + + this.container.getBoundingClientRect().top + // 向上移动时:以顶部距离为准,向下移动时:以底部位置为准 + const isUp = direction === MoveDirection.UP + const x = leftBottom[0] + const y = isUp ? leftTop[1] + prePageY : leftBottom[1] + prePageY + // 查找滚动容器,如果是滚动容器是document,则限制范围为当前窗口 + const scrollContainer = findScrollContainer(this.container) + const rect = { + left: 0, + right: 0, + top: 0, + bottom: 0 + } + if (scrollContainer === document.documentElement) { + rect.right = window.innerWidth + rect.bottom = window.innerHeight + } else { + const { left, right, top, bottom } = + scrollContainer.getBoundingClientRect() + rect.left = left + rect.right = right + rect.top = top + rect.bottom = bottom + } + // 可视范围根据参数调整 + const { maskMargin } = this.options + rect.top += maskMargin[0] + rect.bottom -= maskMargin[2] + // 不在可视范围时,移动滚动条到合适位置 + if ( + !(x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) + ) { + const { scrollLeft, scrollTop } = scrollContainer + isUp + ? scrollContainer.scroll(scrollLeft, scrollTop - (rect.top - y)) + : scrollContainer.scroll(scrollLeft, scrollTop + y - rect.bottom) + } + } +} diff --git a/src/editor/core/cursor/CursorAgent.ts b/src/editor/core/cursor/CursorAgent.ts new file mode 100644 index 0000000..27bbaa3 --- /dev/null +++ b/src/editor/core/cursor/CursorAgent.ts @@ -0,0 +1,75 @@ +import { EDITOR_PREFIX } from '../../dataset/constant/Editor' +import { EventBusMap } from '../../interface/EventBus' +import { Draw } from '../draw/Draw' +import { CanvasEvent } from '../event/CanvasEvent' +import { EventBus } from '../event/eventbus/EventBus' +import { pasteByEvent } from '../event/handlers/paste' + +export class CursorAgent { + private draw: Draw + private container: HTMLDivElement + private agentCursorDom: HTMLTextAreaElement + private canvasEvent: CanvasEvent + private eventBus: EventBus + + constructor(draw: Draw, canvasEvent: CanvasEvent) { + this.draw = draw + this.container = draw.getContainer() + this.canvasEvent = canvasEvent + this.eventBus = draw.getEventBus() + // 代理光标绘制 + const agentCursorDom = document.createElement('textarea') + agentCursorDom.autocomplete = 'off' + agentCursorDom.classList.add(`${EDITOR_PREFIX}-inputarea`) + agentCursorDom.innerText = '' + this.container.append(agentCursorDom) + this.agentCursorDom = agentCursorDom + // 事件 + agentCursorDom.onkeydown = (evt: KeyboardEvent) => this._keyDown(evt) + agentCursorDom.oninput = this._input.bind(this) + agentCursorDom.onpaste = (evt: ClipboardEvent) => this._paste(evt) + agentCursorDom.addEventListener( + 'compositionstart', + this._compositionstart.bind(this) + ) + agentCursorDom.addEventListener( + 'compositionend', + this._compositionend.bind(this) + ) + } + + public getAgentCursorDom(): HTMLTextAreaElement { + return this.agentCursorDom + } + + private _keyDown(evt: KeyboardEvent) { + this.canvasEvent.keydown(evt) + } + + private _input(evt: Event) { + const data = (evt).data + if (data) { + this.canvasEvent.input(data) + } + if (this.eventBus.isSubscribe('input')) { + this.eventBus.emit('input', evt) + } + } + + private _paste(evt: ClipboardEvent) { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + const clipboardData = evt.clipboardData + if (!clipboardData) return + pasteByEvent(this.canvasEvent, evt) + evt.preventDefault() + } + + private _compositionstart() { + this.canvasEvent.compositionstart() + } + + private _compositionend(evt: CompositionEvent) { + this.canvasEvent.compositionend(evt) + } +} diff --git a/src/editor/core/draw/Draw.ts b/src/editor/core/draw/Draw.ts new file mode 100644 index 0000000..b10940e --- /dev/null +++ b/src/editor/core/draw/Draw.ts @@ -0,0 +1,2845 @@ +import { version } from '../../../../package.json' +import { ZERO } from '../../dataset/constant/Common' +import { RowFlex } from '../../dataset/enum/Row' +import { + IAppendElementListOption, + IComputeRowListPayload, + IDrawFloatPayload, + IDrawOption, + IDrawPagePayload, + IDrawRowPayload, + IGetImageOption, + IGetOriginValueOption, + IGetValueOption, + IPainterOption +} from '../../interface/Draw' +import { + IEditorData, + IEditorOption, + IEditorResult, + ISetValueOption +} from '../../interface/Editor' +import { + IElement, + IElementMetrics, + IElementFillRect, + IElementStyle, + ISpliceElementListOption, + IInsertElementListOption +} from '../../interface/Element' +import { IRow, IRowElement } from '../../interface/Row' +import { deepClone, getUUID, nextTick } from '../../utils' +import { Cursor } from '../cursor/Cursor' +import { CanvasEvent } from '../event/CanvasEvent' +import { GlobalEvent } from '../event/GlobalEvent' +import { HistoryManager } from '../history/HistoryManager' +import { Listener } from '../listener/Listener' +import { Position } from '../position/Position' +import { RangeManager } from '../range/RangeManager' +import { Background } from './frame/Background' +import { Highlight } from './richtext/Highlight' +import { Margin } from './frame/Margin' +import { Search } from './interactive/Search' +import { Strikeout } from './richtext/Strikeout' +import { Underline } from './richtext/Underline' +import { ElementType } from '../../dataset/enum/Element' +import { ImageParticle } from './particle/ImageParticle' +import { LaTexParticle } from './particle/latex/LaTexParticle' +import { TextParticle } from './particle/TextParticle' +import { PageNumber } from './frame/PageNumber' +import { ScrollObserver } from '../observer/ScrollObserver' +import { SelectionObserver } from '../observer/SelectionObserver' +import { TableParticle } from './particle/table/TableParticle' +import { TableTool } from './particle/table/TableTool' +import { HyperlinkParticle } from './particle/HyperlinkParticle' +import { Header } from './frame/Header' +import { SuperscriptParticle } from './particle/SuperscriptParticle' +import { SubscriptParticle } from './particle/SubscriptParticle' +import { SeparatorParticle } from './particle/SeparatorParticle' +import { PageBreakParticle } from './particle/PageBreakParticle' +import { Watermark } from './frame/Watermark' +import { + EditorComponent, + EditorMode, + EditorZone, + PageMode, + PaperDirection, + WordBreak +} from '../../dataset/enum/Editor' +import { Control } from './control/Control' +import { + deleteSurroundElementList, + getIsBlockElement, + getSlimCloneElementList, + pickSurroundElementList, + zipElementList +} from '../../utils/element' +import { CheckboxParticle } from './particle/CheckboxParticle' +import { RadioParticle } from './particle/RadioParticle' +import { DeepRequired, IPadding } from '../../interface/Common' +import { + ControlComponent, + ControlIndentation +} from '../../dataset/enum/Control' +import { formatElementList } from '../../utils/element' +import { WorkerManager } from '../worker/WorkerManager' +import { Previewer } from './particle/previewer/Previewer' +import { DateParticle } from './particle/date/DateParticle' +import { IMargin } from '../../interface/Margin' +import { BlockParticle } from './particle/block/BlockParticle' +import { EDITOR_COMPONENT, EDITOR_PREFIX } from '../../dataset/constant/Editor' +import { I18n } from '../i18n/I18n' +import { ImageObserver } from '../observer/ImageObserver' +import { Zone } from '../zone/Zone' +import { Footer } from './frame/Footer' +import { + IMAGE_ELEMENT_TYPE, + TEXTLIKE_ELEMENT_TYPE +} from '../../dataset/constant/Element' +import { ListParticle } from './particle/ListParticle' +import { Placeholder } from './frame/Placeholder' +import { EventBus } from '../event/eventbus/EventBus' +import { EventBusMap } from '../../interface/EventBus' +import { Group } from './interactive/Group' +import { Override } from '../override/Override' +import { FlexDirection, ImageDisplay } from '../../dataset/enum/Common' +import { PUNCTUATION_REG } from '../../dataset/constant/Regular' +import { LineBreakParticle } from './particle/LineBreakParticle' +import { MouseObserver } from '../observer/MouseObserver' +import { LineNumber } from './frame/LineNumber' +import { PageBorder } from './frame/PageBorder' +import { ITd } from '../../interface/table/Td' +import { Actuator } from '../actuator/Actuator' +import { TableOperate } from './particle/table/TableOperate' +import { Area } from './interactive/Area' +import { Badge } from './frame/Badge' + +export class Draw { + private container: HTMLDivElement + private pageContainer: HTMLDivElement + private pageList: HTMLCanvasElement[] + private ctxList: CanvasRenderingContext2D[] + private pageNo: number + private renderCount: number + private pagePixelRatio: number | null + private mode: EditorMode + private options: DeepRequired + private position: Position + private zone: Zone + private elementList: IElement[] + private listener: Listener + private eventBus: EventBus + private override: Override + + private i18n: I18n + private canvasEvent: CanvasEvent + private globalEvent: GlobalEvent + private cursor: Cursor + private range: RangeManager + private margin: Margin + private background: Background + private badge: Badge + private search: Search + private group: Group + private area: Area + private underline: Underline + private strikeout: Strikeout + private highlight: Highlight + private historyManager: HistoryManager + private previewer: Previewer + private imageParticle: ImageParticle + private laTexParticle: LaTexParticle + private textParticle: TextParticle + private tableParticle: TableParticle + private tableTool: TableTool + private tableOperate: TableOperate + private pageNumber: PageNumber + private lineNumber: LineNumber + private waterMark: Watermark + private placeholder: Placeholder + private header: Header + private footer: Footer + private hyperlinkParticle: HyperlinkParticle + private dateParticle: DateParticle + private separatorParticle: SeparatorParticle + private pageBreakParticle: PageBreakParticle + private superscriptParticle: SuperscriptParticle + private subscriptParticle: SubscriptParticle + private checkboxParticle: CheckboxParticle + private radioParticle: RadioParticle + private blockParticle: BlockParticle + private listParticle: ListParticle + private lineBreakParticle: LineBreakParticle + private control: Control + private pageBorder: PageBorder + private workerManager: WorkerManager + private scrollObserver: ScrollObserver + private selectionObserver: SelectionObserver + private imageObserver: ImageObserver + + private LETTER_REG: RegExp + private WORD_LIKE_REG: RegExp + private rowList: IRow[] + private pageRowList: IRow[][] + private painterStyle: IElementStyle | null + private painterOptions: IPainterOption | null + private visiblePageNoList: number[] + private intersectionPageNo: number + private lazyRenderIntersectionObserver: IntersectionObserver | null + private printModeData: Required | null + + constructor( + rootContainer: HTMLElement, + options: DeepRequired, + data: IEditorData, + listener: Listener, + eventBus: EventBus, + override: Override + ) { + this.container = this._wrapContainer(rootContainer) + this.pageList = [] + this.ctxList = [] + this.pageNo = 0 + this.renderCount = 0 + this.pagePixelRatio = null + this.mode = options.mode + this.options = options + this.elementList = data.main + this.listener = listener + this.eventBus = eventBus + this.override = override + + this._formatContainer() + this.pageContainer = this._createPageContainer() + this._createPage(0) + + this.i18n = new I18n(options.locale) + this.historyManager = new HistoryManager(this) + this.position = new Position(this) + this.zone = new Zone(this) + this.range = new RangeManager(this) + this.margin = new Margin(this) + this.background = new Background(this) + this.badge = new Badge(this) + this.search = new Search(this) + this.group = new Group(this) + this.area = new Area(this) + this.underline = new Underline(this) + this.strikeout = new Strikeout(this) + this.highlight = new Highlight(this) + this.previewer = new Previewer(this) + this.imageParticle = new ImageParticle(this) + this.laTexParticle = new LaTexParticle(this) + this.textParticle = new TextParticle(this) + this.tableParticle = new TableParticle(this) + this.tableTool = new TableTool(this) + this.tableOperate = new TableOperate(this) + this.pageNumber = new PageNumber(this) + this.lineNumber = new LineNumber(this) + this.waterMark = new Watermark(this) + this.placeholder = new Placeholder(this) + this.header = new Header(this, data.header) + this.footer = new Footer(this, data.footer) + this.hyperlinkParticle = new HyperlinkParticle(this) + this.dateParticle = new DateParticle(this) + this.separatorParticle = new SeparatorParticle(this) + this.pageBreakParticle = new PageBreakParticle(this) + this.superscriptParticle = new SuperscriptParticle() + this.subscriptParticle = new SubscriptParticle() + this.checkboxParticle = new CheckboxParticle(this) + this.radioParticle = new RadioParticle(this) + this.blockParticle = new BlockParticle(this) + this.listParticle = new ListParticle(this) + this.lineBreakParticle = new LineBreakParticle(this) + this.control = new Control(this) + this.pageBorder = new PageBorder(this) + + this.scrollObserver = new ScrollObserver(this) + this.selectionObserver = new SelectionObserver(this) + this.imageObserver = new ImageObserver() + new MouseObserver(this) + + this.canvasEvent = new CanvasEvent(this) + this.cursor = new Cursor(this, this.canvasEvent) + this.canvasEvent.register() + this.globalEvent = new GlobalEvent(this, this.canvasEvent) + this.globalEvent.register() + + this.workerManager = new WorkerManager(this) + new Actuator(this) + + const { letterClass } = options + this.LETTER_REG = new RegExp(`[${letterClass.join('')}]`) + this.WORD_LIKE_REG = new RegExp( + `${letterClass.map(letter => `[^${letter}][${letter}]`).join('|')}` + ) + this.rowList = [] + this.pageRowList = [] + this.painterStyle = null + this.painterOptions = null + this.visiblePageNoList = [] + this.intersectionPageNo = 0 + this.lazyRenderIntersectionObserver = null + this.printModeData = null + + // 打印模式优先设置打印数据 + if (this.mode === EditorMode.PRINT) { + this.setPrintData() + } + this.render({ + isInit: true, + isSetCursor: false, + isFirstRender: true + }) + } + + // 设置打印数据 + public setPrintData() { + this.printModeData = { + header: this.header.getElementList(), + main: this.elementList, + footer: this.footer.getElementList() + } + // 过滤控件辅助元素 + const clonePrintModeData = deepClone(this.printModeData) + const editorDataKeys: (keyof IEditorData)[] = ['header', 'main', 'footer'] + editorDataKeys.forEach(key => { + clonePrintModeData[key] = this.control.filterAssistElement( + clonePrintModeData[key] + ) + }) + this.setEditorData(clonePrintModeData) + } + + // 还原打印数据 + public clearPrintData() { + if (this.printModeData) { + this.setEditorData(this.printModeData) + this.printModeData = null + } + } + + public getLetterReg(): RegExp { + return this.LETTER_REG + } + + public getMode(): EditorMode { + return this.mode + } + + public setMode(payload: EditorMode) { + if (this.mode === payload) return + // 设置打印模式 + if (payload === EditorMode.PRINT) { + this.setPrintData() + } + // 取消打印模式 + if (this.mode === EditorMode.PRINT) { + this.clearPrintData() + } + this.clearSideEffect() + this.range.clearRange() + this.mode = payload + this.options.mode = payload + this.render({ + isSetCursor: false, + isSubmitHistory: false + }) + } + + public isReadonly() { + if (this.area.getActiveAreaInfo()?.area?.mode) { + return this.area.isReadonly() + } + switch (this.mode) { + case EditorMode.DESIGN: + return false + case EditorMode.READONLY: + case EditorMode.PRINT: + return true + case EditorMode.FORM: + return !this.control.getIsRangeWithinControl() + default: + return false + } + } + + public isDisabled() { + if (this.mode === EditorMode.DESIGN) return false + const { startIndex, endIndex } = this.range.getRange() + const elementList = this.getElementList() + // 优先判断表格单元格 + if (this.getTd()?.disabled) return true + if (startIndex === endIndex) { + const startElement = elementList[startIndex] + const nextElement = elementList[startIndex + 1] + return !!( + (startElement?.title?.disabled && + nextElement?.title?.disabled && + startElement.titleId === nextElement.titleId) || + (startElement?.control?.disabled && + nextElement?.control?.disabled && + startElement.controlId === nextElement.controlId) + ) + } + const selectionElementList = elementList.slice(startIndex + 1, endIndex + 1) + return selectionElementList.some( + element => element.title?.disabled || element.control?.disabled + ) + } + + public isDesignMode() { + return this.mode === EditorMode.DESIGN + } + + public isPrintMode() { + return this.mode === EditorMode.PRINT + } + + public getOriginalWidth(): number { + const { paperDirection, width, height } = this.options + return paperDirection === PaperDirection.VERTICAL ? width : height + } + + public getOriginalHeight(): number { + const { paperDirection, width, height } = this.options + return paperDirection === PaperDirection.VERTICAL ? height : width + } + + public getWidth(): number { + return Math.floor(this.getOriginalWidth() * this.options.scale) + } + + public getHeight(): number { + return Math.floor(this.getOriginalHeight() * this.options.scale) + } + + public getMainHeight(): number { + const pageHeight = this.getHeight() + return pageHeight - this.getMainOuterHeight() + } + + public getMainOuterHeight(): number { + const margins = this.getMargins() + const headerExtraHeight = this.header.getExtraHeight() + const footerExtraHeight = this.footer.getExtraHeight() + return margins[0] + margins[2] + headerExtraHeight + footerExtraHeight + } + + public getCanvasWidth(pageNo = -1): number { + const page = this.getPage(pageNo) + return page.width + } + + public getCanvasHeight(pageNo = -1): number { + const page = this.getPage(pageNo) + return page.height + } + + public getInnerWidth(): number { + const width = this.getWidth() + const margins = this.getMargins() + return width - margins[1] - margins[3] + } + + public getOriginalInnerWidth(): number { + const width = this.getOriginalWidth() + const margins = this.getOriginalMargins() + return width - margins[1] - margins[3] + } + + public getContextInnerWidth(): number { + const positionContext = this.position.getPositionContext() + if (positionContext.isTable) { + const { index, trIndex, tdIndex } = positionContext + const elementList = this.getOriginalElementList() + const td = elementList[index!].trList![trIndex!].tdList[tdIndex!] + const tdPadding = this.getTdPadding() + return td!.width! - tdPadding[1] - tdPadding[3] + } + return this.getOriginalInnerWidth() + } + + public getMargins(): IMargin { + return this.getOriginalMargins().map(m => m * this.options.scale) + } + + public getOriginalMargins(): number[] { + const { margins, paperDirection } = this.options + return paperDirection === PaperDirection.VERTICAL + ? margins + : [margins[1], margins[2], margins[3], margins[0]] + } + + public getPageGap(): number { + return this.options.pageGap * this.options.scale + } + + public getOriginalPageGap(): number { + return this.options.pageGap + } + + public getPageNumberBottom(): number { + const { + pageNumber: { bottom }, + scale + } = this.options + return bottom * scale + } + + public getMarginIndicatorSize(): number { + return this.options.marginIndicatorSize * this.options.scale + } + + public getDefaultBasicRowMarginHeight(): number { + return this.options.defaultBasicRowMarginHeight * this.options.scale + } + + public getHighlightMarginHeight(): number { + return this.options.highlightMarginHeight * this.options.scale + } + + public getTdPadding(): IPadding { + const { + table: { tdPadding }, + scale + } = this.options + return tdPadding.map(m => m * scale) + } + + public getContainer(): HTMLDivElement { + return this.container + } + + public getPageContainer(): HTMLDivElement { + return this.pageContainer + } + + public getVisiblePageNoList(): number[] { + return this.visiblePageNoList + } + + public setVisiblePageNoList(payload: number[]) { + this.visiblePageNoList = payload + if (this.listener.visiblePageNoListChange) { + this.listener.visiblePageNoListChange(this.visiblePageNoList) + } + if (this.eventBus.isSubscribe('visiblePageNoListChange')) { + this.eventBus.emit('visiblePageNoListChange', this.visiblePageNoList) + } + } + + public getIntersectionPageNo(): number { + return this.intersectionPageNo + } + + public setIntersectionPageNo(payload: number) { + this.intersectionPageNo = payload + if (this.listener.intersectionPageNoChange) { + this.listener.intersectionPageNoChange(this.intersectionPageNo) + } + if (this.eventBus.isSubscribe('intersectionPageNoChange')) { + this.eventBus.emit('intersectionPageNoChange', this.intersectionPageNo) + } + } + + public getPageNo(): number { + return this.pageNo + } + + public setPageNo(payload: number) { + this.pageNo = payload + } + + public getRenderCount(): number { + return this.renderCount + } + + public getPage(pageNo = -1): HTMLCanvasElement { + return this.pageList[~pageNo ? pageNo : this.pageNo] + } + + public getPageList(): HTMLCanvasElement[] { + return this.pageList + } + + public getPageCount(): number { + return this.pageList.length + } + + public getTableRowList(sourceElementList: IElement[]): IRow[] { + const positionContext = this.position.getPositionContext() + const { index, trIndex, tdIndex } = positionContext + return sourceElementList[index!].trList![trIndex!].tdList[tdIndex!].rowList! + } + + public getOriginalRowList() { + const zoneManager = this.getZone() + if (zoneManager.isHeaderActive()) { + return this.header.getRowList() + } + if (zoneManager.isFooterActive()) { + return this.footer.getRowList() + } + return this.rowList + } + + public getRowList(): IRow[] { + const positionContext = this.position.getPositionContext() + return positionContext.isTable + ? this.getTableRowList(this.getOriginalElementList()) + : this.getOriginalRowList() + } + + public getPageRowList(): IRow[][] { + return this.pageRowList + } + + public getCtx(): CanvasRenderingContext2D { + return this.ctxList[this.pageNo] + } + + public getOptions(): DeepRequired { + return this.options + } + + public getSearch(): Search { + return this.search + } + + public getGroup(): Group { + return this.group + } + + public getArea(): Area { + return this.area + } + + public getBadge(): Badge { + return this.badge + } + + public getHistoryManager(): HistoryManager { + return this.historyManager + } + + public getPosition(): Position { + return this.position + } + + public getZone(): Zone { + return this.zone + } + + public getRange(): RangeManager { + return this.range + } + + public getLineBreakParticle(): LineBreakParticle { + return this.lineBreakParticle + } + + public getTextParticle(): TextParticle { + return this.textParticle + } + + public getHeaderElementList(): IElement[] { + return this.header.getElementList() + } + + public getTableElementList(sourceElementList: IElement[]): IElement[] { + const positionContext = this.position.getPositionContext() + const { index, trIndex, tdIndex } = positionContext + return ( + sourceElementList[index!].trList?.[trIndex!].tdList[tdIndex!].value || [] + ) + } + + public getElementList(): IElement[] { + const positionContext = this.position.getPositionContext() + const elementList = this.getOriginalElementList() + return positionContext.isTable + ? this.getTableElementList(elementList) + : elementList + } + + public getMainElementList(): IElement[] { + const positionContext = this.position.getPositionContext() + return positionContext.isTable + ? this.getTableElementList(this.elementList) + : this.elementList + } + + public getOriginalElementList() { + const zoneManager = this.getZone() + if (zoneManager.isHeaderActive()) { + return this.getHeaderElementList() + } + if (zoneManager.isFooterActive()) { + return this.getFooterElementList() + } + return this.elementList + } + + public getOriginalMainElementList(): IElement[] { + return this.elementList + } + + public getFooterElementList(): IElement[] { + return this.footer.getElementList() + } + + public getTd(): ITd | null { + const positionContext = this.position.getPositionContext() + const { index, trIndex, tdIndex, isTable } = positionContext + if (isTable) { + const elementList = this.getOriginalElementList() + return elementList[index!].trList![trIndex!].tdList[tdIndex!] + } + return null + } + + public insertElementList( + payload: IElement[], + options: IInsertElementListOption = {} + ) { + if (!payload.length || !this.range.getIsCanInput()) return + const { startIndex, endIndex } = this.range.getRange() + if (!~startIndex && !~endIndex) return + const { isSubmitHistory = true } = options + formatElementList(payload, { + isHandleFirstElement: false, + editorOptions: this.options + }) + let curIndex = -1 + // 判断是否在控件内 + let activeControl = this.control.getActiveControl() + // 光标在控件内如果当前没有被激活,需要手动激活 + if (!activeControl && this.control.getIsRangeWithinControl()) { + this.control.initControl() + activeControl = this.control.getActiveControl() + } + if (activeControl && this.control.getIsRangeWithinControl()) { + curIndex = activeControl.setValue(payload, undefined, { + isIgnoreDisabledRule: true + }) + this.control.emitControlContentChange() + } else { + const elementList = this.getElementList() + const isCollapsed = startIndex === endIndex + const start = startIndex + 1 + if (!isCollapsed) { + this.spliceElementList(elementList, start, endIndex - startIndex) + } + this.spliceElementList(elementList, start, 0, payload) + curIndex = startIndex + payload.length + // 列表前如有换行符则删除-因为列表内已存在 + const preElement = elementList[start - 1] + if ( + payload[0].listId && + preElement && + !preElement.listId && + preElement?.value === ZERO && + (!preElement.type || preElement.type === ElementType.TEXT) + ) { + elementList.splice(startIndex, 1) + curIndex -= 1 + } + } + if (~curIndex) { + this.range.setRange(curIndex, curIndex) + this.render({ + curIndex, + isSubmitHistory + }) + } + } + + public appendElementList( + elementList: IElement[], + options: IAppendElementListOption = {} + ) { + if (!elementList.length) return + formatElementList(elementList, { + isHandleFirstElement: false, + editorOptions: this.options + }) + let curIndex: number + const { isPrepend, isSubmitHistory = true } = options + if (isPrepend) { + this.elementList.splice(1, 0, ...elementList) + curIndex = elementList.length + } else { + this.elementList.push(...elementList) + curIndex = this.elementList.length - 1 + } + this.range.setRange(curIndex, curIndex) + this.render({ + curIndex, + isSubmitHistory + }) + } + + public spliceElementList( + elementList: IElement[], + start: number, + deleteCount: number, + items?: IElement[], + options?: ISpliceElementListOption + ) { + const { isIgnoreDeletedRule = false } = options || {} + const { group, modeRule } = this.options + if (deleteCount > 0) { + // 当最后元素与开始元素列表信息不一致时:清除当前列表信息 + const endIndex = start + deleteCount + const endElement = elementList[endIndex] + const endElementListId = endElement?.listId + if ( + endElementListId && + elementList[start - 1]?.listId !== endElementListId + ) { + let startIndex = endIndex + while (startIndex < elementList.length) { + const curElement = elementList[startIndex] + if ( + curElement.listId !== endElementListId || + curElement.value === ZERO + ) { + break + } + delete curElement.listId + delete curElement.listType + delete curElement.listStyle + startIndex++ + } + } + // 非明确忽略删除规则 && 非设计模式 && 非光标在控件内(控件内控制) =》 校验删除规则 + if ( + !isIgnoreDeletedRule && + !this.isDesignMode() && + !this.control.getIsRangeWithinControl() + ) { + const tdDeletable = this.getTd()?.deletable + let deleteIndex = endIndex - 1 + while (deleteIndex >= start) { + const deleteElement = elementList[deleteIndex] + if ( + deleteElement?.hide || + deleteElement?.control?.hide || + deleteElement?.area?.hide || + (tdDeletable !== false && + deleteElement?.control?.deletable !== false && + (!deleteElement.controlId || + this.mode !== EditorMode.FORM || + !modeRule[this.mode].controlDeletableDisabled) && + deleteElement?.title?.deletable !== false && + (group.deletable !== false || !deleteElement.groupIds?.length) && + (deleteElement?.area?.deletable !== false || + deleteElement?.areaIndex !== 0)) + ) { + elementList.splice(deleteIndex, 1) + } + deleteIndex-- + } + } else { + elementList.splice(start, deleteCount) + } + } + // 循环添加,避免使用解构影响性能 + if (items?.length) { + for (let i = 0; i < items.length; i++) { + elementList.splice(start + i, 0, items[i]) + } + } + } + + public getCanvasEvent(): CanvasEvent { + return this.canvasEvent + } + + public getGlobalEvent(): GlobalEvent { + return this.globalEvent + } + + public getListener(): Listener { + return this.listener + } + + public getEventBus(): EventBus { + return this.eventBus + } + + public getOverride(): Override { + return this.override + } + + public getCursor(): Cursor { + return this.cursor + } + + public getPreviewer(): Previewer { + return this.previewer + } + + public getImageParticle(): ImageParticle { + return this.imageParticle + } + + public getTableTool(): TableTool { + return this.tableTool + } + + public getTableOperate(): TableOperate { + return this.tableOperate + } + + public getTableParticle(): TableParticle { + return this.tableParticle + } + + public getHeader(): Header { + return this.header + } + + public getFooter(): Footer { + return this.footer + } + + public getHyperlinkParticle(): HyperlinkParticle { + return this.hyperlinkParticle + } + + public getDateParticle(): DateParticle { + return this.dateParticle + } + + public getListParticle(): ListParticle { + return this.listParticle + } + + public getCheckboxParticle(): CheckboxParticle { + return this.checkboxParticle + } + + public getRadioParticle(): RadioParticle { + return this.radioParticle + } + + public getControl(): Control { + return this.control + } + + public getWorkerManager(): WorkerManager { + return this.workerManager + } + + public getImageObserver(): ImageObserver { + return this.imageObserver + } + + public getI18n(): I18n { + return this.i18n + } + + public getRowCount(): number { + return this.getRowList().length + } + + public async getDataURL(payload: IGetImageOption = {}): Promise { + const { pixelRatio, mode } = payload + // 放大像素比 + if (pixelRatio) { + this.setPagePixelRatio(pixelRatio) + } + // 不同模式 + const currentMode = this.mode + const isSwitchMode = !!mode && currentMode !== mode + if (isSwitchMode) { + this.setMode(mode) + } + this.render({ + isLazy: false, + isCompute: false, + isSetCursor: false, + isSubmitHistory: false + }) + await this.imageObserver.allSettled() + const dataUrlList = this.pageList.map(c => c.toDataURL()) + // 还原 + if (pixelRatio) { + this.setPagePixelRatio(null) + } + if (isSwitchMode) { + this.setMode(currentMode) + } + return dataUrlList + } + + public getPainterStyle(): IElementStyle | null { + return this.painterStyle && Object.keys(this.painterStyle).length + ? this.painterStyle + : null + } + + public getPainterOptions(): IPainterOption | null { + return this.painterOptions + } + + public setPainterStyle( + payload: IElementStyle | null, + options?: IPainterOption + ) { + this.painterStyle = payload + this.painterOptions = options || null + if (this.getPainterStyle()) { + this.pageList.forEach(c => (c.style.cursor = 'copy')) + } + } + + public setDefaultRange() { + if (!this.elementList.length) return + setTimeout(() => { + const curIndex = this.elementList.length - 1 + this.range.setRange(curIndex, curIndex) + this.range.setRangeStyle() + }) + } + + public getIsPagingMode(): boolean { + return this.options.pageMode === PageMode.PAGING + } + + public setPageMode(payload: PageMode) { + if (!payload || this.options.pageMode === payload) return + this.options.pageMode = payload + // 纸张大小重置 + if (payload === PageMode.PAGING) { + const { height } = this.options + const dpr = this.getPagePixelRatio() + const canvas = this.pageList[0] + canvas.style.height = `${height}px` + canvas.height = height * dpr + // canvas尺寸发生变化,上下文被重置 + this._initPageContext(this.ctxList[0]) + } else { + // 连页模式:移除懒加载监听&清空页眉页脚计算数据 + this._disconnectLazyRender() + this.header.recovery() + this.footer.recovery() + this.zone.setZone(EditorZone.MAIN) + } + const { startIndex } = this.range.getRange() + const isCollapsed = this.range.getIsCollapsed() + this.render({ + isSetCursor: true, + curIndex: startIndex, + isSubmitHistory: false + }) + // 重新定位避免事件监听丢失 + if (!isCollapsed) { + this.cursor.drawCursor({ + isShow: false + }) + } + // 回调 + setTimeout(() => { + if (this.listener.pageModeChange) { + this.listener.pageModeChange(payload) + } + if (this.eventBus.isSubscribe('pageModeChange')) { + this.eventBus.emit('pageModeChange', payload) + } + }) + } + + public setPageScale(payload: number) { + const dpr = this.getPagePixelRatio() + this.options.scale = payload + const width = this.getWidth() + const height = this.getHeight() + this.container.style.width = `${width}px` + this.pageList.forEach((p, i) => { + p.width = width * dpr + p.height = height * dpr + p.style.width = `${width}px` + p.style.height = `${height}px` + p.style.marginBottom = `${this.getPageGap()}px` + this._initPageContext(this.ctxList[i]) + }) + const cursorPosition = this.position.getCursorPosition() + this.render({ + isSubmitHistory: false, + isSetCursor: !!cursorPosition, + curIndex: cursorPosition?.index + }) + if (this.listener.pageScaleChange) { + this.listener.pageScaleChange(payload) + } + if (this.eventBus.isSubscribe('pageScaleChange')) { + this.eventBus.emit('pageScaleChange', payload) + } + } + + public getPagePixelRatio(): number { + return this.pagePixelRatio || window.devicePixelRatio + } + + public setPagePixelRatio(payload: number | null) { + if ( + (!this.pagePixelRatio && payload === window.devicePixelRatio) || + payload === this.pagePixelRatio + ) { + return + } + this.pagePixelRatio = payload + this.setPageDevicePixel() + } + + public setPageDevicePixel() { + const dpr = this.getPagePixelRatio() + const width = this.getWidth() + const height = this.getHeight() + this.pageList.forEach((p, i) => { + p.width = width * dpr + p.height = height * dpr + this._initPageContext(this.ctxList[i]) + }) + this.render({ + isSubmitHistory: false, + isSetCursor: false + }) + } + + public setPaperSize(width: number, height: number) { + this.options.width = width + this.options.height = height + const dpr = this.getPagePixelRatio() + const realWidth = this.getWidth() + const realHeight = this.getHeight() + this.container.style.width = `${realWidth}px` + this.pageList.forEach((p, i) => { + p.width = realWidth * dpr + p.height = realHeight * dpr + p.style.width = `${realWidth}px` + p.style.height = `${realHeight}px` + this._initPageContext(this.ctxList[i]) + }) + this.render({ + isSubmitHistory: false, + isSetCursor: false + }) + } + + public setPaperDirection(payload: PaperDirection) { + const dpr = this.getPagePixelRatio() + this.options.paperDirection = payload + const width = this.getWidth() + const height = this.getHeight() + this.container.style.width = `${width}px` + this.pageList.forEach((p, i) => { + p.width = width * dpr + p.height = height * dpr + p.style.width = `${width}px` + p.style.height = `${height}px` + this._initPageContext(this.ctxList[i]) + }) + this.render({ + isSubmitHistory: false, + isSetCursor: false + }) + } + + public setPaperMargin(payload: IMargin) { + this.options.margins = payload + this.render({ + isSubmitHistory: false, + isSetCursor: false + }) + } + + public getOriginValue( + options: IGetOriginValueOption = {} + ): Required { + const { pageNo } = options + let mainElementList = this.elementList + if ( + Number.isInteger(pageNo) && + pageNo! >= 0 && + pageNo! < this.pageRowList.length + ) { + mainElementList = this.pageRowList[pageNo!].flatMap( + row => row.elementList + ) + } + const data: Required = { + header: this.getHeaderElementList(), + main: mainElementList, + footer: this.getFooterElementList() + } + return data + } + + public getValue(options: IGetValueOption = {}): IEditorResult { + const originData = this.getOriginValue(options) + const { extraPickAttrs } = options + const data: IEditorData = { + header: zipElementList(originData.header, { + extraPickAttrs + }), + main: zipElementList(originData.main, { + extraPickAttrs, + isClassifyArea: true + }), + footer: zipElementList(originData.footer, { + extraPickAttrs + }) + } + return { + version, + data, + options: deepClone(this.options) + } + } + + public setValue(payload: Partial, options?: ISetValueOption) { + const { header, main, footer } = deepClone(payload) + if (!header && !main && !footer) return + const { isSetCursor = false } = options || {} + const pageComponentData = [header, main, footer] + pageComponentData.forEach(data => { + if (!data) return + formatElementList(data, { + editorOptions: this.options, + isForceCompensation: true + }) + }) + this.setEditorData({ + header, + main, + footer + }) + // 渲染&计算&清空历史记录 + this.historyManager.recovery() + const curIndex = isSetCursor + ? main?.length + ? main.length - 1 + : 0 + : undefined + if (curIndex !== undefined) { + this.range.setRange(curIndex, curIndex) + } + this.render({ + curIndex, + isSetCursor, + isFirstRender: true + }) + } + + public setEditorData(payload: Partial) { + const { header, main, footer } = payload + if (header) { + this.header.setElementList(header) + } + if (main) { + this.elementList = main + } + if (footer) { + this.footer.setElementList(footer) + } + } + + private _wrapContainer(rootContainer: HTMLElement): HTMLDivElement { + const container = document.createElement('div') + rootContainer.append(container) + return container + } + + private _formatContainer() { + // 容器宽度需跟随纸张宽度 + this.container.style.position = 'relative' + this.container.style.width = `${this.getWidth()}px` + this.container.setAttribute(EDITOR_COMPONENT, EditorComponent.MAIN) + } + + private _createPageContainer(): HTMLDivElement { + const pageContainer = document.createElement('div') + pageContainer.classList.add(`${EDITOR_PREFIX}-page-container`) + this.container.append(pageContainer) + return pageContainer + } + + private _createPage(pageNo: number) { + const width = this.getWidth() + const height = this.getHeight() + const canvas = document.createElement('canvas') + canvas.style.width = `${width}px` + canvas.style.height = `${height}px` + canvas.style.display = 'block' + canvas.style.backgroundColor = '#ffffff' + canvas.style.marginBottom = `${this.getPageGap()}px` + canvas.setAttribute('data-index', String(pageNo)) + this.pageContainer.append(canvas) + // 调整分辨率 + const dpr = this.getPagePixelRatio() + canvas.width = width * dpr + canvas.height = height * dpr + canvas.style.cursor = 'text' + const ctx = canvas.getContext('2d')! + // 初始化上下文配置 + this._initPageContext(ctx) + // 缓存上下文 + this.pageList.push(canvas) + this.ctxList.push(ctx) + } + + private _initPageContext(ctx: CanvasRenderingContext2D) { + const dpr = this.getPagePixelRatio() + ctx.scale(dpr, dpr) + // 重置以下属性是因部分浏览器(chrome)会应用css样式 + ctx.letterSpacing = '0px' + ctx.wordSpacing = '0px' + ctx.direction = 'ltr' + } + + public getElementFont(el: IElement, scale = 1): string { + const { defaultSize, defaultFont } = this.options + const font = el.font || defaultFont + const size = el.actualSize || el.size || defaultSize + return `${el.italic ? 'italic ' : ''}${el.bold ? 'bold ' : ''}${ + size * scale + }px ${font}` + } + + public getElementSize(el: IElement) { + return el.actualSize || el.size || this.options.defaultSize + } + + public getElementRowMargin(el: IElement) { + const { defaultBasicRowMarginHeight, defaultRowMargin, scale } = + this.options + return ( + defaultBasicRowMarginHeight * (el.rowMargin ?? defaultRowMargin) * scale + ) + } + + public computeRowList(payload: IComputeRowListPayload) { + const { + innerWidth, + elementList, + isPagingMode = false, + isFromTable = false, + startX = 0, + startY = 0, + pageHeight = 0, + mainOuterHeight = 0, + surroundElementList = [] + } = payload + const { + defaultSize, + defaultRowMargin, + scale, + table: { tdPadding, defaultTrMinHeight }, + defaultTabWidth + } = this.options + const defaultBasicRowMarginHeight = this.getDefaultBasicRowMarginHeight() + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') as CanvasRenderingContext2D + // 计算列表偏移宽度 + const listStyleMap = this.listParticle.computeListStyle(ctx, elementList) + const rowList: IRow[] = [] + if (elementList.length) { + rowList.push({ + width: 0, + height: 0, + ascent: 0, + elementList: [], + startIndex: 0, + rowIndex: 0, + rowFlex: elementList?.[0]?.rowFlex || elementList?.[1]?.rowFlex + }) + } + // 起始位置及页码计算 + let x = startX + let y = startY + let pageNo = 0 + // 列表位置 + let listId: string | undefined + let listIndex = 0 + // 控件最小宽度 + let controlRealWidth = 0 + for (let i = 0; i < elementList.length; i++) { + const curRow: IRow = rowList[rowList.length - 1] + const element = elementList[i] + const rowMargin = + defaultBasicRowMarginHeight * (element.rowMargin ?? defaultRowMargin) + const metrics: IElementMetrics = { + width: 0, + height: 0, + boundingBoxAscent: 0, + boundingBoxDescent: 0 + } + // 实际可用宽度 + const offsetX = + curRow.offsetX || + (element.listId && listStyleMap.get(element.listId)) || + 0 + const availableWidth = innerWidth - offsetX + // 增加起始位置坐标偏移量 + const isStartElement = curRow.elementList.length === 1 + x += isStartElement ? offsetX : 0 + y += isStartElement ? curRow.offsetY || 0 : 0 + if ( + (element.hide || element.control?.hide || element.area?.hide) && + !this.isDesignMode() + ) { + const preElement = curRow.elementList[curRow.elementList.length - 1] + metrics.height = + preElement?.metrics.height || this.options.defaultSize * scale + metrics.boundingBoxAscent = preElement?.metrics.boundingBoxAscent || 0 + metrics.boundingBoxDescent = preElement?.metrics.boundingBoxDescent || 0 + } else if ( + element.type === ElementType.IMAGE || + element.type === ElementType.LATEX + ) { + // 浮动图片无需计算数据 + if ( + element.imgDisplay === ImageDisplay.SURROUND || + element.imgDisplay === ImageDisplay.FLOAT_TOP || + element.imgDisplay === ImageDisplay.FLOAT_BOTTOM + ) { + metrics.width = 0 + metrics.height = 0 + metrics.boundingBoxDescent = 0 + } else { + const elementWidth = element.width! * scale + const elementHeight = element.height! * scale + // 图片超出尺寸后自适应(图片大小大于可用宽度时) + if (elementWidth > availableWidth) { + const adaptiveHeight = + (elementHeight * availableWidth) / elementWidth + element.width = availableWidth / scale + element.height = adaptiveHeight / scale + metrics.width = availableWidth + metrics.height = adaptiveHeight + metrics.boundingBoxDescent = adaptiveHeight + } else { + metrics.width = elementWidth + metrics.height = elementHeight + metrics.boundingBoxDescent = elementHeight + } + } + metrics.boundingBoxAscent = 0 + } else if (element.type === ElementType.TABLE) { + const tdPaddingWidth = tdPadding[1] + tdPadding[3] + const tdPaddingHeight = tdPadding[0] + tdPadding[2] + // 表格分页处理进度:https://github.com/Hufe921/canvas-editor/issues/41 + // 查看后续表格是否属于同一个源表格-存在即合并 + if (element.pagingId) { + let tableIndex = i + 1 + let combineCount = 0 + while (tableIndex < elementList.length) { + const nextElement = elementList[tableIndex] + if (nextElement.pagingId === element.pagingId) { + const nexTrList = nextElement.trList!.filter( + tr => !tr.pagingRepeat + ) + element.trList!.push(...nexTrList) + element.height! += nextElement.height! + tableIndex++ + combineCount++ + } else { + break + } + } + if (combineCount) { + elementList.splice(i + 1, combineCount) + } + } + element.pagingIndex = element.pagingIndex ?? 0 + const trList = element.trList! + // 计算前移除上一次的高度 + for (let t = 0; t < trList.length; t++) { + const tr = trList[t] + tr.height = tr.minHeight || defaultTrMinHeight + tr.minHeight = tr.height + } + // 计算表格行列 + this.tableParticle.computeRowColInfo(element) + // 计算表格内元素信息 + for (let t = 0; t < trList.length; t++) { + const tr = trList[t] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const rowList = this.computeRowList({ + innerWidth: (td.width! - tdPaddingWidth) * scale, + elementList: td.value, + isFromTable: true, + isPagingMode + }) + const rowHeight = rowList.reduce((pre, cur) => pre + cur.height, 0) + td.rowList = rowList + // 移除缩放导致的行高变化-渲染时会进行缩放调整 + const curTdHeight = rowHeight / scale + tdPaddingHeight + // 内容高度大于当前单元格高度需增加 + if (td.height! < curTdHeight) { + const extraHeight = curTdHeight - td.height! + const changeTr = trList[t + td.rowspan - 1] + changeTr.height += extraHeight + changeTr.tdList.forEach(changeTd => { + changeTd.height! += extraHeight + if (!changeTd.realHeight) { + changeTd.realHeight = changeTd.height! + } else { + changeTd.realHeight! += extraHeight + } + }) + } + // 当前单元格最小高度及真实高度(包含跨列) + let curTdMinHeight = 0 + let curTdRealHeight = 0 + let i = 0 + while (i < td.rowspan) { + const curTr = trList[i + t] || trList[t] + curTdMinHeight += curTr.minHeight! + curTdRealHeight += curTr.height! + i++ + } + td.realMinHeight = curTdMinHeight + td.realHeight = curTdRealHeight + td.mainHeight = curTdHeight + } + } + // 单元格高度大于实际内容高度需减少 + const reduceTrList = this.tableParticle.getTrListGroupByCol(trList) + for (let t = 0; t < reduceTrList.length; t++) { + const tr = reduceTrList[t] + let reduceHeight = -1 + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const curTdRealHeight = td.realHeight! + const curTdHeight = td.mainHeight! + const curTdMinHeight = td.realMinHeight! + // 获取最大可减少高度 + const curReduceHeight = + curTdHeight < curTdMinHeight + ? curTdRealHeight - curTdMinHeight + : curTdRealHeight - curTdHeight + if (!~reduceHeight || curReduceHeight < reduceHeight) { + reduceHeight = curReduceHeight + } + } + if (reduceHeight > 0) { + const changeTr = trList[t] + changeTr.height -= reduceHeight + changeTr.tdList.forEach(changeTd => { + changeTd.height! -= reduceHeight + changeTd.realHeight! -= reduceHeight + }) + } + } + // 需要重新计算表格内值 + this.tableParticle.computeRowColInfo(element) + // 计算出表格高度 + const tableHeight = this.tableParticle.getTableHeight(element) + const tableWidth = this.tableParticle.getTableWidth(element) + element.width = tableWidth + element.height = tableHeight + const elementWidth = tableWidth * scale + const elementHeight = tableHeight * scale + metrics.width = elementWidth + metrics.height = elementHeight + metrics.boundingBoxDescent = elementHeight + metrics.boundingBoxAscent = -rowMargin + // 后一个元素也是表格则移除行间距 + if (elementList[i + 1]?.type === ElementType.TABLE) { + metrics.boundingBoxAscent -= rowMargin + } + // 表格分页处理(拆分表格) + if (isPagingMode) { + const height = this.getHeight() + const marginHeight = this.getMainOuterHeight() + let curPagePreHeight = marginHeight + for (let r = 0; r < rowList.length; r++) { + const row = rowList[r] + const rowOffsetY = row.offsetY || 0 + if ( + row.height + curPagePreHeight + rowOffsetY > height || + rowList[r - 1]?.isPageBreak + ) { + curPagePreHeight = marginHeight + row.height + rowOffsetY + } else { + curPagePreHeight += row.height + rowOffsetY + } + } + // 当前剩余高度是否能容下当前表格第一行(可拆分)的高度,排除掉表头类型 + const rowMarginHeight = rowMargin * 2 * scale + const firstTrHeight = element.trList![0].height! * scale + if ( + curPagePreHeight + firstTrHeight + rowMarginHeight > height || + (element.pagingIndex !== 0 && element.trList![0].pagingRepeat) + ) { + // 无可拆分行则切换至新页 + curPagePreHeight = marginHeight + } + // 表格高度超过页面高度开始截断行 + if (curPagePreHeight + rowMarginHeight + elementHeight > height) { + const trList = element.trList! + // 计算需要移除的行数 + let deleteStart = 0 + let deleteCount = 0 + let preTrHeight = 0 + // 大于一行时再拆分避免循环 + if (trList.length > 1) { + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + const trHeight = tr.height * scale + if ( + curPagePreHeight + rowMarginHeight + preTrHeight + trHeight > + height + ) { + // 当前行存在跨行中断-暂时忽略分页 + const rowColCount = tr.tdList.reduce( + (pre, cur) => pre + cur.colspan, + 0 + ) + if (element.colgroup?.length !== rowColCount) { + deleteCount = 0 + } + break + } else { + deleteStart = r + 1 + deleteCount = trList.length - deleteStart + preTrHeight += trHeight + } + } + } + if (deleteCount) { + const cloneTrList = trList.splice(deleteStart, deleteCount) + const cloneTrHeight = cloneTrList.reduce( + (pre, cur) => pre + cur.height, + 0 + ) + const cloneTrRealHeight = cloneTrHeight * scale + const pagingId = element.pagingId || getUUID() + element.pagingId = pagingId + element.height -= cloneTrHeight + metrics.height -= cloneTrRealHeight + metrics.boundingBoxDescent -= cloneTrRealHeight + // 追加拆分表格 + const cloneElement = deepClone(element) + cloneElement.pagingId = pagingId + cloneElement.pagingIndex = element.pagingIndex! + 1 + // 处理分页重复表头 + const repeatTrList = trList.filter(tr => tr.pagingRepeat) + if (repeatTrList.length) { + const cloneRepeatTrList = deepClone(repeatTrList) + cloneRepeatTrList.forEach(tr => (tr.id = getUUID())) + cloneTrList.unshift(...cloneRepeatTrList) + } + cloneElement.trList = cloneTrList + cloneElement.id = getUUID() + this.spliceElementList(elementList, i + 1, 0, [cloneElement]) + } + } + // 表格经过分页处理-需要处理上下文 + if (element.pagingId) { + const positionContext = this.position.getPositionContext() + if (positionContext.isTable) { + // 查找光标所在表格索引(根据trId搜索) + let newPositionContextIndex = -1 + let newPositionContextTrIndex = -1 + let tableIndex = i + while (tableIndex < elementList.length) { + const curElement = elementList[tableIndex] + if (curElement.pagingId !== element.pagingId) break + const trIndex = curElement.trList!.findIndex( + r => r.id === positionContext.trId + ) + if (~trIndex) { + newPositionContextIndex = tableIndex + newPositionContextTrIndex = trIndex + break + } + tableIndex++ + } + if (~newPositionContextIndex) { + positionContext.index = newPositionContextIndex + positionContext.trIndex = newPositionContextTrIndex + this.position.setPositionContext(positionContext) + } + } + } + } + } else if (element.type === ElementType.SEPARATOR) { + const { + separator: { lineWidth } + } = this.options + element.width = availableWidth / scale + metrics.width = availableWidth + metrics.height = lineWidth * scale + metrics.boundingBoxAscent = -rowMargin + metrics.boundingBoxDescent = -rowMargin + metrics.height + } else if (element.type === ElementType.PAGE_BREAK) { + element.width = availableWidth / scale + metrics.width = availableWidth + metrics.height = defaultSize + } else if ( + element.type === ElementType.RADIO || + element.controlComponent === ControlComponent.RADIO + ) { + const { width, height, gap } = this.options.radio + const elementWidth = width + gap * 2 + element.width = elementWidth + metrics.width = elementWidth * scale + metrics.height = height * scale + } else if ( + element.type === ElementType.CHECKBOX || + element.controlComponent === ControlComponent.CHECKBOX + ) { + const { width, height, gap } = this.options.checkbox + const elementWidth = width + gap * 2 + element.width = elementWidth + metrics.width = elementWidth * scale + metrics.height = height * scale + } else if (element.type === ElementType.TAB) { + metrics.width = defaultTabWidth * scale + metrics.height = defaultSize * scale + metrics.boundingBoxDescent = 0 + metrics.boundingBoxAscent = metrics.height + } else if (element.type === ElementType.BLOCK) { + if (!element.width) { + metrics.width = availableWidth + } else { + const elementWidth = element.width * scale + metrics.width = Math.min(elementWidth, availableWidth) + } + metrics.height = element.height! * scale + metrics.boundingBoxDescent = metrics.height + metrics.boundingBoxAscent = 0 + } else { + // 设置上下标真实字体尺寸 + const size = element.size || defaultSize + if ( + element.type === ElementType.SUPERSCRIPT || + element.type === ElementType.SUBSCRIPT + ) { + element.actualSize = Math.ceil(size * 0.6) + } + metrics.height = (element.actualSize || size) * scale + ctx.font = this.getElementFont(element) + const fontMetrics = this.textParticle.measureText(ctx, element) + metrics.width = fontMetrics.width * scale + if (element.letterSpacing) { + metrics.width += element.letterSpacing * scale + } + metrics.boundingBoxAscent = + (element.value === ZERO + ? element.size || defaultSize + : fontMetrics.actualBoundingBoxAscent) * scale + metrics.boundingBoxDescent = + fontMetrics.actualBoundingBoxDescent * scale + if (element.type === ElementType.SUPERSCRIPT) { + metrics.boundingBoxAscent += metrics.height / 2 + } else if (element.type === ElementType.SUBSCRIPT) { + metrics.boundingBoxDescent += metrics.height / 2 + } + } + const ascent = + !element.hide && + ((element.imgDisplay !== ImageDisplay.INLINE && + element.type === ElementType.IMAGE) || + element.type === ElementType.LATEX) + ? metrics.height + rowMargin + : metrics.boundingBoxAscent + rowMargin + const height = + rowMargin + + metrics.boundingBoxAscent + + metrics.boundingBoxDescent + + rowMargin + const rowElement: IRowElement = Object.assign(element, { + metrics, + left: 0, + style: this.getElementFont(element, scale) + }) + // 暂时只考虑非换行场景:控件开始时统计宽度,结束时消费宽度及还原 + if (rowElement.control?.minWidth) { + if (rowElement.controlComponent) { + controlRealWidth += metrics.width + } + if (rowElement.controlComponent === ControlComponent.POSTFIX) { + // 设置最小宽度控件属性(字符偏移量) + this.control.setMinWidthControlInfo({ + row: curRow, + rowElement, + availableWidth, + controlRealWidth + }) + controlRealWidth = 0 + } + } + // 超过限定宽度 + const preElement = elementList[i - 1] + let nextElement = elementList[i + 1] + // 累计行宽 + 当前元素宽度 + 排版宽度(英文单词整体宽度 + 后面标点符号宽度) + let curRowWidth = curRow.width + metrics.width + if (this.options.wordBreak === WordBreak.BREAK_WORD) { + if ( + (!preElement?.type || preElement?.type === ElementType.TEXT) && + (!element.type || element.type === ElementType.TEXT) + ) { + // 英文单词 + const word = `${preElement?.value || ''}${element.value}` + if (this.WORD_LIKE_REG.test(word)) { + const { width, endElement } = this.textParticle.measureWord( + ctx, + elementList, + i + ) + // 单词宽度大于行可用宽度,无需折行 + const wordWidth = width * scale + if (wordWidth <= availableWidth) { + curRowWidth += wordWidth + nextElement = endElement + } + } + // 标点符号 + const punctuationWidth = this.textParticle.measurePunctuationWidth( + ctx, + nextElement + ) + curRowWidth += punctuationWidth * scale + } + } + // 列表信息 + if (element.listId) { + if (element.listId !== listId) { + listIndex = 0 + } else if (element.value === ZERO && !element.listWrap) { + listIndex++ + } + } + listId = element.listId + // 计算四周环绕导致的元素偏移量 + const surroundPosition = this.position.setSurroundPosition({ + pageNo, + rowElement, + row: curRow, + rowElementRect: { + x, + y, + height, + width: metrics.width + }, + availableWidth, + surroundElementList + }) + x = surroundPosition.x + curRowWidth += surroundPosition.rowIncreaseWidth + x += metrics.width + // 是否强制换行 + const isForceBreak = + element.type === ElementType.SEPARATOR || + element.type === ElementType.TABLE || + preElement?.type === ElementType.TABLE || + preElement?.type === ElementType.BLOCK || + element.type === ElementType.BLOCK || + preElement?.imgDisplay === ImageDisplay.INLINE || + element.imgDisplay === ImageDisplay.INLINE || + preElement?.listId !== element.listId || + (preElement?.areaId !== element.areaId && !element.area?.hide) || + (element.control?.flexDirection === FlexDirection.COLUMN && + (element.controlComponent === ControlComponent.CHECKBOX || + element.controlComponent === ControlComponent.RADIO) && + preElement?.controlComponent === ControlComponent.VALUE) || + (i !== 0 && element.value === ZERO && !element.area?.hide) + // 是否宽度不足导致换行 + const isWidthNotEnough = curRowWidth > availableWidth + const isWrap = isForceBreak || isWidthNotEnough + // 新行数据处理 + if (isWrap) { + const row: IRow = { + width: metrics.width, + height, + startIndex: i, + elementList: [rowElement], + ascent, + rowIndex: curRow.rowIndex + 1, + rowFlex: elementList[i]?.rowFlex || elementList[i + 1]?.rowFlex, + isPageBreak: element.type === ElementType.PAGE_BREAK + } + // 控件缩进 + if ( + rowElement.controlComponent !== ControlComponent.PREFIX && + rowElement.control?.indentation === ControlIndentation.VALUE_START + ) { + // 查找到非前缀的第一个元素位置 + const preStartIndex = curRow.elementList.findIndex( + el => + el.controlId === rowElement.controlId && + el.controlComponent !== ControlComponent.PREFIX + ) + if (~preStartIndex) { + const preRowPositionList = this.position.computeRowPosition({ + row: curRow, + innerWidth: this.getInnerWidth() + }) + const valueStartPosition = preRowPositionList[preStartIndex] + if (valueStartPosition) { + row.offsetX = valueStartPosition.coordinate.leftTop[0] + } + } + } + // 列表缩进 + if (element.listId) { + row.isList = true + row.offsetX = listStyleMap.get(element.listId!) + row.listIndex = listIndex + } + // Y轴偏移量 + row.offsetY = + !isFromTable && + element.area?.top && + element.areaId !== elementList[i - 1]?.areaId + ? element.area.top * scale + : 0 + rowList.push(row) + } else { + curRow.width += metrics.width + // 减小块元素前第一行空行行高 + if ( + i === 0 && + (getIsBlockElement(elementList[1]) || !!elementList[1]?.areaId) + ) { + curRow.height = defaultBasicRowMarginHeight + curRow.ascent = defaultBasicRowMarginHeight + } else if (curRow.height < height) { + curRow.height = height + curRow.ascent = ascent + } + curRow.elementList.push(rowElement) + } + // 行结束时逻辑 + if (isWrap || i === elementList.length - 1) { + // 换行原因:宽度不足 + curRow.isWidthNotEnough = isWidthNotEnough && !isForceBreak + // 两端对齐、分散对齐 + if ( + !curRow.isSurround && + (preElement?.rowFlex === RowFlex.JUSTIFY || + (preElement?.rowFlex === RowFlex.ALIGNMENT && + curRow.isWidthNotEnough)) + ) { + // 忽略换行符及尾部元素间隔设置 + const rowElementList = + curRow.elementList[0]?.value === ZERO + ? curRow.elementList.slice(1) + : curRow.elementList + const gap = + (availableWidth - curRow.width) / (rowElementList.length - 1) + for (let e = 0; e < rowElementList.length - 1; e++) { + const el = rowElementList[e] + el.metrics.width += gap + } + curRow.width = availableWidth + } + } + // 重新计算坐标、页码、下一行首行元素环绕交叉 + if (isWrap) { + x = startX + y += curRow.height + if ( + isPagingMode && + !isFromTable && + pageHeight && + (y - startY + mainOuterHeight + height > pageHeight || + element.type === ElementType.PAGE_BREAK) + ) { + y = startY + // 删除多余四周环绕型元素 + deleteSurroundElementList(surroundElementList, pageNo) + pageNo += 1 + } + // 计算下一行第一个元素是否存在环绕交叉 + rowElement.left = 0 + const nextRow = rowList[rowList.length - 1] + const surroundPosition = this.position.setSurroundPosition({ + pageNo, + rowElement, + row: nextRow, + rowElementRect: { + x, + y, + height, + width: metrics.width + }, + availableWidth, + surroundElementList + }) + x = surroundPosition.x + x += metrics.width + } + } + return rowList + } + + private _computePageList(): IRow[][] { + const pageRowList: IRow[][] = [[]] + const { + pageMode, + pageNumber: { maxPageNo } + } = this.options + const height = this.getHeight() + const marginHeight = this.getMainOuterHeight() + let pageHeight = marginHeight + let pageNo = 0 + if (pageMode === PageMode.CONTINUITY) { + pageRowList[0] = this.rowList + // 重置高度 + pageHeight += this.rowList.reduce( + (pre, cur) => pre + cur.height + (cur.offsetY || 0), + 0 + ) + const dpr = this.getPagePixelRatio() + const pageDom = this.pageList[0] + const pageDomHeight = Number(pageDom.style.height.replace('px', '')) + if (pageHeight > pageDomHeight) { + pageDom.style.height = `${pageHeight}px` + pageDom.height = pageHeight * dpr + } else { + const reduceHeight = pageHeight < height ? height : pageHeight + pageDom.style.height = `${reduceHeight}px` + pageDom.height = reduceHeight * dpr + } + this._initPageContext(this.ctxList[0]) + } else { + for (let i = 0; i < this.rowList.length; i++) { + const row = this.rowList[i] + const rowOffsetY = row.offsetY || 0 + if ( + row.height + rowOffsetY + pageHeight > height || + this.rowList[i - 1]?.isPageBreak + ) { + if (Number.isInteger(maxPageNo) && pageNo >= maxPageNo!) { + this.elementList = this.elementList.slice(0, row.startIndex) + break + } + pageHeight = marginHeight + row.height + rowOffsetY + pageRowList.push([row]) + pageNo++ + } else { + pageHeight += row.height + rowOffsetY + pageRowList[pageNo].push(row) + } + } + } + return pageRowList + } + + private _drawHighlight( + ctx: CanvasRenderingContext2D, + payload: IDrawRowPayload + ) { + const { rowList, positionList, elementList } = payload + const marginHeight = this.getDefaultBasicRowMarginHeight() + const highlightMarginHeight = this.getHighlightMarginHeight() + for (let i = 0; i < rowList.length; i++) { + const curRow = rowList[i] + for (let j = 0; j < curRow.elementList.length; j++) { + const element = curRow.elementList[j] + const preElement = curRow.elementList[j - 1] + // 高亮配置:元素 > 控件配置 + const highlight = + element.highlight || + this.control.getControlHighlight(elementList, curRow.startIndex + j) + if (highlight) { + // 高亮元素相连需立即绘制,并记录下一元素坐标 + if ( + preElement && + preElement.highlight && + preElement.highlight !== element.highlight + ) { + this.highlight.render(ctx) + } + // 当前元素位置信息记录 + const { + coordinate: { + leftTop: [x, y] + } + } = positionList[curRow.startIndex + j] + // 元素向左偏移量 + const offsetX = element.left || 0 + this.highlight.recordFillInfo( + ctx, + x - offsetX, + y + marginHeight - highlightMarginHeight, // 先减去行margin,再加上高亮margin + element.metrics.width + offsetX, + curRow.height - 2 * marginHeight + 2 * highlightMarginHeight, + highlight + ) + } else if (preElement?.highlight) { + // 之前是高亮元素,当前不是需立即绘制 + this.highlight.render(ctx) + } + } + this.highlight.render(ctx) + } + } + + public drawRow(ctx: CanvasRenderingContext2D, payload: IDrawRowPayload) { + // 优先绘制高亮元素 + this._drawHighlight(ctx, payload) + // 绘制元素、下划线、删除线、选区 + const { + scale, + table: { tdPadding }, + group, + lineBreak + } = this.options + const { + rowList, + pageNo, + elementList, + positionList, + startIndex, + zone, + isDrawLineBreak = !lineBreak.disabled + } = payload + const isPrintMode = this.mode === EditorMode.PRINT + const { isCrossRowCol, tableId } = this.range.getRange() + let index = startIndex + for (let i = 0; i < rowList.length; i++) { + const curRow = rowList[i] + // 选区绘制记录 + const rangeRecord: IElementFillRect = { + x: 0, + y: 0, + width: 0, + height: 0 + } + let tableRangeElement: IElement | null = null + for (let j = 0; j < curRow.elementList.length; j++) { + const element = curRow.elementList[j] + const metrics = element.metrics + // 当前元素位置信息 + const { + ascent: offsetY, + coordinate: { + leftTop: [x, y] + } + } = positionList[curRow.startIndex + j] + const preElement = curRow.elementList[j - 1] + // 元素绘制 + if ( + (element.hide || element.control?.hide || element.area?.hide) && + !this.isDesignMode() + ) { + // 控件隐藏时不绘制 + this.textParticle.complete() + } else if (element.type === ElementType.IMAGE) { + this.textParticle.complete() + // 浮动图片单独绘制 + if ( + element.imgDisplay !== ImageDisplay.SURROUND && + element.imgDisplay !== ImageDisplay.FLOAT_TOP && + element.imgDisplay !== ImageDisplay.FLOAT_BOTTOM + ) { + this.imageParticle.render(ctx, element, x, y + offsetY) + } + } else if (element.type === ElementType.LATEX) { + this.textParticle.complete() + this.laTexParticle.render(ctx, element, x, y + offsetY) + } else if (element.type === ElementType.TABLE) { + if (isCrossRowCol) { + rangeRecord.x = x + rangeRecord.y = y + tableRangeElement = element + } + this.tableParticle.render(ctx, element, x, y) + } else if (element.type === ElementType.HYPERLINK) { + this.textParticle.complete() + this.hyperlinkParticle.render(ctx, element, x, y + offsetY) + } else if (element.type === ElementType.DATE) { + const nextElement = curRow.elementList[j + 1] + // 释放之前的 + if (!preElement || preElement.dateId !== element.dateId) { + this.textParticle.complete() + } + this.textParticle.record(ctx, element, x, y + offsetY) + if (!nextElement || nextElement.dateId !== element.dateId) { + // 手动触发渲染 + this.textParticle.complete() + } + } else if (element.type === ElementType.SUPERSCRIPT) { + this.textParticle.complete() + this.superscriptParticle.render(ctx, element, x, y + offsetY) + } else if (element.type === ElementType.SUBSCRIPT) { + this.underline.render(ctx) + this.textParticle.complete() + this.subscriptParticle.render(ctx, element, x, y + offsetY) + } else if (element.type === ElementType.SEPARATOR) { + this.separatorParticle.render(ctx, element, x, y) + } else if (element.type === ElementType.PAGE_BREAK) { + if (this.mode !== EditorMode.CLEAN && !isPrintMode) { + this.pageBreakParticle.render(ctx, element, x, y) + } + } else if ( + element.type === ElementType.CHECKBOX || + element.controlComponent === ControlComponent.CHECKBOX + ) { + this.textParticle.complete() + this.checkboxParticle.render({ + ctx, + x, + y: y + offsetY, + index: j, + row: curRow + }) + } else if ( + element.type === ElementType.RADIO || + element.controlComponent === ControlComponent.RADIO + ) { + this.textParticle.complete() + this.radioParticle.render({ + ctx, + x, + y: y + offsetY, + index: j, + row: curRow + }) + } else if (element.type === ElementType.TAB) { + this.textParticle.complete() + } else if ( + element.rowFlex === RowFlex.ALIGNMENT || + element.rowFlex === RowFlex.JUSTIFY + ) { + // 如果是两端对齐,因canvas目前不支持letterSpacing需单独绘制文本 + this.textParticle.record(ctx, element, x, y + offsetY) + this.textParticle.complete() + } else if (element.type === ElementType.BLOCK) { + this.textParticle.complete() + this.blockParticle.render(ctx, pageNo, element, x, y + offsetY) + } else { + // 如果当前元素设置左偏移,则上一元素立即绘制 + if (element.left) { + this.textParticle.complete() + } + this.textParticle.record(ctx, element, x, y + offsetY) + // 如果设置字宽、字间距、标点符号(避免浏览器排版缩小间距)需单独绘制 + if ( + element.width || + element.letterSpacing || + PUNCTUATION_REG.test(element.value) + ) { + this.textParticle.complete() + } + } + // 换行符绘制 + if ( + isDrawLineBreak && + !isPrintMode && + this.mode !== EditorMode.CLEAN && + !curRow.isWidthNotEnough && + j === curRow.elementList.length - 1 + ) { + this.lineBreakParticle.render(ctx, element, x, y + curRow.height / 2) + } + // 边框绘制(目前仅支持控件) + if (element.control?.border) { + // 不同控件边框立刻绘制 + if ( + preElement?.control?.border && + preElement.controlId !== element.controlId + ) { + this.control.drawBorder(ctx) + } + // 当前元素位置信息记录 + const rowMargin = this.getElementRowMargin(element) + this.control.recordBorderInfo( + x, + y + rowMargin, + element.metrics.width, + curRow.height - 2 * rowMargin + ) + } else if (preElement?.control?.border) { + this.control.drawBorder(ctx) + } + // 下划线记录 + if (element.underline || element.control?.underline) { + // 下标元素下划线单独绘制 + if ( + preElement?.type === ElementType.SUBSCRIPT && + element.type !== ElementType.SUBSCRIPT + ) { + this.underline.render(ctx) + } + // 行间距 + const rowMargin = this.getElementRowMargin(element) + // 元素向左偏移量 + const offsetX = element.left || 0 + // 下标元素y轴偏移值 + let offsetY = 0 + if (element.type === ElementType.SUBSCRIPT) { + offsetY = this.subscriptParticle.getOffsetY(element) + } + // 占位符不参与颜色计算 + const color = element.control?.underline + ? this.options.underlineColor + : element.color + this.underline.recordFillInfo( + ctx, + x - offsetX, + y + curRow.height - rowMargin + offsetY, + metrics.width + offsetX, + 0, + color, + element.textDecoration?.style + ) + } else if (preElement?.underline || preElement?.control?.underline) { + this.underline.render(ctx) + } + // 删除线记录 + if (element.strikeout) { + // 仅文本类元素支持删除线 + if (!element.type || TEXTLIKE_ELEMENT_TYPE.includes(element.type)) { + // 字体大小不同时需立即绘制 + if ( + preElement && + ((preElement.type === ElementType.SUBSCRIPT && + element.type !== ElementType.SUBSCRIPT) || + (preElement.type === ElementType.SUPERSCRIPT && + element.type !== ElementType.SUPERSCRIPT) || + this.getElementSize(preElement) !== + this.getElementSize(element)) + ) { + this.strikeout.render(ctx) + } + // 基线文字测量信息 + const standardMetrics = this.textParticle.measureBasisWord( + ctx, + this.getElementFont(element) + ) + // 文字渲染位置 + 基线文字下偏移量 - 一半文字高度 + let adjustY = + y + + offsetY + + standardMetrics.actualBoundingBoxDescent * scale - + metrics.height / 2 + // 上下标位置调整 + if (element.type === ElementType.SUBSCRIPT) { + adjustY += this.subscriptParticle.getOffsetY(element) + } else if (element.type === ElementType.SUPERSCRIPT) { + adjustY += this.superscriptParticle.getOffsetY(element) + } + this.strikeout.recordFillInfo(ctx, x, adjustY, metrics.width) + } + } else if (preElement?.strikeout) { + this.strikeout.render(ctx) + } + // 选区记录 + const { + zone: currentZone, + startIndex, + endIndex + } = this.range.getRange() + if ( + currentZone === zone && + startIndex !== endIndex && + startIndex <= index && + index <= endIndex + ) { + const positionContext = this.position.getPositionContext() + // 表格需限定上下文 + if ( + (!positionContext.isTable && !element.tdId) || + positionContext.tdId === element.tdId + ) { + // 从行尾开始-绘制最小宽度 + if (startIndex === index) { + const nextElement = elementList[startIndex + 1] + if (nextElement && nextElement.value === ZERO) { + rangeRecord.x = x + metrics.width + rangeRecord.y = y + rangeRecord.height = curRow.height + rangeRecord.width += this.options.rangeMinWidth + } + } else { + let rangeWidth = metrics.width + // 最小选区宽度 + if (rangeWidth === 0 && curRow.elementList.length === 1) { + rangeWidth = this.options.rangeMinWidth + } + // 记录第一次位置、行高 + if (!rangeRecord.width) { + rangeRecord.x = x + rangeRecord.y = y + rangeRecord.height = curRow.height + } + rangeRecord.width += rangeWidth + } + } + } + // 组信息记录 + if (!group.disabled && element.groupIds) { + this.group.recordFillInfo(element, x, y, metrics.width, curRow.height) + } + index++ + // 绘制表格内元素 + if (element.type === ElementType.TABLE && !element.hide) { + const tdPaddingWidth = tdPadding[1] + tdPadding[3] + for (let t = 0; t < element.trList!.length; t++) { + const tr = element.trList![t] + for (let d = 0; d < tr.tdList!.length; d++) { + const td = tr.tdList[d] + this.drawRow(ctx, { + elementList: td.value, + positionList: td.positionList!, + rowList: td.rowList!, + pageNo, + startIndex: 0, + innerWidth: (td.width! - tdPaddingWidth) * scale, + zone, + isDrawLineBreak + }) + } + } + } + } + // 绘制列表样式 + if (curRow.isList) { + this.listParticle.drawListStyle( + ctx, + curRow, + positionList[curRow.startIndex] + ) + } + // 绘制文字、边框、下划线、删除线 + this.textParticle.complete() + this.control.drawBorder(ctx) + this.underline.render(ctx) + this.strikeout.render(ctx) + // 绘制批注样式 + this.group.render(ctx) + // 绘制选区 + if (!isPrintMode) { + if (rangeRecord.width && rangeRecord.height) { + const { x, y, width, height } = rangeRecord + this.range.render(ctx, x, y, width, height) + } + if ( + isCrossRowCol && + tableRangeElement && + tableRangeElement.id === tableId + ) { + const { + coordinate: { + leftTop: [x, y] + } + } = positionList[curRow.startIndex] + this.tableParticle.drawRange(ctx, tableRangeElement, x, y) + } + } + } + } + + private _drawFloat( + ctx: CanvasRenderingContext2D, + payload: IDrawFloatPayload + ) { + const { scale } = this.options + const floatPositionList = this.position.getFloatPositionList() + const { imgDisplays, pageNo } = payload + for (let e = 0; e < floatPositionList.length; e++) { + const floatPosition = floatPositionList[e] + const element = floatPosition.element + if ( + (pageNo === floatPosition.pageNo || + floatPosition.zone === EditorZone.HEADER || + floatPosition.zone == EditorZone.FOOTER) && + element.imgDisplay && + imgDisplays.includes(element.imgDisplay) && + element.type === ElementType.IMAGE + ) { + const imgFloatPosition = element.imgFloatPosition! + this.imageParticle.render( + ctx, + element, + imgFloatPosition.x * scale, + imgFloatPosition.y * scale + ) + } + } + } + + private _clearPage(pageNo: number) { + const ctx = this.ctxList[pageNo] + const pageDom = this.pageList[pageNo] + ctx.clearRect( + 0, + 0, + Math.max(pageDom.width, this.getWidth()), + Math.max(pageDom.height, this.getHeight()) + ) + this.blockParticle.clear() + } + + private _drawPage(payload: IDrawPagePayload) { + const { elementList, positionList, rowList, pageNo } = payload + const { + inactiveAlpha, + pageMode, + header, + footer, + pageNumber, + lineNumber, + pageBorder + } = this.options + const isPrintMode = this.mode === EditorMode.PRINT + const innerWidth = this.getInnerWidth() + const ctx = this.ctxList[pageNo] + // 判断当前激活区域-非正文区域时元素透明度降低 + ctx.globalAlpha = !this.zone.isMainActive() ? inactiveAlpha : 1 + this._clearPage(pageNo) + // 绘制背景 + this.background.render(ctx, pageNo) + // 绘制区域 + if (!isPrintMode) { + this.area.render(ctx, pageNo) + } + // 绘制水印 + if (pageMode !== PageMode.CONTINUITY && this.options.watermark.data) { + this.waterMark.render(ctx, pageNo) + } + // 绘制页边距 + if (!isPrintMode) { + this.margin.render(ctx, pageNo) + } + // 渲染衬于文字下方元素 + this._drawFloat(ctx, { + pageNo, + imgDisplays: [ImageDisplay.FLOAT_BOTTOM] + }) + // 控件高亮 + if (!isPrintMode) { + this.control.renderHighlightList(ctx, pageNo) + } + // 渲染元素 + const index = rowList[0]?.startIndex + this.drawRow(ctx, { + elementList, + positionList, + rowList, + pageNo, + startIndex: index, + innerWidth, + zone: EditorZone.MAIN + }) + if (this.getIsPagingMode()) { + // 绘制页眉 + if (!header.disabled) { + this.header.render(ctx, pageNo) + } + // 绘制页码 + if (!pageNumber.disabled) { + this.pageNumber.render(ctx, pageNo) + } + // 绘制页脚 + if (!footer.disabled) { + this.footer.render(ctx, pageNo) + } + } + // 渲染浮于文字上方元素 + this._drawFloat(ctx, { + pageNo, + imgDisplays: [ImageDisplay.FLOAT_TOP, ImageDisplay.SURROUND] + }) + // 搜索匹配绘制 + if (!isPrintMode && this.search.getSearchKeyword()) { + this.search.render(ctx, pageNo) + } + // 绘制空白占位符 + if (this.elementList.length <= 1 && !this.elementList[0]?.listId) { + this.placeholder.render(ctx) + } + // 渲染行数 + if (!lineNumber.disabled) { + this.lineNumber.render(ctx, pageNo) + } + // 绘制页面边框 + if (!pageBorder.disabled) { + this.pageBorder.render(ctx) + } + // 绘制签章 + this.badge.render(ctx, pageNo) + } + + private _disconnectLazyRender() { + this.lazyRenderIntersectionObserver?.disconnect() + } + + private _lazyRender() { + const positionList = this.position.getOriginalMainPositionList() + const elementList = this.getOriginalMainElementList() + this._disconnectLazyRender() + this.lazyRenderIntersectionObserver = new IntersectionObserver(entries => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const index = Number((entry.target).dataset.index) + this._drawPage({ + elementList, + positionList, + rowList: this.pageRowList[index], + pageNo: index + }) + } + }) + }) + this.pageList.forEach(el => { + this.lazyRenderIntersectionObserver!.observe(el) + }) + } + + private _immediateRender() { + const positionList = this.position.getOriginalMainPositionList() + const elementList = this.getOriginalMainElementList() + for (let i = 0; i < this.pageRowList.length; i++) { + this._drawPage({ + elementList, + positionList, + rowList: this.pageRowList[i], + pageNo: i + }) + } + } + + public render(payload?: IDrawOption) { + this.renderCount++ + const { header, footer } = this.options + const { + isSubmitHistory = true, + isSetCursor = true, + isCompute = true, + isLazy = true, + isInit = false, + isSourceHistory = false, + isFirstRender = false + } = payload || {} + let { curIndex } = payload || {} + const innerWidth = this.getInnerWidth() + const isPagingMode = this.getIsPagingMode() + // 缓存当前页数信息 + const oldPageSize = this.pageRowList.length + // 计算文档信息 + if (isCompute) { + // 清空浮动元素位置信息 + this.position.setFloatPositionList([]) + if (isPagingMode) { + // 页眉信息 + if (!header.disabled) { + this.header.compute() + } + // 页脚信息 + if (!footer.disabled) { + this.footer.compute() + } + } + // 行信息 + const margins = this.getMargins() + const pageHeight = this.getHeight() + const extraHeight = this.header.getExtraHeight() + const mainOuterHeight = this.getMainOuterHeight() + const startX = margins[3] + const startY = margins[0] + extraHeight + const surroundElementList = pickSurroundElementList(this.elementList) + this.rowList = this.computeRowList({ + startX, + startY, + pageHeight, + mainOuterHeight, + isPagingMode, + innerWidth, + surroundElementList, + elementList: this.elementList + }) + // 页面信息 + this.pageRowList = this._computePageList() + // 位置信息 + this.position.computePositionList() + // 区域信息 + this.area.compute() + if (this.mode !== EditorMode.PRINT) { + // 搜索信息 + const searchKeyword = this.search.getSearchKeyword() + if (searchKeyword) { + this.search.compute(searchKeyword) + } + // 控件关键词高亮 + this.control.computeHighlightList() + } + } + // 清除光标等副作用 + this.imageObserver.clearAll() + this.cursor.recoveryCursor() + // 创建纸张 + for (let i = 0; i < this.pageRowList.length; i++) { + if (!this.pageList[i]) { + this._createPage(i) + } + } + // 移除多余页 + const curPageCount = this.pageRowList.length + const prePageCount = this.pageList.length + if (prePageCount > curPageCount) { + const deleteCount = prePageCount - curPageCount + this.ctxList.splice(curPageCount, deleteCount) + this.pageList + .splice(curPageCount, deleteCount) + .forEach(page => page.remove()) + } + // 绘制元素 + // 连续页因为有高度的变化会导致canvas渲染空白,需立即渲染,否则会出现闪动 + if (isLazy && isPagingMode) { + this._lazyRender() + } else { + this._immediateRender() + } + // 光标重绘 + if (isSetCursor) { + curIndex = this.setCursor(curIndex) + } else if (this.range.getIsSelection()) { + // 存在选区时仅定位避免事件无法捕获 + this.cursor.focus() + } + // 历史记录用于undo、redo(非首次渲染内容变更 || 第一次存在光标时) + if ( + (isSubmitHistory && !isFirstRender) || + (curIndex !== undefined && this.historyManager.isStackEmpty()) + ) { + this.submitHistory(curIndex) + } + // 信息变动回调 + nextTick(() => { + // 选区样式 + this.range.setRangeStyle() + // 重新唤起弹窗类控件 + if (isCompute && this.control.getActiveControl()) { + this.control.reAwakeControl() + } + // 表格工具重新渲染 + if ( + isCompute && + !this.isReadonly() && + this.position.getPositionContext().isTable + ) { + this.tableTool.render() + } + // 页眉指示器重新渲染 + if (isCompute && !this.zone.isMainActive()) { + this.zone.drawZoneIndicator() + } + // 页数改变 + if (oldPageSize !== this.pageRowList.length) { + if (this.listener.pageSizeChange) { + this.listener.pageSizeChange(this.pageRowList.length) + } + if (this.eventBus.isSubscribe('pageSizeChange')) { + this.eventBus.emit('pageSizeChange', this.pageRowList.length) + } + } + // 文档内容改变 + if ((isSubmitHistory || isSourceHistory) && !isInit) { + if (this.listener.contentChange) { + this.listener.contentChange() + } + if (this.eventBus.isSubscribe('contentChange')) { + this.eventBus.emit('contentChange') + } + } + }) + } + + public setCursor(curIndex: number | undefined) { + const positionContext = this.position.getPositionContext() + const positionList = this.position.getPositionList() + if (positionContext.isTable) { + const { index, trIndex, tdIndex } = positionContext + const elementList = this.getOriginalElementList() + const tablePositionList = + elementList[index!].trList?.[trIndex!].tdList[tdIndex!].positionList + if (curIndex === undefined && tablePositionList) { + curIndex = tablePositionList.length - 1 + } + const tablePosition = tablePositionList?.[curIndex!] + this.position.setCursorPosition(tablePosition || null) + } else { + this.position.setCursorPosition( + curIndex !== undefined ? positionList[curIndex] : null + ) + } + // 定位到图片元素并且位置发生变化 + let isShowCursor = true + if ( + curIndex !== undefined && + positionContext.isImage && + positionContext.isDirectHit + ) { + const elementList = this.getElementList() + const element = elementList[curIndex] + if (IMAGE_ELEMENT_TYPE.includes(element.type!)) { + isShowCursor = false + const position = this.position.getCursorPosition() + this.previewer.updateResizer(element, position) + } + } + this.cursor.drawCursor({ + isShow: isShowCursor + }) + return curIndex + } + + public submitHistory(curIndex: number | undefined) { + const positionContext = this.position.getPositionContext() + const oldElementList = getSlimCloneElementList(this.elementList) + const oldHeaderElementList = getSlimCloneElementList( + this.header.getElementList() + ) + const oldFooterElementList = getSlimCloneElementList( + this.footer.getElementList() + ) + const oldRange = deepClone(this.range.getRange()) + const pageNo = this.pageNo + const oldPositionContext = deepClone(positionContext) + const zone = this.zone.getZone() + this.historyManager.execute(() => { + this.zone.setZone(zone) + this.setPageNo(pageNo) + this.position.setPositionContext(deepClone(oldPositionContext)) + this.header.setElementList(deepClone(oldHeaderElementList)) + this.footer.setElementList(deepClone(oldFooterElementList)) + this.elementList = deepClone(oldElementList) + this.range.replaceRange(deepClone(oldRange)) + this.render({ + curIndex, + isSubmitHistory: false, + isSourceHistory: true + }) + }) + } + + public destroy() { + this.container.remove() + this.globalEvent.removeEvent() + this.scrollObserver.removeEvent() + this.selectionObserver.removeEvent() + } + + public clearSideEffect() { + // 预览工具组件 + this.getPreviewer().clearResizer() + // 表格工具组件 + this.getTableTool().dispose() + // 超链接弹窗 + this.getHyperlinkParticle().clearHyperlinkPopup() + // 日期控件 + this.getDateParticle().clearDatePicker() + } +} diff --git a/src/editor/core/draw/control/Control.ts b/src/editor/core/draw/control/Control.ts new file mode 100644 index 0000000..1a9bcce --- /dev/null +++ b/src/editor/core/draw/control/Control.ts @@ -0,0 +1,1676 @@ +import { + ControlComponent, + ControlState, + ControlType +} from '../../../dataset/enum/Control' +import { EditorMode, EditorZone } from '../../../dataset/enum/Editor' +import { ElementType } from '../../../dataset/enum/Element' +import { DeepRequired } from '../../../interface/Common' +import { + IControl, + IControlChangeOption, + IControlChangeResult, + IControlContentChangeResult, + IControlContext, + IControlHighlight, + IControlInitOption, + IControlInstance, + IControlOption, + IControlRuleOption, + IDestroyControlOption, + IGetControlValueOption, + IGetControlValueResult, + IInitNextControlOption, + INextControlContext, + IRepaintControlOption, + ISetControlExtensionOption, + ISetControlProperties, + ISetControlRowFlexOption, + ISetControlValueOption +} from '../../../interface/Control' +import { IEditorData, IEditorOption } from '../../../interface/Editor' +import { IElement, IElementPosition } from '../../../interface/Element' +import { EventBusMap } from '../../../interface/EventBus' +import { IRange } from '../../../interface/Range' +import { + deepClone, + isArray, + isString, + omitObject, + pickObject, + splitText +} from '../../../utils' +import { + formatElementContext, + formatElementList, + getNonHideElementIndex, + pickElementAttr, + zipElementList +} from '../../../utils/element' +import { EventBus } from '../../event/eventbus/EventBus' +import { Listener } from '../../listener/Listener' +import { RangeManager } from '../../range/RangeManager' +import { Draw } from '../Draw' +import { CheckboxControl } from './checkbox/CheckboxControl' +import { RadioControl } from './radio/RadioControl' +import { ControlSearch } from './interactive/ControlSearch' +import { ControlBorder } from './richtext/Border' +import { SelectControl } from './select/SelectControl' +import { TextControl } from './text/TextControl' +import { DateControl } from './date/DateControl' +import { NumberControl } from './number/NumberControl' +import { MoveDirection } from '../../../dataset/enum/Observer' +import { + CONTROL_CONTEXT_ATTR, + CONTROL_STYLE_ATTR, + LIST_CONTEXT_ATTR, + TITLE_CONTEXT_ATTR +} from '../../../dataset/constant/Element' +import { IRowElement } from '../../../interface/Row' +import { RowFlex } from '../../../dataset/enum/Row' +import { ZERO } from '../../../dataset/constant/Common' + +interface IMoveCursorResult { + newIndex: number + newElement: IElement +} +export class Control { + private controlBorder: ControlBorder + private draw: Draw + private range: RangeManager + private listener: Listener + private eventBus: EventBus + private controlSearch: ControlSearch + private options: DeepRequired + private controlOptions: IControlOption + private activeControl: IControlInstance | null + private activeControlValue: IElement[] + private preElement: IElement | null + + constructor(draw: Draw) { + this.controlBorder = new ControlBorder(draw) + + this.draw = draw + this.range = draw.getRange() + this.listener = draw.getListener() + this.eventBus = draw.getEventBus() + this.controlSearch = new ControlSearch(this) + + this.options = draw.getOptions() + this.controlOptions = this.options.control + this.activeControl = null + this.activeControlValue = [] + this.preElement = null + } + + // 搜索高亮匹配 + public setHighlightList(payload: IControlHighlight[]) { + this.controlSearch.setHighlightList(payload) + } + + public computeHighlightList() { + const highlightList = this.controlSearch.getHighlightList() + if (highlightList.length) { + this.controlSearch.computeHighlightList() + } + } + + public renderHighlightList(ctx: CanvasRenderingContext2D, pageNo: number) { + const highlightMatchResult = this.controlSearch.getHighlightMatchResult() + if (highlightMatchResult.length) { + this.controlSearch.renderHighlightList(ctx, pageNo) + } + } + + public getDraw(): Draw { + return this.draw + } + + // 过滤控件辅助元素(前后缀、背景提示) + public filterAssistElement(elementList: IElement[]): IElement[] { + return elementList.filter((element, index) => { + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + td.value = this.filterAssistElement(td.value) + } + } + } + if (!element.controlId) return true + if (element.control?.minWidth) { + if ( + element.controlComponent === ControlComponent.PREFIX || + element.controlComponent === ControlComponent.POSTFIX + ) { + element.value = '' + return true + } + } else { + // 控件存在值时无需过滤前后文本 + if ( + element.control?.preText && + element.controlComponent === ControlComponent.PRE_TEXT + ) { + let isExistValue = false + let start = index + 1 + while (start < elementList.length) { + const nextElement = elementList[start] + if (element.controlId !== nextElement.controlId) break + if (nextElement.controlComponent === ControlComponent.VALUE) { + isExistValue = true + break + } + start++ + } + return isExistValue + } + if ( + element.control?.postText && + element.controlComponent === ControlComponent.POST_TEXT + ) { + let isExistValue = false + let start = index - 1 + while (start < elementList.length) { + const preElement = elementList[start] + if (element.controlId !== preElement.controlId) break + if (preElement.controlComponent === ControlComponent.VALUE) { + isExistValue = true + break + } + start-- + } + return isExistValue + } + } + return ( + element.controlComponent !== ControlComponent.PREFIX && + element.controlComponent !== ControlComponent.POSTFIX && + element.controlComponent !== ControlComponent.PLACEHOLDER + ) + }) + } + + // 是否属于控件可以捕获事件的选区 + public getIsRangeCanCaptureEvent(): boolean { + if (!this.activeControl) return false + const { startIndex, endIndex } = this.getRange() + if (!~startIndex && !~endIndex) return false + const elementList = this.getElementList() + const startElement = elementList[startIndex] + // 闭合光标在后缀处 + if ( + startIndex === endIndex && + startElement.controlComponent === ControlComponent.POSTFIX + ) { + return true + } + // 在控件内 + const endElement = elementList[endIndex] + if ( + startElement.controlId && + startElement.controlId === endElement.controlId && + endElement.controlComponent !== ControlComponent.POSTFIX + ) { + return true + } + return false + } + + // 判断选区是否在后缀处 + public getIsRangeInPostfix(): boolean { + if (!this.activeControl) return false + const { startIndex, endIndex } = this.getRange() + if (startIndex !== endIndex) return false + const elementList = this.getElementList() + const element = elementList[startIndex] + return element.controlComponent === ControlComponent.POSTFIX + } + + // 判断选区是否在控件内 + public getIsRangeWithinControl(): boolean { + const { startIndex, endIndex } = this.getRange() + if (!~startIndex && !~endIndex) return false + const elementList = this.getElementList() + const startElement = elementList[startIndex] + const endElement = elementList[endIndex] + if ( + startElement?.controlId && + startElement.controlId === endElement.controlId && + endElement.controlComponent !== ControlComponent.POSTFIX + ) { + return true + } + return false + } + + // 是否元素包含完整控件元素 + public getIsElementListContainFullControl(elementList: IElement[]): boolean { + if (!elementList.some(element => element.controlId)) return false + let prefixCount = 0 + let postfixCount = 0 + for (let e = 0; e < elementList.length; e++) { + const element = elementList[e] + if (element.controlComponent === ControlComponent.PREFIX) { + prefixCount++ + } else if (element.controlComponent === ControlComponent.POSTFIX) { + postfixCount++ + } + } + if (!prefixCount || !postfixCount) return false + return prefixCount === postfixCount + } + + public getIsDisabledControl(context: IControlContext = {}): boolean { + if (this.draw.isDesignMode() || !this.activeControl) return false + const { startIndex, endIndex } = context.range || this.range.getRange() + if (startIndex === endIndex && ~startIndex && ~endIndex) { + const elementList = context.elementList || this.getElementList() + const startElement = elementList[startIndex] + if (startElement.controlComponent === ControlComponent.POSTFIX) { + return false + } + } + return !!this.activeControl.getElement()?.control?.disabled + } + + public getIsDisabledPasteControl(context: IControlContext = {}): boolean { + if (this.draw.isDesignMode() || !this.activeControl) return false + const { startIndex, endIndex } = context.range || this.range.getRange() + if (startIndex === endIndex && ~startIndex && ~endIndex) { + const elementList = context.elementList || this.getElementList() + const startElement = elementList[startIndex] + if (startElement.controlComponent === ControlComponent.POSTFIX) { + return false + } + } + return !!this.activeControl.getElement()?.control?.pasteDisabled + } + + // 通过索引找到控件并判断控件是否存在值 + public getIsExistValueByElementListIndex( + elementList: IElement[], + index: number + ): boolean { + const element = elementList[index] + // 是否是控件 + if (!element.controlId) return false + // 单选框、复选框仅需验证控件值 + if ( + element.control?.type === ControlType.CHECKBOX || + element.control?.type === ControlType.RADIO + ) { + return !!element.control?.code + } + // 其他控件需校验文本 + if (element.controlComponent === ControlComponent.VALUE) { + return true + } + if (element.controlComponent === ControlComponent.PLACEHOLDER) { + return false + } + // 向后查找值元素 + if ( + element.controlComponent === ControlComponent.PREFIX || + element.controlComponent === ControlComponent.PRE_TEXT + ) { + let i = index + 1 + while (i < elementList.length) { + const nextElement = elementList[i] + if (nextElement.controlId !== element.controlId) { + return false + } + if (nextElement.controlComponent === ControlComponent.VALUE) { + return true + } + if (nextElement.controlComponent === ControlComponent.PLACEHOLDER) { + return false + } + i++ + } + } + // 向前查找值元素 + if ( + element.controlComponent === ControlComponent.POSTFIX || + element.controlComponent === ControlComponent.POST_TEXT + ) { + let i = index - 1 + while (i >= 0) { + const preElement = elementList[i] + if (preElement.controlId !== element.controlId) { + return false + } + if (preElement.controlComponent === ControlComponent.VALUE) { + return true + } + if (preElement.controlComponent === ControlComponent.PLACEHOLDER) { + return false + } + i-- + } + } + return false + } + + public getControlHighlight(elementList: IElement[], index: number) { + return this.controlSearch.getControlHighlight(elementList, index) + } + + public getContainer(): HTMLDivElement { + return this.draw.getContainer() + } + + public getElementList(): IElement[] { + return this.draw.getElementList() + } + + public getPosition(): IElementPosition | null { + const positionList = this.draw.getPosition().getPositionList() + const { endIndex } = this.range.getRange() + return positionList[endIndex] || null + } + + public getPreY(): number { + const height = this.draw.getHeight() + const pageGap = this.draw.getPageGap() + const pageNo = this.getPosition()?.pageNo ?? this.draw.getPageNo() + return pageNo * (height + pageGap) + } + + public getRange(): IRange { + return this.range.getRange() + } + + public shrinkBoundary(context: IControlContext = {}) { + this.range.shrinkBoundary(context) + } + + public getActiveControl(): IControlInstance | null { + return this.activeControl + } + + public getControlElementList(context: IControlContext = {}): IElement[] { + const elementList = context.elementList || this.getElementList() + const { startIndex } = context.range || this.getRange() + const startElement = elementList[startIndex] + if (!startElement?.controlId) return [] + const data: IElement[] = [] + // 向左查找 + let preIndex = startIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if (preElement.controlId !== startElement.controlId) break + data.unshift(preElement) + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if (nextElement.controlId !== startElement.controlId) break + data.push(nextElement) + nextIndex++ + } + return data + } + + public updateActiveControlValue() { + if (this.activeControl) { + this.activeControlValue = this.getControlElementList() + } + } + + public emitControlChange(state: ControlState) { + if (!this.activeControl) return + const isSubscribeControlChange = this.eventBus.isSubscribe('controlChange') + if (!this.listener.controlChange && !isSubscribeControlChange) return + let control: IControl + const value = this.activeControlValue + const activeElement = this.activeControl.getElement() + if (value?.length) { + control = zipElementList(value)[0].control! + } else { + control = pickElementAttr(deepClone(activeElement)).control! + control.value = [] + } + const payload: IControlChangeResult = { + state, + control, + controlId: activeElement.controlId! + } + this.listener.controlChange?.(payload) + if (isSubscribeControlChange) { + this.eventBus.emit('controlChange', payload) + } + } + + public initControl() { + const elementList = this.getElementList() + const range = this.getRange() + const element = elementList[range.startIndex] + // 判断控件是否已经激活 + if (this.activeControl) { + // 弹窗类控件唤醒弹窗,后缀处移除弹窗 + if ( + this.activeControl instanceof SelectControl || + this.activeControl instanceof DateControl + ) { + if (element.controlComponent === ControlComponent.POSTFIX) { + this.activeControl.destroy() + } else { + this.activeControl.awake() + } + } + // 相同控件元素 + if (this.preElement?.controlId === element.controlId) { + // 当前元素在尾部:控件失活事件 + if (element.controlComponent === ControlComponent.POSTFIX) { + this.emitControlChange(ControlState.INACTIVE) + } else if ( + // 之前元素在尾部 && 当前不在尾部:控件激活事件 + this.preElement?.controlComponent === ControlComponent.POSTFIX + ) { + this.emitControlChange(ControlState.ACTIVE) + } + } + // 更新缓存控件数据 + const controlElement = this.activeControl.getElement() + if (element.controlId === controlElement.controlId) { + this.updateActiveControlValue() + this.preElement = element + return + } + } + // 销毁旧激活控件 + this.destroyControl() + // 激活控件 + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + const control = element.control! + if (control.type === ControlType.TEXT) { + this.activeControl = new TextControl(element, this) + } else if (control.type === ControlType.SELECT) { + const selectControl = new SelectControl(element, this) + this.activeControl = selectControl + selectControl.awake() + } else if (control.type === ControlType.CHECKBOX) { + this.activeControl = new CheckboxControl(element, this) + } else if (control.type === ControlType.RADIO) { + this.activeControl = new RadioControl(element, this) + } else if (control.type === ControlType.DATE) { + const dateControl = new DateControl(element, this) + this.activeControl = dateControl + dateControl.awake() + } else if (control.type === ControlType.NUMBER) { + this.activeControl = new NumberControl(element, this) + } + // 缓存控件数据 + this.updateActiveControlValue() + this.preElement = element + // 激活控件回调 + if (element.controlComponent !== ControlComponent.POSTFIX) { + this.emitControlChange(ControlState.ACTIVE) + } + } + + public destroyControl(options: IDestroyControlOption = {}) { + if (!this.activeControl) return + const { isEmitEvent = true } = options + if ( + this.activeControl instanceof SelectControl || + this.activeControl instanceof DateControl + ) { + this.activeControl.destroy() + } + // 销毁控件回调 + if ( + isEmitEvent && + this.preElement?.controlComponent !== ControlComponent.POSTFIX + ) { + this.emitControlChange(ControlState.INACTIVE) + } + // 清空变量 + this.preElement = null + this.activeControl = null + this.activeControlValue = [] + } + + public repaintControl(options: IRepaintControlOption = {}) { + const { + curIndex, + isCompute = true, + isSubmitHistory = true, + isSetCursor = true + } = options + // 重新渲染 + if (curIndex === undefined) { + this.range.clearRange() + this.draw.render({ + isCompute, + isSubmitHistory, + isSetCursor: false + }) + } else { + this.range.setRange(curIndex, curIndex) + this.draw.render({ + curIndex, + isCompute, + isSetCursor, + isSubmitHistory + }) + } + } + + public emitControlContentChange(options?: IControlChangeOption) { + const isSubscribeControlContentChange = this.eventBus.isSubscribe( + 'controlContentChange' + ) + if ( + !isSubscribeControlContentChange && + !this.listener.controlContentChange + ) { + return + } + const controlElement = + options?.controlElement || this.activeControl?.getElement() + if (!controlElement) return + // 控件被删除不触发事件 + const elementList = options?.context?.elementList || this.getElementList() + const { startIndex } = options?.context?.range || this.getRange() + if (!elementList[startIndex]?.controlId) return + // 格式化回调数据 + const controlValue = + options?.controlValue || this.getControlElementList(options?.context) + let control: IControl + if (controlValue?.length) { + control = zipElementList(controlValue)[0].control! + } else { + control = controlElement.control! + control.value = [] + } + if (!control) return + const payload: IControlContentChangeResult = { + control, + controlId: controlElement.controlId! + } + this.listener.controlContentChange?.(payload) + if (isSubscribeControlContentChange) { + this.eventBus.emit('controlContentChange', payload) + } + } + + public reAwakeControl() { + if (!this.activeControl) return + const elementList = this.getElementList() + const range = this.getRange() + const element = elementList[range.startIndex] + this.activeControl.setElement(element) + if ( + (this.activeControl instanceof DateControl || + this.activeControl instanceof SelectControl) && + this.activeControl.getIsPopup() + ) { + this.activeControl.destroy() + this.activeControl.awake() + } + } + + public moveCursor(position: IControlInitOption): IMoveCursorResult { + const { index, trIndex, tdIndex, tdValueIndex } = position + let elementList = this.draw.getOriginalElementList() + let element: IElement + const newIndex = position.isTable ? tdValueIndex! : index + if (position.isTable) { + elementList = elementList[index!].trList![trIndex!].tdList[tdIndex!].value + element = elementList[tdValueIndex!] + } else { + element = elementList[index] + } + // 隐藏元素移动光标 + if (element.hide || element.control?.hide || element.area?.hide) { + const nonHideIndex = getNonHideElementIndex(elementList, newIndex) + return { + newIndex: nonHideIndex, + newElement: elementList[nonHideIndex] + } + } + // 控件内移动光标 + if (element.controlComponent === ControlComponent.VALUE) { + // VALUE-无需移动 + return { + newIndex, + newElement: element + } + } else if (element.controlComponent === ControlComponent.POSTFIX) { + // POSTFIX-移动到最后一个后缀字符后 + let startIndex = newIndex + 1 + while (startIndex < elementList.length) { + const nextElement = elementList[startIndex] + if (nextElement.controlId !== element.controlId) { + return { + newIndex: startIndex - 1, + newElement: elementList[startIndex - 1] + } + } + startIndex++ + } + } else if ( + element.controlComponent === ControlComponent.PREFIX || + element.controlComponent === ControlComponent.PRE_TEXT + ) { + // PREFIX或前文本-移动到最后一个前缀字符后 + let startIndex = newIndex + 1 + while (startIndex < elementList.length) { + const nextElement = elementList[startIndex] + if ( + nextElement.controlId !== element.controlId || + (nextElement.controlComponent !== ControlComponent.PREFIX && + nextElement.controlComponent !== ControlComponent.PRE_TEXT) + ) { + return { + newIndex: startIndex - 1, + newElement: elementList[startIndex - 1] + } + } + startIndex++ + } + } else if ( + element.controlComponent === ControlComponent.PLACEHOLDER || + element.controlComponent === ControlComponent.POST_TEXT + ) { + // PLACEHOLDER或后文本-移动到第一个前缀或内容后 + let startIndex = newIndex - 1 + while (startIndex > 0) { + const preElement = elementList[startIndex] + if ( + preElement.controlId !== element.controlId || + preElement.controlComponent === ControlComponent.VALUE || + preElement.controlComponent === ControlComponent.PREFIX || + preElement.controlComponent === ControlComponent.PRE_TEXT + ) { + return { + newIndex: startIndex, + newElement: elementList[startIndex] + } + } + startIndex-- + } + } + return { + newIndex, + newElement: element + } + } + + public removeControl( + startIndex: number, + context: IControlContext = {} + ): number | null { + const elementList = context.elementList || this.getElementList() + const startElement = elementList[startIndex] + // 设计模式 || 元素隐藏 => 不验证删除权限 + if ( + !this.draw.isDesignMode() && + !startElement?.hide && + !startElement?.control?.hide && + !startElement?.area?.hide + ) { + const { deletable = true } = startElement.control! + if (!deletable) return null + // 表单模式控件删除权限验证 + const mode = this.draw.getMode() + if ( + mode === EditorMode.FORM && + this.options.modeRule[mode].controlDeletableDisabled + ) { + return null + } + } + let leftIndex = -1 + let rightIndex = -1 + // 向左查找 + let preIndex = startIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if (preElement.controlId !== startElement.controlId) { + leftIndex = preIndex + break + } + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if (nextElement.controlId !== startElement.controlId) { + rightIndex = nextIndex - 1 + break + } + nextIndex++ + } + // 控件在最后 + if (nextIndex === elementList.length) { + rightIndex = nextIndex - 1 + } + if (!~leftIndex && !~rightIndex) return startIndex + leftIndex = ~leftIndex ? leftIndex : 0 + // 删除元素 + this.draw.spliceElementList( + elementList, + leftIndex + 1, + rightIndex - leftIndex + ) + return leftIndex + } + + public removePlaceholder(startIndex: number, context: IControlContext = {}) { + const elementList = context.elementList || this.getElementList() + const startElement = elementList[startIndex] + const nextElement = elementList[startIndex + 1] + if ( + startElement.controlComponent === ControlComponent.PLACEHOLDER || + nextElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + let isHasSubmitHistory = false + let index = startIndex + while (index < elementList.length) { + const curElement = elementList[index] + if (curElement.controlId !== startElement.controlId) break + if (curElement.controlComponent === ControlComponent.PLACEHOLDER) { + // 删除占位符时替换前一个历史记录 + if (!isHasSubmitHistory) { + isHasSubmitHistory = true + this.draw.getHistoryManager().popUndo() + this.draw.submitHistory(startIndex) + } + elementList.splice(index, 1) + } else { + index++ + } + } + } + } + + public addPlaceholder(startIndex: number, context: IControlContext = {}) { + const elementList = context.elementList || this.getElementList() + const startElement = elementList[startIndex] + const control = startElement.control! + if (!control.placeholder) return + const placeholderStrList = splitText(control.placeholder) + // 优先使用默认控件样式 + const anchorElementStyleAttr = pickObject(startElement, CONTROL_STYLE_ATTR) + for (let p = 0; p < placeholderStrList.length; p++) { + const value = placeholderStrList[p] + const newElement: IElement = { + ...anchorElementStyleAttr, + value: value === '\n' ? ZERO : value, + controlId: startElement.controlId, + type: ElementType.CONTROL, + control: startElement.control, + controlComponent: ControlComponent.PLACEHOLDER, + color: this.controlOptions.placeholderColor + } + formatElementContext(elementList, [newElement], startIndex, { + editorOptions: this.options + }) + this.draw.spliceElementList(elementList, startIndex + p + 1, 0, [ + newElement + ]) + } + } + + public setValue(data: IElement[]): number { + if (!this.activeControl) { + throw new Error('active control is null') + } + return this.activeControl.setValue(data) + } + + public setControlProperties( + properties: Partial, + context: IControlContext = {} + ) { + const elementList = context.elementList || this.getElementList() + const { startIndex } = context.range || this.getRange() + const startElement = elementList[startIndex] + // 向左查找 + let preIndex = startIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if (preElement.controlId !== startElement.controlId) break + preElement.control = { + ...preElement.control!, + ...properties + } + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if (nextElement.controlId !== startElement.controlId) break + nextElement.control = { + ...nextElement.control!, + ...properties + } + nextIndex++ + } + } + + public keydown(evt: KeyboardEvent): number | null { + if (!this.activeControl) { + throw new Error('active control is null') + } + return this.activeControl.keydown(evt) + } + + public cut(): number { + if (!this.activeControl) { + throw new Error('active control is null') + } + return this.activeControl.cut() + } + + public getValueById(payload: IGetControlValueOption): IGetControlValueResult { + const { id, groupId, conceptId, areaId } = payload + const result: IGetControlValueResult = [] + if (!id && !conceptId && !groupId) return result + const getValue = (elementList: IElement[], zone: EditorZone) => { + let i = 0 + while (i < elementList.length) { + const element = elementList[i] + i++ + // 表格下钻处理 + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + getValue(td.value, zone) + } + } + } + if ( + !element.control || + (groupId && element.control.groupId !== groupId) || + (id && element.controlId !== id) || + (conceptId && element.control.conceptId !== conceptId) || + (areaId && element.areaId !== areaId) + ) { + continue + } + const { type, code, valueSets } = element.control + let j = i + let textControlValue = '' + const textControlElementList = [] + while (j < elementList.length) { + const nextElement = elementList[j] + if (nextElement.controlId !== element.controlId) break + if ( + (type === ControlType.TEXT || + type === ControlType.DATE || + type === ControlType.NUMBER) && + nextElement.controlComponent === ControlComponent.VALUE + ) { + textControlValue += nextElement.value + textControlElementList.push( + omitObject(nextElement, CONTROL_CONTEXT_ATTR) + ) + } + j++ + } + if ( + type === ControlType.TEXT || + type === ControlType.DATE || + type === ControlType.NUMBER + ) { + result.push({ + ...element.control, + zone, + value: textControlValue || null, + innerText: textControlValue || null, + elementList: zipElementList(textControlElementList) + }) + } else if ( + type === ControlType.SELECT || + type === ControlType.CHECKBOX || + type === ControlType.RADIO + ) { + const innerText = code + ?.split(',') + .map( + selectCode => + valueSets?.find(valueSet => valueSet.code === selectCode)?.value + ) + .filter(Boolean) + .join('') + result.push({ + ...element.control, + zone, + value: code || null, + innerText: innerText || null + }) + } + i = j + } + } + const data = [ + { + zone: EditorZone.HEADER, + elementList: this.draw.getHeaderElementList() + }, + { + zone: EditorZone.MAIN, + elementList: this.draw.getOriginalMainElementList() + }, + { + zone: EditorZone.FOOTER, + elementList: this.draw.getFooterElementList() + } + ] + for (const { zone, elementList } of data) { + getValue(elementList, zone) + } + return result + } + + public setValueListById(payload: ISetControlValueOption[]) { + if (!payload.length) return + let isExistSet = false + let isExistSubmitHistory = false + // 设置值 + const setValue = (elementList: IElement[]) => { + let i = 0 + while (i < elementList.length) { + const element = elementList[i] + i++ + // 表格下钻处理 + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + setValue(td.value) + } + } + } + if (!element.control) continue + // 获取设置值优先id、conceptId、areaId并于groupId组合设置 + const payloadItem = payload.find( + p => + (!p.groupId || p.groupId === element.control?.groupId) && + ((p.id && element.controlId === p.id) || + (p.conceptId && element.control!.conceptId === p.conceptId) || + (p.areaId && element.areaId === p.areaId)) + ) + if (!payloadItem) continue + const { value, isSubmitHistory = true } = payloadItem + // 只要存在一次保存历史均记录 + isExistSet = true + if (isSubmitHistory) { + isExistSubmitHistory = true + } + const { type } = element.control! + // 当前控件结束索引 + let currentEndIndex = i + while (currentEndIndex < elementList.length) { + const nextElement = elementList[currentEndIndex] + if (nextElement.controlId !== element.controlId) break + currentEndIndex++ + } + // 模拟光标选区上下文 + const fakeRange = { + startIndex: i - 1, + endIndex: currentEndIndex - 2 + } + const controlContext: IControlContext = { + range: fakeRange, + elementList + } + const controlRule: IControlRuleOption = { + isIgnoreDisabledRule: true, + isIgnoreDeletedRule: true + } + if (type === ControlType.TEXT) { + const formatValue = Array.isArray(value) + ? value + : value + ? [{ value }] + : [] + if (formatValue.length) { + formatElementList(formatValue, { + isHandleFirstElement: false, + editorOptions: this.options + }) + } + const text = new TextControl(element, this) + this.activeControl = text + if (formatValue.length) { + text.setValue(formatValue, controlContext, controlRule) + } else { + text.clearValue(controlContext, controlRule) + } + } else if (type === ControlType.SELECT) { + if (Array.isArray(value)) continue + const select = new SelectControl(element, this) + this.activeControl = select + if (value) { + select.setSelect(value, controlContext, controlRule) + } else { + select.clearSelect(controlContext, controlRule) + } + } else if (type === ControlType.CHECKBOX) { + if (Array.isArray(value)) continue + const checkbox = new CheckboxControl(element, this) + this.activeControl = checkbox + const codes = value ? value.split(',') : [] + checkbox.setSelect(codes, controlContext, controlRule) + } else if (type === ControlType.RADIO) { + if (Array.isArray(value)) continue + const radio = new RadioControl(element, this) + this.activeControl = radio + const codes = value ? [value] : [] + radio.setSelect(codes, controlContext, controlRule) + } else if (type === ControlType.DATE) { + const date = new DateControl(element, this) + this.activeControl = date + if (isArray(value)) { + if (value.length) { + formatElementList(value, { + isHandleFirstElement: false, + editorOptions: this.options + }) + } + date.setValue(value, controlContext, controlRule) + } else if (isString(value)) { + date.setSelect(value, controlContext, controlRule) + } else { + date.clearSelect(controlContext, controlRule) + } + } else if (type === ControlType.NUMBER) { + const formatValue = Array.isArray(value) + ? value + : value + ? [{ value }] + : [] + if (formatValue.length) { + formatElementList(formatValue, { + isHandleFirstElement: false, + editorOptions: this.options + }) + } + const text = new NumberControl(element, this) + this.activeControl = text + if (formatValue.length) { + text.setValue(formatValue, controlContext, controlRule) + } else { + text.clearValue(controlContext, controlRule) + } + } + // 控件值变更事件 + this.emitControlContentChange({ + context: controlContext + }) + // 模拟控件激活后销毁 + this.activeControl = null + // 修改后控件结束索引 + let newEndIndex = i + while (newEndIndex < elementList.length) { + const nextElement = elementList[newEndIndex] + if (nextElement.controlId !== element.controlId) break + newEndIndex++ + } + i = newEndIndex + } + } + // 销毁旧控件 + this.destroyControl({ + isEmitEvent: false + }) + // 页眉、内容区、页脚同时处理 + const data = [ + this.draw.getHeaderElementList(), + this.draw.getOriginalMainElementList(), + this.draw.getFooterElementList() + ] + for (const elementList of data) { + setValue(elementList) + } + if (isExistSet) { + // 不保存历史时需清空之前记录,避免还原 + if (!isExistSubmitHistory) { + this.draw.getHistoryManager().recovery() + } + this.draw.render({ + isSubmitHistory: isExistSubmitHistory, + isSetCursor: false + }) + } + } + + public setExtensionListById(payload: ISetControlExtensionOption[]) { + if (!payload.length) return + const setExtension = (elementList: IElement[]) => { + let i = 0 + while (i < elementList.length) { + const element = elementList[i] + i++ + // 表格下钻处理 + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + setExtension(td.value) + } + } + } + if (!element.control) continue + // 获取设置值优先id、conceptId、areaId并于groupId组合设置 + const payloadItem = payload.find( + p => + (!p.groupId || p.groupId === element.control?.groupId) && + ((p.id && element.controlId === p.id) || + (p.conceptId && element.control!.conceptId === p.conceptId) || + (p.areaId && element.areaId === p.areaId)) + ) + if (!payloadItem) continue + const { extension } = payloadItem + // 设置值 + this.setControlProperties( + { + extension + }, + { + elementList, + range: { startIndex: i, endIndex: i } + } + ) + // 修改后控件结束索引 + let newEndIndex = i + while (newEndIndex < elementList.length) { + const nextElement = elementList[newEndIndex] + if (nextElement.controlId !== element.controlId) break + newEndIndex++ + } + i = newEndIndex + } + } + const data = [ + this.draw.getHeaderElementList(), + this.draw.getOriginalMainElementList(), + this.draw.getFooterElementList() + ] + for (const elementList of data) { + setExtension(elementList) + } + } + + public setPropertiesListById(payload: ISetControlProperties[]) { + if (!payload.length) return + let isExistUpdate = false + let isExistSubmitHistory = false + const setProperties = (elementList: IElement[]) => { + let i = 0 + while (i < elementList.length) { + const element = elementList[i] + i++ + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + setProperties(td.value) + } + } + } + if (!element.control) continue + // 获取设置值优先id、conceptId、areaId并于groupId组合设置 + const payloadItem = payload.find( + p => + (!p.groupId || p.groupId === element.control?.groupId) && + ((p.id && element.controlId === p.id) || + (p.conceptId && element.control!.conceptId === p.conceptId) || + (p.areaId && element.areaId === p.areaId)) + ) + if (!payloadItem) continue + const { properties, isSubmitHistory = true } = payloadItem + isExistUpdate = true + if (isSubmitHistory) { + isExistSubmitHistory = true + } + // 设置属性 + this.setControlProperties( + { + ...element.control, + ...properties, + value: element.control.value + }, + { + elementList, + range: { startIndex: i, endIndex: i } + } + ) + // 控件默认样式 + CONTROL_STYLE_ATTR.forEach(key => { + const controlStyleProperty = properties[key] + if (controlStyleProperty) { + Reflect.set(element, key, controlStyleProperty) + } + }) + // 修改后控件结束索引 + let newEndIndex = i + while (newEndIndex < elementList.length) { + const nextElement = elementList[newEndIndex] + if (nextElement.controlId !== element.controlId) break + newEndIndex++ + } + i = newEndIndex + } + } + // 页眉页脚正文启动搜索 + const pageComponentData: IEditorData = { + header: this.draw.getHeaderElementList(), + main: this.draw.getOriginalMainElementList(), + footer: this.draw.getFooterElementList() + } + for (const key in pageComponentData) { + const elementList = pageComponentData[key]! + setProperties(elementList) + } + if (!isExistUpdate) return + // 强制更新 + for (const key in pageComponentData) { + const pageComponentKey = key + const elementList = zipElementList(pageComponentData[pageComponentKey]!, { + isClassifyArea: true, + extraPickAttrs: ['id'] + }) + pageComponentData[pageComponentKey] = elementList + formatElementList(elementList, { + editorOptions: this.options, + isForceCompensation: true + }) + } + this.draw.setEditorData(pageComponentData) + // 不保存历史时需清空之前记录,避免还原 + if (!isExistSubmitHistory) { + this.draw.getHistoryManager().recovery() + } + this.draw.render({ + isSubmitHistory: isExistSubmitHistory, + isSetCursor: false + }) + } + + public getList(): IElement[] { + const controlElementList: IElement[] = [] + function getControlElementList(elementList: IElement[]) { + for (let e = 0; e < elementList.length; e++) { + const element = elementList[e] + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const tdElement = td.value + getControlElementList(tdElement) + } + } + } + if (element.controlId) { + // 移除控件所在标题及列表上下文信息 + const controlElement = omitObject(element, [ + ...TITLE_CONTEXT_ATTR, + ...LIST_CONTEXT_ATTR + ]) + controlElementList.push(controlElement) + } + } + } + const data = [ + this.draw.getHeader().getElementList(), + this.draw.getOriginalMainElementList(), + this.draw.getFooter().getElementList() + ] + for (const elementList of data) { + getControlElementList(elementList) + } + return zipElementList(controlElementList, { + extraPickAttrs: ['controlId'] + }) + } + + public recordBorderInfo(x: number, y: number, width: number, height: number) { + this.controlBorder.recordBorderInfo(x, y, width, height) + } + + public drawBorder(ctx: CanvasRenderingContext2D) { + this.controlBorder.render(ctx) + } + + public getPreControlContext(): INextControlContext | null { + if (!this.activeControl) return null + const position = this.draw.getPosition() + const positionContext = position.getPositionContext() + if (!positionContext) return null + const controlElement = this.activeControl.getElement() + // 获取上一个控件上下文本信息 + function getPreContext( + elementList: IElement[], + start: number + ): INextControlContext | null { + for (let e = start; e > 0; e--) { + const element = elementList[e] + // 表格元素 + if (element.type === ElementType.TABLE) { + const trList = element.trList || [] + for (let r = trList.length - 1; r >= 0; r--) { + const tr = trList[r] + const tdList = tr.tdList + for (let d = tdList.length - 1; d >= 0; d--) { + const td = tdList[d] + const context = getPreContext(td.value, td.value.length - 1) + if (context) { + return { + positionContext: { + isTable: true, + index: e, + trIndex: r, + tdIndex: d, + tdId: td.id, + trId: tr.id, + tableId: element.id + }, + nextIndex: context.nextIndex + } + } + } + } + } + if ( + !element.controlId || + element.controlId === controlElement.controlId + ) { + continue + } + // 找到尾部第一个非占位符元素 + let nextIndex = e + while (nextIndex > 0) { + const nextElement = elementList[nextIndex] + if ( + nextElement.controlComponent === ControlComponent.VALUE || + nextElement.controlComponent === ControlComponent.PREFIX || + nextElement.controlComponent === ControlComponent.PRE_TEXT + ) { + break + } + nextIndex-- + } + return { + positionContext: { + isTable: false + }, + nextIndex + } + } + return null + } + // 当前上下文控件信息 + const { startIndex } = this.range.getRange() + const elementList = this.getElementList() + const context = getPreContext(elementList, startIndex) + if (context) { + return { + positionContext: positionContext.isTable + ? positionContext + : context.positionContext, + nextIndex: context.nextIndex + } + } + // 控件在单元内时继续循环 + if (controlElement.tableId) { + const originalElementList = this.draw.getOriginalElementList() + const { index, trIndex, tdIndex } = positionContext + const trList = originalElementList[index!].trList! + for (let r = trIndex!; r >= 0; r--) { + const tr = trList[r] + const tdList = tr.tdList + for (let d = tdList.length - 1; d >= 0; d--) { + if (trIndex === r && d >= tdIndex!) continue + const td = tdList[d] + const context = getPreContext(td.value, td.value.length - 1) + if (context) { + return { + positionContext: { + isTable: true, + index: positionContext.index, + trIndex: r, + tdIndex: d, + tdId: td.id, + trId: tr.id, + tableId: controlElement.tableId + }, + nextIndex: context.nextIndex + } + } + } + } + // 跳出表格继续循环 + const context = getPreContext(originalElementList, index! - 1) + if (context) { + return { + positionContext: { + isTable: false + }, + nextIndex: context.nextIndex + } + } + } + return null + } + + public getNextControlContext(): INextControlContext | null { + if (!this.activeControl) return null + const position = this.draw.getPosition() + const positionContext = position.getPositionContext() + if (!positionContext) return null + const controlElement = this.activeControl.getElement() + // 获取下一个控件上下文本信息 + function getNextContext( + elementList: IElement[], + start: number + ): INextControlContext | null { + for (let e = start; e < elementList.length; e++) { + const element = elementList[e] + // 表格元素 + if (element.type === ElementType.TABLE) { + const trList = element.trList || [] + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + const tdList = tr.tdList + for (let d = 0; d < tdList.length; d++) { + const td = tdList[d] + const context = getNextContext(td.value!, 0) + if (context) { + return { + positionContext: { + isTable: true, + index: e, + trIndex: r, + tdIndex: d, + tdId: td.id, + trId: tr.id, + tableId: element.id + }, + nextIndex: context.nextIndex + } + } + } + } + } + if ( + !element.controlId || + element.controlId === controlElement.controlId || + elementList[e + 1]?.controlComponent === ControlComponent.PREFIX || + elementList[e + 1]?.controlComponent === ControlComponent.PRE_TEXT + ) { + continue + } + return { + positionContext: { + isTable: false + }, + nextIndex: e + } + } + return null + } + // 当前上下文控件信息 + const { endIndex } = this.range.getRange() + const elementList = this.getElementList() + const context = getNextContext(elementList, endIndex) + if (context) { + return { + positionContext: positionContext.isTable + ? positionContext + : context.positionContext, + nextIndex: context.nextIndex + } + } + // 控件在单元内时继续循环 + if (controlElement.tableId) { + const originalElementList = this.draw.getOriginalElementList() + const { index, trIndex, tdIndex } = positionContext + const trList = originalElementList[index!].trList! + for (let r = trIndex!; r < trList.length; r++) { + const tr = trList[r] + const tdList = tr.tdList + for (let d = 0; d < tdList.length; d++) { + if (trIndex === r && d <= tdIndex!) continue + const td = tdList[d] + const context = getNextContext(td.value, 0) + if (context) { + return { + positionContext: { + isTable: true, + index: positionContext.index, + trIndex: r, + tdIndex: d, + tdId: td.id, + trId: tr.id, + tableId: controlElement.tableId + }, + nextIndex: context.nextIndex + } + } + } + } + // 跳出表格继续循环 + const context = getNextContext(originalElementList, index! + 1) + if (context) { + return { + positionContext: { + isTable: false + }, + nextIndex: context.nextIndex + } + } + } + return null + } + + public initNextControl(option: IInitNextControlOption = {}) { + const { direction = MoveDirection.DOWN } = option + let context: INextControlContext | null = null + if (direction === MoveDirection.UP) { + context = this.getPreControlContext() + } else { + context = this.getNextControlContext() + } + if (!context) return + const { nextIndex, positionContext } = context + const position = this.draw.getPosition() + // 设置上下文 + position.setPositionContext(positionContext) + this.draw.getRange().replaceRange({ + startIndex: nextIndex, + endIndex: nextIndex + }) + // 重新渲染并定位 + this.draw.render({ + curIndex: nextIndex, + isCompute: false, + isSetCursor: true, + isSubmitHistory: false + }) + const positionList = position.getPositionList() + this.draw.getCursor().moveCursorToVisible({ + cursorPosition: positionList[nextIndex], + direction + }) + } + + public setMinWidthControlInfo(option: ISetControlRowFlexOption) { + const { row, rowElement, controlRealWidth, availableWidth } = option + if (!rowElement.control?.minWidth) return + const { scale } = this.options + const controlMinWidth = rowElement.control.minWidth * scale + // 设置首字符偏移量:如果控件内设置对齐方式&&存在设置最小宽度 + let controlFirstElement: IRowElement | null = null + if ( + rowElement.control?.minWidth && + (rowElement.control?.rowFlex === RowFlex.CENTER || + rowElement.control?.rowFlex === RowFlex.RIGHT) + ) { + // 计算当前控件内容宽度是否超出最小宽度设置 + let controlContentWidth = rowElement.metrics.width + let controlElementIndex = row.elementList.length - 1 + while (controlElementIndex >= 0) { + const controlRowElement = row.elementList[controlElementIndex] + controlContentWidth += controlRowElement.metrics.width + // 找到首字符结束循环 + if ( + row.elementList[controlElementIndex - 1]?.controlComponent === + ControlComponent.PREFIX + ) { + controlFirstElement = controlRowElement + break + } + controlElementIndex-- + } + // 计算首字符偏移量 + if (controlFirstElement) { + if (controlContentWidth < controlMinWidth) { + if (rowElement.control.rowFlex === RowFlex.CENTER) { + controlFirstElement.left = + (controlMinWidth - controlContentWidth) / 2 + } else if (rowElement.control.rowFlex === RowFlex.RIGHT) { + // 最小宽度 - 实际宽度 - 后缀元素宽度 + controlFirstElement.left = + controlMinWidth - controlContentWidth - rowElement.metrics.width + } + } + } + } + // 设置后缀偏移量:消费小于实际最小宽度 + const extraWidth = controlMinWidth - controlRealWidth + if (extraWidth > 0) { + const controlFirstElementLeft = controlFirstElement?.left || 0 + // 超出行宽时截断 + const rowRemainingWidth = + availableWidth - row.width - rowElement.metrics.width + const left = Math.min(rowRemainingWidth, extraWidth) + // 后缀偏移量需减去首字符的偏移量,避免重复偏移 + rowElement.left = left - controlFirstElementLeft + row.width += left - controlFirstElementLeft + } + } +} diff --git a/src/editor/core/draw/control/checkbox/CheckboxControl.ts b/src/editor/core/draw/control/checkbox/CheckboxControl.ts new file mode 100644 index 0000000..3bb2f92 --- /dev/null +++ b/src/editor/core/draw/control/checkbox/CheckboxControl.ts @@ -0,0 +1,154 @@ +import { ControlComponent } from '../../../../dataset/enum/Control' +import { KeyMap } from '../../../../dataset/enum/KeyMap' +import { + IControlContext, + IControlInstance, + IControlRuleOption +} from '../../../../interface/Control' +import { IElement } from '../../../../interface/Element' +import { Control } from '../Control' + +export class CheckboxControl implements IControlInstance { + protected element: IElement + protected control: Control + + constructor(element: IElement, control: Control) { + this.element = element + this.control = control + } + + public setElement(element: IElement) { + this.element = element + } + + public getElement(): IElement { + return this.element + } + + public getCode(): string | null { + return this.element.control?.code || null + } + + public getValue(): IElement[] { + const elementList = this.control.getElementList() + const { startIndex } = this.control.getRange() + const startElement = elementList[startIndex] + const data: IElement[] = [] + // 向左查找 + let preIndex = startIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if ( + preElement.controlId !== startElement.controlId || + preElement.controlComponent === ControlComponent.PREFIX || + preElement.controlComponent === ControlComponent.PRE_TEXT + ) { + break + } + if (preElement.controlComponent === ControlComponent.VALUE) { + data.unshift(preElement) + } + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if ( + nextElement.controlId !== startElement.controlId || + nextElement.controlComponent === ControlComponent.POSTFIX || + nextElement.controlComponent === ControlComponent.POST_TEXT + ) { + break + } + if (nextElement.controlComponent === ControlComponent.VALUE) { + data.push(nextElement) + } + nextIndex++ + } + return data + } + + public setValue(): number { + return -1 + } + + public setSelect( + codes: string[], + context: IControlContext = {}, + options: IControlRuleOption = {} + ) { + // 校验是否可以设置 + if ( + !options.isIgnoreDisabledRule && + this.control.getIsDisabledControl(context) + ) { + return + } + const { control } = this.element + const elementList = context.elementList || this.control.getElementList() + const { startIndex } = context.range || this.control.getRange() + const startElement = elementList[startIndex] + // 向左查找 + let preIndex = startIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if ( + preElement.controlId !== startElement.controlId || + preElement.controlComponent === ControlComponent.PREFIX || + preElement.controlComponent === ControlComponent.PRE_TEXT + ) { + break + } + if (preElement.controlComponent === ControlComponent.CHECKBOX) { + const checkbox = preElement.checkbox! + checkbox.value = codes.includes(checkbox.code!) + } + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if ( + nextElement.controlId !== startElement.controlId || + nextElement.controlComponent === ControlComponent.POSTFIX || + nextElement.controlComponent === ControlComponent.POST_TEXT + ) { + break + } + if (nextElement.controlComponent === ControlComponent.CHECKBOX) { + const checkbox = nextElement.checkbox! + checkbox.value = codes.includes(checkbox.code!) + } + nextIndex++ + } + control!.code = codes.join(',') + this.control.repaintControl({ + curIndex: startIndex, + isSetCursor: false + }) + this.control.emitControlContentChange({ + context + }) + } + + public keydown(evt: KeyboardEvent): number | null { + if (this.control.getIsDisabledControl()) { + return null + } + const range = this.control.getRange() + // 收缩边界到Value内 + this.control.shrinkBoundary() + const { startIndex, endIndex } = range + // 删除 + if (evt.key === KeyMap.Backspace || evt.key === KeyMap.Delete) { + return this.control.removeControl(startIndex) + } + return endIndex + } + + public cut(): number { + return -1 + } +} diff --git a/src/editor/core/draw/control/date/DateControl.ts b/src/editor/core/draw/control/date/DateControl.ts new file mode 100644 index 0000000..cf90192 --- /dev/null +++ b/src/editor/core/draw/control/date/DateControl.ts @@ -0,0 +1,397 @@ +import { + CONTROL_STYLE_ATTR, + EDITOR_ELEMENT_STYLE_ATTR, + TEXTLIKE_ELEMENT_TYPE +} from '../../../../dataset/constant/Element' +import { ControlComponent } from '../../../../dataset/enum/Control' +import { ElementType } from '../../../../dataset/enum/Element' +import { KeyMap } from '../../../../dataset/enum/KeyMap' +import { DeepRequired } from '../../../../interface/Common' +import { + IControlContext, + IControlInstance, + IControlRuleOption +} from '../../../../interface/Control' +import { IEditorOption } from '../../../../interface/Editor' +import { IElement } from '../../../../interface/Element' +import { omitObject, pickObject } from '../../../../utils' +import { formatElementContext } from '../../../../utils/element' +import { Draw } from '../../Draw' +import { DatePicker } from '../../particle/date/DatePicker' +import { Control } from '../Control' + +export class DateControl implements IControlInstance { + private draw: Draw + private element: IElement + private control: Control + private isPopup: boolean + private datePicker: DatePicker | null + private options: DeepRequired + + constructor(element: IElement, control: Control) { + const draw = control.getDraw() + this.draw = draw + this.options = draw.getOptions() + this.element = element + this.control = control + this.isPopup = false + this.datePicker = null + } + + public setElement(element: IElement) { + this.element = element + } + + public getElement(): IElement { + return this.element + } + + public getIsPopup(): boolean { + return this.isPopup + } + + public getValueRange(context: IControlContext = {}): [number, number] | null { + const elementList = context.elementList || this.control.getElementList() + const { startIndex } = context.range || this.control.getRange() + const startElement = elementList[startIndex] + // 向左查找 + let preIndex = startIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if ( + preElement.controlId !== startElement.controlId || + preElement.controlComponent === ControlComponent.PREFIX || + preElement.controlComponent === ControlComponent.PRE_TEXT + ) { + break + } + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if ( + nextElement.controlId !== startElement.controlId || + nextElement.controlComponent === ControlComponent.POSTFIX || + nextElement.controlComponent === ControlComponent.POST_TEXT + ) { + break + } + nextIndex++ + } + if (preIndex === nextIndex) return null + return [preIndex, nextIndex - 1] + } + + public getValue(context: IControlContext = {}): IElement[] { + const elementList = context.elementList || this.control.getElementList() + const range = this.getValueRange(context) + if (!range) return [] + const data: IElement[] = [] + const [startIndex, endIndex] = range + for (let i = startIndex; i <= endIndex; i++) { + const element = elementList[i] + if (element.controlComponent === ControlComponent.VALUE) { + data.push(element) + } + } + return data + } + + public setValue( + data: IElement[], + context: IControlContext = {}, + options: IControlRuleOption = {} + ): number { + // 校验是否可以设置 + if ( + !options.isIgnoreDisabledRule && + this.control.getIsDisabledControl(context) + ) { + return -1 + } + const elementList = context.elementList || this.control.getElementList() + const range = context.range || this.control.getRange() + // 收缩边界到Value内 + this.control.shrinkBoundary(context) + const { startIndex, endIndex } = range + const draw = this.control.getDraw() + // 移除选区元素 + if (startIndex !== endIndex) { + draw.spliceElementList(elementList, startIndex + 1, endIndex - startIndex) + } else { + // 移除空白占位符 + this.control.removePlaceholder(startIndex, context) + } + // 非文本类元素或前缀过渡掉样式属性 + const startElement = elementList[startIndex] + const anchorElement = + (startElement.type && + !TEXTLIKE_ELEMENT_TYPE.includes(startElement.type)) || + startElement.controlComponent === ControlComponent.PREFIX || + startElement.controlComponent === ControlComponent.PRE_TEXT + ? pickObject(startElement, [ + 'control', + 'controlId', + ...CONTROL_STYLE_ATTR + ]) + : omitObject(startElement, ['type']) + // 插入起始位置 + const start = range.startIndex + 1 + for (let i = 0; i < data.length; i++) { + const newElement: IElement = { + ...anchorElement, + ...data[i], + controlComponent: ControlComponent.VALUE + } + formatElementContext(elementList, [newElement], startIndex, { + editorOptions: this.options + }) + draw.spliceElementList(elementList, start + i, 0, [newElement]) + } + return start + data.length - 1 + } + + public clearSelect( + context: IControlContext = {}, + options: IControlRuleOption = {} + ): number { + const { isIgnoreDisabledRule = false, isAddPlaceholder = true } = options + // 校验是否可以设置 + if (!isIgnoreDisabledRule && this.control.getIsDisabledControl(context)) { + return -1 + } + const range = this.getValueRange(context) + if (!range) return -1 + const [leftIndex, rightIndex] = range + if (!~leftIndex || !~rightIndex) return -1 + const elementList = context.elementList || this.control.getElementList() + // 删除元素 + const draw = this.control.getDraw() + draw.spliceElementList( + elementList, + leftIndex + 1, + rightIndex - leftIndex, + [], + { + isIgnoreDeletedRule: options.isIgnoreDeletedRule + } + ) + // 增加占位符 + if (isAddPlaceholder) { + this.control.addPlaceholder(leftIndex, context) + } + return leftIndex + } + + public setSelect( + date: string, + context: IControlContext = {}, + options: IControlRuleOption = {} + ) { + // 校验是否可以设置 + if ( + !options.isIgnoreDisabledRule && + this.control.getIsDisabledControl(context) + ) { + return + } + const elementList = context.elementList || this.control.getElementList() + const range = context.range || this.control.getRange() + // 样式赋值元素-默认值的第一个字符样式,否则取默认样式 + const valueElement = this.getValue(context)[0] + const styleElement = valueElement + ? pickObject(valueElement, EDITOR_ELEMENT_STYLE_ATTR) + : pickObject(elementList[range.startIndex], CONTROL_STYLE_ATTR) + // 清空选项 + const prefixIndex = this.clearSelect(context, { + isAddPlaceholder: false, + isIgnoreDeletedRule: options.isIgnoreDeletedRule + }) + if (!~prefixIndex) return + // 属性赋值元素-默认为前缀属性 + const propertyElement = omitObject( + elementList[prefixIndex], + EDITOR_ELEMENT_STYLE_ATTR + ) + const start = prefixIndex + 1 + const draw = this.control.getDraw() + for (let i = 0; i < date.length; i++) { + const newElement: IElement = { + ...styleElement, + ...propertyElement, + type: ElementType.TEXT, + value: date[i], + controlComponent: ControlComponent.VALUE + } + formatElementContext(elementList, [newElement], prefixIndex, { + editorOptions: this.options + }) + draw.spliceElementList(elementList, start + i, 0, [newElement]) + } + // 重新渲染控件 + if (!context.range) { + const newIndex = start + date.length - 1 + this.control.repaintControl({ + curIndex: newIndex + }) + this.control.emitControlContentChange({ + context + }) + this.destroy() + } + } + + public keydown(evt: KeyboardEvent): number | null { + if (this.control.getIsDisabledControl()) { + return null + } + const elementList = this.control.getElementList() + const range = this.control.getRange() + // 收缩边界到Value内 + this.control.shrinkBoundary() + const { startIndex, endIndex } = range + const startElement = elementList[startIndex] + const endElement = elementList[endIndex] + const draw = this.control.getDraw() + // backspace + if (evt.key === KeyMap.Backspace) { + // 移除选区元素 + if (startIndex !== endIndex) { + draw.spliceElementList( + elementList, + startIndex + 1, + endIndex - startIndex + ) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex) + } + return startIndex + } else { + if ( + startElement.controlComponent === ControlComponent.PREFIX || + startElement.controlComponent === ControlComponent.PRE_TEXT || + endElement.controlComponent === ControlComponent.POSTFIX || + endElement.controlComponent === ControlComponent.POST_TEXT || + startElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + // 前缀、后缀、占位符 + return this.control.removeControl(startIndex) + } else { + // 文本 + draw.spliceElementList(elementList, startIndex, 1) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex - 1) + } + return startIndex - 1 + } + } + } else if (evt.key === KeyMap.Delete) { + // 移除选区元素 + if (startIndex !== endIndex) { + draw.spliceElementList( + elementList, + startIndex + 1, + endIndex - startIndex + ) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex) + } + return startIndex + } else { + const endNextElement = elementList[endIndex + 1] + if ( + ((startElement.controlComponent === ControlComponent.PREFIX || + startElement.controlComponent === ControlComponent.PRE_TEXT) && + endNextElement.controlComponent === ControlComponent.PLACEHOLDER) || + endNextElement.controlComponent === ControlComponent.POSTFIX || + endNextElement.controlComponent === ControlComponent.POST_TEXT || + startElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + // 前缀、后缀、占位符 + return this.control.removeControl(startIndex) + } else { + // 文本 + draw.spliceElementList(elementList, startIndex + 1, 1) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex) + } + return startIndex + } + } + } + return endIndex + } + + public cut(): number { + if (this.control.getIsDisabledControl()) { + return -1 + } + this.control.shrinkBoundary() + const { startIndex, endIndex } = this.control.getRange() + if (startIndex === endIndex) { + return startIndex + } + const draw = this.control.getDraw() + const elementList = this.control.getElementList() + draw.spliceElementList(elementList, startIndex + 1, endIndex - startIndex) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex) + } + return startIndex + } + + public awake() { + if ( + this.isPopup || + this.control.getIsDisabledControl() || + !this.control.getIsRangeWithinControl() + ) { + return + } + const position = this.control.getPosition() + if (!position) return + const elementList = this.draw.getElementList() + const { startIndex } = this.control.getRange() + if (elementList[startIndex + 1]?.controlId !== this.element.controlId) { + return + } + // 渲染日期控件 + this.datePicker = new DatePicker(this.draw, { + onSubmit: this._setDate.bind(this) + }) + const value = + this.getValue() + .map(el => el.value) + .join('') || '' + const dateFormat = this.element.control?.dateFormat + this.datePicker.render({ + value, + position, + dateFormat + }) + // 弹窗状态 + this.isPopup = true + } + + public destroy() { + if (!this.isPopup) return + this.datePicker?.destroy() + this.isPopup = false + } + + private _setDate(date: string) { + if (!date) { + this.clearSelect() + } else { + this.setSelect(date) + } + this.destroy() + } +} diff --git a/src/editor/core/draw/control/interactive/ControlSearch.ts b/src/editor/core/draw/control/interactive/ControlSearch.ts new file mode 100644 index 0000000..c4770ba --- /dev/null +++ b/src/editor/core/draw/control/interactive/ControlSearch.ts @@ -0,0 +1,214 @@ +import { ZERO } from '../../../../dataset/constant/Common' +import { ControlComponent } from '../../../../dataset/enum/Control' +import { ElementType } from '../../../../dataset/enum/Element' +import { DeepRequired } from '../../../../interface/Common' +import { + IControlHighlight, + IControlHighlightRule +} from '../../../../interface/Control' +import { IEditorOption } from '../../../../interface/Editor' +import { IElement, IElementPosition } from '../../../../interface/Element' +import { + ISearchResult, + ISearchResultRestArgs +} from '../../../../interface/Search' +import { Draw } from '../../Draw' +import { Control } from '../Control' + +type IHighlightMatchResult = (ISearchResult & IControlHighlightRule)[] + +export class ControlSearch { + private draw: Draw + private control: Control + private options: DeepRequired + private highlightList: IControlHighlight[] + private highlightMatchResult: IHighlightMatchResult + + constructor(control: Control) { + this.draw = control.getDraw() + this.control = control + this.options = this.draw.getOptions() + + this.highlightList = [] + this.highlightMatchResult = [] + } + + // 获取控件设置高亮信息 + public getControlHighlight(elementList: IElement[], index: number) { + const { + control: { + activeBackgroundColor, + disabledBackgroundColor, + existValueBackgroundColor, + noValueBackgroundColor + } + } = this.options + const element = elementList[index] + const isPrintMode = this.draw.isPrintMode() + const activeControlElement = this.control.getActiveControl()?.getElement() + // 颜色配置:元素 > 控件激活 > 控件禁用 > 控件存在值 > 控件不存在值 + let isActiveControlHighlight = false + let isDisabledControlHighlight = false + let isExitsValueControlHighlight = false + let isNoValueControlHighlight = false + if (!element.highlight) { + // 控件激活时高亮色 + isActiveControlHighlight = + !isPrintMode && + !!activeBackgroundColor && + !!activeControlElement && + element.controlId === activeControlElement.controlId && + !this.control.getIsRangeInPostfix() + } + if (!isActiveControlHighlight) { + // 控件禁用时高亮色 + isDisabledControlHighlight = + !isPrintMode && !!disabledBackgroundColor && !!element.control?.disabled + } + if (!isDisabledControlHighlight) { + // 控件存在值时高亮色 + isExitsValueControlHighlight = + !isPrintMode && + !!existValueBackgroundColor && + !!element.controlId && + this.control.getIsExistValueByElementListIndex(elementList, index) + } + if (!isExitsValueControlHighlight) { + // 控件不存在值时高亮色 + isNoValueControlHighlight = + !isPrintMode && + !!noValueBackgroundColor && + !!element.controlId && + !this.control.getIsExistValueByElementListIndex(elementList, index) + } + return ( + (isActiveControlHighlight ? activeBackgroundColor : '') || + (isDisabledControlHighlight ? disabledBackgroundColor : '') || + (isExitsValueControlHighlight ? existValueBackgroundColor : '') || + (isNoValueControlHighlight ? noValueBackgroundColor : '') + ) + } + + public getHighlightMatchResult(): IHighlightMatchResult { + return this.highlightMatchResult + } + + public getHighlightList(): IControlHighlight[] { + return this.highlightList + } + + public setHighlightList(payload: IControlHighlight[]) { + this.highlightList = payload + } + + public computeHighlightList() { + const search = this.draw.getSearch() + const computeHighlight = ( + elementList: IElement[], + restArgs?: ISearchResultRestArgs + ) => { + let i = 0 + while (i < elementList.length) { + const element = elementList[i] + i++ + // 表格下钻处理 + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const restArgs: ISearchResultRestArgs = { + tableId: element.id, + tableIndex: i - 1, + trIndex: r, + tdIndex: d, + tdId: td.id + } + computeHighlight(td.value, restArgs) + } + } + } + const currentControl = element?.control + if (!currentControl) continue + const highlightIndex = this.highlightList.findIndex( + highlight => + highlight.id === element.controlId || + (currentControl.conceptId && + currentControl.conceptId === highlight.conceptId) + ) + if (!~highlightIndex) continue + // 搜索后控件结束索引 + const startIndex = i + let newEndIndex = i + while (newEndIndex < elementList.length) { + const nextElement = elementList[newEndIndex] + if (nextElement.controlId !== element.controlId) break + newEndIndex++ + } + i = newEndIndex + // 高亮信息 + const controlElementList = elementList + .slice(startIndex, newEndIndex) + .map(element => + element.controlComponent === ControlComponent.VALUE + ? element + : { value: ZERO } + ) + const highlight = this.highlightList[highlightIndex] + const { ruleList } = highlight + for (let r = 0; r < ruleList.length; r++) { + const rule = ruleList[r] + const searchResult = search.getMatchList( + rule.keyword, + controlElementList + ) + this.highlightMatchResult.push( + ...searchResult.map(result => ({ + ...result, + ...rule, + ...restArgs, + index: result.index + startIndex // 实际索引 + })) + ) + } + } + } + this.highlightMatchResult = [] + computeHighlight(this.draw.getOriginalMainElementList()) + } + + public renderHighlightList(ctx: CanvasRenderingContext2D, pageIndex: number) { + if (!this.highlightMatchResult?.length) return + const { searchMatchAlpha, searchMatchColor } = this.options + const positionList = this.draw.getPosition().getOriginalPositionList() + const elementList = this.draw.getOriginalElementList() + ctx.save() + for (let s = 0; s < this.highlightMatchResult.length; s++) { + const searchMatch = this.highlightMatchResult[s] + let position: IElementPosition | null = null + if (searchMatch.tableId) { + const { tableIndex, trIndex, tdIndex, index } = searchMatch + position = + elementList[tableIndex!]?.trList![trIndex!].tdList[tdIndex!] + ?.positionList![index] + } else { + position = positionList[searchMatch.index] + } + if (!position) continue + const { + coordinate: { leftTop, leftBottom, rightTop }, + pageNo + } = position + if (pageNo !== pageIndex) continue + ctx.fillStyle = searchMatch.backgroundColor || searchMatchColor + ctx.globalAlpha = searchMatch.alpha || searchMatchAlpha + const x = leftTop[0] + const y = leftTop[1] + const width = rightTop[0] - leftTop[0] + const height = leftBottom[1] - leftTop[1] + ctx.fillRect(x, y, width, height) + } + ctx.restore() + } +} diff --git a/src/editor/core/draw/control/number/NumberControl.ts b/src/editor/core/draw/control/number/NumberControl.ts new file mode 100644 index 0000000..07ecefb --- /dev/null +++ b/src/editor/core/draw/control/number/NumberControl.ts @@ -0,0 +1,3 @@ +import { TextControl } from '../text/TextControl' + +export class NumberControl extends TextControl {} diff --git a/src/editor/core/draw/control/radio/RadioControl.ts b/src/editor/core/draw/control/radio/RadioControl.ts new file mode 100644 index 0000000..a084439 --- /dev/null +++ b/src/editor/core/draw/control/radio/RadioControl.ts @@ -0,0 +1,68 @@ +import { ControlComponent } from '../../../../dataset/enum/Control' +import { + IControlContext, + IControlRuleOption +} from '../../../../interface/Control' +import { CheckboxControl } from '../checkbox/CheckboxControl' + +export class RadioControl extends CheckboxControl { + public setSelect( + codes: string[], + context: IControlContext = {}, + options: IControlRuleOption = {} + ) { + // 校验是否可以设置 + if ( + !options.isIgnoreDisabledRule && + this.control.getIsDisabledControl(context) + ) { + return + } + const { control } = this.element + const elementList = context.elementList || this.control.getElementList() + const { startIndex } = context.range || this.control.getRange() + const startElement = elementList[startIndex] + // 向左查找 + let preIndex = startIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if ( + preElement.controlId !== startElement.controlId || + preElement.controlComponent === ControlComponent.PREFIX || + preElement.controlComponent === ControlComponent.PRE_TEXT + ) { + break + } + if (preElement.controlComponent === ControlComponent.RADIO) { + const radio = preElement.radio! + radio.value = codes.includes(radio.code!) + } + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if ( + nextElement.controlId !== startElement.controlId || + nextElement.controlComponent === ControlComponent.POSTFIX || + nextElement.controlComponent === ControlComponent.POST_TEXT + ) { + break + } + if (nextElement.controlComponent === ControlComponent.RADIO) { + const radio = nextElement.radio! + radio.value = codes.includes(radio.code!) + } + nextIndex++ + } + control!.code = codes.join(',') + this.control.repaintControl({ + curIndex: startIndex, + isSetCursor: false + }) + this.control.emitControlContentChange({ + context + }) + } +} diff --git a/src/editor/core/draw/control/richtext/Border.ts b/src/editor/core/draw/control/richtext/Border.ts new file mode 100644 index 0000000..934a822 --- /dev/null +++ b/src/editor/core/draw/control/richtext/Border.ts @@ -0,0 +1,52 @@ +import { DeepRequired } from '../../../../interface/Common' +import { IEditorOption } from '../../../../interface/Editor' +import { IElementFillRect } from '../../../../interface/Element' +import { Draw } from '../../Draw' + +export class ControlBorder { + protected borderRect: IElementFillRect + private options: DeepRequired + + constructor(draw: Draw) { + this.borderRect = this.clearBorderInfo() + this.options = draw.getOptions() + } + + public clearBorderInfo() { + this.borderRect = { + x: 0, + y: 0, + width: 0, + height: 0 + } + return this.borderRect + } + + public recordBorderInfo(x: number, y: number, width: number, height: number) { + const isFirstRecord = !this.borderRect.width + if (isFirstRecord) { + this.borderRect.x = x + this.borderRect.y = y + this.borderRect.height = height + } + this.borderRect.width += width + } + + public render(ctx: CanvasRenderingContext2D) { + if (!this.borderRect.width) return + const { + scale, + control: { borderWidth, borderColor } + } = this.options + const { x, y, width, height } = this.borderRect + ctx.save() + ctx.translate(0, 1 * scale) + ctx.lineWidth = borderWidth * scale + ctx.strokeStyle = borderColor + ctx.beginPath() + ctx.rect(x, y, width, height) + ctx.stroke() + ctx.restore() + this.clearBorderInfo() + } +} diff --git a/src/editor/core/draw/control/select/SelectControl.ts b/src/editor/core/draw/control/select/SelectControl.ts new file mode 100644 index 0000000..e0fa272 --- /dev/null +++ b/src/editor/core/draw/control/select/SelectControl.ts @@ -0,0 +1,512 @@ +import { + EDITOR_COMPONENT, + EDITOR_PREFIX +} from '../../../../dataset/constant/Editor' +import { + CONTROL_STYLE_ATTR, + EDITOR_ELEMENT_STYLE_ATTR, + TEXTLIKE_ELEMENT_TYPE +} from '../../../../dataset/constant/Element' +import { ControlComponent } from '../../../../dataset/enum/Control' +import { EditorComponent } from '../../../../dataset/enum/Editor' +import { ElementType } from '../../../../dataset/enum/Element' +import { KeyMap } from '../../../../dataset/enum/KeyMap' +import { DeepRequired } from '../../../../interface/Common' +import { + IControlContext, + IControlInstance, + IControlRuleOption +} from '../../../../interface/Control' +import { IEditorOption } from '../../../../interface/Editor' +import { IElement } from '../../../../interface/Element' +import { + isArrayEqual, + isNonValue, + omitObject, + pickObject, + splitText +} from '../../../../utils' +import { formatElementContext } from '../../../../utils/element' +import { Control } from '../Control' + +export class SelectControl implements IControlInstance { + private element: IElement + private control: Control + private isPopup: boolean + private selectDom: HTMLDivElement | null + private options: DeepRequired + private VALUE_DELIMITER = ',' + private DEFAULT_MULTI_SELECT_DELIMITER = ',' + + constructor(element: IElement, control: Control) { + const draw = control.getDraw() + this.options = draw.getOptions() + this.element = element + this.control = control + this.isPopup = false + this.selectDom = null + } + + public setElement(element: IElement) { + this.element = element + } + + public getElement(): IElement { + return this.element + } + + public getIsPopup(): boolean { + return this.isPopup + } + + public getCodes(): string[] { + return this.element?.control?.code + ? this.element.control.code.split(',') + : [] + } + + public getText(codes: string[]): string | null { + if (!this.element?.control) return null + const control = this.element.control + if (!control.valueSets?.length) return null + const multiSelectDelimiter = + control?.multiSelectDelimiter || this.DEFAULT_MULTI_SELECT_DELIMITER + const valueSets = control.valueSets + const valueList: string[] = [] + codes.forEach(code => { + const valueSet = valueSets.find(v => v.code === code) + if (valueSet && !isNonValue(valueSet.value)) { + valueList.push(valueSet.value) + } + }) + return valueList.join(multiSelectDelimiter) || null + } + + public getValue(context: IControlContext = {}): IElement[] { + const elementList = context.elementList || this.control.getElementList() + const { startIndex } = context.range || this.control.getRange() + const startElement = elementList[startIndex] + const data: IElement[] = [] + // 向左查找 + let preIndex = startIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if ( + preElement.controlId !== startElement.controlId || + preElement.controlComponent === ControlComponent.PREFIX || + preElement.controlComponent === ControlComponent.PRE_TEXT + ) { + break + } + if (preElement.controlComponent === ControlComponent.VALUE) { + data.unshift(preElement) + } + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if ( + nextElement.controlId !== startElement.controlId || + nextElement.controlComponent === ControlComponent.POSTFIX || + nextElement.controlComponent === ControlComponent.POST_TEXT + ) { + break + } + if (nextElement.controlComponent === ControlComponent.VALUE) { + data.push(nextElement) + } + nextIndex++ + } + return data + } + + public setValue( + data: IElement[], + context: IControlContext = {}, + options: IControlRuleOption = {} + ): number { + // 校验是否可以设置 + if ( + !this.element.control?.selectExclusiveOptions?.inputAble || + (!options.isIgnoreDisabledRule && + this.control.getIsDisabledControl(context)) + ) { + return -1 + } + const elementList = context.elementList || this.control.getElementList() + const range = context.range || this.control.getRange() + // 收缩边界到Value内 + this.control.shrinkBoundary(context) + const { startIndex, endIndex } = range + const draw = this.control.getDraw() + // 移除选区元素 + if (startIndex !== endIndex) { + draw.spliceElementList(elementList, startIndex + 1, endIndex - startIndex) + } else { + // 移除空白占位符 + this.control.removePlaceholder(startIndex, context) + } + // 非文本类元素或前缀过渡掉样式属性 + const startElement = elementList[startIndex] + const anchorElement = + (startElement.type && + !TEXTLIKE_ELEMENT_TYPE.includes(startElement.type)) || + startElement.controlComponent === ControlComponent.PREFIX || + startElement.controlComponent === ControlComponent.PRE_TEXT + ? pickObject(startElement, [ + 'control', + 'controlId', + ...CONTROL_STYLE_ATTR + ]) + : omitObject(startElement, ['type']) + // 插入起始位置 + const start = range.startIndex + 1 + for (let i = 0; i < data.length; i++) { + const newElement: IElement = { + ...anchorElement, + ...data[i], + controlComponent: ControlComponent.VALUE + } + formatElementContext(elementList, [newElement], startIndex, { + editorOptions: this.options + }) + draw.spliceElementList(elementList, start + i, 0, [newElement]) + } + return start + data.length - 1 + } + + public keydown(evt: KeyboardEvent): number | null { + if (this.control.getIsDisabledControl()) { + return null + } + const elementList = this.control.getElementList() + const range = this.control.getRange() + // 收缩边界到Value内 + this.control.shrinkBoundary() + const { startIndex, endIndex } = range + const startElement = elementList[startIndex] + const endElement = elementList[endIndex] + // backspace + if (evt.key === KeyMap.Backspace) { + // 清空选项 + if (startIndex !== endIndex) { + return this.clearSelect() + } else { + if ( + startElement.controlComponent === ControlComponent.PREFIX || + startElement.controlComponent === ControlComponent.PRE_TEXT || + endElement.controlComponent === ControlComponent.POSTFIX || + endElement.controlComponent === ControlComponent.POST_TEXT || + startElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + // 前缀、后缀、占位符 + return this.control.removeControl(startIndex) + } else { + // 清空选项 + return this.clearSelect() + } + } + } else if (evt.key === KeyMap.Delete) { + // 移除选区元素 + if (startIndex !== endIndex) { + // 清空选项 + return this.clearSelect() + } else { + const endNextElement = elementList[endIndex + 1] + if ( + ((startElement.controlComponent === ControlComponent.PREFIX || + startElement.controlComponent === ControlComponent.PRE_TEXT) && + endNextElement.controlComponent === ControlComponent.PLACEHOLDER) || + endNextElement.controlComponent === ControlComponent.POSTFIX || + endNextElement.controlComponent === ControlComponent.POST_TEXT || + startElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + // 前缀、后缀、占位符 + return this.control.removeControl(startIndex) + } else { + // 清空选项 + return this.clearSelect() + } + } + } + return endIndex + } + + public cut(): number { + if (this.control.getIsDisabledControl()) { + return -1 + } + this.control.shrinkBoundary() + const { startIndex, endIndex } = this.control.getRange() + if (startIndex === endIndex) { + return startIndex + } + // 清空选项 + return this.clearSelect() + } + + public clearSelect( + context: IControlContext = {}, + options: IControlRuleOption = {} + ): number { + const { isIgnoreDisabledRule = false, isAddPlaceholder = true } = options + // 校验是否可以设置 + if (!isIgnoreDisabledRule && this.control.getIsDisabledControl(context)) { + return -1 + } + const elementList = context.elementList || this.control.getElementList() + const { startIndex } = context.range || this.control.getRange() + const startElement = elementList[startIndex] + let leftIndex = -1 + let rightIndex = -1 + // 向左查找 + let preIndex = startIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if ( + preElement.controlId !== startElement.controlId || + preElement.controlComponent === ControlComponent.PREFIX || + preElement.controlComponent === ControlComponent.PRE_TEXT + ) { + leftIndex = preIndex + break + } + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if ( + nextElement.controlId !== startElement.controlId || + nextElement.controlComponent === ControlComponent.POSTFIX || + nextElement.controlComponent === ControlComponent.POST_TEXT + ) { + rightIndex = nextIndex - 1 + break + } + nextIndex++ + } + if (!~leftIndex || !~rightIndex) return -1 + // 删除元素 + const draw = this.control.getDraw() + draw.spliceElementList( + elementList, + leftIndex + 1, + rightIndex - leftIndex, + [], + { + isIgnoreDeletedRule: options.isIgnoreDeletedRule + } + ) + // 增加占位符 + if (isAddPlaceholder) { + this.control.addPlaceholder(preIndex, context) + } + this.control.setControlProperties( + { + code: null + }, + { + elementList, + range: { startIndex: preIndex, endIndex: preIndex } + } + ) + return preIndex + } + + public setSelect( + code: string, + context: IControlContext = {}, + options: IControlRuleOption = {} + ) { + // 校验是否可以设置 + if ( + !options.isIgnoreDisabledRule && + this.control.getIsDisabledControl(context) + ) { + return + } + const elementList = context.elementList || this.control.getElementList() + const range = context.range || this.control.getRange() + const control = this.element.control! + const newCodes = code?.split(this.VALUE_DELIMITER) || [] + // 缓存旧值 + const oldCode = control.code + const oldCodes = control.code?.split(this.VALUE_DELIMITER) || [] + // 选项相同时无需重复渲染 + const isMultiSelect = control.isMultiSelect + if ( + (!isMultiSelect && code === oldCode) || + (isMultiSelect && isArrayEqual(oldCodes, newCodes)) + ) { + this.control.repaintControl({ + curIndex: range.startIndex, + isCompute: false, + isSubmitHistory: false + }) + this.destroy() + return + } + const valueSets = control.valueSets + if (!Array.isArray(valueSets) || !valueSets.length) return + // 转换文本 + const text = this.getText(newCodes) + if (!text) { + // 之前存在内容时清空文本 + if (oldCode) { + const prefixIndex = this.clearSelect(context, { + isIgnoreDeletedRule: options.isIgnoreDeletedRule + }) + if (~prefixIndex) { + this.control.repaintControl({ + curIndex: prefixIndex + }) + this.control.emitControlContentChange({ + controlValue: [] + }) + } + } + return + } + // 样式赋值元素-默认值的第一个字符样式,否则取默认样式 + const valueElement = this.getValue(context)[0] + const styleElement = valueElement + ? pickObject(valueElement, EDITOR_ELEMENT_STYLE_ATTR) + : pickObject(elementList[range.startIndex], CONTROL_STYLE_ATTR) + // 清空选项 + const prefixIndex = this.clearSelect(context, { + isAddPlaceholder: false, + isIgnoreDeletedRule: options.isIgnoreDeletedRule + }) + if (!~prefixIndex) return + // 当前无值时清空占位符 + if (!oldCode) { + this.control.removePlaceholder(prefixIndex, context) + } + // 属性赋值元素-默认为前缀属性 + const propertyElement = omitObject( + elementList[prefixIndex], + EDITOR_ELEMENT_STYLE_ATTR + ) + const start = prefixIndex + 1 + const data = splitText(text) + const draw = this.control.getDraw() + for (let i = 0; i < data.length; i++) { + const newElement: IElement = { + ...styleElement, + ...propertyElement, + type: ElementType.TEXT, + value: data[i], + controlComponent: ControlComponent.VALUE + } + formatElementContext(elementList, [newElement], prefixIndex, { + editorOptions: this.options + }) + draw.spliceElementList(elementList, start + i, 0, [newElement]) + } + // 设置状态 + this.control.setControlProperties( + { + code + }, + { + elementList, + range: { startIndex: prefixIndex, endIndex: prefixIndex } + } + ) + // 重新渲染控件 + if (!context.range) { + const newIndex = start + data.length - 1 + this.control.repaintControl({ + curIndex: newIndex + }) + this.control.emitControlContentChange({ + context + }) + if (!isMultiSelect) { + this.destroy() + } + } + } + + private _createSelectPopupDom() { + const control = this.element.control! + const valueSets = control.valueSets + if (!Array.isArray(valueSets) || !valueSets.length) return + const position = this.control.getPosition() + if (!position) return + // dom树:
  • item
+ const selectPopupContainer = document.createElement('div') + selectPopupContainer.classList.add(`${EDITOR_PREFIX}-select-control-popup`) + selectPopupContainer.setAttribute(EDITOR_COMPONENT, EditorComponent.POPUP) + const ul = document.createElement('ul') + for (let v = 0; v < valueSets.length; v++) { + const valueSet = valueSets[v] + const li = document.createElement('li') + let codes = this.getCodes() + if (codes.includes(valueSet.code)) { + li.classList.add('active') + } + li.onclick = () => { + const codeIndex = codes.findIndex(code => code === valueSet.code) + if (control.isMultiSelect) { + if (~codeIndex) { + codes.splice(codeIndex, 1) + } else { + codes.push(valueSet.code) + } + } else { + if (~codeIndex) { + codes = [] + } else { + codes = [valueSet.code] + } + } + this.setSelect(codes.join(this.VALUE_DELIMITER)) + } + li.append(document.createTextNode(valueSet.value)) + ul.append(li) + } + selectPopupContainer.append(ul) + // 定位 + const { + coordinate: { + leftTop: [left, top] + }, + lineHeight + } = position + const preY = this.control.getPreY() + selectPopupContainer.style.left = `${left}px` + selectPopupContainer.style.top = `${top + preY + lineHeight}px` + // 追加至container + const container = this.control.getContainer() + container.append(selectPopupContainer) + this.selectDom = selectPopupContainer + } + + public awake() { + if ( + this.isPopup || + this.control.getIsDisabledControl() || + !this.control.getIsRangeWithinControl() + ) { + return + } + const { startIndex } = this.control.getRange() + const elementList = this.control.getElementList() + if (elementList[startIndex + 1]?.controlId !== this.element.controlId) { + return + } + this._createSelectPopupDom() + this.isPopup = true + } + + public destroy() { + if (!this.isPopup) return + this.selectDom?.remove() + this.isPopup = false + } +} diff --git a/src/editor/core/draw/control/text/TextControl.ts b/src/editor/core/draw/control/text/TextControl.ts new file mode 100644 index 0000000..8758685 --- /dev/null +++ b/src/editor/core/draw/control/text/TextControl.ts @@ -0,0 +1,276 @@ +import { + CONTROL_STYLE_ATTR, + TEXTLIKE_ELEMENT_TYPE +} from '../../../../dataset/constant/Element' +import { ControlComponent } from '../../../../dataset/enum/Control' +import { KeyMap } from '../../../../dataset/enum/KeyMap' +import { DeepRequired } from '../../../../interface/Common' +import { + IControlContext, + IControlInstance, + IControlRuleOption +} from '../../../../interface/Control' +import { IEditorOption } from '../../../../interface/Editor' +import { IElement } from '../../../../interface/Element' +import { omitObject, pickObject } from '../../../../utils' +import { formatElementContext } from '../../../../utils/element' +import { Control } from '../Control' + +export class TextControl implements IControlInstance { + private element: IElement + private control: Control + private options: DeepRequired + + constructor(element: IElement, control: Control) { + const draw = control.getDraw() + this.options = draw.getOptions() + this.element = element + this.control = control + } + + public setElement(element: IElement) { + this.element = element + } + + public getElement(): IElement { + return this.element + } + + public getValue(context: IControlContext = {}): IElement[] { + const elementList = context.elementList || this.control.getElementList() + const { startIndex } = context.range || this.control.getRange() + const startElement = elementList[startIndex] + const data: IElement[] = [] + // 向左查找 + let preIndex = startIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if ( + preElement.controlId !== startElement.controlId || + preElement.controlComponent === ControlComponent.PREFIX || + preElement.controlComponent === ControlComponent.PRE_TEXT + ) { + break + } + if (preElement.controlComponent === ControlComponent.VALUE) { + data.unshift(preElement) + } + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if ( + nextElement.controlId !== startElement.controlId || + nextElement.controlComponent === ControlComponent.POSTFIX || + nextElement.controlComponent === ControlComponent.POST_TEXT + ) { + break + } + if (nextElement.controlComponent === ControlComponent.VALUE) { + data.push(nextElement) + } + nextIndex++ + } + return data + } + + public setValue( + data: IElement[], + context: IControlContext = {}, + options: IControlRuleOption = {} + ): number { + // 校验是否可以设置 + if ( + !options.isIgnoreDisabledRule && + this.control.getIsDisabledControl(context) + ) { + return -1 + } + const elementList = context.elementList || this.control.getElementList() + const range = context.range || this.control.getRange() + // 收缩边界到Value内 + this.control.shrinkBoundary(context) + const { startIndex, endIndex } = range + const draw = this.control.getDraw() + // 移除选区元素 + if (startIndex !== endIndex) { + draw.spliceElementList( + elementList, + startIndex + 1, + endIndex - startIndex, + [], + { + isIgnoreDeletedRule: options.isIgnoreDeletedRule + } + ) + } else { + // 移除空白占位符 + this.control.removePlaceholder(startIndex, context) + } + // 非文本类元素或前缀过渡掉样式属性 + const startElement = elementList[startIndex] + const anchorElement = + (startElement.type && + !TEXTLIKE_ELEMENT_TYPE.includes(startElement.type)) || + startElement.controlComponent === ControlComponent.PREFIX || + startElement.controlComponent === ControlComponent.PRE_TEXT + ? pickObject(startElement, [ + 'control', + 'controlId', + ...CONTROL_STYLE_ATTR + ]) + : omitObject(startElement, ['type']) + // 插入起始位置 + const start = range.startIndex + 1 + for (let i = 0; i < data.length; i++) { + const newElement: IElement = { + ...anchorElement, + ...data[i], + controlComponent: ControlComponent.VALUE + } + formatElementContext(elementList, [newElement], startIndex, { + editorOptions: this.options + }) + draw.spliceElementList(elementList, start + i, 0, [newElement]) + } + return start + data.length - 1 + } + + public clearValue( + context: IControlContext = {}, + options: IControlRuleOption = {} + ): number { + // 校验是否可以设置 + if ( + !options.isIgnoreDisabledRule && + this.control.getIsDisabledControl(context) + ) { + return -1 + } + const elementList = context.elementList || this.control.getElementList() + const range = context.range || this.control.getRange() + const { startIndex, endIndex } = range + this.control + .getDraw() + .spliceElementList( + elementList, + startIndex + 1, + endIndex - startIndex, + [], + { + isIgnoreDeletedRule: options.isIgnoreDeletedRule + } + ) + const value = this.getValue(context) + if (!value.length) { + this.control.addPlaceholder(startIndex, context) + } + return startIndex + } + + public keydown(evt: KeyboardEvent): number | null { + if (this.control.getIsDisabledControl()) { + return null + } + const elementList = this.control.getElementList() + const range = this.control.getRange() + // 收缩边界到Value内 + this.control.shrinkBoundary() + const { startIndex, endIndex } = range + const startElement = elementList[startIndex] + const endElement = elementList[endIndex] + const draw = this.control.getDraw() + // backspace + if (evt.key === KeyMap.Backspace) { + // 移除选区元素 + if (startIndex !== endIndex) { + draw.spliceElementList( + elementList, + startIndex + 1, + endIndex - startIndex + ) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex) + } + return startIndex + } else { + if ( + startElement.controlComponent === ControlComponent.PREFIX || + startElement.controlComponent === ControlComponent.PRE_TEXT || + endElement.controlComponent === ControlComponent.POSTFIX || + endElement.controlComponent === ControlComponent.POST_TEXT || + startElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + // 前缀、后缀、占位符 + return this.control.removeControl(startIndex) + } else { + // 文本 + draw.spliceElementList(elementList, startIndex, 1) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex - 1) + } + return startIndex - 1 + } + } + } else if (evt.key === KeyMap.Delete) { + // 移除选区元素 + if (startIndex !== endIndex) { + draw.spliceElementList( + elementList, + startIndex + 1, + endIndex - startIndex + ) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex) + } + return startIndex + } else { + const endNextElement = elementList[endIndex + 1] + if ( + ((startElement.controlComponent === ControlComponent.PREFIX || + startElement.controlComponent === ControlComponent.PRE_TEXT) && + endNextElement.controlComponent === ControlComponent.PLACEHOLDER) || + endNextElement.controlComponent === ControlComponent.POSTFIX || + endNextElement.controlComponent === ControlComponent.POST_TEXT || + startElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + // 前缀、后缀、占位符 + return this.control.removeControl(startIndex) + } else { + // 文本 + draw.spliceElementList(elementList, startIndex + 1, 1) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex) + } + return startIndex + } + } + } + return endIndex + } + + public cut(): number { + if (this.control.getIsDisabledControl()) { + return -1 + } + this.control.shrinkBoundary() + const { startIndex, endIndex } = this.control.getRange() + if (startIndex === endIndex) { + return startIndex + } + const draw = this.control.getDraw() + const elementList = this.control.getElementList() + draw.spliceElementList(elementList, startIndex + 1, endIndex - startIndex) + const value = this.getValue() + if (!value.length) { + this.control.addPlaceholder(startIndex) + } + return startIndex + } +} diff --git a/src/editor/core/draw/frame/Background.ts b/src/editor/core/draw/frame/Background.ts new file mode 100644 index 0000000..a1e4641 --- /dev/null +++ b/src/editor/core/draw/frame/Background.ts @@ -0,0 +1,117 @@ +import { + BackgroundRepeat, + BackgroundSize +} from '../../../dataset/enum/Background' +import { DeepRequired } from '../../../interface/Common' +import { IEditorOption } from '../../../interface/Editor' +import { Draw } from '../Draw' + +export class Background { + private draw: Draw + private options: DeepRequired + private imageCache: Map + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + this.imageCache = new Map() + } + + private _renderBackgroundColor( + ctx: CanvasRenderingContext2D, + color: string, + width: number, + height: number + ) { + ctx.save() + ctx.fillStyle = color + ctx.fillRect(0, 0, width, height) + ctx.restore() + } + + private _drawImage( + ctx: CanvasRenderingContext2D, + imageElement: HTMLImageElement, + width: number, + height: number + ) { + const { background, scale } = this.options + // contain + if (background.size === BackgroundSize.CONTAIN) { + const imageWidth = imageElement.width * scale + const imageHeight = imageElement.height * scale + if ( + !background.repeat || + background.repeat === BackgroundRepeat.NO_REPEAT + ) { + ctx.drawImage(imageElement, 0, 0, imageWidth, imageHeight) + } else { + let startX = 0 + let startY = 0 + const repeatXCount = + background.repeat === BackgroundRepeat.REPEAT || + background.repeat === BackgroundRepeat.REPEAT_X + ? Math.ceil((width * scale) / imageWidth) + : 1 + const repeatYCount = + background.repeat === BackgroundRepeat.REPEAT || + background.repeat === BackgroundRepeat.REPEAT_Y + ? Math.ceil((height * scale) / imageHeight) + : 1 + for (let x = 0; x < repeatXCount; x++) { + for (let y = 0; y < repeatYCount; y++) { + ctx.drawImage(imageElement, startX, startY, imageWidth, imageHeight) + startY += imageHeight + } + startY = 0 + startX += imageWidth + } + } + } else { + // cover + ctx.drawImage(imageElement, 0, 0, width * scale, height * scale) + } + } + + private _renderBackgroundImage( + ctx: CanvasRenderingContext2D, + width: number, + height: number + ) { + const { background } = this.options + const imageElementCache = this.imageCache.get(background.image) + if (imageElementCache) { + this._drawImage(ctx, imageElementCache, width, height) + } else { + const img = new Image() + img.setAttribute('crossOrigin', 'Anonymous') + img.src = background.image + img.onload = () => { + this.imageCache.set(background.image, img) + this._drawImage(ctx, img, width, height) + // 避免层级上浮,触发编辑器二次渲染 + this.draw.render({ + isCompute: false, + isSubmitHistory: false + }) + } + } + } + + public render(ctx: CanvasRenderingContext2D, pageNo: number) { + const { + background: { image, color, applyPageNumbers } + } = this.options + if ( + image && + (!applyPageNumbers?.length || applyPageNumbers.includes(pageNo)) + ) { + const { width, height } = this.options + this._renderBackgroundImage(ctx, width, height) + } else { + const width = this.draw.getCanvasWidth(pageNo) + const height = this.draw.getCanvasHeight(pageNo) + this._renderBackgroundColor(ctx, color, width, height) + } + } +} diff --git a/src/editor/core/draw/frame/Badge.ts b/src/editor/core/draw/frame/Badge.ts new file mode 100644 index 0000000..24dac8f --- /dev/null +++ b/src/editor/core/draw/frame/Badge.ts @@ -0,0 +1,88 @@ +import { IAreaBadge, IBadge } from '../../../interface/Badge' +import { DeepRequired } from '../../../interface/Common' +import { IEditorOption } from '../../../interface/Editor' +import { Draw } from '../Draw' + +export class Badge { + private draw: Draw + private options: DeepRequired + private imageCache: Map + private mainBadge: IBadge | null + private areaBadgeMap: Map + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + this.imageCache = new Map() + this.mainBadge = null + this.areaBadgeMap = new Map() + } + + public setMainBadge(payload: IBadge | null) { + this.mainBadge = payload + } + + public setAreaBadgeMap(payload: IAreaBadge[]) { + this.areaBadgeMap.clear() + payload.forEach(areaBadge => { + this.areaBadgeMap.set(areaBadge.areaId, areaBadge.badge) + }) + } + + private _drawImage( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + width: number, + height: number, + value: string + ) { + if (this.imageCache.has(value)) { + const img = this.imageCache.get(value)! + ctx.drawImage(img, x, y, width, height) + } else { + const img = new Image() + img.setAttribute('crossOrigin', 'Anonymous') + img.src = value + img.onload = () => { + this.imageCache.set(value, img) + ctx.drawImage(img, x, y, width, height) + } + } + } + + public render(ctx: CanvasRenderingContext2D, pageNo: number) { + // 文档签章 + if (pageNo === 0 && this.mainBadge) { + const { scale, badge } = this.options + const { left, top, width, height, value } = this.mainBadge + // 默认从页眉下开始 + const headerTop = + this.draw.getMargins()[0] + this.draw.getHeader().getExtraHeight() + const x = (left || badge.left) * scale + const y = (top || badge.top) * scale + headerTop + this._drawImage(ctx, x, y, width * scale, height * scale, value) + } + // 区域签章 + if (this.areaBadgeMap.size) { + const areaInfo = this.draw.getArea().getAreaInfo() + if (areaInfo.size) { + const { scale, badge } = this.options + for (const areaItem of areaInfo) { + // 忽略非本页区域 + const { positionList } = areaItem[1] + const firstPosition = positionList[0] + if (firstPosition.pageNo !== pageNo) continue + // 忽略未设置签章区域 + const badgeItem = this.areaBadgeMap.get(areaItem[0]) + if (!badgeItem) continue + const { left, top, width, height, value } = badgeItem + const x = (left || badge.left) * scale + const y = + (top || badge.top) * scale + firstPosition.coordinate.leftTop[1] + this._drawImage(ctx, x, y, width * scale, height * scale, value) + } + } + } + } +} diff --git a/src/editor/core/draw/frame/Footer.ts b/src/editor/core/draw/frame/Footer.ts new file mode 100644 index 0000000..62ba285 --- /dev/null +++ b/src/editor/core/draw/frame/Footer.ts @@ -0,0 +1,154 @@ +import { maxHeightRadioMapping } from '../../../dataset/constant/Common' +import { EditorZone } from '../../../dataset/enum/Editor' +import { DeepRequired } from '../../../interface/Common' +import { IEditorOption } from '../../../interface/Editor' +import { IElement, IElementPosition } from '../../../interface/Element' +import { IRow } from '../../../interface/Row' +import { Position } from '../../position/Position' +import { Zone } from '../../zone/Zone' +import { Draw } from '../Draw' + +export class Footer { + private draw: Draw + private position: Position + private zone: Zone + private options: DeepRequired + + private elementList: IElement[] + private rowList: IRow[] + private positionList: IElementPosition[] + + constructor(draw: Draw, data?: IElement[]) { + this.draw = draw + this.position = draw.getPosition() + this.zone = draw.getZone() + this.options = draw.getOptions() + + this.elementList = data || [] + this.rowList = [] + this.positionList = [] + } + + public getRowList(): IRow[] { + return this.rowList + } + + public setElementList(elementList: IElement[]) { + this.elementList = elementList + } + + public getElementList(): IElement[] { + return this.elementList + } + + public getPositionList(): IElementPosition[] { + return this.positionList + } + + public compute() { + this.recovery() + this._computeRowList() + this._computePositionList() + } + + public recovery() { + this.rowList = [] + this.positionList = [] + } + + private _computeRowList() { + const innerWidth = this.draw.getInnerWidth() + this.rowList = this.draw.computeRowList({ + innerWidth, + elementList: this.elementList + }) + } + + private _computePositionList() { + const footerBottom = this.getFooterBottom() + const innerWidth = this.draw.getInnerWidth() + const margins = this.draw.getMargins() + const startX = margins[3] + // 页面高度 - 页脚顶部距离页面底部高度 + const pageHeight = this.draw.getHeight() + const footerHeight = this.getHeight() + const startY = pageHeight - footerBottom - footerHeight + this.position.computePageRowPosition({ + positionList: this.positionList, + rowList: this.rowList, + pageNo: 0, + startRowIndex: 0, + startIndex: 0, + startX, + startY, + innerWidth, + zone: EditorZone.FOOTER + }) + } + + public getFooterBottom(): number { + const { + footer: { bottom, disabled }, + scale + } = this.options + if (disabled) return 0 + return Math.floor(bottom * scale) + } + + public getMaxHeight(): number { + const { + footer: { maxHeightRadio } + } = this.options + const height = this.draw.getHeight() + return Math.floor(height * maxHeightRadioMapping[maxHeightRadio]) + } + + public getHeight(): number { + const maxHeight = this.getMaxHeight() + const rowHeight = this.getRowHeight() + return rowHeight > maxHeight ? maxHeight : rowHeight + } + + public getRowHeight(): number { + return this.rowList.reduce((pre, cur) => pre + cur.height, 0) + } + + public getExtraHeight(): number { + // 页脚下边距 + 实际高 - 页面上边距 + const margins = this.draw.getMargins() + const footerHeight = this.getHeight() + const footerBottom = this.getFooterBottom() + const extraHeight = footerBottom + footerHeight - margins[2] + return extraHeight <= 0 ? 0 : extraHeight + } + + public render(ctx: CanvasRenderingContext2D, pageNo: number) { + ctx.save() + ctx.globalAlpha = this.zone.isFooterActive() + ? 1 + : this.options.footer.inactiveAlpha + const innerWidth = this.draw.getInnerWidth() + const maxHeight = this.getMaxHeight() + // 超出最大高度不渲染 + const rowList: IRow[] = [] + let curRowHeight = 0 + for (let r = 0; r < this.rowList.length; r++) { + const row = this.rowList[r] + if (curRowHeight + row.height > maxHeight) { + break + } + rowList.push(row) + curRowHeight += row.height + } + this.draw.drawRow(ctx, { + elementList: this.elementList, + positionList: this.positionList, + rowList, + pageNo, + startIndex: 0, + innerWidth, + zone: EditorZone.FOOTER + }) + ctx.restore() + } +} diff --git a/src/editor/core/draw/frame/Header.ts b/src/editor/core/draw/frame/Header.ts new file mode 100644 index 0000000..8f86632 --- /dev/null +++ b/src/editor/core/draw/frame/Header.ts @@ -0,0 +1,157 @@ +import { maxHeightRadioMapping } from '../../../dataset/constant/Common' +import { EditorZone } from '../../../dataset/enum/Editor' +import { DeepRequired } from '../../../interface/Common' +import { IEditorOption } from '../../../interface/Editor' +import { IElement, IElementPosition } from '../../../interface/Element' +import { IRow } from '../../../interface/Row' +import { pickSurroundElementList } from '../../../utils/element' +import { Position } from '../../position/Position' +import { Zone } from '../../zone/Zone' +import { Draw } from '../Draw' + +export class Header { + private draw: Draw + private position: Position + private zone: Zone + private options: DeepRequired + + private elementList: IElement[] + private rowList: IRow[] + private positionList: IElementPosition[] + + constructor(draw: Draw, data?: IElement[]) { + this.draw = draw + this.position = draw.getPosition() + this.zone = draw.getZone() + this.options = draw.getOptions() + + this.elementList = data || [] + this.rowList = [] + this.positionList = [] + } + + public getRowList(): IRow[] { + return this.rowList + } + + public setElementList(elementList: IElement[]) { + this.elementList = elementList + } + + public getElementList(): IElement[] { + return this.elementList + } + + public getPositionList(): IElementPosition[] { + return this.positionList + } + + public compute() { + this.recovery() + this._computeRowList() + this._computePositionList() + } + + public recovery() { + this.rowList = [] + this.positionList = [] + } + + private _computeRowList() { + const innerWidth = this.draw.getInnerWidth() + const margins = this.draw.getMargins() + const surroundElementList = pickSurroundElementList(this.elementList) + this.rowList = this.draw.computeRowList({ + startX: margins[3], + startY: this.getHeaderTop(), + innerWidth, + elementList: this.elementList, + surroundElementList + }) + } + + private _computePositionList() { + const headerTop = this.getHeaderTop() + const innerWidth = this.draw.getInnerWidth() + const margins = this.draw.getMargins() + const startX = margins[3] + const startY = headerTop + this.position.computePageRowPosition({ + positionList: this.positionList, + rowList: this.rowList, + pageNo: 0, + startRowIndex: 0, + startIndex: 0, + startX, + startY, + innerWidth, + zone: EditorZone.HEADER + }) + } + + public getHeaderTop(): number { + const { + header: { top, disabled }, + scale + } = this.options + if (disabled) return 0 + return Math.floor(top * scale) + } + + public getMaxHeight(): number { + const { + header: { maxHeightRadio } + } = this.options + const height = this.draw.getHeight() + return Math.floor(height * maxHeightRadioMapping[maxHeightRadio]) + } + + public getHeight(): number { + const maxHeight = this.getMaxHeight() + const rowHeight = this.getRowHeight() + return rowHeight > maxHeight ? maxHeight : rowHeight + } + + public getRowHeight(): number { + return this.rowList.reduce((pre, cur) => pre + cur.height, 0) + } + + public getExtraHeight(): number { + // 页眉上边距 + 实际高 - 页面上边距 + const margins = this.draw.getMargins() + const headerHeight = this.getHeight() + const headerTop = this.getHeaderTop() + const extraHeight = headerTop + headerHeight - margins[0] + return extraHeight <= 0 ? 0 : extraHeight + } + + public render(ctx: CanvasRenderingContext2D, pageNo: number) { + ctx.save() + ctx.globalAlpha = this.zone.isHeaderActive() + ? 1 + : this.options.header.inactiveAlpha + const innerWidth = this.draw.getInnerWidth() + const maxHeight = this.getMaxHeight() + // 超出最大高度不渲染 + const rowList: IRow[] = [] + let curRowHeight = 0 + for (let r = 0; r < this.rowList.length; r++) { + const row = this.rowList[r] + if (curRowHeight + row.height > maxHeight) { + break + } + rowList.push(row) + curRowHeight += row.height + } + this.draw.drawRow(ctx, { + elementList: this.elementList, + positionList: this.positionList, + rowList, + pageNo, + startIndex: 0, + innerWidth, + zone: EditorZone.HEADER + }) + ctx.restore() + } +} diff --git a/src/editor/core/draw/frame/LineNumber.ts b/src/editor/core/draw/frame/LineNumber.ts new file mode 100644 index 0000000..305f875 --- /dev/null +++ b/src/editor/core/draw/frame/LineNumber.ts @@ -0,0 +1,43 @@ +import { LineNumberType } from '../../../dataset/enum/LineNumber' +import { DeepRequired } from '../../../interface/Common' +import { IEditorOption } from '../../../interface/Editor' +import { Draw } from '../Draw' + +export class LineNumber { + private draw: Draw + private options: DeepRequired + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + } + + public render(ctx: CanvasRenderingContext2D, pageNo: number) { + const { + scale, + lineNumber: { color, size, font, right, type } + } = this.options + const textParticle = this.draw.getTextParticle() + const margins = this.draw.getMargins() + const positionList = this.draw.getPosition().getOriginalMainPositionList() + const pageRowList = this.draw.getPageRowList() + const rowList = pageRowList[pageNo] + ctx.save() + ctx.fillStyle = color + ctx.font = `${size * scale}px ${font}` + for (let i = 0; i < rowList.length; i++) { + const row = rowList[i] + const { + coordinate: { leftBottom } + } = positionList[row.startIndex] + const seq = type === LineNumberType.PAGE ? i + 1 : row.rowIndex + 1 + const textMetrics = textParticle.measureText(ctx, { + value: `${seq}` + }) + const x = margins[3] - (textMetrics.width + right) * scale + const y = leftBottom[1] - textMetrics.actualBoundingBoxAscent * scale + ctx.fillText(`${seq}`, x, y) + } + ctx.restore() + } +} diff --git a/src/editor/core/draw/frame/Margin.ts b/src/editor/core/draw/frame/Margin.ts new file mode 100644 index 0000000..124bb82 --- /dev/null +++ b/src/editor/core/draw/frame/Margin.ts @@ -0,0 +1,53 @@ +import { PageMode } from '../../../dataset/enum/Editor' +import { IEditorOption } from '../../../interface/Editor' +import { Draw } from '../Draw' + +export class Margin { + private draw: Draw + private options: Required + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + } + + public render(ctx: CanvasRenderingContext2D, pageNo: number) { + const { marginIndicatorColor, pageMode } = this.options + const width = this.draw.getWidth() + const height = + pageMode === PageMode.CONTINUITY + ? this.draw.getCanvasHeight(pageNo) / this.draw.getPagePixelRatio() + : this.draw.getHeight() + const margins = this.draw.getMargins() + const marginIndicatorSize = this.draw.getMarginIndicatorSize() + ctx.save() + ctx.translate(0.5, 0.5) + ctx.strokeStyle = marginIndicatorColor + ctx.beginPath() + const leftTopPoint: [number, number] = [margins[3], margins[0]] + const rightTopPoint: [number, number] = [width - margins[1], margins[0]] + const leftBottomPoint: [number, number] = [margins[3], height - margins[2]] + const rightBottomPoint: [number, number] = [ + width - margins[1], + height - margins[2] + ] + // 上左 + ctx.moveTo(leftTopPoint[0] - marginIndicatorSize, leftTopPoint[1]) + ctx.lineTo(...leftTopPoint) + ctx.lineTo(leftTopPoint[0], leftTopPoint[1] - marginIndicatorSize) + // 上右 + ctx.moveTo(rightTopPoint[0] + marginIndicatorSize, rightTopPoint[1]) + ctx.lineTo(...rightTopPoint) + ctx.lineTo(rightTopPoint[0], rightTopPoint[1] - marginIndicatorSize) + // 下左 + ctx.moveTo(leftBottomPoint[0] - marginIndicatorSize, leftBottomPoint[1]) + ctx.lineTo(...leftBottomPoint) + ctx.lineTo(leftBottomPoint[0], leftBottomPoint[1] + marginIndicatorSize) + // 下右 + ctx.moveTo(rightBottomPoint[0] + marginIndicatorSize, rightBottomPoint[1]) + ctx.lineTo(...rightBottomPoint) + ctx.lineTo(rightBottomPoint[0], rightBottomPoint[1] + marginIndicatorSize) + ctx.stroke() + ctx.restore() + } +} diff --git a/src/editor/core/draw/frame/PageBorder.ts b/src/editor/core/draw/frame/PageBorder.ts new file mode 100644 index 0000000..3a6da9d --- /dev/null +++ b/src/editor/core/draw/frame/PageBorder.ts @@ -0,0 +1,47 @@ +import { DeepRequired } from '../../../interface/Common' +import { IEditorOption } from '../../../interface/Editor' +import { Draw } from '../Draw' +import { Footer } from './Footer' +import { Header } from './Header' + +export class PageBorder { + private draw: Draw + private header: Header + private footer: Footer + private options: DeepRequired + + constructor(draw: Draw) { + this.draw = draw + this.header = draw.getHeader() + this.footer = draw.getFooter() + this.options = draw.getOptions() + } + + public render(ctx: CanvasRenderingContext2D) { + const { + scale, + pageBorder: { color, lineWidth, padding } + } = this.options + ctx.save() + ctx.translate(0.5, 0.5) + ctx.strokeStyle = color + ctx.lineWidth = lineWidth * scale + const margins = this.draw.getMargins() + // x:左边距 - 左距离正文距离 + const x = margins[3] - padding[3] * scale + // y:页眉上边距 + 页眉高度 - 上距离正文距离 + const y = margins[0] + this.header.getExtraHeight() - padding[0] * scale + // width:页面宽度 + 左右距离正文距离 + const width = this.draw.getInnerWidth() + (padding[1] + padding[3]) * scale + // height:页面高度 - 正文起始位置 - 页脚高度 - 下边距 - 下距离正文距离 + const height = + this.draw.getHeight() - + y - + this.footer.getExtraHeight() - + margins[2] + + padding[2] * scale + ctx.rect(x, y, width, height) + ctx.stroke() + ctx.restore() + } +} diff --git a/src/editor/core/draw/frame/PageNumber.ts b/src/editor/core/draw/frame/PageNumber.ts new file mode 100644 index 0000000..a0ac7db --- /dev/null +++ b/src/editor/core/draw/frame/PageNumber.ts @@ -0,0 +1,88 @@ +import { FORMAT_PLACEHOLDER } from '../../../dataset/constant/PageNumber' +import { NumberType } from '../../../dataset/enum/Common' +import { RowFlex } from '../../../dataset/enum/Row' +import { DeepRequired } from '../../../interface/Common' +import { IEditorOption } from '../../../interface/Editor' +import { convertNumberToChinese } from '../../../utils' +import { Draw } from '../Draw' + +export class PageNumber { + private draw: Draw + private options: DeepRequired + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + } + + static formatNumberPlaceholder( + text: string, + pageNo: number, + replaceReg: RegExp, + numberType: NumberType + ) { + const pageNoText = + numberType === NumberType.CHINESE + ? convertNumberToChinese(pageNo) + : `${pageNo}` + return text.replace(replaceReg, pageNoText) + } + + public render(ctx: CanvasRenderingContext2D, pageNo: number) { + const { + scale, + pageNumber: { + size, + font, + color, + rowFlex, + numberType, + format, + startPageNo, + fromPageNo + } + } = this.options + if (pageNo < fromPageNo) return + // 处理页码格式 + let text = format + const pageNoReg = new RegExp(FORMAT_PLACEHOLDER.PAGE_NO) + if (pageNoReg.test(text)) { + text = PageNumber.formatNumberPlaceholder( + text, + pageNo + startPageNo - fromPageNo, + pageNoReg, + numberType + ) + } + const pageCountReg = new RegExp(FORMAT_PLACEHOLDER.PAGE_COUNT) + if (pageCountReg.test(text)) { + text = PageNumber.formatNumberPlaceholder( + text, + this.draw.getPageCount() - fromPageNo, + pageCountReg, + numberType + ) + } + const width = this.draw.getWidth() + // 计算y位置 + const height = this.draw.getHeight() + const pageNumberBottom = this.draw.getPageNumberBottom() + const y = height - pageNumberBottom + ctx.save() + ctx.fillStyle = color + ctx.font = `${size * scale}px ${font}` + // 计算x位置-居左、居中、居右 + let x = 0 + const margins = this.draw.getMargins() + const { width: textWidth } = ctx.measureText(text) + if (rowFlex === RowFlex.CENTER) { + x = (width - textWidth) / 2 + } else if (rowFlex === RowFlex.RIGHT) { + x = width - textWidth - margins[1] + } else { + x = margins[3] + } + ctx.fillText(text, x, y) + ctx.restore() + } +} diff --git a/src/editor/core/draw/frame/Placeholder.ts b/src/editor/core/draw/frame/Placeholder.ts new file mode 100644 index 0000000..ea1dc7e --- /dev/null +++ b/src/editor/core/draw/frame/Placeholder.ts @@ -0,0 +1,114 @@ +import { IEditorOption, IElement } from '../../..' +import { DeepRequired } from '../../../interface/Common' +import { IElementPosition } from '../../../interface/Element' +import { IPlaceholder } from '../../../interface/Placeholder' +import { IRow } from '../../../interface/Row' +import { formatElementList } from '../../../utils/element' +import { Position } from '../../position/Position' +import { Draw } from '../Draw' +import { LineBreakParticle } from '../particle/LineBreakParticle' + +export interface IPlaceholderRenderOption { + placeholder: Required + startY?: number +} + +export class Placeholder { + private draw: Draw + private position: Position + private options: DeepRequired + + private elementList: IElement[] + private rowList: IRow[] + private positionList: IElementPosition[] + + constructor(draw: Draw) { + this.draw = draw + this.position = draw.getPosition() + this.options = >draw.getOptions() + + this.elementList = [] + this.rowList = [] + this.positionList = [] + } + + private _recovery() { + this.elementList = [] + this.rowList = [] + this.positionList = [] + } + + public _compute(options?: IPlaceholderRenderOption) { + this._computeRowList() + this._computePositionList(options) + } + + private _computeRowList() { + const innerWidth = this.draw.getInnerWidth() + this.rowList = this.draw.computeRowList({ + innerWidth, + elementList: this.elementList + }) + } + + private _computePositionList(options?: IPlaceholderRenderOption) { + const { lineBreak, scale } = this.options + const headerExtraHeight = this.draw.getHeader().getExtraHeight() + const innerWidth = this.draw.getInnerWidth() + const margins = this.draw.getMargins() + let startX = margins[3] + // 换行符绘制开启时,移动起始位置 + if (!lineBreak.disabled) { + startX += (LineBreakParticle.WIDTH + LineBreakParticle.GAP) * scale + } + const startY = options?.startY || margins[0] + headerExtraHeight + this.position.computePageRowPosition({ + positionList: this.positionList, + rowList: this.rowList, + pageNo: 0, + startRowIndex: 0, + startIndex: 0, + startX, + startY, + innerWidth + }) + } + + public render( + ctx: CanvasRenderingContext2D, + options?: IPlaceholderRenderOption + ) { + const { placeholder = this.options.placeholder } = options || {} + const { data, font, size, color, opacity } = placeholder + this._recovery() + // 构建元素列表并格式化 + this.elementList = [ + { + value: data, + font, + size, + color + } + ] + formatElementList(this.elementList, { + editorOptions: this.options, + isForceCompensation: true + }) + // 计算 + this._compute(options) + const innerWidth = this.draw.getInnerWidth() + // 绘制 + ctx.save() + ctx.globalAlpha = opacity + this.draw.drawRow(ctx, { + elementList: this.elementList, + positionList: this.positionList, + rowList: this.rowList, + pageNo: 0, + startIndex: 0, + innerWidth, + isDrawLineBreak: false + }) + ctx.restore() + } +} diff --git a/src/editor/core/draw/frame/Watermark.ts b/src/editor/core/draw/frame/Watermark.ts new file mode 100644 index 0000000..111ae51 --- /dev/null +++ b/src/editor/core/draw/frame/Watermark.ts @@ -0,0 +1,188 @@ +import { IEditorOption } from '../../..' +import { FORMAT_PLACEHOLDER } from '../../../dataset/constant/PageNumber' +import { WatermarkType } from '../../../dataset/enum/Watermark' +import { DeepRequired } from '../../../interface/Common' +import { Draw } from '../Draw' +import { PageNumber } from './PageNumber' + +export class Watermark { + private draw: Draw + private options: DeepRequired + private imageCache: Map + + constructor(draw: Draw) { + this.draw = draw + this.options = >draw.getOptions() + this.imageCache = new Map() + } + + public renderText(ctx: CanvasRenderingContext2D, pageNo: number) { + const { + watermark: { data, opacity, font, size, color, repeat, gap, numberType }, + scale + } = this.options + const width = this.draw.getWidth() + const height = this.draw.getHeight() + // 开始绘制 + ctx.save() + ctx.globalAlpha = opacity + ctx.font = `${size * scale}px ${font}` + // 格式化文本 + let text = data + const pageNoReg = new RegExp(FORMAT_PLACEHOLDER.PAGE_NO) + if (pageNoReg.test(text)) { + text = PageNumber.formatNumberPlaceholder( + text, + pageNo + 1, + pageNoReg, + numberType + ) + } + const pageCountReg = new RegExp(FORMAT_PLACEHOLDER.PAGE_COUNT) + if (pageCountReg.test(text)) { + text = PageNumber.formatNumberPlaceholder( + text, + this.draw.getPageCount(), + pageCountReg, + numberType + ) + } + // 测量长度并绘制 + const measureText = ctx.measureText(text) + if (repeat) { + const dpr = this.draw.getPagePixelRatio() + const temporaryCanvas = document.createElement('canvas') + const temporaryCtx = temporaryCanvas.getContext('2d')! + // 勾股定理计算旋转后的宽高对角线尺寸 a^2 + b^2 = c^2 + const textWidth = measureText.width + const textHeight = + measureText.actualBoundingBoxAscent + + measureText.actualBoundingBoxDescent + const diagonalLength = Math.sqrt( + Math.pow(textWidth, 2) + Math.pow(textHeight, 2) + ) + // 加上 gap 间距 + const patternWidth = diagonalLength + 2 * gap[0] * scale + const patternHeight = diagonalLength + 2 * gap[1] * scale + // 宽高设置 + temporaryCanvas.width = patternWidth + temporaryCanvas.height = patternHeight + temporaryCanvas.style.width = `${patternWidth * dpr}px` + temporaryCanvas.style.height = `${patternHeight * dpr}px` + // 旋转45度 + temporaryCtx.translate(patternWidth / 2, patternHeight / 2) + temporaryCtx.rotate((-45 * Math.PI) / 180) + temporaryCtx.translate(-patternWidth / 2, -patternHeight / 2) + // 绘制文本 + temporaryCtx.font = `${size * scale}px ${font}` + temporaryCtx.fillStyle = color + temporaryCtx.fillText( + text, + (patternWidth - textWidth) / 2, + (patternHeight - textHeight) / 2 + measureText.actualBoundingBoxAscent + ) + // 创建平铺模式 + const pattern = ctx.createPattern(temporaryCanvas, 'repeat') + if (pattern) { + ctx.fillStyle = pattern + ctx.fillRect(0, 0, width, height) + } + } else { + const x = width / 2 + const y = height / 2 + ctx.fillStyle = color + ctx.translate(x, y) + ctx.rotate((-45 * Math.PI) / 180) + ctx.fillText( + text, + -measureText.width / 2, + measureText.actualBoundingBoxAscent - (size * scale) / 2 + ) + } + ctx.restore() + } + + public renderImage(ctx: CanvasRenderingContext2D) { + const { + watermark: { width, height, data, opacity, repeat, gap }, + scale + } = this.options + if (!this.imageCache.has(data)) { + const img = new Image() + img.setAttribute('crossOrigin', 'Anonymous') + img.src = data + img.onload = () => { + this.imageCache.set(data, img) + // 避免层级上浮,触发编辑器二次渲染 + this.draw.render({ + isCompute: false, + isSubmitHistory: false + }) + } + return + } + const docWidth = this.draw.getWidth() + const docHeight = this.draw.getHeight() + const imageWidth = width * scale + const imageHeight = height * scale + // 开始绘制 + ctx.save() + ctx.globalAlpha = opacity + if (repeat) { + const dpr = this.draw.getPagePixelRatio() + const temporaryCanvas = document.createElement('canvas') + const temporaryCtx = temporaryCanvas.getContext('2d')! + // 勾股定理计算旋转后的宽高对角线尺寸 a^2 + b^2 = c^2 + const diagonalLength = Math.sqrt( + Math.pow(imageWidth, 2) + Math.pow(imageHeight, 2) + ) + // 加上 gap 间距 + const patternWidth = diagonalLength + 2 * gap[0] * scale + const patternHeight = diagonalLength + 2 * gap[1] * scale + // 宽高设置 + temporaryCanvas.width = patternWidth + temporaryCanvas.height = patternHeight + temporaryCanvas.style.width = `${patternWidth * dpr}px` + temporaryCanvas.style.height = `${patternHeight * dpr}px` + // 旋转45度 + temporaryCtx.translate(patternWidth / 2, patternHeight / 2) + temporaryCtx.rotate((-45 * Math.PI) / 180) + temporaryCtx.translate(-patternWidth / 2, -patternHeight / 2) + // 绘制图片 + temporaryCtx.drawImage( + this.imageCache.get(data)!, + (patternWidth - imageWidth) / 2, + (patternHeight - imageHeight) / 2, + imageWidth, + imageHeight + ) + // 创建平铺模式 + const pattern = ctx.createPattern(temporaryCanvas, 'repeat') + if (pattern) { + ctx.fillStyle = pattern + ctx.fillRect(0, 0, docWidth, docHeight) + } + } else { + const x = docWidth / 2 + const y = docHeight / 2 + ctx.translate(x, y) + ctx.rotate((-45 * Math.PI) / 180) + ctx.drawImage( + this.imageCache.get(data)!, + -imageWidth / 2, + -imageHeight / 2, + imageWidth, + imageHeight + ) + } + ctx.restore() + } + + public render(ctx: CanvasRenderingContext2D, pageNo: number) { + if (this.options.watermark.type === WatermarkType.IMAGE) { + this.renderImage(ctx) + } else { + this.renderText(ctx, pageNo) + } + } +} diff --git a/src/editor/core/draw/interactive/Area.ts b/src/editor/core/draw/interactive/Area.ts new file mode 100644 index 0000000..9ddd358 --- /dev/null +++ b/src/editor/core/draw/interactive/Area.ts @@ -0,0 +1,309 @@ +import { Draw } from '../Draw' +import { deepClone, getUUID, isNonValue } from '../../../utils' +import { ElementType } from '../../../dataset/enum/Element' +import { + IArea, + IAreaInfo, + IGetAreaValueOption, + IGetAreaValueResult, + IInsertAreaOption, + ILocationAreaOption, + ISetAreaPropertiesOption, + ISetAreaValueOption +} from '../../../interface/Area' +import { EditorZone } from '../../../dataset/enum/Editor' +import { LocationPosition } from '../../../dataset/enum/Common' +import { RangeManager } from '../../range/RangeManager' +import { Zone } from '../../zone/Zone' +import { Position } from '../../position/Position' +import { formatElementList, zipElementList } from '../../../utils/element' +import { AreaMode } from '../../../dataset/enum/Area' +import { IRange } from '../../../interface/Range' +import { IElementPosition } from '../../../interface/Element' +import { Placeholder } from '../frame/Placeholder' +import { defaultPlaceholderOption } from '../../../dataset/constant/Placeholder' +import { DeepRequired } from '../../../interface/Common' +import { IEditorOption } from '../../../interface/Editor' + +export class Area { + private draw: Draw + private zone: Zone + private range: RangeManager + private position: Position + private options: DeepRequired + private areaInfoMap = new Map() + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + this.zone = draw.getZone() + this.range = draw.getRange() + this.position = draw.getPosition() + } + + public getAreaInfo(): Map { + return this.areaInfoMap + } + + public getActiveAreaId(): string | null { + if (!this.areaInfoMap.size) return null + const { startIndex } = this.range.getRange() + const elementList = this.draw.getElementList() + const element = elementList[startIndex] + return element?.areaId || null + } + + public getActiveAreaInfo(): IAreaInfo | null { + const activeAreaId = this.getActiveAreaId() + if (!activeAreaId) return null + return this.areaInfoMap.get(activeAreaId) || null + } + + public isReadonly() { + const activeAreaInfo = this.getActiveAreaInfo() + if (!activeAreaInfo?.area) return false + switch (activeAreaInfo.area.mode) { + case AreaMode.EDIT: + return false + case AreaMode.READONLY: + return true + case AreaMode.FORM: + return !this.draw.getControl().getIsRangeWithinControl() + default: + return false + } + } + + public insertArea(payload: IInsertAreaOption): string | null { + const { id, value, area, position, range } = payload + // 切换至正文 + if (this.zone.getZone() !== EditorZone.MAIN) { + this.zone.setZone(EditorZone.MAIN) + } + // 跳出表格 + this.draw.getPosition().setPositionContext({ + isTable: false + }) + // 通过光标插入area && 不能在area内再次插入area + if (range && !this.getActiveAreaId()) { + const { startIndex, endIndex } = range + // 校验位置合法性 + const elementList = this.draw.getOriginalMainElementList() + if (!elementList[startIndex] || !elementList[endIndex]) { + return null + } + this.range.setRange(range.startIndex, range.endIndex) + } else { + // 设置插入位置 + if (position === LocationPosition.BEFORE) { + this.range.setRange(0, 0) + } else { + const elementList = this.draw.getOriginalMainElementList() + const lastIndex = elementList.length - 1 + this.range.setRange(lastIndex, lastIndex) + } + } + const areaId = id || getUUID() + this.draw.insertElementList([ + { + type: ElementType.AREA, + value: '', + areaId, + valueList: value, + area: deepClone(area) + } + ]) + return areaId + } + + public render(ctx: CanvasRenderingContext2D, pageNo: number) { + if (!this.areaInfoMap.size) return + ctx.save() + const margins = this.draw.getMargins() + const width = this.draw.getInnerWidth() + for (const areaInfoItem of this.areaInfoMap) { + const { area, positionList } = areaInfoItem[1] + if ( + area?.hide || + (!area?.backgroundColor && !area?.borderColor && !area?.placeholder) + ) { + continue + } + const pagePositionList = positionList.filter(p => p.pageNo === pageNo) + if (!pagePositionList.length) continue + ctx.translate(0.5, 0.5) + const firstPosition = pagePositionList[0] + const lastPosition = pagePositionList[pagePositionList.length - 1] + // 起始位置 + const x = margins[3] + const y = Math.ceil(firstPosition.coordinate.leftTop[1]) + const height = Math.ceil(lastPosition.coordinate.rightBottom[1] - y) + // 背景色 + if (area.backgroundColor) { + ctx.fillStyle = area.backgroundColor + ctx.fillRect(x, y, width, height) + } + // 边框 + if (area.borderColor) { + ctx.strokeStyle = area.borderColor + ctx.strokeRect(x, y, width, height) + } + // 提示词 + if (area.placeholder && positionList.length <= 1) { + const placeholder = new Placeholder(this.draw) + placeholder.render(ctx, { + placeholder: { + ...defaultPlaceholderOption, + ...area.placeholder + }, + startY: firstPosition.coordinate.leftTop[1] + }) + } + ctx.translate(-0.5, -0.5) + } + ctx.restore() + } + + public compute() { + this.areaInfoMap.clear() + const elementList = this.draw.getOriginalMainElementList() + const positionList = this.position.getOriginalMainPositionList() + for (let e = 0; e < elementList.length; e++) { + const element = elementList[e] + const areaId = element.areaId + if (areaId) { + const areaInfo = this.areaInfoMap.get(areaId) + if (!areaInfo) { + this.areaInfoMap.set(areaId, { + id: areaId, + area: element.area!, + elementList: [element], + positionList: [positionList[e]] + }) + } else { + areaInfo.elementList.push(element) + areaInfo.positionList.push(positionList[e]) + } + } + } + } + + public getAreaValue( + options: IGetAreaValueOption = {} + ): IGetAreaValueResult | null { + const areaId = options.id || this.getActiveAreaId() + if (!areaId) return null + const areaInfo = this.areaInfoMap.get(areaId) + if (!areaInfo) return null + return { + area: areaInfo.area, + id: areaInfo.id, + startPageNo: areaInfo.positionList[0].pageNo, + endPageNo: areaInfo.positionList[areaInfo.positionList.length - 1].pageNo, + value: zipElementList(areaInfo.elementList) + } + } + + public getContextByAreaId( + areaId: string, + options?: ILocationAreaOption + ): { range: IRange; elementPosition: IElementPosition } | null { + const elementList = this.draw.getOriginalMainElementList() + for (let e = 0; e < elementList.length; e++) { + const element = elementList[e] + if (options?.position === LocationPosition.OUTER_BEFORE) { + // 区域外面最前 + if (elementList[e + 1]?.areaId !== areaId) continue + } else if (options?.position === LocationPosition.AFTER) { + // 区域内部最后 + if ( + !(element.areaId === areaId && elementList[e + 1]?.areaId !== areaId) + ) { + continue + } + } else if (options?.position === LocationPosition.OUTER_AFTER) { + // 区域外部最后 + if ( + !(element.areaId !== areaId && elementList[e - 1]?.areaId === areaId) + ) { + continue + } + } else { + // 区域内部最前 + if (element.areaId !== areaId) continue + } + const positionList = this.position.getOriginalMainPositionList() + return { + range: { + startIndex: e, + endIndex: e + }, + elementPosition: positionList[e] + } + } + return null + } + + public setAreaProperties(payload: ISetAreaPropertiesOption) { + const areaId = payload.id || this.getActiveAreaId() + if (!areaId) return + const areaInfo = this.areaInfoMap.get(areaId) + if (!areaInfo) return + if (!areaInfo.area) { + areaInfo.area = {} + } + // 需要计算的属性 + let isCompute = false + const computeProps: Array = ['top', 'hide'] + // 循环设置 + Object.entries(payload.properties).forEach(([key, value]) => { + if (isNonValue(value)) return + const propKey = key as keyof IArea + areaInfo.area[propKey] = value + if (computeProps.includes(propKey)) { + isCompute = true + } + }) + this.draw.render({ + isCompute, + isSetCursor: false + }) + } + + public setAreaValue(payload: ISetAreaValueOption) { + const areaId = payload.id || this.getActiveAreaId() + if (!areaId) return + const areaInfo = this.areaInfoMap.get(areaId) + if (!areaInfo) return + // 删除旧数据并替换新的格式化数据 + const { positionList } = areaInfo + const elementList = this.draw.getOriginalMainElementList() + const valueList = payload.value + formatElementList( + [ + { + type: ElementType.AREA, + value: '', + valueList, + areaId: areaInfo.id, + area: areaInfo.area + } + ], + { + editorOptions: this.options + } + ) + this.draw.spliceElementList( + elementList, + positionList[0].index, + positionList.length, + valueList, + { + isIgnoreDeletedRule: true + } + ) + this.draw.render({ + isSetCursor: false + }) + } +} diff --git a/src/editor/core/draw/interactive/Group.ts b/src/editor/core/draw/interactive/Group.ts new file mode 100644 index 0000000..1334770 --- /dev/null +++ b/src/editor/core/draw/interactive/Group.ts @@ -0,0 +1,198 @@ +import { EditorZone } from '../../../dataset/enum/Editor' +import { ElementType } from '../../../dataset/enum/Element' +import { DeepRequired } from '../../../interface/Common' +import { IEditorOption } from '../../../interface/Editor' +import { IElement, IElementFillRect } from '../../../interface/Element' +import { IPositionContext } from '../../../interface/Position' +import { IRange } from '../../../interface/Range' +import { getUUID } from '../../../utils' +import { RangeManager } from '../../range/RangeManager' +import { Draw } from '../Draw' + +export class Group { + private draw: Draw + private options: DeepRequired + private range: RangeManager + private fillRectMap: Map + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + this.range = draw.getRange() + this.fillRectMap = new Map() + } + + public setGroup(): string | null { + if ( + this.draw.isReadonly() || + this.draw.getZone().getZone() !== EditorZone.MAIN + ) { + return null + } + const selection = this.range.getSelection() + if (!selection) return null + const groupId = getUUID() + selection.forEach(el => { + if (!Array.isArray(el.groupIds)) { + el.groupIds = [] + } + el.groupIds.push(groupId) + }) + this.draw.render({ + isSetCursor: false, + isCompute: false + }) + return groupId + } + + public getElementListByGroupId( + elementList: IElement[], + groupId: string + ): IElement[] { + const groupElementList: IElement[] = [] + for (let e = 0; e < elementList.length; e++) { + const element = elementList[e] + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const tdGroupElementList = this.getElementListByGroupId( + td.value, + groupId + ) + if (tdGroupElementList.length) { + groupElementList.push(...tdGroupElementList) + return groupElementList + } + } + } + } + if (element?.groupIds?.includes(groupId)) { + groupElementList.push(element) + const nextElement = elementList[e + 1] + if (!nextElement?.groupIds?.includes(groupId)) break + } + } + return groupElementList + } + + public deleteGroup(groupId: string) { + if (this.draw.isReadonly()) return + // 仅主体内容可以成组 + const elementList = this.draw.getOriginalMainElementList() + const groupElementList = this.getElementListByGroupId(elementList, groupId) + if (!groupElementList.length) return + for (let e = 0; e < groupElementList.length; e++) { + const element = groupElementList[e] + const groupIds = element.groupIds! + const groupIndex = groupIds.findIndex(id => id === groupId) + groupIds.splice(groupIndex, 1) + // 不包含成组时删除字段,减少存储及内存占用 + if (!groupIds.length) { + delete element.groupIds + } + } + this.draw.render({ + isSetCursor: false, + isCompute: false + }) + } + + public getContextByGroupId( + elementList: IElement[], + groupId: string + ): (IRange & IPositionContext) | null { + for (let e = 0; e < elementList.length; e++) { + const element = elementList[e] + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const range = this.getContextByGroupId(td.value, groupId) + if (range) { + return { + ...range, + isTable: true, + index: e, + trIndex: r, + tdIndex: d, + tdId: td.id, + trId: tr.id, + tableId: element.tableId + } + } + } + } + } + const nextElement = elementList[e + 1] + if ( + element.groupIds?.includes(groupId) && + !nextElement?.groupIds?.includes(groupId) + ) { + return { + isTable: false, + startIndex: e, + endIndex: e + } + } + } + return null + } + + public clearFillInfo() { + this.fillRectMap.clear() + } + + public recordFillInfo( + element: IElement, + x: number, + y: number, + width: number, + height: number + ) { + const groupIds = element.groupIds + if (!groupIds) return + for (const groupId of groupIds) { + const fillRect = this.fillRectMap.get(groupId) + if (!fillRect) { + this.fillRectMap.set(groupId, { + x, + y, + width, + height + }) + } else { + fillRect.width += width + } + } + } + + public render(ctx: CanvasRenderingContext2D) { + if (!this.fillRectMap.size) return + // 当前激活组信息 + const range = this.range.getRange() + const elementList = this.draw.getElementList() + const anchorGroupIds = elementList[range.endIndex]?.groupIds + const { + group: { backgroundColor, opacity, activeOpacity, activeBackgroundColor } + } = this.options + ctx.save() + this.fillRectMap.forEach((fillRect, groupId) => { + const { x, y, width, height } = fillRect + if (anchorGroupIds?.includes(groupId)) { + ctx.globalAlpha = activeOpacity + ctx.fillStyle = activeBackgroundColor + } else { + ctx.globalAlpha = opacity + ctx.fillStyle = backgroundColor + } + ctx.fillRect(x, y, width, height) + }) + ctx.restore() + this.clearFillInfo() + } +} diff --git a/src/editor/core/draw/interactive/Search.ts b/src/editor/core/draw/interactive/Search.ts new file mode 100644 index 0000000..f074bd8 --- /dev/null +++ b/src/editor/core/draw/interactive/Search.ts @@ -0,0 +1,491 @@ +import { ZERO } from '../../../dataset/constant/Common' +import { TEXTLIKE_ELEMENT_TYPE } from '../../../dataset/constant/Element' +import { ControlComponent } from '../../../dataset/enum/Control' +import { EditorContext } from '../../../dataset/enum/Editor' +import { ElementType } from '../../../dataset/enum/Element' +import { IEditorOption } from '../../../interface/Editor' +import { IElement, IElementPosition } from '../../../interface/Element' +import { + IReplaceOption, + ISearchResult, + ISearchResultRestArgs +} from '../../../interface/Search' +import { getUUID, isNumber } from '../../../utils' +import { Position } from '../../position/Position' +import { Draw } from '../Draw' + +export interface INavigateInfo { + index: number + count: number +} + +export class Search { + private draw: Draw + private options: Required + private position: Position + private searchKeyword: string | null + private searchNavigateIndex: number | null + private searchMatchList: ISearchResult[] + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + this.position = draw.getPosition() + this.searchNavigateIndex = null + this.searchKeyword = null + this.searchMatchList = [] + } + + public getSearchKeyword(): string | null { + return this.searchKeyword + } + + public setSearchKeyword(payload: string | null) { + this.searchKeyword = payload + this.searchNavigateIndex = null + } + + public searchNavigatePre(): number | null { + if (!this.searchMatchList.length || !this.searchKeyword) return null + if (this.searchNavigateIndex === null) { + this.searchNavigateIndex = 0 + } else { + let index = this.searchNavigateIndex - 1 + let isExistPre = false + const searchNavigateId = + this.searchMatchList[this.searchNavigateIndex].groupId + while (index >= 0) { + const match = this.searchMatchList[index] + if (searchNavigateId !== match.groupId) { + isExistPre = true + this.searchNavigateIndex = index - (this.searchKeyword.length - 1) + break + } + index-- + } + if (!isExistPre) { + const lastSearchMatch = + this.searchMatchList[this.searchMatchList.length - 1] + if (lastSearchMatch.groupId === searchNavigateId) return null + this.searchNavigateIndex = + this.searchMatchList.length - 1 - (this.searchKeyword.length - 1) + } + } + return this.searchNavigateIndex + } + + public searchNavigateNext(): number | null { + if (!this.searchMatchList.length || !this.searchKeyword) return null + if (this.searchNavigateIndex === null) { + this.searchNavigateIndex = 0 + } else { + let index = this.searchNavigateIndex + 1 + let isExistNext = false + const searchNavigateId = + this.searchMatchList[this.searchNavigateIndex].groupId + while (index < this.searchMatchList.length) { + const match = this.searchMatchList[index] + if (searchNavigateId !== match.groupId) { + isExistNext = true + this.searchNavigateIndex = index + break + } + index++ + } + if (!isExistNext) { + const firstSearchMatch = this.searchMatchList[0] + if (firstSearchMatch.groupId === searchNavigateId) return null + this.searchNavigateIndex = 0 + } + } + return this.searchNavigateIndex + } + + public searchNavigateScrollIntoView(position: IElementPosition) { + const { + coordinate: { leftTop, leftBottom, rightTop }, + pageNo + } = position + const height = this.draw.getHeight() + const pageGap = this.draw.getPageGap() + const preY = pageNo * (height + pageGap) + // 创建定位锚点 + const anchor = document.createElement('div') + anchor.style.position = 'absolute' + // 扩大搜索词尺寸,使可视范围更广 + const ANCHOR_OVERFLOW_SIZE = 50 + anchor.style.width = `${rightTop[0] - leftTop[0] + ANCHOR_OVERFLOW_SIZE}px` + anchor.style.height = `${ + leftBottom[1] - leftTop[1] + ANCHOR_OVERFLOW_SIZE + }px` + anchor.style.left = `${leftTop[0]}px` + anchor.style.top = `${leftTop[1] + preY}px` + this.draw.getContainer().append(anchor) + // 移动到可视范围 + anchor.scrollIntoView(false) + anchor.remove() + } + + public getSearchNavigateIndexList() { + if (this.searchNavigateIndex === null || !this.searchKeyword) return [] + return new Array(this.searchKeyword.length) + .fill(this.searchNavigateIndex) + .map((navigate, index) => navigate + index) + } + + public getSearchMatchList(): ISearchResult[] { + return this.searchMatchList + } + + public getSearchNavigateInfo(): null | INavigateInfo { + if (!this.searchKeyword || !this.searchMatchList.length) return null + const index = + this.searchNavigateIndex !== null + ? this.searchNavigateIndex / this.searchKeyword.length + 1 + : 0 + let count = 0 + let groupId = null + for (let s = 0; s < this.searchMatchList.length; s++) { + const match = this.searchMatchList[s] + if (groupId === match.groupId) continue + groupId = match.groupId + count += 1 + } + return { + index, + count + } + } + + public getMatchList( + payload: string, + originalElementList: IElement[] + ): ISearchResult[] { + const keyword = payload.toLocaleLowerCase() + const searchMatchList: ISearchResult[] = [] + // 分组 + const elementListGroup: { + type: EditorContext + elementList: IElement[] + index: number + }[] = [] + const originalElementListLength = originalElementList.length + // 查找表格所在位置 + const tableIndexList = [] + for (let e = 0; e < originalElementListLength; e++) { + const element = originalElementList[e] + if (element.type === ElementType.TABLE) { + tableIndexList.push(e) + } + } + let i = 0 + let elementIndex = 0 + while (elementIndex < originalElementListLength - 1) { + const endIndex = tableIndexList.length + ? tableIndexList[i] + : originalElementListLength + const pageElement = originalElementList.slice(elementIndex, endIndex) + if (pageElement.length) { + elementListGroup.push({ + index: elementIndex, + type: EditorContext.PAGE, + elementList: pageElement + }) + } + const tableElement = originalElementList[endIndex] + if (tableElement) { + elementListGroup.push({ + index: endIndex, + type: EditorContext.TABLE, + elementList: [tableElement] + }) + } + elementIndex = endIndex + 1 + i++ + } + // 搜索文本 + function searchClosure( + payload: string | null, + type: EditorContext, + elementList: IElement[], + restArgs?: ISearchResultRestArgs + ) { + if (!payload) return + const text = elementList + .map(e => + !e.type || + (TEXTLIKE_ELEMENT_TYPE.includes(e.type) && + e.controlComponent !== ControlComponent.CHECKBOX && + !e.hide && + !e.control?.hide && + !e.area?.hide) + ? e.value + : ZERO + ) + .filter(Boolean) + .join('') + .toLocaleLowerCase() + const matchStartIndexList = [] + let index = text.indexOf(payload) + while (index !== -1) { + matchStartIndexList.push(index) + index = text.indexOf(payload, index + payload.length) + } + for (let m = 0; m < matchStartIndexList.length; m++) { + const startIndex = matchStartIndexList[m] + const groupId = getUUID() + for (let i = 0; i < payload.length; i++) { + const index = startIndex + i + (restArgs?.startIndex || 0) + searchMatchList.push({ + type, + index, + groupId, + ...restArgs + }) + } + } + } + for (let e = 0; e < elementListGroup.length; e++) { + const group = elementListGroup[e] + if (group.type === EditorContext.TABLE) { + const tableElement = group.elementList[0] + for (let t = 0; t < tableElement.trList!.length; t++) { + const tr = tableElement.trList![t] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const restArgs: ISearchResultRestArgs = { + tableId: tableElement.id, + tableIndex: group.index, + trIndex: t, + tdIndex: d, + tdId: td.id + } + searchClosure(keyword, group.type, td.value, restArgs) + } + } + } else { + searchClosure(keyword, group.type, group.elementList, { + startIndex: group.index + }) + } + } + return searchMatchList + } + + public compute(payload: string) { + this.searchMatchList = this.getMatchList( + payload, + this.draw.getOriginalElementList() + ) + } + + public render(ctx: CanvasRenderingContext2D, pageIndex: number) { + if ( + !this.searchMatchList || + !this.searchMatchList.length || + !this.searchKeyword + ) { + return + } + const { searchMatchAlpha, searchMatchColor, searchNavigateMatchColor } = + this.options + const positionList = this.position.getOriginalPositionList() + const elementList = this.draw.getOriginalElementList() + ctx.save() + ctx.globalAlpha = searchMatchAlpha + for (let s = 0; s < this.searchMatchList.length; s++) { + const searchMatch = this.searchMatchList[s] + let position: IElementPosition | null = null + if (searchMatch.type === EditorContext.TABLE) { + const { tableIndex, trIndex, tdIndex, index } = searchMatch + position = + elementList[tableIndex!]?.trList![trIndex!].tdList[tdIndex!] + ?.positionList![index] + } else { + position = positionList[searchMatch.index] + } + if (!position) continue + const { + coordinate: { leftTop, leftBottom, rightTop }, + pageNo + } = position + if (pageNo !== pageIndex) continue + // 高亮并定位当前搜索词 + const searchMatchIndexList = this.getSearchNavigateIndexList() + if (searchMatchIndexList.includes(s)) { + ctx.fillStyle = searchNavigateMatchColor + // 是否是第一个字符,则移动到可视范围 + const preSearchMatch = this.searchMatchList[s - 1] + if (!preSearchMatch || preSearchMatch.groupId !== searchMatch.groupId) { + this.searchNavigateScrollIntoView(position) + } + } else { + ctx.fillStyle = searchMatchColor + } + const x = leftTop[0] + const y = leftTop[1] + const width = rightTop[0] - leftTop[0] + const height = leftBottom[1] - leftTop[1] + ctx.fillRect(x, y, width, height) + } + ctx.restore() + } + + public replace(payload: string, option?: IReplaceOption) { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + if (payload === undefined || payload === null) return + let matchList = this.getSearchMatchList() + // 替换搜索项 + const replaceIndex = option?.index + if (isNumber(replaceIndex)) { + const matchGroup: ISearchResult[][] = [] + matchList.forEach(match => { + const last = matchGroup[matchGroup.length - 1] + if (!last || last[0].groupId !== match.groupId) { + matchGroup.push([match]) + } else { + last.push(match) + } + }) + matchList = matchGroup[replaceIndex] + } + if (!matchList?.length) return + const isDesignMode = this.draw.isDesignMode() + // 匹配index变化的差值 + let pageDiffCount = 0 + let tableDiffCount = 0 + // 匹配搜索词的组标识 + let curGroupId = '' + // 表格上下文 + let curTdId = '' + // 搜索值 > 替换值:增加元素;搜索值 < 替换值:减少元素 + let firstMatchIndex = -1 + const elementList = this.draw.getOriginalElementList() + for (let m = 0; m < matchList.length; m++) { + const match = matchList[m] + if (match.type === EditorContext.TABLE) { + const { tableIndex, trIndex, tdIndex, index, tdId } = match + if (curTdId && tdId !== curTdId) { + tableDiffCount = 0 + } + curTdId = tdId! + const curTableIndex = tableIndex! + pageDiffCount + const tableElementList = + elementList[curTableIndex].trList![trIndex!].tdList[tdIndex!].value + // 表格内元素 + const curIndex = index + tableDiffCount + const tableElement = tableElementList[curIndex] + // 非设计模式下设置元素不可删除 || 控件结构元素 => 禁止替换 + if ( + !isDesignMode && + (tableElement?.control?.deletable === false || + tableElement?.title?.deletable === false) + ) { + continue + } + if (payload === '') { + this.draw.spliceElementList(tableElementList, curIndex, 1) + tableDiffCount-- + if (!~firstMatchIndex) { + firstMatchIndex = m + } + continue + } + if (curGroupId === match.groupId) { + this.draw.spliceElementList(tableElementList, curIndex, 1) + tableDiffCount-- + continue + } + if (!~firstMatchIndex) { + firstMatchIndex = m + } + for (let p = 0; p < payload.length; p++) { + const value = payload[p] + if (p === 0) { + tableElement.value = value + } else { + this.draw.spliceElementList(tableElementList, curIndex + p, 0, [ + { + ...tableElement, + value + } + ]) + tableDiffCount++ + } + } + } else { + const curIndex = match.index + pageDiffCount + const element = elementList[curIndex] + // 非设计模式下设置元素不可删除 || 控件结构元素 => 禁止替换 + if ( + (!isDesignMode && + (element?.control?.deletable === false || + element?.title?.deletable === false)) || + (element.type === ElementType.CONTROL && + element.controlComponent !== ControlComponent.VALUE) + ) { + continue + } + if (payload === '') { + this.draw.spliceElementList(elementList, curIndex, 1) + pageDiffCount-- + if (!~firstMatchIndex) { + firstMatchIndex = m + } + continue + } + if (!~firstMatchIndex) { + firstMatchIndex = m + } + if (curGroupId === match.groupId) { + this.draw.spliceElementList(elementList, curIndex, 1) + pageDiffCount-- + continue + } + for (let p = 0; p < payload.length; p++) { + const value = payload[p] + if (p === 0) { + element.value = value + } else { + this.draw.spliceElementList(elementList, curIndex + p, 0, [ + { + ...element, + value + } + ]) + pageDiffCount++ + } + } + } + curGroupId = match.groupId + } + if (!~firstMatchIndex) return + // 定位-首个被匹配关键词后 + const firstMatch = matchList[firstMatchIndex] + const firstIndex = firstMatch.index + (payload.length - 1) + if (firstMatch.type === EditorContext.TABLE) { + const { tableIndex, trIndex, tdIndex, index } = firstMatch + const element = + elementList[tableIndex!].trList![trIndex!].tdList[tdIndex!].value[index] + this.position.setPositionContext({ + isTable: true, + index: tableIndex, + trIndex, + tdIndex, + tdId: element.tdId, + trId: element.trId, + tableId: element.tableId + }) + } else { + this.position.setPositionContext({ + isTable: false + }) + } + this.draw.getRange().setRange(firstIndex, firstIndex) + // 重新渲染 + this.draw.render({ + curIndex: firstIndex + }) + } +} diff --git a/src/editor/core/draw/particle/CheckboxParticle.ts b/src/editor/core/draw/particle/CheckboxParticle.ts new file mode 100644 index 0000000..1ec7faf --- /dev/null +++ b/src/editor/core/draw/particle/CheckboxParticle.ts @@ -0,0 +1,111 @@ +import { NBSP, ZERO } from '../../../dataset/constant/Common' +import { VerticalAlign } from '../../../dataset/enum/VerticalAlign' +import { DeepRequired } from '../../../interface/Common' +import { IEditorOption } from '../../../interface/Editor' +import { IElement } from '../../../interface/Element' +import { IRow, IRowElement } from '../../../interface/Row' +import { Draw } from '../Draw' + +interface ICheckboxRenderOption { + ctx: CanvasRenderingContext2D + x: number + y: number + row: IRow + index: number +} + +export class CheckboxParticle { + private draw: Draw + private options: DeepRequired + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + } + + public setSelect(element: IElement) { + const { checkbox } = element + if (checkbox) { + checkbox.value = !checkbox.value + } else { + element.checkbox = { + value: true + } + } + this.draw.render({ + isCompute: false, + isSetCursor: false + }) + } + + public render(payload: ICheckboxRenderOption) { + const { ctx, x, index, row } = payload + let { y } = payload + const { + checkbox: { gap, lineWidth, fillStyle, strokeStyle, verticalAlign }, + scale + } = this.options + const { metrics, checkbox } = row.elementList[index] + // 垂直布局设置 + if ( + verticalAlign === VerticalAlign.TOP || + verticalAlign === VerticalAlign.MIDDLE + ) { + let nextIndex = index + 1 + let nextElement: IRowElement | null = null + while (nextIndex < row.elementList.length) { + nextElement = row.elementList[nextIndex] + if (nextElement.value !== ZERO && nextElement.value !== NBSP) break + nextIndex++ + } + // 以后一个非空格元素为基准 + if (nextElement) { + const { + metrics: { boundingBoxAscent, boundingBoxDescent } + } = nextElement + const textHeight = boundingBoxAscent + boundingBoxDescent + if (textHeight > metrics.height) { + if (verticalAlign === VerticalAlign.TOP) { + y -= boundingBoxAscent - metrics.height + } else if (verticalAlign === VerticalAlign.MIDDLE) { + y -= (textHeight - metrics.height) / 2 + } + } + } + } + // left top 四舍五入避免1像素问题 + const left = Math.round(x + gap * scale) + const top = Math.round(y - metrics.height + lineWidth) + const width = metrics.width - gap * 2 * scale + const height = metrics.height + ctx.save() + ctx.beginPath() + ctx.translate(0.5, 0.5) + // 绘制勾选状态 + if (checkbox?.value) { + // 边框 + ctx.lineWidth = lineWidth + ctx.strokeStyle = fillStyle + ctx.rect(left, top, width, height) + ctx.stroke() + // 背景色 + ctx.beginPath() + ctx.fillStyle = fillStyle + ctx.fillRect(left, top, width, height) + // 勾选对号 + ctx.beginPath() + ctx.strokeStyle = strokeStyle + ctx.lineWidth = lineWidth * 2 * scale + ctx.moveTo(left + 2 * scale, top + height / 2) + ctx.lineTo(left + width / 2, top + height - 3 * scale) + ctx.lineTo(left + width - 2 * scale, top + 3 * scale) + ctx.stroke() + } else { + ctx.lineWidth = lineWidth + ctx.rect(left, top, width, height) + ctx.stroke() + } + ctx.closePath() + ctx.restore() + } +} diff --git a/src/editor/core/draw/particle/HyperlinkParticle.ts b/src/editor/core/draw/particle/HyperlinkParticle.ts new file mode 100644 index 0000000..394bfe0 --- /dev/null +++ b/src/editor/core/draw/particle/HyperlinkParticle.ts @@ -0,0 +1,86 @@ +import { IElement } from '../../..' +import { EDITOR_PREFIX } from '../../../dataset/constant/Editor' +import { IEditorOption } from '../../../interface/Editor' +import { IElementPosition } from '../../../interface/Element' +import { IRowElement } from '../../../interface/Row' +import { Draw } from '../Draw' + +export class HyperlinkParticle { + private draw: Draw + private options: Required + private container: HTMLDivElement + private hyperlinkPopupContainer: HTMLDivElement + private hyperlinkDom: HTMLAnchorElement + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + this.container = draw.getContainer() + const { hyperlinkPopupContainer, hyperlinkDom } = + this._createHyperlinkPopupDom() + this.hyperlinkDom = hyperlinkDom + this.hyperlinkPopupContainer = hyperlinkPopupContainer + } + + private _createHyperlinkPopupDom() { + const hyperlinkPopupContainer = document.createElement('div') + hyperlinkPopupContainer.classList.add(`${EDITOR_PREFIX}-hyperlink-popup`) + const hyperlinkDom = document.createElement('a') + hyperlinkDom.target = '_blank' + hyperlinkDom.rel = 'noopener' + hyperlinkPopupContainer.append(hyperlinkDom) + this.container.append(hyperlinkPopupContainer) + return { hyperlinkPopupContainer, hyperlinkDom } + } + + public drawHyperlinkPopup(element: IElement, position: IElementPosition) { + const { + coordinate: { + leftTop: [left, top] + }, + lineHeight + } = position + const height = this.draw.getHeight() + const pageGap = this.draw.getPageGap() + const preY = this.draw.getPageNo() * (height + pageGap) + // 位置 + this.hyperlinkPopupContainer.style.display = 'block' + this.hyperlinkPopupContainer.style.left = `${left}px` + this.hyperlinkPopupContainer.style.top = `${top + preY + lineHeight}px` + // 标签 + const url = element.url || '#' + this.hyperlinkDom.href = url + this.hyperlinkDom.title = url + this.hyperlinkDom.innerText = url + } + + public clearHyperlinkPopup() { + this.hyperlinkPopupContainer.style.display = 'none' + } + + public openHyperlink(element: IElement) { + const newTab = window.open(element.url, '_blank') + if (newTab) { + newTab.opener = null + } + } + + public render( + ctx: CanvasRenderingContext2D, + element: IRowElement, + x: number, + y: number + ) { + ctx.save() + ctx.font = element.style + if (!element.color) { + element.color = this.options.defaultHyperlinkColor + } + ctx.fillStyle = element.color + if (element.underline === undefined) { + element.underline = true + } + ctx.fillText(element.value, x, y) + ctx.restore() + } +} diff --git a/src/editor/core/draw/particle/ImageParticle.ts b/src/editor/core/draw/particle/ImageParticle.ts new file mode 100644 index 0000000..8886e95 --- /dev/null +++ b/src/editor/core/draw/particle/ImageParticle.ts @@ -0,0 +1,166 @@ +import { EDITOR_PREFIX } from '../../../dataset/constant/Editor' +import { ImageDisplay } from '../../../dataset/enum/Common' +import { ElementType } from '../../../dataset/enum/Element' +import { IEditorOption } from '../../../interface/Editor' +import { IElement } from '../../../interface/Element' +import { convertStringToBase64 } from '../../../utils' +import { Draw } from '../Draw' + +export class ImageParticle { + private draw: Draw + protected options: Required + protected imageCache: Map + private container: HTMLDivElement + private floatImageContainer: HTMLDivElement | null + private floatImage: HTMLImageElement | null + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + this.container = draw.getContainer() + this.imageCache = new Map() + this.floatImageContainer = null + this.floatImage = null + } + + public getOriginalMainImageList(): IElement[] { + const imageList: IElement[] = [] + const getImageList = (elementList: IElement[]) => { + for (const element of elementList) { + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + getImageList(td.value) + } + } + } else if (element.type === ElementType.IMAGE) { + imageList.push(element) + } + } + } + // 获取正文图片列表 + getImageList(this.draw.getOriginalMainElementList()) + return imageList + } + + public createFloatImage(element: IElement) { + const { scale } = this.options + // 复用浮动元素 + let floatImageContainer = this.floatImageContainer + let floatImage = this.floatImage + if (!floatImageContainer) { + floatImageContainer = document.createElement('div') + floatImageContainer.classList.add(`${EDITOR_PREFIX}-float-image`) + this.container.append(floatImageContainer) + this.floatImageContainer = floatImageContainer + } + if (!floatImage) { + floatImage = document.createElement('img') + floatImageContainer.append(floatImage) + this.floatImage = floatImage + } + floatImageContainer.style.display = 'none' + floatImage.style.width = `${element.width! * scale}px` + floatImage.style.height = `${element.height! * scale}px` + // 浮动图片初始信息 + const height = this.draw.getHeight() + const pageGap = this.draw.getPageGap() + const preY = this.draw.getPageNo() * (height + pageGap) + const imgFloatPosition = element.imgFloatPosition! + floatImageContainer.style.left = `${imgFloatPosition.x * scale}px` + floatImageContainer.style.top = `${preY + imgFloatPosition.y * scale}px` + floatImage.src = element.value + } + + public dragFloatImage(movementX: number, movementY: number) { + if (!this.floatImageContainer) return + this.floatImageContainer.style.display = 'block' + // 之前的坐标加移动长度 + const x = parseFloat(this.floatImageContainer.style.left) + movementX + const y = parseFloat(this.floatImageContainer.style.top) + movementY + this.floatImageContainer.style.left = `${x}px` + this.floatImageContainer.style.top = `${y}px` + } + + public destroyFloatImage() { + if (this.floatImageContainer) { + this.floatImageContainer.style.display = 'none' + } + } + + protected addImageObserver(promise: Promise) { + this.draw.getImageObserver().add(promise) + } + + protected getFallbackImage(width: number, height: number): HTMLImageElement { + const tileSize = 8 + const x = (width - Math.ceil(width / tileSize) * tileSize) / 2 + const y = (height - Math.ceil(height / tileSize) * tileSize) / 2 + const svg = ` + + + + + + + + ` + const fallbackImage = new Image() + fallbackImage.src = `data:image/svg+xml;base64,${convertStringToBase64( + svg + )}` + return fallbackImage + } + + public render( + ctx: CanvasRenderingContext2D, + element: IElement, + x: number, + y: number + ) { + const { scale } = this.options + const width = element.width! * scale + const height = element.height! * scale + if (this.imageCache.has(element.value)) { + const img = this.imageCache.get(element.value)! + ctx.drawImage(img, x, y, width, height) + } else { + const cacheRenderCount = this.draw.getRenderCount() + const imageLoadPromise = new Promise((resolve, reject) => { + const img = new Image() + img.setAttribute('crossOrigin', 'Anonymous') + img.src = element.value + img.onload = () => { + this.imageCache.set(element.value, img) + resolve(element) + // 因图片加载异步,图片加载后可能属于上一次渲染方法 + if (cacheRenderCount !== this.draw.getRenderCount()) return + // 衬于文字下方图片需要重新首先绘制 + if (element.imgDisplay === ImageDisplay.FLOAT_BOTTOM) { + this.draw.render({ + isCompute: false, + isSetCursor: false, + isSubmitHistory: false + }) + } else { + ctx.drawImage(img, x, y, width, height) + } + } + img.onerror = error => { + const fallbackImage = this.getFallbackImage(width, height) + fallbackImage.onload = () => { + ctx.drawImage(fallbackImage, x, y, width, height) + this.imageCache.set(element.value, fallbackImage) + } + reject(error) + } + }) + this.addImageObserver(imageLoadPromise) + } + } +} diff --git a/src/editor/core/draw/particle/LineBreakParticle.ts b/src/editor/core/draw/particle/LineBreakParticle.ts new file mode 100644 index 0000000..f6ce167 --- /dev/null +++ b/src/editor/core/draw/particle/LineBreakParticle.ts @@ -0,0 +1,55 @@ +import { DeepRequired } from '../../../interface/Common' +import { IEditorOption } from '../../../interface/Editor' +import { IRowElement } from '../../../interface/Row' +import { Draw } from '../Draw' + +export class LineBreakParticle { + private options: DeepRequired + public static readonly WIDTH = 12 + public static readonly HEIGHT = 9 + public static readonly GAP = 3 // 距离左边间隙 + + constructor(draw: Draw) { + this.options = draw.getOptions() + } + + public render( + ctx: CanvasRenderingContext2D, + element: IRowElement, + x: number, + y: number + ) { + const { + scale, + lineBreak: { color, lineWidth } + } = this.options + ctx.save() + ctx.beginPath() + // 换行符尺寸设置为9像素 + const top = y - (LineBreakParticle.HEIGHT * scale) / 2 + const left = x + element.metrics.width + // 移动位置并设置缩放 + ctx.translate(left, top) + ctx.scale(scale, scale) + // 样式设置 + ctx.strokeStyle = color + ctx.lineWidth = lineWidth + ctx.lineCap = 'round' + ctx.lineJoin = 'round' + ctx.beginPath() + // 回车折线 + ctx.moveTo(8, 0) + ctx.lineTo(12, 0) + ctx.lineTo(12, 6) + ctx.lineTo(3, 6) + // 箭头向上 + ctx.moveTo(3, 6) + ctx.lineTo(6, 3) + // 箭头向下 + ctx.moveTo(3, 6) + ctx.lineTo(6, 9) + ctx.stroke() + ctx.closePath() + ctx.restore() + } +} diff --git a/src/editor/core/draw/particle/ListParticle.ts b/src/editor/core/draw/particle/ListParticle.ts new file mode 100644 index 0000000..24ce643 --- /dev/null +++ b/src/editor/core/draw/particle/ListParticle.ts @@ -0,0 +1,230 @@ +import { ZERO } from '../../../dataset/constant/Common' +import { ulStyleMapping } from '../../../dataset/constant/List' +import { ElementType } from '../../../dataset/enum/Element' +import { KeyMap } from '../../../dataset/enum/KeyMap' +import { ListStyle, ListType, UlStyle } from '../../../dataset/enum/List' +import { DeepRequired } from '../../../interface/Common' +import { IEditorOption } from '../../../interface/Editor' +import { IElement, IElementPosition } from '../../../interface/Element' +import { IRow, IRowElement } from '../../../interface/Row' +import { getUUID } from '../../../utils' +import { RangeManager } from '../../range/RangeManager' +import { Draw } from '../Draw' + +export class ListParticle { + private draw: Draw + private range: RangeManager + private options: DeepRequired + + // 非递增样式直接返回默认值 + private readonly UN_COUNT_STYLE_WIDTH = 20 + private readonly MEASURE_BASE_TEXT = '0' + private readonly LIST_GAP = 10 + + constructor(draw: Draw) { + this.draw = draw + this.range = draw.getRange() + this.options = draw.getOptions() + } + + public setList(listType: ListType | null, listStyle?: ListStyle) { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + const { startIndex, endIndex } = this.range.getRange() + if (!~startIndex && !~endIndex) return + // 需要改变的元素列表 + const changeElementList = this.range.getRangeParagraphElementList() + if (!changeElementList || !changeElementList.length) return + // 如果包含列表则设置为取消列表 + const isUnsetList = changeElementList.find( + el => el.listType === listType && el.listStyle === listStyle + ) + if (isUnsetList || !listType) { + this.unsetList() + return + } + // 设置值 + const listId = getUUID() + changeElementList.forEach(el => { + el.listId = listId + el.listType = listType + el.listStyle = listStyle + }) + // 光标定位 + const isSetCursor = startIndex === endIndex + const curIndex = isSetCursor ? endIndex : startIndex + this.draw.render({ curIndex, isSetCursor }) + } + + public unsetList() { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + const { startIndex, endIndex } = this.range.getRange() + if (!~startIndex && !~endIndex) return + // 需要改变的元素列表 + const changeElementList = this.range + .getRangeParagraphElementList() + ?.filter(el => el.listId) + if (!changeElementList || !changeElementList.length) return + // 如果列表最后字符不是换行符则需插入换行符 + const elementList = this.draw.getElementList() + const endElement = elementList[endIndex] + if (endElement.listId) { + let start = endIndex + 1 + while (start < elementList.length) { + const element = elementList[start] + if (element.value === ZERO && !element.listWrap) break + if (element.listId !== endElement.listId) { + this.draw.spliceElementList(elementList, start, 0, [ + { + value: ZERO + } + ]) + break + } + start++ + } + } + // 取消设置 + changeElementList.forEach(el => { + delete el.listId + delete el.listType + delete el.listStyle + delete el.listWrap + }) + // 光标定位 + const isSetCursor = startIndex === endIndex + const curIndex = isSetCursor ? endIndex : startIndex + this.draw.render({ curIndex, isSetCursor }) + } + + public computeListStyle( + ctx: CanvasRenderingContext2D, + elementList: IElement[] + ): Map { + const listStyleMap = new Map() + let start = 0 + let curListId = elementList[start].listId + let curElementList: IElement[] = [] + const elementLength = elementList.length + while (start < elementLength) { + const curElement = elementList[start] + if (curListId && curListId === curElement.listId) { + curElementList.push(curElement) + } else { + if (curElement.listId && curElement.listId !== curListId) { + // 列表结束 + if (curElementList.length) { + const width = this.getListStyleWidth(ctx, curElementList) + listStyleMap.set(curListId!, width) + } + curListId = curElement.listId + curElementList = curListId ? [curElement] : [] + } + } + start++ + } + if (curElementList.length) { + const width = this.getListStyleWidth(ctx, curElementList) + listStyleMap.set(curListId!, width) + } + return listStyleMap + } + + public getListStyleWidth( + ctx: CanvasRenderingContext2D, + listElementList: IElement[] + ): number { + const { scale, checkbox } = this.options + const startElement = listElementList[0] + // 非递增样式返回固定值 + if ( + startElement.listStyle && + startElement.listStyle !== ListStyle.DECIMAL + ) { + if (startElement.listStyle === ListStyle.CHECKBOX) { + return (checkbox.width + this.LIST_GAP) * scale + } + return this.UN_COUNT_STYLE_WIDTH * scale + } + // 计算列表数量 + const count = listElementList.reduce((pre, cur) => { + if (cur.value === ZERO) { + pre += 1 + } + return pre + }, 0) + if (!count) return 0 + // 以递增样式最大宽度为准 + const text = `${this.MEASURE_BASE_TEXT.repeat(String(count).length)}${ + KeyMap.PERIOD + }` + const textMetrics = ctx.measureText(text) + return Math.ceil((textMetrics.width + this.LIST_GAP) * scale) + } + + public drawListStyle( + ctx: CanvasRenderingContext2D, + row: IRow, + position: IElementPosition + ) { + const { elementList, offsetX, listIndex, ascent } = row + const startElement = elementList[0] + if (startElement.value !== ZERO || startElement.listWrap) return + // tab width + let tabWidth = 0 + const { defaultTabWidth, scale, defaultFont, defaultSize } = this.options + for (let i = 1; i < elementList.length; i++) { + const element = elementList[i] + if (element?.type !== ElementType.TAB) break + tabWidth += defaultTabWidth * scale + } + // 列表样式渲染 + const { + coordinate: { + leftTop: [startX, startY] + } + } = position + const x = startX - offsetX! + tabWidth + const y = startY + ascent + // 复选框样式特殊处理 + if (startElement.listStyle === ListStyle.CHECKBOX) { + const { width, height, gap } = this.options.checkbox + const checkboxRowElement: IRowElement = { + ...startElement, + checkbox: { + value: !!startElement.checkbox?.value + }, + metrics: { + ...startElement.metrics, + width: (width + gap * 2) * scale, + height: height * scale + } + } + this.draw.getCheckboxParticle().render({ + ctx, + x: x - gap * scale, + y, + index: 0, + row: { + ...row, + elementList: [checkboxRowElement, ...row.elementList] + } + }) + } else { + let text = '' + if (startElement.listType === ListType.UL) { + text = + ulStyleMapping[(startElement.listStyle)] || + ulStyleMapping[UlStyle.DISC] + } else { + text = `${listIndex! + 1}${KeyMap.PERIOD}` + } + if (!text) return + ctx.save() + ctx.font = `${defaultSize * scale}px ${defaultFont}` + ctx.fillText(text, x, y) + ctx.restore() + } + } +} diff --git a/src/editor/core/draw/particle/PageBreakParticle.ts b/src/editor/core/draw/particle/PageBreakParticle.ts new file mode 100644 index 0000000..3b4721b --- /dev/null +++ b/src/editor/core/draw/particle/PageBreakParticle.ts @@ -0,0 +1,54 @@ +import { DeepRequired } from '../../../interface/Common' +import { IEditorOption } from '../../../interface/Editor' +import { IRowElement } from '../../../interface/Row' +import { I18n } from '../../i18n/I18n' +import { Draw } from '../Draw' + +export class PageBreakParticle { + private draw: Draw + private options: DeepRequired + private i18n: I18n + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + this.i18n = draw.getI18n() + } + + public render( + ctx: CanvasRenderingContext2D, + element: IRowElement, + x: number, + y: number + ) { + const { + pageBreak: { font, fontSize, lineDash } + } = this.options + const displayName = this.i18n.t('pageBreak.displayName') + const { scale, defaultRowMargin } = this.options + const size = fontSize * scale + const elementWidth = element.width! * scale + const offsetY = + this.draw.getDefaultBasicRowMarginHeight() * defaultRowMargin + ctx.save() + ctx.font = `${size}px ${font}` + const textMeasure = ctx.measureText(displayName) + const halfX = (elementWidth - textMeasure.width) / 2 + // 线段 + ctx.setLineDash(lineDash) + ctx.translate(0, 0.5 + offsetY) + ctx.beginPath() + ctx.moveTo(x, y) + ctx.lineTo(x + halfX, y) + ctx.moveTo(x + halfX + textMeasure.width, y) + ctx.lineTo(x + elementWidth, y) + ctx.stroke() + // 文字 + ctx.fillText( + displayName, + x + halfX, + y + textMeasure.actualBoundingBoxAscent - size / 2 + ) + ctx.restore() + } +} diff --git a/src/editor/core/draw/particle/RadioParticle.ts b/src/editor/core/draw/particle/RadioParticle.ts new file mode 100644 index 0000000..1df91f3 --- /dev/null +++ b/src/editor/core/draw/particle/RadioParticle.ts @@ -0,0 +1,99 @@ +import { NBSP, ZERO } from '../../../dataset/constant/Common' +import { VerticalAlign } from '../../../dataset/enum/VerticalAlign' +import { DeepRequired } from '../../../interface/Common' +import { IEditorOption } from '../../../interface/Editor' +import { IElement } from '../../../interface/Element' +import { IRow, IRowElement } from '../../../interface/Row' +import { Draw } from '../Draw' + +interface IRadioRenderOption { + ctx: CanvasRenderingContext2D + x: number + y: number + row: IRow + index: number +} + +export class RadioParticle { + private draw: Draw + private options: DeepRequired + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + } + + public setSelect(element: IElement) { + const { radio } = element + if (radio) { + radio.value = !radio.value + } else { + element.radio = { + value: true + } + } + this.draw.render({ + isCompute: false, + isSetCursor: false + }) + } + + public render(payload: IRadioRenderOption) { + const { ctx, x, index, row } = payload + let { y } = payload + const { + radio: { gap, lineWidth, fillStyle, strokeStyle, verticalAlign }, + scale + } = this.options + const { metrics, radio } = row.elementList[index] + // 垂直布局设置 + if ( + verticalAlign === VerticalAlign.TOP || + verticalAlign === VerticalAlign.MIDDLE + ) { + let nextIndex = index + 1 + let nextElement: IRowElement | null = null + while (nextIndex < row.elementList.length) { + nextElement = row.elementList[nextIndex] + if (nextElement.value !== ZERO && nextElement.value !== NBSP) break + nextIndex++ + } + // 以后一个非空格元素为基准 + if (nextElement) { + const { + metrics: { boundingBoxAscent, boundingBoxDescent } + } = nextElement + const textHeight = boundingBoxAscent + boundingBoxDescent + if (textHeight > metrics.height) { + if (verticalAlign === VerticalAlign.TOP) { + y -= boundingBoxAscent - metrics.height + } else if (verticalAlign === VerticalAlign.MIDDLE) { + y -= (textHeight - metrics.height) / 2 + } + } + } + } + // left top 四舍五入避免1像素问题 + const left = Math.round(x + gap * scale) + const top = Math.round(y - metrics.height + lineWidth) + const width = metrics.width - gap * 2 * scale + const height = metrics.height + ctx.save() + ctx.beginPath() + ctx.translate(0.5, 0.5) + // 边框 + ctx.strokeStyle = radio?.value ? fillStyle : strokeStyle + ctx.lineWidth = lineWidth + ctx.arc(left + width / 2, top + height / 2, width / 2, 0, Math.PI * 2) + ctx.stroke() + // 填充选中色 + if (radio?.value) { + ctx.beginPath() + ctx.fillStyle = fillStyle + ctx.arc(left + width / 2, top + height / 2, width / 3, 0, Math.PI * 2) + ctx.fill() + } + ctx.closePath() + ctx.restore() + } +} diff --git a/src/editor/core/draw/particle/SeparatorParticle.ts b/src/editor/core/draw/particle/SeparatorParticle.ts new file mode 100644 index 0000000..a8e8ec5 --- /dev/null +++ b/src/editor/core/draw/particle/SeparatorParticle.ts @@ -0,0 +1,37 @@ +import { DeepRequired } from '../../../interface/Common' +import { IEditorOption } from '../../../interface/Editor' +import { IRowElement } from '../../../interface/Row' +import { Draw } from '../Draw' + +export class SeparatorParticle { + private options: DeepRequired + + constructor(draw: Draw) { + this.options = draw.getOptions() + } + + public render( + ctx: CanvasRenderingContext2D, + element: IRowElement, + x: number, + y: number + ) { + ctx.save() + const { + scale, + separator: { lineWidth, strokeStyle } + } = this.options + ctx.lineWidth = lineWidth * scale + ctx.strokeStyle = element.color || strokeStyle + if (element.dashArray?.length) { + ctx.setLineDash(element.dashArray) + } + const offsetY = Math.round(y) // 四舍五入避免绘制模糊 + ctx.translate(0, ctx.lineWidth / 2) + ctx.beginPath() + ctx.moveTo(x, offsetY) + ctx.lineTo(x + element.width! * scale, offsetY) + ctx.stroke() + ctx.restore() + } +} diff --git a/src/editor/core/draw/particle/SubscriptParticle.ts b/src/editor/core/draw/particle/SubscriptParticle.ts new file mode 100644 index 0000000..6922236 --- /dev/null +++ b/src/editor/core/draw/particle/SubscriptParticle.ts @@ -0,0 +1,23 @@ +import { IRowElement } from '../../../interface/Row' + +export class SubscriptParticle { + // 向下偏移字高的一半 + public getOffsetY(element: IRowElement): number { + return element.metrics.height / 2 + } + + public render( + ctx: CanvasRenderingContext2D, + element: IRowElement, + x: number, + y: number + ) { + ctx.save() + ctx.font = element.style + if (element.color) { + ctx.fillStyle = element.color + } + ctx.fillText(element.value, x, y + this.getOffsetY(element)) + ctx.restore() + } +} diff --git a/src/editor/core/draw/particle/SuperscriptParticle.ts b/src/editor/core/draw/particle/SuperscriptParticle.ts new file mode 100644 index 0000000..07d4316 --- /dev/null +++ b/src/editor/core/draw/particle/SuperscriptParticle.ts @@ -0,0 +1,23 @@ +import { IRowElement } from '../../../interface/Row' + +export class SuperscriptParticle { + // 向上偏移字高的一半 + public getOffsetY(element: IRowElement): number { + return -element.metrics.height / 2 + } + + public render( + ctx: CanvasRenderingContext2D, + element: IRowElement, + x: number, + y: number + ) { + ctx.save() + ctx.font = element.style + if (element.color) { + ctx.fillStyle = element.color + } + ctx.fillText(element.value, x, y + this.getOffsetY(element)) + ctx.restore() + } +} diff --git a/src/editor/core/draw/particle/TextParticle.ts b/src/editor/core/draw/particle/TextParticle.ts new file mode 100644 index 0000000..ec4cfc9 --- /dev/null +++ b/src/editor/core/draw/particle/TextParticle.ts @@ -0,0 +1,166 @@ +import { ElementType, IEditorOption, IElement, RenderMode } from '../../..' +import { + PUNCTUATION_LIST, + METRICS_BASIS_TEXT +} from '../../../dataset/constant/Common' +import { DeepRequired } from '../../../interface/Common' +import { IRowElement } from '../../../interface/Row' +import { ITextMetrics } from '../../../interface/Text' +import { Draw } from '../Draw' + +export interface IMeasureWordResult { + width: number + endElement: IElement +} + +export class TextParticle { + private draw: Draw + private options: DeepRequired + + private ctx: CanvasRenderingContext2D + private curX: number + private curY: number + private text: string + private curStyle: string + private curColor?: string + public cacheMeasureText: Map + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + this.ctx = draw.getCtx() + this.curX = -1 + this.curY = -1 + this.text = '' + this.curStyle = '' + this.cacheMeasureText = new Map() + } + + public measureBasisWord( + ctx: CanvasRenderingContext2D, + font: string + ): ITextMetrics { + ctx.save() + ctx.font = font + const textMetrics = this.measureText(ctx, { + value: METRICS_BASIS_TEXT + }) + ctx.restore() + return textMetrics + } + + public measureWord( + ctx: CanvasRenderingContext2D, + elementList: IElement[], + curIndex: number + ): IMeasureWordResult { + const LETTER_REG = this.draw.getLetterReg() + let width = 0 + let endElement: IElement = elementList[curIndex] + let i = curIndex + while (i < elementList.length) { + const element = elementList[i] + if ( + (element.type && element.type !== ElementType.TEXT) || + !LETTER_REG.test(element.value) + ) { + endElement = element + break + } + width += this.measureText(ctx, element).width + i++ + } + return { + width, + endElement + } + } + + public measurePunctuationWidth( + ctx: CanvasRenderingContext2D, + element: IElement + ): number { + if (!element || !PUNCTUATION_LIST.includes(element.value)) return 0 + return this.measureText(ctx, element).width + } + + public measureText( + ctx: CanvasRenderingContext2D, + element: IElement + ): ITextMetrics { + // 优先使用自定义字宽设置 + if (element.width) { + const textMetrics = ctx.measureText(element.value) + // TextMetrics是类无法解构 + return { + width: element.width, + actualBoundingBoxAscent: textMetrics.actualBoundingBoxAscent, + actualBoundingBoxDescent: textMetrics.actualBoundingBoxDescent, + actualBoundingBoxLeft: textMetrics.actualBoundingBoxLeft, + actualBoundingBoxRight: textMetrics.actualBoundingBoxRight, + fontBoundingBoxAscent: textMetrics.fontBoundingBoxAscent, + fontBoundingBoxDescent: textMetrics.fontBoundingBoxDescent + } + } + const id = `${element.value}${ctx.font}` + const cacheTextMetrics = this.cacheMeasureText.get(id) + if (cacheTextMetrics) { + return cacheTextMetrics + } + const textMetrics = ctx.measureText(element.value) + this.cacheMeasureText.set(id, textMetrics) + return textMetrics + } + + public complete() { + this._render() + this.text = '' + } + + public record( + ctx: CanvasRenderingContext2D, + element: IRowElement, + x: number, + y: number + ) { + this.ctx = ctx + // 兼容模式立即绘制 + if (this.options.renderMode === RenderMode.COMPATIBILITY) { + this._setCurXY(x, y) + this.text = element.value + this.curStyle = element.style + this.curColor = element.color + this.complete() + return + } + // 主动完成的重设起始点 + if (!this.text) { + this._setCurXY(x, y) + } + // 样式发生改变 + if ( + (this.curStyle && element.style !== this.curStyle) || + element.color !== this.curColor + ) { + this.complete() + this._setCurXY(x, y) + } + this.text += element.value + this.curStyle = element.style + this.curColor = element.color + } + + private _setCurXY(x: number, y: number) { + this.curX = x + this.curY = y + } + + private _render() { + if (!this.text || !~this.curX || !~this.curX) return + this.ctx.save() + this.ctx.font = this.curStyle + this.ctx.fillStyle = this.curColor || this.options.defaultColor + this.ctx.fillText(this.text, this.curX, this.curY) + this.ctx.restore() + } +} diff --git a/src/editor/core/draw/particle/block/BlockParticle.ts b/src/editor/core/draw/particle/block/BlockParticle.ts new file mode 100644 index 0000000..4915b14 --- /dev/null +++ b/src/editor/core/draw/particle/block/BlockParticle.ts @@ -0,0 +1,76 @@ +import { EDITOR_PREFIX } from '../../../../dataset/constant/Editor' +import { ElementType } from '../../../../dataset/enum/Element' +import { IRowElement } from '../../../../interface/Row' +import { Draw } from '../../Draw' +import { BaseBlock } from './modules/BaseBlock' + +export class BlockParticle { + private draw: Draw + private container: HTMLDivElement + private blockContainer: HTMLDivElement + private blockMap: Map + + constructor(draw: Draw) { + this.draw = draw + this.container = draw.getContainer() + this.blockMap = new Map() + this.blockContainer = this._createBlockContainer() + this.container.append(this.blockContainer) + } + + private _createBlockContainer(): HTMLDivElement { + const blockContainer = document.createElement('div') + blockContainer.classList.add(`${EDITOR_PREFIX}-block-container`) + return blockContainer + } + + public getDraw(): Draw { + return this.draw + } + + public getBlockContainer(): HTMLDivElement { + return this.blockContainer + } + + public render( + ctx: CanvasRenderingContext2D, + pageNo: number, + element: IRowElement, + x: number, + y: number + ) { + // 优先使用缓存block + const id = element.id! + let cacheBlock = this.blockMap.get(id) + if (!cacheBlock) { + cacheBlock = new BaseBlock(this, element) + cacheBlock.render() + this.blockMap.set(id, cacheBlock) + } + // 打印模式截图,其他模式更新位置 + if (this.draw.isPrintMode()) { + cacheBlock.snapshot(ctx, x, y) + } else { + cacheBlock.setClientRects(pageNo, x, y) + } + } + + public clear() { + if (!this.blockMap.size) return + const elementList = this.draw.getOriginalMainElementList() + const blockElementIds: string[] = [] + for (let e = 0; e < elementList.length; e++) { + const element = elementList[e] + if (element.type === ElementType.BLOCK) { + blockElementIds.push(element.id!) + } + } + this.blockMap.forEach(block => { + const id = block.getBlockElement().id! + if (!blockElementIds.includes(id)) { + block.remove() + this.blockMap.delete(id) + } + }) + } +} diff --git a/src/editor/core/draw/particle/block/modules/BaseBlock.ts b/src/editor/core/draw/particle/block/modules/BaseBlock.ts new file mode 100644 index 0000000..03035b4 --- /dev/null +++ b/src/editor/core/draw/particle/block/modules/BaseBlock.ts @@ -0,0 +1,280 @@ +import { EDITOR_PREFIX } from '../../../../../dataset/constant/Editor' +import { BlockType } from '../../../../../dataset/enum/Block' +import { IEditorOption } from '../../../../../interface/Editor' +import { IRowElement } from '../../../../../interface/Row' +import { Draw } from '../../../Draw' +import { BlockParticle } from '../BlockParticle' +import { IFrameBlock } from './IFrameBlock' +import { VideoBlock } from './VideoBlock' + +export class BaseBlock { + private draw: Draw + private options: Required + private element: IRowElement + private block: IFrameBlock | VideoBlock | null + private blockContainer: HTMLDivElement + private blockItem: HTMLDivElement + protected blockCache: Map + // 缩放业务 + private resizerMask: HTMLDivElement + private resizerSelection: HTMLDivElement + private resizerHandleList: HTMLDivElement[] + private width: number + private height: number + private mousedownX: number + private mousedownY: number + private curHandleIndex: number + private isAllowResize: boolean + + constructor(blockParticle: BlockParticle, element: IRowElement) { + this.draw = blockParticle.getDraw() + this.options = this.draw.getOptions() + this.blockContainer = blockParticle.getBlockContainer() + this.element = element + this.block = null + const { blockItem, resizerMask, resizerSelection, resizerHandleList } = + this._createBlockItem() + this.blockItem = blockItem + this.blockContainer.append(this.blockItem) + this.blockCache = new Map() + this.resizerMask = resizerMask + this.resizerSelection = resizerSelection + this.resizerHandleList = resizerHandleList + this.width = 0 + this.height = 0 + this.mousedownX = 0 + this.mousedownY = 0 + this.curHandleIndex = 0 + this.isAllowResize = false + } + + public getBlockElement(): IRowElement { + return this.element + } + + public getBlockWidth(): number { + return this.element.width || this.element.metrics.width + } + + private _createBlockItem() { + const { scale, resizerColor } = this.options + const blockItem = document.createElement('div') + blockItem.classList.add(`${EDITOR_PREFIX}-block-item`) + // 拖拽边框 + const resizerSelection = document.createElement('div') + resizerSelection.style.display = 'none' + resizerSelection.classList.add(`${EDITOR_PREFIX}-resizer-selection`) + resizerSelection.style.borderColor = resizerColor + resizerSelection.style.borderWidth = `${scale}px` + // 拖拽点 + const resizerHandleList: HTMLDivElement[] = [] + for (let i = 0; i < 8; i++) { + const handleDom = document.createElement('div') + handleDom.style.background = resizerColor + handleDom.classList.add(`resizer-handle`) + handleDom.classList.add(`handle-${i}`) + handleDom.setAttribute('data-index', String(i)) + handleDom.onmousedown = this._mousedown.bind(this) + resizerSelection.append(handleDom) + resizerHandleList.push(handleDom) + } + // 拖拽元素遮盖(不遮盖影响mouseup事件执行) + const resizerMask = document.createElement('div') + resizerMask.classList.add(`${EDITOR_PREFIX}-resizer-mask`) + resizerMask.style.display = 'none' + blockItem.append(resizerMask) + // 光标进入block时显示拖拽边框 + blockItem.onmouseenter = () => { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + const { width, height } = this.element.metrics + this._updateResizerRect(width, height) + resizerSelection.style.display = 'block' + } + // 光标离开block时隐藏拖拽边框 + blockItem.onmouseleave = () => { + if (this.isAllowResize) return + resizerSelection.style.display = 'none' + } + blockItem.append(resizerSelection) + return { + blockItem, + resizerMask, + resizerSelection, + resizerHandleList + } + } + + private _updateResizerRect(width: number, height: number) { + const { resizerSize: handleSize, scale } = this.options + this.resizerSelection.style.width = `${width}px` + this.resizerSelection.style.height = `${height}px` + for (let i = 0; i < 8; i++) { + const left = + i === 0 || i === 6 || i === 7 + ? -handleSize + : i === 1 || i === 5 + ? width / 2 + : width - handleSize + const top = + i === 0 || i === 1 || i === 2 + ? -handleSize + : i === 3 || i === 7 + ? height / 2 - handleSize + : height - handleSize + this.resizerHandleList[i].style.transform = `scale(${scale})` + this.resizerHandleList[i].style.left = `${left}px` + this.resizerHandleList[i].style.top = `${top}px` + } + } + + private _mousedown(evt: MouseEvent) { + const canvas = this.draw.getPage() + this.mousedownX = evt.x + this.mousedownY = evt.y + this.isAllowResize = true + const target = evt.target as HTMLDivElement + this.curHandleIndex = Number(target.dataset.index) + // 显示遮盖元素 + this.resizerMask.style.display = 'block' + // 改变光标样式 + const cursor = window.getComputedStyle(target).cursor + document.body.style.cursor = cursor + canvas.style.cursor = cursor + // 追加mousemove事件 + const mousemoveFn = this._mousemove.bind(this) + document.addEventListener('mousemove', mousemoveFn) + // 追加mouseup事件 + document.addEventListener( + 'mouseup', + () => { + this.element.width = Math.min(this.width, this.draw.getInnerWidth()) + this.element.height = this.height + this.isAllowResize = false + this.resizerSelection.style.display = 'none' + this.resizerMask.style.display = 'none' + document.removeEventListener('mousemove', mousemoveFn) + document.body.style.cursor = '' + canvas.style.cursor = 'text' + // 更新文档 + this.draw.render() + }, + { + once: true + } + ) + evt.preventDefault() + } + + private _mousemove(evt: MouseEvent) { + if (!this.isAllowResize) return + const { scale } = this.options + let dx = 0 + let dy = 0 + switch (this.curHandleIndex) { + case 0: + { + const offsetX = this.mousedownX - evt.x + const offsetY = this.mousedownY - evt.y + dx = Math.cbrt(offsetX ** 3 + offsetY ** 3) + dy = (this.element.height! * dx) / this.getBlockWidth() + } + break + case 1: + dy = this.mousedownY - evt.y + break + case 2: + { + const offsetX = evt.x - this.mousedownX + const offsetY = this.mousedownY - evt.y + dx = Math.cbrt(offsetX ** 3 + offsetY ** 3) + dy = (this.element.height! * dx) / this.getBlockWidth() + } + break + case 4: + { + const offsetX = evt.x - this.mousedownX + const offsetY = evt.y - this.mousedownY + dx = Math.cbrt(offsetX ** 3 + offsetY ** 3) + dy = (this.element.height! * dx) / this.getBlockWidth() + } + break + case 3: + dx = evt.x - this.mousedownX + break + case 5: + dy = evt.y - this.mousedownY + break + case 6: + { + const offsetX = this.mousedownX - evt.x + const offsetY = evt.y - this.mousedownY + dx = Math.cbrt(offsetX ** 3 + offsetY ** 3) + dy = (this.element.height! * dx) / this.getBlockWidth() + } + break + case 7: + dx = this.mousedownX - evt.x + break + } + // 图片实际宽高(变化大小除掉缩放比例) + const dw = this.getBlockWidth() + dx / scale + const dh = this.element.height! + dy / scale + if (dw <= 0 || dh <= 0) return + this.width = dw + this.height = dh + // 图片显示宽高 + const elementWidth = dw * scale + const elementHeight = dh * scale + // 更新预览包围框尺寸 + this._updateResizerRect(elementWidth, elementHeight) + this.blockItem.style.width = `${elementWidth}px` + this.blockItem.style.height = `${elementHeight}px` + evt.preventDefault() + } + + public snapshot(ctx: CanvasRenderingContext2D, x: number, y: number) { + const block = this.element.block! + if (block.type === BlockType.VIDEO) { + this.blockItem.style.display = 'none' + if (this.blockCache.has(this.element.id!)) { + const videoBlock = this.blockCache.get(this.element.id!) + videoBlock.snapshot(ctx, x, y) + } else { + this.block = new VideoBlock(this.element) + const promise = this.block.snapshot(ctx, x, y) + this.draw.getImageObserver().add(promise) + this.blockCache.set(this.element.id!, this.block) + } + } + } + + public render() { + const block = this.element.block! + if (block.type === BlockType.IFRAME) { + this.block = new IFrameBlock(this.element) + this.block.render(this.blockItem) + } else if (block.type === BlockType.VIDEO) { + this.block = new VideoBlock(this.element) + this.block.render(this.blockItem) + } + } + + public setClientRects(pageNo: number, x: number, y: number) { + const height = this.draw.getHeight() + const pageGap = this.draw.getPageGap() + const preY = pageNo * (height + pageGap) + // 尺寸 + const { metrics } = this.element + this.blockItem.style.display = 'block' + this.blockItem.style.width = `${metrics.width}px` + this.blockItem.style.height = `${metrics.height}px` + // 位置 + this.blockItem.style.left = `${x}px` + this.blockItem.style.top = `${preY + y}px` + } + + public remove() { + this.blockItem.remove() + } +} diff --git a/src/editor/core/draw/particle/block/modules/IFrameBlock.ts b/src/editor/core/draw/particle/block/modules/IFrameBlock.ts new file mode 100644 index 0000000..ca73d6f --- /dev/null +++ b/src/editor/core/draw/particle/block/modules/IFrameBlock.ts @@ -0,0 +1,41 @@ +import { IRowElement } from '../../../../../interface/Row' + +export class IFrameBlock { + public static readonly sandbox = ['allow-scripts', 'allow-same-origin'] + private element: IRowElement + + constructor(element: IRowElement) { + this.element = element + } + + private _defineIframeProperties(iframeWindow: Window) { + Object.defineProperties(iframeWindow, { + // 禁止获取parent避免安全漏洞 + parent: { + get: () => null + }, + // 用于区分上下文 + __POWERED_BY_CANVAS_EDITOR__: { + get: () => true + } + }) + } + + public render(blockItemContainer: HTMLDivElement) { + const block = this.element.block! + const iframe = document.createElement('iframe') + iframe.setAttribute('data-id', this.element.id!) + iframe.sandbox.add(...IFrameBlock.sandbox) + iframe.style.border = 'none' + iframe.style.width = '100%' + iframe.style.height = '100%' + if (block.iframeBlock?.src) { + iframe.src = block.iframeBlock.src + } else if (block.iframeBlock?.srcdoc) { + iframe.srcdoc = block.iframeBlock.srcdoc + } + blockItemContainer.append(iframe) + // 重新定义iframe上属性 + this._defineIframeProperties(iframe.contentWindow!) + } +} diff --git a/src/editor/core/draw/particle/block/modules/VideoBlock.ts b/src/editor/core/draw/particle/block/modules/VideoBlock.ts new file mode 100644 index 0000000..19db576 --- /dev/null +++ b/src/editor/core/draw/particle/block/modules/VideoBlock.ts @@ -0,0 +1,61 @@ +import { IRowElement } from '../../../../../interface/Row' + +export class VideoBlock { + private element: IRowElement + protected videoCache: Map + + constructor(element: IRowElement) { + this.element = element + this.videoCache = new Map() + } + + public snapshot(ctx: CanvasRenderingContext2D, x: number, y: number) { + return new Promise((resolve, reject) => { + const src = this.element.block?.videoBlock?.src || '' + if (this.videoCache.has(src)) { + const video = this.videoCache.get(src)! + ctx.drawImage( + video, + x, + y, + this.element.metrics.width, + this.element.metrics.height + ) + resolve(this.element) + } else { + const video = document.createElement('video') + video.src = src + video.muted = true + video.crossOrigin = 'anonymous' + video.onloadeddata = () => { + ctx.drawImage( + video, + x, + y, + this.element.metrics.width, + this.element.metrics.height + ) + this.videoCache.set(src, video) + resolve(this.element) + } + video.onerror = error => { + reject(error) + } + video.play().then(() => { + video.pause() + }) + } + }) + } + + public render(blockItemContainer: HTMLDivElement) { + const block = this.element.block! + const video = document.createElement('video') + video.style.width = '100%' + video.style.height = '100%' + video.style.objectFit = 'contain' + video.src = block.videoBlock?.src || '' + video.controls = true + blockItemContainer.append(video) + } +} diff --git a/src/editor/core/draw/particle/date/DateParticle.ts b/src/editor/core/draw/particle/date/DateParticle.ts new file mode 100644 index 0000000..f98f1d1 --- /dev/null +++ b/src/editor/core/draw/particle/date/DateParticle.ts @@ -0,0 +1,111 @@ +import { ElementType } from '../../../../dataset/enum/Element' +import { DeepRequired } from '../../../../interface/Common' +import { IEditorOption } from '../../../../interface/Editor' +import { IElement, IElementPosition } from '../../../../interface/Element' +import { formatElementContext } from '../../../../utils/element' +import { RangeManager } from '../../../range/RangeManager' +import { Draw } from '../../Draw' +import { DatePicker } from './DatePicker' + +export class DateParticle { + private draw: Draw + private range: RangeManager + private datePicker: DatePicker + private options: DeepRequired + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + this.range = draw.getRange() + this.datePicker = new DatePicker(draw, { + onSubmit: this._setValue.bind(this) + }) + } + + private _setValue(date: string) { + if (!date) return + const range = this.getDateElementRange() + if (!range) return + const [leftIndex, rightIndex] = range + const elementList = this.draw.getElementList() + const startElement = elementList[leftIndex + 1] + // 删除旧时间 + this.draw.spliceElementList( + elementList, + leftIndex + 1, + rightIndex - leftIndex + ) + this.range.setRange(leftIndex, leftIndex) + // 插入新时间 + const dateElement: IElement = { + type: ElementType.DATE, + value: '', + dateFormat: startElement.dateFormat, + valueList: [ + { + value: date + } + ] + } + formatElementContext(elementList, [dateElement], leftIndex, { + editorOptions: this.options + }) + this.draw.insertElementList([dateElement]) + } + + public getDateElementRange(): [number, number] | null { + let leftIndex = -1 + let rightIndex = -1 + const { startIndex, endIndex } = this.range.getRange() + if (!~startIndex && !~endIndex) return null + const elementList = this.draw.getElementList() + const startElement = elementList[startIndex] + if (startElement.type !== ElementType.DATE) return null + // 向左查找 + let preIndex = startIndex + while (preIndex >= 0) { + const preElement = elementList[preIndex] + if (preElement.dateId !== startElement.dateId) { + leftIndex = preIndex + break + } + preIndex-- + } + // 向右查找 + let nextIndex = startIndex + 1 + while (nextIndex < elementList.length) { + const nextElement = elementList[nextIndex] + if (nextElement.dateId !== startElement.dateId) { + rightIndex = nextIndex - 1 + break + } + nextIndex++ + } + // 控件在最后 + if (nextIndex === elementList.length) { + rightIndex = nextIndex - 1 + } + if (!~leftIndex || !~rightIndex) return null + return [leftIndex, rightIndex] + } + + public clearDatePicker() { + this.datePicker.dispose() + } + + public renderDatePicker(element: IElement, position: IElementPosition) { + const elementList = this.draw.getElementList() + const range = this.getDateElementRange() + const value = range + ? elementList + .slice(range[0] + 1, range[1] + 1) + .map(el => el.value) + .join('') + : '' + this.datePicker.render({ + value, + position, + dateFormat: element.dateFormat + }) + } +} diff --git a/src/editor/core/draw/particle/date/DatePicker.ts b/src/editor/core/draw/particle/date/DatePicker.ts new file mode 100644 index 0000000..3c6d87e --- /dev/null +++ b/src/editor/core/draw/particle/date/DatePicker.ts @@ -0,0 +1,588 @@ +import { + EDITOR_COMPONENT, + EDITOR_PREFIX +} from '../../../../dataset/constant/Editor' +import { EditorComponent } from '../../../../dataset/enum/Editor' +import { IElementPosition } from '../../../../interface/Element' +import { Draw } from '../../Draw' + +export interface IDatePickerLang { + now: string + confirm: string + return: string + timeSelect: string + weeks: { + sun: string + mon: string + tue: string + wed: string + thu: string + fri: string + sat: string + } + year: string + month: string + hour: string + minute: string + second: string +} + +export interface IDatePickerOption { + onSubmit?: (date: string) => any +} + +interface IDatePickerDom { + container: HTMLDivElement + dateWrap: HTMLDivElement + datePickerWeek: HTMLDivElement + timeWrap: HTMLUListElement + title: { + preYear: HTMLSpanElement + preMonth: HTMLSpanElement + now: HTMLSpanElement + nextMonth: HTMLSpanElement + nextYear: HTMLSpanElement + } + day: HTMLDivElement + time: { + hour: HTMLOListElement + minute: HTMLOListElement + second: HTMLOListElement + } + menu: { + time: HTMLButtonElement + now: HTMLButtonElement + submit: HTMLButtonElement + } +} + +interface IRenderOption { + value: string + position: IElementPosition + dateFormat?: string +} + +export class DatePicker { + private draw: Draw + private options: IDatePickerOption + private now: Date + private dom: IDatePickerDom + private renderOptions: IRenderOption | null + private isDatePicker: boolean + private pickDate: Date | null + private lang: IDatePickerLang + + constructor(draw: Draw, options: IDatePickerOption = {}) { + this.draw = draw + this.options = options + this.lang = this._getLang() + this.now = new Date() + this.dom = this._createDom() + this.renderOptions = null + this.isDatePicker = true + this.pickDate = null + this._bindEvent() + } + + private _createDom(): IDatePickerDom { + const datePickerContainer = document.createElement('div') + datePickerContainer.classList.add(`${EDITOR_PREFIX}-date-container`) + datePickerContainer.setAttribute(EDITOR_COMPONENT, EditorComponent.POPUP) + // title-切换年月、年月显示 + const dateWrap = document.createElement('div') + dateWrap.classList.add(`${EDITOR_PREFIX}-date-wrap`) + const datePickerTitle = document.createElement('div') + datePickerTitle.classList.add(`${EDITOR_PREFIX}-date-title`) + const preYearTitle = document.createElement('span') + preYearTitle.classList.add(`${EDITOR_PREFIX}-date-title__pre-year`) + preYearTitle.innerText = `<<` + const preMonthTitle = document.createElement('span') + preMonthTitle.classList.add(`${EDITOR_PREFIX}-date-title__pre-month`) + preMonthTitle.innerText = `<` + const nowTitle = document.createElement('span') + nowTitle.classList.add(`${EDITOR_PREFIX}-date-title__now`) + const nextMonthTitle = document.createElement('span') + nextMonthTitle.classList.add(`${EDITOR_PREFIX}-date-title__next-month`) + nextMonthTitle.innerText = `>` + const nextYearTitle = document.createElement('span') + nextYearTitle.classList.add(`${EDITOR_PREFIX}-date-title__next-year`) + nextYearTitle.innerText = `>>` + datePickerTitle.append(preYearTitle) + datePickerTitle.append(preMonthTitle) + datePickerTitle.append(nowTitle) + datePickerTitle.append(nextMonthTitle) + datePickerTitle.append(nextYearTitle) + // week-星期显示 + const datePickerWeek = document.createElement('div') + datePickerWeek.classList.add(`${EDITOR_PREFIX}-date-week`) + const { + weeks: { sun, mon, tue, wed, thu, fri, sat } + } = this.lang + const weekList = [sun, mon, tue, wed, thu, fri, sat] + weekList.forEach(week => { + const weekDom = document.createElement('span') + weekDom.innerText = `${week}` + datePickerWeek.append(weekDom) + }) + // day-天数显示 + const datePickerDay = document.createElement('div') + datePickerDay.classList.add(`${EDITOR_PREFIX}-date-day`) + // 日期内容构建 + dateWrap.append(datePickerTitle) + dateWrap.append(datePickerWeek) + dateWrap.append(datePickerDay) + // time-时间选择 + const timeWrap = document.createElement('ul') + timeWrap.classList.add(`${EDITOR_PREFIX}-time-wrap`) + let hourTime: HTMLOListElement + let minuteTime: HTMLOListElement + let secondTime: HTMLOListElement + const timeList = [this.lang.hour, this.lang.minute, this.lang.second] + timeList.forEach((t, i) => { + const li = document.createElement('li') + const timeText = document.createElement('span') + timeText.innerText = t + li.append(timeText) + const ol = document.createElement('ol') + const isHour = i === 0 + const isMinute = i === 1 + const endIndex = isHour ? 24 : 60 + for (let i = 0; i < endIndex; i++) { + const time = document.createElement('li') + time.innerText = `${String(i).padStart(2, '0')}` + time.setAttribute('data-id', `${i}`) + ol.append(time) + } + if (isHour) { + hourTime = ol + } else if (isMinute) { + minuteTime = ol + } else { + secondTime = ol + } + li.append(ol) + timeWrap.append(li) + }) + // menu-选择时间、现在、确定 + const datePickerMenu = document.createElement('div') + datePickerMenu.classList.add(`${EDITOR_PREFIX}-date-menu`) + const timeMenu = document.createElement('button') + timeMenu.classList.add(`${EDITOR_PREFIX}-date-menu__time`) + timeMenu.innerText = this.lang.timeSelect + const nowMenu = document.createElement('button') + nowMenu.classList.add(`${EDITOR_PREFIX}-date-menu__now`) + nowMenu.innerText = this.lang.now + const submitMenu = document.createElement('button') + submitMenu.classList.add(`${EDITOR_PREFIX}-date-menu__submit`) + submitMenu.innerText = this.lang.confirm + datePickerMenu.append(timeMenu) + datePickerMenu.append(nowMenu) + datePickerMenu.append(submitMenu) + // 构建 + datePickerContainer.append(dateWrap) + datePickerContainer.append(timeWrap) + datePickerContainer.append(datePickerMenu) + this.draw.getContainer().append(datePickerContainer) + return { + container: datePickerContainer, + dateWrap, + datePickerWeek, + timeWrap, + title: { + preYear: preYearTitle, + preMonth: preMonthTitle, + now: nowTitle, + nextMonth: nextMonthTitle, + nextYear: nextYearTitle + }, + day: datePickerDay, + time: { + hour: hourTime!, + minute: minuteTime!, + second: secondTime! + }, + menu: { + time: timeMenu, + now: nowMenu, + submit: submitMenu + } + } + } + + private _bindEvent() { + this.dom.title.preYear.onclick = () => { + this._preYear() + } + this.dom.title.preMonth.onclick = () => { + this._preMonth() + } + this.dom.title.nextMonth.onclick = () => { + this._nextMonth() + } + this.dom.title.nextYear.onclick = () => { + this._nextYear() + } + this.dom.menu.time.onclick = () => { + this.isDatePicker = !this.isDatePicker + this._toggleDateTimePicker() + } + this.dom.menu.now.onclick = () => { + this._now() + this._submit() + } + this.dom.menu.submit.onclick = () => { + this.dispose() + this._submit() + } + this.dom.time.hour.onclick = evt => { + if (!this.pickDate) return + const li = evt.target + const id = li.dataset.id + if (!id) return + this.pickDate.setHours(Number(id)) + this._setTimePick(false) + } + this.dom.time.minute.onclick = evt => { + if (!this.pickDate) return + const li = evt.target + const id = li.dataset.id + if (!id) return + this.pickDate.setMinutes(Number(id)) + this._setTimePick(false) + } + this.dom.time.second.onclick = evt => { + if (!this.pickDate) return + const li = evt.target + const id = li.dataset.id + if (!id) return + this.pickDate.setSeconds(Number(id)) + this._setTimePick(false) + } + } + + private _setPosition() { + if (!this.renderOptions) return + const { + position: { + coordinate: { + leftTop: [left, top] + }, + lineHeight, + pageNo + } + } = this.renderOptions + const height = this.draw.getHeight() + const pageGap = this.draw.getPageGap() + const currentPageNo = pageNo ?? this.draw.getPageNo() + const preY = currentPageNo * (height + pageGap) + // 位置 + this.dom.container.style.left = `${left}px` + this.dom.container.style.top = `${top + preY + lineHeight}px` + } + + public isInvalidDate(value: Date): boolean { + return value.toDateString() === 'Invalid Date' + } + + private _setValue() { + const value = this.renderOptions?.value + if (value) { + const setDate = new Date(value) + this.now = this.isInvalidDate(setDate) ? new Date() : setDate + } else { + this.now = new Date() + } + this.pickDate = new Date(this.now) + } + + private _getLang() { + const i18n = this.draw.getI18n() + const t = i18n.t.bind(i18n) + return { + now: t('datePicker.now'), + confirm: t('datePicker.confirm'), + return: t('datePicker.return'), + timeSelect: t('datePicker.timeSelect'), + weeks: { + sun: t('datePicker.weeks.sun'), + mon: t('datePicker.weeks.mon'), + tue: t('datePicker.weeks.tue'), + wed: t('datePicker.weeks.wed'), + thu: t('datePicker.weeks.thu'), + fri: t('datePicker.weeks.fri'), + sat: t('datePicker.weeks.sat') + }, + year: t('datePicker.year'), + month: t('datePicker.month'), + hour: t('datePicker.hour'), + minute: t('datePicker.minute'), + second: t('datePicker.second') + } + } + + private _setLangChange() { + this.dom.menu.time.innerText = this.lang.timeSelect + this.dom.menu.now.innerText = this.lang.now + this.dom.menu.submit.innerText = this.lang.confirm + const { + weeks: { sun, mon, tue, wed, thu, fri, sat } + } = this.lang + const weekList = [sun, mon, tue, wed, thu, fri, sat] + this.dom.datePickerWeek.childNodes.forEach((child, i) => { + const childElement = child + childElement.innerText = weekList[i] + }) + const hourTitle = this.dom.time.hour.previousElementSibling + hourTitle.innerText = this.lang.hour + const minuteTitle = ( + this.dom.time.minute.previousElementSibling + ) + minuteTitle.innerText = this.lang.minute + const secondTitle = ( + this.dom.time.second.previousElementSibling + ) + secondTitle.innerText = this.lang.second + } + + private _update() { + // 本地年月日 + const localDate = new Date() + const localYear = localDate.getFullYear() + const localMonth = localDate.getMonth() + 1 + const localDay = localDate.getDate() + // 选择年月日 + let pickYear: number | null = null + let pickMonth: number | null = null + let pickDay: number | null = null + if (this.pickDate) { + pickYear = this.pickDate.getFullYear() + pickMonth = this.pickDate.getMonth() + 1 + pickDay = this.pickDate.getDate() + } + // 当前年月日 + const year = this.now.getFullYear() + const month = this.now.getMonth() + 1 + this.dom.title.now.innerText = `${year}${this.lang.year} ${String( + month + ).padStart(2, '0')}${this.lang.month}` + // 日期补差 + const curDate = new Date(year, month, 0) // 当月日期 + const curDay = curDate.getDate() // 当月总天数 + let curWeek = new Date(year, month - 1, 1).getDay() // 当月第一天星期几 + if (curWeek === 0) { + curWeek = 7 + } + const preDay = new Date(year, month - 1, 0).getDate() // 上个月天数 + this.dom.day.innerHTML = '' + // 渲染上个月日期 + const preStartDay = preDay - curWeek + 1 + for (let i = preStartDay; i <= preDay; i++) { + const dayDom = document.createElement('div') + dayDom.classList.add('disable') + dayDom.innerText = `${i}` + dayDom.onclick = () => { + const newMonth = month - 2 + this.now = new Date(year, newMonth, i) + this._setDatePick(year, newMonth, i) + } + this.dom.day.append(dayDom) + } + // 渲染当月日期 + for (let i = 1; i <= curDay; i++) { + const dayDom = document.createElement('div') + if (localYear === year && localMonth === month && localDay === i) { + dayDom.classList.add('active') + } + if ( + this.pickDate && + pickYear === year && + pickMonth === month && + pickDay === i + ) { + dayDom.classList.add('select') + } + dayDom.innerText = `${i}` + dayDom.onclick = evt => { + const newMonth = month - 1 + this.now = new Date(year, newMonth, i) + this._setDatePick(year, newMonth, i) + evt.stopPropagation() + } + this.dom.day.append(dayDom) + } + // 渲染下月日期 + const nextEndDay = 6 * 7 - curWeek - curDay + for (let i = 1; i <= nextEndDay; i++) { + const dayDom = document.createElement('div') + dayDom.classList.add('disable') + dayDom.innerText = `${i}` + dayDom.onclick = () => { + this.now = new Date(year, month, i) + this._setDatePick(year, month, i) + } + this.dom.day.append(dayDom) + } + } + + private _toggleDateTimePicker() { + if (this.isDatePicker) { + this.dom.dateWrap.classList.add('active') + this.dom.timeWrap.classList.remove('active') + this.dom.menu.time.innerText = this.lang.timeSelect + } else { + this.dom.dateWrap.classList.remove('active') + this.dom.timeWrap.classList.add('active') + this.dom.menu.time.innerText = this.lang.return + // 设置时分秒选择 + this._setTimePick() + } + } + + private _setDatePick(year: number, month: number, day: number) { + this.now = new Date(year, month, day) + this.pickDate?.setFullYear(year) + this.pickDate?.setMonth(month) + this.pickDate?.setDate(day) + this._update() + } + + private _setTimePick(isIntoView = true) { + const hour = this.pickDate?.getHours() || 0 + const minute = this.pickDate?.getMinutes() || 0 + const second = this.pickDate?.getSeconds() || 0 + const { + hour: hourDom, + minute: minuteDom, + second: secondDom + } = this.dom.time + const timeDomList = [hourDom, minuteDom, secondDom] + // 清空 + timeDomList.forEach(timeDom => { + timeDom + .querySelectorAll('li') + .forEach(li => li.classList.remove('active')) + }) + const pickList: [HTMLOListElement, number][] = [ + [hourDom, hour], + [minuteDom, minute], + [secondDom, second] + ] + pickList.forEach(([dom, time]) => { + const pickDom = dom.querySelector(`[data-id='${time}']`)! + pickDom.classList.add('active') + if (isIntoView) { + this._scrollIntoView(dom, pickDom) + } + }) + } + + private _scrollIntoView(container: HTMLElement, selected: HTMLElement) { + if (!selected) { + container.scrollTop = 0 + return + } + const offsetParents: HTMLElement[] = [] + let pointer = selected.offsetParent + while (pointer && container !== pointer && container.contains(pointer)) { + offsetParents.push(pointer) + pointer = pointer.offsetParent + } + const top = + selected.offsetTop + + offsetParents.reduce((prev, curr) => prev + curr.offsetTop, 0) + const bottom = top + selected.offsetHeight + const viewRectTop = container.scrollTop + const viewRectBottom = viewRectTop + container.clientHeight + if (top < viewRectTop) { + container.scrollTop = top + } else if (bottom > viewRectBottom) { + container.scrollTop = bottom - container.clientHeight + } + } + + private _preMonth() { + this.now.setMonth(this.now.getMonth() - 1) + this._update() + } + + private _nextMonth() { + this.now.setMonth(this.now.getMonth() + 1) + this._update() + } + + private _preYear() { + this.now.setFullYear(this.now.getFullYear() - 1) + this._update() + } + + private _nextYear() { + this.now.setFullYear(this.now.getFullYear() + 1) + this._update() + } + + private _now() { + this.pickDate = new Date() + this.dispose() + } + + private _toggleVisible(isVisible: boolean) { + if (isVisible) { + this.dom.container.classList.add('active') + } else { + this.dom.container.classList.remove('active') + } + } + + private _submit() { + if (this.options.onSubmit && this.pickDate) { + const format = this.renderOptions?.dateFormat + const pickDateString = this.formatDate(this.pickDate, format) + this.options.onSubmit(pickDateString) + } + } + + public formatDate(date: Date, format = 'yyyy-MM-dd hh:mm:ss'): string { + let dateString = format + const dateOption = { + 'y+': date.getFullYear().toString(), + 'M+': (date.getMonth() + 1).toString(), + 'd+': date.getDate().toString(), + 'h+': date.getHours().toString(), + 'm+': date.getMinutes().toString(), + 's+': date.getSeconds().toString() + } + for (const k in dateOption) { + const reg = new RegExp('(' + k + ')').exec(format) + const key = k + if (reg) { + dateString = dateString.replace( + reg[1], + reg[1].length === 1 + ? dateOption[key] + : dateOption[key].padStart(reg[1].length, '0') + ) + } + } + return dateString + } + + public render(option: IRenderOption) { + this.renderOptions = option + this.lang = this._getLang() + this._setLangChange() + this._setValue() + this._update() + this._setPosition() + this.isDatePicker = true + this._toggleDateTimePicker() + this._toggleVisible(true) + } + + public dispose() { + this._toggleVisible(false) + } + + public destroy() { + this.dom.container.remove() + } +} diff --git a/src/editor/core/draw/particle/latex/LaTexParticle.ts b/src/editor/core/draw/particle/latex/LaTexParticle.ts new file mode 100644 index 0000000..bf33c43 --- /dev/null +++ b/src/editor/core/draw/particle/latex/LaTexParticle.ts @@ -0,0 +1,43 @@ +import { IElement } from '../../../../interface/Element' +import { ImageParticle } from '../ImageParticle' +import { LaTexSVG, LaTexUtils } from './utils/LaTexUtils' + +export class LaTexParticle extends ImageParticle { + public static convertLaTextToSVG(laTex: string): LaTexSVG { + return new LaTexUtils(laTex).svg({ + SCALE_X: 10, + SCALE_Y: 10, + MARGIN_X: 0, + MARGIN_Y: 0 + }) + } + + public render( + ctx: CanvasRenderingContext2D, + element: IElement, + x: number, + y: number + ) { + const { scale } = this.options + const width = element.width! * scale + const height = element.height! * scale + if (this.imageCache.has(element.value)) { + const img = this.imageCache.get(element.value)! + ctx.drawImage(img, x, y, width, height) + } else { + const laTexLoadPromise = new Promise((resolve, reject) => { + const img = new Image() + img.src = element.laTexSVG! + img.onload = () => { + ctx.drawImage(img, x, y, width, height) + this.imageCache.set(element.value, img) + resolve(element) + } + img.onerror = error => { + reject(error) + } + }) + this.addImageObserver(laTexLoadPromise) + } + } +} diff --git a/src/editor/core/draw/particle/latex/utils/LaTexUtils.ts b/src/editor/core/draw/particle/latex/utils/LaTexUtils.ts new file mode 100644 index 0000000..a29d802 --- /dev/null +++ b/src/editor/core/draw/particle/latex/utils/LaTexUtils.ts @@ -0,0 +1,1196 @@ +import { HERSHEY } from './hershey' +import { SYMB, Symb, asciiMap } from './symbols' + +const CONFIG: Record = { + SUB_SUP_SCALE: 0.5, + SQRT_MAG_SCALE: 0.5, + FRAC_SCALE: 0.85, + LINE_SPACING: 0.5, + FRAC_SPACING: 0.4 +} + +function tokenize(str: string): string[] { + str = str.replace(/\n/g, ' ') + let i = 0 + const tokens: string[] = [] + let curr = '' + while (i < str.length) { + if (str[i] == ' ') { + if (curr.length) { + tokens.push(curr) + curr = '' + } + } else if (str[i] == '\\') { + if (curr.length == 1 && curr[0] == '\\') { + curr += str[i] + tokens.push(curr) + curr = '' + } else { + if (curr.length) { + tokens.push(curr) + } + curr = str[i] + } + } else if (/[A-Za-z0-9\.]/.test(str[i])) { + curr += str[i] + } else { + if (curr.length && curr != '\\') { + tokens.push(curr) + curr = '' + } + curr += str[i] + tokens.push(curr) + curr = '' + } + i++ + } + if (curr.length) tokens.push(curr) + return tokens +} + +interface Bbox { + x: number + y: number + w: number + h: number +} + +interface Expr { + type: string + text: string + mode: string + chld: Expr[] + bbox: Bbox +} + +function parseAtom(x: string): Expr { + return { + type: SYMB[x] ? 'symb' : 'char', + mode: 'math', + text: x, + chld: [], + // @ts-ignore + bbox: null + } +} + +function parse(tokens: string[]): Expr { + let i = 0 + let expr: Expr = { + type: 'node', + text: '', + mode: 'math', + chld: [], + // @ts-ignore + bbox: null + } + + function takeOpt(): Expr | null { + if (tokens[i] != '[') { + return null + } + let lvl = 0 + let j = i + while (j < tokens.length) { + if (tokens[j] == '[') { + lvl++ + } else if (tokens[j] == ']') { + lvl-- + if (!lvl) { + break + } + } + j++ + } + const ret: Expr = parse(tokens.slice(i + 1, j)) + i = j + return ret + } + + function takeN(n: number): Expr[] { + let j: number = i + let j0: number = j + let lvl = 0 + let cnt = 0 + const ret: Expr[] = [] + while (j < tokens.length) { + if (tokens[j] == '{') { + if (!lvl) { + j0 = j + } + lvl++ + } else if (tokens[j] == '}') { + lvl-- + if (!lvl) { + ret.push(parse(tokens.slice(j0 + 1, j))) + cnt++ + if (cnt == n) { + break + } + } + } else { + if (lvl == 0) { + ret.push(parseAtom(tokens[j])) + cnt++ + if (cnt == n) { + break + } + } + } + j++ + } + i = j + return ret + } + + for (i = 0; i < tokens.length; i++) { + const s: Symb = SYMB[tokens[i]] + const e: Expr = { + type: '', + text: tokens[i], + mode: 'math', + chld: [], + // @ts-ignore + bbox: null + } + if (s) { + if (s.arity) { + i++ + e.type = 'func' + let opt: Expr | null = null + if (s.flags.opt) { + opt = takeOpt() + if (opt) i++ + } + const chld: Expr[] = takeN(s.arity) + e.chld = chld + if (opt) { + e.chld.push(opt) + } + } else { + e.type = 'symb' + } + } else { + if (tokens[i] == '{') { + e.type = 'node' + e.text = '' + e.chld = takeN(1) + } else { + e.type = 'char' + } + } + expr.chld.push(e) + } + if (expr.chld.length == 1) { + expr = expr.chld[0] + } + return expr +} + +function environments(exprs: Expr[]) { + let i = 0 + while (i < exprs.length) { + if (exprs[i].text == '\\begin') { + let j: number + for (j = i; j < exprs.length; j++) { + if (exprs[j].text == '\\end') { + break + } + } + const es: Expr[] = exprs.splice(i + 1, j - (i + 1)) + environments(es) + exprs[i].text = exprs[i].chld[0].text + exprs[i].chld = es + exprs.splice(i + 1, 1) + } + i++ + } +} + +function transform( + expr: Expr, + sclx: number, + scly: number, + x: number, + y: number, + notFirst?: boolean +) { + if (scly == null) { + scly = sclx + } + if (!expr.bbox) return + if (notFirst) { + expr.bbox.x *= sclx + expr.bbox.y *= scly + } + expr.bbox.w *= sclx + expr.bbox.h *= scly + for (let i = 0; i < expr.chld.length; i++) { + transform(expr.chld[i], sclx, scly, 0, 0, true) + } + expr.bbox.x += x + expr.bbox.y += y +} + +function computeBbox(exprs: Expr[]): Bbox { + let xmin = Infinity + let xmax = -Infinity + let ymin = Infinity + let ymax = -Infinity + for (let i = 0; i < exprs.length; i++) { + if (!exprs[i].bbox) { + continue + } + xmin = Math.min(xmin, exprs[i].bbox.x) + ymin = Math.min(ymin, exprs[i].bbox.y) + xmax = Math.max(xmax, exprs[i].bbox.x + exprs[i].bbox.w) + ymax = Math.max(ymax, exprs[i].bbox.y + exprs[i].bbox.h) + } + return { x: xmin, y: ymin, w: xmax - xmin, h: ymax - ymin } +} + +function group(exprs: Expr[]): Expr { + if (!exprs.length) { + // @ts-ignore + return null + } + const bbox: Bbox = computeBbox(exprs) + // console.log(exprs,bbox); + for (let i = 0; i < exprs.length; i++) { + if (!exprs[i].bbox) { + continue + } + exprs[i].bbox.x -= bbox.x + exprs[i].bbox.y -= bbox.y + } + const expr: Expr = { + type: 'node', + text: '', + mode: 'math', + chld: exprs, + bbox + } + return expr +} + +function align(exprs: Expr[], alignment = 'center'): void { + for (let i = 0; i < exprs.length; i++) { + if (exprs[i].text == '^' || exprs[i].text == '\'') { + let h = 0 + let j = i + while ( + j > 0 && + (exprs[j].text == '^' || exprs[j].text == '_' || exprs[j].text == '\'') + ) { + j-- + } + h = exprs[j].bbox.y + if (exprs[i].text == '\'') { + exprs[i].bbox.y = h + } else { + // @ts-ignore + transform(exprs[i], CONFIG.SUB_SUP_SCALE, null, 0, 0) + if (SYMB[exprs[j].text] && SYMB[exprs[j].text].flags.big) { + exprs[i].bbox.y = h - exprs[i].bbox.h + } else if (exprs[j].text == '\\int') { + exprs[i].bbox.y = h + } else { + exprs[i].bbox.y = h - exprs[i].bbox.h / 2 + } + } + } else if (exprs[i].text == '_') { + let h = 1 + let j = i + while ( + j > 0 && + (exprs[j].text == '^' || exprs[j].text == '_' || exprs[j].text == '\'') + ) { + j-- + } + h = exprs[j].bbox.y + exprs[j].bbox.h + // @ts-ignore + transform(exprs[i], CONFIG.SUB_SUP_SCALE, null, 0, 0) + if (SYMB[exprs[j].text] && SYMB[exprs[j].text].flags.big) { + exprs[i].bbox.y = h + } else if (exprs[j].text == '\\int') { + exprs[i].bbox.y = h - exprs[i].bbox.h + } else { + exprs[i].bbox.y = h - exprs[i].bbox.h / 2 + } + } + } + function searchHigh( + i: number, + l: string, + r: string, + dir: number, + lvl0: number + ): number[] { + let j = i + let lvl = lvl0 + let ymin = Infinity + let ymax = -Infinity + while (dir > 0 ? j < exprs.length : j >= 0) { + if (exprs[j].text == l) { + lvl++ + } else if (exprs[j].text == r) { + lvl-- + if (lvl == 0) { + break + } + } else if (exprs[j].text == '^' || exprs[j].text == '_') { + //skip + } else if (exprs[j].bbox) { + ymin = Math.min(ymin, exprs[j].bbox.y) + ymax = Math.max(ymax, exprs[j].bbox.y + exprs[j].bbox.h) + } + j += dir + } + return [ymin, ymax] + } + for (let i = 0; i < exprs.length; i++) { + if (exprs[i].text == '\\left') { + const [ymin, ymax] = searchHigh(i, '\\left', '\\right', 1, 0) + if (ymin != Infinity && ymax != -Infinity) { + exprs[i].bbox.y = ymin + transform(exprs[i], 1, (ymax - ymin) / exprs[i].bbox.h, 0, 0) + } + } else if (exprs[i].text == '\\right') { + const [ymin, ymax] = searchHigh(i, '\\right', '\\left', -1, 0) + if (ymin != Infinity && ymax != -Infinity) { + exprs[i].bbox.y = ymin + transform(exprs[i], 1, (ymax - ymin) / exprs[i].bbox.h, 0, 0) + } + } else if (exprs[i].text == '\\middle') { + const [lmin, lmax] = searchHigh(i, '\\right', '\\left', -1, 1) + const [rmin, rmax] = searchHigh(i, '\\left', '\\right', 1, 1) + const ymin = Math.min(lmin, rmin) + const ymax = Math.max(lmax, rmax) + if (ymin != Infinity && ymax != -Infinity) { + exprs[i].bbox.y = ymin + transform(exprs[i], 1, (ymax - ymin) / exprs[i].bbox.h, 0, 0) + } + } + } + + if (!exprs.some(x => x.text == '&' || x.text == '\\\\')) { + return + } + + const rows: Expr[][][] = [] + let row: Expr[][] = [] + let cell: Expr[] = [] + + for (let i = 0; i < exprs.length; i++) { + if (exprs[i].text == '&') { + row.push(cell) + cell = [] + } else if (exprs[i].text == '\\\\') { + if (cell.length) { + row.push(cell) + cell = [] + } + rows.push(row) + row = [] + } else { + cell.push(exprs[i]) + } + } + if (cell.length) { + row.push(cell) + } + if (row.length) { + rows.push(row) + } + const colws: number[] = [] + const erows: Expr[][] = [] + for (let i = 0; i < rows.length; i++) { + const erow: Expr[] = [] + for (let j = 0; j < rows[i].length; j++) { + const e: Expr = group(rows[i][j]) + if (e) { + colws[j] = colws[j] || 0 + colws[j] = Math.max(e.bbox.w + 1, colws[j]) + } + erow[j] = e + } + erows.push(erow) + } + + const ybds: number[][] = [] + for (let i = 0; i < erows.length; i++) { + let ymin = Infinity + let ymax = -Infinity + for (let j = 0; j < erows[i].length; j++) { + if (!erows[i][j]) { + continue + } + ymin = Math.min(ymin, erows[i][j].bbox.y) + ymax = Math.max(ymax, erows[i][j].bbox.y + erows[i][j].bbox.h) + } + ybds.push([ymin, ymax]) + } + + for (let i = 0; i < ybds.length; i++) { + if (ybds[i][0] == Infinity || ybds[i][1] == Infinity) { + ybds[i][0] = i == 0 ? 0 : ybds[i - 1][1] + ybds[i][1] = ybds[i][0] + 2 + } + } + + for (let i = 1; i < erows.length; i++) { + const shft = ybds[i - 1][1] - ybds[i][0] + CONFIG.LINE_SPACING + for (let j = 0; j < erows[i].length; j++) { + if (erows[i][j]) { + erows[i][j].bbox.y += shft + } + } + ybds[i][0] += shft + ybds[i][1] += shft + } + + exprs.splice(0, exprs.length) + for (let i = 0; i < erows.length; i++) { + let dx = 0 + for (let j = 0; j < erows[i].length; j++) { + const e: Expr = erows[i][j] + if (!e) { + dx += colws[j] + continue + } + e.bbox.x += dx + dx += colws[j] - e.bbox.w + // e.bbox.w = colws[j]; + if (alignment == 'center') { + e.bbox.x += (colws[j] - e.bbox.w) / 2 + } else if (alignment == 'left') { + //ok + } else if (alignment == 'right') { + e.bbox.x += colws[j] - e.bbox.w + } else if (alignment == 'equation') { + if (j != erows[i].length - 1) { + e.bbox.x += colws[j] - e.bbox.w + } + } + exprs.push(e) + } + } +} + +function plan(expr: Expr, mode = 'math'): void { + const tmd: string = + { + '\\text': 'text', + '\\mathnormal': 'math', + '\\mathrm': 'rm', + '\\mathit': 'it', + '\\mathbf': 'bf', + '\\mathsf': 'sf', + '\\mathtt': 'tt', + '\\mathfrak': 'frak', + '\\mathcal': 'cal', + '\\mathbb': 'bb', + '\\mathscr': 'scr', + '\\rm': 'rm', + '\\it': 'it', + '\\bf': 'bf', + '\\sf': 'tt', + '\\tt': 'tt', + '\\frak': 'frak', + '\\cal': 'cal', + '\\bb': 'bb', + '\\scr': 'scr' + }[expr.text] ?? mode + if (!expr.chld.length) { + if (SYMB[expr.text]) { + if (SYMB[expr.text].flags.big) { + if (expr.text == '\\lim') { + expr.bbox = { x: 0, y: 0, w: 3.5, h: 2 } + } else { + expr.bbox = { x: 0, y: -0.5, w: 3, h: 3 } + } + } else if (SYMB[expr.text].flags.txt) { + let w = 0 + for (let i = 1; i < expr.text.length; i++) { + w += HERSHEY(asciiMap(expr.text[i], 'text')).w + } + w /= 16 + expr.bbox = { x: 0, y: 0, w: w, h: 2 } + } else if (SYMB[expr.text].glyph) { + let w = HERSHEY(SYMB[expr.text].glyph).w + w /= 16 + if (expr.text == '\\int' || expr.text == '\\oint') { + expr.bbox = { x: 0, y: -1.5, w: w, h: 5 } + } else { + expr.bbox = { x: 0, y: 0, w: w, h: 2 } + } + } else { + expr.bbox = { x: 0, y: 0, w: 1, h: 2 } + } + } else { + let w = 0 + for (let i = 0; i < expr.text.length; i++) { + if (!HERSHEY(asciiMap(expr.text[i], tmd))) { + continue + } + if (tmd == 'tt') { + w += 16 + } else { + w += HERSHEY(asciiMap(expr.text[i], tmd)).w + } + } + w /= 16 + expr.bbox = { x: 0, y: 0, w: w, h: 2 } + } + expr.mode = tmd + return + } + if (expr.text == '\\frac') { + const a: Expr = expr.chld[0] + const b: Expr = expr.chld[1] + const s: number = CONFIG.FRAC_SCALE + plan(a) + plan(b) + a.bbox.x = 0 + a.bbox.y = 0 + b.bbox.x = 0 + b.bbox.y = 0 + const mw: number = Math.max(a.bbox.w, b.bbox.w) * s + // @ts-ignore + transform(a, s, null, (mw - a.bbox.w * s) / 2, 0) + transform( + b, + s, + // @ts-ignore + null, + (mw - b.bbox.w * s) / 2, + a.bbox.h + CONFIG.FRAC_SPACING + ) + expr.bbox = { + x: 0, + y: -a.bbox.h + 1 - CONFIG.FRAC_SPACING / 2, + w: mw, + h: a.bbox.h + b.bbox.h + CONFIG.FRAC_SPACING + } + } else if (expr.text == '\\binom') { + const a: Expr = expr.chld[0] + const b: Expr = expr.chld[1] + plan(a) + plan(b) + a.bbox.x = 0 + a.bbox.y = 0 + b.bbox.x = 0 + b.bbox.y = 0 + const mw: number = Math.max(a.bbox.w, b.bbox.w) + // @ts-ignore + transform(a, 1, null, (mw - a.bbox.w) / 2 + 1, 0) + // @ts-ignore + transform(b, 1, null, (mw - b.bbox.w) / 2 + 1, a.bbox.h) + expr.bbox = { x: 0, y: -a.bbox.h + 1, w: mw + 2, h: a.bbox.h + b.bbox.h } + } else if (expr.text == '\\sqrt') { + const e: Expr = expr.chld[0] + plan(e) + const f: Expr = expr.chld[1] + let pl = 0 + if (f) { + plan(f) + pl = Math.max(f.bbox.w * CONFIG.SQRT_MAG_SCALE - 0.5, 0) + // @ts-ignore + transform(f, CONFIG.SQRT_MAG_SCALE, null, 0, 0.5) + } + // @ts-ignore + transform(e, 1, null, 1 + pl, 0.5) + expr.bbox = { + x: 0, + y: 2 - e.bbox.h - 0.5, + w: e.bbox.w + 1 + pl, + h: e.bbox.h + 0.5 + } + } else if (SYMB[expr.text] && SYMB[expr.text].flags.hat) { + const e: Expr = expr.chld[0] + plan(e) + const y0 = e.bbox.y - 0.5 + e.bbox.y = 0.5 + expr.bbox = { x: 0, y: y0, w: e.bbox.w, h: e.bbox.h + 0.5 } + } else if (SYMB[expr.text] && SYMB[expr.text].flags.mat) { + const e: Expr = expr.chld[0] + plan(e) + expr.bbox = { x: 0, y: 0, w: e.bbox.w, h: e.bbox.h + 0.5 } + } else { + let dx = 0 + let dy = 0 + let mh = 1 + for (let i = 0; i < expr.chld.length; i++) { + const c: Expr = expr.chld[i] + // @ts-ignore + const spac: number = + { + '\\quad': 2, + '\\,': (2 * 3) / 18, + '\\:': (2 * 4) / 18, + '\\;': (2 * 5) / 18, + '\\!': (2 * -3) / 18 + }[c.text] ?? null + + if (c.text == '\\\\') { + dy += mh + dx = 0 + mh = 1 + continue + } else if (c.text == '&') { + continue + } else if (spac != null) { + dx += spac + continue + } else { + plan(c, tmd) + // @ts-ignore + transform(c, 1, null, dx, dy) + if (c.text == '^' || c.text == '_' || c.text == '\'') { + let j: number = i + while ( + j > 0 && + (expr.chld[j].text == '^' || + expr.chld[j].text == '_' || + expr.chld[j].text == '\'') + ) { + j-- + } + const wasBig = + SYMB[expr.chld[j].text] && SYMB[expr.chld[j].text].flags.big + if (c.text == '\'') { + let k = j + 1 + let nth = 0 + while (k < i) { + if (expr.chld[k].text == '\'') { + nth++ + } + k++ + } + c.bbox.x = + expr.chld[j].bbox.x + expr.chld[j].bbox.w + c.bbox.w * nth + dx = Math.max(dx, c.bbox.x + c.bbox.w) + } else { + if (wasBig) { + const ex = + expr.chld[j].bbox.x + + (expr.chld[j].bbox.w - c.bbox.w * CONFIG.SUB_SUP_SCALE) / 2 + c.bbox.x = ex + dx = Math.max( + dx, + expr.chld[j].bbox.x + + expr.chld[j].bbox.w + + (c.bbox.w * CONFIG.SUB_SUP_SCALE - expr.chld[j].bbox.w) / 2 + ) + } else { + c.bbox.x = expr.chld[j].bbox.x + expr.chld[j].bbox.w + dx = Math.max(dx, c.bbox.x + c.bbox.w * CONFIG.SUB_SUP_SCALE) + } + } + } else { + dx += c.bbox.w + } + if (mode == 'text') { + dx += 1 + } + mh = Math.max(c.bbox.y + c.bbox.h - dy, mh) + } + } + dy += mh + const m2s: Record = { + bmatrix: ['[', ']'], + pmatrix: ['(', ')'], + Bmatrix: ['\\{', '\\}'], + cases: ['\\{'] + } + const alt: string = + { + bmatrix: 'center', + pmatrix: 'center', + Bmatrix: 'center', + cases: 'left', + matrix: 'center', + aligned: 'equation' + }[expr.text] ?? 'left' + + const hasLp = !!m2s[expr.text] + const hasRp = !!m2s[expr.text] && m2s[expr.text].length > 1 + + align(expr.chld, alt) + const bb = computeBbox(expr.chld) + if (expr.text == '\\text') { + bb.x -= 1 + bb.w += 2 + } + + for (let i = 0; i < expr.chld.length; i++) { + // @ts-ignore + transform(expr.chld[i], 1, null, -bb.x + (hasLp ? 1.5 : 0), -bb.y) + } + expr.bbox = { + x: 0, + y: 0, + w: bb.w + 1.5 * Number(hasLp) + 1.5 * Number(hasRp), + h: bb.h + } + + if (hasLp) { + expr.chld.unshift({ + type: 'symb', + text: m2s[expr.text][0], + mode: expr.mode, + chld: [], + bbox: { x: 0, y: 0, w: 1, h: bb.h } + }) + } + if (hasRp) { + expr.chld.push({ + type: 'symb', + text: m2s[expr.text][1], + mode: expr.mode, + chld: [], + bbox: { x: bb.w + 2, y: 0, w: 1, h: bb.h } + }) + } + if (hasLp || hasRp || expr.text == 'matrix') { + expr.type = 'node' + expr.text = '' + expr.bbox.y -= (expr.bbox.h - 2) / 2 + } + } +} + +function flatten(expr: Expr) { + function flat(expr: Expr, dx: number, dy: number): Expr[] { + const ff: Expr[] = [] + if (expr.bbox) { + dx += expr.bbox.x + dy += expr.bbox.y + if (expr.text == '\\frac') { + const h: number = + expr.chld[1].bbox.y - (expr.chld[0].bbox.y + expr.chld[0].bbox.h) + const e: Expr = { + type: 'symb', + mode: expr.mode, + text: '\\bar', + bbox: { + x: dx, + y: dy + (expr.chld[1].bbox.y - h / 2) - h / 2, + w: expr.bbox.w, + h: h + }, + chld: [] + } + ff.push(e) + } else if (expr.text == '\\sqrt') { + const h: number = expr.chld[0].bbox.y + const xx: number = Math.max( + 0, + expr.chld[0].bbox.x - expr.chld[0].bbox.h / 2 + ) + const e: Expr = { + type: 'symb', + mode: expr.mode, + text: '\\sqrt', + bbox: { + x: dx + xx, + y: dy + h / 2, + w: expr.chld[0].bbox.x - xx, + h: expr.bbox.h - h / 2 + }, + chld: [] + } + ff.push(e) + ff.push({ + type: 'symb', + text: '\\bar', + mode: expr.mode, + bbox: { + x: dx + expr.chld[0].bbox.x, + y: dy, + w: expr.bbox.w - expr.chld[0].bbox.x, + h: h + }, + chld: [] + }) + } else if (expr.text == '\\binom') { + const w = Math.min(expr.chld[0].bbox.x, expr.chld[1].bbox.x) + const e: Expr = { + type: 'symb', + mode: expr.mode, + text: '(', + bbox: { + x: dx, + y: dy, + w: w, + h: expr.bbox.h + }, + chld: [] + } + ff.push(e) + ff.push({ + type: 'symb', + text: ')', + mode: expr.mode, + bbox: { + x: dx + expr.bbox.w - w, + y: dy, + w: w, + h: expr.bbox.h + }, + chld: [] + }) + } else if (SYMB[expr.text] && SYMB[expr.text].flags.hat) { + const h: number = expr.chld[0].bbox.y + const e: Expr = { + type: 'symb', + mode: expr.mode, + text: expr.text, + bbox: { + x: dx, + y: dy, + w: expr.bbox.w, + h: h + }, + chld: [] + } + ff.push(e) + } else if (SYMB[expr.text] && SYMB[expr.text].flags.mat) { + const h: number = expr.chld[0].bbox.h + const e: Expr = { + type: 'symb', + text: expr.text, + mode: expr.mode, + bbox: { + x: dx, + y: dy + h, + w: expr.bbox.w, + h: expr.bbox.h - h + }, + chld: [] + } + ff.push(e) + } else if (expr.type != 'node' && expr.text != '^' && expr.text != '_') { + const e: Expr = { + type: expr.type == 'func' ? 'symb' : expr.type, + text: expr.text, + mode: expr.mode, + bbox: { + x: dx, + y: dy, + w: expr.bbox.w, + h: expr.bbox.h + }, + chld: [] + } + ff.push(e) + } + } + for (let i = 0; i < expr.chld.length; i++) { + const f = flat(expr.chld[i], dx, dy) + ff.push(...f) + } + return ff + } + const f = flat(expr, -expr.bbox.x, -expr.bbox.y) + expr.type = 'node' + expr.text = '' + expr.chld = f +} + +function render(expr: Expr): number[][][] { + const o: number[][][] = [] + for (let i = 0; i < expr.chld.length; i++) { + const e: Expr = expr.chld[i] + let s = e.bbox.h / 2 + let isSmallHat = false + if ( + SYMB[e.text] && + SYMB[e.text].flags.hat && + !SYMB[e.text].flags.xfl && + !SYMB[e.text].flags.yfl + ) { + s *= 4 + isSmallHat = true + } + if (SYMB[e.text] && SYMB[e.text].glyph) { + const d = HERSHEY(SYMB[e.text].glyph) + for (let j = 0; j < d.polylines.length; j++) { + const l: number[][] = [] + + for (let k = 0; k < d.polylines[j].length; k++) { + let x = d.polylines[j][k][0] + let y = d.polylines[j][k][1] + + if (SYMB[e.text].flags.xfl) { + x = ((x - d.xmin) / Math.max(d.xmax - d.xmin, 1)) * e.bbox.w + x += e.bbox.x + } else if ((d.w / 16) * s > e.bbox.w) { + x = (x / Math.max(d.w, 1)) * e.bbox.w + x += e.bbox.x + } else { + x = (x / 16) * s + const p = (e.bbox.w - (d.w / 16) * s) / 2 + x += e.bbox.x + p + } + if (SYMB[e.text].flags.yfl) { + y = ((y - d.ymin) / Math.max(d.ymax - d.ymin, 1)) * e.bbox.h + y += e.bbox.y + } else { + y = (y / 16) * s + if (isSmallHat) { + const p = (d.ymax + d.ymin) / 2 + y -= (p / 16) * s + } + y += e.bbox.y + e.bbox.h / 2 + } + l.push([x, y]) + } + o.push(l) + } + } else if ((SYMB[e.text] && SYMB[e.text].flags.txt) || e.type == 'char') { + let x0 = e.bbox.x + const isVerb = !!(SYMB[e.text] && SYMB[e.text].flags.txt) + for (let n = Number(isVerb); n < e.text.length; n++) { + const d = HERSHEY(asciiMap(e.text[n], isVerb ? 'text' : e.mode)) + if (!d) { + console.warn('unmapped character: ' + e.text[n]) + continue + } + for (let j = 0; j < d.polylines.length; j++) { + const l: number[][] = [] + for (let k = 0; k < d.polylines[j].length; k++) { + let x = d.polylines[j][k][0] + let y = d.polylines[j][k][1] + x /= 16 + y /= 16 + x *= s + y *= s + if (e.mode == 'tt') { + if (d.w > 16) { + x *= 16 / d.w + } else { + x += (16 - d.w) / 2 / 16 + } + } + x += x0 + y += e.bbox.y + e.bbox.h / 2 + l.push([x, y]) + } + o.push(l) + } + if (e.mode == 'tt') { + x0 += s + } else { + x0 += (d.w / 16) * s + } + } + } + } + return o +} + +interface ExportOpt { + MIN_CHAR_H?: number + MAX_W?: number + MAX_H?: number + MARGIN_X?: number + MARGIN_Y?: number + SCALE_X?: number + SCALE_Y?: number + STROKE_W?: number + FG_COLOR?: string + BG_COLOR?: string +} + +function nf(x: number): number { + return Math.round(x * 100) / 100 +} + +export interface LaTexSVG { + svg: string + width: number + height: number +} + +export class LaTexUtils { + _latex: string + _tree: Expr + _tokens: string[] + _polylines: number[][][] + + constructor(latex: string) { + this._latex = latex + this._tokens = tokenize(latex) + this._tree = parse(this._tokens) + environments(this._tree.chld) + plan(this._tree) + flatten(this._tree) + this._polylines = render(this._tree) + } + + private resolveScale(opt?: ExportOpt): number[] { + if (opt == undefined) { + return [16, 16, 16, 16] + } + let sclx: number = opt.SCALE_X ?? 16 + let scly: number = opt.SCALE_Y ?? 16 + + if (opt.MIN_CHAR_H != undefined) { + let mh = 0 + for (let i = 0; i < this._tree.chld.length; i++) { + const c: Expr = this._tree.chld[i] + if ( + c.type == 'char' || + (SYMB[c.text] && + (SYMB[c.text].flags.txt || !Object.keys(SYMB[c.text].flags).length)) + ) { + mh = Math.min(c.bbox.h, mh) + } + } + const s: number = Math.max(1, opt.MIN_CHAR_H / mh) + sclx *= s + scly *= s + } + if (opt.MAX_W != undefined) { + const s0 = sclx + sclx = Math.min(sclx, opt.MAX_W / this._tree.bbox.w) + scly *= sclx / s0 + } + if (opt.MAX_H != undefined) { + const s0 = scly + scly = Math.min(scly, opt.MAX_H / this._tree.bbox.h) + sclx *= scly / s0 + } + const px: number = opt.MARGIN_X ?? sclx + const py: number = opt.MARGIN_Y ?? scly + return [px, py, sclx, scly] + } + + polylines(opt?: ExportOpt): number[][][] { + if (!opt) opt = {} + const polylines: number[][][] = [] + const [px, py, sclx, scly] = this.resolveScale(opt) + for (let i = 0; i < this._polylines.length; i++) { + polylines.push([]) + for (let j = 0; j < this._polylines[i].length; j++) { + const [x, y] = this._polylines[i][j] + polylines[polylines.length - 1].push([px + x * sclx, py + y * scly]) + } + } + return polylines + } + + pathd(opt?: ExportOpt): string { + if (!opt) opt = {} + let d = '' + const [px, py, sclx, scly] = this.resolveScale(opt) + for (let i = 0; i < this._polylines.length; i++) { + for (let j = 0; j < this._polylines[i].length; j++) { + const [x, y] = this._polylines[i][j] + d += !j ? 'M' : 'L' + d += `${nf(px + x * sclx)} ${nf(py + y * scly)}` + } + } + return d + } + + svg(opt: ExportOpt): LaTexSVG { + if (!opt) opt = {} + const [px, py, sclx, scly] = this.resolveScale(opt) + const w = nf(this._tree.bbox.w * sclx + px * 2) + const h = nf(this._tree.bbox.h * scly + py * 2) + let o = `` + if (opt.BG_COLOR) { + o += `` + } + o += `` + o += `` + return { + svg: `data:image/svg+xml;base64,${window.btoa(o)}`, + width: Math.ceil(w), + height: Math.ceil(h) + } + } + + pdf(opt: ExportOpt): string { + if (!opt) opt = {} + const [px, py, sclx, scly] = this.resolveScale(opt) + + const width = nf(this._tree.bbox.w * sclx + px * 2) + const height = nf(this._tree.bbox.h * scly + py * 2) + let head = `%PDF-1.1\n%%¥±ë\n1 0 obj\n<< /Type /Catalog\n/Pages 2 0 R\n>>endobj + 2 0 obj\n<< /Type /Pages\n/Kids [3 0 R]\n/Count 1\n/MediaBox [0 0 ${width} ${height}]\n>>\nendobj + 3 0 obj\n<< /Type /Page\n/Parent 2 0 R\n/Resources\n<< /Font\n<< /F1\n<< /Type /Font + /Subtype /Type1\n/BaseFont /Times-Roman\n>>\n>>\n>>\n/Contents [` + let pdf = '' + let count = 4 + for (let i = 0; i < this._polylines.length; i++) { + pdf += `${count} 0 obj \n<< /Length 0 >>\n stream\n 1 j 1 J ${ + opt.STROKE_W ?? 1 + } w\n` + for (let j = 0; j < this._polylines[i].length; j++) { + const [x, y] = this._polylines[i][j] + pdf += `${nf(px + x * sclx)} ${nf(height - (py + y * scly))} ${ + j ? 'l' : 'm' + } ` + } + pdf += '\nS\nendstream\nendobj\n' + head += `${count} 0 R ` + count++ + } + head += ']\n>>\nendobj\n' + pdf += '\ntrailer\n<< /Root 1 0 R \n /Size 0\n >>startxref\n\n%%EOF\n' + return head + pdf + } + + boxes(opt: ExportOpt): Bbox[] { + if (!opt) opt = {} + const [px, py, sclx, scly] = this.resolveScale(opt) + const bs: Bbox[] = [] + for (let i = 0; i < this._tree.chld.length; i++) { + const { x, y, w, h } = this._tree.chld[i].bbox + bs.push({ x: px + x * sclx, y: py + y * scly, w: w * sclx, h: h * scly }) + } + return bs + } + + box(opt: ExportOpt): Bbox { + if (!opt) opt = {} + const [px, py, sclx, scly] = this.resolveScale(opt) + return { + x: px + this._tree.bbox.x * sclx, + y: py + this._tree.bbox.y * scly, + w: this._tree.bbox.w * sclx, + h: this._tree.bbox.h * scly + } + } +} + +const _impl: Record = { + tokenize, + parse, + environments, + plan, + flatten, + render +} + +export { CONFIG, _impl } diff --git a/src/editor/core/draw/particle/latex/utils/hershey.ts b/src/editor/core/draw/particle/latex/utils/hershey.ts new file mode 100644 index 0000000..159347d --- /dev/null +++ b/src/editor/core/draw/particle/latex/utils/hershey.ts @@ -0,0 +1,1632 @@ +interface HersheyEntry { + w: number + xmin: number + xmax: number + ymin: number + ymax: number + polylines: Array>> +} + +const ordR = 'R'.charCodeAt(0) + +export function HERSHEY(i: number): HersheyEntry { + if (data[i] == null) { + compile(i) + } + return data[i] +} + +function compile(i: number): void { + const entry: string = raw[i] + if (entry == null) { + return + } + const bound: string = entry.substring(3, 5) + const xmin: number = 1 * bound.charCodeAt(0) - ordR + const xmax: number = 1 * bound.charCodeAt(1) - ordR + const content: string = entry.substring(5) + + const polylines: Array>> = [[]] + let ymin = Infinity + let ymax = -Infinity + let zmin = Infinity + let zmax = -Infinity + let j = 0 + while (j < content.length) { + const digit: string = content.substring(j, j + 2) + if (digit == ' R') { + polylines.push([]) + } else { + const x: number = digit.charCodeAt(0) - ordR - xmin + const y: number = digit.charCodeAt(1) - ordR + ymin = Math.min(y, ymin) + ymax = Math.max(y, ymax) + zmin = Math.min(x, zmin) + zmax = Math.max(x, zmax) + polylines[polylines.length - 1].push([x, y]) + } + j += 2 + } + data[i] = { + w: xmax - xmin, + xmin: zmin, + xmax: zmax, + ymin: ymin, + ymax: ymax, + polylines: polylines + } +} +const data: Record = {} + +const raw: Record = { + 1: ' 9MWRMNV RRMVV RPSTS', + 2: ' 16MWOMOV ROMSMUNUPSQ ROQSQURUUSVOV', + 3: ' 11MXVNTMRMPNOPOSPURVTVVU', + 4: ' 12MWOMOV ROMRMTNUPUSTURVOV', + 5: ' 12MWOMOV ROMUM ROQSQ ROVUV', + 6: ' 9MVOMOV ROMUM ROQSQ', + 7: ' 15MXVNTMRMPNOPOSPURVTVVUVR RSRVR', + 8: ' 9MWOMOV RUMUV ROQUQ', + 9: ' 3PTRMRV', + 10: ' 7NUSMSTRVPVOTOS', + 11: ' 9MWOMOV RUMOS RQQUV', + 12: ' 6MVOMOV ROVUV', + 13: ' 12LXNMNV RNMRV RVMRV RVMVV', + 14: ' 9MWOMOV ROMUV RUMUV', + 15: ' 14MXRMPNOPOSPURVSVUUVSVPUNSMRM', + 16: ' 10MWOMOV ROMSMUNUQSROR', + 17: ' 17MXRMPNOPOSPURVSVUUVSVPUNSMRM RSTVW', + 18: ' 13MWOMOV ROMSMUNUQSROR RRRUV', + 19: ' 13MWUNSMQMONOOPPTRUSUUSVQVOU', + 20: ' 6MWRMRV RNMVM', + 21: ' 9MXOMOSPURVSVUUVSVM', + 22: ' 6MWNMRV RVMRV', + 23: ' 12LXNMPV RRMPV RRMTV RVMTV', + 24: ' 6MWOMUV RUMOV', + 25: ' 7MWNMRQRV RVMRQ', + 26: ' 9MWUMOV ROMUM ROVUV', + 27: ' 9MWRMNV RRMVV RPSTS', + 28: ' 16MWOMOV ROMSMUNUPSQ ROQSQURUUSVOV', + 29: ' 6MVOMOV ROMUM', + 30: ' 9MWRMNV RRMVV RNVVV', + 31: ' 12MWOMOV ROMUM ROQSQ ROVUV', + 32: ' 9MWUMOV ROMUM ROVUV', + 33: ' 9MWOMOV RUMUV ROQUQ', + 34: ' 20MXRMPNOPOSPURVSVUUVSVPUNSMRM RQQTR RTQQR', + 35: ' 3PTRMRV', + 36: ' 9MWOMOV RUMOS RQQUV', + 37: ' 6MWRMNV RRMVV', + 38: ' 12LXNMNV RNMRV RVMRV RVMVV', + 39: ' 9MWOMOV ROMUV RUMUV', + 40: ' 12MWOMUM RPQTR RTQPR ROVUV', + 41: ' 14MXRMPNOPOSPURVSVUUVSVPUNSMRM', + 42: ' 9MWOMOV RUMUV ROMUM', + 43: ' 10MWOMOV ROMSMUNUQSROR', + 44: ' 10MWOMRQOV ROMUM ROVUV', + 45: ' 6MWRMRV RNMVM', + 46: ' 15MWNONNOMPMQNRPRV RVOVNUMTMSNRP', + 47: ' 13LXRMRV RPONPNSPTTTVSVPTOPO', + 48: ' 6MWOMUV RUMOV', + 49: ' 12LXRMRV RNOOPOSQTSTUSUPVO', + 50: ' 13MXOVQVOROPPNRMSMUNVPVRTVVV', + 200: ' 12MWRMPNOPOSPURVTUUSUPTNRM', + 201: ' 4MWPORMRV', + 202: ' 9MWONQMSMUNUPTROVUV', + 203: ' 15MWONQMSMUNUPSQ RRQSQURUUSVQVOU', + 204: ' 7MWSMSV RSMNSVS', + 205: ' 14MWPMOQQPRPTQUSTURVQVOU RPMTM', + 206: ' 14MWTMRMPNOPOSPURVTUUSTQRPPQOS', + 207: ' 6MWUMQV ROMUM', + 208: ' 19MWQMONOPQQSQUPUNSMQM RQQOROUQVSVUUURSQ', + 209: ' 14MWUPTRRSPROPPNRMTNUPUSTURVPV', + 210: ' 6PURURVSVSURU', + 211: ' 7PUSVRVRUSUSWRY', + 212: ' 12PURPRQSQSPRP RRURVSVSURU', + 213: ' 13PURPRQSQSPRP RSVRVRUSUSWRY', + 214: ' 12PURMRR RSMSR RRURVSVSURU', + 215: ' 17NWPNRMSMUNUPRQRRSRSQUP RRURVSVSURU', + 216: ' 3PTRMRQ', + 217: ' 6NVPMPQ RTMTQ', + 218: ' 10NVQMPNPPQQSQTPTNSMQM', + 219: ' 16MWUNSMQMONOPQQTRUSUUSVQVOU RRLRW', + 220: ' 3MWVLNW', + 221: ' 7OVTLRNQPQSRUTW', + 222: ' 7NUPLRNSPSSRUPW', + 223: ' 3PTRLRW', + 224: ' 3LXNRVR', + 225: ' 6LXRNRV RNRVR', + 226: ' 6LXNPVP RNTVT', + 227: ' 6MWOOUU RUOOU', + 228: ' 9MWRORU ROPUT RUPOT', + 229: ' 6PURQRRSRSQRQ', + 230: ' 7PUSMRORQSQSPRP', + 231: ' 7PUSNRNRMSMSORQ', + 232: ' 7LXSOVRSU RNRVR', + 233: ' 12MXRLPW RULSW ROPVP ROSVS', + 234: ' 21LXVRURTSSURVOVNUNSORRQSPSNRMPMONOPQSSUUVVV', + 235: ' 20LXNNOQOSNV RVNUQUSVV RNNQOSOVN RNVQUSUVV', + 501: ' 9I[RFJ[ RRFZ[ RMTWT', + 502: ' 24G\\KFK[ RKFTFWGXHYJYLXNWOTP RKPTPWQXRYTYWXYWZT[K[', + 503: ' 19H]ZKYIWGUFQFOGMILKKNKSLVMXOZQ[U[WZYXZV', + 504: ' 16G\\KFK[ RKFRFUGWIXKYNYSXVWXUZR[K[', + 505: ' 12H[LFL[ RLFYF RLPTP RL[Y[', + 506: ' 9HZLFL[ RLFYF RLPTP', + 507: ' 23H]ZKYIWGUFQFOGMILKKNKSLVMXOZQ[U[WZYXZVZS RUSZS', + 508: ' 9G]KFK[ RYFY[ RKPYP', + 509: ' 3NVRFR[', + 510: ' 11JZVFVVUYTZR[P[NZMYLVLT', + 511: ' 9G\\KFK[ RYFKT RPOY[', + 512: ' 6HYLFL[ RL[X[', + 513: ' 12F^JFJ[ RJFR[ RZFR[ RZFZ[', + 514: ' 9G]KFK[ RKFY[ RYFY[', + 515: ' 22G]PFNGLIKKJNJSKVLXNZP[T[VZXXYVZSZNYKXIVGTFPF', + 516: ' 14G\\KFK[ RKFTFWGXHYJYMXOWPTQKQ', + 517: ' 25G]PFNGLIKKJNJSKVLXNZP[T[VZXXYVZSZNYKXIVGTFPF RSWY]', + 518: ' 17G\\KFK[ RKFTFWGXHYJYLXNWOTPKP RRPY[', + 519: ' 21H\\YIWGTFPFMGKIKKLMMNOOUQWRXSYUYXWZT[P[MZKX', + 520: ' 6JZRFR[ RKFYF', + 521: ' 11G]KFKULXNZQ[S[VZXXYUYF', + 522: ' 6I[JFR[ RZFR[', + 523: ' 12F^HFM[ RRFM[ RRFW[ R\\FW[', + 524: ' 6H\\KFY[ RYFK[', + 525: ' 7I[JFRPR[ RZFRP', + 526: ' 9H\\YFK[ RKFYF RK[Y[', + 527: ' 9I[RFJ[ RRFZ[ RMTWT', + 528: ' 24G\\KFK[ RKFTFWGXHYJYLXNWOTP RKPTPWQXRYTYWXYWZT[K[', + 529: ' 6HYLFL[ RLFXF', + 530: ' 9I[RFJ[ RRFZ[ RJ[Z[', + 531: ' 12H[LFL[ RLFYF RLPTP RL[Y[', + 532: ' 9H\\YFK[ RKFYF RK[Y[', + 533: ' 9G]KFK[ RYFY[ RKPYP', + 534: ' 25G]PFNGLIKKJNJSKVLXNZP[T[VZXXYVZSZNYKXIVGTFPF ROPUP', + 535: ' 3NVRFR[', + 536: ' 9G\\KFK[ RYFKT RPOY[', + 537: ' 6I[RFJ[ RRFZ[', + 538: ' 12F^JFJ[ RJFR[ RZFR[ RZFZ[', + 539: ' 9G]KFK[ RKFY[ RYFY[', + 540: ' 9I[KFYF ROPUP RK[Y[', + 541: ' 22G]PFNGLIKKJNJSKVLXNZP[T[VZXXYVZSZNYKXIVGTFPF', + 542: ' 9G]KFK[ RYFY[ RKFYF', + 543: ' 14G\\KFK[ RKFTFWGXHYJYMXOWPTQKQ', + 544: ' 10I[KFRPK[ RKFYF RK[Y[', + 545: ' 6JZRFR[ RKFYF', + 546: ' 19I[KKKILGMFOFPGQIRMR[ RYKYIXGWFUFTGSIRM', + 547: ' 21H\\RFR[ RPKMLLMKOKRLTMUPVTVWUXTYRYOXMWLTKPK', + 548: ' 6H\\KFY[ RK[YF', + 549: ' 18G]RFR[ RILJLKMLQMSNTQUSUVTWSXQYMZL[L', + 550: ' 17H\\K[O[LTKPKLLINGQFSFVGXIYLYPXTU[Y[', + 551: ' 20G[G[IZLWOSSLVFV[UXSUQSNQLQKRKTLVNXQZT[Y[', + 552: ' 41F]SHTITLSPRSQUOXMZK[J[IZIWJRKOLMNJPHRGUFXFZG[I[KZMYNWOTP RSPTPWQXRYTYWXYWZU[R[PZOX', + 553: ' 24H\\TLTMUNWNYMZKZIYGWFTFQGOIMLLNKRKVLYMZO[Q[TZVXWV', + 554: ' 35G^TFRGQIPMOSNVMXKZI[G[FZFXGWIWKXMZP[S[VZXXZT[O[KZHYGWFTFRHRJSMUPWRZT\\U', + 555: ' 28H\\VJVKWLYLZKZIYGVFRFOGNINLONPOSPPPMQLRKTKWLYMZP[S[VZXXYV', + 556: ' 28H\\RLPLNKMINGQFTFXG[G]F RXGVNTTRXPZN[L[JZIXIVJULUNV RQPZP', + 557: ' 29G^G[IZMVPQQNRJRGQFPFOGNINLONQOUOXNYMZKZQYVXXVZS[O[LZJXIVIT', + 558: ' 38F^MMKLJJJIKGMFNFPGQIQKPONULYJ[H[GZGX RMRVOXN[L]J^H^G]F\\FZHXLVRUWUZV[W[YZZY\\V', + 559: ' 25IZWVUTSQROQLQIRGSFUFVGWIWLVQTVSXQZO[M[KZJXJVKUMUOV', + 560: ' 25JYT^R[PVOPOJPGRFTFUGVJVMURR[PaOdNfLgKfKdLaN^P\\SZWX', + 561: ' 39F^MMKLJJJIKGMFNFPGQIQKPONULYJ[H[GZGX R^I^G]F\\FZGXIVLTNROPO RROSQSXTZU[V[XZYY[V', + 562: ' 29I\\MRORSQVOXMYKYHXFVFUGTISNRSQVPXNZL[J[IZIXJWLWNXQZT[V[YZ[X', + 563: ' 45@aEMCLBJBICGEFFFHGIIIKHPGTE[ RGTJLLHMGOFPFRGSISKRPQTO[ RQTTLVHWGYFZF\\G]I]K\\PZWZZ[[\\[^Z_YaV', + 564: ' 32E]JMHLGJGIHGJFKFMGNINKMPLTJ[ RLTOLQHRGTFVFXGYIYKXPVWVZW[X[ZZ[Y]V', + 565: ' 29H]TFQGOIMLLNKRKVLYMZO[Q[TZVXXUYSZOZKYHXGVFTFRHRKSNUQWSZU\\V', + 566: ' 31F_SHTITLSPRSQUOXMZK[J[IZIWJRKOLMNJPHRGUFZF\\G]H^J^M]O\\PZQWQUPTO', + 567: ' 32H^ULTNSOQPOPNNNLOIQGTFWFYGZIZMYPWSSWPYNZK[I[HZHXIWKWMXPZS[V[YZ[X', + 568: ' 38F_SHTITLSPRSQUOXMZK[J[IZIWJRKOLMNJPHRGUFYF[G\\H]J]M\\O[PYQVQSPTQUSUXVZX[ZZ[Y]V', + 569: ' 28H\\H[JZLXOTQQSMTJTGSFRFQGPIPKQMSOVQXSYUYWXYWZT[P[MZKXJVJT', + 570: ' 25H[RLPLNKMINGQFTFXG[G]F RXGVNTTRXPZN[L[JZIXIVJULUNV', + 571: ' 33E]JMHLGJGIHGJFKFMGNINKMOLRKVKXLZN[P[RZSYUUXMZF RXMWQVWVZW[X[ZZ[Y]V', + 572: ' 32F]KMILHJHIIGKFLFNGOIOKNOMRLVLYM[O[QZTWVTXPYMZIZGYFXFWGVIVKWNYP[Q', + 573: ' 25C_HMFLEJEIFGHFIFKGLILLK[ RUFK[ RUFS[ RaF_G\\JYNVTS[', + 574: ' 36F^NLLLKKKILGNFPFRGSISLQUQXRZT[V[XZYXYVXUVU R]I]G\\FZFXGVITLPUNXLZJ[H[GZGX', + 575: ' 38F]KMILHJHIIGKFLFNGOIOKNOMRLVLXMZN[P[RZTXVUWSYM R[FYMVWT]RbPfNgMfMdNaP^S[VY[V', + 576: ' 40H]ULTNSOQPOPNNNLOIQGTFWFYGZIZMYPWTTWPZN[K[JZJXKWNWPXQYR[R^QaPcNfLgKfKdLaN^Q[TYZV', + 583: ' 9I[JFR[ RZFR[ RJFZF', + 601: ' 18I\\XMX[ RXPVNTMQMONMPLSLUMXOZQ[T[VZXX', + 602: ' 18H[LFL[ RLPNNPMSMUNWPXSXUWXUZS[P[NZLX', + 603: ' 15I[XPVNTMQMONMPLSLUMXOZQ[T[VZXX', + 604: ' 18I\\XFX[ RXPVNTMQMONMPLSLUMXOZQ[T[VZXX', + 605: ' 18I[LSXSXQWOVNTMQMONMPLSLUMXOZQ[T[VZXX', + 606: ' 9MYWFUFSGRJR[ ROMVM', + 607: ' 23I\\XMX]W`VaTbQbOa RXPVNTMQMONMPLSLUMXOZQ[T[VZXX', + 608: ' 11I\\MFM[ RMQPNRMUMWNXQX[', + 609: ' 9NVQFRGSFREQF RRMR[', + 610: ' 12MWRFSGTFSERF RSMS^RaPbNb', + 611: ' 9IZMFM[ RWMMW RQSX[', + 612: ' 3NVRFR[', + 613: ' 19CaGMG[ RGQJNLMOMQNRQR[ RRQUNWMZM\\N]Q][', + 614: ' 11I\\MMM[ RMQPNRMUMWNXQX[', + 615: ' 18I\\QMONMPLSLUMXOZQ[T[VZXXYUYSXPVNTMQM', + 616: ' 18H[LMLb RLPNNPMSMUNWPXSXUWXUZS[P[NZLX', + 617: ' 18I\\XMXb RXPVNTMQMONMPLSLUMXOZQ[T[VZXX', + 618: ' 9KXOMO[ ROSPPRNTMWM', + 619: ' 18J[XPWNTMQMNNMPNRPSUTWUXWXXWZT[Q[NZMX', + 620: ' 9MYRFRWSZU[W[ ROMVM', + 621: ' 11I\\MMMWNZP[S[UZXW RXMX[', + 622: ' 6JZLMR[ RXMR[', + 623: ' 12G]JMN[ RRMN[ RRMV[ RZMV[', + 624: ' 6J[MMX[ RXMM[', + 625: ' 10JZLMR[ RXMR[P_NaLbKb', + 626: ' 9J[XMM[ RMMXM RM[X[', + 627: ' 24H]QMONMPLRKUKXLZN[P[RZUWWTYPZM RQMSMTNUPWXXZY[Z[', + 628: ' 31I\\UFSGQIOMNPMTLZKb RUFWFYHYKXMWNUORO RROTPVRWTWWVYUZS[Q[OZNYMV', + 629: ' 17I\\JPLNNMOMQNROSRSVR[ RZMYPXRR[P_Ob', + 630: ' 24I[TMQMONMPLSLVMYNZP[R[TZVXWUWRVOTMRKQIQGRFTFVGXI', + 631: ' 19JZWOVNTMQMONOPPRSS RSSOTMVMXNZP[S[UZWX', + 632: ' 23JYTFRGQHQIRJUKXK RXKTMQONRMUMWNYP[S]T_TaSbQbP`', + 633: ' 19H\\IQJOLMNMONOPNTL[ RNTPPRNTMVMXOXRWWTb', + 634: ' 27G\\HQIOKMMMNNNPMUMXNZO[Q[SZUWVUWRXMXJWGUFSFRHRJSMUPWRZT', + 635: ' 9LWRMPTOXOZP[R[TYUW', + 636: ' 19I[OMK[ RYNXMWMUNQROSNS RNSPTQUSZT[U[VZ', + 637: ' 9JZKFMFOGPHX[ RRML[', + 638: ' 21H]OMIb RNQMVMYO[Q[SZUXWT RYMWTVXVZW[Y[[Y\\W', + 639: ' 14I[LMOMNSMXL[ RYMXPWRUURXOZL[', + 640: ' 29JZTFRGQHQIRJUKXK RUKRLPMOOOQQSTTVT RTTPUNVMXMZO\\S^T_TaRbPb', + 641: ' 18J[RMPNNPMSMVNYOZQ[S[UZWXXUXRWOVNTMRM', + 642: ' 13G]PML[ RUMVSWXX[ RIPKNNM[M', + 643: ' 19I[MSMVNYOZQ[S[UZWXXUXRWOVNTMRMPNNPMSIb', + 644: ' 18I][MQMONMPLSLVMYNZP[R[TZVXWUWRVOUNSM', + 645: ' 8H\\SMP[ RJPLNOMZM', + 646: ' 16H\\IQJOLMNMONOPMVMYO[Q[TZVXXTYPYM', + 647: ' 21G]ONMOKQJTJWKYLZN[Q[TZWXYUZRZOXMVMTORSPXMb', + 648: ' 14I[KMMMOOU`WbYb RZMYOWRM]K`Jb', + 649: ' 20F]VFNb RGQHOJMLMMNMPLULXMZO[Q[TZVXXUZP[M', + 650: ' 23F]NMLNJQITIWJZK[M[OZQW RRSQWRZS[U[WZYWZTZQYNXM', + 651: ' 22L\\UUTSRRPRNSMTLVLXMZO[Q[SZTXVRUWUZV[W[YZZY\\V', + 652: ' 23M[MVOSRNSLTITGSFQGPIOMNTNZO[P[RZTXUUURVVWWYW[V', + 653: ' 14MXTTTSSRQROSNTMVMXNZP[S[VYXV', + 654: ' 24L\\UUTSRRPRNSMTLVLXMZO[Q[SZTXZF RVRUWUZV[W[YZZY\\V', + 655: ' 17NXOYQXRWSUSSRRQROSNUNXOZQ[S[UZVYXV', + 656: ' 24OWOVSQUNVLWIWGVFTGSIQQNZKaJdJfKgMfNcOZP[R[TZUYWV', + 657: ' 28L[UUTSRRPRNSMTLVLXMZO[Q[SZTY RVRTYPdOfMgLfLdMaP^S\\U[XY[V', + 658: ' 29M\\MVOSRNSLTITGSFQGPIOMNSM[ RM[NXOVQSSRURVSVUUXUZV[W[YZZY\\V', + 659: ' 16PWSMSNTNTMSM RPVRRPXPZQ[R[TZUYWV', + 660: ' 20PWSMSNTNTMSM RPVRRLdKfIgHfHdIaL^O\\Q[TYWV', + 661: ' 33M[MVOSRNSLTITGSFQGPIOMNSM[ RM[NXOVQSSRURVSVUTVQV RQVSWTZU[V[XZYY[V', + 662: ' 18OWOVQSTNULVIVGUFSGRIQMPTPZQ[R[TZUYWV', + 663: ' 33E^EVGSIRJSJTIXH[ RIXJVLSNRPRQSQTPXO[ RPXQVSSURWRXSXUWXWZX[Y[[Z\\Y^V', + 664: ' 23J\\JVLSNROSOTNXM[ RNXOVQSSRURVSVUUXUZV[W[YZZY\\V', + 665: ' 23LZRRPRNSMTLVLXMZO[Q[SZTYUWUUTSRRQSQURWTXWXYWZV', + 666: ' 24KZKVMSNQMUGg RMUNSPRRRTSUUUWTYSZQ[ RMZO[R[UZWYZV', + 667: ' 27L[UUTSRRPRNSMTLVLXMZO[Q[SZ RVRUUSZPaOdOfPgRfScS\\U[XY[V', + 668: ' 15MZMVOSPQPSSSTTTVSYSZT[U[WZXYZV', + 669: ' 16NYNVPSQQQSSVTXTZR[ RNZP[T[VZWYYV', + 670: ' 16OXOVQSSO RVFPXPZQ[S[UZVYXV RPNWN', + 671: ' 19L[LVNRLXLZM[O[QZSXUU RVRTXTZU[V[XZYY[V', + 672: ' 17L[LVNRMWMZN[O[RZTXUUUR RURVVWWYW[V', + 673: ' 25I^LRJTIWIYJ[L[NZPX RRRPXPZQ[S[UZWXXUXR RXRYVZW\\W^V', + 674: ' 20JZJVLSNRPRQSQZR[U[XYZV RWSVRTRSSOZN[L[KZ', + 675: ' 23L[LVNRLXLZM[O[QZSXUU RVRPdOfMgLfLdMaP^S\\U[XY[V', + 676: ' 23LZLVNSPRRRTTTVSXQZN[P\\Q^QaPdOfMgLfLdMaP^S\\WYZV', + 677: ' 22J\\K[NZQXSVUSWOXKXIWGUFSGRHQJPOPTQXRZT[V[XZYY', + 683: ' 26I[WUWRVOUNSMQMONMPLSLVMYNZP[R[TZVXWUXPXKWHVGTFRFPGNI', + 684: ' 16JZWNUMRMPNNPMSMVNYOZQ[T[VZ RMTUT', + 685: ' 23J[TFRGPJOLNOMTMXNZO[Q[SZUWVUWRXMXIWGVFTF RNPWP', + 686: ' 21H\\VFNb RQMNNLPKSKVLXNZQ[S[VZXXYUYRXPVNSMQM', + 687: ' 16I[XOWNTMQMNNMOLQLSMUOWSZT\\T^S_Q_', + 700: ' 18H\\QFNGLJKOKRLWNZQ[S[VZXWYRYOXJVGSFQF', + 701: ' 5H\\NJPISFS[', + 702: ' 15H\\LKLJMHNGPFTFVGWHXJXLWNUQK[Y[', + 703: ' 16H\\MFXFRNUNWOXPYSYUXXVZS[P[MZLYKW', + 704: ' 7H\\UFKTZT RUFU[', + 705: ' 18H\\WFMFLOMNPMSMVNXPYSYUXXVZS[P[MZLYKW', + 706: ' 24H\\XIWGTFRFOGMJLOLTMXOZR[S[VZXXYUYTXQVOSNRNOOMQLT', + 707: ' 6H\\YFO[ RKFYF', + 708: ' 30H\\PFMGLILKMMONSOVPXRYTYWXYWZT[P[MZLYKWKTLRNPQOUNWMXKXIWGTFPF', + 709: ' 24H\\XMWPURRSQSNRLPKMKLLINGQFRFUGWIXMXRWWUZR[P[MZLX', + 710: ' 6MWRYQZR[SZRY', + 711: ' 9MWSZR[QZRYSZS\\R^Q_', + 712: ' 12MWRMQNROSNRM RRYQZR[SZRY', + 713: ' 15MWRMQNROSNRM RSZR[QZRYSZS\\R^Q_', + 714: ' 9MWRFRT RRYQZR[SZRY', + 715: ' 21I[LKLJMHNGPFTFVGWHXJXLWNVORQRT RRYQZR[SZRY', + 716: ' 3NVRFRM', + 717: ' 6JZNFNM RVFVM', + 718: ' 14KYQFOGNINKOMQNSNUMVKVIUGSFQF', + 719: ' 27H\\PBP_ RTBT_ RYIWGTFPFMGKIKKLMMNOOUQWRXSYUYXWZT[P[MZKX', + 720: ' 3G][BIb', + 721: ' 11KYVBTDRGPKOPOTPYR]T`Vb', + 722: ' 11KYNBPDRGTKUPUTTYR]P`Nb', + 723: ' 3NVRBRb', + 724: ' 3E_IR[R', + 725: ' 6E_RIR[ RIR[R', + 726: ' 6E_IO[O RIU[U', + 727: ' 6G]KKYY RYKKY', + 728: ' 9JZRLRX RMOWU RWOMU', + 729: ' 6MWRQQRRSSRRQ', + 730: ' 8MWSFRGQIQKRLSKRJ', + 731: ' 8MWRHQGRFSGSIRKQL', + 732: ' 9E_UMXP[RXTUW RIR[R', + 733: ' 12H]SBLb RYBRb RLOZO RKUYU', + 734: ' 35E_\\O\\N[MZMYNXPVUTXRZP[L[JZIYHWHUISJRQNRMSKSIRGPFNGMIMKNNPQUXWZY[[[\\Z\\Y', + 735: ' 28G]IIJKKOKUJYI[ R[IZKYOYUZY[[ RIIKJOKUKYJ[I RI[KZOYUYYZ[[', + 737: ' 6KYOBO[ RUBU[', + 738: ' 6F^RBR[ RI[[[', + 739: ' 4F^[BI[[[', + 740: ' 18E_RIQJRKSJRI RIYHZI[JZIY R[YZZ[[\\Z[Y', + 741: ' 33F^RHNLKPJSJUKWMXOXQWRU RRHVLYPZSZUYWWXUXSWRU RRUQYP\\ RRUSYT\\ RP\\T\\', + 742: ' 26F^RNQKPINHMHKIJKJOKRLTNWR\\ RRNSKTIVHWHYIZKZOYRXTVWR\\', + 743: ' 20F^RGPJLOIR RRGTJXO[R RIRLUPZR] R[RXUTZR]', + 744: ' 48F^RTTWVXXXZW[U[SZQXPVPSQ RSQUOVMVKUISHQHOINKNMOOQQ RQQNPLPJQISIUJWLXNXPWRT RRTQYP\\ RRTSYT\\ RP\\T\\', + 745: ' 55F^RRR[Q\\ RRVQ\\ RRIQHOHNINKONRR RRISHUHVIVKUNRR RRRNOLNJNIOIQJR RRRVOXNZN[O[QZR RRRNULVJVIUISJR RRRVUXVZV[U[SZR', + 746: ' 55F^ISJSLTMVMXLZ RISIRJQLQMRNTNWMYLZ RRGPIOLOOQUQXPZR\\ RRGTIULUOSUSXTZR\\ R[S[RZQXQWRVTVWWYXZ R[SZSXTWVWXXZ RKVYV', + 750: ' 18PSSRRSQSPRPQQPRPSQSSRUQV RQQQRRRRQQQ', + 751: ' 16PTQPPQPSQTSTTSTQSPQP RRQQRRSSRRQ', + 752: ' 9NVPOTU RTOPU RNRVR', + 753: ' 28MWRKQMOPMR RRKSMUPWR RRMOQ RRMUQ RROPQ RROTQ RQQSQ RMRWR', + 754: ' 26MWMRMQNOONQMSMUNVOWQWR RPNTN ROOUO RNPVP RNQVQ RMRWR', + 755: ' 14LRLFLRRRLF RLIPQ RLLOR RLOMQ', + 756: ' 10MWRKQMOPMR RRKSMUPWR', + 757: ' 11MWWRWQVOUNSMQMONNOMQMR', + 758: ' 13G]]R]P\\MZJWHTGPGMHJJHMGPGR', + 759: ' 11MWMRMSNUOVQWSWUVVUWSWR', + 760: ' 7LXLPNRQSSSVRXP', + 761: ' 6RURUTTURTPRO', + 762: ' 7RVRRUPVNVLUKTK', + 763: ' 7NRRROPNNNLOKPK', + 764: ' 21MWWHVGTFQFOGNHMJMLNNOOUSVTWVWXVZU[S\\P\\N[MZ', + 765: ' 21G]IWHVGTGQHOINKMMMONPOTUUVWWYW[V\\U]S]P\\N[M', + 766: ' 31G]RRTUUVWWYW[V\\U]S]Q\\O[NYMWMUNTOPUOVMWKWIVHUGSGQHOINKMMMONPORR', + 767: ' 22H\\KFK[ RHF[FQP[Z RZV[Y\\[ RZVZY RWYZY RWYZZ\\[', + 768: ' 30KYUARBPCNELHKLKRLUNWQXSXVWXUYR RKPLMNKQJSJVKXMYPYVXZV]T_R`Oa', + 796: ' 3>f>RfR', + 797: ' 3D`D``D', + 798: ' 3RRR>Rf', + 799: ' 3D`DD``', + 800: ' 3D`DR`R', + 801: ' 3F^FY^K', + 802: ' 3KYK^YF', + 803: ' 3RRRDR`', + 804: ' 3KYKFY^', + 805: ' 3F^FK^Y', + 806: ' 3KYKRYR', + 807: ' 3MWMWWM', + 808: ' 3RRRKRY', + 809: ' 3MWMMWW', + 810: ' 8GRRGPGMHJJHMGPGR', + 811: ' 8GRGRGTHWJZM\\P]R]', + 812: ' 8R]R]T]W\\ZZ\\W]T]R', + 813: ' 8R]]R]P\\MZJWHTGRG', + 814: ' 9D`DOGQKSPTTTYS]Q`O', + 815: ' 9PUUDSGQKPPPTQYS]U`', + 816: ' 9OTODQGSKTPTTSYQ]O`', + 817: ' 9D`DUGSKQPPTPYQ]S`U', + 818: ' 5KYRJYNKVRZ', + 819: ' 5JZJRNKVYZR', + 820: ' 5KYKVKNYVYN', + 821: ' 5JZLXJPZTXL', + 822: ' 23JZJ]L]O\\Q[TXUVVSVOULTJSIQIPJOLNONSOVPXS[U\\X]Z]', + 823: ' 23I]]Z]X\\U[SXPVOSNONLOJPIQISJTLUOVSVVUXT[Q\\O]L]J', + 824: ' 23JZZGXGUHSIPLONNQNUOXPZQ[S[TZUXVUVQUNTLQIOHLGJG', + 825: ' 23G[GJGLHOIQLTNUQVUVXUZT[S[QZPXOUNQNNOLPISHUGXGZ', + 826: ' 21E[EPFRHTJUMVQVUUXSZP[NZLWLSMQNNPLSKVKYL\\M^', + 827: ' 19EYETHVKWPWSVVTXQYNYLXKVKSLPNNQMTMYN\\P_', + 828: ' 26OUQOOQOSQUSUUSUQSOQO RQPPQPSQTSTTSTQSPQP RRQQRRSSRRQ', + 829: ' 11RWRMSMUNVOWQWSVUUVSWRW', + 830: ' 9D`DRJR RORUR RZR`R', + 831: ' 5D`DUDO`O`U', + 832: ' 6JZRDJR RRDZR', + 833: ' 9D`DR`R RJYZY RP`T`', + 834: ' 9D`DR`R RDRRb R`RRb', + 840: ' 18KYQKNLLNKQKSLVNXQYSYVXXVYSYQXNVLSKQK', + 841: ' 6LXLLLXXXXLLL', + 842: ' 5KYRJKVYVRJ', + 843: ' 6LXRHLRR\\XRRH', + 844: ' 12JZRIPOJOOSMYRUWYUSZOTORI', + 845: ' 6KYRKRY RKRYR', + 846: ' 6MWMMWW RWMMW', + 847: ' 9MWRLRX RMOWU RWOMU', + 850: ' 35NVQNOONQNSOUQVSVUUVSVQUOSNQN ROQOS RPPPT RQOQU RRORU RSOSU RTPTT RUQUS', + 851: ' 27NVNNNVVVVNNN ROOOU RPOPU RQOQU RRORU RSOSU RTOTU RUOUU', + 852: ' 17MWRLMUWURL RROOT RROUT RRRQT RRRST', + 853: ' 17LULRUWUMLR RORTU RORTO RRRTS RRRTQ', + 854: ' 17MWRXWOMORX RRUUP RRUOP RRRSP RRRQP', + 855: ' 17OXXROMOWXR RURPO RURPU RRRPQ RRRPS', + 856: ' 22LXRLNWXPLPVWRL RRRRL RRRLP RRRNW RRRVW RRRXP', + 857: ' 11RYRKRY RRKYNRQ RSMVNSO', + 860: ' 13MWRLRX ROOUO RMUOWQXSXUWWU', + 861: ' 11LXRLRX RLQMOWOXQ RPWTW', + 862: ' 14KYMNWX RWNMX ROLLOKQ RULXOYQ', + 863: ' 18I[NII[ RVI[[ RMM[[ RWMI[ RNIVI RMMWM', + 864: ' 21I[RGRV RMJWP RWJMP RIVL\\ R[VX\\ RIV[V RL\\X\\', + 865: ' 11G[MJSV RKPSL RG\\[\\[RG\\', + 866: ' 14LXPLPPLPLTPTPXTXTTXTXPTPTLPL', + 867: ' 32KYYPXNVLSKQKNLLNKQKSLVNXQYSYVXXVYT RYPWNUMSMQNPOOQOSPUQVSWUWWVYT', + 868: ' 10KYRJKVYVRJ RRZYNKNRZ', + 869: ' 34G]PIPGQFSFTGTI RGZHXJVKTLPLKMJOIUIWJXKXPYTZV\\X]Z RGZ]Z RQZP[Q\\S\\T[SZ', + 870: ' 64JZRMRS RRSQ\\ RRSS\\ RQ\\S\\ RRMQJPHNG RQJNG RRMSJTHVG RSJVG RRMNKLKJM RPLLLJM RRMVKXKZM RTLXLZM RRMPNOOOR RRMPOOR RRMTNUOUR RRMTOUR', + 871: ' 94JZRIRK RRNRP RRSRU RRYQ\\ RRYS\\ RQ\\S\\ RRGQIPJ RRGSITJ RPJRITJ RRKPNNOMN RRKTNVOWN RNOPORNTOVO RRPPSNTLTKRKSLT RRPTSVTXTYRYSXT RNTPTRSTTVT RRUPXOYMZLZKYJWJYLZ RRUTXUYWZXZYYZWZYXZ RMZOZRYUZWZ', + 872: ' 40JZRYQ\\ RRYS\\ RQ\\S\\ RRYUZXZZXZUYTWTYRZOYMWLUMVJUHSGQGOHNJOMMLKMJOKRMTKTJUJXLZOZRY', + 873: ' 32JZRYQ\\ RRYS\\ RQ\\S\\ RRYVXVVXUXRZQZLYIXHVHTGPGNHLHKIJLJQLRLUNVNXRY', + 874: ' 15I[IPKR RLKNP RRGRO RXKVP R[PYR', + 899: ' 6QSRQQRRSSRRQ', + 900: ' 10PTQPPQPSQTSTTSTQSPQP', + 901: ' 14NVQNOONQNSOUQVSVUUVSVQUOSNQN', + 902: ' 18MWQMONNOMQMSNUOVQWSWUVVUWSWQVOUNSMQM', + 903: ' 18KYQKNLLNKQKSLVNXQYSYVXXVYSYQXNVLSKQK', + 904: ' 22G]PGMHJJHMGPGTHWJZM\\P]T]W\\ZZ\\W]T]P\\MZJWHTGPG', + 905: ' 34AcPALBJCGEEGCJBLAPATBXCZE]G_JaLbPcTcXbZa]__]aZbXcTcPbLaJ_G]EZCXBTAPA', + 906: ' 34fRAPCMDJDGCEA>H@JAMAZB]D_G`M`PaRc RRATCWDZD]C_AfHdJcMcZb]`_]`W`TaRc', + 909: ' 33AcRAPCMDJDGCEABGAKAPBTDXG\\L`Rc RRATCWDZD]C_AbGcKcPbT`X]\\X`Rc RBHbH', + 997: ' 3MWMXWX', + 998: ' 3JZJZZZ', + 999: ' 3JZJ]Z]', + 1001: ' 18KYRKMX RRNVX RRKWX ROTTT RKXPX RTXYX', + 1002: ' 35JZNKNX ROKOX RLKSKVLWNVPSQ RSKULVNUPSQ ROQSQVRWTWUVWSXLX RSQURVTVUUWSX', + 1003: ' 24KYVLWKWOVLTKQKOLNMMPMSNVOWQXTXVWWU RQKOMNPNSOVQX', + 1004: ' 26JZNKNX ROKOX RLKSKVLWMXPXSWVVWSXLX RSKULVMWPWSVVUWSX', + 1005: ' 22JYNKNX ROKOX RSOSS RLKVKVOUK ROQSQ RLXVXVTUX', + 1006: ' 20JXNKNX ROKOX RSOSS RLKVKVOUK ROQSQ RLXQX', + 1007: ' 36K[VLWKWOVLTKQKOLNMMPMSNVOWQXTXVW RQKOMNPNSOVQX RTXUWVU RVSVX RWSWX RTSYS', + 1008: ' 27J[NKNX ROKOX RVKVX RWKWX RLKQK RTKYK ROQVQ RLXQX RTXYX', + 1009: ' 12NWRKRX RSKSX RPKUK RPXUX', + 1010: ' 19LXSKSURWQX RTKTUSWQXPXNWMUNTOUNV RQKVK', + 1011: ' 27JZNKNX ROKOX RWKOS RQQVX RRQWX RLKQK RTKYK RLXQX RTXYX', + 1012: ' 14KXOKOX RPKPX RMKRK RMXWXWTVX', + 1013: ' 30I\\MKMX RNNRX RNKRU RWKRX RWKWX RXKXX RKKNK RWKZK RKXOX RUXZX', + 1014: ' 21JZNKNX ROMVX ROKVV RVKVX RLKOK RTKXK RLXPX', + 1015: ' 32KZQKOLNMMPMSNVOWQXTXVWWVXSXPWMVLTKQK RQKOMNPNSOVQX RTXVVWSWPVMTK', + 1016: ' 25JYNKNX ROKOX RLKSKVLWNWOVQSROR RSKULVNVOUQSR RLXQX', + 1017: ' 47KZQKOLNMMPMSNVOWQXTXVWWVXSXPWMVLTKQK RQKOMNPNSOVQX RTXVVWSWPVMTK RPWPUQTSTTUUZV[W[XZ RTUUXVZW[', + 1018: ' 37JZNKNX ROKOX RLKSKVLWNWOVQSROR RSKULVNVOUQSR RLXQX RSRTSUWVXWXXW RSRUSVWWX', + 1019: ' 32KZVMWKWOVMULSKQKOLNMNOOPQQTRVSWT RNNOOQPTQVRWSWVVWTXRXPWOVNTNXOV', + 1020: ' 16KZRKRX RSKSX RNKMOMKXKXOWK RPXUX', + 1021: ' 20J[NKNUOWQXTXVWWUWK ROKOUPWQX RLKQK RUKYK', + 1022: ' 15KYMKRX RNKRU RWKRX RKKPK RTKYK', + 1023: ' 24I[LKOX RMKOT RRKOX RRKUX RSKUT RXKUX RJKOK RVKZK', + 1024: ' 21KZNKVX ROKWX RWKNX RLKQK RTKYK RLXQX RTXYX', + 1025: ' 20LYNKRRRX ROKSR RWKSRSX RLKQK RTKYK RPXUX', + 1026: ' 16LYVKNX RWKOX ROKNONKWK RNXWXWTVX', + 1027: ' 18KYRKMX RRNVX RRKWX ROTTT RKXPX RTXYX', + 1028: ' 35JZNKNX ROKOX RLKSKVLWNVPSQ RSKULVNUPSQ ROQSQVRWTWUVWSXLX RSQURVTVUUWSX', + 1029: ' 14KXOKOX RPKPX RMKWKWOVK RMXRX', + 1030: ' 15KYRKLX RRMWX RRKXX RMWVW RLXXX', + 1031: ' 22JYNKNX ROKOX RSOSS RLKVKVOUK ROQSQ RLXVXVTUX', + 1032: ' 16LYVKNX RWKOX ROKNONKWK RNXWXWTVX', + 1033: ' 27J[NKNX ROKOX RVKVX RWKWX RLKQK RTKYK ROQVQ RLXQX RTXYX', + 1034: ' 44KZQKOLNMMPMSNVOWQXTXVWWVXSXPWMVLTKQK RQKOMNPNSOVQX RTXVVWSWPVMTK RQOQT RTOTT RQQTQ RQRTR', + 1035: ' 12NWRKRX RSKSX RPKUK RPXUX', + 1036: ' 27JZNKNX ROKOX RWKOS RQQVX RRQWX RLKQK RTKYK RLXQX RTXYX', + 1037: ' 15KYRKMX RRNVX RRKWX RKXPX RTXYX', + 1038: ' 30I\\MKMX RNNRX RNKRU RWKRX RWKWX RXKXX RKKNK RWKZK RKXOX RUXZX', + 1039: ' 21JZNKNX ROMVX ROKVV RVKVX RLKOK RTKXK RLXPX', + 1040: ' 36JZMJLM RXJWM RPPOS RUPTS RMVLY RXVWY RMKWK RMLWL RPQTQ RPRTR RMWWW RMXWX', + 1041: ' 32KZQKOLNMMPMSNVOWQXTXVWWVXSXPWMVLTKQK RQKOMNPNSOVQX RTXVVWSWPVMTK', + 1042: ' 21J[NKNX ROKOX RVKVX RWKWX RLKYK RLXQX RTXYX', + 1043: ' 25JYNKNX ROKOX RLKSKVLWNWOVQSROR RSKULVNVOUQSR RLXQX', + 1044: ' 20K[MKRQ RNKSQMX RMKWKXOVK RNWWW RMXWXXTVX', + 1045: ' 16KZRKRX RSKSX RNKMOMKXKXOWK RPXUX', + 1046: ' 33KZMONLOKPKQLRORX RXOWLVKUKTLSOSX RMONMOLPLQMRO RXOWMVLULTMSO RPXUX', + 1047: ' 40KZRKRX RSKSX RQNNOMQMRNTQUTUWTXRXQWOTNQN RQNOONQNROTQU RTUVTWRWQVOTN RPKUK RPXUX', + 1048: ' 21KZNKVX ROKWX RWKNX RLKQK RTKYK RLXQX RTXYX', + 1049: ' 33J[RKRX RSKSX RLPMONOOSQU RTUVSWOXOYP RMONROTQUTUVTWRXO RPKUK RPXUX', + 1050: ' 35KZMVNXQXMRMONMOLQKTKVLWMXOXRTXWXXV ROUNRNOOMQK RTKVMWOWRVU RNWPW RUWWW', + 1051: ' 18KYTKKX RSMTX RTKUX RNTTT RIXNX RRXWX', + 1052: ' 34JYPKLX RQKMX RNKUKWLWNVPSQ RUKVLVNUPSQ ROQRQTRUSUUTWQXJX RRQTSTUSWQX', + 1053: ' 25KXVLWLXKWNVLTKRKPLOMNOMRMUNWPXRXTWUU RRKPMOONRNVPX', + 1054: ' 26JYPKLX RQKMX RNKTKVLWNWQVTUVTWQXJX RTKULVNVQUTTVSWQX', + 1055: ' 22JYPKLX RQKMX RSORS RNKXKWNWK ROQRQ RJXTXUUSX', + 1056: ' 20JXPKLX RQKMX RSORS RNKXKWNWK ROQRQ RJXOX', + 1057: ' 33KYVLWLXKWNVLTKRKPLOMNOMRMUNWPXRXTWUVVS RRKPMOONRNVPX RRXTVUS RSSXS', + 1058: ' 27J[PKLX RQKMX RXKTX RYKUX RNKSK RVK[K ROQVQ RJXOX RRXWX', + 1059: ' 12NWTKPX RUKQX RRKWK RNXSX', + 1060: ' 19LXUKRUQWPX RVKSURWPXOXMWLUMTNUMV RSKXK', + 1061: ' 27JZPKLX RQKMX RYKOR RRPTX RSPUX RNKSK RVK[K RJXOX RRXWX', + 1062: ' 14KXQKMX RRKNX ROKTK RKXUXVUTX', + 1063: ' 30I\\OKKX ROMPX RPKQV RYKPX RYKUX RZKVX RMKPK RYK\\K RIXMX RSXXX', + 1064: ' 21JZPKLX RPKTX RQKTU RXKTX RNKQK RVKZK RJXNX', + 1065: ' 32KYRKPLOMNOMRMUNWPXRXTWUVVTWQWNVLTKRK RRKPMOONRNVPX RRXTVUTVQVMTK', + 1066: ' 24JYPKLX RQKMX RNKUKWLXMXOWQTROR RUKWMWOVQTR RJXOX', + 1067: ' 46KYRKPLOMNOMRMUNWPXRXTWUVVTWQWNVLTKRK RRKPMOONRNVPX RRXTVUTVQVMTK ROWOVPUQURVRZS[T[UZ RRVSZT[', + 1068: ' 35JZPKLX RQKMX RNKUKWLXMXOWQTROR RUKWMWOVQTR RSRTWUXVXWW RSRTSUWVX RJXOX', + 1069: ' 28KZWLXLYKXNWLUKRKPLOMOOPPUSVT RONPOURVSVVUWSXPXNWMULXMWNW', + 1070: ' 16KZTKPX RUKQX RPKNNOKZKYNYK RNXSX', + 1071: ' 20J[PKMUMWOXSXUWVUYK RQKNUNWOX RNKSK RWK[K', + 1072: ' 15KYOKPX RPKQV RYKPX RMKRK RVK[K', + 1073: ' 24I[NKMX ROKNV RTKMX RTKSX RUKTV RZKSX RLKQK RXK\\K', + 1074: ' 21KZPKTX RQKUX RYKLX RNKSK RVK[K RJXOX RRXWX', + 1075: ' 20LYPKRQPX RQKSQ RYKSQQX RNKSK RVK[K RNXSX', + 1076: ' 16LYXKLX RYKMX RQKONPKYK RLXUXVUTX', + 1101: ' 32LZQOPPPQOQOPQOTOVQVWWXXX RTOUQUWWX RURRSPTOUOWPXSXTWUU RRSPUPWQX', + 1102: ' 29JYNKNX ROKOX RORPPROTOVPWRWUVWTXRXPWOU RTOUPVRVUUWTX RLKOK', + 1103: ' 24LXVQUQURVRVQUPSOQOOPNRNUOWQXSXUWVV RQOPPOROUPWQX', + 1104: ' 32L[VKVX RWKWX RVRUPSOQOOPNRNUOWQXSXUWVU RQOPPOROUPWQX RTKWK RVXYX', + 1105: ' 26LXOSVSVRUPSOQOOPNRNUOWQXSXUWVV RUSUQSO RQOPPOROUPWQX', + 1106: ' 20LWTKULUMVMVLTKRKPMPX RRKQMQX RNOSO RNXSX', + 1107: ' 42LYQOOQOSQUSUUSUQSOQO RQOPQPSQU RSUTSTQSO RTPUOVO RPTOUOXPYTYVZ ROWPXTXVYV[T\\P\\N[NYPX', + 1108: ' 28J[NKNX ROKOX RORPPROTOVPWRWX RTOUPVRVX RLKOK RLXQX RTXYX', + 1109: ' 18NWRKRLSLSKRK RRORX RSOSX RPOSO RPXUX', + 1110: ' 23NWSKSLTLTKSK RSOSZR\\ RTOTZR\\P\\O[OZPZP[O[ RQOTO', + 1111: ' 27JZNKNX ROKOX RWOOU RRSVX RSSWX RLKOK RTOYO RLXQX RTXYX', + 1112: ' 12NWRKRX RSKSX RPKSK RPXUX', + 1113: ' 44F_JOJX RKOKX RKRLPNOPORPSRSX RPOQPRRRX RSRTPVOXOZP[R[X RXOYPZRZX RHOKO RHXMX RPXUX RXX]X', + 1114: ' 28J[NONX ROOOX RORPPROTOVPWRWX RTOUPVRVX RLOOO RLXQX RTXYX', + 1115: ' 28LYQOOPNRNUOWQXTXVWWUWRVPTOQO RQOPPOROUPWQX RTXUWVUVRUPTO', + 1116: ' 32JYNON\\ ROOO\\ RORPPROTOVPWRWUVWTXRXPWOU RTOUPVRVUUWTX RLOOO RL\\Q\\', + 1117: ' 29KYUOU\\ RVOV\\ RURTPROPONPMRMUNWPXRXTWUU RPOOPNRNUOWPX RS\\X\\', + 1118: ' 22KXOOOX RPOPX RPRQPSOUOVPVQUQUPVP RMOPO RMXRX', + 1119: ' 26LYTOUPUQVQVPTOQOOPORQSTTVU ROQQRTSVTVWTXQXOWOVPVPWQX', + 1120: ' 14LWPKPVRXTXUWUV RQKQVRX RNOTO', + 1121: ' 28J[NONUOWQXSXUWVU ROOOUPWQX RVOVX RWOWX RLOOO RTOWO RVXYX', + 1122: ' 15KYNORX ROORV RVORX RLOQO RTOXO', + 1123: ' 24I[LOOX RMOOU RROOX RROUX RSOUU RXOUX RJOOO RVOZO', + 1124: ' 21KYNOUX ROOVX RVONX RLOQO RTOXO RLXPX RSXXX', + 1125: ' 23KYNORX ROORV RVORXP[N\\M\\L[LZMZM[L[ RLOQO RTOXO', + 1126: ' 16LXUONX RVOOX ROONQNOVO RNXVXVVUX', + 1127: ' 32K[QOOPNQMSMUNWPXQXSWUUWRXO RQOOQNSNUOWPX RQOSOUPWWXX RSOTPVWXXYX', + 1128: ' 40KXRKPMOOMUK\\ RQLPNNTL\\ RRKTKVLVNUPRQ RTKULUNTPRQ RRQTRUTUVTWRXQXOWNT RRQSRTTTVRX', + 1129: ' 19KYLQNOPORPSSSXR\\ RLQNPPPRQSS RWOVRSXQ\\', + 1130: ' 39KYSOQOOPNQMSMUNWPXRXTWUVVTVRUPRNQLQKRJTJUKVM RQOOQNSNVPX RRXTVUTUQSO RQLRKTKVM', + 1131: ' 27LXVPTOQOOPOQPRRS RQOPPPQRS RRSOTNUNWPXSXUW RRSPTOUOWPX', + 1132: ' 28LWRKQLQMSNVNVMSNPOOPNRNTOVPWRXSYS[R\\P\\O[ RSNQOPPOROTPVRX', + 1133: ' 26IYJRKPLONOOPOQMX RMONPNQLX ROQPPROTOVPVRS\\ RTOUPURR\\', + 1134: ' 35IYJSKQLPNPOQOVPX RMPNQNUOWPXQXSWTVUTVQVNULTKRKQLQNRPURWS RQXSVTTUQUNTK', + 1135: ' 13NWROPVPWQXSXUWVU RSOQVQWRX', + 1136: ' 26KYOOLX RPOMX RUOVPWPVOTORQOR RORPSRWTXVWWU RORQSSWTX', + 1137: ' 15LXLKNKPLWX RNKOLVX RRPMX RRPNX', + 1138: ' 26KZOOK\\ RPOL\\ RNUNWOXQXSWTV RVOTVTWUXWXXWYU RWOUVUWVX', + 1139: ' 19JYNOMX ROONUMX RVRVOWOVRTUQWNXMX RLOOO', + 1140: ' 36MXRKQLQMSNVN RTNQOPPPRRSUS RTNROQPQRRS RSSPTOUOWQXSYTZT[S\\Q\\ RSSQTPUPWQX', + 1141: ' 28KXQOOPNQMSMUNWPXRXTWUVVTVRUPSOQO RQOOQNSNVPX RRXTVUTUQSO', + 1142: ' 20IZPPMX RPPNX RTPSX RTPTX RKQMOXO RKQMPXP', + 1143: ' 29JXSOQOOPNQMSJ\\ RQOOQNSK\\ RSOUPVRVTUVTWRXPXNWMU RSOUQUTTVRX', + 1144: ' 28K[YOQOOPNQMSMUNWPXRXTWUVVTVRUPYP RQOOQNSNVPX RRXTVUTUQSO', + 1145: ' 14KZSPQX RSPRX RMQOOXO RMQOPXP', + 1146: ' 24JXKRLPMOOOPPPROUOWPX RNOOPORNUNWPXQXSWUUVRVOUOVP', + 1147: ' 35KZOPNQMSMUNWPXRXUWWUXRXPWOUOTPSRRUO\\ RMUNVPWRWUVWTXR RXQWPUPSR RRUQXP\\', + 1148: ' 17KXMONOPPS[T\\ RNOOPR[T\\U\\ RVOTRNYL\\', + 1149: ' 28I[TKQ\\ RUKP\\ RJRKPLONOOPOVPWSWUVWT RMONPNTOWPXSXUWWTXRYO', + 1150: ' 36JZNPPPPONPMQLSLUMWNXPXQWRUSR RLUNWPWRU RRRRWSXUXWVXTXRWPVOVPWP RRUSWUWWV', + 1151: ' 32KZVOTVTWUXWXXWYU RWOUVUWVX RUSUQSOQOOPNQMSMUNWPXRXTV RQOOQNSNVPX', + 1152: ' 32JXOKMR RPKNRNVPX RNROPQOSOUPVRVTUVTWRXPXNWMUMR RSOUQUTTVRX RMKPK', + 1153: ' 22KXUPUQVQUPSOQOOPNQMSMUNWPXRXTWUV RQOOQNSNVPX', + 1154: ' 35KZWKTVTWUXWXXWYU RXKUVUWVX RUSUQSOQOOPNQMSMUNWPXRXTV RQOOQNSNVPX RUKXK', + 1155: ' 23KWNURTTSURUPSOQOOPNQMSMUNWPXRXTWUV RQOOQNSNVPX', + 1156: ' 23MXWKXLXKVKTLSNPYO[N\\ RVKULTNQYP[N\\L\\L[M\\ RPOVO', + 1157: ' 34KYVOTVSYR[ RWOUVTYR[P\\M\\L[M[N\\ RUSUQSOQOOPNQMSMUNWPXRXTV RQOOQNSNVPX', + 1158: ' 29KZPKLX RQKMX ROQPPROTOVPVRUUUWVX RTOUPURTUTWUXWXXWYU RNKQK', + 1159: ' 26MWSKSLTLTKSK RNROPPOROSPSRRURWSX RQORPRRQUQWRXTXUWVU', + 1160: ' 26MWTKTLULUKTK RORPPQOSOTPTRRYQ[O\\M\\M[N\\ RROSPSRQYP[O\\', + 1161: ' 32KXPKLX RQKMX RVPUQVQVPUOTORQPROR RORPSQWRXTXUWVU RORQSRWSX RNKQK', + 1162: ' 16NVSKPVPWQXSXTWUU RTKQVQWRX RQKTK', + 1163: ' 46F^GRHPIOKOLPLQJX RJOKPKQIX RLQMPOOQOSPSQQX RQORPRQPX RSQTPVOXOZPZRYUYWZX RXOYPYRXUXWYX[X\\W]U', + 1164: ' 33J[KRLPMOOOPPPQNX RNOOPOQMX RPQQPSOUOWPWRVUVWWX RUOVPVRUUUWVXXXYWZU', + 1165: ' 28KXQOOPNQMSMUNWPXRXTWUVVTVRUPSOQO RQOOQNSNVPX RRXTVUTUQSO', + 1166: ' 35JYKRLPMOOOPPPQM\\ RNOOPOQL\\ RPQROTOVPWRWTVVUWSXQXOVOT RTOVQVTUVSX RJ\\O\\', + 1167: ' 28KYVOR\\ RWOS\\ RUSUQSOQOOPNQMSMUNWPXRXTV RQOOQNSNVPX RP\\U\\', + 1168: ' 22LXMRNPOOQORPRQPX RPOQPQQOX RRQSPUOVOWPWQVQWP', + 1169: ' 24LYVPVQWQVPTOQOOPORQSTTVU ROQQRTSVTVWTXQXOWNVOVOW', + 1170: ' 16NWSKPVPWQXSXTWUU RTKQVQWRX RPOUO', + 1171: ' 33IZJRKPLONOOPORNUNWOX RMONPNRMUMWOXQXSWTV RVOTVTWUXWXXWYU RWOUVUWVX', + 1172: ' 24JXKRLPMOOOPPPROUOWPX RNOOPORNUNWPXQXSWUUVRVOUOVP', + 1173: ' 37H\\IRJPKOMONPNRMUMWNX RLOMPMRLULWNXOXQWRV RTORVRWTX RUOSVSWTXUXWWYUZRZOYOZP', + 1174: ' 38JZMRNPPOROSPSR RQORPRRQUPWNXMXLWLVMVLW RXPWQXQXPWOVOTPSRRURWSX RQUQWRXTXVWWU', + 1175: ' 35IYJRKPLONOOPORNUNWOX RMONPNRMUMWOXQXSWTV RVOTVSYR[ RWOUVTYR[P\\M\\L[M[N\\', + 1176: ' 27KYWOWPVQNVMWMX RNQOOROUQ ROPRPUQVQ RNVOVRWUW ROVRXUXVV', + 1177: ' 39H[RKSLSMTMTLRKOKMLLNLX ROKNLMNMX RXKYLYMZMZLXKVKTMTX RVKUMUX RJOWO RJXOX RRXWX', + 1178: ' 29J[UKVLWLWKQKOLNNNX RQKPLONOX RVOVX RWOWX RLOWO RLXQX RTXYX', + 1179: ' 27J[WKQKOLNNNX RQKPLONOX RUKVLVX RWKWX RLOVO RLXQX RTXYX', + 1180: ' 48F_PKQLQMRMRLPKMKKLJNJX RMKLLKNKX RYKZL[L[KUKSLRNRX RUKTLSNSX RZOZX R[O[X RHO[O RHXMX RPXUX RXX]X', + 1181: ' 46F_PKQLQMRMRLPKMKKLJNJX RMKLLKNKX R[KUKSLRNRX RUKTLSNSX RYKZLZX R[K[X RHOZO RHXMX RPXUX RXX]X', + 1182: ' 12NWRORX RSOSX RPOSO RPXUX', + 1184: ' 21LXVPTOROPPOQNSNUOWQXSXUW RROPQOSOVQX ROSSS', + 1185: ' 35LYSKQLPMOONRNUOWPXRXTWUVVTWQWNVLUKSK RSKQMPOOSOVPX RRXTVUTVPVMUK ROQVQ', + 1186: ' 34KZTKQ\\ RUKP\\ RQONPMRMUNWQXTXWWXUXRWPTOQO RQOOPNRNUOWQX RTXVWWUWRVPTO', + 1187: ' 22LXUPVRVQUPSOQOOPNRNTOVRX RQOOQOTPVRXSYS[R\\P\\', + 1191: ' 45I[VKWLXLVKSKQLPMOOLYK[J\\ RSKQMPOMYL[J\\H\\H[I\\ RZK[L[KYKWLVNSYR[Q\\ RYKXLWNTYS[Q\\O\\O[P\\ RLOYO', + 1192: ' 38IZVKWLXLXKSKQLPMOOLYK[J\\ RSKQMPOMYL[J\\H\\H[I\\ RVOTVTWUXWXXWYU RWOUVUWVX RLOWO', + 1193: ' 38IZVKWL RXKSKQLPMOOLYK[J\\ RSKQMPOMYL[J\\H\\H[I\\ RWKTVTWUXWXXWYU RXKUVUWVX RLOVO', + 1194: ' 63F^SKTLTM RULSKPKNLMMLOIYH[G\\ RPKNMMOJYI[G\\E\\E[F\\ RZK[L\\L\\KWKUL RTMSOPYO[N\\ RWKUMTOQYP[N\\L\\L[M\\ RZOXVXWYX[X\\W]U R[OYVYWZX RIO[O', + 1195: ' 63F^SKTLTM RULSKPKNLMMLOIYH[G\\ RPKNMMOJYI[G\\E\\E[F\\ RZK[L R\\KWKUL RTMSOPYO[N\\ RWKUMTOQYP[N\\L\\L[M\\ R[KXVXWYX[X\\W]U R\\KYVYWZX RIOZO', + 1196: ' 20MWNROPPOROSPSRRURWSX RQORPRRQUQWRXTXUWVU', + 1200: ' 28LYQKOLNONTOWQXTXVWWTWOVLTKQK RQKPLOOOTPWQX RTXUWVTVOULTK', + 1201: ' 10LYPNSKSX RRLRX ROXVX', + 1202: ' 35LYOMONNNNMOLQKTKVLWNVPTQQROSNUNX RTKULVNUPTQ RNWOVPVSWVWWV RPVSXVXWVWU', + 1203: ' 39LYOMONNNNMOLQKTKVLWNVPTQ RTKULVNUPTQ RRQTQVRWTWUVWTXQXOWNVNUOUOV RTQURVTVUUWTX', + 1204: ' 13LYSMSX RTKTX RTKMTXT RQXVX', + 1205: ' 33LYOKNQ ROKVK ROLSLVK RNQOPQOTOVPWRWUVWTXQXOWNVNUOUOV RTOUPVRVUUWTX', + 1206: ' 36LYVMVNWNWMVLTKRKPLOMNPNUOWQXTXVWWUWSVQTPQPNR RRKPMOPOUPWQX RTXUWVUVSUQTP', + 1207: ' 22LYNKNO RVMRTPX RWKTQQX RNMPKRKUM RNMPLRLUMVM', + 1208: ' 51LYQKOLNNOPQQTQVPWNVLTKQK RQKPLONPPQQ RTQUPVNULTK RQQORNTNUOWQXTXVWWUWTVRTQ RQQPROTOUPWQX RTXUWVUVTURTQ', + 1209: ' 36LYOVOUNUNVOWQXSXUWVVWSWNVLTKQKOLNNNPORQSTSWQ RSXUVVSVNULTK RQKPLONOPPRQS', + 1210: ' 6NVRVQWRXSWRV', + 1211: ' 8NVSWRXQWRVSWSYQ[', + 1212: ' 12NVROQPRQSPRO RRVQWRXSWRV', + 1213: ' 14NVROQPRQSPRO RSWRXQWRVSWSYQ[', + 1214: ' 15NVRKQLRSSLRK RRLRO RRVQWRXSWRV', + 1215: ' 29LYNNONOONONNOLQKTKVLWNWOVQSRRSRTST RTKVMVPUQSR RRWRXSXSWRW', + 1216: ' 6OVRKRP RSKRP', + 1217: ' 12LXOKOP RPKOP RUKUP RVKUP', + 1218: ' 10MWQKPLPNQOSOTNTLSKQK', + 1219: ' 9MWRJRP ROKUO RUKOO', + 1220: ' 3KZXHM\\', + 1221: ' 16MWUHSJQMPPPTQWSZU\\ RSJRLQPQTRXSZ', + 1222: ' 16MWOHQJSMTPTTSWQZO\\ RQJRLSPSTRXQZ', + 1223: ' 12MWPHP\\ RQHQ\\ RPHUH RP\\U\\', + 1224: ' 12MWSHS\\ RTHT\\ ROHTH RO\\T\\', + 1225: ' 38LWSHQIPJPLRNSP RQIPL RSNRQ RPJQLSNSPRQPRRSSTSVQXPZ RRSSV RPXQ[ RSTRVPXPZQ[S\\', + 1226: ' 38MXQHSITJTLRNQP RSITL RQNRQ RTJSLQNQPRQTRRSQTQVSXTZ RRSQV RTXS[ RQTRVTXTZS[Q\\', + 1227: ' 4MWTHPRT\\', + 1228: ' 4MWPHTRP\\', + 1229: ' 3OURHR\\', + 1230: ' 6MWPHP\\ RTHT\\', + 1231: ' 3I[LRXR', + 1232: ' 6I[RLRX RLRXR', + 1233: ' 9JZRMRX RMRWR RMXWX', + 1234: ' 9JZRMRX RMMWM RMRWR', + 1235: ' 6JZMMWW RWMMW', + 1236: ' 6NVRQQRRSSRRQ', + 1237: ' 15I[RLQMRNSMRL RLRXR RRVQWRXSWRV', + 1238: ' 6I[LPXP RLTXT', + 1239: ' 9I[WLMX RLPXP RLTXT', + 1240: ' 9I[LNXN RLRXR RLVXV', + 1241: ' 4JZWLMRWX', + 1242: ' 4JZMLWRMX', + 1243: ' 10JZWKMOWS RMTWT RMXWX', + 1244: ' 10JZMKWOMS RMTWT RMXWX', + 1245: ' 21H[YUWUUTTSRPQOONNNLOKQKRLTNUOUQTRSTPUOWNYN', + 1246: ' 16JZLTLRMPOPUSWSXR RLRMQOQUTWTXRXP', + 1247: ' 8JZMSRPWS RMSRQWS', + 1248: ' 7NVSKPO RSKTLPO', + 1249: ' 7NVQKTO RQKPLTO', + 1250: ' 14LXNKOMQNSNUMVK RNKONQOSOUNVK', + 1251: ' 8NVSLRMQLRKSLSNQP', + 1252: ' 8NVSKQMQORPSORNQO', + 1253: ' 8NVQLRMSLRKQLQNSP', + 1254: ' 8NVQKSMSORPQORNSO', + 1256: ' 11JZWMQMONNOMQMSNUOVQWWW', + 1257: ' 11JZMMMSNUOVQWSWUVVUWSWM', + 1258: ' 11JZMMSMUNVOWQWSVUUVSWMW', + 1259: ' 11JZMWMQNOONQMSMUNVOWQWW', + 1260: ' 14JZWMQMONNOMQMSNUOVQWWW RMRUR', + 1261: ' 13I[TOUPXRUTTU RUPWRUT RLRWR', + 1262: ' 13MWRMRX ROPPORLTOUP RPORMTO', + 1263: ' 13I[POOPLROTPU ROPMROT RMRXR', + 1264: ' 13MWRLRW ROTPURXTUUT RPURWTU', + 1265: ' 37KYVSUPSOQOOPNQMSMUNWPXRXTWUVVTWQWNVLTKQKPLQLRK RQOOQNSNVPX RRXTVUTVQVNULTK', + 1266: ' 15JZLKRX RMKRV RXKRX RLKXK RNLWL', + 1267: ' 10G[IOLORW RKORX R[FRX', + 1268: ' 26I[XIXJYJYIXHVHTJSLROQUPYO[ RUITKSORUQXPZN\\L\\K[KZLZL[', + 1269: ' 40I[XIXJYJYIXHVHTJSLROQUPYO[ RUITKSORUQXPZN\\L\\K[KZLZL[ RQNOONQNSOUQVSVUUVSVQUOSNQN', + 1270: ' 26H\\ZRYTWUVUTTSSQPPONNMNKOJQJRKTMUNUPTQSSPTOVNWNYOZQZR', + 1271: ' 26JZXKLX ROKPLPNOOMOLNLLMKOKSLVLXK RUTTUTWUXWXXWXUWTUT', + 1272: ' 41J[YPXPXQYQYPXOWOVPUTTVSWQXOXMWLVLTMSORRPSNSLRKPKOLONPQUWWXXXYW ROXMVMTOR RONPPVWWX', + 1273: ' 29J[UPSOQOPQPRQTSTUS RUOUSVTXTYRYQXNVLSKRKOLMNLQLRMUOWRXSXVW', + 1274: ' 34KZQHQ\\ RTHT\\ RWLVLVMWMWLUKPKNLNNOPVSWT RNNOOVRWTWVVWTXQXOWNVNUOUOVNV', + 1275: ' 12KYRKN\\ RVKR\\ RNQWQ RMVVV', + 1276: ' 40LXTLSLSMTMTLSKQKPLPNQPTRUS RPNQOTQUSUUSW RQPOROTPVSXTY ROTPUSWTYT[S\\Q\\P[PZQZQ[P[', + 1277: ' 29LXRKQLRMSLRK RRMRQ RRQQSRVSSRQ RRVR\\ RPOONNOOPPOTOUNVOUPTO', + 1278: ' 42LXRMSLRKQLRMRQQRSURV RRQSRQURVRZQ[R\\S[RZ RPOONNOOPPOTOUNVOUPTO RPXOWNXOYPXTXUWVXUYTX', + 1279: ' 12LYVKVX RNKVK RQQVQ RNXVX', + 1281: ' 24H\\QKNLLNKQKSLVNXQYSYVXXVYSYQXNVLSKQK RRQQRRSSRRQ', + 1282: ' 33LYQKPLPMQN RTKULUMTN RRNPOOQORPTRUSUUTVRVQUOSNRN RRURY RSUSY ROWVW', + 1283: ' 23LYRKPLONOOPQRRSRUQVOVNULSKRK RRRRX RSRSX ROUVU', + 1284: ' 24H\\QKNLLNKQKSLVNXQYSYVXXVYSYQXNVLSKQK RRKRY RKRYR', + 1285: ' 25JYRRPQOQMRLTLUMWOXPXRWSUSTRR RWMRR RRMWMWR RRMVNWR', + 1286: ' 25JZLLMKOKQLRNRPQRPSNT ROKPLQNQQPS RVKUX RWKTX RNTXT', + 1287: ' 27JYNKNU ROKNR RNROPQOSOUPVQVTTVTXUYVYWX RSOUQUTTV RLKOK', + 1288: ' 27LYONRKRQ RVNSKSQ RRQPROTOUPWRXSXUWVUVTURSQ RRTRUSUSTRT', + 1289: ' 27JZRKRY RMKMPNRPSTSVRWPWK RLMMKNM RQMRKSM RVMWKXM ROVUV', + 1290: ' 27JYNKNX ROKOX RLKSKVLWNWOVQSROR RSKULVNVOUQSR RLXVXVUUX', + 1291: ' 20LYWKTKQLONNQNSOVQXTYWY RWKTLRNQQQSRVTXWY', + 1292: ' 23JZRRPQOQMRLTLUMWOXPXRWSUSTRR RSLQQ RWMRR RXQSS', + 1293: ' 12KYPMTW RTMPW RMPWT RWPMT', + 1294: ' 34J[OUMULVLXMYOYPXPVNTMRMONMOLQKTKVLWMXOXRWTUVUXVYXYYXYVXUVU RNMPLULWM', + 1295: ' 34J[OOMOLNLLMKOKPLPNNPMRMUNWOXQYTYVXWWXUXRWPUNULVKXKYLYNXOVO RNWPXUXWW', + 1401: ' 21F^KHK\\ RLHL\\ RXHX\\ RYHY\\ RHH\\H RH\\O\\ RU\\\\\\', + 1402: ' 20H]KHRQJ\\ RJHQQ RJHYHZMXH RK[X[ RJ\\Y\\ZWX\\', + 1403: ' 20KYVBTDRGPKOPOTPYR]T`Vb RTDRHQKPPPTQYR\\T`', + 1404: ' 20KYNBPDRGTKUPUTTYR]P`Nb RPDRHSKTPTTSYR\\P`', + 1405: ' 12KYOBOb RPBPb ROBVB RObVb', + 1406: ' 12KYTBTb RUBUb RNBUB RNbUb', + 1407: ' 40KYTBRCQDPFPHQJRKSMSOQQ RRCQEQGRISJTLTNSPORSTTVTXSZR[Q]Q_Ra RQSSUSWRYQZP\\P^Q`RaTb', + 1408: ' 40KYPBRCSDTFTHSJRKQMQOSQ RRCSESGRIQJPLPNQPURQTPVPXQZR[S]S_Ra RSSQUQWRYSZT\\T^S`RaPb', + 1409: ' 24KYU@RCPFOIOLPOSVTYT\\S_Ra RRCQEPHPKQNTUUXU[T^RaOd', + 1410: ' 24KYO@RCTFUIULTOQVPYP\\Q_Ra RRCSETHTKSNPUOXO[P^RaUd', + 1411: ' 13AXCRGRR` RGSRa RFSRb RX:Rb', + 1412: ' 32F^[CZD[E\\D\\C[BYBWCUETGSJRNPZO^N` RVDUFTJRVQZP]O_MaKbIbHaH`I_J`Ia', + 2001: ' 18H\\RFK[ RRFY[ RRIX[ RMUVU RI[O[ RU[[[', + 2002: ' 45G]LFL[ RMFM[ RIFUFXGYHZJZLYNXOUP RUFWGXHYJYLXNWOUP RMPUPXQYRZTZWYYXZU[I[ RUPWQXRYTYWXYWZU[', + 2003: ' 32G\\XIYLYFXIVGSFQFNGLIKKJNJSKVLXNZQ[S[VZXXYV RQFOGMILKKNKSLVMXOZQ[', + 2004: ' 30G]LFL[ RMFM[ RIFSFVGXIYKZNZSYVXXVZS[I[ RSFUGWIXKYNYSXVWXUZS[', + 2005: ' 22G\\LFL[ RMFM[ RSLST RIFYFYLXF RMPSP RI[Y[YUX[', + 2006: ' 20G[LFL[ RMFM[ RSLST RIFYFYLXF RMPSP RI[P[', + 2007: ' 40G^XIYLYFXIVGSFQFNGLIKKJNJSKVLXNZQ[S[VZXX RQFOGMILKKNKSLVMXOZQ[ RXSX[ RYSY[ RUS\\S', + 2008: ' 27F^KFK[ RLFL[ RXFX[ RYFY[ RHFOF RUF\\F RLPXP RH[O[ RU[\\[', + 2009: ' 12MXRFR[ RSFS[ ROFVF RO[V[', + 2010: ' 20KZUFUWTZR[P[NZMXMVNUOVNW RTFTWSZR[ RQFXF', + 2011: ' 27F\\KFK[ RLFL[ RYFLS RQOY[ RPOX[ RHFOF RUF[F RH[O[ RU[[[', + 2012: ' 14I[NFN[ ROFO[ RKFRF RK[Z[ZUY[', + 2013: ' 30F_KFK[ RLFRX RKFR[ RYFR[ RYFY[ RZFZ[ RHFLF RYF]F RH[N[ RV[][', + 2014: ' 21G^LFL[ RMFYY RMHY[ RYFY[ RIFMF RVF\\F RI[O[', + 2015: ' 44G]QFNGLIKKJOJRKVLXNZQ[S[VZXXYVZRZOYKXIVGSFQF RQFOGMILKKOKRLVMXOZQ[ RS[UZWXXVYRYOXKWIUGSF', + 2016: ' 29G]LFL[ RMFM[ RIFUFXGYHZJZMYOXPUQMQ RUFWGXHYJYMXOWPUQ RI[P[', + 2017: ' 64G]QFNGLIKKJOJRKVLXNZQ[S[VZXXYVZRZOYKXIVGSFQF RQFOGMILKKOKRLVMXOZQ[ RS[UZWXXVYRYOXKWIUGSF RNYNXOVQURUTVUXV_W`Y`Z^Z] RUXV\\W^X_Y_Z^', + 2018: ' 45G]LFL[ RMFM[ RIFUFXGYHZJZLYNXOUPMP RUFWGXHYJYLXNWOUP RI[P[ RRPTQURXYYZZZ[Y RTQUSWZX[Z[[Y[X', + 2019: ' 34H\\XIYFYLXIVGSFPFMGKIKKLMMNOOUQWRYT RKKMMONUPWQXRYTYXWZT[Q[NZLXKUK[LX', + 2020: ' 16I\\RFR[ RSFS[ RLFKLKFZFZLYF RO[V[', + 2021: ' 23F^KFKULXNZQ[S[VZXXYUYF RLFLUMXOZQ[ RHFOF RVF\\F', + 2022: ' 15H\\KFR[ RLFRX RYFR[ RIFOF RUF[F', + 2023: ' 24F^JFN[ RKFNV RRFN[ RRFV[ RSFVV RZFV[ RGFNF RWF]F', + 2024: ' 21H\\KFX[ RLFY[ RYFK[ RIFOF RUF[F RI[O[ RU[[[', + 2025: ' 20H]KFRQR[ RLFSQS[ RZFSQ RIFOF RVF\\F RO[V[', + 2026: ' 16H\\XFK[ RYFL[ RLFKLKFYF RK[Y[YUX[', + 2027: ' 18H\\RFK[ RRFY[ RRIX[ RMUVU RI[O[ RU[[[', + 2028: ' 45G]LFL[ RMFM[ RIFUFXGYHZJZLYNXOUP RUFWGXHYJYLXNWOUP RMPUPXQYRZTZWYYXZU[I[ RUPWQXRYTYWXYWZU[', + 2029: ' 14I[NFN[ ROFO[ RKFZFZLYF RK[R[', + 2030: ' 15H\\RFJ[ RRFZ[ RRIY[ RKZYZ RJ[Z[', + 2031: ' 22G\\LFL[ RMFM[ RSLST RIFYFYLXF RMPSP RI[Y[YUX[', + 2032: ' 16H\\XFK[ RYFL[ RLFKLKFYF RK[Y[YUX[', + 2033: ' 27F^KFK[ RLFL[ RXFX[ RYFY[ RHFOF RUF\\F RLPXP RH[O[ RU[\\[', + 2034: ' 56G]QFNGLIKKJOJRKVLXNZQ[S[VZXXYVZRZOYKXIVGSFQF RQFOGMILKKOKRLVMXOZQ[ RS[UZWXXVYRYOXKWIUGSF ROMOT RUMUT ROPUP ROQUQ', + 2035: ' 12MXRFR[ RSFS[ ROFVF RO[V[', + 2036: ' 27F\\KFK[ RLFL[ RYFLS RQOY[ RPOX[ RHFOF RUF[F RH[O[ RU[[[', + 2037: ' 15H\\RFK[ RRFY[ RRIX[ RI[O[ RU[[[', + 2038: ' 30F_KFK[ RLFRX RKFR[ RYFR[ RYFY[ RZFZ[ RHFLF RYF]F RH[N[ RV[][', + 2039: ' 21G^LFL[ RMFYY RMHY[ RYFY[ RIFMF RVF\\F RI[O[', + 2040: ' 36G]KEJJ RZEYJ RONNS RVNUS RKWJ\\ RZWY\\ RKGYG RKHYH ROPUP ROQUQ RKYYY RKZYZ', + 2041: ' 44G]QFNGLIKKJOJRKVLXNZQ[S[VZXXYVZRZOYKXIVGSFQF RQFOGMILKKOKRLVMXOZQ[ RS[UZWXXVYRYOXKWIUGSF', + 2042: ' 21F^KFK[ RLFL[ RXFX[ RYFY[ RHF\\F RH[O[ RU[\\[', + 2043: ' 29G]LFL[ RMFM[ RIFUFXGYHZJZMYOXPUQMQ RUFWGXHYJYMXOWPUQ RI[P[', + 2044: ' 20H]KFRPJ[ RJFQP RJFYFZLXF RKZXZ RJ[Y[ZUX[', + 2045: ' 16I\\RFR[ RSFS[ RLFKLKFZFZLYF RO[V[', + 2046: ' 33I\\KKKILGMFOFPGQIRMR[ RKIMGOGQI RZKZIYGXFVFUGTISMS[ RZIXGVGTI RO[V[', + 2047: ' 48H]RFR[ RSFS[ RPKMLLMKOKRLTMUPVUVXUYTZRZOYMXLUKPK RPKNLMMLOLRMTNUPV RUVWUXTYRYOXMWLUK ROFVF RO[V[', + 2048: ' 21H\\KFX[ RLFY[ RYFK[ RIFOF RUF[F RI[O[ RU[[[', + 2049: ' 41G^RFR[ RSFS[ RIMJLLMMQNSOTQU RJLKMLQMSNTQUTUWTXSYQZM[L RTUVTWSXQYM[L\\M ROFVF RO[V[', + 2050: ' 43G]JXK[O[MWKSJPJLKIMGPFTFWGYIZLZPYSWWU[Y[ZX RMWLTKPKLLINGPF RTFVGXIYLYPXTWW RKZNZ RVZYZ', + 2051: ' 18H\\UFH[ RUFV[ RTHU[ RLUUU RF[L[ RR[X[', + 2052: ' 41F^OFI[ RPFJ[ RLFWFZG[I[KZNYOVP RWFYGZIZKYNXOVP RMPVPXQYSYUXXVZR[F[ RVPWQXSXUWXUZR[', + 2053: ' 34H]ZH[H\\F[L[JZHYGWFTFQGOIMLLOKSKVLYMZP[S[UZWXXV RTFRGPINLMOLSLVMYNZP[', + 2054: ' 30F]OFI[ RPFJ[ RLFUFXGYHZKZOYSWWUYSZO[F[ RUFWGXHYKYOXSVWTYRZO[', + 2055: ' 22F]OFI[ RPFJ[ RTLRT RLF[FZLZF RMPSP RF[U[WVT[', + 2056: ' 20F\\OFI[ RPFJ[ RTLRT RLF[FZLZF RMPSP RF[M[', + 2057: ' 42H^ZH[H\\F[L[JZHYGWFTFQGOIMLLOKSKVLYMZP[R[UZWXYT RTFRGPINLMOLSLVMYNZP[ RR[TZVXXT RUT\\T', + 2058: ' 27E_NFH[ ROFI[ R[FU[ R\\FV[ RKFRF RXF_F RLPXP RE[L[ RR[Y[', + 2059: ' 12LYUFO[ RVFP[ RRFYF RL[S[', + 2060: ' 21I[XFSWRYQZO[M[KZJXJVKULVKW RWFRWQYO[ RTF[F', + 2061: ' 27F]OFI[ RPFJ[ R]FLS RSOW[ RROV[ RLFSF RYF_F RF[M[ RS[Y[', + 2062: ' 14H\\QFK[ RRFL[ RNFUF RH[W[YUV[', + 2063: ' 30E`NFH[ RNFO[ ROFPY R\\FO[ R\\FV[ R]FW[ RKFOF R\\F`F RE[K[ RS[Z[', + 2064: ' 21F_OFI[ ROFVX ROIV[ R\\FV[ RLFOF RYF_F RF[L[', + 2065: ' 42G]SFPGNILLKOJSJVKYLZN[Q[TZVXXUYRZNZKYHXGVFSF RSFQGOIMLLOKSKVLYN[ RQ[SZUXWUXRYNYKXHVF', + 2066: ' 27F]OFI[ RPFJ[ RLFXF[G\\I\\K[NYPUQMQ RXFZG[I[KZNXPUQ RF[M[', + 2067: ' 61G]SFPGNILLKOJSJVKYLZN[Q[TZVXXUYRZNZKYHXGVFSF RSFQGOIMLLOKSKVLYN[ RQ[SZUXWUXRYNYKXHVF RLYLXMVOUPURVSXS_T`V`W^W] RSXT^U_V_W^', + 2068: ' 42F^OFI[ RPFJ[ RLFWFZG[I[KZNYOVPMP RWFYGZIZKYNXOVP RRPTQURVZW[Y[ZYZX RURWYXZYZZY RF[M[', + 2069: ' 35G^ZH[H\\F[L[JZHYGVFRFOGMIMKNMONVRXT RMKOMVQWRXTXWWYVZS[O[LZKYJWJUI[JYKY', + 2070: ' 16H]UFO[ RVFP[ ROFLLNF]F\\L\\F RL[S[', + 2071: ' 25F_NFKQJUJXKZN[R[UZWXXU\\F ROFLQKUKXLZN[ RKFRF RYF_F', + 2072: ' 15H\\NFO[ ROFPY R\\FO[ RLFRF RXF^F', + 2073: ' 24E_MFK[ RNFLY RUFK[ RUFS[ RVFTY R]FS[ RJFQF RZF`F', + 2074: ' 21G]NFU[ ROFV[ R\\FH[ RLFRF RXF^F RF[L[ RR[X[', + 2075: ' 20H]NFRPO[ ROFSPP[ R]FSP RLFRF RYF_F RL[S[', + 2076: ' 16G][FH[ R\\FI[ ROFLLNF\\F RH[V[XUU[', + 2077: ' 46H\\KILKXWYYY[ RLLXX RKIKKLMXYY[ RPPLTKVKXLZK[ RKVMZ RLTLVMXMZK[ RSSXN RVIVLWNYNYLWKVI RVIWLYN', + 2101: ' 39I]NONPMPMONNPMTMVNWOXQXXYZZ[ RWOWXXZZ[[[ RWQVRPSMTLVLXMZP[S[UZWX RPSNTMVMXNZP[', + 2102: ' 33G\\LFL[ RMFM[ RMPONQMSMVNXPYSYUXXVZS[Q[OZMX RSMUNWPXSXUWXUZS[ RIFMF', + 2103: ' 28H[WPVQWRXQXPVNTMQMNNLPKSKULXNZQ[S[VZXX RQMONMPLSLUMXOZQ[', + 2104: ' 36H]WFW[ RXFX[ RWPUNSMQMNNLPKSKULXNZQ[S[UZWX RQMONMPLSLUMXOZQ[ RTFXF RW[[[', + 2105: ' 31H[LSXSXQWOVNTMQMNNLPKSKULXNZQ[S[VZXX RWSWPVN RQMONMPLSLUMXOZQ[', + 2106: ' 22KXUGTHUIVHVGUFSFQGPIP[ RSFRGQIQ[ RMMUM RM[T[', + 2107: ' 60I\\QMONNOMQMSNUOVQWSWUVVUWSWQVOUNSMQM RONNPNTOV RUVVTVPUN RVOWNYMYNWN RNUMVLXLYM[P\\U\\X]Y^ RLYMZP[U[X\\Y^Y_XaUbObLaK_K^L\\O[', + 2108: ' 28G]LFL[ RMFM[ RMPONRMTMWNXPX[ RTMVNWPW[ RIFMF RI[P[ RT[[[', + 2109: ' 18MXRFQGRHSGRF RRMR[ RSMS[ ROMSM RO[V[', + 2110: ' 25MXSFRGSHTGSF RTMT_SaQbObNaN`O_P`Oa RSMS_RaQb RPMTM', + 2111: ' 27G\\LFL[ RMFM[ RWMMW RRSX[ RQSW[ RIFMF RTMZM RI[P[ RT[Z[', + 2112: ' 12MXRFR[ RSFS[ ROFSF RO[V[', + 2113: ' 44BcGMG[ RHMH[ RHPJNMMOMRNSPS[ ROMQNRPR[ RSPUNXMZM]N^P^[ RZM\\N]P][ RDMHM RD[K[ RO[V[ RZ[a[', + 2114: ' 28G]LML[ RMMM[ RMPONRMTMWNXPX[ RTMVNWPW[ RIMMM RI[P[ RT[[[', + 2115: ' 36H\\QMNNLPKSKULXNZQ[S[VZXXYUYSXPVNSMQM RQMONMPLSLUMXOZQ[ RS[UZWXXUXSWPUNSM', + 2116: ' 36G\\LMLb RMMMb RMPONQMSMVNXPYSYUXXVZS[Q[OZMX RSMUNWPXSXUWXUZS[ RIMMM RIbPb', + 2117: ' 33H\\WMWb RXMXb RWPUNSMQMNNLPKSKULXNZQ[S[UZWX RQMONMPLSLUMXOZQ[ RTb[b', + 2118: ' 23IZNMN[ ROMO[ ROSPPRNTMWMXNXOWPVOWN RKMOM RK[R[', + 2119: ' 32J[WOXMXQWOVNTMPMNNMOMQNRPSUUWVXW RMPNQPRUTWUXVXYWZU[Q[OZNYMWM[NY', + 2120: ' 16KZPFPWQZS[U[WZXX RQFQWRZS[ RMMUM', + 2121: ' 28G]LMLXMZP[R[UZWX RMMMXNZP[ RWMW[ RXMX[ RIMMM RTMXM RW[[[', + 2122: ' 15I[LMR[ RMMRY RXMR[ RJMPM RTMZM', + 2123: ' 24F^JMN[ RKMNX RRMN[ RRMV[ RSMVX RZMV[ RGMNM RWM]M', + 2124: ' 21H\\LMW[ RMMX[ RXML[ RJMPM RTMZM RJ[P[ RT[Z[', + 2125: ' 22H[LMR[ RMMRY RXMR[P_NaLbKbJaK`La RJMPM RTMZM', + 2126: ' 16I[WML[ RXMM[ RMMLQLMXM RL[X[XWW[', + 2127: ' 40G^QMNNLPKRJUJXKZN[P[RZUWWTYPZM RQMONMPLRKUKXLZN[ RQMSMUNVPXXYZZ[ RSMTNUPWXXZZ[[[', + 2128: ' 57G\\TFQGOIMMLPKTJZIb RTFRGPINMMPLTKZJb RTFVFXGYHYKXMWNTOPO RVFXHXKWMVNTO RPOTPVRWTWWVYUZR[P[NZMYLV RPOSPURVTVWUYTZR[', + 2129: ' 28H\\IPKNMMOMQNROSRSVRZOb RJOLNPNRO RZMYPXRSYP^Nb RYMXPWRSY', + 2130: ' 44I\\VNTMRMONMQLTLWMYNZP[R[UZWWXTXQWOSJRHRFSEUEWFYH RRMPNNQMTMXNZ RR[TZVWWTWPVNTKSISGTFVFYH', + 2131: ' 32I[XPVNTMPMNNNPPRSS RPMONOPQRSS RSSNTLVLXMZP[S[UZWX RSSOTMVMXNZP[', + 2132: ' 31I[TFRGQHQIRJUKZKZJWKSMPOMRLULWMYP[S]T_TaSbQbPa RULQONRMUMWNYP[', + 2133: ' 32G]HQIOKMNMONOPNTL[ RMMNNNPMTK[ RNTPPRNTMVMXNYOYRXWUb RVMXOXRWWTb', + 2134: ' 44F]GQHOJMMMNNNPMUMXNZO[ RLMMNMPLULXMZO[Q[SZUXWUXRYMYIXGVFTFRHRJSMUPWRZT RSZUWVUWRXMXIWGVF', + 2135: ' 15LXRMPTOXOZP[S[UYVW RSMQTPXPZQ[', + 2136: ' 29H\\NMJ[ ROMK[ RXMYNZNYMWMUNQROSMS ROSQTSZT[ ROSPTRZS[U[WZYW', + 2137: ' 23H\\KFMFOGPHQJWXXZY[ RMFOHPJVXWZY[Z[ RRMJ[ RRMK[', + 2138: ' 28F]MMGb RNMHb RMPLVLYN[P[RZTXVU RXMUXUZV[Y[[Y\\W RYMVXVZW[', + 2139: ' 24H\\NML[ ROMNSMXL[ RYMXQVU RZMYPXRVUTWQYOZL[ RKMOM', + 2140: ' 45IZTFRGQHQIRJUKXK RUKQLOMNONQPSSTVT RUKRLPMOOOQQSST RSTOUMVLXLZN\\S^T_TaRbPb RSTPUNVMXMZO\\S^', + 2141: ' 32I[RMONMQLTLWMYNZP[R[UZWWXTXQWOVNTMRM RRMPNNQMTMXNZ RR[TZVWWTWPVN', + 2142: ' 22G]PNL[ RPNM[ RVNV[ RVNW[ RIPKNNM[M RIPKONN[N', + 2143: ' 31H[LVMYNZP[R[UZWWXTXQWOVNTMRMONMQLTHb RR[TZVWWTWPVN RRMPNNQMTIb', + 2144: ' 35H][MQMNNLQKTKWLYMZO[Q[TZVWWTWQVOUNSM RQMONMQLTLXMZ RQ[SZUWVTVPUN RUN[N', + 2145: ' 16H\\SNP[ RSNQ[ RJPLNOMZM RJPLOONZN', + 2146: ' 31H\\IQJOLMOMPNPPNVNYP[ RNMONOPMVMYNZP[Q[TZVXXUYRYOXMWNXOYR RXUYO', + 2147: ' 37G]ONMOKQJTJWKYLZN[Q[TZWXYUZRZOXMVMTORSPXMb RJWLYNZQZTYWWYU RZOXNVNTPRSPYNb', + 2148: ' 23I[KMMMONPPU_VaWb RMMNNOPT_UaWbYb RZMYOWRM]K`Jb', + 2149: ' 34F]UFOb RVFNb RGQHOJMMMNNNPMUMXOZRZTYWVYS RLMMNMPLULXMZO[R[TZVXXUYS[M', + 2150: ' 44F]JQLOONNMLNJQITIWJZK[M[OZQWRT RIWJYKZMZOYQW RQTQWRZS[U[WZYWZTZQYNXMWNYOZQ RQWRYSZUZWYYW', + 2151: ' 39H]XMVTUXUZV[Y[[Y\\W RYMWTVXVZW[ RVTVQUNSMQMNNLQKTKWLYMZO[Q[SZUWVT RQMONMQLTLXMZ', + 2152: ' 36H[PFLSLVMYNZ RQFMS RMSNPPNRMTMVNWOXQXTWWUZR[P[NZMWMS RVNWPWTVWTZR[ RMFQF', + 2153: ' 25I[WPWQXQXPWNUMRMONMQLTLWMYNZP[R[UZWW RRMPNNQMTMXNZ', + 2154: ' 42H]ZFVTUXUZV[Y[[Y\\W R[FWTVXVZW[ RVTVQUNSMQMNNLQKTKWLYMZO[Q[SZUWVT RQMONMQLTLXMZ RWF[F', + 2155: ' 26I[MVQUTTWRXPWNUMRMONMQLTLWMYNZP[R[UZWX RRMPNNQMTMXNZ', + 2156: ' 35KZZGYHZI[H[GZFXFVGUHTJSMP[O_Na RXFVHUJTNRWQ[P^O`NaLbJbIaI`J_K`Ja ROMYM', + 2157: ' 43H\\YMU[T^RaObLbJaI`I_J^K_J` RXMT[S^QaOb RVTVQUNSMQMNNLQKTKWLYMZO[Q[SZUWVT RQMONMQLTLXMZ', + 2158: ' 31H]PFJ[ RQFK[ RMTOPQNSMUMWNXOXQVWVZW[ RUMWOWQUWUZV[Y[[Y\\W RMFQF', + 2159: ' 26LYUFTGUHVGUF RMQNOPMSMTNTQRWRZS[ RRMSNSQQWQZR[U[WYXW', + 2160: ' 32LYVFUGVHWGVF RNQOOQMTMUNUQR[Q^P`OaMbKbJaJ`K_L`Ka RSMTNTQQ[P^O`Mb', + 2161: ' 34H\\PFJ[ RQFK[ RXNWOXPYOYNXMWMUNQROSMS ROSQTSZT[ ROSPTRZS[U[WZYW RMFQF', + 2162: ' 18MYUFQTPXPZQ[T[VYWW RVFRTQXQZR[ RRFVF', + 2163: ' 52AbBQCOEMHMINIPHTF[ RGMHNHPGTE[ RHTJPLNNMPMRNSOSQP[ RPMRORQO[ RRTTPVNXMZM\\N]O]Q[W[Z\\[ RZM\\O\\QZWZZ[[^[`YaW', + 2164: ' 37F]GQHOJMMMNNNPMTK[ RLMMNMPLTJ[ RMTOPQNSMUMWNXOXQVWVZW[ RUMWOWQUWUZV[Y[[Y\\W', + 2165: ' 32I[RMONMQLTLWMYNZP[R[UZWWXTXQWOVNTMRM RRMPNNQMTMXNZ RR[TZVWWTWPVN', + 2166: ' 42G\\HQIOKMNMONOPNTJb RMMNNNPMTIb RNTOQQNSMUMWNXOYQYTXWVZS[Q[OZNWNT RWNXPXTWWUZS[ RFbMb', + 2167: ' 33H\\XMRb RYMSb RVTVQUNSMQMNNLQKTKWLYMZO[Q[SZUWVT RQMONMQLTLXMZ RObVb', + 2168: ' 26IZJQKOMMPMQNQPPTN[ ROMPNPPOTM[ RPTRPTNVMXMYNYOXPWOXN', + 2169: ' 28J[XOXPYPYOXNUMRMONNONQORVVWW RNPOQVUWVWYVZS[P[MZLYLXMXMY', + 2170: ' 18KYTFPTOXOZP[S[UYVW RUFQTPXPZQ[ RNMWM', + 2171: ' 37F]GQHOJMMMNNNQLWLYN[ RLMMNMQKWKYLZN[P[RZTXVT RXMVTUXUZV[Y[[Y\\W RYMWTVXVZW[', + 2172: ' 26H\\IQJOLMOMPNPQNWNYP[ RNMONOQMWMYNZP[Q[TZVXXUYQYMXMYO', + 2173: ' 41C`DQEOGMJMKNKQIWIYK[ RIMJNJQHWHYIZK[M[OZQXRV RTMRVRYSZU[W[YZ[X\\V]R]M\\M]O RUMSVSYU[', + 2174: ' 42H\\KQMNOMRMSOSR RQMRORRQVPXNZL[K[JZJYKXLYKZ RQVQYR[U[WZYW RYNXOYPZOZNYMXMVNTPSRRVRYS[', + 2175: ' 41G\\HQIOKMNMONOQMWMYO[ RMMNNNQLWLYMZO[Q[SZUXWT RZMV[U^SaPbMbKaJ`J_K^L_K` RYMU[T^RaPb', + 2176: ' 31H\\YMXOVQNWLYK[ RLQMOOMRMVO RMOONRNVOXO RLYNYRZUZWY RNYR[U[WYXW', + 2177: ' 43G^VGUHVIWHWGUFRFOGMILLL[ RRFPGNIMLM[ R\\G[H\\I]H]G\\FZFXGWIW[ RZFYGXIX[ RIM[M RI[P[ RT[[[', + 2178: ' 33G]WGVHWIXHWGUFRFOGMILLL[ RRFPGNIMLM[ RWMW[ RXMX[ RIMXM RI[P[ RT[[[', + 2179: ' 35G]VGUHVIWHWGUF RXFRFOGMILLL[ RRFPGNIMLM[ RWHW[ RXFX[ RIMWM RI[P[ RT[[[', + 2180: ' 54BcRGQHRISHRGPFMFJGHIGLG[ RMFKGIIHLH[ R]G\\H]I^H]G[FXFUGSIRLR[ RXFVGTISLS[ R]M][ R^M^[ RDM^M RD[K[ RO[V[ RZ[a[', + 2181: ' 56BcRGQHRISHRGPFMFJGHIGLG[ RMFKGIIHLH[ R\\G[H\\I]H]G[F R^FXFUGSIRLR[ RXFVGTISLS[ R]H][ R^F^[ RDM]M RD[K[ RO[V[ RZ[a[', + 2182: ' 12MXRMR[ RSMS[ ROMSM RO[V[', + 2184: ' 25IZWNUMRMONMPLSLVMYNZQ[T[VZ RRMPNNPMSMVNYOZQ[ RMTUT', + 2185: ' 43I\\TFQGOJNLMOLTLXMZO[Q[TZVWWUXRYMYIXGVFTF RTFRGPJOLNOMTMXNZO[ RQ[SZUWVUWRXMXIWGVF RNPWP', + 2186: ' 42G]UFOb RVFNb RQMMNKPJSJVKXMZP[S[WZYXZUZRYPWNTMQM RQMNNLPKSKVLXNZP[ RS[VZXXYUYRXPVNTM', + 2187: ' 27I[TMVNXPXOWNTMQMNNMOLQLSMUOWSZ RQMONNOMQMSNUSZT\\T^S_Q_', + 2190: ' 45G]LMKNJPJRKUOYP[ RJRKTOXP[P]O`MbLbKaJ_J\\KXMTOQRNTMVMYNZPZTYXWZU[T[SZSXTWUXTY RVMXNYPYTXXWZ', + 2191: ' 69E_YGXHYIZHYGWFTFQGOINKMNLRJ[I_Ha RTFRGPIOKNNLWK[J^I`HaFbDbCaC`D_E`Da R_G^H_I`H`G_F]F[GZHYJXMU[T_Sa R]F[HZJYNWWV[U^T`SaQbObNaN`O_P`Oa RIM^M', + 2192: ' 52F^[GZH[I\\H[GXFUFRGPIOKNNMRK[J_Ia RUFSGQIPKONMWL[K^J`IaGbEbDaD`E_F`Ea RYMWTVXVZW[Z[\\Y]W RZMXTWXWZX[ RJMZM', + 2193: ' 54F^YGXHYIZHZGXF R\\FUFRGPIOKNNMRK[J_Ia RUFSGQIPKONMWL[K^J`IaGbEbDaD`E_F`Ea R[FWTVXVZW[Z[\\Y]W R\\FXTWXWZX[ RJMYM', + 2194: ' 86@cTGSHTIUHTGRFOFLGJIIKHNGRE[D_Ca ROFMGKIJKINGWF[E^D`CaAb?b>a>`?_@`?a R`G_H`IaH`G]FZFWGUITKSNRRP[O_Na RZFXGVIUKTNRWQ[P^O`NaLbJbIaI`J_K`Ja R^M\\T[X[Z\\[_[aYbW R_M]T\\X\\Z][ RDM_M', + 2195: ' 88@cTGSHTIUHTGRFOFLGJIIKHNGRE[D_Ca ROFMGKIJKINGWF[E^D`CaAb?b>a>`?_@`?a R^G]H^I_H_G]F RaFZFWGUITKSNRRP[O_Na RZFXGVIUKTNRWQ[P^O`NaLbJbIaI`J_K`Ja R`F\\T[X[Z\\[_[aYbW RaF]T\\X\\Z][ RDM^M', + 2196: ' 20LYMQNOPMSMTNTQRWRZS[ RRMSNSQQWQZR[U[WYXW', + 2200: ' 40H\\QFNGLJKOKRLWNZQ[S[VZXWYRYOXJVGSFQF RQFOGNHMJLOLRMWNYOZQ[ RS[UZVYWWXRXOWJVHUGSF', + 2201: ' 11H\\NJPISFS[ RRGR[ RN[W[', + 2202: ' 45H\\LJMKLLKKKJLHMGPFTFWGXHYJYLXNUPPRNSLUKXK[ RTFVGWHXJXLWNTPPR RKYLXNXSZVZXYYX RNXS[W[XZYXYV', + 2203: ' 47H\\LJMKLLKKKJLHMGPFTFWGXIXLWNTOQO RTFVGWIWLVNTO RTOVPXRYTYWXYWZT[P[MZLYKWKVLUMVLW RWQXTXWWYVZT[', + 2204: ' 13H\\THT[ RUFU[ RUFJUZU RQ[X[', + 2205: ' 39H\\MFKP RKPMNPMSMVNXPYSYUXXVZS[P[MZLYKWKVLUMVLW RSMUNWPXSXUWXUZS[ RMFWF RMGRGWF', + 2206: ' 48H\\WIVJWKXJXIWGUFRFOGMILKKOKULXNZQ[S[VZXXYUYTXQVOSNRNOOMQLT RRFPGNIMKLOLUMXOZQ[ RS[UZWXXUXTWQUOSN', + 2207: ' 31H\\KFKL RKJLHNFPFUIWIXHYF RLHNGPGUI RYFYIXLTQSSRVR[ RXLSQRSQVQ[', + 2208: ' 63H\\PFMGLILLMNPOTOWNXLXIWGTFPF RPFNGMIMLNNPO RTOVNWLWIVGTF RPOMPLQKSKWLYMZP[T[WZXYYWYSXQWPTO RPONPMQLSLWMYNZP[ RT[VZWYXWXSWQVPTO', + 2209: ' 48H\\XMWPURRSQSNRLPKMKLLINGQFSFVGXIYLYRXVWXUZR[O[MZLXLWMVNWMX RQSORMPLMLLMIOGQF RSFUGWIXLXRWVVXTZR[', + 2210: ' 6MWRYQZR[SZRY', + 2211: ' 8MWR[QZRYSZS\\R^Q_', + 2212: ' 12MWRMQNROSNRM RRYQZR[SZRY', + 2213: ' 14MWRMQNROSNRM RR[QZRYSZS\\R^Q_', + 2214: ' 15MWRFQHRTSHRF RRHRN RRYQZR[SZRY', + 2215: ' 32I[MJNKMLLKLJMHNGPFSFVGWHXJXLWNVORQRT RSFUGVHWJWLVNTP RRYQZR[SZRY', + 2216: ' 6NVRFQM RSFQM', + 2217: ' 12JZNFMM ROFMM RVFUM RWFUM', + 2218: ' 14KYQFOGNINKOMQNSNUMVKVIUGSFQF', + 2219: ' 9JZRFRR RMIWO RWIMO', + 2220: ' 3G][BIb', + 2221: ' 20KYVBTDRGPKOPOTPYR]T`Vb RTDRHQKPPPTQYR\\T`', + 2222: ' 20KYNBPDRGTKUPUTTYR]P`Nb RPDRHSKTPTTSYR\\P`', + 2223: ' 12KYOBOb RPBPb ROBVB RObVb', + 2224: ' 12KYTBTb RUBUb RNBUB RNbUb', + 2225: ' 40KYTBRCQDPFPHQJRKSMSOQQ RRCQEQGRISJTLTNSPORSTTVTXSZR[Q]Q_Ra RQSSUSWRYQZP\\P^Q`RaTb', + 2226: ' 40KYPBRCSDTFTHSJRKQMQOSQ RRCSESGRIQJPLPNQPURQTPVPXQZR[S]S_Ra RSSQUQWRYSZT\\T^S`RaPb', + 2227: ' 4KYUBNRUb', + 2228: ' 4KYOBVROb', + 2229: ' 3NVRBRb', + 2230: ' 6KYOBOb RUBUb', + 2231: ' 3E_IR[R', + 2232: ' 6E_RIR[ RIR[R', + 2233: ' 9F^RJR[ RJRZR RJ[Z[', + 2234: ' 9F^RJR[ RJJZJ RJRZR', + 2235: ' 6G]KKYY RYKKY', + 2236: ' 6MWRQQRRSSRRQ', + 2237: ' 15E_RIQJRKSJRI RIR[R RRYQZR[SZRY', + 2238: ' 6E_IO[O RIU[U', + 2239: ' 9E_YIK[ RIO[O RIU[U', + 2240: ' 9E_IM[M RIR[R RIW[W', + 2241: ' 4F^ZIJRZ[', + 2242: ' 4F^JIZRJ[', + 2243: ' 10F^ZFJMZT RJVZV RJ[Z[', + 2244: ' 10F^JFZMJT RJVZV RJ[Z[', + 2245: ' 21F_[WYWWVUTRPQOONMNKOJQJSKUMVOVQURTUPWNYM[M', + 2246: ' 24F^IUISJPLONOPPTSVTXTZS[Q RISJQLPNPPQTTVUXUZT[Q[O', + 2247: ' 8G]JTROZT RJTRPZT', + 2248: ' 7LXTFOL RTFUGOL', + 2249: ' 7LXPFUL RPFOGUL', + 2250: ' 18H\\KFLHNJQKSKVJXHYF RKFLINKQLSLVKXIYF', + 2251: ' 8MWRHQGRFSGSIRKQL', + 2252: ' 8MWSFRGQIQKRLSKRJ', + 2253: ' 8MWRHSGRFQGQIRKSL', + 2254: ' 8MWQFRGSISKRLQKRJ', + 2255: ' 10E[HMLMRY RKMR[ R[BR[', + 2256: ' 13F^ZJSJOKMLKNJQJSKVMXOYSZZZ', + 2257: ' 13F^JJJQKULWNYQZSZVYXWYUZQZJ', + 2258: ' 13F^JJQJUKWLYNZQZSYVWXUYQZJZ', + 2259: ' 13F^JZJSKOLMNKQJSJVKXMYOZSZZ', + 2260: ' 16F^ZJSJOKMLKNJQJSKVMXOYSZZZ RJRVR', + 2261: ' 11E_XP[RXT RUMZRUW RIRZR', + 2262: ' 11JZPLRITL RMORJWO RRJR[', + 2263: ' 11E_LPIRLT ROMJROW RJR[R', + 2264: ' 11JZPXR[TX RMURZWU RRIRZ', + 2265: ' 44I\\XRWOVNTMRMONMQLTLWMYNZP[R[UZWXXUYPYKXHWGUFRFPGOHOIPIPH RRMPNNQMTMXNZ RR[TZVXWUXPXKWHUF', + 2266: ' 15H\\JFR[ RKFRY RZFR[ RJFZF RKGYG', + 2267: ' 10AbDMIMRY RHNR[ Rb:R[', + 2268: ' 32F^[CZD[E\\D\\C[BYBWCUETGSJRNPZO^N` RVDUFTJRVQZP]O_MaKbIbHaH`I_J`Ia', + 2269: ' 50F^[CZD[E\\D\\C[BYBWCUETGSJRNPZO^N` RVDUFTJRVQZP]O_MaKbIbHaH`I_J`Ia RQKNLLNKQKSLVNXQYSYVXXVYSYQXNVLSKQK', + 2270: ' 26F_\\S[UYVWVUUTTQPPONNLNJOIQISJULVNVPUQTTPUOWNYN[O\\Q\\S', + 2271: ' 32F^[FI[ RNFPHPJOLMMKMIKIIJGLFNFPGSHVHYG[F RWTUUTWTYV[X[ZZ[X[VYTWT', + 2272: ' 49F_[NZO[P\\O\\N[MZMYNXPVUTXRZP[M[JZIXIUJSPORMSKSIRGPFNGMIMKNNPQUXWZZ[[[\\Z\\Y RM[KZJXJUKSMQ RMKNMVXXZZ[', + 2273: ' 56E`WNVLTKQKOLNMMPMSNUPVSVUUVS RQKOMNPNSOUPV RWKVSVUXVZV\\T]Q]O\\L[JYHWGTFQFNGLHJJILHOHRIUJWLYNZQ[T[WZYYZX RXKWSWUXV', + 2274: ' 42H\\PBP_ RTBT_ RXIWJXKYJYIWGTFPFMGKIKKLMMNOOUQWRYT RKKMMONUPWQXRYTYXWZT[P[MZKXKWLVMWLX', + 2275: ' 12H]SFLb RYFRb RLQZQ RKWYW', + 2276: ' 46JZUITJUKVJVIUGSFQFOGNINKOMQOVR ROMTPVRWTWVVXTZ RPNNPMRMTNVPXU[ RNVSYU[V]V_UaSbQbOaN_N^O]P^O_', + 2277: ' 30JZRFQHRJSHRF RRFRb RRQQTRbSTRQ RLMNNPMNLLM RLMXM RTMVNXMVLTM', + 2278: ' 56JZRFQHRJSHRF RRFRT RRPQRSVRXQVSRRP RRTRb RR^Q`RbS`R^ RLMNNPMNLLM RLMXM RTMVNXMVLTM RL[N\\P[NZL[ RL[X[ RT[V\\X[VZT[', + 2279: ' 12I\\XFX[ RKFXF RPPXP RK[X[', + 2281: ' 38E`QFNGKIILHOHRIUKXNZQ[T[WZZX\\U]R]O\\LZIWGTFQF RROQPQQRRSRTQTPSORO RRPRQSQSPRP', + 2282: ' 45J[PFNGOIQJ RPFOGOI RUFWGVITJ RUFVGVI RQJOKNLMNMQNSOTQUTUVTWSXQXNWLVKTJQJ RRUR[ RSUS[ RNXWX', + 2283: ' 27I\\RFOGMILLLMMPORRSSSVRXPYMYLXIVGSFRF RRSR[ RSSS[ RNWWW', + 2284: ' 28D`PFMGJIHLGOGSHVJYM[P\\T\\W[ZY\\V]S]O\\LZIWGTFPF RRFR\\ RGQ]Q', + 2285: ' 31G`PMMNKPJSJTKWMYPZQZTYVWWTWSVPTNQMPM R]GWG[HUN R]G]M\\IVO R\\HVN', + 2286: ' 28F\\IIJGLFOFQGRIRLQOPQNSKU ROFPGQIQMPPNS RVFT[ RWFS[ RKUYU', + 2287: ' 30I\\MFMU RNFMQ RMQNOONQMTMWNXPXRWTUV RTMVNWPWRTXTZU[W[YY RKFNF', + 2288: ' 44I\\RNOOMQLTLUMXOZR[S[VZXXYUYTXQVOSNRN RRHNJRFRN RSHWJSFSN RRSQTQURVSVTUTTSSRS RRTRUSUSTRT', + 2289: ' 37G^QHRFR[ RTHSFS[ RJHKFKMLPNRQSRS RMHLFLNMQ R[HZFZMYPWRTSSS RXHYFYNXQ RNWWW', + 2290: ' 31G]LFL[ RMFM[ RIFUFXGYHZJZMYOXPUQMQ RUFWGXHYJYMXOWPUQ RI[Y[YVX[', + 2291: ' 24H[YGUGQHNJLMKPKSLVNYQ[U\\Y\\ RYGVHSJQMPPPSQVSYV[Y\\', + 2292: ' 27F_OQMQKRJSIUIWJYKZM[O[QZRYSWSURSQROQ RSHPQ RZJRR R\\QST', + 2293: ' 12H\\OKUY RUKOY RKOYU RYOKU', + 2294: ' 48F^NVLUKUIVHXHYI[K\\L\\N[OYOXNVKRJOJMKJMHPGTGWHYJZMZOYRVVUXUYV[X\\Y\\[[\\Y\\X[VYUXUVV RJMKKMIPHTHWIYKZM', + 2295: ' 48F^NMLNKNIMHKHJIHKGLGNHOJOKNMKQJTJVKYM[P\\T\\W[YYZVZTYQVMUKUJVHXGYG[H\\J\\K[MYNXNVM RJVKXMZP[T[WZYXZV', + 2301: ' 40F_JMILIJJHLGNGPHQIRKSP RIJKHMHOIPJQLRPR[ R[M\\L\\J[HYGWGUHTISKRP R\\JZHXHVIUJTLSPS[', + 2302: ' 51F^IGJKKMMOPPTPWOYMZK[G RIGJJKLMNPOTOWNYLZJ[G RPONPMQLSLVMXOZQ[S[UZWXXVXSWQVPTO RPPNQMSMVNY RVYWVWSVQTP', + 2303: ' 30F^MJMV RNKNU RVKVU RWJWV RIGKIMJPKTKWJYI[G RIYKWMVPUTUWVYW[Y', + 2304: ' 48F^[ILIJJILINJPLQNQPPQNQLPJ[J RIMJOKPMQ RQMPKOJMI RIXXXZW[U[SZQXPVPTQSSSUTWIW R[TZRYQWP RSTTVUWWX', + 2305: ' 48F]OUMTLTJUIWIXJZL[M[OZPXPWOUJPINIKJILHOGSGWHYJZLZOYRVUUWUYV[X[YZZX RMSKPJNJKKILH RSGVHXJYLYOXRVU', + 2306: ' 48G_HKKHMKMV RJILLLV RMKPHRKRU ROIQLQU RRKUHWKW[ RTIVLV[ RWKZH[J\\M\\P[SZUXWUYP[ RYIZJ[M[PZSYUWWTYP[', + 2307: ' 41F^ISMSLRKOKMLJNHQGSGVHXJYMYOXRWS[S RITOTMRLOLMMJOHQG RSGUHWJXMXOWRUT[T RKXYX RKYYY', + 2308: ' 30F_GLJIMLMX RIJLMLX RMLPISLSX ROJRMRX RSLVIYLYW[Y RUJXMXXZZ]W', + 2309: ' 33G]ZIJY RZIWJQJ RXKUKQJ RZIYLYR RXKXNYR RQRJR RPSMSJR RQRQY RPSPVQY', + 2310: ' 33F^HOJKOU RJMOWRPWPZO[M[KZIXHWHUITKTMUPVRWUWXUZ RWHVIUKUMWQXTXWWYUZ', + 2311: ' 36F^IOLLPN RKMOORLUN RQMTOWLYN RVMXO[L RIULRPT RKSOURRUT RQSTUWRYT RVSXU[R', + 2312: ' 48F^JHNJPLQOQRPUNWJY RJHMIOJQLRO RRRQUOWMXJY RZHWIUJSLRO RRRSUUWWXZY RZHVJTLSOSRTUVWZY RIP[P RIQ[Q', + 2317: ' 12NVQQQSSSSQQQ RQQSS RSQQS', + 2318: ' 18JZMPQRTTVVWYW[V]U^ RMQST RMRPSTUVWWY', + 2319: ' 18JZWKVMTOPQMR RSPMS RUFVGWIWKVNTPQRMT', + 2320: ' 36H\\SMONLPKRKTLVNWQWUVXTYRYPXNVMSM RXNSM RVMQNLP RONKR RLVQW RNWSVXT RUVYR', + 2321: ' 36H\\SMONLPKRKTLVNWQWUVXTYRYPXNVMSM RXNSM RVMQNLP RONKR RLVQW RNWSVXT RUVYR', + 2322: ' 34J[SMPNNPMRMTNVPWRWUVWTXRXPWNUMSM ROPUM RNRVN RMTWO RNUXP ROVWR RPWVT', + 2323: ' 18JZOGO^ RUFU] RMNWL RMOWM RMWWU RMXWV', + 2324: ' 18JZNFNX RVLV^ RNNVL RNOVM RNWVU RNXVV', + 2325: ' 25JZNBNW RNNQLTLVMWOWQVSSUQVNW RNNQMTMVN RUMVOVQUSSU', + 2326: ' 18E_HIHL R\\I\\L RHI\\I RHJ\\J RHK\\K RHL\\L', + 2327: ' 18JZMNMQ RWNWQ RMNWN RMOWO RMPWP RMQWQ', + 2328: ' 49JZMLWX RMLONQOTOVNWMWKUKUMTO RONTO RQOWM RVKVN RULWL RWXUVSUPUNVMWMYOYOWPU RUVPU RSUMW RNVNY RMXOX', + 2329: ' 26JZPOOMOKMKMMNNPOSOUNWL RNKNN RMLOL RMMSO RPOUN RWLWY', + 2330: ' 86A^GfHfIeIdHcGcFdFfGhIiKiNhPfQdR`RUQ;Q4R/S-U,V,X-Y/Y3X6W8U;P?JCHEFHEJDNDREVGYJ[N\\R\\V[XZZW[T[PZMYKWITHPHMIKKJNJRKUMW RGdGeHeHdGd RU;Q?LCIFGIFKENERFVGXJ[ RR\\U[WZYWZTZPYMXKVITH', + 2331: '103EfNSOUQVSVUUVSVQUOSNQNOONPMSMVNYP[S\\V\\Y[[Y\\W]T]P\\MZJXIUHRHOIMJKLIOHSHXI]KaMcPeTfYf]e`cba RKLJNIRIXJ\\L`NbQdUeYe]d_cba RPOTO ROPUP RNQVQ RNRVR RNSVS ROTUT RPUTU RaLaNcNcLaL RbLbN RaMcM RaVaXcXcVaV RbVbX RaWcW', + 2332: ' 30D`H@Hd RM@Md RW@Wd R\\@\\d RMMWK RMNWL RMOWM RMWWU RMXWV RMYWW', + 2367: ' 12NVQQQSSSSQQQ RQQSS RSQQS', + 2368: ' 18JZMPQRTTVVWYW[V]U^ RMQST RMRPSTUVWWY', + 2369: ' 18JZWKVMTOPQMR RSPMS RUFVGWIWKVNTPQRMT', + 2370: ' 32H\\PMMNLOKQKSLUMVPWTWWVXUYSYQXOWNTMPM RMNLPLSMUNVPW RWVXTXQWOVNTM', + 2371: ' 36H\\SMONLPKRKTLVNWQWUVXTYRYPXNVMSM RXNSM RVMQNLP RONKR RLVQW RNWSVXT RUVYR', + 2372: ' 34J[SMPNNPMRMTNVPWRWUVWTXRXPWNUMSM ROPUM RNRVN RMTWO RNUXP ROVWR RPWVT', + 2373: ' 18JZOGO^ RUFU] RMNWL RMOWM RMWWU RMXWV', + 2374: ' 18JZNFNX RVLV^ RNNVL RNOVM RNWVU RNXVV', + 2375: ' 25JZNBNW RNNQLTLVMWOWQVSSUQVNW RNNQMTMVN RUMVOVQUSSU', + 2376: ' 18E_HIHL R\\I\\L RHI\\I RHJ\\J RHK\\K RHL\\L', + 2377: ' 18JZMNMQ RWNWQ RMNWN RMOWO RMPWP RMQWQ', + 2378: ' 36JZQCVMRTRU RULQS RTITKPRRUUY RW\\UYSXQXOYN[N]O_Ra RW\\UZSYOYO]P_Ra RSXPZN]', + 2379: ' 26JZPOOMOKMKMMNNPOSOUNWL RNKNN RMLOL RMMSO RPOUN RWLSY', + 2380: ' 86A^GfHfIeIdHcGcFdFfGhIiKiNhPfQdR`RUQ;Q4R/S-U,V,X-Y/Y3X6W8U;P?JCHEFHEJDNDREVGYJ[N\\R\\V[XZZW[T[PZMYKWITHPHMIKKJNJRKUMW RGdGeHeHdGd RU;Q?LCIFGIFKENERFVGXJ[ RR\\U[WZYWZTZPYMXKVITH', + 2381: ' 89IjNQOOQNSNUOVQVSUUSVQVOUNTMQMNNKPISHWH[I^K`NaRaW`[_]]`ZcVfQiMk RWHZI]K_N`R`W_[^]\\`YcTgQi RPOTO ROPUP RNQVQ RNRVR RNSVS ROTUT RPUTU ReLeNgNgLeL RfLfN ReMgM ReVeXgXgVeV RfVfX ReWgW', + 2382: ' 85D`H>Hf RI>If RM>Mf RQBSBSDQDQAR?T>W>Y?[A\\D\\I[LYNWOUOSNRLQNOQNROSQVRXSVUUWUYV[X\\[\\`[cYeWfTfReQcQ`S`SbQb RRBRD RQCSC RY?ZA[D[IZLYN RRLRNPQNRPSRVRX RYVZX[[[`ZcYe RR`Rb RQaSa', + 2401: ' 21AcHBHb RIBIb R[B[b R\\B\\b RDB`B RDbMb RWb`b', + 2402: ' 23BaGBQPFb RFBPP REBPQ REB\\B^I[B RGa\\a RFb\\b^[[b', + 2403: ' 28I[X+U1R8P=OANFMNMVN^OcPgRlUsXy RU1S6Q +} + +const SYMB: Record = { + '\\frac': { glyph: 0, arity: 2, flags: {} }, + '\\binom': { glyph: 0, arity: 2, flags: {} }, + '\\sqrt': { + glyph: 2267, + arity: 1, + flags: { opt: true, xfl: true, yfl: true } + }, + '^': { glyph: 0, arity: 1, flags: {} }, + _: { glyph: 0, arity: 1, flags: {} }, + '(': { glyph: 2221, arity: 0, flags: { yfl: true } }, + ')': { glyph: 2222, arity: 0, flags: { yfl: true } }, + '[': { glyph: 2223, arity: 0, flags: { yfl: true } }, + ']': { glyph: 2224, arity: 0, flags: { yfl: true } }, + '\\langle': { glyph: 2227, arity: 0, flags: { yfl: true } }, + '\\rangle': { glyph: 2228, arity: 0, flags: { yfl: true } }, + '|': { glyph: 2229, arity: 0, flags: { yfl: true } }, + + '\\|': { glyph: 2230, arity: 0, flags: { yfl: true } }, + '\\{': { glyph: 2225, arity: 0, flags: { yfl: true } }, + '\\}': { glyph: 2226, arity: 0, flags: { yfl: true } }, + + '\\#': { glyph: 2275, arity: 0, flags: {} }, + '\\$': { glyph: 2274, arity: 0, flags: {} }, + '\\&': { glyph: 2273, arity: 0, flags: {} }, + '\\%': { glyph: 2271, arity: 0, flags: {} }, + + /*semantics*/ + '\\begin': { glyph: 0, arity: 1, flags: {} }, + '\\end': { glyph: 0, arity: 1, flags: {} }, + '\\left': { glyph: 0, arity: 1, flags: {} }, + '\\right': { glyph: 0, arity: 1, flags: {} }, + '\\middle': { glyph: 0, arity: 1, flags: {} }, + + /*operators*/ + '\\cdot': { glyph: 2236, arity: 0, flags: {} }, + '\\pm': { glyph: 2233, arity: 0, flags: {} }, + '\\mp': { glyph: 2234, arity: 0, flags: {} }, + '\\times': { glyph: 2235, arity: 0, flags: {} }, + '\\div': { glyph: 2237, arity: 0, flags: {} }, + '\\leqq': { glyph: 2243, arity: 0, flags: {} }, + '\\geqq': { glyph: 2244, arity: 0, flags: {} }, + '\\leq': { glyph: 2243, arity: 0, flags: {} }, + '\\geq': { glyph: 2244, arity: 0, flags: {} }, + '\\propto': { glyph: 2245, arity: 0, flags: {} }, + '\\sim': { glyph: 2246, arity: 0, flags: {} }, + '\\equiv': { glyph: 2240, arity: 0, flags: {} }, + '\\dagger': { glyph: 2277, arity: 0, flags: {} }, + '\\ddagger': { glyph: 2278, arity: 0, flags: {} }, + '\\ell': { glyph: 662, arity: 0, flags: {} }, + + /*accents*/ + '\\vec': { + glyph: 2261, + arity: 1, + flags: { hat: true, xfl: true, yfl: true } + }, + '\\overrightarrow': { + glyph: 2261, + arity: 1, + flags: { hat: true, xfl: true, yfl: true } + }, + '\\overleftarrow': { + glyph: 2263, + arity: 1, + flags: { hat: true, xfl: true, yfl: true } + }, + '\\bar': { glyph: 2231, arity: 1, flags: { hat: true, xfl: true } }, + '\\overline': { glyph: 2231, arity: 1, flags: { hat: true, xfl: true } }, + '\\widehat': { + glyph: 2247, + arity: 1, + flags: { hat: true, xfl: true, yfl: true } + }, + '\\hat': { glyph: 2247, arity: 1, flags: { hat: true } }, + '\\acute': { glyph: 2248, arity: 1, flags: { hat: true } }, + '\\grave': { glyph: 2249, arity: 1, flags: { hat: true } }, + '\\breve': { glyph: 2250, arity: 1, flags: { hat: true } }, + '\\tilde': { glyph: 2246, arity: 1, flags: { hat: true } }, + '\\underline': { glyph: 2231, arity: 1, flags: { mat: true, xfl: true } }, + + '\\not': { glyph: 2220, arity: 1, flags: {} }, + + '\\neq': { glyph: 2239, arity: 1, flags: {} }, + '\\ne': { glyph: 2239, arity: 1, flags: {} }, + '\\exists': { glyph: 2279, arity: 0, flags: {} }, + '\\in': { glyph: 2260, arity: 0, flags: {} }, + '\\subset': { glyph: 2256, arity: 0, flags: {} }, + '\\supset': { glyph: 2258, arity: 0, flags: {} }, + '\\cup': { glyph: 2257, arity: 0, flags: {} }, + '\\cap': { glyph: 2259, arity: 0, flags: {} }, + '\\infty': { glyph: 2270, arity: 0, flags: {} }, + '\\partial': { glyph: 2265, arity: 0, flags: {} }, + '\\nabla': { glyph: 2266, arity: 0, flags: {} }, + '\\aleph': { glyph: 2077, arity: 0, flags: {} }, + '\\wp': { glyph: 2190, arity: 0, flags: {} }, + '\\therefore': { glyph: 740, arity: 0, flags: {} }, + '\\mid': { glyph: 2229, arity: 0, flags: {} }, + + '\\sum': { glyph: 2402, arity: 0, flags: { big: true } }, + '\\prod': { glyph: 2401, arity: 0, flags: { big: true } }, + '\\bigoplus': { glyph: 2284, arity: 0, flags: { big: true } }, + '\\bigodot': { glyph: 2281, arity: 0, flags: { big: true } }, + '\\int': { glyph: 2412, arity: 0, flags: { yfl: true } }, + '\\oint': { glyph: 2269, arity: 0, flags: { yfl: true } }, + '\\oplus': { glyph: 1284, arity: 0, flags: {} }, + '\\odot': { glyph: 1281, arity: 0, flags: {} }, + '\\perp': { glyph: 738, arity: 0, flags: {} }, + '\\angle': { glyph: 739, arity: 0, flags: {} }, + '\\triangle': { glyph: 842, arity: 0, flags: {} }, + '\\Box': { glyph: 841, arity: 0, flags: {} }, + + '\\rightarrow': { glyph: 2261, arity: 0, flags: {} }, + '\\to': { glyph: 2261, arity: 0, flags: {} }, + '\\leftarrow': { glyph: 2263, arity: 0, flags: {} }, + '\\gets': { glyph: 2263, arity: 0, flags: {} }, + '\\circ': { glyph: 902, arity: 0, flags: {} }, + '\\bigcirc': { glyph: 904, arity: 0, flags: {} }, + '\\bullet': { glyph: 828, arity: 0, flags: {} }, + '\\star': { glyph: 856, arity: 0, flags: {} }, + '\\diamond': { glyph: 743, arity: 0, flags: {} }, + '\\ast': { glyph: 728, arity: 0, flags: {} }, + + /*verbatim symbols*/ + '\\log': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\ln': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\exp': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\mod': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\lim': { glyph: 0, arity: 0, flags: { txt: true, big: true } }, + + '\\sin': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\cos': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\tan': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\csc': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\sec': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\cot': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\sinh': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\cosh': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\tanh': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\csch': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\sech': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\coth': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\arcsin': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\arccos': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\arctan': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\arccsc': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\arcsec': { glyph: 0, arity: 0, flags: { txt: true } }, + '\\arccot': { glyph: 0, arity: 0, flags: { txt: true } }, + + /*font modes*/ + '\\text': { glyph: 0, arity: 1, flags: {} }, + '\\mathnormal': { glyph: 0, arity: 1, flags: {} }, + '\\mathrm': { glyph: 0, arity: 1, flags: {} }, + '\\mathit': { glyph: 0, arity: 1, flags: {} }, + '\\mathbf': { glyph: 0, arity: 1, flags: {} }, + '\\mathsf': { glyph: 0, arity: 1, flags: {} }, + '\\mathtt': { glyph: 0, arity: 1, flags: {} }, + '\\mathfrak': { glyph: 0, arity: 1, flags: {} }, + '\\mathcal': { glyph: 0, arity: 1, flags: {} }, + '\\mathbb': { glyph: 0, arity: 1, flags: {} }, + '\\mathscr': { glyph: 0, arity: 1, flags: {} }, + '\\rm': { glyph: 0, arity: 1, flags: {} }, + '\\it': { glyph: 0, arity: 1, flags: {} }, + '\\bf': { glyph: 0, arity: 1, flags: {} }, + '\\sf': { glyph: 0, arity: 1, flags: {} }, + '\\tt': { glyph: 0, arity: 1, flags: {} }, + '\\frak': { glyph: 0, arity: 1, flags: {} }, + '\\cal': { glyph: 0, arity: 1, flags: {} }, + '\\bb': { glyph: 0, arity: 1, flags: {} }, + '\\scr': { glyph: 0, arity: 1, flags: {} }, + + '\\quad': { glyph: 0, arity: 0, flags: {} }, + '\\,': { glyph: 0, arity: 0, flags: {} }, + '\\.': { glyph: 0, arity: 0, flags: {} }, + '\\;': { glyph: 0, arity: 0, flags: {} }, + '\\!': { glyph: 0, arity: 0, flags: {} }, + + /*greek letters*/ + '\\alpha': { glyph: 2127, flags: {} }, + '\\beta': { glyph: 2128, flags: {} }, + '\\gamma': { glyph: 2129, flags: {} }, + '\\delta': { glyph: 2130, flags: {} }, + '\\varepsilon': { glyph: 2131, flags: {} }, + '\\zeta': { glyph: 2132, flags: {} }, + '\\eta': { glyph: 2133, flags: {} }, + '\\vartheta': { glyph: 2134, flags: {} }, + '\\iota': { glyph: 2135, flags: {} }, + '\\kappa': { glyph: 2136, flags: {} }, + '\\lambda': { glyph: 2137, flags: {} }, + '\\mu': { glyph: 2138, flags: {} }, + '\\nu': { glyph: 2139, flags: {} }, + '\\xi': { glyph: 2140, flags: {} }, + '\\omicron': { glyph: 2141, flags: {} }, + '\\pi': { glyph: 2142, flags: {} }, + '\\rho': { glyph: 2143, flags: {} }, + '\\sigma': { glyph: 2144, flags: {} }, + '\\tau': { glyph: 2145, flags: {} }, + '\\upsilon': { glyph: 2146, flags: {} }, + '\\varphi': { glyph: 2147, flags: {} }, + '\\chi': { glyph: 2148, flags: {} }, + '\\psi': { glyph: 2149, flags: {} }, + '\\omega': { glyph: 2150, flags: {} }, + + '\\epsilon': { glyph: 2184, flags: {} }, + '\\theta': { glyph: 2185, flags: {} }, + '\\phi': { glyph: 2186, flags: {} }, + '\\varsigma': { glyph: 2187, flags: {} }, + + '\\Alpha': { glyph: 2027, flags: {} }, + '\\Beta': { glyph: 2028, flags: {} }, + '\\Gamma': { glyph: 2029, flags: {} }, + '\\Delta': { glyph: 2030, flags: {} }, + '\\Epsilon': { glyph: 2031, flags: {} }, + '\\Zeta': { glyph: 2032, flags: {} }, + '\\Eta': { glyph: 2033, flags: {} }, + '\\Theta': { glyph: 2034, flags: {} }, + '\\Iota': { glyph: 2035, flags: {} }, + '\\Kappa': { glyph: 2036, flags: {} }, + '\\Lambda': { glyph: 2037, flags: {} }, + '\\Mu': { glyph: 2038, flags: {} }, + '\\Nu': { glyph: 2039, flags: {} }, + '\\Xi': { glyph: 2040, flags: {} }, + '\\Omicron': { glyph: 2041, flags: {} }, + '\\Pi': { glyph: 2042, flags: {} }, + '\\Rho': { glyph: 2043, flags: {} }, + '\\Sigma': { glyph: 2044, flags: {} }, + '\\Tau': { glyph: 2045, flags: {} }, + '\\Upsilon': { glyph: 2046, flags: {} }, + '\\Phi': { glyph: 2047, flags: {} }, + '\\Chi': { glyph: 2048, flags: {} }, + '\\Psi': { glyph: 2049, flags: {} }, + '\\Omega': { glyph: 2050, flags: {} } +} + +export { SYMB } + +export function asciiMap(x: string, mode = 'math'): number { + const c = x.charCodeAt(0) + if (65 <= c && c <= 90) { + const d = c - 65 + if (mode == 'text' || mode == 'rm') { + return d + 2001 + } else if (mode == 'tt') { + return d + 501 + } else if (mode == 'bf' || mode == 'bb') { + return d + 3001 + } else if (mode == 'sf') { + return d + 2501 + } else if (mode == 'frak') { + return d + 3301 + } else if (mode == 'scr' || mode == 'cal') { + return d + 2551 + } else { + return d + 2051 + } + } + if (97 <= c && c <= 122) { + const d = c - 97 + if (mode == 'text' || mode == 'rm') { + return d + 2101 + } else if (mode == 'tt') { + return d + 601 + } else if (mode == 'bf' || mode == 'bb') { + return d + 3101 + } else if (mode == 'sf') { + return d + 2601 + } else if (mode == 'frak') { + return d + 3401 + } else if (mode == 'scr' || mode == 'cal') { + return d + 2651 + } else { + return d + 2151 + } + } + if (48 <= c && c <= 57) { + const d = c - 48 + if (mode == 'it') { + return d + 2750 + } else if (mode == 'bf') { + return d + 3200 + } else if (mode == 'tt') { + return d + 700 + } else { + return d + 2200 + } + } + + return { + '.': 2210, + ',': 2211, + ':': 2212, + ';': 2213, + '!': 2214, + '?': 2215, + '\'': 2216, + '"': 2217, + '*': 2219, + '/': 2220, + '-': 2231, + '+': 2232, + '=': 2238, + '<': 2241, + '>': 2242, + '~': 2246, + '@': 2273, + '\\': 804 + }[x] +} diff --git a/src/editor/core/draw/particle/previewer/Previewer.ts b/src/editor/core/draw/particle/previewer/Previewer.ts new file mode 100644 index 0000000..aff148f --- /dev/null +++ b/src/editor/core/draw/particle/previewer/Previewer.ts @@ -0,0 +1,582 @@ +import { EDITOR_PREFIX } from '../../../../dataset/constant/Editor' +import { EditorMode } from '../../../../dataset/enum/Editor' +import { IEditorOption } from '../../../../interface/Editor' +import { IElement, IElementPosition } from '../../../../interface/Element' +import { EventBusMap } from '../../../../interface/EventBus' +import { + IPreviewerCreateResult, + IPreviewerDrawOption +} from '../../../../interface/Previewer' +import { downloadFile } from '../../../../utils' +import { EventBus } from '../../../event/eventbus/EventBus' +import { Draw } from '../../Draw' + +export class Previewer { + private container: HTMLDivElement + private canvas: HTMLCanvasElement + private draw: Draw + private options: Required + private curElement: IElement | null + private curElementSrc: string + private previewerDrawOption: IPreviewerDrawOption + private curPosition: IElementPosition | null + private eventBus: EventBus + // 图片列表 + private imageList: IElement[] + private curShowElement: IElement | null + private imageCount: HTMLSpanElement | null + private imagePre: HTMLElement | null + private imageNext: HTMLElement | null + // 拖拽改变尺寸 + private resizerSelection: HTMLDivElement + private resizerHandleList: HTMLDivElement[] + private resizerImageContainer: HTMLDivElement + private resizerImage: HTMLImageElement + private resizerSize: HTMLSpanElement + private width: number + private height: number + private mousedownX: number + private mousedownY: number + private curHandleIndex: number + // 预览选区 + private previewerContainer: HTMLDivElement | null + private previewerImage: HTMLImageElement | null + + constructor(draw: Draw) { + this.container = draw.getContainer() + this.canvas = draw.getPage() + this.draw = draw + this.options = draw.getOptions() + this.curElement = null + this.curElementSrc = '' + this.previewerDrawOption = {} + this.curPosition = null + this.eventBus = draw.getEventBus() + this.imageList = [] + this.curShowElement = null + this.imageCount = null + this.imagePre = null + this.imageNext = null + // 图片尺寸缩放 + const { + resizerSelection, + resizerHandleList, + resizerImageContainer, + resizerImage, + resizerSize + } = this._createResizerDom() + this.resizerSelection = resizerSelection + this.resizerHandleList = resizerHandleList + this.resizerImageContainer = resizerImageContainer + this.resizerImage = resizerImage + this.resizerSize = resizerSize + this.width = 0 + this.height = 0 + this.mousedownX = 0 + this.mousedownY = 0 + this.curHandleIndex = 0 // 默认右下角 + this.previewerContainer = null + this.previewerImage = null + } + + private _getElementPosition( + element: IElement, + position: IElementPosition | null = null + ): { x: number; y: number } { + const { scale } = this.options + let x = 0 + let y = 0 + const height = this.draw.getHeight() + const pageGap = this.draw.getPageGap() + const pageNo = position?.pageNo ?? this.draw.getPageNo() + const preY = pageNo * (height + pageGap) + // 优先使用浮动位置 + if (element.imgFloatPosition) { + x = element.imgFloatPosition.x! * scale + y = element.imgFloatPosition.y * scale + preY + } else if (position) { + const { + coordinate: { + leftTop: [left, top] + }, + ascent + } = position + x = left + y = top + preY + ascent + } + return { x, y } + } + + private _createResizerDom(): IPreviewerCreateResult { + const { scale } = this.options + // 拖拽边框 + const resizerSelection = document.createElement('div') + resizerSelection.classList.add(`${EDITOR_PREFIX}-resizer-selection`) + resizerSelection.style.display = 'none' + resizerSelection.style.borderColor = this.options.resizerColor + resizerSelection.style.borderWidth = `${scale}px` + // 拖拽点 + const resizerHandleList: HTMLDivElement[] = [] + for (let i = 0; i < 8; i++) { + const handleDom = document.createElement('div') + handleDom.style.background = this.options.resizerColor + handleDom.classList.add(`resizer-handle`) + handleDom.classList.add(`handle-${i}`) + handleDom.setAttribute('data-index', String(i)) + handleDom.onmousedown = this._mousedown.bind(this) + resizerSelection.append(handleDom) + resizerHandleList.push(handleDom) + } + this.container.append(resizerSelection) + // 尺寸查看 + const resizerSizeView = document.createElement('div') + resizerSizeView.classList.add(`${EDITOR_PREFIX}-resizer-size-view`) + const resizerSize = document.createElement('span') + resizerSizeView.append(resizerSize) + resizerSelection.append(resizerSizeView) + // 拖拽镜像 + const resizerImageContainer = document.createElement('div') + resizerImageContainer.classList.add(`${EDITOR_PREFIX}-resizer-image`) + resizerImageContainer.style.display = 'none' + const resizerImage = document.createElement('img') + resizerImageContainer.append(resizerImage) + this.container.append(resizerImageContainer) + return { + resizerSelection, + resizerHandleList, + resizerImageContainer, + resizerImage, + resizerSize + } + } + + private _keydown = () => { + // 有键盘事件触发时,主动销毁拖拽选区 + if (this.resizerSelection.style.display === 'block') { + this.clearResizer() + document.removeEventListener('keydown', this._keydown) + } + } + + private _mousedown(evt: MouseEvent) { + this.canvas = this.draw.getPage() + if (!this.curElement) return + const { scale } = this.options + this.mousedownX = evt.x + this.mousedownY = evt.y + const target = evt.target as HTMLDivElement + this.curHandleIndex = Number(target.dataset.index) + // 改变光标 + const cursor = window.getComputedStyle(target).cursor + document.body.style.cursor = cursor + this.canvas.style.cursor = cursor + // 拖拽图片镜像 + this.resizerImage.src = this.curElementSrc + this.resizerImageContainer.style.display = 'block' + // 优先使用浮动位置信息 + const { x: resizerLeft, y: resizerTop } = this._getElementPosition( + this.curElement, + this.curPosition + ) + this.resizerImageContainer.style.left = `${resizerLeft}px` + this.resizerImageContainer.style.top = `${resizerTop}px` + this.resizerImage.style.width = `${this.curElement.width! * scale}px` + this.resizerImage.style.height = `${this.curElement.height! * scale}px` + // 追加全局事件 + const mousemoveFn = this._mousemove.bind(this) + document.addEventListener('mousemove', mousemoveFn) + document.addEventListener( + 'mouseup', + () => { + // 改变尺寸 + if (this.curElement && !this.previewerDrawOption.dragDisable) { + this.curElement.width = this.width + this.curElement.height = this.height + this.draw.render({ + isSetCursor: true, + curIndex: this.curPosition?.index + }) + } + // 还原副作用 + this.resizerImageContainer.style.display = 'none' + document.removeEventListener('mousemove', mousemoveFn) + document.body.style.cursor = '' + this.canvas.style.cursor = 'text' + }, + { + once: true + } + ) + evt.preventDefault() + } + + private _mousemove(evt: MouseEvent) { + if (!this.curElement || this.previewerDrawOption.dragDisable) return + const { scale } = this.options + let dx = 0 + let dy = 0 + switch (this.curHandleIndex) { + case 0: + { + const offsetX = this.mousedownX - evt.x + const offsetY = this.mousedownY - evt.y + dx = Math.cbrt(offsetX ** 3 + offsetY ** 3) + dy = (this.curElement.height! * dx) / this.curElement.width! + } + break + case 1: + dy = this.mousedownY - evt.y + break + case 2: + { + const offsetX = evt.x - this.mousedownX + const offsetY = this.mousedownY - evt.y + dx = Math.cbrt(offsetX ** 3 + offsetY ** 3) + dy = (this.curElement.height! * dx) / this.curElement.width! + } + break + case 4: + { + const offsetX = evt.x - this.mousedownX + const offsetY = evt.y - this.mousedownY + dx = Math.cbrt(offsetX ** 3 + offsetY ** 3) + dy = (this.curElement.height! * dx) / this.curElement.width! + } + break + case 3: + dx = evt.x - this.mousedownX + break + case 5: + dy = evt.y - this.mousedownY + break + case 6: + { + const offsetX = this.mousedownX - evt.x + const offsetY = evt.y - this.mousedownY + dx = Math.cbrt(offsetX ** 3 + offsetY ** 3) + dy = (this.curElement.height! * dx) / this.curElement.width! + } + break + case 7: + dx = this.mousedownX - evt.x + break + } + // 图片实际宽高(变化大小除掉缩放比例) + const dw = this.curElement.width! + dx / scale + const dh = this.curElement.height! + dy / scale + if (dw <= 0 || dh <= 0) return + this.width = dw + this.height = dh + // 图片显示宽高 + const elementWidth = dw * scale + const elementHeight = dh * scale + // 更新影子图片尺寸 + this.resizerImage.style.width = `${elementWidth}px` + this.resizerImage.style.height = `${elementHeight}px` + // 更新预览包围框尺寸 + this._updateResizerRect(elementWidth, elementHeight) + // 尺寸预览 + this._updateResizerSizeView(elementWidth, elementHeight) + evt.preventDefault() + // 图片尺寸发生改变事件 + if (this.eventBus.isSubscribe('imageSizeChange')) { + this.eventBus.emit('imageSizeChange', { + element: this.curElement + }) + } + } + + private _drawPreviewer() { + const previewerContainer = document.createElement('div') + previewerContainer.classList.add(`${EDITOR_PREFIX}-image-previewer`) + // 关闭按钮 + const closeBtn = document.createElement('i') + closeBtn.classList.add('image-close') + closeBtn.onclick = () => { + this._clearPreviewer() + } + previewerContainer.append(closeBtn) + // 图片 + const imgContainer = document.createElement('div') + imgContainer.classList.add(`${EDITOR_PREFIX}-image-container`) + const img = document.createElement('img') + img.src = this.curElementSrc + img.draggable = false + imgContainer.append(img) + this.previewerImage = img + previewerContainer.append(imgContainer) + // 操作栏 + let x = 0 + let y = 0 + let scaleSize = 1 + let rotateSize = 0 + const menuContainer = document.createElement('div') + menuContainer.classList.add(`${EDITOR_PREFIX}-image-menu`) + // 切换上下张图片 + const navigateContainer = document.createElement('div') + navigateContainer.classList.add('image-navigate') + const imagePre = document.createElement('i') + imagePre.classList.add('image-pre') + imagePre.onclick = () => { + const curIndex = this.imageList.findIndex( + el => el.id === this.curShowElement?.id + ) + if (curIndex <= 0) return + this.curShowElement = this.imageList[curIndex - 1] + img.src = this.curShowElement.value + this._updateImageNavigate() + } + navigateContainer.append(imagePre) + this.imagePre = imagePre + const imageCount = document.createElement('span') + imageCount.classList.add('image-count') + this.imageCount = imageCount + navigateContainer.append(imageCount) + const imageNext = document.createElement('i') + imageNext.classList.add('image-next') + imageNext.onclick = () => { + const curIndex = this.imageList.findIndex( + el => el.id === this.curShowElement?.id + ) + if (curIndex >= this.imageList.length - 1) return + this.curShowElement = this.imageList[curIndex + 1] + img.src = this.curShowElement.value + this._updateImageNavigate() + } + this.imageNext = imageNext + navigateContainer.append(imageNext) + menuContainer.append(navigateContainer) + // 缩放 + const zoomIn = document.createElement('i') + zoomIn.classList.add('zoom-in') + zoomIn.onclick = () => { + scaleSize += 0.1 + this._setPreviewerTransform(scaleSize, rotateSize, x, y) + } + menuContainer.append(zoomIn) + const zoomOut = document.createElement('i') + zoomOut.onclick = () => { + if (scaleSize - 0.1 <= 0.1) return + scaleSize -= 0.1 + this._setPreviewerTransform(scaleSize, rotateSize, x, y) + } + zoomOut.classList.add('zoom-out') + menuContainer.append(zoomOut) + // 旋转 + const rotate = document.createElement('i') + rotate.classList.add('rotate') + rotate.onclick = () => { + rotateSize += 1 + this._setPreviewerTransform(scaleSize, rotateSize, x, y) + } + menuContainer.append(rotate) + // 恢复原始大小 + const originalSize = document.createElement('i') + originalSize.classList.add('original-size') + originalSize.onclick = () => { + x = 0 + y = 0 + scaleSize = 1 + rotateSize = 0 + this._setPreviewerTransform(scaleSize, rotateSize, x, y) + } + menuContainer.append(originalSize) + // 下载图片 + const imageDownload = document.createElement('i') + imageDownload.classList.add('image-download') + imageDownload.onclick = () => { + const { mime } = this.previewerDrawOption + downloadFile(img.src, `${this.curElement?.id}.${mime || 'png'}`) + } + menuContainer.append(imageDownload) + previewerContainer.append(menuContainer) + this.previewerContainer = previewerContainer + document.body.append(previewerContainer) + // 拖拽调整位置 + let startX = 0 + let startY = 0 + let isAllowDrag = false + img.onmousedown = evt => { + isAllowDrag = true + startX = evt.x + startY = evt.y + previewerContainer.style.cursor = 'move' + } + previewerContainer.onmousemove = (evt: MouseEvent) => { + if (!isAllowDrag) return + x += evt.x - startX + y += evt.y - startY + startX = evt.x + startY = evt.y + this._setPreviewerTransform(scaleSize, rotateSize, x, y) + } + previewerContainer.onmouseup = () => { + isAllowDrag = false + previewerContainer.style.cursor = 'auto' + } + previewerContainer.onwheel = evt => { + evt.preventDefault() + evt.stopPropagation() + if (evt.deltaY < 0) { + // 放大 + scaleSize += 0.1 + } else { + // 缩小 + if (scaleSize - 0.1 <= 0.1) return + scaleSize -= 0.1 + } + this._setPreviewerTransform(scaleSize, rotateSize, x, y) + } + // 更新图片索引信息 + this._updateImageNavigate() + } + + private _updateImageNavigate() { + // 更新当前图片位置索引 + const currentIndex = this.imageList.findIndex( + el => el.id === this.curShowElement?.id + ) + this.imageCount!.innerText = `${currentIndex + 1} / ${ + this.imageList.length + }` + // 更新按钮权限 + if (currentIndex <= 0) { + this.imagePre!.classList.add('disabled') + } else { + this.imagePre!.classList.remove('disabled') + } + if (currentIndex >= this.imageList.length - 1) { + this.imageNext!.classList.add('disabled') + } else { + this.imageNext!.classList.remove('disabled') + } + } + + public _setPreviewerTransform( + scale: number, + rotate: number, + x: number, + y: number + ) { + if (!this.previewerImage) return + this.previewerImage.style.left = `${x}px` + this.previewerImage.style.top = `${y}px` + this.previewerImage.style.transform = `scale(${scale}) rotate(${ + rotate * 90 + }deg)` + } + + private _clearPreviewer() { + this.previewerContainer?.remove() + this.previewerContainer = null + document.body.style.overflow = 'auto' + } + + public _updateResizerRect(width: number, height: number) { + const { resizerSize: handleSize, scale } = this.options + const isReadonly = this.draw.isReadonly() + this.resizerSelection.style.width = `${width}px` + this.resizerSelection.style.height = `${height}px` + // handle + for (let i = 0; i < 8; i++) { + const left = + i === 0 || i === 6 || i === 7 + ? -handleSize + : i === 1 || i === 5 + ? width / 2 + : width - handleSize + const top = + i === 0 || i === 1 || i === 2 + ? -handleSize + : i === 3 || i === 7 + ? height / 2 - handleSize + : height - handleSize + this.resizerHandleList[i].style.transform = `scale(${scale})` + this.resizerHandleList[i].style.left = `${left}px` + this.resizerHandleList[i].style.top = `${top}px` + this.resizerHandleList[i].style.display = isReadonly ? 'none' : 'block' + } + } + + public _updateResizerSizeView(width: number, height: number) { + this.resizerSize.innerText = `${Math.round(width)} × ${Math.round(height)}` + } + + public render() { + // 图片工具配置禁用又非设计模式时不渲染 + const mode = this.draw.getMode() + if ( + !this.curElement || + (this.curElement.imgToolDisabled && !this.draw.isDesignMode()) || + (mode === EditorMode.PRINT && + this.options.modeRule[EditorMode.PRINT]?.imagePreviewerDisabled) || + (mode === EditorMode.READONLY && + this.options.modeRule[EditorMode.READONLY]?.imagePreviewerDisabled) + ) { + return + } + // 获取所有图片 + this.imageList = this.draw.getImageParticle().getOriginalMainImageList() + this.curShowElement = this.curElement + // 渲染预览框 + this._drawPreviewer() + document.body.style.overflow = 'hidden' + } + + public drawResizer( + element: IElement, + position: IElementPosition | null = null, + options: IPreviewerDrawOption = {} + ) { + // 图片工具配置禁用又非设计模式时不渲染 + const mode = this.draw.getMode() + if ( + (element.imgToolDisabled && !this.draw.isDesignMode()) || + (mode === EditorMode.PRINT && + this.options.modeRule[EditorMode.PRINT]?.imagePreviewerDisabled) || + (mode === EditorMode.READONLY && + this.options.modeRule[EditorMode.READONLY]?.imagePreviewerDisabled) + ) { + return + } + // 缓存配置 + this.previewerDrawOption = options + this.curElementSrc = element[options.srcKey || 'value'] || '' + // 更新渲染尺寸及位置 + this.updateResizer(element, position) + // 监听事件 + document.addEventListener('keydown', this._keydown) + } + + public updateResizer( + element: IElement, + position: IElementPosition | null = null + ) { + const { scale } = this.options + const elementWidth = element.width! * scale + const elementHeight = element.height! * scale + // 尺寸预览 + this._updateResizerSizeView(elementWidth, elementHeight) + // 优先使用浮动位置信息 + const { x: resizerLeft, y: resizerTop } = this._getElementPosition( + element, + position + ) + this.resizerSelection.style.left = `${resizerLeft}px` + this.resizerSelection.style.top = `${resizerTop}px` + this.resizerSelection.style.borderWidth = `${scale}px` + // 更新预览包围框尺寸 + this._updateResizerRect(elementWidth, elementHeight) + this.resizerSelection.style.display = 'block' + // 缓存基础信息 + this.curElement = element + this.curPosition = position + this.width = elementWidth + this.height = elementHeight + } + + public clearResizer() { + this.resizerSelection.style.display = 'none' + document.removeEventListener('keydown', this._keydown) + } +} diff --git a/src/editor/core/draw/particle/table/TableOperate.ts b/src/editor/core/draw/particle/table/TableOperate.ts new file mode 100644 index 0000000..e8cafd9 --- /dev/null +++ b/src/editor/core/draw/particle/table/TableOperate.ts @@ -0,0 +1,987 @@ +import { ElementType, IElement, TableBorder, VerticalAlign } from '../../../..' +import { ZERO } from '../../../../dataset/constant/Common' +import { TABLE_CONTEXT_ATTR } from '../../../../dataset/constant/Element' +import { TdBorder, TdSlash } from '../../../../dataset/enum/table/Table' +import { DeepRequired } from '../../../../interface/Common' +import { IEditorOption } from '../../../../interface/Editor' +import { IColgroup } from '../../../../interface/table/Colgroup' +import { ITd } from '../../../../interface/table/Td' +import { ITr } from '../../../../interface/table/Tr' +import { cloneProperty, getUUID } from '../../../../utils' +import { + formatElementContext, + formatElementList +} from '../../../../utils/element' +import { Position } from '../../../position/Position' +import { RangeManager } from '../../../range/RangeManager' +import { Draw } from '../../Draw' +import { TableParticle } from './TableParticle' +import { TableTool } from './TableTool' + +export class TableOperate { + private draw: Draw + private range: RangeManager + private position: Position + private tableTool: TableTool + private tableParticle: TableParticle + private options: DeepRequired + + constructor(draw: Draw) { + this.draw = draw + this.range = draw.getRange() + this.position = draw.getPosition() + this.tableTool = draw.getTableTool() + this.tableParticle = draw.getTableParticle() + this.options = draw.getOptions() + } + + public insertTable(row: number, col: number) { + const { startIndex, endIndex } = this.range.getRange() + if (!~startIndex && !~endIndex) return + const { defaultTrMinHeight } = this.options.table + const elementList = this.draw.getElementList() + let offsetX = 0 + if (elementList[startIndex]?.listId) { + const positionList = this.position.getPositionList() + const { rowIndex } = positionList[startIndex] + const rowList = this.draw.getRowList() + const row = rowList[rowIndex] + offsetX = row?.offsetX || 0 + } + const innerWidth = this.draw.getContextInnerWidth() - offsetX + // colgroup + const colgroup: IColgroup[] = [] + const colWidth = innerWidth / col + for (let c = 0; c < col; c++) { + colgroup.push({ + width: colWidth + }) + } + // trlist + const trList: ITr[] = [] + for (let r = 0; r < row; r++) { + const tdList: ITd[] = [] + const tr: ITr = { + height: defaultTrMinHeight, + tdList + } + for (let c = 0; c < col; c++) { + tdList.push({ + colspan: 1, + rowspan: 1, + value: [] + }) + } + trList.push(tr) + } + const element: IElement = { + type: ElementType.TABLE, + value: '', + colgroup, + trList + } + // 格式化element + formatElementList([element], { + editorOptions: this.options + }) + formatElementContext(elementList, [element], startIndex, { + editorOptions: this.options + }) + const curIndex = startIndex + 1 + this.draw.spliceElementList( + elementList, + curIndex, + startIndex === endIndex ? 0 : endIndex - startIndex, + [element] + ) + this.range.setRange(curIndex, curIndex) + this.draw.render({ curIndex, isSetCursor: false }) + } + + public insertTableTopRow() { + const positionContext = this.position.getPositionContext() + if (!positionContext.isTable) return + const { index, trIndex, tableId } = positionContext + const originalElementList = this.draw.getOriginalElementList() + const element = originalElementList[index!] + const curTrList = element.trList! + const curTr = curTrList[trIndex!] + // 之前跨行的增加跨行数 + if (curTr.tdList.length < element.colgroup!.length) { + const curTrNo = curTr.tdList[0].rowIndex! + for (let t = 0; t < trIndex!; t++) { + const tr = curTrList[t] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + if (td.rowspan > 1 && td.rowIndex! + td.rowspan >= curTrNo + 1) { + td.rowspan += 1 + } + } + } + } + // 增加当前行 + const newTrId = getUUID() + const newTr: ITr = { + height: curTr.height, + id: newTrId, + tdList: [] + } + for (let t = 0; t < curTr.tdList.length; t++) { + const curTd = curTr.tdList[t] + const newTdId = getUUID() + newTr.tdList.push({ + id: newTdId, + rowspan: 1, + colspan: curTd.colspan, + value: [ + { + value: ZERO, + size: 16, + tableId, + trId: newTrId, + tdId: newTdId + } + ] + }) + } + curTrList.splice(trIndex!, 0, newTr) + // 重新设置上下文 + this.position.setPositionContext({ + isTable: true, + index, + trIndex, + tdIndex: 0, + tdId: newTr.tdList[0].id, + trId: newTr.id, + tableId + }) + this.range.setRange(0, 0) + // 重新渲染 + this.draw.render({ curIndex: 0 }) + this.tableTool.render() + } + + public insertTableBottomRow() { + const positionContext = this.position.getPositionContext() + if (!positionContext.isTable) return + const { index, trIndex, tableId } = positionContext + const originalElementList = this.draw.getOriginalElementList() + const element = originalElementList[index!] + const curTrList = element.trList! + const curTr = curTrList[trIndex!] + const anchorTr = + curTrList.length - 1 === trIndex ? curTr : curTrList[trIndex! + 1] + // 之前/当前行跨行的增加跨行数 + if (anchorTr.tdList.length < element.colgroup!.length) { + const curTrNo = anchorTr.tdList[0].rowIndex! + for (let t = 0; t < trIndex! + 1; t++) { + const tr = curTrList[t] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + if (td.rowspan > 1 && td.rowIndex! + td.rowspan >= curTrNo + 1) { + td.rowspan += 1 + } + } + } + } + // 增加当前行 + const newTrId = getUUID() + const newTr: ITr = { + height: anchorTr.height, + id: newTrId, + tdList: [] + } + for (let t = 0; t < anchorTr.tdList.length; t++) { + const curTd = anchorTr.tdList[t] + const newTdId = getUUID() + newTr.tdList.push({ + id: newTdId, + rowspan: 1, + colspan: curTd.colspan, + value: [ + { + value: ZERO, + size: 16, + tableId, + trId: newTrId, + tdId: newTdId + } + ] + }) + } + curTrList.splice(trIndex! + 1, 0, newTr) + // 重新设置上下文 + this.position.setPositionContext({ + isTable: true, + index, + trIndex: trIndex! + 1, + tdIndex: 0, + tdId: newTr.tdList[0].id, + trId: newTr.id, + tableId: element.id + }) + this.range.setRange(0, 0) + // 重新渲染 + this.draw.render({ curIndex: 0 }) + } + + public adjustColWidth(element: IElement) { + if (element.type !== ElementType.TABLE) return + const { defaultColMinWidth } = this.options.table + const colgroup = element.colgroup! + const colgroupWidth = colgroup.reduce((pre, cur) => pre + cur.width, 0) + const width = this.draw.getOriginalInnerWidth() + if (colgroupWidth > width) { + // 过滤大于最小宽度的列(可能减少宽度的列) + const greaterMinWidthCol = colgroup.filter( + col => col.width > defaultColMinWidth + ) + // 均分多余宽度 + const adjustWidth = (colgroupWidth - width) / greaterMinWidthCol.length + for (let g = 0; g < colgroup.length; g++) { + const group = colgroup[g] + // 小于最小宽度的列不处理 + if (group.width - adjustWidth >= defaultColMinWidth) { + group.width -= adjustWidth + } + } + } + } + + public insertTableLeftCol() { + const positionContext = this.position.getPositionContext() + if (!positionContext.isTable) return + const { index, tdIndex, tableId } = positionContext + const originalElementList = this.draw.getOriginalElementList() + const element = originalElementList[index!] + const curTrList = element.trList! + const curTdIndex = tdIndex! + // 增加列 + for (let t = 0; t < curTrList.length; t++) { + const tr = curTrList[t] + const tdId = getUUID() + tr.tdList.splice(curTdIndex, 0, { + id: tdId, + rowspan: 1, + colspan: 1, + value: [ + { + value: ZERO, + size: 16, + tableId, + trId: tr.id, + tdId + } + ] + }) + } + // 重新计算宽度 + const { defaultColMinWidth } = this.options.table + const colgroup = element.colgroup! + colgroup.splice(curTdIndex, 0, { + width: defaultColMinWidth + }) + this.adjustColWidth(element) + // 重新设置上下文 + this.position.setPositionContext({ + isTable: true, + index, + trIndex: 0, + tdIndex: curTdIndex, + tdId: curTrList[0].tdList[curTdIndex].id, + trId: curTrList[0].id, + tableId + }) + this.range.setRange(0, 0) + // 重新渲染 + this.draw.render({ curIndex: 0 }) + this.tableTool.render() + } + + public insertTableRightCol() { + const positionContext = this.position.getPositionContext() + if (!positionContext.isTable) return + const { index, tdIndex, tableId } = positionContext + const originalElementList = this.draw.getOriginalElementList() + const element = originalElementList[index!] + const curTrList = element.trList! + const curTdIndex = tdIndex! + 1 + // 增加列 + for (let t = 0; t < curTrList.length; t++) { + const tr = curTrList[t] + const tdId = getUUID() + tr.tdList.splice(curTdIndex, 0, { + id: tdId, + rowspan: 1, + colspan: 1, + value: [ + { + value: ZERO, + size: 16, + tableId, + trId: tr.id, + tdId + } + ] + }) + } + // 重新计算宽度 + const { defaultColMinWidth } = this.options.table + const colgroup = element.colgroup! + colgroup.splice(curTdIndex, 0, { + width: defaultColMinWidth + }) + this.adjustColWidth(element) + // 重新设置上下文 + this.position.setPositionContext({ + isTable: true, + index, + trIndex: 0, + tdIndex: curTdIndex, + tdId: curTrList[0].tdList[curTdIndex].id, + trId: curTrList[0].id, + tableId: element.id + }) + this.range.setRange(0, 0) + // 重新渲染 + this.draw.render({ curIndex: 0 }) + } + + public deleteTableRow() { + const positionContext = this.position.getPositionContext() + if (!positionContext.isTable) return + const { index, trIndex, tdIndex } = positionContext + const originalElementList = this.draw.getOriginalElementList() + const element = originalElementList[index!] + const trList = element.trList! + const curTr = trList[trIndex!] + const curTdRowIndex = curTr.tdList[tdIndex!].rowIndex! + // 如果是最后一行,直接删除整个表格(如果是拆分表格按照正常逻辑走) + if (trList.length <= 1 && element.pagingIndex === 0) { + this.deleteTable() + return + } + // 之前行缩小rowspan + for (let r = 0; r < curTdRowIndex; r++) { + const tr = trList[r] + const tdList = tr.tdList + for (let d = 0; d < tdList.length; d++) { + const td = tdList[d] + if (td.rowIndex! + td.rowspan > curTdRowIndex) { + td.rowspan-- + } + } + } + // 补跨行 + for (let d = 0; d < curTr.tdList.length; d++) { + const td = curTr.tdList[d] + if (td.rowspan > 1) { + const tdId = getUUID() + const nextTr = trList[trIndex! + 1] + nextTr.tdList.splice(d, 0, { + id: tdId, + rowspan: td.rowspan - 1, + colspan: td.colspan, + value: [ + { + value: ZERO, + size: 16, + tableId: element.id, + trId: nextTr.id, + tdId + } + ] + }) + } + } + // 删除当前行 + trList.splice(trIndex!, 1) + // 重新设置上下文 + this.position.setPositionContext({ + isTable: false + }) + this.range.clearRange() + // 重新渲染 + this.draw.render({ + curIndex: positionContext.index + }) + this.tableTool.dispose() + } + + public deleteTableCol() { + const positionContext = this.position.getPositionContext() + if (!positionContext.isTable) return + const { index, tdIndex, trIndex } = positionContext + const originalElementList = this.draw.getOriginalElementList() + const element = originalElementList[index!] + const curTrList = element.trList! + const curTd = curTrList[trIndex!].tdList[tdIndex!] + const curColIndex = curTd.colIndex! + // 如果是最后一列,直接删除整个表格 + const moreTdTr = curTrList.find(tr => tr.tdList.length > 1) + if (!moreTdTr) { + this.deleteTable() + return + } + // 缩小colspan或删除与当前列重叠的单元格 + for (let t = 0; t < curTrList.length; t++) { + const tr = curTrList[t] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + if ( + td.colIndex! <= curColIndex && + td.colIndex! + td.colspan > curColIndex + ) { + if (td.colspan > 1) { + td.colspan-- + } else { + tr.tdList.splice(d, 1) + } + } + } + } + element.colgroup?.splice(curColIndex, 1) + // 重新设置上下文 + this.position.setPositionContext({ + isTable: false + }) + this.range.setRange(0, 0) + // 重新渲染 + this.draw.render({ + curIndex: positionContext.index + }) + this.tableTool.dispose() + } + + public deleteTable() { + const positionContext = this.position.getPositionContext() + if (!positionContext.isTable) return + const originalElementList = this.draw.getOriginalElementList() + const tableElement = originalElementList[positionContext.index!] + // 需要删除的表格数量(拆分表格)及位置 + let deleteCount = 1 + let deleteStartIndex = positionContext.index! + if (tableElement.pagingId) { + // 开始删除的下标位置 + deleteStartIndex = positionContext.index! - tableElement.pagingIndex! + // 计算删除的表格数量 + for (let i = deleteStartIndex + 1; i < originalElementList.length; i++) { + if (originalElementList[i].pagingId === tableElement.pagingId) { + deleteCount++ + } else { + break + } + } + } + // 删除 + originalElementList.splice(deleteStartIndex, deleteCount) + const curIndex = deleteStartIndex - 1 + this.position.setPositionContext({ + isTable: false, + index: curIndex + }) + this.range.setRange(curIndex, curIndex) + this.draw.render({ curIndex }) + this.tableTool.dispose() + } + + public mergeTableCell() { + const positionContext = this.position.getPositionContext() + if (!positionContext.isTable) return + const { + isCrossRowCol, + startTdIndex, + endTdIndex, + startTrIndex, + endTrIndex + } = this.range.getRange() + if (!isCrossRowCol) return + const { index } = positionContext + const originalElementList = this.draw.getOriginalElementList() + const element = originalElementList[index!] + const curTrList = element.trList! + let startTd = curTrList[startTrIndex!].tdList[startTdIndex!] + let endTd = curTrList[endTrIndex!].tdList[endTdIndex!] + // 交换起始位置 + if (startTd.x! > endTd.x! || startTd.y! > endTd.y!) { + // prettier-ignore + [startTd, endTd] = [endTd, startTd] + } + const startColIndex = startTd.colIndex! + const endColIndex = endTd.colIndex! + (endTd.colspan - 1) + const startRowIndex = startTd.rowIndex! + const endRowIndex = endTd.rowIndex! + (endTd.rowspan - 1) + // 选区行列 + const rowCol: ITd[][] = [] + for (let t = 0; t < curTrList.length; t++) { + const tr = curTrList[t] + const tdList: ITd[] = [] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const tdColIndex = td.colIndex! + const tdRowIndex = td.rowIndex! + if ( + tdColIndex >= startColIndex && + tdColIndex <= endColIndex && + tdRowIndex >= startRowIndex && + tdRowIndex <= endRowIndex + ) { + tdList.push(td) + } + } + if (tdList.length) { + rowCol.push(tdList) + } + } + if (!rowCol.length) return + // 是否是矩形 + const lastRow = rowCol[rowCol.length - 1] + const leftTop = rowCol[0][0] + const rightBottom = lastRow[lastRow.length - 1] + const startX = leftTop.x! + const startY = leftTop.y! + const endX = rightBottom.x! + rightBottom.width! + const endY = rightBottom.y! + rightBottom.height! + for (let t = 0; t < rowCol.length; t++) { + const tr = rowCol[t] + for (let d = 0; d < tr.length; d++) { + const td = tr[d] + const tdStartX = td.x! + const tdStartY = td.y! + const tdEndX = tdStartX + td.width! + const tdEndY = tdStartY + td.height! + // 存在不符合项 + if ( + startX > tdStartX || + startY > tdStartY || + endX < tdEndX || + endY < tdEndY + ) { + return + } + } + } + // 合并单元格 + const mergeTdIdList: string[] = [] + const anchorTd = rowCol[0][0] + const anchorElement = anchorTd.value[0] + for (let t = 0; t < rowCol.length; t++) { + const tr = rowCol[t] + for (let d = 0; d < tr.length; d++) { + const td = tr[d] + const isAnchorTd = t === 0 && d === 0 + // 缓存待删除单元id并合并单元格内容 + if (!isAnchorTd) { + mergeTdIdList.push(td.id!) + // 被合并单元格没内容时忽略换行符 + const startTdValueIndex = td.value.length > 1 ? 0 : 1 + // 复制表格属性后追加 + for (let d = startTdValueIndex; d < td.value.length; d++) { + const tdElement = td.value[d] + cloneProperty( + TABLE_CONTEXT_ATTR, + anchorElement, + tdElement + ) + anchorTd.value.push(tdElement) + } + } + // 列合并 + if (t === 0 && d !== 0) { + anchorTd.colspan += td.colspan + } + // 行合并 + if (t !== 0) { + if (anchorTd.colIndex === td.colIndex) { + anchorTd.rowspan += td.rowspan + } + } + } + } + // 移除多余单元格 + for (let t = 0; t < curTrList.length; t++) { + const tr = curTrList[t] + let d = 0 + while (d < tr.tdList.length) { + const td = tr.tdList[d] + if (mergeTdIdList.includes(td.id!)) { + tr.tdList.splice(d, 1) + d-- + } + d++ + } + } + // 设置上下文信息 + this.position.setPositionContext({ + ...positionContext, + trIndex: anchorTd.trIndex, + tdIndex: anchorTd.tdIndex + }) + const curIndex = anchorTd.value.length - 1 + this.range.setRange(curIndex, curIndex) + // 重新渲染 + this.draw.render() + this.tableTool.render() + } + + public cancelMergeTableCell() { + const positionContext = this.position.getPositionContext() + if (!positionContext.isTable) return + const { index, tdIndex, trIndex } = positionContext + const originalElementList = this.draw.getOriginalElementList() + const element = originalElementList[index!] + const curTrList = element.trList! + const curTr = curTrList[trIndex!]! + const curTd = curTr.tdList[tdIndex!] + if (curTd.rowspan === 1 && curTd.colspan === 1) return + const colspan = curTd.colspan + // 设置跨列 + if (curTd.colspan > 1) { + for (let c = 1; c < curTd.colspan; c++) { + const tdId = getUUID() + curTr.tdList.splice(tdIndex! + c, 0, { + id: tdId, + rowspan: 1, + colspan: 1, + value: [ + { + value: ZERO, + size: 16, + tableId: element.id, + trId: curTr.id, + tdId + } + ] + }) + } + curTd.colspan = 1 + } + // 设置跨行 + if (curTd.rowspan > 1) { + for (let r = 1; r < curTd.rowspan; r++) { + const tr = curTrList[trIndex! + r] + for (let c = 0; c < colspan; c++) { + const tdId = getUUID() + tr.tdList.splice(curTd.colIndex!, 0, { + id: tdId, + rowspan: 1, + colspan: 1, + value: [ + { + value: ZERO, + size: 16, + tableId: element.id, + trId: tr.id, + tdId + } + ] + }) + } + } + curTd.rowspan = 1 + } + // 重新渲染 + const curIndex = curTd.value.length - 1 + this.range.setRange(curIndex, curIndex) + this.draw.render() + this.tableTool.render() + } + + public splitVerticalTableCell() { + const positionContext = this.position.getPositionContext() + if (!positionContext.isTable) return + // 暂时忽略跨行列选择 + const range = this.range.getRange() + if (range.isCrossRowCol) return + const { index, tdIndex, trIndex } = positionContext + const originalElementList = this.draw.getOriginalElementList() + const element = originalElementList[index!] + const curTrList = element.trList! + const curTr = curTrList[trIndex!]! + const curTd = curTr.tdList[tdIndex!] + // 增加列属性 + element.colgroup!.splice(tdIndex! + 1, 0, { + width: this.options.table.defaultColMinWidth + }) + // 同行增加td,非同行增加跨列数 + for (let t = 0; t < curTrList.length; t++) { + const tr = curTrList[t] + let d = 0 + while (d < tr.tdList.length) { + const td = tr.tdList[d] + // 非同行:存在交叉时增加跨列数 + if (td.rowIndex !== curTd.rowIndex) { + if ( + td.colIndex! <= curTd.colIndex! && + td.colIndex! + td.colspan > curTd.colIndex! + ) { + td.colspan++ + } + } else { + // 当前单元格:往右插入td + if (td.id === curTd.id) { + const tdId = getUUID() + curTr.tdList.splice(d + curTd.colspan, 0, { + id: tdId, + rowspan: curTd.rowspan, + colspan: 1, + value: [ + { + value: ZERO, + size: 16, + tableId: element.id, + trId: tr.id, + tdId + } + ] + }) + d++ + } + } + d++ + } + } + // 重新渲染 + this.draw.render() + this.tableTool.render() + } + + public splitHorizontalTableCell() { + const positionContext = this.position.getPositionContext() + if (!positionContext.isTable) return + // 暂时忽略跨行列选择 + const range = this.range.getRange() + if (range.isCrossRowCol) return + const { index, tdIndex, trIndex } = positionContext + const originalElementList = this.draw.getOriginalElementList() + const element = originalElementList[index!] + const curTrList = element.trList! + const curTr = curTrList[trIndex!]! + const curTd = curTr.tdList[tdIndex!] + // 追加的行跳出循环 + let appendTrIndex = -1 + // 交叉行增加rowspan,当前单元格往下追加一行tr + let t = 0 + while (t < curTrList.length) { + if (t === appendTrIndex) { + t++ + continue + } + const tr = curTrList[t] + let d = 0 + while (d < tr.tdList.length) { + const td = tr.tdList[d] + if (td.id === curTd.id) { + const trId = getUUID() + const tdId = getUUID() + curTrList.splice(t + curTd.rowspan, 0, { + id: trId, + height: this.options.table.defaultTrMinHeight, + tdList: [ + { + id: tdId, + rowspan: 1, + colspan: curTd.colspan, + value: [ + { + value: ZERO, + size: 16, + tableId: element.id, + trId, + tdId + } + ] + } + ] + }) + appendTrIndex = t + curTd.rowspan + } else if ( + td.rowIndex! >= curTd.rowIndex! && + td.rowIndex! < curTd.rowIndex! + curTd.rowspan && + td.rowIndex! + td.rowspan >= curTd.rowIndex! + curTd.rowspan + ) { + // 1. 循环td上方大于等于当前td上方 && 小于当前td的下方=>存在交叉 + // 2. 循环td下方大于或等于当前td下方 + td.rowspan++ + } + d++ + } + t++ + } + // 重新渲染 + this.draw.render() + this.tableTool.render() + } + + public tableTdVerticalAlign(payload: VerticalAlign) { + const rowCol = this.tableParticle.getRangeRowCol() + if (!rowCol) return + for (let r = 0; r < rowCol.length; r++) { + const row = rowCol[r] + for (let c = 0; c < row.length; c++) { + const td = row[c] + if ( + !td || + td.verticalAlign === payload || + (!td.verticalAlign && payload === VerticalAlign.TOP) + ) { + continue + } + // 重设垂直对齐方式 + td.verticalAlign = payload + } + } + const { endIndex } = this.range.getRange() + this.draw.render({ + curIndex: endIndex + }) + } + + public tableBorderType(payload: TableBorder) { + const positionContext = this.position.getPositionContext() + if (!positionContext.isTable) return + const { index } = positionContext + const originalElementList = this.draw.getOriginalElementList() + const element = originalElementList[index!] + if ( + (!element.borderType && payload === TableBorder.ALL) || + element.borderType === payload + ) { + return + } + element.borderType = payload + const { endIndex } = this.range.getRange() + this.draw.render({ + curIndex: endIndex + }) + } + + public tableBorderColor(payload: string) { + const positionContext = this.position.getPositionContext() + if (!positionContext.isTable) return + const { index } = positionContext + const originalElementList = this.draw.getOriginalElementList() + const element = originalElementList[index!] + if ( + (!element.borderColor && + payload === this.options.table.defaultBorderColor) || + element.borderColor === payload + ) { + return + } + element.borderColor = payload + const { endIndex } = this.range.getRange() + this.draw.render({ + curIndex: endIndex, + isCompute: false + }) + } + + public tableTdBorderType(payload: TdBorder) { + const rowCol = this.tableParticle.getRangeRowCol() + if (!rowCol) return + const tdList = rowCol.flat() + // 存在则设置边框类型,否则取消设置 + const isSetBorderType = tdList.some( + td => !td.borderTypes?.includes(payload) + ) + tdList.forEach(td => { + if (!td.borderTypes) { + td.borderTypes = [] + } + const borderTypeIndex = td.borderTypes.findIndex(type => type === payload) + if (isSetBorderType) { + if (!~borderTypeIndex) { + td.borderTypes.push(payload) + } + } else { + if (~borderTypeIndex) { + td.borderTypes.splice(borderTypeIndex, 1) + } + } + // 不存在边框设置时删除字段 + if (!td.borderTypes.length) { + delete td.borderTypes + } + }) + const { endIndex } = this.range.getRange() + this.draw.render({ + curIndex: endIndex + }) + } + + public tableTdSlashType(payload: TdSlash) { + const rowCol = this.tableParticle.getRangeRowCol() + if (!rowCol) return + const tdList = rowCol.flat() + // 存在则设置单元格斜线类型,否则取消设置 + const isSetTdSlashType = tdList.some( + td => !td.slashTypes?.includes(payload) + ) + tdList.forEach(td => { + if (!td.slashTypes) { + td.slashTypes = [] + } + const slashTypeIndex = td.slashTypes.findIndex(type => type === payload) + if (isSetTdSlashType) { + if (!~slashTypeIndex) { + td.slashTypes.push(payload) + } + } else { + if (~slashTypeIndex) { + td.slashTypes.splice(slashTypeIndex, 1) + } + } + // 不存在斜线设置时删除字段 + if (!td.slashTypes.length) { + delete td.slashTypes + } + }) + const { endIndex } = this.range.getRange() + this.draw.render({ + curIndex: endIndex + }) + } + + public tableTdBackgroundColor(payload: string) { + const rowCol = this.tableParticle.getRangeRowCol() + if (!rowCol) return + for (let r = 0; r < rowCol.length; r++) { + const row = rowCol[r] + for (let c = 0; c < row.length; c++) { + const col = row[c] + col.backgroundColor = payload + } + } + const { endIndex } = this.range.getRange() + this.range.setRange(endIndex, endIndex) + this.draw.render({ + isCompute: false + }) + } + + public tableSelectAll() { + const positionContext = this.position.getPositionContext() + const { index, tableId, isTable } = positionContext + if (!isTable || !tableId) return + const { startIndex, endIndex } = this.range.getRange() + const originalElementList = this.draw.getOriginalElementList() + const trList = originalElementList[index!].trList! + // 最后单元格位置 + const endTrIndex = trList.length - 1 + const endTdIndex = trList[endTrIndex].tdList.length - 1 + this.range.replaceRange({ + startIndex, + endIndex, + tableId, + startTdIndex: 0, + endTdIndex, + startTrIndex: 0, + endTrIndex + }) + this.draw.render({ + isCompute: false, + isSubmitHistory: false + }) + } +} diff --git a/src/editor/core/draw/particle/table/TableParticle.ts b/src/editor/core/draw/particle/table/TableParticle.ts new file mode 100644 index 0000000..cb5e2d4 --- /dev/null +++ b/src/editor/core/draw/particle/table/TableParticle.ts @@ -0,0 +1,558 @@ +import { ElementType, IElement, TableBorder } from '../../../..' +import { TdBorder, TdSlash } from '../../../../dataset/enum/table/Table' +import { DeepRequired } from '../../../../interface/Common' +import { IEditorOption } from '../../../../interface/Editor' +import { ITd } from '../../../../interface/table/Td' +import { ITr } from '../../../../interface/table/Tr' +import { deepClone } from '../../../../utils' +import { RangeManager } from '../../../range/RangeManager' +import { Draw } from '../../Draw' + +interface IDrawTableBorderOption { + ctx: CanvasRenderingContext2D + startX: number + startY: number + width: number + height: number + borderExternalWidth?: number + isDrawFullBorder?: boolean +} + +export class TableParticle { + private draw: Draw + private range: RangeManager + private options: DeepRequired + + constructor(draw: Draw) { + this.draw = draw + this.range = draw.getRange() + this.options = draw.getOptions() + } + + public getTrListGroupByCol(payload: ITr[]): ITr[] { + const trList = deepClone(payload) + for (let t = 0; t < payload.length; t++) { + const tr = trList[t] + for (let d = tr.tdList.length - 1; d >= 0; d--) { + const td = tr.tdList[d] + const { rowspan, rowIndex, colIndex } = td + const curRowIndex = rowIndex! + rowspan - 1 + if (curRowIndex !== d) { + const changeTd = tr.tdList.splice(d, 1)[0] + trList[curRowIndex]?.tdList.splice(colIndex!, 0, changeTd) + } + } + } + return trList + } + + public getRangeRowCol(): ITd[][] | null { + const { isTable, index, trIndex, tdIndex } = this.draw + .getPosition() + .getPositionContext() + if (!isTable) return null + const { + isCrossRowCol, + startTdIndex, + endTdIndex, + startTrIndex, + endTrIndex + } = this.range.getRange() + const originalElementList = this.draw.getOriginalElementList() + const element = originalElementList[index!] + const curTrList = element.trList! + // 非跨列直接返回光标所在单元格 + if (!isCrossRowCol) { + return [[curTrList[trIndex!].tdList[tdIndex!]]] + } + let startTd = curTrList[startTrIndex!].tdList[startTdIndex!] + let endTd = curTrList[endTrIndex!].tdList[endTdIndex!] + // 交换起始位置 + if (startTd.x! > endTd.x! || startTd.y! > endTd.y!) { + // prettier-ignore + [startTd, endTd] = [endTd, startTd] + } + const startColIndex = startTd.colIndex! + const endColIndex = endTd.colIndex! + (endTd.colspan - 1) + const startRowIndex = startTd.rowIndex! + const endRowIndex = endTd.rowIndex! + (endTd.rowspan - 1) + // 选区行列 + const rowCol: ITd[][] = [] + for (let t = 0; t < curTrList.length; t++) { + const tr = curTrList[t] + const tdList: ITd[] = [] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const tdColIndex = td.colIndex! + const tdRowIndex = td.rowIndex! + if ( + tdColIndex >= startColIndex && + tdColIndex <= endColIndex && + tdRowIndex >= startRowIndex && + tdRowIndex <= endRowIndex + ) { + tdList.push(td) + } + } + if (tdList.length) { + rowCol.push(tdList) + } + } + return rowCol.length ? rowCol : null + } + + private _drawOuterBorder(payload: IDrawTableBorderOption) { + const { + ctx, + startX, + startY, + width, + height, + isDrawFullBorder, + borderExternalWidth + } = payload + const { scale } = this.options + // 外部边框单独设置 + const lineWidth = ctx.lineWidth + if (borderExternalWidth) { + ctx.lineWidth = borderExternalWidth * scale + } + ctx.beginPath() + const x = Math.round(startX) + const y = Math.round(startY) + ctx.translate(0.5, 0.5) + if (isDrawFullBorder) { + ctx.rect(x, y, width, height) + } else { + ctx.moveTo(x, y + height) + ctx.lineTo(x, y) + ctx.lineTo(x + width, y) + } + ctx.stroke() + // 还原边框设置 + if (borderExternalWidth) { + ctx.lineWidth = lineWidth + } + ctx.translate(-0.5, -0.5) + } + + private _drawSlash( + ctx: CanvasRenderingContext2D, + td: ITd, + startX: number, + startY: number + ) { + const { scale } = this.options + ctx.save() + const width = td.width! * scale + const height = td.height! * scale + const x = Math.round(td.x! * scale + startX) + const y = Math.round(td.y! * scale + startY) + // 正斜线 / + if (td.slashTypes?.includes(TdSlash.FORWARD)) { + ctx.moveTo(x + width, y) + ctx.lineTo(x, y + height) + } + // 反斜线 \ + if (td.slashTypes?.includes(TdSlash.BACK)) { + ctx.moveTo(x, y) + ctx.lineTo(x + width, y + height) + } + ctx.stroke() + ctx.restore() + } + + private _drawBorder( + ctx: CanvasRenderingContext2D, + element: IElement, + startX: number, + startY: number + ) { + const { + colgroup, + trList, + borderType, + borderColor, + borderWidth = 1, + borderExternalWidth + } = element + if (!colgroup || !trList) return + const { + scale, + table: { defaultBorderColor } + } = this.options + const tableWidth = element.width! * scale + const tableHeight = element.height! * scale + // 无边框 + const isEmptyBorderType = borderType === TableBorder.EMPTY + // 仅外边框 + const isExternalBorderType = borderType === TableBorder.EXTERNAL + // 内边框 + const isInternalBorderType = borderType === TableBorder.INTERNAL + ctx.save() + // 虚线 + if (borderType === TableBorder.DASH) { + ctx.setLineDash([3, 3]) + } + ctx.lineWidth = borderWidth * scale + ctx.strokeStyle = borderColor || defaultBorderColor + // 渲染边框 + if (!isEmptyBorderType && !isInternalBorderType) { + this._drawOuterBorder({ + ctx, + startX, + startY, + width: tableWidth, + height: tableHeight, + borderExternalWidth, + isDrawFullBorder: isExternalBorderType + }) + } + // 渲染单元格 + for (let t = 0; t < trList.length; t++) { + const tr = trList[t] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + // 单元格内斜线 + if (td.slashTypes?.length) { + this._drawSlash(ctx, td, startX, startY) + } + // 没有设置单元格边框 && 没有设置表格边框则忽略 + if ( + !td.borderTypes?.length && + (isEmptyBorderType || isExternalBorderType) + ) { + continue + } + const width = td.width! * scale + const height = td.height! * scale + const x = Math.round(td.x! * scale + startX + width) + const y = Math.round(td.y! * scale + startY) + ctx.translate(0.5, 0.5) + // 绘制线条 + ctx.beginPath() + // 单元格边框 + if (td.borderTypes?.includes(TdBorder.TOP)) { + ctx.moveTo(x - width, y) + ctx.lineTo(x, y) + ctx.stroke() + } + if (td.borderTypes?.includes(TdBorder.RIGHT)) { + ctx.moveTo(x, y) + ctx.lineTo(x, y + height) + ctx.stroke() + } + if (td.borderTypes?.includes(TdBorder.BOTTOM)) { + ctx.moveTo(x, y + height) + ctx.lineTo(x - width, y + height) + ctx.stroke() + } + if (td.borderTypes?.includes(TdBorder.LEFT)) { + ctx.moveTo(x - width, y) + ctx.lineTo(x - width, y + height) + ctx.stroke() + } + // 表格线 + if (!isEmptyBorderType && !isExternalBorderType) { + // 右边框 + if ( + !isInternalBorderType || + td.colIndex! + td.colspan < colgroup.length + ) { + ctx.moveTo(x, y) + ctx.lineTo(x, y + height) + // 外部边框宽度设置时 => 最右边框宽度单独设置 + if ( + borderExternalWidth && + borderExternalWidth !== borderWidth && + td.colIndex! + td.colspan === colgroup.length + ) { + const lineWidth = ctx.lineWidth + ctx.lineWidth = borderExternalWidth * scale + ctx.stroke() + // 清空path + ctx.beginPath() + ctx.lineWidth = lineWidth + } + } + // 下边框 + if ( + !isInternalBorderType || + td.rowIndex! + td.rowspan < trList.length + ) { + // 外部边框宽度设置时 => 立即绘制竖线 + const isSetExternalBottomBorder = + borderExternalWidth && + borderExternalWidth !== borderWidth && + td.rowIndex! + td.rowspan === trList.length + if (isSetExternalBottomBorder) { + ctx.stroke() + // 清空path + ctx.beginPath() + } + ctx.moveTo(x, y + height) + ctx.lineTo(x - width, y + height) + // 外部边框宽度设置时 => 最下边框宽度单独设置 + if (isSetExternalBottomBorder) { + const lineWidth = ctx.lineWidth + ctx.lineWidth = borderExternalWidth * scale + ctx.stroke() + // 清空path + ctx.beginPath() + ctx.lineWidth = lineWidth + } + } + ctx.stroke() + } + ctx.translate(-0.5, -0.5) + } + } + ctx.restore() + } + + private _drawBackgroundColor( + ctx: CanvasRenderingContext2D, + element: IElement, + startX: number, + startY: number + ) { + const { trList } = element + if (!trList) return + const { scale } = this.options + for (let t = 0; t < trList.length; t++) { + const tr = trList[t] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + if (!td.backgroundColor) continue + ctx.save() + const width = td.width! * scale + const height = td.height! * scale + const x = Math.round(td.x! * scale + startX) + const y = Math.round(td.y! * scale + startY) + ctx.fillStyle = td.backgroundColor + ctx.fillRect(x, y, width, height) + ctx.restore() + } + } + } + + public getTableWidth(element: IElement): number { + return element.colgroup!.reduce((pre, cur) => pre + cur.width, 0) + } + + public getTableHeight(element: IElement): number { + const trList = element.trList + if (!trList?.length) return 0 + return this.getTdListByColIndex(trList, 0).reduce( + (pre, cur) => pre + cur.height!, + 0 + ) + } + + public getRowCountByColIndex(trList: ITr[], colIndex: number): number { + return this.getTdListByColIndex(trList, colIndex).reduce( + (pre, cur) => pre + cur.rowspan, + 0 + ) + } + + public getTdListByColIndex(trList: ITr[], colIndex: number): ITd[] { + const data: ITd[] = [] + for (let r = 0; r < trList.length; r++) { + const tdList = trList[r].tdList + for (let d = 0; d < tdList.length; d++) { + const td = tdList[d] + const min = td.colIndex! + const max = min + td.colspan - 1 + if (colIndex >= min && colIndex <= max) { + data.push(td) + } + } + } + return data + } + + public getTdListByRowIndex(trList: ITr[], rowIndex: number) { + const data: ITd[] = [] + for (let r = 0; r < trList.length; r++) { + const tdList = trList[r].tdList + for (let d = 0; d < tdList.length; d++) { + const td = tdList[d] + const min = td.rowIndex! + const max = min + td.rowspan - 1 + if (rowIndex >= min && rowIndex <= max) { + data.push(td) + } + } + } + return data + } + + public computeRowColInfo(element: IElement) { + const { colgroup, trList } = element + if (!colgroup || !trList) return + let preX = 0 + for (let t = 0; t < trList.length; t++) { + const tr = trList[t] + // 表格最后一行 + const isLastTr = trList.length - 1 === t + // 当前行最小高度 + let rowMinHeight = 0 + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + // 计算当前td所属列索引 + let colIndex = 0 + // 第一行td位置为当前列索引+上一个单元格colspan,否则从第一行开始计算列偏移量 + if (trList.length > 1 && t !== 0) { + // 当前列起始索引:以之前单元格为起始点 + const preTd = tr.tdList[d - 1] + const start = preTd ? preTd.colIndex! + preTd.colspan : d + for (let c = start; c < colgroup.length; c++) { + // 查找相同索引列之前行数,相加判断是否位置被挤占 + const rowCount = this.getRowCountByColIndex(trList.slice(0, t), c) + // 不存在挤占则默认当前单元格可以存在该位置 + if (rowCount === t) { + colIndex = c + // 重置单元格起始位置坐标 + let preColWidth = 0 + for (let preC = 0; preC < c; preC++) { + preColWidth += colgroup[preC].width + } + preX = preColWidth + break + } + } + } else { + const preTd = tr.tdList[d - 1] + if (preTd) { + colIndex = preTd.colIndex! + preTd.colspan + } + } + // 计算格宽高 + let width = 0 + for (let col = 0; col < td.colspan; col++) { + width += colgroup[col + colIndex].width + } + let height = 0 + for (let row = 0; row < td.rowspan; row++) { + const curTr = trList[row + t] || trList[t] + height += curTr.height + } + // y偏移量 + if (rowMinHeight === 0 || rowMinHeight > height) { + rowMinHeight = height + } + // 当前行最后一个td + const isLastRowTd = tr.tdList.length - 1 === d + // 当前列最后一个td + let isLastColTd = isLastTr + if (!isLastColTd) { + if (td.rowspan > 1) { + const nextTrLength = trList.length - 1 - t + isLastColTd = td.rowspan - 1 === nextTrLength + } + } + // 当前表格最后一个td + const isLastTd = isLastTr && isLastRowTd + td.isLastRowTd = isLastRowTd + td.isLastColTd = isLastColTd + td.isLastTd = isLastTd + // 修改当前格clientBox + td.x = preX + // 之前行相同列的高度 + let preY = 0 + for (let preR = 0; preR < t; preR++) { + const preTdList = trList[preR].tdList + for (let preD = 0; preD < preTdList.length; preD++) { + const td = preTdList[preD] + if ( + colIndex >= td.colIndex! && + colIndex < td.colIndex! + td.colspan + ) { + preY += td.height! + break + } + } + } + td.y = preY + td.width = width + td.height = height + td.rowIndex = t + td.colIndex = colIndex + td.trIndex = t + td.tdIndex = d + // 当前列x轴累加 + preX += width + // 一行中的最后td + if (isLastRowTd && !isLastTd) { + preX = 0 + } + } + } + } + + public drawRange( + ctx: CanvasRenderingContext2D, + element: IElement, + startX: number, + startY: number + ) { + const { scale, rangeAlpha, rangeColor } = this.options + const { type, trList } = element + if (!trList || type !== ElementType.TABLE) return + const { + isCrossRowCol, + startTdIndex, + endTdIndex, + startTrIndex, + endTrIndex + } = this.range.getRange() + // 存在跨行/列 + if (!isCrossRowCol) return + let startTd = trList[startTrIndex!].tdList[startTdIndex!] + let endTd = trList[endTrIndex!].tdList[endTdIndex!] + // 交换起始位置 + if (startTd.x! > endTd.x! || startTd.y! > endTd.y!) { + // prettier-ignore + [startTd, endTd] = [endTd, startTd] + } + const startColIndex = startTd.colIndex! + const endColIndex = endTd.colIndex! + (endTd.colspan - 1) + const startRowIndex = startTd.rowIndex! + const endRowIndex = endTd.rowIndex! + (endTd.rowspan - 1) + ctx.save() + for (let t = 0; t < trList.length; t++) { + const tr = trList[t] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const tdColIndex = td.colIndex! + const tdRowIndex = td.rowIndex! + if ( + tdColIndex >= startColIndex && + tdColIndex <= endColIndex && + tdRowIndex >= startRowIndex && + tdRowIndex <= endRowIndex + ) { + const x = td.x! * scale + const y = td.y! * scale + const width = td.width! * scale + const height = td.height! * scale + ctx.globalAlpha = rangeAlpha + ctx.fillStyle = rangeColor + ctx.fillRect(x + startX, y + startY, width, height) + } + } + } + ctx.restore() + } + + public render( + ctx: CanvasRenderingContext2D, + element: IElement, + startX: number, + startY: number + ) { + this._drawBackgroundColor(ctx, element, startX, startY) + this._drawBorder(ctx, element, startX, startY) + } +} diff --git a/src/editor/core/draw/particle/table/TableTool.ts b/src/editor/core/draw/particle/table/TableTool.ts new file mode 100644 index 0000000..0a13b96 --- /dev/null +++ b/src/editor/core/draw/particle/table/TableTool.ts @@ -0,0 +1,512 @@ +import { IElement } from '../../../..' +import { EDITOR_PREFIX } from '../../../../dataset/constant/Editor' +import { TableOrder } from '../../../../dataset/enum/table/TableTool' +import { DeepRequired } from '../../../../interface/Common' +import { IEditorOption } from '../../../../interface/Editor' +import { Position } from '../../../position/Position' +import { RangeManager } from '../../../range/RangeManager' +import { Draw } from '../../Draw' + +interface IAnchorMouseDown { + evt: MouseEvent + order: TableOrder + index: number + element: IElement +} + +export class TableTool { + // 单元格最小宽度 + private readonly MIN_TD_WIDTH = 20 + // 行列工具相对表格偏移值 + private readonly ROW_COL_OFFSET = 18 + // 快速添加行列工具宽度 + private readonly ROW_COL_QUICK_WIDTH = 16 + // 快速添加行列工具偏移值 + private readonly ROW_COL_QUICK_OFFSET = 5 + // 快速添加行列工具相对表格位置 + private readonly ROW_COL_QUICK_POSITION = + this.ROW_COL_OFFSET + (this.ROW_COL_OFFSET - this.ROW_COL_QUICK_WIDTH) / 2 + // 边框工具宽/高度 + private readonly BORDER_VALUE = 4 + // 快速选择工具偏移值 + private readonly TABLE_SELECT_OFFSET = 20 + + private draw: Draw + private canvas: HTMLCanvasElement + private options: DeepRequired + private position: Position + private range: RangeManager + private container: HTMLDivElement + private toolRowContainer: HTMLDivElement | null + private toolRowAddBtn: HTMLDivElement | null + private toolColAddBtn: HTMLDivElement | null + private toolTableSelectBtn: HTMLDivElement | null + private toolColContainer: HTMLDivElement | null + private toolBorderContainer: HTMLDivElement | null + private anchorLine: HTMLDivElement | null + private mousedownX: number + private mousedownY: number + + constructor(draw: Draw) { + this.draw = draw + this.canvas = draw.getPage() + this.options = draw.getOptions() + this.position = draw.getPosition() + this.range = draw.getRange() + this.container = draw.getContainer() + // x、y轴 + this.toolRowContainer = null + this.toolRowAddBtn = null + this.toolColAddBtn = null + this.toolTableSelectBtn = null + this.toolColContainer = null + this.toolBorderContainer = null + this.anchorLine = null + this.mousedownX = 0 + this.mousedownY = 0 + } + + public dispose() { + this.toolRowContainer?.remove() + this.toolRowAddBtn?.remove() + this.toolColAddBtn?.remove() + this.toolTableSelectBtn?.remove() + this.toolColContainer?.remove() + this.toolBorderContainer?.remove() + this.toolRowContainer = null + this.toolRowAddBtn = null + this.toolColAddBtn = null + this.toolTableSelectBtn = null + this.toolColContainer = null + this.toolBorderContainer = null + } + + public render() { + const { isTable, index, trIndex, tdIndex } = + this.position.getPositionContext() + if (!isTable) return + // 销毁之前工具 + this.dispose() + const elementList = this.draw.getOriginalElementList() + const positionList = this.position.getOriginalPositionList() + const element = elementList[index!] + // 表格工具配置禁用又非设计模式时不渲染 + if (element.tableToolDisabled && !this.draw.isDesignMode()) return + // 渲染所需数据 + const { scale } = this.options + const position = positionList[index!] + const { colgroup, trList } = element + const { + coordinate: { leftTop } + } = position + const height = this.draw.getHeight() + const pageGap = this.draw.getPageGap() + const prePageHeight = this.draw.getPageNo() * (height + pageGap) + const tableX = leftTop[0] + const tableY = leftTop[1] + prePageHeight + const td = element.trList![trIndex!].tdList[tdIndex!] + const rowIndex = td.rowIndex + const colIndex = td.colIndex + const tableHeight = element.height! * scale + const tableWidth = element.width! * scale + // 表格选择工具 + const tableSelectBtn = document.createElement('div') + tableSelectBtn.classList.add(`${EDITOR_PREFIX}-table-tool__select`) + tableSelectBtn.style.height = `${tableHeight * scale}` + tableSelectBtn.style.left = `${tableX}px` + tableSelectBtn.style.top = `${tableY}px` + tableSelectBtn.style.transform = `translate(-${ + this.TABLE_SELECT_OFFSET * scale + }px, ${-this.TABLE_SELECT_OFFSET * scale}px)` + // 快捷全选 + tableSelectBtn.onclick = () => { + this.draw.getTableOperate().tableSelectAll() + } + this.container.append(tableSelectBtn) + this.toolTableSelectBtn = tableSelectBtn + // 渲染行工具 + const rowHeightList = trList!.map(tr => tr.height) + const rowContainer = document.createElement('div') + rowContainer.classList.add(`${EDITOR_PREFIX}-table-tool__row`) + rowContainer.style.transform = `translateX(-${ + this.ROW_COL_OFFSET * scale + }px)` + for (let r = 0; r < rowHeightList.length; r++) { + const rowHeight = rowHeightList[r] * scale + const rowItem = document.createElement('div') + rowItem.classList.add(`${EDITOR_PREFIX}-table-tool__row__item`) + if (r === rowIndex) { + rowItem.classList.add('active') + } + // 快捷行选择 + rowItem.onclick = () => { + const tdList = this.draw + .getTableParticle() + .getTdListByRowIndex(trList!, r) + const firstTd = tdList[0] + const lastTd = tdList[tdList.length - 1] + this.position.setPositionContext({ + index, + isTable: true, + trIndex: firstTd.trIndex, + tdIndex: firstTd.tdIndex, + tableId: element.id + }) + this.range.setRange( + 0, + 0, + element.id, + firstTd.tdIndex, + lastTd.tdIndex, + firstTd.trIndex, + lastTd.trIndex + ) + this.draw.render({ + curIndex: 0, + isCompute: false, + isSubmitHistory: false + }) + this._setAnchorActive(rowContainer, r) + } + const rowItemAnchor = document.createElement('div') + rowItemAnchor.classList.add(`${EDITOR_PREFIX}-table-tool__anchor`) + // 行高度拖拽开始 + rowItemAnchor.onmousedown = evt => { + this._mousedown({ + evt, + element, + index: r, + order: TableOrder.ROW + }) + } + rowItem.append(rowItemAnchor) + rowItem.style.height = `${rowHeight}px` + rowContainer.append(rowItem) + } + rowContainer.style.left = `${tableX}px` + rowContainer.style.top = `${tableY}px` + this.container.append(rowContainer) + this.toolRowContainer = rowContainer + // 添加行按钮 + const rowAddBtn = document.createElement('div') + rowAddBtn.classList.add(`${EDITOR_PREFIX}-table-tool__quick__add`) + rowAddBtn.style.height = `${tableHeight * scale}` + rowAddBtn.style.left = `${tableX}px` + rowAddBtn.style.top = `${tableY + tableHeight}px` + rowAddBtn.style.transform = `translate(-${ + this.ROW_COL_QUICK_POSITION * scale + }px, ${this.ROW_COL_QUICK_OFFSET * scale}px)` + // 快捷添加行 + rowAddBtn.onclick = () => { + this.position.setPositionContext({ + index, + isTable: true, + trIndex: trList!.length - 1, + tdIndex: 0, + tableId: element.id + }) + this.draw.getTableOperate().insertTableBottomRow() + } + this.container.append(rowAddBtn) + this.toolRowAddBtn = rowAddBtn + // 渲染列工具 + const colWidthList = colgroup!.map(col => col.width) + const colContainer = document.createElement('div') + colContainer.classList.add(`${EDITOR_PREFIX}-table-tool__col`) + colContainer.style.transform = `translateY(-${ + this.ROW_COL_OFFSET * scale + }px)` + for (let c = 0; c < colWidthList.length; c++) { + const colWidth = colWidthList[c] * scale + const colItem = document.createElement('div') + colItem.classList.add(`${EDITOR_PREFIX}-table-tool__col__item`) + if (c === colIndex) { + colItem.classList.add('active') + } + // 快捷列选择 + colItem.onclick = () => { + const tdList = this.draw + .getTableParticle() + .getTdListByColIndex(trList!, c) + const firstTd = tdList[0] + const lastTd = tdList[tdList.length - 1] + this.position.setPositionContext({ + index, + isTable: true, + trIndex: firstTd.trIndex, + tdIndex: firstTd.tdIndex, + tableId: element.id + }) + this.range.setRange( + 0, + 0, + element.id, + firstTd.tdIndex, + lastTd.tdIndex, + firstTd.trIndex, + lastTd.trIndex + ) + this.draw.render({ + curIndex: 0, + isCompute: false, + isSubmitHistory: false + }) + this._setAnchorActive(colContainer, c) + } + const colItemAnchor = document.createElement('div') + colItemAnchor.classList.add(`${EDITOR_PREFIX}-table-tool__anchor`) + // 列高度拖拽开始 + colItemAnchor.onmousedown = evt => { + this._mousedown({ + evt, + element, + index: c, + order: TableOrder.COL + }) + } + colItem.append(colItemAnchor) + colItem.style.width = `${colWidth}px` + colContainer.append(colItem) + } + colContainer.style.left = `${tableX}px` + colContainer.style.top = `${tableY}px` + this.container.append(colContainer) + this.toolColContainer = colContainer + // 添加列按钮 + const colAddBtn = document.createElement('div') + colAddBtn.classList.add(`${EDITOR_PREFIX}-table-tool__quick__add`) + colAddBtn.style.height = `${tableHeight * scale}` + colAddBtn.style.left = `${tableX + tableWidth}px` + colAddBtn.style.top = `${tableY}px` + colAddBtn.style.transform = `translate(${ + this.ROW_COL_QUICK_OFFSET * scale + }px, -${this.ROW_COL_QUICK_POSITION * scale}px)` + // 快捷添加列 + colAddBtn.onclick = () => { + this.position.setPositionContext({ + index, + isTable: true, + trIndex: 0, + tdIndex: trList![0].tdList.length - 1 || 0, + tableId: element.id + }) + this.draw.getTableOperate().insertTableRightCol() + } + this.container.append(colAddBtn) + this.toolColAddBtn = colAddBtn + // 渲染单元格边框拖拽工具 + const borderContainer = document.createElement('div') + borderContainer.classList.add(`${EDITOR_PREFIX}-table-tool__border`) + borderContainer.style.height = `${tableHeight}px` + borderContainer.style.width = `${tableWidth}px` + borderContainer.style.left = `${tableX}px` + borderContainer.style.top = `${tableY}px` + for (let r = 0; r < trList!.length; r++) { + const tr = trList![r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const rowBorder = document.createElement('div') + rowBorder.classList.add(`${EDITOR_PREFIX}-table-tool__border__row`) + rowBorder.style.width = `${td.width! * scale}px` + rowBorder.style.height = `${this.BORDER_VALUE}px` + rowBorder.style.top = `${ + (td.y! + td.height!) * scale - this.BORDER_VALUE / 2 + }px` + rowBorder.style.left = `${td.x! * scale}px` + // 行宽度拖拽开始 + rowBorder.onmousedown = evt => { + this._mousedown({ + evt, + element, + index: td.rowIndex! + td.rowspan - 1, + order: TableOrder.ROW + }) + } + borderContainer.appendChild(rowBorder) + const colBorder = document.createElement('div') + colBorder.classList.add(`${EDITOR_PREFIX}-table-tool__border__col`) + colBorder.style.width = `${this.BORDER_VALUE}px` + colBorder.style.height = `${td.height! * scale}px` + colBorder.style.top = `${td.y! * scale}px` + colBorder.style.left = `${ + (td.x! + td.width!) * scale - this.BORDER_VALUE / 2 + }px` + // 列高度拖拽开始 + colBorder.onmousedown = evt => { + this._mousedown({ + evt, + element, + index: td.colIndex! + td.colspan - 1, + order: TableOrder.COL + }) + } + borderContainer.appendChild(colBorder) + } + } + this.container.append(borderContainer) + this.toolBorderContainer = borderContainer + } + + private _setAnchorActive(container: HTMLDivElement, index: number) { + const children = container.children + for (let c = 0; c < children.length; c++) { + const child = children[c] + if (c === index) { + child.classList.add('active') + } else { + child.classList.remove('active') + } + } + } + + private _mousedown(payload: IAnchorMouseDown) { + const { evt, index, order, element } = payload + this.canvas = this.draw.getPage() + const { scale } = this.options + const width = this.draw.getWidth() + const height = this.draw.getHeight() + const pageGap = this.draw.getPageGap() + const prePageHeight = this.draw.getPageNo() * (height + pageGap) + this.mousedownX = evt.x + this.mousedownY = evt.y + const target = evt.target as HTMLDivElement + const canvasRect = this.canvas.getBoundingClientRect() + // 改变光标 + const cursor = window.getComputedStyle(target).cursor + document.body.style.cursor = cursor + this.canvas.style.cursor = cursor + // 拖拽线 + let startX = 0 + let startY = 0 + const anchorLine = document.createElement('div') + anchorLine.classList.add(`${EDITOR_PREFIX}-table-anchor__line`) + if (order === TableOrder.ROW) { + anchorLine.classList.add(`${EDITOR_PREFIX}-table-anchor__line__row`) + anchorLine.style.width = `${width}px` + startX = 0 + startY = prePageHeight + this.mousedownY - canvasRect.top + } else { + anchorLine.classList.add(`${EDITOR_PREFIX}-table-anchor__line__col`) + anchorLine.style.height = `${height}px` + startX = this.mousedownX - canvasRect.left + startY = prePageHeight + } + anchorLine.style.left = `${startX}px` + anchorLine.style.top = `${startY}px` + this.container.append(anchorLine) + this.anchorLine = anchorLine + // 追加全局事件 + let dx = 0 + let dy = 0 + const mousemoveFn = (evt: MouseEvent) => { + const movePosition = this._mousemove(evt, order, startX, startY) + if (movePosition) { + dx = movePosition.dx + dy = movePosition.dy + } + } + document.addEventListener('mousemove', mousemoveFn) + document.addEventListener( + 'mouseup', + () => { + let isChangeSize = false + // 改变尺寸 + if (order === TableOrder.ROW) { + const trList = element.trList! + const tr = trList[index] || trList[index - 1] + // 最大移动高度-向上移动超出最小高度限定,则减少移动量 + const { defaultTrMinHeight } = this.options.table + if (dy < 0 && tr.height + dy < defaultTrMinHeight) { + dy = defaultTrMinHeight - tr.height + } + if (dy) { + tr.height += dy + tr.minHeight = tr.height + isChangeSize = true + } + } else { + const { colgroup } = element + if (colgroup && dx) { + // 宽度分配 + const innerWidth = this.draw.getInnerWidth() + const curColWidth = colgroup[index].width + // 最小移动距离计算-如果向左移动:使单元格小于最小宽度,则减少移动量 + if (dx < 0 && curColWidth + dx < this.MIN_TD_WIDTH) { + dx = this.MIN_TD_WIDTH - curColWidth + } + // 最大移动距离计算-如果向右移动:使后面一个单元格小于最小宽度,则减少移动量 + const nextColWidth = colgroup[index + 1]?.width + if ( + dx > 0 && + nextColWidth && + nextColWidth - dx < this.MIN_TD_WIDTH + ) { + dx = nextColWidth - this.MIN_TD_WIDTH + } + const moveColWidth = curColWidth + dx + // 开始移动,只有表格的最后一列线才会改变表格的宽度,其他场景不用计算表格超出 + if (index === colgroup.length - 1) { + let moveTableWidth = 0 + for (let c = 0; c < colgroup.length; c++) { + const group = colgroup[c] + // 下一列减去偏移量 + if (c === index + 1) { + moveTableWidth -= dx + } + // 当前列加上偏移量 + if (c === index) { + moveTableWidth += moveColWidth + } + if (c !== index) { + moveTableWidth += group.width + } + } + if (moveTableWidth > innerWidth) { + const tableWidth = element.width! + dx = innerWidth - tableWidth + } + } + if (dx) { + // 当前列增加,后列减少 + if (colgroup.length - 1 !== index) { + colgroup[index + 1].width -= dx / scale + } + colgroup[index].width += dx / scale + isChangeSize = true + } + } + } + if (isChangeSize) { + this.draw.render({ isSetCursor: false }) + } + // 还原副作用 + anchorLine.remove() + document.removeEventListener('mousemove', mousemoveFn) + document.body.style.cursor = '' + this.canvas.style.cursor = 'text' + }, + { + once: true + } + ) + evt.preventDefault() + } + + private _mousemove( + evt: MouseEvent, + tableOrder: TableOrder, + startX: number, + startY: number + ): { dx: number; dy: number } | null { + if (!this.anchorLine) return null + const dx = evt.x - this.mousedownX + const dy = evt.y - this.mousedownY + if (tableOrder === TableOrder.ROW) { + this.anchorLine.style.top = `${startY + dy}px` + } else { + this.anchorLine.style.left = `${startX + dx}px` + } + evt.preventDefault() + return { dx, dy } + } +} diff --git a/src/editor/core/draw/richtext/AbstractRichText.ts b/src/editor/core/draw/richtext/AbstractRichText.ts new file mode 100644 index 0000000..5eeca73 --- /dev/null +++ b/src/editor/core/draw/richtext/AbstractRichText.ts @@ -0,0 +1,59 @@ +import { TextDecorationStyle } from '../../../dataset/enum/Text' +import { IElementFillRect } from '../../../interface/Element' + +export abstract class AbstractRichText { + protected fillRect: IElementFillRect + protected fillColor?: string + protected fillDecorationStyle?: TextDecorationStyle + + constructor() { + this.fillRect = this.clearFillInfo() + } + + public clearFillInfo() { + this.fillColor = undefined + this.fillDecorationStyle = undefined + this.fillRect = { + x: 0, + y: 0, + width: 0, + height: 0 + } + return this.fillRect + } + + public recordFillInfo( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + width: number, + height?: number, + color?: string, + decorationStyle?: TextDecorationStyle + ) { + const isFirstRecord = !this.fillRect.width + // 颜色不同时立即绘制 + if ( + !isFirstRecord && + (this.fillColor !== color || this.fillDecorationStyle !== decorationStyle) + ) { + this.render(ctx) + this.clearFillInfo() + // 重新记录 + this.recordFillInfo(ctx, x, y, width, height, color, decorationStyle) + return + } + if (isFirstRecord) { + this.fillRect.x = x + this.fillRect.y = y + } + if (height && this.fillRect.height < height) { + this.fillRect.height = height + } + this.fillRect.width += width + this.fillColor = color + this.fillDecorationStyle = decorationStyle + } + + public abstract render(ctx: CanvasRenderingContext2D): void +} diff --git a/src/editor/core/draw/richtext/Highlight.ts b/src/editor/core/draw/richtext/Highlight.ts new file mode 100644 index 0000000..a6ad738 --- /dev/null +++ b/src/editor/core/draw/richtext/Highlight.ts @@ -0,0 +1,24 @@ +import { AbstractRichText } from './AbstractRichText' +import { IEditorOption } from '../../../interface/Editor' +import { Draw } from '../Draw' + +export class Highlight extends AbstractRichText { + private options: Required + + constructor(draw: Draw) { + super() + this.options = draw.getOptions() + } + + public render(ctx: CanvasRenderingContext2D) { + if (!this.fillRect.width) return + const { highlightAlpha } = this.options + const { x, y, width, height } = this.fillRect + ctx.save() + ctx.globalAlpha = highlightAlpha + ctx.fillStyle = this.fillColor! + ctx.fillRect(x, y, width, height) + ctx.restore() + this.clearFillInfo() + } +} diff --git a/src/editor/core/draw/richtext/Strikeout.ts b/src/editor/core/draw/richtext/Strikeout.ts new file mode 100644 index 0000000..0c456af --- /dev/null +++ b/src/editor/core/draw/richtext/Strikeout.ts @@ -0,0 +1,28 @@ +import { AbstractRichText } from './AbstractRichText' +import { IEditorOption } from '../../../interface/Editor' +import { Draw } from '../Draw' + +export class Strikeout extends AbstractRichText { + private options: Required + + constructor(draw: Draw) { + super() + this.options = draw.getOptions() + } + + public render(ctx: CanvasRenderingContext2D) { + if (!this.fillRect.width) return + const { scale, strikeoutColor } = this.options + const { x, y, width } = this.fillRect + ctx.save() + ctx.lineWidth = scale + ctx.strokeStyle = strikeoutColor + const adjustY = y + 0.5 // 从1处渲染,避免线宽度等于3 + ctx.beginPath() + ctx.moveTo(x, adjustY) + ctx.lineTo(x + width, adjustY) + ctx.stroke() + ctx.restore() + this.clearFillInfo() + } +} diff --git a/src/editor/core/draw/richtext/Underline.ts b/src/editor/core/draw/richtext/Underline.ts new file mode 100644 index 0000000..ff73f7b --- /dev/null +++ b/src/editor/core/draw/richtext/Underline.ts @@ -0,0 +1,106 @@ +import { AbstractRichText } from './AbstractRichText' +import { IEditorOption } from '../../../interface/Editor' +import { Draw } from '../Draw' +import { DashType, TextDecorationStyle } from '../../../dataset/enum/Text' + +export class Underline extends AbstractRichText { + private options: Required + + constructor(draw: Draw) { + super() + this.options = draw.getOptions() + } + + // 下划线 + private _drawLine( + ctx: CanvasRenderingContext2D, + startX: number, + startY: number, + width: number, + dashType?: DashType + ) { + const endX = startX + width + ctx.beginPath() + switch (dashType) { + case DashType.DASHED: + // 长虚线- - - - - - + ctx.setLineDash([3, 1]) + break + case DashType.DOTTED: + // 点虚线 . . . . . . + ctx.setLineDash([1, 1]) + break + } + ctx.moveTo(startX, startY) + ctx.lineTo(endX, startY) + ctx.stroke() + } + + // 双实线 + private _drawDouble( + ctx: CanvasRenderingContext2D, + startX: number, + startY: number, + width: number + ) { + const SPACING = 3 // 双实线间距 + const endX = startX + width + const endY = startY + SPACING * this.options.scale + ctx.beginPath() + ctx.moveTo(startX, startY) + ctx.lineTo(endX, startY) + ctx.stroke() + ctx.beginPath() + ctx.moveTo(startX, endY) + ctx.lineTo(endX, endY) + ctx.stroke() + } + + // 波浪线 + private _drawWave( + ctx: CanvasRenderingContext2D, + startX: number, + startY: number, + width: number + ) { + const { scale } = this.options + const AMPLITUDE = 1.2 * scale // 振幅 + const FREQUENCY = 1 / scale // 频率 + const adjustY = startY + 2 * AMPLITUDE // 增加2倍振幅 + ctx.beginPath() + for (let x = 0; x < width; x++) { + const y = AMPLITUDE * Math.sin(FREQUENCY * x) + ctx.lineTo(startX + x, adjustY + y) + } + ctx.stroke() + } + + public render(ctx: CanvasRenderingContext2D) { + if (!this.fillRect.width) return + const { underlineColor, scale } = this.options + const { x, y, width } = this.fillRect + ctx.save() + ctx.strokeStyle = this.fillColor || underlineColor + ctx.lineWidth = scale + const adjustY = Math.floor(y + 2 * ctx.lineWidth) + 0.5 // +0.5从1处渲染,避免线宽度等于3 + switch (this.fillDecorationStyle) { + case TextDecorationStyle.WAVY: + this._drawWave(ctx, x, adjustY, width) + break + case TextDecorationStyle.DOUBLE: + this._drawDouble(ctx, x, adjustY, width) + break + case TextDecorationStyle.DASHED: + this._drawLine(ctx, x, adjustY, width, DashType.DASHED) + break + case TextDecorationStyle.DOTTED: + this._drawLine(ctx, x, adjustY, width, DashType.DOTTED) + break + default: + this._drawLine(ctx, x, adjustY, width) + break + } + ctx.restore() + this.clearFillInfo() + } +} diff --git a/src/editor/core/event/CanvasEvent.ts b/src/editor/core/event/CanvasEvent.ts new file mode 100644 index 0000000..ecfa1c2 --- /dev/null +++ b/src/editor/core/event/CanvasEvent.ts @@ -0,0 +1,202 @@ +import { ElementStyleKey } from '../../dataset/enum/ElementStyle' +import { IElement, IElementPosition } from '../../interface/Element' +import { ICurrentPosition, IPositionContext } from '../../interface/Position' +import { Draw } from '../draw/Draw' +import { Position } from '../position/Position' +import { RangeManager } from '../range/RangeManager' +import { threeClick } from '../../utils' +import { IRange, IRangeElementStyle } from '../../interface/Range' +import { mousedown } from './handlers/mousedown' +import { mouseup } from './handlers/mouseup' +import { mouseleave } from './handlers/mouseleave' +import { mousemove } from './handlers/mousemove' +import { keydown } from './handlers/keydown' +import { input } from './handlers/input' +import { cut } from './handlers/cut' +import { copy } from './handlers/copy' +import { drop } from './handlers/drop' +import click from './handlers/click' +import composition from './handlers/composition' +import drag from './handlers/drag' +import { isIOS } from '../../utils/ua' +import { ICopyOption } from '../../interface/Event' + +export interface ICompositionInfo { + elementList: IElement[] + startIndex: number + endIndex: number + value: string + defaultStyle: IRangeElementStyle | null +} + +export class CanvasEvent { + public isAllowSelection: boolean + public isComposing: boolean + public compositionInfo: ICompositionInfo | null + + public isAllowDrag: boolean + public isAllowDrop: boolean + public cacheRange: IRange | null + public cacheElementList: IElement[] | null + public cachePositionList: IElementPosition[] | null + public cachePositionContext: IPositionContext | null + public mouseDownStartPosition: ICurrentPosition | null + + private draw: Draw + private pageContainer: HTMLDivElement + private pageList: HTMLCanvasElement[] + private range: RangeManager + private position: Position + + constructor(draw: Draw) { + this.draw = draw + this.pageContainer = draw.getPageContainer() + this.pageList = draw.getPageList() + this.range = this.draw.getRange() + this.position = this.draw.getPosition() + + this.isAllowSelection = false + this.isComposing = false + this.compositionInfo = null + this.isAllowDrag = false + this.isAllowDrop = false + this.cacheRange = null + this.cacheElementList = null + this.cachePositionList = null + this.cachePositionContext = null + this.mouseDownStartPosition = null + } + + public getDraw(): Draw { + return this.draw + } + + public register() { + this.pageContainer.addEventListener('click', this.click.bind(this)) + this.pageContainer.addEventListener('mousedown', this.mousedown.bind(this)) + this.pageContainer.addEventListener('mouseup', this.mouseup.bind(this)) + this.pageContainer.addEventListener( + 'mouseleave', + this.mouseleave.bind(this) + ) + this.pageContainer.addEventListener('mousemove', this.mousemove.bind(this)) + this.pageContainer.addEventListener('dblclick', this.dblclick.bind(this)) + this.pageContainer.addEventListener('dragover', this.dragover.bind(this)) + this.pageContainer.addEventListener('drop', this.drop.bind(this)) + threeClick(this.pageContainer, this.threeClick.bind(this)) + } + + public setIsAllowSelection(payload: boolean) { + this.isAllowSelection = payload + if (!payload) { + this.applyPainterStyle() + } + } + + public setIsAllowDrag(payload: boolean) { + this.isAllowDrag = payload + this.isAllowDrop = payload + } + + public clearPainterStyle() { + this.pageList.forEach(p => { + p.style.cursor = 'text' + }) + this.draw.setPainterStyle(null) + } + + public applyPainterStyle() { + const painterStyle = this.draw.getPainterStyle() + if (!painterStyle) return + const isDisabled = this.draw.isReadonly() || this.draw.isDisabled() + if (isDisabled) return + const selection = this.range.getSelection() + if (!selection) return + const painterStyleKeys = Object.keys(painterStyle) + selection.forEach(s => { + painterStyleKeys.forEach(pKey => { + const key = pKey as keyof typeof ElementStyleKey + s[key] = painterStyle[key] as any + }) + }) + this.draw.render({ isSetCursor: false }) + // 清除格式刷 + const painterOptions = this.draw.getPainterOptions() + if (!painterOptions || !painterOptions.isDblclick) { + this.clearPainterStyle() + } + } + + public selectAll() { + const position = this.position.getPositionList() + this.range.setRange(0, position.length - 1) + this.draw.render({ + isSubmitHistory: false, + isSetCursor: false, + isCompute: false + }) + } + + public mousemove(evt: MouseEvent) { + mousemove(evt, this) + } + + public mousedown(evt: MouseEvent) { + mousedown(evt, this) + } + + public click() { + // IOS系统限制非用户主动触发事件的键盘弹出 + if (isIOS && !this.draw.isReadonly()) { + this.draw.getCursor().getAgentDom().focus() + } + } + + public mouseup(evt: MouseEvent) { + mouseup(evt, this) + } + + public mouseleave(evt: MouseEvent) { + mouseleave(evt, this) + } + + public keydown(evt: KeyboardEvent) { + keydown(evt, this) + } + + public dblclick(evt: MouseEvent) { + click.dblclick(this, evt) + } + + public threeClick() { + click.threeClick(this) + } + + public input(data: string) { + input(data, this) + } + + public cut() { + cut(this) + } + + public copy(options?: ICopyOption) { + copy(this, options) + } + + public compositionstart() { + composition.compositionstart(this) + } + + public compositionend(evt: CompositionEvent) { + composition.compositionend(this, evt) + } + + public drop(evt: DragEvent) { + drop(evt, this) + } + + public dragover(evt: DragEvent | MouseEvent) { + drag.dragover(evt, this) + } +} diff --git a/src/editor/core/event/GlobalEvent.ts b/src/editor/core/event/GlobalEvent.ts new file mode 100644 index 0000000..e4dcc57 --- /dev/null +++ b/src/editor/core/event/GlobalEvent.ts @@ -0,0 +1,173 @@ +import { EDITOR_COMPONENT } from '../../dataset/constant/Editor' +import { IEditorOption } from '../../interface/Editor' +import { findParent } from '../../utils' +import { Cursor } from '../cursor/Cursor' +import { Control } from '../draw/control/Control' +import { Draw } from '../draw/Draw' +import { HyperlinkParticle } from '../draw/particle/HyperlinkParticle' +import { DateParticle } from '../draw/particle/date/DateParticle' +import { Previewer } from '../draw/particle/previewer/Previewer' +import { TableTool } from '../draw/particle/table/TableTool' +import { RangeManager } from '../range/RangeManager' +import { CanvasEvent } from './CanvasEvent' +import { ImageParticle } from '../draw/particle/ImageParticle' +import { INTERNAL_SHORTCUT_KEY } from '../../dataset/constant/Shortcut' + +export class GlobalEvent { + private draw: Draw + private options: Required + private cursor: Cursor | null + private canvasEvent: CanvasEvent + private range: RangeManager + private previewer: Previewer + private tableTool: TableTool + private hyperlinkParticle: HyperlinkParticle + private control: Control + private dateParticle: DateParticle + private imageParticle: ImageParticle + private dprMediaQueryList: MediaQueryList + + constructor(draw: Draw, canvasEvent: CanvasEvent) { + this.draw = draw + this.options = draw.getOptions() + this.canvasEvent = canvasEvent + this.cursor = null + this.range = draw.getRange() + this.previewer = draw.getPreviewer() + this.tableTool = draw.getTableTool() + this.hyperlinkParticle = draw.getHyperlinkParticle() + this.dateParticle = draw.getDateParticle() + this.imageParticle = draw.getImageParticle() + this.control = draw.getControl() + this.dprMediaQueryList = window.matchMedia( + `(resolution: ${window.devicePixelRatio}dppx)` + ) + } + + public register() { + this.cursor = this.draw.getCursor() + this.addEvent() + } + + private addEvent() { + window.addEventListener('blur', this.clearSideEffect) + document.addEventListener('mousedown', this.clearSideEffect) + document.addEventListener('mouseup', this.setCanvasEventAbility) + document.addEventListener('wheel', this.setPageScale, { passive: false }) + document.addEventListener('visibilitychange', this._handleVisibilityChange) + this.dprMediaQueryList.addEventListener('change', this._handleDprChange) + } + + public removeEvent() { + window.removeEventListener('blur', this.clearSideEffect) + document.removeEventListener('mousedown', this.clearSideEffect) + document.removeEventListener('mouseup', this.setCanvasEventAbility) + document.removeEventListener('wheel', this.setPageScale) + document.removeEventListener( + 'visibilitychange', + this._handleVisibilityChange + ) + this.dprMediaQueryList.removeEventListener('change', this._handleDprChange) + } + + public clearSideEffect = (evt: Event) => { + if (!this.cursor) return + // 编辑器内部dom + const target = (evt?.composedPath()[0] || evt.target) + const pageList = this.draw.getPageList() + const innerEditorDom = findParent( + target, + (node: any) => pageList.includes(node), + true + ) + if (innerEditorDom) return + // 编辑器外部组件dom + const outerEditorDom = findParent( + target, + (node: Node & Element) => + !!node && node.nodeType === 1 && !!node.getAttribute(EDITOR_COMPONENT), + true + ) + if (outerEditorDom) { + this.watchCursorActive() + return + } + this.cursor.recoveryCursor() + this.range.recoveryRangeStyle() + this.previewer.clearResizer() + this.tableTool.dispose() + this.hyperlinkParticle.clearHyperlinkPopup() + this.control.destroyControl() + this.dateParticle.clearDatePicker() + this.imageParticle.destroyFloatImage() + } + + public setCanvasEventAbility = () => { + this.canvasEvent.setIsAllowDrag(false) + this.canvasEvent.setIsAllowSelection(false) + } + + public watchCursorActive() { + // 选区闭合&实际光标移出光标代理 + if (!this.range.getIsCollapsed()) return + setTimeout(() => { + // 将模拟光标变成失活显示状态 + if (!this.cursor?.getAgentIsActive()) { + this.cursor?.drawCursor({ + isFocus: false, + isBlink: false + }) + } + }) + } + + public setPageScale = (evt: WheelEvent) => { + // 设置禁用快捷键 + if ( + this.options.shortcutDisableKeys.includes( + INTERNAL_SHORTCUT_KEY.PAGE_SCALE + ) + ) { + return + } + // 仅在按下Ctrl键时生效 + if (!evt.ctrlKey) return + evt.preventDefault() + const { scale } = this.options + if (evt.deltaY < 0) { + // 放大 + const nextScale = scale * 10 + 1 + if (nextScale <= 30) { + this.draw.setPageScale(nextScale / 10) + } + } else { + // 缩小 + const nextScale = scale * 10 - 1 + if (nextScale >= 5) { + this.draw.setPageScale(nextScale / 10) + } + } + } + + private _handleVisibilityChange = () => { + if (document.visibilityState === 'visible') { + // 页面可见时重新渲染激活页面 + const range = this.range.getRange() + const isSetCursor = + !!~range.startIndex && + !!~range.endIndex && + range.startIndex === range.endIndex + this.range.replaceRange(range) + this.draw.render({ + isSetCursor, + isCompute: false, + isSubmitHistory: false, + curIndex: range.startIndex + }) + } + } + + private _handleDprChange = () => { + this.draw.setPageDevicePixel() + } +} diff --git a/src/editor/core/event/eventbus/EventBus.ts b/src/editor/core/event/eventbus/EventBus.ts new file mode 100644 index 0000000..8209462 --- /dev/null +++ b/src/editor/core/event/eventbus/EventBus.ts @@ -0,0 +1,46 @@ +export class EventBus { + private eventHub: Map> + + constructor() { + this.eventHub = new Map() + } + + public on( + eventName: K, + callback: EventMap[K] + ) { + if (!eventName || typeof callback !== 'function') return + const eventSet = this.eventHub.get(eventName) || new Set() + eventSet.add(callback) + this.eventHub.set(eventName, eventSet) + } + + public emit( + eventName: K, + payload?: EventMap[K] extends (payload: infer P) => void ? P : never + ) { + if (!eventName) return + const callBackSet = this.eventHub.get(eventName) + if (!callBackSet) return + if (callBackSet.size === 1) { + const callBack = [...callBackSet] + return callBack[0](payload) + } + callBackSet.forEach(callBack => callBack(payload)) + } + + public off( + eventName: K, + callback: EventMap[K] + ) { + if (!eventName || typeof callback !== 'function') return + const callBackSet = this.eventHub.get(eventName) + if (!callBackSet) return + callBackSet.delete(callback) + } + + public isSubscribe(eventName: K): boolean { + const eventSet = this.eventHub.get(eventName) + return !!eventSet && eventSet.size > 0 + } +} diff --git a/src/editor/core/event/handlers/click.ts b/src/editor/core/event/handlers/click.ts new file mode 100644 index 0000000..e768243 --- /dev/null +++ b/src/editor/core/event/handlers/click.ts @@ -0,0 +1,219 @@ +import { ZERO } from '../../../dataset/constant/Common' +import { TEXTLIKE_ELEMENT_TYPE } from '../../../dataset/constant/Element' +import { NUMBER_LIKE_REG } from '../../../dataset/constant/Regular' +import { ElementType } from '../../../dataset/enum/Element' +import { IRange } from '../../../interface/Range' +import { CanvasEvent } from '../CanvasEvent' + +// 通过分词器获取单词所在选区 +function getWordRangeBySegmenter(host: CanvasEvent): IRange | null { + if (!Intl.Segmenter) return null + const draw = host.getDraw() + const cursorPosition = draw.getPosition().getCursorPosition() + if (!cursorPosition) return null + const rangeManager = draw.getRange() + const paragraphInfo = rangeManager.getRangeParagraphInfo() + if (!paragraphInfo) return null + // 组装段落文本 + const paragraphText = + paragraphInfo?.elementList + ?.map(e => + !e.type || + (e.type !== ElementType.CONTROL && + TEXTLIKE_ELEMENT_TYPE.includes(e.type)) + ? e.value + : ZERO + ) + .join('') || '' + if (!paragraphText) return null + // 光标所在位置:光标在开头时只能选择选择当前行进行分词,光标后移 + const cursorStartIndex = + cursorPosition.isFirstLetter || draw.getCursor().getHitLineStartIndex() + ? cursorPosition.index + 1 + : cursorPosition.index + // 段落首字符相对文档起始位置 + const offset = paragraphInfo.startIndex + const segmenter = new Intl.Segmenter(undefined, { granularity: 'word' }) + const segments = segmenter.segment(paragraphText) + // 新的光标位置 + let startIndex = -1 + let endIndex = -1 + for (const { segment, index, isWordLike } of segments) { + const realSegmentStartIndex = index + offset + if ( + isWordLike && + cursorStartIndex >= realSegmentStartIndex && + cursorStartIndex < realSegmentStartIndex + segment.length + ) { + startIndex = realSegmentStartIndex - 1 + endIndex = startIndex + segment.length + break + } + } + return ~startIndex && ~endIndex ? { startIndex, endIndex } : null +} + +// 通过光标位置获取单词所在选区 +function getWordRangeByCursor(host: CanvasEvent): IRange | null { + const draw = host.getDraw() + const cursorPosition = draw.getPosition().getCursorPosition() + if (!cursorPosition) return null + const { value, index } = cursorPosition + // 判断是否是数字或英文 + const LETTER_REG = draw.getLetterReg() + let upCount = 0 + let downCount = 0 + const isNumber = NUMBER_LIKE_REG.test(value) + if (isNumber || LETTER_REG.test(value)) { + const elementList = draw.getElementList() + // 向上查询 + let upStartIndex = index - 1 + while (upStartIndex > 0) { + const value = elementList[upStartIndex].value + if ( + (isNumber && NUMBER_LIKE_REG.test(value)) || + (!isNumber && LETTER_REG.test(value)) + ) { + upCount++ + upStartIndex-- + } else { + break + } + } + // 向下查询 + let downStartIndex = index + 1 + while (downStartIndex < elementList.length) { + const value = elementList[downStartIndex].value + if ( + (isNumber && NUMBER_LIKE_REG.test(value)) || + (!isNumber && LETTER_REG.test(value)) + ) { + downCount++ + downStartIndex++ + } else { + break + } + } + } + // 新的光标位置 + const startIndex = index - upCount - 1 + if (startIndex < 0) return null + return { + startIndex, + endIndex: index + downCount + } +} + +function dblclick(host: CanvasEvent, evt: MouseEvent) { + const draw = host.getDraw() + const position = draw.getPosition() + const positionContext = position.getPositionByXY({ + x: evt.offsetX, + y: evt.offsetY + }) + // 图片预览 + if (positionContext.isImage && positionContext.isDirectHit) { + draw.getPreviewer().render() + return + } + // 切换区域 + if (draw.getIsPagingMode()) { + if (!~positionContext.index && positionContext.zone) { + draw.getZone().setZone(positionContext.zone) + draw.clearSideEffect() + position.setPositionContext({ + isTable: false + }) + return + } + } + // 复选/单选框双击时是切换选择状态,禁用扩选 + if ( + (positionContext.isCheckbox || positionContext.isRadio) && + positionContext.isDirectHit + ) { + return + } + // 自动扩选文字-分词处理,优先使用分词器否则降级使用光标所在位置 + const rangeManager = draw.getRange() + const segmenterRange = + getWordRangeBySegmenter(host) || getWordRangeByCursor(host) + if (!segmenterRange) return + rangeManager.setRange(segmenterRange.startIndex, segmenterRange.endIndex) + // 刷新文档 + draw.render({ + isSubmitHistory: false, + isSetCursor: false, + isCompute: false + }) + // 更新选区 + rangeManager.setRangeStyle() +} + +function threeClick(host: CanvasEvent) { + const draw = host.getDraw() + const position = draw.getPosition() + const cursorPosition = position.getCursorPosition() + if (!cursorPosition) return + const { index } = cursorPosition + const elementList = draw.getElementList() + // 判断是否是零宽字符 + let upCount = 0 + let downCount = 0 + // 向上查询 + let upStartIndex = index - 1 + while (upStartIndex > 0) { + const element = elementList[upStartIndex] + const preElement = elementList[upStartIndex - 1] + if ( + (element.value === ZERO && !element.listWrap) || + element.listId !== preElement?.listId || + element.titleId !== preElement?.titleId + ) { + break + } + upCount++ + upStartIndex-- + } + // 向下查询 + let downStartIndex = index + 1 + while (downStartIndex < elementList.length) { + const element = elementList[downStartIndex] + const nextElement = elementList[downStartIndex + 1] + if ( + (element.value === ZERO && !element.listWrap) || + element.listId !== nextElement?.listId || + element.titleId !== nextElement?.titleId + ) { + break + } + downCount++ + downStartIndex++ + } + // 设置选中区域-不选择段落首尾换行符 + const rangeManager = draw.getRange() + let newStartIndex = index - upCount - 1 + if (elementList[newStartIndex]?.value !== ZERO) { + newStartIndex -= 1 + } + if (newStartIndex < 0) return + let newEndIndex = index + downCount + 1 + if ( + elementList[newEndIndex]?.value === ZERO || + newEndIndex > elementList.length - 1 + ) { + newEndIndex -= 1 + } + rangeManager.setRange(newStartIndex, newEndIndex) + // 刷新文档 + draw.render({ + isSubmitHistory: false, + isSetCursor: false, + isCompute: false + }) +} + +export default { + dblclick, + threeClick +} diff --git a/src/editor/core/event/handlers/composition.ts b/src/editor/core/event/handlers/composition.ts new file mode 100644 index 0000000..9fb4b6c --- /dev/null +++ b/src/editor/core/event/handlers/composition.ts @@ -0,0 +1,45 @@ +import { isFirefox } from '../../../utils/ua' +import { CanvasEvent } from '../CanvasEvent' +import { input, removeComposingInput } from './input' + +function compositionstart(host: CanvasEvent) { + host.isComposing = true +} + +function compositionend(host: CanvasEvent, evt: CompositionEvent) { + host.isComposing = false + // 处理输入框关闭 + const draw = host.getDraw() + // 不存在值:删除合成输入 + if (!evt.data) { + removeComposingInput(host) + const rangeManager = draw.getRange() + const { endIndex: curIndex } = rangeManager.getRange() + draw.render({ + curIndex, + isSubmitHistory: false + }) + } else { + // 存在值:无法触发input事件需手动检测并触发渲染 + if (isFirefox) { + // 如果为0,火狐浏览器会在input事件之前执行导致重复输入 + setTimeout(() => { + if (host.compositionInfo) { + input(evt.data, host) + } + }, 1) + } else { + if (host.compositionInfo) { + input(evt.data, host) + } + } + } + // 移除代理输入框数据 + const cursor = draw.getCursor() + cursor.clearAgentDomValue() +} + +export default { + compositionstart, + compositionend +} diff --git a/src/editor/core/event/handlers/copy.ts b/src/editor/core/event/handlers/copy.ts new file mode 100644 index 0000000..da70bdb --- /dev/null +++ b/src/editor/core/event/handlers/copy.ts @@ -0,0 +1,72 @@ +import { ElementType } from '../../../dataset/enum/Element' +import { IElement } from '../../../interface/Element' +import { ICopyOption } from '../../../interface/Event' +import { ITr } from '../../../interface/table/Tr' +import { writeElementList } from '../../../utils/clipboard' +import { getTextFromElementList, zipElementList } from '../../../utils/element' +import { IOverrideResult } from '../../override/Override' +import { CanvasEvent } from '../CanvasEvent' + +export function copy(host: CanvasEvent, options?: ICopyOption) { + const draw = host.getDraw() + // 自定义粘贴事件 + const { copy } = draw.getOverride() + if (copy) { + const overrideResult = copy() + // 默认阻止默认事件 + if ((overrideResult)?.preventDefault !== false) return + } + const rangeManager = draw.getRange() + // 光标闭合时复制整行 + let copyElementList: IElement[] | null = null + const range = rangeManager.getRange() + if (range.isCrossRowCol) { + // 原始表格信息 + const tableElement = rangeManager.getRangeTableElement() + if (!tableElement) return + // 选区行列信息 + const rowCol = draw.getTableParticle().getRangeRowCol() + if (!rowCol) return + // 构造表格 + const copyTableElement: IElement = { + type: ElementType.TABLE, + value: '', + colgroup: [], + trList: [] + } + const firstRow = rowCol[0] + const colStartIndex = firstRow[0].colIndex! + const lastCol = firstRow[firstRow.length - 1] + const colEndIndex = lastCol.colIndex! + lastCol.colspan - 1 + for (let c = colStartIndex; c <= colEndIndex; c++) { + copyTableElement.colgroup!.push(tableElement.colgroup![c]) + } + for (let r = 0; r < rowCol.length; r++) { + const row = rowCol[r] + const tr = tableElement.trList![row[0].rowIndex!] + const coptTr: ITr = { + tdList: [], + height: tr.height, + minHeight: tr.minHeight + } + for (let c = 0; c < row.length; c++) { + coptTr.tdList.push(row[c]) + } + copyTableElement.trList!.push(coptTr) + } + copyElementList = zipElementList([copyTableElement]) + } else { + copyElementList = rangeManager.getIsCollapsed() + ? rangeManager.getRangeRowElementList() + : rangeManager.getSelectionElementList() + } + if (options?.isPlainText && copyElementList?.length) { + copyElementList = [ + { + value: getTextFromElementList(copyElementList) + } + ] + } + if (!copyElementList?.length) return + writeElementList(copyElementList, draw.getOptions()) +} diff --git a/src/editor/core/event/handlers/cut.ts b/src/editor/core/event/handlers/cut.ts new file mode 100644 index 0000000..92a8919 --- /dev/null +++ b/src/editor/core/event/handlers/cut.ts @@ -0,0 +1,47 @@ +import { writeElementList } from '../../../utils/clipboard' +import { CanvasEvent } from '../CanvasEvent' + +export function cut(host: CanvasEvent) { + const draw = host.getDraw() + const rangeManager = draw.getRange() + const { startIndex, endIndex } = rangeManager.getRange() + if (!~startIndex && !~startIndex) return + if (draw.isReadonly() || !rangeManager.getIsCanInput()) return + + const elementList = draw.getElementList() + let start = startIndex + let end = endIndex + // 无选区则剪切一行 + if (startIndex === endIndex) { + const position = draw.getPosition() + const positionList = position.getPositionList() + const startPosition = positionList[startIndex] + const curRowNo = startPosition.rowNo + const curPageNo = startPosition.pageNo + const cutElementIndexList: number[] = [] + for (let p = 0; p < positionList.length; p++) { + const position = positionList[p] + if (position.pageNo > curPageNo) break + if (position.pageNo === curPageNo && position.rowNo === curRowNo) { + cutElementIndexList.push(p) + } + } + const firstElementIndex = cutElementIndexList[0] - 1 + start = firstElementIndex < 0 ? 0 : firstElementIndex + end = cutElementIndexList[cutElementIndexList.length - 1] + } + const options = draw.getOptions() + // 写入粘贴板 + writeElementList(elementList.slice(start + 1, end + 1), options) + const control = draw.getControl() + let curIndex: number + if (control.getActiveControl() && control.getIsRangeWithinControl()) { + curIndex = control.cut() + control.emitControlContentChange() + } else { + draw.spliceElementList(elementList, start + 1, end - start) + curIndex = start + } + rangeManager.setRange(curIndex, curIndex) + draw.render({ curIndex }) +} diff --git a/src/editor/core/event/handlers/drag.ts b/src/editor/core/event/handlers/drag.ts new file mode 100644 index 0000000..4777536 --- /dev/null +++ b/src/editor/core/event/handlers/drag.ts @@ -0,0 +1,66 @@ +import { ImageDisplay } from '../../../dataset/enum/Common' +import { ElementType } from '../../../dataset/enum/Element' +import { findParent } from '../../../utils' +import { CanvasEvent } from '../CanvasEvent' + +function dragover(evt: DragEvent | MouseEvent, host: CanvasEvent) { + const draw = host.getDraw() + const isReadonly = draw.isReadonly() + if (isReadonly) return + evt.preventDefault() + // 非编辑器区禁止拖放 + const pageContainer = draw.getPageContainer() + const editorRegion = findParent( + evt.target as Element, + (node: Element) => node === pageContainer, + true + ) + if (!editorRegion) return + const target = evt.target as HTMLDivElement + const pageIndex = target.dataset.index + // 设置pageNo + if (pageIndex) { + draw.setPageNo(Number(pageIndex)) + } + const position = draw.getPosition() + const positionContext = position.adjustPositionContext({ + x: evt.offsetX, + y: evt.offsetY + }) + if (!positionContext) return + const { isTable, tdValueIndex, index } = positionContext + // 设置选区及光标位置 + const positionList = position.getPositionList() + const curIndex = isTable ? tdValueIndex! : index + if (~index) { + const rangeManager = draw.getRange() + rangeManager.setRange(curIndex, curIndex) + position.setCursorPosition(positionList[curIndex]) + } + const cursor = draw.getCursor() + const { + cursor: { dragColor, dragWidth, dragFloatImageDisabled } + } = draw.getOptions() + // 拖拽图片是否定位光标 + if (dragFloatImageDisabled) { + const dragElement = host.cacheElementList?.[host.cacheRange!.startIndex] + if ( + dragElement?.type === ElementType.IMAGE && + (dragElement.imgDisplay === ImageDisplay.FLOAT_TOP || + dragElement.imgDisplay === ImageDisplay.FLOAT_BOTTOM || + dragElement.imgDisplay === ImageDisplay.SURROUND) + ) { + return + } + } + cursor.drawCursor({ + width: dragWidth, + color: dragColor, + isBlink: false, + isFocus: false + }) +} + +export default { + dragover +} diff --git a/src/editor/core/event/handlers/drop.ts b/src/editor/core/event/handlers/drop.ts new file mode 100644 index 0000000..bda83f5 --- /dev/null +++ b/src/editor/core/event/handlers/drop.ts @@ -0,0 +1,28 @@ +import { IOverrideResult } from '../../override/Override' +import { CanvasEvent } from '../CanvasEvent' +import { pasteImage } from './paste' + +export function drop(evt: DragEvent, host: CanvasEvent) { + const draw = host.getDraw() + // 自定义拖放事件 + const { drop } = draw.getOverride() + if (drop) { + const overrideResult = drop(evt) + // 默认阻止默认事件 + if ((overrideResult)?.preventDefault !== false) return + } + evt.preventDefault() + const data = evt.dataTransfer?.getData('text') + if (data) { + host.input(data) + } else { + const files = evt.dataTransfer?.files + if (!files) return + for (let i = 0; i < files.length; i++) { + const file = files[i] + if (file.type.startsWith('image')) { + pasteImage(host, file) + } + } + } +} diff --git a/src/editor/core/event/handlers/input.ts b/src/editor/core/event/handlers/input.ts new file mode 100644 index 0000000..a6b430f --- /dev/null +++ b/src/editor/core/event/handlers/input.ts @@ -0,0 +1,129 @@ +import { ZERO } from '../../../dataset/constant/Common' +import { + EDITOR_ELEMENT_COPY_ATTR, + EDITOR_ELEMENT_STYLE_ATTR +} from '../../../dataset/constant/Element' +import { ElementType } from '../../../dataset/enum/Element' +import { IElement } from '../../../interface/Element' +import { IRangeElementStyle } from '../../../interface/Range' +import { splitText } from '../../../utils' +import { formatElementContext } from '../../../utils/element' +import { CanvasEvent } from '../CanvasEvent' + +export function input(data: string, host: CanvasEvent) { + const draw = host.getDraw() + if (draw.isReadonly() || draw.isDisabled()) return + const position = draw.getPosition() + const cursorPosition = position.getCursorPosition() + if (!data || !cursorPosition) return + const isComposing = host.isComposing + // 正在合成文本进行非输入操作 + if (isComposing && host.compositionInfo?.value === data) return + const rangeManager = draw.getRange() + if (!rangeManager.getIsCanInput()) return + // 移除合成前,缓存设置的默认样式设置 + const defaultStyle = + rangeManager.getDefaultStyle() || host.compositionInfo?.defaultStyle || null + // 移除合成输入 + removeComposingInput(host) + if (!isComposing) { + const cursor = draw.getCursor() + cursor.clearAgentDomValue() + } + const { TEXT, HYPERLINK, SUBSCRIPT, SUPERSCRIPT, DATE, TAB } = ElementType + const text = data.replaceAll(`\n`, ZERO) + const { startIndex, endIndex } = rangeManager.getRange() + // 格式化元素 + const elementList = draw.getElementList() + const copyElement = rangeManager.getRangeAnchorStyle(elementList, endIndex) + if (!copyElement) return + const isDesignMode = draw.isDesignMode() + const inputData: IElement[] = splitText(text).map(value => { + const newElement: IElement = { + value + } + if ( + isDesignMode || + (!copyElement.title?.disabled && !copyElement.control?.disabled) + ) { + const nextElement = elementList[endIndex + 1] + // 文本、超链接、日期、上下标:复制所有信息(元素类型、样式、特殊属性) + if ( + !copyElement.type || + copyElement.type === TEXT || + (copyElement.type === HYPERLINK && nextElement?.type === HYPERLINK) || + (copyElement.type === DATE && nextElement?.type === DATE) || + (copyElement.type === SUBSCRIPT && nextElement?.type === SUBSCRIPT) || + (copyElement.type === SUPERSCRIPT && nextElement?.type === SUPERSCRIPT) + ) { + EDITOR_ELEMENT_COPY_ATTR.forEach(attr => { + // 在分组外无需复制分组信息 + if (attr === 'groupIds' && !nextElement?.groupIds) return + const value = copyElement[attr] as never + if (value !== undefined) { + newElement[attr] = value + } + }) + } + // 仅复制样式:存在默认样式设置 || 无法匹配文本类元素时(TAB) + if (defaultStyle || copyElement.type === TAB) { + EDITOR_ELEMENT_STYLE_ATTR.forEach(attr => { + const value = + defaultStyle?.[attr as keyof IRangeElementStyle] || + copyElement[attr] + if (value !== undefined) { + newElement[attr] = value as never + } + }) + } + if (isComposing) { + newElement.underline = true + } + } + return newElement + }) + // 控件-移除placeholder + const control = draw.getControl() + let curIndex: number + if (control.getActiveControl() && control.getIsRangeWithinControl()) { + curIndex = control.setValue(inputData) + if (!isComposing) { + control.emitControlContentChange() + } + } else { + const start = startIndex + 1 + if (startIndex !== endIndex) { + draw.spliceElementList(elementList, start, endIndex - startIndex) + } + formatElementContext(elementList, inputData, startIndex, { + editorOptions: draw.getOptions() + }) + draw.spliceElementList(elementList, start, 0, inputData) + curIndex = startIndex + inputData.length + } + if (~curIndex) { + rangeManager.setRange(curIndex, curIndex) + draw.render({ + curIndex, + isSubmitHistory: !isComposing + }) + } + if (isComposing) { + host.compositionInfo = { + elementList, + value: text, + startIndex: curIndex - inputData.length, + endIndex: curIndex, + defaultStyle + } + } +} + +export function removeComposingInput(host: CanvasEvent) { + if (!host.compositionInfo) return + const { elementList, startIndex, endIndex } = host.compositionInfo + elementList.splice(startIndex + 1, endIndex - startIndex) + const rangeManager = host.getDraw().getRange() + rangeManager.setRange(startIndex, startIndex) + host.compositionInfo = null +} diff --git a/src/editor/core/event/handlers/keydown/backspace.ts b/src/editor/core/event/handlers/keydown/backspace.ts new file mode 100644 index 0000000..6ec09cc --- /dev/null +++ b/src/editor/core/event/handlers/keydown/backspace.ts @@ -0,0 +1,139 @@ +import { ZERO } from '../../../../dataset/constant/Common' +import { CanvasEvent } from '../../CanvasEvent' + +// 删除光标前隐藏元素 +function backspaceHideElement(host: CanvasEvent) { + const draw = host.getDraw() + const rangeManager = draw.getRange() + const range = rangeManager.getRange() + // 光标所在位置为隐藏元素时触发循环删除 + const elementList = draw.getElementList() + const element = elementList[range.startIndex] + if (!element.hide && !element.control?.hide && !element.area?.hide) return + // 向前删除所有隐藏元素 + let index = range.startIndex + while (index > 0) { + const element = elementList[index] + let newIndex: number | null = null + if (element.controlId) { + newIndex = draw.getControl().removeControl(index) + if (newIndex !== null) { + index = newIndex + } + } else { + draw.spliceElementList(elementList, index, 1) + newIndex = index - 1 + index-- + } + const newElement = elementList[newIndex!] + if ( + !newElement || + (!newElement.hide && !newElement.control?.hide && !newElement.area?.hide) + ) { + // 更新上下文信息 + if (newIndex) { + // 更新选区信息 + range.startIndex = newIndex + range.endIndex = newIndex + rangeManager.replaceRange(range) + // 更新位置信息 + const position = draw.getPosition() + const positionList = position.getPositionList() + position.setCursorPosition(positionList[newIndex]) + } + break + } + } +} + +export function backspace(evt: KeyboardEvent, host: CanvasEvent) { + const draw = host.getDraw() + if (draw.isReadonly()) return + // 可输入性验证 + const rangeManager = draw.getRange() + if (!rangeManager.getIsCanInput()) return + // 隐藏元素删除 + if (rangeManager.getIsCollapsed()) { + backspaceHideElement(host) + } + // 删除操作 + const control = draw.getControl() + const { startIndex, endIndex, isCrossRowCol } = rangeManager.getRange() + let curIndex: number | null + if (isCrossRowCol) { + // 表格跨行列选中时清空单元格内容 + const rowCol = draw.getTableParticle().getRangeRowCol() + if (!rowCol) return + let isDeleted = false + for (let r = 0; r < rowCol.length; r++) { + const row = rowCol[r] + for (let c = 0; c < row.length; c++) { + const col = row[c] + if (col.value.length > 1) { + draw.spliceElementList(col.value, 1, col.value.length - 1) + isDeleted = true + } + } + } + // 删除成功后定位 + curIndex = isDeleted ? 0 : null + } else if ( + control.getActiveControl() && + control.getIsRangeCanCaptureEvent() + ) { + // 光标在控件内 + curIndex = control.keydown(evt) + if (curIndex) { + control.emitControlContentChange() + } + } else { + // 普通元素删除 + const cursorPosition = draw.getPosition().getCursorPosition() + if (!cursorPosition) return + const { index } = cursorPosition + const isCollapsed = rangeManager.getIsCollapsed() + const elementList = draw.getElementList() + // 判断是否允许删除 + if (isCollapsed && index === 0) { + const firstElement = elementList[index] + if (firstElement.value === ZERO) { + // 取消首字符列表设置 + if (firstElement.listId) { + draw.getListParticle().unsetList() + } + evt.preventDefault() + return + } + } + // 替换当前行对齐方式 + const startElement = elementList[startIndex] + if (isCollapsed && startElement.rowFlex && startElement.value === ZERO) { + const rowFlexElementList = rangeManager.getRangeRowElementList() + if (rowFlexElementList) { + const preElement = elementList[startIndex - 1] + rowFlexElementList.forEach(element => { + element.rowFlex = preElement?.rowFlex + }) + } + } + if (!isCollapsed) { + draw.spliceElementList(elementList, startIndex + 1, endIndex - startIndex) + } else { + draw.spliceElementList(elementList, index, 1) + } + curIndex = isCollapsed ? index - 1 : startIndex + } + draw.getGlobalEvent().setCanvasEventAbility() + if (curIndex === null) { + rangeManager.setRange(startIndex, startIndex) + draw.render({ + curIndex: startIndex, + isSubmitHistory: false + }) + } else { + rangeManager.setRange(curIndex, curIndex) + draw.render({ + curIndex + }) + } +} diff --git a/src/editor/core/event/handlers/keydown/delete.ts b/src/editor/core/event/handlers/keydown/delete.ts new file mode 100644 index 0000000..1a3fea3 --- /dev/null +++ b/src/editor/core/event/handlers/keydown/delete.ts @@ -0,0 +1,119 @@ +import { CanvasEvent } from '../../CanvasEvent' + +// 删除光后前隐藏元素 +function deleteHideElement(host: CanvasEvent) { + const draw = host.getDraw() + const rangeManager = draw.getRange() + const range = rangeManager.getRange() + // 光标所在位置为隐藏元素时触发循环删除 + const elementList = draw.getElementList() + const nextElement = elementList[range.startIndex + 1] + if ( + !nextElement.hide && + !nextElement.control?.hide && + !nextElement.area?.hide + ) { + return + } + // 向后删除所有隐藏元素 + const index = range.startIndex + 1 + while (index < elementList.length) { + const element = elementList[index] + let newIndex: number | null = null + if (element.controlId) { + newIndex = draw.getControl().removeControl(index) + } else { + draw.spliceElementList(elementList, index, 1) + newIndex = index + } + const newElement = elementList[newIndex!] + if ( + !newElement || + (!newElement.hide && !newElement.control?.hide && !newElement.area?.hide) + ) { + break + } + } +} + +export function del(evt: KeyboardEvent, host: CanvasEvent) { + const draw = host.getDraw() + if (draw.isReadonly()) return + // 可输入性验证 + const rangeManager = draw.getRange() + if (!rangeManager.getIsCanInput()) return + const { startIndex, endIndex, isCrossRowCol } = rangeManager.getRange() + // 隐藏控件删除 + const elementList = draw.getElementList() + const control = draw.getControl() + if (rangeManager.getIsCollapsed()) { + deleteHideElement(host) + } + // 删除操作 + let curIndex: number | null + if (isCrossRowCol) { + // 表格跨行列选中时清空单元格内容 + const rowCol = draw.getTableParticle().getRangeRowCol() + if (!rowCol) return + let isDeleted = false + for (let r = 0; r < rowCol.length; r++) { + const row = rowCol[r] + for (let c = 0; c < row.length; c++) { + const col = row[c] + if (col.value.length > 1) { + draw.spliceElementList(col.value, 1, col.value.length - 1) + isDeleted = true + } + } + } + // 删除成功后定位 + curIndex = isDeleted ? 0 : null + } else if (control.getActiveControl() && control.getIsRangeWithinControl()) { + // 光标在控件内 + curIndex = control.keydown(evt) + if (curIndex) { + control.emitControlContentChange() + } + } else if (elementList[endIndex + 1]?.controlId) { + // 光标在控件前 + curIndex = control.removeControl(endIndex + 1) + } else { + // 普通元素 + const position = draw.getPosition() + const cursorPosition = position.getCursorPosition() + if (!cursorPosition) return + const { index } = cursorPosition + // 命中图片直接删除 + const positionContext = position.getPositionContext() + if (positionContext.isDirectHit && positionContext.isImage) { + draw.spliceElementList(elementList, index, 1) + curIndex = index - 1 + } else { + const isCollapsed = rangeManager.getIsCollapsed() + if (!isCollapsed) { + draw.spliceElementList( + elementList, + startIndex + 1, + endIndex - startIndex + ) + } else { + if (!elementList[index + 1]) return + draw.spliceElementList(elementList, index + 1, 1) + } + curIndex = isCollapsed ? index : startIndex + } + } + draw.getGlobalEvent().setCanvasEventAbility() + if (curIndex === null) { + rangeManager.setRange(startIndex, startIndex) + draw.render({ + curIndex: startIndex, + isSubmitHistory: false + }) + } else { + rangeManager.setRange(curIndex, curIndex) + draw.render({ + curIndex + }) + } +} diff --git a/src/editor/core/event/handlers/keydown/enter.ts b/src/editor/core/event/handlers/keydown/enter.ts new file mode 100644 index 0000000..a879456 --- /dev/null +++ b/src/editor/core/event/handlers/keydown/enter.ts @@ -0,0 +1,105 @@ +import { ZERO } from '../../../../dataset/constant/Common' +import { + AREA_CONTEXT_ATTR, + EDITOR_ELEMENT_STYLE_ATTR, + EDITOR_ROW_ATTR +} from '../../../../dataset/constant/Element' +import { ControlComponent } from '../../../../dataset/enum/Control' +import { IElement } from '../../../../interface/Element' +import { omitObject } from '../../../../utils' +import { formatElementContext } from '../../../../utils/element' +import { CanvasEvent } from '../../CanvasEvent' + +export function enter(evt: KeyboardEvent, host: CanvasEvent) { + const draw = host.getDraw() + if (draw.isReadonly()) return + const rangeManager = draw.getRange() + if (!rangeManager.getIsCanInput()) return + const { startIndex, endIndex } = rangeManager.getRange() + const isCollapsed = rangeManager.getIsCollapsed() + const elementList = draw.getElementList() + const startElement = elementList[startIndex] + const endElement = elementList[endIndex] + // 最后一个列表项行首回车取消列表设置 + if ( + isCollapsed && + endElement.listId && + endElement.value === ZERO && + elementList[endIndex + 1]?.listId !== endElement.listId + ) { + draw.getListParticle().unsetList() + return + } + // 列表块内换行 + let enterText: IElement = { + value: ZERO + } + if (evt.shiftKey && startElement.listId) { + enterText.listWrap = true + } + // 格式化上下文 + formatElementContext(elementList, [enterText], startIndex, { + isBreakWhenWrap: true, + editorOptions: draw.getOptions() + }) + // shift长按 && 最后位置回车无需复制区域上下文 + if ( + evt.shiftKey && + endElement.areaId && + endElement.areaId !== elementList[endIndex + 1]?.areaId + ) { + enterText = omitObject(enterText, AREA_CONTEXT_ATTR) + } + // 标题结尾处回车无需格式化及样式复制 + if ( + !( + endElement.titleId && + endElement.titleId !== elementList[endIndex + 1]?.titleId + ) + ) { + // 复制样式属性 + const copyElement = rangeManager.getRangeAnchorStyle(elementList, endIndex) + if (copyElement) { + const copyAttr = [...EDITOR_ROW_ATTR] + // 不复制控件后缀样式 + if (copyElement.controlComponent !== ControlComponent.POSTFIX) { + copyAttr.push(...EDITOR_ELEMENT_STYLE_ATTR) + } + copyAttr.forEach(attr => { + const value = copyElement[attr] as never + if (value !== undefined) { + enterText[attr] = value + } + }) + } + } + // 控件或文档插入换行元素 + const control = draw.getControl() + const activeControl = control.getActiveControl() + let curIndex: number + if (activeControl && control.getIsRangeWithinControl()) { + curIndex = control.setValue([enterText]) + control.emitControlContentChange() + } else { + const position = draw.getPosition() + const cursorPosition = position.getCursorPosition() + if (!cursorPosition) return + const { index } = cursorPosition + if (isCollapsed) { + draw.spliceElementList(elementList, index + 1, 0, [enterText]) + } else { + draw.spliceElementList( + elementList, + startIndex + 1, + endIndex - startIndex, + [enterText] + ) + } + curIndex = index + 1 + } + if (~curIndex) { + rangeManager.setRange(curIndex, curIndex) + draw.render({ curIndex }) + } + evt.preventDefault() +} diff --git a/src/editor/core/event/handlers/keydown/index.ts b/src/editor/core/event/handlers/keydown/index.ts new file mode 100644 index 0000000..e108c08 --- /dev/null +++ b/src/editor/core/event/handlers/keydown/index.ts @@ -0,0 +1,69 @@ +import { EditorMode, EditorZone } from '../../../../dataset/enum/Editor' +import { KeyMap } from '../../../../dataset/enum/KeyMap' +import { isMod } from '../../../../utils/hotkey' +import { CanvasEvent } from '../../CanvasEvent' +import { backspace } from './backspace' +import { del } from './delete' +import { enter } from './enter' +import { left } from './left' +import { right } from './right' +import { tab } from './tab' +import { updown } from './updown' + +export function keydown(evt: KeyboardEvent, host: CanvasEvent) { + if (host.isComposing) return + const draw = host.getDraw() + // 键盘事件逻辑分发 + if (evt.key === KeyMap.Backspace) { + backspace(evt, host) + } else if (evt.key === KeyMap.Delete) { + del(evt, host) + } else if (evt.key === KeyMap.Enter) { + enter(evt, host) + } else if (evt.key === KeyMap.Left) { + left(evt, host) + } else if (evt.key === KeyMap.Right) { + right(evt, host) + } else if (evt.key === KeyMap.Up || evt.key === KeyMap.Down) { + updown(evt, host) + } else if (isMod(evt) && evt.key.toLocaleLowerCase() === KeyMap.Z) { + if (draw.isReadonly() && draw.getMode() !== EditorMode.FORM) return + draw.getHistoryManager().undo() + evt.preventDefault() + } else if (isMod(evt) && evt.key.toLocaleLowerCase() === KeyMap.Y) { + if (draw.isReadonly() && draw.getMode() !== EditorMode.FORM) return + draw.getHistoryManager().redo() + evt.preventDefault() + } else if (isMod(evt) && evt.key.toLocaleLowerCase() === KeyMap.C) { + host.copy() + evt.preventDefault() + } else if (isMod(evt) && evt.key.toLocaleLowerCase() === KeyMap.X) { + host.cut() + evt.preventDefault() + } else if (isMod(evt) && evt.key.toLocaleLowerCase() === KeyMap.A) { + host.selectAll() + evt.preventDefault() + } else if (isMod(evt) && evt.key.toLocaleLowerCase() === KeyMap.S) { + if (draw.isReadonly()) return + const listener = draw.getListener() + if (listener.saved) { + listener.saved(draw.getValue()) + } + const eventBus = draw.getEventBus() + if (eventBus.isSubscribe('saved')) { + eventBus.emit('saved', draw.getValue()) + } + evt.preventDefault() + } else if (evt.key === KeyMap.ESC) { + // 退出格式刷 + host.clearPainterStyle() + // 退出页眉页脚编辑 + const zoneManager = draw.getZone() + if (!zoneManager.isMainActive()) { + zoneManager.setZone(EditorZone.MAIN) + } + evt.preventDefault() + } else if (evt.key === KeyMap.TAB) { + tab(evt, host) + } +} diff --git a/src/editor/core/event/handlers/keydown/left.ts b/src/editor/core/event/handlers/keydown/left.ts new file mode 100644 index 0000000..66bd67e --- /dev/null +++ b/src/editor/core/event/handlers/keydown/left.ts @@ -0,0 +1,162 @@ +import { EditorMode } from '../../../..' +import { ControlComponent } from '../../../../dataset/enum/Control' +import { ElementType } from '../../../../dataset/enum/Element' +import { MoveDirection } from '../../../../dataset/enum/Observer' +import { getNonHideElementIndex } from '../../../../utils/element' +import { isMod } from '../../../../utils/hotkey' +import { CanvasEvent } from '../../CanvasEvent' + +export function left(evt: KeyboardEvent, host: CanvasEvent) { + const draw = host.getDraw() + const isReadonly = draw.isReadonly() + if (isReadonly) return + const position = draw.getPosition() + const cursorPosition = position.getCursorPosition() + if (!cursorPosition) return + const positionContext = position.getPositionContext() + const { index } = cursorPosition + if (index <= 0 && !positionContext.isTable) return + const rangeManager = draw.getRange() + const { startIndex, endIndex } = rangeManager.getRange() + const isCollapsed = rangeManager.getIsCollapsed() + const elementList = draw.getElementList() + // 表单模式下控件移动 + const control = draw.getControl() + if ( + draw.getMode() === EditorMode.FORM && + control.getActiveControl() && + (elementList[index]?.controlComponent === ControlComponent.PREFIX || + elementList[index]?.controlComponent === ControlComponent.PRE_TEXT) + ) { + control.initNextControl({ + direction: MoveDirection.UP + }) + return + } + // 单词整体移动 + let moveCount = 1 + if (isMod(evt)) { + const LETTER_REG = draw.getLetterReg() + // 起始位置 + const moveStartIndex = + evt.shiftKey && !isCollapsed && startIndex === cursorPosition?.index + ? endIndex + : startIndex + if (LETTER_REG.test(elementList[moveStartIndex]?.value)) { + let i = moveStartIndex - 1 + while (i > 0) { + const element = elementList[i] + if (!LETTER_REG.test(element.value)) { + break + } + moveCount++ + i-- + } + } + } + const curIndex = startIndex - moveCount + // shift则缩放选区 + let anchorStartIndex = curIndex + let anchorEndIndex = curIndex + if (evt.shiftKey && cursorPosition) { + if (startIndex !== endIndex) { + if (startIndex === cursorPosition.index) { + // 减小选区 + anchorStartIndex = startIndex + anchorEndIndex = endIndex - moveCount + } else { + anchorStartIndex = curIndex + anchorEndIndex = endIndex + } + } else { + anchorEndIndex = endIndex + } + } + // 表格单元格间跳转 + if (!evt.shiftKey) { + const element = elementList[startIndex] + // 之前是表格则进入最后一个单元格最后一个元素 + if (element.type === ElementType.TABLE) { + const trList = element.trList! + const lastTrIndex = trList.length - 1 + const lastTr = trList[lastTrIndex] + const lastTdIndex = lastTr.tdList.length - 1 + const lastTd = lastTr.tdList[lastTdIndex] + position.setPositionContext({ + isTable: true, + index: startIndex, + trIndex: lastTrIndex, + tdIndex: lastTdIndex, + tdId: lastTd.id, + trId: lastTr.id, + tableId: element.id + }) + anchorStartIndex = lastTd.value.length - 1 + anchorEndIndex = anchorStartIndex + draw.getTableTool().render() + } else if (element.tableId) { + // 在表格单元格内&在首位则往前移动单元格 + if (startIndex === 0) { + const originalElementList = draw.getOriginalElementList() + const trList = originalElementList[positionContext.index!].trList! + outer: for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + if (tr.id !== element.trId) continue + const tdList = tr.tdList + for (let d = 0; d < tdList.length; d++) { + const td = tdList[d] + if (td.id !== element.tdId) continue + // 移动到表格前 + if (r === 0 && d === 0) { + position.setPositionContext({ + isTable: false + }) + anchorStartIndex = positionContext.index! - 1 + anchorEndIndex = anchorStartIndex + draw.getTableTool().dispose() + } else { + // 上一个单元格 + let preTrIndex = r + let preTdIndex = d - 1 + if (preTdIndex < 0) { + preTrIndex = r - 1 + preTdIndex = trList[preTrIndex].tdList.length - 1 + } + const preTr = trList[preTrIndex] + const preTd = preTr.tdList[preTdIndex] + position.setPositionContext({ + isTable: true, + index: positionContext.index, + trIndex: preTrIndex, + tdIndex: preTdIndex, + tdId: preTd.id, + trId: preTr.id, + tableId: element.tableId + }) + anchorStartIndex = preTd.value.length - 1 + anchorEndIndex = anchorStartIndex + draw.getTableTool().render() + } + break outer + } + } + } + } + } + // 执行跳转 + if (!~anchorStartIndex || !~anchorEndIndex) return + // 隐藏元素跳过 + const newElementList = draw.getElementList() + anchorStartIndex = getNonHideElementIndex(newElementList, anchorStartIndex) + anchorEndIndex = getNonHideElementIndex(newElementList, anchorEndIndex) + // 设置上下文 + rangeManager.setRange(anchorStartIndex, anchorEndIndex) + const isAnchorCollapsed = anchorStartIndex === anchorEndIndex + draw.render({ + curIndex: isAnchorCollapsed ? anchorStartIndex : undefined, + isSetCursor: isAnchorCollapsed, + isSubmitHistory: false, + isCompute: false + }) + evt.preventDefault() +} diff --git a/src/editor/core/event/handlers/keydown/right.ts b/src/editor/core/event/handlers/keydown/right.ts new file mode 100644 index 0000000..1bf2f5a --- /dev/null +++ b/src/editor/core/event/handlers/keydown/right.ts @@ -0,0 +1,178 @@ +import { LocationPosition } from '../../../../dataset/enum/Common' +import { ControlComponent } from '../../../../dataset/enum/Control' +import { EditorMode } from '../../../../dataset/enum/Editor' +import { ElementType } from '../../../../dataset/enum/Element' +import { MoveDirection } from '../../../../dataset/enum/Observer' +import { getNonHideElementIndex } from '../../../../utils/element' +import { isMod } from '../../../../utils/hotkey' +import { CanvasEvent } from '../../CanvasEvent' + +export function right(evt: KeyboardEvent, host: CanvasEvent) { + const draw = host.getDraw() + const isReadonly = draw.isReadonly() + if (isReadonly) return + const position = draw.getPosition() + const cursorPosition = position.getCursorPosition() + if (!cursorPosition) return + const { index } = cursorPosition + const positionList = position.getPositionList() + const positionContext = position.getPositionContext() + if (index > positionList.length - 1 && !positionContext.isTable) return + const rangeManager = draw.getRange() + const { startIndex, endIndex } = rangeManager.getRange() + const isCollapsed = rangeManager.getIsCollapsed() + let elementList = draw.getElementList() + // 表单模式下控件移动 + const control = draw.getControl() + if ( + draw.getMode() === EditorMode.FORM && + control.getActiveControl() && + (elementList[index + 1]?.controlComponent === ControlComponent.POSTFIX || + elementList[index + 1]?.controlComponent === ControlComponent.POST_TEXT) + ) { + control.initNextControl({ + direction: MoveDirection.DOWN + }) + return + } + // 单词整体移动 + let moveCount = 1 + if (isMod(evt)) { + const LETTER_REG = draw.getLetterReg() + // 起始位置 + const moveStartIndex = + evt.shiftKey && !isCollapsed && startIndex === cursorPosition?.index + ? endIndex + : startIndex + if (LETTER_REG.test(elementList[moveStartIndex + 1]?.value)) { + let i = moveStartIndex + 2 + while (i < elementList.length) { + const element = elementList[i] + if (!LETTER_REG.test(element.value)) { + break + } + moveCount++ + i++ + } + } + } + const curIndex = endIndex + moveCount + // shift则缩放选区 + let anchorStartIndex = curIndex + let anchorEndIndex = curIndex + if (evt.shiftKey && cursorPosition) { + if (startIndex !== endIndex) { + if (startIndex === cursorPosition.index) { + // 增大选区 + anchorStartIndex = startIndex + anchorEndIndex = curIndex + } else { + anchorStartIndex = startIndex + moveCount + anchorEndIndex = endIndex + } + } else { + anchorStartIndex = startIndex + } + } + // 表格单元格间跳转 + if (!evt.shiftKey) { + const element = elementList[endIndex] + const nextElement = elementList[endIndex + 1] + // 后一个元素是表格,则进入单元格第一个起始位置 + if (nextElement?.type === ElementType.TABLE) { + const trList = nextElement.trList! + const nextTr = trList[0] + const nextTd = nextTr.tdList[0] + position.setPositionContext({ + isTable: true, + index: endIndex + 1, + trIndex: 0, + tdIndex: 0, + tdId: nextTd.id, + trId: nextTr.id, + tableId: nextElement.id + }) + anchorStartIndex = 0 + anchorEndIndex = 0 + draw.getTableTool().render() + } else if (element.tableId) { + // 在表格单元格内&单元格元素最后 + if (!nextElement) { + const originalElementList = draw.getOriginalElementList() + const trList = originalElementList[positionContext.index!].trList! + outer: for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + if (tr.id !== element.trId) continue + const tdList = tr.tdList + for (let d = 0; d < tdList.length; d++) { + const td = tdList[d] + if (td.id !== element.tdId) continue + // 移动到表格后 + if (r === trList.length - 1 && d === tdList.length - 1) { + position.setPositionContext({ + isTable: false + }) + anchorStartIndex = positionContext.index! + anchorEndIndex = anchorStartIndex + elementList = draw.getElementList() + draw.getTableTool().dispose() + } else { + // 下一个单元格 + let nextTrIndex = r + let nextTdIndex = d + 1 + if (nextTdIndex > tdList.length - 1) { + nextTrIndex = r + 1 + nextTdIndex = 0 + } + const preTr = trList[nextTrIndex] + const preTd = preTr.tdList[nextTdIndex] + position.setPositionContext({ + isTable: true, + index: positionContext.index, + trIndex: nextTrIndex, + tdIndex: nextTdIndex, + tdId: preTd.id, + trId: preTr.id, + tableId: element.tableId + }) + anchorStartIndex = 0 + anchorEndIndex = anchorStartIndex + draw.getTableTool().render() + } + break outer + } + } + } + } + } + // 执行跳转 + const maxElementListIndex = elementList.length - 1 + if ( + anchorStartIndex > maxElementListIndex || + anchorEndIndex > maxElementListIndex + ) { + return + } + // 隐藏元素跳过 + const newElementList = draw.getElementList() + anchorStartIndex = getNonHideElementIndex( + newElementList, + anchorStartIndex, + LocationPosition.AFTER + ) + anchorEndIndex = getNonHideElementIndex( + newElementList, + anchorEndIndex, + LocationPosition.AFTER + ) + // 设置上下文 + rangeManager.setRange(anchorStartIndex, anchorEndIndex) + const isAnchorCollapsed = anchorStartIndex === anchorEndIndex + draw.render({ + curIndex: isAnchorCollapsed ? anchorStartIndex : undefined, + isSetCursor: isAnchorCollapsed, + isSubmitHistory: false, + isCompute: false + }) + evt.preventDefault() +} diff --git a/src/editor/core/event/handlers/keydown/tab.ts b/src/editor/core/event/handlers/keydown/tab.ts new file mode 100644 index 0000000..02412c4 --- /dev/null +++ b/src/editor/core/event/handlers/keydown/tab.ts @@ -0,0 +1,41 @@ +import { EDITOR_ELEMENT_STYLE_ATTR } from '../../../../dataset/constant/Element' +import { ElementType } from '../../../../dataset/enum/Element' +import { MoveDirection } from '../../../../dataset/enum/Observer' +import { IElement } from '../../../../interface/Element' +import { pickObject } from '../../../../utils' +import { formatElementContext } from '../../../../utils/element' +import { CanvasEvent } from '../../CanvasEvent' + +export function tab(evt: KeyboardEvent, host: CanvasEvent) { + const draw = host.getDraw() + const isReadonly = draw.isReadonly() + if (isReadonly) return + evt.preventDefault() + // 在控件上下文时,tab键控制控件之间移动 + const control = draw.getControl() + const activeControl = control.getActiveControl() + if (activeControl && control.getIsRangeWithinControl()) { + control.initNextControl({ + direction: evt.shiftKey ? MoveDirection.UP : MoveDirection.DOWN + }) + } else { + const rangeManager = draw.getRange() + const elementList = draw.getElementList() + const { startIndex, endIndex } = rangeManager.getRange() + // 插入tab符 + const anchorStyle = rangeManager.getRangeAnchorStyle(elementList, endIndex) + // 仅复制样式 + const copyStyle = anchorStyle + ? pickObject(anchorStyle, EDITOR_ELEMENT_STYLE_ATTR) + : null + const tabElement: IElement = { + ...copyStyle, + type: ElementType.TAB, + value: '' + } + formatElementContext(elementList, [tabElement], startIndex, { + editorOptions: draw.getOptions() + }) + draw.insertElementList([tabElement]) + } +} diff --git a/src/editor/core/event/handlers/keydown/updown.ts b/src/editor/core/event/handlers/keydown/updown.ts new file mode 100644 index 0000000..0354ab3 --- /dev/null +++ b/src/editor/core/event/handlers/keydown/updown.ts @@ -0,0 +1,340 @@ +import { ElementType } from '../../../../dataset/enum/Element' +import { KeyMap } from '../../../../dataset/enum/KeyMap' +import { MoveDirection } from '../../../../dataset/enum/Observer' +import { IElementPosition } from '../../../../interface/Element' +import { CanvasEvent } from '../../CanvasEvent' + +interface IGetNextPositionIndexPayload { + positionList: IElementPosition[] + index: number + rowNo: number + isUp: boolean + cursorX: number +} +// 根据当前位置索引查找上下行最接近的索引位置 +function getNextPositionIndex(payload: IGetNextPositionIndexPayload) { + const { positionList, index, isUp, rowNo, cursorX } = payload + let nextIndex = -1 + // 查找下一行位置列表 + const probablePosition: IElementPosition[] = [] + if (isUp) { + let p = index - 1 + // 等于0的时候上一行是第一行 + while (p >= 0) { + const position = positionList[p] + p-- + if (position.rowNo === rowNo) continue + if (probablePosition[0] && probablePosition[0].rowNo !== position.rowNo) { + break + } + probablePosition.unshift(position) + } + } else { + let p = index + 1 + while (p < positionList.length) { + const position = positionList[p] + p++ + if (position.rowNo === rowNo) continue + if (probablePosition[0] && probablePosition[0].rowNo !== position.rowNo) { + break + } + probablePosition.push(position) + } + } + // 查找下一行位置:第一个存在交叉宽度的元素位置 + for (let p = 0; p < probablePosition.length; p++) { + const nextPosition = probablePosition[p] + const { + coordinate: { + leftTop: [nextLeftX], + rightTop: [nextRightX] + } + } = nextPosition + if (p === probablePosition.length - 1) { + nextIndex = nextPosition.index + } + if (cursorX < nextLeftX || cursorX > nextRightX) continue + nextIndex = nextPosition.index + break + } + return nextIndex +} + +export function updown(evt: KeyboardEvent, host: CanvasEvent) { + const draw = host.getDraw() + const isReadonly = draw.isReadonly() + if (isReadonly) return + const position = draw.getPosition() + const cursorPosition = position.getCursorPosition() + if (!cursorPosition) return + const rangeManager = draw.getRange() + const { startIndex, endIndex } = rangeManager.getRange() + let positionList = position.getPositionList() + const isUp = evt.key === KeyMap.Up + // 新的光标开始结束位置 + let anchorStartIndex = -1 + let anchorEndIndex = -1 + // 单元格之间跳转及跳出表格逻辑 + const positionContext = position.getPositionContext() + if ( + !evt.shiftKey && + positionContext.isTable && + ((isUp && cursorPosition.rowIndex === 0) || + (!isUp && cursorPosition.rowIndex === draw.getRowCount() - 1)) + ) { + const { index, trIndex, tdIndex, tableId } = positionContext + if (isUp) { + // 向上移动-第一行则移出到表格外,否则上一行相同列位置 + if (trIndex === 0) { + position.setPositionContext({ + isTable: false + }) + anchorStartIndex = index! - 1 + anchorEndIndex = anchorStartIndex + draw.getTableTool().dispose() + } else { + // 查找上一行相同列索引位置信息 + let preTrIndex = -1 + let preTdIndex = -1 + const originalElementList = draw.getOriginalElementList() + const trList = originalElementList[index!].trList! + // 当前单元格所在列实际索引 + const curTdColIndex = trList[trIndex!].tdList[tdIndex!].colIndex! + outer: for (let r = trIndex! - 1; r >= 0; r--) { + const tr = trList[r] + const tdList = tr.tdList! + for (let d = 0; d < tdList.length; d++) { + const td = tdList[d] + if ( + td.colIndex === curTdColIndex || + (td.colIndex! + td.colspan - 1 >= curTdColIndex && + td.colIndex! <= curTdColIndex) + ) { + preTrIndex = r + preTdIndex = d + break outer + } + } + } + if (!~preTrIndex || !~preTdIndex) return + const preTr = trList[preTrIndex] + const preTd = preTr.tdList[preTdIndex] + position.setPositionContext({ + isTable: true, + index, + trIndex: preTrIndex, + tdIndex: preTdIndex, + tdId: preTd.id, + trId: preTr.id, + tableId + }) + anchorStartIndex = preTd.value.length - 1 + anchorEndIndex = anchorStartIndex + draw.getTableTool().render() + } + } else { + // 向下移动-最后一行则移出表格外,否则下一行相同列位置 + const originalElementList = draw.getOriginalElementList() + const trList = originalElementList[index!].trList! + if (trIndex === trList.length - 1) { + position.setPositionContext({ + isTable: false + }) + anchorStartIndex = index! + anchorEndIndex = anchorStartIndex + draw.getTableTool().dispose() + } else { + // 查找下一行相同列索引位置信息 + let nexTrIndex = -1 + let nextTdIndex = -1 + // 当前单元格所在列实际索引 + const curTdColIndex = trList[trIndex!].tdList[tdIndex!].colIndex! + outer: for (let r = trIndex! + 1; r < trList.length; r++) { + const tr = trList[r] + const tdList = tr.tdList! + for (let d = 0; d < tdList.length; d++) { + const td = tdList[d] + if ( + td.colIndex === curTdColIndex || + (td.colIndex! + td.colspan - 1 >= curTdColIndex && + td.colIndex! <= curTdColIndex) + ) { + nexTrIndex = r + nextTdIndex = d + break outer + } + } + } + if (!~nexTrIndex || !~nextTdIndex) return + const nextTr = trList[nexTrIndex] + const nextTd = nextTr.tdList[nextTdIndex] + position.setPositionContext({ + isTable: true, + index, + trIndex: nexTrIndex, + tdIndex: nextTdIndex, + tdId: nextTd.id, + trId: nextTr.id, + tableId + }) + anchorStartIndex = nextTd.value.length - 1 + anchorEndIndex = anchorStartIndex + draw.getTableTool().render() + } + } + } else { + // 普通元素及跳进表格逻辑 + let anchorPosition: IElementPosition = cursorPosition + // 扩大选区时,判断移动光标点 + if (evt.shiftKey) { + if (startIndex === cursorPosition.index) { + anchorPosition = positionList[endIndex] + } else { + anchorPosition = positionList[startIndex] + } + } + const { + index, + rowNo, + rowIndex, + coordinate: { + rightTop: [curRightX] + } + } = anchorPosition + // 向上时在首行、向下时在尾行则忽略 + if ( + (isUp && rowIndex === 0) || + (!isUp && rowIndex === draw.getRowCount() - 1) + ) { + return + } + // 查找下一行位置列表 + const nextIndex = getNextPositionIndex({ + positionList, + index, + rowNo, + isUp, + cursorX: curRightX + }) + if (nextIndex < 0) return + // shift则缩放选区 + anchorStartIndex = nextIndex + anchorEndIndex = nextIndex + if (evt.shiftKey) { + if (startIndex !== endIndex) { + if (startIndex === cursorPosition.index) { + anchorStartIndex = startIndex + } else { + anchorEndIndex = endIndex + } + } else { + if (isUp) { + anchorEndIndex = endIndex + } else { + anchorStartIndex = startIndex + } + } + } + // 如果下一行是表格则进入单元格内 + const elementList = draw.getElementList() + const nextElement = elementList[nextIndex] + if (nextElement.type === ElementType.TABLE) { + const { scale } = draw.getOptions() + const margins = draw.getMargins() + const trList = nextElement.trList! + // 查找进入的单元格及元素位置 + let trIndex = -1 + let tdIndex = -1 + let tdPositionIndex = -1 + if (isUp) { + outer: for (let r = trList.length - 1; r >= 0; r--) { + const tr = trList[r] + const tdList = tr.tdList! + for (let d = 0; d < tdList.length; d++) { + const td = tdList[d] + const tdX = td.x! * scale + margins[3] + const tdWidth = td.width! * scale + if (curRightX >= tdX && curRightX <= tdX + tdWidth) { + const tdPositionList = td.positionList! + const lastPosition = tdPositionList[tdPositionList.length - 1] + const nextPositionIndex = + getNextPositionIndex({ + positionList: tdPositionList, + index: lastPosition.index + 1, // 虚拟起始位置+1(从左往右找) + rowNo: lastPosition.rowNo - 1, // 虚拟起始行号-1(从下往上找) + isUp, + cursorX: curRightX + }) || lastPosition.index + trIndex = r + tdIndex = d + tdPositionIndex = nextPositionIndex + break outer + } + } + } + } else { + outer: for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + const tdList = tr.tdList! + for (let d = 0; d < tdList.length; d++) { + const td = tdList[d] + const tdX = td.x! * scale + margins[3] + const tdWidth = td.width! * scale + if (curRightX >= tdX && curRightX <= tdX + tdWidth) { + const tdPositionList = td.positionList! + const nextPositionIndex = + getNextPositionIndex({ + positionList: tdPositionList, + index: -1, // 虚拟起始位置-1(从右往左找) + rowNo: -1, // 虚拟起始行号-1(从上往下找) + isUp, + cursorX: curRightX + }) || 0 + trIndex = r + tdIndex = d + tdPositionIndex = nextPositionIndex + break outer + } + } + } + } + // 设置上下文 + if (~trIndex && ~tdIndex && ~tdPositionIndex) { + const nextTr = trList[trIndex] + const nextTd = nextTr.tdList[tdIndex] + position.setPositionContext({ + isTable: true, + index: nextIndex, + trIndex: trIndex, + tdIndex: tdIndex, + tdId: nextTd.id, + trId: nextTr.id, + tableId: nextElement.id + }) + anchorStartIndex = tdPositionIndex + anchorEndIndex = anchorStartIndex + positionList = position.getPositionList() + draw.getTableTool().render() + } + } + } + // 执行跳转 + if (!~anchorStartIndex || !~anchorEndIndex) return + if (anchorStartIndex > anchorEndIndex) { + // prettier-ignore + [anchorStartIndex, anchorEndIndex] = [anchorEndIndex, anchorStartIndex] + } + rangeManager.setRange(anchorStartIndex, anchorEndIndex) + const isCollapsed = anchorStartIndex === anchorEndIndex + draw.render({ + curIndex: isCollapsed ? anchorStartIndex : undefined, + isSetCursor: isCollapsed, + isSubmitHistory: false, + isCompute: false + }) + // 将光标移动到可视范围内 + draw.getCursor().moveCursorToVisible({ + cursorPosition: positionList[isUp ? anchorStartIndex : anchorEndIndex], + direction: isUp ? MoveDirection.UP : MoveDirection.DOWN + }) +} diff --git a/src/editor/core/event/handlers/mousedown.ts b/src/editor/core/event/handlers/mousedown.ts new file mode 100644 index 0000000..4426366 --- /dev/null +++ b/src/editor/core/event/handlers/mousedown.ts @@ -0,0 +1,251 @@ +import { ImageDisplay } from '../../../dataset/enum/Common' +import { EditorMode } from '../../../dataset/enum/Editor' +import { ElementType } from '../../../dataset/enum/Element' +import { MouseEventButton } from '../../../dataset/enum/Event' +import { ControlComponent } from '../../../dataset/enum/Control' +import { ControlType } from '../../../dataset/enum/Control' +import { IPreviewerDrawOption } from '../../../interface/Previewer' +import { deepClone } from '../../../utils' +import { isMod } from '../../../utils/hotkey' +import { CheckboxControl } from '../../draw/control/checkbox/CheckboxControl' +import { RadioControl } from '../../draw/control/radio/RadioControl' +import { CanvasEvent } from '../CanvasEvent' +import { IElement } from '../../../interface/Element' +import { Draw } from '../../draw/Draw' + +export function setRangeCache(host: CanvasEvent) { + const draw = host.getDraw() + const position = draw.getPosition() + const rangeManager = draw.getRange() + // 缓存选区上下文信息 + host.isAllowDrag = true + host.cacheRange = deepClone(rangeManager.getRange()) + host.cacheElementList = draw.getElementList() + host.cachePositionList = position.getPositionList() + host.cachePositionContext = position.getPositionContext() +} + +export function hitCheckbox(element: IElement, draw: Draw) { + const { checkbox, control } = element + // 复选框不在控件内独立控制 + if (!control) { + draw.getCheckboxParticle().setSelect(element) + } else { + const codes = control?.code ? control.code.split(',') : [] + if (checkbox?.value) { + const codeIndex = codes.findIndex(c => c === checkbox.code) + codes.splice(codeIndex, 1) + } else { + if (checkbox?.code) { + codes.push(checkbox.code) + } + } + const activeControl = draw.getControl().getActiveControl() + if (activeControl instanceof CheckboxControl) { + activeControl.setSelect(codes) + } + } +} + +export function hitRadio(element: IElement, draw: Draw) { + const { radio, control } = element + // 单选框不在控件内独立控制 + if (!control) { + draw.getRadioParticle().setSelect(element) + } else { + const codes = radio?.code ? [radio.code] : [] + const activeControl = draw.getControl().getActiveControl() + if (activeControl instanceof RadioControl) { + activeControl.setSelect(codes) + } + } +} + +export function mousedown(evt: MouseEvent, host: CanvasEvent) { + const draw = host.getDraw() + const isReadonly = draw.isReadonly() + const rangeManager = draw.getRange() + const position = draw.getPosition() + // 存在选区时忽略右键点击 + const range = rangeManager.getRange() + if ( + evt.button === MouseEventButton.RIGHT && + (range.isCrossRowCol || !rangeManager.getIsCollapsed()) + ) { + return + } + // 是否是选区拖拽 + if (!host.isAllowDrag) { + if (!isReadonly && range.startIndex !== range.endIndex) { + const isPointInRange = rangeManager.getIsPointInRange( + evt.offsetX, + evt.offsetY + ) + if (isPointInRange) { + setRangeCache(host) + return + } + } + } + const target = evt.target as HTMLDivElement + const pageIndex = target.dataset.index + // 设置pageNo + if (pageIndex) { + draw.setPageNo(Number(pageIndex)) + } + host.isAllowSelection = true + // 缓存旧上下文信息 + const oldPositionContext = deepClone(position.getPositionContext()) + const positionResult = position.adjustPositionContext({ + x: evt.offsetX, + y: evt.offsetY + }) + if (!positionResult) return + const { + index, + isDirectHit, + isCheckbox, + isRadio, + isImage, + isTable, + tdValueIndex, + hitLineStartIndex + } = positionResult + // 记录选区开始位置 + host.mouseDownStartPosition = { + ...positionResult, + index: isTable ? tdValueIndex! : index, + x: evt.offsetX, + y: evt.offsetY + } + const elementList = draw.getElementList() + const positionList = position.getPositionList() + const curIndex = isTable ? tdValueIndex! : index + const curElement = elementList[curIndex] + // 绘制 + const isDirectHitImage = !!(isDirectHit && isImage) + const isDirectHitCheckbox = !!(isDirectHit && isCheckbox) + const isDirectHitRadio = !!(isDirectHit && isRadio) + if (~index) { + let startIndex = curIndex + let endIndex = curIndex + // shift激活时进行选区处理 + if (evt.shiftKey) { + const { startIndex: oldStartIndex } = rangeManager.getRange() + if (~oldStartIndex) { + const newPositionContext = position.getPositionContext() + if (newPositionContext.tdId === oldPositionContext.tdId) { + if (curIndex > oldStartIndex) { + startIndex = oldStartIndex + } else { + endIndex = oldStartIndex + } + } + } + } + rangeManager.setRange(startIndex, endIndex) + position.setCursorPosition(positionList[curIndex]) + // 复选框 + if (isDirectHitCheckbox && !isReadonly) { + hitCheckbox(curElement, draw) + } else if (isDirectHitRadio && !isReadonly) { + hitRadio(curElement, draw) + } else if ( + curElement.controlComponent === ControlComponent.VALUE && + (curElement.control?.type === ControlType.CHECKBOX || + curElement.control?.type === ControlType.RADIO) + ) { + // 向左查找 + let preIndex = curIndex + while (preIndex > 0) { + const preElement = elementList[preIndex] + if (preElement.controlComponent === ControlComponent.CHECKBOX) { + hitCheckbox(preElement, draw) + break + } else if (preElement.controlComponent === ControlComponent.RADIO) { + hitRadio(preElement, draw) + break + } + preIndex-- + } + } else { + draw.render({ + curIndex, + isCompute: false, + isSubmitHistory: false, + isSetCursor: + !isDirectHitImage && !isDirectHitCheckbox && !isDirectHitRadio + }) + } + // 首字需定位到行首,非上一行最后一个字后 + if (hitLineStartIndex) { + host.getDraw().getCursor().drawCursor({ + hitLineStartIndex + }) + } + } + // 预览工具组件 + const previewer = draw.getPreviewer() + previewer.clearResizer() + if (isDirectHitImage) { + const previewerDrawOption: IPreviewerDrawOption = { + // 只读或控件外表单模式禁用拖拽 + dragDisable: + isReadonly || + (!curElement.controlId && draw.getMode() === EditorMode.FORM) + } + if (curElement.type === ElementType.LATEX) { + previewerDrawOption.mime = 'svg' + previewerDrawOption.srcKey = 'laTexSVG' + } + previewer.drawResizer( + curElement, + positionList[curIndex], + previewerDrawOption + ) + // 光标事件代理丢失,重新定位 + draw.getCursor().drawCursor({ + isShow: false + }) + // 点击图片允许拖拽调整位置 + setRangeCache(host) + // 浮动元素创建镜像图片 + if ( + curElement.imgDisplay === ImageDisplay.SURROUND || + curElement.imgDisplay === ImageDisplay.FLOAT_TOP || + curElement.imgDisplay === ImageDisplay.FLOAT_BOTTOM + ) { + draw.getImageParticle().createFloatImage(curElement) + } + // 图片点击事件 + const eventBus = draw.getEventBus() + if (eventBus.isSubscribe('imageMousedown')) { + eventBus.emit('imageMousedown', { + evt, + element: curElement + }) + } + } + // 表格工具组件 + const tableTool = draw.getTableTool() + tableTool.dispose() + if (isTable && !isReadonly && draw.getMode() !== EditorMode.FORM) { + tableTool.render() + } + // 超链接 + const hyperlinkParticle = draw.getHyperlinkParticle() + hyperlinkParticle.clearHyperlinkPopup() + if (curElement.type === ElementType.HYPERLINK) { + if (isMod(evt)) { + hyperlinkParticle.openHyperlink(curElement) + } else { + hyperlinkParticle.drawHyperlinkPopup(curElement, positionList[curIndex]) + } + } + // 日期控件 + const dateParticle = draw.getDateParticle() + dateParticle.clearDatePicker() + if (curElement.type === ElementType.DATE && !isReadonly) { + dateParticle.renderDatePicker(curElement, positionList[curIndex]) + } +} diff --git a/src/editor/core/event/handlers/mouseleave.ts b/src/editor/core/event/handlers/mouseleave.ts new file mode 100644 index 0000000..c032ba3 --- /dev/null +++ b/src/editor/core/event/handlers/mouseleave.ts @@ -0,0 +1,14 @@ +import { CanvasEvent } from '../CanvasEvent' + +export function mouseleave(evt: MouseEvent, host: CanvasEvent) { + const draw = host.getDraw() + // 鼠标移出页面时选区禁用 + if (!draw.getOptions().pageOuterSelectionDisable) return + // 是否还在canvas内部 + const pageContainer = draw.getPageContainer() + const { x, y, width, height } = pageContainer.getBoundingClientRect() + if (evt.x >= x && evt.x <= x + width && evt.y >= y && evt.y <= y + height) { + return + } + host.setIsAllowSelection(false) +} diff --git a/src/editor/core/event/handlers/mousemove.ts b/src/editor/core/event/handlers/mousemove.ts new file mode 100644 index 0000000..f3e9950 --- /dev/null +++ b/src/editor/core/event/handlers/mousemove.ts @@ -0,0 +1,133 @@ +import { ImageDisplay } from '../../../dataset/enum/Common' +import { ControlComponent } from '../../../dataset/enum/Control' +import { ElementType } from '../../../dataset/enum/Element' +import { CanvasEvent } from '../CanvasEvent' + +export function mousemove(evt: MouseEvent, host: CanvasEvent) { + const draw = host.getDraw() + // 是否是拖拽文字 + if (host.isAllowDrag) { + // 是否允许拖拽到选区 + const x = evt.offsetX + const y = evt.offsetY + const { startIndex, endIndex } = host.cacheRange! + const positionList = host.cachePositionList! + for (let p = startIndex + 1; p <= endIndex; p++) { + const { + coordinate: { leftTop, rightBottom } + } = positionList[p] + if ( + x >= leftTop[0] && + x <= rightBottom[0] && + y >= leftTop[1] && + y <= rightBottom[1] + ) { + return + } + } + const cacheStartIndex = host.cacheRange?.startIndex + if (cacheStartIndex) { + // 浮动元素拖拽调整位置 + const dragElement = host.cacheElementList![cacheStartIndex] + if ( + dragElement?.type === ElementType.IMAGE && + (dragElement.imgDisplay === ImageDisplay.SURROUND || + dragElement.imgDisplay === ImageDisplay.FLOAT_TOP || + dragElement.imgDisplay === ImageDisplay.FLOAT_BOTTOM) + ) { + draw.getPreviewer().clearResizer() + draw.getImageParticle().dragFloatImage(evt.movementX, evt.movementY) + } + } + host.dragover(evt) + host.isAllowDrop = true + return + } + if (!host.isAllowSelection || !host.mouseDownStartPosition) return + const target = evt.target as HTMLDivElement + const pageIndex = target.dataset.index + // 设置pageNo + if (pageIndex) { + draw.setPageNo(Number(pageIndex)) + } + // 结束位置 + const position = draw.getPosition() + const positionResult = position.getPositionByXY({ + x: evt.offsetX, + y: evt.offsetY + }) + if (!~positionResult.index) return + const { + index, + isTable, + tdValueIndex, + tdIndex, + trIndex, + tableId, + trId, + tdId + } = positionResult + const { + index: startIndex, + isTable: startIsTable, + tdIndex: startTdIndex, + trIndex: startTrIndex, + tableId: startTableId + } = host.mouseDownStartPosition + const endIndex = isTable ? tdValueIndex! : index + // 判断是否是表格跨行/列 + const rangeManager = draw.getRange() + if ( + isTable && + startIsTable && + (tdIndex !== startTdIndex || trIndex !== startTrIndex) + ) { + rangeManager.setRange( + endIndex, + endIndex, + tableId, + startTdIndex, + tdIndex, + startTrIndex, + trIndex + ) + position.setPositionContext({ + isTable, + index, + trIndex, + tdIndex, + tdId, + trId, + tableId + }) + } else { + let end = ~endIndex ? endIndex : 0 + // 开始或结束位置存在表格,但是非相同表格则忽略选区设置 + if ((startIsTable || isTable) && startTableId !== tableId) return + // 开始位置 + let start = startIndex + if (start > end) { + // prettier-ignore + [start, end] = [end, start] + } + if (start === end) return + // 背景文本禁止选区 + const elementList = draw.getElementList() + const startElement = elementList[start + 1] + const endElement = elementList[end] + if ( + startElement?.controlComponent === ControlComponent.PLACEHOLDER && + endElement?.controlComponent === ControlComponent.PLACEHOLDER && + startElement.controlId === endElement.controlId + ) { + return + } + rangeManager.setRange(start, end) + } + // 绘制 + draw.render({ + isSubmitHistory: false, + isSetCursor: false, + isCompute: false + }) +} diff --git a/src/editor/core/event/handlers/mouseup.ts b/src/editor/core/event/handlers/mouseup.ts new file mode 100644 index 0000000..5fda346 --- /dev/null +++ b/src/editor/core/event/handlers/mouseup.ts @@ -0,0 +1,341 @@ +import { + CONTROL_CONTEXT_ATTR, + EDITOR_ELEMENT_STYLE_ATTR +} from '../../../dataset/constant/Element' +import { ImageDisplay } from '../../../dataset/enum/Common' +import { ControlComponent, ControlType } from '../../../dataset/enum/Control' +import { ElementType } from '../../../dataset/enum/Element' +import { IElement } from '../../../interface/Element' +import { deepClone, getUUID, omitObject } from '../../../utils' +import { formatElementContext, formatElementList } from '../../../utils/element' +import { CanvasEvent } from '../CanvasEvent' + +type IDragElement = IElement & { dragId: string } + +function createDragId(element: IElement): string { + const dragId = getUUID() + Reflect.set(element, 'dragId', dragId) + return dragId +} + +function getElementIndexByDragId(dragId: string, elementList: IElement[]) { + return (elementList).findIndex(el => el.dragId === dragId) +} + +// 移动悬浮图片位置 +function moveImgPosition( + element: IElement, + evt: MouseEvent, + host: CanvasEvent +) { + const draw = host.getDraw() + if ( + element.imgDisplay === ImageDisplay.SURROUND || + element.imgDisplay === ImageDisplay.FLOAT_TOP || + element.imgDisplay === ImageDisplay.FLOAT_BOTTOM + ) { + const moveX = evt.offsetX - host.mouseDownStartPosition!.x! + const moveY = evt.offsetY - host.mouseDownStartPosition!.y! + const imgFloatPosition = element.imgFloatPosition! + element.imgFloatPosition = { + x: imgFloatPosition.x + moveX, + y: imgFloatPosition.y + moveY, + pageNo: draw.getPageNo() + } + } + draw.getImageParticle().destroyFloatImage() +} + +export function mouseup(evt: MouseEvent, host: CanvasEvent) { + // 判断是否允许拖放 + if (host.isAllowDrop) { + const draw = host.getDraw() + if (draw.isReadonly() || draw.isDisabled()) { + host.mousedown(evt) + return + } + const position = draw.getPosition() + const positionList = position.getPositionList() + const positionContext = position.getPositionContext() + const rangeManager = draw.getRange() + const cacheRange = host.cacheRange! + const cacheElementList = host.cacheElementList! + const cachePositionList = host.cachePositionList! + const cachePositionContext = host.cachePositionContext + const range = rangeManager.getRange() + // 缓存选区的信息 + const isCacheRangeCollapsed = cacheRange.startIndex === cacheRange.endIndex + // 选区闭合时,起始位置向前移动一位进行扩选 + const cacheStartIndex = isCacheRangeCollapsed + ? cacheRange.startIndex - 1 + : cacheRange.startIndex + const cacheEndIndex = cacheRange.endIndex + // 是否需要拖拽-位置发生改变 + if ( + range.startIndex >= cacheStartIndex && + range.endIndex <= cacheEndIndex && + host.cachePositionContext?.tdId === positionContext.tdId + ) { + // 清除渲染副作用 + draw.clearSideEffect() + // 浮动元素拖拽需要提交历史 + let isSubmitHistory = false + let isCompute = false + if (isCacheRangeCollapsed) { + // 图片移动 + const dragElement = cacheElementList[cacheEndIndex] + if ( + dragElement.type === ElementType.IMAGE || + dragElement.type === ElementType.LATEX + ) { + moveImgPosition(dragElement, evt, host) + if ( + dragElement.imgDisplay === ImageDisplay.SURROUND || + dragElement.imgDisplay === ImageDisplay.FLOAT_TOP || + dragElement.imgDisplay === ImageDisplay.FLOAT_BOTTOM + ) { + draw.getPreviewer().drawResizer(dragElement) + isSubmitHistory = true + } else { + const cachePosition = cachePositionList[cacheEndIndex] + draw.getPreviewer().drawResizer(dragElement, cachePosition) + } + // 四周环绕型元素需计算 + isCompute = dragElement.imgDisplay === ImageDisplay.SURROUND + } + } + rangeManager.replaceRange({ + ...cacheRange + }) + draw.render({ + isCompute, + isSubmitHistory, + isSetCursor: false + }) + return + } + // 是否是不可拖拽的控件结构元素 + const dragElementList = cacheElementList.slice( + cacheStartIndex + 1, + cacheEndIndex + 1 + ) + const isContainControl = dragElementList.find(element => element.controlId) + if (isContainControl) { + // 仅允许 (最前/后元素不是控件 || 在控件前后 || 文本控件且是值) 拖拽 + const cacheStartElement = cacheElementList[cacheStartIndex + 1] + const cacheEndElement = cacheElementList[cacheEndIndex] + const isAllowDragControl = + ((!cacheStartElement.controlId || + cacheStartElement.controlComponent === ControlComponent.PREFIX) && + (!cacheEndElement.controlId || + cacheEndElement.controlComponent === ControlComponent.POSTFIX)) || + (cacheStartElement.controlId === cacheEndElement.controlId && + cacheStartElement.controlComponent === ControlComponent.PREFIX && + cacheEndElement.controlComponent === ControlComponent.POSTFIX) || + (cacheStartElement.control?.type === ControlType.TEXT && + cacheStartElement.controlComponent === ControlComponent.VALUE && + cacheEndElement.control?.type === ControlType.TEXT && + cacheEndElement.controlComponent === ControlComponent.VALUE) + if (!isAllowDragControl) { + draw.render({ + curIndex: range.startIndex, + isCompute: false, + isSubmitHistory: false + }) + return + } + } + // 格式化元素 + const control = draw.getControl() + const elementList = draw.getElementList() + // 是否排除控件属性(1.不包含控件 2.新位置在控件内 3.选区不包含完整控件) + const isOmitControlAttr = + !isContainControl || + !!elementList[range.startIndex].controlId || + !control.getIsElementListContainFullControl(dragElementList) + const editorOptions = draw.getOptions() + // 元素属性复制(1.文本提取样式及相关上下文 2.非文本排除相关上下文) + const replaceElementList = dragElementList.map(el => { + if (!el.type || el.type === ElementType.TEXT) { + const newElement: IElement = { + value: el.value + } + const copyAttr = EDITOR_ELEMENT_STYLE_ATTR + if (!isOmitControlAttr) { + copyAttr.push(...CONTROL_CONTEXT_ATTR) + } + copyAttr.forEach(attr => { + const value = el[attr] as never + if (value !== undefined) { + newElement[attr] = value + } + }) + return newElement + } else { + let newElement = deepClone(el) + if (isOmitControlAttr) { + newElement = omitObject(newElement, CONTROL_CONTEXT_ATTR) + } + formatElementList([newElement], { + isHandleFirstElement: false, + editorOptions + }) + return newElement + } + }) + formatElementContext(elementList, replaceElementList, range.startIndex, { + editorOptions: draw.getOptions() + }) + // 缓存拖拽选区开始元素、位置、开始结束id + const cacheStartElement = cacheElementList[cacheStartIndex] + const cacheStartPosition = cachePositionList[cacheStartIndex] + const cacheRangeStartId = createDragId(cacheElementList[cacheStartIndex]) + const cacheRangeEndId = createDragId(cacheElementList[cacheEndIndex]) + // 设置拖拽值 + const replaceLength = replaceElementList.length + let rangeStart = range.startIndex + let rangeEnd = rangeStart + replaceLength + const activeControl = control.getActiveControl() + if ( + activeControl && + cacheElementList[rangeStart].controlComponent !== ControlComponent.POSTFIX + ) { + rangeEnd = activeControl.setValue(replaceElementList) + rangeStart = rangeEnd - replaceLength + } else { + draw.spliceElementList(elementList, rangeStart + 1, 0, replaceElementList) + } + if (!~rangeEnd) { + draw.render({ + isSetCursor: false + }) + return + } + // 缓存当前开始结束id + const rangeStartId = createDragId(elementList[rangeStart]) + const rangeEndId = createDragId(elementList[rangeEnd]) + // 删除原有拖拽元素 + const cacheRangeStartIndex = getElementIndexByDragId( + cacheRangeStartId, + cacheElementList + ) + const cacheRangeEndIndex = getElementIndexByDragId( + cacheRangeEndId, + cacheElementList + ) + const cacheEndElement = cacheElementList[cacheRangeEndIndex] + if ( + cacheEndElement.controlId && + cacheEndElement.controlComponent !== ControlComponent.POSTFIX + ) { + rangeManager.replaceRange({ + ...cacheRange, + startIndex: cacheRangeStartIndex, + endIndex: cacheRangeEndIndex + }) + control.getActiveControl()?.cut() + } else { + // td不可删除判断 + let isTdElementDeletable = true + if (cachePositionContext?.isTable) { + const { tableId, trIndex, tdIndex } = cachePositionContext + const originElementList = draw.getOriginalElementList() + isTdElementDeletable = !originElementList.some( + el => + el.id === tableId && + el?.trList?.[trIndex!]?.tdList?.[tdIndex!]?.deletable === false + ) + } + if (isTdElementDeletable) { + draw.spliceElementList( + cacheElementList, + cacheRangeStartIndex + 1, + cacheRangeEndIndex - cacheRangeStartIndex + ) + } + } + // 重设上下文 + const startElement = elementList[range.startIndex] + const startPosition = positionList[range.startIndex] + let positionContextIndex = positionContext.index + if (positionContextIndex) { + if (startElement.tableId && !cacheStartElement.tableId) { + // 表格外移动到表格内&&表格之前 + if (cacheStartPosition.index < positionContextIndex) { + positionContextIndex -= replaceLength + } + } else if (!startElement.tableId && cacheStartElement.tableId) { + // 表格内移到表格外&&表格之前 + if (startPosition.index < positionContextIndex) { + positionContextIndex += replaceLength + } + } + position.setPositionContext({ + ...positionContext, + index: positionContextIndex + }) + } + // 重设选区 + const rangeStartIndex = getElementIndexByDragId(rangeStartId, elementList) + const rangeEndIndex = getElementIndexByDragId(rangeEndId, elementList) + rangeManager.setRange( + isCacheRangeCollapsed ? rangeEndIndex : rangeStartIndex, + rangeEndIndex, + range.tableId, + range.startTdIndex, + range.endTdIndex, + range.startTrIndex, + range.endTrIndex + ) + // 清除渲染副作用 + draw.clearSideEffect() + // 移动图片 + let imgElement: IElement | null = null + if (isCacheRangeCollapsed) { + const elementList = draw.getElementList() + const dragElement = elementList[rangeEndIndex] + if ( + dragElement.type === ElementType.IMAGE || + dragElement.type === ElementType.LATEX + ) { + moveImgPosition(dragElement, evt, host) + imgElement = dragElement + } + } + // 重新渲染 + draw.render({ + isSetCursor: false + }) + // 控件值变更回调 + if (activeControl) { + control.emitControlContentChange() + } else if (cacheStartElement.controlId) { + control.emitControlContentChange({ + context: { + range: cacheRange, + elementList: cacheElementList + }, + controlElement: cacheStartElement + }) + } + // 拖拽后渲染图片工具 + if (imgElement) { + if ( + imgElement.imgDisplay === ImageDisplay.SURROUND || + imgElement.imgDisplay === ImageDisplay.FLOAT_TOP || + imgElement.imgDisplay === ImageDisplay.FLOAT_BOTTOM + ) { + draw.getPreviewer().drawResizer(imgElement) + } else { + const dragPositionList = position.getPositionList() + const dragPosition = dragPositionList[rangeEndIndex] + draw.getPreviewer().drawResizer(imgElement, dragPosition) + } + } + } else if (host.isAllowDrag) { + // 如果是允许拖拽不允许拖放(点击选区时光标闭合)则光标重置 + if (host.cacheRange?.startIndex !== host.cacheRange?.endIndex) { + host.mousedown(evt) + } + } +} diff --git a/src/editor/core/event/handlers/paste.ts b/src/editor/core/event/handlers/paste.ts new file mode 100644 index 0000000..f692a20 --- /dev/null +++ b/src/editor/core/event/handlers/paste.ts @@ -0,0 +1,221 @@ +import { ZERO } from '../../../dataset/constant/Common' +import { VIRTUAL_ELEMENT_TYPE } from '../../../dataset/constant/Element' +import { ElementType } from '../../../dataset/enum/Element' +import { IElement } from '../../../interface/Element' +import { IPasteOption } from '../../../interface/Event' +import { + getClipboardData, + getIsClipboardContainFile, + removeClipboardData +} from '../../../utils/clipboard' +import { + formatElementContext, + getElementListByHTML +} from '../../../utils/element' +import { CanvasEvent } from '../CanvasEvent' +import { IOverrideResult } from '../../override/Override' +import { normalizeLineBreak } from '../../../utils' + +export function pasteElement(host: CanvasEvent, elementList: IElement[]) { + const draw = host.getDraw() + if ( + draw.isReadonly() || + draw.isDisabled() || + draw.getControl().getIsDisabledPasteControl() + ) { + return + } + const rangeManager = draw.getRange() + const { startIndex } = rangeManager.getRange() + const originalElementList = draw.getElementList() + // 全选粘贴无需格式化上下文 + if (~startIndex && !rangeManager.getIsSelectAll()) { + // 如果是复制到虚拟元素里,则粘贴列表的虚拟元素需扁平化处理,避免产生新的虚拟元素 + const anchorElement = originalElementList[startIndex] + if (anchorElement?.titleId || anchorElement?.listId) { + let start = 0 + while (start < elementList.length) { + const pasteElement = elementList[start] + if (anchorElement.titleId && /^\n/.test(pasteElement.value)) { + break + } + if (VIRTUAL_ELEMENT_TYPE.includes(pasteElement.type!)) { + elementList.splice(start, 1) + if (pasteElement.valueList) { + for (let v = 0; v < pasteElement.valueList.length; v++) { + const element = pasteElement.valueList[v] + if (element.value === ZERO || element.value === '\n') { + continue + } + elementList.splice(start, 0, element) + start++ + } + } + start-- + } + start++ + } + } + formatElementContext(originalElementList, elementList, startIndex, { + isBreakWhenWrap: true, + editorOptions: draw.getOptions() + }) + } + draw.insertElementList(elementList) +} + +export function pasteHTML(host: CanvasEvent, htmlText: string) { + const draw = host.getDraw() + if (draw.isReadonly() || draw.isDisabled()) return + const elementList = getElementListByHTML(htmlText, { + innerWidth: draw.getOriginalInnerWidth() + }) + pasteElement(host, elementList) +} + +export function pasteImage(host: CanvasEvent, file: File | Blob) { + const draw = host.getDraw() + if (draw.isReadonly() || draw.isDisabled()) return + const rangeManager = draw.getRange() + const { startIndex } = rangeManager.getRange() + const elementList = draw.getElementList() + // 创建文件读取器 + const fileReader = new FileReader() + fileReader.readAsDataURL(file) + fileReader.onload = () => { + // 计算宽高 + const image = new Image() + const value = fileReader.result as string + image.src = value + image.onload = () => { + const imageElement: IElement = { + value, + type: ElementType.IMAGE, + width: image.width, + height: image.height + } + if (~startIndex) { + formatElementContext(elementList, [imageElement], startIndex, { + editorOptions: draw.getOptions() + }) + } + draw.insertElementList([imageElement]) + } + } +} + +export function pasteByEvent(host: CanvasEvent, evt: ClipboardEvent) { + const draw = host.getDraw() + if (draw.isReadonly() || draw.isDisabled()) return + const clipboardData = evt.clipboardData + if (!clipboardData) return + // 自定义粘贴事件 + const { paste } = draw.getOverride() + if (paste) { + const overrideResult = paste(evt) + // 默认阻止默认事件 + if ((overrideResult)?.preventDefault !== false) return + } + // 优先读取编辑器内部粘贴板数据(粘贴板不包含文件时) + if (!getIsClipboardContainFile(clipboardData)) { + const clipboardText = clipboardData.getData('text') + const editorClipboardData = getClipboardData() + // 不同系统间默认换行符不同 windows:\r\n mac:\n + if ( + editorClipboardData && + normalizeLineBreak(clipboardText) === + normalizeLineBreak(editorClipboardData.text) + ) { + pasteElement(host, editorClipboardData.elementList) + return + } + } + removeClipboardData() + // 从粘贴板提取数据 + let isHTML = false + for (let i = 0; i < clipboardData.items.length; i++) { + const item = clipboardData.items[i] + if (item.type === 'text/html') { + isHTML = true + break + } + } + for (let i = 0; i < clipboardData.items.length; i++) { + const item = clipboardData.items[i] + if (item.kind === 'string') { + if (item.type === 'text/plain' && !isHTML) { + item.getAsString(plainText => { + host.input(plainText) + }) + break + } + if (item.type === 'text/html' && isHTML) { + item.getAsString(htmlText => { + pasteHTML(host, htmlText) + }) + break + } + } else if (item.kind === 'file') { + if (item.type.includes('image')) { + const file = item.getAsFile() + if (file) { + pasteImage(host, file) + } + } + } + } +} + +export async function pasteByApi(host: CanvasEvent, options?: IPasteOption) { + const draw = host.getDraw() + if (draw.isReadonly() || draw.isDisabled()) return + // 自定义粘贴事件 + const { paste } = draw.getOverride() + if (paste) { + const overrideResult = paste() + // 默认阻止默认事件 + if ((overrideResult)?.preventDefault !== false) return + } + // 优先读取编辑器内部粘贴板数据 + const clipboardText = await navigator.clipboard.readText() + const editorClipboardData = getClipboardData() + if (clipboardText === editorClipboardData?.text) { + pasteElement(host, editorClipboardData.elementList) + return + } + removeClipboardData() + // 从内存粘贴板获取数据 + if (options?.isPlainText) { + if (clipboardText) { + host.input(clipboardText) + } + } else { + const clipboardData = await navigator.clipboard.read() + let isHTML = false + for (const item of clipboardData) { + if (item.types.includes('text/html')) { + isHTML = true + break + } + } + for (const item of clipboardData) { + if (item.types.includes('text/plain') && !isHTML) { + const textBlob = await item.getType('text/plain') + const text = await textBlob.text() + if (text) { + host.input(text) + } + } else if (item.types.includes('text/html') && isHTML) { + const htmlTextBlob = await item.getType('text/html') + const htmlText = await htmlTextBlob.text() + if (htmlText) { + pasteHTML(host, htmlText) + } + } else if (item.types.some(type => type.startsWith('image/'))) { + const type = item.types.find(type => type.startsWith('image/'))! + const imageBlob = await item.getType(type) + pasteImage(host, imageBlob) + } + } + } +} diff --git a/src/editor/core/history/HistoryManager.ts b/src/editor/core/history/HistoryManager.ts new file mode 100644 index 0000000..7b589c1 --- /dev/null +++ b/src/editor/core/history/HistoryManager.ts @@ -0,0 +1,61 @@ +import { Draw } from '../draw/Draw' + +export class HistoryManager { + private undoStack: Array = [] + private redoStack: Array = [] + private maxRecordCount: number + + constructor(draw: Draw) { + // 忽略第一次历史记录 + this.maxRecordCount = draw.getOptions().historyMaxRecordCount + 1 + } + + public undo() { + if (this.undoStack.length > 1) { + const pop = this.undoStack.pop()! + this.redoStack.push(pop) + if (this.undoStack.length) { + this.undoStack[this.undoStack.length - 1]() + } + } + } + + public redo() { + if (this.redoStack.length) { + const pop = this.redoStack.pop()! + this.undoStack.push(pop) + pop() + } + } + + public execute(fn: Function) { + this.undoStack.push(fn) + if (this.redoStack.length) { + this.redoStack = [] + } + while (this.undoStack.length > this.maxRecordCount) { + this.undoStack.shift() + } + } + + public isCanUndo(): boolean { + return this.undoStack.length > 1 + } + + public isCanRedo(): boolean { + return !!this.redoStack.length + } + + public isStackEmpty(): boolean { + return !this.undoStack.length && !this.redoStack.length + } + + public recovery() { + this.undoStack = [] + this.redoStack = [] + } + + public popUndo() { + return this.undoStack.pop() + } +} diff --git a/src/editor/core/i18n/I18n.ts b/src/editor/core/i18n/I18n.ts new file mode 100644 index 0000000..7bdf4ed --- /dev/null +++ b/src/editor/core/i18n/I18n.ts @@ -0,0 +1,51 @@ +import { ILang } from '../../interface/i18n/I18n' +import zhCN from './lang/zh-CN.json' +import en from './lang/en.json' +import { mergeObject } from '../../utils' +import { DeepPartial } from '../../interface/Common' + +export class I18n { + private currentLocale: string + + private langMap: Map = new Map([ + ['zhCN', zhCN], + ['en', en] + ]) + + constructor(locale: string) { + this.currentLocale = locale + } + + public registerLangMap(locale: string, lang: DeepPartial) { + const sourceLang = this.langMap.get(locale) + this.langMap.set(locale, mergeObject(sourceLang || zhCN, lang)) + } + + public getLocale(): string { + return this.currentLocale + } + + public setLocale(locale: string) { + this.currentLocale = locale + } + + public getLang(): ILang { + return this.langMap.get(this.currentLocale) || zhCN + } + + public t(path: string): string { + const keyList = path.split('.') + let value = '' + let item = this.getLang() + for (let k = 0; k < keyList.length; k++) { + const key = keyList[k] + const currentValue = Reflect.get(item, key) + if (currentValue) { + value = item = currentValue + } else { + return '' + } + } + return value + } +} diff --git a/src/editor/core/i18n/lang/en.json b/src/editor/core/i18n/lang/en.json new file mode 100644 index 0000000..5020d32 --- /dev/null +++ b/src/editor/core/i18n/lang/en.json @@ -0,0 +1,92 @@ +{ + "contextmenu": { + "global": { + "cut": "Cut", + "copy": "Copy", + "paste": "Paste", + "selectAll": "Select all", + "print": "Print" + }, + "control": { + "delete": "Delete control" + }, + "hyperlink": { + "delete": "Delete hyperlink", + "cancel": "Cancel hyperlink", + "edit": "Edit hyperlink" + }, + "image": { + "change": "Change image", + "saveAs": "Save as image", + "textWrap": "Text wrap", + "textWrapType": { + "embed": "Embed", + "upDown": "Up down", + "surround": "Surround", + "floatTop": "Float above text", + "floatBottom": "Float below text" + } + }, + "table": { + "insertRowCol": "Insert row col", + "insertTopRow": "Insert top 1 row", + "insertBottomRow": "Insert bottom 1 row", + "insertLeftCol": "Insert left 1 col", + "insertRightCol": "Insert right 1 col", + "deleteRowCol": "Delete row col", + "deleteRow": "Delete 1 row", + "deleteCol": "Delete 1 col", + "deleteTable": "Delete table", + "mergeCell": "Merge cell", + "mergeCancelCell": "Cancel merge cell", + "verticalAlign": "Vertical align", + "verticalAlignTop": "Top", + "verticalAlignMiddle": "Middle", + "verticalAlignBottom": "Bottom", + "border": "Table border", + "borderAll": "All", + "borderEmpty": "Empty", + "borderDash": "Dash", + "borderExternal": "External", + "borderInternal": "Internal", + "borderTd": "Table cell border", + "borderTdTop": "Top", + "borderTdRight": "Right", + "borderTdBottom": "Bottom", + "borderTdLeft": "Left", + "borderTdForward": "Forward", + "borderTdBack": "Back" + } + }, + "datePicker": { + "now": "Now", + "confirm": "Confirm", + "return": "Return", + "timeSelect": "Time select", + "weeks": { + "sun": "Sun", + "mon": "Mon", + "tue": "Tue", + "wed": "Wed", + "thu": "Thu", + "fri": "Fri", + "sat": "Sat" + }, + "year": " ", + "month": " ", + "hour": "Hour", + "minute": "Minute", + "second": "Second" + }, + "frame": { + "header": "Header", + "footer": "Footer" + }, + "pageBreak": { + "displayName": "Page Break" + }, + "zone": { + "headerTip": "Double click to edit header", + "footerTip": "Double click to edit footer" + } +} diff --git a/src/editor/core/i18n/lang/zh-CN.json b/src/editor/core/i18n/lang/zh-CN.json new file mode 100644 index 0000000..cb2b144 --- /dev/null +++ b/src/editor/core/i18n/lang/zh-CN.json @@ -0,0 +1,92 @@ +{ + "contextmenu": { + "global": { + "cut": "剪切", + "copy": "复制", + "paste": "粘贴", + "selectAll": "全选", + "print": "打印" + }, + "control": { + "delete": "删除控件" + }, + "hyperlink": { + "delete": "删除链接", + "cancel": "取消链接", + "edit": "编辑链接" + }, + "image": { + "change": "更改图片", + "saveAs": "另存为图片", + "textWrap": "文字环绕", + "textWrapType": { + "embed": "嵌入型", + "upDown": "上下型环绕", + "surround": "四周型环绕", + "floatTop": "浮于文字上方", + "floatBottom": "衬于文字下方" + } + }, + "table": { + "insertRowCol": "插入行列", + "insertTopRow": "上方插入1行", + "insertBottomRow": "下方插入1行", + "insertLeftCol": "左侧插入1列", + "insertRightCol": "右侧插入1列", + "deleteRowCol": "删除行列", + "deleteRow": "删除1行", + "deleteCol": "删除1列", + "deleteTable": "删除整个表格", + "mergeCell": "合并单元格", + "mergeCancelCell": "取消合并", + "verticalAlign": "垂直对齐", + "verticalAlignTop": "顶端对齐", + "verticalAlignMiddle": "垂直居中", + "verticalAlignBottom": "底端对齐", + "border": "表格边框", + "borderAll": "所有框线", + "borderEmpty": "无框线", + "borderDash": "虚框线", + "borderExternal": "外侧框线", + "borderInternal": "内侧框线", + "borderTd": "单元格边框", + "borderTdTop": "上边框", + "borderTdRight": "右边框", + "borderTdBottom": "下边框", + "borderTdLeft": "左边框", + "borderTdForward": "正斜线", + "borderTdBack": "反斜线" + } + }, + "datePicker": { + "now": "此刻", + "confirm": "确定", + "return": "返回日期", + "timeSelect": "时间选择", + "weeks": { + "sun": "日", + "mon": "一", + "tue": "二", + "wed": "三", + "thu": "四", + "fri": "五", + "sat": "六" + }, + "year": "年", + "month": "月", + "hour": "时", + "minute": "分", + "second": "秒" + }, + "frame": { + "header": "页眉", + "footer": "页脚" + }, + "pageBreak": { + "displayName": "分页符" + }, + "zone": { + "headerTip": "双击编辑页眉", + "footerTip": "双击编辑页脚" + } +} diff --git a/src/editor/core/listener/Listener.ts b/src/editor/core/listener/Listener.ts new file mode 100644 index 0000000..bfdd403 --- /dev/null +++ b/src/editor/core/listener/Listener.ts @@ -0,0 +1,41 @@ +import { + IContentChange, + IControlChange, + IControlContentChange, + IIntersectionPageNoChange, + IPageModeChange, + IPageScaleChange, + IPageSizeChange, + IRangeStyleChange, + ISaved, + IVisiblePageNoListChange, + IZoneChange +} from '../../interface/Listener' + +export class Listener { + public rangeStyleChange: IRangeStyleChange | null + public visiblePageNoListChange: IVisiblePageNoListChange | null + public intersectionPageNoChange: IIntersectionPageNoChange | null + public pageSizeChange: IPageSizeChange | null + public pageScaleChange: IPageScaleChange | null + public saved: ISaved | null + public contentChange: IContentChange | null + public controlChange: IControlChange | null + public controlContentChange: IControlContentChange | null + public pageModeChange: IPageModeChange | null + public zoneChange: IZoneChange | null + + constructor() { + this.rangeStyleChange = null + this.visiblePageNoListChange = null + this.intersectionPageNoChange = null + this.pageSizeChange = null + this.pageScaleChange = null + this.saved = null + this.contentChange = null + this.controlChange = null + this.controlContentChange = null + this.pageModeChange = null + this.zoneChange = null + } +} diff --git a/src/editor/core/observer/ImageObserver.ts b/src/editor/core/observer/ImageObserver.ts new file mode 100644 index 0000000..d91d839 --- /dev/null +++ b/src/editor/core/observer/ImageObserver.ts @@ -0,0 +1,19 @@ +export class ImageObserver { + private promiseList: Promise[] + + constructor() { + this.promiseList = [] + } + + public add(payload: Promise) { + this.promiseList.push(payload) + } + + public clearAll() { + this.promiseList = [] + } + + public allSettled() { + return Promise.allSettled(this.promiseList) + } +} diff --git a/src/editor/core/observer/MouseObserver.ts b/src/editor/core/observer/MouseObserver.ts new file mode 100644 index 0000000..85f6cf1 --- /dev/null +++ b/src/editor/core/observer/MouseObserver.ts @@ -0,0 +1,56 @@ +import { EventBusMap } from '../../interface/EventBus' +import { Draw } from '../draw/Draw' +import { EventBus } from '../event/eventbus/EventBus' + +export class MouseObserver { + private draw: Draw + private eventBus: EventBus + private pageContainer: HTMLDivElement + constructor(draw: Draw) { + this.draw = draw + this.eventBus = this.draw.getEventBus() + this.pageContainer = this.draw.getPageContainer() + this.pageContainer.addEventListener('mousemove', this._mousemove.bind(this)) + this.pageContainer.addEventListener( + 'mouseenter', + this._mouseenter.bind(this) + ) + this.pageContainer.addEventListener( + 'mouseleave', + this._mouseleave.bind(this) + ) + this.pageContainer.addEventListener('mousedown', this._mousedown.bind(this)) + this.pageContainer.addEventListener('mouseup', this._mouseup.bind(this)) + this.pageContainer.addEventListener('click', this._click.bind(this)) + } + + private _mousemove(evt: MouseEvent) { + if (!this.eventBus.isSubscribe('mousemove')) return + this.eventBus.emit('mousemove', evt) + } + + private _mouseenter(evt: MouseEvent) { + if (!this.eventBus.isSubscribe('mouseenter')) return + this.eventBus.emit('mouseenter', evt) + } + + private _mouseleave(evt: MouseEvent) { + if (!this.eventBus.isSubscribe('mouseleave')) return + this.eventBus.emit('mouseleave', evt) + } + + private _mousedown(evt: MouseEvent) { + if (!this.eventBus.isSubscribe('mousedown')) return + this.eventBus.emit('mousedown', evt) + } + + private _mouseup(evt: MouseEvent) { + if (!this.eventBus.isSubscribe('mouseup')) return + this.eventBus.emit('mouseup', evt) + } + + private _click(evt: MouseEvent) { + if (!this.eventBus.isSubscribe('click')) return + this.eventBus.emit('click', evt) + } +} diff --git a/src/editor/core/observer/ScrollObserver.ts b/src/editor/core/observer/ScrollObserver.ts new file mode 100644 index 0000000..82ec566 --- /dev/null +++ b/src/editor/core/observer/ScrollObserver.ts @@ -0,0 +1,88 @@ +import { IEditorOption } from '../../interface/Editor' +import { debounce } from '../../utils' +import { Draw } from '../draw/Draw' + +export interface IElementVisibleInfo { + intersectionHeight: number +} + +export interface IPageVisibleInfo { + intersectionPageNo: number + visiblePageNoList: number[] +} + +export class ScrollObserver { + private draw: Draw + private options: Required + private scrollContainer: Element | Document + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + this.scrollContainer = this.getScrollContainer() + // 监听滚轮 + setTimeout(() => { + if (!window.scrollY) { + this._observer() + } + }) + this._addEvent() + } + + public getScrollContainer(): Element | Document { + return this.options.scrollContainerSelector + ? document.querySelector(this.options.scrollContainerSelector) || document + : document + } + + private _addEvent() { + this.scrollContainer.addEventListener('scroll', this._observer) + } + + public removeEvent() { + this.scrollContainer.removeEventListener('scroll', this._observer) + } + + public getElementVisibleInfo(element: Element): IElementVisibleInfo { + const rect = element.getBoundingClientRect() + const viewHeight = + this.scrollContainer === document + ? Math.max(document.documentElement.clientHeight, window.innerHeight) + : (this.scrollContainer).clientHeight + const visibleHeight = + Math.min(rect.bottom, viewHeight) - Math.max(rect.top, 0) + return { + intersectionHeight: visibleHeight > 0 ? visibleHeight : 0 + } + } + + public getPageVisibleInfo(): IPageVisibleInfo { + const pageList = this.draw.getPageList() + const visiblePageNoList: number[] = [] + let intersectionPageNo = 0 + let intersectionMaxHeight = 0 + for (let i = 0; i < pageList.length; i++) { + const curPage = pageList[i] + const { intersectionHeight } = this.getElementVisibleInfo(curPage) + // 之前页存在交叉 && 当前页不交叉则后续均不交叉,结束循环 + if (intersectionMaxHeight && !intersectionHeight) break + if (intersectionHeight) { + visiblePageNoList.push(i) + } + if (intersectionHeight > intersectionMaxHeight) { + intersectionMaxHeight = intersectionHeight + intersectionPageNo = i + } + } + return { + intersectionPageNo, + visiblePageNoList + } + } + + private _observer = debounce(() => { + const { intersectionPageNo, visiblePageNoList } = this.getPageVisibleInfo() + this.draw.setIntersectionPageNo(intersectionPageNo) + this.draw.setVisiblePageNoList(visiblePageNoList) + }, 150) +} diff --git a/src/editor/core/observer/SelectionObserver.ts b/src/editor/core/observer/SelectionObserver.ts new file mode 100644 index 0000000..f617532 --- /dev/null +++ b/src/editor/core/observer/SelectionObserver.ts @@ -0,0 +1,143 @@ +import { MoveDirection } from '../../dataset/enum/Observer' +import { Draw } from '../draw/Draw' +import { RangeManager } from '../range/RangeManager' + +export class SelectionObserver { + // 每次滚动长度 + private readonly step: number = 5 + // 触发滚动阀值 + private readonly thresholdPoints: [ + top: number, + down: number, + left: number, + right: number + ] = [70, 40, 10, 20] + + private selectionContainer: Element | Document + private rangeManager: RangeManager + private requestAnimationFrameId: number | null + private isMousedown: boolean + private isMoving: boolean + private clientWidth: number + private clientHeight: number + private containerRect: DOMRect | null + + constructor(draw: Draw) { + this.rangeManager = draw.getRange() + // 优先使用配置的滚动容器dom + const { scrollContainerSelector } = draw.getOptions() + this.selectionContainer = scrollContainerSelector + ? document.querySelector(scrollContainerSelector) || document + : document + this.requestAnimationFrameId = null + this.isMousedown = false + this.isMoving = false + // 缓存尺寸 + this.clientWidth = 0 + this.clientHeight = 0 + this.containerRect = null + // 添加监听 + this._addEvent() + } + + private _addEvent() { + const container = this.selectionContainer + container.addEventListener('mousedown', this._mousedown) + container.addEventListener('mousemove', this._mousemove) + container.addEventListener('mouseup', this._mouseup) + document.addEventListener('mouseleave', this._mouseup) + } + + public removeEvent() { + const container = this.selectionContainer + container.removeEventListener('mousedown', this._mousedown) + container.removeEventListener('mousemove', this._mousemove) + container.removeEventListener('mouseup', this._mouseup) + document.removeEventListener('mouseleave', this._mouseup) + } + + private _mousedown = () => { + this.isMousedown = true + // 更新容器宽高 + this.clientWidth = + this.selectionContainer instanceof Document + ? document.documentElement.clientWidth + : this.selectionContainer.clientWidth + this.clientHeight = + this.selectionContainer instanceof Document + ? document.documentElement.clientHeight + : this.selectionContainer.clientHeight + // 更新容器位置信息 + if (!(this.selectionContainer instanceof Document)) { + const rect = this.selectionContainer.getBoundingClientRect() + this.containerRect = rect + } + } + + private _mouseup = () => { + this.isMousedown = false + this._stopMove() + } + + private _mousemove = (evt: MouseEvent) => { + if (!this.isMousedown || this.rangeManager.getIsCollapsed()) return + let { x, y } = evt + if (this.containerRect) { + x = x - this.containerRect.x + y = y - this.containerRect.y + } + if (y < this.thresholdPoints[0]) { + this._startMove(MoveDirection.UP) + } else if (this.clientHeight - y <= this.thresholdPoints[1]) { + this._startMove(MoveDirection.DOWN) + } else if (x < this.thresholdPoints[2]) { + this._startMove(MoveDirection.LEFT) + } else if (this.clientWidth - x < this.thresholdPoints[3]) { + this._startMove(MoveDirection.RIGHT) + } else { + this._stopMove() + } + } + + private _move(direction: MoveDirection) { + // Document使用window + const container = + this.selectionContainer instanceof Document + ? window + : this.selectionContainer + const x = + this.selectionContainer instanceof Document + ? window.scrollX + : (container).scrollLeft + const y = + this.selectionContainer instanceof Document + ? window.scrollY + : (container).scrollTop + if (direction === MoveDirection.DOWN) { + container.scrollTo(x, y + this.step) + } else if (direction === MoveDirection.UP) { + container.scrollTo(x, y - this.step) + } else if (direction === MoveDirection.LEFT) { + container.scrollTo(x - this.step, y) + } else { + container.scrollTo(x + this.step, y) + } + this.requestAnimationFrameId = window.requestAnimationFrame( + this._move.bind(this, direction) + ) + } + + private _startMove(direction: MoveDirection) { + if (this.isMoving) return + this.isMoving = true + this._move(direction) + } + + private _stopMove() { + if (this.requestAnimationFrameId) { + window.cancelAnimationFrame(this.requestAnimationFrameId) + this.requestAnimationFrameId = null + this.isMoving = false + } + } +} diff --git a/src/editor/core/override/Override.ts b/src/editor/core/override/Override.ts new file mode 100644 index 0000000..93825ac --- /dev/null +++ b/src/editor/core/override/Override.ts @@ -0,0 +1,11 @@ +export interface IOverrideResult { + preventDefault?: boolean +} + +export class Override { + public paste: + | ((evt?: ClipboardEvent) => unknown | IOverrideResult) + | undefined + public copy: (() => unknown | IOverrideResult) | undefined + public drop: ((evt: DragEvent) => unknown | IOverrideResult) | undefined +} diff --git a/src/editor/core/plugin/Plugin.ts b/src/editor/core/plugin/Plugin.ts new file mode 100644 index 0000000..d1efcd1 --- /dev/null +++ b/src/editor/core/plugin/Plugin.ts @@ -0,0 +1,17 @@ +import Editor from '../..' +import { PluginFunction } from '../../interface/Plugin' + +export class Plugin { + private editor: Editor + + constructor(editor: Editor) { + this.editor = editor + } + + public use( + pluginFunction: PluginFunction, + options?: Options + ) { + pluginFunction(this.editor, options) + } +} diff --git a/src/editor/core/position/Position.ts b/src/editor/core/position/Position.ts new file mode 100644 index 0000000..ed7e160 --- /dev/null +++ b/src/editor/core/position/Position.ts @@ -0,0 +1,849 @@ +import { ElementType, ListStyle, RowFlex, VerticalAlign } from '../..' +import { ZERO } from '../../dataset/constant/Common' +import { ControlComponent } from '../../dataset/enum/Control' +import { + IComputePageRowPositionPayload, + IComputePageRowPositionResult, + IComputeRowPositionPayload, + IFloatPosition, + IGetFloatPositionByXYPayload, + ISetSurroundPositionPayload +} from '../../interface/Position' +import { IEditorOption } from '../../interface/Editor' +import { IElement, IElementPosition } from '../../interface/Element' +import { + ICurrentPosition, + IGetPositionByXYPayload, + IPositionContext +} from '../../interface/Position' +import { Draw } from '../draw/Draw' +import { EditorMode, EditorZone } from '../../dataset/enum/Editor' +import { deepClone, isRectIntersect } from '../../utils' +import { ImageDisplay } from '../../dataset/enum/Common' +import { DeepRequired } from '../../interface/Common' +import { EventBus } from '../event/eventbus/EventBus' +import { EventBusMap } from '../../interface/EventBus' +import { getIsBlockElement } from '../../utils/element' + +export class Position { + private cursorPosition: IElementPosition | null + private positionContext: IPositionContext + private positionList: IElementPosition[] + private floatPositionList: IFloatPosition[] + + private draw: Draw + private eventBus: EventBus + private options: DeepRequired + + constructor(draw: Draw) { + this.positionList = [] + this.floatPositionList = [] + this.cursorPosition = null + this.positionContext = { + isTable: false, + isControl: false + } + + this.draw = draw + this.eventBus = draw.getEventBus() + this.options = draw.getOptions() + } + + public getFloatPositionList(): IFloatPosition[] { + return this.floatPositionList + } + + public getTablePositionList( + sourceElementList: IElement[] + ): IElementPosition[] { + const { index, trIndex, tdIndex } = this.positionContext + return ( + sourceElementList[index!].trList![trIndex!].tdList[tdIndex!] + .positionList || [] + ) + } + + public getPositionList(): IElementPosition[] { + return this.positionContext.isTable + ? this.getTablePositionList(this.draw.getOriginalElementList()) + : this.getOriginalPositionList() + } + + public getMainPositionList(): IElementPosition[] { + return this.positionContext.isTable + ? this.getTablePositionList(this.draw.getOriginalMainElementList()) + : this.positionList + } + + public getOriginalPositionList(): IElementPosition[] { + const zoneManager = this.draw.getZone() + if (zoneManager.isHeaderActive()) { + const header = this.draw.getHeader() + return header.getPositionList() + } + if (zoneManager.isFooterActive()) { + const footer = this.draw.getFooter() + return footer.getPositionList() + } + return this.positionList + } + + public getOriginalMainPositionList(): IElementPosition[] { + return this.positionList + } + + public getSelectionPositionList(): IElementPosition[] | null { + const { startIndex, endIndex } = this.draw.getRange().getRange() + if (startIndex === endIndex) return null + const positionList = this.getPositionList() + return positionList.slice(startIndex + 1, endIndex + 1) + } + + public setPositionList(payload: IElementPosition[]) { + this.positionList = payload + } + + public setFloatPositionList(payload: IFloatPosition[]) { + this.floatPositionList = payload + } + + public computePageRowPosition( + payload: IComputePageRowPositionPayload + ): IComputePageRowPositionResult { + const { + positionList, + rowList, + pageNo, + startX, + startY, + startRowIndex, + startIndex, + innerWidth, + zone + } = payload + const { + scale, + table: { tdPadding } + } = this.options + let x = startX + let y = startY + let index = startIndex + for (let i = 0; i < rowList.length; i++) { + const curRow = rowList[i] + // 行存在环绕的可能性均不设置行布局 + if (!curRow.isSurround) { + // 计算行偏移量(行居中、居右) + const curRowWidth = curRow.width + (curRow.offsetX || 0) + if (curRow.rowFlex === RowFlex.CENTER) { + x += (innerWidth - curRowWidth) / 2 + } else if (curRow.rowFlex === RowFlex.RIGHT) { + x += innerWidth - curRowWidth + } + } + // 当前行X/Y轴偏移量 + x += curRow.offsetX || 0 + y += curRow.offsetY || 0 + // 当前td所在位置 + const tablePreX = x + const tablePreY = y + for (let j = 0; j < curRow.elementList.length; j++) { + const element = curRow.elementList[j] + const metrics = element.metrics + const offsetY = + !element.hide && + ((element.imgDisplay !== ImageDisplay.INLINE && + element.type === ElementType.IMAGE) || + element.type === ElementType.LATEX) + ? curRow.ascent - metrics.height + : curRow.ascent + // 偏移量 + if (element.left) { + x += element.left + } + const positionItem: IElementPosition = { + pageNo, + index, + value: element.value, + rowIndex: startRowIndex + i, + rowNo: i, + metrics, + left: element.left || 0, + ascent: offsetY, + lineHeight: curRow.height, + isFirstLetter: j === 0, + isLastLetter: j === curRow.elementList.length - 1, + coordinate: { + leftTop: [x, y], + leftBottom: [x, y + curRow.height], + rightTop: [x + metrics.width, y], + rightBottom: [x + metrics.width, y + curRow.height] + } + } + // 缓存浮动元素信息 + if ( + element.imgDisplay === ImageDisplay.SURROUND || + element.imgDisplay === ImageDisplay.FLOAT_TOP || + element.imgDisplay === ImageDisplay.FLOAT_BOTTOM + ) { + // 浮动元素使用上一位置信息 + const prePosition = positionList[positionList.length - 1] + if (prePosition) { + positionItem.metrics = prePosition.metrics + positionItem.coordinate = prePosition.coordinate + } + // 兼容浮动元素初始坐标为空的情况-默认使用左上坐标 + if (!element.imgFloatPosition) { + element.imgFloatPosition = { + x, + y, + pageNo + } + } + this.floatPositionList.push({ + pageNo, + element, + position: positionItem, + isTable: payload.isTable, + index: payload.index, + tdIndex: payload.tdIndex, + trIndex: payload.trIndex, + tdValueIndex: index, + zone + }) + } + positionList.push(positionItem) + index++ + x += metrics.width + // 计算表格内元素位置 + if (element.type === ElementType.TABLE && !element.hide) { + const tdPaddingWidth = tdPadding[1] + tdPadding[3] + const tdPaddingHeight = tdPadding[0] + tdPadding[2] + for (let t = 0; t < element.trList!.length; t++) { + const tr = element.trList![t] + for (let d = 0; d < tr.tdList!.length; d++) { + const td = tr.tdList[d] + td.positionList = [] + const rowList = td.rowList! + const drawRowResult = this.computePageRowPosition({ + positionList: td.positionList, + rowList, + pageNo, + startRowIndex: 0, + startIndex: 0, + startX: (td.x! + tdPadding[3]) * scale + tablePreX, + startY: (td.y! + tdPadding[0]) * scale + tablePreY, + innerWidth: (td.width! - tdPaddingWidth) * scale, + isTable: true, + index: index - 1, + tdIndex: d, + trIndex: t, + zone + }) + // 垂直对齐方式 + if ( + td.verticalAlign === VerticalAlign.MIDDLE || + td.verticalAlign === VerticalAlign.BOTTOM + ) { + const rowsHeight = rowList.reduce( + (pre, cur) => pre + cur.height, + 0 + ) + const blankHeight = + (td.height! - tdPaddingHeight) * scale - rowsHeight + const offsetHeight = + td.verticalAlign === VerticalAlign.MIDDLE + ? blankHeight / 2 + : blankHeight + if (Math.floor(offsetHeight) > 0) { + td.positionList.forEach(tdPosition => { + const { + coordinate: { leftTop, leftBottom, rightBottom, rightTop } + } = tdPosition + leftTop[1] += offsetHeight + leftBottom[1] += offsetHeight + rightBottom[1] += offsetHeight + rightTop[1] += offsetHeight + }) + } + } + x = drawRowResult.x + y = drawRowResult.y + } + } + // 恢复初始x、y + x = tablePreX + y = tablePreY + } + } + x = startX + y += curRow.height + } + return { x, y, index } + } + + public computePositionList() { + // 置空原位置信息 + this.positionList = [] + // 按每页行计算 + const innerWidth = this.draw.getInnerWidth() + const pageRowList = this.draw.getPageRowList() + const margins = this.draw.getMargins() + const startX = margins[3] + // 起始位置受页眉影响 + const header = this.draw.getHeader() + const extraHeight = header.getExtraHeight() + const startY = margins[0] + extraHeight + let startRowIndex = 0 + for (let i = 0; i < pageRowList.length; i++) { + const rowList = pageRowList[i] + const startIndex = rowList[0]?.startIndex + this.computePageRowPosition({ + positionList: this.positionList, + rowList, + pageNo: i, + startRowIndex, + startIndex, + startX, + startY, + innerWidth + }) + startRowIndex += rowList.length + } + } + + public computeRowPosition( + payload: IComputeRowPositionPayload + ): IElementPosition[] { + const { row, innerWidth } = payload + const positionList: IElementPosition[] = [] + this.computePageRowPosition({ + positionList, + innerWidth, + rowList: [deepClone(row)], + pageNo: 0, + startX: 0, + startY: 0, + startIndex: 0, + startRowIndex: 0 + }) + return positionList + } + + public setCursorPosition(position: IElementPosition | null) { + this.cursorPosition = position + } + + public getCursorPosition(): IElementPosition | null { + return this.cursorPosition + } + + public getPositionContext(): IPositionContext { + return this.positionContext + } + + public setPositionContext(payload: IPositionContext) { + this.eventBus.emit('positionContextChange', { + value: payload, + oldValue: this.positionContext + }) + this.positionContext = payload + } + + public getPositionByXY(payload: IGetPositionByXYPayload): ICurrentPosition { + const { x, y, isTable } = payload + let { elementList, positionList } = payload + if (!elementList) { + elementList = this.draw.getOriginalElementList() + } + if (!positionList) { + positionList = this.getOriginalPositionList() + } + const zoneManager = this.draw.getZone() + const curPageNo = payload.pageNo ?? this.draw.getPageNo() + const isMainActive = zoneManager.isMainActive() + const positionNo = isMainActive ? curPageNo : 0 + // 验证浮于文字上方元素 + if (!isTable) { + const floatTopPosition = this.getFloatPositionByXY({ + ...payload, + imgDisplays: [ImageDisplay.FLOAT_TOP, ImageDisplay.SURROUND] + }) + if (floatTopPosition) return floatTopPosition + } + // 普通元素 + for (let j = 0; j < positionList.length; j++) { + const { + index, + pageNo, + left, + isFirstLetter, + coordinate: { leftTop, rightTop, leftBottom } + } = positionList[j] + if (positionNo !== pageNo) continue + if (pageNo > positionNo) break + // 命中元素 + if ( + leftTop[0] - left <= x && + rightTop[0] >= x && + leftTop[1] <= y && + leftBottom[1] >= y + ) { + let curPositionIndex = j + const element = elementList[j] + // 表格被命中 + if (element.type === ElementType.TABLE) { + for (let t = 0; t < element.trList!.length; t++) { + const tr = element.trList![t] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const tablePosition = this.getPositionByXY({ + x, + y, + td, + pageNo: curPageNo, + tablePosition: positionList[j], + isTable: true, + elementList: td.value, + positionList: td.positionList + }) + if (~tablePosition.index) { + const { index: tdValueIndex, hitLineStartIndex } = tablePosition + const tdValueElement = td.value[tdValueIndex] + return { + index, + isCheckbox: + tablePosition.isCheckbox || + tdValueElement.type === ElementType.CHECKBOX || + tdValueElement.controlComponent === + ControlComponent.CHECKBOX, + isRadio: + tdValueElement.type === ElementType.RADIO || + tdValueElement.controlComponent === ControlComponent.RADIO, + isControl: !!tdValueElement.controlId, + isImage: tablePosition.isImage, + isDirectHit: tablePosition.isDirectHit, + isTable: true, + tdIndex: d, + trIndex: t, + tdValueIndex, + tdId: td.id, + trId: tr.id, + tableId: element.id, + hitLineStartIndex + } + } + } + } + } + // 图片区域均为命中 + if ( + element.type === ElementType.IMAGE || + element.type === ElementType.LATEX + ) { + return { + index: curPositionIndex, + isDirectHit: true, + isImage: true + } + } + if ( + element.type === ElementType.CHECKBOX || + element.controlComponent === ControlComponent.CHECKBOX + ) { + return { + index: curPositionIndex, + isDirectHit: true, + isCheckbox: true + } + } + if ( + element.type === ElementType.TAB && + element.listStyle === ListStyle.CHECKBOX + ) { + // 向前找checkbox元素 + let index = curPositionIndex - 1 + while (index > 0) { + const element = elementList[index] + if ( + element.value === ZERO && + element.listStyle === ListStyle.CHECKBOX + ) { + break + } + index-- + } + return { + index, + isDirectHit: true, + isCheckbox: true + } + } + if ( + element.type === ElementType.RADIO || + element.controlComponent === ControlComponent.RADIO + ) { + return { + index: curPositionIndex, + isDirectHit: true, + isRadio: true + } + } + let hitLineStartIndex: number | undefined + // 判断是否在文字中间前后 + if (elementList[index].value !== ZERO) { + const valueWidth = rightTop[0] - leftTop[0] + if (x < leftTop[0] + valueWidth / 2) { + curPositionIndex = j - 1 + if (isFirstLetter) { + hitLineStartIndex = j + } + } + } + return { + isDirectHit: true, + hitLineStartIndex, + index: curPositionIndex, + isControl: !!element.controlId + } + } + } + // 验证衬于文字下方元素 + if (!isTable) { + const floatBottomPosition = this.getFloatPositionByXY({ + ...payload, + imgDisplays: [ImageDisplay.FLOAT_BOTTOM] + }) + if (floatBottomPosition) return floatBottomPosition + } + // 非命中区域 + let isLastArea = false + let curPositionIndex = -1 + let hitLineStartIndex: number | undefined + // 判断是否在表格内 + if (isTable) { + const { scale } = this.options + const { td, tablePosition } = payload + if (td && tablePosition) { + const { leftTop } = tablePosition.coordinate + const tdX = td.x! * scale + leftTop[0] + const tdY = td.y! * scale + leftTop[1] + const tdWidth = td.width! * scale + const tdHeight = td.height! * scale + if (!(tdX < x && x < tdX + tdWidth && tdY < y && y < tdY + tdHeight)) { + return { + index: curPositionIndex + } + } + } + } + // 判断所属行是否存在元素 + const lastLetterList = positionList.filter( + p => p.isLastLetter && p.pageNo === positionNo + ) + for (let j = 0; j < lastLetterList.length; j++) { + const { + index, + rowNo, + coordinate: { leftTop, leftBottom } + } = lastLetterList[j] + if (y > leftTop[1] && y <= leftBottom[1]) { + const headIndex = positionList.findIndex( + p => p.pageNo === positionNo && p.rowNo === rowNo + ) + const headElement = elementList[headIndex] + const headPosition = positionList[headIndex] + // 是否在头部 + const headStartX = + headElement.listStyle === ListStyle.CHECKBOX + ? this.draw.getMargins()[3] + : headPosition.coordinate.leftTop[0] + if (x < headStartX) { + // 头部元素为空元素时无需选中 + if (~headIndex) { + if (headPosition.value === ZERO) { + curPositionIndex = headIndex + } else { + curPositionIndex = headIndex - 1 + hitLineStartIndex = headIndex + } + } else { + curPositionIndex = index + } + } else { + // 是否是复选框列表 + if (headElement.listStyle === ListStyle.CHECKBOX && x < leftTop[0]) { + return { + index: headIndex, + isDirectHit: true, + isCheckbox: true + } + } + curPositionIndex = index + } + isLastArea = true + break + } + } + if (!isLastArea) { + // 页眉底部距离页面顶部距离 + const header = this.draw.getHeader() + const headerHeight = header.getHeight() + const headerBottomY = header.getHeaderTop() + headerHeight + // 页脚上部距离页面顶部距离 + const footer = this.draw.getFooter() + const pageHeight = this.draw.getHeight() + const footerTopY = + pageHeight - (footer.getFooterBottom() + footer.getHeight()) + // 判断所属位置是否属于页眉页脚区域 + if (isMainActive) { + // 页眉:当前位置小于页眉底部位置 + if (y < headerBottomY) { + return { + index: -1, + zone: EditorZone.HEADER + } + } + // 页脚:当前位置大于页脚顶部位置 + if (y > footerTopY) { + return { + index: -1, + zone: EditorZone.FOOTER + } + } + } else { + // main区域:当前位置小于页眉底部位置 && 大于页脚顶部位置 + if (y <= footerTopY && y >= headerBottomY) { + return { + index: -1, + zone: EditorZone.MAIN + } + } + } + // 正文上-循环首行 + const margins = this.draw.getMargins() + if (y <= margins[0]) { + for (let p = 0; p < positionList.length; p++) { + const position = positionList[p] + if (position.pageNo !== positionNo || position.rowNo !== 0) continue + const { leftTop, rightTop } = position.coordinate + // 小于左页边距 || 命中文字 || 首行最后元素 + if ( + x <= margins[3] || + (x >= leftTop[0] && x <= rightTop[0]) || + positionList[p + 1]?.rowNo !== 0 + ) { + return { + index: position.index + } + } + } + } else { + // 正文下-循环尾行 + const lastLetter = lastLetterList[lastLetterList.length - 1] + if (lastLetter) { + const lastRowNo = lastLetter.rowNo + for (let p = 0; p < positionList.length; p++) { + const position = positionList[p] + if ( + position.pageNo !== positionNo || + position.rowNo !== lastRowNo + ) { + continue + } + const { leftTop, rightTop } = position.coordinate + // 小于左页边距 || 命中文字 || 尾行最后元素 + if ( + x <= margins[3] || + (x >= leftTop[0] && x <= rightTop[0]) || + positionList[p + 1]?.rowNo !== lastRowNo + ) { + return { + index: position.index + } + } + } + } + } + // 当前页最后一行 + return { + index: + lastLetterList[lastLetterList.length - 1]?.index || + positionList.length - 1 + } + } + return { + hitLineStartIndex, + index: curPositionIndex, + isControl: !!elementList[curPositionIndex]?.controlId + } + } + + public getFloatPositionByXY( + payload: IGetFloatPositionByXYPayload + ): ICurrentPosition | void { + const { x, y } = payload + const currentPageNo = payload.pageNo ?? this.draw.getPageNo() + const currentZone = this.draw.getZone().getZone() + const { scale } = this.options + for (let f = 0; f < this.floatPositionList.length; f++) { + const { + position, + element, + isTable, + index, + trIndex, + tdIndex, + tdValueIndex, + zone: floatElementZone, + pageNo + } = this.floatPositionList[f] + if ( + currentPageNo === pageNo && + element.type === ElementType.IMAGE && + element.imgDisplay && + payload.imgDisplays.includes(element.imgDisplay) && + (!floatElementZone || floatElementZone === currentZone) + ) { + const imgFloatPosition = element.imgFloatPosition! + const imgFloatPositionX = imgFloatPosition.x * scale + const imgFloatPositionY = imgFloatPosition.y * scale + const elementWidth = element.width! * scale + const elementHeight = element.height! * scale + if ( + x >= imgFloatPositionX && + x <= imgFloatPositionX + elementWidth && + y >= imgFloatPositionY && + y <= imgFloatPositionY + elementHeight + ) { + if (isTable) { + return { + index: index!, + isDirectHit: true, + isImage: true, + isTable, + trIndex, + tdIndex, + tdValueIndex, + tdId: element.tdId, + trId: element.trId, + tableId: element.tableId + } + } + return { + index: position.index, + isDirectHit: true, + isImage: true + } + } + } + } + } + + public adjustPositionContext( + payload: IGetPositionByXYPayload + ): ICurrentPosition | null { + const positionResult = this.getPositionByXY(payload) + if (!~positionResult.index) return null + // 移动控件内光标 + if ( + positionResult.isControl && + this.draw.getMode() !== EditorMode.READONLY + ) { + const { index, isTable, trIndex, tdIndex, tdValueIndex } = positionResult + const control = this.draw.getControl() + const { newIndex } = control.moveCursor({ + index, + isTable, + trIndex, + tdIndex, + tdValueIndex + }) + if (isTable) { + positionResult.tdValueIndex = newIndex + } else { + positionResult.index = newIndex + } + } + const { + index, + isCheckbox, + isRadio, + isControl, + isImage, + isDirectHit, + isTable, + trIndex, + tdIndex, + tdId, + trId, + tableId + } = positionResult + // 设置位置上下文 + this.setPositionContext({ + isTable: isTable || false, + isCheckbox: isCheckbox || false, + isRadio: isRadio || false, + isControl: isControl || false, + isImage: isImage || false, + isDirectHit: isDirectHit || false, + index, + trIndex, + tdIndex, + tdId, + trId, + tableId + }) + return positionResult + } + + public setSurroundPosition(payload: ISetSurroundPositionPayload) { + const { scale } = this.options + const { + pageNo, + row, + rowElement, + rowElementRect, + surroundElementList, + availableWidth + } = payload + let x = rowElementRect.x + let rowIncreaseWidth = 0 + if ( + surroundElementList.length && + !getIsBlockElement(rowElement) && + !rowElement.control?.minWidth + ) { + for (let s = 0; s < surroundElementList.length; s++) { + const surroundElement = surroundElementList[s] + const floatPosition = surroundElement.imgFloatPosition! + if (floatPosition.pageNo !== pageNo) continue + const surroundRect = { + ...floatPosition, + x: floatPosition.x * scale, + y: floatPosition.y * scale, + width: surroundElement.width! * scale, + height: surroundElement.height! * scale + } + if (isRectIntersect(rowElementRect, surroundRect)) { + row.isSurround = true + // 需向左移动距离:浮动元素宽度 + 浮动元素左上坐标 - 元素左上坐标 + const translateX = + surroundRect.width + surroundRect.x - rowElementRect.x + rowElement.left = translateX + // 增加行宽 + row.width += translateX + rowIncreaseWidth += translateX + // 下个元素起始位置:浮动元素右坐标 - 元素宽度 + x = surroundRect.x + surroundRect.width + // 检测宽度是否足够,不够则移动到下一行,并还原状态 + if (row.width + rowElement.metrics.width > availableWidth) { + rowElement.left = 0 + row.width -= rowIncreaseWidth + break + } + } + } + } + return { x, rowIncreaseWidth } + } +} diff --git a/src/editor/core/range/RangeManager.ts b/src/editor/core/range/RangeManager.ts new file mode 100644 index 0000000..d050b7d --- /dev/null +++ b/src/editor/core/range/RangeManager.ts @@ -0,0 +1,720 @@ +import { ElementType } from '../..' +import { ZERO } from '../../dataset/constant/Common' +import { TEXTLIKE_ELEMENT_TYPE } from '../../dataset/constant/Element' +import { ControlComponent } from '../../dataset/enum/Control' +import { EditorContext } from '../../dataset/enum/Editor' +import { IControlContext } from '../../interface/Control' +import { IEditorOption } from '../../interface/Editor' +import { IElement } from '../../interface/Element' +import { EventBusMap } from '../../interface/EventBus' +import { IRangeStyle } from '../../interface/Listener' +import { + IRange, + IRangeElementStyle, + IRangeParagraphInfo, + RangeRowArray, + RangeRowMap +} from '../../interface/Range' +import { getAnchorElement } from '../../utils/element' +import { Draw } from '../draw/Draw' +import { EventBus } from '../event/eventbus/EventBus' +import { HistoryManager } from '../history/HistoryManager' +import { Listener } from '../listener/Listener' +import { Position } from '../position/Position' + +export class RangeManager { + private draw: Draw + private options: Required + private range: IRange + private listener: Listener + private eventBus: EventBus + private position: Position + private historyManager: HistoryManager + private defaultStyle: IRangeElementStyle | null + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + this.listener = draw.getListener() + this.eventBus = draw.getEventBus() + this.position = draw.getPosition() + this.historyManager = draw.getHistoryManager() + this.range = { + startIndex: -1, + endIndex: -1 + } + this.defaultStyle = null + } + + public getRange(): IRange { + return this.range + } + + public clearRange() { + this.setRange(-1, -1) + } + + public setDefaultStyle(style: IRangeElementStyle | null) { + if (!style) { + this.defaultStyle = null + } else { + this.defaultStyle = { + ...this.defaultStyle, + ...style + } + } + } + + public getDefaultStyle(): IRangeElementStyle | null { + return this.defaultStyle + } + + public getRangeAnchorStyle( + elementList: IElement[], + anchorIndex: number + ): IElement | null { + const anchorElement = getAnchorElement(elementList, anchorIndex) + if (!anchorElement) return null + return { + ...anchorElement, + ...this.defaultStyle + } + } + + public getIsRangeChange( + startIndex: number, + endIndex: number, + tableId?: string, + startTdIndex?: number, + endTdIndex?: number, + startTrIndex?: number, + endTrIndex?: number + ): boolean { + return ( + this.range.startIndex !== startIndex || + this.range.endIndex !== endIndex || + this.range.tableId !== tableId || + this.range.startTdIndex !== startTdIndex || + this.range.endTdIndex !== endTdIndex || + this.range.startTrIndex !== startTrIndex || + this.range.endTrIndex !== endTrIndex + ) + } + + public getIsCollapsed(): boolean { + const { startIndex, endIndex } = this.range + return startIndex === endIndex + } + + public getIsSelection(): boolean { + const { startIndex, endIndex } = this.range + if (!~startIndex && !~endIndex) return false + return startIndex !== endIndex + } + + public getSelection(): IElement[] | null { + const { startIndex, endIndex } = this.range + if (startIndex === endIndex) return null + const elementList = this.draw.getElementList() + return elementList.slice(startIndex + 1, endIndex + 1) + } + + public getSelectionElementList(): IElement[] | null { + if (this.range.isCrossRowCol) { + const rowCol = this.draw.getTableParticle().getRangeRowCol() + if (!rowCol) return null + const elementList: IElement[] = [] + for (let r = 0; r < rowCol.length; r++) { + const row = rowCol[r] + for (let c = 0; c < row.length; c++) { + const col = row[c] + elementList.push(...col.value) + } + } + return elementList + } + return this.getSelection() + } + + public getTextLikeSelection(): IElement[] | null { + const selection = this.getSelection() + if (!selection) return null + return selection.filter( + s => !s.type || TEXTLIKE_ELEMENT_TYPE.includes(s.type) + ) + } + + public getTextLikeSelectionElementList(): IElement[] | null { + const selection = this.getSelectionElementList() + if (!selection) return null + return selection.filter( + s => !s.type || TEXTLIKE_ELEMENT_TYPE.includes(s.type) + ) + } + + // 获取光标所选位置行信息 + public getRangeRow(): RangeRowMap | null { + const { startIndex, endIndex } = this.range + if (!~startIndex && !~endIndex) return null + const positionList = this.position.getPositionList() + const rangeRow: RangeRowMap = new Map() + for (let p = startIndex; p < endIndex + 1; p++) { + const { pageNo, rowNo } = positionList[p] + const rowSet = rangeRow.get(pageNo) + if (!rowSet) { + rangeRow.set(pageNo, new Set([rowNo])) + } else { + if (!rowSet.has(rowNo)) { + rowSet.add(rowNo) + } + } + } + return rangeRow + } + + // 获取光标所选位置元素列表 + public getRangeRowElementList(): IElement[] | null { + const { startIndex, endIndex, isCrossRowCol } = this.range + if (!~startIndex && !~endIndex) return null + if (isCrossRowCol) { + return this.getSelectionElementList() + } + // 选区行信息 + const rangeRow = this.getRangeRow() + if (!rangeRow) return null + const positionList = this.position.getPositionList() + const elementList = this.draw.getElementList() + // 当前选区所在行 + const rowElementList: IElement[] = [] + for (let p = 0; p < positionList.length; p++) { + const position = positionList[p] + const rowSet = rangeRow.get(position.pageNo) + if (!rowSet) continue + if (rowSet.has(position.rowNo)) { + rowElementList.push(elementList[p]) + } + } + return rowElementList + } + + // 获取选取段落信息 + public getRangeParagraph(): RangeRowArray | null { + const { startIndex, endIndex } = this.range + if (!~startIndex && !~endIndex) return null + const positionList = this.position.getPositionList() + const elementList = this.draw.getElementList() + const rangeRow: RangeRowArray = new Map() + // 向上查找 + let start = startIndex + while (start >= 0) { + const { pageNo, rowNo } = positionList[start] + let rowArray = rangeRow.get(pageNo) + if (!rowArray) { + rowArray = [] + rangeRow.set(pageNo, rowArray) + } + if (!rowArray.includes(rowNo)) { + rowArray.unshift(rowNo) + } + const element = elementList[start] + const preElement = elementList[start - 1] + if ( + (element.value === ZERO && !element.listWrap) || + element.listId !== preElement?.listId || + element.titleId !== preElement?.titleId + ) { + break + } + start-- + } + const isCollapsed = startIndex === endIndex + // 中间选择 + if (!isCollapsed) { + let middle = startIndex + 1 + while (middle < endIndex) { + const { pageNo, rowNo } = positionList[middle] + let rowArray = rangeRow.get(pageNo) + if (!rowArray) { + rowArray = [] + rangeRow.set(pageNo, rowArray) + } + if (!rowArray.includes(rowNo)) { + rowArray.push(rowNo) + } + middle++ + } + } + // 向下查找 + let end = endIndex + // 闭合选区&&首字符为换行符时继续向下查找 + if (isCollapsed && elementList[startIndex].value === ZERO) { + end += 1 + } + while (end < positionList.length) { + const element = elementList[end] + const nextElement = elementList[end + 1] + if ( + (element.value === ZERO && !element.listWrap) || + element.listId !== nextElement?.listId || + element.titleId !== nextElement?.titleId + ) { + break + } + const { pageNo, rowNo } = positionList[end] + let rowArray = rangeRow.get(pageNo) + if (!rowArray) { + rowArray = [] + rangeRow.set(pageNo, rowArray) + } + if (!rowArray.includes(rowNo)) { + rowArray.push(rowNo) + } + end++ + } + return rangeRow + } + + // 获取选区段落信息 + public getRangeParagraphInfo(): IRangeParagraphInfo | null { + const { startIndex, endIndex } = this.range + if (!~startIndex && !~endIndex) return null + /// 起始元素位置 + let startPositionIndex = -1 + // 需要改变的元素列表 + const rangeElementList: IElement[] = [] + // 选区行信息 + const rangeRow = this.getRangeParagraph() + if (!rangeRow) return null + const elementList = this.draw.getElementList() + const positionList = this.position.getPositionList() + for (let p = 0; p < positionList.length; p++) { + const position = positionList[p] + const rowArray = rangeRow.get(position.pageNo) + if (!rowArray) continue + if (rowArray.includes(position.rowNo)) { + if (!~startPositionIndex) { + startPositionIndex = position.index + } + rangeElementList.push(elementList[p]) + } + } + if (!rangeElementList.length) return null + return { + elementList: rangeElementList, + startIndex: startPositionIndex + } + } + + // 获取选区段落元素列表 + public getRangeParagraphElementList(): IElement[] | null { + return this.getRangeParagraphInfo()?.elementList || null + } + + // 获取选区表格 + public getRangeTableElement(): IElement | null { + const positionContext = this.position.getPositionContext() + if (!positionContext.isTable) return null + const originalElementList = this.draw.getOriginalElementList() + return originalElementList[positionContext.index!] + } + + public getIsSelectAll() { + const elementList = this.draw.getElementList() + const { startIndex, endIndex } = this.range + return ( + startIndex === 0 && + elementList.length - 1 === endIndex && + !this.position.getPositionContext().isTable + ) + } + + public getIsPointInRange(x: number, y: number): boolean { + const { startIndex, endIndex } = this.range + const positionList = this.position.getPositionList() + for (let p = startIndex + 1; p <= endIndex; p++) { + const position = positionList[p] + if (!position) break + const { + coordinate: { leftTop, rightBottom } + } = positionList[p] + if ( + x >= leftTop[0] && + x <= rightBottom[0] && + y >= leftTop[1] && + y <= rightBottom[1] + ) { + return true + } + } + return false + } + + public getKeywordRangeList(payload: string): IRange[] { + const searchMatchList = this.draw + .getSearch() + .getMatchList(payload, this.draw.getOriginalElementList()) + const searchRangeMap: Map = new Map() + for (const searchMatch of searchMatchList) { + const searchRange = searchRangeMap.get(searchMatch.groupId) + if (searchRange) { + searchRange.endIndex += 1 + } else { + const { type, groupId, tableId, index, tdIndex, trIndex } = searchMatch + const range: IRange = { + startIndex: index, + endIndex: index + } + if (type === EditorContext.TABLE) { + range.tableId = tableId + range.startTdIndex = tdIndex + range.endTdIndex = tdIndex + range.startTrIndex = trIndex + range.endTrIndex = trIndex + } + searchRangeMap.set(groupId, range) + } + } + const rangeList: IRange[] = [] + searchRangeMap.forEach(searchRange => { + rangeList.push(searchRange) + }) + return rangeList + } + + public getIsCanInput(): boolean { + const { startIndex, endIndex } = this.getRange() + if (!~startIndex && !~endIndex) return false + const elementList = this.draw.getElementList() + const startElement = elementList[startIndex] + if (startIndex === endIndex) { + return ( + (startElement.controlComponent !== ControlComponent.PRE_TEXT || + elementList[startIndex + 1]?.controlComponent !== + ControlComponent.PRE_TEXT) && + startElement.controlComponent !== ControlComponent.POST_TEXT + ) + } + const endElement = elementList[endIndex] + // 选区前后不是控件 || 选区前不是控件或是后缀&&选区后不是控件或是后缀 || 选区在控件内 + return ( + (!startElement.controlId && !endElement.controlId) || + ((!startElement.controlId || + startElement.controlComponent === ControlComponent.POSTFIX) && + (!endElement.controlId || + endElement.controlComponent === ControlComponent.POSTFIX)) || + (!!startElement.controlId && + endElement.controlId === startElement.controlId && + endElement.controlComponent !== ControlComponent.PRE_TEXT && + endElement.controlComponent !== ControlComponent.POST_TEXT && + endElement.controlComponent !== ControlComponent.POSTFIX) + ) + } + + public setRange( + startIndex: number, + endIndex: number, + tableId?: string, + startTdIndex?: number, + endTdIndex?: number, + startTrIndex?: number, + endTrIndex?: number + ) { + // 判断光标是否改变 + const isChange = this.getIsRangeChange( + startIndex, + endIndex, + tableId, + startTdIndex, + endTdIndex, + startTrIndex, + endTrIndex + ) + if (isChange) { + this.range.startIndex = startIndex + this.range.endIndex = endIndex + this.range.tableId = tableId + this.range.startTdIndex = startTdIndex + this.range.endTdIndex = endTdIndex + this.range.startTrIndex = startTrIndex + this.range.endTrIndex = endTrIndex + this.range.isCrossRowCol = !!( + startTdIndex || + endTdIndex || + startTrIndex || + endTrIndex + ) + this.setDefaultStyle(null) + } + this.range.zone = this.draw.getZone().getZone() + // 激活控件 + const control = this.draw.getControl() + if (~startIndex && ~endIndex) { + const elementList = this.draw.getElementList() + const element = elementList[startIndex] + if (element?.controlId) { + control.initControl() + return + } + } + control.destroyControl() + } + + public replaceRange(range: IRange) { + this.setRange( + range.startIndex, + range.endIndex, + range.tableId, + range.startTdIndex, + range.endTdIndex, + range.startTrIndex, + range.endTrIndex + ) + } + + public shrinkRange() { + const { startIndex, endIndex } = this.range + if (startIndex === endIndex || (!~startIndex && !~endIndex)) return + this.replaceRange({ + ...this.range, + startIndex: endIndex + }) + } + + public setRangeStyle() { + const rangeStyleChangeListener = this.listener.rangeStyleChange + const isSubscribeRangeStyleChange = + this.eventBus.isSubscribe('rangeStyleChange') + if (!rangeStyleChangeListener && !isSubscribeRangeStyleChange) return + // 结束光标位置 + const { startIndex, endIndex, isCrossRowCol } = this.range + if (!~startIndex && !~endIndex) return + let curElement: IElement | null + if (isCrossRowCol) { + // 单元格选择以当前表格定位 + const originalElementList = this.draw.getOriginalElementList() + const positionContext = this.position.getPositionContext() + curElement = originalElementList[positionContext.index!] + } else { + const index = ~endIndex ? endIndex : 0 + // 行首以第一个非换行符元素定位 + const elementList = this.draw.getElementList() + curElement = this.getRangeAnchorStyle(elementList, index) + } + if (!curElement) return + // 选取元素列表 + const curElementList = this.getSelection() || [curElement] + // 类型 + const type = curElement.type || ElementType.TEXT + // 富文本 + const font = curElement.font || this.options.defaultFont + const size = curElement.size || this.options.defaultSize + const bold = !~curElementList.findIndex(el => !el.bold) + const italic = !~curElementList.findIndex(el => !el.italic) + const underline = !~curElementList.findIndex( + el => !el.underline && !el.control?.underline + ) + const strikeout = !~curElementList.findIndex(el => !el.strikeout) + const color = curElement.color || null + const highlight = curElement.highlight || null + const rowFlex = curElement.rowFlex || null + const rowMargin = curElement.rowMargin ?? this.options.defaultRowMargin + const dashArray = curElement.dashArray || [] + const level = curElement.level || null + const listType = curElement.listType || null + const listStyle = curElement.listStyle || null + const textDecoration = underline ? curElement.textDecoration || null : null + // 菜单 + const painter = !!this.draw.getPainterStyle() + const undo = this.historyManager.isCanUndo() + const redo = this.historyManager.isCanRedo() + // 组信息 + const groupIds = curElement.groupIds || null + // 扩展字段 + const extension = curElement.extension ?? null + const rangeStyle: IRangeStyle = { + type, + undo, + redo, + painter, + font, + size, + bold, + italic, + underline, + strikeout, + color, + highlight, + rowFlex, + rowMargin, + dashArray, + level, + listType, + listStyle, + groupIds, + textDecoration, + extension + } + if (rangeStyleChangeListener) { + rangeStyleChangeListener(rangeStyle) + } + if (isSubscribeRangeStyleChange) { + this.eventBus.emit('rangeStyleChange', rangeStyle) + } + } + + public recoveryRangeStyle() { + const rangeStyleChangeListener = this.listener.rangeStyleChange + const isSubscribeRangeStyleChange = + this.eventBus.isSubscribe('rangeStyleChange') + if (!rangeStyleChangeListener && !isSubscribeRangeStyleChange) return + const font = this.options.defaultFont + const size = this.options.defaultSize + const rowMargin = this.options.defaultRowMargin + const painter = !!this.draw.getPainterStyle() + const undo = this.historyManager.isCanUndo() + const redo = this.historyManager.isCanRedo() + const rangeStyle: IRangeStyle = { + type: null, + undo, + redo, + painter, + font, + size, + bold: false, + italic: false, + underline: false, + strikeout: false, + color: null, + highlight: null, + rowFlex: null, + rowMargin, + dashArray: [], + level: null, + listType: null, + listStyle: null, + groupIds: null, + textDecoration: null, + extension: null + } + if (rangeStyleChangeListener) { + rangeStyleChangeListener(rangeStyle) + } + if (isSubscribeRangeStyleChange) { + this.eventBus.emit('rangeStyleChange', rangeStyle) + } + } + + public shrinkBoundary(context: IControlContext = {}) { + const elementList = context.elementList || this.draw.getElementList() + const range = context.range || this.getRange() + const { startIndex, endIndex } = range + if (!~startIndex && !~endIndex) return + const startElement = elementList[startIndex] + const endElement = elementList[endIndex] + if (startIndex === endIndex) { + if (startElement.controlComponent === ControlComponent.PLACEHOLDER) { + // 找到第一个placeholder字符 + let index = startIndex - 1 + while (index > 0) { + const preElement = elementList[index] + if ( + preElement.controlId !== startElement.controlId || + preElement.controlComponent === ControlComponent.PREFIX || + preElement.controlComponent === ControlComponent.PRE_TEXT + ) { + range.startIndex = index + range.endIndex = index + break + } + index-- + } + } + } else { + // 首、尾为占位符时,收缩到最后一个前缀字符后 + if ( + startElement.controlComponent === ControlComponent.PLACEHOLDER || + endElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + let index = endIndex - 1 + while (index > 0) { + const preElement = elementList[index] + if ( + preElement.controlId !== endElement.controlId || + preElement.controlComponent === ControlComponent.PREFIX || + preElement.controlComponent === ControlComponent.PRE_TEXT + ) { + range.startIndex = index + range.endIndex = index + return + } + index-- + } + } + // 向右查找到第一个Value + if (startElement.controlComponent === ControlComponent.PREFIX) { + let index = startIndex + 1 + while (index < elementList.length) { + const nextElement = elementList[index] + if ( + nextElement.controlId !== startElement.controlId || + nextElement.controlComponent === ControlComponent.VALUE + ) { + range.startIndex = index - 1 + break + } else if ( + nextElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + range.startIndex = index - 1 + range.endIndex = index - 1 + return + } + index++ + } + } + // 向左查找到第一个Value + if (endElement.controlComponent !== ControlComponent.VALUE) { + let index = startIndex - 1 + while (index > 0) { + const preElement = elementList[index] + if ( + preElement.controlId !== startElement.controlId || + preElement.controlComponent === ControlComponent.VALUE + ) { + range.startIndex = index + break + } else if ( + preElement.controlComponent === ControlComponent.PLACEHOLDER + ) { + range.startIndex = index + range.endIndex = index + return + } + index-- + } + } + } + } + + public render( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + width: number, + height: number + ) { + ctx.save() + ctx.globalAlpha = this.options.rangeAlpha + ctx.fillStyle = this.options.rangeColor + ctx.fillRect(x, y, width, height) + ctx.restore() + } + + public toString(): string { + const selection = this.getTextLikeSelection() + if (!selection) return '' + return selection + .map(s => s.value) + .join('') + .replace(new RegExp(ZERO, 'g'), '') + } +} diff --git a/src/editor/core/register/Register.ts b/src/editor/core/register/Register.ts new file mode 100644 index 0000000..4401d52 --- /dev/null +++ b/src/editor/core/register/Register.ts @@ -0,0 +1,28 @@ +import { IRegisterContextMenu } from '../../interface/contextmenu/ContextMenu' +import { IRegisterShortcut } from '../../interface/shortcut/Shortcut' +import { ContextMenu } from '../contextmenu/ContextMenu' +import { Shortcut } from '../shortcut/Shortcut' +import { I18n } from '../i18n/I18n' +import { ILang } from '../../interface/i18n/I18n' +import { DeepPartial } from '../../interface/Common' + +interface IRegisterPayload { + contextMenu: ContextMenu + shortcut: Shortcut + i18n: I18n +} + +export class Register { + public contextMenuList: (payload: IRegisterContextMenu[]) => void + public getContextMenuList: () => IRegisterContextMenu[] + public shortcutList: (payload: IRegisterShortcut[]) => void + public langMap: (locale: string, lang: DeepPartial) => void + + constructor(payload: IRegisterPayload) { + const { contextMenu, shortcut, i18n } = payload + this.contextMenuList = contextMenu.registerContextMenuList.bind(contextMenu) + this.getContextMenuList = contextMenu.getContextMenuList.bind(contextMenu) + this.shortcutList = shortcut.registerShortcutList.bind(shortcut) + this.langMap = i18n.registerLangMap.bind(i18n) + } +} diff --git a/src/editor/core/shortcut/Shortcut.ts b/src/editor/core/shortcut/Shortcut.ts new file mode 100644 index 0000000..66aa503 --- /dev/null +++ b/src/editor/core/shortcut/Shortcut.ts @@ -0,0 +1,80 @@ +import { IRegisterShortcut } from '../../interface/shortcut/Shortcut' +import { richtextKeys } from './keys/richtextKeys' +import { Command } from '../command/Command' +import { Draw } from '../draw/Draw' +import { isMod } from '../../utils/hotkey' +import { titleKeys } from './keys/titleKeys' +import { listKeys } from './keys/listKeys' + +export class Shortcut { + private command: Command + private globalShortcutList: IRegisterShortcut[] + private agentShortcutList: IRegisterShortcut[] + + constructor(draw: Draw, command: Command) { + this.command = command + this.globalShortcutList = [] + this.agentShortcutList = [] + // 内部快捷键 + this._addShortcutList([...richtextKeys, ...titleKeys, ...listKeys]) + // 全局快捷键 + this._addEvent() + // 编辑器快捷键 + const agentDom = draw.getCursor().getAgentDom() + agentDom.addEventListener('keydown', this._agentKeydown.bind(this)) + } + + private _addEvent() { + document.addEventListener('keydown', this._globalKeydown) + } + + public removeEvent() { + document.removeEventListener('keydown', this._globalKeydown) + } + + private _addShortcutList(payload: IRegisterShortcut[]) { + for (let s = payload.length - 1; s >= 0; s--) { + const shortCut = payload[s] + if (shortCut.isGlobal) { + this.globalShortcutList.unshift(shortCut) + } else { + this.agentShortcutList.unshift(shortCut) + } + } + } + + public registerShortcutList(payload: IRegisterShortcut[]) { + this._addShortcutList(payload) + } + + private _globalKeydown = (evt: KeyboardEvent) => { + if (!this.globalShortcutList.length) return + this._execute(evt, this.globalShortcutList) + } + + private _agentKeydown(evt: KeyboardEvent) { + if (!this.agentShortcutList.length) return + this._execute(evt, this.agentShortcutList) + } + + private _execute(evt: KeyboardEvent, shortCutList: IRegisterShortcut[]) { + for (let s = 0; s < shortCutList.length; s++) { + const shortCut = shortCutList[s] + if ( + (shortCut.mod + ? isMod(evt) === !!shortCut.mod + : evt.ctrlKey === !!shortCut.ctrl && + evt.metaKey === !!shortCut.meta) && + evt.shiftKey === !!shortCut.shift && + evt.altKey === !!shortCut.alt && + evt.key.toLowerCase() === shortCut.key.toLowerCase() + ) { + if (!shortCut.disable) { + shortCut?.callback?.(this.command) + evt.preventDefault() + } + break + } + } + } +} diff --git a/src/editor/core/shortcut/keys/listKeys.ts b/src/editor/core/shortcut/keys/listKeys.ts new file mode 100644 index 0000000..a56e6d7 --- /dev/null +++ b/src/editor/core/shortcut/keys/listKeys.ts @@ -0,0 +1,22 @@ +import { Command, ListStyle, ListType } from '../../..' +import { KeyMap } from '../../../dataset/enum/KeyMap' +import { IRegisterShortcut } from '../../../interface/shortcut/Shortcut' + +export const listKeys: IRegisterShortcut[] = [ + { + key: KeyMap.I, + shift: true, + mod: true, + callback: (command: Command) => { + command.executeList(ListType.UL, ListStyle.DISC) + } + }, + { + key: KeyMap.U, + shift: true, + mod: true, + callback: (command: Command) => { + command.executeList(ListType.OL) + } + } +] diff --git a/src/editor/core/shortcut/keys/richtextKeys.ts b/src/editor/core/shortcut/keys/richtextKeys.ts new file mode 100644 index 0000000..d4c3306 --- /dev/null +++ b/src/editor/core/shortcut/keys/richtextKeys.ts @@ -0,0 +1,102 @@ +import { Command, RowFlex } from '../../..' +import { KeyMap } from '../../../dataset/enum/KeyMap' +import { IRegisterShortcut } from '../../../interface/shortcut/Shortcut' +import { isApple } from '../../../utils/ua' + +export const richtextKeys: IRegisterShortcut[] = [ + { + key: KeyMap.X, + ctrl: true, + shift: true, + callback: (command: Command) => { + command.executeStrikeout() + } + }, + { + key: KeyMap.LEFT_BRACKET, + mod: true, + callback: (command: Command) => { + command.executeSizeAdd() + } + }, + { + key: KeyMap.RIGHT_BRACKET, + mod: true, + callback: (command: Command) => { + command.executeSizeMinus() + } + }, + { + key: KeyMap.B, + mod: true, + callback: (command: Command) => { + command.executeBold() + } + }, + { + key: KeyMap.I, + mod: true, + callback: (command: Command) => { + command.executeItalic() + } + }, + { + key: KeyMap.U, + mod: true, + callback: (command: Command) => { + command.executeUnderline() + } + }, + { + key: isApple ? KeyMap.COMMA : KeyMap.RIGHT_ANGLE_BRACKET, + mod: true, + shift: true, + callback: (command: Command) => { + command.executeSuperscript() + } + }, + { + key: isApple ? KeyMap.PERIOD : KeyMap.LEFT_ANGLE_BRACKET, + mod: true, + shift: true, + callback: (command: Command) => { + command.executeSubscript() + } + }, + { + key: KeyMap.L, + mod: true, + callback: (command: Command) => { + command.executeRowFlex(RowFlex.LEFT) + } + }, + { + key: KeyMap.E, + mod: true, + callback: (command: Command) => { + command.executeRowFlex(RowFlex.CENTER) + } + }, + { + key: KeyMap.R, + mod: true, + callback: (command: Command) => { + command.executeRowFlex(RowFlex.RIGHT) + } + }, + { + key: KeyMap.J, + mod: true, + callback: (command: Command) => { + command.executeRowFlex(RowFlex.ALIGNMENT) + } + }, + { + key: KeyMap.J, + mod: true, + shift: true, + callback: (command: Command) => { + command.executeRowFlex(RowFlex.JUSTIFY) + } + } +] diff --git a/src/editor/core/shortcut/keys/titleKeys.ts b/src/editor/core/shortcut/keys/titleKeys.ts new file mode 100644 index 0000000..783c2e0 --- /dev/null +++ b/src/editor/core/shortcut/keys/titleKeys.ts @@ -0,0 +1,62 @@ +import { Command, TitleLevel } from '../../..' +import { KeyMap } from '../../../dataset/enum/KeyMap' +import { IRegisterShortcut } from '../../../interface/shortcut/Shortcut' + +export const titleKeys: IRegisterShortcut[] = [ + { + key: KeyMap.ZERO, + alt: true, + ctrl: true, + callback: (command: Command) => { + command.executeTitle(null) + } + }, + { + key: KeyMap.ONE, + alt: true, + ctrl: true, + callback: (command: Command) => { + command.executeTitle(TitleLevel.FIRST) + } + }, + { + key: KeyMap.TWO, + alt: true, + ctrl: true, + callback: (command: Command) => { + command.executeTitle(TitleLevel.SECOND) + } + }, + { + key: KeyMap.THREE, + alt: true, + ctrl: true, + callback: (command: Command) => { + command.executeTitle(TitleLevel.THIRD) + } + }, + { + key: KeyMap.FOUR, + alt: true, + ctrl: true, + callback: (command: Command) => { + command.executeTitle(TitleLevel.FOURTH) + } + }, + { + key: KeyMap.FIVE, + alt: true, + ctrl: true, + callback: (command: Command) => { + command.executeTitle(TitleLevel.FIFTH) + } + }, + { + key: KeyMap.SIX, + alt: true, + ctrl: true, + callback: (command: Command) => { + command.executeTitle(TitleLevel.SIXTH) + } + } +] diff --git a/src/editor/core/worker/WorkerManager.ts b/src/editor/core/worker/WorkerManager.ts new file mode 100644 index 0000000..09a19d6 --- /dev/null +++ b/src/editor/core/worker/WorkerManager.ts @@ -0,0 +1,96 @@ +import { version } from '../../../../package.json' +import { Draw } from '../draw/Draw' +import WordCountWorker from './works/wordCount?worker&inline' +import CatalogWorker from './works/catalog?worker&inline' +import GroupWorker from './works/group?worker&inline' +import ValueWorker from './works/value?worker&inline' +import { ICatalog } from '../../interface/Catalog' +import { IEditorResult } from '../../interface/Editor' +import { IGetValueOption } from '../../interface/Draw' +import { deepClone } from '../../utils' + +export class WorkerManager { + private draw: Draw + private wordCountWorker: Worker + private catalogWorker: Worker + private groupWorker: Worker + private valueWorker: Worker + + constructor(draw: Draw) { + this.draw = draw + this.wordCountWorker = new WordCountWorker() + this.catalogWorker = new CatalogWorker() + this.groupWorker = new GroupWorker() + this.valueWorker = new ValueWorker() + } + + public getWordCount(): Promise { + return new Promise((resolve, reject) => { + this.wordCountWorker.onmessage = evt => { + resolve(evt.data) + } + + this.wordCountWorker.onerror = evt => { + reject(evt) + } + + const elementList = this.draw.getOriginalMainElementList() + this.wordCountWorker.postMessage(elementList) + }) + } + + public getCatalog(): Promise { + return new Promise((resolve, reject) => { + this.catalogWorker.onmessage = evt => { + resolve(evt.data) + } + + this.catalogWorker.onerror = evt => { + reject(evt) + } + + const elementList = this.draw.getOriginalMainElementList() + const positionList = this.draw.getPosition().getOriginalMainPositionList() + this.catalogWorker.postMessage({ + elementList, + positionList + }) + }) + } + + public getGroupIds(): Promise { + return new Promise((resolve, reject) => { + this.groupWorker.onmessage = evt => { + resolve(evt.data) + } + + this.groupWorker.onerror = evt => { + reject(evt) + } + + const elementList = this.draw.getOriginalMainElementList() + this.groupWorker.postMessage(elementList) + }) + } + + public getValue(options?: IGetValueOption): Promise { + return new Promise((resolve, reject) => { + this.valueWorker.onmessage = evt => { + resolve({ + version, + data: evt.data, + options: deepClone(this.draw.getOptions()) + }) + } + + this.valueWorker.onerror = evt => { + reject(evt) + } + + this.valueWorker.postMessage({ + data: this.draw.getOriginValue(options), + options + }) + }) + } +} diff --git a/src/editor/core/worker/works/catalog.ts b/src/editor/core/worker/works/catalog.ts new file mode 100644 index 0000000..cc7e76f --- /dev/null +++ b/src/editor/core/worker/works/catalog.ts @@ -0,0 +1,187 @@ +import { ICatalog, ICatalogItem } from '../../../interface/Catalog' +import { IElement, IElementPosition } from '../../../interface/Element' + +interface IGetCatalogPayload { + elementList: IElement[] + positionList: IElementPosition[] +} + +type ICatalogElement = IElement & { + pageNo: number +} + +enum ElementType { + TEXT = 'text', + IMAGE = 'image', + TABLE = 'table', + HYPERLINK = 'hyperlink', + SUPERSCRIPT = 'superscript', + SUBSCRIPT = 'subscript', + SEPARATOR = 'separator', + PAGE_BREAK = 'pageBreak', + CONTROL = 'control', + CHECKBOX = 'checkbox', + RADIO = 'radio', + LATEX = 'latex', + TAB = 'tab', + DATE = 'date', + BLOCK = 'block', + TITLE = 'title', + AREA = 'area', + LIST = 'list' +} + +enum TitleLevel { + FIRST = 'first', + SECOND = 'second', + THIRD = 'third', + FOURTH = 'fourth', + FIFTH = 'fifth', + SIXTH = 'sixth' +} + +const titleOrderNumberMapping: Record = { + [TitleLevel.FIRST]: 1, + [TitleLevel.SECOND]: 2, + [TitleLevel.THIRD]: 3, + [TitleLevel.FOURTH]: 4, + [TitleLevel.FIFTH]: 5, + [TitleLevel.SIXTH]: 6 +} + +const TEXTLIKE_ELEMENT_TYPE: ElementType[] = [ + ElementType.TEXT, + ElementType.HYPERLINK, + ElementType.SUBSCRIPT, + ElementType.SUPERSCRIPT, + ElementType.CONTROL, + ElementType.DATE +] + +const ZERO = '\u200B' + +function isTextLikeElement(element: IElement): boolean { + return !element.type || TEXTLIKE_ELEMENT_TYPE.includes(element.type) +} + +function getCatalog(payload: IGetCatalogPayload): ICatalog | null { + const { elementList, positionList } = payload + // 筛选标题 + const titleElementList: ICatalogElement[] = [] + let t = 0 + while (t < elementList.length) { + const element = elementList[t] + const getElementInfo = ( + element: IElement, + elementList: IElement[], + position: number + ) => { + const titleId = element.titleId + const level = element.level + const titleElement: ICatalogElement = { + type: ElementType.TITLE, + value: '', + level, + titleId, + pageNo: positionList[t].pageNo + } + const valueList: IElement[] = [] + while (position < elementList.length) { + const titleE = elementList[position] + if (titleId !== titleE.titleId) { + position-- + break + } + valueList.push(titleE) + position++ + } + titleElement.value = valueList + .filter(el => isTextLikeElement(el)) + .map(el => el.value) + .join('') + .replace(new RegExp(ZERO, 'g'), '') + return { position, titleElement } + } + if (element.titleId) { + const { position, titleElement } = getElementInfo(element, elementList, t) + t = position + titleElementList.push(titleElement) + } + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const value = td.value + if (value.length > 1) { + let index = 1 + while (index < value.length) { + if (value[index]?.titleId) { + const { titleElement, position } = getElementInfo( + value[index], + value, + index + ) + titleElementList.push(titleElement) + index = position + } + index++ + } + } + } + } + } + t++ + } + if (!titleElementList.length) return null + // 查找到比最新元素大的标题时终止 + const recursiveInsert = ( + title: ICatalogElement, + catalogItem: ICatalogItem + ) => { + const subCatalogItem = + catalogItem.subCatalog[catalogItem.subCatalog.length - 1] + const catalogItemLevel = titleOrderNumberMapping[subCatalogItem?.level] + const titleLevel = titleOrderNumberMapping[title.level!] + if (subCatalogItem && titleLevel > catalogItemLevel) { + recursiveInsert(title, subCatalogItem) + } else { + catalogItem.subCatalog.push({ + id: title.titleId!, + name: title.value, + level: title.level!, + pageNo: title.pageNo, + subCatalog: [] + }) + } + } + // 循环标题组 + // 如果当前列表级别小于标题组最新标题级别:则递归查找最小级别并追加 + // 如果大于:则直接追加至当前标题组 + const catalog: ICatalog = [] + for (let e = 0; e < titleElementList.length; e++) { + const title = titleElementList[e] + const catalogItem = catalog[catalog.length - 1] + const catalogItemLevel = titleOrderNumberMapping[catalogItem?.level] + const titleLevel = titleOrderNumberMapping[title.level!] + if (catalogItem && titleLevel > catalogItemLevel) { + recursiveInsert(title, catalogItem) + } else { + catalog.push({ + id: title.titleId!, + name: title.value, + level: title.level!, + pageNo: title.pageNo, + subCatalog: [] + }) + } + } + return catalog +} + +onmessage = evt => { + const payload = evt.data + const catalog = getCatalog(payload) + postMessage(catalog) +} diff --git a/src/editor/core/worker/works/group.ts b/src/editor/core/worker/works/group.ts new file mode 100644 index 0000000..67ae7af --- /dev/null +++ b/src/editor/core/worker/works/group.ts @@ -0,0 +1,34 @@ +import { IElement } from '../../../interface/Element' + +enum ElementType { + TABLE = 'table' +} + +function getGroupIds(elementList: IElement[]): string[] { + const groupIds: string[] = [] + for (const element of elementList) { + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + groupIds.push(...getGroupIds(td.value)) + } + } + } + if (!element.groupIds) continue + for (const groupId of element.groupIds) { + if (!groupIds.includes(groupId)) { + groupIds.push(groupId) + } + } + } + return groupIds +} + +onmessage = evt => { + const elementList = evt.data + const groupIds = getGroupIds(elementList) + postMessage(groupIds) +} diff --git a/src/editor/core/worker/works/value.ts b/src/editor/core/worker/works/value.ts new file mode 100644 index 0000000..33c5c11 --- /dev/null +++ b/src/editor/core/worker/works/value.ts @@ -0,0 +1,32 @@ +import { IGetValueOption } from '../../../interface/Draw' +import { IEditorData } from '../../../interface/Editor' +import { zipElementList } from '../../../utils/element' + +interface IGetValueWorkerOption { + data: Required + options: IGetValueOption +} + +onmessage = evt => { + const payload = evt.data + const { options, data } = payload + const { extraPickAttrs = [] } = options || {} + + const editorData: IEditorData = { + header: zipElementList(data.header, { + extraPickAttrs, + isClone: false + }), + main: zipElementList(data.main, { + extraPickAttrs, + isClassifyArea: true, + isClone: false + }), + footer: zipElementList(data.footer, { + extraPickAttrs, + isClone: false + }) + } + + postMessage(editorData) +} diff --git a/src/editor/core/worker/works/wordCount.ts b/src/editor/core/worker/works/wordCount.ts new file mode 100644 index 0000000..1c69ee4 --- /dev/null +++ b/src/editor/core/worker/works/wordCount.ts @@ -0,0 +1,132 @@ +import { IElement } from '../../../interface/Element' + +enum ElementType { + TEXT = 'text', + TABLE = 'table', + HYPERLINK = 'hyperlink', + CONTROL = 'control' +} + +enum ControlComponent { + VALUE = 'value' +} + +const ZERO = '\u200B' +const WRAP = '\n' + +function pickText(elementList: IElement[]): string { + let text = '' + let e = 0 + while (e < elementList.length) { + const element = elementList[e] + // 表格、超链接递归处理 + if (element.type === ElementType.TABLE) { + if (element.trList) { + for (let t = 0; t < element.trList.length; t++) { + const tr = element.trList[t] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + text += pickText(td.value) + } + } + } + } else if (element.type === ElementType.HYPERLINK) { + const hyperlinkId = element.hyperlinkId + const valueList: IElement[] = [] + while (e < elementList.length) { + const hyperlinkE = elementList[e] + if (hyperlinkId !== hyperlinkE.hyperlinkId) { + e-- + break + } + delete hyperlinkE.type + valueList.push(hyperlinkE) + e++ + } + text += pickText(valueList) + } else if (element.controlId) { + if (!element.control?.hide) { + const controlId = element.controlId + const valueList: IElement[] = [] + while (e < elementList.length) { + const controlE = elementList[e] + if (controlId !== controlE.controlId) { + e-- + break + } + if (controlE.controlComponent === ControlComponent.VALUE) { + delete controlE.controlId + valueList.push(controlE) + } + e++ + } + text += pickText(valueList) + } + } else if ( + (!element.type || element.type === ElementType.TEXT) && + !element.area?.hide + ) { + text += element.value + } + e++ + } + return text +} + +function groupText(text: string): string[] { + const characterList: string[] = [] + // 英文或数字整体分隔为一个字数 + const numberReg = /[0-9]/ + const letterReg = /[A-Za-z]/ + const blankReg = /\s/ + // for of 循环字符 + let isPreLetter = false + let isPreNumber = false + let compositionText = '' + // 处理组合文本 + function pushCompositionText() { + if (compositionText) { + characterList.push(compositionText) + compositionText = '' + } + } + for (const t of text) { + if (letterReg.test(t)) { + if (!isPreLetter) { + pushCompositionText() + } + compositionText += t + isPreLetter = true + isPreNumber = false + } else if (numberReg.test(t)) { + if (!isPreNumber) { + pushCompositionText() + } + compositionText += t + isPreLetter = false + isPreNumber = true + } else { + pushCompositionText() + isPreLetter = false + isPreNumber = false + if (!blankReg.test(t)) { + characterList.push(t) + } + } + } + pushCompositionText() + return characterList +} + +onmessage = evt => { + const elementList = evt.data + // 提取文本 + const originText = pickText(elementList) + // 过滤文本 + const filterText = originText + .replace(new RegExp(`^${ZERO}`), '') + .replace(new RegExp(ZERO, 'g'), WRAP) + // 文本分组 + const textGroup = groupText(filterText) + postMessage(textGroup.length) +} diff --git a/src/editor/core/zone/Zone.ts b/src/editor/core/zone/Zone.ts new file mode 100644 index 0000000..f1ff5a3 --- /dev/null +++ b/src/editor/core/zone/Zone.ts @@ -0,0 +1,183 @@ +import { EDITOR_PREFIX } from '../../dataset/constant/Editor' +import { EditorZone } from '../../dataset/enum/Editor' +import { IEditorOption } from '../../interface/Editor' +import { nextTick } from '../../utils' +import { Draw } from '../draw/Draw' +import { I18n } from '../i18n/I18n' +import { ZoneTip } from './ZoneTip' + +export class Zone { + private readonly INDICATOR_PADDING = 2 + private readonly INDICATOR_TITLE_TRANSLATE = [20, 5] + + private draw: Draw + private options: Required + private i18n: I18n + private container: HTMLDivElement + + private currentZone: EditorZone + private indicatorContainer: HTMLDivElement | null + + constructor(draw: Draw) { + this.draw = draw + this.i18n = draw.getI18n() + this.options = draw.getOptions() + this.container = draw.getContainer() + this.currentZone = EditorZone.MAIN + this.indicatorContainer = null + // 区域提示 + if (!this.options.zone.tipDisabled) { + new ZoneTip(draw, this) + } + } + + public isHeaderActive(): boolean { + return this.getZone() === EditorZone.HEADER + } + + public isMainActive(): boolean { + return this.getZone() === EditorZone.MAIN + } + + public isFooterActive(): boolean { + return this.getZone() === EditorZone.FOOTER + } + + public getZone(): EditorZone { + return this.currentZone + } + + public setZone(payload: EditorZone) { + const { header, footer } = this.options + if ( + (!header.editable && payload === EditorZone.HEADER) || + (!footer.editable && payload === EditorZone.FOOTER) + ) { + return + } + if (this.currentZone === payload) return + this.currentZone = payload + this.draw.getRange().clearRange() + this.draw.render({ + isSubmitHistory: false, + isSetCursor: false, + isCompute: false + }) + // 指示器 + this.drawZoneIndicator() + // 回调 + nextTick(() => { + const listener = this.draw.getListener() + if (listener.zoneChange) { + listener.zoneChange(payload) + } + const eventBus = this.draw.getEventBus() + if (eventBus.isSubscribe('zoneChange')) { + eventBus.emit('zoneChange', payload) + } + }) + } + + public getZoneByY(y: number): EditorZone { + // 页眉底部距离页面顶部距离 + const header = this.draw.getHeader() + const headerBottomY = header.getHeaderTop() + header.getHeight() + // 页脚上部距离页面顶部距离 + const footer = this.draw.getFooter() + const pageHeight = this.draw.getHeight() + const footerTopY = + pageHeight - (footer.getFooterBottom() + footer.getHeight()) + // 页眉:当前位置小于页眉底部位置 + if (y < headerBottomY) { + return EditorZone.HEADER + } + // 页脚:当前位置大于页脚顶部位置 + if (y > footerTopY) { + return EditorZone.FOOTER + } + return EditorZone.MAIN + } + + public drawZoneIndicator() { + this._clearZoneIndicator() + if (!this.isHeaderActive() && !this.isFooterActive()) return + const { scale } = this.options + const isHeaderActive = this.isHeaderActive() + const [offsetX, offsetY] = this.INDICATOR_TITLE_TRANSLATE + const pageList = this.draw.getPageList() + const margins = this.draw.getMargins() + const innerWidth = this.draw.getInnerWidth() + const pageHeight = this.draw.getHeight() + const pageGap = this.draw.getPageGap() + const preY = pageHeight + pageGap + // 创建指示器容器 + this.indicatorContainer = document.createElement('div') + this.indicatorContainer.classList.add(`${EDITOR_PREFIX}-zone-indicator`) + // 指示器位置 + const header = this.draw.getHeader() + const footer = this.draw.getFooter() + const indicatorHeight = isHeaderActive + ? header.getHeight() + : footer.getHeight() + const indicatorTop = isHeaderActive + ? header.getHeaderTop() + : pageHeight - footer.getFooterBottom() - indicatorHeight + for (let p = 0; p < pageList.length; p++) { + const startY = preY * p + indicatorTop + const indicatorLeftX = margins[3] - this.INDICATOR_PADDING + const indicatorRightX = margins[3] + innerWidth + this.INDICATOR_PADDING + const indicatorTopY = isHeaderActive + ? startY - this.INDICATOR_PADDING + : startY + indicatorHeight + this.INDICATOR_PADDING + const indicatorBottomY = isHeaderActive + ? startY + indicatorHeight + this.INDICATOR_PADDING + : startY - this.INDICATOR_PADDING + // 标题 + const indicatorTitle = document.createElement('div') + indicatorTitle.innerText = this.i18n.t( + `frame.${isHeaderActive ? 'header' : 'footer'}` + ) + indicatorTitle.style.top = `${indicatorBottomY}px` + indicatorTitle.style.transform = `translate(${offsetX * scale}px, ${ + offsetY * scale + }px) scale(${scale})` + this.indicatorContainer.append(indicatorTitle) + + // 上边线 + const lineTop = document.createElement('span') + lineTop.classList.add(`${EDITOR_PREFIX}-zone-indicator-border__top`) + lineTop.style.top = `${indicatorTopY}px` + lineTop.style.width = `${innerWidth}px` + lineTop.style.marginLeft = `${margins[3]}px` + this.indicatorContainer.append(lineTop) + + // 左边线 + const lineLeft = document.createElement('span') + lineLeft.classList.add(`${EDITOR_PREFIX}-zone-indicator-border__left`) + lineLeft.style.top = `${startY}px` + lineLeft.style.height = `${indicatorHeight}px` + lineLeft.style.left = `${indicatorLeftX}px` + this.indicatorContainer.append(lineLeft) + + // 下边线 + const lineBottom = document.createElement('span') + lineBottom.classList.add(`${EDITOR_PREFIX}-zone-indicator-border__bottom`) + lineBottom.style.top = `${indicatorBottomY}px` + this.indicatorContainer.append(lineBottom) + + // 右边线 + const lineRight = document.createElement('span') + lineRight.classList.add(`${EDITOR_PREFIX}-zone-indicator-border__right`) + lineRight.style.top = `${startY}px` + lineRight.style.height = `${indicatorHeight}px` + lineRight.style.left = `${indicatorRightX}px` + this.indicatorContainer.append(lineRight) + } + this.container.append(this.indicatorContainer) + } + + private _clearZoneIndicator() { + this.indicatorContainer?.remove() + this.indicatorContainer = null + } +} diff --git a/src/editor/core/zone/ZoneTip.ts b/src/editor/core/zone/ZoneTip.ts new file mode 100644 index 0000000..7ae7286 --- /dev/null +++ b/src/editor/core/zone/ZoneTip.ts @@ -0,0 +1,108 @@ +import { EDITOR_PREFIX } from '../../dataset/constant/Editor' +import { EditorZone } from '../../dataset/enum/Editor' +import { throttle } from '../../utils' +import { Draw } from '../draw/Draw' +import { I18n } from '../i18n/I18n' +import { Zone } from './Zone' + +export class ZoneTip { + private draw: Draw + private zone: Zone + private i18n: I18n + private container: HTMLDivElement + private pageContainer: HTMLDivElement + + private isDisableMouseMove: boolean + private tipContainer: HTMLDivElement + private tipContent: HTMLSpanElement + private currentMoveZone: EditorZone | undefined + + constructor(draw: Draw, zone: Zone) { + this.draw = draw + this.zone = zone + this.i18n = draw.getI18n() + this.container = draw.getContainer() + this.pageContainer = draw.getPageContainer() + + const { tipContainer, tipContent } = this._drawZoneTip() + this.tipContainer = tipContainer + this.tipContent = tipContent + this.isDisableMouseMove = true + this.currentMoveZone = EditorZone.MAIN + // 监听区域 + const watchZones: EditorZone[] = [] + const { header, footer } = draw.getOptions() + if (!header.disabled) { + watchZones.push(EditorZone.HEADER) + } + if (!footer.disabled) { + watchZones.push(EditorZone.FOOTER) + } + if (watchZones.length) { + this._watchMouseMoveZoneChange(watchZones) + } + } + + private _watchMouseMoveZoneChange(watchZones: EditorZone[]) { + this.pageContainer.addEventListener( + 'mousemove', + throttle((evt: MouseEvent) => { + if (this.isDisableMouseMove || !this.draw.getIsPagingMode()) return + if (!evt.offsetY) return + if (evt.target instanceof HTMLCanvasElement) { + const mousemoveZone = this.zone.getZoneByY(evt.offsetY) + if (!watchZones.includes(mousemoveZone)) { + this._updateZoneTip(false) + return + } + this.currentMoveZone = mousemoveZone + // 激活区域是正文,移动区域是页眉、页脚时绘制 + this._updateZoneTip( + this.zone.getZone() === EditorZone.MAIN && + (mousemoveZone === EditorZone.HEADER || + mousemoveZone === EditorZone.FOOTER), + evt.x, + evt.y + ) + } else { + this._updateZoneTip(false) + } + }, 250) + ) + // mouseenter后mousemove有效,避免因节流导致的mouseleave后继续执行逻辑 + this.pageContainer.addEventListener('mouseenter', () => { + this.isDisableMouseMove = false + }) + this.pageContainer.addEventListener('mouseleave', () => { + this.isDisableMouseMove = true + this._updateZoneTip(false) + }) + } + + private _drawZoneTip() { + const tipContainer = document.createElement('div') + tipContainer.classList.add(`${EDITOR_PREFIX}-zone-tip`) + const tipContent = document.createElement('span') + tipContainer.append(tipContent) + this.container.append(tipContainer) + return { + tipContainer, + tipContent + } + } + + private _updateZoneTip(visible: boolean, left?: number, top?: number) { + if (visible) { + this.tipContainer.classList.add('show') + this.tipContainer.style.left = `${left}px` + this.tipContainer.style.top = `${top}px` + this.tipContent.innerText = this.i18n.t( + `zone.${ + this.currentMoveZone === EditorZone.HEADER ? 'headerTip' : 'footerTip' + }` + ) + } else { + this.tipContainer.classList.remove('show') + } + } +} diff --git a/src/editor/dataset/constant/Background.ts b/src/editor/dataset/constant/Background.ts new file mode 100644 index 0000000..7f6f1ca --- /dev/null +++ b/src/editor/dataset/constant/Background.ts @@ -0,0 +1,10 @@ +import { IBackgroundOption } from '../../interface/Background' +import { BackgroundRepeat, BackgroundSize } from '../enum/Background' + +export const defaultBackground: Readonly> = { + color: '#FFFFFF', + image: '', + size: BackgroundSize.COVER, + repeat: BackgroundRepeat.NO_REPEAT, + applyPageNumbers: [] +} diff --git a/src/editor/dataset/constant/Badge.ts b/src/editor/dataset/constant/Badge.ts new file mode 100644 index 0000000..6614363 --- /dev/null +++ b/src/editor/dataset/constant/Badge.ts @@ -0,0 +1,6 @@ +import { IBadgeOption } from '../../interface/Badge' + +export const defaultBadgeOption: Readonly> = { + top: 0, + left: 5 +} diff --git a/src/editor/dataset/constant/Checkbox.ts b/src/editor/dataset/constant/Checkbox.ts new file mode 100644 index 0000000..5ff7b04 --- /dev/null +++ b/src/editor/dataset/constant/Checkbox.ts @@ -0,0 +1,12 @@ +import { ICheckboxOption } from '../../interface/Checkbox' +import { VerticalAlign } from '../enum/VerticalAlign' + +export const defaultCheckboxOption: Readonly> = { + width: 14, + height: 14, + gap: 5, + lineWidth: 1, + fillStyle: '#5175f4', + strokeStyle: '#ffffff', + verticalAlign: VerticalAlign.BOTTOM +} diff --git a/src/editor/dataset/constant/Common.ts b/src/editor/dataset/constant/Common.ts new file mode 100644 index 0000000..e4cbf2c --- /dev/null +++ b/src/editor/dataset/constant/Common.ts @@ -0,0 +1,44 @@ +import { MaxHeightRatio } from '../enum/Common' + +export const ZERO = '\u200B' +export const WRAP = '\n' +export const HORIZON_TAB = '\t' +export const NBSP = '\u0020' +export const NON_BREAKING_SPACE = ' ' +export const PUNCTUATION_LIST = [ + '·', + '、', + ':', + ':', + ',', + ',', + '.', + '。', + ';', + ';', + '?', + '?', + '!', + '!' +] + +export const maxHeightRadioMapping: Record = { + [MaxHeightRatio.HALF]: 1 / 2, + [MaxHeightRatio.ONE_THIRD]: 1 / 3, + [MaxHeightRatio.QUARTER]: 1 / 4 +} + +export const LETTER_CLASS = { + ENGLISH: 'A-Za-z', + SPANISH: 'A-Za-zÁÉÍÓÚáéíóúÑñÜü', + FRENCH: 'A-Za-zÀÂÇàâçÉéÈèÊêËëÎîÏïÔôÙùÛûŸÿ', + GERMAN: 'A-Za-zÄäÖöÜüß', + RUSSIAN: 'А-Яа-яЁё', + PORTUGUESE: 'A-Za-zÁÉÍÓÚáéíóúÃÕãõÇç', + ITALIAN: 'A-Za-zÀàÈèÉéÌìÍíÎîÓóÒòÙù', + DUTCH: 'A-Za-zÀàÁáÂâÄäÈèÉéÊêËëÌìÍíÎîÏïÓóÒòÔôÖöÙùÛûÜü', + SWEDISH: 'A-Za-zÅåÄäÖö', + GREEK: 'ΑαΒβΓγΔδΕεΖζΗηΘθΙιΚκΛλΜμΝνΞξΟοΠπΡρΣσςΤτΥυΦφΧχΨψΩω' +} + +export const METRICS_BASIS_TEXT = '日' diff --git a/src/editor/dataset/constant/ContextMenu.ts b/src/editor/dataset/constant/ContextMenu.ts new file mode 100644 index 0000000..4934783 --- /dev/null +++ b/src/editor/dataset/constant/ContextMenu.ts @@ -0,0 +1,61 @@ +export const NAME_PLACEHOLDER = { + SELECTED_TEXT: '%s' +} + +export const INTERNAL_CONTEXT_MENU_KEY = { + GLOBAL: { + CUT: 'globalCut', + COPY: 'globalCopy', + PASTE: 'globalPaste', + SELECT_ALL: 'globalSelectAll', + PRINT: 'globalPrint' + }, + CONTROL: { + DELETE: 'controlDelete' + }, + HYPERLINK: { + DELETE: 'hyperlinkDelete', + CANCEL: 'hyperlinkCancel', + EDIT: 'hyperlinkEdit' + }, + IMAGE: { + CHANGE: 'imageChange', + SAVE_AS: 'imageSaveAs', + TEXT_WRAP: 'imageTextWrap', + TEXT_WRAP_EMBED: 'imageTextWrapEmbed', + TEXT_WRAP_UP_DOWN: 'imageTextWrapUpDown', + TEXT_WRAP_SURROUND: 'imageTextWrapSurround', + TEXT_WRAP_FLOAT_TOP: 'imageTextWrapFloatTop', + TEXT_WRAP_FLOAT_BOTTOM: 'imageTextWrapFloatBottom' + }, + TABLE: { + BORDER: 'border', + BORDER_ALL: 'tableBorderAll', + BORDER_EMPTY: 'tableBorderEmpty', + BORDER_DASH: 'tableBorderDash', + BORDER_EXTERNAL: 'tableBorderExternal', + BORDER_INTERNAL: 'tableBorderInternal', + BORDER_TD: 'tableBorderTd', + BORDER_TD_TOP: 'tableBorderTdTop', + BORDER_TD_RIGHT: 'tableBorderTdRight', + BORDER_TD_BOTTOM: 'tableBorderTdBottom', + BORDER_TD_LEFT: 'tableBorderTdLeft', + BORDER_TD_FORWARD: 'tableBorderTdForward', + BORDER_TD_BACK: 'tableBorderTdBack', + VERTICAL_ALIGN: 'tableVerticalAlign', + VERTICAL_ALIGN_TOP: 'tableVerticalAlignTop', + VERTICAL_ALIGN_MIDDLE: 'tableVerticalAlignMiddle', + VERTICAL_ALIGN_BOTTOM: 'tableVerticalAlignBottom', + INSERT_ROW_COL: 'tableInsertRowCol', + INSERT_TOP_ROW: 'tableInsertTopRow', + INSERT_BOTTOM_ROW: 'tableInsertBottomRow', + INSERT_LEFT_COL: 'tableInsertLeftCol', + INSERT_RIGHT_COL: 'tableInsertRightCol', + DELETE_ROW_COL: 'tableDeleteRowCol', + DELETE_ROW: 'tableDeleteRow', + DELETE_COL: 'tableDeleteCol', + DELETE_TABLE: 'tableDeleteTable', + MERGE_CELL: 'tableMergeCell', + CANCEL_MERGE_CELL: 'tableCancelMergeCell' + } +} diff --git a/src/editor/dataset/constant/Control.ts b/src/editor/dataset/constant/Control.ts new file mode 100644 index 0000000..c107596 --- /dev/null +++ b/src/editor/dataset/constant/Control.ts @@ -0,0 +1,14 @@ +import { IControlOption } from '../../interface/Control' + +export const defaultControlOption: Readonly> = { + placeholderColor: '#9c9b9b', + bracketColor: '#000000', + prefix: '{', + postfix: '}', + borderWidth: 1, + borderColor: '#000000', + activeBackgroundColor: '', + disabledBackgroundColor: '', + existValueBackgroundColor: '', + noValueBackgroundColor: '' +} diff --git a/src/editor/dataset/constant/Cursor.ts b/src/editor/dataset/constant/Cursor.ts new file mode 100644 index 0000000..55e9398 --- /dev/null +++ b/src/editor/dataset/constant/Cursor.ts @@ -0,0 +1,11 @@ +import { ICursorOption } from '../../interface/Cursor' + +export const CURSOR_AGENT_OFFSET_HEIGHT = 12 + +export const defaultCursorOption: Readonly> = { + width: 1, + color: '#000000', + dragWidth: 2, + dragColor: '#0000FF', + dragFloatImageDisabled: false +} diff --git a/src/editor/dataset/constant/Editor.ts b/src/editor/dataset/constant/Editor.ts new file mode 100644 index 0000000..9a3f269 --- /dev/null +++ b/src/editor/dataset/constant/Editor.ts @@ -0,0 +1,18 @@ +import { DeepRequired } from '../../interface/Common' +import { IModeRule } from '../../interface/Editor' + +export const EDITOR_COMPONENT = 'editor-component' +export const EDITOR_PREFIX = 'ce' +export const EDITOR_CLIPBOARD = `${EDITOR_PREFIX}-clipboard` + +export const defaultModeRuleOption: Readonly> = { + print: { + imagePreviewerDisabled: false + }, + readonly: { + imagePreviewerDisabled: false + }, + form: { + controlDeletableDisabled: false + } +} diff --git a/src/editor/dataset/constant/Element.ts b/src/editor/dataset/constant/Element.ts new file mode 100644 index 0000000..58a21ab --- /dev/null +++ b/src/editor/dataset/constant/Element.ts @@ -0,0 +1,166 @@ +import { ElementType } from '../enum/Element' +import { IElement } from '../../interface/Element' +import { ITd } from '../../interface/table/Td' +import { IControlStyle } from '../../interface/Control' + +export const EDITOR_ELEMENT_STYLE_ATTR: Array = [ + 'bold', + 'color', + 'highlight', + 'font', + 'size', + 'italic', + 'underline', + 'strikeout', + 'textDecoration' +] + +export const EDITOR_ROW_ATTR: Array = ['rowFlex', 'rowMargin'] + +export const EDITOR_ELEMENT_COPY_ATTR: Array = [ + 'type', + 'font', + 'size', + 'bold', + 'color', + 'italic', + 'highlight', + 'underline', + 'strikeout', + 'rowFlex', + 'url', + 'areaId', + 'hyperlinkId', + 'dateId', + 'dateFormat', + 'groupIds', + 'rowMargin', + 'textDecoration' +] + +export const EDITOR_ELEMENT_ZIP_ATTR: Array = [ + 'type', + 'font', + 'size', + 'bold', + 'color', + 'italic', + 'highlight', + 'underline', + 'strikeout', + 'rowFlex', + 'rowMargin', + 'dashArray', + 'trList', + 'tableToolDisabled', + 'borderType', + 'borderColor', + 'width', + 'height', + 'url', + 'colgroup', + 'valueList', + 'control', + 'checkbox', + 'radio', + 'dateFormat', + 'block', + 'level', + 'title', + 'listType', + 'listStyle', + 'listWrap', + 'groupIds', + 'conceptId', + 'imgDisplay', + 'imgFloatPosition', + 'imgToolDisabled', + 'textDecoration', + 'extension', + 'externalId', + 'areaId', + 'area', + 'hide' +] + +export const TABLE_TD_ZIP_ATTR: Array = [ + 'conceptId', + 'extension', + 'externalId', + 'verticalAlign', + 'backgroundColor', + 'borderTypes', + 'slashTypes', + 'disabled', + 'deletable' +] + +export const TABLE_CONTEXT_ATTR: Array = [ + 'tdId', + 'trId', + 'tableId' +] + +export const TITLE_CONTEXT_ATTR: Array = [ + 'level', + 'titleId', + 'title' +] + +export const LIST_CONTEXT_ATTR: Array = [ + 'listId', + 'listType', + 'listStyle' +] + +export const CONTROL_CONTEXT_ATTR: Array = [ + 'control', + 'controlId', + 'controlComponent' +] + +export const CONTROL_STYLE_ATTR: Array = [ + 'font', + 'size', + 'bold', + 'highlight', + 'italic', + 'strikeout' +] + +export const AREA_CONTEXT_ATTR: Array = ['areaId', 'area'] + +export const EDITOR_ELEMENT_CONTEXT_ATTR: Array = [ + ...TABLE_CONTEXT_ATTR, + ...TITLE_CONTEXT_ATTR, + ...LIST_CONTEXT_ATTR, + ...AREA_CONTEXT_ATTR +] + +export const TEXTLIKE_ELEMENT_TYPE: ElementType[] = [ + ElementType.TEXT, + ElementType.HYPERLINK, + ElementType.SUBSCRIPT, + ElementType.SUPERSCRIPT, + ElementType.CONTROL, + ElementType.DATE +] + +export const IMAGE_ELEMENT_TYPE: ElementType[] = [ + ElementType.IMAGE, + ElementType.LATEX +] + +export const BLOCK_ELEMENT_TYPE: ElementType[] = [ + ElementType.BLOCK, + ElementType.PAGE_BREAK, + ElementType.SEPARATOR, + ElementType.TABLE +] + +export const INLINE_NODE_NAME: string[] = ['HR', 'TABLE', 'UL', 'OL'] + +export const VIRTUAL_ELEMENT_TYPE: ElementType[] = [ + ElementType.TITLE, + ElementType.LIST +] diff --git a/src/editor/dataset/constant/Footer.ts b/src/editor/dataset/constant/Footer.ts new file mode 100644 index 0000000..34f800d --- /dev/null +++ b/src/editor/dataset/constant/Footer.ts @@ -0,0 +1,10 @@ +import { IFooter } from '../../interface/Footer' +import { MaxHeightRatio } from '../enum/Common' + +export const defaultFooterOption: Readonly> = { + bottom: 30, + inactiveAlpha: 1, + maxHeightRadio: MaxHeightRatio.HALF, + disabled: false, + editable: true +} diff --git a/src/editor/dataset/constant/Group.ts b/src/editor/dataset/constant/Group.ts new file mode 100644 index 0000000..488611b --- /dev/null +++ b/src/editor/dataset/constant/Group.ts @@ -0,0 +1,10 @@ +import { IGroup } from '../../interface/Group' + +export const defaultGroupOption: Readonly> = { + opacity: 0.1, + backgroundColor: '#E99D00', + activeOpacity: 0.5, + activeBackgroundColor: '#E99D00', + disabled: false, + deletable: true +} diff --git a/src/editor/dataset/constant/Header.ts b/src/editor/dataset/constant/Header.ts new file mode 100644 index 0000000..53fbd07 --- /dev/null +++ b/src/editor/dataset/constant/Header.ts @@ -0,0 +1,10 @@ +import { IHeader } from '../../interface/Header' +import { MaxHeightRatio } from '../enum/Common' + +export const defaultHeaderOption: Readonly> = { + top: 30, + inactiveAlpha: 1, + maxHeightRadio: MaxHeightRatio.HALF, + disabled: false, + editable: true +} diff --git a/src/editor/dataset/constant/LineBreak.ts b/src/editor/dataset/constant/LineBreak.ts new file mode 100644 index 0000000..520f751 --- /dev/null +++ b/src/editor/dataset/constant/LineBreak.ts @@ -0,0 +1,7 @@ +import { ILineBreakOption } from '../../interface/LineBreak' + +export const defaultLineBreak: Readonly> = { + disabled: true, + color: '#CCCCCC', + lineWidth: 1.5 +} diff --git a/src/editor/dataset/constant/LineNumber.ts b/src/editor/dataset/constant/LineNumber.ts new file mode 100644 index 0000000..141d034 --- /dev/null +++ b/src/editor/dataset/constant/LineNumber.ts @@ -0,0 +1,11 @@ +import { ILineNumberOption } from '../../interface/LineNumber' +import { LineNumberType } from '../enum/LineNumber' + +export const defaultLineNumberOption: Readonly> = { + size: 12, + font: 'Microsoft YaHei', + color: '#000000', + disabled: true, + right: 20, + type: LineNumberType.CONTINUITY +} diff --git a/src/editor/dataset/constant/List.ts b/src/editor/dataset/constant/List.ts new file mode 100644 index 0000000..c240cb9 --- /dev/null +++ b/src/editor/dataset/constant/List.ts @@ -0,0 +1,21 @@ +import { ListStyle, ListType, UlStyle } from '../enum/List' + +export const ulStyleMapping: Record = { + [UlStyle.DISC]: '•', + [UlStyle.CIRCLE]: '◦', + [UlStyle.SQUARE]: '▫︎', + [UlStyle.CHECKBOX]: '☑️' +} + +export const listTypeElementMapping: Record = { + [ListType.OL]: 'ol', + [ListType.UL]: 'ul' +} + +export const listStyleCSSMapping: Record = { + [ListStyle.DISC]: 'disc', + [ListStyle.CIRCLE]: 'circle', + [ListStyle.SQUARE]: 'square', + [ListStyle.DECIMAL]: 'decimal', + [ListStyle.CHECKBOX]: 'checkbox' +} diff --git a/src/editor/dataset/constant/PageBorder.ts b/src/editor/dataset/constant/PageBorder.ts new file mode 100644 index 0000000..5751c37 --- /dev/null +++ b/src/editor/dataset/constant/PageBorder.ts @@ -0,0 +1,8 @@ +import { IPageBorderOption } from '../../interface/PageBorder' + +export const defaultPageBorderOption: Readonly> = { + color: '#000000', + lineWidth: 1, + padding: [0, 5, 0, 5], + disabled: true +} diff --git a/src/editor/dataset/constant/PageBreak.ts b/src/editor/dataset/constant/PageBreak.ts new file mode 100644 index 0000000..91550b2 --- /dev/null +++ b/src/editor/dataset/constant/PageBreak.ts @@ -0,0 +1,7 @@ +import { IPageBreak } from '../../interface/PageBreak' + +export const defaultPageBreakOption: Readonly> = { + font: 'Microsoft YaHei', + fontSize: 12, + lineDash: [3, 1] +} diff --git a/src/editor/dataset/constant/PageNumber.ts b/src/editor/dataset/constant/PageNumber.ts new file mode 100644 index 0000000..1bd14f1 --- /dev/null +++ b/src/editor/dataset/constant/PageNumber.ts @@ -0,0 +1,22 @@ +import { IPageNumber } from '../../interface/PageNumber' +import { NumberType } from '../enum/Common' +import { RowFlex } from '../enum/Row' + +export const FORMAT_PLACEHOLDER = { + PAGE_NO: '{pageNo}', + PAGE_COUNT: '{pageCount}' +} + +export const defaultPageNumberOption: Readonly> = { + bottom: 60, + size: 12, + font: 'Microsoft YaHei', + color: '#000000', + rowFlex: RowFlex.CENTER, + format: FORMAT_PLACEHOLDER.PAGE_NO, + numberType: NumberType.ARABIC, + disabled: false, + startPageNo: 1, + fromPageNo: 0, + maxPageNo: null +} diff --git a/src/editor/dataset/constant/Placeholder.ts b/src/editor/dataset/constant/Placeholder.ts new file mode 100644 index 0000000..3b91365 --- /dev/null +++ b/src/editor/dataset/constant/Placeholder.ts @@ -0,0 +1,9 @@ +import { IPlaceholder } from '../../interface/Placeholder' + +export const defaultPlaceholderOption: Readonly> = { + data: '', + color: '#DCDFE6', + opacity: 1, + size: 16, + font: 'Microsoft YaHei' +} diff --git a/src/editor/dataset/constant/Radio.ts b/src/editor/dataset/constant/Radio.ts new file mode 100644 index 0000000..a16c0e9 --- /dev/null +++ b/src/editor/dataset/constant/Radio.ts @@ -0,0 +1,12 @@ +import { IRadioOption } from '../../interface/Radio' +import { VerticalAlign } from '../enum/VerticalAlign' + +export const defaultRadioOption: Readonly> = { + width: 14, + height: 14, + gap: 5, + lineWidth: 1, + fillStyle: '#5175f4', + strokeStyle: '#000000', + verticalAlign: VerticalAlign.BOTTOM +} diff --git a/src/editor/dataset/constant/Regular.ts b/src/editor/dataset/constant/Regular.ts new file mode 100644 index 0000000..8549ac9 --- /dev/null +++ b/src/editor/dataset/constant/Regular.ts @@ -0,0 +1,19 @@ +import { ZERO } from './Common' + +export const NUMBER_REG = /[0-9]/ +export const NUMBER_LIKE_REG = /[0-9.]/ +export const CHINESE_REG = /[\u4e00-\u9fa5]/ +export const SURROGATE_PAIR_REG = /[\uD800-\uDBFF][\uDC00-\uDFFF]/ // unicode代理对(surrogate pair) +// https://github.com/mathiasbynens/emoji-test-regex-pattern +export const EMOJI_REG = + /[#*0-9]\uFE0F?\u20E3|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26AA\u26B0\u26B1\u26BD\u26BE\u26C4\u26C8\u26CF\u26D1\u26E9\u26F0-\u26F5\u26F7\u26F8\u26FA\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B55\u3030\u303D\u3297\u3299]\uFE0F?|[\u261D\u270C\u270D](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u23E9-\u23EC\u23F0\u23F3\u25FD\u2693\u26A1\u26AB\u26C5\u26CE\u26D4\u26EA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2795-\u2797\u27B0\u27BF\u2B50]|\u26D3\uFE0F?(?:\u200D\uD83D\uDCA5)?|\u26F9(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\u2764\uFE0F?(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\uD83C(?:[\uDC04\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]\uFE0F?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF43\uDF45-\uDF4A\uDF4C-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDF44(?:\u200D\uD83D\uDFEB)?|\uDF4B(?:\u200D\uD83D\uDFE9)?|\uDFC3(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDFF3\uFE0F?(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?)|\uD83D(?:[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4\uDEB5](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD74\uDD90](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC25\uDC27-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE41\uDE43\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED7\uDEDC-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uDC08(?:\u200D\u2B1B)?|\uDC15(?:\u200D\uD83E\uDDBA)?|\uDC26(?:\u200D(?:\u2B1B|\uD83D\uDD25))?|\uDC3B(?:\u200D\u2744\uFE0F?)?|\uDC41\uFE0F?(?:\u200D\uD83D\uDDE8\uFE0F?)?|\uDC68(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE])))?))?|\uDC69(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?[\uDC68\uDC69]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE])))?))?|\uDC6F(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDD75(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDE2E(?:\u200D\uD83D\uDCA8)?|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?|\uDE42(?:\u200D[\u2194\u2195]\uFE0F?)?|\uDEB6(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?)|\uD83E(?:[\uDD0C\uDD0F\uDD18-\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5\uDEC3-\uDEC5\uDEF0\uDEF2-\uDEF8](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD\uDDCF\uDDD4\uDDD6-\uDDDD](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD0D\uDD0E\uDD10-\uDD17\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCC\uDDD0\uDDE0-\uDDFF\uDE70-\uDE7C\uDE80-\uDE88\uDE90-\uDEBD\uDEBF-\uDEC2\uDECE-\uDEDB\uDEE0-\uDEE8]|\uDD3C(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF])?|\uDDCE(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDDD1(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1|\uDDD1\u200D\uD83E\uDDD2(?:\u200D\uD83E\uDDD2)?|\uDDD2(?:\u200D\uD83E\uDDD2)?))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?))?|\uDEF1(?:\uD83C(?:\uDFFB(?:\u200D\uD83E\uDEF2\uD83C[\uDFFC-\uDFFF])?|\uDFFC(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFD-\uDFFF])?|\uDFFD(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])?|\uDFFE(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFD\uDFFF])?|\uDFFF(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFE])?))?)/g + +export const UNICODE_SYMBOL_REG = new RegExp( + `${EMOJI_REG.source}|${SURROGATE_PAIR_REG.source}`, + 'g' +) + +export const PUNCTUATION_REG = + /[、,。?!;:……「」“”‘’*()【】〔〕〖〗〘〙〚〛《》———﹝﹞–—\\/·.,!?;:`~<>()[\]{}'"|]/ + +export const START_LINE_BREAK_REG = new RegExp(`^[${ZERO}\n]`) diff --git a/src/editor/dataset/constant/Separator.ts b/src/editor/dataset/constant/Separator.ts new file mode 100644 index 0000000..f998eef --- /dev/null +++ b/src/editor/dataset/constant/Separator.ts @@ -0,0 +1,6 @@ +import { ISeparatorOption } from '../../interface/Separator' + +export const defaultSeparatorOption: Readonly> = { + lineWidth: 1, + strokeStyle: '#000000' +} diff --git a/src/editor/dataset/constant/Shortcut.ts b/src/editor/dataset/constant/Shortcut.ts new file mode 100644 index 0000000..a8069b5 --- /dev/null +++ b/src/editor/dataset/constant/Shortcut.ts @@ -0,0 +1,3 @@ +export const INTERNAL_SHORTCUT_KEY = { + PAGE_SCALE: 'pageScale' +} diff --git a/src/editor/dataset/constant/Table.ts b/src/editor/dataset/constant/Table.ts new file mode 100644 index 0000000..62b969e --- /dev/null +++ b/src/editor/dataset/constant/Table.ts @@ -0,0 +1,8 @@ +import { ITableOption } from '../../interface/table/Table' + +export const defaultTableOption: Readonly> = { + tdPadding: [0, 5, 5, 5], + defaultTrMinHeight: 42, + defaultColMinWidth: 40, + defaultBorderColor: '#000000' +} diff --git a/src/editor/dataset/constant/Title.ts b/src/editor/dataset/constant/Title.ts new file mode 100644 index 0000000..93fda74 --- /dev/null +++ b/src/editor/dataset/constant/Title.ts @@ -0,0 +1,38 @@ +import { ITitleOption, ITitleSizeOption } from '../../interface/Title' +import { TitleLevel } from '../enum/Title' + +export const defaultTitleOption: Readonly> = { + defaultFirstSize: 26, + defaultSecondSize: 24, + defaultThirdSize: 22, + defaultFourthSize: 20, + defaultFifthSize: 18, + defaultSixthSize: 16 +} + +export const titleSizeMapping: Record = { + [TitleLevel.FIRST]: 'defaultFirstSize', + [TitleLevel.SECOND]: 'defaultSecondSize', + [TitleLevel.THIRD]: 'defaultThirdSize', + [TitleLevel.FOURTH]: 'defaultFourthSize', + [TitleLevel.FIFTH]: 'defaultFifthSize', + [TitleLevel.SIXTH]: 'defaultSixthSize' +} + +export const titleOrderNumberMapping: Record = { + [TitleLevel.FIRST]: 1, + [TitleLevel.SECOND]: 2, + [TitleLevel.THIRD]: 3, + [TitleLevel.FOURTH]: 4, + [TitleLevel.FIFTH]: 5, + [TitleLevel.SIXTH]: 6 +} + +export const titleNodeNameMapping: Record = { + H1: TitleLevel.FIRST, + H2: TitleLevel.SECOND, + H3: TitleLevel.THIRD, + H4: TitleLevel.FOURTH, + H5: TitleLevel.FIFTH, + H6: TitleLevel.SIXTH +} diff --git a/src/editor/dataset/constant/Watermark.ts b/src/editor/dataset/constant/Watermark.ts new file mode 100644 index 0000000..89fd85f --- /dev/null +++ b/src/editor/dataset/constant/Watermark.ts @@ -0,0 +1,17 @@ +import { IWatermark } from '../../interface/Watermark' +import { NumberType } from '../enum/Common' +import { WatermarkType } from '../enum/Watermark' + +export const defaultWatermarkOption: Readonly> = { + data: '', + type: WatermarkType.TEXT, + width: 0, + height: 0, + color: '#AEB5C0', + opacity: 0.3, + size: 200, + font: 'Microsoft YaHei', + repeat: false, + gap: [10, 10], + numberType: NumberType.ARABIC +} diff --git a/src/editor/dataset/constant/Zone.ts b/src/editor/dataset/constant/Zone.ts new file mode 100644 index 0000000..4cb3d30 --- /dev/null +++ b/src/editor/dataset/constant/Zone.ts @@ -0,0 +1,5 @@ +import { IZoneOption } from '../../interface/Zone' + +export const defaultZoneOption: Readonly> = { + tipDisabled: true +} diff --git a/src/editor/dataset/enum/Area.ts b/src/editor/dataset/enum/Area.ts new file mode 100644 index 0000000..40fc53e --- /dev/null +++ b/src/editor/dataset/enum/Area.ts @@ -0,0 +1,5 @@ +export enum AreaMode { + EDIT = 'edit', // 编辑模式(文档可编辑、辅助元素均存在) + READONLY = 'readonly', // 只读模式(文档不可编辑) + FORM = 'form' // 表单模式(仅控件内可编辑) +} diff --git a/src/editor/dataset/enum/Background.ts b/src/editor/dataset/enum/Background.ts new file mode 100644 index 0000000..9ec0f41 --- /dev/null +++ b/src/editor/dataset/enum/Background.ts @@ -0,0 +1,11 @@ +export enum BackgroundSize { + CONTAIN = 'contain', + COVER = 'cover' +} + +export enum BackgroundRepeat { + REPEAT = 'repeat', + NO_REPEAT = 'no-repeat', + REPEAT_X = 'repeat-x', + REPEAT_Y = 'repeat-y' +} diff --git a/src/editor/dataset/enum/Block.ts b/src/editor/dataset/enum/Block.ts new file mode 100644 index 0000000..c881fc1 --- /dev/null +++ b/src/editor/dataset/enum/Block.ts @@ -0,0 +1,4 @@ +export enum BlockType { + IFRAME = 'iframe', + VIDEO = 'video' +} diff --git a/src/editor/dataset/enum/Common.ts b/src/editor/dataset/enum/Common.ts new file mode 100644 index 0000000..6eb6aac --- /dev/null +++ b/src/editor/dataset/enum/Common.ts @@ -0,0 +1,30 @@ +export enum MaxHeightRatio { + HALF = 'half', + ONE_THIRD = 'one-third', + QUARTER = 'quarter' +} + +export enum NumberType { + ARABIC = 'arabic', + CHINESE = 'chinese' +} + +export enum ImageDisplay { + INLINE = 'inline', + BLOCK = 'block', + SURROUND = 'surround', + FLOAT_TOP = 'float-top', + FLOAT_BOTTOM = 'float-bottom' +} + +export enum LocationPosition { + BEFORE = 'before', + AFTER = 'after', + OUTER_BEFORE = 'outer-before', + OUTER_AFTER = 'outer-after' +} + +export enum FlexDirection { + ROW = 'row', + COLUMN = 'column' +} diff --git a/src/editor/dataset/enum/Control.ts b/src/editor/dataset/enum/Control.ts new file mode 100644 index 0000000..b894c13 --- /dev/null +++ b/src/editor/dataset/enum/Control.ts @@ -0,0 +1,31 @@ +export enum ControlType { + TEXT = 'text', + SELECT = 'select', + CHECKBOX = 'checkbox', + RADIO = 'radio', + DATE = 'date', + NUMBER = 'number' +} + +export enum ControlComponent { + PREFIX = 'prefix', + POSTFIX = 'postfix', + PRE_TEXT = 'preText', + POST_TEXT = 'postText', + PLACEHOLDER = 'placeholder', + VALUE = 'value', + CHECKBOX = 'checkbox', + RADIO = 'radio' +} + +// 控件内容缩进方式 +export enum ControlIndentation { + ROW_START = 'rowStart', // 从行起始位置缩进 + VALUE_START = 'valueStart' // 从值起始位置缩进 +} + +// 控件状态 +export enum ControlState { + ACTIVE = 'active', + INACTIVE = 'inactive' +} diff --git a/src/editor/dataset/enum/Editor.ts b/src/editor/dataset/enum/Editor.ts new file mode 100644 index 0000000..454f0a9 --- /dev/null +++ b/src/editor/dataset/enum/Editor.ts @@ -0,0 +1,50 @@ +export enum EditorComponent { + COMPONENT = 'component', + MENU = 'menu', + MAIN = 'main', + FOOTER = 'footer', + CONTEXTMENU = 'contextmenu', + POPUP = 'popup', + CATALOG = 'catalog', + COMMENT = 'comment' +} + +export enum EditorContext { + PAGE = 'page', + TABLE = 'table' +} + +export enum EditorMode { + EDIT = 'edit', // 编辑模式(文档可编辑、辅助元素均存在) + CLEAN = 'clean', // 清洁模式(隐藏辅助元素) + READONLY = 'readonly', // 只读模式(文档不可编辑) + FORM = 'form', // 表单模式(仅控件内可编辑) + PRINT = 'print', // 打印模式(文档不可编辑、隐藏辅助元素、选区、未书写控件及边框) + DESIGN = 'design' // 设计模式(不可删除、只读等配置不控制) +} + +export enum EditorZone { + HEADER = 'header', + MAIN = 'main', + FOOTER = 'footer' +} + +export enum PageMode { + PAGING = 'paging', + CONTINUITY = 'continuity' +} + +export enum PaperDirection { + VERTICAL = 'vertical', + HORIZONTAL = 'horizontal' +} + +export enum WordBreak { + BREAK_ALL = 'break-all', + BREAK_WORD = 'break-word' +} + +export enum RenderMode { + SPEED = 'speed', + COMPATIBILITY = 'compatibility' +} diff --git a/src/editor/dataset/enum/Element.ts b/src/editor/dataset/enum/Element.ts new file mode 100644 index 0000000..d37e76d --- /dev/null +++ b/src/editor/dataset/enum/Element.ts @@ -0,0 +1,20 @@ +export enum ElementType { + TEXT = 'text', + IMAGE = 'image', + TABLE = 'table', + HYPERLINK = 'hyperlink', + SUPERSCRIPT = 'superscript', + SUBSCRIPT = 'subscript', + SEPARATOR = 'separator', + PAGE_BREAK = 'pageBreak', + CONTROL = 'control', + AREA = 'area', + CHECKBOX = 'checkbox', + RADIO = 'radio', + LATEX = 'latex', + TAB = 'tab', + DATE = 'date', + BLOCK = 'block', + TITLE = 'title', + LIST = 'list' +} diff --git a/src/editor/dataset/enum/ElementStyle.ts b/src/editor/dataset/enum/ElementStyle.ts new file mode 100644 index 0000000..4f21723 --- /dev/null +++ b/src/editor/dataset/enum/ElementStyle.ts @@ -0,0 +1,12 @@ +export enum ElementStyleKey { + font = 'font', + size = 'size', + width = 'width', + height = 'height', + bold = 'bold', + color = 'color', + highlight = 'highlight', + italic = 'italic', + underline = 'underline', + strikeout = 'strikeout' +} diff --git a/src/editor/dataset/enum/Event.ts b/src/editor/dataset/enum/Event.ts new file mode 100644 index 0000000..c72f601 --- /dev/null +++ b/src/editor/dataset/enum/Event.ts @@ -0,0 +1,5 @@ +export enum MouseEventButton { + LEFT = 0, + CENTER = 1, + RIGHT = 2 +} diff --git a/src/editor/dataset/enum/KeyMap.ts b/src/editor/dataset/enum/KeyMap.ts new file mode 100644 index 0000000..d7d1a7a --- /dev/null +++ b/src/editor/dataset/enum/KeyMap.ts @@ -0,0 +1,83 @@ +export enum KeyMap { + Delete = 'Delete', + Backspace = 'Backspace', + Enter = 'Enter', + Left = 'ArrowLeft', + Right = 'ArrowRight', + Up = 'ArrowUp', + Down = 'ArrowDown', + ESC = 'Escape', + TAB = 'Tab', + META = 'Meta', + LEFT_BRACKET = '[', + RIGHT_BRACKET = ']', + COMMA = ',', + PERIOD = '.', + LEFT_ANGLE_BRACKET = '<', + RIGHT_ANGLE_BRACKET = '>', + EQUAL = '=', + MINUS = '-', + PLUS = '+', + A = 'a', + B = 'b', + C = 'c', + D = 'd', + E = 'e', + F = 'f', + G = 'g', + H = 'h', + I = 'i', + J = 'j', + K = 'k', + L = 'l', + M = 'm', + N = 'n', + O = 'o', + P = 'p', + Q = 'q', + R = 'r', + S = 's', + T = 't', + U = 'u', + V = 'v', + W = 'w', + X = 'x', + Y = 'y', + Z = 'z', + A_UPPERCASE = 'A', + B_UPPERCASE = 'B', + C_UPPERCASE = 'C', + D_UPPERCASE = 'D', + E_UPPERCASE = 'E', + F_UPPERCASE = 'F', + G_UPPERCASE = 'G', + H_UPPERCASE = 'H', + I_UPPERCASE = 'I', + J_UPPERCASE = 'J', + K_UPPERCASE = 'K', + L_UPPERCASE = 'L', + M_UPPERCASE = 'M', + N_UPPERCASE = 'N', + O_UPPERCASE = 'O', + P_UPPERCASE = 'P', + Q_UPPERCASE = 'Q', + R_UPPERCASE = 'R', + S_UPPERCASE = 'S', + T_UPPERCASE = 'T', + U_UPPERCASE = 'U', + V_UPPERCASE = 'V', + W_UPPERCASE = 'W', + X_UPPERCASE = 'X', + Y_UPPERCASE = 'Y', + Z_UPPERCASE = 'Z', + ZERO = '0', + ONE = '1', + TWO = '2', + THREE = '3', + FOUR = '4', + FIVE = '5', + SIX = '6', + SEVEN = '7', + EIGHT = '8', + NINE = '9' +} diff --git a/src/editor/dataset/enum/LineNumber.ts b/src/editor/dataset/enum/LineNumber.ts new file mode 100644 index 0000000..53e689b --- /dev/null +++ b/src/editor/dataset/enum/LineNumber.ts @@ -0,0 +1,4 @@ +export enum LineNumberType { + PAGE = 'page', + CONTINUITY = 'continuity' +} diff --git a/src/editor/dataset/enum/List.ts b/src/editor/dataset/enum/List.ts new file mode 100644 index 0000000..efffe56 --- /dev/null +++ b/src/editor/dataset/enum/List.ts @@ -0,0 +1,23 @@ +export enum ListType { + UL = 'ul', + OL = 'ol' +} + +export enum UlStyle { + DISC = 'disc', // 实心圆点 + CIRCLE = 'circle', // 空心圆点 + SQUARE = 'square', // 实心方块 + CHECKBOX = 'checkbox' // 复选框 +} + +export enum OlStyle { + DECIMAL = 'decimal' // 阿拉伯数字 +} + +export enum ListStyle { + DISC = UlStyle.DISC, + CIRCLE = UlStyle.CIRCLE, + SQUARE = UlStyle.SQUARE, + DECIMAL = OlStyle.DECIMAL, + CHECKBOX = UlStyle.CHECKBOX +} diff --git a/src/editor/dataset/enum/Observer.ts b/src/editor/dataset/enum/Observer.ts new file mode 100644 index 0000000..9dbf1fa --- /dev/null +++ b/src/editor/dataset/enum/Observer.ts @@ -0,0 +1,6 @@ +export enum MoveDirection { + UP = 'top', + DOWN = 'down', + LEFT = 'left', + RIGHT = 'right' +} diff --git a/src/editor/dataset/enum/Row.ts b/src/editor/dataset/enum/Row.ts new file mode 100644 index 0000000..0f44493 --- /dev/null +++ b/src/editor/dataset/enum/Row.ts @@ -0,0 +1,7 @@ +export enum RowFlex { + LEFT = 'left', + CENTER = 'center', + RIGHT = 'right', + ALIGNMENT = 'alignment', + JUSTIFY = 'justify' +} diff --git a/src/editor/dataset/enum/Text.ts b/src/editor/dataset/enum/Text.ts new file mode 100644 index 0000000..9a023de --- /dev/null +++ b/src/editor/dataset/enum/Text.ts @@ -0,0 +1,13 @@ +export enum TextDecorationStyle { + SOLID = 'solid', + DOUBLE = 'double', + DASHED = 'dashed', + DOTTED = 'dotted', + WAVY = 'wavy' +} + +export enum DashType { + SOLID = 'solid', + DASHED = 'dashed', + DOTTED = 'dotted' +} diff --git a/src/editor/dataset/enum/Title.ts b/src/editor/dataset/enum/Title.ts new file mode 100644 index 0000000..2ed83a0 --- /dev/null +++ b/src/editor/dataset/enum/Title.ts @@ -0,0 +1,8 @@ +export enum TitleLevel { + FIRST = 'first', + SECOND = 'second', + THIRD = 'third', + FOURTH = 'fourth', + FIFTH = 'fifth', + SIXTH = 'sixth' +} diff --git a/src/editor/dataset/enum/VerticalAlign.ts b/src/editor/dataset/enum/VerticalAlign.ts new file mode 100644 index 0000000..64b0f32 --- /dev/null +++ b/src/editor/dataset/enum/VerticalAlign.ts @@ -0,0 +1,5 @@ +export enum VerticalAlign { + TOP = 'top', + MIDDLE = 'middle', + BOTTOM = 'bottom' +} diff --git a/src/editor/dataset/enum/Watermark.ts b/src/editor/dataset/enum/Watermark.ts new file mode 100644 index 0000000..703db35 --- /dev/null +++ b/src/editor/dataset/enum/Watermark.ts @@ -0,0 +1,4 @@ +export enum WatermarkType { + TEXT = 'text', + IMAGE = 'image' +} diff --git a/src/editor/dataset/enum/table/Table.ts b/src/editor/dataset/enum/table/Table.ts new file mode 100644 index 0000000..c5bb6f7 --- /dev/null +++ b/src/editor/dataset/enum/table/Table.ts @@ -0,0 +1,19 @@ +export enum TableBorder { + ALL = 'all', + EMPTY = 'empty', + EXTERNAL = 'external', + INTERNAL = 'internal', + DASH = 'dash' +} + +export enum TdBorder { + TOP = 'top', + RIGHT = 'right', + BOTTOM = 'bottom', + LEFT = 'left' +} + +export enum TdSlash { + FORWARD = 'forward', // 正斜线 / + BACK = 'back' // 反斜线 \ +} diff --git a/src/editor/dataset/enum/table/TableTool.ts b/src/editor/dataset/enum/table/TableTool.ts new file mode 100644 index 0000000..d1c55c4 --- /dev/null +++ b/src/editor/dataset/enum/table/TableTool.ts @@ -0,0 +1,4 @@ +export enum TableOrder { + ROW = 'row', + COL = 'col' +} diff --git a/src/editor/index.ts b/src/editor/index.ts new file mode 100644 index 0000000..59c9647 --- /dev/null +++ b/src/editor/index.ts @@ -0,0 +1,232 @@ +import './assets/css/index.css' +import { IEditorData, IEditorOption, IEditorResult } from './interface/Editor' +import { IElement } from './interface/Element' +import { Draw } from './core/draw/Draw' +import { Command } from './core/command/Command' +import { CommandAdapt } from './core/command/CommandAdapt' +import { Listener } from './core/listener/Listener' +import { RowFlex } from './dataset/enum/Row' +import { + FlexDirection, + ImageDisplay, + LocationPosition +} from './dataset/enum/Common' +import { ElementType } from './dataset/enum/Element' +import { formatElementList } from './utils/element' +import { Register } from './core/register/Register' +import { ContextMenu } from './core/contextmenu/ContextMenu' +import { + IContextMenuContext, + IRegisterContextMenu +} from './interface/contextmenu/ContextMenu' +import { + EditorComponent, + EditorZone, + EditorMode, + PageMode, + PaperDirection, + WordBreak, + RenderMode +} from './dataset/enum/Editor' +import { EDITOR_CLIPBOARD, EDITOR_COMPONENT } from './dataset/constant/Editor' +import { IWatermark } from './interface/Watermark' +import { + ControlComponent, + ControlIndentation, + ControlState, + ControlType +} from './dataset/enum/Control' +import { INavigateInfo } from './core/draw/interactive/Search' +import { Shortcut } from './core/shortcut/Shortcut' +import { KeyMap } from './dataset/enum/KeyMap' +import { BlockType } from './dataset/enum/Block' +import { IBlock } from './interface/Block' +import { ILang } from './interface/i18n/I18n' +import { VerticalAlign } from './dataset/enum/VerticalAlign' +import { TableBorder, TdBorder, TdSlash } from './dataset/enum/table/Table' +import { MaxHeightRatio, NumberType } from './dataset/enum/Common' +import { TitleLevel } from './dataset/enum/Title' +import { ListStyle, ListType } from './dataset/enum/List' +import { ICatalog, ICatalogItem } from './interface/Catalog' +import { Plugin } from './core/plugin/Plugin' +import { UsePlugin } from './interface/Plugin' +import { EventBus } from './core/event/eventbus/EventBus' +import { EventBusMap } from './interface/EventBus' +import { IRangeStyle } from './interface/Listener' +import { Override } from './core/override/Override' +import { LETTER_CLASS } from './dataset/constant/Common' +import { INTERNAL_CONTEXT_MENU_KEY } from './dataset/constant/ContextMenu' +import { IRange } from './interface/Range' +import { deepClone, splitText } from './utils' +import { + createDomFromElementList, + getElementListByHTML, + getTextFromElementList, + type IGetElementListByHTMLOption +} from './utils/element' +import { BackgroundRepeat, BackgroundSize } from './dataset/enum/Background' +import { TextDecorationStyle } from './dataset/enum/Text' +import { mergeOption } from './utils/option' +import { LineNumberType } from './dataset/enum/LineNumber' +import { AreaMode } from './dataset/enum/Area' +import { IBadge } from './interface/Badge' +import { WatermarkType } from './dataset/enum/Watermark' +import { INTERNAL_SHORTCUT_KEY } from './dataset/constant/Shortcut' + +export default class Editor { + public command: Command + public listener: Listener + public eventBus: EventBus + public override: Override + public register: Register + public destroy: () => void + public use: UsePlugin + + constructor( + container: HTMLDivElement, + data: IEditorData | IElement[], + options: IEditorOption = {} + ) { + // 合并配置 + const editorOptions = mergeOption(options) + // 数据处理 + data = deepClone(data) + let headerElementList: IElement[] = [] + let mainElementList: IElement[] = [] + let footerElementList: IElement[] = [] + if (Array.isArray(data)) { + mainElementList = data + } else { + headerElementList = data.header || [] + mainElementList = data.main + footerElementList = data.footer || [] + } + const pageComponentData = [ + headerElementList, + mainElementList, + footerElementList + ] + pageComponentData.forEach(elementList => { + formatElementList(elementList, { + editorOptions, + isForceCompensation: true + }) + }) + // 监听 + this.listener = new Listener() + // 事件 + this.eventBus = new EventBus() + // 重写 + this.override = new Override() + // 启动 + const draw = new Draw( + container, + editorOptions, + { + header: headerElementList, + main: mainElementList, + footer: footerElementList + }, + this.listener, + this.eventBus, + this.override + ) + // 命令 + this.command = new Command(new CommandAdapt(draw)) + // 菜单 + const contextMenu = new ContextMenu(draw, this.command) + // 快捷键 + const shortcut = new Shortcut(draw, this.command) + // 注册 + this.register = new Register({ + contextMenu, + shortcut, + i18n: draw.getI18n() + }) + // 注册销毁方法 + this.destroy = () => { + draw.destroy() + shortcut.removeEvent() + contextMenu.removeEvent() + } + // 插件 + const plugin = new Plugin(this) + this.use = plugin.use.bind(plugin) + } +} + +// 对外方法 +export { + splitText, + createDomFromElementList, + getElementListByHTML, + getTextFromElementList +} + +// 对外常量 +export { + EDITOR_COMPONENT, + LETTER_CLASS, + INTERNAL_CONTEXT_MENU_KEY, + INTERNAL_SHORTCUT_KEY, + EDITOR_CLIPBOARD +} + +// 对外枚举 +export { + Editor, + RowFlex, + VerticalAlign, + EditorZone, + EditorMode, + ElementType, + ControlType, + EditorComponent, + PageMode, + RenderMode, + ImageDisplay, + Command, + KeyMap, + BlockType, + PaperDirection, + TableBorder, + TdBorder, + TdSlash, + MaxHeightRatio, + NumberType, + TitleLevel, + ListType, + ListStyle, + WordBreak, + ControlIndentation, + ControlComponent, + BackgroundRepeat, + BackgroundSize, + TextDecorationStyle, + LineNumberType, + LocationPosition, + AreaMode, + ControlState, + FlexDirection, + WatermarkType +} + +// 对外类型 +export type { + IElement, + IEditorData, + IEditorOption, + IEditorResult, + IContextMenuContext, + IRegisterContextMenu, + IWatermark, + INavigateInfo, + IBlock, + ILang, + ICatalog, + ICatalogItem, + IRange, + IRangeStyle, + IBadge, + IGetElementListByHTMLOption +} diff --git a/src/editor/interface/Area.ts b/src/editor/interface/Area.ts new file mode 100644 index 0000000..df53d52 --- /dev/null +++ b/src/editor/interface/Area.ts @@ -0,0 +1,66 @@ +import { AreaMode } from '../dataset/enum/Area' +import { LocationPosition } from '../dataset/enum/Common' +import { IElement, IElementPosition } from './Element' +import { IPlaceholder } from './Placeholder' +import { IRange } from './Range' + +export interface IAreaBasic { + extension?: unknown + placeholder?: IPlaceholder +} + +export interface IAreaStyle { + top?: number + borderColor?: string + backgroundColor?: string +} + +export interface IAreaRule { + mode?: AreaMode + hide?: boolean + deletable?: boolean +} + +export type IArea = IAreaBasic & IAreaStyle & IAreaRule + +export interface IInsertAreaOption { + id?: string + area: IArea + value: IElement[] + position?: LocationPosition + range?: Pick +} + +export interface ISetAreaValueOption { + id?: string + value: IElement[] +} + +export interface ISetAreaPropertiesOption { + id?: string + properties: IArea +} + +export interface IGetAreaValueOption { + id?: string +} + +export interface IGetAreaValueResult { + id?: string + area: IArea + startPageNo: number + endPageNo: number + value: IElement[] +} + +export interface IAreaInfo { + id: string + area: IArea + elementList: IElement[] + positionList: IElementPosition[] +} + +export interface ILocationAreaOption { + position: LocationPosition + isAppendLastLineBreak?: boolean +} diff --git a/src/editor/interface/Background.ts b/src/editor/interface/Background.ts new file mode 100644 index 0000000..58b3e26 --- /dev/null +++ b/src/editor/interface/Background.ts @@ -0,0 +1,9 @@ +import { BackgroundRepeat, BackgroundSize } from '../dataset/enum/Background' + +export interface IBackgroundOption { + color?: string + image?: string + size?: BackgroundSize + repeat?: BackgroundRepeat + applyPageNumbers?: number[] +} diff --git a/src/editor/interface/Badge.ts b/src/editor/interface/Badge.ts new file mode 100644 index 0000000..3c9d116 --- /dev/null +++ b/src/editor/interface/Badge.ts @@ -0,0 +1,17 @@ +export interface IBadge { + top?: number + left?: number + width: number + height: number + value: string +} + +export interface IBadgeOption { + top?: number + left?: number +} + +export interface IAreaBadge { + areaId: string + badge: IBadge +} diff --git a/src/editor/interface/Block.ts b/src/editor/interface/Block.ts new file mode 100644 index 0000000..4145728 --- /dev/null +++ b/src/editor/interface/Block.ts @@ -0,0 +1,16 @@ +import { BlockType } from '../dataset/enum/Block' + +export interface IIFrameBlock { + src?: string + srcdoc?: string +} + +export interface IVideoBlock { + src: string +} + +export interface IBlock { + type: BlockType + iframeBlock?: IIFrameBlock + videoBlock?: IVideoBlock +} diff --git a/src/editor/interface/Catalog.ts b/src/editor/interface/Catalog.ts new file mode 100644 index 0000000..f1ac618 --- /dev/null +++ b/src/editor/interface/Catalog.ts @@ -0,0 +1,11 @@ +import { TitleLevel } from '../dataset/enum/Title' + +export interface ICatalogItem { + id: string + name: string + level: TitleLevel + pageNo: number + subCatalog: ICatalogItem[] +} + +export type ICatalog = ICatalogItem[] diff --git a/src/editor/interface/Checkbox.ts b/src/editor/interface/Checkbox.ts new file mode 100644 index 0000000..456bd24 --- /dev/null +++ b/src/editor/interface/Checkbox.ts @@ -0,0 +1,17 @@ +import { VerticalAlign } from '../dataset/enum/VerticalAlign' + +export interface ICheckbox { + value: boolean | null + code?: string + disabled?: boolean +} + +export interface ICheckboxOption { + width?: number + height?: number + gap?: number + lineWidth?: number + fillStyle?: string + strokeStyle?: string + verticalAlign?: VerticalAlign +} diff --git a/src/editor/interface/Command.ts b/src/editor/interface/Command.ts new file mode 100644 index 0000000..4074338 --- /dev/null +++ b/src/editor/interface/Command.ts @@ -0,0 +1,3 @@ +export interface IRichtextOption { + isIgnoreDisabledRule: boolean +} diff --git a/src/editor/interface/Common.ts b/src/editor/interface/Common.ts new file mode 100644 index 0000000..1914ddb --- /dev/null +++ b/src/editor/interface/Common.ts @@ -0,0 +1,43 @@ +export type Primitive = + | string + | number + | boolean + | bigint + | symbol + | undefined + | null + +export type Builtin = Primitive | Function | Date | Error | RegExp + +export type DeepRequired = T extends Error + ? Required + : T extends Builtin + ? T + : T extends Map + ? Map, DeepRequired> + : T extends ReadonlyMap + ? ReadonlyMap, DeepRequired> + : T extends WeakMap + ? WeakMap, DeepRequired> + : T extends Set + ? Set> + : T extends ReadonlySet + ? ReadonlySet> + : T extends WeakSet + ? WeakSet> + : T extends Promise + ? Promise> + : T extends {} + ? { [K in keyof T]-?: DeepRequired } + : Required + +export type DeepPartial = { + [P in keyof T]?: DeepPartial +} + +export type IPadding = [ + top: number, + right: number, + bottom: number, + left: number +] diff --git a/src/editor/interface/Control.ts b/src/editor/interface/Control.ts new file mode 100644 index 0000000..c5c9d5e --- /dev/null +++ b/src/editor/interface/Control.ts @@ -0,0 +1,243 @@ +import { FlexDirection, LocationPosition } from '../dataset/enum/Common' +import { + ControlType, + ControlIndentation, + ControlState +} from '../dataset/enum/Control' +import { EditorZone } from '../dataset/enum/Editor' +import { MoveDirection } from '../dataset/enum/Observer' +import { RowFlex } from '../dataset/enum/Row' +import { IDrawOption } from './Draw' +import { IElement } from './Element' +import { IPositionContext } from './Position' +import { IRange } from './Range' +import { IRow, IRowElement } from './Row' + +export interface IValueSet { + value: string + code: string +} + +export interface IControlSelect { + code: string | null + valueSets: IValueSet[] + isMultiSelect?: boolean + multiSelectDelimiter?: string + selectExclusiveOptions?: { + inputAble?: boolean + } +} + +export interface IControlCheckbox { + code: string | null + min?: number + max?: number + flexDirection: FlexDirection + valueSets: IValueSet[] +} + +export interface IControlRadio { + code: string | null + flexDirection: FlexDirection + valueSets: IValueSet[] +} + +export interface IControlDate { + dateFormat?: string +} + +export interface IControlHighlightRule { + keyword: string + alpha?: number + backgroundColor?: string +} + +export interface IControlHighlight { + ruleList: IControlHighlightRule[] + id?: string + conceptId?: string +} + +export interface IControlRule { + deletable?: boolean + disabled?: boolean + pasteDisabled?: boolean + hide?: boolean +} + +export interface IControlBasic { + type: ControlType + value: IElement[] | null + placeholder?: string + conceptId?: string + groupId?: string + prefix?: string + postfix?: string + minWidth?: number + underline?: boolean + border?: boolean + extension?: unknown + indentation?: ControlIndentation + rowFlex?: RowFlex + preText?: string + postText?: string +} + +export interface IControlStyle { + font?: string + size?: number + bold?: boolean + highlight?: string + italic?: boolean + strikeout?: boolean +} + +export type IControl = IControlBasic & + IControlRule & + Partial & + Partial & + Partial & + Partial & + Partial + +export interface IControlOption { + placeholderColor?: string + bracketColor?: string + prefix?: string + postfix?: string + borderWidth?: number + borderColor?: string + activeBackgroundColor?: string + disabledBackgroundColor?: string + existValueBackgroundColor?: string + noValueBackgroundColor?: string +} + +export interface IControlInitOption { + index: number + isTable?: boolean + trIndex?: number + tdIndex?: number + tdValueIndex?: number +} + +export interface IControlInitResult { + newIndex: number +} + +export interface IControlInstance { + setElement(element: IElement): void + getElement(): IElement + getValue(context?: IControlContext): IElement[] + setValue( + data: IElement[], + context?: IControlContext, + options?: IControlRuleOption + ): number + keydown(evt: KeyboardEvent): number | null + cut(): number +} + +export interface IControlContext { + range?: IRange + elementList?: IElement[] +} + +export interface IControlRuleOption { + isIgnoreDisabledRule?: boolean // 忽略禁用校验规则 + isIgnoreDeletedRule?: boolean // 忽略删除校验规则 + isAddPlaceholder?: boolean // 是否添加占位符 +} + +export interface IGetControlValueOption { + id?: string + groupId?: string + conceptId?: string + areaId?: string +} + +export type IGetControlValueResult = (Omit & { + value: string | null + innerText: string | null + zone: EditorZone + elementList?: IElement[] +})[] + +export interface ISetControlValueOption { + id?: string + groupId?: string + conceptId?: string + areaId?: string + value: string | IElement[] | null + isSubmitHistory?: boolean +} + +export interface ISetControlExtensionOption { + id?: string + groupId?: string + conceptId?: string + areaId?: string + extension: unknown +} + +export type ISetControlHighlightOption = IControlHighlight[] + +export type ISetControlProperties = { + id?: string + groupId?: string + conceptId?: string + areaId?: string + properties: Partial> + isSubmitHistory?: boolean +} + +export type IRepaintControlOption = Pick< + IDrawOption, + 'curIndex' | 'isCompute' | 'isSubmitHistory' | 'isSetCursor' +> + +export interface IControlChangeOption { + context?: IControlContext + controlElement?: IElement + controlValue?: IElement[] +} + +export interface INextControlContext { + positionContext: IPositionContext + nextIndex: number +} + +export interface IInitNextControlOption { + direction?: MoveDirection +} + +export interface ILocationControlOption { + position: LocationPosition +} + +export interface ISetControlRowFlexOption { + row: IRow + rowElement: IRowElement + availableWidth: number + controlRealWidth: number +} + +export interface IControlChangeResult { + state: ControlState + control: IControl + controlId: string +} + +export interface IControlContentChangeResult { + control: IControl + controlId: string +} + +export interface IDestroyControlOption { + isEmitEvent?: boolean +} + +export interface IRemoveControlOption { + id?: string + conceptId?: string +} diff --git a/src/editor/interface/Cursor.ts b/src/editor/interface/Cursor.ts new file mode 100644 index 0000000..88343bb --- /dev/null +++ b/src/editor/interface/Cursor.ts @@ -0,0 +1,7 @@ +export interface ICursorOption { + width?: number + color?: string + dragWidth?: number + dragColor?: string + dragFloatImageDisabled?: boolean +} diff --git a/src/editor/interface/Draw.ts b/src/editor/interface/Draw.ts new file mode 100644 index 0000000..1d1d995 --- /dev/null +++ b/src/editor/interface/Draw.ts @@ -0,0 +1,85 @@ +import { ImageDisplay } from '../dataset/enum/Common' +import { EditorMode, EditorZone } from '../dataset/enum/Editor' +import { IElement, IElementPosition } from './Element' +import { IRow } from './Row' + +export interface IDrawOption { + curIndex?: number + isSetCursor?: boolean + isSubmitHistory?: boolean + isCompute?: boolean + isLazy?: boolean + isInit?: boolean + isSourceHistory?: boolean + isFirstRender?: boolean +} + +export interface IForceUpdateOption { + isSubmitHistory?: boolean +} + +export interface IDrawImagePayload { + id?: string + conceptId?: string + width: number + height: number + value: string + imgDisplay?: ImageDisplay + extension?: unknown +} + +export interface IDrawRowPayload { + elementList: IElement[] + positionList: IElementPosition[] + rowList: IRow[] + pageNo: number + startIndex: number + innerWidth: number + zone?: EditorZone + isDrawLineBreak?: boolean +} + +export interface IDrawFloatPayload { + pageNo: number + imgDisplays: ImageDisplay[] +} + +export interface IDrawPagePayload { + elementList: IElement[] + positionList: IElementPosition[] + rowList: IRow[] + pageNo: number +} + +export interface IPainterOption { + isDblclick: boolean +} + +export interface IGetValueOption { + pageNo?: number + extraPickAttrs?: Array +} + +export type IGetOriginValueOption = Omit + +export interface IAppendElementListOption { + isPrepend?: boolean + isSubmitHistory?: boolean +} + +export interface IGetImageOption { + pixelRatio?: number + mode?: EditorMode +} + +export interface IComputeRowListPayload { + innerWidth: number + elementList: IElement[] + startX?: number + startY?: number + isFromTable?: boolean + isPagingMode?: boolean + pageHeight?: number + mainOuterHeight?: number + surroundElementList?: IElement[] +} diff --git a/src/editor/interface/Editor.ts b/src/editor/interface/Editor.ts new file mode 100644 index 0000000..6c4ead5 --- /dev/null +++ b/src/editor/interface/Editor.ts @@ -0,0 +1,161 @@ +import { + EditorMode, + PageMode, + PaperDirection, + RenderMode, + WordBreak +} from '../dataset/enum/Editor' +import { IBackgroundOption } from './Background' +import { ICheckboxOption } from './Checkbox' +import { IRadioOption } from './Radio' +import { IControlOption } from './Control' +import { ICursorOption } from './Cursor' +import { IFooter } from './Footer' +import { IGroup } from './Group' +import { IHeader } from './Header' +import { ILineBreakOption } from './LineBreak' +import { IMargin } from './Margin' +import { IPageBreak } from './PageBreak' +import { IPageNumber } from './PageNumber' +import { IPlaceholder } from './Placeholder' +import { ITitleOption } from './Title' +import { IWatermark } from './Watermark' +import { IZoneOption } from './Zone' +import { ISeparatorOption } from './Separator' +import { ITableOption } from './table/Table' +import { ILineNumberOption } from './LineNumber' +import { IPageBorderOption } from './PageBorder' +import { IBadgeOption } from './Badge' +import { IElement } from './Element' +import { LocationPosition } from '../dataset/enum/Common' +import { IRange } from './Range' + +export interface IEditorData { + header?: IElement[] + main: IElement[] + footer?: IElement[] +} + +export interface IEditorOption { + mode?: EditorMode + locale?: string + defaultType?: string + defaultColor?: string + defaultFont?: string + defaultSize?: number + minSize?: number + maxSize?: number + defaultBasicRowMarginHeight?: number + defaultRowMargin?: number + defaultTabWidth?: number + width?: number + height?: number + scale?: number + pageGap?: number + underlineColor?: string + strikeoutColor?: string + rangeColor?: string + rangeAlpha?: number + rangeMinWidth?: number + searchMatchColor?: string + searchNavigateMatchColor?: string + searchMatchAlpha?: number + highlightAlpha?: number + highlightMarginHeight?: number + resizerColor?: string + resizerSize?: number + marginIndicatorSize?: number + marginIndicatorColor?: string + margins?: IMargin + pageMode?: PageMode + renderMode?: RenderMode + defaultHyperlinkColor?: string + paperDirection?: PaperDirection + inactiveAlpha?: number + historyMaxRecordCount?: number + printPixelRatio?: number + maskMargin?: IMargin + letterClass?: string[] + contextMenuDisableKeys?: string[] + shortcutDisableKeys?: string[] + scrollContainerSelector?: string + pageOuterSelectionDisable?: boolean + wordBreak?: WordBreak + table?: ITableOption + header?: IHeader + footer?: IFooter + pageNumber?: IPageNumber + watermark?: IWatermark + control?: IControlOption + checkbox?: ICheckboxOption + radio?: IRadioOption + cursor?: ICursorOption + title?: ITitleOption + placeholder?: IPlaceholder + group?: IGroup + pageBreak?: IPageBreak + zone?: IZoneOption + background?: IBackgroundOption + lineBreak?: ILineBreakOption + separator?: ISeparatorOption + lineNumber?: ILineNumberOption + pageBorder?: IPageBorderOption + badge?: IBadgeOption + modeRule?: IModeRule +} + +export interface IEditorResult { + version: string + data: IEditorData + options: IEditorOption +} + +export interface IEditorHTML { + header: string + main: string + footer: string +} + +export type IEditorText = IEditorHTML + +export type IUpdateOption = Omit< + IEditorOption, + | 'mode' + | 'width' + | 'height' + | 'scale' + | 'pageGap' + | 'pageMode' + | 'paperDirection' + | 'historyMaxRecordCount' + | 'scrollContainerSelector' +> + +export interface ISetValueOption { + isSetCursor?: boolean +} + +export interface IFocusOption { + rowNo?: number + range?: IRange + position?: LocationPosition + isMoveCursorToVisible?: boolean +} + +export interface IPrintModeRule { + imagePreviewerDisabled?: boolean +} + +export interface IReadonlyModeRule { + imagePreviewerDisabled?: boolean +} + +export interface IFormModeRule { + controlDeletableDisabled?: boolean +} + +export interface IModeRule { + [EditorMode.PRINT]?: IPrintModeRule + [EditorMode.READONLY]?: IReadonlyModeRule + [EditorMode.FORM]?: IFormModeRule +} diff --git a/src/editor/interface/Element.ts b/src/editor/interface/Element.ts new file mode 100644 index 0000000..b2fe411 --- /dev/null +++ b/src/editor/interface/Element.ts @@ -0,0 +1,229 @@ +import { ImageDisplay } from '../dataset/enum/Common' +import { ControlComponent } from '../dataset/enum/Control' +import { ElementType } from '../dataset/enum/Element' +import { ListStyle, ListType } from '../dataset/enum/List' +import { RowFlex } from '../dataset/enum/Row' +import { TitleLevel } from '../dataset/enum/Title' +import { TableBorder } from '../dataset/enum/table/Table' +import { IArea } from './Area' +import { IBlock } from './Block' +import { ICheckbox } from './Checkbox' +import { IControl } from './Control' +import { IRadio } from './Radio' +import { ITextDecoration } from './Text' +import { ITitle } from './Title' +import { IColgroup } from './table/Colgroup' +import { ITr } from './table/Tr' + +export interface IElementBasic { + id?: string + type?: ElementType + value: string + extension?: unknown + externalId?: string +} + +export interface IElementStyle { + font?: string + size?: number + width?: number + height?: number + bold?: boolean + color?: string + highlight?: string + italic?: boolean + underline?: boolean + strikeout?: boolean + rowFlex?: RowFlex + rowMargin?: number + letterSpacing?: number + textDecoration?: ITextDecoration +} + +export interface IElementRule { + hide?: boolean +} + +export interface IElementGroup { + groupIds?: string[] +} + +export interface ITitleElement { + valueList?: IElement[] + level?: TitleLevel + titleId?: string + title?: ITitle +} + +export interface IListElement { + valueList?: IElement[] + listType?: ListType + listStyle?: ListStyle + listId?: string + listWrap?: boolean +} + +export interface ITableAttr { + colgroup?: IColgroup[] + trList?: ITr[] + borderType?: TableBorder + borderColor?: string + borderWidth?: number + borderExternalWidth?: number +} + +export interface ITableRule { + tableToolDisabled?: boolean +} + +export interface ITableElement { + tdId?: string + trId?: string + tableId?: string + conceptId?: string + pagingId?: string // 用于区分拆分的表格同属一个源表格 + pagingIndex?: number // 拆分的表格索引 +} + +export type ITable = ITableAttr & ITableRule & ITableElement + +export interface IHyperlinkElement { + valueList?: IElement[] + url?: string + hyperlinkId?: string +} + +export interface ISuperscriptSubscript { + actualSize?: number +} + +export interface ISeparator { + dashArray?: number[] +} + +export interface IControlElement { + control?: IControl + controlId?: string + controlComponent?: ControlComponent +} + +export interface ICheckboxElement { + checkbox?: ICheckbox +} + +export interface IRadioElement { + radio?: IRadio +} + +export interface ILaTexElement { + laTexSVG?: string +} + +export interface IDateElement { + dateFormat?: string + dateId?: string +} + +export interface IImageRule { + imgToolDisabled?: boolean +} + +export interface IImageBasic { + imgDisplay?: ImageDisplay + imgFloatPosition?: { + x: number + y: number + pageNo?: number + } +} + +export type IImageElement = IImageBasic & IImageRule + +export interface IBlockElement { + block?: IBlock +} + +export interface IAreaElement { + valueList?: IElement[] + areaId?: string + areaIndex?: number + area?: IArea +} + +export type IElement = IElementBasic & + IElementStyle & + IElementRule & + IElementGroup & + ITable & + IHyperlinkElement & + ISuperscriptSubscript & + ISeparator & + IControlElement & + ICheckboxElement & + IRadioElement & + ILaTexElement & + IDateElement & + IImageElement & + IBlockElement & + ITitleElement & + IListElement & + IAreaElement + +export interface IElementMetrics { + width: number + height: number + boundingBoxAscent: number + boundingBoxDescent: number +} + +export interface IElementPosition { + pageNo: number + index: number + value: string + rowIndex: number + rowNo: number + ascent: number + lineHeight: number + left: number + metrics: IElementMetrics + isFirstLetter: boolean + isLastLetter: boolean + coordinate: { + leftTop: number[] + leftBottom: number[] + rightTop: number[] + rightBottom: number[] + } +} + +export interface IElementFillRect { + x: number + y: number + width: number + height: number +} + +export interface IUpdateElementByIdOption { + id?: string + conceptId?: string + properties: Omit, 'id'> +} + +export interface IDeleteElementByIdOption { + id?: string + conceptId?: string +} + +export interface IGetElementByIdOption { + id?: string + conceptId?: string +} + +export interface IInsertElementListOption { + isReplace?: boolean + isSubmitHistory?: boolean +} + +export interface ISpliceElementListOption { + isIgnoreDeletedRule?: boolean +} diff --git a/src/editor/interface/Event.ts b/src/editor/interface/Event.ts new file mode 100644 index 0000000..c72c191 --- /dev/null +++ b/src/editor/interface/Event.ts @@ -0,0 +1,27 @@ +import { IElement } from './Element' +import { RangeRect } from './Range' + +export interface IPasteOption { + isPlainText: boolean +} + +export interface ITableInfoByEvent { + element: IElement + trIndex: number + tdIndex: number +} + +export interface IPositionContextByEventResult { + pageNo: number + element: IElement | null + rangeRect: RangeRect | null + tableInfo: ITableInfoByEvent | null +} + +export interface IPositionContextByEventOption { + isMustDirectHit?: boolean +} + +export interface ICopyOption { + isPlainText: boolean +} diff --git a/src/editor/interface/EventBus.ts b/src/editor/interface/EventBus.ts new file mode 100644 index 0000000..c663eff --- /dev/null +++ b/src/editor/interface/EventBus.ts @@ -0,0 +1,42 @@ +import { + IContentChange, + IControlChange, + IControlContentChange, + IImageMousedown, + IImageSizeChange, + IInputEventChange, + IIntersectionPageNoChange, + IMouseEventChange, + IPageModeChange, + IPageScaleChange, + IPageSizeChange, + IPositionContextChange, + IRangeStyleChange, + ISaved, + IVisiblePageNoListChange, + IZoneChange +} from './Listener' + +export interface EventBusMap { + rangeStyleChange: IRangeStyleChange + visiblePageNoListChange: IVisiblePageNoListChange + intersectionPageNoChange: IIntersectionPageNoChange + pageSizeChange: IPageSizeChange + pageScaleChange: IPageScaleChange + saved: ISaved + contentChange: IContentChange + controlChange: IControlChange + controlContentChange: IControlContentChange + pageModeChange: IPageModeChange + zoneChange: IZoneChange + mousemove: IMouseEventChange + mouseleave: IMouseEventChange + mouseenter: IMouseEventChange + mousedown: IMouseEventChange + mouseup: IMouseEventChange + click: IMouseEventChange + input: IInputEventChange + positionContextChange: IPositionContextChange + imageSizeChange: IImageSizeChange + imageMousedown: IImageMousedown +} diff --git a/src/editor/interface/Footer.ts b/src/editor/interface/Footer.ts new file mode 100644 index 0000000..2cf02c8 --- /dev/null +++ b/src/editor/interface/Footer.ts @@ -0,0 +1,9 @@ +import { MaxHeightRatio } from '../dataset/enum/Common' + +export interface IFooter { + bottom?: number + inactiveAlpha?: number + maxHeightRadio?: MaxHeightRatio + disabled?: boolean + editable?: boolean +} diff --git a/src/editor/interface/Group.ts b/src/editor/interface/Group.ts new file mode 100644 index 0000000..84d898c --- /dev/null +++ b/src/editor/interface/Group.ts @@ -0,0 +1,8 @@ +export interface IGroup { + opacity?: number + backgroundColor?: string + activeOpacity?: number + activeBackgroundColor?: string + disabled?: boolean + deletable?: boolean +} diff --git a/src/editor/interface/Header.ts b/src/editor/interface/Header.ts new file mode 100644 index 0000000..b035388 --- /dev/null +++ b/src/editor/interface/Header.ts @@ -0,0 +1,9 @@ +import { MaxHeightRatio } from '../dataset/enum/Common' + +export interface IHeader { + top?: number + inactiveAlpha?: number + maxHeightRadio?: MaxHeightRatio + disabled?: boolean + editable?: boolean +} diff --git a/src/editor/interface/LineBreak.ts b/src/editor/interface/LineBreak.ts new file mode 100644 index 0000000..16625df --- /dev/null +++ b/src/editor/interface/LineBreak.ts @@ -0,0 +1,5 @@ +export interface ILineBreakOption { + disabled?: boolean + color?: string + lineWidth?: number +} diff --git a/src/editor/interface/LineNumber.ts b/src/editor/interface/LineNumber.ts new file mode 100644 index 0000000..7b4dfe1 --- /dev/null +++ b/src/editor/interface/LineNumber.ts @@ -0,0 +1,10 @@ +import { LineNumberType } from '../dataset/enum/LineNumber' + +export interface ILineNumberOption { + size?: number + font?: string + color?: string + disabled?: boolean + right?: number + type?: LineNumberType +} diff --git a/src/editor/interface/Listener.ts b/src/editor/interface/Listener.ts new file mode 100644 index 0000000..a1204d7 --- /dev/null +++ b/src/editor/interface/Listener.ts @@ -0,0 +1,78 @@ +import { EditorZone, PageMode } from '../dataset/enum/Editor' +import { ElementType } from '../dataset/enum/Element' +import { ListStyle, ListType } from '../dataset/enum/List' +import { RowFlex } from '../dataset/enum/Row' +import { TitleLevel } from '../dataset/enum/Title' +import { IControlChangeResult, IControlContentChangeResult } from './Control' +import { IEditorResult } from './Editor' +import { IElement } from './Element' +import { IPositionContext } from './Position' +import { ITextDecoration } from './Text' + +export interface IRangeStyle { + type: ElementType | null + undo: boolean + redo: boolean + painter: boolean + font: string + size: number + bold: boolean + italic: boolean + underline: boolean + strikeout: boolean + color: string | null + highlight: string | null + rowFlex: RowFlex | null + rowMargin: number + dashArray: number[] + level: TitleLevel | null + listType: ListType | null + listStyle: ListStyle | null + groupIds: string[] | null + textDecoration: ITextDecoration | null + extension?: unknown | null +} + +export type IRangeStyleChange = (payload: IRangeStyle) => void + +export type IVisiblePageNoListChange = (payload: number[]) => void + +export type IIntersectionPageNoChange = (payload: number) => void + +export type IPageSizeChange = (payload: number) => void + +export type IPageScaleChange = (payload: number) => void + +export type ISaved = (payload: IEditorResult) => void + +export type IContentChange = () => void + +export type IControlChange = (payload: IControlChangeResult) => void + +export type IControlContentChange = ( + payload: IControlContentChangeResult +) => void + +export type IPageModeChange = (payload: PageMode) => void + +export type IZoneChange = (payload: EditorZone) => void + +export type IMouseEventChange = (evt: MouseEvent) => void + +export type IInputEventChange = (evt: Event) => void + +export interface IPositionContextChangePayload { + value: IPositionContext + oldValue: IPositionContext +} + +export type IPositionContextChange = ( + payload: IPositionContextChangePayload +) => void + +export type IImageSizeChange = (payload: { element: IElement }) => void + +export type IImageMousedown = (payload: { + evt: MouseEvent + element: IElement +}) => void diff --git a/src/editor/interface/Margin.ts b/src/editor/interface/Margin.ts new file mode 100644 index 0000000..e4d995d --- /dev/null +++ b/src/editor/interface/Margin.ts @@ -0,0 +1 @@ +export type IMargin = [top: number, right: number, bottom: number, left: number] diff --git a/src/editor/interface/PageBorder.ts b/src/editor/interface/PageBorder.ts new file mode 100644 index 0000000..1fe8d25 --- /dev/null +++ b/src/editor/interface/PageBorder.ts @@ -0,0 +1,8 @@ +import { IPadding } from './Common' + +export interface IPageBorderOption { + color?: string + lineWidth?: number + padding?: IPadding + disabled?: boolean +} diff --git a/src/editor/interface/PageBreak.ts b/src/editor/interface/PageBreak.ts new file mode 100644 index 0000000..683b27d --- /dev/null +++ b/src/editor/interface/PageBreak.ts @@ -0,0 +1,5 @@ +export interface IPageBreak { + font?: string + fontSize?: number + lineDash?: number[] +} diff --git a/src/editor/interface/PageNumber.ts b/src/editor/interface/PageNumber.ts new file mode 100644 index 0000000..4f669a9 --- /dev/null +++ b/src/editor/interface/PageNumber.ts @@ -0,0 +1,16 @@ +import { NumberType } from '../dataset/enum/Common' +import { RowFlex } from '../dataset/enum/Row' + +export interface IPageNumber { + bottom?: number + size?: number + font?: string + color?: string + rowFlex?: RowFlex + format?: string + numberType?: NumberType + disabled?: boolean + startPageNo?: number + fromPageNo?: number + maxPageNo?: number | null +} diff --git a/src/editor/interface/Placeholder.ts b/src/editor/interface/Placeholder.ts new file mode 100644 index 0000000..e226c00 --- /dev/null +++ b/src/editor/interface/Placeholder.ts @@ -0,0 +1,7 @@ +export interface IPlaceholder { + data: string + color?: string + opacity?: number + size?: number + font?: string +} diff --git a/src/editor/interface/Plugin.ts b/src/editor/interface/Plugin.ts new file mode 100644 index 0000000..f0f2210 --- /dev/null +++ b/src/editor/interface/Plugin.ts @@ -0,0 +1,8 @@ +import Editor from '..' + +export type PluginFunction = (editor: Editor, options?: Options) => any + +export type UsePlugin = ( + pluginFunction: PluginFunction, + options?: Options +) => void diff --git a/src/editor/interface/Position.ts b/src/editor/interface/Position.ts new file mode 100644 index 0000000..13d309f --- /dev/null +++ b/src/editor/interface/Position.ts @@ -0,0 +1,111 @@ +import { ImageDisplay } from '../dataset/enum/Common' +import { EditorZone } from '../dataset/enum/Editor' +import { IElement, IElementFillRect, IElementPosition } from './Element' +import { IRange } from './Range' +import { IRow, IRowElement } from './Row' +import { ITd } from './table/Td' + +export interface ICurrentPosition { + index: number + x?: number + y?: number + isCheckbox?: boolean + isRadio?: boolean + isControl?: boolean + isImage?: boolean + isTable?: boolean + isDirectHit?: boolean + trIndex?: number + tdIndex?: number + tdValueIndex?: number + tdId?: string + trId?: string + tableId?: string + zone?: EditorZone + hitLineStartIndex?: number +} + +export interface IGetPositionByXYPayload { + x: number + y: number + pageNo?: number + isTable?: boolean + td?: ITd + tablePosition?: IElementPosition + elementList?: IElement[] + positionList?: IElementPosition[] +} + +export type IGetFloatPositionByXYPayload = IGetPositionByXYPayload & { + imgDisplays: ImageDisplay[] +} + +export interface IPositionContext { + isTable: boolean + isCheckbox?: boolean + isRadio?: boolean + isControl?: boolean + isImage?: boolean + isDirectHit?: boolean + index?: number + trIndex?: number + tdIndex?: number + tdId?: string + trId?: string + tableId?: string +} + +export interface IComputeRowPositionPayload { + row: IRow + innerWidth: number +} + +export interface IComputePageRowPositionPayload { + positionList: IElementPosition[] + rowList: IRow[] + pageNo: number + startRowIndex: number + startIndex: number + startX: number + startY: number + innerWidth: number + isTable?: boolean + index?: number + tdIndex?: number + trIndex?: number + tdValueIndex?: number + zone?: EditorZone +} + +export interface IComputePageRowPositionResult { + x: number + y: number + index: number +} + +export interface IFloatPosition { + pageNo: number + element: IElement + position: IElementPosition + isTable?: boolean + index?: number + tdIndex?: number + trIndex?: number + tdValueIndex?: number + zone?: EditorZone +} + +export interface ILocationPosition { + zone: EditorZone + range: IRange + positionContext: IPositionContext +} + +export interface ISetSurroundPositionPayload { + row: IRow + rowElement: IRowElement + rowElementRect: IElementFillRect + pageNo: number + availableWidth: number + surroundElementList: IElement[] +} diff --git a/src/editor/interface/Previewer.ts b/src/editor/interface/Previewer.ts new file mode 100644 index 0000000..73672a8 --- /dev/null +++ b/src/editor/interface/Previewer.ts @@ -0,0 +1,15 @@ +import { IElement } from './Element' + +export interface IPreviewerCreateResult { + resizerSelection: HTMLDivElement + resizerHandleList: HTMLDivElement[] + resizerImageContainer: HTMLDivElement + resizerImage: HTMLImageElement + resizerSize: HTMLSpanElement +} + +export interface IPreviewerDrawOption { + mime?: 'png' | 'jpg' | 'jpeg' | 'svg' + srcKey?: keyof Pick + dragDisable?: boolean +} diff --git a/src/editor/interface/Radio.ts b/src/editor/interface/Radio.ts new file mode 100644 index 0000000..95a42dc --- /dev/null +++ b/src/editor/interface/Radio.ts @@ -0,0 +1,17 @@ +import { VerticalAlign } from '../dataset/enum/VerticalAlign' + +export interface IRadio { + value: boolean | null + code?: string + disabled?: boolean +} + +export interface IRadioOption { + width?: number + height?: number + gap?: number + lineWidth?: number + fillStyle?: string + strokeStyle?: string + verticalAlign?: VerticalAlign +} diff --git a/src/editor/interface/Range.ts b/src/editor/interface/Range.ts new file mode 100644 index 0000000..2e3303c --- /dev/null +++ b/src/editor/interface/Range.ts @@ -0,0 +1,59 @@ +import { EditorZone } from '../dataset/enum/Editor' +import { IElement, IElementFillRect, IElementStyle } from './Element' + +export interface IRange { + startIndex: number + endIndex: number + isCrossRowCol?: boolean + tableId?: string + startTdIndex?: number + endTdIndex?: number + startTrIndex?: number + endTrIndex?: number + zone?: EditorZone +} + +export type RangeRowArray = Map + +export type RangeRowMap = Map> + +export type RangeRect = IElementFillRect + +export type RangeContext = { + isCollapsed: boolean + startElement: IElement + endElement: IElement + startPageNo: number + endPageNo: number + startRowNo: number + endRowNo: number + startColNo: number + endColNo: number + rangeRects: RangeRect[] + zone: EditorZone + isTable: boolean + trIndex: number | null + tdIndex: number | null + tableElement: IElement | null + selectionText: string | null + selectionElementList: IElement[] + titleId: string | null + titleStartPageNo: number | null +} + +export interface IRangeParagraphInfo { + elementList: IElement[] + startIndex: number +} + +export type IRangeElementStyle = Pick< + IElementStyle, + | 'bold' + | 'color' + | 'highlight' + | 'font' + | 'size' + | 'italic' + | 'underline' + | 'strikeout' +> diff --git a/src/editor/interface/Row.ts b/src/editor/interface/Row.ts new file mode 100644 index 0000000..f62b6b2 --- /dev/null +++ b/src/editor/interface/Row.ts @@ -0,0 +1,25 @@ +import { RowFlex } from '../dataset/enum/Row' +import { IElement, IElementMetrics } from './Element' + +export type IRowElement = IElement & { + metrics: IElementMetrics + style: string + left?: number +} + +export interface IRow { + width: number + height: number + ascent: number + rowFlex?: RowFlex + startIndex: number + isPageBreak?: boolean + isList?: boolean + listIndex?: number + offsetX?: number + offsetY?: number + elementList: IRowElement[] + isWidthNotEnough?: boolean + rowIndex: number + isSurround?: boolean +} diff --git a/src/editor/interface/Search.ts b/src/editor/interface/Search.ts new file mode 100644 index 0000000..de2e7c0 --- /dev/null +++ b/src/editor/interface/Search.ts @@ -0,0 +1,30 @@ +import { EditorContext } from '../dataset/enum/Editor' +import { IElementPosition } from './Element' +import { IRange } from './Range' + +export interface ISearchResultBasic { + type: EditorContext + index: number + groupId: string +} + +export interface ISearchResultRestArgs { + tableId?: string + tableIndex?: number + trIndex?: number + tdIndex?: number + tdId?: string + startIndex?: number +} + +export type ISearchResult = ISearchResultBasic & ISearchResultRestArgs + +export interface ISearchResultContext { + range: IRange + startPosition: IElementPosition + endPosition: IElementPosition +} + +export interface IReplaceOption { + index?: number +} diff --git a/src/editor/interface/Separator.ts b/src/editor/interface/Separator.ts new file mode 100644 index 0000000..e0d44a7 --- /dev/null +++ b/src/editor/interface/Separator.ts @@ -0,0 +1,4 @@ +export interface ISeparatorOption { + strokeStyle?: string + lineWidth?: number +} diff --git a/src/editor/interface/Text.ts b/src/editor/interface/Text.ts new file mode 100644 index 0000000..3124dd7 --- /dev/null +++ b/src/editor/interface/Text.ts @@ -0,0 +1,15 @@ +import { TextDecorationStyle } from '../dataset/enum/Text' + +export interface ITextMetrics { + width: number + actualBoundingBoxAscent: number + actualBoundingBoxDescent: number + actualBoundingBoxLeft: number + actualBoundingBoxRight: number + fontBoundingBoxAscent: number + fontBoundingBoxDescent: number +} + +export interface ITextDecoration { + style?: TextDecorationStyle +} diff --git a/src/editor/interface/Title.ts b/src/editor/interface/Title.ts new file mode 100644 index 0000000..066aa6b --- /dev/null +++ b/src/editor/interface/Title.ts @@ -0,0 +1,32 @@ +import { EditorZone } from '../dataset/enum/Editor' +import { IElement } from './Element' + +export interface ITitleSizeOption { + defaultFirstSize?: number + defaultSecondSize?: number + defaultThirdSize?: number + defaultFourthSize?: number + defaultFifthSize?: number + defaultSixthSize?: number +} + +export type ITitleOption = ITitleSizeOption & {} + +export interface ITitleRule { + deletable?: boolean + disabled?: boolean +} + +export type ITitle = ITitleRule & { + conceptId?: string +} + +export interface IGetTitleValueOption { + conceptId: string +} + +export type IGetTitleValueResult = (ITitle & { + value: string | null + elementList: IElement[] + zone: EditorZone +})[] diff --git a/src/editor/interface/Watermark.ts b/src/editor/interface/Watermark.ts new file mode 100644 index 0000000..8b44f9e --- /dev/null +++ b/src/editor/interface/Watermark.ts @@ -0,0 +1,16 @@ +import { NumberType } from '../dataset/enum/Common' +import { WatermarkType } from '../dataset/enum/Watermark' + +export interface IWatermark { + data: string + type?: WatermarkType + width?: number + height?: number + color?: string + opacity?: number + size?: number + font?: string + repeat?: boolean + numberType?: NumberType + gap?: [horizontal: number, vertical: number] +} diff --git a/src/editor/interface/Zone.ts b/src/editor/interface/Zone.ts new file mode 100644 index 0000000..bec8627 --- /dev/null +++ b/src/editor/interface/Zone.ts @@ -0,0 +1,3 @@ +export interface IZoneOption { + tipDisabled?: boolean +} diff --git a/src/editor/interface/contextmenu/ContextMenu.ts b/src/editor/interface/contextmenu/ContextMenu.ts new file mode 100644 index 0000000..1a351c4 --- /dev/null +++ b/src/editor/interface/contextmenu/ContextMenu.ts @@ -0,0 +1,73 @@ +import { Command } from '../../core/command/Command' +import { EditorZone } from '../../dataset/enum/Editor' +import { DeepRequired } from '../Common' +import { IEditorOption } from '../Editor' +import { IElement } from '../Element' + +export interface IContextMenuContext { + startElement: IElement | null + endElement: IElement | null + isReadonly: boolean + editorHasSelection: boolean + editorTextFocus: boolean + isInTable: boolean + isCrossRowCol: boolean + zone: EditorZone + trIndex: number | null + tdIndex: number | null + tableElement: IElement | null + options: DeepRequired +} + +export interface IRegisterContextMenu { + key?: string + i18nPath?: string + isDivider?: boolean + icon?: string + name?: string + shortCut?: string + disable?: boolean + when?: (payload: IContextMenuContext) => boolean + callback?: (command: Command, context: IContextMenuContext) => any + childMenus?: IRegisterContextMenu[] +} + +export interface IContextmenuLang { + global: { + cut: string + copy: string + paste: string + selectAll: string + print: string + } + control: { + delete: string + } + hyperlink: { + delete: string + cancel: string + edit: string + } + image: { + change: string + saveAs: string + textWrap: string + textWrapType: { + embed: string + upDown: string + } + } + table: { + insertRowCol: string + insertTopRow: string + insertBottomRow: string + insertLeftCol: string + insertRightCol: string + deleteRowCol: string + deleteRow: string + deleteCol: string + deleteTable: string + mergeCell: string + mergeCancelCell: string + } +} diff --git a/src/editor/interface/i18n/I18n.ts b/src/editor/interface/i18n/I18n.ts new file mode 100644 index 0000000..f7e7a1b --- /dev/null +++ b/src/editor/interface/i18n/I18n.ts @@ -0,0 +1,7 @@ +import { IDatePickerLang } from '../../core/draw/particle/date/DatePicker' +import { IContextmenuLang } from '../contextmenu/ContextMenu' + +export interface ILang { + contextmenu: IContextmenuLang + datePicker: IDatePickerLang +} diff --git a/src/editor/interface/shortcut/Shortcut.ts b/src/editor/interface/shortcut/Shortcut.ts new file mode 100644 index 0000000..0ceb07d --- /dev/null +++ b/src/editor/interface/shortcut/Shortcut.ts @@ -0,0 +1,14 @@ +import { Command } from '../../core/command/Command' +import { KeyMap } from '../../dataset/enum/KeyMap' + +export interface IRegisterShortcut { + key: KeyMap + ctrl?: boolean + meta?: boolean + mod?: boolean // windows:ctrl || mac:command + shift?: boolean + alt?: boolean // windows:alt || mac:option + isGlobal?: boolean + callback?: (command: Command) => any + disable?: boolean +} diff --git a/src/editor/interface/table/Colgroup.ts b/src/editor/interface/table/Colgroup.ts new file mode 100644 index 0000000..2a7d467 --- /dev/null +++ b/src/editor/interface/table/Colgroup.ts @@ -0,0 +1,4 @@ +export interface IColgroup { + id?: string + width: number +} diff --git a/src/editor/interface/table/Table.ts b/src/editor/interface/table/Table.ts new file mode 100644 index 0000000..0b24428 --- /dev/null +++ b/src/editor/interface/table/Table.ts @@ -0,0 +1,8 @@ +import { IPadding } from '../Common' + +export interface ITableOption { + tdPadding?: IPadding + defaultTrMinHeight?: number + defaultColMinWidth?: number + defaultBorderColor?: string +} diff --git a/src/editor/interface/table/Td.ts b/src/editor/interface/table/Td.ts new file mode 100644 index 0000000..7431ddc --- /dev/null +++ b/src/editor/interface/table/Td.ts @@ -0,0 +1,36 @@ +import { VerticalAlign } from '../../dataset/enum/VerticalAlign' +import { TdBorder, TdSlash } from '../../dataset/enum/table/Table' +import { IElement, IElementPosition } from '../Element' +import { IRow } from '../Row' + +export interface ITd { + conceptId?: string + id?: string + extension?: unknown + externalId?: string + x?: number + y?: number + width?: number + height?: number + colspan: number + rowspan: number + value: IElement[] + trIndex?: number + tdIndex?: number + isLastRowTd?: boolean + isLastColTd?: boolean + isLastTd?: boolean + rowIndex?: number + colIndex?: number + rowList?: IRow[] + positionList?: IElementPosition[] + verticalAlign?: VerticalAlign + backgroundColor?: string + borderTypes?: TdBorder[] + slashTypes?: TdSlash[] + mainHeight?: number // 内容 + 内边距高度 + realHeight?: number // 真实高度(包含跨列) + realMinHeight?: number // 真实最小高度(包含跨列) + disabled?: boolean // 内容不可编辑 + deletable?: boolean // 内容不可删除 +} diff --git a/src/editor/interface/table/Tr.ts b/src/editor/interface/table/Tr.ts new file mode 100644 index 0000000..8e66a83 --- /dev/null +++ b/src/editor/interface/table/Tr.ts @@ -0,0 +1,11 @@ +import { ITd } from './Td' + +export interface ITr { + id?: string + extension?: unknown + externalId?: string + height: number + tdList: ITd[] + minHeight?: number + pagingRepeat?: boolean // 在各页顶端以标题行的形式重复出现 +} diff --git a/src/editor/types/index.d.ts b/src/editor/types/index.d.ts new file mode 100644 index 0000000..8c46597 --- /dev/null +++ b/src/editor/types/index.d.ts @@ -0,0 +1,5 @@ +// 部分浏览器canvas上下文支持设置以下属性 +interface CanvasRenderingContext2D { + letterSpacing: string + wordSpacing: string +} diff --git a/src/editor/utils/clipboard.ts b/src/editor/utils/clipboard.ts new file mode 100644 index 0000000..d54b7b1 --- /dev/null +++ b/src/editor/utils/clipboard.ts @@ -0,0 +1,94 @@ +import { EDITOR_CLIPBOARD } from '../dataset/constant/Editor' +import { DeepRequired } from '../interface/Common' +import { IEditorOption } from '../interface/Editor' +import { IElement } from '../interface/Element' +import { createDomFromElementList, zipElementList } from './element' + +export interface IClipboardData { + text: string + elementList: IElement[] +} + +export function setClipboardData(data: IClipboardData) { + localStorage.setItem( + EDITOR_CLIPBOARD, + JSON.stringify({ + text: data.text, + elementList: data.elementList + }) + ) +} + +export function getClipboardData(): IClipboardData | null { + const clipboardText = localStorage.getItem(EDITOR_CLIPBOARD) + return clipboardText ? JSON.parse(clipboardText) : null +} + +export function removeClipboardData() { + localStorage.removeItem(EDITOR_CLIPBOARD) +} + +export function writeClipboardItem( + text: string, + html: string, + elementList: IElement[] +) { + if (!text && !html && !elementList.length) return + const plainText = new Blob([text], { type: 'text/plain' }) + const htmlText = new Blob([html], { type: 'text/html' }) + if (window.ClipboardItem) { + // @ts-ignore + const item = new ClipboardItem({ + [plainText.type]: plainText, + [htmlText.type]: htmlText + }) + window.navigator.clipboard.write([item]) + } else { + const fakeElement = document.createElement('div') + fakeElement.setAttribute('contenteditable', 'true') + fakeElement.innerHTML = html + document.body.append(fakeElement) + // add new range + const selection = window.getSelection() + const range = document.createRange() + // 增加尾行换行字符避免dom复制缺失 + const br = document.createElement('span') + br.innerText = '\n' + fakeElement.append(br) + // 扩选选区并执行复制 + range.selectNodeContents(fakeElement) + selection?.removeAllRanges() + selection?.addRange(range) + document.execCommand('copy') + fakeElement.remove() + } + // 编辑器结构化数据 + setClipboardData({ text, elementList }) +} + +export function writeElementList( + elementList: IElement[], + options: DeepRequired +) { + const clipboardDom = createDomFromElementList(elementList, options) + // 写入剪贴板 + document.body.append(clipboardDom) + const text = clipboardDom.innerText + // 先追加后移除,否则innerText无法解析换行符 + clipboardDom.remove() + const html = clipboardDom.innerHTML + if (!text && !html && !elementList.length) return + writeClipboardItem(text, html, zipElementList(elementList)) +} + +export function getIsClipboardContainFile(clipboardData: DataTransfer) { + let isFile = false + for (let i = 0; i < clipboardData.items.length; i++) { + const item = clipboardData.items[i] + if (item.kind === 'file') { + isFile = true + break + } + } + return isFile +} diff --git a/src/editor/utils/element.ts b/src/editor/utils/element.ts new file mode 100644 index 0000000..f661d0a --- /dev/null +++ b/src/editor/utils/element.ts @@ -0,0 +1,1832 @@ +import { + cloneProperty, + deepClone, + deepCloneOmitKeys, + getUUID, + isArrayEqual, + omitObject, + pickObject, + splitText +} from '.' +import { IFrameBlock } from '../core/draw/particle/block/modules/IFrameBlock' +import { LaTexParticle } from '../core/draw/particle/latex/LaTexParticle' +import { NON_BREAKING_SPACE, ZERO } from '../dataset/constant/Common' +import { + AREA_CONTEXT_ATTR, + BLOCK_ELEMENT_TYPE, + CONTROL_STYLE_ATTR, + EDITOR_ELEMENT_CONTEXT_ATTR, + EDITOR_ELEMENT_ZIP_ATTR, + EDITOR_ROW_ATTR, + INLINE_NODE_NAME, + TABLE_CONTEXT_ATTR, + TABLE_TD_ZIP_ATTR, + TEXTLIKE_ELEMENT_TYPE, + TITLE_CONTEXT_ATTR +} from '../dataset/constant/Element' +import { + listStyleCSSMapping, + listTypeElementMapping, + ulStyleMapping +} from '../dataset/constant/List' +import { START_LINE_BREAK_REG } from '../dataset/constant/Regular' +import { + titleNodeNameMapping, + titleOrderNumberMapping, + titleSizeMapping +} from '../dataset/constant/Title' +import { BlockType } from '../dataset/enum/Block' +import { ImageDisplay, LocationPosition } from '../dataset/enum/Common' +import { ControlComponent, ControlType } from '../dataset/enum/Control' +import { EditorMode } from '../dataset/enum/Editor' +import { ElementType } from '../dataset/enum/Element' +import { ListStyle, ListType, UlStyle } from '../dataset/enum/List' +import { RowFlex } from '../dataset/enum/Row' +import { TableBorder, TdBorder } from '../dataset/enum/table/Table' +import { VerticalAlign } from '../dataset/enum/VerticalAlign' +import { DeepRequired } from '../interface/Common' +import { IControlSelect } from '../interface/Control' +import { IEditorOption } from '../interface/Editor' +import { IElement } from '../interface/Element' +import { IRowElement } from '../interface/Row' +import { ITd } from '../interface/table/Td' +import { ITr } from '../interface/table/Tr' +import { mergeOption } from './option' + +export function unzipElementList(elementList: IElement[]): IElement[] { + const result: IElement[] = [] + for (let v = 0; v < elementList.length; v++) { + const valueItem = elementList[v] + const textList = splitText(valueItem.value) + for (let d = 0; d < textList.length; d++) { + result.push({ ...valueItem, value: textList[d] }) + } + } + return result +} + +interface IFormatElementListOption { + isHandleFirstElement?: boolean // 根据上下文确定首字符处理逻辑(处理首字符补偿) + isForceCompensation?: boolean // 强制补偿字符 + editorOptions: DeepRequired +} + +export function formatElementList( + elementList: IElement[], + options: IFormatElementListOption +) { + const { + isHandleFirstElement = true, + isForceCompensation = false, + editorOptions + } = options + const startElement = elementList[0] + // 非首字符零宽节点文本元素则补偿-列表元素内部会补偿此处忽略 + if ( + isForceCompensation || + (isHandleFirstElement && + startElement?.type !== ElementType.LIST && + ((startElement?.type && startElement.type !== ElementType.TEXT) || + !START_LINE_BREAK_REG.test(startElement?.value))) + ) { + elementList.unshift({ + value: ZERO + }) + } + let i = 0 + while (i < elementList.length) { + let el = elementList[i] + // 优先处理虚拟元素 + if (el.type === ElementType.TITLE) { + // 移除父节点 + elementList.splice(i, 1) + // 格式化元素 + const valueList = el.valueList || [] + formatElementList(valueList, { + ...options, + isHandleFirstElement: false, + isForceCompensation: false + }) + // 追加节点 + if (valueList.length) { + const titleId = el.titleId || getUUID() + const titleOptions = editorOptions.title + for (let v = 0; v < valueList.length; v++) { + const value = valueList[v] + value.title = el.title + if (el.level) { + value.titleId = titleId + value.level = el.level + } + // 文本型元素设置字体及加粗 + if (isTextLikeElement(value)) { + if (!value.size) { + value.size = titleOptions[titleSizeMapping[value.level!]] + } + if (value.bold === undefined) { + value.bold = true + } + } + elementList.splice(i, 0, value) + i++ + } + } + i-- + } else if (el.type === ElementType.LIST) { + // 移除父节点 + elementList.splice(i, 1) + // 格式化元素 + const valueList = el.valueList || [] + formatElementList(valueList, { + ...options, + isHandleFirstElement: true, + isForceCompensation: false + }) + // 追加节点 + if (valueList.length) { + const listId = getUUID() + for (let v = 0; v < valueList.length; v++) { + const value = valueList[v] + value.listId = listId + value.listType = el.listType + value.listStyle = el.listStyle + elementList.splice(i, 0, value) + i++ + } + } + i-- + } else if (el.type === ElementType.AREA) { + // 移除父节点 + elementList.splice(i, 1) + // 格式化元素 + const valueList = el?.valueList || [] + formatElementList(valueList, { + ...options, + isHandleFirstElement: true, + isForceCompensation: true + }) + if (valueList.length) { + const areaId = getUUID() + for (let v = 0; v < valueList.length; v++) { + const value = valueList[v] + value.areaId = el.areaId || areaId + value.area = el.area + value.areaIndex = v + if (value.type === ElementType.TABLE) { + const trList = value.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const tdValueList = td.value + for (let t = 0; t < tdValueList.length; t++) { + const tdValue = tdValueList[t] + tdValue.areaId = el.areaId || areaId + tdValue.area = el.area + } + } + } + } + elementList.splice(i, 0, value) + i++ + } + } + i-- + } else if (el.type === ElementType.TABLE) { + const tableId = el.id || getUUID() + el.id = tableId + if (el.trList) { + const { defaultTrMinHeight } = editorOptions.table + for (let t = 0; t < el.trList.length; t++) { + const tr = el.trList[t] + const trId = tr.id || getUUID() + tr.id = trId + if (!tr.minHeight || tr.minHeight < defaultTrMinHeight) { + tr.minHeight = defaultTrMinHeight + } + if (tr.height < tr.minHeight) { + tr.height = tr.minHeight + } + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const tdId = td.id || getUUID() + td.id = tdId + formatElementList(td.value, { + ...options, + isHandleFirstElement: true, + isForceCompensation: true + }) + // 首字符字体大小默认使用首个字符元素字体大小 + if ( + !td.value[0].size && + td.value[1]?.size && + isTextLikeElement(td.value[1]) + ) { + td.value[0].size = td.value[1].size + } + for (let v = 0; v < td.value.length; v++) { + const value = td.value[v] + value.tdId = tdId + value.trId = trId + value.tableId = tableId + } + } + } + } + } else if (el.type === ElementType.HYPERLINK) { + // 移除父节点 + elementList.splice(i, 1) + // 元素展开 + const valueList = unzipElementList(el.valueList || []) + // 追加节点 + if (valueList.length) { + const hyperlinkId = getUUID() + for (let v = 0; v < valueList.length; v++) { + const value = valueList[v] + value.type = el.type + value.url = el.url + value.hyperlinkId = hyperlinkId + elementList.splice(i, 0, value) + i++ + } + } + i-- + } else if (el.type === ElementType.DATE) { + // 移除父节点 + elementList.splice(i, 1) + // 元素展开 + const valueList = unzipElementList(el.valueList || []) + // 追加节点 + if (valueList.length) { + const dateId = getUUID() + for (let v = 0; v < valueList.length; v++) { + const value = valueList[v] + value.type = el.type + value.dateFormat = el.dateFormat + value.dateId = dateId + elementList.splice(i, 0, value) + i++ + } + } + i-- + } else if (el.type === ElementType.CONTROL) { + // 兼容控件内容类型错误 + if (!el.control) { + i++ + continue + } + const { + prefix, + postfix, + preText, + postText, + value, + placeholder, + code, + type, + valueSets + } = el.control + const { + editorOptions: { + control: controlOption, + checkbox: checkboxOption, + radio: radioOption + } + } = options + const controlId = el.controlId || getUUID() + // 移除父节点 + elementList.splice(i, 1) + // 控件上下文提取(压缩后的控件上下文无法提取) + const controlContext = pickObject(el, [ + ...EDITOR_ELEMENT_CONTEXT_ATTR, + ...EDITOR_ROW_ATTR + ]) + // 控件设置的默认样式(以前缀为基准) + const controlDefaultStyle = pickObject( + (el.control), + CONTROL_STYLE_ATTR + ) + // 前后缀个性化设置 + const thePrePostfixArg: Omit = { + ...controlDefaultStyle, + color: editorOptions.control.bracketColor + } + // 前缀 + const prefixStrList = splitText(prefix || controlOption.prefix) + for (let p = 0; p < prefixStrList.length; p++) { + const value = prefixStrList[p] + elementList.splice(i, 0, { + ...controlContext, + ...thePrePostfixArg, + controlId, + value, + type: el.type, + control: el.control, + controlComponent: ControlComponent.PREFIX + }) + i++ + } + // 前文本 + if (preText) { + const preTextStrList = splitText(preText) + for (let p = 0; p < preTextStrList.length; p++) { + const value = preTextStrList[p] + elementList.splice(i, 0, { + ...controlContext, + ...controlDefaultStyle, + controlId, + value, + type: el.type, + control: el.control, + controlComponent: ControlComponent.PRE_TEXT + }) + i++ + } + } + // 值 + if ( + (value && value.length) || + type === ControlType.CHECKBOX || + type === ControlType.RADIO || + (type === ControlType.SELECT && code && (!value || !value.length)) + ) { + let valueList: IElement[] = value ? deepClone(value) : [] + if (type === ControlType.CHECKBOX) { + const codeList = code ? code.split(',') : [] + if (Array.isArray(valueSets) && valueSets.length) { + // 拆分valueList优先使用其属性 + const valueStyleList = valueList.reduce( + (pre, cur) => + pre.concat( + cur.value.split('').map(v => ({ ...cur, value: v })) + ), + [] as IElement[] + ) + let valueStyleIndex = 0 + for (let v = 0; v < valueSets.length; v++) { + const valueSet = valueSets[v] + // checkbox组件 + elementList.splice(i, 0, { + ...controlContext, + ...controlDefaultStyle, + controlId, + value: '', + type: el.type, + control: el.control, + controlComponent: ControlComponent.CHECKBOX, + checkbox: { + code: valueSet.code, + value: codeList.includes(valueSet.code) + } + }) + i++ + // 文本 + const valueStrList = splitText(valueSet.value) + for (let e = 0; e < valueStrList.length; e++) { + const value = valueStrList[e] + const isLastLetter = e === valueStrList.length - 1 + elementList.splice(i, 0, { + ...controlContext, + ...controlDefaultStyle, + ...valueStyleList[valueStyleIndex], + controlId, + value: value === '\n' ? ZERO : value, + letterSpacing: isLastLetter ? checkboxOption.gap : 0, + control: el.control, + controlComponent: ControlComponent.VALUE + }) + valueStyleIndex++ + i++ + } + } + } + } else if (type === ControlType.RADIO) { + if (Array.isArray(valueSets) && valueSets.length) { + // 拆分valueList优先使用其属性 + const valueStyleList = valueList.reduce( + (pre, cur) => + pre.concat( + cur.value.split('').map(v => ({ ...cur, value: v })) + ), + [] as IElement[] + ) + let valueStyleIndex = 0 + for (let v = 0; v < valueSets.length; v++) { + const valueSet = valueSets[v] + // radio组件 + elementList.splice(i, 0, { + ...controlContext, + ...controlDefaultStyle, + controlId, + value: '', + type: el.type, + control: el.control, + controlComponent: ControlComponent.RADIO, + radio: { + code: valueSet.code, + value: code === valueSet.code + } + }) + i++ + // 文本 + const valueStrList = splitText(valueSet.value) + for (let e = 0; e < valueStrList.length; e++) { + const value = valueStrList[e] + const isLastLetter = e === valueStrList.length - 1 + elementList.splice(i, 0, { + ...controlContext, + ...controlDefaultStyle, + ...valueStyleList[valueStyleIndex], + controlId, + value: value === '\n' ? ZERO : value, + letterSpacing: isLastLetter ? radioOption.gap : 0, + control: el.control, + controlComponent: ControlComponent.VALUE + }) + valueStyleIndex++ + i++ + } + } + } + } else { + if (!value || !value.length) { + if (Array.isArray(valueSets) && valueSets.length) { + const valueSet = valueSets.find(v => v.code === code) + if (valueSet) { + valueList = [ + { + value: valueSet.value + } + ] + } + } + } + formatElementList(valueList, { + ...options, + isHandleFirstElement: false, + isForceCompensation: false + }) + for (let v = 0; v < valueList.length; v++) { + const element = valueList[v] + const value = element.value + elementList.splice(i, 0, { + ...controlContext, + ...controlDefaultStyle, + ...element, + controlId, + value: value === '\n' ? ZERO : value, + type: element.type || ElementType.TEXT, + control: el.control, + controlComponent: ControlComponent.VALUE + }) + i++ + } + } + } else if (placeholder) { + // placeholder + const thePlaceholderArgs: Omit = { + ...controlDefaultStyle, + color: editorOptions.control.placeholderColor + } + const placeholderStrList = splitText(placeholder) + for (let p = 0; p < placeholderStrList.length; p++) { + const value = placeholderStrList[p] + elementList.splice(i, 0, { + ...controlContext, + ...thePlaceholderArgs, + controlId, + value: value === '\n' ? ZERO : value, + type: el.type, + control: el.control, + controlComponent: ControlComponent.PLACEHOLDER + }) + i++ + } + } + // 后文本 + if (postText) { + const postTextStrList = splitText(postText) + for (let p = 0; p < postTextStrList.length; p++) { + const value = postTextStrList[p] + elementList.splice(i, 0, { + ...controlContext, + ...controlDefaultStyle, + controlId, + value, + type: el.type, + control: el.control, + controlComponent: ControlComponent.POST_TEXT + }) + i++ + } + } + // 后缀 + const postfixStrList = splitText(postfix || controlOption.postfix) + for (let p = 0; p < postfixStrList.length; p++) { + const value = postfixStrList[p] + elementList.splice(i, 0, { + ...controlContext, + ...thePrePostfixArg, + controlId, + value, + type: el.type, + control: el.control, + controlComponent: ControlComponent.POSTFIX + }) + i++ + } + i-- + } else if ( + (!el.type || TEXTLIKE_ELEMENT_TYPE.includes(el.type)) && + el.value?.length > 1 + ) { + elementList.splice(i, 1) + const valueList = splitText(el.value) + for (let v = 0; v < valueList.length; v++) { + elementList.splice(i + v, 0, { ...el, value: valueList[v] }) + } + el = elementList[i] + } + if (el.value === '\n' || el.value == '\r\n') { + el.value = ZERO + } + if (el.type === ElementType.IMAGE || el.type === ElementType.BLOCK) { + el.id = el.id || getUUID() + } + if (el.type === ElementType.LATEX) { + const { svg, width, height } = LaTexParticle.convertLaTextToSVG(el.value) + el.width = el.width || width + el.height = el.height || height + el.laTexSVG = svg + el.id = el.id || getUUID() + } + i++ + } +} + +export function isSameElementExceptValue( + source: IElement, + target: IElement +): boolean { + const sourceKeys = Object.keys(source) + const targetKeys = Object.keys(target) + if (sourceKeys.length !== targetKeys.length) return false + for (let s = 0; s < sourceKeys.length; s++) { + const key = sourceKeys[s] as never + // 值不需要校验 + if (key === 'value') continue + // groupIds数组需特殊校验数组是否相等 + if ( + key === 'groupIds' && + Array.isArray(source[key]) && + Array.isArray(target[key]) && + isArrayEqual(source[key], target[key]) + ) { + continue + } + if (source[key] !== target[key]) { + return false + } + } + return true +} +interface IPickElementOption { + extraPickAttrs?: Array +} +export function pickElementAttr( + payload: IElement, + option: IPickElementOption = {} +): IElement { + const { extraPickAttrs } = option + const zipAttrs = [...EDITOR_ELEMENT_ZIP_ATTR] + if (extraPickAttrs) { + zipAttrs.push(...extraPickAttrs) + } + const element: IElement = { + value: payload.value === ZERO ? `\n` : payload.value + } + zipAttrs.forEach(attr => { + const value = payload[attr] as never + if (value !== undefined) { + element[attr] = value + } + }) + return element +} + +interface IZipElementListOption { + extraPickAttrs?: Array + isClassifyArea?: boolean + isClone?: boolean +} +export function zipElementList( + payload: IElement[], + options: IZipElementListOption = {} +): IElement[] { + const { extraPickAttrs, isClassifyArea = false, isClone = true } = options + const elementList = isClone ? deepClone(payload) : payload + const zipElementListData: IElement[] = [] + let e = 0 + while (e < elementList.length) { + let element = elementList[e] + // 上下文首字符(占位符)-列表首字符要保留避免是复选框 + if ( + e === 0 && + element.value === ZERO && + !element.listId && + (!element.type || element.type === ElementType.TEXT) + ) { + e++ + continue + } + // 优先处理虚拟元素,后表格、超链接、日期、控件特殊处理 + if (element.areaId) { + const areaId = element.areaId + const area = element.area + // 收集并压缩数据 + const valueList: IElement[] = [] + while (e < elementList.length) { + const areaE = elementList[e] + if (areaId !== areaE.areaId) { + e-- + break + } + delete areaE.area + delete areaE.areaId + valueList.push(areaE) + e++ + } + const areaElementList = zipElementList(valueList, options) + // 不归类区域元素 + if (isClassifyArea) { + const areaElement: IElement = { + type: ElementType.AREA, + value: '', + areaId, + area + } + areaElement.valueList = areaElementList + element = areaElement + } else { + zipElementListData.splice(e, 0, ...areaElementList) + continue + } + } else if (element.titleId && element.level) { + // 标题处理 + const titleId = element.titleId + if (titleId) { + const level = element.level + const titleElement: IElement = { + type: ElementType.TITLE, + title: element.title, + titleId, + value: '', + level + } + const valueList: IElement[] = [] + while (e < elementList.length) { + const titleE = elementList[e] + if (titleId !== titleE.titleId) { + e-- + break + } + delete titleE.level + delete titleE.title + valueList.push(titleE) + e++ + } + titleElement.valueList = zipElementList(valueList, options) + element = titleElement + } + } else if (element.listId && element.listType) { + // 列表处理 + const listId = element.listId + if (listId) { + const listType = element.listType + const listStyle = element.listStyle + const listElement: IElement = { + type: ElementType.LIST, + value: '', + listId, + listType, + listStyle + } + const valueList: IElement[] = [] + while (e < elementList.length) { + const listE = elementList[e] + if (listId !== listE.listId) { + e-- + break + } + delete listE.listType + delete listE.listStyle + valueList.push(listE) + e++ + } + listElement.valueList = zipElementList(valueList, options) + element = listElement + } + } else if (element.type === ElementType.TABLE) { + // 分页表格先进行合并 + if (element.pagingId) { + let tableIndex = e + 1 + let combineCount = 0 + while (tableIndex < elementList.length) { + const nextElement = elementList[tableIndex] + if (nextElement.pagingId === element.pagingId) { + element.height! += nextElement.height! + element.trList!.push(...nextElement.trList!) + tableIndex++ + combineCount++ + } else { + break + } + } + e += combineCount + } + if (element.trList) { + for (let t = 0; t < element.trList.length; t++) { + const tr = element.trList[t] + delete tr.id + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const zipTd: ITd = { + colspan: td.colspan, + rowspan: td.rowspan, + value: zipElementList(td.value, { + ...options, + isClassifyArea: false + }) + } + // 压缩单元格属性 + TABLE_TD_ZIP_ATTR.forEach(attr => { + const value = td[attr] as never + if (value !== undefined) { + zipTd[attr] = value + } + }) + tr.tdList[d] = zipTd + } + } + } + } else if (element.type === ElementType.HYPERLINK) { + // 超链接处理 + const hyperlinkId = element.hyperlinkId + if (hyperlinkId) { + const hyperlinkElement: IElement = { + type: ElementType.HYPERLINK, + value: '', + url: element.url + } + const valueList: IElement[] = [] + while (e < elementList.length) { + const hyperlinkE = elementList[e] + if (hyperlinkId !== hyperlinkE.hyperlinkId) { + e-- + break + } + delete hyperlinkE.type + delete hyperlinkE.url + valueList.push(hyperlinkE) + e++ + } + hyperlinkElement.valueList = zipElementList(valueList, options) + element = hyperlinkElement + } + } else if (element.type === ElementType.DATE) { + const dateId = element.dateId + if (dateId) { + const dateElement: IElement = { + type: ElementType.DATE, + value: '', + dateFormat: element.dateFormat + } + const valueList: IElement[] = [] + while (e < elementList.length) { + const dateE = elementList[e] + if (dateId !== dateE.dateId) { + e-- + break + } + delete dateE.type + delete dateE.dateFormat + valueList.push(dateE) + e++ + } + dateElement.valueList = zipElementList(valueList, options) + element = dateElement + } + } else if (element.controlId) { + const controlId = element.controlId + // 控件包含前后缀则转换为控件 + if (element.controlComponent === ControlComponent.PREFIX) { + const valueList: IElement[] = [] + let isFull = false + let start = e + while (start < elementList.length) { + const controlE = elementList[start] + if (controlId !== controlE.controlId) break + if (controlE.controlComponent === ControlComponent.VALUE) { + delete controlE.control + delete controlE.controlId + valueList.push(controlE) + } + if (controlE.controlComponent === ControlComponent.POSTFIX) { + isFull = true + } + start++ + } + if (isFull) { + // 以前缀为基准更新控件默认样式 + const controlDefaultStyle = ( + (pickObject(element, CONTROL_STYLE_ATTR)) + ) + const control = { + ...element.control!, + ...controlDefaultStyle + } + const controlElement: IElement = { + ...pickObject(element, EDITOR_ROW_ATTR), + type: ElementType.CONTROL, + value: '', + control, + controlId + } + controlElement.control!.value = zipElementList(valueList, options) + element = pickElementAttr(controlElement, { extraPickAttrs }) + // 控件元素数量 - 1(当前元素) + e += start - e - 1 + } + } + // 不完整的控件元素不转化为控件,如果不是文本则直接忽略 + if (element.controlComponent) { + delete element.control + delete element.controlId + if ( + element.controlComponent !== ControlComponent.VALUE && + element.controlComponent !== ControlComponent.PRE_TEXT && + element.controlComponent !== ControlComponent.POST_TEXT + ) { + e++ + continue + } + } + } + // 组合元素 + const pickElement = pickElementAttr(element, { extraPickAttrs }) + if ( + !element.type || + element.type === ElementType.TEXT || + element.type === ElementType.SUBSCRIPT || + element.type === ElementType.SUPERSCRIPT + ) { + while (e < elementList.length) { + const nextElement = elementList[e + 1] + e++ + if ( + nextElement && + isSameElementExceptValue( + pickElement, + pickElementAttr(nextElement, { extraPickAttrs }) + ) + ) { + const nextValue = + nextElement.value === ZERO ? '\n' : nextElement.value + pickElement.value += nextValue + } else { + break + } + } + } else { + e++ + } + zipElementListData.push(pickElement) + } + return zipElementListData +} + +export function convertTextAlignToRowFlex(node: HTMLElement) { + const textAlign = window.getComputedStyle(node).textAlign + switch (textAlign) { + case 'left': + case 'start': + return RowFlex.LEFT + case 'center': + return RowFlex.CENTER + case 'right': + case 'end': + return RowFlex.RIGHT + case 'justify': + return RowFlex.ALIGNMENT + case 'justify-all': + return RowFlex.JUSTIFY + default: + return RowFlex.LEFT + } +} + +export function convertRowFlexToTextAlign(rowFlex: RowFlex) { + return rowFlex === RowFlex.ALIGNMENT ? 'justify' : rowFlex +} + +export function convertRowFlexToJustifyContent(rowFlex: RowFlex) { + switch (rowFlex) { + case RowFlex.LEFT: + return 'flex-start' + case RowFlex.CENTER: + return 'center' + case RowFlex.RIGHT: + return 'flex-end' + case RowFlex.ALIGNMENT: + case RowFlex.JUSTIFY: + return 'space-between' + default: + return 'flex-start' + } +} + +export function isTextLikeElement(element: IElement): boolean { + return !element.type || TEXTLIKE_ELEMENT_TYPE.includes(element.type) +} + +export function getAnchorElement( + elementList: IElement[], + anchorIndex: number +): IElement | null { + const anchorElement = elementList[anchorIndex] + if (!anchorElement) return null + const anchorNextElement = elementList[anchorIndex + 1] + // 非列表元素 && 当前元素是换行符 && 下一个元素不是换行符 && 区域相同 => 则以下一个元素作为参考元素 + return !anchorElement.listId && + anchorElement.value === ZERO && + anchorNextElement && + anchorNextElement.value !== ZERO && + anchorElement.areaId === anchorNextElement.areaId + ? anchorNextElement + : anchorElement +} + +export interface IFormatElementContextOption { + isBreakWhenWrap?: boolean + editorOptions?: DeepRequired +} + +export function formatElementContext( + sourceElementList: IElement[], + formatElementList: IElement[], + anchorIndex: number, + options?: IFormatElementContextOption +) { + let copyElement = getAnchorElement(sourceElementList, anchorIndex) + if (!copyElement) return + const { isBreakWhenWrap = false, editorOptions } = options || {} + const { mode } = editorOptions || {} + // 非设计模式时:标题元素禁用时不复制标题属性 + if (mode !== EditorMode.DESIGN && copyElement.title?.disabled) { + copyElement = omitObject(copyElement, TITLE_CONTEXT_ATTR) + } + // 是否已经换行 + let isBreakWarped = false + for (let e = 0; e < formatElementList.length; e++) { + const targetElement = formatElementList[e] + if ( + isBreakWhenWrap && + !copyElement.listId && + START_LINE_BREAK_REG.test(targetElement.value) + ) { + isBreakWarped = true + } + // 1. 即使换行停止也要处理表格上下文信息 + // 2. 定位元素非列表,无需处理粘贴列表的上下文,仅处理表格及行上下文信息 + if ( + isBreakWarped || + (!copyElement.listId && targetElement.type === ElementType.LIST) + ) { + const cloneAttr = [ + ...TABLE_CONTEXT_ATTR, + ...EDITOR_ROW_ATTR, + ...AREA_CONTEXT_ATTR + ] + cloneProperty(cloneAttr, copyElement!, targetElement) + targetElement.valueList?.forEach(valueItem => { + cloneProperty(cloneAttr, copyElement!, valueItem) + }) + continue + } + if (targetElement.valueList?.length) { + formatElementContext( + sourceElementList, + targetElement.valueList, + anchorIndex, + options + ) + } + // 非块类元素,需处理行属性 + const cloneAttr = [...EDITOR_ELEMENT_CONTEXT_ATTR] + if (!getIsBlockElement(targetElement)) { + cloneAttr.push(...EDITOR_ROW_ATTR) + } + cloneProperty(cloneAttr, copyElement, targetElement) + } +} + +export function convertElementToDom( + element: IElement, + options: DeepRequired +): HTMLElement { + let tagName: keyof HTMLElementTagNameMap = 'span' + if (element.type === ElementType.SUPERSCRIPT) { + tagName = 'sup' + } else if (element.type === ElementType.SUBSCRIPT) { + tagName = 'sub' + } + const dom = document.createElement(tagName) + dom.style.fontFamily = element.font || options.defaultFont + if (element.rowFlex) { + dom.style.textAlign = convertRowFlexToTextAlign(element.rowFlex) + } + if (element.color) { + dom.style.color = element.color + } + if (element.bold) { + dom.style.fontWeight = '600' + } + if (element.italic) { + dom.style.fontStyle = 'italic' + } + dom.style.fontSize = `${element.size || options.defaultSize}px` + if (element.highlight) { + dom.style.backgroundColor = element.highlight + } + if (element.underline) { + dom.style.textDecoration = 'underline' + } + if (element.strikeout) { + dom.style.textDecoration += ' line-through' + } + dom.innerText = element.value.replace(new RegExp(`${ZERO}`, 'g'), '\n') + return dom +} + +export function splitListElement( + elementList: IElement[] +): Map { + let curListIndex = 0 + const listElementListMap: Map = new Map() + for (let e = 0; e < elementList.length; e++) { + const element = elementList[e] + // 移除列表首行换行字符-如果是复选框直接忽略 + if (e === 0) { + if (element.checkbox) continue + element.value = element.value.replace(START_LINE_BREAK_REG, '') + } + if (element.listWrap) { + const listElementList = listElementListMap.get(curListIndex) || [] + listElementList.push(element) + listElementListMap.set(curListIndex, listElementList) + } else { + const valueList = element.value.split('\n') + for (let c = 0; c < valueList.length; c++) { + if (c > 0) { + curListIndex += 1 + } + const value = valueList[c] + const listElementList = listElementListMap.get(curListIndex) || [] + listElementList.push({ + ...element, + value + }) + listElementListMap.set(curListIndex, listElementList) + } + } + } + return listElementListMap +} + +export interface IElementListGroupRowFlex { + rowFlex: RowFlex | null + data: IElement[] +} + +export function groupElementListByRowFlex( + elementList: IElement[] +): IElementListGroupRowFlex[] { + const elementListGroupList: IElementListGroupRowFlex[] = [] + if (!elementList.length) return elementListGroupList + let currentRowFlex: RowFlex | null = elementList[0]?.rowFlex || null + elementListGroupList.push({ + rowFlex: currentRowFlex, + data: [elementList[0]] + }) + for (let e = 1; e < elementList.length; e++) { + const element = elementList[e] + const rowFlex = element.rowFlex || null + // 行布局相同&非块元素时追加数据,否则新增分组 + if ( + currentRowFlex === rowFlex && + !getIsBlockElement(element) && + !getIsBlockElement(elementList[e - 1]) + ) { + const lastElementListGroup = + elementListGroupList[elementListGroupList.length - 1] + lastElementListGroup.data.push(element) + } else { + elementListGroupList.push({ + rowFlex, + data: [element] + }) + currentRowFlex = rowFlex + } + } + // 压缩数据 + for (let g = 0; g < elementListGroupList.length; g++) { + const elementListGroup = elementListGroupList[g] + elementListGroup.data = zipElementList(elementListGroup.data) + } + return elementListGroupList +} + +export function createDomFromElementList( + elementList: IElement[], + options?: IEditorOption +) { + const editorOptions = mergeOption(options) + function buildDom(payload: IElement[]): HTMLDivElement { + const clipboardDom = document.createElement('div') + for (let e = 0; e < payload.length; e++) { + const element = payload[e] + // 构造表格 + if (element.type === ElementType.TABLE) { + const tableDom: HTMLTableElement = document.createElement('table') + tableDom.setAttribute('cellSpacing', '0') + tableDom.setAttribute('cellpadding', '0') + tableDom.setAttribute('border', '0') + const borderStyle = '1px solid #000000' + // 表格边框 + if (!element.borderType || element.borderType === TableBorder.ALL) { + tableDom.style.borderTop = borderStyle + tableDom.style.borderLeft = borderStyle + } else if (element.borderType === TableBorder.EXTERNAL) { + tableDom.style.border = borderStyle + } + tableDom.style.width = `${element.width}px` + // colgroup + const colgroupDom = document.createElement('colgroup') + for (let c = 0; c < element.colgroup!.length; c++) { + const colgroup = element.colgroup![c] + const colDom = document.createElement('col') + colDom.setAttribute('width', `${colgroup.width}`) + colgroupDom.append(colDom) + } + tableDom.append(colgroupDom) + // tr + const trList = element.trList! + for (let t = 0; t < trList.length; t++) { + const trDom = document.createElement('tr') + const tr = trList[t] + trDom.style.height = `${tr.height}px` + for (let d = 0; d < tr.tdList.length; d++) { + const tdDom = document.createElement('td') + if (!element.borderType || element.borderType === TableBorder.ALL) { + tdDom.style.borderBottom = tdDom.style.borderRight = '1px solid' + } + const td = tr.tdList[d] + tdDom.colSpan = td.colspan + tdDom.rowSpan = td.rowspan + tdDom.style.verticalAlign = td.verticalAlign || 'top' + // 单元格边框 + if (td.borderTypes?.includes(TdBorder.TOP)) { + tdDom.style.borderTop = borderStyle + } + if (td.borderTypes?.includes(TdBorder.RIGHT)) { + tdDom.style.borderRight = borderStyle + } + if (td.borderTypes?.includes(TdBorder.BOTTOM)) { + tdDom.style.borderBottom = borderStyle + } + if (td.borderTypes?.includes(TdBorder.LEFT)) { + tdDom.style.borderLeft = borderStyle + } + const childDom = createDomFromElementList(td.value!, options) + tdDom.innerHTML = childDom.innerHTML + if (td.backgroundColor) { + tdDom.style.backgroundColor = td.backgroundColor + } + trDom.append(tdDom) + } + tableDom.append(trDom) + } + clipboardDom.append(tableDom) + } else if (element.type === ElementType.HYPERLINK) { + const a = document.createElement('a') + a.innerText = element.valueList!.map(v => v.value).join('') + if (element.url) { + a.href = element.url + } + clipboardDom.append(a) + } else if (element.type === ElementType.TITLE) { + const h = document.createElement( + `h${titleOrderNumberMapping[element.level!]}` + ) + const childDom = buildDom(element.valueList!) + h.innerHTML = childDom.innerHTML + clipboardDom.append(h) + } else if (element.type === ElementType.LIST) { + const list = document.createElement( + listTypeElementMapping[element.listType!] + ) + if (element.listStyle) { + list.style.listStyleType = listStyleCSSMapping[element.listStyle] + } + // 按照换行符拆分 + const zipList = zipElementList(element.valueList!) + const listElementListMap = splitListElement(zipList) + listElementListMap.forEach(listElementList => { + const li = document.createElement('li') + const childDom = buildDom(listElementList) + li.innerHTML = childDom.innerHTML + list.append(li) + }) + clipboardDom.append(list) + } else if (element.type === ElementType.IMAGE) { + const img = document.createElement('img') + if (element.value) { + img.src = element.value + img.width = element.width! + img.height = element.height! + } + clipboardDom.append(img) + } else if (element.type === ElementType.BLOCK) { + if (element.block?.type === BlockType.VIDEO) { + const src = element.block.videoBlock?.src + if (src) { + const video = document.createElement('video') + video.style.display = 'block' + video.controls = true + video.src = src + video.width = element.width! || options?.width || window.innerWidth + video.height = element.height! + clipboardDom.append(video) + } + } else if (element.block?.type === BlockType.IFRAME) { + const { src, srcdoc } = element.block.iframeBlock || {} + if (src || srcdoc) { + const iframe = document.createElement('iframe') + iframe.sandbox.add(...IFrameBlock.sandbox) + iframe.style.display = 'block' + iframe.style.border = 'none' + if (src) { + iframe.src = src + } else if (srcdoc) { + iframe.srcdoc = srcdoc + } + iframe.width = `${ + element.width || options?.width || window.innerWidth + }` + iframe.height = `${element.height!}` + clipboardDom.append(iframe) + } + } + } else if (element.type === ElementType.SEPARATOR) { + const hr = document.createElement('hr') + clipboardDom.append(hr) + } else if (element.type === ElementType.CHECKBOX) { + const checkbox = document.createElement('input') + checkbox.type = 'checkbox' + if (element.checkbox?.value) { + checkbox.setAttribute('checked', 'true') + } + clipboardDom.append(checkbox) + } else if (element.type === ElementType.RADIO) { + const radio = document.createElement('input') + radio.type = 'radio' + if (element.radio?.value) { + radio.setAttribute('checked', 'true') + } + clipboardDom.append(radio) + } else if (element.type === ElementType.TAB) { + const tab = document.createElement('span') + tab.innerHTML = `${NON_BREAKING_SPACE}${NON_BREAKING_SPACE}` + clipboardDom.append(tab) + } else if (element.type === ElementType.CONTROL) { + const controlElement = document.createElement('span') + const childDom = buildDom(element.control?.value || []) + controlElement.innerHTML = childDom.innerHTML + clipboardDom.append(controlElement) + } else if ( + !element.type || + element.type === ElementType.LATEX || + TEXTLIKE_ELEMENT_TYPE.includes(element.type) + ) { + let text = '' + if (element.type === ElementType.DATE) { + text = element.valueList?.map(v => v.value).join('') || '' + } else { + text = element.value + } + if (!text) continue + const dom = convertElementToDom(element, editorOptions) + // 前一个元素是标题,移除首行换行符 + if (payload[e - 1]?.type === ElementType.TITLE) { + text = text.replace(/^\n/, '') + } + dom.innerText = text.replace(new RegExp(`${ZERO}`, 'g'), '\n') + clipboardDom.append(dom) + } + } + return clipboardDom + } + // 按行布局分类创建dom + const clipboardDom = document.createElement('div') + const groupElementList = groupElementListByRowFlex(elementList) + for (let g = 0; g < groupElementList.length; g++) { + const elementGroupRowFlex = groupElementList[g] + // 行布局样式设置 + const isDefaultRowFlex = + !elementGroupRowFlex.rowFlex || + elementGroupRowFlex.rowFlex === RowFlex.LEFT + // 块元素使用flex否则使用text-align + const rowFlexDom = document.createElement('div') + if (!isDefaultRowFlex) { + const firstElement = elementGroupRowFlex.data[0] + if (getIsBlockElement(firstElement)) { + rowFlexDom.style.display = 'flex' + rowFlexDom.style.justifyContent = convertRowFlexToJustifyContent( + firstElement.rowFlex! + ) + } else { + rowFlexDom.style.textAlign = convertRowFlexToTextAlign( + elementGroupRowFlex.rowFlex! + ) + } + } + // 布局内容 + rowFlexDom.innerHTML = buildDom(elementGroupRowFlex.data).innerHTML + // 未设置行布局时无需行布局容器 + if (!isDefaultRowFlex) { + clipboardDom.append(rowFlexDom) + } else { + rowFlexDom.childNodes.forEach(child => { + clipboardDom.append(child.cloneNode(true)) + }) + } + } + return clipboardDom +} + +export function convertTextNodeToElement( + textNode: Element | Node +): IElement | null { + if (!textNode || textNode.nodeType !== 3) return null + const parentNode = textNode.parentNode + const anchorNode = + parentNode.nodeName === 'FONT' + ? parentNode.parentNode + : parentNode + const rowFlex = convertTextAlignToRowFlex(anchorNode) + const value = textNode.textContent + const style = window.getComputedStyle(anchorNode) + if (!value || anchorNode.nodeName === 'STYLE') return null + const element: IElement = { + value, + color: style.color, + bold: Number(style.fontWeight) > 500, + italic: style.fontStyle.includes('italic'), + size: Math.floor(parseFloat(style.fontSize)) + } + // 元素类型-默认文本 + if (anchorNode.nodeName === 'SUB' || style.verticalAlign === 'sub') { + element.type = ElementType.SUBSCRIPT + } else if (anchorNode.nodeName === 'SUP' || style.verticalAlign === 'super') { + element.type = ElementType.SUPERSCRIPT + } + // 行对齐 + if (rowFlex !== RowFlex.LEFT) { + element.rowFlex = rowFlex + } + // 高亮色 + if (style.backgroundColor !== 'rgba(0, 0, 0, 0)') { + element.highlight = style.backgroundColor + } + // 下划线 + if (style.textDecorationLine.includes('underline')) { + element.underline = true + } + // 删除线 + if (style.textDecorationLine.includes('line-through')) { + element.strikeout = true + } + return element +} + +export interface IGetElementListByHTMLOption { + innerWidth: number +} + +export function getElementListByHTML( + htmlText: string, + options: IGetElementListByHTMLOption +): IElement[] { + const elementList: IElement[] = [] + function findTextNode(dom: Element | Node) { + if (dom.nodeType === 3) { + const element = convertTextNodeToElement(dom) + if (element) { + elementList.push(element) + } + } else if (dom.nodeType === 1) { + const childNodes = dom.childNodes + for (let n = 0; n < childNodes.length; n++) { + const node = childNodes[n] + // br元素与display:block元素需换行 + if (node.nodeName === 'BR') { + elementList.push({ + value: '\n' + }) + } else if (node.nodeName === 'A') { + const aElement = node as HTMLLinkElement + const value = aElement.innerText + if (value) { + elementList.push({ + type: ElementType.HYPERLINK, + value: '', + valueList: [ + { + value + } + ], + url: aElement.href + }) + } + } else if (/H[1-6]/.test(node.nodeName)) { + const hElement = node as HTMLTitleElement + const valueList = getElementListByHTML( + replaceHTMLElementTag(hElement, 'div').outerHTML, + options + ) + elementList.push({ + value: '', + type: ElementType.TITLE, + level: titleNodeNameMapping[node.nodeName], + valueList + }) + if ( + node.nextSibling && + !INLINE_NODE_NAME.includes(node.nextSibling.nodeName) + ) { + elementList.push({ + value: '\n' + }) + } + } else if (node.nodeName === 'UL' || node.nodeName === 'OL') { + const listNode = node as HTMLOListElement | HTMLUListElement + const listElement: IElement = { + value: '', + type: ElementType.LIST, + valueList: [] + } + if (node.nodeName === 'OL') { + listElement.listType = ListType.OL + } else { + listElement.listType = ListType.UL + listElement.listStyle = ( + (listNode.style.listStyleType) + ) + } + listNode.querySelectorAll('li').forEach(li => { + const liValueList = getElementListByHTML(li.innerHTML, options) + liValueList.forEach(list => { + if (list.value === '\n') { + list.listWrap = true + } + }) + liValueList.unshift({ + value: '\n' + }) + listElement.valueList!.push(...liValueList) + }) + elementList.push(listElement) + } else if (node.nodeName === 'HR') { + elementList.push({ + value: '\n', + type: ElementType.SEPARATOR + }) + } else if (node.nodeName === 'IMG') { + const { src, width, height } = node as HTMLImageElement + if (src && width && height) { + elementList.push({ + width, + height, + value: src, + type: ElementType.IMAGE + }) + } + } else if (node.nodeName === 'VIDEO') { + const { src, width, height } = node as HTMLVideoElement + if (src && width && height) { + elementList.push({ + value: '', + type: ElementType.BLOCK, + block: { + type: BlockType.VIDEO, + videoBlock: { + src + } + }, + width, + height + }) + } + } else if (node.nodeName === 'IFRAME') { + const { src, srcdoc, width, height } = node as HTMLIFrameElement + if ((src || srcdoc) && width && height) { + elementList.push({ + value: '', + type: ElementType.BLOCK, + block: { + type: BlockType.IFRAME, + iframeBlock: { + src, + srcdoc + } + }, + width: parseInt(width), + height: parseInt(height) + }) + } + } else if (node.nodeName === 'TABLE') { + const tableElement = node as HTMLTableElement + const element: IElement = { + type: ElementType.TABLE, + value: '\n', + colgroup: [], + trList: [] + } + // colgroup + const colElements = tableElement.querySelectorAll('colgroup col') + // 基础数据 + tableElement.querySelectorAll('tr').forEach(trElement => { + const trHeightStr = Number( + window.getComputedStyle(trElement).height.replace('px', '') + ) + const tr: ITr = { + height: trHeightStr, + minHeight: trHeightStr, + tdList: [] + } + trElement.querySelectorAll('th,td').forEach(tdElement => { + const tableCell = tdElement + const valueList = getElementListByHTML( + tableCell.innerHTML, + options + ) + const td: ITd = { + colspan: tableCell.colSpan, + rowspan: tableCell.rowSpan, + value: valueList, + verticalAlign: window.getComputedStyle(tdElement) + .verticalAlign as VerticalAlign, + width: parseFloat(window.getComputedStyle(tdElement).width) + } + if (tableCell.style.backgroundColor) { + td.backgroundColor = tableCell.style.backgroundColor + } + tr.tdList.push(td) + }) + element.trList!.push(tr) + }) + if (element.trList!.length) { + // 列选项数据 + const tdCount = element.trList![0].tdList.reduce( + (pre, cur) => pre + cur.colspan, + 0 + ) + const width = Math.ceil(options.innerWidth / tdCount) + for (let i = 0; i < tdCount; i++) { + const colElement = colElements[i]?.getAttribute('width') + element.colgroup!.push({ + width: colElement ? parseFloat(colElement) : width + }) + } + elementList.push(element) + } + } else if ( + node.nodeName === 'INPUT' && + (node).type === ControlComponent.CHECKBOX + ) { + elementList.push({ + type: ElementType.CHECKBOX, + value: '', + checkbox: { + value: (node).checked + } + }) + } else if ( + node.nodeName === 'INPUT' && + (node).type === ControlComponent.RADIO + ) { + elementList.push({ + type: ElementType.RADIO, + value: '', + radio: { + value: (node).checked + } + }) + } else { + findTextNode(node) + if (node.nodeType === 1 && n !== childNodes.length - 1) { + const nodeElement = node as Element + const display = window.getComputedStyle(nodeElement).display + if ( + display === 'block' && + !/(\n|\r\n)$/.test(nodeElement.textContent!) + ) { + elementList.push({ + value: '\n' + }) + } + } + } + } + } + } + // 追加dom + const clipboardDom = document.createElement('div') + clipboardDom.innerHTML = htmlText + document.body.appendChild(clipboardDom) + const deleteNodes: ChildNode[] = [] + clipboardDom.childNodes.forEach(child => { + if (child.nodeType !== 1 && !child.textContent?.trim()) { + deleteNodes.push(child) + } + }) + deleteNodes.forEach(node => node.remove()) + // 搜索文本节点 + findTextNode(clipboardDom) + // 移除dom + clipboardDom.remove() + return elementList +} + +export function getTextFromElementList(elementList: IElement[]) { + function buildText(payload: IElement[]): string { + let text = '' + for (let e = 0; e < payload.length; e++) { + const element = payload[e] + // 构造表格 + if (element.type === ElementType.TABLE) { + text += `\n` + const trList = element.trList! + for (let t = 0; t < trList.length; t++) { + const tr = trList[t] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const tdText = buildText(zipElementList(td.value!)) + const isFirst = d === 0 + const isLast = tr.tdList.length - 1 === d + text += `${!isFirst ? ` ` : ``}${tdText}${isLast ? `\n` : ``}` + } + } + } else if (element.type === ElementType.TAB) { + text += `\t` + } else if (element.type === ElementType.HYPERLINK) { + text += element.valueList!.map(v => v.value).join('') + } else if (element.type === ElementType.TITLE) { + text += `${buildText(zipElementList(element.valueList!))}` + } else if (element.type === ElementType.LIST) { + // 按照换行符拆分 + const zipList = zipElementList(element.valueList!) + const listElementListMap = splitListElement(zipList) + // 无序列表前缀 + let ulListStyleText = '' + if (element.listType === ListType.UL) { + ulListStyleText = + ulStyleMapping[(element.listStyle)] + } + listElementListMap.forEach((listElementList, listIndex) => { + const isLast = listElementListMap.size - 1 === listIndex + text += `\n${ulListStyleText || `${listIndex + 1}.`}${buildText( + listElementList + )}${isLast ? `\n` : ``}` + }) + } else if (element.type === ElementType.CHECKBOX) { + text += element.checkbox?.value ? `☑` : `□` + } else if (element.type === ElementType.RADIO) { + text += element.radio?.value ? `☉` : `○` + } else if ( + !element.type || + element.type === ElementType.LATEX || + TEXTLIKE_ELEMENT_TYPE.includes(element.type) + ) { + let textLike = '' + if (element.type === ElementType.CONTROL) { + const controlValue = element.control!.value?.[0]?.value || '' + textLike = controlValue + ? `${element.control?.preText || ''}${controlValue}${ + element.control?.postText || '' + }` + : '' + } else if (element.type === ElementType.DATE) { + textLike = element.valueList?.map(v => v.value).join('') || '' + } else { + textLike = element.value + } + text += textLike.replace(new RegExp(`${ZERO}`, 'g'), '\n') + } + } + return text + } + return buildText(zipElementList(elementList)) +} + +export function getSlimCloneElementList(elementList: IElement[]) { + return deepCloneOmitKeys(elementList, [ + 'metrics', + 'style' + ]) +} + +export function getIsBlockElement(element?: IElement) { + return ( + !!element?.type && + (BLOCK_ELEMENT_TYPE.includes(element.type) || + element.imgDisplay === ImageDisplay.INLINE) + ) +} + +export function replaceHTMLElementTag( + oldDom: HTMLElement, + tagName: keyof HTMLElementTagNameMap +): HTMLElement { + const newDom = document.createElement(tagName) + for (let i = 0; i < oldDom.attributes.length; i++) { + const attr = oldDom.attributes[i] + newDom.setAttribute(attr.name, attr.value) + } + newDom.innerHTML = oldDom.innerHTML + return newDom +} + +export function pickSurroundElementList(elementList: IElement[]) { + const surroundElementList = [] + for (let e = 0; e < elementList.length; e++) { + const element = elementList[e] + if (element.imgDisplay === ImageDisplay.SURROUND) { + surroundElementList.push(element) + } + } + return surroundElementList +} + +export function deleteSurroundElementList( + elementList: IElement[], + pageNo: number +) { + for (let s = elementList.length - 1; s >= 0; s--) { + const surroundElement = elementList[s] + if (surroundElement.imgFloatPosition?.pageNo === pageNo) { + elementList.splice(s, 1) + } + } +} + +export function getNonHideElementIndex( + elementList: IElement[], + index: number, + position: LocationPosition = LocationPosition.BEFORE +) { + if ( + !elementList[index]?.hide && + !elementList[index]?.control?.hide && + !elementList[index]?.area?.hide + ) { + return index + } + let i = index + if (position === LocationPosition.BEFORE) { + i = index - 1 + while (i > 0) { + if ( + !elementList[i]?.hide && + !elementList[i]?.control?.hide && + !elementList[i]?.area?.hide + ) { + return i + } + i-- + } + } else { + i = index + 1 + while (i < elementList.length) { + if ( + !elementList[i]?.hide && + !elementList[i]?.control?.hide && + !elementList[i]?.area?.hide + ) { + return i + } + i++ + } + } + return i +} diff --git a/src/editor/utils/hotkey.ts b/src/editor/utils/hotkey.ts new file mode 100644 index 0000000..93c54f9 --- /dev/null +++ b/src/editor/utils/hotkey.ts @@ -0,0 +1,5 @@ +import { isApple } from './ua' + +export function isMod(evt: KeyboardEvent | MouseEvent) { + return isApple ? evt.metaKey : evt.ctrlKey +} diff --git a/src/editor/utils/index.ts b/src/editor/utils/index.ts new file mode 100644 index 0000000..43fb9c2 --- /dev/null +++ b/src/editor/utils/index.ts @@ -0,0 +1,371 @@ +import { UNICODE_SYMBOL_REG } from '../dataset/constant/Regular' +import { IElementFillRect } from '../interface/Element' + +export function debounce( + func: (...arg: T) => unknown, + delay: number +) { + let timer: number + return function (this: unknown, ...args: T) { + if (timer) { + window.clearTimeout(timer) + } + timer = window.setTimeout(() => { + func.apply(this, args) + }, delay) + } +} + +export function throttle( + func: (...arg: T) => unknown, + delay: number +) { + let lastExecTime = 0 + let timer: number + return function (this: unknown, ...args: T) { + const currentTime = Date.now() + if (currentTime - lastExecTime >= delay) { + window.clearTimeout(timer) + func.apply(this, args) + lastExecTime = currentTime + } else { + window.clearTimeout(timer) + timer = window.setTimeout(() => { + func.apply(this, args) + lastExecTime = currentTime + }, delay) + } + } +} + +export function deepCloneOmitKeys(obj: T, omitKeys: (keyof K)[]): T { + if (!obj || typeof obj !== 'object') { + return obj + } + let newObj: any = {} + if (Array.isArray(obj)) { + newObj = obj.map(item => deepCloneOmitKeys(item, omitKeys)) + } else { + // prettier-ignore + (Object.keys(obj) as (keyof K)[]).forEach(key => { + if (omitKeys.includes(key)) return + return (newObj[key] = deepCloneOmitKeys((obj[key as unknown as keyof T] ), omitKeys)) + }) + } + return newObj +} + +export function deepClone(obj: T): T { + if (typeof structuredClone === 'function') { + return structuredClone(obj) + } + if (!obj || typeof obj !== 'object') { + return obj + } + let newObj: any = {} + if (Array.isArray(obj)) { + newObj = obj.map(item => deepClone(item)) + } else { + // prettier-ignore + (Object.keys(obj) as (keyof T)[]).forEach(key => { + return (newObj[key] = deepClone(obj[key])) + }) + } + return newObj +} + +export function isBody(node: Element): boolean { + return node && node.nodeType === 1 && node.tagName.toLowerCase() === 'body' +} + +export function findParent( + node: Element, + filterFn: Function, + includeSelf: boolean +) { + if (node && !isBody(node)) { + node = includeSelf ? node : (node.parentNode as Element) + while (node) { + if (!filterFn || filterFn(node) || isBody(node)) { + return filterFn && !filterFn(node) && isBody(node) ? null : node + } + node = node.parentNode as Element + } + } + return null +} + +export function getUUID(): string { + function S4(): string { + return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1) + } + return ( + S4() + + S4() + + '-' + + S4() + + '-' + + S4() + + '-' + + S4() + + '-' + + S4() + + S4() + + S4() + ) +} + +export function splitText(text: string): string[] { + const data: string[] = [] + if (Intl.Segmenter) { + const segmenter = new Intl.Segmenter() + const segments = segmenter.segment(text) + for (const { segment } of segments) { + data.push(segment) + } + } else { + const symbolMap = new Map() + for (const match of text.matchAll(UNICODE_SYMBOL_REG)) { + symbolMap.set(match.index!, match[0]) + } + let t = 0 + while (t < text.length) { + const symbol = symbolMap.get(t) + if (symbol) { + data.push(symbol) + t += symbol.length + } else { + data.push(text[t]) + t++ + } + } + } + return data +} + +export function downloadFile(href: string, fileName: string) { + const a = document.createElement('a') + a.href = href + a.download = fileName + a.click() +} + +export function threeClick(dom: HTMLElement, fn: (evt: MouseEvent) => any) { + nClickEvent(3, dom, fn) +} + +function nClickEvent( + n: number, + dom: HTMLElement, + fn: (evt: MouseEvent) => any +) { + let count = 0 + let lastTime = 0 + + const handler = function (evt: MouseEvent) { + const currentTime = new Date().getTime() + count = currentTime - lastTime < 300 ? count + 1 : 0 + lastTime = new Date().getTime() + if (count >= n - 1) { + fn(evt) + count = 0 + } + } + + dom.addEventListener('click', handler) +} + +export function isObject(type: unknown): type is Record { + return Object.prototype.toString.call(type) === '[object Object]' +} + +export function isArray(type: unknown): type is Array { + return Array.isArray(type) +} + +export function isNumber(type: unknown): type is number { + return Object.prototype.toString.call(type) === '[object Number]' +} + +export function isString(type: unknown): type is string { + return Object.prototype.toString.call(type) === '[object String]' +} + +export function mergeObject(source: T, target: T): T { + if (isObject(source) && isObject(target)) { + const objectTarget = >target + for (const [key, val] of Object.entries(source)) { + if (!objectTarget[key]) { + objectTarget[key] = val + } else { + objectTarget[key] = mergeObject(val, objectTarget[key]) + } + } + } else if (isArray(source) && isArray(target)) { + target.push(...source) + } + return target +} + +export function nextTick(fn: Function) { + setTimeout(() => { + fn() + }, 0) +} + +export function convertNumberToChinese(num: number) { + const chineseNum = [ + '零', + '一', + '二', + '三', + '四', + '五', + '六', + '七', + '八', + '九' + ] + const chineseUnit = [ + '', + '十', + '百', + '千', + '万', + '十', + '百', + '千', + '亿', + '十', + '百', + '千', + '万', + '十', + '百', + '千', + '亿' + ] + if (!num || isNaN(num)) return '零' + const numStr = num.toString().split('') + let result = '' + for (let i = 0; i < numStr.length; i++) { + const desIndex = numStr.length - 1 - i + result = `${chineseUnit[i]}${result}` + result = `${chineseNum[Number(numStr[desIndex])]}${result}` + } + result = result.replace(/零(千|百|十)/g, '零').replace(/十零/g, '十') + result = result.replace(/零+/g, '零') + result = result.replace(/零亿/g, '亿').replace(/零万/g, '万') + result = result.replace(/亿万/g, '亿') + result = result.replace(/零+$/, '') + result = result.replace(/^一十/g, '十') + return result +} + +export function cloneProperty( + properties: (keyof T)[], + sourceElement: T, + targetElement: T +) { + for (let i = 0; i < properties.length; i++) { + const property = properties[i] + const value = sourceElement[property] + if (value !== undefined) { + targetElement[property] = value + } else { + delete targetElement[property] + } + } +} + +export function pickObject(object: T, pickKeys: (keyof T)[]): T { + const newObject: T = {} + for (const key in object) { + if (pickKeys.includes(key)) { + newObject[key] = object[key] + } + } + return newObject +} + +export function omitObject(object: T, omitKeys: (keyof T)[]): T { + const newObject: T = {} + for (const key in object) { + if (!omitKeys.includes(key)) { + newObject[key] = object[key] + } + } + return newObject +} + +export function convertStringToBase64(input: string) { + const encoder = new TextEncoder() + const data = encoder.encode(input) + const charArray = Array.from(data, byte => String.fromCharCode(byte)) + const base64 = window.btoa(charArray.join('')) + return base64 +} + +export function findScrollContainer(element: HTMLElement) { + let parent = element.parentElement + while (parent) { + const style = window.getComputedStyle(parent) + const overflowY = style.getPropertyValue('overflow-y') + if ( + parent.scrollHeight > parent.clientHeight && + (overflowY === 'auto' || overflowY === 'scroll') + ) { + return parent + } + parent = parent.parentElement + } + return document.documentElement +} + +export function isArrayEqual(arr1: unknown[], arr2: unknown[]): boolean { + if (arr1.length !== arr2.length) { + return false + } + return !arr1.some(item => !arr2.includes(item)) +} + +export function isObjectEqual(obj1: unknown, obj2: unknown): boolean { + if (!isObject(obj1) || !isObject(obj2)) return false + const obj1Keys = Object.keys(obj1) + const obj2Keys = Object.keys(obj2) + if (obj1Keys.length !== obj2Keys.length) { + return false + } + return !obj1Keys.some(key => obj2[key] !== obj1[key]) +} + +export function isRectIntersect( + rect1: IElementFillRect, + rect2: IElementFillRect +): boolean { + const rect1Left = rect1.x + const rect1Right = rect1.x + rect1.width + const rect1Top = rect1.y + const rect1Bottom = rect1.y + rect1.height + const rect2Left = rect2.x + const rect2Right = rect2.x + rect2.width + const rect2Top = rect2.y + const rect2Bottom = rect2.y + rect2.height + if ( + rect1Left > rect2Right || + rect1Right < rect2Left || + rect1Top > rect2Bottom || + rect1Bottom < rect2Top + ) { + return false + } + return true +} + +export function isNonValue(value: unknown): boolean { + return value === undefined || value === null +} + +export function normalizeLineBreak(text: string): string { + return text.replace(/\r\n|\r/g, '\n') +} diff --git a/src/editor/utils/option.ts b/src/editor/utils/option.ts new file mode 100644 index 0000000..86ab21c --- /dev/null +++ b/src/editor/utils/option.ts @@ -0,0 +1,219 @@ +import { defaultBackground } from '../dataset/constant/Background' +import { defaultCheckboxOption } from '../dataset/constant/Checkbox' +import { LETTER_CLASS } from '../dataset/constant/Common' +import { defaultControlOption } from '../dataset/constant/Control' +import { defaultCursorOption } from '../dataset/constant/Cursor' +import { defaultFooterOption } from '../dataset/constant/Footer' +import { defaultGroupOption } from '../dataset/constant/Group' +import { defaultHeaderOption } from '../dataset/constant/Header' +import { defaultLineBreak } from '../dataset/constant/LineBreak' +import { defaultPageBreakOption } from '../dataset/constant/PageBreak' +import { defaultPageNumberOption } from '../dataset/constant/PageNumber' +import { defaultPlaceholderOption } from '../dataset/constant/Placeholder' +import { defaultRadioOption } from '../dataset/constant/Radio' +import { defaultSeparatorOption } from '../dataset/constant/Separator' +import { defaultTableOption } from '../dataset/constant/Table' +import { defaultTitleOption } from '../dataset/constant/Title' +import { defaultWatermarkOption } from '../dataset/constant/Watermark' +import { defaultZoneOption } from '../dataset/constant/Zone' +import { defaultLineNumberOption } from '../dataset/constant/LineNumber' +import { IBackgroundOption } from '../interface/Background' +import { ICheckboxOption } from '../interface/Checkbox' +import { DeepRequired } from '../interface/Common' +import { IControlOption } from '../interface/Control' +import { ICursorOption } from '../interface/Cursor' +import { IEditorOption, IModeRule } from '../interface/Editor' +import { IFooter } from '../interface/Footer' +import { IGroup } from '../interface/Group' +import { IHeader } from '../interface/Header' +import { ILineBreakOption } from '../interface/LineBreak' +import { IPageBreak } from '../interface/PageBreak' +import { IPageNumber } from '../interface/PageNumber' +import { IPlaceholder } from '../interface/Placeholder' +import { IRadioOption } from '../interface/Radio' +import { ISeparatorOption } from '../interface/Separator' +import { ITableOption } from '../interface/table/Table' +import { ITitleOption } from '../interface/Title' +import { IWatermark } from '../interface/Watermark' +import { IZoneOption } from '../interface/Zone' +import { ILineNumberOption } from '../interface/LineNumber' +import { IPageBorderOption } from '../interface/PageBorder' +import { defaultPageBorderOption } from '../dataset/constant/PageBorder' +import { + EditorMode, + PageMode, + PaperDirection, + RenderMode, + WordBreak +} from '../dataset/enum/Editor' +import { defaultBadgeOption } from '../dataset/constant/Badge' +import { IBadgeOption } from '../interface/Badge' +import { defaultModeRuleOption } from '../dataset/constant/Editor' + +export function mergeOption( + options: IEditorOption = {} +): DeepRequired { + const tableOptions: Required = { + ...defaultTableOption, + ...options.table + } + const headerOptions: Required = { + ...defaultHeaderOption, + ...options.header + } + const footerOptions: Required = { + ...defaultFooterOption, + ...options.footer + } + const pageNumberOptions: Required = { + ...defaultPageNumberOption, + ...options.pageNumber + } + const waterMarkOptions: Required = { + ...defaultWatermarkOption, + ...options.watermark + } + const controlOptions: Required = { + ...defaultControlOption, + ...options.control + } + const checkboxOptions: Required = { + ...defaultCheckboxOption, + ...options.checkbox + } + const radioOptions: Required = { + ...defaultRadioOption, + ...options.radio + } + const cursorOptions: Required = { + ...defaultCursorOption, + ...options.cursor + } + const titleOptions: Required = { + ...defaultTitleOption, + ...options.title + } + const placeholderOptions: Required = { + ...defaultPlaceholderOption, + ...options.placeholder + } + const groupOptions: Required = { + ...defaultGroupOption, + ...options.group + } + const pageBreakOptions: Required = { + ...defaultPageBreakOption, + ...options.pageBreak + } + const zoneOptions: Required = { + ...defaultZoneOption, + ...options.zone + } + const backgroundOptions: Required = { + ...defaultBackground, + ...options.background + } + const lineBreakOptions: Required = { + ...defaultLineBreak, + ...options.lineBreak + } + const separatorOptions: Required = { + ...defaultSeparatorOption, + ...options.separator + } + const lineNumberOptions: Required = { + ...defaultLineNumberOption, + ...options.lineNumber + } + const pageBorderOptions: Required = { + ...defaultPageBorderOption, + ...options.pageBorder + } + const badgeOptions: Required = { + ...defaultBadgeOption, + ...options.badge + } + const modeRuleOption: DeepRequired = { + print: { + ...defaultModeRuleOption.print, + ...options.modeRule?.print + }, + readonly: { + ...defaultModeRuleOption.readonly, + ...options.modeRule?.readonly + }, + form: { + ...defaultModeRuleOption.form, + ...options.modeRule?.form + } + } + + return { + mode: EditorMode.EDIT, + locale: 'zhCN', + defaultType: 'TEXT', + defaultColor: '#000000', + defaultFont: 'Microsoft YaHei', + defaultSize: 16, + minSize: 5, + maxSize: 72, + defaultRowMargin: 1, + defaultBasicRowMarginHeight: 8, + defaultTabWidth: 32, + width: 794, + height: 1123, + scale: 1, + pageGap: 20, + underlineColor: '#000000', + strikeoutColor: '#FF0000', + rangeAlpha: 0.6, + rangeColor: '#AECBFA', + rangeMinWidth: 5, + searchMatchAlpha: 0.6, + searchMatchColor: '#FFFF00', + searchNavigateMatchColor: '#AAD280', + highlightAlpha: 0.6, + highlightMarginHeight: 8, + resizerColor: '#4182D9', + resizerSize: 5, + marginIndicatorSize: 35, + marginIndicatorColor: '#BABABA', + margins: [100, 120, 100, 120], + pageMode: PageMode.PAGING, + renderMode: RenderMode.SPEED, + defaultHyperlinkColor: '#0000FF', + paperDirection: PaperDirection.VERTICAL, + inactiveAlpha: 0.6, + historyMaxRecordCount: 100, + wordBreak: WordBreak.BREAK_WORD, + printPixelRatio: 3, + maskMargin: [0, 0, 0, 0], + letterClass: [LETTER_CLASS.ENGLISH], + contextMenuDisableKeys: [], + shortcutDisableKeys: [], + scrollContainerSelector: '', + pageOuterSelectionDisable: false, + ...options, + table: tableOptions, + header: headerOptions, + footer: footerOptions, + pageNumber: pageNumberOptions, + watermark: waterMarkOptions, + control: controlOptions, + checkbox: checkboxOptions, + radio: radioOptions, + cursor: cursorOptions, + title: titleOptions, + placeholder: placeholderOptions, + group: groupOptions, + pageBreak: pageBreakOptions, + zone: zoneOptions, + background: backgroundOptions, + lineBreak: lineBreakOptions, + separator: separatorOptions, + lineNumber: lineNumberOptions, + pageBorder: pageBorderOptions, + badge: badgeOptions, + modeRule: modeRuleOption + } +} diff --git a/src/editor/utils/print.ts b/src/editor/utils/print.ts new file mode 100644 index 0000000..7a9ef04 --- /dev/null +++ b/src/editor/utils/print.ts @@ -0,0 +1,99 @@ +import { PaperDirection } from '../dataset/enum/Editor' + +function convertPxToPaperSize(width: number, height: number) { + if (width === 1125 && height === 1593) { + return { + size: 'a3', + width: '297mm', + height: '420mm' + } + } + if (width === 794 && height === 1123) { + return { + size: 'a4', + width: '210mm', + height: '297mm' + } + } + if (width === 565 && height === 796) { + return { + size: 'a5', + width: '148mm', + height: '210mm' + } + } + // 其他默认不转换 + return { + size: '', + width: `${width}px`, + height: `${height}px` + } +} + +export interface IPrintImageBase64Option { + width: number + height: number + direction?: PaperDirection +} +export function printImageBase64( + base64List: string[], + options: IPrintImageBase64Option +) { + const { width, height, direction = PaperDirection.VERTICAL } = options + const iframe = document.createElement('iframe') + // 离屏渲染 + iframe.style.visibility = 'hidden' + iframe.style.position = 'absolute' + iframe.style.left = '0' + iframe.style.top = '0' + iframe.style.width = '0' + iframe.style.height = '0' + iframe.style.border = 'none' + document.body.append(iframe) + const contentWindow = iframe.contentWindow! + const doc = contentWindow.document + doc.open() + const container = document.createElement('div') + const paperSize = convertPxToPaperSize(width, height) + base64List.forEach(base64 => { + const image = document.createElement('img') + image.style.width = + direction === PaperDirection.HORIZONTAL + ? paperSize.height + : paperSize.width + image.style.height = + direction === PaperDirection.HORIZONTAL + ? paperSize.width + : paperSize.height + image.src = base64 + container.append(image) + }) + const style = document.createElement('style') + const stylesheet = ` + * { + margin: 0; + padding: 0; + } + @page { + margin: 0; + size: ${paperSize.size} ${ + direction === PaperDirection.HORIZONTAL ? `landscape` : `portrait` + }; + }` + style.append(document.createTextNode(stylesheet)) + setTimeout(() => { + doc.write(`${style.outerHTML}${container.innerHTML}`) + contentWindow.print() + doc.close() + // 移除iframe + window.addEventListener( + 'mouseover', + () => { + iframe?.remove() + }, + { + once: true + } + ) + }) +} diff --git a/src/editor/utils/ua.ts b/src/editor/utils/ua.ts new file mode 100644 index 0000000..07c52c9 --- /dev/null +++ b/src/editor/utils/ua.ts @@ -0,0 +1,13 @@ +export const isApple = + typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent) + +export const isIOS = + typeof navigator !== 'undefined' && /iPad|iPhone/.test(navigator.userAgent) + +export const isMobile = + /Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + navigator.userAgent + ) + +export const isFirefox = + typeof navigator !== 'undefined' && /Firefox/.test(navigator.userAgent) diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..ac0d2e0 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,1958 @@ +import { commentList, data, options } from './mock' +import './style.css' +import prism from 'prismjs' +import Editor, { + BlockType, + Command, + ControlState, + ControlType, + EditorMode, + EditorZone, + ElementType, + IBlock, + ICatalogItem, + IElement, + KeyMap, + ListStyle, + ListType, + PageMode, + PaperDirection, + RowFlex, + TextDecorationStyle, + TitleLevel, + splitText +} from './editor' +import { Dialog } from './components/dialog/Dialog' +import { formatPrismToken } from './utils/prism' +import { Signature } from './components/signature/Signature' +import { debounce, nextTick, scrollIntoView } from './utils' + +window.onload = function () { + const isApple = + typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent) + + // 1. 初始化编辑器 + const container = document.querySelector('.editor')! + const instance = new Editor( + container, + { + header: [ + { + value: '第一人民医院', + size: 32, + rowFlex: RowFlex.CENTER + }, + { + value: '\n门诊病历', + size: 18, + rowFlex: RowFlex.CENTER + }, + { + value: '\n', + type: ElementType.SEPARATOR + } + ], + main: data, + footer: [ + { + value: 'canvas-editor', + size: 12 + } + ] + }, + options + ) + console.log('实例: ', instance) + // cypress使用 + Reflect.set(window, 'editor', instance) + + // 菜单弹窗销毁 + window.addEventListener( + 'click', + evt => { + const visibleDom = document.querySelector('.visible') + if (!visibleDom || visibleDom.contains(evt.target)) return + visibleDom.classList.remove('visible') + }, + { + capture: true + } + ) + + // 2. | 撤销 | 重做 | 格式刷 | 清除格式 | + const undoDom = document.querySelector('.menu-item__undo')! + undoDom.title = `撤销(${isApple ? '⌘' : 'Ctrl'}+Z)` + undoDom.onclick = function () { + console.log('undo') + instance.command.executeUndo() + } + + const redoDom = document.querySelector('.menu-item__redo')! + redoDom.title = `重做(${isApple ? '⌘' : 'Ctrl'}+Y)` + redoDom.onclick = function () { + console.log('redo') + instance.command.executeRedo() + } + + const painterDom = document.querySelector( + '.menu-item__painter' + )! + + let isFirstClick = true + let painterTimeout: number + painterDom.onclick = function () { + if (isFirstClick) { + isFirstClick = false + painterTimeout = window.setTimeout(() => { + console.log('painter-click') + isFirstClick = true + instance.command.executePainter({ + isDblclick: false + }) + }, 200) + } else { + window.clearTimeout(painterTimeout) + } + } + + painterDom.ondblclick = function () { + console.log('painter-dblclick') + isFirstClick = true + window.clearTimeout(painterTimeout) + instance.command.executePainter({ + isDblclick: true + }) + } + + document.querySelector('.menu-item__format')!.onclick = + function () { + console.log('format') + instance.command.executeFormat() + } + + // 3. | 字体 | 字体变大 | 字体变小 | 加粗 | 斜体 | 下划线 | 删除线 | 上标 | 下标 | 字体颜色 | 背景色 | + const fontDom = document.querySelector('.menu-item__font')! + const fontSelectDom = fontDom.querySelector('.select')! + const fontOptionDom = fontDom.querySelector('.options')! + fontDom.onclick = function () { + console.log('font') + fontOptionDom.classList.toggle('visible') + } + fontOptionDom.onclick = function (evt) { + const li = evt.target as HTMLLIElement + instance.command.executeFont(li.dataset.family!) + } + + const sizeSetDom = document.querySelector('.menu-item__size')! + const sizeSelectDom = sizeSetDom.querySelector('.select')! + const sizeOptionDom = sizeSetDom.querySelector('.options')! + sizeSetDom.title = `设置字号` + sizeSetDom.onclick = function () { + console.log('size') + sizeOptionDom.classList.toggle('visible') + } + sizeOptionDom.onclick = function (evt) { + const li = evt.target as HTMLLIElement + instance.command.executeSize(Number(li.dataset.size!)) + } + + const sizeAddDom = document.querySelector( + '.menu-item__size-add' + )! + sizeAddDom.title = `增大字号(${isApple ? '⌘' : 'Ctrl'}+[)` + sizeAddDom.onclick = function () { + console.log('size-add') + instance.command.executeSizeAdd() + } + + const sizeMinusDom = document.querySelector( + '.menu-item__size-minus' + )! + sizeMinusDom.title = `减小字号(${isApple ? '⌘' : 'Ctrl'}+])` + sizeMinusDom.onclick = function () { + console.log('size-minus') + instance.command.executeSizeMinus() + } + + const boldDom = document.querySelector('.menu-item__bold')! + boldDom.title = `加粗(${isApple ? '⌘' : 'Ctrl'}+B)` + boldDom.onclick = function () { + console.log('bold') + instance.command.executeBold() + } + + const italicDom = + document.querySelector('.menu-item__italic')! + italicDom.title = `斜体(${isApple ? '⌘' : 'Ctrl'}+I)` + italicDom.onclick = function () { + console.log('italic') + instance.command.executeItalic() + } + + const underlineDom = document.querySelector( + '.menu-item__underline' + )! + underlineDom.title = `下划线(${isApple ? '⌘' : 'Ctrl'}+U)` + const underlineOptionDom = + underlineDom.querySelector('.options')! + underlineDom.querySelector('.select')!.onclick = + function () { + underlineOptionDom.classList.toggle('visible') + } + underlineDom.querySelector('i')!.onclick = function () { + console.log('underline') + instance.command.executeUnderline() + underlineOptionDom.classList.remove('visible') + } + underlineDom.querySelector('ul')!.onmousedown = function ( + evt + ) { + const li = evt.target as HTMLLIElement + const decorationStyle = li.dataset.decorationStyle + instance.command.executeUnderline({ + style: decorationStyle + }) + underlineOptionDom.classList.remove('visible') + } + + const strikeoutDom = document.querySelector( + '.menu-item__strikeout' + )! + strikeoutDom.onclick = function () { + console.log('strikeout') + instance.command.executeStrikeout() + } + + const superscriptDom = document.querySelector( + '.menu-item__superscript' + )! + superscriptDom.title = `上标(${isApple ? '⌘' : 'Ctrl'}+Shift+,)` + superscriptDom.onclick = function () { + console.log('superscript') + instance.command.executeSuperscript() + } + + const subscriptDom = document.querySelector( + '.menu-item__subscript' + )! + subscriptDom.title = `下标(${isApple ? '⌘' : 'Ctrl'}+Shift+.)` + subscriptDom.onclick = function () { + console.log('subscript') + instance.command.executeSubscript() + } + + const colorControlDom = document.querySelector('#color')! + colorControlDom.oninput = function () { + instance.command.executeColor(colorControlDom.value) + } + const colorDom = document.querySelector('.menu-item__color')! + const colorSpanDom = colorDom.querySelector('span')! + colorDom.onclick = function () { + console.log('color') + colorControlDom.click() + } + + const highlightControlDom = + document.querySelector('#highlight')! + highlightControlDom.oninput = function () { + instance.command.executeHighlight(highlightControlDom.value) + } + const highlightDom = document.querySelector( + '.menu-item__highlight' + )! + const highlightSpanDom = highlightDom.querySelector('span')! + highlightDom.onclick = function () { + console.log('highlight') + highlightControlDom?.click() + } + + const titleDom = document.querySelector('.menu-item__title')! + const titleSelectDom = titleDom.querySelector('.select')! + const titleOptionDom = titleDom.querySelector('.options')! + titleOptionDom.querySelectorAll('li').forEach((li, index) => { + li.title = `Ctrl+${isApple ? 'Option' : 'Alt'}+${index}` + }) + + titleDom.onclick = function () { + console.log('title') + titleOptionDom.classList.toggle('visible') + } + titleOptionDom.onclick = function (evt) { + const li = evt.target as HTMLLIElement + const level = li.dataset.level + instance.command.executeTitle(level || null) + } + + const leftDom = document.querySelector('.menu-item__left')! + leftDom.title = `左对齐(${isApple ? '⌘' : 'Ctrl'}+L)` + leftDom.onclick = function () { + console.log('left') + instance.command.executeRowFlex(RowFlex.LEFT) + } + + const centerDom = + document.querySelector('.menu-item__center')! + centerDom.title = `居中对齐(${isApple ? '⌘' : 'Ctrl'}+E)` + centerDom.onclick = function () { + console.log('center') + instance.command.executeRowFlex(RowFlex.CENTER) + } + + const rightDom = document.querySelector('.menu-item__right')! + rightDom.title = `右对齐(${isApple ? '⌘' : 'Ctrl'}+R)` + rightDom.onclick = function () { + console.log('right') + instance.command.executeRowFlex(RowFlex.RIGHT) + } + + const alignmentDom = document.querySelector( + '.menu-item__alignment' + )! + alignmentDom.title = `两端对齐(${isApple ? '⌘' : 'Ctrl'}+J)` + alignmentDom.onclick = function () { + console.log('alignment') + instance.command.executeRowFlex(RowFlex.ALIGNMENT) + } + + const justifyDom = document.querySelector( + '.menu-item__justify' + )! + justifyDom.title = `分散对齐(${isApple ? '⌘' : 'Ctrl'}+Shift+J)` + justifyDom.onclick = function () { + console.log('justify') + instance.command.executeRowFlex(RowFlex.JUSTIFY) + } + + const rowMarginDom = document.querySelector( + '.menu-item__row-margin' + )! + const rowOptionDom = rowMarginDom.querySelector('.options')! + rowMarginDom.onclick = function () { + console.log('row-margin') + rowOptionDom.classList.toggle('visible') + } + rowOptionDom.onclick = function (evt) { + const li = evt.target as HTMLLIElement + instance.command.executeRowMargin(Number(li.dataset.rowmargin!)) + } + + const listDom = document.querySelector('.menu-item__list')! + listDom.title = `列表(${isApple ? '⌘' : 'Ctrl'}+Shift+U)` + const listOptionDom = listDom.querySelector('.options')! + listDom.onclick = function () { + console.log('list') + listOptionDom.classList.toggle('visible') + } + listOptionDom.onclick = function (evt) { + const li = evt.target as HTMLLIElement + const listType = li.dataset.listType || null + const listStyle = (li.dataset.listStyle) + instance.command.executeList(listType, listStyle) + } + + // 4. | 表格 | 图片 | 超链接 | 分割线 | 水印 | 代码块 | 分隔符 | 控件 | 复选框 | LaTeX | 日期选择器 + const tableDom = document.querySelector('.menu-item__table')! + const tablePanelContainer = document.querySelector( + '.menu-item__table__collapse' + )! + const tableClose = document.querySelector('.table-close')! + const tableTitle = document.querySelector('.table-select')! + const tablePanel = document.querySelector('.table-panel')! + // 绘制行列 + const tableCellList: HTMLDivElement[][] = [] + for (let i = 0; i < 10; i++) { + const tr = document.createElement('tr') + tr.classList.add('table-row') + const trCellList: HTMLDivElement[] = [] + for (let j = 0; j < 10; j++) { + const td = document.createElement('td') + td.classList.add('table-cel') + tr.append(td) + trCellList.push(td) + } + tablePanel.append(tr) + tableCellList.push(trCellList) + } + let colIndex = 0 + let rowIndex = 0 + // 移除所有格选择 + function removeAllTableCellSelect() { + tableCellList.forEach(tr => { + tr.forEach(td => td.classList.remove('active')) + }) + } + // 设置标题内容 + function setTableTitle(payload: string) { + tableTitle.innerText = payload + } + // 恢复初始状态 + function recoveryTable() { + // 还原选择样式、标题、选择行列 + removeAllTableCellSelect() + setTableTitle('插入') + colIndex = 0 + rowIndex = 0 + // 隐藏panel + tablePanelContainer.style.display = 'none' + } + tableDom.onclick = function () { + console.log('table') + tablePanelContainer!.style.display = 'block' + } + tablePanel.onmousemove = function (evt) { + const celSize = 16 + const rowMarginTop = 10 + const celMarginRight = 6 + const { offsetX, offsetY } = evt + // 移除所有选择 + removeAllTableCellSelect() + colIndex = Math.ceil(offsetX / (celSize + celMarginRight)) || 1 + rowIndex = Math.ceil(offsetY / (celSize + rowMarginTop)) || 1 + // 改变选择样式 + tableCellList.forEach((tr, trIndex) => { + tr.forEach((td, tdIndex) => { + if (tdIndex < colIndex && trIndex < rowIndex) { + td.classList.add('active') + } + }) + }) + // 改变表格标题 + setTableTitle(`${rowIndex}×${colIndex}`) + } + tableClose.onclick = function () { + recoveryTable() + } + tablePanel.onclick = function () { + // 应用选择 + instance.command.executeInsertTable(rowIndex, colIndex) + recoveryTable() + } + + const imageDom = document.querySelector('.menu-item__image')! + const imageFileDom = document.querySelector('#image')! + imageDom.onclick = function () { + imageFileDom.click() + } + imageFileDom.onchange = function () { + const file = imageFileDom.files![0]! + const fileReader = new FileReader() + fileReader.readAsDataURL(file) + fileReader.onload = function () { + // 计算宽高 + const image = new Image() + const value = fileReader.result as string + image.src = value + image.onload = function () { + instance.command.executeImage({ + value, + width: image.width, + height: image.height + }) + imageFileDom.value = '' + } + } + } + + const hyperlinkDom = document.querySelector( + '.menu-item__hyperlink' + )! + hyperlinkDom.onclick = function () { + console.log('hyperlink') + new Dialog({ + title: '超链接', + data: [ + { + type: 'text', + label: '文本', + name: 'name', + required: true, + placeholder: '请输入文本', + value: instance.command.getRangeText() + }, + { + type: 'text', + label: '链接', + name: 'url', + required: true, + placeholder: '请输入链接' + } + ], + onConfirm: payload => { + const name = payload.find(p => p.name === 'name')?.value + if (!name) return + const url = payload.find(p => p.name === 'url')?.value + if (!url) return + instance.command.executeHyperlink({ + type: ElementType.HYPERLINK, + value: '', + url, + valueList: splitText(name).map(n => ({ + value: n, + size: 16 + })) + }) + } + }) + } + + const separatorDom = document.querySelector( + '.menu-item__separator' + )! + const separatorOptionDom = + separatorDom.querySelector('.options')! + separatorDom.onclick = function () { + console.log('separator') + separatorOptionDom.classList.toggle('visible') + } + separatorOptionDom.onmousedown = function (evt) { + let payload: number[] = [] + const li = evt.target as HTMLLIElement + const separatorDash = li.dataset.separator?.split(',').map(Number) + if (separatorDash) { + const isSingleLine = separatorDash.every(d => d === 0) + if (!isSingleLine) { + payload = separatorDash + } + } + instance.command.executeSeparator(payload) + } + + const pageBreakDom = document.querySelector( + '.menu-item__page-break' + )! + pageBreakDom.onclick = function () { + console.log('pageBreak') + instance.command.executePageBreak() + } + + const watermarkDom = document.querySelector( + '.menu-item__watermark' + )! + const watermarkOptionDom = + watermarkDom.querySelector('.options')! + watermarkDom.onclick = function () { + console.log('watermark') + watermarkOptionDom.classList.toggle('visible') + } + watermarkOptionDom.onmousedown = function (evt) { + const li = evt.target as HTMLLIElement + const menu = li.dataset.menu! + watermarkOptionDom.classList.toggle('visible') + if (menu === 'add') { + new Dialog({ + title: '水印', + data: [ + { + type: 'text', + label: '内容', + name: 'data', + required: true, + placeholder: '请输入内容' + }, + { + type: 'color', + label: '颜色', + name: 'color', + required: true, + value: '#AEB5C0' + }, + { + type: 'number', + label: '字体大小', + name: 'size', + required: true, + value: '120' + }, + { + type: 'number', + label: '透明度', + name: 'opacity', + required: true, + value: '0.3' + }, + { + type: 'select', + label: '重复', + name: 'repeat', + value: '0', + required: false, + options: [ + { + label: '不重复', + value: '0' + }, + { + label: '重复', + value: '1' + } + ] + }, + { + type: 'number', + label: '水平间隔', + name: 'horizontalGap', + required: false, + value: '10' + }, + { + type: 'number', + label: '垂直间隔', + name: 'verticalGap', + required: false, + value: '10' + } + ], + onConfirm: payload => { + const nullableIndex = payload.findIndex(p => !p.value) + if (~nullableIndex) return + const watermark = payload.reduce((pre, cur) => { + pre[cur.name] = cur.value + return pre + }, {}) + const repeat = watermark.repeat === '1' + instance.command.executeAddWatermark({ + data: watermark.data, + color: watermark.color, + size: Number(watermark.size), + opacity: Number(watermark.opacity), + repeat, + gap: + repeat && watermark.horizontalGap && watermark.verticalGap + ? [ + Number(watermark.horizontalGap), + Number(watermark.verticalGap) + ] + : undefined + }) + } + }) + } else { + instance.command.executeDeleteWatermark() + } + } + + const codeblockDom = document.querySelector( + '.menu-item__codeblock' + )! + codeblockDom.onclick = function () { + console.log('codeblock') + new Dialog({ + title: '代码块', + data: [ + { + type: 'textarea', + name: 'codeblock', + placeholder: '请输入代码', + width: 500, + height: 300 + } + ], + onConfirm: payload => { + const codeblock = payload.find(p => p.name === 'codeblock')?.value + if (!codeblock) return + const tokenList = prism.tokenize(codeblock, prism.languages.javascript) + const formatTokenList = formatPrismToken(tokenList) + const elementList: IElement[] = [] + for (let i = 0; i < formatTokenList.length; i++) { + const formatToken = formatTokenList[i] + const tokenStringList = splitText(formatToken.content) + for (let j = 0; j < tokenStringList.length; j++) { + const value = tokenStringList[j] + const element: IElement = { + value + } + if (formatToken.color) { + element.color = formatToken.color + } + if (formatToken.bold) { + element.bold = true + } + if (formatToken.italic) { + element.italic = true + } + elementList.push(element) + } + } + elementList.unshift({ + value: '\n' + }) + instance.command.executeInsertElementList(elementList) + } + }) + } + + const controlDom = document.querySelector( + '.menu-item__control' + )! + const controlOptionDom = controlDom.querySelector('.options')! + controlDom.onclick = function () { + console.log('control') + controlOptionDom.classList.toggle('visible') + } + controlOptionDom.onmousedown = function (evt) { + controlOptionDom.classList.toggle('visible') + const li = evt.target as HTMLLIElement + const type = li.dataset.control + switch (type) { + case ControlType.TEXT: + new Dialog({ + title: '文本控件', + data: [ + { + type: 'text', + label: '占位符', + name: 'placeholder', + required: true, + placeholder: '请输入占位符' + }, + { + type: 'text', + label: '默认值', + name: 'value', + placeholder: '请输入默认值' + } + ], + onConfirm: payload => { + const placeholder = payload.find( + p => p.name === 'placeholder' + )?.value + if (!placeholder) return + const value = payload.find(p => p.name === 'value')?.value || '' + instance.command.executeInsertControl({ + type: ElementType.CONTROL, + value: '', + control: { + type, + value: value + ? [ + { + value + } + ] + : null, + placeholder + } + }) + } + }) + break + case ControlType.SELECT: + new Dialog({ + title: '列举控件', + data: [ + { + type: 'text', + label: '占位符', + name: 'placeholder', + required: true, + placeholder: '请输入占位符' + }, + { + type: 'text', + label: '默认值', + name: 'code', + placeholder: '请输入默认值' + }, + { + type: 'textarea', + label: '值集', + name: 'valueSets', + required: true, + height: 100, + placeholder: `请输入值集JSON,例:\n[{\n"value":"有",\n"code":"98175"\n}]` + } + ], + onConfirm: payload => { + const placeholder = payload.find( + p => p.name === 'placeholder' + )?.value + if (!placeholder) return + const valueSets = payload.find(p => p.name === 'valueSets')?.value + if (!valueSets) return + const code = payload.find(p => p.name === 'code')?.value + instance.command.executeInsertControl({ + type: ElementType.CONTROL, + value: '', + control: { + type, + code, + value: null, + placeholder, + valueSets: JSON.parse(valueSets) + } + }) + } + }) + break + case ControlType.CHECKBOX: + new Dialog({ + title: '复选框控件', + data: [ + { + type: 'text', + label: '默认值', + name: 'code', + placeholder: '请输入默认值,多个值以英文逗号分割' + }, + { + type: 'textarea', + label: '值集', + name: 'valueSets', + required: true, + height: 100, + placeholder: `请输入值集JSON,例:\n[{\n"value":"有",\n"code":"98175"\n}]` + } + ], + onConfirm: payload => { + const valueSets = payload.find(p => p.name === 'valueSets')?.value + if (!valueSets) return + const code = payload.find(p => p.name === 'code')?.value + instance.command.executeInsertControl({ + type: ElementType.CONTROL, + value: '', + control: { + type, + code, + value: null, + valueSets: JSON.parse(valueSets) + } + }) + } + }) + break + case ControlType.RADIO: + new Dialog({ + title: '单选框控件', + data: [ + { + type: 'text', + label: '默认值', + name: 'code', + placeholder: '请输入默认值' + }, + { + type: 'textarea', + label: '值集', + name: 'valueSets', + required: true, + height: 100, + placeholder: `请输入值集JSON,例:\n[{\n"value":"有",\n"code":"98175"\n}]` + } + ], + onConfirm: payload => { + const valueSets = payload.find(p => p.name === 'valueSets')?.value + if (!valueSets) return + const code = payload.find(p => p.name === 'code')?.value + instance.command.executeInsertControl({ + type: ElementType.CONTROL, + value: '', + control: { + type, + code, + value: null, + valueSets: JSON.parse(valueSets) + } + }) + } + }) + break + case ControlType.DATE: + new Dialog({ + title: '日期控件', + data: [ + { + type: 'text', + label: '占位符', + name: 'placeholder', + required: true, + placeholder: '请输入占位符' + }, + { + type: 'text', + label: '默认值', + name: 'value', + placeholder: '请输入默认值' + }, + { + type: 'select', + label: '日期格式', + name: 'dateFormat', + value: 'yyyy-MM-dd hh:mm:ss', + required: true, + options: [ + { + label: 'yyyy-MM-dd hh:mm:ss', + value: 'yyyy-MM-dd hh:mm:ss' + }, + { + label: 'yyyy-MM-dd', + value: 'yyyy-MM-dd' + } + ] + } + ], + onConfirm: payload => { + const placeholder = payload.find( + p => p.name === 'placeholder' + )?.value + if (!placeholder) return + const value = payload.find(p => p.name === 'value')?.value || '' + const dateFormat = + payload.find(p => p.name === 'dateFormat')?.value || '' + instance.command.executeInsertControl({ + type: ElementType.CONTROL, + value: '', + control: { + type, + dateFormat, + value: value + ? [ + { + value + } + ] + : null, + placeholder + } + }) + } + }) + break + case ControlType.NUMBER: + new Dialog({ + title: '数值控件', + data: [ + { + type: 'text', + label: '占位符', + name: 'placeholder', + required: true, + placeholder: '请输入占位符' + }, + { + type: 'text', + label: '默认值', + name: 'value', + placeholder: '请输入默认值' + } + ], + onConfirm: payload => { + const placeholder = payload.find( + p => p.name === 'placeholder' + )?.value + if (!placeholder) return + const value = payload.find(p => p.name === 'value')?.value || '' + instance.command.executeInsertControl({ + type: ElementType.CONTROL, + value: '', + control: { + type, + value: value + ? [ + { + value + } + ] + : null, + placeholder + } + }) + } + }) + break + default: + break + } + } + + const checkboxDom = document.querySelector( + '.menu-item__checkbox' + )! + checkboxDom.onclick = function () { + console.log('checkbox') + instance.command.executeInsertElementList([ + { + type: ElementType.CHECKBOX, + checkbox: { + value: false + }, + value: '' + } + ]) + } + + const radioDom = document.querySelector('.menu-item__radio')! + radioDom.onclick = function () { + console.log('radio') + instance.command.executeInsertElementList([ + { + type: ElementType.RADIO, + checkbox: { + value: false + }, + value: '' + } + ]) + } + + const latexDom = document.querySelector('.menu-item__latex')! + latexDom.onclick = function () { + console.log('LaTeX') + new Dialog({ + title: 'LaTeX', + data: [ + { + type: 'textarea', + height: 100, + name: 'value', + placeholder: '请输入LaTeX文本' + } + ], + onConfirm: payload => { + const value = payload.find(p => p.name === 'value')?.value + if (!value) return + instance.command.executeInsertElementList([ + { + type: ElementType.LATEX, + value + } + ]) + } + }) + } + + const dateDom = document.querySelector('.menu-item__date')! + const dateDomOptionDom = dateDom.querySelector('.options')! + dateDom.onclick = function () { + console.log('date') + dateDomOptionDom.classList.toggle('visible') + // 定位调整 + const bodyRect = document.body.getBoundingClientRect() + const dateDomOptionRect = dateDomOptionDom.getBoundingClientRect() + if (dateDomOptionRect.left + dateDomOptionRect.width > bodyRect.width) { + dateDomOptionDom.style.right = '0px' + dateDomOptionDom.style.left = 'unset' + } else { + dateDomOptionDom.style.right = 'unset' + dateDomOptionDom.style.left = '0px' + } + // 当前日期 + const date = new Date() + const year = date.getFullYear().toString() + const month = (date.getMonth() + 1).toString().padStart(2, '0') + const day = date.getDate().toString().padStart(2, '0') + const hour = date.getHours().toString().padStart(2, '0') + const minute = date.getMinutes().toString().padStart(2, '0') + const second = date.getSeconds().toString().padStart(2, '0') + const dateString = `${year}-${month}-${day}` + const dateTimeString = `${dateString} ${hour}:${minute}:${second}` + dateDomOptionDom.querySelector('li:first-child')!.innerText = + dateString + dateDomOptionDom.querySelector('li:last-child')!.innerText = + dateTimeString + } + dateDomOptionDom.onmousedown = function (evt) { + const li = evt.target as HTMLLIElement + const dateFormat = li.dataset.format! + dateDomOptionDom.classList.toggle('visible') + instance.command.executeInsertElementList([ + { + type: ElementType.DATE, + value: '', + dateFormat, + valueList: [ + { + value: li.innerText.trim() + } + ] + } + ]) + } + + const blockDom = document.querySelector('.menu-item__block')! + blockDom.onclick = function () { + console.log('block') + new Dialog({ + title: '内容块', + data: [ + { + type: 'select', + label: '类型', + name: 'type', + value: 'iframe', + required: true, + options: [ + { + label: '网址', + value: 'iframe' + }, + { + label: '视频', + value: 'video' + } + ] + }, + { + type: 'number', + label: '宽度', + name: 'width', + placeholder: '请输入宽度(默认页面内宽度)' + }, + { + type: 'number', + label: '高度', + name: 'height', + required: true, + placeholder: '请输入高度' + }, + { + type: 'input', + label: '地址', + name: 'src', + required: false, + placeholder: '请输入地址' + }, + { + type: 'textarea', + label: 'HTML', + height: 100, + name: 'srcdoc', + required: false, + placeholder: '请输入HTML代码(仅网址类型有效)' + } + ], + onConfirm: payload => { + const type = payload.find(p => p.name === 'type')?.value + if (!type) return + const width = payload.find(p => p.name === 'width')?.value + const height = payload.find(p => p.name === 'height')?.value + if (!height) return + // 地址或HTML代码至少存在一项 + const src = payload.find(p => p.name === 'src')?.value + const srcdoc = payload.find(p => p.name === 'srcdoc')?.value + const block: IBlock = { + type: type + } + if (block.type === BlockType.IFRAME) { + if (!src && !srcdoc) return + block.iframeBlock = { + src, + srcdoc + } + } else if (block.type === BlockType.VIDEO) { + if (!src) return + block.videoBlock = { + src + } + } + const blockElement: IElement = { + type: ElementType.BLOCK, + value: '', + height: Number(height), + block + } + if (width) { + blockElement.width = Number(width) + } + instance.command.executeInsertElementList([blockElement]) + } + }) + } + + // 5. | 搜索&替换 | 打印 | + const searchCollapseDom = document.querySelector( + '.menu-item__search__collapse' + )! + const searchInputDom = document.querySelector( + '.menu-item__search__collapse__search input' + )! + const replaceInputDom = document.querySelector( + '.menu-item__search__collapse__replace input' + )! + const searchDom = + document.querySelector('.menu-item__search')! + searchDom.title = `搜索与替换(${isApple ? '⌘' : 'Ctrl'}+F)` + const searchResultDom = + searchCollapseDom.querySelector('.search-result')! + function setSearchResult() { + const result = instance.command.getSearchNavigateInfo() + if (result) { + const { index, count } = result + searchResultDom.innerText = `${index}/${count}` + } else { + searchResultDom.innerText = '' + } + } + searchDom.onclick = function () { + console.log('search') + searchCollapseDom.style.display = 'block' + const bodyRect = document.body.getBoundingClientRect() + const searchRect = searchDom.getBoundingClientRect() + const searchCollapseRect = searchCollapseDom.getBoundingClientRect() + if (searchRect.left + searchCollapseRect.width > bodyRect.width) { + searchCollapseDom.style.right = '0px' + searchCollapseDom.style.left = 'unset' + } else { + searchCollapseDom.style.right = 'unset' + } + searchInputDom.focus() + } + searchCollapseDom.querySelector('span')!.onclick = + function () { + searchCollapseDom.style.display = 'none' + searchInputDom.value = '' + replaceInputDom.value = '' + instance.command.executeSearch(null) + setSearchResult() + } + searchInputDom.oninput = function () { + instance.command.executeSearch(searchInputDom.value || null) + setSearchResult() + } + searchInputDom.onkeydown = function (evt) { + if (evt.key === 'Enter') { + instance.command.executeSearch(searchInputDom.value || null) + setSearchResult() + } + } + searchCollapseDom.querySelector('button')!.onclick = + function () { + const searchValue = searchInputDom.value + const replaceValue = replaceInputDom.value + if (searchValue && searchValue !== replaceValue) { + instance.command.executeReplace(replaceValue) + } + } + searchCollapseDom.querySelector('.arrow-left')!.onclick = + function () { + instance.command.executeSearchNavigatePre() + setSearchResult() + } + searchCollapseDom.querySelector('.arrow-right')!.onclick = + function () { + instance.command.executeSearchNavigateNext() + setSearchResult() + } + + const printDom = document.querySelector('.menu-item__print')! + printDom.title = `打印(${isApple ? '⌘' : 'Ctrl'}+P)` + printDom.onclick = function () { + console.log('print') + instance.command.executePrint() + } + + // 6. 目录显隐 | 页面模式 | 纸张缩放 | 纸张大小 | 纸张方向 | 页边距 | 全屏 | 设置 + const editorOptionDom = + document.querySelector('.editor-option')! + editorOptionDom.onclick = function () { + const options = instance.command.getOptions() + new Dialog({ + title: '编辑器配置', + data: [ + { + type: 'textarea', + name: 'option', + width: 350, + height: 300, + required: true, + value: JSON.stringify(options, null, 2), + placeholder: '请输入编辑器配置' + } + ], + onConfirm: payload => { + const newOptionValue = payload.find(p => p.name === 'option')?.value + if (!newOptionValue) return + const newOption = JSON.parse(newOptionValue) + instance.command.executeUpdateOptions(newOption) + } + }) + } + + async function updateCatalog() { + const catalog = await instance.command.getCatalog() + const catalogMainDom = + document.querySelector('.catalog__main')! + catalogMainDom.innerHTML = '' + if (catalog) { + const appendCatalog = ( + parent: HTMLDivElement, + catalogItems: ICatalogItem[] + ) => { + for (let c = 0; c < catalogItems.length; c++) { + const catalogItem = catalogItems[c] + const catalogItemDom = document.createElement('div') + catalogItemDom.classList.add('catalog-item') + // 渲染 + const catalogItemContentDom = document.createElement('div') + catalogItemContentDom.classList.add('catalog-item__content') + const catalogItemContentSpanDom = document.createElement('span') + catalogItemContentSpanDom.innerText = catalogItem.name + catalogItemContentDom.append(catalogItemContentSpanDom) + // 定位 + catalogItemContentDom.onclick = () => { + instance.command.executeLocationCatalog(catalogItem.id) + } + catalogItemDom.append(catalogItemContentDom) + if (catalogItem.subCatalog && catalogItem.subCatalog.length) { + appendCatalog(catalogItemDom, catalogItem.subCatalog) + } + // 追加 + parent.append(catalogItemDom) + } + } + appendCatalog(catalogMainDom, catalog) + } + } + let isCatalogShow = true + const catalogDom = document.querySelector('.catalog')! + const catalogModeDom = + document.querySelector('.catalog-mode')! + const catalogHeaderCloseDom = document.querySelector( + '.catalog__header__close' + )! + const switchCatalog = () => { + isCatalogShow = !isCatalogShow + if (isCatalogShow) { + catalogDom.style.display = 'block' + updateCatalog() + } else { + catalogDom.style.display = 'none' + } + } + catalogModeDom.onclick = switchCatalog + catalogHeaderCloseDom.onclick = switchCatalog + + const pageModeDom = document.querySelector('.page-mode')! + const pageModeOptionsDom = + pageModeDom.querySelector('.options')! + pageModeDom.onclick = function () { + pageModeOptionsDom.classList.toggle('visible') + } + pageModeOptionsDom.onclick = function (evt) { + const li = evt.target as HTMLLIElement + instance.command.executePageMode(li.dataset.pageMode!) + } + + document.querySelector('.page-scale-percentage')!.onclick = + function () { + console.log('page-scale-recovery') + instance.command.executePageScaleRecovery() + } + + document.querySelector('.page-scale-minus')!.onclick = + function () { + console.log('page-scale-minus') + instance.command.executePageScaleMinus() + } + + document.querySelector('.page-scale-add')!.onclick = + function () { + console.log('page-scale-add') + instance.command.executePageScaleAdd() + } + + // 纸张大小 + const paperSizeDom = document.querySelector('.paper-size')! + const paperSizeDomOptionsDom = + paperSizeDom.querySelector('.options')! + paperSizeDom.onclick = function () { + paperSizeDomOptionsDom.classList.toggle('visible') + } + paperSizeDomOptionsDom.onclick = function (evt) { + const li = evt.target as HTMLLIElement + const paperType = li.dataset.paperSize! + const [width, height] = paperType.split('*').map(Number) + instance.command.executePaperSize(width, height) + // 纸张状态回显 + paperSizeDomOptionsDom + .querySelectorAll('li') + .forEach(child => child.classList.remove('active')) + li.classList.add('active') + } + + // 纸张方向 + const paperDirectionDom = + document.querySelector('.paper-direction')! + const paperDirectionDomOptionsDom = + paperDirectionDom.querySelector('.options')! + paperDirectionDom.onclick = function () { + paperDirectionDomOptionsDom.classList.toggle('visible') + } + paperDirectionDomOptionsDom.onclick = function (evt) { + const li = evt.target as HTMLLIElement + const paperDirection = li.dataset.paperDirection! + instance.command.executePaperDirection(paperDirection) + // 纸张方向状态回显 + paperDirectionDomOptionsDom + .querySelectorAll('li') + .forEach(child => child.classList.remove('active')) + li.classList.add('active') + } + + // 页面边距 + const paperMarginDom = + document.querySelector('.paper-margin')! + paperMarginDom.onclick = function () { + const [topMargin, rightMargin, bottomMargin, leftMargin] = + instance.command.getPaperMargin() + new Dialog({ + title: '页边距', + data: [ + { + type: 'text', + label: '上边距', + name: 'top', + required: true, + value: `${topMargin}`, + placeholder: '请输入上边距' + }, + { + type: 'text', + label: '下边距', + name: 'bottom', + required: true, + value: `${bottomMargin}`, + placeholder: '请输入下边距' + }, + { + type: 'text', + label: '左边距', + name: 'left', + required: true, + value: `${leftMargin}`, + placeholder: '请输入左边距' + }, + { + type: 'text', + label: '右边距', + name: 'right', + required: true, + value: `${rightMargin}`, + placeholder: '请输入右边距' + } + ], + onConfirm: payload => { + const top = payload.find(p => p.name === 'top')?.value + if (!top) return + const bottom = payload.find(p => p.name === 'bottom')?.value + if (!bottom) return + const left = payload.find(p => p.name === 'left')?.value + if (!left) return + const right = payload.find(p => p.name === 'right')?.value + if (!right) return + instance.command.executeSetPaperMargin([ + Number(top), + Number(right), + Number(bottom), + Number(left) + ]) + } + }) + } + + // 全屏 + const fullscreenDom = document.querySelector('.fullscreen')! + fullscreenDom.onclick = toggleFullscreen + window.addEventListener('keydown', evt => { + if (evt.key === 'F11') { + toggleFullscreen() + evt.preventDefault() + } + }) + document.addEventListener('fullscreenchange', () => { + fullscreenDom.classList.toggle('exist') + }) + function toggleFullscreen() { + console.log('fullscreen') + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen() + } else { + document.exitFullscreen() + } + } + + // 7. 编辑器使用模式 + let modeIndex = 0 + const modeList = [ + { + mode: EditorMode.EDIT, + name: '编辑模式' + }, + { + mode: EditorMode.CLEAN, + name: '清洁模式' + }, + { + mode: EditorMode.READONLY, + name: '只读模式' + }, + { + mode: EditorMode.FORM, + name: '表单模式' + }, + { + mode: EditorMode.PRINT, + name: '打印模式' + }, + { + mode: EditorMode.DESIGN, + name: '设计模式' + } + ] + const modeElement = document.querySelector('.editor-mode')! + modeElement.onclick = function () { + // 模式选择循环 + modeIndex === modeList.length - 1 ? (modeIndex = 0) : modeIndex++ + // 设置模式 + const { name, mode } = modeList[modeIndex] + modeElement.innerText = name + instance.command.executeMode(mode) + // 设置菜单栏权限视觉反馈 + const isReadonly = mode === EditorMode.READONLY + const enableMenuList = ['search', 'print'] + document.querySelectorAll('.menu-item>div').forEach(dom => { + const menu = dom.dataset.menu + isReadonly && (!menu || !enableMenuList.includes(menu)) + ? dom.classList.add('disable') + : dom.classList.remove('disable') + }) + } + + // 模拟批注 + const commentDom = document.querySelector('.comment')! + async function updateComment() { + const groupIds = await instance.command.getGroupIds() + for (const comment of commentList) { + const activeCommentDom = commentDom.querySelector( + `.comment-item[data-id='${comment.id}']` + ) + // 编辑器是否存在对应成组id + if (groupIds.includes(comment.id)) { + // 当前dom是否存在-不存在则追加 + if (!activeCommentDom) { + const commentItem = document.createElement('div') + commentItem.classList.add('comment-item') + commentItem.setAttribute('data-id', comment.id) + commentItem.onclick = () => { + instance.command.executeLocationGroup(comment.id) + } + commentDom.append(commentItem) + // 选区信息 + const commentItemTitle = document.createElement('div') + commentItemTitle.classList.add('comment-item__title') + commentItemTitle.append(document.createElement('span')) + const commentItemTitleContent = document.createElement('span') + commentItemTitleContent.innerText = comment.rangeText + commentItemTitle.append(commentItemTitleContent) + const closeDom = document.createElement('i') + closeDom.onclick = () => { + instance.command.executeDeleteGroup(comment.id) + } + commentItemTitle.append(closeDom) + commentItem.append(commentItemTitle) + // 基础信息 + const commentItemInfo = document.createElement('div') + commentItemInfo.classList.add('comment-item__info') + const commentItemInfoName = document.createElement('span') + commentItemInfoName.innerText = comment.userName + const commentItemInfoDate = document.createElement('span') + commentItemInfoDate.innerText = comment.createdDate + commentItemInfo.append(commentItemInfoName) + commentItemInfo.append(commentItemInfoDate) + commentItem.append(commentItemInfo) + // 详细评论 + const commentItemContent = document.createElement('div') + commentItemContent.classList.add('comment-item__content') + commentItemContent.innerText = comment.content + commentItem.append(commentItemContent) + commentDom.append(commentItem) + } + } else { + // 编辑器内不存在对应成组id则dom则移除 + activeCommentDom?.remove() + } + } + } + // 8. 内部事件监听 + instance.listener.rangeStyleChange = function (payload) { + // 控件类型 + payload.type === ElementType.SUBSCRIPT + ? subscriptDom.classList.add('active') + : subscriptDom.classList.remove('active') + payload.type === ElementType.SUPERSCRIPT + ? superscriptDom.classList.add('active') + : superscriptDom.classList.remove('active') + payload.type === ElementType.SEPARATOR + ? separatorDom.classList.add('active') + : separatorDom.classList.remove('active') + separatorOptionDom + .querySelectorAll('li') + .forEach(li => li.classList.remove('active')) + if (payload.type === ElementType.SEPARATOR) { + const separator = payload.dashArray.join(',') || '0,0' + const curSeparatorDom = separatorOptionDom.querySelector( + `[data-separator='${separator}']` + )! + if (curSeparatorDom) { + curSeparatorDom.classList.add('active') + } + } + + // 富文本 + fontOptionDom + .querySelectorAll('li') + .forEach(li => li.classList.remove('active')) + const curFontDom = fontOptionDom.querySelector( + `[data-family='${payload.font}']` + ) + if (curFontDom) { + fontSelectDom.innerText = curFontDom.innerText + fontSelectDom.style.fontFamily = payload.font + curFontDom.classList.add('active') + } + sizeOptionDom + .querySelectorAll('li') + .forEach(li => li.classList.remove('active')) + const curSizeDom = sizeOptionDom.querySelector( + `[data-size='${payload.size}']` + ) + if (curSizeDom) { + sizeSelectDom.innerText = curSizeDom.innerText + curSizeDom.classList.add('active') + } else { + sizeSelectDom.innerText = `${payload.size}` + } + payload.bold + ? boldDom.classList.add('active') + : boldDom.classList.remove('active') + payload.italic + ? italicDom.classList.add('active') + : italicDom.classList.remove('active') + payload.underline + ? underlineDom.classList.add('active') + : underlineDom.classList.remove('active') + payload.strikeout + ? strikeoutDom.classList.add('active') + : strikeoutDom.classList.remove('active') + if (payload.color) { + colorDom.classList.add('active') + colorControlDom.value = payload.color + colorSpanDom.style.backgroundColor = payload.color + } else { + colorDom.classList.remove('active') + colorControlDom.value = '#000000' + colorSpanDom.style.backgroundColor = '#000000' + } + if (payload.highlight) { + highlightDom.classList.add('active') + highlightControlDom.value = payload.highlight + highlightSpanDom.style.backgroundColor = payload.highlight + } else { + highlightDom.classList.remove('active') + highlightControlDom.value = '#ffff00' + highlightSpanDom.style.backgroundColor = '#ffff00' + } + + // 行布局 + leftDom.classList.remove('active') + centerDom.classList.remove('active') + rightDom.classList.remove('active') + alignmentDom.classList.remove('active') + justifyDom.classList.remove('active') + if (payload.rowFlex && payload.rowFlex === 'right') { + rightDom.classList.add('active') + } else if (payload.rowFlex && payload.rowFlex === 'center') { + centerDom.classList.add('active') + } else if (payload.rowFlex && payload.rowFlex === 'alignment') { + alignmentDom.classList.add('active') + } else if (payload.rowFlex && payload.rowFlex === 'justify') { + justifyDom.classList.add('active') + } else { + leftDom.classList.add('active') + } + + // 行间距 + rowOptionDom + .querySelectorAll('li') + .forEach(li => li.classList.remove('active')) + const curRowMarginDom = rowOptionDom.querySelector( + `[data-rowmargin='${payload.rowMargin}']` + )! + curRowMarginDom.classList.add('active') + + // 功能 + payload.undo + ? undoDom.classList.remove('no-allow') + : undoDom.classList.add('no-allow') + payload.redo + ? redoDom.classList.remove('no-allow') + : redoDom.classList.add('no-allow') + payload.painter + ? painterDom.classList.add('active') + : painterDom.classList.remove('active') + + // 标题 + titleOptionDom + .querySelectorAll('li') + .forEach(li => li.classList.remove('active')) + if (payload.level) { + const curTitleDom = titleOptionDom.querySelector( + `[data-level='${payload.level}']` + )! + titleSelectDom.innerText = curTitleDom.innerText + curTitleDom.classList.add('active') + } else { + titleSelectDom.innerText = '正文' + titleOptionDom.querySelector('li:first-child')!.classList.add('active') + } + + // 列表 + listOptionDom + .querySelectorAll('li') + .forEach(li => li.classList.remove('active')) + if (payload.listType) { + listDom.classList.add('active') + const listType = payload.listType + const listStyle = + payload.listType === ListType.OL ? ListStyle.DECIMAL : payload.listType + const curListDom = listOptionDom.querySelector( + `[data-list-type='${listType}'][data-list-style='${listStyle}']` + ) + if (curListDom) { + curListDom.classList.add('active') + } + } else { + listDom.classList.remove('active') + } + + // 批注 + commentDom + .querySelectorAll('.comment-item') + .forEach(commentItemDom => { + commentItemDom.classList.remove('active') + }) + if (payload.groupIds) { + const [id] = payload.groupIds + const activeCommentDom = commentDom.querySelector( + `.comment-item[data-id='${id}']` + ) + if (activeCommentDom) { + activeCommentDom.classList.add('active') + scrollIntoView(commentDom, activeCommentDom) + } + } + + // 行列信息 + const rangeContext = instance.command.getRangeContext() + if (rangeContext) { + document.querySelector('.row-no')!.innerText = `${ + rangeContext.startRowNo + 1 + }` + document.querySelector('.col-no')!.innerText = `${ + rangeContext.startColNo + 1 + }` + } + } + + instance.listener.visiblePageNoListChange = function (payload) { + const text = payload.map(i => i + 1).join('、') + document.querySelector('.page-no-list')!.innerText = text + } + + instance.listener.pageSizeChange = function (payload) { + document.querySelector( + '.page-size' + )!.innerText = `${payload}` + } + + instance.listener.intersectionPageNoChange = function (payload) { + document.querySelector('.page-no')!.innerText = `${ + payload + 1 + }` + } + + instance.listener.pageScaleChange = function (payload) { + document.querySelector( + '.page-scale-percentage' + )!.innerText = `${Math.floor(payload * 10 * 10)}%` + } + + instance.listener.controlChange = function (payload) { + const disableMenusInControlContext = [ + 'table', + 'hyperlink', + 'separator', + 'page-break', + 'control' + ] + // 菜单操作权限 + disableMenusInControlContext.forEach(menu => { + const menuDom = document.querySelector( + `.menu-item__${menu}` + )! + payload.state === ControlState.ACTIVE + ? menuDom.classList.add('disable') + : menuDom.classList.remove('disable') + }) + } + + instance.listener.pageModeChange = function (payload) { + const activeMode = pageModeOptionsDom.querySelector( + `[data-page-mode='${payload}']` + )! + pageModeOptionsDom + .querySelectorAll('li') + .forEach(li => li.classList.remove('active')) + activeMode.classList.add('active') + } + + const handleContentChange = async function () { + // 字数 + const wordCount = await instance.command.getWordCount() + document.querySelector('.word-count')!.innerText = `${ + wordCount || 0 + }` + // 目录 + if (isCatalogShow) { + nextTick(() => { + updateCatalog() + }) + } + // 批注 + nextTick(() => { + updateComment() + }) + } + instance.listener.contentChange = debounce(handleContentChange, 200) + handleContentChange() + + instance.listener.saved = function (payload) { + console.log('elementList: ', payload) + } + + // 9. 右键菜单注册 + instance.register.contextMenuList([ + { + name: '批注', + when: payload => { + return ( + !payload.isReadonly && + payload.editorHasSelection && + payload.zone === EditorZone.MAIN + ) + }, + callback: (command: Command) => { + new Dialog({ + title: '批注', + data: [ + { + type: 'textarea', + label: '批注', + height: 100, + name: 'value', + required: true, + placeholder: '请输入批注' + } + ], + onConfirm: payload => { + const value = payload.find(p => p.name === 'value')?.value + if (!value) return + const groupId = command.executeSetGroup() + if (!groupId) return + commentList.push({ + id: groupId, + content: value, + userName: 'Hufe', + rangeText: command.getRangeText(), + createdDate: new Date().toLocaleString() + }) + } + }) + } + }, + { + name: '签名', + icon: 'signature', + when: payload => { + return !payload.isReadonly && payload.editorTextFocus + }, + callback: (command: Command) => { + new Signature({ + onConfirm(payload) { + if (!payload) return + const { value, width, height } = payload + if (!value || !width || !height) return + command.executeInsertElementList([ + { + value, + width, + height, + type: ElementType.IMAGE + } + ]) + } + }) + } + }, + { + name: '格式整理', + icon: 'word-tool', + when: payload => { + return !payload.isReadonly + }, + callback: (command: Command) => { + command.executeWordTool() + } + } + ]) + + // 10. 快捷键注册 + instance.register.shortcutList([ + { + key: KeyMap.P, + mod: true, + isGlobal: true, + callback: (command: Command) => { + command.executePrint() + } + }, + { + key: KeyMap.F, + mod: true, + isGlobal: true, + callback: (command: Command) => { + const text = command.getRangeText() + searchDom.click() + if (text) { + searchInputDom.value = text + instance.command.executeSearch(text) + setSearchResult() + } + } + }, + { + key: KeyMap.MINUS, + ctrl: true, + isGlobal: true, + callback: (command: Command) => { + command.executePageScaleMinus() + } + }, + { + key: KeyMap.EQUAL, + ctrl: true, + isGlobal: true, + callback: (command: Command) => { + command.executePageScaleAdd() + } + }, + { + key: KeyMap.ZERO, + ctrl: true, + isGlobal: true, + callback: (command: Command) => { + command.executePageScaleRecovery() + } + } + ]) +} diff --git a/src/mock.ts b/src/mock.ts new file mode 100644 index 0000000..c74a257 --- /dev/null +++ b/src/mock.ts @@ -0,0 +1,505 @@ +import { + ControlType, + ElementType, + IEditorOption, + IElement, + ListType, + TitleLevel +} from './editor' + +const text = `主诉:\n发热三天,咳嗽五天。\n现病史:\n患者于三天前无明显诱因,感冒后发现面部水肿,无皮疹,尿量减少,出现乏力,在外治疗无好转,现来我院就诊。\n既往史:\n有糖尿病10年,有高血压2年,有传染性疾病1年。报告其他既往疾病。\n流行病史:\n否认14天内接触过确诊患者、疑似患者、无症状感染者及其密切接触者;否认14天内去过以下场所:水产、肉类批发市场,农贸市场,集市,大型超市,夜市;否认14天内与以下场所工作人员密切接触:水产、肉类批发市场,农贸市场,集市,大型超市;否认14天内周围(如家庭、办公室)有2例以上聚集性发病;否认14天内接触过有发热或呼吸道症状的人员;否认14天内自身有发热或呼吸道症状;否认14天内接触过纳入隔离观察的人员及其他可能与新冠肺炎关联的情形;陪同家属无以上情况。\n体格检查:\nT:39.5℃,P:80bpm,R:20次/分,BP:120/80mmHg;\n辅助检查:\n2020年6月10日,普放:血细胞比容36.50%(偏低)40~50;单核细胞绝对值0.75*10/L(偏高)参考值:0.1~0.6;\n门诊诊断:处置治疗:电子签名:【】\n其他记录:` + +// 模拟标题 +const titleText = [ + '主诉:', + '现病史:', + '既往史:', + '流行病史:', + '体格检查:', + '辅助检查:', + '门诊诊断:', + '处置治疗:', + '电子签名:', + '其他记录:' +] +const titleMap: Map = new Map() +for (let t = 0; t < titleText.length; t++) { + const value = titleText[t] + const i = text.indexOf(value) + if (~i) { + titleMap.set(i, value) + } +} + +// 模拟颜色字 +const colorText = ['传染性疾病'] +const colorIndex: number[] = colorText + .map(b => { + const i = text.indexOf(b) + return ~i + ? Array(b.length) + .fill(i) + .map((_, j) => i + j) + : [] + }) + .flat() + +// 模拟高亮字 +const highlightText = ['血细胞比容'] +const highlightIndex: number[] = highlightText + .map(b => { + const i = text.indexOf(b) + return ~i + ? Array(b.length) + .fill(i) + .map((_, j) => i + j) + : [] + }) + .flat() + +const elementList: IElement[] = [] +// 组合纯文本数据 +const textList = text.split('') +let index = 0 +while (index < textList.length) { + const value = textList[index] + const title = titleMap.get(index) + if (title) { + elementList.push({ + value: '', + type: ElementType.TITLE, + level: TitleLevel.FIRST, + valueList: [ + { + value: title, + size: 18 + } + ] + }) + index += title.length - 1 + } else if (colorIndex.includes(index)) { + elementList.push({ + value, + color: '#FF0000', + size: 16 + }) + } else if (highlightIndex.includes(index)) { + elementList.push({ + value, + highlight: '#F2F27F', + groupIds: ['1'] // 模拟批注 + }) + } else { + elementList.push({ + value, + size: 16 + }) + } + index++ +} + +// 模拟文本控件 +elementList.splice(12, 0, { + type: ElementType.CONTROL, + value: '', + control: { + conceptId: '1', + type: ControlType.TEXT, + value: null, + placeholder: '其他补充', + prefix: '{', + postfix: '}' + } +}) + +// 模拟下拉控件 +elementList.splice(94, 0, { + type: ElementType.CONTROL, + value: '', + control: { + conceptId: '2', + type: ControlType.SELECT, + value: null, + code: null, + placeholder: '有无', + prefix: '{', + postfix: '}', + valueSets: [ + { + value: '有', + code: '98175' + }, + { + value: '无', + code: '98176' + }, + { + value: '不详', + code: '98177' + } + ] + } +}) + +// 模拟超链接 +elementList.splice(116, 0, { + type: ElementType.HYPERLINK, + value: '', + valueList: [ + { + value: '新', + size: 16 + }, + { + value: '冠', + size: 16 + }, + { + value: '肺', + size: 16 + }, + { + value: '炎', + size: 16 + } + ], + url: 'https://hufe.club/canvas-editor' +}) + +// 模拟文本控件(前后文本) +elementList.splice(335, 0, { + type: ElementType.CONTROL, + value: '', + control: { + conceptId: '6', + type: ControlType.TEXT, + value: null, + placeholder: '内容', + preText: '其他:', + postText: '。' + } +}) + +// 模拟下标 +elementList.splice(346, 0, { + value: '∆', + color: '#FF0000', + type: ElementType.SUBSCRIPT +}) + +// 模拟上标 +elementList.splice(430, 0, { + value: '9', + type: ElementType.SUPERSCRIPT +}) + +// 模拟列表 +elementList.splice(451, 0, { + value: '', + type: ElementType.LIST, + listType: ListType.OL, + valueList: [ + { + value: '高血压\n糖尿病\n病毒性感冒\n过敏性鼻炎\n过敏性鼻息肉' + } + ] +}) + +elementList.splice(453, 0, { + value: '', + type: ElementType.LIST, + listType: ListType.OL, + valueList: [ + { + value: + '超声引导下甲状腺细针穿刺术;\n乙型肝炎表面抗体测定;\n膜式病变细胞采集术、后颈皮下肤层;' + } + ] +}) + +// 模拟图片 +elementList.splice(456, 0, { + value: `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALAAAAA+CAYAAACLDZH2AAAMPmlDQ1BJQ0MgUHJvZmlsZQAASImVVwdYU8kWnltSIbTQpYTeBJEaQEoILYD0ItgISYBQYgwEFTuyqODaxQI2dFVEsQNiR+wsir0viKgo62LBrrxJAV33le/N982d//5z5j9nzp259w4A6ie5YnEuqgFAnqhAEhcayBiTksogPQUEYAr0ABloc3n5YlZMTCSAZbD9e3l3EyCy9pqjTOuf/f+1aPIF+TwAkBiI0/n5vDyIDwKAV/HEkgIAiDLeYkqBWIZhBdoSGCDEC2Q4U4GrZDhdgffKbRLi2BC3AEBW5XIlmQCoXYE8o5CXCTXU+iB2FvGFIgDUGRD75eVN4kOcBrEttBFDLNNnpv+gk/k3zfQhTS43cwgr5iIv5CBhvjiXO+3/TMf/Lnm50kEf1rCqZknC4mRzhnm7nTMpQoZVIe4VpUdFQ6wF8QchX24PMUrNkoYlKuxRI14+G+YM6ELszOcGRUBsBHGIKDcqUsmnZwhDOBDDFYJOFRZwEiDWh3iBID84XmmzSTIpTukLrc+QsFlK/jxXIvcr8/VQmpPIUuq/zhJwlPqYWlFWQjLEVIgtC4VJURCrQeyUnxMfobQZVZTFjhq0kUjjZPFbQhwnEIUGKvSxwgxJSJzSviwvf3C+2KYsISdKifcXZCWEKfKDtfC48vjhXLArAhErcVBHkD8mcnAufEFQsGLu2DOBKDFeqfNBXBAYpxiLU8W5MUp73FyQGyrjzSF2yy+MV47FkwrgglTo4xnigpgERZx4UTY3PEYRD74URAI2CAIMIIU1HUwC2UDY1tvQC+8UPSGACyQgEwiAo5IZHJEs7xHBazwoAn9CJAD5Q+MC5b0CUAj5r0Os4uoIMuS9hfIROeAJxHkgAuTCe6l8lGjIWxJ4DBnhP7xzYeXBeHNhlfX/e36Q/c6wIBOpZKSDHhnqg5bEYGIQMYwYQrTDDXE/3AePhNcAWF1wJu41OI/v9oQnhHbCI8INQgfhzkRhseSnKEeDDqgfosxF+o+5wK2hpjseiPtCdaiM6+KGwBF3g35YuD/07A5ZtjJuWVYYP2n/bQY/PA2lHcWZglL0KAEU259HqtmruQ+pyHL9Y34UsaYP5Zs91POzf/YP2efDNuJnS2wBdgA7h53CLmBHsQbAwE5gjVgrdkyGh1bXY/nqGvQWJ48nB+oI/+Fv8MnKMpnvXOvc4/xF0VcgmCp7RwP2JPE0iTAzq4DBgl8EAYMj4jkNZ7g4u7gCIPu+KF5fb2Ll3w1Et/U7N+8PAHxPDAwMHPnOhZ8AYJ8n3P6Hv3O2TPjpUAHg/GGeVFKo4HDZhQDfEupwpxkAE2ABbOF8XIAH8AEBIBiEg2iQAFLABBh9FlznEjAFzABzQSkoB0vBKrAObARbwA6wG+wHDeAoOAXOgkvgCrgB7sHV0w1egD7wDnxGEISE0BA6YoCYIlaIA+KCMBE/JBiJROKQFCQNyUREiBSZgcxDypHlyDpkM1KD7EMOI6eQC0g7cgfpRHqQ18gnFENVUW3UGLVGR6BMlIVGoAnoeDQTnYwWoSXoYnQNWo3uQuvRU+gl9Abagb5A+zGAqWC6mBnmiDExNhaNpWIZmASbhZVhFVg1Voc1wed8DevAerGPOBGn4wzcEa7gMDwR5+GT8Vn4InwdvgOvx1vwa3gn3od/I9AIRgQHgjeBQxhDyCRMIZQSKgjbCIcIZ+Be6ia8IxKJukQboifciynEbOJ04iLieuIe4kliO7GL2E8ikQxIDiRfUjSJSyoglZLWknaRTpCukrpJH8gqZFOyCzmEnEoWkYvJFeSd5OPkq+Sn5M8UDYoVxZsSTeFTplGWULZSmiiXKd2Uz1RNqg3Vl5pAzabOpa6h1lHPUO9T36ioqJireKnEqghV5qisUdmrcl6lU+WjqpaqvSpbdZyqVHWx6nbVk6p3VN/QaDRrWgAtlVZAW0yroZ2mPaR9UKOrOalx1Phqs9Uq1erVrqq9VKeoW6mz1CeoF6lXqB9Qv6zeq0HRsNZga3A1ZmlUahzWuKXRr0nXHKkZrZmnuUhzp+YFzWdaJC1rrWAtvlaJ1hat01pddIxuQWfTefR59K30M/RubaK2jTZHO1u7XHu3dpt2n46WjptOks5UnUqdYzodupiutS5HN1d3ie5+3Zu6n/SM9Vh6Ar2FenV6V/Xe6w/TD9AX6Jfp79G/of/JgGEQbJBjsMygweCBIW5obxhrOMVwg+EZw95h2sN8hvGGlQ3bP+yuEWpkbxRnNN1oi1GrUb+xiXGosdh4rfFp414TXZMAk2yTlSbHTXpM6aZ+pkLTlaYnTJ8zdBgsRi5jDaOF0WdmZBZmJjXbbNZm9tncxjzRvNh8j/kDC6oF0yLDYqVFs0WfpanlaMsZlrWWd60oVkyrLKvVVues3lvbWCdbz7dusH5mo2/DsSmyqbW5b0uz9bedbFtte92OaMe0y7Fbb3fFHrV3t8+yr7S/7IA6eDgIHdY7tA8nDPcaLhpePfyWo6ojy7HQsdax00nXKdKp2KnB6eUIyxGpI5aNODfim7O7c67zVud7I7VGho8sHtk08rWLvQvPpdLluivNNcR1tmuj6ys3BzeB2wa32+5099Hu892b3b96eHpIPOo8ejwtPdM8qzxvMbWZMcxFzPNeBK9Ar9leR70+ent4F3jv9/7Lx9Enx2enz7NRNqMEo7aO6vI19+X6bvbt8GP4pflt8uvwN/Pn+lf7PwqwCOAHbAt4yrJjZbN2sV4GOgdKAg8Fvmd7s2eyTwZhQaFBZUFtwVrBicHrgh+GmIdkhtSG9IW6h04PPRlGCIsIWxZ2i2PM4XFqOH3hnuEzw1siVCPiI9ZFPIq0j5RENo1GR4ePXjH6fpRVlCiqIRpEc6JXRD+IsYmZHHMklhgbE1sZ+yRuZNyMuHPx9PiJ8Tvj3yUEJixJuJdomyhNbE5STxqXVJP0PjkoeXlyx5gRY2aOuZRimCJMaUwlpSalbkvtHxs8dtXY7nHu40rH3RxvM37q+AsTDCfkTjg2UX0id+KBNEJactrOtC/caG41tz+dk16V3sdj81bzXvAD+Cv5PQJfwXLB0wzfjOUZzzJ9M1dk9mT5Z1Vk9QrZwnXCV9lh2Ruz3+dE52zPGchNzt2TR85Lyzss0hLliFommUyaOqld7CAuFXdM9p68anKfJEKyLR/JH5/fWKANf+RbpbbSX6SdhX6FlYUfpiRNOTBVc6poaus0+2kLpz0tCin6bTo+nTe9eYbZjLkzOmeyZm6ehcxKn9U822J2yezuOaFzdsylzs2Z+3uxc/Hy4rfzkuc1lRiXzCnp+iX0l9pStVJJ6a35PvM3LsAXCBe0LXRduHbhtzJ+2cVy5/KK8i+LeIsu/jry1zW/DizOWNy2xGPJhqXEpaKlN5f5L9uxXHN50fKuFaNX1K9krCxb+XbVxFUXKtwqNq6mrpau7lgTuaZxreXapWu/rMtad6MysHJPlVHVwqr36/nrr24I2FC30Xhj+cZPm4Sbbm8O3VxfbV1dsYW4pXDLk61JW8/9xvytZpvhtvJtX7eLtnfsiNvRUuNZU7PTaOeSWrRWWtuza9yuK7uDdjfWOdZt3qO7p3wv2Cvd+3xf2r6b+yP2Nx9gHqg7aHWw6hD9UFk9Uj+tvq8hq6GjMaWx/XD44eYmn6ZDR5yObD9qdrTymM6xJcepx0uOD5woOtF/Unyy91Tmqa7mic33To85fb0ltqXtTMSZ82dDzp4+xzp34rzv+aMXvC8cvsi82HDJ41J9q3vrod/dfz/U5tFWf9nzcuMVrytN7aPaj1/1v3rqWtC1s9c51y/diLrRfjPx5u1b42513ObffnYn986ru4V3P9+bc59wv+yBxoOKh0YPq/+w+2NPh0fHsc6gztZH8Y/udfG6XjzOf/ylu+QJ7UnFU9OnNc9cnh3tCem58nzs8+4X4hefe0v/1Pyz6qXty4N/BfzV2jemr/uV5NXA60VvDN5sf+v2trk/pv/hu7x3n9+XfTD4sOMj8+O5T8mfnn6e8oX0Zc1Xu69N3yK+3R/IGxgQcyVc+a8ABiuakQHA6+0A0FIAoMPzGXWs4vwnL4jizCpH4D9hxRlRXjwAqIP/77G98O/mFgB7t8LjF9RXHwdADA2ABC+AuroO1cGzmvxcKStEeA7YFP01PS8d/JuiOHP+EPfPLZCpuoGf238Bw1F8YKne048AAAA4ZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAALCgAwAEAAAAAQAAAD4AAAAA/UM7vwAADO1JREFUeAHtXQXoFM8Xf3Z3N3Y3tlgYWKjYLYoioqiILQp2Y2KAiYig2N2J3YHd3d26//eZPzO/ufvenff9fndv9+67D+52dnd25s2bd7Mv5+IZDOSCS4EwpUD8MMXbRdulgKBA2DKw++JwORgUCEsGnjBhAsWPH5/69OnjzmIcp0DYMfD169dp1KhRYtrcVTiOcy8PP+wYeNy4cWrWunXrpsrehSNHjlCjRo1o//793rfc8wiiQLxwskLcvn2bChYsKMhfrFgxunr1qs+pwMpcpEgRunnzJtWpU4f27dvns557MfwpEFYrsL6aBlp9z507J5gX05MhQwb69u0b/fz5M/xnyx1BFAokjHLFwRcOHTqksIN48P79e7pz5w7dunVLfCAfX7lyhS5duqTqrV27li5evCgYesSIEQQF0IXIoUDYMPC7d+9oy5YtivJVqlShT58+qfNABYgSgM+fPweq5t4LQwo4loE/fPhAp0+fpqNHj9KuXbvoxIkTHuQNhnnz5ctH48ePp79//1KyZMmoXLlyHm24JxFAAShxToKZM2capUqVgns74IeVOKN3797GkiVLjLNnzxpv3rwRw2jatKl6btmyZU4amtGvXz8DeM+bN89ReIUzMuQ05HnVVAyoMzEmXp4PHTrUJ9os+6o6OXPmNFhx81nPjovfv39XuGEcEydOtAONiOvTcSLE+vXraefOnfT06VPKmjUrwVxWu3Ztevv2LeXNm5fnnvxaFGbMmCHu42v06NGUKFEidW534c+fPx4oQKHs2bMnZcyY0eO6exI9CjiOgUuXLk34eMOvX7/UJV/K2IMHD2jFihWiTubMmalz586qvhMLhQoVIm+mdiKeTsfJcQzsj2ApUqRQt3wpcLNnz1b3sfomTZpUnaPw+/dvevHihbAJp0+fnvAJJfC726O7bdu2UZYsWTyuuSfRp0BYeeLixYsnRgixQvfCsQKnXsWpUqWie/fu0fnz52nPnj3iCFvx3bt3PagD23GBAgU8rll5grcGcJPw48cPSpw4sTx1jzGkQNiswBgfGPfatWvi8/XrV0qePLkY9oIFC9TwsToHI1dCpg4lwJSng8u8OjViXg4rBq5Ro4ZgXgwXHjfEO0Dpmzp1akAKQN6sVKkSlS1bVsjXJUuWpEyZMgV8xuyb3gxsdvtxtb2wYuCKFSvSwoULxVxBSZMeNu/Jw6u6Xbt21KxZM6pZsyalTJnSu0rIz71l4JAjEKEdhhUDp02bVk2DN/Oy3Zc6dOhA7MigypUrU8KEzhqavgLrsrAakFuIEQWcNct+hoAwSgThLF++PEqNatWq0aBBgwTjOo1pdWR1kxnc2jGBZ8+eUZIkSUJuQYkJriF7xsmuGbYeGD169PDwYDFhxDmLEMaZM2cE+mwjNo4dO2awZu/Y4TDzqXGwnTpoPOHBW7dundGgQQP1PIeLBv18pFd0nCsZBGczmNGrVy81YZJp+dVrjBkzxnjy5InHvCC2AHUGDhzocd1JJ8BZjgPu8n8BK6nGkCFDDIxZPiePHOD0r8fjzH1HMTBHoBmc7xZlwrBiIWjny5cvPicGTC0nF6uxE+HRo0cKR7aK+ESRQ0YNBCCxDK/qynHh2LhxY/Gm8fmwQy5yTLaYK8SrTJs2zWCzpqWYOYKB2UsmJg6Mqk8YymBOdgIEJAJWXvnc/fv3A9a16ybeKhJHBCZJgIiwefNmo02bNuq+rCePrVu3FhF38hmnHLFYXLhwwVi8eLHBGTIGAqgkzvLYokULS9G1nYEPHjzoM3wSk8beM4/BYwXGSuYNrOApwnEyp/dtR5yzIqpwRLgo8ORtAXyKCJh8iA7sEjc4xsMR+AOJly9fGtu3bxeLCucaqvFIZvV1xDisBFsZeMCAAVGIgNWJc9+ijBm/9lq1aon6UIh0WLRokWpnzZo1+i3HlI8fP65w9DXR8hrGuHr1aoPz+GzDnU1+YqFgV7wxZ84co3v37oa/MFeJt68jJxAYGzZssHQctprR9HBH/qXSlClTRIihL3PYyJEjiVdrgr03Xbp0TK//QD+HZ44VJpEvlyZNGkJWBlzQhQsX/u8Bi0sIHGJRRuTmIatk9+7dhERTf4AxIbSyY8eOlD9/fn/VYnUdOPHbS9jHYcZDLAaCmxC2ig9o9vjxY7px4waxRSdafWHuKlSoIDydZcqUoeLFi1PRokWjBFRFq9FgK1v68/hH4zB7wfyF1xLHJvitDTMSj0d8oORIgIKA1cqf0iOfwRGvPDPNTxy6abAN2sAqA+UKVhNkg+ANovfrrwx5n+3XYvxsI5ZDMv0I+gJPf3hE9zpWYsjrkyZNMrZu3WpA58CKbRfYKkIEM2gQRyp3UBIgSkDT9WVmC2YyduzYEUy3AesgfSmYvvzVAUNZybQ68jFlXsiu+NFDQcaicerUKcstCjrewZZtFSF4gv8JCJtk5UHU69Spk3g1I4jHH/CEEcvEIlINEWdICmU5ToVTdu3aVbQRU28Y+oVbGAFC3u5sHSeILYjdwAfJpMBZusIhOmFvt1BA9erVA4oEePUDN2wYg/BSiFw4Yj+NcADHM/DevXsVHVu1akWbNm1S5yggaKd///6ENHsAQiwhgwGQglS+fHnq0qWLcDVDtsOPYdWqVULmFJVi8IVwzcuXL4s0f8i6CO3MlSuXmHz0mT17dkqQIIFHy7yiqHN+i6iy1QW44KtWrUqc+CrkXjAq9AEwKVK2wh6CXartqqe7UCFOcCC6wVFmBgzlkL8k4JXHkyHkPXlNP/LmJuq1D1nVDgB++EBudsEcCjh+Bb7H2RUAaOrIyMDKsXHjRnFN/4JIgGB2fHwB217FConMDGRr2AnsvLCz+4jqOzSCWCxIBvMPAAwcCGTM7/Pnz4ltwYLJDx8+TB8/flSPSRMVzEWyXXUzBAWYmwAuA5tHbMevwNImrIcj6sPHPmjIRpY5b5Bx27dvr1ehvn370qxZs0iXPWW7HhUtPpFvCXejQfMI7XgGRuYutH04BFhqEmIEVrCVK1cKa0MgB4EkE0erCWXu4cOH4hIsBHaAzINzGdg86juegbNly6ZGi4BuiAWDBw8WXiN1QyvAvDV//nxhjUC2MrxN2GcN+XRylZaihPZYSIoyCRXbvbpgDgUcz8AwSUnIkSOHLKoj7L6855gwp+EizER169ZV91GAmYsN8uqa3OFHXQhRQYot/hTNEKERUd04Xonz57RA7hv2/YWjom3btmpSvFPqISMjG5lD/lSdPHnyqHIoC/pmKxCHXIg9BRzPwGA+HWCNgDODY2gJpjGALlNKDxIYBKIEtqny9pjVr19fbzJkZd37pyuUIUMgAjtyvAhRokQJD7JjF3Z9myncfP36taoDV+irV6+Ic+k8NsSWFcD03m3Ke1YfdfcxGFgqdVb3G8ntO34FhuIDf70E3a4rr8GdKwGODzCovpu7tL+iDsIWfQHMdIhxsBL0VVd/a1jZZ6S37XgGxgQ0b95czQNigr0BO1NKwBarMvgHjAtRQyptOEcwjzcgNhamNQTbIIXfKtB/fOjThdhTwDYGhscMDofp06f/c+Vr2bKlGimCw3UAI/hiaogKsBFjZZV/+oL/mNNXY9kOzFqQk2EdGDZsmLxs+lFnYF2cML2juNSgOSEV0W/lwIEDUMPFJ5gt92VcKzOgiKVFfhz+jkDGCsu2cBw+fLjIYGZ3sQowRyYwm9P8IqoHoiNoyAoA7sAPRxfMoYBtAe1gJp358PcAgYBlWsXwLOcaY8eOVeeSeVlWFlmysh2OJVYMgyi2QKBvoMIrcaCqMb4n8USguAvmUMA2EQImJQ6J5Dn9PyDW19fO6/J+kyZNCPItHBJwbsjgHdzHJn68otPJkyc9dneHDRkmN7ihEcUWCKSXDHWsUOZ07xtilF0wiQLm/A5i1gpWOuRY8VDEB6n0rKkH3RjL0VF26Qn6Ya+KDRs2VHhYke6D9Hg5TuwD4YI5FLBNhJDo86qpJhYTjI0wQp1SDnFGyqes/EnUTD1iAxCMD/2wMmdq23G5MdtECJ5MAcgZQ6ijBN5HQJjN8DeyoYKlS5eqQHj+AVnSberUqUW7kydP9mkJsaTTuNCoU369c+fO9ViJoeDxHg+Wo4dN93RlEn+aaBVERzyyCodIa9d2EUInKP8xt3qV8+IhGBqWBbYVi1w4va4ZZXY5GzCvyb448N2MZt02QkgBRzEwxg2m0k1akrnMtp2yiCKSK2X7UCatMp+FcD7jXFeO/ZstZE8gt4031CCUsb8BOy6Y32IP8N7Vq1ePeIM90RjcyLzhCeXOnTv2jbsthJQCjmVgK6mAvcBkcDz+ngB/jCiVLCv7dds2nwJxkoFBRhmWiQ1JXAhfCsRZBg7fKXMx1ylgux1YR8YtuxSILgVcBo4uxdz6jqKAy8COmg4XmehS4H9f7COrHME8MAAAAABJRU5ErkJggg==`, + width: 89, + height: 32, + id: 'signature', + type: ElementType.IMAGE +}) + +// 模拟表格 +elementList.push({ + type: ElementType.TABLE, + value: '', + colgroup: [ + { + width: 180 + }, + { + width: 80 + }, + { + width: 130 + }, + { + width: 130 + } + ], + trList: [ + { + height: 40, + tdList: [ + { + colspan: 1, + rowspan: 2, + value: [ + { value: `1`, size: 16 }, + { value: '.', size: 16 } + ] + }, + { + colspan: 1, + rowspan: 1, + value: [ + { value: `2`, size: 16 }, + { value: '.', size: 16 } + ] + }, + { + colspan: 2, + rowspan: 1, + value: [ + { value: `3`, size: 16 }, + { value: '.', size: 16 } + ] + } + ] + }, + { + height: 40, + tdList: [ + { + colspan: 1, + rowspan: 1, + value: [ + { value: `4`, size: 16 }, + { value: '.', size: 16 } + ] + }, + { + colspan: 1, + rowspan: 1, + value: [ + { value: `5`, size: 16 }, + { value: '.', size: 16 } + ] + }, + { + colspan: 1, + rowspan: 1, + value: [ + { value: `6`, size: 16 }, + { value: '.', size: 16 } + ] + } + ] + }, + { + height: 40, + tdList: [ + { + colspan: 1, + rowspan: 1, + value: [ + { value: `7`, size: 16 }, + { value: '.', size: 16 } + ] + }, + { + colspan: 1, + rowspan: 1, + value: [ + { value: `8`, size: 16 }, + { value: '.', size: 16 } + ] + }, + { + colspan: 1, + rowspan: 1, + value: [ + { value: `9`, size: 16 }, + { value: '.', size: 16 } + ] + }, + { + colspan: 1, + rowspan: 1, + value: [ + { value: `1`, size: 16 }, + { value: `0`, size: 16 }, + { value: '.', size: 16 } + ] + } + ] + } + ] +}) + +// 模拟checkbox +elementList.push( + ...([ + { + value: '是否同意以上内容:' + }, + { + type: ElementType.CONTROL, + control: { + conceptId: '3', + type: ControlType.CHECKBOX, + code: '98175', + value: '', + valueSets: [ + { + value: '同意', + code: '98175' + }, + { + value: '否定', + code: '98176' + } + ] + }, + value: '' + }, + { + value: '\n' + } + ]) +) + +// LaTex公式 +elementList.push( + ...([ + { + value: '医学公式:' + }, + { + value: `{E_k} = hv - {W_0}`, + type: ElementType.LATEX + }, + { + value: '\n' + } + ]) +) + +// 日期选择 +elementList.push( + ...([ + { + value: '签署日期:' + }, + { + type: ElementType.CONTROL, + value: '', + control: { + conceptId: '5', + type: ControlType.DATE, + value: [ + { + value: `2022-08-10 17:30:01` + } + ], + placeholder: '签署日期' + } + }, + { + value: '\n' + } + ]) +) + +// 模拟固定长度下划线 +elementList.push( + ...[ + { + value: '患者签名:' + }, + { + type: ElementType.CONTROL, + value: '', + control: { + conceptId: '4', + type: ControlType.TEXT, + value: null, + placeholder: '', + prefix: '\u200c', + postfix: '\u200c', + minWidth: 160, + underline: true + } + } + ] +) + +// 模拟结尾文本 +elementList.push( + ...[ + { + value: '\n' + }, + { + value: '', + type: ElementType.TAB + }, + { + value: 'E', + size: 16 + }, + { + value: 'O', + size: 16 + }, + { + value: 'F', + size: 16 + } + ] +) + +export const data: IElement[] = elementList + +interface IComment { + id: string + content: string + userName: string + rangeText: string + createdDate: string +} +export const commentList: IComment[] = [ + { + id: '1', + content: + '红细胞比容(HCT)是指每单位容积中红细胞所占全血容积的比值,用于反映红细胞和血浆的比例。', + userName: 'Hufe', + rangeText: '血细胞比容', + createdDate: '2023-08-20 23:10:55' + } +] + +export const options: IEditorOption = { + margins: [100, 120, 100, 120], + watermark: { + data: 'CANVAS-EDITOR', + size: 120 + }, + pageNumber: { + format: '第{pageNo}页/共{pageCount}页' + }, + placeholder: { + data: '请输入正文' + }, + zone: { + tipDisabled: false + }, + maskMargin: [60, 0, 30, 0] // 菜单栏高度60,底部工具栏30为遮盖层 +} diff --git a/src/plugins/copy/index.ts b/src/plugins/copy/index.ts new file mode 100644 index 0000000..fee3fe4 --- /dev/null +++ b/src/plugins/copy/index.ts @@ -0,0 +1,30 @@ +// 复制内容时带入版权信息,代码仅为参考 +import Editor from '../../editor' + +export interface ICopyWithCopyrightOption { + copyrightText: string +} + +export function copyWithCopyrightPlugin( + editor: Editor, + options?: ICopyWithCopyrightOption +) { + const copy = editor.command.executeCopy + + editor.command.executeCopy = () => { + const { copyrightText } = options || {} + if (copyrightText) { + const rangeText = editor.command.getRangeText() + if (!rangeText) return + const text = `${rangeText}${copyrightText}` + const plainText = new Blob([text], { type: 'text/plain' }) + // @ts-ignore + const item = new ClipboardItem({ + [plainText.type]: plainText + }) + window.navigator.clipboard.write([item]) + } else { + copy() + } + } +} diff --git a/src/plugins/markdown/index.ts b/src/plugins/markdown/index.ts new file mode 100644 index 0000000..7c293cf --- /dev/null +++ b/src/plugins/markdown/index.ts @@ -0,0 +1,118 @@ +// 简化版markdown转IElement插件示例,代码仅为参考 +import Editor, { + Command, + ElementType, + IElement, + ListType, + TitleLevel +} from '../../editor' + +export type CommandWithMarkdown = Command & { + executeInsertMarkdown(markdown: string): void +} + +export const titleNodeNameMapping: Record = { + '1': TitleLevel.FIRST, + '2': TitleLevel.SECOND, + '3': TitleLevel.THIRD, + '4': TitleLevel.FOURTH, + '5': TitleLevel.FIFTH, + '6': TitleLevel.SIXTH +} + +function convertMarkdownToElement(markdown: string): IElement[] { + const elementList: IElement[] = [] + const lines = markdown.trim().split('\n') + for (let l = 0; l < lines.length; l++) { + const line = lines[l] + if (line.startsWith('#')) { + const level = line.indexOf(' ') + elementList.push({ + type: ElementType.TITLE, + level: titleNodeNameMapping[level], + value: '', + valueList: [ + { + value: line.slice(level + 1) + } + ] + }) + } else if (line.startsWith('- ')) { + elementList.push({ + type: ElementType.LIST, + listType: ListType.UL, + value: '', + valueList: [ + { + value: line.slice(2) + } + ] + }) + } else if (/^\d+\.\s/.test(line)) { + elementList.push({ + type: ElementType.LIST, + listType: ListType.OL, + value: '', + valueList: [ + { + value: line.replace(/^\d+\.\s/, '') + } + ] + }) + } else if (/^\[.*?\]\(.*?\)$/.test(line)) { + const match = line.match(/^\[(.*?)\]\((.*?)\)$/) + elementList.push({ + type: ElementType.HYPERLINK, + value: '', + valueList: [ + { + value: match![1] + } + ], + url: match![2] + }) + } else if (/^\*\*(.*?)\*\*$/.test(line)) { + const match = line.match(/^\*\*(.*?)\*\*$/) + elementList.push({ + type: ElementType.TEXT, + value: match![1], + bold: true + }) + } else if (/^\*(.*?)\*$/.test(line)) { + const match = line.match(/^\*(.*?)\*$/) + elementList.push({ + type: ElementType.TEXT, + value: match![1], + italic: true + }) + } else if (/^__(.*?)__$/.test(line)) { + const match = line.match(/^__(.*?)__$/) + elementList.push({ + type: ElementType.TEXT, + value: match![1], + underline: true + }) + } else if (/^~~(.*?)~~$/.test(line)) { + const match = line.match(/^~~(.*?)~~$/) + elementList.push({ + type: ElementType.TEXT, + value: match![1], + strikeout: true + }) + } else { + elementList.push({ + type: ElementType.TEXT, + value: line + }) + } + } + return elementList +} + +export function markdownPlugin(editor: Editor) { + const command = editor.command + command.executeInsertMarkdown = (markdown: string) => { + const elementList = convertMarkdownToElement(markdown) + command.executeInsertElementList(elementList) + } +} diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..efcbc46 --- /dev/null +++ b/src/style.css @@ -0,0 +1,1063 @@ +::-webkit-scrollbar { + height: 16px; + width: 16px; + overflow: visible +} + +::-webkit-scrollbar-button { + width: 0; + height: 0 +} + +::-webkit-scrollbar-corner { + background: transparent +} + +::-webkit-scrollbar-thumb { + background-color: #ddd; + background-clip: padding-box; + border: 4px solid #f2f4f7; + border-radius: 8px; + min-height: 24px +} + +::-webkit-scrollbar-thumb:hover { + background-color: #c9c9c9 +} + +::-webkit-scrollbar-track { + background: #f2f4f7; + background-clip: padding-box +} + +* { + margin: 0; + padding: 0; +} + +body { + background-color: #F2F4F7; +} + +ul { + list-style: none; +} + +.menu { + width: 100%; + height: 60px; + top: 0; + z-index: 9; + position: fixed; + display: flex; + align-items: center; + justify-content: center; + background: #F2F4F7; + box-shadow: 0 2px 4px 0 transparent; +} + +.menu-divider { + width: 1px; + height: 16px; + margin: 0 8px; + display: inline-block; + background-color: #cfd2d8; +} + +.menu-item { + height: 24px; + display: flex; + align-items: center; + position: relative; +} + +.menu-item>div { + width: 24px; + height: 24px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + margin: 0 2px; +} + +.menu-item>div:hover { + background: rgba(25, 55, 88, .04); +} + +.menu-item>div.active { + background: rgba(25, 55, 88, .08); +} + +.menu-item i { + width: 16px; + height: 16px; + display: inline-block; + background-repeat: no-repeat; + background-size: 100% 100%; +} + +.menu-item>div>span { + width: 16px; + height: 3px; + display: inline-block; + border: 1px solid #e2e6ed; +} + +.menu-item .select { + border: none; + font-size: 12px; + line-height: 24px; + user-select: none; +} + +.menu-item .select::after { + position: absolute; + content: ""; + top: 11px; + width: 0; + height: 0; + right: 2px; + border-color: #767c85 transparent transparent; + border-style: solid solid none; + border-width: 3px 3px 0; +} + +.menu-item .options { + width: 70px; + position: absolute; + left: 0; + top: 25px; + padding: 10px; + background: #fff; + font-size: 14px; + box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%); + border: 1px solid #e2e6ed; + border-radius: 2px; + display: none; +} + +.menu-item .options.visible { + display: block; +} + +.menu-item .options li { + padding: 5px; + margin: 5px 0; + user-select: none; + transition: all .3s; +} + +.menu-item .options li:hover { + background-color: #ebecef; +} + +.menu-item .options li.active { + background-color: #e2e6ed; +} + +.menu-item .menu-item__font { + width: 65px; + position: relative; +} + +.menu-item .menu-item__size { + width: 50px; + text-align: center; + position: relative; +} + +.menu-item__font .select, +.menu-item__size .select { + width: 100%; + height: 100%; +} + +.menu-item__undo.no-allow, +.menu-item__redo.no-allow, +.menu-item>div.disable { + color: #c0c4cc; + cursor: not-allowed; + opacity: 0.4; + pointer-events: none; +} + +.menu-item__undo i { + background-image: url('./assets/images/undo.svg'); +} + +.menu-item__redo i { + background-image: url('./assets/images/redo.svg'); +} + +.menu-item__painter i { + background-image: url('./assets/images/painter.svg'); +} + +.menu-item__format i { + background-image: url('./assets/images/format.svg'); +} + +.menu-item__size-add i { + background-image: url('./assets/images/size-add.svg'); +} + +.menu-item__size-minus i { + background-image: url('./assets/images/size-minus.svg'); +} + +.menu-item__bold i { + background-image: url('./assets/images/bold.svg'); +} + +.menu-item__italic i { + background-image: url('./assets/images/italic.svg'); +} + +.menu-item .menu-item__underline { + width: 30px; + position: relative; +} + +.menu-item__underline>i { + flex-shrink: 0; + background-image: url('./assets/images/underline.svg'); +} + +.menu-item__underline .select { + width: 100%; + height: 100%; +} + +.menu-item .menu-item__underline .options { + width: 128px; +} + +.menu-item .menu-item__underline li { + padding: 1px 5px; +} + +.menu-item__underline li i { + pointer-events: none; +} + +.menu-item__underline li[data-decoration-style="solid"] { + background-image: url('./assets/images/line-single.svg'); +} + +.menu-item__underline li[data-decoration-style="double"] { + background-image: url('./assets/images/line-double.svg') +} + +.menu-item__underline li[data-decoration-style="dashed"] { + background-image: url('./assets/images/line-dash-small-gap.svg'); +} + +.menu-item__underline li[data-decoration-style="dotted"] { + background-image: url('./assets/images/line-dot.svg'); +} + +.menu-item__underline li[data-decoration-style="wavy"] { + background-image: url('./assets/images/line-wavy.svg'); +} + +.menu-item__strikeout i { + background-image: url('./assets/images/strikeout.svg'); +} + +.menu-item__superscript i { + background-image: url('./assets/images/superscript.svg'); +} + +.menu-item__subscript i { + background-image: url('./assets/images/subscript.svg'); +} + +.menu-item__color, +.menu-item__highlight { + display: flex; + flex-direction: column; +} + +.menu-item__color #color, +.menu-item__highlight #highlight { + width: 1px; + height: 1px; + visibility: hidden; + outline: none; + appearance: none; +} + +.menu-item__color i { + background-image: url('./assets/images/color.svg'); +} + +.menu-item__color span { + background-color: #000000; +} + +.menu-item__highlight i { + background-image: url('./assets/images/highlight.svg'); +} + +.menu-item__highlight span { + background-color: #ffff00; +} + +.menu-item .menu-item__title { + width: 60px; + position: relative; +} + +.menu-item__title .select { + width: calc(100% - 20px); + height: 100%; +} + +.menu-item__title i { + transform: translateX(-5px); + background-image: url('./assets/images/title.svg'); +} + +.menu-item__title .options { + width: 80px; +} + +.menu-item__left i { + background-image: url('./assets/images/left.svg'); +} + +.menu-item__center i { + background-image: url('./assets/images/center.svg'); +} + +.menu-item__right i { + background-image: url('./assets/images/right.svg'); +} + +.menu-item__alignment i { + background-image: url('./assets/images/alignment.svg'); +} + +.menu-item__justify i { + background-image: url('./assets/images/justify.svg'); +} + +.menu-item__row-margin { + position: relative; +} + +.menu-item__row-margin i { + background-image: url('./assets/images/row-margin.svg'); +} + +.menu-item__list { + position: relative; +} + +.menu-item__list i { + background-image: url('./assets/images/list.svg'); +} + +.menu-item__list .options { + width: 110px; +} + +.menu-item__list .options>ul>li * { + pointer-events: none; +} + +.menu-item__list .options>ul>li li { + margin-left: 18px; +} + +.menu-item__list .options>ul>li[data-list-style='checkbox'] li::marker { + font-size: 11px; +} + +.menu-item__image i { + background-image: url('./assets/images/image.svg'); +} + +.menu-item__image input { + display: none; +} + +.menu-item__table { + position: relative; +} + +.menu-item__table i { + background-image: url('./assets/images/table.svg'); +} + +.menu-item .menu-item__table__collapse { + width: 270px; + height: 310px; + background: #fff; + box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%); + border: 1px solid #e2e6ed; + box-sizing: border-box; + border-radius: 2px; + position: absolute; + display: none; + z-index: 99; + top: 25px; + left: 0; + padding: 14px 27px; + cursor: auto; +} + +.menu-item .menu-item__table__collapse .table-close { + position: absolute; + right: 10px; + top: 5px; + cursor: pointer; +} + +.menu-item .menu-item__table__collapse .table-close:hover { + color: #7d7e80; +} + +.menu-item .menu-item__table__collapse:hover { + background: #fff; +} + +.menu-item .menu-item__table__collapse .table-title { + display: flex; + justify-content: flex-start; + padding-bottom: 5px; + border-bottom: 1px solid #e2e6ed; +} + +.table-title span { + font-size: 12px; + color: #3d4757; + display: inline; + margin: 0; +} + +.table-panel { + cursor: pointer; +} + +.table-panel .table-row { + display: flex; + flex-wrap: nowrap; + margin-top: 10px; + pointer-events: none; +} + +.table-panel .table-cel { + width: 16px; + height: 16px; + box-sizing: border-box; + border: 1px solid #e2e6ed; + background: #fff; + position: relative; + margin-right: 6px; + pointer-events: none; +} + +.table-panel .table-cel.active { + border: 1px solid rgba(73, 145, 242, .2); + background: rgba(73, 145, 242, .15); +} + +.table-panel .table-row .table-cel:last-child { + margin-right: 0; +} + +.menu-item__hyperlink i { + background-image: url('./assets/images/hyperlink.svg'); +} + +.menu-item__separator { + position: relative; +} + +.menu-item__separator>i { + background-image: url('./assets/images/separator.svg'); +} + +.menu-item .menu-item__separator .options { + width: 128px; +} + +.menu-item .menu-item__separator li { + padding: 1px 5px; +} + +.menu-item__separator li i { + pointer-events: none; +} + +.menu-item__separator li[data-separator="0,0"] { + background-image: url('./assets/images/line-single.svg'); +} + +.menu-item__separator li[data-separator="1,1"] { + background-image: url('./assets/images/line-dot.svg'); +} + +.menu-item__separator li[data-separator="3,1"] { + background-image: url('./assets/images/line-dash-small-gap.svg'); +} + +.menu-item__separator li[data-separator="4,4"] { + background-image: url('./assets/images/line-dash-large-gap.svg'); +} + +.menu-item__separator li[data-separator="7,3,3,3"] { + background-image: url('./assets/images/line-dash-dot.svg'); +} + +.menu-item__separator li[data-separator="6,2,2,2,2,2"] { + background-image: url('./assets/images/line-dash-dot-dot.svg'); +} + +.menu-item__watermark>i { + background-image: url('./assets/images/watermark.svg'); +} + +.menu-item__watermark { + position: relative; +} + +.menu-item__codeblock i { + background-image: url('./assets/images/codeblock.svg'); +} + +.menu-item__page-break i { + background-image: url('./assets/images/page-break.svg'); +} + +.menu-item__control { + position: relative; +} + +.menu-item__control i { + background-image: url('./assets/images/control.svg'); +} + +.menu-item__checkbox i { + background-image: url('./assets/images/checkbox.svg'); +} + +.menu-item__radio i { + background-image: url('./assets/images/radio.svg'); +} + +.menu-item__latex i { + background-image: url('./assets/images/latex.svg'); +} + +.menu-item__date { + position: relative; +} + +.menu-item__date i { + background-image: url('./assets/images/date.svg'); +} + +.menu-item__date .options { + width: 160px; +} + +.menu-item__block i { + background-image: url('./assets/images/block.svg'); +} + +.menu-item .menu-item__control .options { + width: 55px; +} + +.menu-item__search { + position: relative; +} + +.menu-item__search i { + background-image: url('./assets/images/search.svg'); +} + +.menu-item .menu-item__search__collapse { + width: 260px; + height: 72px; + box-sizing: border-box; + position: absolute; + display: none; + z-index: 99; + top: 25px; + left: 0; + background: #ffffff; + box-shadow: 0px 5px 5px #e3dfdf; +} + +.menu-item .menu-item__search__collapse:hover { + background: #ffffff; +} + +.menu-item .menu-item__search__collapse>div { + width: 250px; + height: 36px; + padding: 0 5px; + line-height: 36px; + display: flex; + align-items: center; + justify-content: space-between; + border-radius: 4px; +} + +.menu-item .menu-item__search__collapse>div input { + width: 205px; + height: 27px; + appearance: none; + background-color: #fff; + background-image: none; + border-radius: 4px; + border: 1px solid #ebebeb; + box-sizing: border-box; + color: #606266; + display: inline-block; + line-height: 27px; + outline: none; + padding: 0 5px; +} + +.menu-item .menu-item__search__collapse>div span { + height: 100%; + color: #dcdfe6; + font-size: 25px; + display: inline-block; + border: 0; + padding: 0 10px; +} + +.menu-item .menu-item__search__collapse__replace button { + display: inline-block; + border: 1px solid #e2e6ed; + border-radius: 2px; + background: #fff; + line-height: 22px; + padding: 0 6px; + white-space: nowrap; + margin-left: 4px; + cursor: pointer; + font-size: 12px; +} + +.menu-item .menu-item__search__collapse__replace button:hover { + background: rgba(25, 55, 88, .04); +} + +.menu-item .menu-item__search__collapse__search { + position: relative; +} + +.menu-item .menu-item__search__collapse__search label { + right: 110px; + font-size: 12px; + color: #3d4757; + position: absolute; +} + +.menu-item .menu-item__search__collapse__search>input { + padding: 5px 90px 5px 5px !important; +} + +.menu-item .menu-item__search__collapse__search>div { + width: 28px; + height: 27px; + display: flex; + justify-content: center; + align-items: center; + position: absolute; + border-left: 1px solid #e2e6ed; + transition: all .5s; +} + +.menu-item .menu-item__search__collapse__search>div:hover { + background-color: rgba(25, 55, 88, .04); +} + +.menu-item .menu-item__search__collapse__search i { + width: 6px; + height: 8px; + transform: translateY(1px); +} + +.menu-item .menu-item__search__collapse__search .arrow-left { + right: 76px; +} + +.menu-item .menu-item__search__collapse__search .arrow-left i { + background: url(./assets/images/arrow-left.svg) no-repeat; +} + +.menu-item .menu-item__search__collapse__search .arrow-right { + right: 48px; +} + +.menu-item .menu-item__search__collapse__search .arrow-right i { + background: url(./assets/images/arrow-right.svg) no-repeat; +} + +.menu-item__print i { + background-image: url('./assets/images/print.svg'); +} + +.catalog { + width: 250px; + position: fixed; + left: 0; + bottom: 0; + top: 100px; + padding: 0 20px 40px 20px; +} + +.catalog .catalog__header { + height: 48px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid #e2e6ed; +} + +.catalog .catalog__header span { + color: #3d4757; + font-size: 14px; + font-weight: bold; +} + +.catalog .catalog__header i { + width: 16px; + height: 16px; + cursor: pointer; + display: inline-block; + background: url(./assets/images/close.svg) no-repeat; + transition: all .2s; +} + +.catalog .catalog__header>div:hover { + background: rgba(235, 238, 241); +} + +.catalog__main { + height: calc(100% - 60px); + padding: 10px 0; + overflow-y: auto; + overflow-x: hidden; +} + +.catalog__main .catalog-item { + width: 100%; + padding-left: 10px; + box-sizing: border-box; +} + +.catalog__main>.catalog-item { + padding-left: 0; +} + +.catalog__main .catalog-item .catalog-item__content { + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.catalog__main .catalog-item .catalog-item__content:hover>span { + color: #4991f2; +} + +.catalog__main .catalog-item .catalog-item__content span { + color: #3d4757; + line-height: 30px; + font-size: 12px; + white-space: nowrap; + cursor: pointer; + user-select: none; +} + +.editor>div { + margin: 80px auto; +} + +.ce-page-container canvas { + box-shadow: rgb(158 161 165 / 40%) 0px 2px 12px 0px; +} + +.comment { + width: 250px; + height: 650px; + position: fixed; + transform: translateX(420px); + top: 200px; + left: 50%; + overflow-y: auto; +} + +.comment-item { + background: #ffffff; + border: 1px solid #e2e6ed; + position: relative; + border-radius: 8px; + padding: 15px; + font-size: 14px; + margin-bottom: 20px; + cursor: pointer; + transition: all .5s; +} + +.comment-item:hover { + border-color: #c0c6cf; + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1); +} + +.comment-item.active { + border-color: #E99D00; + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1); +} + +.comment-item__title { + height: 22px; + position: relative; + display: flex; + align-items: center; + color: #c1c6ce; +} + +.comment-item__title span:first-child { + background-color: #dbdbdb; + width: 4px; + height: 16px; + margin-right: 5px; + display: inline-block; + border-radius: 999px; +} + +.comment-item__title span:nth-child(2) { + width: 200px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.comment-item__title i { + width: 16px; + height: 16px; + cursor: pointer; + position: absolute; + right: -8px; + top: -8px; + background: url(./assets/images/close.svg) no-repeat; +} + +.comment-item__title i:hover { + opacity: 0.6; +} + +.comment-item__info { + height: 28px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.comment-item__info>span:first-child { + font-weight: 600; +} + +.comment-item__info>span:last-child { + color: #c1c6ce; +} + +.comment-item__content { + line-height: 22px; +} + +.footer { + width: 100%; + height: 30px; + display: flex; + align-items: center; + justify-content: space-between; + background: #f2f4f7; + z-index: 9; + position: fixed; + bottom: 0; + left: 0; + font-size: 12px; + padding: 0 4px 0 20px; + box-sizing: border-box; +} + +.footer>div:first-child { + display: flex; + align-items: center; +} + +.footer .catalog-mode { + padding: 1px; + position: relative; +} + +.footer .catalog-mode i { + width: 16px; + height: 16px; + margin-right: 5px; + cursor: pointer; + display: inline-block; + background-image: url('./assets/images/catalog.svg'); +} + +.footer .page-mode { + padding: 1px; + position: relative; +} + +.footer .page-mode i { + width: 16px; + height: 16px; + margin-right: 5px; + cursor: pointer; + display: inline-block; + background-image: url('./assets/images/page-mode.svg'); +} + +.footer .options { + width: 70px; + position: absolute; + left: 0; + bottom: 25px; + padding: 10px; + background: #fff; + font-size: 14px; + box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%); + border: 1px solid #e2e6ed; + border-radius: 2px; + display: none; +} + +.footer .options.visible { + display: block; +} + +.footer .options li { + padding: 5px; + margin: 5px 0; + user-select: none; + transition: all .3s; + text-align: center; + cursor: pointer; +} + +.footer .options li:hover { + background-color: #ebecef; +} + +.footer .options li.active { + background-color: #e2e6ed; +} + +.footer>div:first-child>span { + display: inline-block; + margin-right: 5px; + letter-spacing: 1px; +} + +.footer>div:last-child { + display: flex; + align-items: center; + justify-content: space-between; +} + +.footer>div:last-child>div { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; +} + +.footer>div:last-child>div:hover { + background: rgba(25, 55, 88, .04); +} + +.footer>div:last-child i { + width: 16px; + height: 16px; + display: inline-block; + cursor: pointer; +} + +.footer .editor-option i { + background-image: url('./assets/images/option.svg'); +} + +.footer .page-scale-minus i { + background-image: url('./assets/images/page-scale-minus.svg'); +} + +.footer .page-scale-add i { + background-image: url('./assets/images/page-scale-add.svg'); +} + +.footer .page-scale-percentage { + cursor: pointer; + user-select: none; +} + +.footer .fullscreen i { + background-image: url('./assets/images/request-fullscreen.svg'); +} + +.footer .fullscreen.exist i { + background-image: url('./assets/images/exit-fullscreen.svg'); +} + +.footer .paper-margin i { + background-image: url('./assets/images/paper-margin.svg'); +} + +.footer .editor-mode { + cursor: pointer; + user-select: none; + position: absolute; + left: 50%; + transform: translateX(-50%); +} + +.footer .paper-size { + position: relative; +} + +.footer .paper-size i { + background-image: url('./assets/images/paper-size.svg'); +} + +.footer .paper-size .options { + right: 0; + left: unset; +} + +.footer .paper-direction { + position: relative; +} + +.footer .paper-direction i { + background-image: url('./assets/images/paper-direction.svg'); +} + +.footer .paper-direction .options { + right: 0; + left: unset; +} + +.ce-contextmenu-signature { + background-image: url('./assets/images/signature.svg'); +} + +.ce-contextmenu-word-tool { + background-image: url('./assets/images/word-tool.svg'); +} \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..5bf47f5 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,45 @@ +export function debounce( + func: (...arg: T) => unknown, + delay: number +) { + let timer: number + return function (this: unknown, ...args: T) { + if (timer) { + window.clearTimeout(timer) + } + timer = window.setTimeout(() => { + func.apply(this, args) + }, delay) + } +} + +export function scrollIntoView(container: HTMLElement, selected: HTMLElement) { + if (!selected) { + container.scrollTop = 0 + return + } + const offsetParents: HTMLElement[] = [] + let pointer = selected.offsetParent + while (pointer && container !== pointer && container.contains(pointer)) { + offsetParents.push(pointer) + pointer = pointer.offsetParent + } + const top = + selected.offsetTop + + offsetParents.reduce((prev, curr) => prev + curr.offsetTop, 0) + const bottom = top + selected.offsetHeight + const viewRectTop = container.scrollTop + const viewRectBottom = viewRectTop + container.clientHeight + if (top < viewRectTop) { + container.scrollTop = top + } else if (bottom > viewRectBottom) { + container.scrollTop = bottom - container.clientHeight + } +} + +export function nextTick(fn: Function) { + const callback = window.requestIdleCallback || window.setTimeout + callback(() => { + fn() + }) +} diff --git a/src/utils/prism.ts b/src/utils/prism.ts new file mode 100644 index 0000000..993f32d --- /dev/null +++ b/src/utils/prism.ts @@ -0,0 +1,89 @@ +interface IPrismKindStyle { + color?: string + italic?: boolean + opacity?: number + bold?: boolean +} + +export function getPrismKindStyle(payload: string): IPrismKindStyle | null { + switch (payload) { + case 'comment': + case 'prolog': + case 'doctype': + case 'cdata': + return { color: '#008000', italic: true } + case 'namespace': + return { opacity: 0.7 } + case 'string': + return { color: '#A31515' } + case 'punctuation': + case 'operator': + return { color: '#393A34' } + case 'url': + case 'symbol': + case 'number': + case 'boolean': + case 'variable': + case 'constant': + case 'inserted': + return { color: '#36acaa' } + case 'atrule': + case 'keyword': + case 'attr-value': + return { color: '#0000ff' } + case 'function': + return { color: '#b9a40a' } + case 'deleted': + case 'tag': + return { color: '#9a050f' } + case 'selector': + return { color: '#00009f' } + case 'important': + return { color: '#e90', bold: true } + case 'italic': + return { italic: true } + case 'class-name': + case 'property': + return { color: '#2B91AF' } + case 'attr-name': + case 'regex': + case 'entity': + return { color: '#ff0000' } + default: + return null + } +} + +type IFormatPrismToken = { + type?: string + content: string +} & IPrismKindStyle + +export function formatPrismToken( + payload: (Prism.Token | string)[] +): IFormatPrismToken[] { + const formatTokenList: IFormatPrismToken[] = [] + function format(tokenList: (Prism.Token | string)[]) { + for (let i = 0; i < tokenList.length; i++) { + const element = tokenList[i] + if (typeof element === 'string') { + formatTokenList.push({ + content: element + }) + } else if (Array.isArray(element.content)) { + format(element.content) + } else { + const { type, content } = element + if (typeof content === 'string') { + formatTokenList.push({ + type, + content, + ...getPrismKindStyle(type) + }) + } + } + } + } + format(payload) + return formatTokenList +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ca05e19 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "ignoreDeprecations": "6.0", + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "declaration": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "outDir": "dist", + "rootDir": "", + }, + "include": ["./src/"], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..432d941 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,46 @@ +import { defineConfig } from 'vite' +import typescript from '@rollup/plugin-typescript' +import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js' +import * as path from 'path' + +export default defineConfig(({ mode }) => { + const name = 'canvas-editor' + if (mode === 'lib') { + return { + plugins: [ + cssInjectedByJsPlugin({ + styleId: `${name}-style`, + topExecutionPriority: true + }), + { + ...typescript({ + tsconfig: './tsconfig.json', + include: ['./src/editor/**'] + }), + apply: 'build', + declaration: true, + declarationDir: 'types/', + rootDir: '/' + } + ], + build: { + lib: { + name, + fileName: name, + entry: path.resolve(__dirname, 'src/editor/index.ts') + }, + rollupOptions: { + output: { + sourcemap: true + } + } + } + } + } + return { + base: `/${name}/`, + server: { + host: '0.0.0.0' + } + } +})