Image Comparison

block.json

{
    "$schema": "https://schemas.wp.org/trunk/block.json",
    "apiVersion": 3,
    "name": "wlc/image-comparison",
    "title": "Image Comparison",
    "category": "theme",
    "icon": "image-flip-horizontal",
    "description": "Image comparison",
    "keywords": [ "image", "comparison" ],
    "version": "1.0.0",
    "textdomain": "WLC",
    "supports": {
        "renaming": true,
        "align": true,
        "anchor": true,
        "shadow": true,
        "spacing": {
            "margin": true
        }
    },
    "attributes": {
        "vertical": {
            "type": "boolean",
            "default": false
        },
        "grabLength": {
            "type": "string",
            "default": 100
        },
        "grabWidth": {
            "type": "string",
            "default": 5
        },
        "displayIcon": {
            "type": "boolean",
            "default": true
        }
    },
    "editorScript": "file:./index.js",
    "viewScript": "file:./view.js",
    "viewStyle": "file:./style-index.css",
    "editorStyle": "file:./index.css"
}

edit.js

import { useBlockProps, InspectorControls,InnerBlocks } from '@wordpress/block-editor'
import { PanelBody, ToggleControl, TextControl } from '@wordpress/components'
import { __ } from '@wordpress/i18n'

const edit = ( { attributes: {vertical, grabLength, grabWidth, displayIcon }, setAttributes } ) => {

    const blockProps = useBlockProps( {
        className: `wlc-image-comparison ${ vertical ? 'vertical' : '' }`
    } );

    const allowedBlocks = [ 'core/image' ]
    const innerTemplate = [
        [ 'core/image', { className: 'first' } ],
        [ 'core/image', { className: 'second' } ]
    ]

    const setVertical = vertical => setAttributes( { vertical } );
    const setGrabLength = grabLength => setAttributes( { grabLength } );
    const setGrabWidth = grabWidth => setAttributes( { grabWidth } );
    const setDisplayIcon = displayIcon => setAttributes( { displayIcon } );

    const grabStyle = { 
        [vertical ? 'width' : 'height' ]: `${grabLength}%`,
        [vertical ? 'height' : 'width' ]: `${grabWidth/16}rem`,
        '--display-icon': displayIcon ? 'block' : 'none'
    }


    return (
        <>
            <InspectorControls>
                <PanelBody title={ __( 'Comparison Setings', 'WLC') }>
                    <ToggleControl
                        label={ __( 'Vertical', 'WLC') }
                        checked={ vertical }
                        onChange={ setVertical }
                    />
                    <ToggleControl
                        label={ __( 'Handle Icon', 'WLC') }
                        checked={ displayIcon }
                        onChange={ setDisplayIcon }
                    />
                    <TextControl
                        label={ __( 'Divider length in %', 'WLC') }
                        type="number"
                        min="0"
                        max="100"
                        value={ grabLength }
                        onChange={ setGrabLength }
                    />
                    <TextControl
                        label={ __( 'Divider width', 'WLC') }
                        type="number"
                        min="0"
                        max="10"
                        value={ grabWidth }
                        onChange={ setGrabWidth }
                    />
                </PanelBody>
            </InspectorControls>
            <div { ...blockProps }>
                <InnerBlocks
                    template={ innerTemplate }
                    allowedBlocks={ allowedBlocks }
                    templateLock="all"
                />
                <div className="grab" style={ grabStyle }/>
            </div>
        </>
    )
}

export default edit;

editor.css

.wlc-image-comparison {
    & .block-editor-block-list__layout {
        @apply grid grid-cols-2 grid-rows-2 gap-5;
    }

    .wp-block-image {
        @apply row-start-1 row-span-2 col-start-1 col-span-2;

        &:has(.components-placeholder) {
            @apply row-start-1 row-span-2 col-span-1 first:col-start-2 last:col-start-1;
        }

        &.first {
            &.is-selected {
                @apply order-2;
            }
        }

        &.second {
            &:not(:has(.components-placeholder)) {
                clip-path: xywh( 0 0 50% 100%);

                .vertical & {
                    clip-path: xywh( 0 0 100% 50%);
                }

          
                
            }

            
            &.is-selected {
                clip-path:none !important;
            }
        }
    }

    & > .grab {
        @apply absolute block bg-white w-[.3125rem] rounded-full;
        @apply before:block before:absolute; 
        @apply before:content-icon-arrows-horizontal before:bg-white before:w-8 before:h-8;
        @apply before:leading-0 before:content-center before:text-center before:rounded-full;
        @apply before:left-1/2 before:-translate-x-1/2 before:top-1/2 before:-translate-y-1/2;

        &::before {
            display: var(--display-icon, block);
        }

        :not(.vertical) > & {
            @apply w-[.3125rem] h-full top-1/2 -translate-y-1/2 left-[var(--x,50%)] -translate-x-1/2;
        }

        .vertical > & {
            @apply h-[.3125rem] w-full left-1/2 -translate-x-1/2 top-[var(--y,50%)] -translate-y-1/2;
            @apply before:rotate-90;
        }
    }

}

