Skip to content

Instantly share code, notes, and snippets.

@MichaelFedora
Last active June 5, 2021 02:38
Show Gist options
  • Save MichaelFedora/c46d412c59074667bc6e95f9b1b92b6f to your computer and use it in GitHub Desktop.
Save MichaelFedora/c46d412c59074667bc6e95f9b1b92b6f to your computer and use it in GitHub Desktop.
<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