| 1 |
const { registerBlockType } = wp.blocks; |
| 2 |
const { __ } = wp.i18n; |
| 3 |
const { useState, useEffect } = wp.element; |
| 4 |
const { |
| 5 |
PanelBody, |
| 6 |
TextControl, |
| 7 |
ToggleControl, |
| 8 |
RangeControl, |
| 9 |
Button, |
| 10 |
SelectControl, |
| 11 |
} = wp.components; |
| 12 |
const { |
| 13 |
InspectorControls, |
| 14 |
useBlockProps, |
| 15 |
RichText, |
| 16 |
MediaUpload, |
| 17 |
MediaUploadCheck, |
| 18 |
} = wp.blockEditor || wp.editor; |
| 19 |
import "./style.css"; |
| 20 |
|
| 21 |
registerBlockType("ai-builder/aibui-carousel", { |
| 22 |
title: __("Carousel (AI Builder)", "ai-builder"), |
| 23 |
icon: "images-alt2", |
| 24 |
category: "widgets", |
| 25 |
supports: { align: ["wide", "full"], html: false }, |
| 26 |
attributes: { |
| 27 |
slides: { type: "array", default: [] }, // [{url, alt, caption}] |
| 28 |
showArrows: { type: "boolean", default: true }, |
| 29 |
showDots: { type: "boolean", default: true }, |
| 30 |
autoplay: { type: "boolean", default: true }, |
| 31 |
autoplayDelay: { type: "number", default: 4 }, // seconds |
| 32 |
height: { type: "string", default: "420px" }, |
| 33 |
fit: { type: "string", default: "cover" }, |
| 34 |
}, |
| 35 |
edit: (props) => { |
| 36 |
const { attributes, setAttributes } = props; |
| 37 |
const { |
| 38 |
slides, |
| 39 |
showArrows, |
| 40 |
showDots, |
| 41 |
autoplay, |
| 42 |
autoplayDelay, |
| 43 |
height, |
| 44 |
fit, |
| 45 |
} = attributes; |
| 46 |
const blockProps = useBlockProps({ className: "aibui-carousel" }); |
| 47 |
const [index, setIndex] = useState(0); |
| 48 |
useEffect(() => { |
| 49 |
setIndex(0); |
| 50 |
}, [slides.length]); |
| 51 |
useEffect(() => { |
| 52 |
if (!autoplay || slides.length <= 1) return; |
| 53 |
const t = setInterval( |
| 54 |
() => setIndex((i) => (i + 1) % slides.length), |
| 55 |
autoplayDelay * 1000 |
| 56 |
); |
| 57 |
return () => clearInterval(t); |
| 58 |
}, [autoplay, autoplayDelay, slides.length]); |
| 59 |
const go = (n) => setIndex((n + slides.length) % slides.length); |
| 60 |
|
| 61 |
const addSlide = (media) => { |
| 62 |
const items = Array.isArray(media) ? media : [media]; |
| 63 |
const appended = items |
| 64 |
.filter((m) => m && m.url) |
| 65 |
.map((m) => ({ url: m.url, alt: m.alt || "", caption: "" })); |
| 66 |
if (appended.length) setAttributes({ slides: [...slides, ...appended] }); |
| 67 |
}; |
| 68 |
|
| 69 |
const updateSlide = (idx, patch) => { |
| 70 |
const next = slides.map((s, i) => (i === idx ? { ...s, ...patch } : s)); |
| 71 |
setAttributes({ slides: next }); |
| 72 |
}; |
| 73 |
|
| 74 |
const removeSlide = (idx) => { |
| 75 |
const next = slides.filter((_, i) => i !== idx); |
| 76 |
setAttributes({ slides: next }); |
| 77 |
}; |
| 78 |
|
| 79 |
return ( |
| 80 |
<div {...blockProps}> |
| 81 |
<InspectorControls> |
| 82 |
<PanelBody title={__("Settings", "ai-builder")} initialOpen> |
| 83 |
<ToggleControl |
| 84 |
label={__("Show arrows", "ai-builder")} |
| 85 |
checked={showArrows} |
| 86 |
onChange={(v) => setAttributes({ showArrows: v })} |
| 87 |
/> |
| 88 |
<ToggleControl |
| 89 |
label={__("Show dots", "ai-builder")} |
| 90 |
checked={showDots} |
| 91 |
onChange={(v) => setAttributes({ showDots: v })} |
| 92 |
/> |
| 93 |
<ToggleControl |
| 94 |
label={__("Autoplay", "ai-builder")} |
| 95 |
checked={autoplay} |
| 96 |
onChange={(v) => setAttributes({ autoplay: v })} |
| 97 |
/> |
| 98 |
{autoplay && ( |
| 99 |
<RangeControl |
| 100 |
label={__("Autoplay delay (s)", "ai-builder")} |
| 101 |
min={2} |
| 102 |
max={12} |
| 103 |
value={autoplayDelay} |
| 104 |
onChange={(v) => setAttributes({ autoplayDelay: v })} |
| 105 |
/> |
| 106 |
)} |
| 107 |
<TextControl |
| 108 |
label={__("Height", "ai-builder")} |
| 109 |
value={height} |
| 110 |
onChange={(v) => setAttributes({ height: v })} |
| 111 |
help={__("Any CSS size (e.g. 420px, 50vh)", "ai-builder")} |
| 112 |
/> |
| 113 |
<SelectControl |
| 114 |
label={__("Image fit", "ai-builder")} |
| 115 |
value={fit} |
| 116 |
options={[ |
| 117 |
{ label: "Cover", value: "cover" }, |
| 118 |
{ label: "Contain", value: "contain" }, |
| 119 |
]} |
| 120 |
onChange={(v) => setAttributes({ fit: v })} |
| 121 |
/> |
| 122 |
<MediaUploadCheck> |
| 123 |
<MediaUpload |
| 124 |
multiple |
| 125 |
gallery |
| 126 |
onSelect={addSlide} |
| 127 |
allowedTypes={["image"]} |
| 128 |
render={({ open }) => ( |
| 129 |
<Button isPrimary onClick={open} style={{ marginTop: 8 }}> |
| 130 |
{__("Add slides", "ai-builder")} |
| 131 |
</Button> |
| 132 |
)} |
| 133 |
/> |
| 134 |
</MediaUploadCheck> |
| 135 |
</PanelBody> |
| 136 |
</InspectorControls> |
| 137 |
|
| 138 |
<div className="aibui-carousel-preview" style={{ height }}> |
| 139 |
<div |
| 140 |
className="aibui-carousel-track" |
| 141 |
style={{ transform: `translateX(-${index * 100}%)` }} |
| 142 |
> |
| 143 |
{slides.map((s, i) => ( |
| 144 |
<div key={i} className="aibui-carousel-slide"> |
| 145 |
{s.url ? ( |
| 146 |
<img |
| 147 |
src={s.url} |
| 148 |
alt={s.alt || ""} |
| 149 |
style={{ objectFit: fit }} |
| 150 |
/> |
| 151 |
) : ( |
| 152 |
<div className="aibui-carousel-empty"> |
| 153 |
{__("Select an image", "ai-builder")} |
| 154 |
</div> |
| 155 |
)} |
| 156 |
<RichText |
| 157 |
tagName="div" |
| 158 |
className="aibui-carousel-caption" |
| 159 |
value={s.caption} |
| 160 |
placeholder={__("Caption (optional)", "ai-builder")} |
| 161 |
onChange={(val) => updateSlide(i, { caption: val })} |
| 162 |
/> |
| 163 |
<button |
| 164 |
type="button" |
| 165 |
className="aibui-carousel-remove" |
| 166 |
onClick={() => removeSlide(i)} |
| 167 |
> |
| 168 |
× |
| 169 |
</button> |
| 170 |
</div> |
| 171 |
))} |
| 172 |
</div> |
| 173 |
<div className="aibui-carousel-controls"> |
| 174 |
{showArrows && ( |
| 175 |
<> |
| 176 |
<button |
| 177 |
className="aibui-carousel-arrow aibui-carousel-prev" |
| 178 |
aria-label={__("Previous", "ai-builder")} |
| 179 |
onClick={() => go(index - 1)} |
| 180 |
> |
| 181 |
‹ |
| 182 |
</button> |
| 183 |
<button |
| 184 |
className="aibui-carousel-arrow aibui-carousel-next" |
| 185 |
aria-label={__("Next", "ai-builder")} |
| 186 |
onClick={() => go(index + 1)} |
| 187 |
> |
| 188 |
› |
| 189 |
</button> |
| 190 |
</> |
| 191 |
)} |
| 192 |
{showDots && ( |
| 193 |
<div className="aibui-carousel-dots"> |
| 194 |
{slides.map((_, i) => ( |
| 195 |
<span |
| 196 |
key={i} |
| 197 |
className={`aibui-carousel-dot${ |
| 198 |
i === index ? " is-active" : "" |
| 199 |
}`} |
| 200 |
onClick={() => go(i)} |
| 201 |
/> |
| 202 |
))} |
| 203 |
</div> |
| 204 |
)} |
| 205 |
</div> |
| 206 |
</div> |
| 207 |
|
| 208 |
{/* actions moved into panel */} |
| 209 |
</div> |
| 210 |
); |
| 211 |
}, |
| 212 |
save: (props) => { |
| 213 |
const { attributes } = props; |
| 214 |
const { |
| 215 |
slides, |
| 216 |
showArrows, |
| 217 |
showDots, |
| 218 |
autoplay, |
| 219 |
autoplayDelay, |
| 220 |
height, |
| 221 |
fit, |
| 222 |
} = attributes; |
| 223 |
const blockProps = wp.blockEditor |
| 224 |
? wp.blockEditor.useBlockProps.save({ className: "aibui-carousel" }) |
| 225 |
: {}; |
| 226 |
return ( |
| 227 |
<div |
| 228 |
{...blockProps} |
| 229 |
data-autoplay={autoplay ? "1" : "0"} |
| 230 |
data-delay={autoplayDelay} |
| 231 |
> |
| 232 |
<div className="aibui-carousel-preview" style={{ height }}> |
| 233 |
<div className="aibui-carousel-track"> |
| 234 |
{slides.map((s, i) => ( |
| 235 |
<div key={i} className="aibui-carousel-slide"> |
| 236 |
{s.url ? ( |
| 237 |
<img |
| 238 |
src={s.url} |
| 239 |
alt={s.alt || ""} |
| 240 |
style={{ objectFit: fit || "cover" }} |
| 241 |
/> |
| 242 |
) : null} |
| 243 |
{s.caption ? ( |
| 244 |
<div |
| 245 |
className="aibui-carousel-caption" |
| 246 |
dangerouslySetInnerHTML={{ __html: s.caption }} |
| 247 |
/> |
| 248 |
) : null} |
| 249 |
</div> |
| 250 |
))} |
| 251 |
</div> |
| 252 |
<div className="aibui-carousel-controls"> |
| 253 |
{showArrows ? ( |
| 254 |
<> |
| 255 |
<button |
| 256 |
className="aibui-carousel-arrow aibui-carousel-prev" |
| 257 |
aria-label="Previous" |
| 258 |
> |
| 259 |
‹ |
| 260 |
</button> |
| 261 |
<button |
| 262 |
className="aibui-carousel-arrow aibui-carousel-next" |
| 263 |
aria-label="Next" |
| 264 |
> |
| 265 |
› |
| 266 |
</button> |
| 267 |
</> |
| 268 |
) : null} |
| 269 |
{showDots ? ( |
| 270 |
<div className="aibui-carousel-dots"> |
| 271 |
{slides.map((_, i) => ( |
| 272 |
<span |
| 273 |
key={i} |
| 274 |
className={`aibui-carousel-dot${ |
| 275 |
i === 0 ? " is-active" : "" |
| 276 |
}`} |
| 277 |
/> |
| 278 |
))} |
| 279 |
</div> |
| 280 |
) : null} |
| 281 |
</div> |
| 282 |
</div> |
| 283 |
</div> |
| 284 |
); |
| 285 |
}, |
| 286 |
}); |
| 287 |
|