| 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 |
|