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 / waterfall-chart / index.js

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

553 lines 28.6 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 useState = wp.element.useState;
5 var __ = wp.i18n.__;
6 var registerBlockType = wp.blocks.registerBlockType;
7 var InspectorControls = wp.blockEditor.InspectorControls;
8 var useBlockProps = wp.blockEditor.useBlockProps;
9 var PanelColorSettings = wp.blockEditor.PanelColorSettings;
10 var PanelBody = wp.components.PanelBody;
11 var ToggleControl = wp.components.ToggleControl;
12 var RangeControl = wp.components.RangeControl;
13 var SelectControl = wp.components.SelectControl;
14 var TextControl = wp.components.TextControl;
15 var Button = wp.components.Button;
16
17 // Resolve the shared components lazily without defining globals for each block.
18 function _tc(label, key, attrs, setA) {
19 var Control = window.bkbgTypographyControl;
20 return Control ? el(Control, {
21 label: label,
22 value: attrs[key] || {},
23 onChange: function (value) { var update = {}; update[key] = value; setA(update); }
24 }) : null;
25 }
26 function _tvf(vars, key, attrs, prefix) {
27 var cssVars = window.bkbgTypoCssVars;
28 if (cssVars) Object.assign(vars, cssVars(attrs[key] || {}, prefix));
29 return vars;
30 }
31 function getTypoControl(label, key, attrs, setA) { return _tc(label, key, attrs, setA); }
32 function getTypoCssVars(attrs) {
33 var v = {};
34 _tvf(v, 'titleTypo', attrs, '--bkwfc-tt-');
35 return v;
36 }
37
38 /* ── updateItem (ES5 safe) ── */
39 function updateItem(arr, idx, field, val) {
40 return arr.map(function (item, i) {
41 if (i !== idx) return item;
42 var p = {}; p[field] = val;
43 return Object.assign({}, item, p);
44 });
45 }
46
47 /* ── abbreviate large numbers ── */
48 function abbr(n, prefix, suffix, doAbbr) {
49 var abs = Math.abs(n);
50 var sign = n < 0 ? '-' : '';
51 var str;
52 if (!doAbbr || abs < 1000) {
53 str = abs.toLocaleString();
54 } else if (abs < 1000000) {
55 str = (abs / 1000).toFixed(1).replace(/\.0$/, '') + 'K';
56 } else if (abs < 1000000000) {
57 str = (abs / 1000000).toFixed(1).replace(/\.0$/, '') + 'M';
58 } else {
59 str = (abs / 1000000000).toFixed(1).replace(/\.0$/, '') + 'B';
60 }
61 return sign + prefix + str + suffix;
62 }
63
64 /* ── type label ── */
65 var TYPE_LABELS = {
66 start: __('Starting value', 'blockenberg'),
67 increase: __('Increase (+)', 'blockenberg'),
68 decrease: __('Decrease (−)', 'blockenberg'),
69 total: __('Total / Net', 'blockenberg')
70 };
71 var TYPE_OPTIONS = [
72 { label: __('Starting value', 'blockenberg'), value: 'start' },
73 { label: __('Increase (+)', 'blockenberg'), value: 'increase' },
74 { label: __('Decrease (−)', 'blockenberg'), value: 'decrease' },
75 { label: __('Total / Net', 'blockenberg'), value: 'total' }
76 ];
77
78 /* ── color for type ── */
79 function typeColor(type, a) {
80 if (type === 'start') return a.startColor;
81 if (type === 'increase') return a.increaseColor;
82 if (type === 'decrease') return a.decreaseColor;
83 return a.totalColor;
84 }
85
86 /* ── compute bar segments for preview ── */
87 /* Returns array of { label, type, base, top, barColor, displayValue } */
88 function computeSegments(items, a) {
89 var running = 0;
90 return items.map(function (item) {
91 var v = parseFloat(item.value) || 0;
92 var type = item.type || 'increase';
93 var base, top;
94
95 if (type === 'start') {
96 base = 0; top = v; running = v;
97 } else if (type === 'total') {
98 base = 0; top = running; /* running stays same */
99 } else if (type === 'increase') {
100 base = running; top = running + v; running = top;
101 } else { /* decrease */
102 top = running; base = running + v; running = base;
103 }
104
105 return {
106 label: item.label,
107 type: type,
108 base: base,
109 top: top,
110 barColor: typeColor(type, a),
111 displayValue: v,
112 runningAfter: running
113 };
114 });
115 }
116
117 /* ── SVG bar chart preview in editor ── */
118 function WaterfallPreview(props) {
119 var a = props.attributes;
120 var items = a.items;
121 if (!items || !items.length) return el('div', {}, 'No data.');
122
123 var segs = computeSegments(items, a);
124 var allVals = segs.map(function (s) { return s.top; }).concat(segs.map(function (s) { return s.base; }));
125 var maxVal = Math.max.apply(null, allVals);
126 var minVal = Math.min.apply(null, allVals.concat([0]));
127 var range = maxVal - minVal || 1;
128
129 var svgW = 600, svgH = Math.min(a.height, 320);
130 var padLeft = 55, padRight = 16, padTop = 20, padBottom = 36;
131 var chartW = svgW - padLeft - padRight;
132 var chartH = svgH - padTop - padBottom;
133 var barW = Math.min(a.barThickness, Math.floor(chartW / items.length) - 6);
134 var step = chartW / items.length;
135
136 function yPx(v) { return padTop + chartH - ((v - minVal) / range) * chartH; }
137
138 var bars = segs.map(function (s, i) {
139 var x = padLeft + i * step + (step - barW) / 2;
140 var y1 = yPx(Math.max(s.base, s.top));
141 var y2 = yPx(Math.min(s.base, s.top));
142 var bh = Math.max(y2 - y1, 2);
143 return { x: x, y: y1, width: barW, height: bh, seg: s, cx: x + barW / 2 };
144 });
145
146 var gridLines = [];
147 var gridCount = 5;
148 for (var g = 0; g <= gridCount; g++) {
149 var gv = minVal + (range / gridCount) * g;
150 var gy = yPx(gv);
151 gridLines.push(el('line', { key: 'gl' + g, x1: padLeft, x2: svgW - padRight, y1: gy, y2: gy, stroke: a.gridColor, strokeWidth: 1, strokeDasharray: '4,3' }));
152 gridLines.push(el('text', { key: 'glt' + g, x: padLeft - 4, y: gy + 4, textAnchor: 'end', fontSize: 9, fill: a.labelColor },
153 abbr(gv, a.valuePrefix, a.valueSuffix, a.abbreviate)
154 ));
155 }
156
157 var connectors = [];
158 if (a.showConnectors) {
159 for (var ci = 0; ci < bars.length - 1; ci++) {
160 var curr = bars[ci];
161 var next = bars[ci + 1];
162 var connY = yPx(segs[ci].type === 'decrease' ? segs[ci].base : segs[ci].top);
163 connectors.push(el('line', {
164 key: 'conn' + ci,
165 x1: curr.x + curr.width, x2: next.x,
166 y1: connY, y2: connY,
167 stroke: a.connectorColor, strokeWidth: 1.5, strokeDasharray: '4,3'
168 }));
169 }
170 }
171
172 return el('div', { style: { background: a.bgColor, borderRadius: '8px' } },
173 a.showTitle && a.title ? el('div', {
174 className: 'bkbg-wfc-title',
175 style: { color: a.titleColor, padding: '0 0 12px' }
176 }, a.title) : null,
177 el('svg', { width: '100%', viewBox: '0 0 600 ' + svgH, style: { display: 'block', maxWidth: '100%' } },
178 a.showGrid ? el('g', {}, gridLines) : null,
179 el('line', { x1: padLeft, x2: svgW - padRight, y1: yPx(0), y2: yPx(0), stroke: a.gridColor, strokeWidth: 1.5 }),
180 el('g', {}, connectors),
181 el('g', {},
182 bars.map(function (b, i) {
183 return el('g', { key: i },
184 el('rect', {
185 x: b.x, y: b.y, width: b.width, height: b.height,
186 fill: b.seg.barColor, rx: a.borderRadius
187 }),
188 a.showDataLabels ? el('text', {
189 x: b.cx, y: b.y - 4, textAnchor: 'middle',
190 fontSize: a.valueSize, fill: b.seg.barColor, fontWeight: 700, fontFamily: 'system-ui,sans-serif'
191 }, abbr(b.seg.displayValue, a.valuePrefix, a.valueSuffix, a.abbreviate)) : null,
192 el('text', {
193 x: b.cx, y: svgH - 4, textAnchor: 'middle',
194 fontSize: a.labelSize, fill: a.labelColor, fontFamily: 'system-ui,sans-serif'
195 }, b.seg.label.length > 10 ? b.seg.label.slice(0, 9) + '…' : b.seg.label)
196 );
197 })
198 )
199 ),
200 a.showLegend ? el('div', {
201 style: { display: 'flex', flexWrap: 'wrap', gap: '10px 18px', padding: '10px 0 0', fontFamily: 'system-ui,sans-serif' }
202 },
203 [['start', 'Start', a.startColor], ['increase', 'Increase', a.increaseColor], ['decrease', 'Decrease', a.decreaseColor], ['total', 'Total', a.totalColor]].map(function (row) {
204 return el('div', { key: row[0], style: { display: 'flex', alignItems: 'center', gap: '5px' } },
205 el('span', { style: { width: '12px', height: '12px', borderRadius: '3px', background: row[2], display: 'inline-block', flexShrink: 0 } }),
206 el('span', { style: { fontSize: '12px', color: a.labelColor } }, row[1])
207 );
208 })
209 ) : null
210 );
211 }
212
213 /* ── item list editor ── */
214 function ItemEditor(props) {
215 var items = props.items;
216 var setItems = props.setItems;
217 var expanded = props.expanded;
218 var setExpanded = props.setExpanded;
219
220 return el('div', {},
221 items.map(function (item, i) {
222 var isOpen = expanded === i;
223 var col = {start:'#6366f1',increase:'#10b981',decrease:'#ef4444',total:'#8b5cf6'}[item.type] || '#6b7280';
224 return el('div', {
225 key: i,
226 style: { border: '1px solid #e5e7eb', borderRadius: '6px', marginBottom: '5px', overflow: 'hidden' }
227 },
228 el('div', {
229 onClick: function () { setExpanded(isOpen ? -1 : i); },
230 style: {
231 padding: '7px 10px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px',
232 background: isOpen ? '#f9fafb' : '#fff',
233 borderBottom: isOpen ? '1px solid #e5e7eb' : 'none'
234 }
235 },
236 el('span', { style: { width: '10px', height: '10px', borderRadius: '2px', background: col, flexShrink: 0 } }),
237 el('span', { style: { flex: 1, fontSize: '12px', color: '#374151', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } },
238 item.label || '(empty)'
239 ),
240 el('span', { style: { fontSize: '11px', color: '#9ca3af', marginRight: '4px' } },
241 (item.value > 0 ? '+' : '') + (item.value || 0)
242 ),
243 el('button', {
244 onClick: function (e) {
245 e.stopPropagation();
246 if (items.length <= 2) return;
247 setItems(items.filter(function (_, j) { return j !== i; }));
248 setExpanded(-1);
249 },
250 style: { background: 'none', border: 'none', cursor: 'pointer', color: '#ef4444', fontSize: '14px', padding: '0 2px', lineHeight: 1 }
251 }, '×'),
252 el('span', { style: { color: '#9ca3af', fontSize: '10px' } }, isOpen ? '▲' : '▼')
253 ),
254 isOpen ? el('div', { style: { padding: '10px' } },
255 el(TextControl, {
256 __nextHasNoMarginBottom: true,
257 label: __('Label', 'blockenberg'),
258 value: item.label,
259 onChange: function (v) { setItems(updateItem(items, i, 'label', v)); }
260 }),
261 el('div', { style: { marginTop: '8px' } },
262 el(TextControl, {
263 __nextHasNoMarginBottom: true,
264 label: __('Value (negative = decrease)', 'blockenberg'),
265 type: 'number',
266 value: item.value,
267 onChange: function (v) { setItems(updateItem(items, i, 'value', parseFloat(v) || 0)); }
268 })
269 ),
270 el('div', { style: { marginTop: '8px' } },
271 el(SelectControl, {
272 __nextHasNoMarginBottom: true,
273 label: __('Type', 'blockenberg'),
274 value: item.type,
275 options: TYPE_OPTIONS,
276 onChange: function (v) { setItems(updateItem(items, i, 'type', v)); }
277 })
278 ),
279 el('div', { style: { display: 'flex', gap: '6px', marginTop: '8px' } },
280 i > 0 ? el(Button, {
281 variant: 'secondary', style: { fontSize: '11px' },
282 onClick: function () {
283 var arr = items.slice(); var tmp = arr[i - 1]; arr[i - 1] = arr[i]; arr[i] = tmp;
284 setItems(arr); setExpanded(i - 1);
285 }
286 }, '↑') : null,
287 i < items.length - 1 ? el(Button, {
288 variant: 'secondary', style: { fontSize: '11px' },
289 onClick: function () {
290 var arr = items.slice(); var tmp = arr[i + 1]; arr[i + 1] = arr[i]; arr[i] = tmp;
291 setItems(arr); setExpanded(i + 1);
292 }
293 }, '↓') : null,
294 el(Button, {
295 variant: 'secondary', style: { fontSize: '11px' },
296 onClick: function () {
297 var copy = Object.assign({}, item, { label: item.label + ' (copy)' });
298 var arr = items.slice(); arr.splice(i + 1, 0, copy);
299 setItems(arr); setExpanded(i + 1);
300 }
301 }, __('Dup', 'blockenberg'))
302 )
303 ) : null
304 );
305 }),
306 el('div', { style: { marginTop: '8px', display: 'flex', gap: '6px' } },
307 el(Button, {
308 variant: 'primary', style: { flex: 1, justifyContent: 'center' },
309 onClick: function () {
310 var arr = items.slice();
311 arr.push({ label: 'New item', value: 0, type: 'increase' });
312 setItems(arr); setExpanded(arr.length - 1);
313 }
314 }, __('+ Add Item', 'blockenberg')),
315 el(Button, {
316 variant: 'secondary', style: { justifyContent: 'center' },
317 onClick: function () {
318 var arr = items.slice();
319 arr.push({ label: 'Total', value: 0, type: 'total' });
320 setItems(arr); setExpanded(arr.length - 1);
321 }
322 }, __('+ Total', 'blockenberg'))
323 )
324 );
325 }
326
327 /* ── register ── */
328 registerBlockType('blockenberg/waterfall-chart', {
329 edit: function (props) {
330 var a = props.attributes;
331 var setAttributes = props.setAttributes;
332
333 var expState = useState(-1);
334 var expanded = expState[0];
335 var setExpanded = expState[1];
336
337 function setItems(v) { setAttributes({ items: v }); }
338
339 var blockProps = useBlockProps({ className: 'bkbg-wfc-editor-wrap' });
340
341 return el(Fragment, {},
342
343 el(InspectorControls, {},
344
345 /* ── Data ── */
346 el(PanelBody, { title: __('Chart Data', 'blockenberg'), initialOpen: true },
347 el(ItemEditor, {
348 items: a.items,
349 setItems: setItems,
350 expanded: expanded,
351 setExpanded: setExpanded
352 })
353 ),
354
355 /* ── Labels ── */
356 el(PanelBody, { title: __('Labels & Format', 'blockenberg'), initialOpen: false },
357 el(TextControl, {
358 __nextHasNoMarginBottom: true,
359 label: __('Chart title', 'blockenberg'),
360 value: a.title,
361 onChange: function (v) { setAttributes({ title: v }); }
362 }),
363 el('div', { style: { marginTop: '10px' } },
364 el(ToggleControl, {
365 __nextHasNoMarginBottom: true,
366 label: __('Show title', 'blockenberg'),
367 checked: a.showTitle,
368 onChange: function (v) { setAttributes({ showTitle: v }); }
369 })
370 ),
371 el('div', { style: { marginTop: '10px', display: 'flex', gap: '8px' } },
372 el('div', { style: { flex: 1 } },
373 el(TextControl, {
374 __nextHasNoMarginBottom: true,
375 label: __('Value prefix', 'blockenberg'),
376 value: a.valuePrefix,
377 onChange: function (v) { setAttributes({ valuePrefix: v }); }
378 })
379 ),
380 el('div', { style: { flex: 1 } },
381 el(TextControl, {
382 __nextHasNoMarginBottom: true,
383 label: __('Value suffix', 'blockenberg'),
384 value: a.valueSuffix,
385 onChange: function (v) { setAttributes({ valueSuffix: v }); }
386 })
387 )
388 ),
389 el('div', { style: { marginTop: '10px' } },
390 el(ToggleControl, {
391 __nextHasNoMarginBottom: true,
392 label: __('Abbreviate large values (K, M, B)', 'blockenberg'),
393 checked: a.abbreviate,
394 onChange: function (v) { setAttributes({ abbreviate: v }); }
395 })
396 )
397 ),
398
399 /* ── Appearance ── */
400 el(PanelBody, { title: __('Appearance', 'blockenberg'), initialOpen: false },
401 el(RangeControl, {
402 __nextHasNoMarginBottom: true,
403 label: __('Chart height (px)', 'blockenberg'),
404 value: a.height, min: 200, max: 700,
405 onChange: function (v) { setAttributes({ height: v }); }
406 }),
407 el('div', { style: { marginTop: '10px' } },
408 el(RangeControl, {
409 __nextHasNoMarginBottom: true,
410 label: __('Bar max thickness (px)', 'blockenberg'),
411 value: a.barThickness, min: 20, max: 100,
412 onChange: function (v) { setAttributes({ barThickness: v }); }
413 })
414 ),
415 el('div', { style: { marginTop: '10px' } },
416 el(RangeControl, {
417 __nextHasNoMarginBottom: true,
418 label: __('Bar border radius (px)', 'blockenberg'),
419 value: a.borderRadius, min: 0, max: 20,
420 onChange: function (v) { setAttributes({ borderRadius: v }); }
421 })
422 ),
423 el('div', { style: { marginTop: '10px' } },
424 el(RangeControl, {
425 __nextHasNoMarginBottom: true,
426 label: __('Max width (px)', 'blockenberg'),
427 value: a.maxWidth, min: 400, max: 1400,
428 onChange: function (v) { setAttributes({ maxWidth: v }); }
429 })
430 ),
431 el('div', { style: { marginTop: '10px' } },
432 el(ToggleControl, {
433 __nextHasNoMarginBottom: true,
434 label: __('Show connector lines', 'blockenberg'),
435 checked: a.showConnectors,
436 onChange: function (v) { setAttributes({ showConnectors: v }); }
437 })
438 ),
439 el('div', { style: { marginTop: '4px' } },
440 el(ToggleControl, {
441 __nextHasNoMarginBottom: true,
442 label: __('Show data labels', 'blockenberg'),
443 checked: a.showDataLabels,
444 onChange: function (v) { setAttributes({ showDataLabels: v }); }
445 })
446 ),
447 el('div', { style: { marginTop: '4px' } },
448 el(ToggleControl, {
449 __nextHasNoMarginBottom: true,
450 label: __('Show grid lines', 'blockenberg'),
451 checked: a.showGrid,
452 onChange: function (v) { setAttributes({ showGrid: v }); }
453 })
454 ),
455 el('div', { style: { marginTop: '4px' } },
456 el(ToggleControl, {
457 __nextHasNoMarginBottom: true,
458 label: __('Show legend', 'blockenberg'),
459 checked: a.showLegend,
460 onChange: function (v) { setAttributes({ showLegend: v }); }
461 })
462 ),
463 el('div', { style: { marginTop: '10px', display: 'flex', gap: '8px' } },
464 el('div', { style: { flex: 1 } },
465 el(RangeControl, {
466 __nextHasNoMarginBottom: true,
467 label: __('Padding top', 'blockenberg'),
468 value: a.paddingTop, min: 0, max: 120,
469 onChange: function (v) { setAttributes({ paddingTop: v }); }
470 })
471 ),
472 el('div', { style: { flex: 1 } },
473 el(RangeControl, {
474 __nextHasNoMarginBottom: true,
475 label: __('Padding bottom', 'blockenberg'),
476 value: a.paddingBottom, min: 0, max: 120,
477 onChange: function (v) { setAttributes({ paddingBottom: v }); }
478 })
479 )
480 )
481 ),
482
483 /* ── Colors ── */
484
485 el( PanelBody, { title: __( 'Typography', 'blockenberg' ), initialOpen: false },
486 getTypoControl( __( 'Title', 'blockenberg' ), 'titleTypo', a, setAttributes ),
487 el('div', { style: { marginTop: '10px', display: 'flex', gap: '8px' } },
488 el('div', { style: { flex: 1 } },
489 el(RangeControl, {
490 __nextHasNoMarginBottom: true,
491 label: __('Label size', 'blockenberg'),
492 value: a.labelSize, min: 8, max: 18,
493 onChange: function (v) { setAttributes({ labelSize: v }); }
494 })
495 ),
496 el('div', { style: { flex: 1 } },
497 el(RangeControl, {
498 __nextHasNoMarginBottom: true,
499 label: __('Value size', 'blockenberg'),
500 value: a.valueSize, min: 8, max: 18,
501 onChange: function (v) { setAttributes({ valueSize: v }); }
502 })
503 )
504 )
505 ),
506 el(PanelColorSettings, {
507 title: __('Colors', 'blockenberg'),
508 initialOpen: false,
509 colorSettings: [
510 { label: __('Background', 'blockenberg'), value: a.bgColor, onChange: function (v) { setAttributes({ bgColor: v || '#ffffff' }); } },
511 { label: __('Title', 'blockenberg'), value: a.titleColor, onChange: function (v) { setAttributes({ titleColor: v || '#111827' }); } },
512 { label: __('Labels / axis', 'blockenberg'), value: a.labelColor, onChange: function (v) { setAttributes({ labelColor: v || '#6b7280' }); } },
513 { label: __('Grid lines', 'blockenberg'), value: a.gridColor, onChange: function (v) { setAttributes({ gridColor: v || '#e5e7eb' }); } },
514 { label: __('Connectors', 'blockenberg'), value: a.connectorColor, onChange: function (v) { setAttributes({ connectorColor: v || '#9ca3af' }); } },
515 { label: __('Start bars', 'blockenberg'), value: a.startColor, onChange: function (v) { setAttributes({ startColor: v || '#6366f1' }); } },
516 { label: __('Increase bars', 'blockenberg'), value: a.increaseColor, onChange: function (v) { setAttributes({ increaseColor: v || '#10b981' }); } },
517 { label: __('Decrease bars', 'blockenberg'), value: a.decreaseColor, onChange: function (v) { setAttributes({ decreaseColor: v || '#ef4444' }); } },
518 { label: __('Total bars', 'blockenberg'), value: a.totalColor, onChange: function (v) { setAttributes({ totalColor: v || '#8b5cf6' }); } }
519 ]
520 })
521 ),
522
523 /* ── canvas ── */
524 el('div', blockProps,
525 el('div', {
526 style: {
527 background: a.bgColor,
528 borderRadius: '10px',
529 padding: '24px',
530 paddingTop: a.paddingTop ? a.paddingTop + 'px' : '24px',
531 paddingBottom: a.paddingBottom ? a.paddingBottom + 'px' : '24px',
532 maxWidth: a.maxWidth + 'px',
533 margin: '0 auto',
534 fontFamily: 'system-ui, sans-serif'
535 }
536 },
537 el(WaterfallPreview, { attributes: a })
538 )
539 )
540 );
541 },
542
543 save: function (props) {
544 return el('div', useBlockProps.save({ style: getTypoCssVars(props.attributes) }),
545 el('div', {
546 className: 'bkbg-wfc-app',
547 'data-opts': JSON.stringify(props.attributes)
548 })
549 );
550 }
551 });
552 }() );
553