Popup

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque nec laoreet metus.

Customisable popup block

Simple popup with default styling and delaed activtion after page load

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lectus sed sapien pellentesque vehicula. In nibh arcu, porta at facilisis sed, tempor eget turpis. Sed luctus ligula in ligula gravida tempus. Pellentesque bibendum vel sapien vel dignissim. Aenean venenatis, nunc efficitur pharetra iaculis, enim dolor malesuada augue, eget condimentum arcu lectus et neque. Duis eget tempus ante. Maecenas vitae lobortis lectus. Aenean nec pharetra magna, vitae fermentum metus. Nunc sed interdum tortor. Vestibulum eu lectus consectetur, iaculis felis vel, interdum velit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nunc efficitur at eros vitae fringilla. Cras interdum faucibus nunc eu imperdiet. Nam eu justo vel turpis iaculis congue ac quis magna. Proin mauris justo, hendrerit ut nisl facilisis, hendrerit placerat sapien.

Popup with customised appearance

Changed popup max width, padding, border color, width and radius, background and backdrop colors

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lectus sed sapien pellentesque vehicula. In nibh arcu, porta at facilisis sed, tempor eget turpis. Sed luctus ligula in ligula gravida tempus. Pellentesque bibendum vel sapien vel dignissim. Aenean venenatis, nunc efficitur pharetra iaculis, enim dolor malesuada augue, eget condimentum arcu lectus et neque. Duis eget tempus ante. Maecenas vitae lobortis lectus. Aenean nec pharetra magna, vitae fermentum metus. Nunc sed interdum tortor. Vestibulum eu lectus consectetur, iaculis felis vel, interdum velit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nunc efficitur at eros vitae fringilla. Cras interdum faucibus nunc eu imperdiet. Nam eu justo vel turpis iaculis congue ac quis magna. Proin mauris justo, hendrerit ut nisl facilisis, hendrerit placerat sapien.

block.json

{
    "$schema": "https://schemas.wp.org/trunk/block.json",
    "apiVersion": 3,
    "name": "wlc/popup",
    "title": "Popup",
    "category": "theme",
    "icon": "fullscreen-alt",
    "description": "Popup",
    "keywords": [ "popup", "container" ],
    "version": "1.0.0",
    "textdomain": "WLC",
    "attributes": {
        "activation": {
            "type": "object",
            "default": {
                "manual": true,
                "delayed": false,
                "backdropClose": false,
                "autoplay": false
            }
        },
        "anchor": {
            "type": "string",
            "source": "attribute",
            "selector": ".wlc-popup__dialog",
            "attribute": "id"
        },
        "delay": {
            "type": "number",
            "default": 500
        },
        "preview": {
            "type": "boolean",
            "default": true
        },
        "styles": {
            "type": "object",
            "default": {
                "dialog": {
                    "width": "50rem",
                    "borderColor": "#000000",
                    "borderStyle": "solid",
                    "borderWidth": "1px",
                    "borderRadius": "0"
                },
                "backdrop": {
                    "background": "rgba(0,0,0,0.5)"
                },
                "wrapper": {
                    "padding": "1rem",
                    "background": "#ffffff"
                }
            }
        }
    },
    "supports": {
        "renaming": true
    },
    "editorScript": "file:./index.js",
    "viewScript": "file:./view.js",
    "style": "file:./style-index.css"
}

edit.js

import { InspectorControls, useBlockProps, InnerBlocks } from '@wordpress/block-editor';
import { 
    PanelBody,
    ToggleControl,
    RangeControl,
    TextControl,
    Dropdown,
    Button,
    ColorPalette,
    ColorIndicator,
    __experimentalBorderControl as BorderControl,
} from '@wordpress/components';
import { select } from '@wordpress/data';
import { __ } from '@wordpress/i18n';

