Customisable popup block
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} /> { __( '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} /> { __( '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')}>×</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);
}
});
});