Modal

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.

Vue Component

BpModal

Props

  • block - the block class (defaults to ‘accordion’)
  • openButtonElement - the open button element (defaults to ‘openButton’)
  • overlayElement - the overlay element (defaults to ‘overlay’)
  • containerElement - the container element (defaults to ‘container’)
  • loadingSpinnerElement - the loading spinner element (defaults to ‘loadingSpinner’)
  • contentElement - the content element (defaults to ‘content’)
  • closeButtonElement - the close button element (defaults to ‘closeButton’)
  • wrapperElement - the wrapper element (defaults to ‘wrapper’)
  • closeButtonIconElement - the close button icon element (defaults to ‘closeButtonIcon’)

Slots

BpModal also provides some named slots that can be used to override certain elements

  • open-button - To provide a custom button for opening the modal (openModal method is passed through the open-button slot prop)
  • close-button - To provide a custom button for closing the modal (closeModal method is passed through the close-button slot prop)
  • loading-spinner - the provide a custom spinner (loading data attribute is passed through the loading slot prop)

    Parameters

None.

<bp-modal>
    &lt;script src=&quot;//fast.wistia.com/embed/medias/j38ihh83m5.jsonp&quot; async&gt;&lt;/script&gt;
    &lt;script src=&quot;//fast.wistia.com/assets/external/E-v1.js&quot; async&gt;&lt;/script&gt;
    &lt;div class=&quot;wistia_embed wistia_async_j38ihh83m5&quot; style=&quot;height:349px;width:620px&quot;&gt;&amp;nbsp;&lt;/div&gt;
</bp-modal>
  • Content:
    <template>
        <div :class="`${block}`">
            <slot
                name="open-button"
                :open-modal="openModal"
            >
                <button
                    :class="`${block}__${openButtonElement}`"
                    @click="openModal"
                >
                    Open Modal
                </button>
            </slot>
            <MountingPortal
                v-if="open"
                mount-to="#modal"
                append
            >
                <div
                    :class="`${block}__${overlayElement}`"
                    @click="clickOut"
                >
                    <div
                        ref="modalWrapper"
                        :class="`${block}__${wrapperElement}`"
                    >
                        <div
                            ref="modalContainer"
                            :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="/media/spinner.gif"
                                    alt=""
                                >
                            </slot>
                        </div>
                        <slot
                            name="close-button"
                            :close-modal="closeModal"
                        >
                            <button
                                :class="`${block}__${closeButtonElement}`"
                                @click="closeModal"
                            >
                                Close Modal
                                <svg
                                    :class="`${block}__${closeButtonIconElement}`"
                                    viewBox="0 0 320 512"
                                >
                                    <path
                                        fill="currentColor"
                                        d="M193.94 256L296.5 153.44l21.15-21.15c3.12-3.12 3.12-8.19 0-11.31l-22.63-22.63c-3.12-3.12-8.19-3.12-11.31 0L160 222.06 36.29 98.34c-3.12-3.12-8.19-3.12-11.31 0L2.34 120.97c-3.12 3.12-3.12 8.19 0 11.31L126.06 256 2.34 379.71c-3.12 3.12-3.12 8.19 0 11.31l22.63 22.63c3.12 3.12 8.19 3.12 11.31 0L160 289.94 262.56 392.5l21.15 21.15c3.12 3.12 8.19 3.12 11.31 0l22.63-22.63c3.12-3.12 3.12-8.19 0-11.31L193.94 256z"
                                    />
                                </svg>
                            </button>
                        </slot>
                    </div>
                </div>
            </MountingPortal>
        </div>
    </template>
    
    <script>
    export default {
    
        props: {
            block: {
                type: String,
                default: 'modal',
            },
    
            openButtonElement: {
                type: String,
                default: 'openButton',
            },
    
            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',
            },
        },
    
        data: () => ({
            open: false,
            loading: true,
        }),
    
        methods: {
            async openModal () {
                this.open = true
                this.lockScroll()
                window.addEventListener('keyup', this.keyboardEvent)
                await this.handleContent()
                this.loading = false
            },
    
            closeModal () {
                this.unlockScroll()
                this.open = false
            },
    
            handleContent () {
                return new Promise((resolve, reject) => {
                    this.$nextTick(async () => {
                        const { modalContent } = this.$refs
                        const { innerHTML } = modalContent
    
                        const parsedDocument = this.htmlDecode(innerHTML)
                        const body = parsedDocument.querySelector('body')
                        const scripts = [...parsedDocument.querySelectorAll('script')]
    
                        scripts.forEach(script => script.remove())
                        modalContent.innerHTML = body.innerHTML
    
                        modalContent.focus()
    
                        if (scripts.length) {
                            await this.loadScripts(scripts)
                            resolve()
                        } else {
                            resolve()
                        }
                    })
                })
            },
    
            htmlDecode (string) {
                const doc = new DOMParser().parseFromString(string, 'text/html')
                const body = doc.querySelector('body')
                body.innerHTML = doc.documentElement.textContent
                return doc
            },
    
            async loadScripts (scripts) {
                const scriptsWithSrc = scripts.filter(script => script.src)
                const inlineScripts = scripts.filter(script => !script.src)
    
                // inline scripts most likely rely on an external api, run first
                if (scriptsWithSrc.length) {
                    await this.fetchExternalScripts(scriptsWithSrc)
                }
    
                if (inlineScripts.length) {
                    this.runInlineScripts(inlineScripts)
                }
            },
    
            runInlineScripts (scripts) {
                scripts.forEach(inlineScript => {
                    // eslint-disable-next-line no-eval
                    eval(inlineScript.innerHTML)
                })
            },
    
            fetchExternalScripts (scripts) {
                return new Promise((resolve, reject) => {
                    const scriptPromises = scripts.map(scriptTag => new Promise((resolve, reject) => {
                        const scriptEl = document.createElement('script')
                        scriptEl.onload = () => { resolve() }
                        scriptEl.src = scriptTag.src
                        document.body.appendChild(scriptEl)
                    }))
    
                    Promise.all(scriptPromises).then(resolve)
                })
            },
    
            lockScroll () {
                const { documentElement, body } = document
                documentElement.classList.add('-lock')
                body.classList.add('-lock')
            },
    
            unlockScroll () {
                const { documentElement, body } = document
                documentElement.classList.remove('-lock')
                body.classList.remove('-lock')
            },
    
            keyboardEvent ({ key }) {
                if (key === 'Escape') {
                    window.removeEventListener('keyup', this.keyboardEvent)
                    this.closeModal()
                }
            },
    
            clickOut ({ path }) {
                const { modalWrapper } = this.$refs
    
                if (!path.includes(modalWrapper)) {
                    this.closeModal()
                }
            },
        },
    
    }
    </script>
    
  • URL: /components/raw/modal/BpModal.vue
  • Filesystem Path: resources/styles/organisms/modal/BpModal.vue
  • Size: 7.5 KB
  • Content:
    .modal {
        &__overlay {
            display: flex;
            align-items: center;
            justify-content: center;
            position: fixed;
            top: 0;
            right: 0;
            bottom: 0;
            left: 0;
            background-color: rgba($black, .5);
        }
    
        &__wrapper {
            position: relative;
            max-width: 60rem;
            background-color: $white;
            padding: $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
    body.-lock,
    html.-lock {
        position: relative;
        overflow: hidden;
        touch-action: none;
        -ms-touch-action: none;
    }
    
  • URL: /components/raw/modal/modal.scss
  • Filesystem Path: resources/styles/organisms/modal/modal.scss
  • Size: 946 Bytes