const edit = ( { attributes, setAttributes } ) => {

    const { activation, anchor, delay, styles, preview } = attributes;
    const globals = select( "core/editor" ).getEditorSettings();

    const blockProps = useBlockProps( { className: 'wlc-popup alignfull' } );

    const dialogStyles = {style: { ...styles.dialog, '--wlc-popup-backdrop-color': styles.backdrop.background } };
    const wrapperStyles = {style: { ...styles.wrapper } };
    const backdropStyles = {style: { ...styles.backdrop, padding: '1rem' } };

    const PopupBorderControl = () => {
        const border = {
            color: styles.dialog.borderColor,
            style: styles.dialog.borderStyle,
            width: styles.dialog.borderWidth
        }
        return (
            <BorderControl
                label={ __( 'Border', 'WLC' ) }
                colors={ globals.colors }
                value={ border }
                onChange={ value => {
                    console.log( value );
                    const newBorder = {
                        borderColor: value.color,
                        borderStyle: value.style,
                        borderWidth: value.width
                    }
                    setAttributes( 
                        { 
                            styles: { 
                                ...styles, 
                                dialog: {...styles.dialog, ...newBorder }
                            } 
                        }
                    )
                } }
                isCompact={ true }
                showDropdownHeader={ true }
            />
        );
    }

    return (
        <>
            <InspectorControls>
                <PanelBody title={ __( 'Popup settings', 'WLC' ) }>
                    <ToggleControl
                        label={__( 'Manual activation', 'WLC')}
                        help={ __( 'Popup opens by clicking on other elements with appropriate attribute: "href" (on links) or "data-popup-open" (on other elements)', 'WLC') }
                        checked={ activation.manual }
                        onChange={ value => setAttributes( { activation: { ...activation, manual: value } } ) }
                    />
                    {activation.manual && (
                        <TextControl
                            label={ __( 'Anchor', 'WLC' ) }
                            help={ __( 'identifier used to open popup - do not use # as prefix', 'WLC' ) }
                            value={ anchor }
                            onChange={ value => setAttributes( { anchor: value } ) }
                        />
                    )}
                    <ToggleControl
                        label={__( 'Delayed activation', 'WLC')}
                        help={__( 'Popup opens automatically after specified amount of time', 'WLC')}
                        checked={ activation.delayed }
                        onChange={ value => setAttributes( { activation: { ...activation, delayed: value } } ) }
                    />
                    {activation.delayed && (
                        <RangeControl
                            label={ __( 'Delay', 'WLC' ) }
                            help={ __( 'time in miliseconds', 'WLC' ) }
                            value={ delay }
                            onChange={ value => setAttributes( { delay: value } ) }
                            min={ 0 }
                            max={ 10000 }
                            step={ 500 }
                        />
                    )}
                    <ToggleControl
                        label={__( 'Close with click on backdrop', 'WLC')}
                        help={__( 'Popup can be closed by clicking on it\'s backdrop area outside', 'WLC')}
                        checked={ activation.backdropClose }
                        onChange={ value => setAttributes( { activation: { ...activation, backdropClose: value } } ) }
                    />
                    <ToggleControl
                        label={__( 'Autoplay on videos', 'WLC')}
                        help={__( 'YT Videos can automaticaly play when popup opens', 'WLC')}
                        checked={ activation.autoplay}
                        onChange={ value => setAttributes( { activation: { ...activation, autoplay: value } } ) }
                    />
                </PanelBody>
            </InspectorControls>
            <InspectorControls group="styles">
                <PanelBody title={ __( 'Popup styles', 'WLC' ) }>
                    <PopupBorderControl/>
                    <RangeControl
                        label={ __( 'Border Radius', 'WLC' ) }
                        value={ parseInt(styles.dialog.borderRadius) }
                        onChange={ value => setAttributes( { styles: { ...styles, dialog: { ...styles.dialog, borderRadius: value } } } ) }
                        min={ 0 }
                        max={ 50 }
                    />
                    <RangeControl
                        label={ __( 'Width', 'WLC' ) }
                        help={ __( 'Popup width', 'WLC' ) }
                        value={ parseInt(styles.dialog.width) }
                        onChange={ value => setAttributes( { styles: { ...styles, dialog: { ...styles.dialog, width : value } } } ) } 
                        min={ 50 }
                        max={ 2000 }
                    />
                    <RangeControl
                        label={ __( 'Padding', 'WLC' ) }
                        value={ parseInt(styles.wrapper.padding) }
                        onChange={ value => setAttributes( { styles: { ...styles, wrapper: { ...styles.wrapper, padding : value } } } ) }
                        min={ 0 }
                        max={ 200 }
                    />
                   <Dropdown
                        popoverProps={ { placement: 'left-start' } }
                        renderToggle={ ( { isOpen, onToggle } ) => (
                        <Button
                            onClick={ onToggle }
                            aria-expanded={ isOpen }
                        >
                            <ColorIndicator colorValue={styles.wrapper.background} />&nbsp;{ __( 'Select Background Color', 'WLC' ) }
                        </Button>
                        ) }
                        renderContent={ () =>
                            <ColorPalette
                                value={styles.wrapper.background}
                                colors= { globals.colors }
                                onChange={ value => setAttributes( { styles: { ...styles, wrapper: { ...styles.wrapper, background : value } } } ) }
                                enableAlpha
                                clearable
                                defaultValue="#ffffff"
                            />
                        }
                    />
                    <Dropdown
                        popoverProps={ { placement: 'left-start' } }
                        renderToggle={ ( { isOpen, onToggle } ) => (
                        <Button
                            onClick={ onToggle }
                            aria-expanded={ isOpen }
                        >
                            <ColorIndicator colorValue={styles.backdrop.background} />&nbsp;{ __( 'Select Backdrop Color', 'WLC' ) }
                        </Button>
                        ) }
                        renderContent={ () =>
                            <ColorPalette
                                value={styles.backdrop.background}
                                colors= { globals.colors }
                                onChange={ value => setAttributes( { styles: { ...styles, backdrop: { ...styles.backdrop, background : value } } } ) }
                                enableAlpha
                                clearable
                                defaultValue="#00000080"
                            />
                        }
                    />
                </PanelBody>
            </InspectorControls>
            <div { ...blockProps } { ...backdropStyles }>
                <ToggleControl
                    label={__( 'Preview Popup' ,'WLC' ) }
                    checked={ preview }
                    onChange={ value => setAttributes( { preview: value } ) }
                />
                <dialog className="wlc-popup__dialog" open={preview} { ...dialogStyles }>
                    <div className="wlc-popup__wrapper" { ...wrapperStyles }>
                        <InnerBlocks/>
                    </div>
                </dialog>
            </div>
        </>
    )
}

