From 9e5491fdbf2d9d40d974f5ad0e879a6d5c6d1e55 Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Fri, 11 Aug 2023 19:35:26 +0200 Subject: [PATCH] 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. --- docs/presentation.md | 15 +- package-lock.json | 529 +++++++++++++++++- package.json | 8 +- src/presentation/assets/styles/_mixins.scss | 18 + .../bootstrapping/ApplicationBootstrapper.ts | 2 - .../Modules/VModalBootstrapper.ts | 8 - .../Code/CodeButtons/TheCodeButtons.vue | 10 +- .../Shared/Modal/Hooks/UseAllTrueWatcher.ts | 37 ++ .../Modal/Hooks/UseCurrentFocusToggle.ts | 23 + .../Modal/Hooks/UseEscapeKeyListener.ts | 17 + .../Hooks/UseLockBodyBackgroundScroll.ts | 37 ++ .../Shared/Modal/ModalContainer.vue | 150 +++++ .../components/Shared/Modal/ModalContent.vue | 95 ++++ .../components/Shared/Modal/ModalDialog.vue | 87 +++ .../components/Shared/Modal/ModalOverlay.vue | 65 +++ .../components/Shared/ModalDialog.vue | 92 --- .../components/TheFooter/TheFooter.vue | 15 +- src/presentation/shims-vue.d.ts | 6 +- tests/bootstrap/setup.ts | 4 + .../View/Cards/NonCollapsingDirective.spec.ts | 20 +- .../Modal/Hooks/UseAllTrueWatcher.spec.ts | 164 ++++++ .../Modal/Hooks/UseCurrentFocusToggle.spec.ts | 164 ++++++ .../Modal/Hooks/UseEscapeKeyListener.spec.ts | 119 ++++ .../Hooks/UseLockBodyBackgroundScroll.spec.ts | 113 ++++ .../Shared/Modal/ModalContainer.spec.ts | 206 +++++++ .../Shared/Modal/ModalContent.spec.ts | 104 ++++ .../Shared/Modal/ModalDialog.spec.ts | 83 +++ .../Shared/Modal/ModalOverlay.spec.ts | 106 ++++ 28 files changed, 2126 insertions(+), 171 deletions(-) delete mode 100644 src/presentation/bootstrapping/Modules/VModalBootstrapper.ts create mode 100644 src/presentation/components/Shared/Modal/Hooks/UseAllTrueWatcher.ts create mode 100644 src/presentation/components/Shared/Modal/Hooks/UseCurrentFocusToggle.ts create mode 100644 src/presentation/components/Shared/Modal/Hooks/UseEscapeKeyListener.ts create mode 100644 src/presentation/components/Shared/Modal/Hooks/UseLockBodyBackgroundScroll.ts create mode 100644 src/presentation/components/Shared/Modal/ModalContainer.vue create mode 100644 src/presentation/components/Shared/Modal/ModalContent.vue create mode 100644 src/presentation/components/Shared/Modal/ModalDialog.vue create mode 100644 src/presentation/components/Shared/Modal/ModalOverlay.vue delete mode 100644 src/presentation/components/Shared/ModalDialog.vue create mode 100644 tests/bootstrap/setup.ts create mode 100644 tests/unit/presentation/components/Shared/Modal/Hooks/UseAllTrueWatcher.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Modal/Hooks/UseCurrentFocusToggle.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Modal/Hooks/UseEscapeKeyListener.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Modal/Hooks/UseLockBodyBackgroundScroll.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Modal/ModalContainer.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Modal/ModalContent.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Modal/ModalDialog.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Modal/ModalOverlay.spec.ts diff --git a/docs/presentation.md b/docs/presentation.md index 85c64bc9..7dee1eb3 100644 --- a/docs/presentation.md +++ b/docs/presentation.md @@ -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. -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. - **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 -[ModalDialog.vue](./../src/presentation/components/Shared/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 - -
Hello world
-
-
Show dialog
-``` +- [ModalDialog.vue](./../src/presentation/components/Shared/Modal/ModalDialog.vue) is a shared component utilized for rendering modal windows. ## Sass naming convention diff --git a/package-lock.json b/package-lock.json index 851bb873..d6ef7d8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,8 +25,7 @@ "markdown-it": "^13.0.1", "npm": "^9.8.1", "v-tooltip": "2.1.3", - "vue": "^2.7.14", - "vue-js-modal": "^2.0.1" + "vue": "^2.7.14" }, "devDependencies": { "@rushstack/eslint-patch": "^1.3.2", @@ -44,6 +43,7 @@ "@vue/cli-service": "~5.0.8", "@vue/eslint-config-airbnb-with-typescript": "^7.0.0", "@vue/eslint-config-typescript": "^11.0.3", + "@vue/test-utils": "^1.3.6", "chai": "^4.3.7", "cypress": "^12.17.2", "electron": "^25.3.2", @@ -3311,6 +3311,12 @@ "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": { "version": "0.11.0", "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": { "name": "vue-loader", "version": "15.10.0", @@ -5043,6 +5064,12 @@ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "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": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -7053,6 +7080,36 @@ "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": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.4.tgz", @@ -7834,6 +7891,13 @@ "integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==", "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": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -8419,6 +8483,12 @@ "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": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -8554,6 +8624,57 @@ "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": { "version": "1.1.1", "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", "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -12156,6 +12289,12 @@ "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": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -12228,6 +12367,15 @@ "integrity": "sha512-F2FnH/otLNJv0J6wc73A5Xo7oHLNnqplYqZhUu01tD54DIPvxIRSTSLkrUB/M0nHO4vo1O9PDfN4KoTxCzLh/w==", "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": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -12575,6 +12723,15 @@ "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": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -12808,6 +12965,66 @@ "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", "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": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz", @@ -13158,6 +13375,18 @@ "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": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", @@ -15637,6 +15866,21 @@ "dev": 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": { "version": "2.5.0", "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" } }, + "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": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -20580,6 +20838,12 @@ "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": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -22259,11 +22523,6 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "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": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -25767,17 +26026,6 @@ "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", "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": { "version": "17.0.0", "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.0.0.tgz", @@ -25903,6 +26151,17 @@ "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", "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": { "version": "1.9.1", "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==", "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": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -30605,6 +30870,17 @@ "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": { "version": "npm:vue-loader@15.10.0", "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.0.tgz", @@ -30808,6 +31084,12 @@ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "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": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -32322,6 +32604,35 @@ "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": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.4.tgz", @@ -32894,6 +33205,13 @@ "integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==", "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": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -33305,6 +33623,12 @@ "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": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -33413,6 +33737,44 @@ "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": { "version": "1.1.1", "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", "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -36104,6 +36475,12 @@ "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": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -36149,6 +36526,12 @@ "integrity": "sha512-F2FnH/otLNJv0J6wc73A5Xo7oHLNnqplYqZhUu01tD54DIPvxIRSTSLkrUB/M0nHO4vo1O9PDfN4KoTxCzLh/w==", "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": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -36381,6 +36764,12 @@ "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": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -36559,6 +36948,51 @@ "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", "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": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz", @@ -36863,6 +37297,15 @@ "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": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", @@ -38685,6 +39128,15 @@ "integrity": "sha512-7Ws63oC+215smeKJQCxzrK21VFVlCFBkwl0MOObt0HOpVQXs3u483sAmtkF33nNqZ5rSOQjB76fgyPBmAUrtCA==", "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": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -41856,6 +42308,17 @@ "dev": 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": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -42024,6 +42487,12 @@ "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": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -43324,11 +43793,6 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "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": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -45949,14 +46413,6 @@ "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", "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": { "version": "17.0.0", "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": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz", diff --git a/package.json b/package.json index c800b137..c86c2e04 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "scripts": { "serve": "vue-cli-service serve", "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", "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", @@ -21,7 +21,7 @@ "lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml", "postinstall": "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", "dependencies": { @@ -41,8 +41,7 @@ "markdown-it": "^13.0.1", "npm": "^9.8.1", "v-tooltip": "2.1.3", - "vue": "^2.7.14", - "vue-js-modal": "^2.0.1" + "vue": "^2.7.14" }, "devDependencies": { "@rushstack/eslint-patch": "^1.3.2", @@ -60,6 +59,7 @@ "@vue/cli-service": "~5.0.8", "@vue/eslint-config-airbnb-with-typescript": "^7.0.0", "@vue/eslint-config-typescript": "^11.0.3", + "@vue/test-utils": "^1.3.6", "chai": "^4.3.7", "cypress": "^12.17.2", "electron": "^25.3.2", diff --git a/src/presentation/assets/styles/_mixins.scss b/src/presentation/assets/styles/_mixins.scss index ffb0e8b4..c6d2d3fd 100644 --- a/src/presentation/assets/styles/_mixins.scss +++ b/src/presentation/assets/styles/_mixins.scss @@ -27,3 +27,21 @@ */ -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); + } + } +} \ No newline at end of file diff --git a/src/presentation/bootstrapping/ApplicationBootstrapper.ts b/src/presentation/bootstrapping/ApplicationBootstrapper.ts index 3c2830aa..357350ed 100644 --- a/src/presentation/bootstrapping/ApplicationBootstrapper.ts +++ b/src/presentation/bootstrapping/ApplicationBootstrapper.ts @@ -1,4 +1,3 @@ -import { VModalBootstrapper } from './Modules/VModalBootstrapper'; import { TreeBootstrapper } from './Modules/TreeBootstrapper'; import { IconBootstrapper } from './Modules/IconBootstrapper'; import { VueConstructor, IVueBootstrapper } from './IVueBootstrapper'; @@ -19,7 +18,6 @@ export class ApplicationBootstrapper implements IVueBootstrapper { new TreeBootstrapper(), new VueBootstrapper(), new TooltipBootstrapper(), - new VModalBootstrapper(), ]; } } diff --git a/src/presentation/bootstrapping/Modules/VModalBootstrapper.ts b/src/presentation/bootstrapping/Modules/VModalBootstrapper.ts deleted file mode 100644 index 2c59a995..00000000 --- a/src/presentation/bootstrapping/Modules/VModalBootstrapper.ts +++ /dev/null @@ -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 }); - } -} diff --git a/src/presentation/components/Code/CodeButtons/TheCodeButtons.vue b/src/presentation/components/Code/CodeButtons/TheCodeButtons.vue index 682e95cd..1b8e22e1 100644 --- a/src/presentation/components/Code/CodeButtons/TheCodeButtons.vue +++ b/src/presentation/components/Code/CodeButtons/TheCodeButtons.vue @@ -19,7 +19,7 @@ icon-prefix="fas" icon-name="copy" /> - + @@ -30,7 +30,7 @@ import { defineComponent, ref, computed } from 'vue'; import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState'; import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog'; 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 { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; @@ -57,7 +57,7 @@ export default defineComponent({ currentState, currentContext, onStateChange, events, } = useCollectionState(); - const instructionsDialog = ref(); + const areInstructionsVisible = ref(false); const canRun = computed(() => getCanRunState(currentState.value.os)); const fileName = computed(() => buildFileName(currentState.value.collection.scripting)); const hasCode = ref(false); @@ -73,7 +73,7 @@ export default defineComponent({ function saveCode() { saveCodeToDisk(fileName.value, currentState.value); - instructionsDialog.value?.show(); + areInstructionsVisible.value = true; } async function executeCode() { @@ -103,7 +103,7 @@ export default defineComponent({ hasCode, instructions, fileName, - instructionsDialog, + areInstructionsVisible, copyCode, saveCode, executeCode, diff --git a/src/presentation/components/Shared/Modal/Hooks/UseAllTrueWatcher.ts b/src/presentation/components/Shared/Modal/Hooks/UseAllTrueWatcher.ts new file mode 100644 index 00000000..75da19e1 --- /dev/null +++ b/src/presentation/components/Shared/Modal/Hooks/UseAllTrueWatcher.ts @@ -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[] +) { + 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, + }; +} diff --git a/src/presentation/components/Shared/Modal/Hooks/UseCurrentFocusToggle.ts b/src/presentation/components/Shared/Modal/Hooks/UseCurrentFocusToggle.ts new file mode 100644 index 00000000..357f1d55 --- /dev/null +++ b/src/presentation/components/Shared/Modal/Hooks/UseCurrentFocusToggle.ts @@ -0,0 +1,23 @@ +import { Ref, watchEffect } from 'vue'; + +/** + * Manages focus transitions, ensuring good usability and accessibility. + */ +export function useCurrentFocusToggle(shouldDisableFocus: Ref) { + 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; + } + }); +} diff --git a/src/presentation/components/Shared/Modal/Hooks/UseEscapeKeyListener.ts b/src/presentation/components/Shared/Modal/Hooks/UseEscapeKeyListener.ts new file mode 100644 index 00000000..8fc920ab --- /dev/null +++ b/src/presentation/components/Shared/Modal/Hooks/UseEscapeKeyListener.ts @@ -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); + }); +} diff --git a/src/presentation/components/Shared/Modal/Hooks/UseLockBodyBackgroundScroll.ts b/src/presentation/components/Shared/Modal/Hooks/UseLockBodyBackgroundScroll.ts new file mode 100644 index 00000000..6fcd4fe9 --- /dev/null +++ b/src/presentation/components/Shared/Modal/Hooks/UseLockBodyBackgroundScroll.ts @@ -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) { + 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(); + }); +} diff --git a/src/presentation/components/Shared/Modal/ModalContainer.vue b/src/presentation/components/Shared/Modal/ModalContainer.vue new file mode 100644 index 00000000..8d231da5 --- /dev/null +++ b/src/presentation/components/Shared/Modal/ModalContainer.vue @@ -0,0 +1,150 @@ + + + + + diff --git a/src/presentation/components/Shared/Modal/ModalContent.vue b/src/presentation/components/Shared/Modal/ModalContent.vue new file mode 100644 index 00000000..6b65be99 --- /dev/null +++ b/src/presentation/components/Shared/Modal/ModalContent.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/presentation/components/Shared/Modal/ModalDialog.vue b/src/presentation/components/Shared/Modal/ModalDialog.vue new file mode 100644 index 00000000..2f9bf380 --- /dev/null +++ b/src/presentation/components/Shared/Modal/ModalDialog.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/src/presentation/components/Shared/Modal/ModalOverlay.vue b/src/presentation/components/Shared/Modal/ModalOverlay.vue new file mode 100644 index 00000000..e37dc9cf --- /dev/null +++ b/src/presentation/components/Shared/Modal/ModalOverlay.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/src/presentation/components/Shared/ModalDialog.vue b/src/presentation/components/Shared/ModalDialog.vue deleted file mode 100644 index c208676c..00000000 --- a/src/presentation/components/Shared/ModalDialog.vue +++ /dev/null @@ -1,92 +0,0 @@ - - - - - diff --git a/src/presentation/components/TheFooter/TheFooter.vue b/src/presentation/components/TheFooter/TheFooter.vue index 6474ab0a..d0f1d4c7 100644 --- a/src/presentation/components/TheFooter/TheFooter.vue +++ b/src/presentation/components/TheFooter/TheFooter.vue @@ -33,11 +33,11 @@ - + @@ -46,7 +46,7 @@