diff --git a/assets/stacked-images/frontblocks-stacked-images-frontend.js b/assets/stacked-images/frontblocks-stacked-images-frontend.js new file mode 100644 index 0000000..2225436 --- /dev/null +++ b/assets/stacked-images/frontblocks-stacked-images-frontend.js @@ -0,0 +1,121 @@ +/** + * FrontBlocks Stacked Images Frontend Animation + * + * @package FrontBlocks + */ + +(function () { + 'use strict'; + + // Track initialized containers to avoid re-initialization. + const initializedContainers = new WeakSet(); + + /** + * Initialize stacked images animation. + */ + function initStackedImages() { + const containers = document.querySelectorAll('.frbl-stacked-images-wrapper:not(.frbl-initialized)'); + + containers.forEach(container => { + // Skip if already initialized. + if (initializedContainers.has(container)) { + return; + } + + const direction = container.dataset.direction || 'bottom'; + const duration = parseInt(container.dataset.duration) || 1000; + const delay = parseInt(container.dataset.delay) || 500; + const images = container.querySelectorAll('.frbl-stacked-image'); + + if (images.length === 0) { + return; + } + + // Mark as initialized. + container.classList.add('frbl-initialized'); + initializedContainers.add(container); + + // Set initial position for each image based on direction. + images.forEach((image, index) => { + // Set transition FIRST. + image.style.transition = `opacity ${duration}ms ease-out, transform ${duration}ms ease-out`; + image.style.position = 'absolute'; + image.style.top = '0'; + image.style.left = '0'; + image.style.width = '100%'; + image.style.height = '100%'; + + // Generate random rotation between -10 and 10 degrees. + const randomRotation = (Math.random() * 20) - 10; + image.dataset.rotation = randomRotation; + + // Force a reflow to ensure styles are applied. + void image.offsetHeight; + + // Then set initial state (hidden and off-screen). + image.style.opacity = '0'; + + // Set initial transform based on direction with rotation. + let initialTransform = ''; + switch (direction) { + case 'bottom': + initialTransform = `translateY(100%) rotate(${randomRotation}deg)`; + break; + case 'top': + initialTransform = `translateY(-100%) rotate(${randomRotation}deg)`; + break; + case 'left': + initialTransform = `translateX(-100%) rotate(${randomRotation}deg)`; + break; + case 'right': + initialTransform = `translateX(100%) rotate(${randomRotation}deg)`; + break; + } + image.style.transform = initialTransform; + }); + + // Animate images one by one using Intersection Observer. + const observer = new IntersectionObserver( + (entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + animateImages(images, duration, delay); + observer.unobserve(entry.target); + } + }); + }, + { + threshold: 0.2, + rootMargin: '0px', + } + ); + + observer.observe(container); + }); + } + + /** + * Animate images sequentially. + * + * @param {NodeList} images List of image elements. + * @param {number} duration Animation duration. + * @param {number} delay Delay between images. + */ + function animateImages(images, duration, delay) { + images.forEach((image, index) => { + setTimeout(() => { + const rotation = image.dataset.rotation || 0; + image.style.opacity = '1'; + // Final state: no translation, but keep the random rotation. + image.style.transform = `translate(0, 0) rotate(${rotation}deg)`; + }, index * delay); + }); + } + + // Initialize on DOM ready. + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initStackedImages); + } else { + initStackedImages(); + } +})(); diff --git a/assets/stacked-images/frontblocks-stacked-images-option.jsx b/assets/stacked-images/frontblocks-stacked-images-option.jsx new file mode 100644 index 0000000..6270a6b --- /dev/null +++ b/assets/stacked-images/frontblocks-stacked-images-option.jsx @@ -0,0 +1,203 @@ +const { registerBlockType } = wp.blocks; +const { Fragment, useState } = wp.element; +const { InspectorControls, MediaUpload, MediaUploadCheck, useBlockProps } = wp.blockEditor; +const { PanelBody, SelectControl, RangeControl, Button, Placeholder } = wp.components; +const { __ } = wp.i18n; + +/** + * Edit component for Stacked Images block. + * + * @param {Object} props - Block properties. + * @return {JSX.Element} Block edit component. + */ +function StackedImagesEdit(props) { + const { attributes, setAttributes } = props; + const { images, direction, animationDuration, animationDelay, containerHeight } = attributes; + + const blockProps = useBlockProps(); + + const onSelectImages = (newImages) => { + setAttributes({ + images: newImages.map(img => ({ + id: img.id, + url: img.url, + alt: img.alt || '', + })), + }); + }; + + const removeImage = (indexToRemove) => { + const newImages = images.filter((img, index) => index !== indexToRemove); + setAttributes({ images: newImages }); + }; + + return ( + + + + + img.id)} + render={({ open }) => ( + + )} + /> + + {images.length > 0 && ( + + )} + + + + setAttributes({ direction: value })} + /> + setAttributes({ animationDuration: value })} + min={200} + max={3000} + step={100} + /> + setAttributes({ animationDelay: value })} + min={0} + max={2000} + step={100} + /> + setAttributes({ containerHeight: value })} + min={200} + max={1000} + step={50} + /> + + + +
+
+ {images.length === 0 ? ( + + ) : ( +
+
+ {images.map((image, index) => ( +
+ {image.alt} +
+ ))} +
+
+

+ {images.length} {images.length === 1 ? __('image', 'frontblocks') : __('images', 'frontblocks')} | + {__(' Direction: ', 'frontblocks')} {direction} | + {__(' Duration: ', 'frontblocks')} {animationDuration}ms | + {__(' Delay: ', 'frontblocks')} {animationDelay}ms +

+
+
+ )} +
+
+
+ ); +} + +// Register the block. +registerBlockType('frontblocks/stacked-images', { + title: __('Stacked Images', 'frontblocks'), + description: __('Display images with stacked animation effect from different directions.', 'frontblocks'), + category: 'media', + icon: 'format-gallery', + keywords: [ + __('images', 'frontblocks'), + __('stacked', 'frontblocks'), + __('animation', 'frontblocks'), + __('gallery', 'frontblocks'), + ], + attributes: { + images: { + type: 'array', + default: [], + }, + direction: { + type: 'string', + default: 'bottom', + }, + animationDuration: { + type: 'number', + default: 1000, + }, + animationDelay: { + type: 'number', + default: 500, + }, + containerHeight: { + type: 'number', + default: 500, + }, + }, + edit: StackedImagesEdit, + save: function () { + return null; // Dynamic block, render on server side. + }, +}); diff --git a/assets/stacked-images/frontblocks-stacked-images.css b/assets/stacked-images/frontblocks-stacked-images.css new file mode 100644 index 0000000..6ce317b --- /dev/null +++ b/assets/stacked-images/frontblocks-stacked-images.css @@ -0,0 +1,109 @@ +/** + * FrontBlocks Stacked Images Styles + * + * @package FrontBlocks + */ + +/* Wrapper */ +.frbl-stacked-images-wrapper { + position: relative; + width: 100%; + overflow: visible; + display: block; + background: transparent; + padding: 50px; + margin: -50px; +} + +/* Container */ +.frbl-stacked-images-container { + position: relative; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + overflow: visible; +} + +/* Individual image */ +.frbl-stacked-image { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + will-change: transform, opacity; + opacity: 0; + visibility: hidden; + overflow: visible; +} + +/* Show when initialized */ +.frbl-initialized .frbl-stacked-image { + visibility: visible; +} + +.frbl-stacked-image img { + max-width: 100%; + max-height: 100%; + width: auto; + height: auto; + object-fit: contain; + display: block; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +/* Placeholder */ +.frbl-stacked-images-placeholder { + padding: 40px; + text-align: center; + background: #f0f0f0; + border: 2px dashed #ccc; + border-radius: 8px; + color: #666; + font-size: 16px; +} + +/* Editor styles */ +.frbl-stacked-images-editor { + min-height: 200px; +} + +.frbl-stacked-images-preview { + border: 2px dashed #ddd; + border-radius: 8px; + overflow: visible; + background: #fafafa; + position: relative; + padding: 50px; + margin: 20px; +} + +.frbl-stacked-image-preview { + border-radius: 4px; + overflow: visible; + transition: transform 0.3s ease; +} + +.frbl-stacked-image-preview:hover { + transform: scale(1.02) !important; + z-index: 999 !important; +} + +.frbl-stacked-image-preview img { + display: block; + transition: all 0.3s ease; + pointer-events: none; +} + +/* Responsive */ +@media (max-width: 768px) { + .frbl-stacked-images-wrapper { + height: auto !important; + min-height: 300px; + } +} diff --git a/assets/stacked-images/frontblocks-stacked-images.js b/assets/stacked-images/frontblocks-stacked-images.js new file mode 100644 index 0000000..c089f6c --- /dev/null +++ b/assets/stacked-images/frontblocks-stacked-images.js @@ -0,0 +1,227 @@ +"use strict"; + +var registerBlockType = wp.blocks.registerBlockType; +var _wp$element = wp.element, + Fragment = _wp$element.Fragment, + useState = _wp$element.useState; +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, + SelectControl = _wp$components.SelectControl, + RangeControl = _wp$components.RangeControl, + Button = _wp$components.Button, + Placeholder = _wp$components.Placeholder; +var __ = wp.i18n.__; + +/** + * Edit component for Stacked Images block. + * + * @param {Object} props - Block properties. + * @return {JSX.Element} Block edit component. + */ +function StackedImagesEdit(props) { + var attributes = props.attributes, + setAttributes = props.setAttributes; + var images = attributes.images, + direction = attributes.direction, + animationDuration = attributes.animationDuration, + animationDelay = attributes.animationDelay, + containerHeight = attributes.containerHeight; + var blockProps = useBlockProps(); + var onSelectImages = function onSelectImages(newImages) { + setAttributes({ + images: newImages.map(function (img) { + return { + id: img.id, + url: img.url, + alt: img.alt || '' + }; + }) + }); + }; + var removeImage = function removeImage(indexToRemove) { + var newImages = images.filter(function (img, index) { + return index !== indexToRemove; + }); + setAttributes({ + images: newImages + }); + }; + return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(InspectorControls, null, /*#__PURE__*/React.createElement(PanelBody, { + title: __('Images', 'frontblocks'), + initialOpen: true + }, /*#__PURE__*/React.createElement(MediaUploadCheck, null, /*#__PURE__*/React.createElement(MediaUpload, { + onSelect: onSelectImages, + allowedTypes: ['image'], + multiple: true, + value: images.map(function (img) { + return img.id; + }), + render: function render(_ref) { + var open = _ref.open; + return /*#__PURE__*/React.createElement(Button, { + isPrimary: true, + onClick: open, + style: { + width: '100%', + marginBottom: '10px' + } + }, images.length === 0 ? __('Add Images', 'frontblocks') : __('Change Images (' + images.length + ')', 'frontblocks')); + } + })), images.length > 0 && /*#__PURE__*/React.createElement(Button, { + isDestructive: true, + onClick: function onClick() { + return setAttributes({ + images: [] + }); + }, + style: { + width: '100%' + } + }, __('Remove All Images', 'frontblocks'))), /*#__PURE__*/React.createElement(PanelBody, { + title: __('Settings', 'frontblocks'), + initialOpen: true + }, /*#__PURE__*/React.createElement(SelectControl, { + label: __('Animation Direction', 'frontblocks'), + value: direction, + options: [{ + label: __('From Bottom', 'frontblocks'), + value: 'bottom' + }, { + label: __('From Top', 'frontblocks'), + value: 'top' + }, { + label: __('From Left', 'frontblocks'), + value: 'left' + }, { + label: __('From Right', 'frontblocks'), + value: 'right' + }], + onChange: function onChange(value) { + return setAttributes({ + direction: value + }); + } + }), /*#__PURE__*/React.createElement(RangeControl, { + label: __('Animation Duration (ms)', 'frontblocks'), + value: animationDuration, + onChange: function onChange(value) { + return setAttributes({ + animationDuration: value + }); + }, + min: 200, + max: 3000, + step: 100 + }), /*#__PURE__*/React.createElement(RangeControl, { + label: __('Delay Between Images (ms)', 'frontblocks'), + value: animationDelay, + onChange: function onChange(value) { + return setAttributes({ + animationDelay: value + }); + }, + min: 0, + max: 2000, + step: 100 + }), /*#__PURE__*/React.createElement(RangeControl, { + label: __('Container Height (px)', 'frontblocks'), + value: containerHeight, + onChange: function onChange(value) { + return setAttributes({ + containerHeight: value + }); + }, + min: 200, + max: 1000, + step: 50 + }))), /*#__PURE__*/React.createElement("div", blockProps, /*#__PURE__*/React.createElement("div", { + className: "frbl-stacked-images-editor" + }, images.length === 0 ? /*#__PURE__*/React.createElement(Placeholder, { + icon: "format-gallery", + label: __('Stacked Images', 'frontblocks'), + instructions: __('Use the settings panel on the right to add images →', 'frontblocks') + }) : /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { + className: "frbl-stacked-images-preview", + style: { + height: containerHeight + 'px', + position: 'relative' + } + }, images.map(function (image, index) { + return /*#__PURE__*/React.createElement("div", { + key: index, + className: "frbl-stacked-image-preview", + style: { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + zIndex: index + 1, + transform: "rotate(".concat(Math.random() * 20 - 10, "deg)") + } + }, /*#__PURE__*/React.createElement("img", { + src: image.url, + alt: image.alt, + style: { + width: '100%', + height: '100%', + objectFit: 'contain', + boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)' + } + })); + })), /*#__PURE__*/React.createElement("div", { + style: { + marginTop: '20px', + textAlign: 'center', + padding: '10px', + background: '#f0f0f0', + borderRadius: '4px' + } + }, /*#__PURE__*/React.createElement("p", { + style: { + margin: '0', + fontSize: '13px', + color: '#666' + } + }, /*#__PURE__*/React.createElement("strong", null, images.length), " ", images.length === 1 ? __('image', 'frontblocks') : __('images', 'frontblocks'), " |", __(' Direction: ', 'frontblocks'), " ", /*#__PURE__*/React.createElement("strong", null, direction), " |", __(' Duration: ', 'frontblocks'), " ", /*#__PURE__*/React.createElement("strong", null, animationDuration, "ms"), " |", __(' Delay: ', 'frontblocks'), " ", /*#__PURE__*/React.createElement("strong", null, animationDelay, "ms"))))))); +} + +// Register the block. +registerBlockType('frontblocks/stacked-images', { + title: __('Stacked Images', 'frontblocks'), + description: __('Display images with stacked animation effect from different directions.', 'frontblocks'), + category: 'media', + icon: 'format-gallery', + keywords: [__('images', 'frontblocks'), __('stacked', 'frontblocks'), __('animation', 'frontblocks'), __('gallery', 'frontblocks')], + attributes: { + images: { + type: 'array', + default: [] + }, + direction: { + type: 'string', + default: 'bottom' + }, + animationDuration: { + type: 'number', + default: 1000 + }, + animationDelay: { + type: 'number', + default: 500 + }, + containerHeight: { + type: 'number', + default: 500 + } + }, + edit: StackedImagesEdit, + save: function save() { + return null; // Dynamic block, render on server side. + } +}); diff --git a/includes/Frontend/StackedImages.php b/includes/Frontend/StackedImages.php new file mode 100644 index 0000000..9089c18 --- /dev/null +++ b/includes/Frontend/StackedImages.php @@ -0,0 +1,185 @@ + + * @copyright 2025 Closemarketing + * @version 1.0 + */ + +namespace FrontBlocks\Frontend; + +use WP_Block_Type_Registry; + +defined( 'ABSPATH' ) || exit; + +/** + * StackedImages class. + * + * @since 1.0.0 + */ +class StackedImages { + + /** + * Constructor. + */ + public function __construct() { + add_action( 'init', array( $this, 'register_stacked_images_block' ), 20 ); + add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ) ); + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_scripts' ) ); + } + + /** + * Enqueue frontend scripts and styles. + * + * @return void + */ + public function enqueue_frontend_scripts() { + wp_register_style( + 'frontblocks-stacked-images-style', + FRBL_PLUGIN_URL . 'assets/stacked-images/frontblocks-stacked-images.css', + array(), + FRBL_VERSION + ); + + wp_register_script( + 'frontblocks-stacked-images-frontend', + FRBL_PLUGIN_URL . 'assets/stacked-images/frontblocks-stacked-images-frontend.js', + array(), + FRBL_VERSION, + true + ); + + if ( is_admin() || has_block( 'frontblocks/stacked-images' ) ) { + wp_enqueue_style( 'frontblocks-stacked-images-style' ); + wp_enqueue_script( 'frontblocks-stacked-images-frontend' ); + } + } + + /** + * Enqueue block editor assets. + * + * @return void + */ + public function enqueue_block_editor_assets() { + // Enqueue styles for editor. + wp_enqueue_style( + 'frontblocks-stacked-images-style', + FRBL_PLUGIN_URL . 'assets/stacked-images/frontblocks-stacked-images.css', + array(), + FRBL_VERSION + ); + + wp_enqueue_script( + 'frontblocks-stacked-images-option', + FRBL_PLUGIN_URL . 'assets/stacked-images/frontblocks-stacked-images.js', + array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-data', 'wp-editor', 'wp-block-editor', 'wp-compose', 'wp-i18n' ), + FRBL_VERSION, + true + ); + } + + /** + * Register the Stacked Images block. + * + * @return void + */ + public function register_stacked_images_block() { + $args = array( + 'editor_script' => 'frontblocks-stacked-images-option', + 'render_callback' => array( $this, 'render_stacked_images_block' ), + 'attributes' => array( + 'images' => array( + 'type' => 'array', + 'default' => array(), + ), + 'direction' => array( + 'type' => 'string', + 'default' => 'bottom', + ), + 'animationDuration' => array( + 'type' => 'number', + 'default' => 1000, + ), + 'animationDelay' => array( + 'type' => 'number', + 'default' => 500, + ), + 'containerHeight' => array( + 'type' => 'number', + 'default' => 500, + ), + 'className' => array( + 'type' => 'string', + 'default' => '', + ), + ), + ); + + if ( ! WP_Block_Type_Registry::get_instance()->is_registered( 'frontblocks/stacked-images' ) ) { + register_block_type( + 'frontblocks/stacked-images', + $args + ); + } + } + + /** + * Render the Stacked Images block on frontend. + * + * @param array $attributes Block attributes. + * @return string HTML output. + */ + public function render_stacked_images_block( $attributes ) { + $images = $attributes['images'] ?? array(); + $direction = sanitize_text_field( $attributes['direction'] ?? 'bottom' ); + $animation_duration = absint( $attributes['animationDuration'] ?? 1000 ); + $animation_delay = absint( $attributes['animationDelay'] ?? 500 ); + $container_height = absint( $attributes['containerHeight'] ?? 500 ); + + if ( empty( $images ) ) { + return '
' . esc_html__( 'Please add images to the Stacked Images block.', 'frontblocks' ) . '
'; + } + + $wrapper_class = 'frbl-stacked-images-wrapper'; + if ( ! empty( $attributes['className'] ) ) { + $wrapper_class .= ' ' . esc_attr( $attributes['className'] ); + } + + ob_start(); + ?> +
+
+ $image ) : ?> + +
+ <?php echo $image_alt; ?> +
+ +
+
+