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

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

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