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

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

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