ytptube/ui/app/components/Dialog.vue
2026-04-22 19:04:11 +03:00

142 lines
4 KiB
Vue

<template>
<UModal
v-if="state.current"
:open="true"
:title="state.current?.opts.title ?? defaultTitle"
:dismissible="true"
:ui="{ content: 'max-w-lg', body: 'space-y-4', footer: 'justify-end gap-2' }"
@update:open="(open) => !open && cancel()"
@after:enter="focusInput"
>
<template #body>
<p v-if="state.current?.opts.message" class="whitespace-pre-line wrap-break-word">
{{ state.current?.opts.message }}
</p>
<UFormField v-if="'prompt' === state.current?.type" :error="state.errorMsg || undefined">
<UInput
ref="inputEl"
v-model="localInput"
:placeholder="(state.current?.opts as PromptOptions)?.placeholder ?? ''"
class="w-full"
@keydown.enter.stop.prevent="onEnter"
/>
</UFormField>
<div
v-if="
'confirm' === state.current?.type &&
(state.current?.opts as ConfirmOptions)?.options?.length
"
class="space-y-3 border-t border-default pt-4"
>
<UCheckbox
v-for="opt in (state.current?.opts as ConfirmOptions).options"
:key="opt.key"
v-model="selected[opt.key]"
:label="opt.label"
/>
</div>
</template>
<template #footer>
<template v-if="'alert' === state.current?.type">
<UButton
id="primaryButton"
:color="state.current?.opts.confirmColor ?? 'primary'"
@click="onEnter"
>
{{ state.current?.opts.confirmText ?? 'OK' }}
</UButton>
</template>
<template v-else-if="'confirm' === state.current?.type || 'prompt' === state.current?.type">
<UButton
id="primaryButton"
:color="state.current?.opts.confirmColor ?? 'primary'"
:disabled="
'prompt' === state.current?.type &&
localInput === (state.current?.opts as PromptOptions)?.initial
"
@click="onEnter"
>
{{ state.current?.opts.confirmText ?? 'OK' }}
</UButton>
<UButton color="neutral" variant="outline" @click="cancel">
{{ (state.current?.opts as PromptOptions | ConfirmOptions)?.cancelText ?? 'Cancel' }}
</UButton>
</template>
</template>
</UModal>
</template>
<script setup lang="ts">
import { ref, watch, nextTick, computed } from 'vue';
import { UButton, UCheckbox, UInput } from '#components';
import { disableOpacity, enableOpacity } from '~/utils';
import { useDialog, type ConfirmOptions, type PromptOptions } from '~/composables/useDialog';
const { state, confirm, cancel } = useDialog();
const localInput = ref('');
const selected = ref<Record<string, boolean>>({});
watch(
() => state.current,
(cur) => {
if (state.current) {
disableOpacity();
} else {
enableOpacity();
}
localInput.value = 'prompt' === cur?.type ? ((cur.opts as PromptOptions).initial ?? '') : '';
if ('confirm' === cur?.type) {
selected.value = Object.fromEntries(
((cur.opts as ConfirmOptions).options ?? []).map((opt) => [opt.key, Boolean(opt.checked)]),
);
return;
}
selected.value = {};
},
{ immediate: true },
);
const inputEl = ref<{ inputRef?: { value?: HTMLInputElement | null } } | null>(null);
const focusPrimary = () => {
const root = document.getElementById('primaryButton');
root?.focus();
};
const focusInput = async () => {
await nextTick();
if ('prompt' === state.current?.type) {
requestAnimationFrame(() => inputEl.value?.inputRef?.value?.focus?.({ preventScroll: true }));
return;
}
requestAnimationFrame(focusPrimary);
};
const onEnter = () =>
confirm('confirm' === state.current?.type ? selected.value : 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>