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 / stats-counter / index.js

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

1,169 lines 57.8 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 useMemo = wp.element.useMemo;
7 var registerBlockType = wp.blocks.registerBlockType;
8 var InspectorControls = wp.blockEditor.InspectorControls;
9 var BlockControls = wp.blockEditor.BlockControls;
10 var useBlockProps = wp.blockEditor.useBlockProps;
11 var PanelBody = wp.components.PanelBody;
12 var PanelColorSettings = wp.blockEditor.PanelColorSettings;
13 var ToggleControl = wp.components.ToggleControl;
14 var RangeControl = wp.components.RangeControl;
15 var SelectControl = wp.components.SelectControl;
16 var TextControl = wp.components.TextControl;
17 var Button = wp.components.Button;
18 var ColorPicker = wp.components.ColorPicker;
19 var Popover = wp.components.Popover;
20 var ToolbarGroup = wp.components.ToolbarGroup;
21 var ToolbarButton = wp.components.ToolbarButton;
22 var TabPanel = wp.components.TabPanel;
23
24 // Full Dashicons list (300+ icons)
25 var dashicons = [
26 // Admin Menu
27 'menu', 'admin-site', 'admin-site-alt', 'admin-site-alt2', 'admin-site-alt3', 'dashboard',
28 'admin-post', 'admin-media', 'admin-links', 'admin-page', 'admin-comments',
29 'admin-appearance', 'admin-plugins', 'admin-users', 'admin-tools', 'admin-settings',
30 'admin-network', 'admin-home', 'admin-generic', 'admin-collapse', 'filter',
31 'admin-customizer', 'admin-multisite',
32 // Welcome Screen
33 'welcome-write-blog', 'welcome-add-page', 'welcome-view-site', 'welcome-widgets-menus',
34 'welcome-comments', 'welcome-learn-more',
35 // Post Formats
36 'format-aside', 'format-image', 'format-gallery', 'format-video', 'format-status',
37 'format-quote', 'format-chat', 'format-audio',
38 // Media
39 'camera', 'camera-alt', 'images-alt', 'images-alt2', 'video-alt', 'video-alt2',
40 'video-alt3', 'media-archive', 'media-audio', 'media-code', 'media-default',
41 'media-document', 'media-interactive', 'media-spreadsheet', 'media-text',
42 'media-video', 'playlist-audio', 'playlist-video', 'controls-play',
43 'controls-pause', 'controls-forward', 'controls-skipforward', 'controls-back',
44 'controls-skipback', 'controls-repeat', 'controls-volumeon', 'controls-volumeoff',
45 // Image Editing
46 'image-crop', 'image-rotate', 'image-rotate-left', 'image-rotate-right',
47 'image-flip-vertical', 'image-flip-horizontal', 'image-filter',
48 // Sorting & Misc UI
49 'align-left', 'align-right', 'align-center', 'align-none', 'align-pull-left', 'align-pull-right',
50 'align-full-width', 'align-wide',
51 'lock', 'unlock', 'calendar', 'calendar-alt', 'visibility', 'hidden',
52 'post-status', 'edit', 'trash', 'sticky',
53 // Arrows & Navigation
54 'arrow-up', 'arrow-down', 'arrow-right', 'arrow-left',
55 'arrow-up-alt', 'arrow-down-alt', 'arrow-right-alt', 'arrow-left-alt',
56 'arrow-up-alt2', 'arrow-down-alt2', 'arrow-right-alt2', 'arrow-left-alt2',
57 'sort', 'leftright', 'randomize', 'list-view', 'excerpt-view', 'grid-view',
58 'move', 'undo', 'redo',
59 // Social & Sharing
60 'share', 'share-alt', 'share-alt2', 'rss', 'email', 'email-alt', 'email-alt2',
61 'networking', 'amazon', 'facebook', 'facebook-alt', 'google',
62 'twitter', 'twitter-alt', 'instagram', 'linkedin', 'pinterest', 'podio',
63 'reddit', 'spotify', 'twitch', 'whatsapp', 'xing', 'youtube',
64 // WordPress.org Specific
65 'hammer', 'art', 'migrate', 'performance', 'universal-access',
66 'universal-access-alt', 'tickets', 'nametag', 'clipboard', 'heart', 'megaphone',
67 'schedule', 'tide', 'rest-api', 'code-standards',
68 // BuddyPress
69 'buddicons-activity', 'buddicons-bbpress-logo', 'buddicons-buddypress-logo',
70 'buddicons-community', 'buddicons-forums', 'buddicons-friends',
71 'buddicons-groups', 'buddicons-pm', 'buddicons-replies',
72 'buddicons-topics', 'buddicons-tracking',
73 // WordPress Core
74 'wordpress', 'wordpress-alt', 'pressthis', 'update', 'update-alt',
75 'screenoptions', 'info', 'cart', 'feedback', 'cloud', 'cloud-saved',
76 'cloud-upload', 'translation', 'tag', 'category', 'archive',
77 // Status & Alerts
78 'yes', 'yes-alt', 'no', 'no-alt', 'plus', 'plus-alt', 'plus-alt2',
79 'minus', 'dismiss', 'marker', 'star-filled', 'star-half', 'star-empty',
80 'flag', 'warning', 'thumbs-up', 'thumbs-down', 'smiley',
81 // Location & Places
82 'location', 'location-alt', 'vault', 'shield', 'shield-alt',
83 'sos', 'search', 'slides', 'text-page', 'analytics', 'chart-pie',
84 'chart-bar', 'chart-line', 'chart-area', 'groups', 'businessman', 'businesswoman',
85 'businessperson', 'id', 'id-alt', 'products', 'awards', 'forms', 'testimonial',
86 'portfolio', 'book', 'book-alt', 'download', 'upload', 'backup',
87 'clock', 'lightbulb', 'microphone', 'desktop', 'laptop', 'tablet',
88 'smartphone', 'phone', 'index-card', 'carrot', 'building', 'store',
89 'album', 'palmtree', 'tickets-alt', 'money', 'money-alt', 'bank',
90 // Misc Objects
91 'layout', 'paperclip', 'color-picker', 'edit-large', 'edit-page',
92 'airplane', 'beer', 'bell', 'bitcoin', 'calculator', 'car', 'coffee',
93 'database', 'database-add', 'database-export', 'database-import',
94 'database-remove', 'database-view', 'drumstick', 'ellipsis', 'exit',
95 'food', 'fullscreen-alt', 'fullscreen-exit-alt', 'games', 'hourglass',
96 'html', 'insert', 'insert-after', 'insert-before', 'open-folder',
97 'pdf', 'pets', 'plugins-checked', 'printer', 'privacy', 'remove',
98 'saved', 'shortcode', 'superhero', 'superhero-alt', 'text', 'external',
99 // Table Operations
100 'table-col-after', 'table-col-before', 'table-col-delete',
101 'table-row-after', 'table-row-before', 'table-row-delete',
102 // Editor Controls
103 'editor-aligncenter', 'editor-alignleft', 'editor-alignright', 'editor-bold',
104 'editor-break', 'editor-code', 'editor-contract', 'editor-customchar',
105 'editor-expand', 'editor-help', 'editor-indent', 'editor-insertmore',
106 'editor-italic', 'editor-justify', 'editor-kitchensink', 'editor-ltr',
107 'editor-ol', 'editor-ol-rtl', 'editor-outdent', 'editor-paragraph',
108 'editor-paste-text', 'editor-paste-word', 'editor-quote', 'editor-removeformatting',
109 'editor-rtl', 'editor-spellcheck', 'editor-strikethrough', 'editor-table',
110 'editor-textcolor', 'editor-ul', 'editor-underline', 'editor-unlink', 'editor-video'
111 ];
112
113 // Helper: parse icon value (supports legacy string or new object format)
114 function parseIcon(iconValue) {
115 if (!iconValue) return { type: 'dashicon', value: 'groups' };
116 if (typeof iconValue === 'string') {
117 // Legacy format - just the icon name
118 return { type: 'dashicon', value: iconValue };
119 }
120 return iconValue;
121 }
122
123 registerBlockType('blockenberg/stats-counter', {
124 title: __('Stats Counter', 'blockenberg'),
125 icon: 'performance',
126 category: 'blockenberg',
127 description: __('Display animated number counters with icons, labels, and rich customization.', 'blockenberg'),
128
129 edit: function (props) {
130 var attributes = props.attributes;
131 var setAttributes = props.setAttributes;
132 var isSelected = props.isSelected;
133 var a = attributes;
134
135 // State for editing
136 var editingState = useState(null);
137 var editing = editingState[0];
138 var setEditing = editingState[1];
139
140 // State for hovered item
141 var hoveredState = useState(null);
142 var hovered = hoveredState[0];
143 var setHovered = hoveredState[1];
144
145 // State for color picker
146 var colorPickerState = useState(null);
147 var colorPicker = colorPickerState[0];
148 var setColorPicker = colorPickerState[1];
149
150 // State for icon picker popover
151 var iconPickerState = useState(null);
152 var openIconPicker = iconPickerState[0];
153 var setOpenIconPicker = iconPickerState[1];
154
155 // Search state for dashicons
156 var searchState = useState('');
157 var iconSearch = searchState[0];
158 var setIconSearch = searchState[1];
159
160 // Filter dashicons based on search
161 var filteredDashicons = useMemo(function() {
162 if (!iconSearch) return dashicons;
163 var search = iconSearch.toLowerCase();
164 return dashicons.filter(function(icon) {
165 return icon.toLowerCase().includes(search);
166 });
167 }, [iconSearch]);
168
169 var layoutOptions = [
170 { label: __('Grid', 'blockenberg'), value: 'grid' },
171 { label: __('Inline', 'blockenberg'), value: 'inline' },
172 { label: __('Stacked', 'blockenberg'), value: 'stacked' }
173 ];
174
175 var iconPositionOptions = [
176 { label: __('Top', 'blockenberg'), value: 'top' },
177 { label: __('Left', 'blockenberg'), value: 'left' },
178 { label: __('Right', 'blockenberg'), value: 'right' }
179 ];
180
181 var iconStyleOptions = [
182 { label: __('Default (Icon only)', 'blockenberg'), value: 'default' },
183 { label: __('Filled Background', 'blockenberg'), value: 'filled' },
184 { label: __('Outlined', 'blockenberg'), value: 'outlined' }
185 ];
186
187 var labelPositionOptions = [
188 { label: __('Below Number', 'blockenberg'), value: 'below' },
189 { label: __('Above Number', 'blockenberg'), value: 'above' }
190 ];
191
192 var textAlignOptions = [
193 { label: __('Left', 'blockenberg'), value: 'left' },
194 { label: __('Center', 'blockenberg'), value: 'center' },
195 { label: __('Right', 'blockenberg'), value: 'right' }
196 ];
197
198 var fontWeightOptions = [
199 { label: '400', value: 400 },
200 { label: '500', value: 500 },
201 { label: '600', value: 600 },
202 { label: '700', value: 700 },
203 { label: '800', value: 800 },
204 { label: '900', value: 900 }
205 ];
206
207 var separatorOptions = [
208 { label: __('Comma (1,000)', 'blockenberg'), value: ',' },
209 { label: __('Space (1 000)', 'blockenberg'), value: ' ' },
210 { label: __('Period (1.000)', 'blockenberg'), value: '.' },
211 { label: __('None (1000)', 'blockenberg'), value: '' }
212 ];
213
214 // Update item
215 function updateItem(index, key, value) {
216 var newItems = a.items.slice();
217 newItems[index] = Object.assign({}, newItems[index]);
218 newItems[index][key] = value;
219 setAttributes({ items: newItems });
220 }
221
222 // Add item
223 function addItem() {
224 var colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16'];
225 var icons = ['groups', 'yes-alt', 'chart-bar', 'star-filled', 'heart', 'thumbs-up', 'awards', 'building'];
226 var newColor = colors[a.items.length % colors.length];
227 var newIcon = icons[a.items.length % icons.length];
228 var newItems = a.items.concat([{
229 number: 100,
230 suffix: '+',
231 prefix: '',
232 label: __('New Stat', 'blockenberg'),
233 icon: newIcon,
234 color: newColor,
235 numberColor: '',
236 labelColor: ''
237 }]);
238 setAttributes({ items: newItems });
239 }
240
241 // Remove item
242 function removeItem(index) {
243 if (a.items.length <= 1) return;
244 var newItems = a.items.filter(function (_, i) { return i !== index; });
245 setAttributes({ items: newItems });
246 }
247
248 // Move item
249 function moveItem(index, direction) {
250 var newIndex = index + direction;
251 if (newIndex < 0 || newIndex >= a.items.length) return;
252 var newItems = a.items.slice();
253 var temp = newItems[index];
254 newItems[index] = newItems[newIndex];
255 newItems[newIndex] = temp;
256 setAttributes({ items: newItems });
257 }
258
259 // Duplicate item
260 function duplicateItem(index) {
261 var newItems = a.items.slice();
262 newItems.splice(index + 1, 0, Object.assign({}, a.items[index]));
263 setAttributes({ items: newItems });
264 }
265
266 // Format number with separator
267 function formatNumber(num, separator) {
268 if (!separator) return String(num);
269 return String(num).replace(/\B(?=(\d{3})+(?!\d))/g, separator);
270 }
271
272 // Render full icon picker popover with tabs
273 function renderIconPicker(index, position) {
274 var currentIcon = parseIcon(a.items[index].icon);
275 var stop = function (e) {
276 if (e && e.stopPropagation) e.stopPropagation();
277 };
278
279 return el(Popover, {
280 position: position || 'bottom center',
281 className: 'bkbg-sc-icon-popover',
282 onClose: function () {
283 setOpenIconPicker(null);
284 setIconSearch('');
285 }
286 },
287 el('div', { className: 'bkbg-sc-icon-picker-modal', onMouseDown: stop, onClick: stop },
288 el('div', { className: 'bkbg-sc-icon-picker-header' },
289 el('h3', {}, __('Choose Icon', 'blockenberg')),
290 el(Button, {
291 icon: 'no-alt',
292 onClick: function() {
293 setOpenIconPicker(null);
294 setIconSearch('');
295 },
296 className: 'bkbg-sc-icon-picker-close'
297 })
298 ),
299 el(TabPanel, {
300 className: 'bkbg-sc-icon-tabs',
301 activeClass: 'is-active',
302 tabs: [
303 { name: 'dashicons', title: __('Dashicons', 'blockenberg'), className: 'bkbg-sc-tab-dashicons' },
304 { name: 'media', title: __('Media Library', 'blockenberg'), className: 'bkbg-sc-tab-media' }
305 ]
306 }, function(tab) {
307 if (tab.name === 'dashicons') {
308 return el('div', { className: 'bkbg-sc-dashicons-panel' },
309 // Search box
310 el(TextControl, {
311 placeholder: __('Search icons...', 'blockenberg'),
312 value: iconSearch,
313 onChange: setIconSearch,
314 className: 'bkbg-sc-icon-search'
315 }),
316 // Icons grid
317 el('div', { className: 'bkbg-sc-dashicons-grid' },
318 filteredDashicons.length === 0
319 ? el('p', { className: 'bkbg-sc-no-icons' }, __('No icons found', 'blockenberg'))
320 : filteredDashicons.map(function(icon) {
321 var isActive = currentIcon.type === 'dashicon' && currentIcon.value === icon;
322 return el('button', {
323 key: icon,
324 type: 'button',
325 className: 'bkbg-sc-dashicon-btn' + (isActive ? ' is-active' : ''),
326 onClick: function() {
327 updateItem(index, 'icon', icon);
328 setOpenIconPicker(null);
329 setIconSearch('');
330 },
331 title: icon
332 },
333 el('span', { className: 'dashicons dashicons-' + icon })
334 );
335 })
336 )
337 );
338 } else {
339 // Media Library tab
340 var openMediaLibrary = function() {
341 var frame = wp.media({
342 title: __('Select Icon Image', 'blockenberg'),
343 button: { text: __('Use as Icon', 'blockenberg') },
344 multiple: false,
345 library: { type: 'image' }
346 });
347 frame.on('select', function() {
348 var attachment = frame.state().get('selection').first().toJSON();
349 updateItem(index, 'icon', { type: 'custom', value: attachment.url });
350 setOpenIconPicker(null);
351 });
352 frame.open();
353 };
354
355 return el('div', { className: 'bkbg-sc-media-panel' },
356 // Current custom icon preview
357 currentIcon.type === 'custom' && el('div', { className: 'bkbg-sc-current-media' },
358 el('img', { src: currentIcon.value, alt: '' }),
359 el(Button, {
360 isDestructive: true,
361 variant: 'secondary',
362 onClick: function() {
363 updateItem(index, 'icon', 'groups');
364 }
365 }, __('Remove', 'blockenberg'))
366 ),
367 // Media upload button
368 el(Button, {
369 variant: 'secondary',
370 onClick: openMediaLibrary,
371 className: 'bkbg-sc-media-btn'
372 },
373 el('span', { className: 'dashicons dashicons-upload' }),
374 ' ',
375 __('Select from Media Library', 'blockenberg')
376 ),
377 el('p', { className: 'bkbg-sc-media-hint' },
378 __('Upload or select an image to use as a custom icon.', 'blockenberg')
379 )
380 );
381 }
382 })
383 )
384 );
385 }
386
387 // Inspector controls
388 var inspector = el(InspectorControls, {},
389 el(PanelBody, { title: __('Layout', 'blockenberg'), initialOpen: true },
390 el(SelectControl, {
391 label: __('Layout Style', 'blockenberg'),
392 value: a.layoutStyle,
393 options: layoutOptions,
394 onChange: function (v) { setAttributes({ layoutStyle: v }); }
395 }),
396 (a.layoutStyle === 'grid') && el(RangeControl, {
397 label: __('Columns', 'blockenberg'),
398 value: a.columns,
399 onChange: function (v) { setAttributes({ columns: v }); },
400 min: 1,
401 max: 6
402 }),
403 el(RangeControl, {
404 label: __('Gap', 'blockenberg'),
405 value: a.gap,
406 onChange: function (v) { setAttributes({ gap: v }); },
407 min: 0,
408 max: 80
409 }),
410 el(SelectControl, {
411 label: __('Text Align', 'blockenberg'),
412 value: a.textAlign,
413 options: textAlignOptions,
414 onChange: function (v) { setAttributes({ textAlign: v }); }
415 }),
416 el(ToggleControl, {
417 label: __('Dividers between items', 'blockenberg'),
418 checked: a.divider,
419 __nextHasNoMarginBottom: true,
420 onChange: function (v) { setAttributes({ divider: v }); }
421 })
422 ),
423
424 el(PanelBody, { title: __('Icon', 'blockenberg'), initialOpen: false },
425 el(ToggleControl, {
426 label: __('Show Icon', 'blockenberg'),
427 checked: a.showIcon,
428 __nextHasNoMarginBottom: true,
429 onChange: function (v) { setAttributes({ showIcon: v }); }
430 }),
431 a.showIcon && el(SelectControl, {
432 label: __('Icon Position', 'blockenberg'),
433 value: a.iconPosition,
434 options: iconPositionOptions,
435 onChange: function (v) { setAttributes({ iconPosition: v }); }
436 }),
437 a.showIcon && el(SelectControl, {
438 label: __('Icon Style', 'blockenberg'),
439 value: a.iconStyle,
440 options: iconStyleOptions,
441 onChange: function (v) { setAttributes({ iconStyle: v }); }
442 }),
443 a.showIcon && el(RangeControl, {
444 label: __('Icon Size', 'blockenberg'),
445 value: a.iconSize,
446 onChange: function (v) { setAttributes({ iconSize: v }); },
447 min: 16,
448 max: 96
449 }),
450 a.showIcon && (a.iconStyle === 'filled' || a.iconStyle === 'outlined') && el(RangeControl, {
451 label: __('Background Size', 'blockenberg'),
452 value: a.iconBgSize,
453 onChange: function (v) { setAttributes({ iconBgSize: v }); },
454 min: 40,
455 max: 160
456 }),
457 a.showIcon && (a.iconStyle === 'filled' || a.iconStyle === 'outlined') && el(RangeControl, {
458 label: __('Border Radius', 'blockenberg'),
459 value: a.iconRadius,
460 onChange: function (v) { setAttributes({ iconRadius: v }); },
461 min: 0,
462 max: 50,
463 help: __('50 = circle', 'blockenberg')
464 }),
465 a.showIcon && el(ToggleControl, {
466 label: __('Use item colors for icons', 'blockenberg'),
467 checked: a.useItemColors,
468 __nextHasNoMarginBottom: true,
469 onChange: function (v) { setAttributes({ useItemColors: v }); }
470 })
471 ),
472
473 el(PanelBody, { title: __('Number', 'blockenberg'), initialOpen: false },
474 el(RangeControl, {
475 label: __('Number Size', 'blockenberg'),
476 value: a.numberSize,
477 onChange: function (v) { setAttributes({ numberSize: v }); },
478 min: 20,
479 max: 120
480 }),
481 el(SelectControl, {
482 label: __('Number Weight', 'blockenberg'),
483 value: a.numberWeight,
484 options: fontWeightOptions,
485 onChange: function (v) { setAttributes({ numberWeight: v }); }
486 }),
487 el(SelectControl, {
488 label: __('Thousands Separator', 'blockenberg'),
489 value: a.separator,
490 options: separatorOptions,
491 onChange: function (v) { setAttributes({ separator: v }); }
492 })
493 ),
494
495 el(PanelBody, { title: __('Label', 'blockenberg'), initialOpen: false },
496 el(ToggleControl, {
497 label: __('Show Label', 'blockenberg'),
498 checked: a.showLabel,
499 __nextHasNoMarginBottom: true,
500 onChange: function (v) { setAttributes({ showLabel: v }); }
501 }),
502 a.showLabel && el(SelectControl, {
503 label: __('Label Position', 'blockenberg'),
504 value: a.labelPosition,
505 options: labelPositionOptions,
506 onChange: function (v) { setAttributes({ labelPosition: v }); }
507 }),
508 a.showLabel && el(RangeControl, {
509 label: __('Label Size', 'blockenberg'),
510 value: a.labelSize,
511 onChange: function (v) { setAttributes({ labelSize: v }); },
512 min: 12,
513 max: 32
514 }),
515 a.showLabel && el(SelectControl, {
516 label: __('Label Weight', 'blockenberg'),
517 value: a.labelWeight,
518 options: fontWeightOptions,
519 onChange: function (v) { setAttributes({ labelWeight: v }); }
520 })
521 ),
522
523 el(PanelBody, { title: __('Card Style', 'blockenberg'), initialOpen: false },
524 el(ToggleControl, {
525 label: __('Enable Card Style', 'blockenberg'),
526 checked: a.cardStyle,
527 __nextHasNoMarginBottom: true,
528 onChange: function (v) { setAttributes({ cardStyle: v }); }
529 }),
530 a.cardStyle && el(RangeControl, {
531 label: __('Padding', 'blockenberg'),
532 value: a.cardPadding,
533 onChange: function (v) { setAttributes({ cardPadding: v }); },
534 min: 8,
535 max: 80
536 }),
537 a.cardStyle && el(RangeControl, {
538 label: __('Border Radius', 'blockenberg'),
539 value: a.cardRadius,
540 onChange: function (v) { setAttributes({ cardRadius: v }); },
541 min: 0,
542 max: 40
543 }),
544 a.cardStyle && el(ToggleControl, {
545 label: __('Border', 'blockenberg'),
546 checked: a.cardBorder,
547 __nextHasNoMarginBottom: true,
548 onChange: function (v) { setAttributes({ cardBorder: v }); }
549 }),
550 a.cardStyle && a.cardBorder && el(RangeControl, {
551 label: __('Border Width', 'blockenberg'),
552 value: a.cardBorderWidth,
553 onChange: function (v) { setAttributes({ cardBorderWidth: v }); },
554 min: 1,
555 max: 6
556 }),
557 a.cardStyle && el(ToggleControl, {
558 label: __('Shadow', 'blockenberg'),
559 checked: a.cardShadow,
560 __nextHasNoMarginBottom: true,
561 onChange: function (v) { setAttributes({ cardShadow: v }); }
562 })
563 ),
564
565 el(PanelBody, { title: __('Animation', 'blockenberg'), initialOpen: false },
566 el(ToggleControl, {
567 label: __('Animate on Scroll', 'blockenberg'),
568 checked: a.animateOnScroll,
569 help: __('Numbers count up when visible on the page.', 'blockenberg'),
570 __nextHasNoMarginBottom: true,
571 onChange: function (v) { setAttributes({ animateOnScroll: v }); }
572 }),
573 a.animateOnScroll && el(RangeControl, {
574 label: __('Animation Duration (ms)', 'blockenberg'),
575 value: a.animationDuration,
576 onChange: function (v) { setAttributes({ animationDuration: v }); },
577 min: 500,
578 max: 5000,
579 step: 100
580 })
581 ),
582
583 el(PanelBody, { title: __('Title', 'blockenberg'), initialOpen: false },
584 el(ToggleControl, {
585 label: __('Show Title', 'blockenberg'),
586 checked: a.showTitle,
587 __nextHasNoMarginBottom: true,
588 onChange: function (v) { setAttributes({ showTitle: v }); }
589 }),
590 a.showTitle && el(RangeControl, {
591 label: __('Title Size', 'blockenberg'),
592 value: a.titleSize,
593 onChange: function (v) { setAttributes({ titleSize: v }); },
594 min: 16,
595 max: 60
596 })
597 ),
598
599 el(PanelBody, { title: __('Stats Items', 'blockenberg'), initialOpen: false },
600 a.items.map(function (item, index) {
601 return el('div', { key: index, className: 'bkbg-sc-item-control' },
602 el('div', { className: 'bkbg-sc-item-header' },
603 el('span', { className: 'bkbg-sc-item-title' },
604 (item.prefix || '') + formatNumber(item.number, a.separator) + (item.suffix || '')
605 ),
606 el('div', { className: 'bkbg-sc-item-actions' },
607 el(Button, {
608 icon: 'arrow-up-alt2',
609 size: 'small',
610 disabled: index === 0,
611 onClick: function () { moveItem(index, -1); }
612 }),
613 el(Button, {
614 icon: 'arrow-down-alt2',
615 size: 'small',
616 disabled: index === a.items.length - 1,
617 onClick: function () { moveItem(index, 1); }
618 }),
619 el(Button, {
620 icon: 'trash',
621 size: 'small',
622 isDestructive: true,
623 disabled: a.items.length <= 1,
624 onClick: function () { removeItem(index); }
625 })
626 )
627 ),
628 el(TextControl, {
629 label: __('Number', 'blockenberg'),
630 type: 'number',
631 value: String(item.number),
632 onChange: function (v) {
633 var n = parseInt(v, 10);
634 if (isNaN(n)) n = 0;
635 updateItem(index, 'number', n);
636 }
637 }),
638 el('div', { className: 'bkbg-sc-prefix-suffix' },
639 el(TextControl, {
640 label: __('Prefix', 'blockenberg'),
641 value: item.prefix || '',
642 placeholder: '$',
643 onChange: function (v) { updateItem(index, 'prefix', v); }
644 }),
645 el(TextControl, {
646 label: __('Suffix', 'blockenberg'),
647 value: item.suffix || '',
648 placeholder: '+',
649 onChange: function (v) { updateItem(index, 'suffix', v); }
650 })
651 ),
652 el(TextControl, {
653 label: __('Label', 'blockenberg'),
654 value: item.label,
655 onChange: function (v) { updateItem(index, 'label', v); }
656 }),
657 a.showIcon && el('div', { className: 'bkbg-sc-icon-row' },
658 el('span', {}, __('Icon', 'blockenberg')),
659 el('div', { className: 'bkbg-sc-icon-btn-wrap' },
660 (function() {
661 var iconData = parseIcon(item.icon);
662 return el('button', {
663 type: 'button',
664 className: 'bkbg-sc-icon-btn',
665 onClick: function (e) {
666 e.preventDefault();
667 e.stopPropagation();
668 setOpenIconPicker(openIconPicker === 'panel-' + index ? null : 'panel-' + index);
669 },
670 title: __('Click to change icon', 'blockenberg')
671 },
672 iconData.type === 'custom'
673 ? el('img', { src: iconData.value, alt: '' })
674 : el('span', { className: 'dashicons dashicons-' + iconData.value })
675 );
676 })(),
677 openIconPicker === 'panel-' + index && renderIconPicker(index, 'middle left')
678 )
679 ),
680 el('div', { className: 'bkbg-sc-color-row' },
681 el('span', {}, __('Icon Color', 'blockenberg')),
682 el('button', {
683 className: 'bkbg-sc-color-btn',
684 style: { backgroundColor: item.color },
685 onClick: function () {
686 setColorPicker(colorPicker === index ? null : index);
687 }
688 }),
689 colorPicker === index && el(Popover, {
690 position: 'bottom left',
691 onClose: function () { setColorPicker(null); }
692 },
693 el(ColorPicker, {
694 color: item.color,
695 onChangeComplete: function (c) {
696 updateItem(index, 'color', c.hex);
697 }
698 })
699 )
700 ),
701 el('div', { className: 'bkbg-sc-color-row' },
702 el('span', {}, __('Number Color', 'blockenberg')),
703 el('button', {
704 className: 'bkbg-sc-color-btn',
705 style: { backgroundColor: item.numberColor || a.numberColor || '#1f2937' },
706 onClick: function () {
707 setColorPicker(colorPicker === 'number-' + index ? null : 'number-' + index);
708 }
709 }),
710 colorPicker === 'number-' + index && el(Popover, {
711 position: 'bottom left',
712 onClose: function () { setColorPicker(null); }
713 },
714 el(ColorPicker, {
715 color: item.numberColor || a.numberColor || '#1f2937',
716 onChangeComplete: function (c) {
717 updateItem(index, 'numberColor', c.hex);
718 }
719 })
720 )
721 ),
722 el('div', { className: 'bkbg-sc-color-row' },
723 el('span', {}, __('Label Color', 'blockenberg')),
724 el('button', {
725 className: 'bkbg-sc-color-btn',
726 style: { backgroundColor: item.labelColor || a.labelColor || '#6b7280' },
727 onClick: function () {
728 setColorPicker(colorPicker === 'label-' + index ? null : 'label-' + index);
729 }
730 }),
731 colorPicker === 'label-' + index && el(Popover, {
732 position: 'bottom left',
733 onClose: function () { setColorPicker(null); }
734 },
735 el(ColorPicker, {
736 color: item.labelColor || a.labelColor || '#6b7280',
737 onChangeComplete: function (c) {
738 updateItem(index, 'labelColor', c.hex);
739 }
740 })
741 )
742 )
743 );
744 }),
745 el(Button, {
746 variant: 'secondary',
747 onClick: addItem,
748 className: 'bkbg-sc-add-btn'
749 }, __('+ Add Stat', 'blockenberg'))
750 ),
751
752 el(PanelColorSettings, {
753 title: __('Colors', 'blockenberg'),
754 initialOpen: false,
755 colorSettings: [
756 {
757 value: a.numberColor,
758 onChange: function (v) { setAttributes({ numberColor: v }); },
759 label: __('Number Color', 'blockenberg')
760 },
761 {
762 value: a.labelColor,
763 onChange: function (v) { setAttributes({ labelColor: v }); },
764 label: __('Label Color', 'blockenberg')
765 },
766 {
767 value: a.titleColor,
768 onChange: function (v) { setAttributes({ titleColor: v }); },
769 label: __('Title Color', 'blockenberg')
770 },
771 a.cardStyle && {
772 value: a.cardBg,
773 onChange: function (v) { setAttributes({ cardBg: v }); },
774 label: __('Card Background', 'blockenberg')
775 },
776 a.cardStyle && a.cardBorder && {
777 value: a.cardBorderColor,
778 onChange: function (v) { setAttributes({ cardBorderColor: v }); },
779 label: __('Card Border Color', 'blockenberg')
780 },
781 a.divider && {
782 value: a.dividerColor,
783 onChange: function (v) { setAttributes({ dividerColor: v }); },
784 label: __('Divider Color', 'blockenberg')
785 }
786 ].filter(Boolean)
787 })
788 );
789
790 // CSS variables
791 var wrapStyle = {
792 '--bkbg-sc-columns': a.columns,
793 '--bkbg-sc-gap': a.gap + 'px',
794 '--bkbg-sc-icon-size': a.iconSize + 'px',
795 '--bkbg-sc-icon-bg-size': a.iconBgSize + 'px',
796 '--bkbg-sc-icon-radius': a.iconRadius + '%',
797 '--bkbg-sc-number-size': a.numberSize + 'px',
798 '--bkbg-sc-number-weight': a.numberWeight,
799 '--bkbg-sc-number-color': a.numberColor,
800 '--bkbg-sc-label-size': a.labelSize + 'px',
801 '--bkbg-sc-label-weight': a.labelWeight,
802 '--bkbg-sc-label-color': a.labelColor,
803 '--bkbg-sc-title-size': a.titleSize + 'px',
804 '--bkbg-sc-title-color': a.titleColor,
805 '--bkbg-sc-card-bg': a.cardBg,
806 '--bkbg-sc-card-border-color': a.cardBorderColor,
807 '--bkbg-sc-card-border-width': a.cardBorderWidth + 'px',
808 '--bkbg-sc-card-radius': a.cardRadius + 'px',
809 '--bkbg-sc-card-padding': a.cardPadding + 'px',
810 '--bkbg-sc-divider-color': a.dividerColor,
811 '--bkbg-sc-text-align': a.textAlign
812 };
813
814 // Title element
815 var titleEl = null;
816 if (a.showTitle) {
817 if (editing === 'title') {
818 titleEl = el('input', {
819 type: 'text',
820 className: 'bkbg-sc-title bkbg-sc-input-active',
821 value: a.title,
822 autoFocus: true,
823 placeholder: __('Stats Title', 'blockenberg'),
824 onChange: function (e) { setAttributes({ title: e.target.value }); },
825 onBlur: function () { setEditing(null); },
826 onKeyDown: function (e) { if (e.key === 'Enter') setEditing(null); }
827 });
828 } else {
829 titleEl = el('div', {
830 className: 'bkbg-sc-title bkbg-sc-clickable',
831 onClick: function () { setEditing('title'); }
832 }, a.title || __('Stats Title', 'blockenberg'));
833 }
834 }
835
836 // Render icon (with index for icon picker)
837 function renderIcon(item, index) {
838 var iconColor = a.useItemColors ? item.color : 'currentColor';
839 var bgColor = a.useItemColors ? item.color : '#3b82f6';
840 var iconData = parseIcon(item.icon);
841
842 // Create the icon element (dashicon or custom image)
843 var iconEl;
844 if (iconData.type === 'custom') {
845 iconEl = el('img', {
846 src: iconData.value,
847 alt: '',
848 style: {
849 width: a.iconSize + 'px',
850 height: a.iconSize + 'px',
851 objectFit: 'contain'
852 }
853 });
854 } else {
855 iconEl = el('span', {
856 className: 'dashicons dashicons-' + iconData.value,
857 style: {
858 color: a.iconStyle === 'filled' ? '#fff' : iconColor,
859 fontSize: a.iconSize + 'px',
860 width: a.iconSize + 'px',
861 height: a.iconSize + 'px'
862 }
863 });
864 }
865
866 // Wrapper with click handler to open icon picker
867 var wrapperProps = {
868 className: 'bkbg-sc-icon bkbg-sc-icon-clickable' +
869 (a.iconStyle === 'filled' ? ' bkbg-sc-icon--filled' : '') +
870 (a.iconStyle === 'outlined' ? ' bkbg-sc-icon--outlined' : ''),
871 onClick: function(e) {
872 e.stopPropagation();
873 setOpenIconPicker(openIconPicker === 'editor-' + index ? null : 'editor-' + index);
874 },
875 title: __('Click to change icon', 'blockenberg'),
876 style: {}
877 };
878
879 if (a.iconStyle === 'filled') {
880 wrapperProps.style = {
881 backgroundColor: bgColor,
882 width: a.iconBgSize + 'px',
883 height: a.iconBgSize + 'px',
884 borderRadius: a.iconRadius + '%'
885 };
886 } else if (a.iconStyle === 'outlined') {
887 wrapperProps.style = {
888 borderColor: bgColor,
889 width: a.iconBgSize + 'px',
890 height: a.iconBgSize + 'px',
891 borderRadius: a.iconRadius + '%'
892 };
893 }
894
895 return el('div', wrapperProps,
896 iconEl,
897 openIconPicker === 'editor-' + index && renderIconPicker(index)
898 );
899 }
900
901 // Build stat items
902 var statItems = a.items.map(function (item, index) {
903 var numberKey = 'number-' + index;
904 var labelKey = 'label-' + index;
905 var prefixKey = 'prefix-' + index;
906 var suffixKey = 'suffix-' + index;
907
908 // Icon
909 var iconEl = a.showIcon ? renderIcon(item, index) : null;
910
911 // Number display
912 var numberDisplay = formatNumber(item.number, a.separator);
913 var prefixEl = item.prefix ? el('span', { className: 'bkbg-sc-prefix' }, item.prefix) : null;
914 var suffixEl = item.suffix ? el('span', { className: 'bkbg-sc-suffix' }, item.suffix) : null;
915
916 var numberEl;
917 var numberStyle = item.numberColor ? { color: item.numberColor } : {};
918 if (editing === numberKey) {
919 numberEl = el('input', {
920 type: 'number',
921 className: 'bkbg-sc-number bkbg-sc-input-active',
922 value: item.number,
923 style: numberStyle,
924 autoFocus: true,
925 onChange: function (e) {
926 var n = parseInt(e.target.value, 10);
927 if (isNaN(n)) n = 0;
928 updateItem(index, 'number', n);
929 },
930 onBlur: function () { setEditing(null); },
931 onKeyDown: function (e) { if (e.key === 'Enter') setEditing(null); }
932 });
933 } else {
934 numberEl = el('span', {
935 className: 'bkbg-sc-number bkbg-sc-clickable',
936 style: numberStyle,
937 onClick: function () { setEditing(numberKey); }
938 }, prefixEl, numberDisplay, suffixEl);
939 }
940
941 // Label
942 var labelEl = null;
943 var labelStyle = item.labelColor ? { color: item.labelColor } : {};
944 if (a.showLabel) {
945 if (editing === labelKey) {
946 labelEl = el('input', {
947 type: 'text',
948 className: 'bkbg-sc-label bkbg-sc-input-active',
949 value: item.label,
950 style: labelStyle,
951 autoFocus: true,
952 onChange: function (e) { updateItem(index, 'label', e.target.value); },
953 onBlur: function () { setEditing(null); },
954 onKeyDown: function (e) { if (e.key === 'Enter') setEditing(null); }
955 });
956 } else {
957 labelEl = el('span', {
958 className: 'bkbg-sc-label bkbg-sc-clickable',
959 style: labelStyle,
960 onClick: function () { setEditing(labelKey); }
961 }, item.label || __('Label', 'blockenberg'));
962 }
963 }
964
965 // Content wrapper (number + label)
966 var contentEl = el('div', { className: 'bkbg-sc-content' },
967 a.labelPosition === 'above' && labelEl,
968 numberEl,
969 a.labelPosition === 'below' && labelEl
970 );
971
972 // Item actions (visible on hover)
973 var itemActions = el('div', { className: 'bkbg-sc-item-actions-overlay' },
974 el(Button, {
975 icon: 'arrow-left-alt2',
976 size: 'small',
977 disabled: index === 0,
978 onClick: function (e) { e.stopPropagation(); moveItem(index, -1); },
979 label: __('Move Left', 'blockenberg')
980 }),
981 el(Button, {
982 icon: 'admin-page',
983 size: 'small',
984 onClick: function (e) { e.stopPropagation(); duplicateItem(index); },
985 label: __('Duplicate', 'blockenberg')
986 }),
987 el(Button, {
988 icon: 'no-alt',
989 size: 'small',
990 disabled: a.items.length <= 1,
991 onClick: function (e) {
992 e.stopPropagation();
993 if (a.items.length > 1 && window.confirm(__('Delete this stat?', 'blockenberg'))) {
994 removeItem(index);
995 }
996 },
997 label: __('Delete', 'blockenberg')
998 }),
999 el(Button, {
1000 icon: 'arrow-right-alt2',
1001 size: 'small',
1002 disabled: index === a.items.length - 1,
1003 onClick: function (e) { e.stopPropagation(); moveItem(index, 1); },
1004 label: __('Move Right', 'blockenberg')
1005 })
1006 );
1007
1008 var itemClass = 'bkbg-sc-item';
1009 if (hovered === index) itemClass += ' bkbg-sc-item-hovered';
1010 if (a.cardStyle) itemClass += ' bkbg-sc-item--card';
1011 if (a.cardShadow) itemClass += ' bkbg-sc-item--shadow';
1012
1013 // Build item based on icon position
1014 var itemContent;
1015 if (a.iconPosition === 'left' && a.showIcon) {
1016 itemContent = el('div', { className: 'bkbg-sc-item-inner bkbg-sc-item-inner--left' },
1017 iconEl,
1018 contentEl
1019 );
1020 } else if (a.iconPosition === 'right' && a.showIcon) {
1021 itemContent = el('div', { className: 'bkbg-sc-item-inner bkbg-sc-item-inner--right' },
1022 contentEl,
1023 iconEl
1024 );
1025 } else {
1026 itemContent = el('div', { className: 'bkbg-sc-item-inner bkbg-sc-item-inner--top' },
1027 iconEl,
1028 contentEl
1029 );
1030 }
1031
1032 return el('div', {
1033 className: itemClass,
1034 key: index,
1035 onMouseEnter: function () { setHovered(index); },
1036 onMouseLeave: function () { setHovered(null); }
1037 },
1038 itemActions,
1039 itemContent
1040 );
1041 });
1042
1043 // Layout classes
1044 var layoutClass = 'bkbg-sc-wrap bkbg-editor-wrap';
1045 layoutClass += ' bkbg-sc-' + a.layoutStyle;
1046 layoutClass += ' bkbg-sc-icon-' + a.iconPosition;
1047 if (a.divider) layoutClass += ' bkbg-sc-divider';
1048 if (isSelected) layoutClass += ' bkbg-block-selected';
1049
1050 var blockProps = useBlockProps({
1051 className: layoutClass,
1052 style: wrapStyle,
1053 'data-layout': a.layoutStyle,
1054 'data-block-label': 'Stats'
1055 });
1056
1057 // Block toolbar
1058 var blockToolbar = el(BlockControls, {},
1059 el(ToolbarGroup, {},
1060 el(ToolbarButton, {
1061 icon: 'plus-alt2',
1062 label: __('Add Stat', 'blockenberg'),
1063 onClick: addItem
1064 })
1065 )
1066 );
1067
1068 // Add button
1069 var addButton = el('div', { className: 'bkbg-editor-actions' },
1070 el(Button, {
1071 variant: 'secondary',
1072 icon: 'plus-alt2',
1073 onClick: addItem
1074 }, __('Add Stat', 'blockenberg'))
1075 );
1076
1077 return el(Fragment, {},
1078 blockToolbar,
1079 inspector,
1080 el('div', blockProps,
1081 titleEl,
1082 el('div', { className: 'bkbg-sc-list' }, statItems),
1083 addButton
1084 )
1085 );
1086 },
1087
1088 save: function (props) {
1089 var a = props.attributes;
1090
1091 // CSS variables
1092 var wrapStyle = {
1093 '--bkbg-sc-columns': a.columns,
1094 '--bkbg-sc-gap': a.gap + 'px',
1095 '--bkbg-sc-icon-size': a.iconSize + 'px',
1096 '--bkbg-sc-icon-bg-size': a.iconBgSize + 'px',
1097 '--bkbg-sc-icon-radius': a.iconRadius + '%',
1098 '--bkbg-sc-number-size': a.numberSize + 'px',
1099 '--bkbg-sc-number-weight': a.numberWeight,
1100 '--bkbg-sc-number-color': a.numberColor,
1101 '--bkbg-sc-label-size': a.labelSize + 'px',
1102 '--bkbg-sc-label-weight': a.labelWeight,
1103 '--bkbg-sc-label-color': a.labelColor,
1104 '--bkbg-sc-title-size': a.titleSize + 'px',
1105 '--bkbg-sc-title-color': a.titleColor,
1106 '--bkbg-sc-card-bg': a.cardBg,
1107 '--bkbg-sc-card-border-color': a.cardBorderColor,
1108 '--bkbg-sc-card-border-width': a.cardBorderWidth + 'px',
1109 '--bkbg-sc-card-radius': a.cardRadius + 'px',
1110 '--bkbg-sc-card-padding': a.cardPadding + 'px',
1111 '--bkbg-sc-divider-color': a.dividerColor,
1112 '--bkbg-sc-text-align': a.textAlign,
1113 '--bkbg-sc-animation-duration': a.animationDuration + 'ms'
1114 };
1115
1116 // Serialize items for frontend
1117 var itemsData = a.items.map(function (item) {
1118 // Handle icon - could be string or object
1119 var iconStr = item.icon || '';
1120 if (typeof item.icon === 'object' && item.icon !== null) {
1121 iconStr = JSON.stringify(item.icon);
1122 }
1123 return [
1124 item.number,
1125 item.prefix || '',
1126 item.suffix || '',
1127 item.label || '',
1128 iconStr,
1129 item.color || '#3b82f6',
1130 item.numberColor || '',
1131 item.labelColor || ''
1132 ].join('|');
1133 }).join(';;');
1134
1135 // Layout classes
1136 var layoutClass = 'bkbg-sc-wrap';
1137 layoutClass += ' bkbg-sc-' + a.layoutStyle;
1138 layoutClass += ' bkbg-sc-icon-' + a.iconPosition;
1139 if (a.divider) layoutClass += ' bkbg-sc-divider';
1140
1141 // Title element
1142 var titleEl = null;
1143 if (a.showTitle && a.title) {
1144 titleEl = el('div', { className: 'bkbg-sc-title' }, a.title);
1145 }
1146
1147 return el('div', {
1148 className: layoutClass,
1149 style: wrapStyle,
1150 'data-items': itemsData,
1151 'data-animate': a.animateOnScroll ? '1' : '0',
1152 'data-duration': a.animationDuration,
1153 'data-separator': a.separator,
1154 'data-show-icon': a.showIcon ? '1' : '0',
1155 'data-icon-style': a.iconStyle,
1156 'data-icon-position': a.iconPosition,
1157 'data-show-label': a.showLabel ? '1' : '0',
1158 'data-label-position': a.labelPosition,
1159 'data-card-style': a.cardStyle ? '1' : '0',
1160 'data-card-shadow': a.cardShadow ? '1' : '0',
1161 'data-use-item-colors': a.useItemColors ? '1' : '0'
1162 },
1163 titleEl,
1164 el('div', { className: 'bkbg-sc-list' })
1165 );
1166 }
1167 });
1168 });
1169