| 1 |
( function () { |
| 2 |
var el = wp.element.createElement; |
| 3 |
var useState = wp.element.useState; |
| 4 |
var Fragment = wp.element.Fragment; |
| 5 |
var registerBlockType = wp.blocks.registerBlockType; |
| 6 |
var __ = wp.i18n.__; |
| 7 |
var InspectorControls = wp.blockEditor.InspectorControls; |
| 8 |
var PanelColorSettings = wp.blockEditor.PanelColorSettings; |
| 9 |
var useBlockProps = wp.blockEditor.useBlockProps; |
| 10 |
var PanelBody = wp.components.PanelBody; |
| 11 |
var RangeControl = wp.components.RangeControl; |
| 12 |
var SelectControl = wp.components.SelectControl; |
| 13 |
var TextControl = wp.components.TextControl; |
| 14 |
var ToggleControl = wp.components.ToggleControl; |
| 15 |
var Button = wp.components.Button; |
| 16 |
|
| 17 |
/* ── Typography helpers (lazy) ───────────────────────────────── */ |
| 18 |
var _tc, _tvf; |
| 19 |
Object.defineProperty(window, '__bkwc_tc', { get: function () { return _tc || (_tc = window.bkbgTypographyControl); } }); |
| 20 |
Object.defineProperty(window, '__bkwc_tvf', { get: function () { return _tvf || (_tvf = window.bkbgTypoCssVars); } }); |
| 21 |
function getTypoControl(label, typoObj, setAttributes, attrName) { |
| 22 |
var fn = window.__bkwc_tc; |
| 23 |
return fn ? fn({ label: label, value: typoObj || {}, onChange: function (v) { var o = {}; o[attrName] = v; setAttributes(o); } }) : null; |
| 24 |
} |
| 25 |
function getTypoCssVars(a) { |
| 26 |
var fn = window.__bkwc_tvf; |
| 27 |
var s = {}; |
| 28 |
if (fn) { |
| 29 |
Object.assign(s, fn(a.titleTypo || {}, '--bkwc-tt-')); |
| 30 |
Object.assign(s, fn(a.labelTypo || {}, '--bkwc-lb-')); |
| 31 |
Object.assign(s, fn(a.timeTypo || {}, '--bkwc-tm-')); |
| 32 |
} |
| 33 |
return s; |
| 34 |
} |
| 35 |
|
| 36 |
/* ── Common TZ list used for autocomplete hint ───── */ |
| 37 |
var COMMON_TZ = [ |
| 38 |
'America/New_York','America/Chicago','America/Denver','America/Los_Angeles', |
| 39 |
'America/Sao_Paulo','America/Mexico_City','America/Toronto', |
| 40 |
'Europe/London','Europe/Paris','Europe/Berlin','Europe/Moscow','Europe/Istanbul', |
| 41 |
'Asia/Dubai','Asia/Kolkata','Asia/Shanghai','Asia/Tokyo','Asia/Singapore','Asia/Seoul', |
| 42 |
'Africa/Cairo','Africa/Johannesburg','Australia/Sydney','Pacific/Auckland' |
| 43 |
]; |
| 44 |
|
| 45 |
/* ── helpers ──────────────────────────────────────── */ |
| 46 |
function getTimeParts(tz, fmt, showSec) { |
| 47 |
try { |
| 48 |
var now = new Date(); |
| 49 |
var opts12 = { timeZone: tz, hour: 'numeric', minute: '2-digit', second: showSec ? '2-digit' : undefined, hour12: fmt === '12h' }; |
| 50 |
var optsDate = { timeZone: tz, month: 'short', day: 'numeric', weekday: 'short' }; |
| 51 |
return { |
| 52 |
time: new Intl.DateTimeFormat('en-US', opts12).format(now), |
| 53 |
date: new Intl.DateTimeFormat('en-US', optsDate).format(now) |
| 54 |
}; |
| 55 |
} catch (e) { |
| 56 |
return { time: '--:--', date: '' }; |
| 57 |
} |
| 58 |
} |
| 59 |
|
| 60 |
function getAnglesParts(tz) { |
| 61 |
try { |
| 62 |
var now = new Date(); |
| 63 |
var parts = new Intl.DateTimeFormat('en-US', { |
| 64 |
timeZone: tz, hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false |
| 65 |
}).format(now).split(':').map(Number); |
| 66 |
var h = parts[0] % 12, m = parts[1], s = parts[2]; |
| 67 |
return { |
| 68 |
hourAngle: (h * 30) + (m * 0.5), |
| 69 |
minAngle: m * 6 + s * 0.1, |
| 70 |
secAngle: s * 6 |
| 71 |
}; |
| 72 |
} catch (e) { |
| 73 |
return { hourAngle: 0, minAngle: 0, secAngle: 0 }; |
| 74 |
} |
| 75 |
} |
| 76 |
|
| 77 |
/* ── Analog clock face SVG ──────────────────────── */ |
| 78 |
function AnalogClock(props) { |
| 79 |
var a = props, size = a.size || 120, r = size / 2; |
| 80 |
var ang = getAnglesParts(a.tz); |
| 81 |
// tick marks |
| 82 |
var ticks = []; |
| 83 |
for (var i = 0; i < 12; i++) { |
| 84 |
var rad = (i * 30 - 90) * Math.PI / 180; |
| 85 |
var rIn = r * 0.82, rOut = r * 0.92; |
| 86 |
ticks.push(el('line', { |
| 87 |
key: i, |
| 88 |
x1: r + rIn * Math.cos(rad), y1: r + rIn * Math.sin(rad), |
| 89 |
x2: r + rOut * Math.cos(rad), y2: r + rOut * Math.sin(rad), |
| 90 |
stroke: a.accentColor, strokeWidth: 1.5 |
| 91 |
})); |
| 92 |
} |
| 93 |
function hand(angle, length, color, width) { |
| 94 |
var rad = (angle - 90) * Math.PI / 180; |
| 95 |
return el('line', { |
| 96 |
x1: r, y1: r, |
| 97 |
x2: r + length * Math.cos(rad), |
| 98 |
y2: r + length * Math.sin(rad), |
| 99 |
stroke: color, strokeWidth: width, strokeLinecap: 'round' |
| 100 |
}); |
| 101 |
} |
| 102 |
return el('svg', { width: size, height: size, viewBox: '0 0 ' + size + ' ' + size }, |
| 103 |
el('circle', { cx: r, cy: r, r: r - 2, fill: a.clockFace, stroke: a.accentColor, strokeWidth: 2 }), |
| 104 |
ticks, |
| 105 |
hand(ang.hourAngle, r * 0.5, a.handHour, 3), |
| 106 |
hand(ang.minAngle, r * 0.7, a.handMin, 2), |
| 107 |
a.showSeconds !== false ? hand(ang.secAngle, r * 0.75, a.handSec, 1) : null, |
| 108 |
el('circle', { cx: r, cy: r, r: 3, fill: a.handSec }) |
| 109 |
); |
| 110 |
} |
| 111 |
|
| 112 |
/* ── Single clock card ──────────────────────────── */ |
| 113 |
function ClockCard(props) { |
| 114 |
var a = props.attrs, zone = props.zone; |
| 115 |
var parts = getTimeParts(zone.tz, a.timeFormat, a.showSeconds); |
| 116 |
var cardStyle = { |
| 117 |
background: a.cardBg, |
| 118 |
border: '1px solid ' + a.borderColor, |
| 119 |
borderRadius: a.cardRadius + 'px', |
| 120 |
padding: a.cardPadding + 'px', |
| 121 |
display: 'flex', |
| 122 |
flexDirection: 'column', |
| 123 |
alignItems: 'center', |
| 124 |
gap: 8 |
| 125 |
}; |
| 126 |
return el('div', { className: 'bkbg-wc-card', style: cardStyle }, |
| 127 |
a.clockStyle === 'analog' ? el(AnalogClock, { |
| 128 |
tz: zone.tz, size: a.analogSize, |
| 129 |
clockFace: a.clockFace, accentColor: a.accentColor, |
| 130 |
handHour: a.handHour, handMin: a.handMin, handSec: a.handSec, |
| 131 |
showSeconds: a.showSeconds |
| 132 |
}) : null, |
| 133 |
el('div', { className: 'bkbg-wc-label', style: { color: a.labelColor, textAlign: 'center' } }, zone.label), |
| 134 |
a.clockStyle === 'digital' ? el('div', { className: 'bkbg-wc-time', style: { color: a.timeColor } }, parts.time) : null, |
| 135 |
a.showDate ? el('div', { className: 'bkbg-wc-date', style: { color: a.dateColor } }, parts.date) : null |
| 136 |
); |
| 137 |
} |
| 138 |
|
| 139 |
/* ── Editor preview ───────────────────────────────── */ |
| 140 |
function WorldClockPreview(props) { |
| 141 |
var a = props.attrs; |
| 142 |
var gridStyle = { |
| 143 |
display: 'grid', |
| 144 |
gridTemplateColumns: 'repeat(' + a.columns + ', 1fr)', |
| 145 |
gap: a.gap + 'px', |
| 146 |
maxWidth: a.maxWidth + 'px', |
| 147 |
margin: '0 auto' |
| 148 |
}; |
| 149 |
return el('div', { className: 'bkbg-wc-preview', style: { paddingTop: a.paddingTop + 'px', paddingBottom: a.paddingBottom + 'px', background: a.sectionBg || undefined } }, |
| 150 |
(a.showTitle || a.showSubtitle) ? el('div', { style: { textAlign: 'center', marginBottom: 32, maxWidth: a.maxWidth + 'px', margin: '0 auto 32px' } }, |
| 151 |
a.showTitle ? el('h3', { className: 'bkbg-wc-title', style: { color: a.titleColor, margin: '0 0 8px' } }, a.title) : null, |
| 152 |
a.showSubtitle ? el('p', { className: 'bkbg-wc-subtitle', style: { color: a.subtitleColor, margin: 0 } }, a.subtitle) : null |
| 153 |
) : null, |
| 154 |
el('div', { style: gridStyle }, |
| 155 |
a.zones.map(function (zone, i) { |
| 156 |
return el(ClockCard, { key: i, zone: zone, attrs: a }); |
| 157 |
}) |
| 158 |
) |
| 159 |
); |
| 160 |
} |
| 161 |
|
| 162 |
/* ── Edit component ───────────────────────────────── */ |
| 163 |
function WorldClockEdit(props) { |
| 164 |
var a = props.attributes; |
| 165 |
var set = props.setAttributes; |
| 166 |
var blockProps = useBlockProps((function () { |
| 167 |
var s = getTypoCssVars(a); |
| 168 |
return { className: 'bkbg-wc-editor', style: s }; |
| 169 |
})()); |
| 170 |
|
| 171 |
function s(key) { return function (v) { var o = {}; o[key] = v; set(o); }; } |
| 172 |
function n(key) { return function (v) { var o = {}; o[key] = Number(v) || 0; set(o); }; } |
| 173 |
function t(key) { return function (v) { var o = {}; o[key] = v; set(o); }; } |
| 174 |
|
| 175 |
function updateZone(i, field, val) { |
| 176 |
var z = a.zones.slice(); |
| 177 |
z[i] = Object.assign({}, z[i]); |
| 178 |
z[i][field] = val; |
| 179 |
set({ zones: z }); |
| 180 |
} |
| 181 |
|
| 182 |
function addZone() { |
| 183 |
set({ zones: a.zones.concat([{ label: 'City', tz: 'UTC' }]) }); |
| 184 |
} |
| 185 |
|
| 186 |
function removeZone(i) { |
| 187 |
if (a.zones.length <= 1) return; |
| 188 |
var z = a.zones.filter(function (_, idx) { return idx !== i; }); |
| 189 |
set({ zones: z }); |
| 190 |
} |
| 191 |
|
| 192 |
return el(Fragment, null, |
| 193 |
el(InspectorControls, null, |
| 194 |
|
| 195 |
/* Title */ |
| 196 |
el(PanelBody, { title: __('Header', 'blockenberg'), initialOpen: true }, |
| 197 |
el(ToggleControl, { label: __('Show Title', 'blockenberg'), checked: a.showTitle, onChange: t('showTitle'), __nextHasNoMarginBottom: true }), |
| 198 |
a.showTitle ? el(TextControl, { label: __('Title', 'blockenberg'), value: a.title, onChange: s('title') }) : null, |
| 199 |
el(ToggleControl, { label: __('Show Subtitle', 'blockenberg'), checked: a.showSubtitle, onChange: t('showSubtitle'), __nextHasNoMarginBottom: true }), |
| 200 |
a.showSubtitle ? el(TextControl, { label: __('Subtitle', 'blockenberg'), value: a.subtitle, onChange: s('subtitle') }) : null |
| 201 |
), |
| 202 |
|
| 203 |
/* Zones */ |
| 204 |
el(PanelBody, { title: __('Timezone Zones', 'blockenberg'), initialOpen: true }, |
| 205 |
a.zones.map(function (zone, i) { |
| 206 |
return el('div', { key: i, style: { marginBottom: 16, background: '#f9f9f9', borderRadius: 8, padding: '8px 12px' } }, |
| 207 |
el('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 } }, |
| 208 |
el('strong', null, 'Zone ' + (i + 1)), |
| 209 |
el(Button, { isDestructive: true, isSmall: true, onClick: function () { removeZone(i); } }, '✕') |
| 210 |
), |
| 211 |
el(TextControl, { label: __('Label', 'blockenberg'), value: zone.label, onChange: function (v) { updateZone(i, 'label', v); } }), |
| 212 |
el(TextControl, { label: __('IANA Timezone', 'blockenberg'), value: zone.tz, onChange: function (v) { updateZone(i, 'tz', v); }, help: 'e.g. America/New_York, Europe/London' }) |
| 213 |
); |
| 214 |
}), |
| 215 |
el(Button, { isPrimary: true, onClick: addZone, style: { marginTop: 8 } }, __('+ Add Zone', 'blockenberg')) |
| 216 |
), |
| 217 |
|
| 218 |
/* Clock Settings */ |
| 219 |
el(PanelBody, { title: __('Clock Settings', 'blockenberg'), initialOpen: false }, |
| 220 |
el(SelectControl, { label: __('Clock Style', 'blockenberg'), value: a.clockStyle, options: [{ label: 'Digital', value: 'digital' }, { label: 'Analog', value: 'analog' }], onChange: s('clockStyle') }), |
| 221 |
el(SelectControl, { label: __('Time Format', 'blockenberg'), value: a.timeFormat, options: [{ label: '12-hour (AM/PM)', value: '12h' }, { label: '24-hour', value: '24h' }], onChange: s('timeFormat') }), |
| 222 |
el(ToggleControl, { label: __('Show Date', 'blockenberg'), checked: a.showDate, onChange: t('showDate'), __nextHasNoMarginBottom: true }), |
| 223 |
el(ToggleControl, { label: __('Show Seconds', 'blockenberg'), checked: a.showSeconds, onChange: t('showSeconds'), __nextHasNoMarginBottom: true }) |
| 224 |
), |
| 225 |
|
| 226 |
/* Colors */ |
| 227 |
|
| 228 |
el( PanelBody, { title: __( 'Typography', 'blockenberg' ), initialOpen: false }, |
| 229 |
getTypoControl( 'Title', a.titleTypo, set, 'titleTypo' ), |
| 230 |
getTypoControl( 'Label', a.labelTypo, set, 'labelTypo' ), |
| 231 |
getTypoControl( 'Time', a.timeTypo, set, 'timeTypo' ) |
| 232 |
), |
| 233 |
el(PanelColorSettings, { |
| 234 |
title: __('Colors', 'blockenberg'), |
| 235 |
initialOpen: false, |
| 236 |
colorSettings: [ |
| 237 |
{ value: a.accentColor, onChange: s('accentColor'), label: __('Accent', 'blockenberg') }, |
| 238 |
{ value: a.cardBg, onChange: s('cardBg'), label: __('Card Background', 'blockenberg') }, |
| 239 |
{ value: a.borderColor, onChange: s('borderColor'), label: __('Card Border', 'blockenberg') }, |
| 240 |
{ value: a.timeColor, onChange: s('timeColor'), label: __('Time Color', 'blockenberg') }, |
| 241 |
{ value: a.labelColor, onChange: s('labelColor'), label: __('City Label', 'blockenberg') }, |
| 242 |
{ value: a.dateColor, onChange: s('dateColor'), label: __('Date Color', 'blockenberg') }, |
| 243 |
{ value: a.clockFace, onChange: s('clockFace'), label: __('Clock Face', 'blockenberg') }, |
| 244 |
{ value: a.handHour, onChange: s('handHour'), label: __('Hour Hand', 'blockenberg') }, |
| 245 |
{ value: a.handMin, onChange: s('handMin'), label: __('Minute Hand', 'blockenberg') }, |
| 246 |
{ value: a.handSec, onChange: s('handSec'), label: __('Second Hand', 'blockenberg') }, |
| 247 |
{ value: a.titleColor, onChange: s('titleColor'), label: __('Title Color', 'blockenberg') }, |
| 248 |
{ value: a.subtitleColor, onChange: s('subtitleColor'), label: __('Subtitle Color', 'blockenberg') }, |
| 249 |
{ value: a.sectionBg, onChange: s('sectionBg'), label: __('Section Background', 'blockenberg') } |
| 250 |
] |
| 251 |
}), |
| 252 |
|
| 253 |
/* Layout & Sizing */ |
| 254 |
el(PanelBody, { title: __('Layout & Sizing', 'blockenberg'), initialOpen: false }, |
| 255 |
el(RangeControl, { label: __('Columns', 'blockenberg'), value: a.columns, onChange: n('columns'), min: 1, max: 6 }), |
| 256 |
el(RangeControl, { label: __('Gap (px)', 'blockenberg'), value: a.gap, onChange: n('gap'), min: 4, max: 48 }), |
| 257 |
el(RangeControl, { label: __('Card Radius (px)', 'blockenberg'), value: a.cardRadius, onChange: n('cardRadius'), min: 0, max: 32 }), |
| 258 |
el(RangeControl, { label: __('Card Padding (px)', 'blockenberg'), value: a.cardPadding, onChange: n('cardPadding'), min: 8, max: 64 }), |
| 259 |
el(RangeControl, { label: __('Time Size (px)', 'blockenberg'), value: a.timeSize, onChange: n('timeSize'), min: 16, max: 64 }), |
| 260 |
a.clockStyle === 'analog' ? el(RangeControl, { label: __('Analog Clock Size (px)', 'blockenberg'), value: a.analogSize, onChange: n('analogSize'), min: 60, max: 240 }) : null, |
| 261 |
el(RangeControl, { label: __('Max Width (px)', 'blockenberg'), value: a.maxWidth, onChange: n('maxWidth'), min: 400, max: 1400, step: 20 }), |
| 262 |
el(RangeControl, { label: __('Padding Top (px)', 'blockenberg'), value: a.paddingTop, onChange: n('paddingTop'), min: 0, max: 160 }), |
| 263 |
el(RangeControl, { label: __('Padding Bottom (px)', 'blockenberg'), value: a.paddingBottom, onChange: n('paddingBottom'), min: 0, max: 160 }) |
| 264 |
) |
| 265 |
), |
| 266 |
|
| 267 |
el('div', blockProps, |
| 268 |
el(WorldClockPreview, { attrs: a }) |
| 269 |
) |
| 270 |
); |
| 271 |
} |
| 272 |
|
| 273 |
registerBlockType('blockenberg/world-clock', { |
| 274 |
edit: WorldClockEdit, |
| 275 |
save: function (props) { |
| 276 |
var a = props.attributes; |
| 277 |
return el('div', wp.blockEditor.useBlockProps.save({ |
| 278 |
className: 'bkbg-wc-app', |
| 279 |
style: getTypoCssVars(a), |
| 280 |
'data-opts': JSON.stringify(a) |
| 281 |
})); |
| 282 |
} |
| 283 |
}); |
| 284 |
}() ); |
| 285 |
|