export default edit;

index.js

import { registerBlockType } from "@wordpress/blocks";
import metadata from './block.json';
import save from "./save";
import edit from './edit';
import './style.css'

registerBlockType( metadata, {
    edit: edit,
    save: save,
});

save.js

import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';

const save = ( { attributes } ) => {

    const { activation, anchor, delay, styles } = attributes;
    const dataProps= JSON.stringify( { activation, anchor, delay} );
    const blockProps = useBlockProps.save( { className: 'wlc-popup', 'data-props': dataProps } );
    const dialogStyles = {style: { ...styles.dialog, '--wlc-popup-backdrop-color': styles.backdrop.background } };
    const wrapperStyles = {style: { ...styles.wrapper } };

    return (
        <div { ...blockProps }>
            <dialog className="wlc-popup__dialog" id={anchor} { ...dialogStyles }>
                <div className="wlc-popup__wrapper" { ...wrapperStyles }>
                    <InnerBlocks.Content/>
                </div>
                <button className="wlc-popup__close" aria-label={ __( 'Close', 'WLC')}>&times;</button>
            </dialog>
        </div>
    )
}

export default save;

style.css

.wlc-popup__dialog {
    @apply relative overflow-auto;

    &::backdrop {
        background: var(--wlc-popup-backdrop-color, #000 );
    }

    &[open] {
        @apply block;
        animation: fadeIn 0.3s linear;
        &::backdrop {
            animation: fadeIn 0.3s linear;
        }
    }

    &.fading {
        animation: fadeOut 0.3s linear;
        &::backdrop {
            animation: fadeOut 0.3s linear;
        }
    }

    & .wlc-popup__close {
        @apply absolute p-0 leading-4 text-30 right-4 top-4 border-none;
    }
}

@keyframes fadeIn {
    from {
        opacity: 0;
    }
}
@keyframes fadeOut {
    to {
        opacity: 0;
    }
}

view.js

import domReady from '@wordpress/dom-ready';

class Popup {
    
    #dialog; //holds reference to the html dialog element
    #props; //holds the popup "data-props" settings

    constructor(el) {
        this.#dialog = el.querySelector('.wlc-popup__dialog');
        this.#props = JSON.parse( el.dataset.props );
        this.#init();
    }

    open() {
        if (!this.#dialog.open) {
            this.#dialog.showModal();
            this.#dispatch( 'open' );
        }
    }

    async close() {
        if (this.#dialog.open) {
            await this.#fade();
            this.#dialog.close();
            this.#dispatch( 'close' );
        }
    }

    openAfterDelay(ms) {
        setTimeout( () => this.open(), ms );
    }

    #init() {
        const closeBtn = this.#dialog.querySelector('.wlc-popup__close');
        closeBtn.addEventListener( 'click', () => this.close() );
        if (this.#props.activation.manual) {
            document.addEventListener('click', e => {
                let url;
                const id = this.#dialog.id;
                if (!id ) return;

                if ( e.target.href ) {
                    url = new URL(e.target.href);
                    url = url.hash;
                }
                if ( url == `#${id}` || e.target.dataset?.popupOpen == id ) {
                    e.preventDefault();
                    this.open();
                }
            } );
        }
        if (this.#props.activation.delayed) {
            const delay = this.#props.delay || 0;
            this.openAfterDelay(delay);
        }
        if (this.#props.activation.backdropClose) {
            this.#dialog.addEventListener('click', e => {
                if ( this.#dialog.open && e.target.closest('.wlc-popup__wrapper') == null )
                this.close();
            });
        }
    }

    #fade() {
        this.#dialog.classList.add('fading');
        return new Promise( resolve => {
            const listen = () => {
                this.#dialog.removeEventListener('animationend', listen );
                this.#dialog.classList.remove('fading');
                resolve();
            }
            this.#dialog.addEventListener('animationend', listen )
        })
    }

    getYTvideos() {
        let YTvideos = Array();
        const iframes = Array.from( this.#dialog.querySelectorAll('iframe') );
        if ( iframes.length > 0 ) {
            YTvideos = iframes.filter( iframe => {
                let iframeUrl = URL.parse( iframe.src );
                return (iframeUrl.origin == 'https://www.youtube.com' && iframeUrl.pathname.substring( 0, 7) == '/embed/' );
            } );
        }
        return YTvideos;
    }

    get hasAutoplay() {
        return this.#props.activation.autoplay;
    }

    on ( eventName, callback ) {
        this.#dialog.addEventListener( eventName, callback );
    }

    #dispatch( eventName ) {
        const event = new Event( eventName );
        this.#dialog.dispatchEvent( event );
    }

    get hasYTvideos() {
        const YTvideos = this.getYTvideos();
        return YTvideos.length > 0;
    }
}

const setVideos = popup => {
    const YTvideos = popup.getYTvideos();
    YTvideos.forEach( video => {
        const url = video.src;
        popup.on( 'close', () => {
            video.src = url;
        } )
        if ( popup.hasAutoplay ) {
            popup.on( 'open', () => {
                let url = new URL(video.src);
                url.searchParams.append('autoplay', '1');
                video.src = url.href;
            } );
        }
    });
    
}

domReady( () => {
    const popups = document.querySelectorAll('.wlc-popup');
    popups.forEach( el => {
        const popup = new Popup( el );
        if (popup.hasYTvideos) {
            setVideos(popup);
        }
    });
});