FEATURE introducing block

This commit is contained in:
Antoine M 2025-12-11 15:31:52 +01:00
parent d994a331eb
commit 02eb19eb9e
7 changed files with 436 additions and 0 deletions

View File

@ -0,0 +1,65 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "carhop-blocks/image-stack",
"version": "0.1.0",
"title": "Image Stack (Focal Point)",
"category": "carhop-blocks",
"icon": "images-alt2",
"description": "Layer multiple images with individual focal point positioning.",
"example": {
"attributes": {
"images": [
{
"id": 1,
"url": "https://images.unsplash.com/photo-1506905925346-21bda4d32df4",
"alt": "Mountain landscape",
"focalPoint": {
"x": 0.5,
"y": 0.3
},
"scale": 0.8
},
{
"id": 2,
"url": "https://images.unsplash.com/photo-1511884642898-4c92249e20b6",
"alt": "Forest scene",
"focalPoint": {
"x": 0.7,
"y": 0.6
},
"scale": 0.6
}
],
"height": 400
}
},
"attributes": {
"images": {
"type": "array",
"default": [],
"items": {
"type": "object"
}
},
"height": {
"type": "number",
"default": 400
}
},
"supports": {
"html": false,
"align": [
"wide",
"full"
],
"spacing": {
"margin": true,
"padding": true
}
},
"textdomain": "image-stack",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css"
}

View File

@ -0,0 +1,243 @@
import { __ } from "@wordpress/i18n";
import {
useBlockProps,
InspectorControls,
MediaUpload,
MediaUploadCheck,
} from "@wordpress/block-editor";
import {
PanelBody,
Button,
FocalPointPicker,
RangeControl,
ToolbarGroup,
ToolbarButton,
} from "@wordpress/components";
import { BlockControls } from "@wordpress/block-editor";
import { useState } from "@wordpress/element";
import "./editor.scss";
export default function Edit({ attributes, setAttributes }) {
const { images, height } = attributes;
const [selectedImageIndex, setSelectedImageIndex] = useState(null);
const blockProps = useBlockProps();
const onSelectImages = (media) => {
const newImages = media.map((img) => ({
id: img.id,
url: img.url,
alt: img.alt || "",
focalPoint: { x: 0.5, y: 0.5 },
scale: 0.8,
rotation: 0,
}));
setAttributes({ images: [...images, ...newImages] });
};
const updateImageFocalPoint = (index, focalPoint) => {
const newImages = [...images];
newImages[index] = { ...newImages[index], focalPoint };
setAttributes({ images: newImages });
};
const updateImageScale = (index, scale) => {
const newImages = [...images];
newImages[index] = { ...newImages[index], scale };
setAttributes({ images: newImages });
};
const updateImageRotation = (index, rotation) => {
const newImages = [...images];
newImages[index] = { ...newImages[index], rotation };
setAttributes({ images: newImages });
};
const removeImage = (index) => {
const newImages = images.filter((_, i) => i !== index);
setAttributes({ images: newImages });
if (selectedImageIndex === index) {
setSelectedImageIndex(null);
}
};
const moveImage = (index, direction) => {
const newImages = [...images];
const newIndex = index + direction;
if (newIndex >= 0 && newIndex < images.length) {
[newImages[index], newImages[newIndex]] = [
newImages[newIndex],
newImages[index],
];
setAttributes({ images: newImages });
setSelectedImageIndex(newIndex);
}
};
const getImageStyle = (image) => {
const focalPoint = image.focalPoint || { x: 0.5, y: 0.5 };
const scale = image.scale || 0.8;
const rotation = image.rotation || 0;
return {
left: `${focalPoint.x * 100}%`,
top: `${focalPoint.y * 100}%`,
transform: ` rotate(${rotation}deg) scale(${scale})`,
};
};
return (
<>
<BlockControls>
<ToolbarGroup>
<MediaUploadCheck>
<MediaUpload
onSelect={onSelectImages}
allowedTypes={["image"]}
multiple
gallery
value={images.map((img) => img.id)}
render={({ open }) => (
<ToolbarButton onClick={open}>
{__("Add Images", "image-stack")}
</ToolbarButton>
)}
/>
</MediaUploadCheck>
</ToolbarGroup>
</BlockControls>
<InspectorControls>
<PanelBody title={__("Container Settings", "image-stack")}>
<RangeControl
label={__("Container Height", "image-stack")}
value={height}
onChange={(value) => setAttributes({ height: value })}
min={200}
max={800}
step={10}
/>
</PanelBody>
{images.length > 0 && (
<PanelBody title={__("Images", "image-stack")} initialOpen={true}>
{images.map((image, index) => (
<PanelBody
key={image.id}
title={`${__("Image", "image-stack")} ${index + 1}`}
initialOpen={selectedImageIndex === index}
onToggle={() =>
setSelectedImageIndex(
selectedImageIndex === index ? null : index
)
}
>
<div className="image-stack-image-preview">
<img src={image.url} alt={image.alt} />
</div>
<FocalPointPicker
label={__("Focal Point", "image-stack")}
url={image.url}
value={image.focalPoint || { x: 0.5, y: 0.5 }}
onChange={(focalPoint) =>
updateImageFocalPoint(index, focalPoint)
}
/>
<RangeControl
label={__("Scale", "image-stack")}
value={image.scale || 0.8}
onChange={(scale) => updateImageScale(index, scale)}
min={0.1}
max={3}
step={0.05}
/>
<RangeControl
label={__("Rotation (deg)", "image-stack")}
value={image.rotation || 0}
onChange={(rotation) => updateImageRotation(index, rotation)}
min={-180}
max={180}
step={1}
/>
<div className="image-stack-image-controls">
<Button
isSecondary
isSmall
disabled={index === 0}
onClick={() => moveImage(index, -1)}
>
{__("↑ Move Up", "image-stack")}
</Button>
<Button
isSecondary
isSmall
disabled={index === images.length - 1}
onClick={() => moveImage(index, 1)}
>
{__("↓ Move Down", "image-stack")}
</Button>
<Button
isDestructive
isSmall
onClick={() => removeImage(index)}
>
{__("Remove", "image-stack")}
</Button>
</div>
</PanelBody>
))}
</PanelBody>
)}
</InspectorControls>
<div {...blockProps}>
<div
className="image-stack-container"
style={{ height: `${height}px` }}
>
{images.length === 0 && (
<div className="image-stack-placeholder">
<MediaUploadCheck>
<MediaUpload
onSelect={onSelectImages}
allowedTypes={["image"]}
multiple
gallery
render={({ open }) => (
<Button variant="primary" onClick={open}>
{__("Ajouter une Image", "image-stack")}
</Button>
)}
/>
</MediaUploadCheck>
</div>
)}
{images.map((image, index) => (
<div
key={image.id}
className={`image-stack-item ${
selectedImageIndex === index ? "is-selected" : ""
}`}
onClick={() => setSelectedImageIndex(index)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
setSelectedImageIndex(index);
}
}}
style={getImageStyle(image)}
>
<img src={image.url} alt={image.alt} />
</div>
))}
</div>
</div>
</>
);
}

