BpModal is a modal component that allows for easy access throughout templates and other components. Uses Portal-Vue to always send content to the end of body. Will also lazyload any scripts provided.
For lazyloading to work, HTML must be an encoded URI. Twig’s escape filter can be used to achieve this.
BpModal
BpModal also provides some named slots that can be used to override certain elements
openModal
method is passed through the open-button
slot prop)closeModal
method is passed through the close-button
slot prop)loading
data attribute is passed through the loading
slot prop)None.
<bp-modal>
<template #open-button="{ open }">
<button class="button" @click="open">Watch Video</button>
</template>
<script src="//fast.wistia.com/embed/medias/j38ihh83m5.jsonp" async></script>
<script src="//fast.wistia.com/assets/external/E-v1.js" async></script>
<div class="wistia_embed wistia_async_j38ihh83m5" style="height:349px;width:620px">&nbsp;</div>
</bp-modal>
<template>
<slot
name="open-button"
:open="open"
/>
<teleport to="body">
<div
v-if="isOpen"
:class="block"
>
<div
:class="`${block}__${overlayElement}`"
>
<div
ref="modalWrapper"
:class="`${block}__${wrapperElement}`"
>
<div
:class="`${block}__${containerElement}`"
>
<div
ref="modalContent"
:class="`${block}__${contentElement}`"
tabindex="0"
>
<slot />
</div>
<slot
v-if="loading"
name="loading-spinner"
:loading="loading"
>
<img
:class="`${block}__${loadingSpinnerElement}`"
src="/resources/media/spinner.gif"
alt=""
>
</slot>
</div>
<slot
name="close-button"
:close="close"
>
<button
:class="`${block}__${closeButtonElement}`"
@click="close"
>
Close Modal
<svg
:class="`${block}__${closeButtonIconElement}`"
viewBox="0 0 24 24"
stroke="currentColor"
fill="none"
>
<line
x1="18"
y1="6"
x2="6"
y2="18"
/>
<line
x1="6"
y1="6"
x2="18"
y2="18"
/>
</svg>
</button>
</slot>
</div>
</div>
</div>
</teleport>
</template>
<script setup>
import { nextTick, ref, watch } from 'vue'
import useOpenable from '@resources/js/components/UseOpenable.vue'
import useLazyEmbed from '@resources/js/components/UseLazyEmbed.js'
defineProps({
block: {
type: String,
default: 'modal',
},
overlayElement: {
type: String,
default: 'overlay',
},
containerElement: {
type: String,
default: 'container',
},
loadingSpinnerElement: {
type: String,
default: 'loadingSpinner',
},
contentElement: {
type: String,
default: 'content',
},
closeButtonElement: {
type: String,
default: 'closeButton',
},
wrapperElement: {
type: String,
default: 'wrapper',
},
closeButtonIconElement: {
type: String,
default: 'closeButtonIcon',
},
})
const loading = ref(false)
const modalContent = ref(null)
const modalWrapper = ref(null)
const { isOpen, open, close } = useOpenable(modalWrapper)
const { loadEmbed } = useLazyEmbed(modalContent)
watch(isOpen, async isOpen => {
if (isOpen) {
loading.value = true
lockScroll()
await nextTick(loadEmbed)
} else {
unlockScroll()
}
loading.value = false
})
const lockScroll = () => {
document.documentElement.classList.add('-lock')
}
const unlockScroll = () => {
document.documentElement.classList.remove('-lock')
}
</script>
@use "/resources/styles/common" as *;
@use "/resources/styles/config";
.modal {
$b: &;
&__overlay {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(config.$black, .5);
}
&__wrapper {
position: relative;
max-width: 60rem;
background-color: config.$white;
padding: config.$padding;
max-height: 100vh;
}
&__loadingSpinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
&__closeButton {
background: none;
border: none;
padding: 0;
position: absolute;
top: 0.2rem;
right: 0.5rem;
font-size: 0;
}
&__closeButtonIcon {
width: 1rem;
}
}
// Scroll Lock
html.-lock,
html.-lock body {
position: relative;
overflow: hidden;
touch-action: none;
-ms-touch-action: none;
}