From 614b1e5a910e415c3dfe1c2c0bc3f49e9426ce6d Mon Sep 17 00:00:00 2001 From: castellon Date: Thu, 30 Apr 2026 08:28:25 +0200 Subject: [PATCH 1/7] new block --- blocks/before-after/block.json | 50 ++++++ blocks/before-after/edit.js | 211 ++++++++++++++++++++++ blocks/before-after/edit.jsx | 284 ++++++++++++++++++++++++++++++ blocks/before-after/style.css | 130 ++++++++++++++ blocks/before-after/view.js | 104 +++++++++++ includes/Frontend/BeforeAfter.php | 133 ++++++++++++++ includes/Plugin_Main.php | 3 + package.json | 5 +- 8 files changed, 918 insertions(+), 2 deletions(-) create mode 100644 blocks/before-after/block.json create mode 100644 blocks/before-after/edit.js create mode 100644 blocks/before-after/edit.jsx create mode 100644 blocks/before-after/style.css create mode 100644 blocks/before-after/view.js create mode 100644 includes/Frontend/BeforeAfter.php diff --git a/blocks/before-after/block.json b/blocks/before-after/block.json new file mode 100644 index 0000000..41dd5b5 --- /dev/null +++ b/blocks/before-after/block.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "frontblocks/before-after", + "version": "1.0.0", + "title": "Before After", + "category": "media", + "icon": "image-flip-horizontal", + "description": "Compare two images with a draggable before/after slider.", + "keywords": [ "before", "after", "comparison", "slider", "image" ], + "textdomain": "frontblocks", + "attributes": { + "beforeImageId": { + "type": "integer" + }, + "beforeImageUrl": { + "type": "string", + "default": "" + }, + "afterImageId": { + "type": "integer" + }, + "afterImageUrl": { + "type": "string", + "default": "" + }, + "beforeLabel": { + "type": "string", + "default": "Before" + }, + "afterLabel": { + "type": "string", + "default": "After" + }, + "initialPosition": { + "type": "number", + "default": 50 + }, + "align": { + "type": "string" + } + }, + "supports": { + "html": false, + "align": [ "wide", "full" ] + }, + "editorScript": "frontblocks-before-after-editor", + "viewScript": "frontblocks-before-after-view", + "style": "frontblocks-before-after-style" +} diff --git a/blocks/before-after/edit.js b/blocks/before-after/edit.js new file mode 100644 index 0000000..d569445 --- /dev/null +++ b/blocks/before-after/edit.js @@ -0,0 +1,211 @@ +"use strict"; + +(function () { + var registerBlockType = wp.blocks.registerBlockType; + var _wp$blockEditor = wp.blockEditor, + MediaUpload = _wp$blockEditor.MediaUpload, + MediaUploadCheck = _wp$blockEditor.MediaUploadCheck, + InspectorControls = _wp$blockEditor.InspectorControls, + RichText = _wp$blockEditor.RichText, + useBlockProps = _wp$blockEditor.useBlockProps; + var _wp$components = wp.components, + PanelBody = _wp$components.PanelBody, + Button = _wp$components.Button, + RangeControl = _wp$components.RangeControl, + TextControl = _wp$components.TextControl, + Placeholder = _wp$components.Placeholder; + var __ = wp.i18n.__; + function EditBeforeAfter(_ref) { + var attributes = _ref.attributes, + setAttributes = _ref.setAttributes; + var beforeImageId = attributes.beforeImageId, + beforeImageUrl = attributes.beforeImageUrl, + afterImageId = attributes.afterImageId, + afterImageUrl = attributes.afterImageUrl, + beforeLabel = attributes.beforeLabel, + afterLabel = attributes.afterLabel, + initialPosition = attributes.initialPosition; + var blockProps = useBlockProps({ + className: 'frbl-before-after frbl-before-after--editor', + 'data-initial-position': initialPosition + }); + var hasImages = beforeImageUrl && afterImageUrl; + return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(InspectorControls, null, /*#__PURE__*/React.createElement(PanelBody, { + title: __('Before Image', 'frontblocks'), + initialOpen: true + }, /*#__PURE__*/React.createElement(MediaUploadCheck, null, /*#__PURE__*/React.createElement(MediaUpload, { + onSelect: function onSelect(media) { + return setAttributes({ + beforeImageId: media.id, + beforeImageUrl: media.url + }); + }, + allowedTypes: ['image'], + value: beforeImageId, + render: function render(_ref2) { + var open = _ref2.open; + return /*#__PURE__*/React.createElement(React.Fragment, null, beforeImageUrl && /*#__PURE__*/React.createElement("img", { + src: beforeImageUrl, + alt: "", + style: { + width: '100%', + marginBottom: '8px', + borderRadius: '2px' + } + }), /*#__PURE__*/React.createElement(Button, { + onClick: open, + variant: beforeImageUrl ? 'secondary' : 'primary', + style: { + width: '100%' + } + }, beforeImageUrl ? __('Replace Before Image', 'frontblocks') : __('Select Before Image', 'frontblocks')), beforeImageUrl && /*#__PURE__*/React.createElement(Button, { + onClick: function onClick() { + return setAttributes({ + beforeImageId: undefined, + beforeImageUrl: '' + }); + }, + variant: "link", + isDestructive: true, + style: { + marginTop: '4px', + display: 'block' + } + }, __('Remove', 'frontblocks'))); + } + })), /*#__PURE__*/React.createElement(TextControl, { + label: __('Before Label', 'frontblocks'), + value: beforeLabel, + onChange: function onChange(val) { + return setAttributes({ + beforeLabel: val + }); + }, + style: { + marginTop: '12px' + } + })), /*#__PURE__*/React.createElement(PanelBody, { + title: __('After Image', 'frontblocks'), + initialOpen: true + }, /*#__PURE__*/React.createElement(MediaUploadCheck, null, /*#__PURE__*/React.createElement(MediaUpload, { + onSelect: function onSelect(media) { + return setAttributes({ + afterImageId: media.id, + afterImageUrl: media.url + }); + }, + allowedTypes: ['image'], + value: afterImageId, + render: function render(_ref3) { + var open = _ref3.open; + return /*#__PURE__*/React.createElement(React.Fragment, null, afterImageUrl && /*#__PURE__*/React.createElement("img", { + src: afterImageUrl, + alt: "", + style: { + width: '100%', + marginBottom: '8px', + borderRadius: '2px' + } + }), /*#__PURE__*/React.createElement(Button, { + onClick: open, + variant: afterImageUrl ? 'secondary' : 'primary', + style: { + width: '100%' + } + }, afterImageUrl ? __('Replace After Image', 'frontblocks') : __('Select After Image', 'frontblocks')), afterImageUrl && /*#__PURE__*/React.createElement(Button, { + onClick: function onClick() { + return setAttributes({ + afterImageId: undefined, + afterImageUrl: '' + }); + }, + variant: "link", + isDestructive: true, + style: { + marginTop: '4px', + display: 'block' + } + }, __('Remove', 'frontblocks'))); + } + })), /*#__PURE__*/React.createElement(TextControl, { + label: __('After Label', 'frontblocks'), + value: afterLabel, + onChange: function onChange(val) { + return setAttributes({ + afterLabel: val + }); + }, + style: { + marginTop: '12px' + } + })), /*#__PURE__*/React.createElement(PanelBody, { + title: __('Slider Settings', 'frontblocks'), + initialOpen: false + }, /*#__PURE__*/React.createElement(RangeControl, { + label: __('Initial Handle Position (%)', 'frontblocks'), + value: initialPosition, + onChange: function onChange(val) { + return setAttributes({ + initialPosition: val + }); + }, + min: 0, + max: 100 + }))), !hasImages ? /*#__PURE__*/React.createElement("div", blockProps, /*#__PURE__*/React.createElement(Placeholder, { + icon: "image-flip-horizontal", + label: __('Before / After', 'frontblocks'), + instructions: __('Select a "before" image and an "after" image from the sidebar.', 'frontblocks') + })) : /*#__PURE__*/React.createElement("div", blockProps, /*#__PURE__*/React.createElement("div", { + className: "frbl-before-after__after" + }, /*#__PURE__*/React.createElement("img", { + src: afterImageUrl, + alt: "" + }), /*#__PURE__*/React.createElement("span", { + className: "frbl-before-after__label frbl-before-after__label--after" + }, afterLabel)), /*#__PURE__*/React.createElement("div", { + className: "frbl-before-after__before", + style: { + clipPath: "inset(0 ".concat(100 - initialPosition, "% 0 0)") + } + }, /*#__PURE__*/React.createElement("img", { + src: beforeImageUrl, + alt: "" + }), /*#__PURE__*/React.createElement("span", { + className: "frbl-before-after__label frbl-before-after__label--before" + }, beforeLabel)), /*#__PURE__*/React.createElement("div", { + className: "frbl-before-after__handle", + style: { + left: "".concat(initialPosition, "%") + } + }, /*#__PURE__*/React.createElement("span", { + className: "frbl-before-after__handle-line" + }), /*#__PURE__*/React.createElement("span", { + className: "frbl-before-after__handle-thumb" + }, /*#__PURE__*/React.createElement("svg", { + viewBox: "0 0 24 24", + width: "20", + height: "20", + fill: "none", + stroke: "currentColor", + strokeWidth: "2", + strokeLinecap: "round", + strokeLinejoin: "round" + }, /*#__PURE__*/React.createElement("polyline", { + points: "15 18 9 12 15 6" + }), /*#__PURE__*/React.createElement("polyline", { + points: "9 18 3 12 9 6" + }), /*#__PURE__*/React.createElement("polyline", { + points: "15 18 21 12 15 6" + }), /*#__PURE__*/React.createElement("polyline", { + points: "9 18 15 12 9 6" + }))), /*#__PURE__*/React.createElement("span", { + className: "frbl-before-after__handle-line" + })))); + } + registerBlockType('frontblocks/before-after', { + edit: EditBeforeAfter, + save: function save() { + return null; + } + }); +})(); diff --git a/blocks/before-after/edit.jsx b/blocks/before-after/edit.jsx new file mode 100644 index 0000000..5b14d47 --- /dev/null +++ b/blocks/before-after/edit.jsx @@ -0,0 +1,284 @@ +( function () { + const { registerBlockType } = wp.blocks; + const { + MediaUpload, + MediaUploadCheck, + InspectorControls, + RichText, + useBlockProps, + } = wp.blockEditor; + const { PanelBody, Button, RangeControl, TextControl, Placeholder } = + wp.components; + const { __ } = wp.i18n; + + function EditBeforeAfter( { attributes, setAttributes } ) { + const { + beforeImageId, + beforeImageUrl, + afterImageId, + afterImageUrl, + beforeLabel, + afterLabel, + initialPosition, + } = attributes; + + const blockProps = useBlockProps( { + className: 'frbl-before-after frbl-before-after--editor', + 'data-initial-position': initialPosition, + } ); + + const hasImages = beforeImageUrl && afterImageUrl; + + return ( + <> + + + + + setAttributes( { + beforeImageId: media.id, + beforeImageUrl: media.url, + } ) + } + allowedTypes={ [ 'image' ] } + value={ beforeImageId } + render={ ( { open } ) => ( + <> + { beforeImageUrl && ( + + ) } + + { beforeImageUrl && ( + + ) } + + ) } + /> + + + setAttributes( { beforeLabel: val } ) + } + style={ { marginTop: '12px' } } + /> + + + + + + setAttributes( { + afterImageId: media.id, + afterImageUrl: media.url, + } ) + } + allowedTypes={ [ 'image' ] } + value={ afterImageId } + render={ ( { open } ) => ( + <> + { afterImageUrl && ( + + ) } + + { afterImageUrl && ( + + ) } + + ) } + /> + + + setAttributes( { afterLabel: val } ) + } + style={ { marginTop: '12px' } } + /> + + + + + setAttributes( { initialPosition: val } ) + } + min={ 0 } + max={ 100 } + /> + + + + { ! hasImages ? ( +
+ +
+ ) : ( +
+
+ + + { afterLabel } + +
+
+ + + { beforeLabel } + +
+
+ + + + + + + + + + +
+
+ ) } + + ); + } + + registerBlockType( 'frontblocks/before-after', { + edit: EditBeforeAfter, + save: function () { + return null; + }, + } ); +} )(); diff --git a/blocks/before-after/style.css b/blocks/before-after/style.css new file mode 100644 index 0000000..8380278 --- /dev/null +++ b/blocks/before-after/style.css @@ -0,0 +1,130 @@ +/* ============================================================================= + Before After Block — FrontBlocks + ============================================================================= */ + +.frbl-before-after { + position: relative; + overflow: hidden; + cursor: col-resize; + -webkit-user-select: none; + user-select: none; + touch-action: pan-y; + display: block; + width: 100%; +} + +/* After image — defines the block height */ +.frbl-before-after__after { + position: relative; + width: 100%; + display: block; +} + +.frbl-before-after__after img { + display: block; + width: 100%; + height: auto; + pointer-events: none; +} + +/* Before image — absolutely positioned on top, clipped from the right */ +.frbl-before-after__before { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + clip-path: inset( 0 50% 0 0 ); +} + +.frbl-before-after__before img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + pointer-events: none; +} + +/* Labels */ +.frbl-before-after__label { + position: absolute; + top: 16px; + padding: 4px 12px; + background: rgba( 0, 0, 0, 0.55 ); + color: #fff; + font-size: 13px; + font-weight: 600; + line-height: 1.4; + border-radius: 3px; + letter-spacing: 0.02em; + z-index: 5; + pointer-events: none; +} + +.frbl-before-after__label--before { + left: 16px; +} + +.frbl-before-after__label--after { + right: 16px; +} + +/* Handle */ +.frbl-before-after__handle { + position: absolute; + top: 0; + bottom: 0; + left: 50%; + transform: translateX( -50% ); + width: 44px; + display: flex; + flex-direction: column; + align-items: center; + z-index: 10; + cursor: col-resize; +} + +.frbl-before-after__handle:focus { + outline: none; +} + +.frbl-before-after__handle:focus-visible .frbl-before-after__handle-thumb { + outline: 2px solid #007cba; + outline-offset: 2px; +} + +.frbl-before-after__handle-line { + flex: 1; + width: 2px; + background: #fff; + box-shadow: 0 0 4px rgba( 0, 0, 0, 0.4 ); +} + +.frbl-before-after__handle-thumb { + flex-shrink: 0; + width: 44px; + height: 44px; + border-radius: 50%; + background: #fff; + box-shadow: 0 2px 10px rgba( 0, 0, 0, 0.35 ); + display: flex; + align-items: center; + justify-content: center; + color: #555; +} + +.frbl-before-after__handle-thumb svg { + display: block; + flex-shrink: 0; +} + +/* Editor preview adjustments */ +.frbl-before-after--editor { + cursor: default; +} + +.frbl-before-after--editor .frbl-before-after__handle { + cursor: default; + pointer-events: none; +} diff --git a/blocks/before-after/view.js b/blocks/before-after/view.js new file mode 100644 index 0000000..7b04320 --- /dev/null +++ b/blocks/before-after/view.js @@ -0,0 +1,104 @@ +( function () { + 'use strict'; + + function initBeforeAfterBlock( block ) { + var initialPosition = + parseFloat( block.dataset.initialPosition ) || 50; + var beforeEl = block.querySelector( '.frbl-before-after__before' ); + var handle = block.querySelector( '.frbl-before-after__handle' ); + + if ( ! beforeEl || ! handle ) { + return; + } + + var isDragging = false; + + function clamp( value, min, max ) { + return Math.max( min, Math.min( max, value ) ); + } + + function setPosition( pos ) { + pos = clamp( pos, 0, 100 ); + beforeEl.style.clipPath = + 'inset(0 ' + ( 100 - pos ) + '% 0 0)'; + handle.style.left = pos + '%'; + handle.setAttribute( 'aria-valuenow', Math.round( pos ) ); + } + + function getPositionFromEvent( e ) { + var rect = block.getBoundingClientRect(); + var clientX = + e.touches && e.touches.length + ? e.touches[ 0 ].clientX + : e.clientX; + return ( ( clientX - rect.left ) / rect.width ) * 100; + } + + setPosition( initialPosition ); + + handle.addEventListener( 'mousedown', function ( e ) { + isDragging = true; + e.preventDefault(); + } ); + + handle.addEventListener( + 'touchstart', + function () { + isDragging = true; + }, + { passive: true } + ); + + document.addEventListener( 'mousemove', function ( e ) { + if ( ! isDragging ) { + return; + } + setPosition( getPositionFromEvent( e ) ); + } ); + + document.addEventListener( + 'touchmove', + function ( e ) { + if ( ! isDragging ) { + return; + } + setPosition( getPositionFromEvent( e ) ); + }, + { passive: true } + ); + + document.addEventListener( 'mouseup', function () { + isDragging = false; + } ); + + document.addEventListener( 'touchend', function () { + isDragging = false; + } ); + + block.addEventListener( 'keydown', function ( e ) { + var current = parseFloat( handle.style.left ) || initialPosition; + if ( e.key === 'ArrowLeft' ) { + setPosition( current - 1 ); + e.preventDefault(); + } else if ( e.key === 'ArrowRight' ) { + setPosition( current + 1 ); + e.preventDefault(); + } + } ); + + handle.setAttribute( 'tabindex', '0' ); + } + + function init() { + var blocks = document.querySelectorAll( + '.frbl-before-after:not(.frbl-before-after--editor)' + ); + blocks.forEach( initBeforeAfterBlock ); + } + + if ( document.readyState === 'loading' ) { + document.addEventListener( 'DOMContentLoaded', init ); + } else { + init(); + } +} )(); diff --git a/includes/Frontend/BeforeAfter.php b/includes/Frontend/BeforeAfter.php new file mode 100644 index 0000000..fe79ff7 --- /dev/null +++ b/includes/Frontend/BeforeAfter.php @@ -0,0 +1,133 @@ + array( $this, 'render_block' ), + ) + ); + } + + /** + * Render the block on the frontend. + * + * @param array $attributes Block attributes. + * @return string Block HTML. + */ + public function render_block( $attributes ) { + $before_url = isset( $attributes['beforeImageUrl'] ) ? $attributes['beforeImageUrl'] : ''; + $after_url = isset( $attributes['afterImageUrl'] ) ? $attributes['afterImageUrl'] : ''; + + if ( empty( $before_url ) || empty( $after_url ) ) { + return ''; + } + + $initial_position = isset( $attributes['initialPosition'] ) ? (int) $attributes['initialPosition'] : 50; + $before_label = isset( $attributes['beforeLabel'] ) ? $attributes['beforeLabel'] : __( 'Before', 'frontblocks' ); + $after_label = isset( $attributes['afterLabel'] ) ? $attributes['afterLabel'] : __( 'After', 'frontblocks' ); + $align = isset( $attributes['align'] ) ? sanitize_html_class( 'align' . $attributes['align'] ) : ''; + + $classes = trim( 'wp-block-frontblocks-before-after frbl-before-after ' . $align ); + + ob_start(); + ?> +
+
+ + + + +
+
+ + + + +
+
+ + + + + +
+
+ Date: Thu, 30 Apr 2026 08:38:02 +0200 Subject: [PATCH 2/7] fix pattern --- .../frontblocks-before-after-frontend.js | 57 ++-- .../frontblocks-before-after-option.jsx | 245 +++++++++++++++ .../before-after/frontblocks-before-after.css | 3 +- .../before-after/frontblocks-before-after.js | 244 +++++++++++++++ blocks/before-after/block.json | 50 --- blocks/before-after/edit.js | 211 ------------- blocks/before-after/edit.jsx | 284 ------------------ includes/Frontend/BeforeAfter.php | 135 ++++++--- package.json | 2 +- 9 files changed, 607 insertions(+), 624 deletions(-) rename blocks/before-after/view.js => assets/before-after/frontblocks-before-after-frontend.js (59%) create mode 100644 assets/before-after/frontblocks-before-after-option.jsx rename blocks/before-after/style.css => assets/before-after/frontblocks-before-after.css (97%) create mode 100644 assets/before-after/frontblocks-before-after.js delete mode 100644 blocks/before-after/block.json delete mode 100644 blocks/before-after/edit.js delete mode 100644 blocks/before-after/edit.jsx diff --git a/blocks/before-after/view.js b/assets/before-after/frontblocks-before-after-frontend.js similarity index 59% rename from blocks/before-after/view.js rename to assets/before-after/frontblocks-before-after-frontend.js index 7b04320..d160e7b 100644 --- a/blocks/before-after/view.js +++ b/assets/before-after/frontblocks-before-after-frontend.js @@ -2,10 +2,9 @@ 'use strict'; function initBeforeAfterBlock( block ) { - var initialPosition = - parseFloat( block.dataset.initialPosition ) || 50; - var beforeEl = block.querySelector( '.frbl-before-after__before' ); - var handle = block.querySelector( '.frbl-before-after__handle' ); + var initialPosition = parseFloat( block.dataset.initialPosition ) || 50; + var beforeEl = block.querySelector( '.frbl-before-after__before' ); + var handle = block.querySelector( '.frbl-before-after__handle' ); if ( ! beforeEl || ! handle ) { return; @@ -19,18 +18,14 @@ function setPosition( pos ) { pos = clamp( pos, 0, 100 ); - beforeEl.style.clipPath = - 'inset(0 ' + ( 100 - pos ) + '% 0 0)'; - handle.style.left = pos + '%'; + beforeEl.style.clipPath = 'inset(0 ' + ( 100 - pos ) + '% 0 0)'; + handle.style.left = pos + '%'; handle.setAttribute( 'aria-valuenow', Math.round( pos ) ); } function getPositionFromEvent( e ) { - var rect = block.getBoundingClientRect(); - var clientX = - e.touches && e.touches.length - ? e.touches[ 0 ].clientX - : e.clientX; + var rect = block.getBoundingClientRect(); + var clientX = e.touches && e.touches.length ? e.touches[ 0 ].clientX : e.clientX; return ( ( clientX - rect.left ) / rect.width ) * 100; } @@ -41,31 +36,19 @@ e.preventDefault(); } ); - handle.addEventListener( - 'touchstart', - function () { - isDragging = true; - }, - { passive: true } - ); + handle.addEventListener( 'touchstart', function () { + isDragging = true; + }, { passive: true } ); document.addEventListener( 'mousemove', function ( e ) { - if ( ! isDragging ) { - return; - } + if ( ! isDragging ) { return; } setPosition( getPositionFromEvent( e ) ); } ); - document.addEventListener( - 'touchmove', - function ( e ) { - if ( ! isDragging ) { - return; - } - setPosition( getPositionFromEvent( e ) ); - }, - { passive: true } - ); + document.addEventListener( 'touchmove', function ( e ) { + if ( ! isDragging ) { return; } + setPosition( getPositionFromEvent( e ) ); + }, { passive: true } ); document.addEventListener( 'mouseup', function () { isDragging = false; @@ -75,7 +58,9 @@ isDragging = false; } ); - block.addEventListener( 'keydown', function ( e ) { + handle.setAttribute( 'tabindex', '0' ); + + handle.addEventListener( 'keydown', function ( e ) { var current = parseFloat( handle.style.left ) || initialPosition; if ( e.key === 'ArrowLeft' ) { setPosition( current - 1 ); @@ -85,14 +70,10 @@ e.preventDefault(); } } ); - - handle.setAttribute( 'tabindex', '0' ); } function init() { - var blocks = document.querySelectorAll( - '.frbl-before-after:not(.frbl-before-after--editor)' - ); + var blocks = document.querySelectorAll( '.frbl-before-after:not(.frbl-before-after--editor)' ); blocks.forEach( initBeforeAfterBlock ); } diff --git a/assets/before-after/frontblocks-before-after-option.jsx b/assets/before-after/frontblocks-before-after-option.jsx new file mode 100644 index 0000000..b86158c --- /dev/null +++ b/assets/before-after/frontblocks-before-after-option.jsx @@ -0,0 +1,245 @@ +const { registerBlockType } = wp.blocks; +const { Fragment } = wp.element; +const { InspectorControls, MediaUpload, MediaUploadCheck, useBlockProps } = wp.blockEditor; +const { PanelBody, RangeControl, Button, Placeholder, TextControl } = wp.components; +const { __ } = wp.i18n; + +/** + * Edit component for Before After block. + */ +function BeforeAfterEdit( props ) { + const { attributes, setAttributes } = props; + const { + beforeImageId, + beforeImageUrl, + afterImageId, + afterImageUrl, + beforeLabel, + afterLabel, + initialPosition, + } = attributes; + + const blockProps = useBlockProps(); + + const hasImages = beforeImageUrl && afterImageUrl; + + return ( + + + + + + setAttributes( { + beforeImageId: media.id, + beforeImageUrl: media.url, + } ) + } + allowedTypes={ [ 'image' ] } + value={ beforeImageId } + render={ ( { open } ) => ( +
+ { beforeImageUrl && ( + + ) } + + { beforeImageUrl && ( + + ) } +
+ ) } + /> +
+ setAttributes( { beforeLabel: value } ) } + style={ { marginTop: '12px' } } + /> +
+ + + + + setAttributes( { + afterImageId: media.id, + afterImageUrl: media.url, + } ) + } + allowedTypes={ [ 'image' ] } + value={ afterImageId } + render={ ( { open } ) => ( +
+ { afterImageUrl && ( + + ) } + + { afterImageUrl && ( + + ) } +
+ ) } + /> +
+ setAttributes( { afterLabel: value } ) } + style={ { marginTop: '12px' } } + /> +
+ + + setAttributes( { initialPosition: value } ) } + min={ 0 } + max={ 100 } + /> + +
+ +
+ { ! hasImages ? ( + + ) : ( +
+
+ + + { afterLabel } + +
+
+ + + { beforeLabel } + +
+
+ + + + + +
+
+ ) } +
+
+ ); +} + +// Register the block. +registerBlockType( 'frontblocks/before-after', { + title: __( 'Before After', 'frontblocks' ), + description: __( 'Compare two images with a draggable before/after slider.', 'frontblocks' ), + category: 'media', + icon: 'image-flip-horizontal', + keywords: [ + __( 'before', 'frontblocks' ), + __( 'after', 'frontblocks' ), + __( 'comparison', 'frontblocks' ), + __( 'slider', 'frontblocks' ), + ], + attributes: { + beforeImageId: { + type: 'integer', + }, + beforeImageUrl: { + type: 'string', + default: '', + }, + afterImageId: { + type: 'integer', + }, + afterImageUrl: { + type: 'string', + default: '', + }, + beforeLabel: { + type: 'string', + default: 'Before', + }, + afterLabel: { + type: 'string', + default: 'After', + }, + initialPosition: { + type: 'number', + default: 50, + }, + }, + edit: BeforeAfterEdit, + save: function () { + return null; // Dynamic block, rendered server-side. + }, +} ); diff --git a/blocks/before-after/style.css b/assets/before-after/frontblocks-before-after.css similarity index 97% rename from blocks/before-after/style.css rename to assets/before-after/frontblocks-before-after.css index 8380278..9e03dc3 100644 --- a/blocks/before-after/style.css +++ b/assets/before-after/frontblocks-before-after.css @@ -116,10 +116,9 @@ .frbl-before-after__handle-thumb svg { display: block; - flex-shrink: 0; } -/* Editor preview adjustments */ +/* Editor — disable interaction */ .frbl-before-after--editor { cursor: default; } diff --git a/assets/before-after/frontblocks-before-after.js b/assets/before-after/frontblocks-before-after.js new file mode 100644 index 0000000..17f4fba --- /dev/null +++ b/assets/before-after/frontblocks-before-after.js @@ -0,0 +1,244 @@ +"use strict"; + +var registerBlockType = wp.blocks.registerBlockType; +var Fragment = wp.element.Fragment; +var _wp$blockEditor = wp.blockEditor, + InspectorControls = _wp$blockEditor.InspectorControls, + MediaUpload = _wp$blockEditor.MediaUpload, + MediaUploadCheck = _wp$blockEditor.MediaUploadCheck, + useBlockProps = _wp$blockEditor.useBlockProps; +var _wp$components = wp.components, + PanelBody = _wp$components.PanelBody, + RangeControl = _wp$components.RangeControl, + Button = _wp$components.Button, + Placeholder = _wp$components.Placeholder, + TextControl = _wp$components.TextControl; +var __ = wp.i18n.__; + +/** + * Edit component for Before After block. + */ +function BeforeAfterEdit(props) { + var attributes = props.attributes, + setAttributes = props.setAttributes; + var beforeImageId = attributes.beforeImageId, + beforeImageUrl = attributes.beforeImageUrl, + afterImageId = attributes.afterImageId, + afterImageUrl = attributes.afterImageUrl, + beforeLabel = attributes.beforeLabel, + afterLabel = attributes.afterLabel, + initialPosition = attributes.initialPosition; + var blockProps = useBlockProps(); + var hasImages = beforeImageUrl && afterImageUrl; + return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(InspectorControls, null, /*#__PURE__*/React.createElement(PanelBody, { + title: __('Before Image', 'frontblocks'), + initialOpen: true + }, /*#__PURE__*/React.createElement(MediaUploadCheck, null, /*#__PURE__*/React.createElement(MediaUpload, { + onSelect: function onSelect(media) { + return setAttributes({ + beforeImageId: media.id, + beforeImageUrl: media.url + }); + }, + allowedTypes: ['image'], + value: beforeImageId, + render: function render(_ref) { + var open = _ref.open; + return /*#__PURE__*/React.createElement("div", null, beforeImageUrl && /*#__PURE__*/React.createElement("img", { + src: beforeImageUrl, + alt: "", + style: { + width: '100%', + marginBottom: '8px', + borderRadius: '2px' + } + }), /*#__PURE__*/React.createElement(Button, { + onClick: open, + variant: beforeImageUrl ? 'secondary' : 'primary', + style: { + width: '100%' + } + }, beforeImageUrl ? __('Replace Before Image', 'frontblocks') : __('Select Before Image', 'frontblocks')), beforeImageUrl && /*#__PURE__*/React.createElement(Button, { + onClick: function onClick() { + return setAttributes({ + beforeImageId: undefined, + beforeImageUrl: '' + }); + }, + variant: "link", + isDestructive: true, + style: { + marginTop: '4px', + display: 'block' + } + }, __('Remove', 'frontblocks'))); + } + })), /*#__PURE__*/React.createElement(TextControl, { + label: __('Before Label', 'frontblocks'), + value: beforeLabel, + onChange: function onChange(value) { + return setAttributes({ + beforeLabel: value + }); + }, + style: { + marginTop: '12px' + } + })), /*#__PURE__*/React.createElement(PanelBody, { + title: __('After Image', 'frontblocks'), + initialOpen: true + }, /*#__PURE__*/React.createElement(MediaUploadCheck, null, /*#__PURE__*/React.createElement(MediaUpload, { + onSelect: function onSelect(media) { + return setAttributes({ + afterImageId: media.id, + afterImageUrl: media.url + }); + }, + allowedTypes: ['image'], + value: afterImageId, + render: function render(_ref2) { + var open = _ref2.open; + return /*#__PURE__*/React.createElement("div", null, afterImageUrl && /*#__PURE__*/React.createElement("img", { + src: afterImageUrl, + alt: "", + style: { + width: '100%', + marginBottom: '8px', + borderRadius: '2px' + } + }), /*#__PURE__*/React.createElement(Button, { + onClick: open, + variant: afterImageUrl ? 'secondary' : 'primary', + style: { + width: '100%' + } + }, afterImageUrl ? __('Replace After Image', 'frontblocks') : __('Select After Image', 'frontblocks')), afterImageUrl && /*#__PURE__*/React.createElement(Button, { + onClick: function onClick() { + return setAttributes({ + afterImageId: undefined, + afterImageUrl: '' + }); + }, + variant: "link", + isDestructive: true, + style: { + marginTop: '4px', + display: 'block' + } + }, __('Remove', 'frontblocks'))); + } + })), /*#__PURE__*/React.createElement(TextControl, { + label: __('After Label', 'frontblocks'), + value: afterLabel, + onChange: function onChange(value) { + return setAttributes({ + afterLabel: value + }); + }, + style: { + marginTop: '12px' + } + })), /*#__PURE__*/React.createElement(PanelBody, { + title: __('Slider Settings', 'frontblocks'), + initialOpen: false + }, /*#__PURE__*/React.createElement(RangeControl, { + label: __('Initial Handle Position (%)', 'frontblocks'), + value: initialPosition, + onChange: function onChange(value) { + return setAttributes({ + initialPosition: value + }); + }, + min: 0, + max: 100 + }))), /*#__PURE__*/React.createElement("div", blockProps, !hasImages ? /*#__PURE__*/React.createElement(Placeholder, { + icon: "image-flip-horizontal", + label: __('Before / After', 'frontblocks'), + instructions: __('Select a "before" image and an "after" image from the sidebar.', 'frontblocks') + }) : /*#__PURE__*/React.createElement("div", { + className: "frbl-before-after frbl-before-after--editor", + "data-initial-position": initialPosition + }, /*#__PURE__*/React.createElement("div", { + className: "frbl-before-after__after" + }, /*#__PURE__*/React.createElement("img", { + src: afterImageUrl, + alt: "" + }), /*#__PURE__*/React.createElement("span", { + className: "frbl-before-after__label frbl-before-after__label--after" + }, afterLabel)), /*#__PURE__*/React.createElement("div", { + className: "frbl-before-after__before", + style: { + clipPath: "inset(0 ".concat(100 - initialPosition, "% 0 0)") + } + }, /*#__PURE__*/React.createElement("img", { + src: beforeImageUrl, + alt: "" + }), /*#__PURE__*/React.createElement("span", { + className: "frbl-before-after__label frbl-before-after__label--before" + }, beforeLabel)), /*#__PURE__*/React.createElement("div", { + className: "frbl-before-after__handle", + style: { + left: "".concat(initialPosition, "%") + } + }, /*#__PURE__*/React.createElement("span", { + className: "frbl-before-after__handle-line" + }), /*#__PURE__*/React.createElement("span", { + className: "frbl-before-after__handle-thumb" + }, /*#__PURE__*/React.createElement("svg", { + viewBox: "0 0 20 20", + width: "18", + height: "18", + fill: "currentColor", + "aria-hidden": "true" + }, /*#__PURE__*/React.createElement("path", { + d: "M7 4l-4 6 4 6M13 4l4 6-4 6", + stroke: "currentColor", + strokeWidth: "2", + fill: "none", + strokeLinecap: "round", + strokeLinejoin: "round" + }))), /*#__PURE__*/React.createElement("span", { + className: "frbl-before-after__handle-line" + }))))); +} + +// Register the block. +registerBlockType('frontblocks/before-after', { + title: __('Before After', 'frontblocks'), + description: __('Compare two images with a draggable before/after slider.', 'frontblocks'), + category: 'media', + icon: 'image-flip-horizontal', + keywords: [__('before', 'frontblocks'), __('after', 'frontblocks'), __('comparison', 'frontblocks'), __('slider', 'frontblocks')], + attributes: { + beforeImageId: { + type: 'integer' + }, + beforeImageUrl: { + type: 'string', + default: '' + }, + afterImageId: { + type: 'integer' + }, + afterImageUrl: { + type: 'string', + default: '' + }, + beforeLabel: { + type: 'string', + default: 'Before' + }, + afterLabel: { + type: 'string', + default: 'After' + }, + initialPosition: { + type: 'number', + default: 50 + } + }, + edit: BeforeAfterEdit, + save: function save() { + return null; // Dynamic block, rendered server-side. + } +}); diff --git a/blocks/before-after/block.json b/blocks/before-after/block.json deleted file mode 100644 index 41dd5b5..0000000 --- a/blocks/before-after/block.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 3, - "name": "frontblocks/before-after", - "version": "1.0.0", - "title": "Before After", - "category": "media", - "icon": "image-flip-horizontal", - "description": "Compare two images with a draggable before/after slider.", - "keywords": [ "before", "after", "comparison", "slider", "image" ], - "textdomain": "frontblocks", - "attributes": { - "beforeImageId": { - "type": "integer" - }, - "beforeImageUrl": { - "type": "string", - "default": "" - }, - "afterImageId": { - "type": "integer" - }, - "afterImageUrl": { - "type": "string", - "default": "" - }, - "beforeLabel": { - "type": "string", - "default": "Before" - }, - "afterLabel": { - "type": "string", - "default": "After" - }, - "initialPosition": { - "type": "number", - "default": 50 - }, - "align": { - "type": "string" - } - }, - "supports": { - "html": false, - "align": [ "wide", "full" ] - }, - "editorScript": "frontblocks-before-after-editor", - "viewScript": "frontblocks-before-after-view", - "style": "frontblocks-before-after-style" -} diff --git a/blocks/before-after/edit.js b/blocks/before-after/edit.js deleted file mode 100644 index d569445..0000000 --- a/blocks/before-after/edit.js +++ /dev/null @@ -1,211 +0,0 @@ -"use strict"; - -(function () { - var registerBlockType = wp.blocks.registerBlockType; - var _wp$blockEditor = wp.blockEditor, - MediaUpload = _wp$blockEditor.MediaUpload, - MediaUploadCheck = _wp$blockEditor.MediaUploadCheck, - InspectorControls = _wp$blockEditor.InspectorControls, - RichText = _wp$blockEditor.RichText, - useBlockProps = _wp$blockEditor.useBlockProps; - var _wp$components = wp.components, - PanelBody = _wp$components.PanelBody, - Button = _wp$components.Button, - RangeControl = _wp$components.RangeControl, - TextControl = _wp$components.TextControl, - Placeholder = _wp$components.Placeholder; - var __ = wp.i18n.__; - function EditBeforeAfter(_ref) { - var attributes = _ref.attributes, - setAttributes = _ref.setAttributes; - var beforeImageId = attributes.beforeImageId, - beforeImageUrl = attributes.beforeImageUrl, - afterImageId = attributes.afterImageId, - afterImageUrl = attributes.afterImageUrl, - beforeLabel = attributes.beforeLabel, - afterLabel = attributes.afterLabel, - initialPosition = attributes.initialPosition; - var blockProps = useBlockProps({ - className: 'frbl-before-after frbl-before-after--editor', - 'data-initial-position': initialPosition - }); - var hasImages = beforeImageUrl && afterImageUrl; - return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(InspectorControls, null, /*#__PURE__*/React.createElement(PanelBody, { - title: __('Before Image', 'frontblocks'), - initialOpen: true - }, /*#__PURE__*/React.createElement(MediaUploadCheck, null, /*#__PURE__*/React.createElement(MediaUpload, { - onSelect: function onSelect(media) { - return setAttributes({ - beforeImageId: media.id, - beforeImageUrl: media.url - }); - }, - allowedTypes: ['image'], - value: beforeImageId, - render: function render(_ref2) { - var open = _ref2.open; - return /*#__PURE__*/React.createElement(React.Fragment, null, beforeImageUrl && /*#__PURE__*/React.createElement("img", { - src: beforeImageUrl, - alt: "", - style: { - width: '100%', - marginBottom: '8px', - borderRadius: '2px' - } - }), /*#__PURE__*/React.createElement(Button, { - onClick: open, - variant: beforeImageUrl ? 'secondary' : 'primary', - style: { - width: '100%' - } - }, beforeImageUrl ? __('Replace Before Image', 'frontblocks') : __('Select Before Image', 'frontblocks')), beforeImageUrl && /*#__PURE__*/React.createElement(Button, { - onClick: function onClick() { - return setAttributes({ - beforeImageId: undefined, - beforeImageUrl: '' - }); - }, - variant: "link", - isDestructive: true, - style: { - marginTop: '4px', - display: 'block' - } - }, __('Remove', 'frontblocks'))); - } - })), /*#__PURE__*/React.createElement(TextControl, { - label: __('Before Label', 'frontblocks'), - value: beforeLabel, - onChange: function onChange(val) { - return setAttributes({ - beforeLabel: val - }); - }, - style: { - marginTop: '12px' - } - })), /*#__PURE__*/React.createElement(PanelBody, { - title: __('After Image', 'frontblocks'), - initialOpen: true - }, /*#__PURE__*/React.createElement(MediaUploadCheck, null, /*#__PURE__*/React.createElement(MediaUpload, { - onSelect: function onSelect(media) { - return setAttributes({ - afterImageId: media.id, - afterImageUrl: media.url - }); - }, - allowedTypes: ['image'], - value: afterImageId, - render: function render(_ref3) { - var open = _ref3.open; - return /*#__PURE__*/React.createElement(React.Fragment, null, afterImageUrl && /*#__PURE__*/React.createElement("img", { - src: afterImageUrl, - alt: "", - style: { - width: '100%', - marginBottom: '8px', - borderRadius: '2px' - } - }), /*#__PURE__*/React.createElement(Button, { - onClick: open, - variant: afterImageUrl ? 'secondary' : 'primary', - style: { - width: '100%' - } - }, afterImageUrl ? __('Replace After Image', 'frontblocks') : __('Select After Image', 'frontblocks')), afterImageUrl && /*#__PURE__*/React.createElement(Button, { - onClick: function onClick() { - return setAttributes({ - afterImageId: undefined, - afterImageUrl: '' - }); - }, - variant: "link", - isDestructive: true, - style: { - marginTop: '4px', - display: 'block' - } - }, __('Remove', 'frontblocks'))); - } - })), /*#__PURE__*/React.createElement(TextControl, { - label: __('After Label', 'frontblocks'), - value: afterLabel, - onChange: function onChange(val) { - return setAttributes({ - afterLabel: val - }); - }, - style: { - marginTop: '12px' - } - })), /*#__PURE__*/React.createElement(PanelBody, { - title: __('Slider Settings', 'frontblocks'), - initialOpen: false - }, /*#__PURE__*/React.createElement(RangeControl, { - label: __('Initial Handle Position (%)', 'frontblocks'), - value: initialPosition, - onChange: function onChange(val) { - return setAttributes({ - initialPosition: val - }); - }, - min: 0, - max: 100 - }))), !hasImages ? /*#__PURE__*/React.createElement("div", blockProps, /*#__PURE__*/React.createElement(Placeholder, { - icon: "image-flip-horizontal", - label: __('Before / After', 'frontblocks'), - instructions: __('Select a "before" image and an "after" image from the sidebar.', 'frontblocks') - })) : /*#__PURE__*/React.createElement("div", blockProps, /*#__PURE__*/React.createElement("div", { - className: "frbl-before-after__after" - }, /*#__PURE__*/React.createElement("img", { - src: afterImageUrl, - alt: "" - }), /*#__PURE__*/React.createElement("span", { - className: "frbl-before-after__label frbl-before-after__label--after" - }, afterLabel)), /*#__PURE__*/React.createElement("div", { - className: "frbl-before-after__before", - style: { - clipPath: "inset(0 ".concat(100 - initialPosition, "% 0 0)") - } - }, /*#__PURE__*/React.createElement("img", { - src: beforeImageUrl, - alt: "" - }), /*#__PURE__*/React.createElement("span", { - className: "frbl-before-after__label frbl-before-after__label--before" - }, beforeLabel)), /*#__PURE__*/React.createElement("div", { - className: "frbl-before-after__handle", - style: { - left: "".concat(initialPosition, "%") - } - }, /*#__PURE__*/React.createElement("span", { - className: "frbl-before-after__handle-line" - }), /*#__PURE__*/React.createElement("span", { - className: "frbl-before-after__handle-thumb" - }, /*#__PURE__*/React.createElement("svg", { - viewBox: "0 0 24 24", - width: "20", - height: "20", - fill: "none", - stroke: "currentColor", - strokeWidth: "2", - strokeLinecap: "round", - strokeLinejoin: "round" - }, /*#__PURE__*/React.createElement("polyline", { - points: "15 18 9 12 15 6" - }), /*#__PURE__*/React.createElement("polyline", { - points: "9 18 3 12 9 6" - }), /*#__PURE__*/React.createElement("polyline", { - points: "15 18 21 12 15 6" - }), /*#__PURE__*/React.createElement("polyline", { - points: "9 18 15 12 9 6" - }))), /*#__PURE__*/React.createElement("span", { - className: "frbl-before-after__handle-line" - })))); - } - registerBlockType('frontblocks/before-after', { - edit: EditBeforeAfter, - save: function save() { - return null; - } - }); -})(); diff --git a/blocks/before-after/edit.jsx b/blocks/before-after/edit.jsx deleted file mode 100644 index 5b14d47..0000000 --- a/blocks/before-after/edit.jsx +++ /dev/null @@ -1,284 +0,0 @@ -( function () { - const { registerBlockType } = wp.blocks; - const { - MediaUpload, - MediaUploadCheck, - InspectorControls, - RichText, - useBlockProps, - } = wp.blockEditor; - const { PanelBody, Button, RangeControl, TextControl, Placeholder } = - wp.components; - const { __ } = wp.i18n; - - function EditBeforeAfter( { attributes, setAttributes } ) { - const { - beforeImageId, - beforeImageUrl, - afterImageId, - afterImageUrl, - beforeLabel, - afterLabel, - initialPosition, - } = attributes; - - const blockProps = useBlockProps( { - className: 'frbl-before-after frbl-before-after--editor', - 'data-initial-position': initialPosition, - } ); - - const hasImages = beforeImageUrl && afterImageUrl; - - return ( - <> - - - - - setAttributes( { - beforeImageId: media.id, - beforeImageUrl: media.url, - } ) - } - allowedTypes={ [ 'image' ] } - value={ beforeImageId } - render={ ( { open } ) => ( - <> - { beforeImageUrl && ( - - ) } - - { beforeImageUrl && ( - - ) } - - ) } - /> - - - setAttributes( { beforeLabel: val } ) - } - style={ { marginTop: '12px' } } - /> - - - - - - setAttributes( { - afterImageId: media.id, - afterImageUrl: media.url, - } ) - } - allowedTypes={ [ 'image' ] } - value={ afterImageId } - render={ ( { open } ) => ( - <> - { afterImageUrl && ( - - ) } - - { afterImageUrl && ( - - ) } - - ) } - /> - - - setAttributes( { afterLabel: val } ) - } - style={ { marginTop: '12px' } } - /> - - - - - setAttributes( { initialPosition: val } ) - } - min={ 0 } - max={ 100 } - /> - - - - { ! hasImages ? ( -
- -
- ) : ( -
-
- - - { afterLabel } - -
-
- - - { beforeLabel } - -
-
- - - - - - - - - - -
-
- ) } - - ); - } - - registerBlockType( 'frontblocks/before-after', { - edit: EditBeforeAfter, - save: function () { - return null; - }, - } ); -} )(); diff --git a/includes/Frontend/BeforeAfter.php b/includes/Frontend/BeforeAfter.php index fe79ff7..8016699 100644 --- a/includes/Frontend/BeforeAfter.php +++ b/includes/Frontend/BeforeAfter.php @@ -1,19 +1,23 @@ array( $this, 'render_block' ), - ) + wp_enqueue_script( + 'frontblocks-before-after-option', + FRBL_PLUGIN_URL . 'assets/before-after/frontblocks-before-after.js', + array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-i18n' ), + FRBL_VERSION, + true + ); + } + + /** + * Register the Before After block. + * + * @return void + */ + public function register_before_after_block() { + if ( ! function_exists( 'register_block_type' ) ) { + return; + } + + $args = array( + 'editor_script' => 'frontblocks-before-after-option', + 'render_callback' => array( $this, 'render_before_after_block' ), + 'attributes' => array( + 'beforeImageId' => array( + 'type' => 'integer', + ), + 'beforeImageUrl' => array( + 'type' => 'string', + 'default' => '', + ), + 'afterImageId' => array( + 'type' => 'integer', + ), + 'afterImageUrl' => array( + 'type' => 'string', + 'default' => '', + ), + 'beforeLabel' => array( + 'type' => 'string', + 'default' => 'Before', + ), + 'afterLabel' => array( + 'type' => 'string', + 'default' => 'After', + ), + 'initialPosition' => array( + 'type' => 'number', + 'default' => 50, + ), + ), ); + + if ( ! WP_Block_Type_Registry::get_instance()->is_registered( 'frontblocks/before-after' ) ) { + register_block_type( 'frontblocks/before-after', $args ); + } } /** - * Render the block on the frontend. + * Render the Before After block on the frontend. * * @param array $attributes Block attributes. - * @return string Block HTML. + * @return string HTML output. */ - public function render_block( $attributes ) { + public function render_before_after_block( $attributes ) { $before_url = isset( $attributes['beforeImageUrl'] ) ? $attributes['beforeImageUrl'] : ''; $after_url = isset( $attributes['afterImageUrl'] ) ? $attributes['afterImageUrl'] : ''; @@ -84,14 +144,16 @@ public function render_block( $attributes ) { $initial_position = isset( $attributes['initialPosition'] ) ? (int) $attributes['initialPosition'] : 50; $before_label = isset( $attributes['beforeLabel'] ) ? $attributes['beforeLabel'] : __( 'Before', 'frontblocks' ); $after_label = isset( $attributes['afterLabel'] ) ? $attributes['afterLabel'] : __( 'After', 'frontblocks' ); - $align = isset( $attributes['align'] ) ? sanitize_html_class( 'align' . $attributes['align'] ) : ''; - $classes = trim( 'wp-block-frontblocks-before-after frbl-before-after ' . $align ); + $wrapper_class = 'frbl-before-after'; + if ( ! empty( $attributes['className'] ) ) { + $wrapper_class .= ' ' . esc_attr( $attributes['className'] ); + } ob_start(); ?>
@@ -117,11 +179,8 @@ class="frbl-before-after__handle" > - diff --git a/package.json b/package.json index fd2ff0e..5ebd40f 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "build:edge-alignment": "babel assets/container-edge-alignment/frontblocks-edge-alignment-option.jsx --out-file assets/container-edge-alignment/frontblocks-edge-alignment.js", "build:shape-animations": "babel assets/shape-animations/frontblocks-shape-animation-option.jsx --out-file assets/shape-animations/frontblocks-shape-animation-option.js", "build:gf-inline": "babel assets/gravityforms-inline/frontblocks-gf-inline-option.jsx --out-file assets/gravityforms-inline/frontblocks-gf-inline-option.js", - "build:before-after": "babel blocks/before-after/edit.jsx --out-file blocks/before-after/edit.js", + "build:before-after": "babel assets/before-after/frontblocks-before-after-option.jsx --out-file assets/before-after/frontblocks-before-after.js", "build:css": "postcss assets/admin/settings-src.css -o assets/admin/settings.css", "watch:css": "postcss assets/admin/settings-src.css -o assets/admin/settings.css --watch", From f0bdcfb8e260e675cd72c48b1bab2ad7e9160b0f Mon Sep 17 00:00:00 2001 From: castellon Date: Thu, 30 Apr 2026 08:44:58 +0200 Subject: [PATCH 3/7] add height option --- .../frontblocks-before-after-option.jsx | 28 ++++++++++++- .../before-after/frontblocks-before-after.css | 10 +++++ .../before-after/frontblocks-before-after.js | 39 +++++++++++++++++-- includes/Frontend/BeforeAfter.php | 14 +++++++ 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/assets/before-after/frontblocks-before-after-option.jsx b/assets/before-after/frontblocks-before-after-option.jsx index b86158c..79a838d 100644 --- a/assets/before-after/frontblocks-before-after-option.jsx +++ b/assets/before-after/frontblocks-before-after-option.jsx @@ -1,7 +1,7 @@ const { registerBlockType } = wp.blocks; const { Fragment } = wp.element; const { InspectorControls, MediaUpload, MediaUploadCheck, useBlockProps } = wp.blockEditor; -const { PanelBody, RangeControl, Button, Placeholder, TextControl } = wp.components; +const { PanelBody, RangeControl, Button, Placeholder, TextControl, ToggleControl } = wp.components; const { __ } = wp.i18n; /** @@ -17,6 +17,8 @@ function BeforeAfterEdit( props ) { beforeLabel, afterLabel, initialPosition, + blockHeight, + fixedHeight, } = attributes; const blockProps = useBlockProps(); @@ -146,6 +148,21 @@ function BeforeAfterEdit( props ) { min={ 0 } max={ 100 } /> + setAttributes( { fixedHeight: value } ) } + /> + { fixedHeight && ( + setAttributes( { blockHeight: value } ) } + min={ 100 } + max={ 1000 } + step={ 10 } + /> + ) } @@ -163,6 +180,7 @@ function BeforeAfterEdit( props ) {
@@ -237,6 +255,14 @@ registerBlockType( 'frontblocks/before-after', { type: 'number', default: 50, }, + fixedHeight: { + type: 'boolean', + default: false, + }, + blockHeight: { + type: 'number', + default: 400, + }, }, edit: BeforeAfterEdit, save: function () { diff --git a/assets/before-after/frontblocks-before-after.css b/assets/before-after/frontblocks-before-after.css index 9e03dc3..b967c82 100644 --- a/assets/before-after/frontblocks-before-after.css +++ b/assets/before-after/frontblocks-before-after.css @@ -27,6 +27,16 @@ pointer-events: none; } +/* When a fixed height is set, images fill the container */ +.frbl-before-after[style*="height"] .frbl-before-after__after { + height: 100%; +} + +.frbl-before-after[style*="height"] .frbl-before-after__after img { + height: 100%; + object-fit: cover; +} + /* Before image — absolutely positioned on top, clipped from the right */ .frbl-before-after__before { position: absolute; diff --git a/assets/before-after/frontblocks-before-after.js b/assets/before-after/frontblocks-before-after.js index 17f4fba..a5118a1 100644 --- a/assets/before-after/frontblocks-before-after.js +++ b/assets/before-after/frontblocks-before-after.js @@ -12,7 +12,8 @@ var _wp$components = wp.components, RangeControl = _wp$components.RangeControl, Button = _wp$components.Button, Placeholder = _wp$components.Placeholder, - TextControl = _wp$components.TextControl; + TextControl = _wp$components.TextControl, + ToggleControl = _wp$components.ToggleControl; var __ = wp.i18n.__; /** @@ -27,7 +28,9 @@ function BeforeAfterEdit(props) { afterImageUrl = attributes.afterImageUrl, beforeLabel = attributes.beforeLabel, afterLabel = attributes.afterLabel, - initialPosition = attributes.initialPosition; + initialPosition = attributes.initialPosition, + blockHeight = attributes.blockHeight, + fixedHeight = attributes.fixedHeight; var blockProps = useBlockProps(); var hasImages = beforeImageUrl && afterImageUrl; return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(InspectorControls, null, /*#__PURE__*/React.createElement(PanelBody, { @@ -151,13 +154,35 @@ function BeforeAfterEdit(props) { }, min: 0, max: 100 + }), /*#__PURE__*/React.createElement(ToggleControl, { + label: __('Fixed Height', 'frontblocks'), + checked: fixedHeight, + onChange: function onChange(value) { + return setAttributes({ + fixedHeight: value + }); + } + }), fixedHeight && /*#__PURE__*/React.createElement(RangeControl, { + label: __('Height (px)', 'frontblocks'), + value: blockHeight, + onChange: function onChange(value) { + return setAttributes({ + blockHeight: value + }); + }, + min: 100, + max: 1000, + step: 10 }))), /*#__PURE__*/React.createElement("div", blockProps, !hasImages ? /*#__PURE__*/React.createElement(Placeholder, { icon: "image-flip-horizontal", label: __('Before / After', 'frontblocks'), instructions: __('Select a "before" image and an "after" image from the sidebar.', 'frontblocks') }) : /*#__PURE__*/React.createElement("div", { className: "frbl-before-after frbl-before-after--editor", - "data-initial-position": initialPosition + "data-initial-position": initialPosition, + style: fixedHeight && blockHeight ? { + height: blockHeight + 'px' + } : {} }, /*#__PURE__*/React.createElement("div", { className: "frbl-before-after__after" }, /*#__PURE__*/React.createElement("img", { @@ -235,6 +260,14 @@ registerBlockType('frontblocks/before-after', { initialPosition: { type: 'number', default: 50 + }, + fixedHeight: { + type: 'boolean', + default: false + }, + blockHeight: { + type: 'number', + default: 400 } }, edit: BeforeAfterEdit, diff --git a/includes/Frontend/BeforeAfter.php b/includes/Frontend/BeforeAfter.php index 8016699..0b3ce56 100644 --- a/includes/Frontend/BeforeAfter.php +++ b/includes/Frontend/BeforeAfter.php @@ -119,6 +119,14 @@ public function register_before_after_block() { 'type' => 'number', 'default' => 50, ), + 'fixedHeight' => array( + 'type' => 'boolean', + 'default' => false, + ), + 'blockHeight' => array( + 'type' => 'number', + 'default' => 400, + ), ), ); @@ -145,16 +153,22 @@ public function render_before_after_block( $attributes ) { $before_label = isset( $attributes['beforeLabel'] ) ? $attributes['beforeLabel'] : __( 'Before', 'frontblocks' ); $after_label = isset( $attributes['afterLabel'] ) ? $attributes['afterLabel'] : __( 'After', 'frontblocks' ); + $fixed_height = ! empty( $attributes['fixedHeight'] ); + $block_height = isset( $attributes['blockHeight'] ) ? (int) $attributes['blockHeight'] : 400; + $wrapper_class = 'frbl-before-after'; if ( ! empty( $attributes['className'] ) ) { $wrapper_class .= ' ' . esc_attr( $attributes['className'] ); } + $wrapper_style = $fixed_height && $block_height ? ' style="height:' . esc_attr( $block_height ) . 'px"' : ''; + ob_start(); ?>
>
From 4a96753899294ebe3622b28492927cde6231be7e Mon Sep 17 00:00:00 2001 From: castellon Date: Thu, 30 Apr 2026 08:56:44 +0200 Subject: [PATCH 4/7] fix images --- .../frontblocks-before-after-option.jsx | 2 +- assets/before-after/frontblocks-before-after.css | 13 ++++++++++--- assets/before-after/frontblocks-before-after.js | 2 +- includes/Frontend/BeforeAfter.php | 3 +++ 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/assets/before-after/frontblocks-before-after-option.jsx b/assets/before-after/frontblocks-before-after-option.jsx index 79a838d..660e975 100644 --- a/assets/before-after/frontblocks-before-after-option.jsx +++ b/assets/before-after/frontblocks-before-after-option.jsx @@ -178,7 +178,7 @@ function BeforeAfterEdit( props ) { /> ) : (
diff --git a/assets/before-after/frontblocks-before-after.css b/assets/before-after/frontblocks-before-after.css index b967c82..de465d7 100644 --- a/assets/before-after/frontblocks-before-after.css +++ b/assets/before-after/frontblocks-before-after.css @@ -27,14 +27,21 @@ pointer-events: none; } -/* When a fixed height is set, images fill the container */ -.frbl-before-after[style*="height"] .frbl-before-after__after { +/* Fixed height mode: images scale to fit without cropping */ +.frbl-before-after--fixed-height .frbl-before-after__after { + position: absolute; + top: 0; + left: 0; + width: 100%; height: 100%; } -.frbl-before-after[style*="height"] .frbl-before-after__after img { +.frbl-before-after--fixed-height .frbl-before-after__after img, +.frbl-before-after--fixed-height .frbl-before-after__before img { + width: 100%; height: 100%; object-fit: cover; + object-position: center; } /* Before image — absolutely positioned on top, clipped from the right */ diff --git a/assets/before-after/frontblocks-before-after.js b/assets/before-after/frontblocks-before-after.js index a5118a1..0ea97f8 100644 --- a/assets/before-after/frontblocks-before-after.js +++ b/assets/before-after/frontblocks-before-after.js @@ -178,7 +178,7 @@ function BeforeAfterEdit(props) { label: __('Before / After', 'frontblocks'), instructions: __('Select a "before" image and an "after" image from the sidebar.', 'frontblocks') }) : /*#__PURE__*/React.createElement("div", { - className: "frbl-before-after frbl-before-after--editor", + className: 'frbl-before-after frbl-before-after--editor' + (fixedHeight ? ' frbl-before-after--fixed-height' : ''), "data-initial-position": initialPosition, style: fixedHeight && blockHeight ? { height: blockHeight + 'px' diff --git a/includes/Frontend/BeforeAfter.php b/includes/Frontend/BeforeAfter.php index 0b3ce56..9fe4771 100644 --- a/includes/Frontend/BeforeAfter.php +++ b/includes/Frontend/BeforeAfter.php @@ -157,6 +157,9 @@ public function render_before_after_block( $attributes ) { $block_height = isset( $attributes['blockHeight'] ) ? (int) $attributes['blockHeight'] : 400; $wrapper_class = 'frbl-before-after'; + if ( $fixed_height ) { + $wrapper_class .= ' frbl-before-after--fixed-height'; + } if ( ! empty( $attributes['className'] ) ) { $wrapper_class .= ' ' . esc_attr( $attributes['className'] ); } From c97c2433460e47cec0a020776eccb1ce8692b919 Mon Sep 17 00:00:00 2001 From: castellon Date: Thu, 30 Apr 2026 09:00:03 +0200 Subject: [PATCH 5/7] square never show if text doesn't exist --- .../frontblocks-before-after-option.jsx | 16 ++++++++++------ assets/before-after/frontblocks-before-after.css | 4 ++++ assets/before-after/frontblocks-before-after.js | 4 ++-- includes/Frontend/BeforeAfter.php | 4 ++++ 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/assets/before-after/frontblocks-before-after-option.jsx b/assets/before-after/frontblocks-before-after-option.jsx index 660e975..dc07ef3 100644 --- a/assets/before-after/frontblocks-before-after-option.jsx +++ b/assets/before-after/frontblocks-before-after-option.jsx @@ -184,18 +184,22 @@ function BeforeAfterEdit( props ) { >
- - { afterLabel } - + { afterLabel && ( + + { afterLabel } + + ) }
- - { beforeLabel } - + { beforeLabel && ( + + { beforeLabel } + + ) }
+ +
+ +
Date: Thu, 30 Apr 2026 09:16:24 +0200 Subject: [PATCH 6/7] fix lint --- includes/Frontend/BeforeAfter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/Frontend/BeforeAfter.php b/includes/Frontend/BeforeAfter.php index bb8b306..6a7212b 100644 --- a/includes/Frontend/BeforeAfter.php +++ b/includes/Frontend/BeforeAfter.php @@ -153,8 +153,8 @@ public function render_before_after_block( $attributes ) { $before_label = isset( $attributes['beforeLabel'] ) ? $attributes['beforeLabel'] : __( 'Before', 'frontblocks' ); $after_label = isset( $attributes['afterLabel'] ) ? $attributes['afterLabel'] : __( 'After', 'frontblocks' ); - $fixed_height = ! empty( $attributes['fixedHeight'] ); - $block_height = isset( $attributes['blockHeight'] ) ? (int) $attributes['blockHeight'] : 400; + $fixed_height = ! empty( $attributes['fixedHeight'] ); + $block_height = isset( $attributes['blockHeight'] ) ? (int) $attributes['blockHeight'] : 400; $wrapper_class = 'frbl-before-after'; if ( $fixed_height ) { From f8866232a7c30f37c40b338347a36ed9f6fe4c8f Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Thu, 30 Apr 2026 16:15:47 +0200 Subject: [PATCH 7/7] added changelog --- readme.txt | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index 2f98f47..472ff95 100644 --- a/readme.txt +++ b/readme.txt @@ -46,7 +46,7 @@ The animations are based on [Animate.css](https://animate.style/): Attention see **Container Effects** Apply glassmorphism effects to any block with customizable blur intensity. In the block settings, open the 'Container Effects' panel to enable the glass effect and adjust the blur level (0-50px) for a modern, frosted glass appearance. The effect includes a semi-transparent background, subtle border, and soft shadow, creating a beautiful layered design. Perfect for hero sections, cards, and overlays. -**FrontBlocks Hover Effects** +**Hover Effects** Add smooth zoom effects to background images when users hover over elements. Perfect for post grids, galleries, and cards. In the block settings, open the 'FrontBlocks Hover Effects' panel to enable background scaling. Features: - Compatible with GenerateBlocks Query Loop (--inline-bg-image) - Works with standard CSS background-image @@ -108,6 +108,16 @@ Simply enable "Fluid Typography" in FrontBlocks settings, and all your responsiv **Custom SVG Animations:** Add animated graphics to GenerateBlocks Shape blocks by importing JSON files. Supports two formats that are automatically detected: **Lottie/Bodymovin** (import JSON from After Effects or LottieFiles.com) and **Custom CSS** (SVG + @keyframes). +**Before/After Comparison Block:** +Display an interactive image comparison slider that lets visitors drag a handle to reveal the difference between two images. Perfect for showcasing makeovers, retouching results, renovation work, or any visual transformation. Add the block from the Gutenberg inserter under the FrontBlocks category. + +Features: +- Upload separate "Before" and "After" images from the WordPress media library. +- Draggable handle with left/right arrows — also controllable via keyboard for accessibility. +- Customizable labels displayed over each image (default: "Before" / "After"). +- Initial slider position control (0–100%) to choose how much of each image is revealed on load. +- Fixed height mode with a configurable pixel value, keeping the block compact regardless of image proportions. + **Gravity Forms Inline Layout:** Display Gravity Forms with fields and buttons on the same line. Perfect for newsletter signup forms (email + subscribe button) or search forms (input + search button). @@ -153,6 +163,10 @@ More information in the [FrontBlocks PRO](https://close.technology/en/wordpress- == Changelog == +== next == +* Added: Before/After Comparison block - Interactive drag-to-reveal image comparison block for Gutenberg. + + == 1.3.3 == * Fixed: Carousel bullets display and behavior. * Fixed: Carousel editor styling and functionality.