| 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 _TypographyControl, _typoCssVars; |
| 20 |
function getTypographyControl() { return _TypographyControl || (_TypographyControl = window.bkbgTypographyControl); } |
| 21 |
function getTypoCssVars() { return _typoCssVars || (_typoCssVars = window.bkbgTypoCssVars); } |
| 22 |
|
| 23 |
var LEGEND_POS_OPTIONS = [ |
| 24 |
{ label: __('Top', 'blockenberg'), value: 'top' }, |
| 25 |
{ label: __('Bottom', 'blockenberg'), value: 'bottom' }, |
| 26 |
{ label: __('Left', 'blockenberg'), value: 'left' }, |
| 27 |
{ label: __('Right', 'blockenberg'), value: 'right' }, |
| 28 |
]; |
| 29 |
|
| 30 |
function makeId() { return 'ac' + Math.random().toString(36).substr(2, 5); } |
| 31 |
|
| 32 |
function buildWrapStyle(a) { |
| 33 |
var _tv = getTypoCssVars(); |
| 34 |
var s = { |
| 35 |
'--bkbg-ac-bg': a.bgColor || '#ffffff', |
| 36 |
'--bkbg-ac-card-r': a.cardRadius + 'px', |
| 37 |
'--bkbg-ac-title-color': a.titleColor, |
| 38 |
'--bkbg-ac-title-sz': a.titleSize + 'px', |
| 39 |
'--bkbg-ac-subtitle-color':a.subtitleColor, |
| 40 |
paddingTop: a.paddingTop + 'px', |
| 41 |
paddingBottom: a.paddingBottom + 'px', |
| 42 |
backgroundColor: a.bgColor || undefined, |
| 43 |
}; |
| 44 |
Object.assign(s, _tv(a.titleTypo || {}, '--bkbg-ac-title-')); |
| 45 |
Object.assign(s, _tv(a.subtitleTypo || {}, '--bkbg-ac-subtitle-')); |
| 46 |
return s; |
| 47 |
} |
| 48 |
|
| 49 |
function renderColorControl(key, label, value, onChange, openKey, setOpenKey) { |
| 50 |
var isOpen = openKey === key; |
| 51 |
return el('div', { key: key, style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0', gap: '8px' } }, |
| 52 |
el('span', { style: { fontSize: '12px', color: '#1e1e1e', flex: 1, lineHeight: 1.4 } }, label), |
| 53 |
el('div', { style: { position: 'relative', flexShrink: 0 } }, |
| 54 |
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' } }), |
| 55 |
isOpen && el(Popover, { position: 'bottom left', onClose: function () { setOpenKey(null); } }, |
| 56 |
el('div', { style: { padding: '8px' } }, |
| 57 |
el(ColorPicker, { color: value, enableAlpha: true, onChange: onChange }) |
| 58 |
) |
| 59 |
) |
| 60 |
) |
| 61 |
); |
| 62 |
} |
| 63 |
|
| 64 |
/* ── Dataset editor row ─────────────────────────────────────────── */ |
| 65 |
function DatasetRow(props) { |
| 66 |
var ds = props.ds; |
| 67 |
var onUpdate = props.onUpdate; |
| 68 |
var onRemove = props.onRemove; |
| 69 |
var openKey = props.openKey; |
| 70 |
var setOpenKey = props.setOpenKey; |
| 71 |
|
| 72 |
return el('div', { style: { border: '1px solid #e0e0e0', borderRadius: '8px', padding: '10px', marginBottom: '10px', background: '#fafafa' } }, |
| 73 |
el(TextControl, { label: __('Label', 'blockenberg'), value: ds.label, onChange: function (v) { onUpdate('label', v); } }), |
| 74 |
el(TextControl, { label: __('Data (CSV)', 'blockenberg'), value: ds.data, onChange: function (v) { onUpdate('data', v); }, help: __('e.g. 40,55,72,65', 'blockenberg') }), |
| 75 |
renderColorControl('ds-' + ds.id + '-color', __('Color', 'blockenberg'), ds.color, function (v) { onUpdate('color', v); }, openKey, setOpenKey), |
| 76 |
el(ToggleControl, { label: __('Fill area', 'blockenberg'), checked: ds.fill, onChange: function (v) { onUpdate('fill', v); }, __nextHasNoMarginBottom: true }), |
| 77 |
el(RangeControl, { label: __('Line tension', 'blockenberg'), value: ds.tension, min: 0, max: 0.9, step: 0.05, onChange: function (v) { onUpdate('tension', v); } }), |
| 78 |
el(Button, { isDestructive: true, variant: 'tertiary', size: 'compact', onClick: onRemove, style: { marginTop: '6px' } }, __('Remove dataset', 'blockenberg')) |
| 79 |
); |
| 80 |
} |
| 81 |
|
| 82 |
registerBlockType('blockenberg/area-chart', { |
| 83 |
title: __('Area Chart', 'blockenberg'), |
| 84 |
icon: 'chart-area', |
| 85 |
category: 'bkbg-charts', |
| 86 |
|
| 87 |
edit: function (props) { |
| 88 |
var a = props.attributes; |
| 89 |
var setAttributes = props.setAttributes; |
| 90 |
|
| 91 |
var openColorKeyState = useState(null); |
| 92 |
var openColorKey = openColorKeyState[0]; |
| 93 |
var setOpenColorKey = openColorKeyState[1]; |
| 94 |
|
| 95 |
var blockProps = useBlockProps({ style: buildWrapStyle(a) }); |
| 96 |
|
| 97 |
function updateDataset(id, key, val) { |
| 98 |
setAttributes({ datasets: a.datasets.map(function (ds) { |
| 99 |
if (ds.id !== id) return ds; |
| 100 |
var u = Object.assign({}, ds); u[key] = val; return u; |
| 101 |
}) }); |
| 102 |
} |
| 103 |
|
| 104 |
function addDataset() { |
| 105 |
var idx = a.datasets.length + 1; |
| 106 |
setAttributes({ datasets: a.datasets.concat([{ |
| 107 |
id: makeId(), label: 'Series ' + idx, |
| 108 |
data: '0,0,0,0,0,0,0,0', color: '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6,'0'), |
| 109 |
fill: true, tension: 0.45 |
| 110 |
}]) }); |
| 111 |
} |
| 112 |
|
| 113 |
function removeDataset(id) { |
| 114 |
setAttributes({ datasets: a.datasets.filter(function (ds) { return ds.id !== id; }) }); |
| 115 |
} |
| 116 |
|
| 117 |
/* Sparkline SVG preview */ |
| 118 |
function buildSparklinePath(data, W, H, padX, padY) { |
| 119 |
var pts = data.split(',').map(function (v) { return parseFloat(v.trim()) || 0; }); |
| 120 |
if (pts.length < 2) return ''; |
| 121 |
var mn = Math.min.apply(null, pts); |
| 122 |
var mx = Math.max.apply(null, pts); |
| 123 |
var rng = mx - mn || 1; |
| 124 |
var xs = pts.map(function (_, i) { return padX + i * (W - 2*padX) / (pts.length - 1); }); |
| 125 |
var ys = pts.map(function (v) { return padY + (1 - (v-mn)/rng) * (H - 2*padY); }); |
| 126 |
var d = 'M ' + xs[0] + ' ' + ys[0]; |
| 127 |
for (var i = 1; i < xs.length; i++) { |
| 128 |
var cpx = (xs[i-1] + xs[i]) / 2; |
| 129 |
d += ' C ' + cpx + ' ' + ys[i-1] + ' ' + cpx + ' ' + ys[i] + ' ' + xs[i] + ' ' + ys[i]; |
| 130 |
} |
| 131 |
return d; |
| 132 |
} |
| 133 |
|
| 134 |
var svgW = 560; var svgH = a.height; |
| 135 |
var padX = 32; var padY = 24; |
| 136 |
|
| 137 |
return el(Fragment, null, |
| 138 |
el(InspectorControls, null, |
| 139 |
el(PanelBody, { title: __('Data', 'blockenberg'), initialOpen: true }, |
| 140 |
el(TextControl, { label: __('X-Axis Labels (CSV)', 'blockenberg'), value: a.labels, onChange: function (v) { setAttributes({ labels: v }); }, help: __('e.g. Jan,Feb,Mar,Apr', 'blockenberg') }), |
| 141 |
a.datasets.map(function (ds) { |
| 142 |
return el(DatasetRow, { |
| 143 |
key: ds.id, ds: ds, openKey: openColorKey, setOpenKey: setOpenColorKey, |
| 144 |
onUpdate: function (k, v) { updateDataset(ds.id, k, v); }, |
| 145 |
onRemove: function () { removeDataset(ds.id); }, |
| 146 |
}); |
| 147 |
}), |
| 148 |
el(Button, { variant: 'secondary', onClick: addDataset, style: { width: '100%', justifyContent: 'center', marginTop: '4px' } }, '+ ' + __('Add Dataset', 'blockenberg')) |
| 149 |
), |
| 150 |
el(PanelBody, { title: __('Chart Options', 'blockenberg'), initialOpen: false }, |
| 151 |
el(RangeControl, { label: __('Height (px)', 'blockenberg'), value: a.height, min: 160, max: 700, onChange: function (v) { setAttributes({ height: v }); } }), |
| 152 |
el(ToggleControl, { label: __('Gradient fill', 'blockenberg'), checked: a.gradientFill, onChange: function (v) { setAttributes({ gradientFill: v }); }, __nextHasNoMarginBottom: true }), |
| 153 |
a.gradientFill && el(RangeControl, { label: __('Gradient alpha %', 'blockenberg'), value: a.gradientAlpha, min: 5, max: 60, onChange: function (v) { setAttributes({ gradientAlpha: v }); } }), |
| 154 |
el(ToggleControl, { label: __('Show grid', 'blockenberg'), checked: a.showGrid, onChange: function (v) { setAttributes({ showGrid: v }); }, __nextHasNoMarginBottom: true }), |
| 155 |
el(ToggleControl, { label: __('Show legend', 'blockenberg'), checked: a.showLegend, onChange: function (v) { setAttributes({ showLegend: v }); }, __nextHasNoMarginBottom: true }), |
| 156 |
a.showLegend && el(SelectControl, { label: __('Legend position', 'blockenberg'), value: a.legendPos, options: LEGEND_POS_OPTIONS, onChange: function (v) { setAttributes({ legendPos: v }); } }), |
| 157 |
el(ToggleControl, { label: __('Show data points', 'blockenberg'), checked: a.showPoints, onChange: function (v) { setAttributes({ showPoints: v }); }, __nextHasNoMarginBottom: true }), |
| 158 |
a.showPoints && el(RangeControl, { label: __('Point size (px)', 'blockenberg'), value: a.pointSize, min: 2, max: 10, onChange: function (v) { setAttributes({ pointSize: v }); } }), |
| 159 |
el(RangeControl, { label: __('Line width (px)', 'blockenberg'), value: a.lineWidth, min: 1, max: 6, onChange: function (v) { setAttributes({ lineWidth: v }); } }), |
| 160 |
el(ToggleControl, { label: __('Animate on load', 'blockenberg'), checked: a.animate, onChange: function (v) { setAttributes({ animate: v }); }, __nextHasNoMarginBottom: true }), |
| 161 |
a.animate && el(RangeControl, { label: __('Animation duration (ms)', 'blockenberg'), value: a.animDuration, min: 200, max: 2000, step: 100, onChange: function (v) { setAttributes({ animDuration: v }); } }) |
| 162 |
), |
| 163 |
el(PanelBody, { title: __('Axes', 'blockenberg'), initialOpen: false }, |
| 164 |
el(TextControl, { label: __('X Axis label', 'blockenberg'), value: a.xLabel, onChange: function (v) { setAttributes({ xLabel: v }); } }), |
| 165 |
el(TextControl, { label: __('Y Axis label', 'blockenberg'), value: a.yLabel, onChange: function (v) { setAttributes({ yLabel: v }); } }), |
| 166 |
el(TextControl, { label: __('Y Min (leave blank for auto)', 'blockenberg'), value: a.yMin, onChange: function (v) { setAttributes({ yMin: v }); } }), |
| 167 |
el(TextControl, { label: __('Y Max (leave blank for auto)', 'blockenberg'), value: a.yMax, onChange: function (v) { setAttributes({ yMax: v }); } }) |
| 168 |
), |
| 169 |
el(PanelBody, { title: __('Card Style', 'blockenberg'), initialOpen: false }, |
| 170 |
renderColorControl('bgColor', __('Card background', 'blockenberg'), a.bgColor, function (v) { setAttributes({ bgColor: v }); }, openColorKey, setOpenColorKey), |
| 171 |
el(RangeControl, { label: __('Card radius (px)', 'blockenberg'), value: a.cardRadius, min: 0, max: 32, onChange: function (v) { setAttributes({ cardRadius: v }); } }) |
| 172 |
), |
| 173 |
el(PanelBody, { title: __('Typography', 'blockenberg'), initialOpen: false }, |
| 174 |
el(getTypographyControl(), { label: __('Title Typography', 'blockenberg'), value: a.titleTypo || {}, onChange: function (v) { setAttributes({ titleTypo: v }); } }), |
| 175 |
el(getTypographyControl(), { label: __('Subtitle Typography', 'blockenberg'), value: a.subtitleTypo || {}, onChange: function (v) { setAttributes({ subtitleTypo: v }); } }) |
| 176 |
), |
| 177 |
el(PanelBody, { title: __('Colors', 'blockenberg'), initialOpen: false }, |
| 178 |
renderColorControl('titleColor', __('Title color', 'blockenberg'), a.titleColor, function (v) { setAttributes({ titleColor: v }); }, openColorKey, setOpenColorKey), |
| 179 |
renderColorControl('subtitleColor', __('Subtitle color', 'blockenberg'), a.subtitleColor, function (v) { setAttributes({ subtitleColor: v }); }, openColorKey, setOpenColorKey) |
| 180 |
), |
| 181 |
el(PanelBody, { title: __('Spacing', 'blockenberg'), initialOpen: false }, |
| 182 |
el(RangeControl, { label: __('Padding top (px)', 'blockenberg'), value: a.paddingTop, min: 0, max: 200, onChange: function (v) { setAttributes({ paddingTop: v }); } }), |
| 183 |
el(RangeControl, { label: __('Padding bottom (px)', 'blockenberg'), value: a.paddingBottom, min: 0, max: 200, onChange: function (v) { setAttributes({ paddingBottom: v }); } }) |
| 184 |
) |
| 185 |
), |
| 186 |
|
| 187 |
el('div', blockProps, |
| 188 |
el('div', { className: 'bkbg-ac-card', style: { background: a.bgColor || '#fff', borderRadius: a.cardRadius + 'px', padding: '24px', boxSizing: 'border-box' } }, |
| 189 |
(a.title || a.subtitle) && el('div', { className: 'bkbg-ac-header', style: { marginBottom: '16px' } }, |
| 190 |
el(RichText, { tagName: 'h3', className: 'bkbg-ac-title', value: a.title, onChange: function (v) { setAttributes({ title: v }); }, placeholder: __('Chart title…', 'blockenberg'), style: { color: a.titleColor, margin: '0 0 4px' } }), |
| 191 |
el(RichText, { tagName: 'p', className: 'bkbg-ac-subtitle', value: a.subtitle, onChange: function (v) { setAttributes({ subtitle: v }); }, placeholder: __('Optional subtitle…', 'blockenberg'), style: { color: a.subtitleColor, margin: 0 } }) |
| 192 |
), |
| 193 |
/* SVG sparkline preview */ |
| 194 |
el('div', { className: 'bkbg-ac-preview', style: { position: 'relative', height: a.height + 'px', background: '#f9fafb', borderRadius: '8px', overflow: 'hidden' } }, |
| 195 |
el('svg', { viewBox: '0 0 ' + svgW + ' ' + svgH, preserveAspectRatio: 'none', style: { position: 'absolute', inset: 0, width: '100%', height: '100%' } }, |
| 196 |
a.datasets.map(function (ds) { |
| 197 |
var path = buildSparklinePath(ds.data, svgW, svgH, padX, padY); |
| 198 |
if (!path) return null; |
| 199 |
return el(Fragment, { key: ds.id }, |
| 200 |
ds.fill && el('path', { d: path + ' L ' + (svgW-padX) + ' ' + (svgH-padY) + ' L ' + padX + ' ' + (svgH-padY) + ' Z', fill: ds.color, opacity: 0.15 }), |
| 201 |
el('path', { d: path, fill: 'none', stroke: ds.color, strokeWidth: a.lineWidth, strokeLinejoin: 'round', strokeLinecap: 'round' }) |
| 202 |
); |
| 203 |
}) |
| 204 |
), |
| 205 |
el('div', { style: { position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '12px', pointerEvents: 'none' } }, |
| 206 |
a.datasets.map(function (ds) { |
| 207 |
return el('div', { key: ds.id, style: { display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px', color: '#374151', background: 'rgba(255,255,255,0.85)', padding: '4px 10px', borderRadius: '99px' } }, |
| 208 |
el('span', { style: { width: '10px', height: '10px', borderRadius: '50%', background: ds.color, flexShrink: 0 } }), |
| 209 |
ds.label |
| 210 |
); |
| 211 |
}) |
| 212 |
) |
| 213 |
) |
| 214 |
) |
| 215 |
) |
| 216 |
); |
| 217 |
}, |
| 218 |
|
| 219 |
save: function (props) { |
| 220 |
var a = props.attributes; |
| 221 |
return el('div', wp.blockEditor.useBlockProps.save({ className: 'bkbg-ac-wrapper', style: buildWrapStyle(a) }), |
| 222 |
el('div', { |
| 223 |
className: 'bkbg-ac-card', |
| 224 |
'data-type': 'area', |
| 225 |
'data-labels': a.labels, |
| 226 |
'data-datasets': JSON.stringify(a.datasets), |
| 227 |
'data-height': a.height, |
| 228 |
'data-gradient': a.gradientFill ? 'true' : 'false', |
| 229 |
'data-gradient-alpha': a.gradientAlpha, |
| 230 |
'data-grid': a.showGrid ? '1' : '0', |
| 231 |
'data-legend': a.showLegend ? '1' : '0', |
| 232 |
'data-legend-pos': a.legendPos, |
| 233 |
'data-points': a.showPoints ? '1' : '0', |
| 234 |
'data-point-size': a.pointSize, |
| 235 |
'data-line-width': a.lineWidth, |
| 236 |
'data-animate': a.animate ? '1' : '0', |
| 237 |
'data-anim-duration': a.animDuration, |
| 238 |
'data-y-min': a.yMin, |
| 239 |
'data-y-max': a.yMax, |
| 240 |
'data-x-label': a.xLabel, |
| 241 |
'data-y-label': a.yLabel, |
| 242 |
style: { background: a.bgColor || '#fff', borderRadius: a.cardRadius + 'px', padding: '24px', boxSizing: 'border-box' }, |
| 243 |
}, |
| 244 |
(a.title || a.subtitle) && el('div', { className: 'bkbg-ac-header' }, |
| 245 |
a.title && el(RichText.Content, { tagName: 'h3', className: 'bkbg-ac-title', value: a.title }), |
| 246 |
a.subtitle && el(RichText.Content, { tagName: 'p', className: 'bkbg-ac-subtitle', value: a.subtitle }) |
| 247 |
), |
| 248 |
el('div', { className: 'bkbg-ac-chart-wrap' }, |
| 249 |
el('canvas', { className: 'bkbg-ac-canvas', style: { height: a.height + 'px' } }) |
| 250 |
) |
| 251 |
) |
| 252 |
); |
| 253 |
} |
| 254 |
}); |
| 255 |
}() ); |
| 256 |
|