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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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-name="copy"
|
||||
/>
|
||||
<ModalDialog v-if="instructions" ref="instructionsDialog">
|
||||
<ModalDialog v-if="instructions" v-model="areInstructionsVisible">
|
||||
<InstructionList :data="instructions" />
|
||||
</ModalDialog>
|
||||
</div>
|
||||
@@ -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<typeof ModalDialog>();
|
||||
const areInstructionsVisible = ref(false);
|
||||
const canRun = computed<boolean>(() => getCanRunState(currentState.value.os));
|
||||
const fileName = computed<string>(() => 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,
|
||||
|
||||
@@ -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 class="footer__section__item">
|
||||
<font-awesome-icon class="icon" :icon="['fas', 'user-secret']" />
|
||||
<a @click="privacyDialog.show()">Privacy</a>
|
||||
<a @click="showPrivacyDialog()">Privacy</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ModalDialog ref="privacyDialog">
|
||||
<ModalDialog v-model="isPrivacyDialogVisible">
|
||||
<PrivacyPolicy />
|
||||
</ModalDialog>
|
||||
</div>
|
||||
@@ -46,7 +46,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed } from 'vue';
|
||||
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 DownloadUrlList from './DownloadUrlList.vue';
|
||||
import PrivacyPolicy from './PrivacyPolicy.vue';
|
||||
@@ -62,7 +62,7 @@ export default defineComponent({
|
||||
setup() {
|
||||
const { info } = useApplication();
|
||||
|
||||
const privacyDialog = ref<typeof ModalDialog>();
|
||||
const isPrivacyDialogVisible = ref(false);
|
||||
|
||||
const version = computed<string>(() => info.version.toString());
|
||||
|
||||
@@ -74,9 +74,14 @@ export default defineComponent({
|
||||
|
||||
const feedbackUrl = computed<string>(() => info.feedbackUrl);
|
||||
|
||||
function showPrivacyDialog() {
|
||||
isPrivacyDialogVisible.value = true;
|
||||
}
|
||||
|
||||
return {
|
||||
isDesktop,
|
||||
privacyDialog,
|
||||
isPrivacyDialogVisible,
|
||||
showPrivacyDialog,
|
||||
version,
|
||||
homepageUrl,
|
||||
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' {
|
||||
export default Vue;
|
||||
import { DefineComponent } from 'vue';
|
||||
const component: DefineComponent;
|
||||
export default component;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user