PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.6
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.6
2.0.13 2.0.12 2.0.11 2.0.10 2.0.9 trunk 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8
blockenberg / blocks / testimonials / index.js

index.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.6, at blocks/testimonials/index.js

1,126 lines 55.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 wp.domReady(function () {
2 var el = wp.element.createElement;
3 var Fragment = wp.element.Fragment;
4 var __ = wp.i18n.__;
5 var useState = wp.element.useState;
6 var useRef = wp.element.useRef;
7 var useEffect = wp.element.useEffect;
8
9 var registerBlockType = wp.blocks.registerBlockType;
10
11 var InspectorControls = wp.blockEditor.InspectorControls;
12 var BlockControls = wp.blockEditor.BlockControls;
13 var RichText = wp.blockEditor.RichText;
14 var useBlockProps = wp.blockEditor.useBlockProps;
15
16 var PanelBody = wp.components.PanelBody;
17 var PanelColorSettings = wp.blockEditor.PanelColorSettings;
18 var ToggleControl = wp.components.ToggleControl;
19 var RangeControl = wp.components.RangeControl;
20 var SelectControl = wp.components.SelectControl;
21 var Button = wp.components.Button;
22 var ToolbarGroup = wp.components.ToolbarGroup;
23 var ToolbarButton = wp.components.ToolbarButton;
24
25 function clone(obj) {
26 var out = {};
27 for (var k in obj) out[k] = obj[k];
28 return out;
29 }
30
31 function getAvatarRadius(shape, radiusPx) {
32 if (shape === 'circle') return '999px';
33 if (shape === 'square') return '0px';
34 return (radiusPx || 12) + 'px';
35 }
36
37 function openMedia(onSelect) {
38 if (!wp || !wp.media) return;
39 var frame = wp.media({
40 title: __('Select Image', 'blockenberg'),
41 button: { text: __('Use image', 'blockenberg') },
42 multiple: false
43 });
44
45 frame.on('select', function () {
46 var attachment = frame.state().get('selection').first().toJSON();
47 onSelect(attachment);
48 });
49
50 frame.open();
51 }
52
53 function clamp(n, min, max) {
54 return Math.max(min, Math.min(max, n));
55 }
56
57 function renderStars(rating, maxStars, onSelect) {
58 var stars = [];
59 for (var i = 1; i <= maxStars; i++) {
60 var icon = i <= rating ? 'star-filled' : 'star-empty';
61 if (typeof onSelect === 'function') {
62 stars.push(
63 el('button', {
64 key: i,
65 type: 'button',
66 className: 'bkbg-ts-star-btn',
67 'aria-label': __('Set rating', 'blockenberg') + ': ' + i,
68 onClick: (function (val) {
69 return function (e) {
70 e.preventDefault();
71 e.stopPropagation();
72 onSelect(val);
73 };
74 })(i)
75 },
76 el('span', { className: 'bkbg-ts-star dashicons dashicons-' + icon, 'aria-hidden': 'true' })
77 )
78 );
79 } else {
80 stars.push(el('span', { key: i, className: 'bkbg-ts-star dashicons dashicons-' + icon, 'aria-hidden': 'true' }));
81 }
82 }
83 return stars;
84 }
85
86 registerBlockType('blockenberg/testimonials', {
87 title: __('Testimonials', 'blockenberg'),
88 icon: 'testimonial',
89 category: 'blockenberg',
90 description: __('Customer testimonials with avatars, ratings, and beautiful layouts.', 'blockenberg'),
91
92 edit: function (props) {
93 var attributes = props.attributes;
94 var setAttributes = props.setAttributes;
95 var isSelected = props.isSelected;
96 var a = attributes;
97
98 var activeState = useState(0);
99 var activeIndex = activeState[0];
100 var setActiveIndex = activeState[1];
101
102 function setItems(items) {
103 setAttributes({ items: items });
104 }
105
106 function updateItem(index, field, value) {
107 var next = a.items.map(function (it, i) {
108 if (i !== index) return it;
109 var ni = clone(it);
110 ni[field] = value;
111 return ni;
112 });
113 setItems(next);
114 }
115
116 function updateAvatar(index, image) {
117 var img = {
118 id: image && image.id ? image.id : 0,
119 url: image && image.url ? image.url : '',
120 alt: image && image.alt ? image.alt : ''
121 };
122 updateItem(index, 'avatar', img);
123 }
124
125 function addItem() {
126 var next = a.items.concat([
127 {
128 quote: __('Write the testimonial here. Click to edit.', 'blockenberg'),
129 name: __('Name', 'blockenberg'),
130 role: __('Role', 'blockenberg'),
131 company: __('Company', 'blockenberg'),
132 rating: 5,
133 avatar: { id: 0, url: '', alt: '' }
134 }
135 ]);
136 setItems(next);
137 setActiveIndex(next.length - 1);
138 }
139
140 function removeItem(index) {
141 if (a.items.length <= 1) return;
142 var next = a.items.filter(function (_, i) { return i !== index; });
143 setItems(next);
144 setActiveIndex(Math.max(0, Math.min(activeIndex, next.length - 1)));
145 }
146
147 function duplicateItem(index) {
148 var next = a.items.slice();
149 var copy = clone(a.items[index]);
150 copy.avatar = clone(copy.avatar || { id: 0, url: '', alt: '' });
151 next.splice(index + 1, 0, copy);
152 setItems(next);
153 setActiveIndex(index + 1);
154 }
155
156 function moveItem(index, dir) {
157 var ni = index + dir;
158 if (ni < 0 || ni >= a.items.length) return;
159 var next = a.items.slice();
160 var tmp = next[index];
161 next[index] = next[ni];
162 next[ni] = tmp;
163 setItems(next);
164 setActiveIndex(ni);
165 }
166
167 var fontWeightOptions = [
168 { label: '300', value: 300 },
169 { label: '400', value: 400 },
170 { label: '500', value: 500 },
171 { label: '600', value: 600 },
172 { label: '700', value: 700 },
173 { label: '800', value: 800 }
174 ];
175
176 var columnsOptions = [
177 { label: '1', value: 1 },
178 { label: '2', value: 2 },
179 { label: '3', value: 3 },
180 { label: '4', value: 4 }
181 ];
182
183 var alignOptions = [
184 { label: __('Left', 'blockenberg'), value: 'left' },
185 { label: __('Center', 'blockenberg'), value: 'center' },
186 { label: __('Right', 'blockenberg'), value: 'right' }
187 ];
188
189 var layoutOptions = [
190 { label: __('Grid', 'blockenberg'), value: 'grid' },
191 { label: __('Carousel', 'blockenberg'), value: 'carousel' }
192 ];
193
194 var avatarShapeOptions = [
195 { label: __('Circle', 'blockenberg'), value: 'circle' },
196 { label: __('Rounded', 'blockenberg'), value: 'rounded' },
197 { label: __('Square', 'blockenberg'), value: 'square' }
198 ];
199
200 var activeItem = a.items[activeIndex] || a.items[0];
201
202 var inspector = el(InspectorControls, {},
203 el(PanelBody, { title: __('Layout', 'blockenberg'), initialOpen: true },
204 el(SelectControl, {
205 label: __('Style', 'blockenberg'),
206 value: a.layoutStyle,
207 options: layoutOptions,
208 onChange: function (v) { setAttributes({ layoutStyle: v }); }
209 }),
210 a.layoutStyle === 'grid' && el(SelectControl, {
211 label: __('Columns', 'blockenberg'),
212 value: a.columns,
213 options: columnsOptions,
214 onChange: function (v) { setAttributes({ columns: parseInt(v, 10) }); }
215 }),
216 el(RangeControl, {
217 label: __('Gap', 'blockenberg'),
218 value: a.gap,
219 min: 8,
220 max: 60,
221 onChange: function (v) { setAttributes({ gap: v }); }
222 }),
223 el(SelectControl, {
224 label: __('Text Align', 'blockenberg'),
225 value: a.textAlign,
226 options: alignOptions,
227 onChange: function (v) { setAttributes({ textAlign: v }); }
228 }),
229 a.layoutStyle === 'carousel' && el(ToggleControl, {
230 label: __('Peek Next Slide', 'blockenberg'),
231 checked: a.carouselPeek,
232 __nextHasNoMarginBottom: true,
233 onChange: function (v) { setAttributes({ carouselPeek: v }); }
234 }),
235 a.layoutStyle === 'carousel' && el(ToggleControl, {
236 label: __('Show Navigation', 'blockenberg'),
237 checked: a.carouselNav,
238 __nextHasNoMarginBottom: true,
239 onChange: function (v) { setAttributes({ carouselNav: v }); }
240 }),
241 a.layoutStyle === 'carousel' && el(SelectControl, {
242 label: __('Arrow Style', 'blockenberg'),
243 value: a.carouselNavStyle,
244 options: [
245 { label: __('Top Right', 'blockenberg'), value: 'top' },
246 { label: __('Overlay Sides', 'blockenberg'), value: 'overlay' },
247 { label: __('Bottom Center', 'blockenberg'), value: 'bottom' }
248 ],
249 onChange: function (v) { setAttributes({ carouselNavStyle: v }); }
250 }),
251 a.layoutStyle === 'carousel' && el(SelectControl, {
252 label: __('Indicators', 'blockenberg'),
253 value: a.carouselIndicators,
254 options: [
255 { label: __('None', 'blockenberg'), value: 'none' },
256 { label: __('Dots', 'blockenberg'), value: 'dots' },
257 { label: __('Progress Bar', 'blockenberg'), value: 'progress' },
258 { label: __('Fraction (1/6)', 'blockenberg'), value: 'fraction' }
259 ],
260 onChange: function (v) { setAttributes({ carouselIndicators: v }); }
261 }),
262 a.layoutStyle === 'carousel' && a.carouselIndicators === 'dots' && el(SelectControl, {
263 label: __('Dots Style', 'blockenberg'),
264 value: a.carouselDotsStyle,
265 options: [
266 { label: __('Dots', 'blockenberg'), value: 'default' },
267 { label: __('Active Pill', 'blockenberg'), value: 'pill' }
268 ],
269 onChange: function (v) { setAttributes({ carouselDotsStyle: v }); }
270 }),
271 a.layoutStyle === 'carousel' && a.carouselIndicators === 'dots' && el(RangeControl, {
272 label: __('Dot Size', 'blockenberg'),
273 value: a.carouselIndicatorSize,
274 min: 6,
275 max: 16,
276 onChange: function (v) { setAttributes({ carouselIndicatorSize: v }); }
277 }),
278 a.layoutStyle === 'carousel' && a.carouselIndicators === 'progress' && el(SelectControl, {
279 label: __('Progress Style', 'blockenberg'),
280 value: a.carouselProgressStyle,
281 options: [
282 { label: __('Contained', 'blockenberg'), value: 'default' },
283 { label: __('Edge-to-edge', 'blockenberg'), value: 'edge' }
284 ],
285 onChange: function (v) { setAttributes({ carouselProgressStyle: v }); }
286 }),
287 a.layoutStyle === 'carousel' && a.carouselIndicators === 'progress' && el(RangeControl, {
288 label: __('Progress Height', 'blockenberg'),
289 value: a.carouselProgressHeight,
290 min: 2,
291 max: 14,
292 onChange: function (v) { setAttributes({ carouselProgressHeight: v }); }
293 }),
294 a.layoutStyle === 'carousel' && a.carouselIndicators === 'fraction' && el(RangeControl, {
295 label: __('Fraction Text Size', 'blockenberg'),
296 value: a.carouselFractionSize,
297 min: 10,
298 max: 18,
299 onChange: function (v) { setAttributes({ carouselFractionSize: v }); }
300 }),
301 a.layoutStyle === 'carousel' && a.carouselIndicators !== 'none' && el(SelectControl, {
302 label: __('Indicators Position', 'blockenberg'),
303 value: a.carouselIndicatorsPosition,
304 options: [
305 { label: __('Below', 'blockenberg'), value: 'below' },
306 { label: __('Above', 'blockenberg'), value: 'top' }
307 ],
308 onChange: function (v) { setAttributes({ carouselIndicatorsPosition: v }); }
309 }),
310 a.layoutStyle === 'carousel' && el(ToggleControl, {
311 label: __('Infinite Loop', 'blockenberg'),
312 checked: a.infiniteScroll !== false,
313 __nextHasNoMarginBottom: true,
314 onChange: function (v) { setAttributes({ infiniteScroll: !!v }); }
315 }),
316 a.layoutStyle === 'carousel' && el(ToggleControl, {
317 label: __('Auto-scroll (continuous)', 'blockenberg'),
318 help: __('Runs on the live site (frontend). Please check it there.', 'blockenberg'),
319 checked: !!a.autoScroll,
320 __nextHasNoMarginBottom: true,
321 onChange: function (v) { setAttributes({ autoScroll: !!v }); }
322 }),
323 a.layoutStyle === 'carousel' && a.autoScroll && el(RangeControl, {
324 label: __('Auto-scroll speed (px/sec)', 'blockenberg'),
325 value: a.autoScrollSpeed || 40,
326 min: 5,
327 max: 200,
328 step: 1,
329 onChange: function (v) { setAttributes({ autoScrollSpeed: v }); }
330 }),
331 a.layoutStyle === 'carousel' && a.autoScroll && el(ToggleControl, {
332 label: __('Pause on hover', 'blockenberg'),
333 checked: a.autoScrollPauseOnHover !== false,
334 __nextHasNoMarginBottom: true,
335 onChange: function (v) { setAttributes({ autoScrollPauseOnHover: !!v }); }
336 })
337 ),
338
339 el(PanelBody, { title: __('Card', 'blockenberg'), initialOpen: false },
340 el(RangeControl, {
341 label: __('Padding', 'blockenberg'),
342 value: a.cardPadding,
343 min: 8,
344 max: 60,
345 onChange: function (v) { setAttributes({ cardPadding: v }); }
346 }),
347 el(RangeControl, {
348 label: __('Border Radius', 'blockenberg'),
349 value: a.cardRadius,
350 min: 0,
351 max: 40,
352 onChange: function (v) { setAttributes({ cardRadius: v }); }
353 }),
354 el(RangeControl, {
355 label: __('Border Width', 'blockenberg'),
356 value: a.cardBorderWidth,
357 min: 0,
358 max: 6,
359 onChange: function (v) { setAttributes({ cardBorderWidth: v }); }
360 }),
361 el(ToggleControl, {
362 label: __('Shadow', 'blockenberg'),
363 checked: a.cardShadow,
364 __nextHasNoMarginBottom: true,
365 onChange: function (v) { setAttributes({ cardShadow: v }); }
366 })
367 ),
368
369 el(PanelBody, { title: __('Elements', 'blockenberg'), initialOpen: false },
370 el(ToggleControl, {
371 label: __('Show Quote Icon', 'blockenberg'),
372 checked: a.showQuoteIcon,
373 __nextHasNoMarginBottom: true,
374 onChange: function (v) { setAttributes({ showQuoteIcon: v }); }
375 }),
376 a.showQuoteIcon && el(RangeControl, {
377 label: __('Quote Icon Size', 'blockenberg'),
378 value: a.quoteIconSize,
379 min: 12,
380 max: 48,
381 onChange: function (v) { setAttributes({ quoteIconSize: v }); }
382 }),
383 el(ToggleControl, {
384 label: __('Show Avatar', 'blockenberg'),
385 checked: a.showAvatar,
386 __nextHasNoMarginBottom: true,
387 onChange: function (v) { setAttributes({ showAvatar: v }); }
388 }),
389 a.showAvatar && el(SelectControl, {
390 label: __('Avatar Shape', 'blockenberg'),
391 value: a.avatarShape,
392 options: avatarShapeOptions,
393 onChange: function (v) { setAttributes({ avatarShape: v }); }
394 }),
395 a.showAvatar && el(RangeControl, {
396 label: __('Avatar Size', 'blockenberg'),
397 value: a.avatarSize,
398 min: 24,
399 max: 120,
400 onChange: function (v) { setAttributes({ avatarSize: v }); }
401 }),
402 el(ToggleControl, {
403 label: __('Show Rating', 'blockenberg'),
404 checked: a.showRating,
405 __nextHasNoMarginBottom: true,
406 onChange: function (v) { setAttributes({ showRating: v }); }
407 }),
408 a.showRating && el(RangeControl, {
409 label: __('Max Stars', 'blockenberg'),
410 value: a.ratingStars,
411 min: 3,
412 max: 5,
413 onChange: function (v) { setAttributes({ ratingStars: v }); }
414 }),
415 el(ToggleControl, {
416 label: __('Show Name', 'blockenberg'),
417 checked: a.showName,
418 __nextHasNoMarginBottom: true,
419 onChange: function (v) { setAttributes({ showName: v }); }
420 }),
421 el(ToggleControl, {
422 label: __('Show Role', 'blockenberg'),
423 checked: a.showRole,
424 __nextHasNoMarginBottom: true,
425 onChange: function (v) { setAttributes({ showRole: v }); }
426 }),
427 el(ToggleControl, {
428 label: __('Show Company', 'blockenberg'),
429 checked: a.showCompany,
430 __nextHasNoMarginBottom: true,
431 onChange: function (v) { setAttributes({ showCompany: v }); }
432 })
433 ),
434
435 el(PanelBody, { title: __('Typography', 'blockenberg'), initialOpen: false },
436 el(RangeControl, {
437 label: __('Quote Size', 'blockenberg'),
438 value: a.quoteSize,
439 min: 12,
440 max: 24,
441 onChange: function (v) { setAttributes({ quoteSize: v }); }
442 }),
443 el(SelectControl, {
444 label: __('Quote Weight', 'blockenberg'),
445 value: a.quoteWeight,
446 options: fontWeightOptions,
447 onChange: function (v) { setAttributes({ quoteWeight: parseInt(v, 10) }); }
448 }),
449 el(RangeControl, {
450 label: __('Name Size', 'blockenberg'),
451 value: a.nameSize,
452 min: 12,
453 max: 28,
454 onChange: function (v) { setAttributes({ nameSize: v }); }
455 }),
456 el(SelectControl, {
457 label: __('Name Weight', 'blockenberg'),
458 value: a.nameWeight,
459 options: fontWeightOptions,
460 onChange: function (v) { setAttributes({ nameWeight: parseInt(v, 10) }); }
461 }),
462 el(RangeControl, {
463 label: __('Role Size', 'blockenberg'),
464 value: a.roleSize,
465 min: 10,
466 max: 20,
467 onChange: function (v) { setAttributes({ roleSize: v }); }
468 }),
469 el(SelectControl, {
470 label: __('Role Weight', 'blockenberg'),
471 value: a.roleWeight,
472 options: fontWeightOptions,
473 onChange: function (v) { setAttributes({ roleWeight: parseInt(v, 10) }); }
474 }),
475 el(RangeControl, {
476 label: __('Rating Size', 'blockenberg'),
477 value: a.ratingSize,
478 min: 12,
479 max: 24,
480 onChange: function (v) { setAttributes({ ratingSize: v }); }
481 })
482 ),
483
484 el(PanelColorSettings, {
485 title: __('Colors', 'blockenberg'),
486 initialOpen: false,
487 colorSettings: [
488 { value: a.cardBg, onChange: function (c) { setAttributes({ cardBg: c }); }, label: __('Card Background', 'blockenberg') },
489 { value: a.cardBorderColor, onChange: function (c) { setAttributes({ cardBorderColor: c }); }, label: __('Card Border', 'blockenberg') },
490 { value: a.featuredBorderColor, onChange: function (c) { setAttributes({ featuredBorderColor: c }); }, label: __('Featured Border', 'blockenberg') },
491 { value: a.quoteIconColor, onChange: function (c) { setAttributes({ quoteIconColor: c }); }, label: __('Quote Icon', 'blockenberg') },
492 { value: a.quoteColor, onChange: function (c) { setAttributes({ quoteColor: c }); }, label: __('Quote', 'blockenberg') },
493 { value: a.nameColor, onChange: function (c) { setAttributes({ nameColor: c }); }, label: __('Name', 'blockenberg') },
494 { value: a.roleColor, onChange: function (c) { setAttributes({ roleColor: c }); }, label: __('Role', 'blockenberg') },
495 { value: a.companyColor, onChange: function (c) { setAttributes({ companyColor: c }); }, label: __('Company', 'blockenberg') },
496 { value: a.ratingColor, onChange: function (c) { setAttributes({ ratingColor: c }); }, label: __('Rating', 'blockenberg') },
497
498 { value: a.carouselIndicatorColor, onChange: function (c) { setAttributes({ carouselIndicatorColor: c }); }, label: __('Carousel Indicator', 'blockenberg') },
499 { value: a.carouselIndicatorActiveColor, onChange: function (c) { setAttributes({ carouselIndicatorActiveColor: c }); }, label: __('Carousel Indicator (Active)', 'blockenberg') },
500 { value: a.carouselProgressBg, onChange: function (c) { setAttributes({ carouselProgressBg: c }); }, label: __('Carousel Progress (Track)', 'blockenberg') },
501 { value: a.carouselProgressFg, onChange: function (c) { setAttributes({ carouselProgressFg: c }); }, label: __('Carousel Progress (Fill)', 'blockenberg') },
502 { value: a.carouselFractionColor, onChange: function (c) { setAttributes({ carouselFractionColor: c }); }, label: __('Carousel Fraction', 'blockenberg') }
503 ]
504 }),
505
506 el(PanelBody, { title: __('Selected Testimonial', 'blockenberg'), initialOpen: false },
507 activeItem ? el('div', {},
508 el('div', { style: { marginBottom: '10px', fontWeight: 600 } }, __('Item', 'blockenberg') + ': ' + (activeIndex + 1)),
509 el(ToggleControl, {
510 label: __('Featured Testimonial', 'blockenberg'),
511 checked: a.featuredIndex === activeIndex,
512 __nextHasNoMarginBottom: true,
513 onChange: function (v) {
514 setAttributes({ featuredIndex: v ? activeIndex : -1 });
515 }
516 }),
517 el(Button, {
518 isSecondary: true,
519 onClick: function () {
520 openMedia(function (att) {
521 updateAvatar(activeIndex, { id: att.id, url: att.url, alt: att.alt || '' });
522 });
523 }
524 }, activeItem.avatar && activeItem.avatar.url ? __('Change Avatar', 'blockenberg') : __('Select Avatar', 'blockenberg')),
525 activeItem.avatar && activeItem.avatar.url && el(Button, {
526 isLink: true,
527 isDestructive: true,
528 onClick: function () { updateAvatar(activeIndex, { id: 0, url: '', alt: '' }); }
529 }, __('Remove Avatar', 'blockenberg')),
530 a.showRating && el(RangeControl, {
531 label: __('Rating', 'blockenberg'),
532 value: clamp(parseInt(activeItem.rating || 0, 10), 0, 5),
533 min: 0,
534 max: 5,
535 onChange: function (v) { updateItem(activeIndex, 'rating', v); }
536 })
537 ) : el('div', {}, __('Select a card to edit its settings.', 'blockenberg'))
538 )
539 );
540
541 var blockControls = el(BlockControls, {},
542 el(ToolbarGroup, {},
543 el(ToolbarButton, {
544 icon: 'plus',
545 label: __('Add Testimonial', 'blockenberg'),
546 onClick: addItem
547 }),
548 el(ToolbarButton, {
549 icon: (a.featuredIndex === activeIndex) ? 'star-filled' : 'star-empty',
550 label: __('Toggle Featured', 'blockenberg'),
551 onClick: function () {
552 setAttributes({ featuredIndex: (a.featuredIndex === activeIndex) ? -1 : activeIndex });
553 }
554 })
555 )
556 );
557
558 var styleVars = {
559 '--bkbg-ts-columns': String(a.columns),
560 '--bkbg-ts-gap': a.gap + 'px',
561 '--bkbg-ts-card-bg': a.cardBg,
562 '--bkbg-ts-card-border-color': a.cardBorderColor,
563 '--bkbg-ts-card-border-width': a.cardBorderWidth + 'px',
564 '--bkbg-ts-card-radius': a.cardRadius + 'px',
565 '--bkbg-ts-card-padding': a.cardPadding + 'px',
566 '--bkbg-ts-text-align': a.textAlign,
567 '--bkbg-ts-featured-border-color': a.featuredBorderColor,
568 '--bkbg-ts-quote-icon-color': a.quoteIconColor,
569 '--bkbg-ts-quote-icon-size': a.quoteIconSize + 'px',
570 '--bkbg-ts-quote-color': a.quoteColor,
571 '--bkbg-ts-quote-size': a.quoteSize + 'px',
572 '--bkbg-ts-quote-weight': String(a.quoteWeight),
573 '--bkbg-ts-name-color': a.nameColor,
574 '--bkbg-ts-name-size': a.nameSize + 'px',
575 '--bkbg-ts-name-weight': String(a.nameWeight),
576 '--bkbg-ts-role-color': a.roleColor,
577 '--bkbg-ts-role-size': a.roleSize + 'px',
578 '--bkbg-ts-role-weight': String(a.roleWeight),
579 '--bkbg-ts-company-color': a.companyColor,
580 '--bkbg-ts-rating-color': a.ratingColor,
581 '--bkbg-ts-rating-size': a.ratingSize + 'px',
582 '--bkbg-ts-avatar-size': a.avatarSize + 'px',
583 '--bkbg-ts-avatar-radius': getAvatarRadius(a.avatarShape, 12),
584
585 '--bkbg-ts-indicator-size': (a.carouselIndicatorSize || 8) + 'px',
586 '--bkbg-ts-indicator-color': a.carouselIndicatorColor,
587 '--bkbg-ts-indicator-active-color': a.carouselIndicatorActiveColor,
588 '--bkbg-ts-progress-height': (a.carouselProgressHeight || 6) + 'px',
589 '--bkbg-ts-progress-bg': a.carouselProgressBg,
590 '--bkbg-ts-progress-fg': a.carouselProgressFg,
591 '--bkbg-ts-fraction-color': a.carouselFractionColor,
592 '--bkbg-ts-fraction-size': (a.carouselFractionSize || 13) + 'px'
593 };
594
595 var classes = ['bkbg-ts-wrap', 'bkbg-ts-align-' + a.textAlign];
596 if (a.layoutStyle === 'carousel') {
597 classes.push('bkbg-ts-carousel');
598 if (a.carouselPeek) classes.push('bkbg-ts-carousel-peek');
599 classes.push('bkbg-ts-nav-' + (a.carouselNavStyle || 'top'));
600 classes.push('bkbg-ts-indicators-' + (a.carouselIndicators || 'none'));
601 classes.push('bkbg-ts-indicators-pos-' + (a.carouselIndicatorsPosition || 'below'));
602
603 if (a.carouselIndicators === 'dots') {
604 classes.push('bkbg-ts-dots-style-' + (a.carouselDotsStyle || 'default'));
605 }
606 if (a.carouselIndicators === 'progress') {
607 classes.push('bkbg-ts-progress-style-' + (a.carouselProgressStyle || 'default'));
608 }
609 }
610
611 var blockProps = useBlockProps({
612 className: 'bkbg-editor-wrap ' + classes.join(' '),
613 'data-block-label': 'Testimonials'
614 });
615
616 function renderAvatar(item, index) {
617 if (!a.showAvatar) return null;
618 var hasImg = item.avatar && item.avatar.url;
619
620 return el('div', {
621 className: 'bkbg-ts-avatar',
622 onClick: function (e) {
623 e.preventDefault();
624 e.stopPropagation();
625 setActiveIndex(index);
626 openMedia(function (att) {
627 updateAvatar(index, { id: att.id, url: att.url, alt: att.alt || '' });
628 });
629 },
630 title: __('Click to change avatar', 'blockenberg')
631 },
632 hasImg
633 ? el('img', { src: item.avatar.url, alt: item.avatar.alt || '' })
634 : el('span', { className: 'dashicons dashicons-format-image', 'aria-hidden': 'true' })
635 );
636 }
637
638 function renderRoleLine(item, index) {
639 if (!a.showRole && !a.showCompany) return null;
640
641 return el('p', { className: 'bkbg-ts-roleline' },
642 a.showRole && el(RichText, {
643 tagName: 'span',
644 value: item.role,
645 placeholder: __('Role', 'blockenberg'),
646 onChange: function (v) { updateItem(index, 'role', v); }
647 }),
648 a.showCompany && el(Fragment, {},
649 a.showRole && item.company && el('span', { 'aria-hidden': 'true' }, ' · '),
650 el(RichText, {
651 tagName: 'span',
652 className: 'bkbg-ts-company',
653 value: item.company,
654 placeholder: __('Company', 'blockenberg'),
655 onChange: function (v) { updateItem(index, 'company', v); }
656 })
657 )
658 );
659 }
660
661 function renderHeader(item, index) {
662 return el('div', { className: 'bkbg-ts-meta' },
663 renderAvatar(item, index),
664 el('div', { className: 'bkbg-ts-author' },
665 a.showName && el(RichText, {
666 tagName: 'p',
667 className: 'bkbg-ts-name',
668 value: item.name,
669 placeholder: __('Name', 'blockenberg'),
670 onChange: function (v) { updateItem(index, 'name', v); }
671 }),
672 renderRoleLine(item, index)
673 )
674 );
675 }
676
677 var trackRef = useRef(null);
678 var activeSlideState = useState(0);
679 var activeSlide = activeSlideState[0];
680 var setActiveSlide = activeSlideState[1];
681
682 var progressState = useState(0);
683 var progressPct = progressState[0];
684 var setProgressPct = progressState[1];
685
686 function getSlides() {
687 if (!trackRef.current) return [];
688 return trackRef.current.querySelectorAll('.bkbg-ts-slide');
689 }
690
691 function scrollToIndex(i) {
692 if (!trackRef.current) return;
693 var slides = getSlides();
694 if (!slides.length) return;
695 var idx = Math.max(0, Math.min(slides.length - 1, i));
696 slides[idx].scrollIntoView({ behavior: 'smooth', inline: 'start', block: 'nearest' });
697 }
698
699 function scrollByOne(dir) {
700 if (!trackRef.current) return;
701 var slides = getSlides();
702 if (!slides.length) return;
703
704 var trackRect = trackRef.current.getBoundingClientRect();
705 var currentIndex = 0;
706 for (var i = 0; i < slides.length; i++) {
707 var r = slides[i].getBoundingClientRect();
708 if (r.left >= trackRect.left - 10) {
709 currentIndex = i;
710 break;
711 }
712 }
713 scrollToIndex(currentIndex + dir);
714 }
715
716 useEffect(function () {
717 if (!trackRef.current) return;
718 var track = trackRef.current;
719
720 function getScrollPct() {
721 var max = track.scrollWidth - track.clientWidth;
722 if (max <= 0) return 0;
723 var pct = (track.scrollLeft / max) * 100;
724 if (pct < 0) pct = 0;
725 if (pct > 100) pct = 100;
726 return pct;
727 }
728
729 function updateActiveFromScroll() {
730 var slides = getSlides();
731 if (!slides.length) return;
732 var trackRect = track.getBoundingClientRect();
733 var idx = 0;
734 for (var i = 0; i < slides.length; i++) {
735 var r = slides[i].getBoundingClientRect();
736 if (r.left >= trackRect.left - 10) {
737 idx = i;
738 break;
739 }
740 }
741 setActiveSlide(idx);
742
743 // Smooth progress update
744 setProgressPct(getScrollPct());
745 }
746
747 updateActiveFromScroll();
748 track.addEventListener('scroll', updateActiveFromScroll, { passive: true });
749 return function () {
750 track.removeEventListener('scroll', updateActiveFromScroll);
751 };
752 }, [a.layoutStyle, a.items.length]);
753
754 function renderIndicators() {
755 if (a.carouselIndicators === 'none') return null;
756 if (a.carouselIndicators === 'progress') {
757 return el('div', { className: 'bkbg-ts-indicators' },
758 el('div', { className: 'bkbg-ts-progress' },
759 el('div', { className: 'bkbg-ts-progress-bar', style: { width: progressPct + '%' } })
760 )
761 );
762 }
763
764 if (a.carouselIndicators === 'fraction') {
765 var total = Math.max(1, (a.items || []).length);
766 return el('div', { className: 'bkbg-ts-indicators' },
767 el('div', { className: 'bkbg-ts-fraction', 'aria-label': __('Carousel position', 'blockenberg') },
768 el('span', { className: 'bkbg-ts-fraction-current' }, String(activeSlide + 1)),
769 el('span', { 'aria-hidden': 'true' }, '/'),
770 el('span', { className: 'bkbg-ts-fraction-total' }, String(total))
771 )
772 );
773 }
774
775 // dots
776 return el('div', { className: 'bkbg-ts-indicators' },
777 el('div', { className: 'bkbg-ts-dots', role: 'tablist', 'aria-label': __('Testimonials Carousel', 'blockenberg') },
778 (a.items || []).map(function (_, i) {
779 return el('button', {
780 key: i,
781 type: 'button',
782 className: 'bkbg-ts-dot' + (i === activeSlide ? ' is-active' : ''),
783 'aria-label': __('Go to slide', 'blockenberg') + ' ' + (i + 1),
784 onClick: function (e) { e.preventDefault(); e.stopPropagation(); scrollToIndex(i); }
785 });
786 })
787 )
788 );
789 }
790
791 function renderCard(item, index) {
792 var isActive = index === activeIndex;
793 var isFeatured = a.featuredIndex === index;
794 var cardClasses = ['bkbg-ts-card'];
795 if (a.cardShadow) cardClasses.push('has-shadow');
796 if (isActive && isSelected) cardClasses.push('is-active');
797 if (isFeatured) cardClasses.push('is-featured');
798
799 return el('div', {
800 key: index,
801 className: cardClasses.join(' '),
802 onClick: function () { setActiveIndex(index); }
803 },
804 el('div', { className: 'bkbg-ts-actions' },
805 el(Button, {
806 icon: 'arrow-left-alt2',
807 label: __('Move Left', 'blockenberg'),
808 disabled: index === 0,
809 onClick: function (e) { e.preventDefault(); e.stopPropagation(); moveItem(index, -1); }
810 }),
811 el(Button, {
812 icon: 'arrow-right-alt2',
813 label: __('Move Right', 'blockenberg'),
814 disabled: index === a.items.length - 1,
815 onClick: function (e) { e.preventDefault(); e.stopPropagation(); moveItem(index, 1); }
816 }),
817 el(Button, {
818 icon: isFeatured ? 'star-filled' : 'star-empty',
819 label: __('Toggle Featured', 'blockenberg'),
820 onClick: function (e) {
821 e.preventDefault();
822 e.stopPropagation();
823 setAttributes({ featuredIndex: isFeatured ? -1 : index });
824 }
825 }),
826 el(Button, {
827 icon: 'admin-page',
828 label: __('Duplicate', 'blockenberg'),
829 onClick: function (e) { e.preventDefault(); e.stopPropagation(); duplicateItem(index); }
830 }),
831 el(Button, {
832 icon: 'trash',
833 label: __('Remove', 'blockenberg'),
834 isDestructive: true,
835 disabled: a.items.length <= 1,
836 onClick: function (e) { e.preventDefault(); e.stopPropagation(); removeItem(index); }
837 })
838 ),
839
840 a.showQuoteIcon && el('div', { className: 'bkbg-ts-quote-icon' },
841 el('span', { className: 'dashicons dashicons-format-quote', 'aria-hidden': 'true' })
842 ),
843
844 el(RichText, {
845 tagName: 'p',
846 className: 'bkbg-ts-quote',
847 value: item.quote,
848 placeholder: __('Testimonial…', 'blockenberg'),
849 onChange: function (v) { updateItem(index, 'quote', v); }
850 }),
851
852 a.showRating && el('div', { className: 'bkbg-ts-rating', 'aria-label': __('Rating', 'blockenberg') },
853 renderStars(
854 clamp(parseInt(item.rating || 0, 10), 0, 5),
855 a.ratingStars,
856 function (val) {
857 var current = clamp(parseInt(item.rating || 0, 10), 0, 5);
858 updateItem(index, 'rating', (val === current) ? 0 : val);
859 }
860 )
861 ),
862
863 el('div', {}, renderHeader(item, index))
864 );
865 }
866
867 var content;
868 if (a.layoutStyle === 'carousel') {
869 var indicators = renderIndicators();
870 var navStyle = a.carouselNavStyle || 'top';
871 var nav = a.carouselNav && el('div', { className: 'bkbg-ts-nav' },
872 el('button', {
873 type: 'button',
874 className: 'bkbg-ts-nav-btn',
875 'data-bkbg-nav': 'prev',
876 'aria-label': __('Previous', 'blockenberg'),
877 onClick: function (e) { e.preventDefault(); e.stopPropagation(); scrollByOne(-1); }
878 }, el('span', { className: 'dashicons dashicons-arrow-left-alt2', 'aria-hidden': 'true' })),
879 el('button', {
880 type: 'button',
881 className: 'bkbg-ts-nav-btn',
882 'data-bkbg-nav': 'next',
883 'aria-label': __('Next', 'blockenberg'),
884 onClick: function (e) { e.preventDefault(); e.stopPropagation(); scrollByOne(1); }
885 }, el('span', { className: 'dashicons dashicons-arrow-right-alt2', 'aria-hidden': 'true' }))
886 );
887
888 var track = el('div', { className: 'bkbg-ts-carousel-track', ref: trackRef },
889 a.items.map(function (it, idx) {
890 return el('div', { key: idx, className: 'bkbg-ts-slide' }, renderCard(it, idx));
891 })
892 );
893
894 // For bottom nav style, render nav outside carousel-inner to avoid z-index issues
895 if (navStyle === 'bottom') {
896 content = el('div', { className: 'bkbg-ts-carousel' },
897 (a.carouselIndicatorsPosition === 'top') && indicators,
898 el('div', { className: 'bkbg-ts-carousel-inner' },
899 track
900 ),
901 nav,
902 (a.carouselIndicatorsPosition !== 'top') && indicators
903 );
904 } else {
905 content = el('div', { className: 'bkbg-ts-carousel' },
906 (a.carouselIndicatorsPosition === 'top') && indicators,
907 el('div', { className: 'bkbg-ts-carousel-inner' },
908 nav,
909 track
910 ),
911 (a.carouselIndicatorsPosition !== 'top') && indicators
912 );
913 }
914 } else {
915 content = el('div', { className: 'bkbg-ts-grid' }, a.items.map(renderCard));
916 }
917
918 return el(Fragment, {},
919 inspector,
920 blockControls,
921 el('div', Object.assign({}, blockProps, { style: styleVars }),
922 content,
923
924 el('div', { className: 'bkbg-editor-actions' },
925 el(Button, { variant: 'secondary', icon: 'plus-alt2', onClick: addItem }, __('Add Testimonial', 'blockenberg'))
926 )
927 )
928 );
929 },
930
931 save: function (props) {
932 var a = props.attributes;
933
934 function getAvatarRadius(shape, radiusPx) {
935 if (shape === 'circle') return '999px';
936 if (shape === 'square') return '0px';
937 return (radiusPx || 12) + 'px';
938 }
939
940 function clamp(n, min, max) {
941 return Math.max(min, Math.min(max, n));
942 }
943
944 function renderStars(rating, maxStars) {
945 var stars = [];
946 for (var i = 1; i <= maxStars; i++) {
947 var icon = i <= rating ? 'star-filled' : 'star-empty';
948 stars.push(el('span', { key: i, className: 'bkbg-ts-star dashicons dashicons-' + icon, 'aria-hidden': 'true' }));
949 }
950 return stars;
951 }
952
953 var styleVars = {
954 '--bkbg-ts-columns': String(a.columns),
955 '--bkbg-ts-gap': a.gap + 'px',
956 '--bkbg-ts-card-bg': a.cardBg,
957 '--bkbg-ts-card-border-color': a.cardBorderColor,
958 '--bkbg-ts-card-border-width': a.cardBorderWidth + 'px',
959 '--bkbg-ts-card-radius': a.cardRadius + 'px',
960 '--bkbg-ts-card-padding': a.cardPadding + 'px',
961 '--bkbg-ts-text-align': a.textAlign,
962 '--bkbg-ts-featured-border-color': a.featuredBorderColor,
963 '--bkbg-ts-quote-icon-color': a.quoteIconColor,
964 '--bkbg-ts-quote-icon-size': a.quoteIconSize + 'px',
965 '--bkbg-ts-quote-color': a.quoteColor,
966 '--bkbg-ts-quote-size': a.quoteSize + 'px',
967 '--bkbg-ts-quote-weight': String(a.quoteWeight),
968 '--bkbg-ts-name-color': a.nameColor,
969 '--bkbg-ts-name-size': a.nameSize + 'px',
970 '--bkbg-ts-name-weight': String(a.nameWeight),
971 '--bkbg-ts-role-color': a.roleColor,
972 '--bkbg-ts-role-size': a.roleSize + 'px',
973 '--bkbg-ts-role-weight': String(a.roleWeight),
974 '--bkbg-ts-company-color': a.companyColor,
975 '--bkbg-ts-rating-color': a.ratingColor,
976 '--bkbg-ts-rating-size': a.ratingSize + 'px',
977 '--bkbg-ts-avatar-size': a.avatarSize + 'px',
978 '--bkbg-ts-avatar-radius': getAvatarRadius(a.avatarShape, 12),
979
980 '--bkbg-ts-indicator-size': (a.carouselIndicatorSize || 8) + 'px',
981 '--bkbg-ts-indicator-color': a.carouselIndicatorColor,
982 '--bkbg-ts-indicator-active-color': a.carouselIndicatorActiveColor,
983 '--bkbg-ts-progress-height': (a.carouselProgressHeight || 6) + 'px',
984 '--bkbg-ts-progress-bg': a.carouselProgressBg,
985 '--bkbg-ts-progress-fg': a.carouselProgressFg,
986 '--bkbg-ts-fraction-color': a.carouselFractionColor,
987 '--bkbg-ts-fraction-size': (a.carouselFractionSize || 13) + 'px'
988 };
989
990 var classes = ['bkbg-ts-wrap', 'bkbg-ts-align-' + a.textAlign];
991 if (a.layoutStyle === 'carousel') {
992 classes.push('bkbg-ts-carousel');
993 if (a.carouselPeek) classes.push('bkbg-ts-carousel-peek');
994 classes.push('bkbg-ts-nav-' + (a.carouselNavStyle || 'top'));
995 classes.push('bkbg-ts-indicators-' + (a.carouselIndicators || 'none'));
996 classes.push('bkbg-ts-indicators-pos-' + (a.carouselIndicatorsPosition || 'below'));
997
998 if (a.carouselIndicators === 'dots') {
999 classes.push('bkbg-ts-dots-style-' + (a.carouselDotsStyle || 'default'));
1000 }
1001 if (a.carouselIndicators === 'progress') {
1002 classes.push('bkbg-ts-progress-style-' + (a.carouselProgressStyle || 'default'));
1003 }
1004 }
1005
1006 var blockProps = useBlockProps.save({
1007 className: classes.join(' '),
1008 style: styleVars,
1009 'data-bkbg-infinite': a.infiniteScroll === false ? '0' : '1',
1010 'data-bkbg-autoscroll': a.autoScroll ? '1' : '0',
1011 'data-bkbg-autoscroll-speed': String(a.autoScrollSpeed || 40),
1012 'data-bkbg-autoscroll-hover': a.autoScrollPauseOnHover === false ? '0' : '1'
1013 });
1014
1015 function renderRoleLine(item) {
1016 if (!a.showRole && !a.showCompany) return null;
1017
1018 return el('p', { className: 'bkbg-ts-roleline' },
1019 a.showRole && el(RichText.Content, { tagName: 'span', value: item.role }),
1020 a.showCompany && el(Fragment, {},
1021 a.showRole && item.company && el('span', { 'aria-hidden': 'true' }, ' · '),
1022 el(RichText.Content, { tagName: 'span', className: 'bkbg-ts-company', value: item.company })
1023 )
1024 );
1025 }
1026
1027 function renderHeader(item) {
1028 return el('div', { className: 'bkbg-ts-meta' },
1029 a.showAvatar && el('div', { className: 'bkbg-ts-avatar' },
1030 item.avatar && item.avatar.url
1031 ? el('img', { src: item.avatar.url, alt: (item.avatar.alt || '') })
1032 : el('span', { className: 'dashicons dashicons-format-image', 'aria-hidden': 'true' })
1033 ),
1034 el('div', { className: 'bkbg-ts-author' },
1035 a.showName && el(RichText.Content, { tagName: 'p', className: 'bkbg-ts-name', value: item.name }),
1036 renderRoleLine(item)
1037 )
1038 );
1039 }
1040
1041 function renderCard(item, index) {
1042 var cardClasses = ['bkbg-ts-card'];
1043 if (a.cardShadow) cardClasses.push('has-shadow');
1044 if (a.featuredIndex === index) cardClasses.push('is-featured');
1045
1046 return el('div', {
1047 key: index,
1048 className: (a.layoutStyle === 'carousel' ? 'bkbg-ts-slide ' : '') + cardClasses.join(' ')
1049 },
1050 a.showQuoteIcon && el('div', { className: 'bkbg-ts-quote-icon' },
1051 el('span', { className: 'dashicons dashicons-format-quote', 'aria-hidden': 'true' })
1052 ),
1053 el(RichText.Content, { tagName: 'p', className: 'bkbg-ts-quote', value: item.quote }),
1054 a.showRating && el('div', { className: 'bkbg-ts-rating', 'aria-label': __('Rating', 'blockenberg') },
1055 renderStars(clamp(parseInt(item.rating || 0, 10), 0, 5), a.ratingStars)
1056 ),
1057 renderHeader(item)
1058 );
1059 }
1060
1061 function renderIndicators() {
1062 if (a.carouselIndicators === 'none') return null;
1063
1064 if (a.carouselIndicators === 'progress') {
1065 return el('div', { className: 'bkbg-ts-indicators' },
1066 el('div', { className: 'bkbg-ts-progress' },
1067 el('div', { className: 'bkbg-ts-progress-bar', style: { width: '0%' } })
1068 )
1069 );
1070 }
1071
1072 if (a.carouselIndicators === 'fraction') {
1073 var total = Math.max(1, (a.items || []).length);
1074 return el('div', { className: 'bkbg-ts-indicators' },
1075 el('div', { className: 'bkbg-ts-fraction', 'aria-label': __('Carousel position', 'blockenberg') },
1076 el('span', { className: 'bkbg-ts-fraction-current', 'data-bkbg-fraction': 'current' }, '1'),
1077 el('span', { 'aria-hidden': 'true' }, '/'),
1078 el('span', { className: 'bkbg-ts-fraction-total', 'data-bkbg-fraction': 'total' }, String(total))
1079 )
1080 );
1081 }
1082
1083 return el('div', { className: 'bkbg-ts-indicators' },
1084 el('div', { className: 'bkbg-ts-dots', role: 'tablist', 'aria-label': __('Testimonials Carousel', 'blockenberg') },
1085 (a.items || []).map(function (_, i) {
1086 return el('button', {
1087 key: i,
1088 type: 'button',
1089 className: 'bkbg-ts-dot' + (i === 0 ? ' is-active' : ''),
1090 'data-bkbg-dot': String(i),
1091 'aria-label': __('Go to slide', 'blockenberg') + ' ' + (i + 1)
1092 });
1093 })
1094 )
1095 );
1096 }
1097
1098 var content;
1099 if (a.layoutStyle === 'carousel') {
1100 var indicators = renderIndicators();
1101 content = el('div', { className: 'bkbg-ts-carousel' },
1102 (a.carouselIndicatorsPosition === 'top') && indicators,
1103 el('div', { className: 'bkbg-ts-carousel-inner' },
1104 a.carouselNav && el('div', { className: 'bkbg-ts-nav' },
1105 el('button', { type: 'button', className: 'bkbg-ts-nav-btn', 'data-bkbg-nav': 'prev', 'data-ts-nav': 'prev', 'aria-label': __('Previous', 'blockenberg') },
1106 el('span', { className: 'dashicons dashicons-arrow-left-alt2', 'aria-hidden': 'true' })
1107 ),
1108 el('button', { type: 'button', className: 'bkbg-ts-nav-btn', 'data-bkbg-nav': 'next', 'data-ts-nav': 'next', 'aria-label': __('Next', 'blockenberg') },
1109 el('span', { className: 'dashicons dashicons-arrow-right-alt2', 'aria-hidden': 'true' })
1110 )
1111 ),
1112 el('div', { className: 'bkbg-ts-carousel-track' },
1113 (a.items || []).map(renderCard)
1114 )
1115 ),
1116 (a.carouselIndicatorsPosition !== 'top') && indicators
1117 );
1118 } else {
1119 content = el('div', { className: 'bkbg-ts-grid' }, (a.items || []).map(renderCard));
1120 }
1121
1122 return el('div', blockProps, content);
1123 }
1124 });
1125 });
1126