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

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

302 lines 20.0 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 RichText = wp.blockEditor.RichText;
9 var useBlockProps = wp.blockEditor.useBlockProps;
10 var PanelBody = wp.components.PanelBody;
11 var Button = wp.components.Button;
12 var ToggleControl = wp.components.ToggleControl;
13 var RangeControl = wp.components.RangeControl;
14 var SelectControl = wp.components.SelectControl;
15 var TextControl = wp.components.TextControl;
16 var ColorPicker = wp.components.ColorPicker;
17 var Popover = wp.components.Popover;
18
19 var _fncTC, _fncTV;
20 function _tc() { return _fncTC || (_fncTC = window.bkbgTypographyControl); }
21 function _tv() { return _fncTV || (_fncTV = window.bkbgTypoCssVars); }
22 var IP = function () { return window.bkbgIconPicker; };
23
24 var ORIENTATION_OPTIONS = [
25 { label: __('Vertical', 'blockenberg'), value: 'vertical' },
26 { label: __('Horizontal', 'blockenberg'), value: 'horizontal' },
27 ];
28 var STYLE_OPTIONS = [
29 { label: __('Trapezoid', 'blockenberg'), value: 'trapezoid' },
30 { label: __('Bars', 'blockenberg'), value: 'bars' },
31 { label: __('Chevron', 'blockenberg'), value: 'chevron' },
32 ];
33 var VALUE_FORMAT_OPTIONS = [
34 { label: __('Number', 'blockenberg'), value: 'number' },
35 { label: __('Compact', 'blockenberg'), value: 'compact' },
36 { label: __('Percent of top', 'blockenberg'), value: 'pct_top' },
37 ];
38
39 function makeId() { return 'fn' + Math.random().toString(36).substr(2, 5); }
40
41 function formatVal(v, format) {
42 if (format === 'compact') {
43 if (v >= 1000000) return (v / 1000000).toFixed(1) + 'M';
44 if (v >= 1000) return (v / 1000).toFixed(1) + 'K';
45 return String(v);
46 }
47 return v.toLocaleString();
48 }
49
50 function buildWrapStyle(a) {
51 return Object.assign(
52 {
53 '--bkbg-fn-label-color': a.labelColor,
54 '--bkbg-fn-title-color': a.titleColor,
55 '--bkbg-fn-subtitle-color': a.subtitleColor,
56 '--bkbg-fn-drop-off-color': a.dropOffColor,
57 '--bkbg-fn-card-bg': a.cardBg,
58 '--bkbg-fn-card-r': a.cardRadius + 'px',
59 paddingTop: a.paddingTop + 'px',
60 paddingBottom: a.paddingBottom + 'px',
61 backgroundColor: a.bgColor || undefined,
62 },
63 _tv()(a.typoTitle, '--bkbg-fnc-tt-'),
64 _tv()(a.typoSubtitle, '--bkbg-fnc-st-'),
65 _tv()(a.typoLabel, '--bkbg-fnc-lb-'),
66 _tv()(a.typoValue, '--bkbg-fnc-vl-')
67 );
68 }
69
70 function renderColorControl(key, label, value, onChange, openKey, setOpenKey) {
71 var isOpen = openKey === key;
72 return el('div', { key: key, style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0', gap: '8px' } },
73 el('span', { style: { fontSize: '12px', color: '#1e1e1e', flex: 1, lineHeight: 1.4 } }, label),
74 el('div', { style: { position: 'relative', flexShrink: 0 } },
75 el('button', { type: 'button', onClick: function () { setOpenKey(isOpen ? null : key); }, style: { width: '28px', height: '28px', borderRadius: '4px', border: isOpen ? '2px solid #007cba' : '2px solid #ddd', cursor: 'pointer', padding: 0, background: value || '#ccc' } }),
76 isOpen && el(Popover, { position: 'bottom left', onClose: function () { setOpenKey(null); } },
77 el('div', { style: { padding: '8px' } },
78 el(ColorPicker, { color: value, enableAlpha: true, onChange: onChange })
79 )
80 )
81 )
82 );
83 }
84
85 /* ── Funnel stage component ─────────────────────────────────────── */
86 function FunnelStage(props) {
87 var stage = props.stage;
88 var idx = props.idx;
89 var total = props.total;
90 var topVal = props.topVal;
91 var nextStage = props.nextStage;
92 var a = props.a;
93 var widthPct = topVal > 0 ? (stage.value / topVal) * 100 : 100;
94 var pct = topVal > 0 ? Math.round((stage.value / topVal) * 100) : 100;
95 var dropOff = nextStage ? Math.round(((stage.value - nextStage.value) / stage.value) * 100) : null;
96
97 var stageStyle = {
98 background: stage.color,
99 borderRadius: '4px',
100 display: 'flex',
101 alignItems: 'center',
102 justifyContent: 'center',
103 gap: '8px',
104 padding: '10px 16px',
105 minHeight: a.stageHeight + 'px',
106 transition: 'width 0.5s ease',
107 boxSizing: 'border-box',
108 color: a.labelColor,
109 };
110
111 if (a.style === 'trapezoid') {
112 stageStyle.width = widthPct + '%';
113 stageStyle.borderRadius = '4px';
114 } else if (a.style === 'bars') {
115 stageStyle.width = widthPct + '%';
116 stageStyle.borderRadius = '6px';
117 } else {
118 /* chevron: full width, clip-path */
119 stageStyle.width = '100%';
120 stageStyle.clipPath = idx < total - 1
121 ? 'polygon(0 0, 100% 0, 92% 50%, 100% 100%, 0 100%, 8% 50%)'
122 : 'polygon(0 0, 100% 0, 100% 100%, 0 100%, 8% 50%)';
123 stageStyle.borderRadius = '0';
124 }
125
126 return el('div', { className: 'bkbg-fn-stage-wrap', 'data-idx': idx, style: { marginBottom: a.stageGap + 'px' } },
127 el('div', { className: 'bkbg-fn-stage', style: { display: 'flex', justifyContent: 'center' } },
128 el('div', { className: 'bkbg-fn-stage-bar', style: stageStyle },
129 a.showIcons && stage.icon && el('span', { className: 'bkbg-fn-stage-icon', style: { fontSize: '18px', lineHeight: 1 } }, (stage.iconType || 'custom-char') !== 'custom-char' && IP() ? IP().buildEditorIcon(el, stage.iconType, stage.icon, stage.iconDashicon, stage.iconImageUrl, stage.iconDashiconColor) : stage.icon),
130 el('span', { className: 'bkbg-fn-stage-label', style: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, stage.label),
131 a.showValues && el('span', { className: 'bkbg-fn-stage-value', style: { opacity: 0.9 } },
132 a.valueFormat === 'pct_top' ? pct + '%' : formatVal(stage.value, a.valueFormat)
133 ),
134 a.showPercent && a.valueFormat !== 'pct_top' && idx < total && el('span', { className: 'bkbg-fn-stage-pct', style: { opacity: 0.75 } }, '(' + pct + '%)')
135 )
136 ),
137 dropOff !== null && a.showDropOff && el('div', { className: 'bkbg-fn-drop-off', style: { textAlign: 'center', color: a.dropOffColor, padding: '3px 0' } },
138 '' + dropOff + '% ' + __('drop-off', 'blockenberg')
139 )
140 );
141 }
142
143 registerBlockType('blockenberg/funnel-chart', {
144 title: __('Funnel Chart', 'blockenberg'),
145 icon: 'filter',
146 category: 'bkbg-charts',
147
148 edit: function (props) {
149 var a = props.attributes;
150 var setAttributes = props.setAttributes;
151
152 var openColorKeyState = useState(null);
153 var openColorKey = openColorKeyState[0];
154 var setOpenColorKey = openColorKeyState[1];
155
156 var blockProps = useBlockProps({ style: buildWrapStyle(a) });
157
158 var topVal = a.stages.length > 0 ? a.stages[0].value : 1;
159
160 function updateStage(id, key, val) {
161 setAttributes({ stages: a.stages.map(function (s) {
162 if (s.id !== id) return s;
163 var u = Object.assign({}, s); u[key] = val; return u;
164 }) });
165 }
166
167 function addStage() {
168 var last = a.stages[a.stages.length - 1];
169 setAttributes({ stages: a.stages.concat([{
170 id: makeId(), label: __('New Stage', 'blockenberg'),
171 value: last ? Math.round(last.value * 0.5) : 100,
172 icon: '📌', iconType: 'custom-char', iconDashicon: '', iconImageUrl: '', color: '#7c3aed'
173 }]) });
174 }
175
176 function removeStage(id) {
177 setAttributes({ stages: a.stages.filter(function (s) { return s.id !== id; }) });
178 }
179
180 function cc(key, label, attrKey) {
181 return renderColorControl(key, label, a[attrKey], function (val) {
182 var upd = {}; upd[attrKey] = val; setAttributes(upd);
183 }, openColorKey, setOpenColorKey);
184 }
185
186 return el(Fragment, null,
187 el(InspectorControls, null,
188 el(PanelBody, { title: __('Stages', 'blockenberg'), initialOpen: true },
189 a.stages.map(function (s, si) {
190 return el('div', { key: s.id, style: { border: '1px solid #e0e0e0', borderRadius: '8px', padding: '10px', marginBottom: '10px', background: '#fafafa' } },
191 el(TextControl, { label: __('Label', 'blockenberg'), value: s.label, onChange: function (v) { updateStage(s.id, 'label', v); } }),
192 el(TextControl, { label: __('Value', 'blockenberg'), value: String(s.value), type: 'number', onChange: function (v) { updateStage(s.id, 'value', parseFloat(v) || 0); } }),
193 IP() ? el(IP().IconPickerControl, {
194 iconType: s.iconType || 'custom-char',
195 customChar: s.icon || '',
196 dashicon: s.iconDashicon || '',
197 imageUrl: s.iconImageUrl || '',
198 onChangeType: function (v) { updateStage(s.id, 'iconType', v); },
199 onChangeChar: function (v) { updateStage(s.id, 'icon', v); },
200 onChangeDashicon: function (v) { updateStage(s.id, 'iconDashicon', v); },
201 onChangeImageUrl: function (v) { updateStage(s.id, 'iconImageUrl', v); },
202 label: __('Icon', 'blockenberg')
203 }) : el(TextControl, { label: __('Icon (emoji)', 'blockenberg'), value: s.icon, onChange: function (v) { updateStage(s.id, 'icon', v); } }),
204 renderColorControl('s-' + s.id + '-color', __('Color', 'blockenberg'), s.color, function (v) { updateStage(s.id, 'color', v); }, openColorKey, setOpenColorKey),
205 el(Button, { isDestructive: true, variant: 'tertiary', size: 'compact', onClick: function () { removeStage(s.id); }, style: { marginTop: '6px' } }, __('Remove', 'blockenberg'))
206 );
207 }),
208 el(Button, { variant: 'secondary', onClick: addStage, style: { width: '100%', justifyContent: 'center' } }, '+ ' + __('Add Stage', 'blockenberg'))
209 ),
210 el(PanelBody, { title: __('Layout', 'blockenberg'), initialOpen: false },
211 el(SelectControl, { label: __('Style', 'blockenberg'), value: a.style, options: STYLE_OPTIONS, onChange: function (v) { setAttributes({ style: v }); } }),
212 el(SelectControl, { label: __('Orientation', 'blockenberg'), value: a.orientation, options: ORIENTATION_OPTIONS, onChange: function (v) { setAttributes({ orientation: v }); } }),
213 el(SelectControl, { label: __('Value format', 'blockenberg'), value: a.valueFormat, options: VALUE_FORMAT_OPTIONS, onChange: function (v) { setAttributes({ valueFormat: v }); } }),
214 el(RangeControl, { label: __('Stage height (px)', 'blockenberg'), value: a.stageHeight, min: 40, max: 120, onChange: function (v) { setAttributes({ stageHeight: v }); } }),
215 el(RangeControl, { label: __('Stage gap (px)', 'blockenberg'), value: a.stageGap, min: 0, max: 20, onChange: function (v) { setAttributes({ stageGap: v }); } }),
216 el(RangeControl, { label: __('Max width (px)', 'blockenberg'), value: a.maxWidth, min: 300, max: 900, onChange: function (v) { setAttributes({ maxWidth: v }); } })
217 ),
218 el(PanelBody, { title: __('Display Options', 'blockenberg'), initialOpen: false },
219 el(ToggleControl, { label: __('Show values', 'blockenberg'), checked: a.showValues, onChange: function (v) { setAttributes({ showValues: v }); }, __nextHasNoMarginBottom: true }),
220 el(ToggleControl, { label: __('Show %', 'blockenberg'), checked: a.showPercent, onChange: function (v) { setAttributes({ showPercent: v }); }, __nextHasNoMarginBottom: true }),
221 el(ToggleControl, { label: __('Show drop-off', 'blockenberg'), checked: a.showDropOff, onChange: function (v) { setAttributes({ showDropOff: v }); }, __nextHasNoMarginBottom: true }),
222 el(ToggleControl, { label: __('Show icons', 'blockenberg'), checked: a.showIcons, onChange: function (v) { setAttributes({ showIcons: v }); }, __nextHasNoMarginBottom: true }),
223 ),
224
225 el( PanelBody, { title: __( 'Typography', 'blockenberg' ), initialOpen: false },
226 _tc()({ label: __('Title', 'blockenberg'), value: a.typoTitle, onChange: function (v) { setAttributes({ typoTitle: v }); } }),
227 _tc()({ label: __('Subtitle', 'blockenberg'), value: a.typoSubtitle, onChange: function (v) { setAttributes({ typoSubtitle: v }); } }),
228 _tc()({ label: __('Stage Label', 'blockenberg'), value: a.typoLabel, onChange: function (v) { setAttributes({ typoLabel: v }); } }),
229 _tc()({ label: __('Stage Value', 'blockenberg'), value: a.typoValue, onChange: function (v) { setAttributes({ typoValue: v }); } })
230 ),
231 el(PanelBody, { title: __('Colors', 'blockenberg'), initialOpen: false },
232 cc('labelColor', __('Stage label text', 'blockenberg'), 'labelColor'),
233 cc('dropOffColor', __('Drop-off text', 'blockenberg'), 'dropOffColor'),
234 cc('titleColor', __('Title', 'blockenberg'), 'titleColor'),
235 cc('subtitleColor', __('Subtitle', 'blockenberg'), 'subtitleColor'),
236 cc('cardBg', __('Card background', 'blockenberg'), 'cardBg'),
237 cc('bgColor', __('Section bg', 'blockenberg'), 'bgColor')
238 ),
239 el(PanelBody, { title: __('Card Style', 'blockenberg'), initialOpen: false },
240 el(RangeControl, { label: __('Card radius (px)', 'blockenberg'), value: a.cardRadius, min: 0, max: 32, onChange: function (v) { setAttributes({ cardRadius: v }); } }),
241 ),
242 el(PanelBody, { title: __('Spacing', 'blockenberg'), initialOpen: false },
243 el(RangeControl, { label: __('Padding top', 'blockenberg'), value: a.paddingTop, min: 0, max: 200, onChange: function (v) { setAttributes({ paddingTop: v }); } }),
244 el(RangeControl, { label: __('Padding bottom', 'blockenberg'), value: a.paddingBottom, min: 0, max: 200, onChange: function (v) { setAttributes({ paddingBottom: v }); } })
245 )
246 ),
247
248 el('div', blockProps,
249 el('div', { className: 'bkbg-fn-card', style: { background: a.cardBg, borderRadius: a.cardRadius + 'px', padding: '28px', boxSizing: 'border-box' } },
250 (a.title || a.subtitle) && el('div', { className: 'bkbg-fn-header', style: { textAlign: 'center', marginBottom: '24px' } },
251 el(RichText, { tagName: 'h3', className: 'bkbg-fn-title', value: a.title, onChange: function (v) { setAttributes({ title: v }); }, placeholder: __('Funnel title…', 'blockenberg'), style: { color: a.titleColor, margin: '0 0 6px' } }),
252 el(RichText, { tagName: 'p', className: 'bkbg-fn-subtitle', value: a.subtitle, onChange: function (v) { setAttributes({ subtitle: v }); }, placeholder: __('Optional subtitle…', 'blockenberg'), style: { color: a.subtitleColor, margin: 0 } })
253 ),
254 el('div', { className: 'bkbg-fn-funnel bkbg-fn-funnel--' + a.style + ' bkbg-fn-funnel--' + a.orientation, style: { maxWidth: a.maxWidth + 'px', margin: '0 auto' } },
255 a.stages.map(function (s, si) {
256 return el(FunnelStage, {
257 key: s.id, stage: s, idx: si, total: a.stages.length, topVal: topVal,
258 nextStage: a.stages[si + 1] || null, a: a
259 });
260 })
261 )
262 )
263 )
264 );
265 },
266
267 save: function (props) {
268 var a = props.attributes;
269 var topVal = a.stages.length > 0 ? a.stages[0].value : 1;
270 return el('div', wp.blockEditor.useBlockProps.save({ className: 'bkbg-fn-wrapper', style: buildWrapStyle(a) }),
271 el('div', { className: 'bkbg-fn-card', style: { background: a.cardBg, borderRadius: a.cardRadius + 'px', padding: '28px', boxSizing: 'border-box' } },
272 (a.title || a.subtitle) && el('div', { className: 'bkbg-fn-header' },
273 a.title && el(RichText.Content, { tagName: 'h3', className: 'bkbg-fn-title', value: a.title }),
274 a.subtitle && el(RichText.Content, { tagName: 'p', className: 'bkbg-fn-subtitle', value: a.subtitle })
275 ),
276 el('div', { className: 'bkbg-fn-funnel bkbg-fn-funnel--' + a.style + ' bkbg-fn-funnel--' + a.orientation, style: { maxWidth: a.maxWidth + 'px', margin: '0 auto' } },
277 a.stages.map(function (s, si) {
278 var widthPct = topVal > 0 ? (s.value / topVal) * 100 : 100;
279 var pct = topVal > 0 ? Math.round((s.value / topVal) * 100) : 100;
280 var nextStage = a.stages[si + 1] || null;
281 var dropOff = nextStage ? Math.round(((s.value - nextStage.value) / s.value) * 100) : null;
282
283 var stageStyle = { background: s.color, '--bkbg-fn-w': widthPct + '%' };
284 return el('div', { key: s.id, className: 'bkbg-fn-stage-wrap', 'data-idx': si, 'data-value': s.value, 'data-pct': pct, style: { marginBottom: a.stageGap + 'px' } },
285 el('div', { className: 'bkbg-fn-stage' },
286 el('div', { className: 'bkbg-fn-stage-bar', style: stageStyle },
287 a.showIcons && s.icon && el('span', { className: 'bkbg-fn-stage-icon', 'aria-hidden': 'true' }, (s.iconType || 'custom-char') !== 'custom-char' && IP() ? IP().buildSaveIcon(el, s.iconType, s.icon, s.iconDashicon, s.iconImageUrl, s.iconDashiconColor) : s.icon),
288 el('span', { className: 'bkbg-fn-stage-label' }, s.label),
289 a.showValues && el('span', { className: 'bkbg-fn-stage-value' }, a.valueFormat === 'pct_top' ? pct + '%' : s.value.toLocaleString()),
290 a.showPercent && a.valueFormat !== 'pct_top' && el('span', { className: 'bkbg-fn-stage-pct' }, '(' + pct + '%)')
291 )
292 ),
293 dropOff !== null && a.showDropOff && el('div', { className: 'bkbg-fn-drop-off' }, '' + dropOff + '% drop-off')
294 );
295 })
296 )
297 )
298 );
299 }
300 });
301 }() );
302