PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.13
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.13
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 / blocks / row / index.js

index.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.13, at blocks/row/index.js

1,082 lines 48.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 ( function () {
2 var el = wp.element.createElement;
3 var useState = wp.element.useState;
4 var useRef = wp.element.useRef;
5 var useEffect = wp.element.useEffect;
6 var useCallback = wp.element.useCallback;
7 var Fragment = wp.element.Fragment;
8 var __ = wp.i18n.__;
9 var registerBlockType = wp.blocks.registerBlockType;
10 var InspectorControls = wp.blockEditor.InspectorControls;
11 var BlockControls = wp.blockEditor.BlockControls;
12 var useBlockProps = wp.blockEditor.useBlockProps;
13 var InnerBlocks = wp.blockEditor.InnerBlocks;
14 var useSelect = wp.data.useSelect;
15 var useDispatch = wp.data.useDispatch;
16 var PanelBody = wp.components.PanelBody;
17 var SelectControl = wp.components.SelectControl;
18 var ToggleControl = wp.components.ToggleControl;
19 var RangeControl = wp.components.RangeControl;
20 var ToolbarGroup = wp.components.ToolbarGroup;
21 var ToolbarButton = wp.components.ToolbarButton;
22 var ToolbarDropdownMenu = wp.components.ToolbarDropdownMenu;
23 var Button = wp.components.Button;
24 var ButtonGroup = wp.components.ButtonGroup;
25 var Placeholder = wp.components.Placeholder;
26 var Popover = wp.components.Popover;
27
28 // Gap token scale (legacy support in saved attributes/classes)
29
30 // Responsive mode options
31 var responsiveModes = [
32 { value: 'desktop', label: __('Desktop', 'blockenberg'), icon: 'desktop' },
33 { value: 'tablet', label: __('Tablet', 'blockenberg'), icon: 'tablet' },
34 { value: 'mobile', label: __('Mobile', 'blockenberg'), icon: 'smartphone' }
35 ];
36
37 var spacingUnitOptions = [
38 { label: 'px', value: 'px' },
39 { label: '%', value: '%' },
40 { label: 'em', value: 'em' },
41 { label: 'rem', value: 'rem' },
42 { label: 'vw', value: 'vw' },
43 { label: 'vh', value: 'vh' },
44 { label: __('Custom', 'blockenberg'), value: 'custom' }
45 ];
46
47 function getSpacingSideValue(attrs, type, side) {
48 var cap = side.charAt(0).toUpperCase() + side.slice(1);
49 var value = attrs[type + cap];
50 var unit = attrs[type + cap + 'Unit'] || 'px';
51 var customUnit = attrs[type + cap + 'CustomUnit'] || '';
52
53 if (value === undefined || value === null) return undefined;
54 var raw = String(value).trim();
55 if (!raw) return undefined;
56
57 if (unit === 'custom') {
58 return customUnit ? raw + customUnit : raw;
59 }
60
61 return raw + unit;
62 }
63
64 function getSpacingStyles(attrs) {
65 return {
66 paddingTop: getSpacingSideValue(attrs, 'padding', 'top'),
67 paddingRight: getSpacingSideValue(attrs, 'padding', 'right'),
68 paddingBottom: getSpacingSideValue(attrs, 'padding', 'bottom'),
69 paddingLeft: getSpacingSideValue(attrs, 'padding', 'left'),
70 marginTop: getSpacingSideValue(attrs, 'margin', 'top'),
71 marginRight: getSpacingSideValue(attrs, 'margin', 'right'),
72 marginBottom: getSpacingSideValue(attrs, 'margin', 'bottom'),
73 marginLeft: getSpacingSideValue(attrs, 'margin', 'left')
74 };
75 }
76
77 // Column presets for quick setup
78 var columnPresets = [
79 { label: '1 Column (100)', value: '1-full', widths: [100], icon: '' },
80 { label: '2 Columns (50/50)', value: '2-equal', widths: [50, 50], icon: '⬜⬜' },
81 { label: '3 Columns (33/33/33)', value: '3-equal', widths: [33.33, 33.33, 33.34], icon: '⬜⬜⬜' },
82 { label: '4 Columns (25/25/25/25)', value: '4-equal', widths: [25, 25, 25, 25], icon: '⬜⬜⬜⬜' },
83 { label: '2 Columns (66/33)', value: '2-wide-left', widths: [66.66, 33.34], icon: '⬛⬜' },
84 { label: '2 Columns (33/66)', value: '2-wide-right', widths: [33.34, 66.66], icon: '⬜⬛' },
85 { label: '3 Columns (25/50/25)', value: '3-wide-center', widths: [25, 50, 25], icon: '⬜⬛⬜' }
86 ];
87
88 // Row icon
89 var rowIcon = el('svg', {
90 width: 24,
91 height: 24,
92 viewBox: '0 0 24 24',
93 xmlns: 'http://www.w3.org/2000/svg'
94 },
95 el('path', {
96 d: 'M4 4h16v16H4V4zm2 2v12h5V6H6zm7 0v12h5V6h-5z',
97 fill: 'currentColor'
98 })
99 );
100
101 // Global cleanup function for resize state
102 function cleanupAllResizeStates() {
103 var allResizing = document.querySelectorAll('.bkbg-row--is-resizing');
104 allResizing.forEach(function(el) {
105 el.classList.remove('bkbg-row--is-resizing');
106 });
107 // Also clean in iframe if exists
108 var editorCanvas = document.querySelector('iframe[name="editor-canvas"]');
109 if (editorCanvas && editorCanvas.contentDocument) {
110 var iframeResizing = editorCanvas.contentDocument.querySelectorAll('.bkbg-row--is-resizing');
111 iframeResizing.forEach(function(el) {
112 el.classList.remove('bkbg-row--is-resizing');
113 });
114 }
115 }
116
117 // Resize Handle Component
118 function ResizeHandle(props) {
119 var onResizeStart = props.onResizeStart;
120 var onResize = props.onResize;
121 var onResizeEnd = props.onResizeEnd;
122 var leftIndex = props.leftIndex;
123 var minPct = props.minPct;
124 var leftWidth = props.leftWidth;
125 var rightWidth = props.rightWidth;
126 var rowClientId = props.rowClientId;
127
128 var handleRef = useRef(null);
129 var rowRef = useRef(null);
130 var isDragging = useRef(false);
131 var startXRef = useRef(0);
132 var startLeftRef = useRef(0);
133 var startRightRef = useRef(0);
134 var rowWidthRef = useRef(0);
135 var cleanupFnRef = useRef(null);
136 var documentRef = useRef(null);
137
138 // Cleanup on unmount or when component loses focus
139 useEffect(function() {
140 // Get the document (might be inside iframe)
141 var ownerDoc = handleRef.current ? handleRef.current.ownerDocument : document;
142 var ownerWin = ownerDoc.defaultView || window;
143 documentRef.current = ownerDoc;
144
145 // Global mouseup handler to catch any missed events
146 function globalMouseUp() {
147 if (isDragging.current) {
148 isDragging.current = false;
149 cleanupAllResizeStates();
150 onResizeEnd();
151 }
152 }
153
154 // Also cleanup on window blur (user switched tabs/apps)
155 function windowBlur() {
156 if (isDragging.current) {
157 isDragging.current = false;
158 cleanupAllResizeStates();
159 onResizeEnd();
160 }
161 }
162
163 // Listen on both the owner document and window
164 ownerDoc.addEventListener('mouseup', globalMouseUp);
165 ownerWin.addEventListener('blur', windowBlur);
166 // Also on parent window if in iframe
167 if (ownerWin !== window) {
168 window.addEventListener('mouseup', globalMouseUp);
169 window.addEventListener('blur', windowBlur);
170 }
171
172 return function() {
173 ownerDoc.removeEventListener('mouseup', globalMouseUp);
174 ownerWin.removeEventListener('blur', windowBlur);
175 if (ownerWin !== window) {
176 window.removeEventListener('mouseup', globalMouseUp);
177 window.removeEventListener('blur', windowBlur);
178 }
179 // Cleanup on unmount
180 cleanupAllResizeStates();
181 };
182 }, [onResizeEnd]);
183
184 var onMouseDown = useCallback(function (e) {
185 e.preventDefault();
186 e.stopPropagation();
187
188 var handle = handleRef.current;
189 if (!handle) return;
190
191 var row = handle.closest('.bkbg-row');
192 if (!row) return;
193
194 // Get the correct document (handles iframe case)
195 var ownerDoc = handle.ownerDocument || document;
196
197 // Store row reference for cleanup
198 rowRef.current = row;
199 isDragging.current = true;
200 rowWidthRef.current = row.offsetWidth;
201 startXRef.current = e.clientX;
202 startLeftRef.current = leftWidth;
203 startRightRef.current = rightWidth;
204
205 onResizeStart();
206
207 // Add dragging class to row
208 row.classList.add('bkbg-row--is-resizing');
209
210 function onMouseMove(moveEvent) {
211 if (!isDragging.current) return;
212
213 var deltaPx = moveEvent.clientX - startXRef.current;
214 var deltaPct = (deltaPx / rowWidthRef.current) * 100;
215
216 // Apply shift for snap to 1%
217 if (moveEvent.shiftKey) {
218 deltaPct = Math.round(deltaPct);
219 }
220
221 var newLeft = startLeftRef.current + deltaPct;
222 var newRight = startRightRef.current - deltaPct;
223
224 // Enforce minimum width
225 if (newLeft < minPct) {
226 newLeft = minPct;
227 newRight = startLeftRef.current + startRightRef.current - minPct;
228 }
229 if (newRight < minPct) {
230 newRight = minPct;
231 newLeft = startLeftRef.current + startRightRef.current - minPct;
232 }
233
234 // Round to 1 decimal place
235 newLeft = Math.round(newLeft * 10) / 10;
236 newRight = Math.round(newRight * 10) / 10;
237
238 onResize(leftIndex, newLeft, newRight);
239 }
240
241 function onMouseUp() {
242 isDragging.current = false;
243
244 // Remove dragging class using stored reference
245 if (rowRef.current) {
246 rowRef.current.classList.remove('bkbg-row--is-resizing');
247 rowRef.current = null;
248 }
249
250 // Global cleanup for safety
251 cleanupAllResizeStates();
252
253 onResizeEnd();
254 ownerDoc.removeEventListener('mousemove', onMouseMove);
255 ownerDoc.removeEventListener('mouseup', onMouseUp);
256 }
257
258 // Store cleanup function
259 cleanupFnRef.current = onMouseUp;
260
261 ownerDoc.addEventListener('mousemove', onMouseMove);
262 ownerDoc.addEventListener('mouseup', onMouseUp);
263 }, [leftWidth, rightWidth, minPct, leftIndex, onResize, onResizeEnd, onResizeStart]);
264
265 // Double-click to equalize pair
266 var onDoubleClick = useCallback(function (e) {
267 e.preventDefault();
268 e.stopPropagation();
269 var avg = (leftWidth + rightWidth) / 2;
270 avg = Math.round(avg * 10) / 10;
271 onResize(leftIndex, avg, avg);
272 }, [leftWidth, rightWidth, leftIndex, onResize]);
273
274 return el('div', {
275 ref: handleRef,
276 className: 'bkbg-row__resize-handle',
277 onMouseDown: onMouseDown,
278 onDoubleClick: onDoubleClick,
279 title: __('Drag to resize. Double-click to equalize.', 'blockenberg')
280 },
281 el('div', { className: 'bkbg-row__resize-handle-line' }),
282 el('div', { className: 'bkbg-row__resize-indicator' },
283 el('span', { className: 'bkbg-row__resize-pct bkbg-row__resize-pct--left' },
284 Math.round(leftWidth) + '%'
285 ),
286 el('span', { className: 'bkbg-row__resize-pct bkbg-row__resize-pct--right' },
287 Math.round(rightWidth) + '%'
288 )
289 )
290 );
291 }
292
293 registerBlockType('blockenberg/row', {
294 title: __('Row', 'blockenberg'),
295 icon: rowIcon,
296 category: 'bkbg-layout',
297 parent: ['blockenberg/section'],
298 description: __('A row container for columns with drag-resize support.', 'blockenberg'),
299
300 edit: function (props) {
301 var attributes = props.attributes;
302 var setAttributes = props.setAttributes;
303 var clientId = props.clientId;
304 var a = attributes;
305
306 var isResizingState = useState(false);
307 var isResizing = isResizingState[0];
308 var setIsResizing = isResizingState[1];
309
310 var hasSelectedLayoutState = useState(false);
311 var hasSelectedLayout = hasSelectedLayoutState[0];
312 var setHasSelectedLayout = hasSelectedLayoutState[1];
313
314 var isStructureOpenState = useState(false);
315 var isStructureOpen = isStructureOpenState[0];
316 var setIsStructureOpen = isStructureOpenState[1];
317
318 var updateBlockAttributes = useDispatch('core/block-editor').updateBlockAttributes;
319 var selectBlock = useDispatch('core/block-editor').selectBlock;
320 var insertBlock = useDispatch('core/block-editor').insertBlock;
321 var replaceInnerBlocks = useDispatch('core/block-editor').replaceInnerBlocks;
322 var removeBlock = useDispatch('core/block-editor').removeBlock;
323 var moveBlockToPosition = useDispatch('core/block-editor').moveBlockToPosition;
324 var createBlock = wp.blocks.createBlock;
325
326 function openRowSettings(e) {
327 if (e && e.preventDefault) e.preventDefault();
328 if (e && e.stopPropagation) e.stopPropagation();
329
330 if (typeof selectBlock === 'function') {
331 selectBlock(clientId);
332 } else {
333 try {
334 wp.data.dispatch('core/block-editor').selectBlock(clientId);
335 } catch (err) {}
336 }
337
338 // Try to open the block settings sidebar (works in Post Editor; best-effort elsewhere)
339 try {
340 var editPostDispatch = wp.data.dispatch('core/edit-post');
341 if (editPostDispatch && typeof editPostDispatch.openGeneralSidebar === 'function') {
342 editPostDispatch.openGeneralSidebar('edit-post/block');
343 }
344 } catch (err) {}
345
346 try {
347 var editSiteDispatch = wp.data.dispatch('core/edit-site');
348 if (editSiteDispatch && typeof editSiteDispatch.openGeneralSidebar === 'function') {
349 editSiteDispatch.openGeneralSidebar('edit-site/block-inspector');
350 }
351 } catch (err) {}
352
353 try {
354 var interfaceDispatch = wp.data.dispatch('core/interface');
355 if (interfaceDispatch && typeof interfaceDispatch.enableComplementaryArea === 'function') {
356 interfaceDispatch.enableComplementaryArea('core/edit-post', 'edit-post/block');
357 }
358 } catch (err) {}
359 }
360
361 // Get inner blocks (columns)
362 var innerBlocksData = useSelect(function (select) {
363 var innerBlocks = select('core/block-editor').getBlocks(clientId);
364 return innerBlocks.map(function (block) {
365 return {
366 clientId: block.clientId,
367 widths: block.attributes.widths || { desktop: 50, tablet: 50, mobile: 100 },
368 uid: block.attributes.uid
369 };
370 });
371 }, [clientId]);
372
373 // Get parent section info and row position
374 var parentSectionData = useSelect(function (select) {
375 var parents = select('core/block-editor').getBlockParents(clientId);
376 for (var i = parents.length - 1; i >= 0; i--) {
377 var parent = select('core/block-editor').getBlock(parents[i]);
378 if (parent && parent.name === 'blockenberg/section') {
379 var siblingRows = parent.innerBlocks || [];
380 var rowIndex = siblingRows.findIndex(function (block) { return block.clientId === clientId; });
381 return {
382 parentClientId: parents[i],
383 rowIndex: rowIndex,
384 rowCount: siblingRows.length
385 };
386 }
387 }
388 return { parentClientId: null, rowIndex: -1, rowCount: 0 };
389 }, [clientId]);
390
391 var columnCount = innerBlocksData.length;
392
393 // Check if we need to show layout selector
394 var showLayoutSelector = columnCount === 0 && !hasSelectedLayout;
395
396 // Select a preset layout
397 function selectLayout(preset) {
398 var presetData = columnPresets.find(function (p) { return p.value === preset; });
399 if (!presetData) return;
400
401 var columns = presetData.widths.map(function (width, index) {
402 return createBlock('blockenberg/column', {
403 widths: { desktop: width, tablet: width, mobile: 100 },
404 uid: 'col-' + Date.now() + '-' + index
405 });
406 });
407
408 replaceInnerBlocks(clientId, columns);
409 setAttributes({ columnsCount: presetData.widths.length });
410 setHasSelectedLayout(true);
411 }
412
413 // Get widths for current responsive mode
414 function getColumnWidth(columnData, mode) {
415 var widths = columnData.widths || {};
416 // Fallback chain: requested mode -> tablet -> desktop
417 if (mode === 'mobile') {
418 return widths.mobile !== undefined ? widths.mobile :
419 (widths.tablet !== undefined ? widths.tablet :
420 (widths.desktop !== undefined ? widths.desktop : 50));
421 }
422 if (mode === 'tablet') {
423 return widths.tablet !== undefined ? widths.tablet :
424 (widths.desktop !== undefined ? widths.desktop : 50);
425 }
426 return widths.desktop !== undefined ? widths.desktop : 50;
427 }
428
429 // Get all current widths
430 function getCurrentWidths() {
431 return innerBlocksData.map(function (col) {
432 return getColumnWidth(col, a.responsiveMode);
433 });
434 }
435
436 function setWidthLabelsVisibilityDuringResize(show) {
437 var selector = '[data-block="' + clientId + '"] .bkbg-column__width-label';
438
439 function updateInDocument(doc) {
440 if (!doc || !doc.querySelectorAll) return;
441 var labels = doc.querySelectorAll(selector);
442 labels.forEach(function (label) {
443 if (show) {
444 label.style.opacity = '1';
445 } else {
446 label.style.opacity = '';
447 }
448 });
449 }
450
451 updateInDocument(document);
452
453 var editorCanvas = document.querySelector('iframe[name="editor-canvas"]');
454 if (editorCanvas && editorCanvas.contentDocument) {
455 updateInDocument(editorCanvas.contentDocument);
456 }
457 }
458
459 // Handle resize start
460 function handleResizeStart() {
461 setIsResizing(true);
462 setWidthLabelsVisibilityDuringResize(true);
463 }
464
465 // Handle resize - update attributes immediately
466 function handleResize(leftIndex, newLeftPct, newRightPct) {
467 var mode = a.responsiveMode;
468 var leftCol = innerBlocksData[leftIndex];
469 var rightCol = innerBlocksData[leftIndex + 1];
470
471 if (leftCol) {
472 var leftWidths = Object.assign({}, leftCol.widths || { desktop: 50, tablet: 50, mobile: 100 });
473 leftWidths[mode] = newLeftPct;
474 updateBlockAttributes(leftCol.clientId, { widths: leftWidths });
475 }
476
477 if (rightCol) {
478 var rightWidths = Object.assign({}, rightCol.widths || { desktop: 50, tablet: 50, mobile: 100 });
479 rightWidths[mode] = newRightPct;
480 updateBlockAttributes(rightCol.clientId, { widths: rightWidths });
481 }
482 }
483
484 // Handle resize end
485 function handleResizeEnd() {
486 setIsResizing(false);
487 setWidthLabelsVisibilityDuringResize(false);
488 // Ensure CSS class is also cleaned up
489 cleanupAllResizeStates();
490 }
491
492 // Add column - rebuild all columns with new widths
493 function addColumn() {
494 var newCount = columnCount + 1;
495 if (newCount > 6) return;
496
497 // Calculate equal widths
498 var equalWidth = Math.round((100 / newCount) * 10) / 10;
499 var lastWidth = 100 - (equalWidth * (newCount - 1));
500 lastWidth = Math.round(lastWidth * 10) / 10;
501
502 // Get existing column inner blocks content
503 var existingBlocks = wp.data.select('core/block-editor').getBlocks(clientId);
504
505 // Create new columns array
506 var newColumns = [];
507
508 // Recreate existing columns with new widths
509 for (var i = 0; i < existingBlocks.length; i++) {
510 var existingCol = existingBlocks[i];
511 var width = (i === newCount - 1) ? lastWidth : equalWidth;
512
513 var newCol = createBlock('blockenberg/column', {
514 widths: { desktop: width, tablet: width, mobile: 100 },
515 uid: existingCol.attributes.uid || ('col-' + Date.now() + '-' + i),
516 paddingInner: existingCol.attributes.paddingInner || 'none'
517 }, existingCol.innerBlocks);
518
519 newCol.attributes = Object.assign({}, existingCol.attributes || {}, newCol.attributes || {});
520
521 newColumns.push(newCol);
522 }
523
524 // Add new empty column
525 var newColWidth = (newCount - 1 === columnCount) ? lastWidth : equalWidth;
526 var newColumn = createBlock('blockenberg/column', {
527 widths: { desktop: newColWidth, tablet: newColWidth, mobile: 100 },
528 uid: 'col-' + Date.now()
529 });
530 newColumns.push(newColumn);
531
532 // Replace all inner blocks
533 replaceInnerBlocks(clientId, newColumns, false);
534 setAttributes({ columnsCount: newCount });
535 }
536
537 // Equalize all columns
538 function equalizeColumns() {
539 if (columnCount === 0) return;
540
541 var mode = a.responsiveMode;
542 var equalWidth = Math.round((100 / columnCount) * 100) / 100;
543
544 // Adjust last column to ensure sum is exactly 100
545 var widthsArray = [];
546 for (var i = 0; i < columnCount - 1; i++) {
547 widthsArray.push(equalWidth);
548 }
549 widthsArray.push(100 - (equalWidth * (columnCount - 1)));
550
551 innerBlocksData.forEach(function (col, index) {
552 var newWidths = Object.assign({}, col.widths);
553 newWidths[mode] = widthsArray[index];
554 updateBlockAttributes(col.clientId, { widths: newWidths });
555 });
556 }
557
558 // Insert column at specific position (before/after index)
559 function insertColumnAt(index, position) {
560 if (columnCount >= 6) return;
561
562 var newCount = columnCount + 1;
563 var equalWidth = Math.round((100 / newCount) * 10) / 10;
564
565 // Get existing column inner blocks content
566 var existingBlocks = wp.data.select('core/block-editor').getBlocks(clientId);
567
568 // Determine insert position
569 var insertIndex = position === 'before' ? index : index + 1;
570
571 // Create new columns array with recalculated widths
572 var newColumns = [];
573 var colIndex = 0;
574
575 for (var i = 0; i < newCount; i++) {
576 var width = equalWidth;
577 if (i === newCount - 1) {
578 width = 100 - (equalWidth * (newCount - 1));
579 width = Math.round(width * 10) / 10;
580 }
581
582 if (i === insertIndex) {
583 // Insert new column here
584 var newCol = createBlock('blockenberg/column', {
585 widths: { desktop: width, tablet: width, mobile: 100 },
586 uid: 'col-' + Date.now()
587 });
588 newColumns.push(newCol);
589 } else {
590 // Use existing column
591 var existingCol = existingBlocks[colIndex];
592 if (existingCol) {
593 var recreatedCol = createBlock('blockenberg/column', {
594 widths: { desktop: width, tablet: width, mobile: 100 },
595 uid: existingCol.attributes.uid || ('col-' + Date.now() + '-' + colIndex),
596 paddingInner: existingCol.attributes.paddingInner || 'none'
597 }, existingCol.innerBlocks);
598
599 recreatedCol.attributes = Object.assign({}, existingCol.attributes || {}, recreatedCol.attributes || {});
600 newColumns.push(recreatedCol);
601 colIndex++;
602 }
603 }
604 }
605
606 replaceInnerBlocks(clientId, newColumns, false);
607 setAttributes({ columnsCount: newCount });
608 }
609
610 // Move column left or right
611 function moveColumn(index, direction) {
612 var newIndex = direction === 'left' ? index - 1 : index + 1;
613
614 // Check bounds
615 if (newIndex < 0 || newIndex >= columnCount) return;
616
617 // Get the column block to move
618 var blocks = wp.data.select('core/block-editor').getBlocks(clientId);
619 var blockToMove = blocks[index];
620
621 if (blockToMove) {
622 moveBlockToPosition(blockToMove.clientId, clientId, clientId, newIndex);
623 }
624 }
625
626 // Remove column at index
627 function removeColumnAt(index) {
628 if (columnCount <= 1) return;
629
630 var blocks = wp.data.select('core/block-editor').getBlocks(clientId);
631 var blockToRemove = blocks[index];
632
633 if (blockToRemove) {
634 var newCount = columnCount - 1;
635 var equalWidth = Math.round((100 / newCount) * 10) / 10;
636
637 // Rebuild columns without the removed one
638 var newColumns = [];
639 var newIndex = 0;
640
641 for (var i = 0; i < blocks.length; i++) {
642 if (i === index) continue;
643
644 var existingCol = blocks[i];
645 var width = equalWidth;
646 if (newIndex === newCount - 1) {
647 width = 100 - (equalWidth * (newCount - 1));
648 width = Math.round(width * 10) / 10;
649 }
650
651 var recreatedCol = createBlock('blockenberg/column', {
652 widths: { desktop: width, tablet: width, mobile: 100 },
653 uid: existingCol.attributes.uid || ('col-' + Date.now() + '-' + newIndex),
654 paddingInner: existingCol.attributes.paddingInner || 'none'
655 }, existingCol.innerBlocks);
656
657 recreatedCol.attributes = Object.assign({}, existingCol.attributes || {}, recreatedCol.attributes || {});
658 newColumns.push(recreatedCol);
659 newIndex++;
660 }
661
662 replaceInnerBlocks(clientId, newColumns, false);
663 setAttributes({ columnsCount: newCount });
664 }
665 }
666
667 // ========================================
668 // Row Management Functions
669 // ========================================
670
671 // Insert new row above or below current row
672 function insertRowAt(position) {
673 if (!parentSectionData.parentClientId) return;
674
675 var insertIndex = position === 'before'
676 ? parentSectionData.rowIndex
677 : parentSectionData.rowIndex + 1;
678
679 var newRow = createBlock('blockenberg/row', { columnsCount: 1 }, [
680 createBlock('blockenberg/column', {
681 widths: { desktop: 100, tablet: 100, mobile: 100 },
682 uid: 'col-' + Date.now() + '-0'
683 })
684 ]);
685
686 insertBlock(newRow, insertIndex, parentSectionData.parentClientId);
687 }
688
689 // Move row up or down within section
690 function moveRow(direction) {
691 if (!parentSectionData.parentClientId) return;
692
693 var newIndex = direction === 'up'
694 ? parentSectionData.rowIndex - 1
695 : parentSectionData.rowIndex + 1;
696
697 // Check bounds
698 if (newIndex < 0 || newIndex >= parentSectionData.rowCount) return;
699
700 moveBlockToPosition(
701 clientId,
702 parentSectionData.parentClientId,
703 parentSectionData.parentClientId,
704 newIndex
705 );
706 }
707
708 // Apply preset
709 function applyPreset(preset) {
710 var presetData = columnPresets.find(function (p) { return p.value === preset; });
711 if (!presetData) return;
712
713 var targetCount = presetData.widths.length;
714 var mode = a.responsiveMode;
715
716 // Add or remove columns to match preset
717 if (columnCount < targetCount) {
718 // Add columns
719 for (var i = columnCount; i < targetCount; i++) {
720 var newColumn = createBlock('blockenberg/column', {
721 widths: {
722 desktop: presetData.widths[i],
723 tablet: presetData.widths[i],
724 mobile: 100
725 },
726 uid: 'col-' + Date.now() + '-' + i
727 });
728 insertBlock(newColumn, i, clientId);
729 }
730 }
731
732 if (columnCount > targetCount) {
733 // Remove extra columns from the end
734 var blocksToTrim = wp.data.select('core/block-editor').getBlocks(clientId);
735 for (var r = blocksToTrim.length - 1; r >= targetCount; r--) {
736 if (blocksToTrim[r] && blocksToTrim[r].clientId) {
737 removeBlock(blocksToTrim[r].clientId);
738 }
739 }
740 }
741
742 // Update existing column widths
743 setTimeout(function () {
744 var blocks = wp.data.select('core/block-editor').getBlocks(clientId);
745 blocks.slice(0, targetCount).forEach(function (block, index) {
746 var newWidths = Object.assign({}, block.attributes.widths || {});
747 newWidths[mode] = presetData.widths[index];
748 updateBlockAttributes(block.clientId, { widths: newWidths });
749 });
750 }, 100);
751
752 setAttributes({ columnsCount: targetCount });
753 }
754
755 var blockProps = useBlockProps({
756 className: [
757 'bkbg-row',
758 'bkbg-row--gap-' + a.gap,
759 'bkbg-row--mode-' + a.responsiveMode,
760 a.stackOnMobile ? 'bkbg-row--stack-mobile' : '',
761 isResizing ? 'bkbg-row--is-resizing' : ''
762 ].filter(Boolean).join(' '),
763 style: getSpacingStyles(a),
764 'data-responsive-mode': a.responsiveMode
765 });
766
767 function renderSpacingControl(type, label) {
768 var sides = [
769 { key: 'Top', label: __('Top', 'blockenberg') },
770 { key: 'Left', label: __('Left', 'blockenberg') },
771 { key: 'Bottom', label: __('Bottom', 'blockenberg') },
772 { key: 'Right', label: __('Right', 'blockenberg') }
773 ];
774
775 return el('div', { className: 'bkbg-spacing-control' },
776 el('div', { className: 'bkbg-spacing-control__title' }, label),
777 el('div', { className: 'bkbg-spacing-control__grid' },
778 sides.map(function (side) {
779 var valueKey = type + side.key;
780 var unitKey = type + side.key + 'Unit';
781 var customUnitKey = type + side.key + 'CustomUnit';
782
783 return el('div', { className: 'bkbg-spacing-control__item', key: valueKey },
784 el('label', { className: 'bkbg-spacing-control__label' }, side.label),
785 el('div', { className: 'bkbg-spacing-control__row' },
786 el('input', {
787 type: 'text',
788 className: 'components-text-control__input',
789 value: a[valueKey] || '',
790 placeholder: '0',
791 onChange: function (e) {
792 var next = {};
793 next[valueKey] = e.target.value;
794 setAttributes(next);
795 }
796 }),
797 el('select', {
798 className: 'components-select-control__input',
799 value: a[unitKey] || 'px',
800 onChange: function (e) {
801 var next = {};
802 next[unitKey] = e.target.value;
803 setAttributes(next);
804 }
805 }, spacingUnitOptions.map(function (opt) {
806 return el('option', { key: opt.value, value: opt.value }, opt.label);
807 }))
808 ),
809 (a[unitKey] || 'px') === 'custom' && el('input', {
810 type: 'text',
811 className: 'components-text-control__input bkbg-spacing-control__custom-unit',
812 value: a[customUnitKey] || '',
813 placeholder: __('unit, e.g. ch', 'blockenberg'),
814 onChange: function (e) {
815 var next = {};
816 next[customUnitKey] = e.target.value;
817 setAttributes(next);
818 }
819 })
820 );
821 })
822 )
823 );
824 }
825
826 // Get responsive mode icon
827 function getModeIcon(mode) {
828 switch (mode) {
829 case 'tablet': return 'tablet';
830 case 'mobile': return 'smartphone';
831 default: return 'desktop';
832 }
833 }
834
835 // Toolbar controls
836 var toolbar = el(BlockControls, {},
837 // Responsive mode switcher
838 el(ToolbarGroup, {},
839 el(ToolbarDropdownMenu, {
840 icon: getModeIcon(a.responsiveMode),
841 label: __('Responsive Mode', 'blockenberg'),
842 controls: responsiveModes.map(function (mode) {
843 return {
844 title: mode.label,
845 icon: mode.icon,
846 isActive: a.responsiveMode === mode.value,
847 onClick: function () { setAttributes({ responsiveMode: mode.value }); }
848 };
849 })
850 })
851 ),
852 // Column actions
853 el(ToolbarGroup, {},
854 el(ToolbarButton, {
855 icon: 'plus',
856 label: __('Add Column', 'blockenberg'),
857 onClick: addColumn,
858 disabled: columnCount >= 6
859 }),
860 el(ToolbarButton, {
861 icon: 'editor-contract',
862 label: __('Equalize Columns', 'blockenberg'),
863 onClick: equalizeColumns,
864 disabled: columnCount < 2
865 })
866 ),
867 null
868 );
869
870 // Inspector controls
871 var inspector = el(InspectorControls, {},
872 el(PanelBody, { title: __('Row Settings', 'blockenberg'), initialOpen: true },
873 el(ToggleControl, {
874 label: __('Stack on Mobile', 'blockenberg'),
875 help: __('Columns will stack vertically on mobile devices.', 'blockenberg'),
876 checked: a.stackOnMobile,
877 __nextHasNoMarginBottom: true,
878 onChange: function (v) { setAttributes({ stackOnMobile: v }); }
879 }),
880 el(RangeControl, {
881 label: __('Minimum Column Width (%)', 'blockenberg'),
882 value: a.minColumnPct,
883 onChange: function (v) { setAttributes({ minColumnPct: v }); },
884 min: 5,
885 max: 25,
886 step: 1
887 }),
888 ),
889 el(PanelBody, { title: __('Column Presets', 'blockenberg'), initialOpen: false },
890 el('div', { className: 'bkbg-row-presets' },
891 columnPresets.map(function (preset) {
892 return el(Button, {
893 key: preset.value,
894 variant: 'secondary',
895 className: 'bkbg-row-preset-btn',
896 onClick: function () { applyPreset(preset.value); }
897 }, preset.label);
898 })
899 )
900 )
901 );
902
903 // Build inner blocks with resize handles
904 var currentWidths = getCurrentWidths();
905
906 // No appender in Row - columns are added via toolbar button
907
908 // Generate column template based on count
909 function getColumnTemplate() {
910 var count = Math.max(1, a.columnsCount || 1);
911 var template = [];
912 var defaultWidth = Math.round((100 / count) * 100) / 100;
913 for (var i = 0; i < count; i++) {
914 var w = i === count - 1 ? (100 - defaultWidth * (count - 1)) : defaultWidth;
915 template.push(['blockenberg/column', {
916 widths: { desktop: w, tablet: w, mobile: 100 },
917 uid: 'col-' + i
918 }]);
919 }
920 return template;
921 }
922
923 // Layout selector for initial setup
924 if (showLayoutSelector) {
925 return el('div', blockProps,
926 el(Placeholder, {
927 icon: rowIcon,
928 label: __('Row', 'blockenberg'),
929 instructions: __('Select a column layout to start.', 'blockenberg'),
930 className: 'bkbg-row-layout-selector'
931 },
932 el('div', { className: 'bkbg-row-layout-options' },
933 columnPresets.map(function (preset) {
934 return el(Button, {
935 key: preset.value,
936 variant: 'secondary',
937 className: 'bkbg-row-layout-option',
938 onClick: function () { selectLayout(preset.value); }
939 },
940 el('span', { className: 'bkbg-row-layout-icon' }, preset.icon),
941 el('span', { className: 'bkbg-row-layout-label' }, preset.label)
942 );
943 })
944 ),
945 el(Button, {
946 variant: 'link',
947 className: 'bkbg-row-skip-layout',
948 onClick: function () { setHasSelectedLayout(true); }
949 }, __('Skip and add columns manually', 'blockenberg'))
950 )
951 );
952 }
953
954 return el('div', blockProps,
955 toolbar,
956 inspector,
957 el('div', {
958 className: 'bkbg-row__inner'
959 },
960 el(InnerBlocks, {
961 allowedBlocks: ['blockenberg/column'],
962 template: getColumnTemplate(),
963 templateLock: false,
964 orientation: 'horizontal',
965 renderAppender: false
966 })
967 ),
968 // Render resize handles overlay (hide in mobile mode)
969 columnCount > 1 && a.responsiveMode !== 'mobile' && el('div', { className: 'bkbg-row__handles-overlay' },
970 currentWidths.slice(0, -1).map(function (leftWidth, index) {
971 var rightWidth = currentWidths[index + 1];
972 var leftOffset = currentWidths.slice(0, index + 1).reduce(function (sum, w) { return sum + w; }, 0);
973 return el('div', {
974 key: 'handle-' + index,
975 className: 'bkbg-row__handle-wrapper',
976 style: { left: leftOffset + '%' }
977 },
978 el(ResizeHandle, {
979 leftIndex: index,
980 leftWidth: leftWidth,
981 rightWidth: rightWidth,
982 minPct: a.minColumnPct,
983 onResizeStart: handleResizeStart,
984 onResize: handleResize,
985 onResizeEnd: handleResizeEnd,
986 rowClientId: clientId
987 })
988 );
989 })
990 ),
991 // Floating structure popup trigger
992 el('div', { className: 'bkbg-row__structure-trigger' },
993 el(Button, {
994 className: 'bkbg-row__structure-btn',
995 icon: 'layout',
996 label: __('Row structure', 'blockenberg'),
997 onClick: function() { setIsStructureOpen(!isStructureOpen); }
998 }),
999 isStructureOpen && el(Popover, {
1000 className: 'bkbg-row__structure-popover',
1001 position: 'bottom center',
1002 onClose: function() { setIsStructureOpen(false); },
1003 focusOnMount: 'container'
1004 },
1005 el('div', { className: 'bkbg-row__structure-content' },
1006 // Header
1007 el('div', { className: 'bkbg-row__structure-header' },
1008 el('span', { className: 'bkbg-row__structure-title' }, __('Row Structure', 'blockenberg')),
1009 el(Button, {
1010 className: 'bkbg-row__structure-close',
1011 icon: 'no-alt',
1012 label: __('Close', 'blockenberg'),
1013 onClick: function() { setIsStructureOpen(false); }
1014 })
1015 ),
1016 // Visual column bars
1017 el('div', { className: 'bkbg-row__structure-columns' },
1018 currentWidths.map(function(width, index) {
1019 return el('div', {
1020 key: 'col-' + index,
1021 className: 'bkbg-row__structure-col',
1022 style: { width: width + '%' }
1023 },
1024 el('span', { className: 'bkbg-row__structure-col-label' }, Math.round(width) + '%')
1025 );
1026 })
1027 ),
1028 // Column actions
1029 el('div', { className: 'bkbg-row__structure-actions' },
1030 el(Button, {
1031 className: 'bkbg-row__structure-action',
1032 icon: 'plus',
1033 onClick: addColumn,
1034 disabled: columnCount >= 6
1035 }, __('Add Column', 'blockenberg')),
1036 el(Button, {
1037 className: 'bkbg-row__structure-action',
1038 icon: 'editor-contract',
1039 onClick: equalizeColumns,
1040 disabled: columnCount < 2
1041 }, __('Equalize', 'blockenberg'))
1042 ),
1043 // Row actions
1044 el('div', { className: 'bkbg-row__structure-row-actions' },
1045 el(Button, {
1046 className: 'bkbg-row__structure-action bkbg-row__structure-action--secondary',
1047 icon: 'insert-before',
1048 onClick: function() { insertRowAt('before'); setIsStructureOpen(false); }
1049 }, __('Row Above', 'blockenberg')),
1050 el(Button, {
1051 className: 'bkbg-row__structure-action bkbg-row__structure-action--secondary',
1052 icon: 'insert-after',
1053 onClick: function() { insertRowAt('after'); setIsStructureOpen(false); }
1054 }, __('Row Below', 'blockenberg'))
1055 )
1056 )
1057 )
1058 )
1059 );
1060 },
1061
1062 save: function (props) {
1063 var a = props.attributes;
1064 var blockProps = useBlockProps.save({
1065 className: [
1066 'bkbg-row',
1067 'bkbg-row--gap-' + a.gap,
1068 a.stackOnMobile ? 'bkbg-row--stack-mobile' : ''
1069 ].filter(Boolean).join(' '),
1070 style: getSpacingStyles(a),
1071 'data-stack-mobile': a.stackOnMobile ? '1' : '0'
1072 });
1073
1074 return el('div', blockProps,
1075 el('div', { className: 'bkbg-row__inner' },
1076 el(InnerBlocks.Content)
1077 )
1078 );
1079 }
1080 });
1081 }() );
1082