View File

@ -0,0 +1,56 @@
/**
* The following styles get applied inside the editor only.
*/
.wp-block-carhop-blocks-image-stack {
overflow: visible;
.image-stack-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background-color: rgba(240, 240, 240, 0.5);
background-color: red;
border: 2px dashed #ccc;
}
.image-stack-item {
cursor: pointer;
transition: opacity 0.2s ease;
&:hover {
opacity: 0.9;
}
&.is-selected {
outline: 3px solid #007cba;
outline-offset: -3px;
z-index: 10;
}
}
}
.image-stack-image-preview {
margin-bottom: 16px;
img {
width: 100%;
height: auto;
display: block;
border-radius: 4px;
}
}
.image-stack-image-controls {
display: flex;
gap: 8px;
margin-top: 16px;
flex-wrap: wrap;
.components-button {
flex: 1;
min-width: 80px;
justify-content: center;
}
}

View File

@ -0,0 +1,11 @@
import { registerBlockType } from "@wordpress/blocks";
import "./style.scss";
import Edit from "./edit";
import save from "./save";
import metadata from "./block.json";
registerBlockType(metadata.name, {
edit: Edit,
save,
});

View File

@ -0,0 +1,33 @@
import { useBlockProps } from "@wordpress/block-editor";
export default function save({ attributes }) {
const { images, height } = attributes;
const blockProps = useBlockProps.save();
const getImageStyle = (image) => {
const focalPoint = image.focalPoint || { x: 0.5, y: 0.5 };
const scale = image.scale || 0.8;
const rotation = image.rotation || 0;
return {
left: `${focalPoint.x * 100}%`,
top: `${focalPoint.y * 100}%`,
transform: `rotate(${rotation}deg) scale(${scale})`,
};
};
return (
<div {...blockProps}>
<div className="image-stack-container" style={{ height: `${height}px` }}>
{images.map((image) => (
<div
key={image.id}
className="image-stack-item"
style={getImageStyle(image)}
>
<img src={image.url} alt={image.alt} />
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,23 @@
/**
* The following styles get applied both on the front of your site
* and in the editor.
*/
.wp-block-carhop-blocks-image-stack {
.image-stack-container {
position: relative;
width: 100%;
overflow: visible;
}
.image-stack-item {
position: absolute;
img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
}
}

View File

@ -0,0 +1,5 @@
/**
* Frontend JavaScript for the Image Stack block.
* Currently no interactive features required, but this file
* is available for future enhancements.
*/