PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.7
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.7
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.7, at blocks/testimonials/index.js

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