PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.14
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.14
2.0.14 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.14, at assets/js/inspector-tabs.js

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