| 1 |
( function () { |
| 2 |
var el = wp.element.createElement; |
| 3 |
var __ = wp.i18n.__; |
| 4 |
var registerBlockType = wp.blocks.registerBlockType; |
| 5 |
var useBlockProps = wp.blockEditor.useBlockProps; |
| 6 |
var InspectorControls = wp.blockEditor.InspectorControls; |
| 7 |
var PanelColorSettings = wp.blockEditor.PanelColorSettings; |
| 8 |
var PanelBody = wp.components.PanelBody; |
| 9 |
var TextControl = wp.components.TextControl; |
| 10 |
var TextareaControl = wp.components.TextareaControl; |
| 11 |
var ToggleControl = wp.components.ToggleControl; |
| 12 |
var RangeControl = wp.components.RangeControl; |
| 13 |
var Button = wp.components.Button; |
| 14 |
var useState = wp.element.useState; |
| 15 |
|
| 16 |
function getTypographyControl() { |
| 17 |
return (window.bkbgTypographyControl || function () { return null; }); |
| 18 |
} |
| 19 |
|
| 20 |
function _tv(typo, prefix) { |
| 21 |
if (!typo) return {}; |
| 22 |
var s = {}; |
| 23 |
if (typo.family) s[prefix + 'font-family'] = "'" + typo.family + "', sans-serif"; |
| 24 |
if (typo.weight) s[prefix + 'font-weight'] = typo.weight; |
| 25 |
if (typo.transform) s[prefix + 'text-transform'] = typo.transform; |
| 26 |
if (typo.style) s[prefix + 'font-style'] = typo.style; |
| 27 |
if (typo.decoration) s[prefix + 'text-decoration'] = typo.decoration; |
| 28 |
var su = typo.sizeUnit || 'px'; |
| 29 |
if (typo.sizeDesktop !== '' && typo.sizeDesktop != null) s[prefix + 'font-size-d'] = typo.sizeDesktop + su; |
| 30 |
if (typo.sizeTablet !== '' && typo.sizeTablet != null) s[prefix + 'font-size-t'] = typo.sizeTablet + su; |
| 31 |
if (typo.sizeMobile !== '' && typo.sizeMobile != null) s[prefix + 'font-size-m'] = typo.sizeMobile + su; |
| 32 |
var lhu = typo.lineHeightUnit || ''; |
| 33 |
if (typo.lineHeightDesktop !== '' && typo.lineHeightDesktop != null) s[prefix + 'line-height-d'] = typo.lineHeightDesktop + lhu; |
| 34 |
if (typo.lineHeightTablet !== '' && typo.lineHeightTablet != null) s[prefix + 'line-height-t'] = typo.lineHeightTablet + lhu; |
| 35 |
if (typo.lineHeightMobile !== '' && typo.lineHeightMobile != null) s[prefix + 'line-height-m'] = typo.lineHeightMobile + lhu; |
| 36 |
var lsu = typo.letterSpacingUnit || 'px'; |
| 37 |
if (typo.letterSpacingDesktop !== '' && typo.letterSpacingDesktop != null) s[prefix + 'letter-spacing-d'] = typo.letterSpacingDesktop + lsu; |
| 38 |
if (typo.letterSpacingTablet !== '' && typo.letterSpacingTablet != null) s[prefix + 'letter-spacing-t'] = typo.letterSpacingTablet + lsu; |
| 39 |
if (typo.letterSpacingMobile !== '' && typo.letterSpacingMobile != null) s[prefix + 'letter-spacing-m'] = typo.letterSpacingMobile + lsu; |
| 40 |
var wsu = typo.wordSpacingUnit || 'px'; |
| 41 |
if (typo.wordSpacingDesktop !== '' && typo.wordSpacingDesktop != null) s[prefix + 'word-spacing-d'] = typo.wordSpacingDesktop + wsu; |
| 42 |
if (typo.wordSpacingTablet !== '' && typo.wordSpacingTablet != null) s[prefix + 'word-spacing-t'] = typo.wordSpacingTablet + wsu; |
| 43 |
if (typo.wordSpacingMobile !== '' && typo.wordSpacingMobile != null) s[prefix + 'word-spacing-m'] = typo.wordSpacingMobile + wsu; |
| 44 |
return s; |
| 45 |
} |
| 46 |
|
| 47 |
var DAYS_SHORT = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']; |
| 48 |
var MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December']; |
| 49 |
|
| 50 |
// ── Calendar preview ── |
| 51 |
function CalendarPreview(props) { |
| 52 |
var attr = props.attr; |
| 53 |
var today = new Date(2026, 1, 23); // Fixed demo date |
| 54 |
var year = today.getFullYear(); |
| 55 |
var month = today.getMonth(); |
| 56 |
var firstDay = new Date(year, month, 1).getDay(); |
| 57 |
var daysInMonth = new Date(year, month + 1, 0).getDate(); |
| 58 |
var blocked = attr.blockedDays || [0, 6]; |
| 59 |
|
| 60 |
var cells = []; |
| 61 |
for (var pad = 0; pad < firstDay; pad++) { |
| 62 |
cells.push(el('div', { key: 'p' + pad, style: { padding: '8px 4px', fontSize: 13, color: 'transparent' } }, '·')); |
| 63 |
} |
| 64 |
for (var d = 1; d <= daysInMonth; d++) { |
| 65 |
var dow = (firstDay + d - 1) % 7; |
| 66 |
var isBlocked = blocked.indexOf(dow) !== -1; |
| 67 |
var isSel = d === today.getDate(); |
| 68 |
cells.push(el('div', { |
| 69 |
key: d, |
| 70 |
style: { |
| 71 |
padding: '8px 4px', textAlign: 'center', borderRadius: 8, fontSize: 13, fontWeight: 500, |
| 72 |
background: isSel ? attr.selectedDayBg : 'transparent', |
| 73 |
color: isSel ? attr.selectedDayColor : isBlocked ? '#cbd5e1' : attr.textColor, |
| 74 |
cursor: isBlocked ? 'default' : 'pointer', |
| 75 |
opacity: isBlocked ? 0.4 : 1 |
| 76 |
} |
| 77 |
}, d)); |
| 78 |
} |
| 79 |
|
| 80 |
return el('div', { style: { background: attr.calendarBg, borderRadius: attr.calendarRadius + 'px', overflow: 'hidden', marginBottom: 20 } }, |
| 81 |
// Header |
| 82 |
el('div', { style: { background: attr.calendarHeaderBg, color: attr.calendarHeaderColor, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 20px' } }, |
| 83 |
el('button', { style: { background: 'none', border: 'none', color: 'inherit', fontSize: 18, cursor: 'pointer', padding: '0 4px' } }, '‹'), |
| 84 |
el('span', { style: { fontWeight: 700, fontSize: 15 } }, MONTHS[month] + ' ' + year), |
| 85 |
el('button', { style: { background: 'none', border: 'none', color: 'inherit', fontSize: 18, cursor: 'pointer', padding: '0 4px' } }, '›') |
| 86 |
), |
| 87 |
// Day names |
| 88 |
el('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', padding: '8px 12px 0', gap: 2 } }, |
| 89 |
DAYS_SHORT.map(function(d) { return el('div', { key: d, style: { textAlign: 'center', fontSize: 11, fontWeight: 700, color: attr.mutedColor, padding: '4px 0' } }, d); }) |
| 90 |
), |
| 91 |
// Cells |
| 92 |
el('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', padding: '4px 12px 12px', gap: 2 } }, ...cells) |
| 93 |
); |
| 94 |
} |
| 95 |
|
| 96 |
registerBlockType('blockenberg/booking-form', { |
| 97 |
edit: function (props) { |
| 98 |
var attr = props.attributes; |
| 99 |
var setAttr = props.setAttributes; |
| 100 |
var blockProps = useBlockProps({ style: Object.assign({ background: attr.sectionBg || undefined }, _tv(attr.typoTitle, '--bkbg-bfm-title-'), _tv(attr.typoSubtitle, '--bkbg-bfm-sub-')) }); |
| 101 |
|
| 102 |
// Service list editor |
| 103 |
function ServiceRow(srProps) { |
| 104 |
var s = srProps.s; |
| 105 |
var idx = srProps.idx; |
| 106 |
function update(patch) { |
| 107 |
var svc = JSON.parse(JSON.stringify(attr.services)); |
| 108 |
Object.assign(svc[idx], patch); |
| 109 |
setAttr({ services: svc }); |
| 110 |
} |
| 111 |
return el('div', { style: { border: '1px solid #e2e8f0', borderRadius: 8, padding: '8px 10px', marginBottom: 8 } }, |
| 112 |
el(TextControl, { label: 'Service name', value: s.label, onChange: function(v){ update({ label: v }); }, __nextHasNoMarginBottom: true }), |
| 113 |
el('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, marginTop: 6 } }, |
| 114 |
el(TextControl, { label: 'Duration (min)', value: String(s.duration), onChange: function(v){ update({ duration: parseInt(v) || 0 }); }, __nextHasNoMarginBottom: true }), |
| 115 |
el(TextControl, { label: 'Price', value: s.price, onChange: function(v){ update({ price: v }); }, __nextHasNoMarginBottom: true }) |
| 116 |
), |
| 117 |
el(Button, { |
| 118 |
variant: 'link', isDestructive: true, size: 'small', style: { marginTop: 6 }, |
| 119 |
onClick: function() { var svc = JSON.parse(JSON.stringify(attr.services)); svc.splice(idx, 1); setAttr({ services: svc }); } |
| 120 |
}, '✕ Remove service') |
| 121 |
); |
| 122 |
} |
| 123 |
|
| 124 |
// Time slots editor |
| 125 |
function updateSlots(rawText) { |
| 126 |
var slots = rawText.split('\n').map(function(s){ return s.trim(); }).filter(Boolean); |
| 127 |
setAttr({ timeSlots: slots }); |
| 128 |
} |
| 129 |
|
| 130 |
return el('div', blockProps, |
| 131 |
el(InspectorControls, {}, |
| 132 |
|
| 133 |
// Content |
| 134 |
el(PanelBody, { title: 'Form Content', initialOpen: true }, |
| 135 |
el(TextControl, { label: 'Form Title', value: attr.formTitle, onChange: function(v){ setAttr({ formTitle: v }); }, __nextHasNoMarginBottom: true }), |
| 136 |
el(TextControl, { label: 'Form Subtitle', value: attr.formSubtitle, onChange: function(v){ setAttr({ formSubtitle: v }); }, __nextHasNoMarginBottom: true }), |
| 137 |
el(TextControl, { label: 'Submit Button Label', value: attr.submitLabel, onChange: function(v){ setAttr({ submitLabel: v }); }, __nextHasNoMarginBottom: true }), |
| 138 |
el(TextControl, { label: 'Confirmation Title', value: attr.confirmationTitle, onChange: function(v){ setAttr({ confirmationTitle: v }); }, __nextHasNoMarginBottom: true }), |
| 139 |
el(TextareaControl, { label: 'Confirmation Message', value: attr.confirmationMsg, onChange: function(v){ setAttr({ confirmationMsg: v }); }, __nextHasNoMarginBottom: true }) |
| 140 |
), |
| 141 |
|
| 142 |
// Services |
| 143 |
el(PanelBody, { title: 'Services', initialOpen: false }, |
| 144 |
el(ToggleControl, { label: 'Show service selector', checked: attr.showServiceSelect, onChange: function(v){ setAttr({ showServiceSelect: v }); }, __nextHasNoMarginBottom: true }), |
| 145 |
el(ToggleControl, { label: 'Show price', checked: attr.showPrice, onChange: function(v){ setAttr({ showPrice: v }); }, __nextHasNoMarginBottom: true }), |
| 146 |
el('hr', { style: { borderColor: '#e2e8f0', margin: '8px 0' } }), |
| 147 |
attr.services.map(function(s, i) { return el(ServiceRow, { key: i, s: s, idx: i }); }), |
| 148 |
el(Button, { |
| 149 |
variant: 'secondary', size: 'small', |
| 150 |
onClick: function() { var svc = JSON.parse(JSON.stringify(attr.services)); svc.push({ label: 'New Service', duration: 60, price: '' }); setAttr({ services: svc }); } |
| 151 |
}, '+ Add Service') |
| 152 |
), |
| 153 |
|
| 154 |
// Time slots |
| 155 |
el(PanelBody, { title: 'Time Slots', initialOpen: false }, |
| 156 |
el(TextareaControl, { |
| 157 |
label: 'Time slots (one per line, HH:MM)', |
| 158 |
value: (attr.timeSlots || []).join('\n'), |
| 159 |
onChange: updateSlots, |
| 160 |
rows: 8, |
| 161 |
__nextHasNoMarginBottom: true |
| 162 |
}), |
| 163 |
el('p', { style: { fontSize: 11, color: '#64748b', margin: '4px 0 0' } }, 'Blocked days (0=Sun, 6=Sat): ' + (attr.blockedDays || []).join(', ')) |
| 164 |
), |
| 165 |
|
| 166 |
// Fields |
| 167 |
el(PanelBody, { title: 'Form Fields', initialOpen: false }, |
| 168 |
el(ToggleControl, { label: 'Name field', checked: attr.showNameField, onChange: function(v){ setAttr({ showNameField: v }); }, __nextHasNoMarginBottom: true }), |
| 169 |
el(ToggleControl, { label: 'Email field', checked: attr.showEmailField, onChange: function(v){ setAttr({ showEmailField: v }); }, __nextHasNoMarginBottom: true }), |
| 170 |
el(ToggleControl, { label: 'Phone field', checked: attr.showPhoneField, onChange: function(v){ setAttr({ showPhoneField: v }); }, __nextHasNoMarginBottom: true }), |
| 171 |
el(ToggleControl, { label: 'Notes / message field', checked: attr.showNotes, onChange: function(v){ setAttr({ showNotes: v }); }, __nextHasNoMarginBottom: true }) |
| 172 |
), |
| 173 |
|
| 174 |
// Layout |
| 175 |
el(PanelBody, { title: 'Layout', initialOpen: false }, |
| 176 |
el(RangeControl, { label: 'Form Width (px)', value: attr.formWidth, min: 360, max: 900, step: 10, onChange: function(v){ setAttr({ formWidth: v }); }, __nextHasNoMarginBottom: true }), |
| 177 |
el(RangeControl, { label: 'Form Radius (px)', value: attr.formRadius, min: 0, max: 40, onChange: function(v){ setAttr({ formRadius: v }); }, __nextHasNoMarginBottom: true }), |
| 178 |
el(RangeControl, { label: 'Input Radius (px)', value: attr.inputRadius, min: 0, max: 24, onChange: function(v){ setAttr({ inputRadius: v }); }, __nextHasNoMarginBottom: true }), |
| 179 |
el(RangeControl, { label: 'Calendar Radius (px)', value: attr.calendarRadius, min: 0, max: 24, onChange: function(v){ setAttr({ calendarRadius: v }); }, __nextHasNoMarginBottom: true }) |
| 180 |
), |
| 181 |
|
| 182 |
// Typography |
| 183 |
el(PanelBody, { title: 'Typography', initialOpen: false }, |
| 184 |
el(getTypographyControl(), { label: __('Title', 'blockenberg'), value: attr.typoTitle, onChange: function (v) { setAttr({ typoTitle: v }); } }), |
| 185 |
el(getTypographyControl(), { label: __('Subtitle', 'blockenberg'), value: attr.typoSubtitle, onChange: function (v) { setAttr({ typoSubtitle: v }); } }) |
| 186 |
), |
| 187 |
|
| 188 |
// Colors |
| 189 |
el(PanelColorSettings, { |
| 190 |
title: 'Colors', |
| 191 |
initialOpen: false, |
| 192 |
colorSettings: [ |
| 193 |
{ label: 'Form Background', value: attr.formBg, onChange: function(v){ setAttr({ formBg: v || '#ffffff' }); } }, |
| 194 |
{ label: 'Form Border', value: attr.formBorder, onChange: function(v){ setAttr({ formBorder: v || '#e2e8f0' }); } }, |
| 195 |
{ label: 'Calendar Background', value: attr.calendarBg, onChange: function(v){ setAttr({ calendarBg: v || '#f8fafc' }); } }, |
| 196 |
{ label: 'Calendar Header BG', value: attr.calendarHeaderBg, onChange: function(v){ setAttr({ calendarHeaderBg: v || '#6366f1' }); } }, |
| 197 |
{ label: 'Calendar Header Text', value: attr.calendarHeaderColor,onChange: function(v){ setAttr({ calendarHeaderColor:v || '#ffffff' }); } }, |
| 198 |
{ label: 'Selected Day BG', value: attr.selectedDayBg, onChange: function(v){ setAttr({ selectedDayBg: v || '#6366f1' }); } }, |
| 199 |
{ label: 'Selected Day Text', value: attr.selectedDayColor, onChange: function(v){ setAttr({ selectedDayColor: v || '#ffffff' }); } }, |
| 200 |
{ label: 'Time Slot BG', value: attr.slotBg, onChange: function(v){ setAttr({ slotBg: v || '#f1f5f9' }); } }, |
| 201 |
{ label: 'Selected Slot BG', value: attr.slotSelectedBg, onChange: function(v){ setAttr({ slotSelectedBg: v || '#6366f1' }); } }, |
| 202 |
{ label: 'Selected Slot Text', value: attr.slotSelectedColor, onChange: function(v){ setAttr({ slotSelectedColor: v || '#ffffff' }); } }, |
| 203 |
{ label: 'Input Background', value: attr.inputBg, onChange: function(v){ setAttr({ inputBg: v || '#f8fafc' }); } }, |
| 204 |
{ label: 'Input Border', value: attr.inputBorder, onChange: function(v){ setAttr({ inputBorder: v || '#e2e8f0' }); } }, |
| 205 |
{ label: 'Submit Button BG', value: attr.btnBg, onChange: function(v){ setAttr({ btnBg: v || '#6366f1' }); } }, |
| 206 |
{ label: 'Submit Button Text', value: attr.btnColor, onChange: function(v){ setAttr({ btnColor: v || '#ffffff' }); } }, |
| 207 |
{ label: 'Text Color', value: attr.textColor, onChange: function(v){ setAttr({ textColor: v || '#0f172a' }); } }, |
| 208 |
{ label: 'Muted Color', value: attr.mutedColor, onChange: function(v){ setAttr({ mutedColor: v || '#64748b' }); } }, |
| 209 |
{ label: 'Section Background', value: attr.sectionBg, onChange: function(v){ setAttr({ sectionBg: v || '' }); } } |
| 210 |
] |
| 211 |
}) |
| 212 |
), |
| 213 |
|
| 214 |
// ── Editor preview ── |
| 215 |
el('div', { style: { maxWidth: attr.formWidth + 'px', margin: '0 auto', fontFamily: 'system-ui,sans-serif' } }, |
| 216 |
el('div', { style: { background: attr.formBg, border: '1px solid ' + attr.formBorder, borderRadius: attr.formRadius + 'px', padding: '28px 28px 24px', boxShadow: '0 4px 24px rgba(0,0,0,0.06)' } }, |
| 217 |
// Header |
| 218 |
el('h2', { className: 'bkbg-bfm-title', style: { color: attr.textColor, margin: '0 0 6px' } }, attr.formTitle), |
| 219 |
el('p', { className: 'bkbg-bfm-subtitle', style: { color: attr.mutedColor, margin: '0 0 24px' } }, attr.formSubtitle), |
| 220 |
|
| 221 |
// Service select preview |
| 222 |
attr.showServiceSelect && el('div', { style: { marginBottom: 18 } }, |
| 223 |
el('label', { style: { display: 'block', fontSize: 13, fontWeight: 600, color: attr.textColor, marginBottom: 6 } }, 'Select Service'), |
| 224 |
el('select', { style: { width: '100%', padding: '11px 13px', borderRadius: attr.inputRadius + 'px', border: '1px solid ' + attr.inputBorder, background: attr.inputBg, color: attr.textColor, fontSize: 14 } }, |
| 225 |
attr.services.map(function(s, i) { |
| 226 |
return el('option', { key: i }, s.label + (attr.showPrice && s.price ? ' — ' + s.price : '')); |
| 227 |
}) |
| 228 |
) |
| 229 |
), |
| 230 |
|
| 231 |
// Calendar preview |
| 232 |
el(CalendarPreview, { attr: attr }), |
| 233 |
|
| 234 |
// Time slots |
| 235 |
el('div', { style: { marginBottom: 18 } }, |
| 236 |
el('p', { style: { fontSize: 13, fontWeight: 600, color: attr.textColor, margin: '0 0 10px' } }, 'Available Times'), |
| 237 |
el('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } }, |
| 238 |
(attr.timeSlots || []).slice(0, 8).map(function(t, i) { |
| 239 |
return el('div', { |
| 240 |
key: i, |
| 241 |
style: { |
| 242 |
padding: '7px 14px', borderRadius: attr.inputRadius + 'px', fontSize: 13, fontWeight: 500, cursor: 'pointer', |
| 243 |
background: i === 0 ? attr.slotSelectedBg : attr.slotBg, |
| 244 |
color: i === 0 ? attr.slotSelectedColor : attr.textColor |
| 245 |
} |
| 246 |
}, t); |
| 247 |
}), |
| 248 |
(attr.timeSlots || []).length > 8 && el('div', { style: { padding: '7px 14px', fontSize: 12, color: attr.mutedColor } }, '+' + ((attr.timeSlots.length - 8)) + ' more') |
| 249 |
) |
| 250 |
), |
| 251 |
|
| 252 |
// Submit |
| 253 |
el('button', { style: { width: '100%', padding: '13px', background: attr.btnBg, color: attr.btnColor, border: 'none', borderRadius: attr.inputRadius + 'px', fontSize: 15, fontWeight: 700, cursor: 'pointer' } }, attr.submitLabel) |
| 254 |
) |
| 255 |
) |
| 256 |
); |
| 257 |
}, |
| 258 |
|
| 259 |
save: function (props) { |
| 260 |
var attr = props.attributes; |
| 261 |
var blockProps = useBlockProps.save(); |
| 262 |
return el('div', blockProps, |
| 263 |
el('div', { |
| 264 |
className: 'bkbg-bf-app', |
| 265 |
'data-opts': JSON.stringify(attr) |
| 266 |
}) |
| 267 |
); |
| 268 |
} |
| 269 |
}); |
| 270 |
}() ); |
| 271 |
|