PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.6
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.6
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 / before-after / index.js

index.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.6, at blocks/before-after/index.js

961 lines 49.8 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 Fragment = wp.element.Fragment;
4 var __ = wp.i18n.__;
5 var useState = wp.element.useState;
6 var useMemo = wp.element.useMemo;
7
8 var registerBlockType = wp.blocks.registerBlockType;
9 var InspectorControls = wp.blockEditor.InspectorControls;
10 var BlockControls = wp.blockEditor.BlockControls;
11 var useBlockProps = wp.blockEditor.useBlockProps;
12
13 var PanelBody = wp.components.PanelBody;
14 var ToggleControl = wp.components.ToggleControl;
15 var RangeControl = wp.components.RangeControl;
16 var SelectControl = wp.components.SelectControl;
17 var TextControl = wp.components.TextControl;
18 var Button = wp.components.Button;
19 var ToolbarGroup = wp.components.ToolbarGroup;
20 var ToolbarButton = wp.components.ToolbarButton;
21 var Popover = wp.components.Popover;
22 var ColorPicker = wp.components.ColorPicker;
23 var TabPanel = wp.components.TabPanel;
24
25 // Full Dashicons list (300+ icons)
26 var dashicons = [
27 // Admin Menu
28 'menu', 'admin-site', 'admin-site-alt', 'admin-site-alt2', 'admin-site-alt3', 'dashboard',
29 'admin-post', 'admin-media', 'admin-links', 'admin-page', 'admin-comments',
30 'admin-appearance', 'admin-plugins', 'admin-users', 'admin-tools', 'admin-settings',
31 'admin-network', 'admin-home', 'admin-generic', 'admin-collapse', 'filter',
32 'admin-customizer', 'admin-multisite',
33 // Welcome Screen
34 'welcome-write-blog', 'welcome-add-page', 'welcome-view-site', 'welcome-widgets-menus',
35 'welcome-comments', 'welcome-learn-more',
36 // Post Formats
37 'format-aside', 'format-image', 'format-gallery', 'format-video', 'format-status',
38 'format-quote', 'format-chat', 'format-audio',
39 // Media
40 'camera', 'camera-alt', 'images-alt', 'images-alt2', 'video-alt', 'video-alt2',
41 'video-alt3', 'media-archive', 'media-audio', 'media-code', 'media-default',
42 'media-document', 'media-interactive', 'media-spreadsheet', 'media-text',
43 'media-video', 'playlist-audio', 'playlist-video', 'controls-play',
44 'controls-pause', 'controls-forward', 'controls-skipforward', 'controls-back',
45 'controls-skipback', 'controls-repeat', 'controls-volumeon', 'controls-volumeoff',
46 // Image Editing
47 'image-crop', 'image-rotate', 'image-rotate-left', 'image-rotate-right',
48 'image-flip-vertical', 'image-flip-horizontal', 'image-filter',
49 // Sorting & Misc UI
50 'align-left', 'align-right', 'align-center', 'align-none', 'align-pull-left', 'align-pull-right',
51 'align-full-width', 'align-wide',
52 'lock', 'unlock', 'calendar', 'calendar-alt', 'visibility', 'hidden',
53 'post-status', 'edit', 'trash', 'sticky',
54 // Arrows & Navigation
55 'arrow-up', 'arrow-down', 'arrow-right', 'arrow-left',
56 'arrow-up-alt', 'arrow-down-alt', 'arrow-right-alt', 'arrow-left-alt',
57 'arrow-up-alt2', 'arrow-down-alt2', 'arrow-right-alt2', 'arrow-left-alt2',
58 'sort', 'leftright', 'randomize', 'list-view', 'excerpt-view', 'grid-view',
59 'move', 'undo', 'redo',
60 // Social & Sharing
61 'share', 'share-alt', 'share-alt2', 'rss', 'email', 'email-alt', 'email-alt2',
62 'networking', 'amazon', 'facebook', 'facebook-alt', 'google',
63 'twitter', 'twitter-alt', 'instagram', 'linkedin', 'pinterest', 'podio',
64 'reddit', 'spotify', 'twitch', 'whatsapp', 'xing', 'youtube',
65 // WordPress.org Specific
66 'hammer', 'art', 'migrate', 'performance', 'universal-access',
67 'universal-access-alt', 'tickets', 'nametag', 'clipboard', 'heart', 'megaphone',
68 'schedule', 'tide', 'rest-api', 'code-standards',
69 // BuddyPress
70 'buddicons-activity', 'buddicons-bbpress-logo', 'buddicons-buddypress-logo',
71 'buddicons-community', 'buddicons-forums', 'buddicons-friends',
72 'buddicons-groups', 'buddicons-pm', 'buddicons-replies',
73 'buddicons-topics', 'buddicons-tracking',
74 // WordPress Core
75 'wordpress', 'wordpress-alt', 'pressthis', 'update', 'update-alt',
76 'screenoptions', 'info', 'cart', 'feedback', 'cloud', 'cloud-saved',
77 'cloud-upload', 'translation', 'tag', 'category', 'archive',
78 // Status & Alerts
79 'yes', 'yes-alt', 'no', 'no-alt', 'plus', 'plus-alt', 'plus-alt2',
80 'minus', 'dismiss', 'marker', 'star-filled', 'star-half', 'star-empty',
81 'flag', 'warning', 'thumbs-up', 'thumbs-down', 'smiley',
82 // Location & Places
83 'location', 'location-alt', 'vault', 'shield', 'shield-alt',
84 'sos', 'search', 'slides', 'text-page', 'analytics', 'chart-pie',
85 'chart-bar', 'chart-line', 'chart-area', 'groups', 'businessman', 'businesswoman',
86 'businessperson', 'id', 'id-alt', 'products', 'awards', 'forms', 'testimonial',
87 'portfolio', 'book', 'book-alt', 'download', 'upload', 'backup',
88 'clock', 'lightbulb', 'microphone', 'desktop', 'laptop', 'tablet',
89 'smartphone', 'phone', 'index-card', 'carrot', 'building', 'store',
90 'album', 'palmtree', 'tickets-alt', 'money', 'money-alt', 'bank',
91 // Misc Objects
92 'layout', 'paperclip', 'color-picker', 'edit-large', 'edit-page',
93 'airplane', 'beer', 'bell', 'bitcoin', 'calculator', 'car', 'coffee',
94 'database', 'database-add', 'database-export', 'database-import',
95 'database-remove', 'database-view', 'drumstick', 'ellipsis', 'exit',
96 'food', 'fullscreen-alt', 'fullscreen-exit-alt', 'games', 'hourglass',
97 'html', 'insert', 'insert-after', 'insert-before', 'open-folder',
98 'pdf', 'pets', 'plugins-checked', 'printer', 'privacy', 'remove',
99 'saved', 'shortcode', 'superhero', 'superhero-alt', 'text', 'external',
100 // Table Operations
101 'table-col-after', 'table-col-before', 'table-col-delete',
102 'table-row-after', 'table-row-before', 'table-row-delete',
103 // Editor Controls
104 'editor-aligncenter', 'editor-alignleft', 'editor-alignright', 'editor-bold',
105 'editor-break', 'editor-code', 'editor-contract', 'editor-customchar',
106 'editor-expand', 'editor-help', 'editor-indent', 'editor-insertmore',
107 'editor-italic', 'editor-justify', 'editor-kitchensink', 'editor-ltr',
108 'editor-ol', 'editor-ol-rtl', 'editor-outdent', 'editor-paragraph',
109 'editor-paste-text', 'editor-paste-word', 'editor-quote', 'editor-removeformatting',
110 'editor-rtl', 'editor-spellcheck', 'editor-strikethrough', 'editor-table',
111 'editor-textcolor', 'editor-ul', 'editor-underline', 'editor-unlink', 'editor-video'
112 ];
113
114 function parseIcon(iconValue) {
115 if (!iconValue) return { type: 'dashicon', value: 'leftright' };
116 if (typeof iconValue === 'string') return { type: 'dashicon', value: iconValue };
117 return iconValue;
118 }
119
120 function clamp(n, min, max) {
121 n = parseFloat(n);
122 if (isNaN(n)) n = min;
123 return Math.max(min, Math.min(max, n));
124 }
125
126 function colorToRgbaString(color) {
127 if (!color || !color.rgb) return '';
128 var a = (typeof color.rgb.a === 'number') ? color.rgb.a : 1;
129 return 'rgba(' + color.rgb.r + ',' + color.rgb.g + ',' + color.rgb.b + ',' + a + ')';
130 }
131
132 function openMedia(onSelect) {
133 if (!wp || !wp.media) return;
134 var frame = wp.media({
135 title: __('Select Image', 'blockenberg'),
136 button: { text: __('Use image', 'blockenberg') },
137 multiple: false,
138 library: { type: 'image' }
139 });
140 frame.on('select', function () {
141 var attachment = frame.state().get('selection').first().toJSON();
142 onSelect(attachment);
143 });
144 frame.open();
145 }
146
147 function normalizeImage(att) {
148 return {
149 id: att && att.id ? att.id : 0,
150 url: att && att.url ? att.url : '',
151 alt: att && att.alt ? att.alt : ''
152 };
153 }
154
155 registerBlockType('blockenberg/before-after', {
156 title: __('Before/After Slider', 'blockenberg'),
157 icon: 'image-flip-horizontal',
158 category: 'blockenberg',
159 description: __('Compare two images with a draggable slider handle.', 'blockenberg'),
160
161 edit: function (props) {
162 var a = props.attributes;
163 var setAttributes = props.setAttributes;
164 var isSelected = props.isSelected;
165
166 var dragState = useState(null);
167 var dragging = dragState[0];
168 var setDragging = dragState[1];
169
170 var colorPopoverState = useState(null);
171 var openColor = colorPopoverState[0];
172 var setOpenColor = colorPopoverState[1];
173
174 // Handle icon picker popover
175 var iconPickerState = useState(false);
176 var openHandleIconPicker = iconPickerState[0];
177 var setOpenHandleIconPicker = iconPickerState[1];
178
179 // Dashicon search
180 var searchState = useState('');
181 var iconSearch = searchState[0];
182 var setIconSearch = searchState[1];
183
184 var filteredDashicons = useMemo(function () {
185 if (!iconSearch) return dashicons;
186 var s = iconSearch.toLowerCase();
187 return dashicons.filter(function (name) {
188 return name.toLowerCase().includes(s);
189 });
190 }, [iconSearch]);
191
192 function setBeforeImage(att) {
193 setAttributes({ beforeImage: normalizeImage(att) });
194 }
195
196 function setAfterImage(att) {
197 setAttributes({ afterImage: normalizeImage(att) });
198 }
199
200 function swapImages() {
201 setAttributes({ beforeImage: a.afterImage, afterImage: a.beforeImage });
202 }
203
204 function setPos(next) {
205 setAttributes({ startPosition: clamp(next, 0, 100) });
206 }
207
208 function posFromEvent(e, containerEl) {
209 if (!containerEl) return a.startPosition;
210
211 var rect = containerEl.getBoundingClientRect();
212 var clientX = (e.touches && e.touches[0]) ? e.touches[0].clientX : e.clientX;
213 var clientY = (e.touches && e.touches[0]) ? e.touches[0].clientY : e.clientY;
214
215 if (a.orientation === 'vertical') {
216 var y = clientY - rect.top;
217 return clamp((y / rect.height) * 100, 0, 100);
218 }
219
220 var x = clientX - rect.left;
221 return clamp((x / rect.width) * 100, 0, 100);
222 }
223
224 function beginDrag(e) {
225 // Don't hijack clicks on controls (e.g., the Select buttons in empty states)
226 if (e && e.target && e.target.closest) {
227 if (e.target.closest('button, a, input, textarea, select, .components-button')) {
228 return false;
229 }
230 }
231 e.preventDefault();
232 setDragging(true);
233 return true;
234 }
235
236 function onMove(e, containerEl) {
237 if (!dragging) return;
238 e.preventDefault();
239 setPos(posFromEvent(e, containerEl));
240 }
241
242 function endDrag() {
243 setDragging(false);
244 }
245
246 var layoutClass = 'bkbg-ba-wrap bkbg-editor-wrap';
247 if (isSelected) layoutClass += ' bkbg-block-selected';
248 layoutClass += a.orientation === 'vertical' ? ' is-vertical' : ' is-horizontal';
249 layoutClass += a.shadow ? ' has-shadow' : '';
250 if (a.handleHoverAnim) {
251 layoutClass += ' has-handle-anim';
252 layoutClass += a.handleHoverAnimType ? (' is-handle-anim-' + a.handleHoverAnimType) : ' is-handle-anim-pulse';
253 }
254 if (a.handleHoverScale) {
255 layoutClass += ' has-handle-hover';
256 }
257
258 var styleVars = {
259 '--bkbg-ba-pos': a.startPosition + '%',
260 '--bkbg-ba-radius': a.borderRadius + 'px',
261 '--bkbg-ba-handle-size': a.handleSize + 'px',
262 '--bkbg-ba-handle-icon-size': (a.handleIconSize || 18) + 'px',
263 '--bkbg-ba-handle-anim-dur': (a.handleHoverAnimDuration || 650) + 'ms',
264 '--bkbg-ba-handle-hover-scale': (a.handleHoverScaleAmount || 1.08),
265 '--bkbg-ba-handle-bg': a.handleBg,
266 '--bkbg-ba-handle-color': a.handleColor,
267 '--bkbg-ba-line-color': a.lineColor,
268 '--bkbg-ba-line-width': a.lineWidth + 'px',
269 '--bkbg-ba-label-bg': a.labelBg,
270 '--bkbg-ba-label-color': a.labelColor,
271 '--bkbg-ba-label-size': a.labelSize + 'px',
272 '--bkbg-ba-label-radius': a.labelRadius + 'px',
273 '--bkbg-ba-min-height': a.minHeight + 'px'
274 };
275
276 function renderHandleIcon() {
277 var icon = parseIcon(a.handleIcon);
278 if (!icon || icon.type === 'none') return null;
279 if (icon.type === 'custom') {
280 return el('img', { className: 'bkbg-ba-handle-img', src: icon.value, alt: '' });
281 }
282 return el('span', { className: 'dashicons dashicons-' + icon.value + ' bkbg-ba-handle-icon' });
283 }
284
285 function renderHandleIconWrapped() {
286 return el('span', { className: 'bkbg-ba-handle-surface' },
287 el('span', { className: 'bkbg-ba-handle-anim' },
288 el('span', { className: 'bkbg-ba-handle-hover' },
289 renderHandleIcon()
290 )
291 )
292 );
293 }
294
295 function renderHandleIconPicker(position) {
296 var currentIcon = parseIcon(a.handleIcon);
297
298 var stop = function (e) {
299 if (e && e.stopPropagation) e.stopPropagation();
300 };
301
302 var openMediaLibrary = function () {
303 var frame = wp.media({
304 title: __('Select Icon Image', 'blockenberg'),
305 button: { text: __('Use as Icon', 'blockenberg') },
306 multiple: false,
307 library: { type: 'image' }
308 });
309 frame.on('select', function () {
310 var attachment = frame.state().get('selection').first().toJSON();
311 setAttributes({ handleIcon: { type: 'custom', value: attachment.url } });
312 setOpenHandleIconPicker(false);
313 });
314 frame.open();
315 };
316
317 return el(Popover, {
318 position: position || 'bottom center',
319 className: 'bkbg-ba-icon-popover',
320 onClose: function () {
321 setOpenHandleIconPicker(false);
322 setIconSearch('');
323 }
324 },
325 el('div', { className: 'bkbg-ba-icon-picker-modal', onMouseDown: stop, onClick: stop },
326 el('div', { className: 'bkbg-ba-icon-picker-header' },
327 el('h3', {}, __('Choose Handle Icon', 'blockenberg')),
328 el(Button, {
329 icon: 'no-alt',
330 onClick: function () {
331 setOpenHandleIconPicker(false);
332 setIconSearch('');
333 },
334 className: 'bkbg-ba-icon-picker-close'
335 })
336 ),
337 el(TabPanel, {
338 className: 'bkbg-ba-icon-tabs',
339 activeClass: 'is-active',
340 tabs: [
341 { name: 'dashicons', title: __('Dashicons', 'blockenberg'), className: 'bkbg-ba-tab-dashicons' },
342 { name: 'media', title: __('Media Library', 'blockenberg'), className: 'bkbg-ba-tab-media' }
343 ]
344 }, function (tab) {
345 if (tab.name === 'dashicons') {
346 return el('div', { className: 'bkbg-ba-dashicons-panel' },
347 el('div', { className: 'bkbg-ba-icon-actions' },
348 el(Button, {
349 variant: 'secondary',
350 onClick: function () {
351 setAttributes({ handleIcon: { type: 'none', value: '' } });
352 setOpenHandleIconPicker(false);
353 setIconSearch('');
354 }
355 }, __('No icon', 'blockenberg')),
356 el(Button, {
357 variant: 'secondary',
358 onClick: function () {
359 setAttributes({ handleIcon: { type: 'dashicon', value: 'leftright' } });
360 setOpenHandleIconPicker(false);
361 setIconSearch('');
362 }
363 }, __('Reset', 'blockenberg'))
364 ),
365 el(TextControl, {
366 placeholder: __('Search icons...', 'blockenberg'),
367 value: iconSearch,
368 onChange: setIconSearch,
369 className: 'bkbg-ba-icon-search'
370 }),
371 el('div', { className: 'bkbg-ba-dashicons-grid' },
372 filteredDashicons.length === 0
373 ? el('p', { className: 'bkbg-ba-no-icons' }, __('No icons found', 'blockenberg'))
374 : filteredDashicons.map(function (name) {
375 var isActive = currentIcon.type === 'dashicon' && currentIcon.value === name;
376 return el('button', {
377 key: name,
378 type: 'button',
379 className: 'bkbg-ba-dashicon-btn' + (isActive ? ' is-active' : ''),
380 onClick: function () {
381 setAttributes({ handleIcon: { type: 'dashicon', value: name } });
382 setOpenHandleIconPicker(false);
383 setIconSearch('');
384 },
385 title: name
386 },
387 el('span', { className: 'dashicons dashicons-' + name })
388 );
389 })
390 )
391 );
392 }
393
394 // Media tab
395 return el('div', { className: 'bkbg-ba-media-panel' },
396 currentIcon.type === 'custom' && el('div', { className: 'bkbg-ba-current-media' },
397 el('img', { src: currentIcon.value, alt: '' }),
398 el(Button, {
399 isDestructive: true,
400 variant: 'secondary',
401 onClick: function () {
402 setAttributes({ handleIcon: { type: 'dashicon', value: 'leftright' } });
403 }
404 }, __('Remove', 'blockenberg'))
405 ),
406 el(Button, {
407 variant: 'secondary',
408 onClick: openMediaLibrary,
409 className: 'bkbg-ba-media-btn'
410 },
411 el('span', { className: 'dashicons dashicons-upload' }),
412 ' ',
413 __('Select from Media Library', 'blockenberg')
414 ),
415 el('p', { className: 'bkbg-ba-media-hint' },
416 __('Upload or select an image to use as a custom handle icon.', 'blockenberg')
417 )
418 );
419 })
420 )
421 );
422 }
423
424 var blockProps = useBlockProps({
425 className: layoutClass,
426 style: styleVars,
427 'data-block-label': 'Before/After'
428 });
429
430 var inspector = el(InspectorControls, {},
431 el(PanelBody, { title: __('Images', 'blockenberg'), initialOpen: true },
432 el('div', { className: 'bkbg-ba-media-row' },
433 el('div', { className: 'bkbg-ba-media-col' },
434 el('div', { className: 'bkbg-ba-media-title' }, __('Before image', 'blockenberg')),
435 a.beforeImage && a.beforeImage.url
436 ? el('img', { className: 'bkbg-ba-media-thumb', src: a.beforeImage.url, alt: a.beforeImage.alt || '' })
437 : el('div', { className: 'bkbg-ba-media-placeholder' }, __('No image selected', 'blockenberg')),
438 el(Button, {
439 variant: 'secondary',
440 onClick: function () { openMedia(setBeforeImage); }
441 }, a.beforeImage && a.beforeImage.url ? __('Replace', 'blockenberg') : __('Select', 'blockenberg')),
442 (a.beforeImage && a.beforeImage.url) && el(Button, {
443 variant: 'tertiary',
444 isDestructive: true,
445 onClick: function () { setAttributes({ beforeImage: { id: 0, url: '', alt: '' } }); }
446 }, __('Remove', 'blockenberg'))
447 ),
448 el('div', { className: 'bkbg-ba-media-col' },
449 el('div', { className: 'bkbg-ba-media-title' }, __('After image', 'blockenberg')),
450 a.afterImage && a.afterImage.url
451 ? el('img', { className: 'bkbg-ba-media-thumb', src: a.afterImage.url, alt: a.afterImage.alt || '' })
452 : el('div', { className: 'bkbg-ba-media-placeholder' }, __('No image selected', 'blockenberg')),
453 el(Button, {
454 variant: 'secondary',
455 onClick: function () { openMedia(setAfterImage); }
456 }, a.afterImage && a.afterImage.url ? __('Replace', 'blockenberg') : __('Select', 'blockenberg')),
457 (a.afterImage && a.afterImage.url) && el(Button, {
458 variant: 'tertiary',
459 isDestructive: true,
460 onClick: function () { setAttributes({ afterImage: { id: 0, url: '', alt: '' } }); }
461 }, __('Remove', 'blockenberg'))
462 )
463 ),
464 el(Button, {
465 variant: 'secondary',
466 disabled: !(a.beforeImage && a.beforeImage.url && a.afterImage && a.afterImage.url),
467 onClick: swapImages
468 }, __('Swap images', 'blockenberg'))
469 ),
470
471 el(PanelBody, { title: __('Slider', 'blockenberg'), initialOpen: false },
472 el(SelectControl, {
473 label: __('Orientation', 'blockenberg'),
474 value: a.orientation,
475 options: [
476 { label: __('Horizontal', 'blockenberg'), value: 'horizontal' },
477 { label: __('Vertical', 'blockenberg'), value: 'vertical' }
478 ],
479 onChange: function (v) { setAttributes({ orientation: v }); }
480 }),
481 el(RangeControl, {
482 label: __('Start position (%)', 'blockenberg'),
483 value: a.startPosition,
484 onChange: setPos,
485 min: 0,
486 max: 100
487 }),
488 el(RangeControl, {
489 label: __('Minimum height (px)', 'blockenberg'),
490 value: a.minHeight,
491 onChange: function (v) { setAttributes({ minHeight: v }); },
492 min: 120,
493 max: 900
494 }),
495 el(SelectControl, {
496 label: __('Aspect ratio', 'blockenberg'),
497 value: a.aspect,
498 options: [
499 { label: __('Auto', 'blockenberg'), value: 'auto' },
500 { label: '1:1', value: '1/1' },
501 { label: '4:3', value: '4/3' },
502 { label: '3:2', value: '3/2' },
503 { label: '16:9', value: '16/9' },
504 { label: '21:9', value: '21/9' }
505 ],
506 onChange: function (v) { setAttributes({ aspect: v }); }
507 })
508 ),
509
510 el(PanelBody, { title: __('Labels', 'blockenberg'), initialOpen: false },
511 el(ToggleControl, {
512 label: __('Show labels', 'blockenberg'),
513 checked: a.showLabels,
514 __nextHasNoMarginBottom: true,
515 onChange: function (v) { setAttributes({ showLabels: v }); }
516 }),
517 a.showLabels && el(SelectControl, {
518 label: __('Label position', 'blockenberg'),
519 value: a.labelPosition,
520 options: [
521 { label: __('Top', 'blockenberg'), value: 'top' },
522 { label: __('Bottom', 'blockenberg'), value: 'bottom' }
523 ],
524 onChange: function (v) { setAttributes({ labelPosition: v }); }
525 }),
526 a.showLabels && el(TextControl, {
527 label: __('Before label', 'blockenberg'),
528 value: a.beforeLabel,
529 onChange: function (v) { setAttributes({ beforeLabel: v }); }
530 }),
531 a.showLabels && el(TextControl, {
532 label: __('After label', 'blockenberg'),
533 value: a.afterLabel,
534 onChange: function (v) { setAttributes({ afterLabel: v }); }
535 }),
536 a.showLabels && el('div', { className: 'bkbg-ba-color-row' },
537 el('span', {}, __('Label background', 'blockenberg')),
538 el('button', {
539 className: 'bkbg-ba-color-btn',
540 style: { backgroundColor: a.labelBg },
541 onClick: function () { setOpenColor(openColor === 'labelBg' ? null : 'labelBg'); }
542 }),
543 openColor === 'labelBg' && el(Popover, { position: 'bottom left', onClose: function () { setOpenColor(null); } },
544 el(ColorPicker, {
545 color: a.labelBg,
546 onChangeComplete: function (c) {
547 var rgba = colorToRgbaString(c);
548 setAttributes({ labelBg: rgba || (c.hex ? c.hex : a.labelBg) });
549 }
550 })
551 )
552 ),
553 a.showLabels && el('div', { className: 'bkbg-ba-color-row' },
554 el('span', {}, __('Label text', 'blockenberg')),
555 el('button', {
556 className: 'bkbg-ba-color-btn',
557 style: { backgroundColor: a.labelColor },
558 onClick: function () { setOpenColor(openColor === 'labelColor' ? null : 'labelColor'); }
559 }),
560 openColor === 'labelColor' && el(Popover, { position: 'bottom left', onClose: function () { setOpenColor(null); } },
561 el(ColorPicker, {
562 color: a.labelColor,
563 onChangeComplete: function (c) { setAttributes({ labelColor: c.hex }); }
564 })
565 )
566 ),
567 a.showLabels && el(RangeControl, {
568 label: __('Label size', 'blockenberg'),
569 value: a.labelSize,
570 onChange: function (v) { setAttributes({ labelSize: v }); },
571 min: 10,
572 max: 24
573 }),
574 a.showLabels && el(RangeControl, {
575 label: __('Label radius', 'blockenberg'),
576 value: a.labelRadius,
577 onChange: function (v) { setAttributes({ labelRadius: v }); },
578 min: 0,
579 max: 30
580 })
581 ),
582
583 el(PanelBody, { title: __('Handle', 'blockenberg'), initialOpen: false },
584 el(SelectControl, {
585 label: __('Handle style', 'blockenberg'),
586 value: a.handleStyle,
587 options: [
588 { label: __('Circle', 'blockenberg'), value: 'circle' },
589 { label: __('Square', 'blockenberg'), value: 'square' }
590 ],
591 onChange: function (v) { setAttributes({ handleStyle: v }); }
592 }),
593 el(RangeControl, {
594 label: __('Handle size', 'blockenberg'),
595 value: a.handleSize,
596 onChange: function (v) { setAttributes({ handleSize: v }); },
597 min: 28,
598 max: 80
599 }),
600
601 el('div', { className: 'bkbg-ba-icon-row' },
602 el('span', { className: 'bkbg-ba-icon-row-label' }, __('Handle icon', 'blockenberg')),
603 el('button', {
604 type: 'button',
605 className: 'bkbg-ba-icon-picker-btn',
606 onMouseDown: function (e) { e.stopPropagation(); },
607 onClick: function (e) {
608 e.preventDefault();
609 e.stopPropagation();
610 setOpenHandleIconPicker(!openHandleIconPicker);
611 }
612 },
613 (function () {
614 var icon = parseIcon(a.handleIcon);
615 if (icon.type === 'custom') {
616 return el('img', { className: 'bkbg-ba-icon-btn-img', src: icon.value, alt: '' });
617 }
618 if (icon.type === 'none') {
619 return el('span', { className: 'bkbg-ba-icon-btn-none' }, __('None', 'blockenberg'));
620 }
621 return el('span', { className: 'dashicons dashicons-' + icon.value });
622 })()
623 ),
624 openHandleIconPicker && el('div', { className: 'bkbg-ba-icon-popover-anchor' },
625 renderHandleIconPicker('middle left')
626 )
627 ),
628 el(RangeControl, {
629 label: __('Handle icon size', 'blockenberg'),
630 value: a.handleIconSize || 18,
631 onChange: function (v) { setAttributes({ handleIconSize: v }); },
632 min: 10,
633 max: 34
634 }),
635 el(RangeControl, {
636 label: __('Divider line width', 'blockenberg'),
637 value: a.lineWidth,
638 onChange: function (v) { setAttributes({ lineWidth: v }); },
639 min: 1,
640 max: 8
641 }),
642 el('div', { className: 'bkbg-ba-color-row' },
643 el('span', {}, __('Handle background', 'blockenberg')),
644 el('button', {
645 className: 'bkbg-ba-color-btn',
646 style: { backgroundColor: a.handleBg },
647 onClick: function () { setOpenColor(openColor === 'handleBg' ? null : 'handleBg'); }
648 }),
649 openColor === 'handleBg' && el(Popover, { position: 'bottom left', onClose: function () { setOpenColor(null); } },
650 el(ColorPicker, {
651 color: a.handleBg,
652 onChangeComplete: function (c) { setAttributes({ handleBg: c.hex }); }
653 })
654 )
655 ),
656 el('div', { className: 'bkbg-ba-color-row' },
657 el('span', {}, __('Handle icon', 'blockenberg')),
658 el('button', {
659 className: 'bkbg-ba-color-btn',
660 style: { backgroundColor: a.handleColor },
661 onClick: function () { setOpenColor(openColor === 'handleColor' ? null : 'handleColor'); }
662 }),
663 openColor === 'handleColor' && el(Popover, { position: 'bottom left', onClose: function () { setOpenColor(null); } },
664 el(ColorPicker, {
665 color: a.handleColor,
666 onChangeComplete: function (c) { setAttributes({ handleColor: c.hex }); }
667 })
668 )
669 ),
670 el('div', { className: 'bkbg-ba-color-row' },
671 el('span', {}, __('Line color', 'blockenberg')),
672 el('button', {
673 className: 'bkbg-ba-color-btn',
674 style: { backgroundColor: a.lineColor },
675 onClick: function () { setOpenColor(openColor === 'lineColor' ? null : 'lineColor'); }
676 }),
677 openColor === 'lineColor' && el(Popover, { position: 'bottom left', onClose: function () { setOpenColor(null); } },
678 el(ColorPicker, {
679 color: a.lineColor,
680 onChangeComplete: function (c) {
681 var rgba = colorToRgbaString(c);
682 setAttributes({ lineColor: rgba || c.hex });
683 }
684 })
685 )
686 ),
687
688 el(ToggleControl, {
689 label: __('Animate handle', 'blockenberg'),
690 checked: !!a.handleHoverAnim,
691 __nextHasNoMarginBottom: true,
692 onChange: function (v) { setAttributes({ handleHoverAnim: v }); }
693 }),
694 a.handleHoverAnim && el(SelectControl, {
695 label: __('Animation', 'blockenberg'),
696 value: a.handleHoverAnimType || 'pulse',
697 options: [
698 { label: __('Pulse', 'blockenberg'), value: 'pulse' },
699 { label: __('Wiggle', 'blockenberg'), value: 'wiggle' },
700 { label: __('Bounce', 'blockenberg'), value: 'bounce' },
701 { label: __('Glow', 'blockenberg'), value: 'glow' }
702 ],
703 onChange: function (v) { setAttributes({ handleHoverAnimType: v }); }
704 }),
705 a.handleHoverAnim && el(RangeControl, {
706 label: __('Animation duration (ms)', 'blockenberg'),
707 value: a.handleHoverAnimDuration || 650,
708 onChange: function (v) { setAttributes({ handleHoverAnimDuration: v }); },
709 min: 150,
710 max: 2000
711 }),
712
713 el(ToggleControl, {
714 label: __('Enlarge icon area on hover', 'blockenberg'),
715 checked: !!a.handleHoverScale,
716 __nextHasNoMarginBottom: true,
717 onChange: function (v) { setAttributes({ handleHoverScale: v }); }
718 }),
719 a.handleHoverScale && el(RangeControl, {
720 label: __('Hover scale', 'blockenberg'),
721 value: a.handleHoverScaleAmount || 1.08,
722 onChange: function (v) { setAttributes({ handleHoverScaleAmount: v }); },
723 min: 1,
724 max: 1.25,
725 step: 0.01
726 })
727 ),
728
729 el(PanelBody, { title: __('Frame', 'blockenberg'), initialOpen: false },
730 el(RangeControl, {
731 label: __('Border radius', 'blockenberg'),
732 value: a.borderRadius,
733 onChange: function (v) { setAttributes({ borderRadius: v }); },
734 min: 0,
735 max: 40
736 }),
737 el(ToggleControl, {
738 label: __('Shadow', 'blockenberg'),
739 checked: a.shadow,
740 __nextHasNoMarginBottom: true,
741 onChange: function (v) { setAttributes({ shadow: v }); }
742 })
743 )
744 );
745
746 var toolbar = el(BlockControls, {},
747 el(ToolbarGroup, {},
748 el(ToolbarButton, {
749 icon: 'image-flip-horizontal',
750 label: __('Swap images', 'blockenberg'),
751 disabled: !(a.beforeImage && a.beforeImage.url && a.afterImage && a.afterImage.url),
752 onClick: swapImages
753 }),
754 el(ToolbarButton, {
755 icon: 'image-rotate',
756 label: __('Reset position', 'blockenberg'),
757 onClick: function () { setPos(50); }
758 })
759 )
760 );
761
762 function renderInner() {
763 var hasBefore = a.beforeImage && a.beforeImage.url;
764 var hasAfter = a.afterImage && a.afterImage.url;
765
766 // Empty / partial state: show two side-by-side panels so selectors never overlap.
767 if (!hasBefore || !hasAfter) {
768 return el('div', { className: 'bkbg-ba-inner bkbg-ba-empty-inner' },
769 el('div', { className: 'bkbg-ba-empty-grid' },
770 el('div', { className: 'bkbg-ba-empty-panel is-before' },
771 el('div', { className: 'bkbg-ba-empty-panel-title' }, __('Before', 'blockenberg')),
772 hasBefore
773 ? el('img', { className: 'bkbg-ba-empty-thumb', src: a.beforeImage.url, alt: a.beforeImage.alt || '' })
774 : el('div', { className: 'bkbg-ba-empty-copy' }, __('Select the BEFORE image', 'blockenberg')),
775 el(Button, {
776 variant: 'secondary',
777 onMouseDown: function (e) { e.stopPropagation(); },
778 onTouchStart: function (e) { e.stopPropagation(); },
779 onClick: function () { openMedia(setBeforeImage); }
780 }, hasBefore ? __('Replace Before', 'blockenberg') : __('Select Before', 'blockenberg'))
781 ),
782 el('div', { className: 'bkbg-ba-empty-panel is-after' },
783 el('div', { className: 'bkbg-ba-empty-panel-title' }, __('After', 'blockenberg')),
784 hasAfter
785 ? el('img', { className: 'bkbg-ba-empty-thumb', src: a.afterImage.url, alt: a.afterImage.alt || '' })
786 : el('div', { className: 'bkbg-ba-empty-copy' }, __('Select the AFTER image', 'blockenberg')),
787 el(Button, {
788 variant: 'secondary',
789 onMouseDown: function (e) { e.stopPropagation(); },
790 onTouchStart: function (e) { e.stopPropagation(); },
791 onClick: function () { openMedia(setAfterImage); }
792 }, hasAfter ? __('Replace After', 'blockenberg') : __('Select After', 'blockenberg'))
793 )
794 )
795 );
796 }
797
798 return el('div', {
799 className: 'bkbg-ba-inner',
800 onMouseMove: function (e) { onMove(e, e.currentTarget); },
801 onTouchMove: function (e) { onMove(e, e.currentTarget); },
802 onMouseUp: endDrag,
803 onMouseLeave: endDrag,
804 onTouchEnd: endDrag,
805 onTouchCancel: endDrag
806 },
807 el('div', { className: 'bkbg-ba-layer bkbg-ba-before' },
808 el('img', { src: a.beforeImage.url, alt: a.beforeImage.alt || '' })
809 ),
810 el('div', { className: 'bkbg-ba-layer bkbg-ba-after' },
811 el('img', { src: a.afterImage.url, alt: a.afterImage.alt || '' })
812 ),
813 el('div', { className: 'bkbg-ba-divider' },
814 el('div', {
815 className: 'bkbg-ba-handle' + (a.handleStyle === 'square' ? ' is-square' : ' is-circle'),
816 onMouseDown: function (e) {
817 if (beginDrag(e)) {
818 setPos(posFromEvent(e, e.currentTarget.closest('.bkbg-ba-inner')));
819 }
820 },
821 onTouchStart: function (e) {
822 if (beginDrag(e)) {
823 setPos(posFromEvent(e, e.currentTarget.closest('.bkbg-ba-inner')));
824 }
825 }
826 },
827 renderHandleIconWrapped()
828 )
829 ),
830 a.showLabels && el('div', { className: 'bkbg-ba-labels is-' + a.labelPosition },
831 el('div', { className: 'bkbg-ba-label bkbg-ba-label-before' }, a.beforeLabel || __('Before', 'blockenberg')),
832 el('div', { className: 'bkbg-ba-label bkbg-ba-label-after' }, a.afterLabel || __('After', 'blockenberg'))
833 ),
834 // Accessible range input
835 el('input', {
836 className: 'bkbg-ba-range',
837 type: 'range',
838 min: 0,
839 max: 100,
840 value: a.startPosition,
841 onChange: function (e) { setPos(e.target.value); },
842 'aria-label': __('Before/After position', 'blockenberg')
843 })
844 );
845 }
846
847 var content = el('div', blockProps,
848 el('div', {
849 className: 'bkbg-ba-aspect' + (a.aspect !== 'auto' ? ' has-aspect' : ''),
850 style: a.aspect !== 'auto' ? { aspectRatio: a.aspect } : {}
851 },
852 renderInner()
853 )
854 );
855
856 return el(Fragment, {},
857 toolbar,
858 inspector,
859 content
860 );
861 },
862
863 save: function (props) {
864 var a = props.attributes;
865
866 var cls = 'bkbg-ba-wrap';
867 cls += a.orientation === 'vertical' ? ' is-vertical' : ' is-horizontal';
868 if (a.shadow) cls += ' has-shadow';
869 if (a.handleHoverAnim) {
870 cls += ' has-handle-anim';
871 cls += a.handleHoverAnimType ? (' is-handle-anim-' + a.handleHoverAnimType) : ' is-handle-anim-pulse';
872 }
873 if (a.handleHoverScale) {
874 cls += ' has-handle-hover';
875 }
876
877 var styleVars = {
878 '--bkbg-ba-pos': (a.startPosition || 50) + '%',
879 '--bkbg-ba-radius': (a.borderRadius || 0) + 'px',
880 '--bkbg-ba-handle-size': (a.handleSize || 44) + 'px',
881 '--bkbg-ba-handle-icon-size': (a.handleIconSize || 18) + 'px',
882 '--bkbg-ba-handle-anim-dur': (a.handleHoverAnimDuration || 650) + 'ms',
883 '--bkbg-ba-handle-hover-scale': (a.handleHoverScaleAmount || 1.08),
884 '--bkbg-ba-handle-bg': a.handleBg,
885 '--bkbg-ba-handle-color': a.handleColor,
886 '--bkbg-ba-line-color': a.lineColor,
887 '--bkbg-ba-line-width': (a.lineWidth || 2) + 'px',
888 '--bkbg-ba-label-bg': a.labelBg,
889 '--bkbg-ba-label-color': a.labelColor,
890 '--bkbg-ba-label-size': (a.labelSize || 13) + 'px',
891 '--bkbg-ba-label-radius': (a.labelRadius || 6) + 'px',
892 '--bkbg-ba-min-height': (a.minHeight || 260) + 'px'
893 };
894
895 function renderSavedHandleIcon() {
896 var icon = parseIcon(a.handleIcon);
897 if (!icon || icon.type === 'none') return null;
898 if (icon.type === 'custom') {
899 return el('img', { className: 'bkbg-ba-handle-img', src: icon.value, alt: '' });
900 }
901 return el('span', { className: 'dashicons dashicons-' + icon.value + ' bkbg-ba-handle-icon' });
902 }
903
904 function renderSavedHandleIconWrapped() {
905 return el('span', { className: 'bkbg-ba-handle-surface' },
906 el('span', { className: 'bkbg-ba-handle-anim' },
907 el('span', { className: 'bkbg-ba-handle-hover' },
908 renderSavedHandleIcon()
909 )
910 )
911 );
912 }
913
914 var aspectStyle = (a.aspect && a.aspect !== 'auto') ? { aspectRatio: a.aspect } : {};
915
916 var blockProps = useBlockProps.save({
917 className: cls,
918 style: styleVars,
919 'data-pos': String(a.startPosition || 50),
920 'data-orientation': a.orientation || 'horizontal',
921 'data-show-labels': a.showLabels ? '1' : '0',
922 'data-label-position': a.labelPosition || 'top'
923 });
924
925 return el('div', blockProps,
926 el('div', Object.assign({ className: 'bkbg-ba-aspect' + (a.aspect !== 'auto' ? ' has-aspect' : '') }, aspectStyle),
927 el('div', { className: 'bkbg-ba-inner' },
928 el('div', { className: 'bkbg-ba-layer bkbg-ba-before' },
929 a.beforeImage && a.beforeImage.url
930 ? el('img', { src: a.beforeImage.url, alt: (a.beforeImage.alt || '') })
931 : null
932 ),
933 el('div', { className: 'bkbg-ba-layer bkbg-ba-after' },
934 a.afterImage && a.afterImage.url
935 ? el('img', { src: a.afterImage.url, alt: (a.afterImage.alt || '') })
936 : null
937 ),
938 el('div', { className: 'bkbg-ba-divider' },
939 el('div', { className: 'bkbg-ba-handle' + (a.handleStyle === 'square' ? ' is-square' : ' is-circle') },
940 renderSavedHandleIconWrapped()
941 )
942 ),
943 a.showLabels && el('div', { className: 'bkbg-ba-labels is-' + (a.labelPosition || 'top') },
944 el('div', { className: 'bkbg-ba-label bkbg-ba-label-before' }, a.beforeLabel || __('Before', 'blockenberg')),
945 el('div', { className: 'bkbg-ba-label bkbg-ba-label-after' }, a.afterLabel || __('After', 'blockenberg'))
946 ),
947 el('input', {
948 className: 'bkbg-ba-range',
949 type: 'range',
950 min: 0,
951 max: 100,
952 value: a.startPosition || 50,
953 'aria-label': __('Before/After position', 'blockenberg')
954 })
955 )
956 )
957 );
958 }
959 });
960 });
961