index.js

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

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

save.js

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

const save = ( { attributes: { vertical, grabLength, grabWidth, displayIcon } } ) => {

    const blockProps = useBlockProps.save( {
        className: 'wlc-image-comparison'
    } );

    const grabStyle = { 
        [vertical ? 'width' : 'height' ]: `${grabLength}%`,
        [vertical ? 'height' : 'width' ]: `${grabWidth/16}rem`,
        '--display-icon': displayIcon ? 'block' : 'none'
    }

    const context = JSON.stringify( {
        x: vertical ? '100%' : '50%',
        y: vertical ? '50%': '100%',
        vertical
    } )

    return (
        <div 
            { ...blockProps}
            data-context={context}
        >
            <InnerBlocks.Content/>
            <div className="grab" style={ grabStyle }></div>
        </div> 
    )
}

export default save;

style.css

.wlc-image-comparison {
    @apply relative grid grid-rows-1 grid-cols-1;

    & > .wp-block-image {

        & {
            @apply row-start-1 col-start-1;
        }

        &.second {
            clip-path: xywh(0 0 var(--x, 50%) var(--y, 50%) );
        }

        img {
            @apply pointer-events-none select-none;
        }
    }

    & > .grab {
        @apply absolute block bg-white w-[.3125rem] rounded-full;
        @apply before:block before:absolute;
        @apply before:content-icon-arrows-horizontal before:bg-white before:w-8 before:h-8;
        @apply before:leading-0 before:content-center before:text-center before:rounded-full;
        @apply before:left-1/2 before:-translate-x-1/2 before:top-1/2 before:-translate-y-1/2;

        &::before {
            display: var(--display-icon, block);
        }

        :not(.vertical) > & {
            @apply w-[.3125rem] h-full top-1/2 -translate-y-1/2 left-[var(--x,50%)] -translate-x-1/2;
        }

        .vertical > & {
            @apply h-[.3125rem] w-full left-1/2 -translate-x-1/2 top-[var(--y,50%)] -translate-y-1/2;
            @apply before:rotate-90;
        }
    }
}

view.js

const comparisons = document.querySelectorAll('.wlc-image-comparison');

const getContext = element => JSON.parse( element.dataset.context )

const setProps = (element, x, y) => {
    element.style.setProperty( '--x', x )
    element.style.setProperty( '--y', y )
}

const minmax = value => Math.max(Math.min(value, 100), 0 )

const setCoords = ({ currentTarget, clientX, clientY }) => {
    
    const { vertical } = getContext(currentTarget)
    
    const { left, top } = currentTarget.getBoundingClientRect()
    
    const x = (clientX-left)/currentTarget.clientWidth*100
    const y = (clientY-top)/currentTarget.clientHeight*100

    const coords = { 
        x: vertical ? 100 : minmax(x),
        y: vertical ? minmax(y) : 100,
    };
    
    setProps( currentTarget, `${coords.x}%`, `${coords.y}%` )

}

const setComparison = comparison => {
    const { x, y, vertical } = getContext(comparison);
    setProps( comparison, x, y )
    comparison.classList.toggle('vertical', vertical );
    comparison.querySelectorAll('.wp-block-image img').forEach(img => {
        img.draggable=false;
    })

    comparison.addEventListener('mousedown', e => {
        setCoords(e);
        comparison.addEventListener('mousemove', setCoords );
        document.addEventListener('mouseup', e => {
            comparison.removeEventListener('mousemove', setCoords);
        })
    }, true )

}

comparisons.forEach( setComparison )