ytptube/ui/app/components/Dialog.vue
2026-02-20 22:46:30 +03:00

185 lines
5.5 KiB
Vue

<style scoped>
/* container fades */
.dialog-enter-active,
.dialog-leave-active {
transition: opacity 0.18s ease;
}
.dialog-enter-from,
.dialog-leave-to {
opacity: 0;
}
/* animate the card itself */
.dialog-enter-active .modal-card,
.dialog-leave-active .modal-card {
transition:
transform 0.18s ease,
opacity 0.18s ease;
}
.dialog-enter-from .modal-card {
transform: translateY(-8px);
opacity: 0.98;
}
.dialog-leave-to .modal-card {
transform: translateY(-8px);
opacity: 0.98;
}
</style>
<template>
<Teleport to="body">
<transition name="dialog" @after-enter="focusInput">
<div
id="app-dialog-host"
v-if="state.current"
class="modal is-active"
@keydown.esc="onCancel"
>
<div class="modal-background" @click="onCancel" />
<div class="modal-card" @keydown.enter.stop.prevent="onEnter">
<header class="modal-card-head p-4">
<p class="modal-card-title">{{ state.current?.opts.title ?? defaultTitle }}</p>
<button class="delete" aria-label="close" @click="onCancel" />
</header>
<section class="modal-card-body">
<p v-if="state.current?.opts.message" class="mb-3">
{{ state.current?.opts.message }}
</p>
<!-- prompt input -->
<div v-if="'prompt' === state.current?.type" class="field">
<div class="control">
<input
ref="inputEl"
class="input"
type="text"
v-model="localInput"
:placeholder="(state.current?.opts as any)?.placeholder ?? ''"
@keyup.stop
/>
</div>
<p v-if="state.errorMsg" class="help is-danger is-bold is-unselectable">
<span class="icon-text">
<span class="icon"><i class="fas fa-exclamation-triangle" /></span>
<span>{{ state.errorMsg }}</span>
</span>
</p>
</div>
<div
v-else-if="
'confirm' === state.current?.type &&
(state.current?.opts as ConfirmOptions)?.rawHTML
"
class="content"
v-html="(state.current?.opts as ConfirmOptions)?.rawHTML"
/>
</section>
<footer class="modal-card-foot p-4 is-justify-content-flex-end">
<template v-if="'alert' === state.current?.type">
<button id="primaryButton" class="button is-danger" @click="onEnter">
<span class="icon-text">
<span class="icon"><i class="fas fa-check" /></span>
<span>{{ (state.current?.opts as any)?.confirmText ?? 'OK' }}</span>
</span>
</button>
</template>
<template
v-else-if="'confirm' === state.current?.type || 'prompt' === state.current?.type"
>
<div class="field is-grouped">
<div class="control">
<button
id="primaryButton"
class="button"
@click="onEnter"
:class="state.current?.opts.confirmColor ?? 'is-primary'"
:disabled="localInput === (state.current?.opts as PromptOptions)?.initial"
>
<span class="icon-text">
<span class="icon"><i class="fas fa-check" /></span>
<span>{{ (state.current?.opts as any)?.confirmText ?? 'OK' }}</span>
</span>
</button>
</div>
<div class="control">
<button class="button is-info" @click="onCancel">
<span class="icon-text">
<span class="icon"><i class="fas fa-times" /></span>
<span>{{ (state.current?.opts as any)?.cancelText ?? 'Cancel' }}</span>
</span>
</button>
</div>
</div>
</template>
</footer>
</div>
</div>
</transition>
</Teleport>
</template>
<script setup lang="ts">
import { ref, watch, nextTick, computed } from 'vue';
import { useDialog, type ConfirmOptions, type PromptOptions } from '~/composables/useDialog';
const { state, confirm, cancel } = useDialog();
const localInput = ref('');
watch(
() => state.current,
(cur) => {
if (state.current) {
disableOpacity();
} else {
enableOpacity();
}
localInput.value = 'prompt' === cur?.type ? ((cur.opts as any).initial ?? '') : '';
},
{ immediate: true },
);
const inputEl = ref<HTMLInputElement>();
const focusPrimary = () => {
const root = document.getElementById('app-dialog-host');
if (!root) {
return;
}
const btn = root.querySelector<HTMLButtonElement>('#primaryButton');
btn?.focus();
};
const focusInput = async () => {
await nextTick();
if ('prompt' === state.current?.type) {
requestAnimationFrame(() => inputEl.value?.focus({ preventScroll: true }));
return;
}
requestAnimationFrame(focusPrimary);
};
const onCancel = () => cancel();
const onEnter = () => confirm(localInput.value);
const defaultTitle = computed(() => {
if (!state.current) {
return '';
}
switch (state.current.type) {
case 'alert':
return 'Alert';
case 'confirm':
return 'Confirm';
case 'prompt':
return 'Input required';
default:
return 'Dialog';
}
});
</script>