Implement custom lightweight modal #230
Introduce a brand new lightweight and efficient modal component. It is designed to be visually similar to the previous one to not introduce a change in feel of the application in a patch release, but behind the scenes it features: - Enhanced application speed and reduced bundle size. - New flexbox-driven layout, eliminating JS calculations. - Composition API ready for Vue 3.0 #230. Other changes: - Adopt idiomatic Vue via `v-modal` binding. - Add unit tests for both the modal and dialog. - Remove `vue-js-modal` dependency in favor of the new implementation. - Adjust modal shadow color to better match theme. - Add `@vue/test-utils` for unit testing.
This commit is contained in:
@@ -50,9 +50,9 @@ This project uses a singleton instance of the application state, making it avail
|
|||||||
|
|
||||||
The decision to not use third-party state management libraries like [`vuex`](https://web.archive.org/web/20230801191617/https://vuex.vuejs.org/) or [`pinia`](https://web.archive.org/web/20230801191743/https://pinia.vuejs.org/) was made to promote code independence and enhance portability.
|
The decision to not use third-party state management libraries like [`vuex`](https://web.archive.org/web/20230801191617/https://vuex.vuejs.org/) or [`pinia`](https://web.archive.org/web/20230801191743/https://pinia.vuejs.org/) was made to promote code independence and enhance portability.
|
||||||
|
|
||||||
Stateful components can mutate and/or react to state changes (e.g., user selection, search queries) in the [ApplicationContext](./../src/application/Context/ApplicationContext.ts). Vue components import [`CollectionState.ts`](./../src/presentation/components/Shared/CollectionState.ts) to access both the application context and the state.
|
Stateful components can mutate and/or react to state changes (e.g., user selection, search queries) in the [ApplicationContext](./../src/application/Context/ApplicationContext.ts). Vue components import [`CollectionState.ts`](./../src/presentation/components/Shared/Hooks/UseCollectionState.ts) to access both the application context and the state.
|
||||||
|
|
||||||
[`CollectionState.ts`](./../src/presentation/components/Shared/CollectionState.ts) provides several functionalities including:
|
[`UseCollectionState.ts`](./../src/presentation/components/Shared/Hooks/UseCollectionState.ts) provides several functionalities including:
|
||||||
|
|
||||||
- **Singleton State Instance**: It creates a singleton instance of the state, which is shared across the presentation layer. The singleton instance ensures that there's a single source of truth for the application's state.
|
- **Singleton State Instance**: It creates a singleton instance of the state, which is shared across the presentation layer. The singleton instance ensures that there's a single source of truth for the application's state.
|
||||||
- **State Change Callback and Lifecycle Management**: It offers a mechanism to register callbacks, which will be invoked when the state initializes or mutates. It ensures that components unsubscribe from state events when they are no longer in use or when [ApplicationContext](./../src/application/Context/ApplicationContext.ts) switches the active [collection](./collection-files.md).
|
- **State Change Callback and Lifecycle Management**: It offers a mechanism to register callbacks, which will be invoked when the state initializes or mutates. It ensures that components unsubscribe from state events when they are no longer in use or when [ApplicationContext](./../src/application/Context/ApplicationContext.ts) switches the active [collection](./collection-files.md).
|
||||||
@@ -63,16 +63,7 @@ Stateful components can mutate and/or react to state changes (e.g., user selecti
|
|||||||
|
|
||||||
## Modals
|
## Modals
|
||||||
|
|
||||||
[ModalDialog.vue](./../src/presentation/components/Shared/ModalDialog.vue) is a shared component utilized for rendering modal windows.
|
- [ModalDialog.vue](./../src/presentation/components/Shared/Modal/ModalDialog.vue) is a shared component utilized for rendering modal windows.
|
||||||
|
|
||||||
Use the component by wrapping the desired content within its slot and calling the .show() function on its reference, as shown below:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<ModalDialog ref="testDialog">
|
|
||||||
<div>Hello world</div>
|
|
||||||
</ModalDialog>
|
|
||||||
<div @click="$refs.testDialog.show()">Show dialog</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sass naming convention
|
## Sass naming convention
|
||||||
|
|
||||||
|
|||||||
529
package-lock.json
generated
529
package-lock.json
generated
@@ -25,8 +25,7 @@
|
|||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"npm": "^9.8.1",
|
"npm": "^9.8.1",
|
||||||
"v-tooltip": "2.1.3",
|
"v-tooltip": "2.1.3",
|
||||||
"vue": "^2.7.14",
|
"vue": "^2.7.14"
|
||||||
"vue-js-modal": "^2.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.3.2",
|
"@rushstack/eslint-patch": "^1.3.2",
|
||||||
@@ -44,6 +43,7 @@
|
|||||||
"@vue/cli-service": "~5.0.8",
|
"@vue/cli-service": "~5.0.8",
|
||||||
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
|
"@vue/test-utils": "^1.3.6",
|
||||||
"chai": "^4.3.7",
|
"chai": "^4.3.7",
|
||||||
"cypress": "^12.17.2",
|
"cypress": "^12.17.2",
|
||||||
"electron": "^25.3.2",
|
"electron": "^25.3.2",
|
||||||
@@ -3311,6 +3311,12 @@
|
|||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@one-ini/wasm": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
||||||
|
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@pkgjs/parseargs": {
|
"node_modules/@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
@@ -4826,6 +4832,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vue/test-utils": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-udMmmF1ts3zwxUJEIAj5ziioR900reDrt6C9H3XpWPsLBx2lpHKoA4BTdd9HNIYbkGltWw+JjWJ+5O6QBwiyEw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"dom-event-types": "^1.0.0",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
|
"pretty": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "2.x",
|
||||||
|
"vue-template-compiler": "^2.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vue/vue-loader-v15": {
|
"node_modules/@vue/vue-loader-v15": {
|
||||||
"name": "vue-loader",
|
"name": "vue-loader",
|
||||||
"version": "15.10.0",
|
"version": "15.10.0",
|
||||||
@@ -5043,6 +5064,12 @@
|
|||||||
"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
|
"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/abbrev": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
@@ -7053,6 +7080,36 @@
|
|||||||
"typedarray": "^0.0.6"
|
"typedarray": "^0.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/condense-newlines": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/condense-newlines/-/condense-newlines-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"extend-shallow": "^2.0.1",
|
||||||
|
"is-whitespace": "^0.3.0",
|
||||||
|
"kind-of": "^3.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/config-chain": {
|
||||||
|
"version": "1.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
|
||||||
|
"integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ini": "^1.3.4",
|
||||||
|
"proto-list": "~1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/config-chain/node_modules/ini": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/config-file-ts": {
|
"node_modules/config-file-ts": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.4.tgz",
|
||||||
@@ -7834,6 +7891,13 @@
|
|||||||
"integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==",
|
"integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/de-indent": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
|
||||||
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
@@ -8419,6 +8483,12 @@
|
|||||||
"utila": "~0.4"
|
"utila": "~0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-event-types": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-event-types/-/dom-event-types-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-jNCX+uNJ3v38BKvPbpki6j5ItVlnSqVV6vDWGS6rExzCMjsc39frLjm1n91o6YaKK6AZl0wLloItW6C6mr61BQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/dom-serializer": {
|
"node_modules/dom-serializer": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
||||||
@@ -8554,6 +8624,57 @@
|
|||||||
"safer-buffer": "^2.1.0"
|
"safer-buffer": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/editorconfig": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@one-ini/wasm": "0.1.1",
|
||||||
|
"commander": "^10.0.0",
|
||||||
|
"minimatch": "9.0.1",
|
||||||
|
"semver": "^7.5.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"editorconfig": "bin/editorconfig"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/editorconfig/node_modules/brace-expansion": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/editorconfig/node_modules/commander": {
|
||||||
|
"version": "10.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
||||||
|
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/editorconfig/node_modules/minimatch": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -10374,6 +10495,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||||
},
|
},
|
||||||
|
"node_modules/extend-shallow": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"is-extendable": "^0.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/extract-zip": {
|
"node_modules/extract-zip": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||||
@@ -12156,6 +12289,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-buffer": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/is-callable": {
|
"node_modules/is-callable": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||||
@@ -12228,6 +12367,15 @@
|
|||||||
"integrity": "sha512-F2FnH/otLNJv0J6wc73A5Xo7oHLNnqplYqZhUu01tD54DIPvxIRSTSLkrUB/M0nHO4vo1O9PDfN4KoTxCzLh/w==",
|
"integrity": "sha512-F2FnH/otLNJv0J6wc73A5Xo7oHLNnqplYqZhUu01tD54DIPvxIRSTSLkrUB/M0nHO4vo1O9PDfN4KoTxCzLh/w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/is-extendable": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
||||||
|
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
@@ -12575,6 +12723,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-whitespace": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-whitespace/-/is-whitespace-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-wsl": {
|
"node_modules/is-wsl": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||||
@@ -12808,6 +12965,66 @@
|
|||||||
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
|
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/js-beautify": {
|
||||||
|
"version": "1.14.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.9.tgz",
|
||||||
|
"integrity": "sha512-coM7xq1syLcMyuVGyToxcj2AlzhkDjmfklL8r0JgJ7A76wyGMpJ1oA35mr4APdYNO/o/4YY8H54NQIJzhMbhBg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"config-chain": "^1.1.13",
|
||||||
|
"editorconfig": "^1.0.3",
|
||||||
|
"glob": "^8.1.0",
|
||||||
|
"nopt": "^6.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"css-beautify": "js/bin/css-beautify.js",
|
||||||
|
"html-beautify": "js/bin/html-beautify.js",
|
||||||
|
"js-beautify": "js/bin/js-beautify.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/js-beautify/node_modules/brace-expansion": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/js-beautify/node_modules/glob": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^5.0.1",
|
||||||
|
"once": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/js-beautify/node_modules/minimatch": {
|
||||||
|
"version": "5.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||||
|
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-message": {
|
"node_modules/js-message": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz",
|
||||||
@@ -13158,6 +13375,18 @@
|
|||||||
"json-buffer": "3.0.0"
|
"json-buffer": "3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/kind-of": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"is-buffer": "^1.1.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/klaw": {
|
"node_modules/klaw": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz",
|
||||||
@@ -15637,6 +15866,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true
|
"hasInstallScript": true
|
||||||
},
|
},
|
||||||
|
"node_modules/nopt": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"abbrev": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"nopt": "bin/nopt.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/normalize-package-data": {
|
"node_modules/normalize-package-data": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||||
@@ -20360,6 +20604,20 @@
|
|||||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pretty": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pretty/-/pretty-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"condense-newlines": "^0.2.1",
|
||||||
|
"extend-shallow": "^2.0.1",
|
||||||
|
"js-beautify": "^1.6.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pretty-bytes": {
|
"node_modules/pretty-bytes": {
|
||||||
"version": "5.6.0",
|
"version": "5.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
||||||
@@ -20580,6 +20838,12 @@
|
|||||||
"levenshtein-edit-distance": "^1.0.0"
|
"levenshtein-edit-distance": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proto-list": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@@ -22259,11 +22523,6 @@
|
|||||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/resize-observer-polyfill": {
|
|
||||||
"version": "1.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
|
||||||
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
|
|
||||||
},
|
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.1",
|
"version": "1.22.1",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
||||||
@@ -25767,17 +26026,6 @@
|
|||||||
"integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==",
|
"integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/vue-js-modal": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-js-modal/-/vue-js-modal-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-5FUwsH2zoxRKX4a7wkFAqX0eITCcIMunJDEfIxzHs2bHw9o20+Iqm+uQvBcg1jkzyo1+tVgThR/7NGU8djbD8Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"resize-observer-polyfill": "^1.5.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"vue": "^2.6.11"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vue-loader": {
|
"node_modules/vue-loader": {
|
||||||
"version": "17.0.0",
|
"version": "17.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.0.0.tgz",
|
||||||
@@ -25903,6 +26151,17 @@
|
|||||||
"integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==",
|
"integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-template-compiler": {
|
||||||
|
"version": "2.7.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz",
|
||||||
|
"integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==",
|
||||||
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"de-indent": "^1.0.2",
|
||||||
|
"he": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-template-es2015-compiler": {
|
"node_modules/vue-template-es2015-compiler": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz",
|
||||||
@@ -29399,6 +29658,12 @@
|
|||||||
"integrity": "sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==",
|
"integrity": "sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@one-ini/wasm": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
||||||
|
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@pkgjs/parseargs": {
|
"@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
@@ -30605,6 +30870,17 @@
|
|||||||
"vue-eslint-parser": "^9.1.1"
|
"vue-eslint-parser": "^9.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@vue/test-utils": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-udMmmF1ts3zwxUJEIAj5ziioR900reDrt6C9H3XpWPsLBx2lpHKoA4BTdd9HNIYbkGltWw+JjWJ+5O6QBwiyEw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"dom-event-types": "^1.0.0",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
|
"pretty": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@vue/vue-loader-v15": {
|
"@vue/vue-loader-v15": {
|
||||||
"version": "npm:vue-loader@15.10.0",
|
"version": "npm:vue-loader@15.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.0.tgz",
|
||||||
@@ -30808,6 +31084,12 @@
|
|||||||
"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
|
"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"abbrev": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"accepts": {
|
"accepts": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
@@ -32322,6 +32604,35 @@
|
|||||||
"typedarray": "^0.0.6"
|
"typedarray": "^0.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"condense-newlines": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/condense-newlines/-/condense-newlines-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"extend-shallow": "^2.0.1",
|
||||||
|
"is-whitespace": "^0.3.0",
|
||||||
|
"kind-of": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config-chain": {
|
||||||
|
"version": "1.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
|
||||||
|
"integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"ini": "^1.3.4",
|
||||||
|
"proto-list": "~1.2.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ini": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"config-file-ts": {
|
"config-file-ts": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.4.tgz",
|
||||||
@@ -32894,6 +33205,13 @@
|
|||||||
"integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==",
|
"integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"de-indent": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
|
||||||
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
@@ -33305,6 +33623,12 @@
|
|||||||
"utila": "~0.4"
|
"utila": "~0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dom-event-types": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-event-types/-/dom-event-types-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-jNCX+uNJ3v38BKvPbpki6j5ItVlnSqVV6vDWGS6rExzCMjsc39frLjm1n91o6YaKK6AZl0wLloItW6C6mr61BQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"dom-serializer": {
|
"dom-serializer": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
||||||
@@ -33413,6 +33737,44 @@
|
|||||||
"safer-buffer": "^2.1.0"
|
"safer-buffer": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"editorconfig": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@one-ini/wasm": "0.1.1",
|
||||||
|
"commander": "^10.0.0",
|
||||||
|
"minimatch": "9.0.1",
|
||||||
|
"semver": "^7.5.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"commander": {
|
||||||
|
"version": "10.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
||||||
|
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"minimatch": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"ee-first": {
|
"ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -34795,6 +35157,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||||
},
|
},
|
||||||
|
"extend-shallow": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"is-extendable": "^0.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"extract-zip": {
|
"extract-zip": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||||
@@ -36104,6 +36475,12 @@
|
|||||||
"has-tostringtag": "^1.0.0"
|
"has-tostringtag": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"is-buffer": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"is-callable": {
|
"is-callable": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||||
@@ -36149,6 +36526,12 @@
|
|||||||
"integrity": "sha512-F2FnH/otLNJv0J6wc73A5Xo7oHLNnqplYqZhUu01tD54DIPvxIRSTSLkrUB/M0nHO4vo1O9PDfN4KoTxCzLh/w==",
|
"integrity": "sha512-F2FnH/otLNJv0J6wc73A5Xo7oHLNnqplYqZhUu01tD54DIPvxIRSTSLkrUB/M0nHO4vo1O9PDfN4KoTxCzLh/w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"is-extendable": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
||||||
|
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"is-extglob": {
|
"is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
@@ -36381,6 +36764,12 @@
|
|||||||
"call-bind": "^1.0.2"
|
"call-bind": "^1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"is-whitespace": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-whitespace/-/is-whitespace-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"is-wsl": {
|
"is-wsl": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||||
@@ -36559,6 +36948,51 @@
|
|||||||
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
|
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"js-beautify": {
|
||||||
|
"version": "1.14.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.9.tgz",
|
||||||
|
"integrity": "sha512-coM7xq1syLcMyuVGyToxcj2AlzhkDjmfklL8r0JgJ7A76wyGMpJ1oA35mr4APdYNO/o/4YY8H54NQIJzhMbhBg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"config-chain": "^1.1.13",
|
||||||
|
"editorconfig": "^1.0.3",
|
||||||
|
"glob": "^8.1.0",
|
||||||
|
"nopt": "^6.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"glob": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^5.0.1",
|
||||||
|
"once": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimatch": {
|
||||||
|
"version": "5.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||||
|
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"js-message": {
|
"js-message": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz",
|
||||||
@@ -36863,6 +37297,15 @@
|
|||||||
"json-buffer": "3.0.0"
|
"json-buffer": "3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"kind-of": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"is-buffer": "^1.1.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"klaw": {
|
"klaw": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz",
|
||||||
@@ -38685,6 +39128,15 @@
|
|||||||
"integrity": "sha512-7Ws63oC+215smeKJQCxzrK21VFVlCFBkwl0MOObt0HOpVQXs3u483sAmtkF33nNqZ5rSOQjB76fgyPBmAUrtCA==",
|
"integrity": "sha512-7Ws63oC+215smeKJQCxzrK21VFVlCFBkwl0MOObt0HOpVQXs3u483sAmtkF33nNqZ5rSOQjB76fgyPBmAUrtCA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"nopt": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"abbrev": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"normalize-package-data": {
|
"normalize-package-data": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||||
@@ -41856,6 +42308,17 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"pretty": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pretty/-/pretty-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"condense-newlines": "^0.2.1",
|
||||||
|
"extend-shallow": "^2.0.1",
|
||||||
|
"js-beautify": "^1.6.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"pretty-bytes": {
|
"pretty-bytes": {
|
||||||
"version": "5.6.0",
|
"version": "5.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
||||||
@@ -42024,6 +42487,12 @@
|
|||||||
"levenshtein-edit-distance": "^1.0.0"
|
"levenshtein-edit-distance": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"proto-list": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"proxy-addr": {
|
"proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@@ -43324,11 +43793,6 @@
|
|||||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"resize-observer-polyfill": {
|
|
||||||
"version": "1.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
|
||||||
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
|
|
||||||
},
|
|
||||||
"resolve": {
|
"resolve": {
|
||||||
"version": "1.22.1",
|
"version": "1.22.1",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
||||||
@@ -45949,14 +46413,6 @@
|
|||||||
"integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==",
|
"integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"vue-js-modal": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-js-modal/-/vue-js-modal-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-5FUwsH2zoxRKX4a7wkFAqX0eITCcIMunJDEfIxzHs2bHw9o20+Iqm+uQvBcg1jkzyo1+tVgThR/7NGU8djbD8Q==",
|
|
||||||
"requires": {
|
|
||||||
"resize-observer-polyfill": "^1.5.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"vue-loader": {
|
"vue-loader": {
|
||||||
"version": "17.0.0",
|
"version": "17.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.0.0.tgz",
|
||||||
@@ -46056,6 +46512,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"vue-template-compiler": {
|
||||||
|
"version": "2.7.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz",
|
||||||
|
"integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==",
|
||||||
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
|
"requires": {
|
||||||
|
"de-indent": "^1.0.2",
|
||||||
|
"he": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"vue-template-es2015-compiler": {
|
"vue-template-es2015-compiler": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-cli-service build",
|
||||||
"test:unit": "vue-cli-service test:unit",
|
"test:unit": "vue-cli-service test:unit --include ./tests/bootstrap/setup.ts",
|
||||||
"test:e2e": "vue-cli-service test:e2e",
|
"test:e2e": "vue-cli-service test:e2e",
|
||||||
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
|
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
|
||||||
"create-icons": "node img/logo-update.mjs",
|
"create-icons": "node img/logo-update.mjs",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"postuninstall": "electron-builder install-app-deps",
|
"postuninstall": "electron-builder install-app-deps",
|
||||||
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\""
|
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\" --include ./tests/bootstrap/setup.ts"
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -41,8 +41,7 @@
|
|||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"npm": "^9.8.1",
|
"npm": "^9.8.1",
|
||||||
"v-tooltip": "2.1.3",
|
"v-tooltip": "2.1.3",
|
||||||
"vue": "^2.7.14",
|
"vue": "^2.7.14"
|
||||||
"vue-js-modal": "^2.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.3.2",
|
"@rushstack/eslint-patch": "^1.3.2",
|
||||||
@@ -60,6 +59,7 @@
|
|||||||
"@vue/cli-service": "~5.0.8",
|
"@vue/cli-service": "~5.0.8",
|
||||||
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
|
"@vue/test-utils": "^1.3.6",
|
||||||
"chai": "^4.3.7",
|
"chai": "^4.3.7",
|
||||||
"cypress": "^12.17.2",
|
"cypress": "^12.17.2",
|
||||||
"electron": "^25.3.2",
|
"electron": "^25.3.2",
|
||||||
|
|||||||
@@ -27,3 +27,21 @@
|
|||||||
*/
|
*/
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin fade-slide-transition($name, $duration, $offset-upward: null) {
|
||||||
|
.#{$name}-enter-active,
|
||||||
|
.#{$name}-leave-active {
|
||||||
|
transition: all $duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$name}-leave-active,
|
||||||
|
.#{$name}-enter, // Vue 2.X compatibility
|
||||||
|
.#{$name}-enter-from // Vue 3.X compatibility
|
||||||
|
{
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
@if $offset-upward {
|
||||||
|
transform: translateY($offset-upward);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { VModalBootstrapper } from './Modules/VModalBootstrapper';
|
|
||||||
import { TreeBootstrapper } from './Modules/TreeBootstrapper';
|
import { TreeBootstrapper } from './Modules/TreeBootstrapper';
|
||||||
import { IconBootstrapper } from './Modules/IconBootstrapper';
|
import { IconBootstrapper } from './Modules/IconBootstrapper';
|
||||||
import { VueConstructor, IVueBootstrapper } from './IVueBootstrapper';
|
import { VueConstructor, IVueBootstrapper } from './IVueBootstrapper';
|
||||||
@@ -19,7 +18,6 @@ export class ApplicationBootstrapper implements IVueBootstrapper {
|
|||||||
new TreeBootstrapper(),
|
new TreeBootstrapper(),
|
||||||
new VueBootstrapper(),
|
new VueBootstrapper(),
|
||||||
new TooltipBootstrapper(),
|
new TooltipBootstrapper(),
|
||||||
new VModalBootstrapper(),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import VModal from 'vue-js-modal';
|
|
||||||
import { VueConstructor, IVueBootstrapper } from '../IVueBootstrapper';
|
|
||||||
|
|
||||||
export class VModalBootstrapper implements IVueBootstrapper {
|
|
||||||
public bootstrap(vue: VueConstructor): void {
|
|
||||||
vue.use(VModal, { dynamic: true, injectModalsContainer: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
icon-prefix="fas"
|
icon-prefix="fas"
|
||||||
icon-name="copy"
|
icon-name="copy"
|
||||||
/>
|
/>
|
||||||
<ModalDialog v-if="instructions" ref="instructionsDialog">
|
<ModalDialog v-if="instructions" v-model="areInstructionsVisible">
|
||||||
<InstructionList :data="instructions" />
|
<InstructionList :data="instructions" />
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,7 +30,7 @@ import { defineComponent, ref, computed } from 'vue';
|
|||||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
||||||
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
||||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||||
import ModalDialog from '@/presentation/components/Shared/ModalDialog.vue';
|
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
||||||
import { Environment } from '@/application/Environment/Environment';
|
import { Environment } from '@/application/Environment/Environment';
|
||||||
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
@@ -57,7 +57,7 @@ export default defineComponent({
|
|||||||
currentState, currentContext, onStateChange, events,
|
currentState, currentContext, onStateChange, events,
|
||||||
} = useCollectionState();
|
} = useCollectionState();
|
||||||
|
|
||||||
const instructionsDialog = ref<typeof ModalDialog>();
|
const areInstructionsVisible = ref(false);
|
||||||
const canRun = computed<boolean>(() => getCanRunState(currentState.value.os));
|
const canRun = computed<boolean>(() => getCanRunState(currentState.value.os));
|
||||||
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
||||||
const hasCode = ref(false);
|
const hasCode = ref(false);
|
||||||
@@ -73,7 +73,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
function saveCode() {
|
function saveCode() {
|
||||||
saveCodeToDisk(fileName.value, currentState.value);
|
saveCodeToDisk(fileName.value, currentState.value);
|
||||||
instructionsDialog.value?.show();
|
areInstructionsVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeCode() {
|
async function executeCode() {
|
||||||
@@ -103,7 +103,7 @@ export default defineComponent({
|
|||||||
hasCode,
|
hasCode,
|
||||||
instructions,
|
instructions,
|
||||||
fileName,
|
fileName,
|
||||||
instructionsDialog,
|
areInstructionsVisible,
|
||||||
copyCode,
|
copyCode,
|
||||||
saveCode,
|
saveCode,
|
||||||
executeCode,
|
executeCode,
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Ref, computed, watch } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function monitors a set of conditions (represented as refs) and
|
||||||
|
* maintains a composite status based on all conditions.
|
||||||
|
*/
|
||||||
|
export function useAllTrueWatcher(
|
||||||
|
...conditions: Ref<boolean>[]
|
||||||
|
) {
|
||||||
|
const allMetCallbacks = new Array<() => void>();
|
||||||
|
|
||||||
|
const areAllConditionsMet = computed(() => conditions.every((condition) => condition.value));
|
||||||
|
|
||||||
|
watch(areAllConditionsMet, (areMet) => {
|
||||||
|
if (areMet) {
|
||||||
|
allMetCallbacks.forEach((action) => action());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetAllConditions() {
|
||||||
|
conditions.forEach((condition) => {
|
||||||
|
condition.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAllConditionsMet(callback: () => void) {
|
||||||
|
allMetCallbacks.push(callback);
|
||||||
|
if (areAllConditionsMet.value) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
resetAllConditions,
|
||||||
|
onAllConditionsMet,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Ref, watchEffect } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages focus transitions, ensuring good usability and accessibility.
|
||||||
|
*/
|
||||||
|
export function useCurrentFocusToggle(shouldDisableFocus: Ref<boolean>) {
|
||||||
|
let previouslyFocusedElement: HTMLElement | undefined;
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (shouldDisableFocus.value) {
|
||||||
|
previouslyFocusedElement = document.activeElement as HTMLElement | null;
|
||||||
|
previouslyFocusedElement?.blur();
|
||||||
|
} else {
|
||||||
|
if (!previouslyFocusedElement || previouslyFocusedElement.tagName === 'BODY') {
|
||||||
|
// It doesn't make sense to return focus to the body after the modal is
|
||||||
|
// closed because the body itself doesn't offer meaningful interactivity
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
previouslyFocusedElement.focus();
|
||||||
|
previouslyFocusedElement = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { onBeforeMount, onBeforeUnmount } from 'vue';
|
||||||
|
|
||||||
|
export function useEscapeKeyListener(callback: () => void) {
|
||||||
|
const onKeyUp = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
window.addEventListener('keyup', onKeyUp);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('keyup', onKeyUp);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Ref, watch, onBeforeUnmount } from 'vue';
|
||||||
|
|
||||||
|
/*
|
||||||
|
It blocks background scrolling.
|
||||||
|
Designed to be used by modals, overlays etc.
|
||||||
|
*/
|
||||||
|
export function useLockBodyBackgroundScroll(isActive: Ref<boolean>) {
|
||||||
|
const originalStyles = {
|
||||||
|
overflow: document.body.style.overflow,
|
||||||
|
width: document.body.style.width,
|
||||||
|
};
|
||||||
|
|
||||||
|
const block = () => {
|
||||||
|
originalStyles.overflow = document.body.style.overflow;
|
||||||
|
originalStyles.width = document.body.style.width;
|
||||||
|
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
document.body.style.width = '100vw';
|
||||||
|
};
|
||||||
|
|
||||||
|
const unblock = () => {
|
||||||
|
document.body.style.overflow = originalStyles.overflow;
|
||||||
|
document.body.style.width = originalStyles.width;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(isActive, (shouldBlock) => {
|
||||||
|
if (shouldBlock) {
|
||||||
|
block();
|
||||||
|
} else {
|
||||||
|
unblock();
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
unblock();
|
||||||
|
});
|
||||||
|
}
|
||||||
150
src/presentation/components/Shared/Modal/ModalContainer.vue
Normal file
150
src/presentation/components/Shared/Modal/ModalContainer.vue
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="isRendered"
|
||||||
|
class="modal-container"
|
||||||
|
>
|
||||||
|
<ModalOverlay
|
||||||
|
@transitionedOut="onOverlayTransitionedOut"
|
||||||
|
@click="onBackgroundOverlayClick"
|
||||||
|
:show="isOpen"
|
||||||
|
/>
|
||||||
|
<ModalContent
|
||||||
|
class="modal-content"
|
||||||
|
:show="isOpen"
|
||||||
|
@transitionedOut="onContentTransitionedOut"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</ModalContent>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
defineComponent, ref, watchEffect, nextTick,
|
||||||
|
} from 'vue';
|
||||||
|
import ModalOverlay from './ModalOverlay.vue';
|
||||||
|
import ModalContent from './ModalContent.vue';
|
||||||
|
import { useLockBodyBackgroundScroll } from './Hooks/UseLockBodyBackgroundScroll';
|
||||||
|
import { useCurrentFocusToggle } from './Hooks/UseCurrentFocusToggle';
|
||||||
|
import { useEscapeKeyListener } from './Hooks/UseEscapeKeyListener';
|
||||||
|
import { useAllTrueWatcher } from './Hooks/UseAllTrueWatcher';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
input: (isOpen: boolean) => true,
|
||||||
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
closeOnOutsideClick: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const isRendered = ref(false);
|
||||||
|
const isOpen = ref(false);
|
||||||
|
const overlayTransitionedOut = ref(false);
|
||||||
|
const contentTransitionedOut = ref(false);
|
||||||
|
|
||||||
|
useLockBodyBackgroundScroll(isOpen);
|
||||||
|
useCurrentFocusToggle(isOpen);
|
||||||
|
useEscapeKeyListener(() => handleEscapeKeyUp());
|
||||||
|
|
||||||
|
const {
|
||||||
|
onAllConditionsMet: onModalFullyTransitionedOut,
|
||||||
|
resetAllConditions: resetTransitionStatus,
|
||||||
|
} = useAllTrueWatcher(overlayTransitionedOut, contentTransitionedOut);
|
||||||
|
|
||||||
|
onModalFullyTransitionedOut(() => {
|
||||||
|
isRendered.value = false;
|
||||||
|
resetTransitionStatus();
|
||||||
|
if (props.value) {
|
||||||
|
emit('input', false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (props.value) {
|
||||||
|
open();
|
||||||
|
} else {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function onOverlayTransitionedOut() {
|
||||||
|
overlayTransitionedOut.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onContentTransitionedOut() {
|
||||||
|
contentTransitionedOut.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEscapeKeyUp() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
if (!isRendered.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isOpen.value = false;
|
||||||
|
|
||||||
|
if (props.value) {
|
||||||
|
emit('input', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
if (isRendered.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRendered.value = true;
|
||||||
|
|
||||||
|
nextTick(() => { // Let the modal render first
|
||||||
|
isOpen.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!props.value) {
|
||||||
|
emit('input', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBackgroundOverlayClick() {
|
||||||
|
if (props.closeOnOutsideClick) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isRendered,
|
||||||
|
onBackgroundOverlayClick,
|
||||||
|
onOverlayTransitionedOut,
|
||||||
|
onContentTransitionedOut,
|
||||||
|
isOpen,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.modal-container {
|
||||||
|
position: fixed;
|
||||||
|
box-sizing: border-box;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
95
src/presentation/components/Shared/Modal/ModalContent.vue
Normal file
95
src/presentation/components/Shared/Modal/ModalContent.vue
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<transition
|
||||||
|
name="modal-content-transition"
|
||||||
|
@after-leave="onAfterTransitionLeave"
|
||||||
|
>
|
||||||
|
<div v-if="show" class="modal-content-wrapper">
|
||||||
|
<div
|
||||||
|
ref="modalElement"
|
||||||
|
class="modal-content-content"
|
||||||
|
role="dialog"
|
||||||
|
aria-expanded="true"
|
||||||
|
aria-modal="true"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: [
|
||||||
|
'transitionedOut',
|
||||||
|
],
|
||||||
|
setup(_, { emit }) {
|
||||||
|
const modalElement = ref<HTMLElement>();
|
||||||
|
|
||||||
|
function onAfterTransitionLeave() {
|
||||||
|
emit('transitionedOut');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
onAfterTransitionLeave,
|
||||||
|
modalElement,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use "@/presentation/assets/styles/main" as *;
|
||||||
|
|
||||||
|
$modal-content-transition-duration: 400ms;
|
||||||
|
$modal-content-color-shadow: $color-on-surface;
|
||||||
|
$modal-content-color-background: $color-surface;
|
||||||
|
$modal-content-offset-upward: 20px;
|
||||||
|
|
||||||
|
@mixin scrollable() {
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content-content {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
max-width: 600px;
|
||||||
|
|
||||||
|
background-color: $color-surface;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 20px 60px -2px $color-on-surface;
|
||||||
|
|
||||||
|
@include scrollable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include fade-slide-transition('modal-content-transition', $modal-content-transition-duration, $modal-content-offset-upward);
|
||||||
|
</style>
|
||||||
87
src/presentation/components/Shared/Modal/ModalDialog.vue
Normal file
87
src/presentation/components/Shared/Modal/ModalDialog.vue
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<ModalContainer
|
||||||
|
v-model="showDialog"
|
||||||
|
>
|
||||||
|
<div class="dialog">
|
||||||
|
<div class="dialog__content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="dialog__close-button"
|
||||||
|
@click="hide"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'times']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, computed } from 'vue';
|
||||||
|
import ModalContainer from './ModalContainer.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
ModalContainer,
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
input: (isOpen: boolean) => true,
|
||||||
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const showDialog = computed({
|
||||||
|
get: () => props.value,
|
||||||
|
set: (value) => {
|
||||||
|
if (value !== props.value) {
|
||||||
|
emit('input', value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
showDialog.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
showDialog,
|
||||||
|
hide,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use "@/presentation/assets/styles/main" as *;
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
margin: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close-button {
|
||||||
|
color: $color-primary-dark;
|
||||||
|
width: auto;
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin-right: 0.25em;
|
||||||
|
align-self: flex-start;
|
||||||
|
@include clickable;
|
||||||
|
@include hover-or-touch {
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
65
src/presentation/components/Shared/Modal/ModalOverlay.vue
Normal file
65
src/presentation/components/Shared/Modal/ModalOverlay.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<transition
|
||||||
|
name="modal-overlay-transition"
|
||||||
|
@after-leave="onAfterTransitionLeave"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
class="modal-overlay-background"
|
||||||
|
aria-expanded="true"
|
||||||
|
@click.self.stop="onClick"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: [
|
||||||
|
'click',
|
||||||
|
'transitionedOut',
|
||||||
|
],
|
||||||
|
setup(_, { emit }) {
|
||||||
|
function onAfterTransitionLeave() {
|
||||||
|
emit('transitionedOut');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
emit('click');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
onAfterTransitionLeave,
|
||||||
|
onClick,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use "@/presentation/assets/styles/main" as *;
|
||||||
|
|
||||||
|
$modal-overlay-transition-duration: 50ms;
|
||||||
|
$modal-overlay-color-background: $color-on-surface;
|
||||||
|
|
||||||
|
.modal-overlay-background {
|
||||||
|
position: fixed;
|
||||||
|
box-sizing: border-box;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background: rgba($modal-overlay-color-background, 0.3);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include fade-slide-transition('modal-overlay-transition', $modal-overlay-transition-duration);
|
||||||
|
</style>
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
<template>
|
|
||||||
<modal
|
|
||||||
:name="name"
|
|
||||||
:adaptive="true"
|
|
||||||
height="auto">
|
|
||||||
<div class="dialog">
|
|
||||||
<div class="dialog__content">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
<div class="dialog__close-button">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'times']"
|
|
||||||
@click="hide"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, onMounted } from 'vue';
|
|
||||||
|
|
||||||
let idCounter = 0;
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
setup() {
|
|
||||||
const name = (++idCounter).toString();
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
let modal: any;
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
// Hack until Vue 3, so we can use vue-js-modal
|
|
||||||
const main = await import('@/presentation/main');
|
|
||||||
const { getVue } = main;
|
|
||||||
modal = getVue().$modal;
|
|
||||||
});
|
|
||||||
|
|
||||||
function show(): void {
|
|
||||||
modal.show(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide(): void {
|
|
||||||
modal.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
modal,
|
|
||||||
hide,
|
|
||||||
show,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@use "@/presentation/assets/styles/main" as *;
|
|
||||||
|
|
||||||
@mixin scrollable() {
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
color: $color-surface;
|
|
||||||
font-family: $font-normal;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
@include scrollable;
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
color: $color-on-surface;
|
|
||||||
width: 100%;
|
|
||||||
margin: 5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__close-button {
|
|
||||||
color: $color-primary-dark;
|
|
||||||
width: auto;
|
|
||||||
font-size: 1.5em;
|
|
||||||
margin-right: 0.25em;
|
|
||||||
align-self: flex-start;
|
|
||||||
@include clickable;
|
|
||||||
@include hover-or-touch {
|
|
||||||
color: $color-primary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -33,11 +33,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="footer__section__item">
|
<div class="footer__section__item">
|
||||||
<font-awesome-icon class="icon" :icon="['fas', 'user-secret']" />
|
<font-awesome-icon class="icon" :icon="['fas', 'user-secret']" />
|
||||||
<a @click="privacyDialog.show()">Privacy</a>
|
<a @click="showPrivacyDialog()">Privacy</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ModalDialog ref="privacyDialog">
|
<ModalDialog v-model="isPrivacyDialogVisible">
|
||||||
<PrivacyPolicy />
|
<PrivacyPolicy />
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref, computed } from 'vue';
|
import { defineComponent, ref, computed } from 'vue';
|
||||||
import { Environment } from '@/application/Environment/Environment';
|
import { Environment } from '@/application/Environment/Environment';
|
||||||
import ModalDialog from '@/presentation/components/Shared/ModalDialog.vue';
|
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
||||||
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
||||||
import DownloadUrlList from './DownloadUrlList.vue';
|
import DownloadUrlList from './DownloadUrlList.vue';
|
||||||
import PrivacyPolicy from './PrivacyPolicy.vue';
|
import PrivacyPolicy from './PrivacyPolicy.vue';
|
||||||
@@ -62,7 +62,7 @@ export default defineComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
const { info } = useApplication();
|
const { info } = useApplication();
|
||||||
|
|
||||||
const privacyDialog = ref<typeof ModalDialog>();
|
const isPrivacyDialogVisible = ref(false);
|
||||||
|
|
||||||
const version = computed<string>(() => info.version.toString());
|
const version = computed<string>(() => info.version.toString());
|
||||||
|
|
||||||
@@ -74,9 +74,14 @@ export default defineComponent({
|
|||||||
|
|
||||||
const feedbackUrl = computed<string>(() => info.feedbackUrl);
|
const feedbackUrl = computed<string>(() => info.feedbackUrl);
|
||||||
|
|
||||||
|
function showPrivacyDialog() {
|
||||||
|
isPrivacyDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDesktop,
|
isDesktop,
|
||||||
privacyDialog,
|
isPrivacyDialogVisible,
|
||||||
|
showPrivacyDialog,
|
||||||
version,
|
version,
|
||||||
homepageUrl,
|
homepageUrl,
|
||||||
repositoryUrl,
|
repositoryUrl,
|
||||||
|
|||||||
6
src/presentation/shims-vue.d.ts
vendored
6
src/presentation/shims-vue.d.ts
vendored
@@ -1,5 +1,7 @@
|
|||||||
import Vue from 'vue';
|
/* eslint-disable */
|
||||||
|
|
||||||
declare module '*.vue' {
|
declare module '*.vue' {
|
||||||
export default Vue;
|
import { DefineComponent } from 'vue';
|
||||||
|
const component: DefineComponent;
|
||||||
|
export default component;
|
||||||
}
|
}
|
||||||
|
|||||||
4
tests/bootstrap/setup.ts
Normal file
4
tests/bootstrap/setup.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { enableAutoDestroy } from '@vue/test-utils';
|
||||||
|
|
||||||
|
enableAutoDestroy(afterEach);
|
||||||
@@ -8,17 +8,18 @@ describe('NonCollapsingDirective', () => {
|
|||||||
describe('NonCollapsing', () => {
|
describe('NonCollapsing', () => {
|
||||||
it('adds expected attribute to the element when inserted', () => {
|
it('adds expected attribute to the element when inserted', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const element = getElementMock();
|
const element = createElementMock();
|
||||||
// act
|
// act
|
||||||
NonCollapsing.inserted(element, undefined, undefined, undefined);
|
NonCollapsing.inserted(element, undefined, undefined, undefined);
|
||||||
// assert
|
// assert
|
||||||
expect(element.hasAttribute(expectedAttributeName));
|
expect(element.hasAttribute(expectedAttributeName));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('hasDirective', () => {
|
describe('hasDirective', () => {
|
||||||
it('returns true if the element has expected attribute', () => {
|
it('returns true if the element has expected attribute', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const element = getElementMock();
|
const element = createElementMock();
|
||||||
element.setAttribute(expectedAttributeName, undefined);
|
element.setAttribute(expectedAttributeName, undefined);
|
||||||
// act
|
// act
|
||||||
const actual = hasDirective(element);
|
const actual = hasDirective(element);
|
||||||
@@ -27,8 +28,8 @@ describe('NonCollapsingDirective', () => {
|
|||||||
});
|
});
|
||||||
it('returns true if the element has a parent with expected attribute', () => {
|
it('returns true if the element has a parent with expected attribute', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const parent = getElementMock();
|
const parent = createElementMock();
|
||||||
const element = getElementMock();
|
const element = createElementMock();
|
||||||
parent.appendChild(element);
|
parent.appendChild(element);
|
||||||
element.setAttribute(expectedAttributeName, undefined);
|
element.setAttribute(expectedAttributeName, undefined);
|
||||||
// act
|
// act
|
||||||
@@ -38,16 +39,15 @@ describe('NonCollapsingDirective', () => {
|
|||||||
});
|
});
|
||||||
it('returns false if nor the element or its parent has expected attribute', () => {
|
it('returns false if nor the element or its parent has expected attribute', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const element = getElementMock();
|
const element = createElementMock();
|
||||||
// act
|
// act
|
||||||
const actual = hasDirective(element);
|
const actual = hasDirective(element);
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.equal(false);
|
expect(actual).to.equal(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
function getElementMock(): HTMLElement {
|
function createElementMock(): HTMLElement {
|
||||||
const element = document.createElement('div');
|
return document.createElement('div');
|
||||||
return element;
|
}
|
||||||
}
|
});
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { ref, nextTick } from 'vue';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { useAllTrueWatcher } from '@/presentation/components/Shared/Modal/Hooks/UseAllTrueWatcher';
|
||||||
|
|
||||||
|
describe('useAllTrueWatcher', () => {
|
||||||
|
describe('onAllConditionsMet', () => {
|
||||||
|
it('triggers the callback when all conditions turn true', async () => {
|
||||||
|
// arrange
|
||||||
|
const condition1 = ref(false);
|
||||||
|
const condition2 = ref(false);
|
||||||
|
const { onAllConditionsMet } = useAllTrueWatcher(condition1, condition2);
|
||||||
|
|
||||||
|
let callbackCalled = false;
|
||||||
|
|
||||||
|
// act
|
||||||
|
onAllConditionsMet(() => {
|
||||||
|
callbackCalled = true;
|
||||||
|
});
|
||||||
|
condition1.value = true;
|
||||||
|
condition2.value = true;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(callbackCalled).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('instantly triggers the callback if conditions are true on callback registration', async () => {
|
||||||
|
// arrange
|
||||||
|
const condition1 = ref(true);
|
||||||
|
const condition2 = ref(true);
|
||||||
|
const { onAllConditionsMet } = useAllTrueWatcher(condition1, condition2);
|
||||||
|
|
||||||
|
let callbackCalled = false;
|
||||||
|
|
||||||
|
// act
|
||||||
|
onAllConditionsMet(() => {
|
||||||
|
callbackCalled = true;
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(callbackCalled).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not trigger the callback unless all conditions are met', async () => {
|
||||||
|
// arrange
|
||||||
|
const condition1 = ref(true);
|
||||||
|
const condition2 = ref(false);
|
||||||
|
const { onAllConditionsMet } = useAllTrueWatcher(condition1, condition2);
|
||||||
|
|
||||||
|
let callbackCalled = false;
|
||||||
|
|
||||||
|
// act
|
||||||
|
onAllConditionsMet(() => {
|
||||||
|
callbackCalled = true;
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(callbackCalled).to.be.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers all registered callbacks once all conditions are satisfied', async () => {
|
||||||
|
// arrange
|
||||||
|
const condition1 = ref(false);
|
||||||
|
const condition2 = ref(false);
|
||||||
|
const { onAllConditionsMet } = useAllTrueWatcher(condition1, condition2);
|
||||||
|
|
||||||
|
let callbackCount = 0;
|
||||||
|
|
||||||
|
// act
|
||||||
|
onAllConditionsMet(() => callbackCount++);
|
||||||
|
onAllConditionsMet(() => callbackCount++);
|
||||||
|
condition1.value = true;
|
||||||
|
condition2.value = true;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(callbackCount).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ensures each callback is invoked only once for a single condition set', async () => {
|
||||||
|
// arrange
|
||||||
|
const condition1 = ref(false);
|
||||||
|
const condition2 = ref(false);
|
||||||
|
const { onAllConditionsMet } = useAllTrueWatcher(condition1, condition2);
|
||||||
|
|
||||||
|
let callbackCount = 0;
|
||||||
|
|
||||||
|
// act
|
||||||
|
onAllConditionsMet(() => callbackCount++);
|
||||||
|
condition1.value = true;
|
||||||
|
condition2.value = true;
|
||||||
|
condition1.value = false;
|
||||||
|
condition1.value = true;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(callbackCount).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers the callback after conditions are sequentially met post-reset', async () => {
|
||||||
|
// arrange
|
||||||
|
const condition1 = ref(false);
|
||||||
|
const condition2 = ref(false);
|
||||||
|
const { onAllConditionsMet, resetAllConditions } = useAllTrueWatcher(condition1, condition2);
|
||||||
|
|
||||||
|
let callbackCalled = false;
|
||||||
|
|
||||||
|
// act
|
||||||
|
onAllConditionsMet(() => {
|
||||||
|
callbackCalled = true;
|
||||||
|
});
|
||||||
|
condition1.value = true;
|
||||||
|
resetAllConditions();
|
||||||
|
condition1.value = true;
|
||||||
|
condition2.value = true;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(callbackCalled).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('avoids triggering the callback for single condition post-reset', async () => {
|
||||||
|
// arrange
|
||||||
|
const condition1 = ref(false);
|
||||||
|
const condition2 = ref(false);
|
||||||
|
const { onAllConditionsMet, resetAllConditions } = useAllTrueWatcher(condition1, condition2);
|
||||||
|
|
||||||
|
let callbackCalled = false;
|
||||||
|
|
||||||
|
// act
|
||||||
|
condition1.value = true;
|
||||||
|
condition2.value = true;
|
||||||
|
resetAllConditions();
|
||||||
|
onAllConditionsMet(() => {
|
||||||
|
callbackCalled = true;
|
||||||
|
});
|
||||||
|
condition1.value = true;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(callbackCalled).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resetAllConditions', () => {
|
||||||
|
it('returns all conditions to their default false state', async () => {
|
||||||
|
// arrange
|
||||||
|
const condition1 = ref(true);
|
||||||
|
const condition2 = ref(true);
|
||||||
|
const { resetAllConditions } = useAllTrueWatcher(condition1, condition2);
|
||||||
|
|
||||||
|
// act
|
||||||
|
resetAllConditions();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(condition1.value).to.be.equal(false);
|
||||||
|
expect(condition2.value).to.be.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { ref, nextTick } from 'vue';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { useCurrentFocusToggle } from '@/presentation/components/Shared/Modal/Hooks/UseCurrentFocusToggle';
|
||||||
|
|
||||||
|
describe('useCurrentFocusToggle', () => {
|
||||||
|
describe('initialization', () => {
|
||||||
|
it('blurs active element when initialized with disabled focus', async () => {
|
||||||
|
// arrange
|
||||||
|
const shouldDisableFocus = ref(true);
|
||||||
|
const testElement = createElementInBody('input');
|
||||||
|
testElement.focus();
|
||||||
|
|
||||||
|
// act
|
||||||
|
useCurrentFocusToggle(shouldDisableFocus);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(!isFocused(testElement));
|
||||||
|
});
|
||||||
|
it('doesn\'t blur active element when initialized with enabled focus', async () => {
|
||||||
|
// arrange
|
||||||
|
const isCurrentFocusDisabled = ref(false);
|
||||||
|
const testElement = createElementInBody('input');
|
||||||
|
|
||||||
|
// act
|
||||||
|
testElement.focus();
|
||||||
|
useCurrentFocusToggle(isCurrentFocusDisabled);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(isFocused(testElement));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('focus toggling', () => {
|
||||||
|
it('blurs when focus disabled programmatically', async () => {
|
||||||
|
// arrange
|
||||||
|
const shouldDisableFocus = ref(false);
|
||||||
|
const testElement = createElementInBody('input');
|
||||||
|
testElement.focus();
|
||||||
|
|
||||||
|
// act
|
||||||
|
useCurrentFocusToggle(shouldDisableFocus);
|
||||||
|
shouldDisableFocus.value = true;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(!isFocused(testElement));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores focus when re-enabled', async () => {
|
||||||
|
// arrange
|
||||||
|
const isCurrentFocusDisabled = ref(true);
|
||||||
|
const testElement = createElementInBody('input');
|
||||||
|
|
||||||
|
// act
|
||||||
|
useCurrentFocusToggle(isCurrentFocusDisabled);
|
||||||
|
testElement.focus();
|
||||||
|
isCurrentFocusDisabled.value = true;
|
||||||
|
await nextTick();
|
||||||
|
isCurrentFocusDisabled.value = false;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(isFocused(testElement));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maintains focus if not disabled', async () => {
|
||||||
|
// arrange
|
||||||
|
const isCurrentFocusDisabled = ref(false);
|
||||||
|
const testElement = createElementInBody('input');
|
||||||
|
|
||||||
|
// act
|
||||||
|
testElement.focus();
|
||||||
|
useCurrentFocusToggle(isCurrentFocusDisabled);
|
||||||
|
isCurrentFocusDisabled.value = false;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(isFocused(testElement));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles multiple toggles correctly', async () => {
|
||||||
|
// arrange
|
||||||
|
const shouldDisableFocus = ref(false);
|
||||||
|
const testElement = createElementInBody('input');
|
||||||
|
testElement.focus();
|
||||||
|
|
||||||
|
// act
|
||||||
|
useCurrentFocusToggle(shouldDisableFocus);
|
||||||
|
shouldDisableFocus.value = true;
|
||||||
|
await nextTick();
|
||||||
|
shouldDisableFocus.value = false;
|
||||||
|
await nextTick();
|
||||||
|
shouldDisableFocus.value = true;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(!isFocused(testElement));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('document.body handling', () => {
|
||||||
|
it('blurs body when focus is disabled while body is active', async () => {
|
||||||
|
// arrange
|
||||||
|
document.body.focus();
|
||||||
|
|
||||||
|
const shouldDisableFocus = ref(false);
|
||||||
|
|
||||||
|
// act
|
||||||
|
useCurrentFocusToggle(shouldDisableFocus);
|
||||||
|
shouldDisableFocus.value = true;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(!isFocused(document.body));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('doesn\'t restore focus to document body once focus is re-enabled', async () => {
|
||||||
|
// arrange
|
||||||
|
document.body.focus();
|
||||||
|
|
||||||
|
const shouldDisableFocus = ref(false);
|
||||||
|
|
||||||
|
// act
|
||||||
|
useCurrentFocusToggle(shouldDisableFocus);
|
||||||
|
shouldDisableFocus.value = true;
|
||||||
|
await nextTick();
|
||||||
|
shouldDisableFocus.value = false;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(!isFocused(document.body));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles removal of a previously focused element gracefully', async () => {
|
||||||
|
// arrange
|
||||||
|
const shouldDisableFocus = ref(true);
|
||||||
|
const testElement = createElementInBody('input');
|
||||||
|
testElement.focus();
|
||||||
|
|
||||||
|
useCurrentFocusToggle(shouldDisableFocus);
|
||||||
|
shouldDisableFocus.value = true;
|
||||||
|
await nextTick();
|
||||||
|
testElement.remove();
|
||||||
|
shouldDisableFocus.value = false;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(!isFocused(testElement));
|
||||||
|
});
|
||||||
|
|
||||||
|
function createElementInBody(tagName: keyof HTMLElementTagNameMap): HTMLElement {
|
||||||
|
const element = document.createElement(tagName);
|
||||||
|
document.body.appendChild(element);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function isFocused(element: HTMLElement): boolean {
|
||||||
|
return document.activeElement === element;
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { nextTick, defineComponent } from 'vue';
|
||||||
|
import { useEscapeKeyListener } from '@/presentation/components/Shared/Modal/Hooks/UseEscapeKeyListener';
|
||||||
|
|
||||||
|
describe('useEscapeKeyListener', () => {
|
||||||
|
it('executes the callback when the Escape key is pressed', async () => {
|
||||||
|
// arrange
|
||||||
|
let callbackCalled = false;
|
||||||
|
const callback = () => {
|
||||||
|
callbackCalled = true;
|
||||||
|
};
|
||||||
|
createComponent(callback);
|
||||||
|
|
||||||
|
// act
|
||||||
|
const event = new KeyboardEvent('keyup', { key: 'Escape' });
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(callbackCalled).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not execute the callback for other key presses', async () => {
|
||||||
|
// arrange
|
||||||
|
let callbackCalled = false;
|
||||||
|
const callback = () => {
|
||||||
|
callbackCalled = true;
|
||||||
|
};
|
||||||
|
createComponent(callback);
|
||||||
|
|
||||||
|
// act
|
||||||
|
const event = new KeyboardEvent('keyup', { key: 'Enter' });
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(callbackCalled).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds an event listener on component mount', () => {
|
||||||
|
// arrange
|
||||||
|
const { restore, isAddEventCalled } = createWindowEventSpies();
|
||||||
|
|
||||||
|
// act
|
||||||
|
createComponent();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(isAddEventCalled()).to.equal(true);
|
||||||
|
restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes the event listener on component unmount', async () => {
|
||||||
|
// arrange
|
||||||
|
const { restore, isRemoveEventCalled } = createWindowEventSpies();
|
||||||
|
|
||||||
|
// act
|
||||||
|
const wrapper = createComponent();
|
||||||
|
wrapper.destroy();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(isRemoveEventCalled()).to.equal(true);
|
||||||
|
restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createComponent(callback = () => {}) {
|
||||||
|
return shallowMount(defineComponent({
|
||||||
|
setup() {
|
||||||
|
useEscapeKeyListener(callback);
|
||||||
|
},
|
||||||
|
template: '<div></div>',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWindowEventSpies() {
|
||||||
|
let addEventCalled = false;
|
||||||
|
let removeEventCalled = false;
|
||||||
|
|
||||||
|
const originalAddEventListener = window.addEventListener;
|
||||||
|
const originalRemoveEventListener = window.removeEventListener;
|
||||||
|
|
||||||
|
window.addEventListener = (
|
||||||
|
type: string,
|
||||||
|
listener: EventListenerOrEventListenerObject,
|
||||||
|
options?: boolean | AddEventListenerOptions,
|
||||||
|
): void => {
|
||||||
|
if (type === 'keyup' && typeof listener === 'function') {
|
||||||
|
addEventCalled = true;
|
||||||
|
}
|
||||||
|
originalAddEventListener(type, listener, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.removeEventListener = (
|
||||||
|
type: string,
|
||||||
|
listener: EventListenerOrEventListenerObject,
|
||||||
|
options?: boolean | EventListenerOptions,
|
||||||
|
): void => {
|
||||||
|
if (type === 'keyup' && typeof listener === 'function') {
|
||||||
|
removeEventCalled = true;
|
||||||
|
}
|
||||||
|
originalRemoveEventListener(type, listener, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
restore: () => {
|
||||||
|
window.addEventListener = originalAddEventListener;
|
||||||
|
window.removeEventListener = originalRemoveEventListener;
|
||||||
|
},
|
||||||
|
isAddEventCalled() {
|
||||||
|
return addEventCalled;
|
||||||
|
},
|
||||||
|
isRemoveEventCalled() {
|
||||||
|
return removeEventCalled;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { ref, nextTick, defineComponent } from 'vue';
|
||||||
|
import { useLockBodyBackgroundScroll } from '@/presentation/components/Shared/Modal/Hooks/UseLockBodyBackgroundScroll';
|
||||||
|
|
||||||
|
describe('useLockBodyBackgroundScroll', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
document.body.style.width = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initialization', () => {
|
||||||
|
it('blocks scroll if initially active', async () => {
|
||||||
|
// arrange
|
||||||
|
createComponent(true);
|
||||||
|
|
||||||
|
// act
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(document.body.style.overflow).to.equal('hidden');
|
||||||
|
expect(document.body.style.width).to.equal('100vw');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves initial styles if inactive', async () => {
|
||||||
|
// arrange
|
||||||
|
const originalOverflow = 'scroll';
|
||||||
|
const originalWidth = '90vw';
|
||||||
|
document.body.style.overflow = originalOverflow;
|
||||||
|
document.body.style.width = originalWidth;
|
||||||
|
|
||||||
|
// act
|
||||||
|
createComponent(false);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(document.body.style.overflow).to.equal(originalOverflow);
|
||||||
|
expect(document.body.style.width).to.equal(originalWidth);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toggling', () => {
|
||||||
|
it('blocks scroll when activated', async () => {
|
||||||
|
// arrange
|
||||||
|
const { isActive } = createComponent(false);
|
||||||
|
|
||||||
|
// act
|
||||||
|
isActive.value = true;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(document.body.style.overflow).to.equal('hidden');
|
||||||
|
expect(document.body.style.width).to.equal('100vw');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unblocks scroll when deactivated', async () => {
|
||||||
|
// arrange
|
||||||
|
const { isActive } = createComponent(true);
|
||||||
|
|
||||||
|
// act
|
||||||
|
isActive.value = false;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(document.body.style.overflow).not.to.equal('hidden');
|
||||||
|
expect(document.body.style.width).not.to.equal('100vw');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unmounting', () => {
|
||||||
|
it('restores original styles on unmount', async () => {
|
||||||
|
// arrange
|
||||||
|
const originalOverflow = 'scroll';
|
||||||
|
const originalWidth = '90vw';
|
||||||
|
document.body.style.overflow = originalOverflow;
|
||||||
|
document.body.style.width = originalWidth;
|
||||||
|
|
||||||
|
// act
|
||||||
|
const { component } = createComponent(true);
|
||||||
|
component.destroy();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(document.body.style.overflow).to.equal(originalOverflow);
|
||||||
|
expect(document.body.style.width).to.equal(originalWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets styles on unmount', async () => {
|
||||||
|
// arrange
|
||||||
|
const { component } = createComponent(true);
|
||||||
|
|
||||||
|
// act
|
||||||
|
component.destroy();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(document.body.style.overflow).to.equal('');
|
||||||
|
expect(document.body.style.width).to.equal('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createComponent(initialIsActiveValue: boolean) {
|
||||||
|
const isActive = ref(initialIsActiveValue);
|
||||||
|
const component = shallowMount(defineComponent({
|
||||||
|
setup() {
|
||||||
|
useLockBodyBackgroundScroll(isActive);
|
||||||
|
},
|
||||||
|
template: '<div></div>',
|
||||||
|
}));
|
||||||
|
return { component, isActive };
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import ModalContainer from '@/presentation/components/Shared/Modal/ModalContainer.vue';
|
||||||
|
|
||||||
|
const DOM_MODAL_CONTAINER_SELECTOR = '.modal-container';
|
||||||
|
const COMPONENT_MODAL_OVERLAY_NAME = 'ModalOverlay';
|
||||||
|
const COMPONENT_MODAL_CONTENT_NAME = 'ModalContent';
|
||||||
|
|
||||||
|
describe('ModalContainer.vue', () => {
|
||||||
|
describe('rendering based on model prop', () => {
|
||||||
|
it('does not render when model prop is absent or false', () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ modelValue: false });
|
||||||
|
|
||||||
|
// act
|
||||||
|
const modalContainer = wrapper.find(DOM_MODAL_CONTAINER_SELECTOR);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(modalContainer.exists()).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders modal container when model prop is true', () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ modelValue: true });
|
||||||
|
|
||||||
|
// act
|
||||||
|
const modalContainer = wrapper.find(DOM_MODAL_CONTAINER_SELECTOR);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(modalContainer.exists()).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('modal open/close', () => {
|
||||||
|
it('opens when model prop changes from false to true', async () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ modelValue: false });
|
||||||
|
|
||||||
|
// act
|
||||||
|
await wrapper.setProps({ value: true });
|
||||||
|
|
||||||
|
// assert after updating props
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
expect((wrapper.vm as any).isRendered).to.equal(true);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
expect((wrapper.vm as any).isOpen).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes when model prop changes from true to false', async () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ modelValue: true });
|
||||||
|
|
||||||
|
// act
|
||||||
|
await wrapper.setProps({ value: false });
|
||||||
|
|
||||||
|
// assert after updating props
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
expect((wrapper.vm as any).isOpen).to.equal(false);
|
||||||
|
// isRendered will not be true directly due to transition
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes on pressing ESC key', async () => {
|
||||||
|
// arrange
|
||||||
|
const { triggerKeyUp, restore } = createWindowEventSpies();
|
||||||
|
const wrapper = mountComponent({ modelValue: true });
|
||||||
|
|
||||||
|
// act
|
||||||
|
const escapeEvent = new KeyboardEvent('keyup', { key: 'Escape' });
|
||||||
|
triggerKeyUp(escapeEvent);
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(wrapper.emitted().input[0]).to.deep.equal([false]);
|
||||||
|
restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emit false value after overlay and content transitions out and model prop is true', async () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ modelValue: true });
|
||||||
|
const overlayMock = wrapper.findComponent({ name: COMPONENT_MODAL_OVERLAY_NAME });
|
||||||
|
const contentMock = wrapper.findComponent({ name: COMPONENT_MODAL_CONTENT_NAME });
|
||||||
|
|
||||||
|
// act
|
||||||
|
overlayMock.vm.$emit('transitionedOut');
|
||||||
|
contentMock.vm.$emit('transitionedOut');
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(wrapper.emitted().input[0]).to.deep.equal([false]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders provided slot content', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedText = 'Slot content';
|
||||||
|
const slotContentClass = 'slot-content';
|
||||||
|
|
||||||
|
// act
|
||||||
|
const wrapper = mountComponent({
|
||||||
|
modelValue: true,
|
||||||
|
slotHtml: `<div class="${slotContentClass}">${expectedText}</div>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// assert
|
||||||
|
const slotWrapper = wrapper.find(`.${slotContentClass}`);
|
||||||
|
const slotText = slotWrapper.text();
|
||||||
|
expect(slotText).to.equal(expectedText);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('closeOnOutsideClick', () => {
|
||||||
|
it('does not close on overlay click if prop is false', async () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ modelValue: true, closeOnOutsideClick: false });
|
||||||
|
|
||||||
|
// act
|
||||||
|
const overlayMock = wrapper.findComponent({ name: COMPONENT_MODAL_OVERLAY_NAME });
|
||||||
|
overlayMock.vm.$emit('click');
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(wrapper.emitted().input).to.equal(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes on overlay click if prop is true', async () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ modelValue: true, closeOnOutsideClick: true });
|
||||||
|
|
||||||
|
// act
|
||||||
|
const overlayMock = wrapper.findComponent({ name: COMPONENT_MODAL_OVERLAY_NAME });
|
||||||
|
overlayMock.vm.$emit('click');
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(wrapper.emitted().input[0]).to.deep.equal([false]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function mountComponent(options: {
|
||||||
|
readonly modelValue: boolean,
|
||||||
|
readonly closeOnOutsideClick?: boolean,
|
||||||
|
readonly slotHtml?: string,
|
||||||
|
readonly attachToDocument?: boolean,
|
||||||
|
}) {
|
||||||
|
return shallowMount(ModalContainer as unknown, {
|
||||||
|
propsData: {
|
||||||
|
value: options.modelValue,
|
||||||
|
...(options.closeOnOutsideClick !== undefined ? {
|
||||||
|
closeOnOutsideClick: options.closeOnOutsideClick,
|
||||||
|
} : {}),
|
||||||
|
},
|
||||||
|
slots: options.slotHtml !== undefined ? { default: options.slotHtml } : undefined,
|
||||||
|
stubs: {
|
||||||
|
[COMPONENT_MODAL_OVERLAY_NAME]: {
|
||||||
|
name: COMPONENT_MODAL_OVERLAY_NAME,
|
||||||
|
template: '<div />',
|
||||||
|
},
|
||||||
|
[COMPONENT_MODAL_CONTENT_NAME]: {
|
||||||
|
name: COMPONENT_MODAL_CONTENT_NAME,
|
||||||
|
template: '<slot />',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWindowEventSpies() {
|
||||||
|
const originalAddEventListener = window.addEventListener;
|
||||||
|
const originalRemoveEventListener = window.removeEventListener;
|
||||||
|
|
||||||
|
let savedListener: EventListenerOrEventListenerObject | null = null;
|
||||||
|
|
||||||
|
window.addEventListener = (
|
||||||
|
type: string,
|
||||||
|
listener: EventListenerOrEventListenerObject,
|
||||||
|
options?: boolean | AddEventListenerOptions,
|
||||||
|
): void => {
|
||||||
|
if (type === 'keyup' && typeof listener === 'function') {
|
||||||
|
savedListener = listener;
|
||||||
|
}
|
||||||
|
originalAddEventListener.call(window, type, listener, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.removeEventListener = (
|
||||||
|
type: string,
|
||||||
|
listener: EventListenerOrEventListenerObject,
|
||||||
|
options?: boolean | EventListenerOptions,
|
||||||
|
): void => {
|
||||||
|
if (type === 'keyup' && typeof listener === 'function') {
|
||||||
|
savedListener = null;
|
||||||
|
}
|
||||||
|
originalRemoveEventListener.call(window, type, listener, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
triggerKeyUp: (event: KeyboardEvent) => {
|
||||||
|
if (savedListener) {
|
||||||
|
(savedListener as EventListener)(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
restore: () => {
|
||||||
|
window.addEventListener = originalAddEventListener;
|
||||||
|
window.removeEventListener = originalRemoveEventListener;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import ModalContent from '@/presentation/components/Shared/Modal/ModalContent.vue';
|
||||||
|
|
||||||
|
const DOM_MODAL_CONTENT_SELECTOR = '.modal-content-content';
|
||||||
|
const DOM_MODAL_CONTENT_WRAPPER_SELECTOR = '.modal-content-wrapper';
|
||||||
|
|
||||||
|
describe('ModalContent.vue', () => {
|
||||||
|
describe('rendering based on `show` prop', () => {
|
||||||
|
it('renders modal content when `show` prop is true', () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ showProperty: true });
|
||||||
|
|
||||||
|
// act
|
||||||
|
const modalContentWrapper = wrapper.find(DOM_MODAL_CONTENT_WRAPPER_SELECTOR);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(modalContentWrapper.exists()).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render modal content when `show` prop is false', () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ showProperty: false });
|
||||||
|
|
||||||
|
// act
|
||||||
|
const modalContentWrapper = wrapper.find(DOM_MODAL_CONTENT_WRAPPER_SELECTOR);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(modalContentWrapper.exists()).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render modal content by default', () => {
|
||||||
|
// arrange & act
|
||||||
|
const wrapper = mountComponent();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
const modalContentWrapper = wrapper.find(DOM_MODAL_CONTENT_WRAPPER_SELECTOR);
|
||||||
|
expect(modalContentWrapper.exists()).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders slot content when provided', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedText = 'Slot content';
|
||||||
|
const slotContentClass = 'slot-content';
|
||||||
|
|
||||||
|
// act
|
||||||
|
const wrapper = mountComponent({
|
||||||
|
showProperty: true,
|
||||||
|
slotHtml: `<div class="${slotContentClass}">${expectedText}</div>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// assert
|
||||||
|
const slotWrapper = wrapper.find(`.${slotContentClass}`);
|
||||||
|
const slotText = slotWrapper.text();
|
||||||
|
expect(slotText).to.equal(expectedText);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('aria attributes', () => {
|
||||||
|
it('sets aria-expanded to `true` when visible', () => {
|
||||||
|
// arrange & act
|
||||||
|
const wrapper = mountComponent({ showProperty: true });
|
||||||
|
|
||||||
|
// assert
|
||||||
|
const modalContent = wrapper.find(DOM_MODAL_CONTENT_SELECTOR);
|
||||||
|
expect(modalContent.attributes('aria-expanded')).to.equal('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('always sets aria-modal to true for the modal content', () => {
|
||||||
|
// arrange & act
|
||||||
|
const wrapper = mountComponent({ showProperty: true });
|
||||||
|
|
||||||
|
// assert
|
||||||
|
const modalContent = wrapper.find(DOM_MODAL_CONTENT_SELECTOR);
|
||||||
|
expect(modalContent.attributes('aria-modal')).to.equal('true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits `transitionedOut` event after the transition leave', async () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ showProperty: true });
|
||||||
|
|
||||||
|
// act
|
||||||
|
await wrapper.vm.$nextTick(); // Ensure the component reflects initial prop
|
||||||
|
wrapper.setProps({ show: false }); // Trigger the transition
|
||||||
|
await wrapper.vm.$nextTick(); // Allow the component to update
|
||||||
|
const transitionWrapper = wrapper.findComponent({ name: 'transition' });
|
||||||
|
transitionWrapper.vm.$emit('after-leave'); // Simulate the after-leave lifecycle hook of the transition
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(wrapper.emitted().transitionedOut).to.have.length(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function mountComponent(options?: {
|
||||||
|
readonly showProperty?: boolean,
|
||||||
|
readonly slotHtml?: string,
|
||||||
|
}) {
|
||||||
|
return shallowMount(ModalContent as unknown, {
|
||||||
|
propsData: options?.showProperty !== undefined ? { show: options?.showProperty } : undefined,
|
||||||
|
slots: options?.slotHtml !== undefined ? { default: options?.slotHtml } : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { shallowMount, mount } from '@vue/test-utils';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
||||||
|
import ModalContainer from '@/presentation/components/Shared/Modal/ModalContainer.vue';
|
||||||
|
|
||||||
|
const DOM_CLOSE_BUTTON_SELECTOR = '.dialog__close-button';
|
||||||
|
const MODAL_CONTAINER_COMPONENT_NAME = 'ModalContainer';
|
||||||
|
|
||||||
|
describe('ModalDialog.vue', () => {
|
||||||
|
it(`renders ${MODAL_CONTAINER_COMPONENT_NAME}`, () => {
|
||||||
|
// arrange & act
|
||||||
|
const wrapper = mountComponent();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
const modalContainerWrapper = wrapper.findComponent({ name: MODAL_CONTAINER_COMPONENT_NAME });
|
||||||
|
expect(modalContainerWrapper.exists()).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`binds the visibility flag ${MODAL_CONTAINER_COMPONENT_NAME}`, () => {
|
||||||
|
it('given true', () => {
|
||||||
|
// arrange & act
|
||||||
|
const wrapper = mountComponent({ modelValue: true, deepMount: true });
|
||||||
|
|
||||||
|
// assert
|
||||||
|
const modalContainerWrapper = wrapper.findComponent(ModalContainer);
|
||||||
|
expect(modalContainerWrapper.props('value')).to.equal(true);
|
||||||
|
});
|
||||||
|
it('given false', () => {
|
||||||
|
// arrange & act
|
||||||
|
const wrapper = mountComponent({ modelValue: false, deepMount: true });
|
||||||
|
|
||||||
|
// assert
|
||||||
|
const modalContainerWrapper = wrapper.findComponent(ModalContainer);
|
||||||
|
expect(modalContainerWrapper.props('value')).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('close button', () => {
|
||||||
|
it('renders the close button', async () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ modelValue: true });
|
||||||
|
|
||||||
|
// act
|
||||||
|
const closeButton = wrapper.find(DOM_CLOSE_BUTTON_SELECTOR);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(closeButton.exists()).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes the modal when close button is clicked', async () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ modelValue: true });
|
||||||
|
|
||||||
|
// act
|
||||||
|
const closeButton = wrapper.find(DOM_CLOSE_BUTTON_SELECTOR);
|
||||||
|
await closeButton.trigger('click');
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(wrapper.emitted().input[0]).to.deep.equal([false]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function mountComponent(options?: {
|
||||||
|
readonly modelValue?: boolean,
|
||||||
|
readonly slotHtml?: string,
|
||||||
|
readonly deepMount?: boolean,
|
||||||
|
}) {
|
||||||
|
const mountFunction = options?.deepMount === true ? mount : shallowMount;
|
||||||
|
const wrapper = mountFunction(ModalDialog as unknown, {
|
||||||
|
propsData: options?.modelValue !== undefined ? { value: options?.modelValue } : undefined,
|
||||||
|
slots: options?.slotHtml !== undefined ? { default: options?.slotHtml } : undefined,
|
||||||
|
stubs: options?.deepMount === true ? undefined : {
|
||||||
|
[MODAL_CONTAINER_COMPONENT_NAME]: {
|
||||||
|
name: MODAL_CONTAINER_COMPONENT_NAME,
|
||||||
|
template: '<slot />',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import ModalOverlay from '@/presentation/components/Shared/Modal/ModalOverlay.vue';
|
||||||
|
|
||||||
|
const DOM_MODAL_OVERLAY_BACKGROUND_SELECTOR = '.modal-overlay-background';
|
||||||
|
|
||||||
|
describe('ModalOverlay.vue', () => {
|
||||||
|
describe('show', () => {
|
||||||
|
it('renders when prop is true', () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ showProperty: true });
|
||||||
|
|
||||||
|
// act
|
||||||
|
const modalOverlayBackground = wrapper.find(DOM_MODAL_OVERLAY_BACKGROUND_SELECTOR);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(modalOverlayBackground.exists()).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render prop is false', () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ showProperty: false });
|
||||||
|
|
||||||
|
// act
|
||||||
|
const modalOverlayBackground = wrapper.find(DOM_MODAL_OVERLAY_BACKGROUND_SELECTOR);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(modalOverlayBackground.exists()).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets aria-expanded to `true` prop is true', () => {
|
||||||
|
// arrange & act
|
||||||
|
const wrapper = mountComponent({ showProperty: true });
|
||||||
|
|
||||||
|
// assert
|
||||||
|
const modalOverlayBackground = wrapper.find(DOM_MODAL_OVERLAY_BACKGROUND_SELECTOR);
|
||||||
|
expect(modalOverlayBackground.attributes('aria-expanded')).to.equal('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on modification', () => {
|
||||||
|
it('does not render when initially visible then turned invisible', async () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ showProperty: true });
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// act
|
||||||
|
wrapper.setProps({ show: false });
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
const modalOverlayBackground = wrapper.find(DOM_MODAL_OVERLAY_BACKGROUND_SELECTOR);
|
||||||
|
expect(modalOverlayBackground.exists()).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders when initially invisible then turned visible', async () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ showProperty: false });
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// act
|
||||||
|
wrapper.setProps({ show: true });
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
const modalOverlayBackground = wrapper.find(DOM_MODAL_OVERLAY_BACKGROUND_SELECTOR);
|
||||||
|
expect(modalOverlayBackground.exists()).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('event emission', () => {
|
||||||
|
it('emits `click` event when clicked', async () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ showProperty: true });
|
||||||
|
|
||||||
|
// act
|
||||||
|
const overlayBackground = wrapper.find(DOM_MODAL_OVERLAY_BACKGROUND_SELECTOR);
|
||||||
|
await overlayBackground.trigger('click.self.stop');
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(wrapper.emitted().click).to.have.length(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits `transitionedOut` event after leaving transition', async () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ showProperty: true });
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// act
|
||||||
|
wrapper.setProps({ show: false });
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
const transitionWrapper = wrapper.findComponent({ name: 'transition' });
|
||||||
|
transitionWrapper.vm.$emit('after-leave');
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(wrapper.emitted().transitionedOut).to.have.length(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function mountComponent(options?: { readonly showProperty?: boolean }) {
|
||||||
|
return shallowMount(ModalOverlay as unknown, {
|
||||||
|
propsData: options?.showProperty !== undefined ? { show: options?.showProperty } : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user