diff --git a/cypress.config.ts b/cypress.config.ts index 58f69cfe7..adf88f226 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -12,7 +12,7 @@ export default defineConfig({ videosFolder: 'test/cypress/videos', e2e: { // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these. + // You may want to clean this up later by importing new cypress plugins here. setupNodeEvents(on, config) { on('file:preprocessor', vitePreprocessor({ configFile: path.resolve(__dirname, './vite.config.test.js'), diff --git a/package-lock.json b/package-lock.json index 7ff829bd9..605413baa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "@editorjs/caret": "^1.0.1", "codex-notifier": "^1.1.2", - "codex-tooltip": "^1.0.5" + "codex-tooltip": "^1.0.5", + "vanilla-caret-js": "^1.1.0" }, "devDependencies": { "@babel/register": "^7.21.0", @@ -76,6 +77,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -122,7 +124,6 @@ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", "dev": true, - "peer": true, "dependencies": { "@babel/types": "^7.29.7" }, @@ -151,7 +152,6 @@ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", @@ -173,7 +173,6 @@ "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", @@ -191,7 +190,6 @@ "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", @@ -208,7 +206,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, - "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -225,8 +222,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@babel/helper-globals": { "version": "7.29.7", @@ -242,7 +238,6 @@ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", "dev": true, - "peer": true, "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" @@ -286,7 +281,6 @@ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", "dev": true, - "peer": true, "dependencies": { "@babel/types": "^7.29.7" }, @@ -299,7 +293,6 @@ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "dev": true, - "peer": true, "engines": { "node": ">=6.9.0" } @@ -309,7 +302,6 @@ "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-wrap-function": "^7.29.7", @@ -327,7 +319,6 @@ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", @@ -345,7 +336,6 @@ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", "dev": true, - "peer": true, "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" @@ -386,7 +376,6 @@ "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", "dev": true, - "peer": true, "dependencies": { "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", @@ -429,7 +418,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" @@ -446,7 +434,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -462,7 +449,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -478,7 +464,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" @@ -495,7 +480,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", @@ -513,7 +497,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" @@ -530,7 +513,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "dev": true, - "peer": true, "engines": { "node": ">=6.9.0" }, @@ -543,7 +525,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -559,7 +540,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -575,7 +555,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -592,7 +571,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -608,7 +586,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-remap-async-to-generator": "^7.29.7", @@ -626,7 +603,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", @@ -644,7 +620,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -660,7 +635,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -676,7 +650,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -693,7 +666,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -710,7 +682,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", @@ -731,7 +702,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/template": "^7.29.7" @@ -748,7 +718,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" @@ -765,7 +734,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -782,7 +750,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -798,7 +765,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -815,7 +781,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -831,7 +796,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-transform-destructuring": "^7.29.7" @@ -848,7 +812,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -864,7 +827,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -880,7 +842,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" @@ -897,7 +858,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", @@ -915,7 +875,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -931,7 +890,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -947,7 +905,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -963,7 +920,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -979,7 +935,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -996,7 +951,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -1013,7 +967,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", @@ -1032,7 +985,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -1049,7 +1001,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -1066,7 +1017,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1082,7 +1032,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1098,7 +1047,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1114,7 +1062,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", @@ -1134,7 +1081,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7" @@ -1151,7 +1097,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1167,7 +1112,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" @@ -1184,7 +1128,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1200,7 +1143,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -1217,7 +1159,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", @@ -1235,7 +1176,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1251,7 +1191,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1267,7 +1206,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -1284,7 +1222,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1300,7 +1237,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1316,7 +1252,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" @@ -1333,7 +1268,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1349,7 +1283,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1365,7 +1298,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1381,7 +1313,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1397,7 +1328,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -1414,7 +1344,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -1431,7 +1360,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -1534,7 +1462,6 @@ "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", @@ -1703,6 +1630,7 @@ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.1.0.tgz", "integrity": "sha512-KP8TicdXpUyeB1NMlbHud/1l39xvLGvqNFWMpG4qC6H1zs9SadGUHe5SO92n/659sDW9aGDvm9AMru0DZkN1Bw==", "dev": true, + "peer": true, "engines": { "node": "^14 || ^16 || >=18" }, @@ -1719,6 +1647,7 @@ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.1.0.tgz", "integrity": "sha512-dtqFyoJBHUxGi9zPZdpCKP1xk8tq6KPHJ/NY4qWXiYo6IcSGwzk3L8x2XzZbbyOyBs9xQARoGveU2AsgLj6D2A==", "dev": true, + "peer": true, "engines": { "node": "^14 || ^16 || >=18" }, @@ -1798,6 +1727,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", "dev": true, + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -1986,6 +1916,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", "dev": true, + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3110,7 +3041,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -3171,8 +3101,7 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@types/json-schema": { "version": "7.0.15", @@ -3304,6 +3233,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.54.1.tgz", "integrity": "sha512-8zaIXJp/nG9Ff9vQNh7TI+C3nA6q6iIsGJ4B4L6MhZ7mHnTMR4YP5vp2xydmFXIy8rpyIVbNAG44871LMt6ujg==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.54.1", "@typescript-eslint/types": "5.54.1", @@ -3566,7 +3496,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, - "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -3576,29 +3505,25 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, - "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -3609,15 +3534,13 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -3630,7 +3553,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, - "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -3640,7 +3562,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, - "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -3649,15 +3570,13 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -3674,7 +3593,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -3688,7 +3606,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -3701,7 +3618,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -3716,7 +3632,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -3726,21 +3641,20 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/acorn": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3753,7 +3667,6 @@ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, - "peer": true, "engines": { "node": ">=10.13.0" }, @@ -3788,6 +3701,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3804,7 +3718,6 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -3822,7 +3735,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3838,15 +3750,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "peer": true + "dev": true }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, - "peer": true, "peerDependencies": { "ajv": "^6.9.1" } @@ -4155,7 +4065,6 @@ "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.4.1.tgz", "integrity": "sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA==", "dev": true, - "peer": true, "dependencies": { "find-cache-dir": "^3.3.1", "loader-utils": "^2.0.4", @@ -4175,7 +4084,6 @@ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "dev": true, - "peer": true, "dependencies": { "commondir": "^1.0.1", "make-dir": "^3.0.2", @@ -4193,7 +4101,6 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, - "peer": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -4207,7 +4114,6 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, - "peer": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -4220,7 +4126,6 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, - "peer": true, "dependencies": { "semver": "^6.0.0" }, @@ -4236,7 +4141,6 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, - "peer": true, "dependencies": { "p-try": "^2.0.0" }, @@ -4252,7 +4156,6 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, - "peer": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -4265,7 +4168,6 @@ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, - "peer": true, "dependencies": { "find-up": "^4.0.0" }, @@ -4278,7 +4180,6 @@ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", "dev": true, - "peer": true, "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-define-polyfill-provider": "^0.6.8", @@ -4293,7 +4194,6 @@ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.8", "core-js-compat": "^3.48.0" @@ -4307,7 +4207,6 @@ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.8" }, @@ -4439,6 +4338,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.38", "caniuse-lite": "^1.0.30001799", @@ -4735,7 +4635,6 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, - "peer": true, "engines": { "node": ">=6.0" } @@ -4951,7 +4850,6 @@ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", "dev": true, - "peer": true, "dependencies": { "browserslist": "^4.28.1" }, @@ -5115,6 +5013,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", "dev": true, + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -5186,6 +5085,7 @@ "integrity": "sha512-hUxPrdbJXhUOTzuML+y9Av7CKoYznbD83pt8g3klgpioEha0emfx4WNIuVRx0C76r0xV2MIwAW9WYiXfVJYFQw==", "dev": true, "hasInstallScript": true, + "peer": true, "dependencies": { "@cypress/request": "^3.0.1", "@cypress/xvfb": "^1.2.4", @@ -5596,7 +5496,6 @@ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.1.tgz", "integrity": "sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw==", "dev": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" @@ -5610,6 +5509,7 @@ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, + "peer": true, "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -5699,8 +5599,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/es-set-tostringtag": { "version": "2.0.1", @@ -5812,6 +5711,7 @@ "integrity": "sha512-NU3Ps9nI05GUoVMxcZx1J8CNR6xOvUT4jAUMH5+z8lpp3aEdPVCImKw6PWG4PY+Vfkpr+jvMpxs/qoE7wq0sPw==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", @@ -6036,6 +5936,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.4", "array.prototype.flat": "^1.2.5", @@ -6144,6 +6045,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.6.1.tgz", "integrity": "sha512-R9xw9OtCRxxaxaszTQmQAlPgM+RdGjaL1akWuY/Fv9fRAi8Wj4CUKc6iYVG8QNRjRuo8/BqVYIpfqberJUEacA==", "dev": true, + "peer": true, "dependencies": { "builtins": "^5.0.1", "eslint-plugin-es": "^4.1.0", @@ -6202,6 +6104,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.1.tgz", "integrity": "sha512-uM4Tgo5u3UWQiroOyDEsYcVMOo7re3zmno0IZmB5auxoaQNIceAbXEkSt8RNrKtaYehARHG06pYK6K1JhtP0Zw==", "dev": true, + "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -6380,7 +6283,6 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, - "peer": true, "engines": { "node": ">=0.8.x" } @@ -6503,8 +6405,7 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ], - "peer": true + ] }, "node_modules/fastest-levenshtein": { "version": "1.0.16", @@ -7898,7 +7799,6 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, - "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -7913,7 +7813,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -8123,7 +8022,6 @@ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", "dev": true, - "peer": true, "engines": { "node": ">=6.11.5" }, @@ -8171,8 +8069,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, - "peer": true + "dev": true }, "node_modules/lodash.flattendeep": { "version": "4.4.0", @@ -8499,7 +8396,6 @@ "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", "integrity": "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==", "dev": true, - "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -8577,7 +8473,6 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -8589,15 +8484,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "peer": true + "dev": true }, "node_modules/minimizer-webpack-plugin/node_modules/schema-utils": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -8803,8 +8696,7 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/node-preload": { "version": "0.2.1", @@ -9534,6 +9426,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -10122,6 +10015,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", "dev": true, + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -10719,15 +10613,13 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true, - "peer": true + "dev": true }, "node_modules/regenerate-unicode-properties": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", "dev": true, - "peer": true, "dependencies": { "regenerate": "^1.4.2" }, @@ -10769,7 +10661,6 @@ "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", "dev": true, - "peer": true, "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", @@ -10786,15 +10677,13 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "dev": true, - "peer": true + "dev": true }, "node_modules/regjsparser": { "version": "0.13.2", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz", "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", "dev": true, - "peer": true, "dependencies": { "jsesc": "~3.1.0" }, @@ -10933,6 +10822,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.30.0.tgz", "integrity": "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==", "dev": true, + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -11050,7 +10940,6 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", "dev": true, - "peer": true, "dependencies": { "@types/json-schema": "^7.0.5", "ajv": "^6.12.4", @@ -11582,6 +11471,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", "dev": true, + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -11715,7 +11605,6 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, - "peer": true, "engines": { "node": ">=6" }, @@ -11729,7 +11618,6 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", "dev": true, - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -11747,8 +11635,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/test-exclude": { "version": "6.0.0", @@ -12156,6 +12043,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.3.tgz", "integrity": "sha512-xv8mOEDnigb/tN9PSMTwSEqAnUvkoXMQlicOb0IUVDBSQCgBSaAAROUZYy2IcUy5qU6XajK5jjjO7TMWqBTKZA==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12184,7 +12072,6 @@ "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "dev": true, - "peer": true, "engines": { "node": ">=4" } @@ -12194,7 +12081,6 @@ "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "dev": true, - "peer": true, "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" @@ -12208,7 +12094,6 @@ "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", "dev": true, - "peer": true, "engines": { "node": ">=4" } @@ -12218,7 +12103,6 @@ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", "dev": true, - "peer": true, "engines": { "node": ">=4" } @@ -12328,6 +12212,12 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/vanilla-caret-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vanilla-caret-js/-/vanilla-caret-js-1.1.0.tgz", + "integrity": "sha512-vl3R4Xjqb+xnM0gYyg+wcqWGYwKKnkz58Yj29/FQB+w+yP7R8IR1DGoJnDIs05moEZiGlQUTabPMoEtnQNcrpQ==", + "license": "MIT" + }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -12347,6 +12237,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", @@ -12411,7 +12302,6 @@ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", "dev": true, - "peer": true, "dependencies": { "graceful-fs": "^4.1.2" }, @@ -12470,7 +12360,6 @@ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", "dev": true, - "peer": true, "engines": { "node": ">=10.13.0" } @@ -12503,7 +12392,6 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -12516,7 +12404,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -12530,7 +12417,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, - "peer": true, "engines": { "node": ">=4.0" } @@ -12539,15 +12425,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "peer": true + "dev": true }, "node_modules/webpack/node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, - "peer": true, "engines": { "node": ">= 0.6" } @@ -12557,7 +12441,6 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/package.json b/package.json index ef97b89a7..133a0bcca 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "dependencies": { "@editorjs/caret": "^1.0.1", "codex-notifier": "^1.1.2", - "codex-tooltip": "^1.0.5" + "codex-tooltip": "^1.0.5", + "vanilla-caret-js": "^1.1.0" } -} \ No newline at end of file +} diff --git a/src/components/block/index.ts b/src/components/block/index.ts index 36c4aa2ae..bb4893c92 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -867,15 +867,12 @@ export default class Block extends EventsDispatcher { * - undefined — manual triggering of block.dispatchChange() */ private readonly didMutated = (mutationsOrInputEvent: MutationRecord[] | InputEvent = undefined): void => { - /** - * Block API have dispatchChange() method. In this case, mutations list will be undefined. - */ const isManuallyDispatched = mutationsOrInputEvent === undefined; + const isInputEventHandler = mutationsOrInputEvent instanceof InputEvent; /** - * True if didMutated has been called as "input" event handler + * Block API have dispatchChange() method. In this case, mutations list will be undefined. */ - const isInputEventHandler = mutationsOrInputEvent instanceof InputEvent; /** * If tool updates its own root element, we need to renew it in our memory diff --git a/src/components/core.ts b/src/components/core.ts index a7e307c32..b0407185d 100644 --- a/src/components/core.ts +++ b/src/components/core.ts @@ -253,6 +253,7 @@ export default class Core { 'RectangleSelection', 'CrossBlockSelection', 'ReadOnly', + 'History', ]; await modulesToPrepare.reduce( diff --git a/src/components/dom.ts b/src/components/dom.ts index 2f67e49a7..16182d3fb 100644 --- a/src/components/dom.ts +++ b/src/components/dom.ts @@ -568,12 +568,12 @@ export default class Dom { /** * Returns the closest ancestor anchor (A tag) of the given element (including itself) - * + * * @param element - element to check * @returns {HTMLAnchorElement | null} */ public static getClosestAnchor(element: Element): HTMLAnchorElement | null { - return element.closest("a"); + return element.closest('a'); } /** diff --git a/src/components/modules/api/history.ts b/src/components/modules/api/history.ts new file mode 100644 index 000000000..fe5d344b0 --- /dev/null +++ b/src/components/modules/api/history.ts @@ -0,0 +1,56 @@ +import type { History as HistoryInterface } from '../../../../types/api'; +import Module from '../../__module'; + +/** + * @class HistoryAPI + * @classdesc Public API for undo/redo operations + */ +export default class HistoryAPI extends Module { + /** + * Available methods exposed on editor.history + */ + public get methods(): HistoryInterface { + return { + undo: (): Promise => this.undo(), + redo: (): Promise => this.redo(), + canUndo: (): boolean => this.canUndo(), + canRedo: (): boolean => this.canRedo(), + clear: (): Promise => this.clear(), + }; + } + + /** + * Undo the last change + */ + public undo(): Promise { + return this.Editor.History.undo(); + } + + /** + * Redo the last undone change + */ + public redo(): Promise { + return this.Editor.History.redo(); + } + + /** + * Returns true if undo is possible + */ + public canUndo(): boolean { + return this.Editor.History.canUndo(); + } + + /** + * Returns true if redo is possible + */ + public canRedo(): boolean { + return this.Editor.History.canRedo(); + } + + /** + * Clear the history stack + */ + public async clear(): Promise { + await this.Editor.History.clear(); + } +} diff --git a/src/components/modules/api/index.ts b/src/components/modules/api/index.ts index e334244de..8856ba85f 100644 --- a/src/components/modules/api/index.ts +++ b/src/components/modules/api/index.ts @@ -33,6 +33,7 @@ export default class API extends Module { i18n: this.Editor.I18nAPI.methods, readOnly: this.Editor.ReadOnlyAPI.methods, ui: this.Editor.UiAPI.methods, + history: this.Editor.HistoryAPI.methods, }; } diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index be8e1e247..feb34e5a8 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -895,8 +895,8 @@ export default class BlockManager extends Module { const queue = new PromiseQueue(); // Create a copy of the blocks array to avoid issues with array modification during iteration - const blocksToRemove = [...this.blocks]; - + const blocksToRemove = [ ...this.blocks ]; + blocksToRemove.forEach((block) => { queue.add(async () => { await this.removeBlock(block, false); diff --git a/src/components/modules/history.ts b/src/components/modules/history.ts new file mode 100644 index 000000000..f9b15a27b --- /dev/null +++ b/src/components/modules/history.ts @@ -0,0 +1,1235 @@ +import Module from '../__module'; +import type { BlockToolData } from '../../../types'; +import type { BlockMutationEvent, BlockMutationType } from '../../../types/events/block'; +import { BlockChangedMutationType } from '../../../types/events/block/BlockChanged'; +import { BlockAddedMutationType } from '../../../types/events/block/BlockAdded'; +import { BlockRemovedMutationType } from '../../../types/events/block/BlockRemoved'; +import type { BlockRemovedEvent } from '../../../types/events/block/BlockRemoved'; +import { BlockMovedMutationType } from '../../../types/events/block/BlockMoved'; +import type { BlockMovedEvent } from '../../../types/events/block/BlockMoved'; +import { BlockChanged } from '../events'; +import type { BlockChangedPayload } from '../events/BlockChanged'; +import VanillaCaret from 'vanilla-caret-js'; + +// eslint-disable-next-line jsdoc/require-jsdoc +const requestIdleCallbackPolyfill = typeof window !== 'undefined' && typeof window.requestIdleCallback === 'function' + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + ? window.requestIdleCallback.bind(window) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + : ((cb: () => void) => setTimeout(cb, 0)) as any; + +// eslint-disable-next-line jsdoc/require-jsdoc +const cancelIdleCallbackPolyfill = typeof window !== 'undefined' && typeof window.cancelIdleCallback === 'function' + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + ? window.cancelIdleCallback.bind(window) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + : ((handle: number) => clearTimeout(handle)) as any; + +/** + * Cached data for a block: its tool name and its data. + */ +interface BlockCache { + /** + * The tool name (e.g. "paragraph", "header") + */ + tool: string; + + /** + * The block's data + */ + data: BlockToolData; +} + +/** + * Describes a single entry in the history stack + */ +interface HistoryEntry { + /** + * What kind of mutation occurred + */ + operation: BlockMutationType; + + /** + * Block index at the time of the mutation + */ + blockIndex: number; + + /** + * ID of the affected block + */ + blockId: string; + + /** + * Tool name of the affected block (e.g. "paragraph", "header") + */ + blockTool: string; + + /** + * Data snapshot of the block at the time of the mutation. + * For 'block-removed', this stores the data that was removed so we can re-insert. + * For 'block-added', this stores the data that was added. + * For 'block-changed', this stores the new data. + */ + blockData: BlockToolData; + + /** + * For 'block-changed': the data before the change. Used to restore on undo. + */ + previousBlockData?: BlockToolData; + + /** + * For 'block-moved': the original index before the move + */ + sourceIndex?: number; + + /** + * For 'block-moved': the destination index after the move + */ + destinationIndex?: number; + + /** + * Snapshot of caret position at the time of entry creation. + * Captured using text-content offsets (not innerHTML) so it works + * correctly with getNodeByOffset. + * + * For undo of block-changed: used to restore caret (clamps to restored text length). + * For redo of block-changed/block-moved: used to restore caret. + */ + caretSnapshot: CaretSnapshot | null; +} + +/** + * Stores enough info to restore caret position + */ +interface CaretSnapshot { + /** + * Block index + */ + blockIndex: number; + + /** + * Offset within the block's first input element + */ + textOffset: number; +} + +/** + * Pending debounced change for a single block. + * Captures the pre-change data and tracks the latest post-change data. + */ +interface PendingBlockChange { + /** + * The data before the mutation burst started (from knownBlockData at first event) + */ + previousData: BlockToolData; + + /** + * Tool name of the block + */ + tool: string; + + /** + * Timer handle for the debounce + */ + timer: ReturnType; +} + +/** + * Delay in milliseconds before restoring the caret after a DOM change. + * Lets the browser finish layout after BlockManager.update / insert / remove. + * Same pattern as editorjs-undo uses (50ms there, 20ms here since our DOM + * changes are more targeted). + */ +const CARET_RESTORE_DELAY_MS = 0; + +/** + * Maximum number of entries to keep in the history stack + */ +const DEFAULT_STACK_LIMIT = 100; + +/** + * Debounce window in milliseconds. Rapid mutations within this window + * are collapsed into a single history entry. + */ +const DEBOUNCE_MS = 300; + +/** + * Polling interval in milliseconds for waiting on the mutation queue to drain + */ +const QUEUE_POLL_MS = 10; + +/** + * Module that manages undo/redo history for the editor. + * + * Listens to block mutation events and records each operation so it can be replayed + * in reverse (undo) or forward (redo). + * + * Uses debouncing for block-changed events: rapid mutations (e.g. keystrokes) + * within a short window are collapsed into a single history entry so that one + * undo restores the entire editing burst. + */ +export default class History extends Module { + /** + * The stack of recorded operations + */ + private stack: HistoryEntry[] = []; + + /** + * Current position within the stack. Points to the latest applied entry. + * Starts at -1 (no undoable entries after initial baseline). + */ + private pointer = -1; + + /** + * Guard flag to prevent recording changes that we ourselves are making during undo/redo + */ + private applying = false; + + /** + * Maximum number of entries allowed in the stack + */ + private stackLimit = DEFAULT_STACK_LIMIT; + + /** + * Cache of the last known data and tool name for each block, keyed by block ID. + * Used to detect whether a block-changed event actually has different data + * and to populate `previousBlockData`. + */ + private readonly knownBlockData = new Map(); + + /** + * Pending debounced changes keyed by block ID. + * When a block-changed event fires, we start/reset a timer. When the timer + * fires, we push a single entry if the data actually changed. + */ + private readonly pendingChanges = new Map(); + + /** + * Reference to the bound keydown handler so we can remove it on destroy + */ + private keydownHandler: ((event: KeyboardEvent) => void) | null = null; + + /** + * Ordered async queue for serializing mutation handlers. + * Ensures pushEntry calls occur in the same order as mutation events arrive, + * even when handlers involve async work like block.save(). + */ + private mutationQueue: Array<() => Promise> = []; + + /** + * Whether the mutation queue is currently being drained + */ + private mutationQueueDraining = false; + + /** + * Handle returned by requestIdleCallback (or setTimeout fallback) in prepare(). + * Stored so destroy() can cancel it. + */ + private initIdleHandle: number | null = null; + + /** + * Whether the requestIdleCallback polyfill fallback (setTimeout) was used + */ + private isUsingIdleFallback = false; + + /** + * Whether captureInitialState has completed. + * Used to prevent initial-render block-added events from being recorded in the stack. + */ + private initialRenderComplete = false; + + /** + * Prepare the module: subscribe to events and register shortcuts + */ + public async prepare(): Promise { + this.stackLimit = this.config.history?.stackLimit ?? DEFAULT_STACK_LIMIT; + + this.eventsDispatcher.on(BlockChanged, (payload) => { + this.onBlockMutation(payload); + }); + + this.registerShortcuts(); + + /** + * Defer captureInitialState to after blocks are rendered. + * prepare() is called before Renderer.render(), so BlockManager.blocks is empty. + * Using requestIdleCallback ensures we capture initial state after blocks are in the DOM. + */ + this.isUsingIdleFallback = typeof window === 'undefined' || typeof window.requestIdleCallback !== 'function'; + this.initIdleHandle = requestIdleCallbackPolyfill(() => { + this.initIdleHandle = null; + void this.captureInitialState(); + }, { timeout: 2000 }); + } + + /** + * Undo the last operation. + * Flushes any pending debounced changes before proceeding. + * + * Caret is always restored AFTER the operation. The target position depends + * on the operation type — we use a small setTimeout to let the browser + * finish layout after DOM changes (same pattern as editorjs-undo). + */ + public async undo(): Promise { + /** + * Set the applying guard BEFORE the async flush to prevent a second + * rapid undo() call from passing the guard while the first is still flushing. + */ + if (this.applying) { + return; + } + + this.applying = true; + + await this.flushPendingChanges(); + + if (!this.canUndo()) { + this.applying = false; + + return; + } + + this.Editor.ModificationsObserver.disable(); + + const entry = this.stack[this.pointer]; + + try { + await this.applyOperation(entry, 'backward'); + this.pointer--; + this.restoreCaretForOperation(entry, 'backward'); + } catch (err) { + console.error('[History] Failed to undo:', err); + } finally { + this.Editor.ModificationsObserver.enable(); + this.applying = false; + } + } + + /** + * Redo the next operation. + * Cancel pending debounced changes rather than flushing them. + */ + public async redo(): Promise { + if (this.applying) { + return; + } + + this.applying = true; + this.cancelPendingChanges(); + + if (!this.canRedo()) { + this.applying = false; + + return; + } + + this.Editor.ModificationsObserver.disable(); + + this.pointer++; + + const entry = this.stack[this.pointer]; + + try { + await this.applyOperation(entry, 'forward'); + this.pointer++; + this.restoreCaretForOperation(entry, 'forward'); + } catch (err) { + console.error('[History] Failed to redo:', err); + } finally { + this.Editor.ModificationsObserver.enable(); + this.applying = false; + } + } + + /** + * Returns true if there are entries to undo. + * Also returns true if there are pending debounced changes that haven't been + * finalized yet (i.e. the user just typed something). + */ + public canUndo(): boolean { + if (this.Editor.ReadOnly.isEnabled) { + return false; + } + + return this.pointer >= 0 || this.pendingChanges.size > 0; + } + + /** + * Returns true if there are entries to redo + */ + public canRedo(): boolean { + return !this.Editor.ReadOnly.isEnabled && this.pointer < this.stack.length - 1; + } + + /** + * Reset the history stack. + * Re-seeds the baseline state after clearing. + */ + public async clear(): Promise { + this.cancelPendingChanges(); + this.stack = []; + this.pointer = -1; + this.knownBlockData.clear(); + this.initialRenderComplete = false; + await this.captureInitialState(); + } + + /** + * Clean up keyboard shortcuts, pending timers, and idle callbacks on destroy + */ + public destroy(): void { + this.cancelPendingChanges(); + + if (this.keydownHandler !== null) { + const holder = this.Editor.UI.nodes.redactor; + + holder.removeEventListener('keydown', this.keydownHandler); + this.keydownHandler = null; + } + + if (this.initIdleHandle !== null) { + if (this.isUsingIdleFallback) { + clearTimeout(this.initIdleHandle); + } else { + cancelIdleCallbackPolyfill(this.initIdleHandle); + } + this.initIdleHandle = null; + } + + this.mutationQueue = []; + this.knownBlockData.clear(); + } + + /** + * Enqueue an async task to be executed in order. + * Tasks are drained one at a time so pushEntry calls maintain mutation order. + * + * @param task - Async function to enqueue + */ + private enqueueMutation(task: () => Promise): void { + this.mutationQueue.push(task); + void this.drainMutationQueue(); + } + + /** + * Drain the mutation queue sequentially + */ + private async drainMutationQueue(): Promise { + if (this.mutationQueueDraining) { + return; + } + + this.mutationQueueDraining = true; + + while (this.mutationQueue.length > 0) { + const task = this.mutationQueue.shift(); + + if (task !== undefined) { + try { + await task(); + } catch (err) { + console.error('[History] Mutation queue task failed:', err); + } + } + } + + this.mutationQueueDraining = false; + } + + /** + * Handle a block mutation event from the editor + * + * @param payload - The block change event payload + */ + private onBlockMutation(payload: BlockChangedPayload): void { + if (this.applying) { + return; + } + + const event = payload.event as BlockMutationEvent; + const detail = event.detail; + const mutationType = event.type as BlockMutationType; + + const blockId = detail.target.id; + + if (mutationType === BlockChangedMutationType) { + if (!this.knownBlockData.has(blockId)) { + const block = this.Editor.BlockManager.getBlockById(blockId); + + if (block) { + const data = this.captureBlockDataSync(block); + + if (data !== null) { + this.knownBlockData.set(blockId, { + tool: block.name, + data, + }); + } + } + + return; + } + + this.scheduleBlockChanged(blockId); + + return; + } + + if (mutationType === BlockAddedMutationType) { + /** + * handleBlockAdded is async (does block.save()) but onBlockMutation is + * synchronous (called from EventsDispatcher.emit). We enqueue the work + * so pushEntry calls maintain the order mutations arrive. + */ + this.enqueueMutation(() => this.handleBlockAdded(blockId)); + + return; + } + + if (mutationType === BlockRemovedMutationType) { + this.cancelPendingChangeForBlock(blockId); + this.handleBlockRemoved(blockId, event as BlockRemovedEvent); + + return; + } + + if (mutationType === BlockMovedMutationType) { + this.handleBlockMoved(event as BlockMovedEvent); + } + } + + /** + * Synchronously capture a block's current data by reading from its DOM. + * Used to seed knownBlockData on the first block-changed event for a block + * before captureInitialState completes. + * + * Supports: paragraph, header. Other tools return null to indicate + * that synchronous capture is not possible — these blocks will be + * seeded by captureInitialState instead. + * + * @param block - The block to capture data from + */ + private captureBlockDataSync(block: { name: string; holder: HTMLElement }): BlockToolData | null { + if (block.name === 'paragraph') { + const editable = block.holder.querySelector('[contenteditable="true"]'); + + return { text: editable?.innerHTML ?? '' }; + } + + if (block.name === 'header') { + const editable = block.holder.querySelector('[contenteditable="true"]') as HTMLElement | null; + + if (editable) { + const tag = editable.tagName.toLowerCase(); + const levelMatch = tag.match(/^h(\d)$/); + const level = levelMatch ? parseInt(levelMatch[1], 10) : 1; + + return { + text: editable.innerHTML, + level, + }; + } + } + + return null; + } + + /** + * Schedule a debounced recording for a block-changed event. + * + * On the first call for a given blockId, captures the pre-change data from + * knownBlockData and starts a debounce timer. Subsequent calls for the same + * blockId reset the timer but keep the original pre-change data. + * When the timer fires, pushes a single entry if data actually changed. + * + * @param blockId - ID of the changed block + */ + private scheduleBlockChanged(blockId: string): void { + const existing = this.pendingChanges.get(blockId); + + if (existing !== undefined) { + clearTimeout(existing.timer); + + existing.timer = setTimeout(() => { + this.finalizeBlockChanged(blockId); + }, DEBOUNCE_MS); + + return; + } + + const cached = this.knownBlockData.get(blockId); + const block = this.Editor.BlockManager.getBlockById(blockId); + + if (!block || cached === undefined) { + return; + } + + const timer = setTimeout(() => { + this.finalizeBlockChanged(blockId); + }, DEBOUNCE_MS); + + this.pendingChanges.set(blockId, { + previousData: cached.data, + tool: block.name, + timer, + }); + } + + /** + * Finalize a debounced block-changed event. + * Enqueues the async work (block.save + pushEntry) so it maintains + * ordering with other mutation handlers. + * + * @param blockId - ID of the changed block + */ + private finalizeBlockChanged(blockId: string): void { + const pending = this.pendingChanges.get(blockId); + + this.pendingChanges.delete(blockId); + + if (pending === undefined) { + return; + } + + clearTimeout(pending.timer); + + this.enqueueMutation(() => this.doFinalizeBlockChanged(blockId, pending)); + } + + /** + * Perform the actual async finalization: read block data, compare, and push entry. + * + * @param blockId - ID of the changed block + * @param pending - The pending change data captured at scheduling time + */ + private async doFinalizeBlockChanged(blockId: string, pending: PendingBlockChange): Promise { + const block = this.Editor.BlockManager.getBlockById(blockId); + + if (!block) { + return; + } + + const saved = await block.save(); + + if (!saved) { + return; + } + + const currentData = saved.data; + + if (JSON.stringify(currentData) === JSON.stringify(pending.previousData)) { + return; + } + + const blockIndex = this.Editor.BlockManager.getBlockIndex(block); + + this.pushEntry({ + operation: BlockChangedMutationType, + blockIndex, + blockId, + blockTool: pending.tool, + blockData: currentData, + previousBlockData: pending.previousData, + caretSnapshot: this.captureCaret(), + }); + + this.knownBlockData.set(blockId, { + tool: pending.tool, + data: currentData, + }); + } + + /** + * Flush all pending debounced changes immediately. + * Called before undo/redo operations. Enqueues all pending finalizations + * and waits for the mutation queue to drain so ordering is preserved. + */ + private async flushPendingChanges(): Promise { + const entries = Array.from(this.pendingChanges.keys()); + + for (const blockId of entries) { + this.finalizeBlockChanged(blockId); + } + + await this.waitForMutationQueue(); + } + + /** + * Wait for the mutation queue to fully drain + */ + private waitForMutationQueue(): Promise { + if (this.mutationQueue.length === 0 && !this.mutationQueueDraining) { + return Promise.resolve(); + } + + return new Promise((resolve) => { + const check = (): void => { + if (this.mutationQueue.length === 0 && !this.mutationQueueDraining) { + resolve(); + } else { + setTimeout(check, QUEUE_POLL_MS); + } + }; + + void check(); + }); + } + + /** + * Synchronously cancel a pending change for a specific block. + * Used before block-removed events where we don't need to record a + * block-changed entry since the block is being removed. + * + * @param blockId - The block ID whose pending change to cancel + */ + private cancelPendingChangeForBlock(blockId: string): void { + const pending = this.pendingChanges.get(blockId); + + if (pending === undefined) { + return; + } + + clearTimeout(pending.timer); + this.pendingChanges.delete(blockId); + } + + /** + * Cancel all pending debounced changes without recording entries. + */ + private cancelPendingChanges(): void { + for (const [, pending] of this.pendingChanges) { + clearTimeout(pending.timer); + } + + this.pendingChanges.clear(); + } + + /** + * Handle a block-added mutation. + * Records a new entry for blocks that appear after the initial render. + * + * @param blockId - ID of the added block + */ + private async handleBlockAdded(blockId: string): Promise { + const block = this.Editor.BlockManager.getBlockById(blockId); + + if (!block) { + return; + } + + /** + * Capture initial state synchronously from the DOM. + * This runs immediately when the block is inserted, before the user + * can type into it. Using block.save() (async) would risk capturing + * user-typed content if the save resolves after keystrokes. + */ + const initialData = this.captureBlockDataSync(block) ?? {}; + + if (!this.knownBlockData.has(blockId)) { + this.knownBlockData.set(blockId, { + tool: block.name, + data: initialData, + }); + } + + if (!this.initialRenderComplete) { + return; + } + + const blockIndex = this.Editor.BlockManager.getBlockIndex(block); + + /** + * Finalize any pending debounced change for the previous block before + * pushing this block-added entry. Without this, block-added entries + * (which fire immediately) appear at the end of the stack BEFORE + * block-changed entries (which are debounced 300ms), so undo of the + * last entry undoes a text change instead of the expected line removal. + */ + if (blockIndex > 0) { + const prevBlock = this.Editor.BlockManager.getBlockByIndex(blockIndex - 1); + + if (prevBlock) { + const pending = this.pendingChanges.get(prevBlock.id); + + if (pending !== undefined) { + this.pendingChanges.delete(prevBlock.id); + clearTimeout(pending.timer); + await this.doFinalizeBlockChanged(prevBlock.id, pending); + } + } + } + + this.pushEntry({ + operation: BlockAddedMutationType, + blockIndex, + blockId, + blockTool: block.name, + blockData: initialData, + caretSnapshot: this.captureCaret(), + }); + } + + /** + * Handle a block-removed mutation + * + * @param blockId - ID of the removed block + * @param event - The typed BlockRemoved event containing detail.index + */ + private handleBlockRemoved(blockId: string, event: BlockRemovedEvent): void { + const cached = this.knownBlockData.get(blockId); + + if (cached === undefined) { + return; + } + + const block = this.Editor.BlockManager.getBlockById(blockId); + const blockIndex = block ? this.Editor.BlockManager.getBlockIndex(block) : event.detail.index; + + this.pushEntry({ + operation: BlockRemovedMutationType, + blockIndex, + blockId, + blockTool: cached.tool, + blockData: cached.data, + caretSnapshot: this.captureCaret(), + }); + + this.knownBlockData.delete(blockId); + } + + /** + * Handle a block-moved mutation + * + * @param movedEvent - The typed BlockMoved event + */ + private handleBlockMoved(movedEvent: BlockMovedEvent): void { + const { fromIndex, toIndex } = movedEvent.detail; + const blockId = movedEvent.detail.target.id; + + this.pushEntry({ + operation: BlockMovedMutationType, + blockIndex: toIndex, + blockId, + blockTool: '', + blockData: {}, + sourceIndex: fromIndex, + destinationIndex: toIndex, + caretSnapshot: this.captureCaret(), + }); + } + + /** + * Push an entry onto the stack, discarding any redo entries ahead of the pointer + * + * @param entry - The history entry to push + */ + private pushEntry(entry: HistoryEntry): void { + this.stack = this.stack.slice(0, this.pointer + 1); + this.stack.push(entry); + this.pointer = this.stack.length - 1; + + while (this.stack.length > this.stackLimit) { + this.stack.shift(); + this.pointer--; + } + } + + /** + * Apply an operation in the given direction + * + * @param entry - The history entry to apply + * @param direction - Whether to apply forward (redo) or backward (undo) + */ + private async applyOperation(entry: HistoryEntry, direction: 'forward' | 'backward'): Promise { + const { operation } = entry; + + switch (operation) { + case BlockAddedMutationType: + if (direction === 'backward') { + await this.removeBlockFromEditor(entry); + } else { + await this.insertBlockToEditor(entry); + } + break; + + case BlockRemovedMutationType: + if (direction === 'backward') { + await this.insertBlockToEditor(entry); + } else { + await this.removeBlockFromEditor(entry); + } + break; + + case BlockChangedMutationType: + if (direction === 'backward') { + await this.restoreBlockData(entry, entry.previousBlockData); + } else { + await this.restoreBlockData(entry, entry.blockData); + } + break; + + case BlockMovedMutationType: + if (direction === 'backward' && entry.sourceIndex !== undefined && entry.destinationIndex !== undefined) { + this.Editor.BlockManager.move(entry.sourceIndex, entry.destinationIndex); + } else if (direction === 'forward' && entry.sourceIndex !== undefined && entry.destinationIndex !== undefined) { + this.Editor.BlockManager.move(entry.destinationIndex, entry.sourceIndex); + } + break; + } + } + + /** + * Insert a block into the editor from a history entry + * + * @param entry - The entry containing block data to insert + */ + private async insertBlockToEditor(entry: HistoryEntry): Promise { + this.Editor.BlockManager.insert({ + id: entry.blockId, + tool: entry.blockTool, + data: entry.blockData, + index: entry.blockIndex, + needToFocus: false, + }); + + this.knownBlockData.set(entry.blockId, { + tool: entry.blockTool, + data: entry.blockData, + }); + } + + /** + * Remove a block from the editor by its entry's block ID + * + * @param entry - The entry identifying the block to remove + */ + private async removeBlockFromEditor(entry: HistoryEntry): Promise { + const block = this.Editor.BlockManager.getBlockById(entry.blockId); + + if (block) { + await this.Editor.BlockManager.removeBlock(block, false); + this.knownBlockData.delete(entry.blockId); + } + } + + /** + * Restore block data (used for both undo and redo of block-changed). + * + * For tools with a `text` property (paragraph, header), we update the + * contenteditable's innerHTML directly to avoid creating a new Block + * instance. This prevents the visual "blink" caused by DOM element + * replacement during BlockManager.update(). + * + * For other tools, falls back to BlockManager.update(). + * + * @param entry - The history entry containing the block to restore + * @param data - The data to restore the block to + */ + private async restoreBlockData(entry: HistoryEntry, data: BlockToolData | undefined): Promise { + if (data === undefined) { + return; + } + + const block = this.Editor.BlockManager.getBlockById(entry.blockId); + + if (!block) { + return; + } + + const hasTextProperty = data !== null && typeof data === 'object' && 'text' in data; + + if (hasTextProperty) { + const input = block.currentInput ?? block.firstInput; + + if (input) { + const html = (data as { text: string }).text ?? ''; + + input.innerHTML = html; + + this.knownBlockData.set(entry.blockId, { + tool: entry.blockTool, + data, + }); + + return; + } + } + + await this.Editor.BlockManager.update(block, data); + this.knownBlockData.set(entry.blockId, { + tool: entry.blockTool, + data, + }); + } + + /** + * Capture the current editor state as a non-undoable baseline. + * Seeds the knownBlockData cache without pushing entries to the stack, + * so undo is unavailable immediately after load and only becomes available + * after a user edit. + */ + private async captureInitialState(): Promise { + if (this.Editor?.BlockManager === undefined) { + return; + } + + const blocks = this.Editor.BlockManager.blocks; + + if (blocks.length === 0) { + return; + } + + const blockSnapshots = await Promise.all( + blocks.map(async (block) => { + const saved = await block.save(); + + return { + id: block.id, + tool: block.name, + data: saved?.data ?? {}, + }; + }) + ); + + blockSnapshots.forEach((snapshot) => { + this.knownBlockData.set(snapshot.id, { + tool: snapshot.tool, + data: snapshot.data, + }); + }); + + this.initialRenderComplete = true; + } + + /** + * Capture the current caret position using text-content offsets. + * Uses range.toString().length which naturally gives plain-text character count, + * avoiding the innerHTML vs textContent mismatch. + */ + /** + * Capture the current caret position using vanilla-caret-js. + * Returns a plain-text character offset via VanillaCaret.getPos(), + * which uses range.toString().length internally. + */ + private captureCaret(): CaretSnapshot | null { + const { BlockManager } = this.Editor; + + if (!BlockManager.currentBlock) { + return null; + } + + const blockIndex = BlockManager.currentBlockIndex; + const input = BlockManager.currentBlock.currentInput; + + if (!input) { + return { + blockIndex, + textOffset: 0, + }; + } + + const caret = new VanillaCaret(input); + const pos = caret.getPos(); + + return { + blockIndex, + textOffset: pos === -1 ? 0 : pos, + }; + } + + /** + * Compute the caret snapshot for undo of a block-added entry. + * The undo removes the new block, so the caret goes to the end of the block before it. + * + * @param blockIndex - Index of the block that was added + */ + private caretSnapshotForUndoOfBlockAdded(blockIndex: number): CaretSnapshot { + const prevIndex = blockIndex - 1; + + if (prevIndex < 0) { + return { blockIndex: 0, + textOffset: 0 }; + } + + const block = this.Editor.BlockManager.getBlockByIndex(prevIndex); + + if (!block) { + return { blockIndex: prevIndex, + textOffset: 0 }; + } + + const input = block.currentInput ?? block.firstInput; + + if (!input) { + return { blockIndex: prevIndex, + textOffset: 0 }; + } + + return { blockIndex: prevIndex, + textOffset: (input.textContent ?? '').length }; + } + + /** + * Restore the caret to a previously captured position. + * Uses a small setTimeout to let the browser finish layout after + * DOM changes (BlockManager.update / insert / remove). + */ + /** + * Restore the caret to a previously captured position using vanilla-caret-js. + * Uses a 50ms setTimeout (same as editorjs-undo) to let the browser finish + * layout after DOM changes (BlockManager.update / insert / remove). + * + * @param snapshot - The caret position snapshot to restore, or null to skip + */ + private restoreCaret(snapshot: CaretSnapshot | null): void { + if (!snapshot) { + return; + } + + const { BlockManager } = this.Editor; + + setTimeout(() => { + const block = BlockManager.getBlockByIndex(snapshot.blockIndex); + + if (block === undefined || !block.focusable) { + return; + } + + const input = block.currentInput ?? block.firstInput; + + if (!input) { + return; + } + + const caret = new VanillaCaret(input); + + /** + * Clamp textOffset to the actual text length. + * VanillaCaret.setPos() does NOT clamp — it walks the DOM tree and + * places the caret at a browser-dependent offset when the position + * exceeds the available text nodes. This causes the caret to land + * at random positions after undo (e.g., position 2 on an empty block + * when the captured offset was 22). + */ + const maxOffset = (input.textContent ?? '').length; + + /** + * Set the selection range BEFORE focusing to avoid a microsecond + * caret flash at position 0. With the range pre-set, the browser + * renders the caret at the correct position on the first paint + * when focus() fires. + */ + caret.setPos(Math.min(snapshot.textOffset, maxOffset)); + input.focus(); + }, CARET_RESTORE_DELAY_MS); + } + + /** + * Determine the correct caret position after an undo or redo operation + * and restore it. + * + * @param entry - The history entry that was just applied + * @param direction - 'backward' for undo, 'forward' for redo + */ + private restoreCaretForOperation(entry: HistoryEntry, direction: 'forward' | 'backward'): void { + const { operation } = entry; + + if (direction === 'backward') { + switch (operation) { + case BlockChangedMutationType: + /** + * Undo restores previousBlockData. The recorded caretSnapshot was captured + * at the time of the change (after typing). Caret.setToBlock clamps the offset + * to the text length, so the caret lands at the end of the restored text. + */ + this.restoreCaret(entry.caretSnapshot); + break; + + case BlockAddedMutationType: + /** + * Undo removes the newly added block. The caret should be at the end + * of the block that precedes it. + */ + this.restoreCaret(this.caretSnapshotForUndoOfBlockAdded(entry.blockIndex)); + break; + + case BlockRemovedMutationType: + /** + * Undo re-inserts the removed block. The caret should be at the + * start of the re-inserted block. + */ + this.restoreCaret({ blockIndex: entry.blockIndex, + textOffset: 0 }); + break; + + case BlockMovedMutationType: + this.restoreCaret(entry.caretSnapshot); + break; + + default: + this.restoreCaret(entry.caretSnapshot); + break; + } + } else { + switch (operation) { + case BlockChangedMutationType: + this.restoreCaret(entry.caretSnapshot); + break; + + case BlockAddedMutationType: + /** + * Redo re-inserts the added block. The caret should be at the + * start of the re-inserted block. + */ + this.restoreCaret({ blockIndex: entry.blockIndex, + textOffset: 0 }); + break; + + case BlockRemovedMutationType: + /** + * Redo removes the block. The caret should be at the end of the + * block that precedes it. + */ + this.restoreCaret(this.caretSnapshotForUndoOfBlockAdded(entry.blockIndex)); + break; + + case BlockMovedMutationType: + this.restoreCaret(entry.caretSnapshot); + break; + + default: + this.restoreCaret(entry.caretSnapshot); + break; + } + } + } + + /** + * Register Ctrl/Cmd+Z and Ctrl/Cmd+Shift+Z keyboard shortcuts + */ + private registerShortcuts(): void { + const redactor = this.Editor.UI.nodes.redactor; + + this.keydownHandler = (event: KeyboardEvent) => { + if (!(event.ctrlKey || event.metaKey)) { + return; + } + + const key = event.key.toLowerCase(); + + if (key === 'z' && !event.shiftKey) { + event.preventDefault(); + this.undo(); + + return; + } + + if ((key === 'z' && event.shiftKey) || key === 'y') { + event.preventDefault(); + this.redo(); + } + }; + + redactor.addEventListener('keydown', this.keydownHandler); + } +} diff --git a/src/components/modules/index.ts b/src/components/modules/index.ts index 2c945f43d..c86de9bb4 100644 --- a/src/components/modules/index.ts +++ b/src/components/modules/index.ts @@ -16,6 +16,7 @@ import StylesAPI from './api/styles'; import ToolbarAPI from './api/toolbar'; import TooltipAPI from './api/tooltip'; import UiAPI from './api/ui'; +import HistoryAPI from './api/history'; /** ./toolbar */ import BlockSettings from './toolbar/blockSettings'; @@ -37,6 +38,7 @@ import Renderer from './renderer'; import Saver from './saver'; import Tools from './tools'; import UI from './ui'; +import History from './history'; export default { // API Modules @@ -57,6 +59,7 @@ export default { ToolbarAPI, TooltipAPI, UiAPI, + HistoryAPI, // Toolbar Modules BlockSettings, @@ -78,4 +81,5 @@ export default { Saver, Tools, UI, + History, }; diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index d8dc37989..8f8992146 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -774,7 +774,7 @@ export default class UI extends Module { const element = event.target as Element; const ctrlKey = event.metaKey || event.ctrlKey; const anchor = $.getClosestAnchor(element); - + if (anchor && ctrlKey) { event.stopImmediatePropagation(); event.stopPropagation(); diff --git a/src/components/utils/mutations.ts b/src/components/utils/mutations.ts index 3d5e8f25d..033858583 100644 --- a/src/components/utils/mutations.ts +++ b/src/components/utils/mutations.ts @@ -14,10 +14,29 @@ export function isMutationBelongsToElement(mutationRecord: MutationRecord, eleme return false; } + /** + * For characterData mutations, use block-boundary check to prevent cross-block matching. + * This handles the case where toolRenderedElement may reference a parent/shared element. + */ + if (type === 'characterData') { + const targetParent = target.parentElement as HTMLElement | null; + + if (targetParent !== null) { + const targetBlock = targetParent.closest('.ce-block'); + const elementBlock = element.closest('.ce-block'); + + if (targetBlock !== null && elementBlock !== null) { + return targetBlock === elementBlock; + } + } + } + + const containsResult = element.contains(target); + /** * Covers all types of mutations happened to the element or it's descendants with the only one exception - removing/adding the element itself; */ - if (element.contains(target)) { + if (containsResult) { return true; } diff --git a/src/types-internal/editor-modules.d.ts b/src/types-internal/editor-modules.d.ts index 3e455c349..8988f9a4c 100644 --- a/src/types-internal/editor-modules.d.ts +++ b/src/types-internal/editor-modules.d.ts @@ -15,6 +15,7 @@ import StylesAPI from '../components/modules/api/styles'; import ToolbarAPI from '../components/modules/api/toolbar'; import TooltipAPI from '../components/modules/api/tooltip'; import UiAPI from '../components/modules/api/ui'; +import HistoryAPI from '../components/modules/api/history'; /** ./toolbar */ import BlockSettings from '../components/modules/toolbar/blockSettings'; @@ -36,6 +37,7 @@ import Renderer from '../components/modules/renderer'; import Saver from '../components/modules/saver'; import Tools from '../components/modules/tools'; import UI from '../components/modules/ui'; +import History from '../components/modules/history'; import ToolsAPI from '../components/modules/api/tools'; export interface EditorModules { @@ -57,6 +59,7 @@ export interface EditorModules { ToolbarAPI: ToolbarAPI, TooltipAPI: TooltipAPI, UiAPI: UiAPI, + HistoryAPI: HistoryAPI, // Toolbar Modules BlockSettings: BlockSettings, @@ -78,4 +81,5 @@ export interface EditorModules { Saver: Saver, Tools: Tools, UI: UI, + History: History, } diff --git a/test/cypress/fixtures/test.html b/test/cypress/fixtures/test.html index 38d93ec7c..fd6247d54 100644 --- a/test/cypress/fixtures/test.html +++ b/test/cypress/fixtures/test.html @@ -2,7 +2,7 @@ - +

Editor.js test page

diff --git a/test/cypress/support/commands.ts b/test/cypress/support/commands.ts index 35392fe2f..9250d864c 100644 --- a/test/cypress/support/commands.ts +++ b/test/cypress/support/commands.ts @@ -29,7 +29,7 @@ Cypress.Commands.add('createEditor', (editorConfig: EditorConfig = {}): Chainabl window.document.body.appendChild(editorContainer); - const editorInstance: EditorJS = new window.EditorJS(editorConfig); + const editorInstance: EditorJS = new window.MonkeysEditor(editorConfig); editorInstance.isReady.then(() => { resolve(editorInstance); diff --git a/test/cypress/tests/modules/History.cy.ts b/test/cypress/tests/modules/History.cy.ts new file mode 100644 index 000000000..796b76859 --- /dev/null +++ b/test/cypress/tests/modules/History.cy.ts @@ -0,0 +1,329 @@ +import type EditorJS from '../../../../types/index'; + +describe('History module', function () { + describe('undo()', function () { + it('should undo text changes in a block', function () { + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Initial text', + }, + }, + ], + }, + }).then((editor: EditorJS) => { + cy.get('[data-cy=editorjs]') + .get('.ce-paragraph') + .click() + .type('{selectall}Modified text') + .then(() => { + return editor.history.undo(); + }) + .then(async () => { + const data = await editor.save(); + + expect(data.blocks.length).to.equal(1); + expect(data.blocks[0].data.text).to.equal('Initial text'); + }); + }); + }); + + it('should undo block removal', function () { + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'First block', + }, + }, + { + type: 'paragraph', + data: { + text: 'Second block', + }, + }, + ], + }, + }).then((editor: EditorJS) => { + cy.get('[data-cy=editorjs]') + .get('.ce-block') + .eq(1) + .click() + .type('{backspace}') + .then(() => { + return editor.history.undo(); + }) + .then(async () => { + const data = await editor.save(); + + expect(data.blocks.length).to.equal(2); + expect(data.blocks[0].data.text).to.equal('First block'); + expect(data.blocks[1].data.text).to.equal('Second block'); + }); + }); + }); + + it('should undo multiple typed lines without content reappearing', function () { + cy.createEditor({ + tools: { + paragraph: { + config: { + preserveBlank: true, + }, + }, + }, + data: { + blocks: [ + { + type: 'paragraph', + data: { text: '' }, + }, + { + type: 'paragraph', + data: { text: '' }, + }, + ], + }, + }).then((editor: EditorJS) => { + cy.get('[data-cy=editorjs]') + .get('.ce-paragraph') + .eq(0) + .click() + .type('Line 1') + .wait(400) + .then(() => { + return editor.history.undo(); + }) + .then(async () => { + const data = await editor.save(); + + expect(data.blocks[0].data.text).to.equal(''); + }) + .get('.ce-paragraph') + .eq(1) + .click() + .type('Line 2') + .wait(400) + .then(() => { + return editor.history.undo(); + }) + .then(async () => { + const data = await editor.save(); + + expect(data.blocks[0].data.text).to.equal(''); + expect(data.blocks[1].data.text).to.equal(''); + }); + }); + }); + + it('should not have initial blocks in the undo stack', function () { + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Block one', + }, + }, + { + type: 'paragraph', + data: { + text: 'Block two', + }, + }, + ], + }, + }).then((editor: EditorJS) => { + expect(editor.history.canUndo()).to.equal(false); + + cy.get('[data-cy=editorjs]') + .get('.ce-paragraph') + .eq(1) + .click() + .type('{selectall}Modified two') + .wait(400) + .then(() => { + expect(editor.history.canUndo()).to.equal(true); + + return editor.history.undo(); + }) + .then(async () => { + const data = await editor.save(); + + expect(data.blocks.length).to.equal(2); + expect(data.blocks[0].data.text).to.equal('Block one'); + expect(data.blocks[1].data.text).to.equal('Block two'); + }); + }); + }); + }); + + describe('redo()', function () { + it('should redo text changes after undo', function () { + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Initial text', + }, + }, + ], + }, + }).then((editor: EditorJS) => { + cy.get('[data-cy=editorjs]') + .get('.ce-paragraph') + .click() + .type('{selectall}Modified text') + .then(() => { + return editor.history.undo(); + }) + .then(() => { + return editor.history.redo(); + }) + .then(async () => { + const data = await editor.save(); + + expect(data.blocks.length).to.equal(1); + expect(data.blocks[0].data.text).to.equal('Modified text'); + }); + }); + }); + }); + + describe('canUndo() and canRedo()', function () { + it('should report correct state for undo and redo', function () { + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Initial text', + }, + }, + ], + }, + }).then((editor: EditorJS) => { + expect(editor.history.canUndo()).to.equal(false); + expect(editor.history.canRedo()).to.equal(false); + + cy.get('[data-cy=editorjs]') + .get('.ce-paragraph') + .click() + .type('{selectall}Modified text') + .wait(400) + .then(() => { + expect(editor.history.canUndo()).to.equal(true); + expect(editor.history.canRedo()).to.equal(false); + + return editor.history.undo(); + }) + .then(() => { + expect(editor.history.canUndo()).to.equal(false); + expect(editor.history.canRedo()).to.equal(true); + + return editor.history.redo(); + }) + .then(() => { + expect(editor.history.canUndo()).to.equal(true); + expect(editor.history.canRedo()).to.equal(false); + }); + }); + }); + }); + + describe('keyboard shortcuts', function () { + it('should undo with Ctrl+Z', function () { + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Initial text', + }, + }, + ], + }, + }).then((editor: EditorJS) => { + cy.get('[data-cy=editorjs]') + .get('.ce-paragraph') + .click() + .type('{selectall}Modified text') + .type('{ctrl+z}') + .then(async () => { + const data = await editor.save(); + + expect(data.blocks[0].data.text).to.equal('Initial text'); + }); + }); + }); + + it('should redo with Ctrl+Shift+Z', function () { + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Initial text', + }, + }, + ], + }, + }).then((editor: EditorJS) => { + cy.get('[data-cy=editorjs]') + .get('.ce-paragraph') + .click() + .type('{selectall}Modified text') + .type('{ctrl+z}'); + + cy.get('[data-cy=editorjs]') + .find('.ce-paragraph') + .click() + .type('{ctrl+shift+z}') + .then(async () => { + const data = await editor.save(); + + expect(data.blocks[0].data.text).to.equal('Modified text'); + }); + }); + }); + }); + + describe('clear()', function () { + it('should clear history stack', function () { + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Initial text', + }, + }, + ], + }, + }).then((editor: EditorJS) => { + cy.get('[data-cy=editorjs]') + .get('.ce-paragraph') + .click() + .type('{selectall}Modified text') + .wait(400) + .then(() => { + expect(editor.history.canUndo()).to.equal(true); + editor.history.clear(); + expect(editor.history.canUndo()).to.equal(false); + }); + }); + }); + }); +}); diff --git a/types/api/history.d.ts b/types/api/history.d.ts new file mode 100644 index 000000000..be5d0ce61 --- /dev/null +++ b/types/api/history.d.ts @@ -0,0 +1,29 @@ +/** + * Describes methods available on editor.history for undo/redo + */ +export interface History { + /** + * Undo the last change + */ + undo(): Promise; + + /** + * Redo the last undone change + */ + redo(): Promise; + + /** + * Returns true if there are entries to undo + */ + canUndo(): boolean; + + /** + * Returns true if there are entries to redo + */ + canRedo(): boolean; + + /** + * Clear the history stack and re-seed baseline state + */ + clear(): Promise; +} diff --git a/types/api/index.d.ts b/types/api/index.d.ts index 2b6f51a33..0c875b59f 100644 --- a/types/api/index.d.ts +++ b/types/api/index.d.ts @@ -15,3 +15,4 @@ export * from './readonly'; export * from './i18n'; export * from './ui'; export * from './tools'; +export * from './history'; diff --git a/types/configs/editor-config.d.ts b/types/configs/editor-config.d.ts index 0f60a3fd2..a384b12d5 100644 --- a/types/configs/editor-config.d.ts +++ b/types/configs/editor-config.d.ts @@ -115,4 +115,14 @@ export interface EditorConfig { */ nonce?: string; } + + /** + * History (undo/redo) configuration + */ + history?: { + /** + * Maximum number of entries to keep in the undo/redo stack + */ + stackLimit?: number; + }; } diff --git a/types/index.d.ts b/types/index.d.ts index 66f0eb38c..953436363 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -29,6 +29,7 @@ import { I18n, Ui, Tools, + History, } from './api'; import { OutputData } from './data-formats'; @@ -120,6 +121,7 @@ export interface API { i18n: I18n; readOnly: ReadOnly; ui: Ui; + history: History; } /** @@ -139,6 +141,7 @@ declare class EditorJS { public toolbar: Toolbar; public inlineToolbar: InlineToolbar; public readOnly: ReadOnly; + public history: History; constructor(configuration?: EditorConfig|string); /** diff --git a/yarn.lock b/yarn.lock index e32e7dc36..d6cb7fac7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6162,6 +6162,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +vanilla-caret-js@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/vanilla-caret-js/-/vanilla-caret-js-1.1.0.tgz" + integrity sha512-vl3R4Xjqb+xnM0gYyg+wcqWGYwKKnkz58Yj29/FQB+w+yP7R8IR1DGoJnDIs05moEZiGlQUTabPMoEtnQNcrpQ== + verror@1.10.0: version "1.10.0" resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz"