185 lines
5.5 KiB
Vue
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>
|