157 lines
5.1 KiB
Vue
157 lines
5.1 KiB
Vue
<style scoped>
|
|
/* container fades */
|
|
.dialog-enter-active,
|
|
.dialog-leave-active {
|
|
transition: opacity .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 .18s ease, opacity .18s ease;
|
|
}
|
|
|
|
.dialog-enter-from .modal-card {
|
|
transform: translateY(-8px);
|
|
opacity: .98;
|
|
}
|
|
|
|
.dialog-leave-to .modal-card {
|
|
transform: translateY(-8px);
|
|
opacity: .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>
|