Last active
June 5, 2021 02:38
-
-
Save MichaelFedora/c46d412c59074667bc6e95f9b1b92b6f to your computer and use it in GitHub Desktop.
A vue 3 modal. Compare to: https://github.com/MichaelFedora/tiny-host-common/blob/7258af049f29f96c3ca071472ebda27e094acfb1/src/web/components/modal.vue
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<transition name='fade'> | |
<div v-if='active' | |
class='modal is-active' | |
:class='{ [type]: true }' | |
role='dialog' | |
aria-modal='true'> | |
<div class='modal-background' @click='cancel()'></div> | |
<div class='modal-card'> | |
<header v-show='title'> | |
<span>{{title}}</span> | |
</header> | |
<section> | |
<p>{{message}}</p> | |
<div class='field' v-if='inputAttrs'> | |
<input ref='input' | |
:class='{ [type]: true }' | |
v-bind='inputAttrs' | |
v-model='text' /> | |
<span class='error'>{{ inputAttrs.required && text === '' ? 'required' : '' }}</span> | |
</div> | |
</section> | |
<footer> | |
<button v-if='!alert' @click='cancel'>cancel</button> | |
<button | |
:class='{ [type]: true }' | |
ref='submit' | |
:disabled='Boolean(inputAttrs && inputAttrs.required && !text)' | |
@click='confirm' | |
@keydown.esc='cancel' | |
> | |
{{ alert ? 'ok' : 'confirm' }} | |
</button> | |
</footer> | |
</div> | |
</div> | |
</transition> | |
</template> | |
<script lang='ts'> | |
import { defineComponent, nextTick, onMounted, onUnmounted, Prop, reactive, Ref, ref, toRefs } from 'vue'; | |
interface InputAttrs { | |
value: string; | |
} | |
export default defineComponent({ | |
name: 'modal', | |
props: { | |
title: { type: String, default: '' }, | |
message: { type: String, default: '' }, | |
type: { type: String, default: 'primary' }, | |
alert: Boolean, | |
prompt: { type: Object, default: null } as Prop<Partial<InputAttrs>> | |
}, | |
setup(props, { attrs, slots, emit }) { | |
const data = reactive({ | |
title: props.title, | |
message: props.message, | |
type: props.type, | |
alert: props.alert, | |
active: ref(false), | |
text: ref(props.prompt?.value || undefined) as Ref<string | undefined>, | |
input: ref(null) as Ref<HTMLInputElement | null>, | |
submit: ref(null) as Ref<HTMLButtonElement | null> | |
}); | |
const inputAttrs = props.prompt | |
? Object.assign({ | |
type: 'text', | |
placeholder: '', | |
required: false, | |
readonly: Boolean(props.alert) | |
}, props.prompt) | |
: null; | |
if(inputAttrs?.value != null) | |
delete inputAttrs.value; | |
function close() { | |
if(!data.active) return; | |
data.active = false; | |
emit('close'); | |
} | |
function confirm() { | |
if(!data.active) return; | |
emit('confirm', inputAttrs ? data.text : true); | |
close(); | |
} | |
function cancel() { | |
if(!data.active) return; | |
emit('cancel'); | |
close(); | |
} | |
function onKey(ev: KeyboardEvent) { | |
if(!data.active) return; | |
if(ev.key === 'Enter') | |
confirm(); | |
else if(ev.key === 'Escape') | |
cancel(); | |
} | |
onMounted(() => { | |
data.active = true; | |
nextTick(() => inputAttrs | |
? data.input?.focus() | |
: data.submit?.focus()); | |
data.text += ' bye!'; | |
window.addEventListener('keyup', onKey); | |
}); | |
onUnmounted(() => window.removeEventListener('keyup', onKey)); | |
return { | |
...toRefs(data), | |
close, | |
cancel, | |
confirm, | |
onKey, | |
inputAttrs | |
}; | |
} | |
}); | |
/* | |
* you would call it with this code below | |
*/ | |
interface ModalProps { | |
[key: string]: unknown; | |
title?: string; | |
message?: string; | |
type?: string; | |
alert?: boolean; | |
prompt?: Partial<InputHTMLAttributes>; | |
} | |
export function openModal(props: ModalProps): Promise<string | undefined> { | |
return new Promise((res, rej) => { | |
let app: App<Element> | null; | |
try { | |
app = createApp(ModalComponent, { | |
...props, | |
onCancel: () => res(undefined), | |
onConfirm: (val: string) => res(val), | |
onClose: () => { if(app) { app.unmount(); app = null; } } | |
}); | |
const div = document.createElement('div'); | |
document.body.appendChild(div); | |
app.mount(div); | |
} catch(e) { | |
rej(e); | |
} | |
}); | |
} | |
</script> | |
<style lang='scss'> | |
@import '~frontend/colors.scss'; | |
div.modal { | |
position: fixed; | |
left: 0; | |
top: 0; | |
right: 0; | |
bottom: 0; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
> div.modal-background { | |
position: absolute; | |
left: 0; | |
top: 0; | |
right: 0; | |
bottom: 0; | |
background-color: rgba(0, 0, 0, 0.15); | |
z-index: 1024; | |
} | |
> div.modal-card { | |
display: flex; | |
flex-flow: column; | |
overflow: hidden; | |
min-width: 320px; | |
max-width: 480px; | |
z-index: 1025; | |
background-color: $background; | |
border-radius: 5px; | |
box-shadow: 0 0 4px -1px rgba(0, 0, 0, 0.15); | |
> * { padding: 1rem; } | |
> :not(section) { | |
background-color: $grey-lightest; | |
} | |
> header { | |
color: $black-ter; | |
font-size: $size-3; | |
font-weight: bold; | |
border-bottom: 1px solid $grey-lighter; | |
} | |
// > section { } | |
> footer { | |
display: flex; | |
justify-content: flex-end; | |
border-top: 1px solid $grey-lighter; | |
> *:not(:last-child) { margin-right: 0.5rem; } | |
} | |
} | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment