Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions assets/stacked-images/frontblocks-stacked-images-frontend.js
Original file line number Diff line number Diff line change
@@ -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();
}
})();
203 changes: 203 additions & 0 deletions assets/stacked-images/frontblocks-stacked-images-option.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<Fragment>
<InspectorControls>
<PanelBody title={__('Images', 'frontblocks')} initialOpen={true}>
<MediaUploadCheck>
<MediaUpload
onSelect={onSelectImages}
allowedTypes={['image']}
multiple={true}
value={images.map(img => img.id)}
render={({ open }) => (
<Button
isPrimary
onClick={open}
style={{ width: '100%', marginBottom: '10px' }}
>
{images.length === 0
? __('Add Images', 'frontblocks')
: __('Change Images (' + images.length + ')', 'frontblocks')
}
</Button>
)}
/>
</MediaUploadCheck>
{images.length > 0 && (
<Button
isDestructive
onClick={() => setAttributes({ images: [] })}
style={{ width: '100%' }}
>
{__('Remove All Images', 'frontblocks')}
</Button>
)}
</PanelBody>

<PanelBody title={__('Settings', 'frontblocks')} initialOpen={true}>
<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={(value) => setAttributes({ direction: value })}
/>
<RangeControl
label={__('Animation Duration (ms)', 'frontblocks')}
value={animationDuration}
onChange={(value) => setAttributes({ animationDuration: value })}
min={200}
max={3000}
step={100}
/>
<RangeControl
label={__('Delay Between Images (ms)', 'frontblocks')}
value={animationDelay}
onChange={(value) => setAttributes({ animationDelay: value })}
min={0}
max={2000}
step={100}
/>
<RangeControl
label={__('Container Height (px)', 'frontblocks')}
value={containerHeight}
onChange={(value) => setAttributes({ containerHeight: value })}
min={200}
max={1000}
step={50}
/>
</PanelBody>
</InspectorControls>

<div {...blockProps}>
<div className="frbl-stacked-images-editor">
{images.length === 0 ? (
<Placeholder
icon="format-gallery"
label={__('Stacked Images', 'frontblocks')}
instructions={__('Use the settings panel on the right to add images →', 'frontblocks')}
/>
) : (
<div>
<div
className="frbl-stacked-images-preview"
style={{ height: containerHeight + 'px', position: 'relative' }}
>
{images.map((image, index) => (
<div
key={index}
className="frbl-stacked-image-preview"
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: index + 1,
transform: `rotate(${(Math.random() * 20) - 10}deg)`,
}}
>
<img
src={image.url}
alt={image.alt}
style={{
width: '100%',
height: '100%',
objectFit: 'contain',
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)',
}}
/>
</div>
))}
</div>
<div style={{ marginTop: '20px', textAlign: 'center', padding: '10px', background: '#f0f0f0', borderRadius: '4px' }}>
<p style={{ margin: '0', fontSize: '13px', color: '#666' }}>
<strong>{images.length}</strong> {images.length === 1 ? __('image', 'frontblocks') : __('images', 'frontblocks')} |
{__(' Direction: ', 'frontblocks')} <strong>{direction}</strong> |
{__(' Duration: ', 'frontblocks')} <strong>{animationDuration}ms</strong> |
{__(' Delay: ', 'frontblocks')} <strong>{animationDelay}ms</strong>
</p>
</div>
</div>
)}
</div>
</div>
</Fragment>
);
}

// 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.
},
});
Loading
Loading