| 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, SelectControl, Button } = window.wp.components; |
| 6 |
const { __ } = window.wp.i18n; |
| 7 |
|
| 8 |
var _tc, _tv; |
| 9 |
function getTypoControl() { return _tc || (_tc = window.bkbgTypographyControl); } |
| 10 |
function getTypoCssVars() { return _tv || (_tv = window.bkbgTypoCssVars); } |
| 11 |
|
| 12 |
// ── Formatters ──────────────────────────────────────────────────────────── |
| 13 |
function fmtValue( v, fmt, unit ) { |
| 14 |
const n = parseFloat( v ) || 0; |
| 15 |
let str; |
| 16 |
if ( fmt === 'currency' ) { |
| 17 |
str = unit + n.toLocaleString( 'en-US', { maximumFractionDigits: 0 } ); |
| 18 |
} else if ( fmt === 'percent' ) { |
| 19 |
str = n.toFixed( 1 ) + '%'; |
| 20 |
} else { |
| 21 |
str = n.toLocaleString( 'en-US', { maximumFractionDigits: 2 } ); |
| 22 |
if ( unit ) str += ' ' + unit; |
| 23 |
} |
| 24 |
return str; |
| 25 |
} |
| 26 |
|
| 27 |
function changePct( before, after ) { |
| 28 |
const b = parseFloat( before ) || 0; |
| 29 |
const a = parseFloat( after ) || 0; |
| 30 |
if ( b === 0 ) return null; |
| 31 |
return ( ( a - b ) / Math.abs( b ) ) * 100; |
| 32 |
} |
| 33 |
|
| 34 |
function barFraction( before, after ) { |
| 35 |
const b = Math.abs( parseFloat( before ) ) || 0; |
| 36 |
const a = Math.abs( parseFloat( after ) ) || 0; |
| 37 |
const mx = Math.max( b, a ) || 1; |
| 38 |
return { bFrac: b / mx, aFrac: a / mx }; |
| 39 |
} |
| 40 |
|
| 41 |
// ── One card ───────────────────────────────────────────────────────────── |
| 42 |
function MetricCard( props ) { |
| 43 |
const { m, a, inlineTypo } = props; |
| 44 |
const pct = changePct( m.valueBefore, m.valueAfter ); |
| 45 |
const isPos = m.higherIsBetter ? pct >= 0 : pct <= 0; |
| 46 |
const isNeg = ! isPos; |
| 47 |
const isNull = pct === null; |
| 48 |
const chgColor = isNull ? a.neutralColor : isPos ? a.posColor : a.negColor; |
| 49 |
const arrow = pct === null ? '↔' : pct > 0 ? '▲' : pct < 0 ? '▼' : '↔'; |
| 50 |
const { bFrac, aFrac } = barFraction( m.valueBefore, m.valueAfter ); |
| 51 |
const cardStyle = { |
| 52 |
background: a.cardBg, |
| 53 |
borderRadius: a.borderRadius + 'px', |
| 54 |
padding: a.cardPadding + 'px', |
| 55 |
boxSizing: 'border-box', |
| 56 |
display: 'flex', |
| 57 |
flexDirection: 'column', |
| 58 |
gap: 6, |
| 59 |
}; |
| 60 |
var labelSt = { color: a.labelColor }; |
| 61 |
if (inlineTypo) { labelSt.fontSize = (a.labelFontSize || 13); labelSt.fontWeight = 600; labelSt.lineHeight = (a.lineHeight || 1.3); labelSt.textTransform = 'uppercase'; labelSt.letterSpacing = '0.05em'; } |
| 62 |
var valueSt = { color: a.textColor }; |
| 63 |
if (inlineTypo) { valueSt.fontSize = (a.valueFontSize || 32); valueSt.fontWeight = (a.titleFontWeight || 700); valueSt.lineHeight = (a.lineHeight || 1.3); } |
| 64 |
|
| 65 |
return el( 'div', { className: 'bkbg-mc-card', style: cardStyle }, |
| 66 |
// Label |
| 67 |
el( 'div', { className: 'bkbg-mc-label', style: labelSt }, m.label ), |
| 68 |
|
| 69 |
// Main value (after) |
| 70 |
el( 'div', { className: 'bkbg-mc-value', style: valueSt }, |
| 71 |
fmtValue( m.valueAfter, m.format, m.unit ) |
| 72 |
), |
| 73 |
|
| 74 |
// Change row |
| 75 |
a.showChange && el( 'div', { className: 'bkbg-mc-change', style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 13 } }, |
| 76 |
a.showArrows && el( 'span', { style: { color: chgColor, fontWeight: 700, fontSize: 15 } }, arrow ), |
| 77 |
el( 'span', { style: { color: chgColor, fontWeight: 600 } }, pct !== null ? Math.abs( pct ).toFixed( 1 ) + '%' : '—' ), |
| 78 |
el( 'span', { style: { color: a.labelColor, fontSize: 11 } }, `vs ${ a.beforeLabel }: ${ fmtValue( m.valueBefore, m.format, m.unit ) }` ), |
| 79 |
), |
| 80 |
|
| 81 |
// Mini bar |
| 82 |
a.showBar && el( 'div', { className: 'bkbg-mc-bars', style: { marginTop: 6, display: 'flex', flexDirection: 'column', gap: 3 } }, |
| 83 |
el( 'div', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 10, color: a.labelColor } }, |
| 84 |
el( 'span', { style: { width: 24, textAlign: 'right', flexShrink: 0 } }, a.beforeLabel ), |
| 85 |
el( 'div', { style: { flex: 1, height: 6, background: '#e5e7eb', borderRadius: 3, overflow: 'hidden' } }, |
| 86 |
el( 'div', { style: { width: ( bFrac * 100 ) + '%', height: '100%', background: a.neutralColor, borderRadius: 3 } } ) |
| 87 |
), |
| 88 |
), |
| 89 |
el( 'div', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 10, color: a.labelColor } }, |
| 90 |
el( 'span', { style: { width: 24, textAlign: 'right', flexShrink: 0 } }, a.afterLabel ), |
| 91 |
el( 'div', { style: { flex: 1, height: 6, background: '#e5e7eb', borderRadius: 3, overflow: 'hidden' } }, |
| 92 |
el( 'div', { style: { width: ( aFrac * 100 ) + '%', height: '100%', background: chgColor, borderRadius: 3 } } ) |
| 93 |
), |
| 94 |
), |
| 95 |
), |
| 96 |
); |
| 97 |
} |
| 98 |
|
| 99 |
// ── Grid renderer ───────────────────────────────────────────────────────── |
| 100 |
function renderGrid( a, opts ) { |
| 101 |
opts = opts || {}; |
| 102 |
const metrics = a.metrics || []; |
| 103 |
return el( 'div', { |
| 104 |
className: 'bkbg-mc-grid', |
| 105 |
style: { |
| 106 |
display: 'grid', |
| 107 |
gridTemplateColumns: `repeat(${ a.columns }, 1fr)`, |
| 108 |
gap: '16px', |
| 109 |
} |
| 110 |
}, ...metrics.map( ( m, i ) => el( MetricCard, { key: i, m, a, inlineTypo: opts.inlineTypo } ) ) ); |
| 111 |
} |
| 112 |
|
| 113 |
function updMetric( setAttributes, metrics, mi, field, val ) { |
| 114 |
setAttributes( { metrics: metrics.map( ( m, i ) => i === mi ? { ...m, [field]: val } : m ) } ); |
| 115 |
} |
| 116 |
|
| 117 |
registerBlockType( 'blockenberg/metric-comparison', { |
| 118 |
deprecated: [{ |
| 119 |
save: function ( { attributes: a } ) { |
| 120 |
var blockProps = useBlockProps.save( { className: 'bkbg-mc-wrap' } ); |
| 121 |
return el( 'div', blockProps, |
| 122 |
el( 'div', { className: 'bkbg-mc-inner', style: { background: a.bgColor, padding: '16px', borderRadius: 8, boxSizing: 'border-box' } }, |
| 123 |
a.showTitle && a.title ? el( 'h3', { className: 'bkbg-mc-title', style: { color: a.titleColor, margin: '0 0 16px', fontSize: (a.titleFontSize || 20) + 'px', fontWeight: (a.titleFontWeight || 700) } }, a.title ) : null, |
| 124 |
renderGrid( a, { inlineTypo: true } ), |
| 125 |
), |
| 126 |
); |
| 127 |
}, |
| 128 |
}], |
| 129 |
edit: function ( props ) { |
| 130 |
const { attributes: a, setAttributes } = props; |
| 131 |
var blockProps = useBlockProps((function () { |
| 132 |
var _tvf = getTypoCssVars(); |
| 133 |
var s = {}; |
| 134 |
Object.assign(s, _tvf(a.titleTypo, '--bkbg-mc-tt-')); |
| 135 |
Object.assign(s, _tvf(a.labelTypo, '--bkbg-mc-lb-')); |
| 136 |
Object.assign(s, _tvf(a.valueTypo, '--bkbg-mc-vl-')); |
| 137 |
return { className: 'bkbg-mc-wrap', style: s }; |
| 138 |
})()); |
| 139 |
|
| 140 |
return el( 'div', blockProps, |
| 141 |
el( InspectorControls, {}, |
| 142 |
|
| 143 |
el( PanelBody, { title: __( 'Settings', 'blockenberg' ), initialOpen: true }, |
| 144 |
el( TextControl, { label: __( 'Title', 'blockenberg' ), value: a.title, onChange: v => setAttributes( { title: v } ) } ), |
| 145 |
el( ToggleControl, { label: __( 'Show Title', 'blockenberg' ), checked: a.showTitle, onChange: v => setAttributes( { showTitle: v } ) } ), |
| 146 |
el( TextControl, { label: __( 'Before Label', 'blockenberg' ), value: a.beforeLabel, onChange: v => setAttributes( { beforeLabel: v } ) } ), |
| 147 |
el( TextControl, { label: __( 'After Label', 'blockenberg' ), value: a.afterLabel, onChange: v => setAttributes( { afterLabel: v } ) } ), |
| 148 |
el( RangeControl, { label: __( 'Columns', 'blockenberg' ), value: a.columns, onChange: v => setAttributes( { columns: v } ), min: 1, max: 6 } ), |
| 149 |
el( ToggleControl, { label: __( 'Show Change', 'blockenberg' ), checked: a.showChange, onChange: v => setAttributes( { showChange: v } ) } ), |
| 150 |
el( ToggleControl, { label: __( 'Show Arrows', 'blockenberg' ), checked: a.showArrows, onChange: v => setAttributes( { showArrows: v } ) } ), |
| 151 |
el( ToggleControl, { label: __( 'Show Bar', 'blockenberg' ), checked: a.showBar, onChange: v => setAttributes( { showBar: v } ) } ), |
| 152 |
), |
| 153 |
|
| 154 |
el( PanelBody, { title: __( 'Card Style', 'blockenberg' ), initialOpen: false }, |
| 155 |
el( RangeControl, { label: __( 'Border Radius', 'blockenberg' ), value: a.borderRadius, onChange: v => setAttributes( { borderRadius: v } ), min: 0, max: 30 } ), |
| 156 |
el( RangeControl, { label: __( 'Padding', 'blockenberg' ), value: a.cardPadding, onChange: v => setAttributes( { cardPadding: v } ), min: 8, max: 48 } ), |
| 157 |
), |
| 158 |
|
| 159 |
el( PanelColorSettings, { |
| 160 |
title: __( 'Colors', 'blockenberg' ), initialOpen: false, |
| 161 |
colorSettings: [ |
| 162 |
{ label: __( 'Wrapper BG', 'blockenberg' ), value: a.bgColor, onChange: v => setAttributes( { bgColor: v || '#ffffff' } ) }, |
| 163 |
{ label: __( 'Card BG', 'blockenberg' ), value: a.cardBg, onChange: v => setAttributes( { cardBg: v || '#f9fafb' } ) }, |
| 164 |
{ label: __( 'Positive', 'blockenberg' ), value: a.posColor, onChange: v => setAttributes( { posColor: v || '#10b981' } ) }, |
| 165 |
{ label: __( 'Negative', 'blockenberg' ), value: a.negColor, onChange: v => setAttributes( { negColor: v || '#ef4444' } ) }, |
| 166 |
{ label: __( 'Neutral', 'blockenberg' ), value: a.neutralColor, onChange: v => setAttributes( { neutralColor: v || '#6b7280' } ) }, |
| 167 |
{ label: __( 'Title', 'blockenberg' ), value: a.titleColor, onChange: v => setAttributes( { titleColor: v || '#111827' } ) }, |
| 168 |
{ label: __( 'Value Text', 'blockenberg' ), value: a.textColor, onChange: v => setAttributes( { textColor: v || '#111827' } ) }, |
| 169 |
{ label: __( 'Labels', 'blockenberg' ), value: a.labelColor, onChange: v => setAttributes( { labelColor: v || '#6b7280' } ) }, |
| 170 |
] |
| 171 |
} ), |
| 172 |
|
| 173 |
el( PanelBody, { title: __( 'Typography', 'blockenberg' ), initialOpen: false }, |
| 174 |
el( getTypoControl(), { label: __( 'Title', 'blockenberg' ), value: a.titleTypo, onChange: function (v) { setAttributes({ titleTypo: v }); } }), |
| 175 |
el( getTypoControl(), { label: __( 'Label', 'blockenberg' ), value: a.labelTypo, onChange: function (v) { setAttributes({ labelTypo: v }); } }), |
| 176 |
el( getTypoControl(), { label: __( 'Value', 'blockenberg' ), value: a.valueTypo, onChange: function (v) { setAttributes({ valueTypo: v }); } }) |
| 177 |
), |
| 178 |
|
| 179 |
el( PanelBody, { title: __( 'Metrics', 'blockenberg' ), initialOpen: false }, |
| 180 |
el( Button, { variant: 'secondary', style: { marginBottom: 10 }, onClick: () => setAttributes( { metrics: [ ...( a.metrics || [] ), { label: 'New Metric', valueBefore: 100, valueAfter: 120, unit: '', format: 'number', higherIsBetter: true } ] } ) }, __( '+ Add Metric', 'blockenberg' ) ), |
| 181 |
a.metrics.map( ( m, mi ) => |
| 182 |
el( PanelBody, { key: mi, title: m.label, initialOpen: false }, |
| 183 |
el( TextControl, { label: __( 'Label', 'blockenberg' ), value: m.label, onChange: v => updMetric( setAttributes, a.metrics, mi, 'label', v ) } ), |
| 184 |
el( TextControl, { label: `${ a.beforeLabel } value`, value: String( m.valueBefore ), type: 'number', onChange: v => updMetric( setAttributes, a.metrics, mi, 'valueBefore', parseFloat( v ) || 0 ) } ), |
| 185 |
el( TextControl, { label: `${ a.afterLabel } value`, value: String( m.valueAfter ), type: 'number', onChange: v => updMetric( setAttributes, a.metrics, mi, 'valueAfter', parseFloat( v ) || 0 ) } ), |
| 186 |
el( TextControl, { label: __( 'Unit', 'blockenberg' ), value: m.unit, onChange: v => updMetric( setAttributes, a.metrics, mi, 'unit', v ) } ), |
| 187 |
el( SelectControl, { |
| 188 |
label: __( 'Format', 'blockenberg' ), |
| 189 |
value: m.format, |
| 190 |
options: [ |
| 191 |
{ label: __( 'Number', 'blockenberg' ), value: 'number' }, |
| 192 |
{ label: __( 'Currency', 'blockenberg' ), value: 'currency' }, |
| 193 |
{ label: __( 'Percent', 'blockenberg' ), value: 'percent' }, |
| 194 |
], |
| 195 |
onChange: v => updMetric( setAttributes, a.metrics, mi, 'format', v ), |
| 196 |
} ), |
| 197 |
el( ToggleControl, { label: __( 'Higher is better', 'blockenberg' ), checked: m.higherIsBetter, onChange: v => updMetric( setAttributes, a.metrics, mi, 'higherIsBetter', v ) } ), |
| 198 |
el( Button, { isDestructive: true, isSmall: true, onClick: () => setAttributes( { metrics: a.metrics.filter( ( _, x ) => x !== mi ) } ) }, __( 'Remove', 'blockenberg' ) ), |
| 199 |
) |
| 200 |
), |
| 201 |
), |
| 202 |
), |
| 203 |
|
| 204 |
el( 'div', { className: 'bkbg-mc-inner', style: { background: a.bgColor, padding: '16px', borderRadius: 8, boxSizing: 'border-box' } }, |
| 205 |
a.showTitle && a.title && el( 'h3', { className: 'bkbg-mc-title', style: { color: a.titleColor, margin: '0 0 16px' } }, a.title ), |
| 206 |
renderGrid( a ), |
| 207 |
), |
| 208 |
); |
| 209 |
}, |
| 210 |
|
| 211 |
save: function ( { attributes: a } ) { |
| 212 |
var blockProps = useBlockProps.save((function () { |
| 213 |
var _tvf = getTypoCssVars(); |
| 214 |
var s = {}; |
| 215 |
Object.assign(s, _tvf(a.titleTypo, '--bkbg-mc-tt-')); |
| 216 |
Object.assign(s, _tvf(a.labelTypo, '--bkbg-mc-lb-')); |
| 217 |
Object.assign(s, _tvf(a.valueTypo, '--bkbg-mc-vl-')); |
| 218 |
return { className: 'bkbg-mc-wrap', style: s }; |
| 219 |
})()); |
| 220 |
return el( 'div', blockProps, |
| 221 |
el( 'div', { className: 'bkbg-mc-inner', style: { background: a.bgColor, padding: '16px', borderRadius: 8, boxSizing: 'border-box' } }, |
| 222 |
a.showTitle && a.title ? el( 'h3', { className: 'bkbg-mc-title', style: { color: a.titleColor, margin: '0 0 16px' } }, a.title ) : null, |
| 223 |
renderGrid( a ), |
| 224 |
), |
| 225 |
); |
| 226 |
}, |
| 227 |
} ); |
| 228 |
}() ); |
| 229 |
|