PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.14
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.14
2.0.14 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 / waffle-chart / index.js

index.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.14, at blocks/waffle-chart/index.js

209 lines 12.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 ( function () {
2 const el = window.wp.element.createElement;
3 const { registerBlockType } = window.wp.blocks;
4 const { InspectorControls, useBlockProps, PanelColorSettings } = window.wp.blockEditor;
5 const { PanelBody, RangeControl, ToggleControl, TextControl, Button, ColorPicker, Popover } = window.wp.components;
6 const { __ } = window.wp.i18n;
7 const { useState } = window.wp.element;
8
9 // Resolve the shared components lazily without defining globals for each block.
10 function _tc(label, key, attrs, setA) {
11 var Control = window.bkbgTypographyControl;
12 return Control ? el(Control, {
13 label: label,
14 value: attrs[key] || {},
15 onChange: function (value) { var update = {}; update[key] = value; setA(update); }
16 }) : null;
17 }
18 function _tvf(vars, key, attrs, prefix) {
19 var cssVars = window.bkbgTypoCssVars;
20 if (cssVars) Object.assign(vars, cssVars(attrs[key] || {}, prefix));
21 return vars;
22 }
23 function getTypoControl(label, key, attrs, setA) { return _tc(label, key, attrs, setA); }
24 function getTypoCssVars(attrs) {
25 var v = {};
26 _tvf(v, 'titleTypo', attrs, '--bkwc-tt-');
27 _tvf(v, 'legendTypo', attrs, '--bkwc-lg-');
28 return v;
29 }
30
31 const COLS = 10, ROWS = 10, TOTAL = COLS * ROWS;
32
33 // ── Build cell color map from segments ────────────────────────────────────
34 // Fills cells from bottom-left to top-right, row by row from bottom.
35 function buildCellMap( segments ) {
36 const total = segments.reduce( ( s, seg ) => s + ( seg.value || 0 ), 0 ) || 100;
37
38 // Allocate exact cells per segment using largest-remainder method
39 const raw = segments.map( seg => ( ( seg.value || 0 ) / total ) * TOTAL );
40 const floor = raw.map( Math.floor );
41 let remaining = TOTAL - floor.reduce( ( s, v ) => s + v, 0 );
42
43 const fracs = raw.map( ( v, i ) => ( { i, frac: v - floor[i] } ) )
44 .sort( ( a, b ) => b.frac - a.frac );
45 for ( let k = 0; k < remaining; k++ ) floor[ fracs[k].i ]++;
46
47 // Build cell array [100] with segment index (or -1 for empty)
48 const cells = new Array( TOTAL ).fill( -1 );
49 let cursor = 0;
50 segments.forEach( ( _, segIdx ) => {
51 for ( let c = 0; c < floor[segIdx]; c++ ) {
52 if ( cursor < TOTAL ) cells[cursor++] = segIdx;
53 }
54 } );
55 return cells;
56 }
57
58 // ── Render waffle grid (SVG) ──────────────────────────────────────────────
59 function renderWaffle( a ) {
60 const { cellSize: CS, cellGap: CG, cellRadius: CR } = a;
61 const segments = a.segments || [];
62 const cells = buildCellMap( segments );
63
64 const total = segments.reduce( ( s, seg ) => s + ( seg.value || 0 ), 0 ) || 100;
65
66 const W = COLS * CS + ( COLS - 1 ) * CG;
67 const H = ROWS * CS + ( ROWS - 1 ) * CG;
68
69 const svgEls = [];
70
71 // Background
72 svgEls.push( el( 'rect', { key: 'bg', x: 0, y: 0, width: W, height: H, fill: a.bgColor || '#ffffff', rx: 8 } ) );
73
74 // Cells — rendered bottom-to-top (row 0 = bottom)
75 cells.forEach( ( segIdx, cellIdx ) => {
76 const col = cellIdx % COLS;
77 const row = ROWS - 1 - Math.floor( cellIdx / COLS ); // flip so row 0 is at bottom
78 const x = col * ( CS + CG );
79 const y = row * ( CS + CG );
80 const fill = segIdx >= 0 ? ( segments[segIdx].color || '#4f46e5' ) : ( a.emptyColor || '#f3f4f6' );
81
82 svgEls.push( el( 'rect', {
83 key: 'c' + cellIdx,
84 x, y, width: CS, height: CS,
85 fill, rx: CR,
86 } ) );
87 } );
88
89 return el( 'svg', {
90 viewBox: `0 0 ${ W } ${ H }`, width: '100%',
91 style: { display: 'block', maxWidth: W + 'px', margin: '0 auto' }
92 }, ...svgEls );
93 }
94
95 // ── Render legend ─────────────────────────────────────────────────────────
96 function renderLegend( a ) {
97 const segments = a.segments || [];
98 const total = segments.reduce( ( s, seg ) => s + ( seg.value || 0 ), 0 ) || 100;
99
100 return el( 'div', { className: 'bkbg-wc-legend' },
101 segments.map( ( seg, i ) => {
102 const pct = ( ( seg.value || 0 ) / total * 100 ).toFixed( 1 );
103 return el( 'div', { key: i, className: 'bkbg-wc-legend-item' },
104 el( 'span', { className: 'bkbg-wc-swatch', style: { background: seg.color || '#4f46e5' } } ),
105 el( 'span', { className: 'bkbg-wc-seg-label', style: { color: a.textColor } }, seg.label || '' ),
106 el( 'span', { className: 'bkbg-wc-seg-val', style: { color: seg.color || '#4f46e5' } },
107 a.showPercent ? pct + '%'
108 : a.showValues ? String( seg.value || 0 )
109 : pct + '%'
110 ),
111 );
112 } )
113 );
114 }
115
116 // ── Helpers ───────────────────────────────────────────────────────────────
117 function updSeg( setAttributes, segments, idx, field, val ) {
118 setAttributes( { segments: segments.map( ( s, i ) => i === idx ? { ...s, [field]: val } : s ) } );
119 }
120
121 // ── Block ───────────────────────────────────────────────────────────────── /* ── colour-swatch + popover ── */
122 function BkbgColorSwatch(p) {
123 var st = useState(false), open = st[0], setOpen = st[1];
124 return el('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'4px 0', gap:'8px' } },
125 el('span', { style:{ fontSize:'12px', color:'#1e1e1e', flex:1, lineHeight:1.4 } }, p.label),
126 el('div', { style:{ position:'relative', flexShrink:0 } },
127 el('button', { type:'button', title: p.value||'none', onClick: function(){ setOpen(!open); },
128 style:{ width:'28px', height:'28px', borderRadius:'4px', border: open ? '2px solid #007cba' : '2px solid #ddd', cursor:'pointer', padding:0, display:'block', background: p.value||'#ffffff', flexShrink:0 } }),
129 open && el(Popover, { position:'bottom left', onClose: function(){ setOpen(false); } },
130 el('div', { style:{ padding:'8px' }, onMouseDown: function(e){ e.stopPropagation(); } },
131 el('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:'6px' } },
132 el('strong', { style:{ fontSize:'12px' } }, p.label),
133 el(Button, { icon:'no-alt', isSmall:true, onClick: function(){ setOpen(false); } })
134 ),
135 el(ColorPicker, { color: p.value, enableAlpha:true, onChange: p.onChange })
136 )
137 )
138 )
139 );
140 }
141 registerBlockType( 'blockenberg/waffle-chart', {
142 edit: function ( props ) {
143 const { attributes: a, setAttributes } = props;
144 const blockProps = useBlockProps( { className: 'bkbg-waffle-wrap', style: getTypoCssVars( a ) } );
145 const segs = a.segments || [];
146
147 return el( 'div', blockProps,
148 el( InspectorControls, {},
149
150 el( PanelBody, { title: __( 'Chart Settings', 'blockenberg' ), initialOpen: true },
151 el( TextControl, { label: __( 'Title', 'blockenberg' ), value: a.title, onChange: v => setAttributes( { title: v } ) } ),
152 el( ToggleControl, { label: __( 'Show Title', 'blockenberg' ), checked: a.showTitle, onChange: v => setAttributes( { showTitle: v } ) } ),
153 el( ToggleControl, { label: __( 'Show Legend', 'blockenberg' ), checked: a.showLegend, onChange: v => setAttributes( { showLegend: v } ) } ),
154 el( ToggleControl, { label: __( 'Show as Percent', 'blockenberg' ), checked: a.showPercent, onChange: v => setAttributes( { showPercent: v, showValues: v ? false : a.showValues } ) } ),
155 el( ToggleControl, { label: __( 'Show Raw Values', 'blockenberg' ), checked: a.showValues, onChange: v => setAttributes( { showValues: v, showPercent: v ? false : a.showPercent } ) } ),
156 ),
157
158 el( PanelBody, { title: __( 'Grid Appearance', 'blockenberg' ), initialOpen: false },
159 el( RangeControl, { label: __( 'Cell Gap (px)', 'blockenberg' ), value: a.cellGap, onChange: v => setAttributes( { cellGap: v } ), min: 0, max: 16 } ),
160 el( RangeControl, { label: __( 'Cell Radius (px)', 'blockenberg' ), value: a.cellRadius, onChange: v => setAttributes( { cellRadius: v } ), min: 0, max: 20 } ),
161 ),
162
163
164 el( PanelBody, { title: __( 'Typography', 'blockenberg' ), initialOpen: false },
165 el( RangeControl, { label: __( 'Cell Size (px)', 'blockenberg' ), value: a.cellSize, onChange: v => setAttributes( { cellSize: v } ), min: 14, max: 60 } ),
166 getTypoControl( __( 'Title', 'blockenberg' ), 'titleTypo', a, setAttributes ),
167 getTypoControl( __( 'Legend', 'blockenberg' ), 'legendTypo', a, setAttributes ),
168 ),
169 el( PanelColorSettings, {
170 title: __( 'Colors', 'blockenberg' ), initialOpen: false,
171 colorSettings: [
172 { label: __( 'Background', 'blockenberg' ), value: a.bgColor, onChange: v => setAttributes( { bgColor: v || '#ffffff' } ) },
173 { label: __( 'Empty Cell', 'blockenberg' ), value: a.emptyColor, onChange: v => setAttributes( { emptyColor: v || '#f3f4f6' } ) },
174 { label: __( 'Title Color', 'blockenberg' ), value: a.titleColor, onChange: v => setAttributes( { titleColor: v || '#111827' } ) },
175 { label: __( 'Text Color', 'blockenberg' ), value: a.textColor, onChange: v => setAttributes( { textColor: v || '#374151' } ) },
176 ]
177 } ),
178
179 el( PanelBody, { title: __( 'Segments', 'blockenberg' ), initialOpen: false },
180 el( 'p', { style: { fontSize: 12, color: '#6b7280', marginBottom: 8 } }, __( 'Values are normalized to 100 cells total automatically.', 'blockenberg' ) ),
181 el( Button, { variant: 'secondary', style: { marginBottom: 10 }, onClick: () => setAttributes( { segments: [ ...segs, { label: 'New Segment', value: 10, color: '#6b7280' } ] } ) }, __( '+ Add Segment', 'blockenberg' ) ),
182 segs.map( ( seg, i ) =>
183 el( PanelBody, { key: i, title: `${ seg.label || `Seg ${ i + 1 }` } (${ seg.value })`, initialOpen: false },
184 el( TextControl, { label: __( 'Label', 'blockenberg' ), value: seg.label, onChange: v => updSeg( setAttributes, segs, i, 'label', v ) } ),
185 el( RangeControl, { label: __( 'Value', 'blockenberg' ), value: seg.value, onChange: v => updSeg( setAttributes, segs, i, 'value', v ), min: 0, max: 10000 } ),
186 el( BkbgColorSwatch, { label: __( 'Color', 'blockenberg' ), value: seg.color, onChange: v => updSeg( setAttributes, segs, i, 'color', v ) } ),
187 el( Button, { isDestructive: true, isSmall: true, onClick: () => setAttributes( { segments: segs.filter( ( _, x ) => x !== i ) } ) }, __( 'Remove', 'blockenberg' ) ),
188 )
189 ),
190 ),
191 ),
192
193 a.showTitle && a.title && el( 'h3', { className: 'bkbg-wc-title', style: { color: a.titleColor } }, a.title ),
194 el( 'div', { className: 'bkbg-wc-grid' }, renderWaffle( a ) ),
195 a.showLegend && renderLegend( a ),
196 );
197 },
198
199 save: function ( { attributes: a } ) {
200 const blockProps = useBlockProps.save( { className: 'bkbg-waffle-wrap', style: getTypoCssVars( a ) } );
201 return el( 'div', blockProps,
202 a.showTitle && a.title ? el( 'h3', { className: 'bkbg-wc-title', style: { color: a.titleColor } }, a.title ) : null,
203 el( 'div', { className: 'bkbg-wc-grid' }, renderWaffle( a ) ),
204 a.showLegend ? renderLegend( a ) : null,
205 );
206 },
207 } );
208 }() );
209