PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.8
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.8
2.0.13 2.0.12 2.0.11 2.0.10 2.0.9 trunk 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8
blockenberg / assets / js / inspector-tabs.js

inspector-tabs.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.8, at assets/js/inspector-tabs.js

2,049 lines 86.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Blockenberg Inspector Tabs
3 * Adds General / Advanced tab navigation to all Blockenberg block inspector panels.
4 * One file that applies to ALL blockenberg/* blocks via the editor.BlockEdit filter.
5 */
6 (function () {
7 'use strict';
8
9 var el = wp.element.createElement;
10 var Fragment = wp.element.Fragment;
11 var useState = wp.element.useState;
12 var useEffect = wp.element.useEffect;
13 var useRef = wp.element.useRef;
14 var useCallback = wp.element.useCallback;
15 var createHigherOrderComponent = wp.compose.createHigherOrderComponent;
16 var addFilter = wp.hooks.addFilter;
17 var InspectorControls = wp.blockEditor.InspectorControls;
18 var MediaUpload = wp.blockEditor.MediaUpload;
19 var PanelBody = wp.components.PanelBody;
20 var TabPanel = wp.components.TabPanel;
21 var Button = wp.components.Button;
22 var Popover = wp.components.Popover;
23 var TextControl = wp.components.TextControl;
24 var ColorPicker = wp.components.ColorPicker;
25 var SelectControl = wp.components.SelectControl;
26 var __ = wp.i18n.__;
27
28 /* =====================================================
29 Attribute definitions added to every blockenberg/* block
30 ===================================================== */
31
32 var SIDES = ['Top', 'Right', 'Bottom', 'Left'];
33 var DEVICES = ['', 'Tablet', 'Mobile']; // '' = desktop
34
35 // Build attribute map once
36 var advancedAttributes = {};
37
38 // Margin & Padding — per side, per device, with unit
39 ['bkbgMargin', 'bkbgPadding'].forEach(function (prefix) {
40 SIDES.forEach(function (side) {
41 DEVICES.forEach(function (device) {
42 var key = prefix + side + device;
43 advancedAttributes[key] = { type: 'string', default: '' };
44 advancedAttributes[key + 'Unit'] = { type: 'string', default: 'px' };
45 });
46 });
47 // Linked toggle per device
48 DEVICES.forEach(function (device) {
49 advancedAttributes[prefix + 'Linked' + device] = { type: 'boolean', default: true };
50 });
51 });
52
53 // Z-Index per device
54 DEVICES.forEach(function (device) {
55 advancedAttributes['bkbgZIndex' + device] = { type: 'string', default: '' };
56 });
57
58 // CSS ID & CSS Classes
59 advancedAttributes.bkbgCssId = { type: 'string', default: '' };
60 advancedAttributes.bkbgCssClasses = { type: 'string', default: '' };
61
62 /* =====================================================
63 Background attributes
64 ===================================================== */
65
66 // Background Type (normal & hover)
67 advancedAttributes.bkbgBgType = { type: 'string', default: '' };
68 advancedAttributes.bkbgBgHoverType = { type: 'string', default: '' };
69
70 // Classic - Normal: Color
71 advancedAttributes.bkbgBgColor = { type: 'string', default: '' };
72
73 // Classic - Normal: Image (responsive)
74 DEVICES.forEach(function (device) {
75 advancedAttributes['bkbgBgImage' + device] = { type: 'string', default: '' };
76 advancedAttributes['bkbgBgImageId' + device] = { type: 'number', default: 0 };
77 });
78
79 // Classic - Normal: Image settings (position/repeat/size are responsive, attachment is global)
80 DEVICES.forEach(function (device) {
81 advancedAttributes['bkbgBgPosition' + device] = { type: 'string', default: '' };
82 advancedAttributes['bkbgBgRepeat' + device] = { type: 'string', default: '' };
83 advancedAttributes['bkbgBgSize' + device] = { type: 'string', default: '' };
84 advancedAttributes['bkbgBgSizeCustomW' + device] = { type: 'string', default: '' };
85 advancedAttributes['bkbgBgSizeCustomH' + device] = { type: 'string', default: '' };
86 advancedAttributes['bkbgBgPositionCustomX' + device] = { type: 'string', default: '' };
87 advancedAttributes['bkbgBgPositionCustomY' + device] = { type: 'string', default: '' };
88 });
89 advancedAttributes.bkbgBgAttachment = { type: 'string', default: '' };
90
91 // Gradient - Normal
92 advancedAttributes.bkbgBgGradColor1 = { type: 'string', default: '' };
93 advancedAttributes.bkbgBgGradColor2 = { type: 'string', default: '' };
94 advancedAttributes.bkbgBgGradType = { type: 'string', default: 'linear' };
95 DEVICES.forEach(function (device) {
96 advancedAttributes['bkbgBgGradLoc1' + device] = { type: 'string', default: '' };
97 advancedAttributes['bkbgBgGradLoc2' + device] = { type: 'string', default: '' };
98 advancedAttributes['bkbgBgGradAngle' + device] = { type: 'string', default: '' };
99 advancedAttributes['bkbgBgGradPosition' + device] = { type: 'string', default: '' };
100 });
101
102 // Classic - Hover: Color
103 advancedAttributes.bkbgBgHoverColor = { type: 'string', default: '' };
104
105 // Classic - Hover: Image (responsive)
106 DEVICES.forEach(function (device) {
107 advancedAttributes['bkbgBgHoverImage' + device] = { type: 'string', default: '' };
108 advancedAttributes['bkbgBgHoverImageId' + device] = { type: 'number', default: 0 };
109 });
110
111 // Classic - Hover: Image settings
112 DEVICES.forEach(function (device) {
113 advancedAttributes['bkbgBgHoverPosition' + device] = { type: 'string', default: '' };
114 advancedAttributes['bkbgBgHoverRepeat' + device] = { type: 'string', default: '' };
115 advancedAttributes['bkbgBgHoverSize' + device] = { type: 'string', default: '' };
116 advancedAttributes['bkbgBgHoverSizeCustomW' + device] = { type: 'string', default: '' };
117 advancedAttributes['bkbgBgHoverSizeCustomH' + device] = { type: 'string', default: '' };
118 advancedAttributes['bkbgBgHoverPositionCustomX' + device] = { type: 'string', default: '' };
119 advancedAttributes['bkbgBgHoverPositionCustomY' + device] = { type: 'string', default: '' };
120 });
121 advancedAttributes.bkbgBgHoverAttachment = { type: 'string', default: '' };
122
123 // Gradient - Hover
124 advancedAttributes.bkbgBgHoverGradColor1 = { type: 'string', default: '' };
125 advancedAttributes.bkbgBgHoverGradColor2 = { type: 'string', default: '' };
126 advancedAttributes.bkbgBgHoverGradType = { type: 'string', default: 'linear' };
127 DEVICES.forEach(function (device) {
128 advancedAttributes['bkbgBgHoverGradLoc1' + device] = { type: 'string', default: '' };
129 advancedAttributes['bkbgBgHoverGradLoc2' + device] = { type: 'string', default: '' };
130 advancedAttributes['bkbgBgHoverGradAngle' + device] = { type: 'string', default: '' };
131 advancedAttributes['bkbgBgHoverGradPosition' + device] = { type: 'string', default: '' };
132 });
133
134
135
136 /* =====================================================
137 Border attributes
138 ===================================================== */
139
140 // Border Type (normal & hover) — not responsive
141 advancedAttributes.bkbgBorderType = { type: 'string', default: '' };
142 advancedAttributes.bkbgBorderHoverType = { type: 'string', default: '' };
143
144 // Border Width — per side, per device, with unit (same pattern as margin/padding)
145 ['bkbgBorderWidth', 'bkbgBorderHoverWidth'].forEach(function (prefix) {
146 SIDES.forEach(function (side) {
147 DEVICES.forEach(function (device) {
148 var key = prefix + side + device;
149 advancedAttributes[key] = { type: 'string', default: '' };
150 advancedAttributes[key + 'Unit'] = { type: 'string', default: 'px' };
151 });
152 });
153 DEVICES.forEach(function (device) {
154 advancedAttributes[prefix + 'Linked' + device] = { type: 'boolean', default: true };
155 });
156 });
157
158 // Border Color (normal & hover)
159 advancedAttributes.bkbgBorderColor = { type: 'string', default: '' };
160 advancedAttributes.bkbgBorderHoverColor = { type: 'string', default: '' };
161
162 // Border Radius — per corner, per device, with unit
163 ['bkbgBorderRadius', 'bkbgBorderHoverRadius'].forEach(function (prefix) {
164 SIDES.forEach(function (side) {
165 DEVICES.forEach(function (device) {
166 var key = prefix + side + device;
167 advancedAttributes[key] = { type: 'string', default: '' };
168 advancedAttributes[key + 'Unit'] = { type: 'string', default: 'px' };
169 });
170 });
171 DEVICES.forEach(function (device) {
172 advancedAttributes[prefix + 'Linked' + device] = { type: 'boolean', default: true };
173 });
174 });
175
176 // Box Shadow (normal)
177 advancedAttributes.bkbgShadowColor = { type: 'string', default: '' };
178 advancedAttributes.bkbgShadowH = { type: 'string', default: '' };
179 advancedAttributes.bkbgShadowV = { type: 'string', default: '' };
180 advancedAttributes.bkbgShadowBlur = { type: 'string', default: '' };
181 advancedAttributes.bkbgShadowSpread = { type: 'string', default: '' };
182 advancedAttributes.bkbgShadowPosition = { type: 'string', default: '' };
183
184 // Box Shadow (hover)
185 advancedAttributes.bkbgShadowHoverColor = { type: 'string', default: '' };
186 advancedAttributes.bkbgShadowHoverH = { type: 'string', default: '' };
187 advancedAttributes.bkbgShadowHoverV = { type: 'string', default: '' };
188 advancedAttributes.bkbgShadowHoverBlur = { type: 'string', default: '' };
189 advancedAttributes.bkbgShadowHoverSpread = { type: 'string', default: '' };
190 advancedAttributes.bkbgShadowHoverPosition = { type: 'string', default: '' };
191
192
193
194 // Responsive visibility
195 advancedAttributes.bkbgHideDesktop = { type: 'boolean', default: false };
196 advancedAttributes.bkbgHideTablet = { type: 'boolean', default: false };
197 advancedAttributes.bkbgHideMobile = { type: 'boolean', default: false };
198
199 /* =====================================================
200 Filter: inject attributes into every blockenberg/* block
201 ===================================================== */
202
203 addFilter(
204 'blocks.registerBlockType',
205 'blockenberg/advanced-layout-attributes',
206 function (settings, name) {
207 if (!name || name.indexOf('blockenberg/') !== 0) return settings;
208
209 var existing = settings.attributes || {};
210 var merged = {};
211 var k;
212 for (k in existing) { merged[k] = existing[k]; }
213 for (k in advancedAttributes) {
214 if (!merged[k]) merged[k] = advancedAttributes[k];
215 }
216 settings.attributes = merged;
217 return settings;
218 }
219 );
220
221 /* =====================================================
222 Unit options
223 ===================================================== */
224
225 var UNIT_OPTIONS = [
226 { label: 'px', value: 'px' },
227 { label: '%', value: '%' },
228 { label: 'em', value: 'em' },
229 { label: 'rem', value: 'rem' },
230 { label: 'vw', value: 'vw' }
231 ];
232
233 /* =====================================================
234 Device icons (SVG)
235 ===================================================== */
236
237 function deviceIcon(type, isActive) {
238 var color = isActive ? '#e50087' : 'currentColor';
239 if (type === 'desktop') {
240 return el('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg' },
241 el('path', { d: 'M20 3H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h6l-1 2H8v2h8v-2h-1l-1-2h6c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 12H4V5h16v10z', fill: color })
242 );
243 }
244 if (type === 'tablet') {
245 return el('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg' },
246 el('path', { d: 'M18.5 0h-14A2.5 2.5 0 0 0 2 2.5v19A2.5 2.5 0 0 0 4.5 24h14a2.5 2.5 0 0 0 2.5-2.5v-19A2.5 2.5 0 0 0 18.5 0zm-7 23c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm7.5-4H4V3h15v16z', fill: color })
247 );
248 }
249 // mobile
250 return el('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg' },
251 el('path', { d: 'M15.5 1h-8A2.5 2.5 0 0 0 5 3.5v17A2.5 2.5 0 0 0 7.5 23h8a2.5 2.5 0 0 0 2.5-2.5v-17A2.5 2.5 0 0 0 15.5 1zm-4 21c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4.5-4H7V4h9v14z', fill: color })
252 );
253 }
254
255 function normalizePreviewDeviceName(raw) {
256 if (!raw) return 'desktop';
257 var value = String(raw).toLowerCase();
258 if (value === 'tablet') return 'tablet';
259 if (value === 'mobile') return 'mobile';
260 return 'desktop';
261 }
262
263 function setEditorPreviewDevice(deviceName) {
264 var mapped = deviceName === 'desktop' ? 'Desktop' : (deviceName === 'tablet' ? 'Tablet' : 'Mobile');
265 var stores = ['core/edit-post', 'core/edit-site'];
266
267 stores.forEach(function (storeName) {
268 var dispatcher = wp.data && wp.data.dispatch ? wp.data.dispatch(storeName) : null;
269 if (!dispatcher) return;
270
271 if (typeof dispatcher.__experimentalSetPreviewDeviceType === 'function') {
272 dispatcher.__experimentalSetPreviewDeviceType(mapped);
273 return;
274 }
275
276 if (typeof dispatcher.setDeviceType === 'function') {
277 dispatcher.setDeviceType(mapped);
278 }
279 });
280 }
281
282 function getEditorPreviewDevice() {
283 var stores = ['core/edit-post', 'core/edit-site'];
284 var i;
285
286 for (i = 0; i < stores.length; i++) {
287 var selector = wp.data && wp.data.select ? wp.data.select(stores[i]) : null;
288 if (!selector) continue;
289
290 if (typeof selector.__experimentalGetPreviewDeviceType === 'function') {
291 return normalizePreviewDeviceName(selector.__experimentalGetPreviewDeviceType());
292 }
293
294 if (typeof selector.getDeviceType === 'function') {
295 return normalizePreviewDeviceName(selector.getDeviceType());
296 }
297 }
298
299 return 'desktop';
300 }
301
302 /* =====================================================
303 Responsive Device Switcher Component
304 ===================================================== */
305
306 function DeviceSwitcher(props) {
307 var device = props.device;
308 var onChange = props.onChange;
309 var _pop = useState(false);
310 var isOpen = _pop[0];
311 var setOpen = _pop[1];
312
313 return el('div', { className: 'bkbg-device-switcher' },
314 el(Button, {
315 className: 'bkbg-device-btn',
316 onClick: function () { setOpen(!isOpen); },
317 'aria-label': __('Switch device', 'blockenberg')
318 }, deviceIcon(device, false)),
319 isOpen && el(Popover, {
320 position: 'bottom center',
321 onClose: function () { setOpen(false); },
322 noArrow: true,
323 focusOnMount: 'container',
324 className: 'bkbg-device-popover'
325 },
326 el('div', { className: 'bkbg-device-list' },
327 ['desktop', 'tablet', 'mobile'].map(function (d) {
328 return el(Button, {
329 key: d,
330 className: 'bkbg-device-option' + (d === device ? ' is-active' : ''),
331 onClick: function () {
332 onChange(d);
333 setEditorPreviewDevice(d);
334 setOpen(false);
335 }
336 }, deviceIcon(d, d === device));
337 })
338 )
339 )
340 );
341 }
342
343 /* =====================================================
344 Unit Selector Component
345 ===================================================== */
346
347 function UnitSelector(props) {
348 var unit = props.unit || 'px';
349 var onChange = props.onChange;
350 var _pop = useState(false);
351 var isOpen = _pop[0];
352 var setOpen = _pop[1];
353
354 return el('div', { className: 'bkbg-unit-selector' },
355 el(Button, {
356 className: 'bkbg-unit-btn',
357 onClick: function () { setOpen(!isOpen); }
358 },
359 el('span', { className: 'bkbg-unit-value' }, unit),
360 el('span', {
361 className: 'dashicons dashicons-arrow-down-alt2 bkbg-unit-caret',
362 'aria-hidden': true
363 })
364 ),
365 isOpen && el(Popover, {
366 position: 'bottom right',
367 onClose: function () { setOpen(false); },
368 noArrow: true,
369 focusOnMount: 'container',
370 className: 'bkbg-unit-popover'
371 },
372 el('div', { className: 'bkbg-unit-list' },
373 UNIT_OPTIONS.map(function (opt) {
374 return el(Button, {
375 key: opt.value,
376 className: 'bkbg-unit-option' + (opt.value === unit ? ' is-active' : ''),
377 onClick: function () { onChange(opt.value); setOpen(false); }
378 }, opt.label);
379 })
380 )
381 )
382 );
383 }
384
385 /* =====================================================
386 Spacing Control Component (Margin / Padding)
387 ===================================================== */
388
389 function SpacingControl(props) {
390 var label = props.label;
391 var attrPrefix = props.attrPrefix; // 'bkbgMargin' or 'bkbgPadding'
392 var attributes = props.attributes;
393 var setAttributes = props.setAttributes;
394 var device = props.device || 'desktop';
395 var setDevice = props.onDeviceChange;
396
397 var suffix = device === 'desktop' ? '' : (device === 'tablet' ? 'Tablet' : 'Mobile');
398
399 // Get current linked state
400 var linkedKey = attrPrefix + 'Linked' + suffix;
401 var isLinked = attributes[linkedKey] !== false;
402
403 // Get current unit (use Top as reference)
404 var unitKey = attrPrefix + 'Top' + suffix + 'Unit';
405 var currentUnit = attributes[unitKey] || 'px';
406
407 // Get side values
408 function getSideValue(side) {
409 return attributes[attrPrefix + side + suffix] || '';
410 }
411
412 function setSideValue(side, value) {
413 var updates = {};
414 if (isLinked) {
415 SIDES.forEach(function (s) {
416 updates[attrPrefix + s + suffix] = value;
417 });
418 } else {
419 updates[attrPrefix + side + suffix] = value;
420 }
421 setAttributes(updates);
422 }
423
424 function setUnit(newUnit) {
425 var updates = {};
426 SIDES.forEach(function (s) {
427 updates[attrPrefix + s + suffix + 'Unit'] = newUnit;
428 });
429 setAttributes(updates);
430 }
431
432 function toggleLinked() {
433 var updates = {};
434 updates[linkedKey] = !isLinked;
435 setAttributes(updates);
436 }
437
438 return el('div', { className: 'bkbg-spacing-control' },
439 // Header row: label + device + unit
440 el('div', { className: 'bkbg-spacing-header' },
441 el('span', { className: 'bkbg-spacing-label' }, label),
442 el(DeviceSwitcher, { device: device, onChange: setDevice }),
443 el(UnitSelector, { unit: currentUnit, onChange: setUnit })
444 ),
445 // Input row
446 el('div', { className: 'bkbg-spacing-inputs' },
447 SIDES.map(function (side) {
448 return el('div', { key: side, className: 'bkbg-spacing-side' },
449 el('input', {
450 type: 'number',
451 className: 'bkbg-spacing-input',
452 value: getSideValue(side),
453 placeholder: '',
454 onChange: function (e) {
455 setSideValue(side, e.target.value);
456 }
457 }),
458 el('span', { className: 'bkbg-spacing-side-label' }, side)
459 );
460 }),
461 el(Button, {
462 className: 'bkbg-link-btn' + (isLinked ? ' is-linked' : ''),
463 onClick: toggleLinked,
464 'aria-label': isLinked ? __('Unlink sides', 'blockenberg') : __('Link sides', 'blockenberg'),
465 icon: 'admin-links'
466 })
467 )
468 );
469 }
470
471 /* =====================================================
472 Z-Index Control Component
473 ===================================================== */
474
475 function ZIndexControl(props) {
476 var attributes = props.attributes;
477 var setAttributes = props.setAttributes;
478 var device = props.device || 'desktop';
479 var setDevice = props.onDeviceChange;
480
481 var suffix = device === 'desktop' ? '' : (device === 'tablet' ? 'Tablet' : 'Mobile');
482 var attrKey = 'bkbgZIndex' + suffix;
483
484 return el('div', { className: 'bkbg-zindex-control' },
485 el('div', { className: 'bkbg-spacing-header' },
486 el('span', { className: 'bkbg-spacing-label' }, __('Z-Index', 'blockenberg')),
487 el(DeviceSwitcher, { device: device, onChange: setDevice })
488 ),
489 el('div', { className: 'bkbg-zindex-input-wrap' },
490 el('input', {
491 type: 'number',
492 className: 'bkbg-zindex-input',
493 value: attributes[attrKey] || '',
494 placeholder: '',
495 onChange: function (e) {
496 var updates = {};
497 updates[attrKey] = e.target.value;
498 setAttributes(updates);
499 }
500 })
501 )
502 );
503 }
504
505 /* =====================================================
506 CSS ID / CSS Classes Control Component
507 ===================================================== */
508
509 function CssControl(props) {
510 var label = props.label;
511 var attrKey = props.attrKey;
512 var attributes = props.attributes;
513 var setAttributes = props.setAttributes;
514
515 return el('div', { className: 'bkbg-css-control' },
516 el('div', { className: 'bkbg-spacing-header' },
517 el('span', { className: 'bkbg-spacing-label' }, label)
518 ),
519 el('div', { className: 'bkbg-css-input-wrap' },
520 el('input', {
521 type: 'text',
522 className: 'bkbg-css-input',
523 value: attributes[attrKey] || '',
524 placeholder: '',
525 onChange: function (e) {
526 var updates = {};
527 updates[attrKey] = e.target.value;
528 setAttributes(updates);
529 }
530 })
531 )
532 );
533 }
534
535 /* =====================================================
536 Background Icons (SVG)
537 ===================================================== */
538
539 function bgClassicIcon() {
540 return el('svg', { width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg' },
541 el('path', {
542 d: 'M16.56 8.94L7.62 0 6.21 1.41l2.38 2.38-5.15 5.15c-.59.59-.59 1.54 0 2.12l5.5 5.5c.29.29.68.44 1.06.44s.77-.15 1.06-.44l5.5-5.5c.59-.58.59-1.53 0-2.12zM5.21 10L10 5.21 14.79 10H5.21zM19 11.5s-2 2.17-2 3.5c0 1.1.9 2 2 2s2-.9 2-2c0-1.33-2-3.5-2-3.5z',
543 fill: 'currentColor'
544 })
545 );
546 }
547
548 function bgGradientIcon() {
549 return el('svg', { width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg' },
550 el('defs', {},
551 el('linearGradient', { id: 'bkbg-grad-icon', x1: '0%', y1: '0%', x2: '100%', y2: '100%' },
552 el('stop', { offset: '0%', stopColor: 'currentColor', stopOpacity: '1' }),
553 el('stop', { offset: '100%', stopColor: 'currentColor', stopOpacity: '0.15' })
554 )
555 ),
556 el('rect', { x: 2, y: 2, width: 20, height: 20, rx: 3, fill: 'url(#bkbg-grad-icon)' })
557 );
558 }
559
560 /* bgGlobeIcon removed */
561
562 /* =====================================================
563 Color Swatch + Popover Picker
564 ===================================================== */
565
566 function ColorSwatchControl(props) {
567 var color = props.color || '';
568 var onChange = props.onChange;
569 var label = props.label;
570 var _pop = useState(false);
571 var isOpen = _pop[0];
572 var setOpen = _pop[1];
573
574 return el('div', { className: 'bkbg-color-row' },
575 el('span', { className: 'bkbg-spacing-label' }, label),
576 el('div', { className: 'bkbg-color-controls' },
577 el('button', {
578 className: 'bkbg-color-swatch',
579 onClick: function () { setOpen(!isOpen); },
580 style: { backgroundColor: color || 'transparent' },
581 'aria-label': __('Choose Color', 'blockenberg')
582 }),
583 isOpen && el(Popover, {
584 placement: 'left-start',
585 offset: 16,
586 onClose: function () { setOpen(false); },
587 noArrow: true,
588 focusOnMount: 'container',
589 shift: true,
590 flip: true,
591 resize: false,
592 className: 'bkbg-color-popover'
593 },
594 el('div', { className: 'bkbg-color-picker-wrap' },
595 el(ColorPicker, {
596 color: color || '#000000',
597 onChangeComplete: function (val) {
598 if (val && val.hex) {
599 // Check if has alpha < 1
600 if (val.rgb && val.rgb.a < 1) {
601 onChange('rgba(' + val.rgb.r + ',' + val.rgb.g + ',' + val.rgb.b + ',' + val.rgb.a + ')');
602 } else {
603 onChange(val.hex);
604 }
605 }
606 },
607 onChange: function (val) {
608 if (typeof val === 'string') {
609 onChange(val);
610 }
611 }
612 }),
613 color && el(Button, {
614 className: 'bkbg-color-clear',
615 isSmall: true,
616 variant: 'secondary',
617 onClick: function () { onChange(''); setOpen(false); }
618 }, __('Clear', 'blockenberg'))
619 )
620 )
621 )
622 );
623 }
624
625 /* =====================================================
626 Image Upload Control
627 ===================================================== */
628
629 function ImageUploadControl(props) {
630 var attributes = props.attributes;
631 var setAttributes = props.setAttributes;
632 var prefix = props.prefix; // 'bkbgBg' or 'bkbgBgHover'
633 var device = props.device || 'desktop';
634 var onDeviceChange = props.onDeviceChange;
635
636 var suffix = device === 'desktop' ? '' : (device === 'tablet' ? 'Tablet' : 'Mobile');
637 var imageKey = prefix + 'Image' + suffix;
638 var imageIdKey = prefix + 'ImageId' + suffix;
639 var imageUrl = attributes[imageKey] || '';
640 var imageId = attributes[imageIdKey] || 0;
641
642 function onSelectImage(media) {
643 if (!media || !media.url) return;
644 var updates = {};
645 updates[imageKey] = media.url;
646 updates[imageIdKey] = media.id;
647 setAttributes(updates);
648 }
649
650 function onRemoveImage() {
651 var updates = {};
652 updates[imageKey] = '';
653 updates[imageIdKey] = 0;
654 setAttributes(updates);
655 }
656
657 return el('div', { className: 'bkbg-image-control' },
658 el('div', { className: 'bkbg-spacing-header' },
659 el('span', { className: 'bkbg-spacing-label' }, __('Image', 'blockenberg')),
660 el(DeviceSwitcher, { device: device, onChange: onDeviceChange })
661 ),
662 el(MediaUpload, {
663 onSelect: onSelectImage,
664 allowedTypes: ['image'],
665 value: imageId,
666 render: function (obj) {
667 return el('div', { className: 'bkbg-image-upload-area' },
668 imageUrl
669 ? el('div', { className: 'bkbg-image-preview' },
670 el('img', { src: imageUrl, alt: '' }),
671 el('div', { className: 'bkbg-image-actions' },
672 el(Button, {
673 className: 'bkbg-image-replace',
674 isSmall: true,
675 variant: 'secondary',
676 onClick: obj.open
677 }, __('Replace', 'blockenberg')),
678 el(Button, {
679 className: 'bkbg-image-remove',
680 isSmall: true,
681 isDestructive: true,
682 onClick: onRemoveImage
683 }, __('Remove', 'blockenberg'))
684 )
685 )
686 : el('div', { className: 'bkbg-image-placeholder', onClick: obj.open },
687 el('span', { className: 'bkbg-image-plus' }, '+'),
688 el(Button, {
689 className: 'bkbg-image-choose',
690 variant: 'secondary',
691 isSmall: true,
692 onClick: function (e) { e.stopPropagation(); obj.open(); }
693 }, __('Choose Image', 'blockenberg'))
694 )
695 );
696 }
697 })
698 );
699 }
700
701 /* =====================================================
702 Range Slider Control (for Location / Angle)
703 ===================================================== */
704
705 function RangeSliderControl(props) {
706 var label = props.label;
707 var value = props.value;
708 var onChange = props.onChange;
709 var min = props.min || 0;
710 var max = props.max || 100;
711 var unitLabel = props.unitLabel || '%';
712 var device = props.device;
713 var onDeviceChange = props.onDeviceChange;
714 var showDevice = props.showDevice !== false;
715
716 var numVal = value !== '' && value !== undefined ? parseFloat(value) : '';
717
718 return el('div', { className: 'bkbg-range-control' },
719 el('div', { className: 'bkbg-spacing-header' },
720 el('span', { className: 'bkbg-spacing-label' }, label),
721 showDevice && el(DeviceSwitcher, { device: device, onChange: onDeviceChange }),
722 el('span', { className: 'bkbg-range-unit' }, unitLabel)
723 ),
724 el('div', { className: 'bkbg-range-inputs' },
725 el('input', {
726 type: 'range',
727 className: 'bkbg-range-slider',
728 min: min,
729 max: max,
730 value: numVal !== '' ? numVal : min,
731 onChange: function (e) { onChange(e.target.value); }
732 }),
733 el('input', {
734 type: 'number',
735 className: 'bkbg-range-number',
736 min: min,
737 max: max,
738 value: numVal !== '' ? numVal : '',
739 placeholder: '',
740 onChange: function (e) { onChange(e.target.value); }
741 })
742 )
743 );
744 }
745
746 /* =====================================================
747 Background Option Arrays
748 ===================================================== */
749
750 var BG_POSITION_OPTIONS = [
751 { label: __('Default', 'blockenberg'), value: '' },
752 { label: __('Center Center', 'blockenberg'), value: 'center center' },
753 { label: __('Center Left', 'blockenberg'), value: 'center left' },
754 { label: __('Center Right', 'blockenberg'), value: 'center right' },
755 { label: __('Top Center', 'blockenberg'), value: 'top center' },
756 { label: __('Top Left', 'blockenberg'), value: 'top left' },
757 { label: __('Top Right', 'blockenberg'), value: 'top right' },
758 { label: __('Bottom Center', 'blockenberg'), value: 'bottom center' },
759 { label: __('Bottom Left', 'blockenberg'), value: 'bottom left' },
760 { label: __('Bottom Right', 'blockenberg'), value: 'bottom right' },
761 { label: __('Custom', 'blockenberg'), value: 'custom' }
762 ];
763
764 var BG_ATTACHMENT_OPTIONS = [
765 { label: __('Default', 'blockenberg'), value: '' },
766 { label: __('Scroll', 'blockenberg'), value: 'scroll' },
767 { label: __('Fixed', 'blockenberg'), value: 'fixed' }
768 ];
769
770 var BG_REPEAT_OPTIONS = [
771 { label: __('Default', 'blockenberg'), value: '' },
772 { label: __('No-repeat', 'blockenberg'), value: 'no-repeat' },
773 { label: __('Repeat', 'blockenberg'), value: 'repeat' },
774 { label: __('Repeat-x', 'blockenberg'), value: 'repeat-x' },
775 { label: __('Repeat-y', 'blockenberg'), value: 'repeat-y' }
776 ];
777
778 var BG_SIZE_OPTIONS = [
779 { label: __('Default', 'blockenberg'), value: '' },
780 { label: __('Auto', 'blockenberg'), value: 'auto' },
781 { label: __('Cover', 'blockenberg'), value: 'cover' },
782 { label: __('Contain', 'blockenberg'), value: 'contain' },
783 { label: __('Custom', 'blockenberg'), value: 'custom' }
784 ];
785
786 var BG_GRAD_POSITION_OPTIONS = [
787 { label: __('Center Center', 'blockenberg'), value: 'center center' },
788 { label: __('Center Left', 'blockenberg'), value: 'center left' },
789 { label: __('Center Right', 'blockenberg'), value: 'center right' },
790 { label: __('Top Center', 'blockenberg'), value: 'top center' },
791 { label: __('Top Left', 'blockenberg'), value: 'top left' },
792 { label: __('Top Right', 'blockenberg'), value: 'top right' },
793 { label: __('Bottom Center', 'blockenberg'), value: 'bottom center' },
794 { label: __('Bottom Left', 'blockenberg'), value: 'bottom left' },
795 { label: __('Bottom Right', 'blockenberg'), value: 'bottom right' }
796 ];
797
798 /* =====================================================
799 Dropdown Row Control (label + optional device + select)
800 ===================================================== */
801
802 function DropdownRow(props) {
803 var label = props.label;
804 var value = props.value || '';
805 var onChange = props.onChange;
806 var options = props.options;
807 var device = props.device;
808 var onDeviceChange = props.onDeviceChange;
809 var showDevice = props.showDevice !== false;
810
811 return el('div', { className: 'bkbg-dropdown-row' },
812 el('div', { className: 'bkbg-dropdown-label-side' },
813 el('span', { className: 'bkbg-spacing-label' }, label),
814 showDevice && device && el(DeviceSwitcher, { device: device, onChange: onDeviceChange })
815 ),
816 el('div', { className: 'bkbg-dropdown-select-side' },
817 el('select', {
818 className: 'bkbg-dropdown-select',
819 value: value,
820 onChange: function (e) { onChange(e.target.value); }
821 },
822 options.map(function (opt) {
823 return el('option', { key: opt.value, value: opt.value }, opt.label);
824 })
825 )
826 )
827 );
828 }
829
830 /* =====================================================
831 Classic Image Settings (Position, Attachment, Repeat, Size)
832 ===================================================== */
833
834 function ImageSettingsControls(props) {
835 var prefix = props.prefix; // 'bkbgBg' or 'bkbgBgHover'
836 var attributes = props.attributes;
837 var setAttributes = props.setAttributes;
838 var device = props.device;
839 var onDeviceChange = props.onDeviceChange;
840
841 var suffix = device === 'desktop' ? '' : (device === 'tablet' ? 'Tablet' : 'Mobile');
842 var posKey = prefix + 'Position' + suffix;
843 var attachKey = prefix.replace('Hover', 'Hover') + 'Attachment';
844 if (prefix === 'bkbgBg') attachKey = 'bkbgBgAttachment';
845 else attachKey = 'bkbgBgHoverAttachment';
846 var repeatKey = prefix + 'Repeat' + suffix;
847 var sizeKey = prefix + 'Size' + suffix;
848 var sizeCustomWKey = prefix + 'SizeCustomW' + suffix;
849 var sizeCustomHKey = prefix + 'SizeCustomH' + suffix;
850 var posCustomXKey = prefix + 'PositionCustomX' + suffix;
851 var posCustomYKey = prefix + 'PositionCustomY' + suffix;
852
853 var posValue = attributes[posKey] || '';
854 var sizeValue = attributes[sizeKey] || '';
855
856 function setAttrVal(key, val) {
857 var u = {};
858 u[key] = val;
859 setAttributes(u);
860 }
861
862 return el(Fragment, {},
863 el('div', { className: 'bkbg-adv-separator' }),
864 el(DropdownRow, {
865 label: __('Position', 'blockenberg'),
866 value: posValue,
867 options: BG_POSITION_OPTIONS,
868 device: device,
869 onDeviceChange: onDeviceChange,
870 onChange: function (val) { setAttrVal(posKey, val); }
871 }),
872 posValue === 'custom' && el('div', { className: 'bkbg-custom-pos-row' },
873 el('div', { className: 'bkbg-custom-pos-field' },
874 el('span', { className: 'bkbg-custom-pos-lbl' }, 'X'),
875 el('input', {
876 type: 'number',
877 className: 'bkbg-custom-pos-input',
878 value: attributes[posCustomXKey] || '',
879 placeholder: '0',
880 onChange: function (e) { setAttrVal(posCustomXKey, e.target.value); }
881 }),
882 el('span', { className: 'bkbg-custom-pos-unit' }, '%')
883 ),
884 el('div', { className: 'bkbg-custom-pos-field' },
885 el('span', { className: 'bkbg-custom-pos-lbl' }, 'Y'),
886 el('input', {
887 type: 'number',
888 className: 'bkbg-custom-pos-input',
889 value: attributes[posCustomYKey] || '',
890 placeholder: '0',
891 onChange: function (e) { setAttrVal(posCustomYKey, e.target.value); }
892 }),
893 el('span', { className: 'bkbg-custom-pos-unit' }, '%')
894 )
895 ),
896 el(DropdownRow, {
897 label: __('Attachment', 'blockenberg'),
898 value: attributes[attachKey] || '',
899 options: BG_ATTACHMENT_OPTIONS,
900 showDevice: false,
901 onChange: function (val) { setAttrVal(attachKey, val); }
902 }),
903 el(DropdownRow, {
904 label: __('Repeat', 'blockenberg'),
905 value: attributes[repeatKey] || '',
906 options: BG_REPEAT_OPTIONS,
907 device: device,
908 onDeviceChange: onDeviceChange,
909 onChange: function (val) { setAttrVal(repeatKey, val); }
910 }),
911 el(DropdownRow, {
912 label: __('Display Size', 'blockenberg'),
913 value: sizeValue,
914 options: BG_SIZE_OPTIONS,
915 device: device,
916 onDeviceChange: onDeviceChange,
917 onChange: function (val) { setAttrVal(sizeKey, val); }
918 }),
919 sizeValue === 'custom' && el('div', { className: 'bkbg-custom-pos-row' },
920 el('div', { className: 'bkbg-custom-pos-field' },
921 el('span', { className: 'bkbg-custom-pos-lbl' }, 'W'),
922 el('input', {
923 type: 'number',
924 className: 'bkbg-custom-pos-input',
925 value: attributes[sizeCustomWKey] || '',
926 placeholder: 'auto',
927 onChange: function (e) { setAttrVal(sizeCustomWKey, e.target.value); }
928 }),
929 el('span', { className: 'bkbg-custom-pos-unit' }, 'px')
930 ),
931 el('div', { className: 'bkbg-custom-pos-field' },
932 el('span', { className: 'bkbg-custom-pos-lbl' }, 'H'),
933 el('input', {
934 type: 'number',
935 className: 'bkbg-custom-pos-input',
936 value: attributes[sizeCustomHKey] || '',
937 placeholder: 'auto',
938 onChange: function (e) { setAttrVal(sizeCustomHKey, e.target.value); }
939 }),
940 el('span', { className: 'bkbg-custom-pos-unit' }, 'px')
941 )
942 )
943 );
944 }
945
946 /* =====================================================
947 Classic Background Controls
948 ===================================================== */
949
950 function ClassicBgControls(props) {
951 var prefix = props.prefix; // 'bkbgBg' or 'bkbgBgHover'
952 var attributes = props.attributes;
953 var setAttributes = props.setAttributes;
954 var device = props.device;
955 var onDeviceChange = props.onDeviceChange;
956
957 var colorKey = prefix + 'Color';
958
959 // Check if any device has an image set
960 var suffix = device === 'desktop' ? '' : (device === 'tablet' ? 'Tablet' : 'Mobile');
961 var hasImage = !!(attributes[prefix + 'Image'] || attributes[prefix + 'ImageTablet'] || attributes[prefix + 'ImageMobile']);
962
963 return el(Fragment, {},
964 el(ColorSwatchControl, {
965 label: __('Color', 'blockenberg'),
966 color: attributes[colorKey] || '',
967 onChange: function (val) {
968 var updates = {};
969 updates[colorKey] = val;
970 setAttributes(updates);
971 }
972 }),
973 el(ImageUploadControl, {
974 prefix: prefix,
975 attributes: attributes,
976 setAttributes: setAttributes,
977 device: device,
978 onDeviceChange: onDeviceChange
979 }),
980 hasImage && el(ImageSettingsControls, {
981 prefix: prefix,
982 attributes: attributes,
983 setAttributes: setAttributes,
984 device: device,
985 onDeviceChange: onDeviceChange
986 })
987 );
988 }
989
990 /* =====================================================
991 Gradient Background Controls
992 ===================================================== */
993
994 function GradientBgControls(props) {
995 var prefix = props.prefix; // 'bkbgBg' or 'bkbgBgHover'
996 var attributes = props.attributes;
997 var setAttributes = props.setAttributes;
998 var device = props.device;
999 var onDeviceChange = props.onDeviceChange;
1000
1001 var suffix = device === 'desktop' ? '' : (device === 'tablet' ? 'Tablet' : 'Mobile');
1002
1003 var gradColor1Key = prefix + 'GradColor1';
1004 var gradColor2Key = prefix + 'GradColor2';
1005 var gradLoc1Key = prefix + 'GradLoc1' + suffix;
1006 var gradLoc2Key = prefix + 'GradLoc2' + suffix;
1007 var gradTypeKey = prefix + 'GradType';
1008 var gradAngleKey = prefix + 'GradAngle' + suffix;
1009 var gradPositionKey = prefix + 'GradPosition' + suffix;
1010
1011 var gradType = attributes[gradTypeKey] || 'linear';
1012
1013 return el(Fragment, {},
1014 el('div', { className: 'bkbg-grad-notice' },
1015 el('span', { className: 'bkbg-grad-notice-icon' }, '\u26A0'),
1016 el('span', null, __('Set locations and angle for each breakpoint to ensure the gradient adapts to different screen sizes.', 'blockenberg'))
1017 ),
1018 el(ColorSwatchControl, {
1019 label: __('Color', 'blockenberg'),
1020 color: attributes[gradColor1Key] || '',
1021 onChange: function (val) {
1022 var u = {};
1023 u[gradColor1Key] = val;
1024 setAttributes(u);
1025 }
1026 }),
1027 el(RangeSliderControl, {
1028 label: __('Location', 'blockenberg'),
1029 value: attributes[gradLoc1Key] || '',
1030 min: 0,
1031 max: 100,
1032 unitLabel: '%',
1033 device: device,
1034 onDeviceChange: onDeviceChange,
1035 onChange: function (val) {
1036 var u = {};
1037 u[gradLoc1Key] = val;
1038 setAttributes(u);
1039 }
1040 }),
1041 el(ColorSwatchControl, {
1042 label: __('Second Color', 'blockenberg'),
1043 color: attributes[gradColor2Key] || '',
1044 onChange: function (val) {
1045 var u = {};
1046 u[gradColor2Key] = val;
1047 setAttributes(u);
1048 }
1049 }),
1050 el(RangeSliderControl, {
1051 label: __('Location', 'blockenberg'),
1052 value: attributes[gradLoc2Key] || '',
1053 min: 0,
1054 max: 100,
1055 unitLabel: '%',
1056 device: device,
1057 onDeviceChange: onDeviceChange,
1058 onChange: function (val) {
1059 var u = {};
1060 u[gradLoc2Key] = val;
1061 setAttributes(u);
1062 }
1063 }),
1064 el('div', { className: 'bkbg-grad-type-row' },
1065 el('span', { className: 'bkbg-spacing-label' }, __('Type', 'blockenberg')),
1066 el('div', { className: 'bkbg-grad-type-select' },
1067 el('select', {
1068 className: 'bkbg-dropdown-select',
1069 value: gradType,
1070 onChange: function (e) {
1071 var u = {};
1072 u[gradTypeKey] = e.target.value;
1073 setAttributes(u);
1074 }
1075 },
1076 el('option', { value: 'linear' }, __('Linear', 'blockenberg')),
1077 el('option', { value: 'radial' }, __('Radial', 'blockenberg'))
1078 )
1079 )
1080 ),
1081 gradType === 'radial' && el(DropdownRow, {
1082 label: __('Position', 'blockenberg'),
1083 value: attributes[gradPositionKey] || 'center center',
1084 options: BG_GRAD_POSITION_OPTIONS,
1085 device: device,
1086 onDeviceChange: onDeviceChange,
1087 onChange: function (val) {
1088 var u = {};
1089 u[gradPositionKey] = val;
1090 setAttributes(u);
1091 }
1092 }),
1093 gradType === 'linear' && el(RangeSliderControl, {
1094 label: __('Angle', 'blockenberg'),
1095 value: attributes[gradAngleKey] || '',
1096 min: 0,
1097 max: 360,
1098 unitLabel: 'deg',
1099 device: device,
1100 onDeviceChange: onDeviceChange,
1101 onChange: function (val) {
1102 var u = {};
1103 u[gradAngleKey] = val;
1104 setAttributes(u);
1105 }
1106 })
1107 );
1108 }
1109
1110 /* =====================================================
1111 Background Panel — combines all background controls
1112 ===================================================== */
1113
1114 function BackgroundPanel(props) {
1115 var attributes = props.attributes;
1116 var setAttributes = props.setAttributes;
1117
1118 // Shared device state
1119 var _dev = useState(getEditorPreviewDevice());
1120 var activeDevice = _dev[0];
1121 var setActiveDevice = _dev[1];
1122
1123 useEffect(function () {
1124 if (!wp.data || typeof wp.data.subscribe !== 'function') return;
1125
1126 var unsubscribe = wp.data.subscribe(function () {
1127 var editorDevice = getEditorPreviewDevice();
1128 setActiveDevice(function (current) {
1129 return current === editorDevice ? current : editorDevice;
1130 });
1131 });
1132
1133 return function () {
1134 if (typeof unsubscribe === 'function') unsubscribe();
1135 };
1136 }, []);
1137
1138 // State tab: normal or hover
1139 var _stateTab = useState('normal');
1140 var stateTab = _stateTab[0];
1141 var setStateTab = _stateTab[1];
1142
1143 // Determine prefix based on state
1144 var prefix = stateTab === 'hover' ? 'bkbgBgHover' : 'bkbgBg';
1145 var typeKey = prefix + 'Type';
1146 var bgType = attributes[typeKey] || '';
1147
1148 return el(Fragment, {},
1149 // Normal / Hover toggle
1150 el('div', { className: 'bkbg-bg-state-tabs' },
1151 el(Button, {
1152 className: 'bkbg-bg-state-tab' + (stateTab === 'normal' ? ' is-active' : ''),
1153 onClick: function () { setStateTab('normal'); }
1154 }, __('Normal', 'blockenberg')),
1155 el(Button, {
1156 className: 'bkbg-bg-state-tab' + (stateTab === 'hover' ? ' is-active' : ''),
1157 onClick: function () { setStateTab('hover'); }
1158 }, __('Hover', 'blockenberg'))
1159 ),
1160
1161 // Background Type: Classic / Gradient
1162 el('div', { className: 'bkbg-bg-type-row' },
1163 el('span', { className: 'bkbg-spacing-label' }, __('Background Type', 'blockenberg')),
1164 el('div', { className: 'bkbg-bg-type-buttons' },
1165 el(Button, {
1166 className: 'bkbg-bg-type-btn' + (bgType === 'classic' ? ' is-active' : ''),
1167 onClick: function () {
1168 var u = {};
1169 u[typeKey] = bgType === 'classic' ? '' : 'classic';
1170 setAttributes(u);
1171 },
1172 'aria-label': __('Classic', 'blockenberg')
1173 }, bgClassicIcon()),
1174 el(Button, {
1175 className: 'bkbg-bg-type-btn' + (bgType === 'gradient' ? ' is-active' : ''),
1176 onClick: function () {
1177 var u = {};
1178 u[typeKey] = bgType === 'gradient' ? '' : 'gradient';
1179 setAttributes(u);
1180 },
1181 'aria-label': __('Gradient', 'blockenberg')
1182 }, bgGradientIcon())
1183 )
1184 ),
1185
1186 // Controls based on type
1187 bgType === 'classic' && el(ClassicBgControls, {
1188 prefix: prefix,
1189 attributes: attributes,
1190 setAttributes: setAttributes,
1191 device: activeDevice,
1192 onDeviceChange: setActiveDevice
1193 }),
1194 bgType === 'gradient' && el(GradientBgControls, {
1195 prefix: prefix,
1196 attributes: attributes,
1197 setAttributes: setAttributes,
1198 device: activeDevice,
1199 onDeviceChange: setActiveDevice
1200 }),
1201
1202 );
1203 }
1204
1205 /* =====================================================
1206 Border Option Arrays
1207 ===================================================== */
1208
1209 var BORDER_TYPE_OPTIONS = [
1210 { label: __('Default', 'blockenberg'), value: '' },
1211 { label: __('None', 'blockenberg'), value: 'none' },
1212 { label: __('Solid', 'blockenberg'), value: 'solid' },
1213 { label: __('Double', 'blockenberg'), value: 'double' },
1214 { label: __('Dotted', 'blockenberg'), value: 'dotted' },
1215 { label: __('Dashed', 'blockenberg'), value: 'dashed' },
1216 { label: __('Groove', 'blockenberg'), value: 'groove' }
1217 ];
1218
1219 var SHADOW_POSITION_OPTIONS = [
1220 { label: __('Outline', 'blockenberg'), value: '' },
1221 { label: __('Inset', 'blockenberg'), value: 'inset' }
1222 ];
1223
1224 /* =====================================================
1225 Box Shadow Controls (expandable via pencil icon)
1226 ===================================================== */
1227
1228 function BoxShadowControl(props) {
1229 var prefix = props.prefix; // 'bkbgShadow' or 'bkbgShadowHover'
1230 var attributes = props.attributes;
1231 var setAttributes = props.setAttributes;
1232
1233 var _open = useState(false);
1234 var isOpen = _open[0];
1235 var setOpen = _open[1];
1236
1237 var colorKey = prefix + 'Color';
1238 var hKey = prefix + 'H';
1239 var vKey = prefix + 'V';
1240 var blurKey = prefix + 'Blur';
1241 var spreadKey = prefix + 'Spread';
1242 var posKey = prefix + 'Position';
1243
1244 var hasValues = !!(attributes[colorKey] || attributes[hKey] || attributes[vKey] ||
1245 attributes[blurKey] || attributes[spreadKey]);
1246
1247 function resetShadow() {
1248 var u = {};
1249 u[colorKey] = '';
1250 u[hKey] = '';
1251 u[vKey] = '';
1252 u[blurKey] = '';
1253 u[spreadKey] = '';
1254 u[posKey] = '';
1255 setAttributes(u);
1256 }
1257
1258 function setVal(key, val) {
1259 var u = {};
1260 u[key] = val;
1261 setAttributes(u);
1262 }
1263
1264 return el('div', { className: 'bkbg-shadow-control' },
1265 el('div', { className: 'bkbg-shadow-header' },
1266 el('span', { className: 'bkbg-spacing-label' }, __('Box Shadow', 'blockenberg')),
1267 el('div', { className: 'bkbg-shadow-actions' },
1268 hasValues && el(Button, {
1269 className: 'bkbg-shadow-reset-btn',
1270 onClick: resetShadow,
1271 'aria-label': __('Reset', 'blockenberg')
1272 }, el('svg', { width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none' },
1273 el('path', { d: 'M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z', fill: 'currentColor' })
1274 )),
1275 el(Button, {
1276 className: 'bkbg-shadow-toggle-btn' + (isOpen ? ' is-active' : ''),
1277 onClick: function () { setOpen(!isOpen); },
1278 'aria-label': __('Edit Shadow', 'blockenberg')
1279 }, el('svg', { width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none' },
1280 el('path', { d: 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 000-1.41l-2.34-2.34a1 1 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z', fill: 'currentColor' })
1281 ))
1282 )
1283 ),
1284 isOpen && el('div', { className: 'bkbg-shadow-body' },
1285 el(ColorSwatchControl, {
1286 label: __('Color', 'blockenberg'),
1287 color: attributes[colorKey] || '',
1288 onChange: function (val) { setVal(colorKey, val); }
1289 }),
1290 el(RangeSliderControl, {
1291 label: __('Horizontal', 'blockenberg'),
1292 value: attributes[hKey] || '',
1293 min: -50,
1294 max: 50,
1295 unitLabel: 'px',
1296 showDevice: false,
1297 onChange: function (val) { setVal(hKey, val); }
1298 }),
1299 el(RangeSliderControl, {
1300 label: __('Vertical', 'blockenberg'),
1301 value: attributes[vKey] || '',
1302 min: -50,
1303 max: 50,
1304 unitLabel: 'px',
1305 showDevice: false,
1306 onChange: function (val) { setVal(vKey, val); }
1307 }),
1308 el(RangeSliderControl, {
1309 label: __('Blur', 'blockenberg'),
1310 value: attributes[blurKey] || '',
1311 min: 0,
1312 max: 100,
1313 unitLabel: 'px',
1314 showDevice: false,
1315 onChange: function (val) { setVal(blurKey, val); }
1316 }),
1317 el(RangeSliderControl, {
1318 label: __('Spread', 'blockenberg'),
1319 value: attributes[spreadKey] || '',
1320 min: -50,
1321 max: 50,
1322 unitLabel: 'px',
1323 showDevice: false,
1324 onChange: function (val) { setVal(spreadKey, val); }
1325 }),
1326 el(DropdownRow, {
1327 label: __('Position', 'blockenberg'),
1328 value: attributes[posKey] || '',
1329 options: SHADOW_POSITION_OPTIONS,
1330 showDevice: false,
1331 onChange: function (val) { setVal(posKey, val); }
1332 })
1333 )
1334 );
1335 }
1336
1337 /* =====================================================
1338 Border Panel — combines all border controls
1339 ===================================================== */
1340
1341 function BorderPanel(props) {
1342 var attributes = props.attributes;
1343 var setAttributes = props.setAttributes;
1344
1345 // Shared device state
1346 var _dev = useState(getEditorPreviewDevice());
1347 var activeDevice = _dev[0];
1348 var setActiveDevice = _dev[1];
1349
1350 useEffect(function () {
1351 if (!wp.data || typeof wp.data.subscribe !== 'function') return;
1352 var unsubscribe = wp.data.subscribe(function () {
1353 var editorDevice = getEditorPreviewDevice();
1354 setActiveDevice(function (current) {
1355 return current === editorDevice ? current : editorDevice;
1356 });
1357 });
1358 return function () {
1359 if (typeof unsubscribe === 'function') unsubscribe();
1360 };
1361 }, []);
1362
1363 // State tab: normal or hover
1364 var _stateTab = useState('normal');
1365 var stateTab = _stateTab[0];
1366 var setStateTab = _stateTab[1];
1367
1368 var isHover = stateTab === 'hover';
1369 var typeKey = isHover ? 'bkbgBorderHoverType' : 'bkbgBorderType';
1370 var borderType = attributes[typeKey] || '';
1371 var showBorderControls = borderType && borderType !== 'none';
1372 var widthPrefix = isHover ? 'bkbgBorderHoverWidth' : 'bkbgBorderWidth';
1373 var colorKey = isHover ? 'bkbgBorderHoverColor' : 'bkbgBorderColor';
1374 var radiusPrefix = isHover ? 'bkbgBorderHoverRadius' : 'bkbgBorderRadius';
1375 var shadowPrefix = isHover ? 'bkbgShadowHover' : 'bkbgShadow';
1376
1377 return el(Fragment, {},
1378 // Normal / Hover toggle
1379 el('div', { className: 'bkbg-bg-state-tabs' },
1380 el(Button, {
1381 className: 'bkbg-bg-state-tab' + (stateTab === 'normal' ? ' is-active' : ''),
1382 onClick: function () { setStateTab('normal'); }
1383 }, __('Normal', 'blockenberg')),
1384 el(Button, {
1385 className: 'bkbg-bg-state-tab' + (stateTab === 'hover' ? ' is-active' : ''),
1386 onClick: function () { setStateTab('hover'); }
1387 }, __('Hover', 'blockenberg'))
1388 ),
1389
1390 // Border Type
1391 el(DropdownRow, {
1392 label: __('Border Type', 'blockenberg'),
1393 value: borderType,
1394 options: BORDER_TYPE_OPTIONS,
1395 showDevice: false,
1396 onChange: function (val) {
1397 var u = {};
1398 u[typeKey] = val;
1399 setAttributes(u);
1400 }
1401 }),
1402
1403 // Border Width (only when type is set and not none)
1404 showBorderControls && el(SpacingControl, {
1405 label: __('Border Width', 'blockenberg'),
1406 attrPrefix: widthPrefix,
1407 attributes: attributes,
1408 setAttributes: setAttributes,
1409 device: activeDevice,
1410 onDeviceChange: setActiveDevice
1411 }),
1412
1413 // Border Color (only when type is set and not none)
1414 showBorderControls && el(ColorSwatchControl, {
1415 label: __('Border Color', 'blockenberg'),
1416 color: attributes[colorKey] || '',
1417 onChange: function (val) {
1418 var u = {};
1419 u[colorKey] = val;
1420 setAttributes(u);
1421 }
1422 }),
1423
1424 // Border Radius (always visible)
1425 el(SpacingControl, {
1426 label: __('Border Radius', 'blockenberg'),
1427 attrPrefix: radiusPrefix,
1428 attributes: attributes,
1429 setAttributes: setAttributes,
1430 device: activeDevice,
1431 onDeviceChange: setActiveDevice
1432 }),
1433
1434 // Box Shadow
1435 el('div', { className: 'bkbg-adv-separator' }),
1436 el(BoxShadowControl, {
1437 prefix: shadowPrefix,
1438 attributes: attributes,
1439 setAttributes: setAttributes
1440 }),
1441
1442 );
1443 }
1444
1445 /* =====================================================
1446 Responsive Panel — device visibility toggles
1447 ===================================================== */
1448
1449 function ResponsivePanel(props) {
1450 var attributes = props.attributes;
1451 var setAttributes = props.setAttributes;
1452
1453 var toggles = [
1454 { label: 'Hide On Desktop', attr: 'bkbgHideDesktop' },
1455 { label: 'Hide On Tablet Portrait', attr: 'bkbgHideTablet' },
1456 { label: 'Hide On Mobile Portrait', attr: 'bkbgHideMobile' }
1457 ];
1458
1459 return el(Fragment, {},
1460 el('p', {
1461 className: 'bkbg-responsive-notice',
1462 style: { fontStyle: 'italic', color: '#666', fontSize: '13px', lineHeight: '1.5', margin: '0 0 16px' }
1463 },
1464 'Responsive visibility will take effect only on ',
1465 el('strong', { style: { color: '#1e40af' } }, 'preview mode'),
1466 ' or live page, and not while editing in the editor.'
1467 ),
1468 toggles.map(function (item) {
1469 var isHidden = !!attributes[item.attr];
1470 return el('div', {
1471 key: item.attr,
1472 className: 'bkbg-responsive-row'
1473 },
1474 el('span', { className: 'bkbg-responsive-label' }, item.label),
1475 el('div', {
1476 className: 'bkbg-responsive-toggle' + (isHidden ? ' is-active' : ''),
1477 onClick: function () {
1478 var obj = {};
1479 obj[item.attr] = !isHidden;
1480 setAttributes(obj);
1481 },
1482 role: 'button',
1483 tabIndex: 0
1484 },
1485 el('div', { className: 'bkbg-toggle-track' },
1486 el('div', { className: 'bkbg-toggle-thumb' })
1487 ),
1488 el('span', { className: 'bkbg-toggle-label' }, isHidden ? 'Hide' : 'Show')
1489 )
1490 );
1491 })
1492 );
1493 }
1494
1495 /* =====================================================
1496 Layout Panel — combines all controls
1497 ===================================================== */
1498
1499 function LayoutPanel(props) {
1500 var attributes = props.attributes;
1501 var setAttributes = props.setAttributes;
1502 var _dev = useState(getEditorPreviewDevice());
1503 var activeDevice = _dev[0];
1504 var setActiveDevice = _dev[1];
1505
1506 useEffect(function () {
1507 if (!wp.data || typeof wp.data.subscribe !== 'function') return;
1508
1509 var unsubscribe = wp.data.subscribe(function () {
1510 var editorDevice = getEditorPreviewDevice();
1511 setActiveDevice(function (current) {
1512 return current === editorDevice ? current : editorDevice;
1513 });
1514 });
1515
1516 return function () {
1517 if (typeof unsubscribe === 'function') unsubscribe();
1518 };
1519 }, []);
1520
1521 return el(Fragment, {},
1522 el(SpacingControl, {
1523 label: __('Margin', 'blockenberg'),
1524 attrPrefix: 'bkbgMargin',
1525 attributes: attributes,
1526 setAttributes: setAttributes,
1527 device: activeDevice,
1528 onDeviceChange: setActiveDevice
1529 }),
1530 el(SpacingControl, {
1531 label: __('Padding', 'blockenberg'),
1532 attrPrefix: 'bkbgPadding',
1533 attributes: attributes,
1534 setAttributes: setAttributes,
1535 device: activeDevice,
1536 onDeviceChange: setActiveDevice
1537 }),
1538 el('div', { className: 'bkbg-adv-separator' }),
1539 el(ZIndexControl, {
1540 attributes: attributes,
1541 setAttributes: setAttributes,
1542 device: activeDevice,
1543 onDeviceChange: setActiveDevice
1544 }),
1545 el('div', { className: 'bkbg-adv-separator' }),
1546 el(CssControl, {
1547 label: __('CSS ID', 'blockenberg'),
1548 attrKey: 'bkbgCssId',
1549 attributes: attributes,
1550 setAttributes: setAttributes
1551 }),
1552 el(CssControl, {
1553 label: __('CSS Classes', 'blockenberg'),
1554 attrKey: 'bkbgCssClasses',
1555 attributes: attributes,
1556 setAttributes: setAttributes
1557 })
1558 );
1559 }
1560
1561 /* =====================================================
1562 HOC – inject General / Advanced tabs
1563 ===================================================== */
1564
1565 var withAdvancedInspector = createHigherOrderComponent(function (BlockEdit) {
1566 return function (props) {
1567 if (!props.name || props.name.indexOf('blockenberg/') !== 0) {
1568 return el(BlockEdit, props);
1569 }
1570
1571 var _tab = useState('general');
1572 var activeTab = _tab[0];
1573 var setActiveTab = _tab[1];
1574 var wrapRef = useRef(null);
1575 var attrsRef = useRef(props.attributes);
1576 var previewDeviceRef = useRef(getEditorPreviewDevice());
1577
1578 useEffect(function () {
1579 attrsRef.current = props.attributes;
1580 }, [props.attributes]);
1581
1582 useEffect(function () {
1583 setActiveTab('general');
1584 }, [props.clientId]);
1585
1586 useEffect(function () {
1587 var node = wrapRef.current;
1588 if (!node) return;
1589 var inspector = node.closest('.block-editor-block-inspector');
1590 if (!inspector) return;
1591 inspector.setAttribute('data-bkbg-tab', activeTab);
1592 return function () {
1593 inspector.removeAttribute('data-bkbg-tab');
1594 };
1595 }, [activeTab, props.isSelected]);
1596
1597 // Live preview: apply responsive layout styles to the block wrapper in editor
1598 useEffect(function () {
1599 // Small delay to ensure block DOM is rendered
1600 var timer = setTimeout(function () {
1601 applyEditorStyles(props.clientId, props.attributes);
1602 }, 50);
1603 return function () {
1604 clearTimeout(timer);
1605 // Clean up background <style> tag when unmounting
1606 var styleId = 'bkbg-bg-' + props.clientId;
1607 var styleEl = document.getElementById(styleId);
1608 if (styleEl) styleEl.remove();
1609 // Also check inside iframe
1610 var iframe = document.querySelector('iframe[name="editor-canvas"]');
1611 if (iframe && iframe.contentDocument) {
1612 var iframeStyle = iframe.contentDocument.getElementById(styleId);
1613 if (iframeStyle) iframeStyle.remove();
1614 }
1615 };
1616 }, [props.clientId, props.attributes]);
1617
1618 useEffect(function () {
1619 if (!wp.data || typeof wp.data.subscribe !== 'function') return;
1620
1621 var unsubscribe = wp.data.subscribe(function () {
1622 var currentDevice = getEditorPreviewDevice();
1623 if (currentDevice === previewDeviceRef.current) return;
1624
1625 previewDeviceRef.current = currentDevice;
1626 applyEditorStyles(props.clientId, attrsRef.current || {});
1627 });
1628
1629 return function () {
1630 if (typeof unsubscribe === 'function') unsubscribe();
1631 };
1632 }, [props.clientId]);
1633
1634 var tabs = [
1635 { name: 'general', title: __('General', 'blockenberg') },
1636 { name: 'advanced', title: __('Advanced', 'blockenberg') }
1637 ];
1638
1639 return el(Fragment, {},
1640 el(InspectorControls, {},
1641 el('div', {
1642 ref: wrapRef,
1643 className: 'bkbg-inspector-tabs-wrap'
1644 },
1645 el(TabPanel, {
1646 key: 'bkbg-tabs-' + props.clientId,
1647 className: 'bkbg-inspector-tab-panel',
1648 activeClass: 'is-active',
1649 tabs: tabs,
1650 onSelect: function (tabName) {
1651 setActiveTab(tabName);
1652 var node = wrapRef.current;
1653 if (node) {
1654 var insp = node.closest('.block-editor-block-inspector');
1655 if (insp) {
1656 insp.setAttribute('data-bkbg-tab', tabName);
1657 }
1658 }
1659 }
1660 }, function (tab) {
1661 if (tab.name === 'advanced') {
1662 return el('div', { className: 'bkbg-advanced-panels' },
1663 el('div', { className: 'bkbg-advanced-header' },
1664 __('Advanced Styles', 'blockenberg')
1665 ),
1666 el(PanelBody, {
1667 title: __('Layout', 'blockenberg'),
1668 initialOpen: true,
1669 className: 'bkbg-adv-panel'
1670 },
1671 el(LayoutPanel, {
1672 attributes: props.attributes,
1673 setAttributes: props.setAttributes
1674 })
1675 ),
1676 el(PanelBody, {
1677 title: __('Background', 'blockenberg'),
1678 initialOpen: false,
1679 className: 'bkbg-adv-panel'
1680 },
1681 el(BackgroundPanel, {
1682 attributes: props.attributes,
1683 setAttributes: props.setAttributes
1684 })
1685 ),
1686 el(PanelBody, {
1687 title: __('Border', 'blockenberg'),
1688 initialOpen: false,
1689 className: 'bkbg-adv-panel'
1690 },
1691 el(BorderPanel, {
1692 attributes: props.attributes,
1693 setAttributes: props.setAttributes
1694 })
1695 ),
1696 el(PanelBody, {
1697 title: __('Responsive', 'blockenberg'),
1698 initialOpen: false,
1699 className: 'bkbg-adv-panel'
1700 },
1701 el(ResponsivePanel, {
1702 attributes: props.attributes,
1703 setAttributes: props.setAttributes
1704 })
1705 )
1706 );
1707 }
1708 return null;
1709 })
1710 )
1711 ),
1712 el(BlockEdit, props)
1713 );
1714 };
1715 }, 'withAdvancedInspector');
1716
1717 addFilter(
1718 'editor.BlockEdit',
1719 'blockenberg/advanced-inspector',
1720 withAdvancedInspector
1721 );
1722
1723 /* =====================================================
1724 Helpers: compute responsive value strings for editor
1725 ===================================================== */
1726
1727 function buildValue(attrs, prefix, side, suffix) {
1728 var val = attrs[prefix + side + suffix];
1729 var unit = attrs[prefix + side + suffix + 'Unit'] || 'px';
1730 if (!val && val !== 0) return '';
1731 var v = String(val).trim();
1732 if (!v) return '';
1733 return v + unit;
1734 }
1735
1736 function getEditorSpacingValue(attrs, prefix, side, device) {
1737 if (device === 'mobile') {
1738 return buildValue(attrs, prefix, side, 'Mobile')
1739 || buildValue(attrs, prefix, side, 'Tablet')
1740 || buildValue(attrs, prefix, side, '');
1741 }
1742
1743 if (device === 'tablet') {
1744 return buildValue(attrs, prefix, side, 'Tablet')
1745 || buildValue(attrs, prefix, side, '');
1746 }
1747
1748 return buildValue(attrs, prefix, side, '');
1749 }
1750
1751 function getEditorZIndexValue(attrs, device) {
1752 if (device === 'mobile') {
1753 return attrs.bkbgZIndexMobile || attrs.bkbgZIndexTablet || attrs.bkbgZIndex || '';
1754 }
1755
1756 if (device === 'tablet') {
1757 return attrs.bkbgZIndexTablet || attrs.bkbgZIndex || '';
1758 }
1759
1760 return attrs.bkbgZIndex || '';
1761 }
1762
1763 /* =====================================================
1764 Editor live preview via useEffect in the HOC
1765 – finds block wrapper element(s) in editor DOM
1766 (handles both iframed and non-iframed editors)
1767 and applies responsive margin/padding/z-index inline.
1768 ===================================================== */
1769
1770 function findBlockNode(clientId) {
1771 var sel = '[data-block="' + clientId + '"]';
1772 // Iframed editor (Site Editor / newer WP)
1773 var iframe = document.querySelector('iframe[name="editor-canvas"]');
1774 if (iframe && iframe.contentDocument) {
1775 var n = iframe.contentDocument.querySelector(sel);
1776 if (n) return n;
1777 }
1778 // Classic / non-iframed editor
1779 return document.querySelector(sel);
1780 }
1781
1782 function getEditorStyleTargets(clientId) {
1783 var targets = [];
1784 var wrapper = findBlockNode(clientId);
1785 if (!wrapper) return targets;
1786
1787 targets.push(wrapper);
1788
1789 return targets;
1790 }
1791
1792 function setImportantStyle(node, prop, value) {
1793 if (!node || !node.style) return;
1794 if (value || value === 0) {
1795 node.style.setProperty(prop, String(value), 'important');
1796 } else {
1797 node.style.removeProperty(prop);
1798 }
1799 }
1800
1801 function getResponsiveBgProp(attrs, prefix, prop, device) {
1802 var suffix = device === 'desktop' ? '' : (device === 'tablet' ? 'Tablet' : 'Mobile');
1803 var val = attrs[prefix + prop + suffix] || '';
1804 if (!val && suffix) val = attrs[prefix + prop] || '';
1805 return val;
1806 }
1807
1808 function applyEditorSpacingStyleTag(clientId, attrs, wrapper) {
1809 if (!wrapper) return;
1810 var doc = wrapper.ownerDocument;
1811 var styleId = 'bkbg-space-' + clientId;
1812 var existing = doc.getElementById(styleId);
1813
1814 var device = getEditorPreviewDevice();
1815 var mt = getEditorSpacingValue(attrs, 'bkbgMargin', 'Top', device);
1816 var mr = getEditorSpacingValue(attrs, 'bkbgMargin', 'Right', device);
1817 var mb = getEditorSpacingValue(attrs, 'bkbgMargin', 'Bottom', device);
1818 var ml = getEditorSpacingValue(attrs, 'bkbgMargin', 'Left', device);
1819
1820 var pt = getEditorSpacingValue(attrs, 'bkbgPadding', 'Top', device);
1821 var pr = getEditorSpacingValue(attrs, 'bkbgPadding', 'Right', device);
1822 var pb = getEditorSpacingValue(attrs, 'bkbgPadding', 'Bottom', device);
1823 var pl = getEditorSpacingValue(attrs, 'bkbgPadding', 'Left', device);
1824
1825 var zi = getEditorZIndexValue(attrs, device);
1826
1827 var rules = [];
1828 if (mt) rules.push('margin-top:' + mt + ' !important');
1829 if (mr) rules.push('margin-right:' + mr + ' !important');
1830 if (mb) rules.push('margin-bottom:' + mb + ' !important');
1831 if (ml) rules.push('margin-left:' + ml + ' !important');
1832
1833 if (pt) rules.push('padding-top:' + pt + ' !important');
1834 if (pr) rules.push('padding-right:' + pr + ' !important');
1835 if (pb) rules.push('padding-bottom:' + pb + ' !important');
1836 if (pl) rules.push('padding-left:' + pl + ' !important');
1837
1838 if (zi || zi === 0 || zi === '0') {
1839 var z = String(zi).trim();
1840 if (z) {
1841 rules.push('z-index:' + z + ' !important');
1842 rules.push('position:relative !important');
1843 }
1844 }
1845
1846 if (!rules.length) {
1847 if (existing) existing.remove();
1848 return;
1849 }
1850
1851 if (!existing) {
1852 existing = doc.createElement('style');
1853 existing.id = styleId;
1854 doc.head.appendChild(existing);
1855 }
1856
1857 var sel = '[data-block="' + clientId + '"]';
1858 existing.textContent = sel + '{' + rules.join(';') + '}';
1859 }
1860
1861 function applyEditorStyles(clientId, attrs) {
1862 var nodes = getEditorStyleTargets(clientId);
1863 if (!nodes.length) return;
1864
1865 // Spacing & z-index are applied only when set in Advanced.
1866 // This avoids stripping block-provided inline styles by default.
1867 applyEditorSpacingStyleTag(clientId, attrs, nodes[0]);
1868
1869 // Background styles via <style> tag so :hover can properly override
1870 applyEditorBgStyleTag(clientId, attrs, nodes[0]);
1871 }
1872
1873 function buildBgRulesForPrefix(attrs, prefix, device) {
1874 var rules = [];
1875 var typeKey = prefix + 'Type';
1876 var bgType = attrs[typeKey] || '';
1877
1878 if (bgType === 'classic') {
1879 var cColor = attrs[prefix + 'Color'] || '';
1880 if (cColor) rules.push('background-color:' + cColor + ' !important');
1881 var cImg = getResponsiveBgProp(attrs, prefix, 'Image', device);
1882 if (cImg) {
1883 rules.push('background-image:url(' + cImg + ') !important');
1884 var pos = getResponsiveBgProp(attrs, prefix, 'Position', device);
1885 if (pos === 'custom') {
1886 var cx = getResponsiveBgProp(attrs, prefix, 'PositionCustomX', device);
1887 var cy = getResponsiveBgProp(attrs, prefix, 'PositionCustomY', device);
1888 rules.push('background-position:' + (cx ? cx + '%' : '50%') + ' ' + (cy ? cy + '%' : '50%') + ' !important');
1889 } else {
1890 rules.push('background-position:' + (pos || 'center') + ' !important');
1891 }
1892 var attach = attrs[prefix + 'Attachment'] || '';
1893 if (attach) rules.push('background-attachment:' + attach + ' !important');
1894 var repeat = getResponsiveBgProp(attrs, prefix, 'Repeat', device);
1895 if (repeat) rules.push('background-repeat:' + repeat + ' !important');
1896 var sz = getResponsiveBgProp(attrs, prefix, 'Size', device);
1897 if (sz === 'custom') {
1898 var sw = getResponsiveBgProp(attrs, prefix, 'SizeCustomW', device);
1899 var sh = getResponsiveBgProp(attrs, prefix, 'SizeCustomH', device);
1900 rules.push('background-size:' + (sw ? sw + 'px' : 'auto') + ' ' + (sh ? sh + 'px' : 'auto') + ' !important');
1901 } else {
1902 rules.push('background-size:' + (sz || 'cover') + ' !important');
1903 }
1904 }
1905 } else if (bgType === 'gradient') {
1906 var grad = getEditorGradientValue(attrs, prefix, device);
1907 if (grad) rules.push('background-image:' + grad + ' !important');
1908 }
1909 return rules;
1910 }
1911
1912 function buildBorderRules(attrs, typeKey, widthPrefix, colorKey, radiusPrefix, shadowPrefix, device) {
1913 var rules = [];
1914 var borderType = attrs[typeKey] || '';
1915
1916 if (borderType && borderType !== 'none') {
1917 rules.push('border-style:' + borderType + ' !important');
1918 var bColor = attrs[colorKey] || '';
1919 if (bColor) rules.push('border-color:' + bColor + ' !important');
1920
1921 var suffix = device === 'desktop' ? '' : (device === 'tablet' ? 'Tablet' : 'Mobile');
1922 ['Top', 'Right', 'Bottom', 'Left'].forEach(function (side) {
1923 var val = attrs[widthPrefix + side + suffix] || attrs[widthPrefix + side] || '';
1924 var unit = attrs[widthPrefix + side + suffix + 'Unit'] || attrs[widthPrefix + side + 'Unit'] || 'px';
1925 if (val !== '') rules.push('border-' + side.toLowerCase() + '-width:' + val + unit + ' !important');
1926 });
1927 } else if (borderType === 'none') {
1928 rules.push('border:none !important');
1929 }
1930
1931 // Border radius
1932 var rSuffix = device === 'desktop' ? '' : (device === 'tablet' ? 'Tablet' : 'Mobile');
1933 var rParts = {};
1934 ['Top', 'Right', 'Bottom', 'Left'].forEach(function (side) {
1935 var val = attrs[radiusPrefix + side + rSuffix] || attrs[radiusPrefix + side] || '';
1936 var unit = attrs[radiusPrefix + side + rSuffix + 'Unit'] || attrs[radiusPrefix + side + 'Unit'] || 'px';
1937 if (val !== '') rParts[side] = val + unit;
1938 });
1939 if (Object.keys(rParts).length) {
1940 var tl = rParts.Top || '0px';
1941 var tr = rParts.Right || '0px';
1942 var br = rParts.Bottom || '0px';
1943 var bl = rParts.Left || '0px';
1944 rules.push('border-radius:' + tl + ' ' + tr + ' ' + br + ' ' + bl + ' !important');
1945 }
1946
1947 // Box shadow
1948 var sh = attrs[shadowPrefix + 'H'];
1949 var sv = attrs[shadowPrefix + 'V'];
1950 var sb = attrs[shadowPrefix + 'Blur'];
1951 var ss = attrs[shadowPrefix + 'Spread'];
1952 var sc = attrs[shadowPrefix + 'Color'] || '';
1953 var sp = attrs[shadowPrefix + 'Position'] || '';
1954 if (sh !== undefined && sh !== '' || sv !== undefined && sv !== '' || sb !== undefined && sb !== '' || ss !== undefined && ss !== '' || sc) {
1955 var hv = (sh !== undefined && sh !== '') ? sh + 'px' : '0px';
1956 var vv = (sv !== undefined && sv !== '') ? sv + 'px' : '0px';
1957 var bv = (sb !== undefined && sb !== '') ? sb + 'px' : '0px';
1958 var spv = (ss !== undefined && ss !== '') ? ss + 'px' : '0px';
1959 var cv = sc || 'rgba(0,0,0,0.5)';
1960 var shadowVal = hv + ' ' + vv + ' ' + bv + ' ' + spv + ' ' + cv;
1961 if (sp === 'inset') shadowVal = 'inset ' + shadowVal;
1962 rules.push('box-shadow:' + shadowVal + ' !important');
1963 }
1964
1965 return rules;
1966 }
1967
1968 function applyEditorBgStyleTag(clientId, attrs, wrapper) {
1969 var doc = wrapper.ownerDocument;
1970 var styleId = 'bkbg-bg-' + clientId;
1971 var existing = doc.getElementById(styleId);
1972
1973 var device = getEditorPreviewDevice();
1974 var sel = '[data-block="' + clientId + '"]';
1975 var cssText = '';
1976
1977 // ── Normal rules ──
1978 var normalRules = buildBgRulesForPrefix(attrs, 'bkbgBg', device);
1979 var normalBorderRules = buildBorderRules(attrs, 'bkbgBorderType', 'bkbgBorderWidth', 'bkbgBorderColor', 'bkbgBorderRadius', 'bkbgShadow', device);
1980 var allNormal = normalRules.concat(normalBorderRules);
1981 if (allNormal.length) {
1982 cssText += sel + '{' + allNormal.join(';') + '}';
1983 }
1984
1985 // ── Hover rules ──
1986 var hoverBgRules = [];
1987 var hoverType = attrs.bkbgBgHoverType || '';
1988 if (hoverType) {
1989 hoverBgRules = buildBgRulesForPrefix(attrs, 'bkbgBgHover', device);
1990 }
1991 var hoverBorderRules = buildBorderRules(attrs, 'bkbgBorderHoverType', 'bkbgBorderHoverWidth', 'bkbgBorderHoverColor', 'bkbgBorderHoverRadius', 'bkbgShadowHover', device);
1992 var allHover = hoverBgRules.concat(hoverBorderRules);
1993
1994 if (allHover.length) {
1995 cssText += sel + ':hover{' + allHover.join(';') + '}';
1996 }
1997
1998 if (!cssText) {
1999 if (existing) existing.remove();
2000 return;
2001 }
2002
2003 if (!existing) {
2004 existing = doc.createElement('style');
2005 existing.id = styleId;
2006 doc.head.appendChild(existing);
2007 }
2008 existing.textContent = cssText;
2009 }
2010
2011 /* =====================================================
2012 Background gradient value builder for editor preview
2013 ===================================================== */
2014
2015 function getEditorGradientValue(attrs, prefix, device) {
2016 var c1 = attrs[prefix + 'GradColor1'] || '';
2017 var c2 = attrs[prefix + 'GradColor2'] || '';
2018 if (!c1 && !c2) return '';
2019 if (!c1) c1 = 'transparent';
2020 if (!c2) c2 = 'transparent';
2021
2022 var suffix = device === 'desktop' ? '' : (device === 'tablet' ? 'Tablet' : 'Mobile');
2023
2024 var loc1 = attrs[prefix + 'GradLoc1' + suffix] || attrs[prefix + 'GradLoc1'] || '';
2025 var loc2 = attrs[prefix + 'GradLoc2' + suffix] || attrs[prefix + 'GradLoc2'] || '';
2026 var angle = '';
2027 if (attrs[prefix + 'GradAngle' + suffix] !== undefined && attrs[prefix + 'GradAngle' + suffix] !== '') {
2028 angle = attrs[prefix + 'GradAngle' + suffix];
2029 } else if (attrs[prefix + 'GradAngle'] !== undefined && attrs[prefix + 'GradAngle'] !== '') {
2030 angle = attrs[prefix + 'GradAngle'];
2031 }
2032
2033 var type = attrs[prefix + 'GradType'] || 'linear';
2034
2035 var stop1 = c1 + (loc1 !== '' ? ' ' + loc1 + '%' : '');
2036 var stop2 = c2 + (loc2 !== '' ? ' ' + loc2 + '%' : '');
2037
2038 if (type === 'radial') {
2039 var gradPos = getResponsiveBgProp(attrs, prefix, 'GradPosition', device);
2040 var atPart = gradPos ? ' at ' + gradPos : '';
2041 return 'radial-gradient(circle' + atPart + ',' + stop1 + ',' + stop2 + ')';
2042 }
2043
2044 var anglePart = angle !== '' ? angle + 'deg,' : '';
2045 return 'linear-gradient(' + anglePart + stop1 + ',' + stop2 + ')';
2046 }
2047
2048 })();
2049