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 / marquee / index.js

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

537 lines 26.0 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 useEffect = wp.element.useEffect;
7
8 var registerBlockType = wp.blocks.registerBlockType;
9
10 var InspectorControls = wp.blockEditor.InspectorControls;
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 Popover = wp.components.Popover;
20 var ColorPicker = wp.components.ColorPicker;
21
22 function clampMin(n, min) {
23 n = parseFloat(n);
24 if (isNaN(n)) return min;
25 return Math.max(min, n);
26 }
27
28 function clone(obj) {
29 var out = {};
30 for (var k in obj) out[k] = obj[k];
31 return out;
32 }
33
34 function colorToHex(c) {
35 if (!c) return '';
36 if (c.hex) return c.hex;
37 if (c.rgb) {
38 var r = c.rgb;
39 var a = typeof r.a === 'number' ? r.a : 1;
40 if (a < 1) {
41 return 'rgba(' + r.r + ',' + r.g + ',' + r.b + ',' + a + ')';
42 }
43 return '#' + [r.r, r.g, r.b].map(function (x) {
44 var h = x.toString(16);
45 return h.length === 1 ? '0' + h : h;
46 }).join('');
47 }
48 return c;
49 }
50
51 var SEPARATOR_OPTIONS = [
52 { label: '', value: '' },
53 { label: '|', value: '|' },
54 { label: '', value: '' },
55 { label: '�
56 ', value: '�
57 ' },
58 { label: '', value: '' },
59 { label: '/', value: '/' },
60 { label: __('None', 'blockenberg'), value: '' }
61 ];
62
63 var TRANSFORM_OPTIONS = [
64 { label: __('None', 'blockenberg'), value: 'none' },
65 { label: __('Uppercase', 'blockenberg'), value: 'uppercase' },
66 { label: __('Lowercase', 'blockenberg'), value: 'lowercase' },
67 { label: __('Capitalize', 'blockenberg'), value: 'capitalize' }
68 ];
69
70 registerBlockType('blockenberg/marquee', {
71 title: __('Marquee / Announcement Bar', 'blockenberg'),
72 icon: 'megaphone',
73 category: 'blockenberg',
74 description: __('Scrolling announcement bar with pause on hover and accessibility support.', 'blockenberg'),
75
76 edit: function (props) {
77 var a = props.attributes;
78 var setAttributes = props.setAttributes;
79 var isSelected = props.isSelected;
80
81 useEffect(function () {
82 if (!a.instanceId) {
83 setAttributes({ instanceId: 'bkbg-mq-' + Math.random().toString(36).slice(2, 10) });
84 }
85 }, [a.instanceId]);
86
87 var colorPopoverState = useState(null);
88 var openColor = colorPopoverState[0];
89 var setOpenColor = colorPopoverState[1];
90
91 function updateItem(index, patch) {
92 var next = (a.items || []).map(function (it, i) {
93 if (i !== index) return it;
94 var updated = clone(it || {});
95 for (var k in patch) updated[k] = patch[k];
96 return updated;
97 });
98 setAttributes({ items: next });
99 }
100
101 function addItem() {
102 var next = (a.items || []).slice();
103 next.push({ text: __('New announcement', 'blockenberg'), link: '', newTab: false });
104 setAttributes({ items: next });
105 }
106
107 function removeItem(index) {
108 var arr = (a.items || []).slice();
109 if (arr.length <= 1) return;
110 arr.splice(index, 1);
111 setAttributes({ items: arr });
112 }
113
114 function moveItem(index, dir) {
115 var arr = (a.items || []).slice();
116 var to = index + dir;
117 if (to < 0 || to >= arr.length) return;
118 var tmp = arr[index];
119 arr[index] = arr[to];
120 arr[to] = tmp;
121 setAttributes({ items: arr });
122 }
123
124 var styleVars = {
125 '--bkbg-mq-bg': a.backgroundColor,
126 '--bkbg-mq-color': a.textColor,
127 '--bkbg-mq-link-hover': a.linkHoverColor,
128 '--bkbg-mq-py': a.paddingY + 'px',
129 '--bkbg-mq-px': a.paddingX + 'px',
130 '--bkbg-mq-font-size': a.fontSize + 'px',
131 '--bkbg-mq-font-weight': a.fontWeight,
132 '--bkbg-mq-letter-spacing': a.letterSpacing + 'px',
133 // Prefer explicit duration (seconds). Fallback for legacy blocks using the old 10..100 "speed" slider.
134 '--bkbg-mq-speed': (a.durationSeconds && a.durationSeconds > 0
135 ? clampMin(a.durationSeconds, 5)
136 : clampMin((150 - (a.speed || 50)), 5)
137 ) + 's',
138 '--bkbg-mq-sep-spacing': a.separatorSpacing + 'px',
139 '--bkbg-mq-border-color': a.borderColor,
140 '--bkbg-mq-border-width': a.borderWidth + 'px',
141 '--bkbg-mq-close-color': a.closeButtonColor
142 };
143
144 var wrapperClass = 'bkbg-marquee-wrap bkbg-editor-wrap';
145 if (a.shadow) wrapperClass += ' has-shadow';
146 if (a.borderTop) wrapperClass += ' has-border-top';
147 if (a.borderBottom) wrapperClass += ' has-border-bottom';
148 if (a.textTransform !== 'none') wrapperClass += ' text-' + a.textTransform;
149 if (a.reduceMotion) wrapperClass += ' respects-motion';
150
151 var blockProps = useBlockProps({
152 className: wrapperClass,
153 style: styleVars,
154 'data-block-label': 'Marquee',
155 'data-instance-id': a.instanceId || undefined,
156 'data-direction': a.direction,
157 'data-pause-hover': a.pauseOnHover ? '1' : '0'
158 });
159
160 function ColorButton(colorKey, label) {
161 return el('div', { className: 'bkbg-mq-color-row', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' } },
162 el('span', {}, label),
163 el('button', {
164 style: { width: 28, height: 28, borderRadius: '50%', border: '2px solid #fff', boxShadow: '0 0 0 1px rgba(0,0,0,0.15)', backgroundColor: a[colorKey], cursor: 'pointer' },
165 onClick: function () { setOpenColor(openColor === colorKey ? null : colorKey); }
166 }),
167 openColor === colorKey && el(Popover, { position: 'bottom left', onClose: function () { setOpenColor(null); } },
168 el(ColorPicker, {
169 color: a[colorKey],
170 onChangeComplete: function (c) {
171 var obj = {};
172 obj[colorKey] = colorToHex(c);
173 setAttributes(obj);
174 }
175 })
176 )
177 );
178 }
179
180 var inspector = el(InspectorControls, {},
181 el(PanelBody, { title: __('Announcements', 'blockenberg'), initialOpen: true },
182 el('div', { className: 'bkbg-marquee-editor-items' },
183 (a.items || []).map(function (item, index) {
184 return el('div', { key: index, className: 'bkbg-marquee-editor-item' },
185 el('div', { className: 'bkbg-marquee-editor-item-fields' },
186 el(TextControl, {
187 label: __('Text', 'blockenberg'),
188 value: item.text || '',
189 onChange: function (v) { updateItem(index, { text: v }); }
190 }),
191 el(TextControl, {
192 label: __('Link (optional)', 'blockenberg'),
193 value: item.link || '',
194 onChange: function (v) { updateItem(index, { link: v }); },
195 type: 'url'
196 }),
197 item.link && el(ToggleControl, {
198 label: __('Open in new tab', 'blockenberg'),
199 checked: !!item.newTab,
200 __nextHasNoMarginBottom: true,
201 onChange: function (v) { updateItem(index, { newTab: v }); }
202 })
203 ),
204 el('div', { className: 'bkbg-marquee-editor-item-actions' },
205 el(Button, { isSmall: true, icon: 'arrow-up-alt2', label: __('Move up', 'blockenberg'), disabled: index === 0, onClick: function () { moveItem(index, -1); } }),
206 el(Button, { isSmall: true, icon: 'arrow-down-alt2', label: __('Move down', 'blockenberg'), disabled: index === a.items.length - 1, onClick: function () { moveItem(index, 1); } }),
207 el(Button, { isSmall: true, icon: 'trash', label: __('Remove', 'blockenberg'), isDestructive: true, disabled: a.items.length <= 1, onClick: function () { removeItem(index); } })
208 )
209 );
210 })
211 ),
212 el(Button, { variant: 'secondary', onClick: addItem }, __('+ Add Announcement', 'blockenberg'))
213 ),
214
215 el(PanelBody, { title: __('Animation', 'blockenberg'), initialOpen: false },
216 el(SelectControl, {
217 label: __('Direction', 'blockenberg'),
218 value: a.direction,
219 options: [
220 { label: __('Left', 'blockenberg'), value: 'left' },
221 { label: __('Right', 'blockenberg'), value: 'right' }
222 ],
223 onChange: function (v) { setAttributes({ direction: v }); }
224 }),
225 el(TextControl, {
226 label: __('Duration (seconds per loop)', 'blockenberg'),
227 type: 'number',
228 value: (typeof a.durationSeconds === 'number' && a.durationSeconds > 0)
229 ? a.durationSeconds
230 : clampMin((150 - (a.speed || 50)), 5),
231 min: 5,
232 step: 0.5,
233 onChange: function (v) { setAttributes({ durationSeconds: clampMin(v, 5) }); },
234 help: __('Lower = faster. Minimum 5s.', 'blockenberg')
235 }),
236 el(ToggleControl, {
237 label: __('Pause on hover', 'blockenberg'),
238 checked: !!a.pauseOnHover,
239 __nextHasNoMarginBottom: true,
240 onChange: function (v) { setAttributes({ pauseOnHover: v }); }
241 }),
242 el(ToggleControl, {
243 label: __('Pause on click', 'blockenberg'),
244 checked: !!a.pauseOnClick,
245 __nextHasNoMarginBottom: true,
246 onChange: function (v) { setAttributes({ pauseOnClick: v }); }
247 }),
248 el(ToggleControl, {
249 label: __('Respect reduced motion preference', 'blockenberg'),
250 checked: !!a.reduceMotion,
251 __nextHasNoMarginBottom: true,
252 onChange: function (v) { setAttributes({ reduceMotion: v }); },
253 help: __('Stops animation when user has enabled reduced motion in system settings.', 'blockenberg')
254 })
255 ),
256
257 el(PanelBody, { title: __('Separator', 'blockenberg'), initialOpen: false },
258 el(ToggleControl, {
259 label: __('Show separator', 'blockenberg'),
260 checked: !!a.showSeparator,
261 __nextHasNoMarginBottom: true,
262 onChange: function (v) { setAttributes({ showSeparator: v }); }
263 }),
264 a.showSeparator && el(SelectControl, {
265 label: __('Separator style', 'blockenberg'),
266 value: a.separator,
267 options: SEPARATOR_OPTIONS,
268 onChange: function (v) { setAttributes({ separator: v }); }
269 }),
270 a.showSeparator && el(RangeControl, {
271 label: __('Separator spacing', 'blockenberg'),
272 value: a.separatorSpacing,
273 onChange: function (v) { setAttributes({ separatorSpacing: v }); },
274 min: 0,
275 max: 100
276 })
277 ),
278
279 el(PanelBody, { title: __('Colors', 'blockenberg'), initialOpen: false },
280 ColorButton('backgroundColor', __('Background', 'blockenberg')),
281 ColorButton('textColor', __('Text', 'blockenberg')),
282 ColorButton('linkHoverColor', __('Link hover', 'blockenberg'))
283 ),
284
285 el(PanelBody, { title: __('Typography', 'blockenberg'), initialOpen: false },
286 el(RangeControl, {
287 label: __('Font size', 'blockenberg'),
288 value: a.fontSize,
289 onChange: function (v) { setAttributes({ fontSize: v }); },
290 min: 10,
291 max: 32
292 }),
293 el(RangeControl, {
294 label: __('Font weight', 'blockenberg'),
295 value: a.fontWeight,
296 onChange: function (v) { setAttributes({ fontWeight: v }); },
297 min: 300,
298 max: 900,
299 step: 100
300 }),
301 el(RangeControl, {
302 label: __('Letter spacing', 'blockenberg'),
303 value: a.letterSpacing,
304 onChange: function (v) { setAttributes({ letterSpacing: v }); },
305 min: -2,
306 max: 10,
307 step: 0.5
308 }),
309 el(SelectControl, {
310 label: __('Text transform', 'blockenberg'),
311 value: a.textTransform,
312 options: TRANSFORM_OPTIONS,
313 onChange: function (v) { setAttributes({ textTransform: v }); }
314 })
315 ),
316
317 el(PanelBody, { title: __('Spacing', 'blockenberg'), initialOpen: false },
318 el(RangeControl, {
319 label: __('Vertical padding', 'blockenberg'),
320 value: a.paddingY,
321 onChange: function (v) { setAttributes({ paddingY: v }); },
322 min: 4,
323 max: 40
324 }),
325 el(RangeControl, {
326 label: __('Horizontal padding', 'blockenberg'),
327 value: a.paddingX,
328 onChange: function (v) { setAttributes({ paddingX: v }); },
329 min: 0,
330 max: 60
331 })
332 ),
333
334 el(PanelBody, { title: __('Border & Shadow', 'blockenberg'), initialOpen: false },
335 el(ToggleControl, {
336 label: __('Border top', 'blockenberg'),
337 checked: !!a.borderTop,
338 __nextHasNoMarginBottom: true,
339 onChange: function (v) { setAttributes({ borderTop: v }); }
340 }),
341 el(ToggleControl, {
342 label: __('Border bottom', 'blockenberg'),
343 checked: !!a.borderBottom,
344 __nextHasNoMarginBottom: true,
345 onChange: function (v) { setAttributes({ borderBottom: v }); }
346 }),
347 (a.borderTop || a.borderBottom) && el(RangeControl, {
348 label: __('Border width', 'blockenberg'),
349 value: a.borderWidth,
350 onChange: function (v) { setAttributes({ borderWidth: v }); },
351 min: 1,
352 max: 5
353 }),
354 (a.borderTop || a.borderBottom) && ColorButton('borderColor', __('Border color', 'blockenberg')),
355 el(ToggleControl, {
356 label: __('Shadow', 'blockenberg'),
357 checked: !!a.shadow,
358 __nextHasNoMarginBottom: true,
359 onChange: function (v) { setAttributes({ shadow: v }); }
360 })
361 ),
362
363 el(PanelBody, { title: __('Close Button', 'blockenberg'), initialOpen: false },
364 el(ToggleControl, {
365 label: __('Show close button', 'blockenberg'),
366 checked: !!a.showCloseButton,
367 __nextHasNoMarginBottom: true,
368 onChange: function (v) { setAttributes({ showCloseButton: v }); }
369 }),
370 a.showCloseButton && ColorButton('closeButtonColor', __('Close button color', 'blockenberg')),
371 a.showCloseButton && el(ToggleControl, {
372 label: __('Remember dismissal', 'blockenberg'),
373 checked: !!a.closePersist,
374 __nextHasNoMarginBottom: true,
375 onChange: function (v) { setAttributes({ closePersist: v }); },
376 help: __('Uses a cookie to remember when visitor closes the bar.', 'blockenberg')
377 }),
378 a.showCloseButton && a.closePersist && el(RangeControl, {
379 label: __('Remember for (days)', 'blockenberg'),
380 value: a.closePersistDays,
381 onChange: function (v) { setAttributes({ closePersistDays: v }); },
382 min: 1,
383 max: 365
384 })
385 ),
386
387 el(PanelBody, { title: __('Sticky Position', 'blockenberg'), initialOpen: false },
388 el(ToggleControl, {
389 label: __('Sticky', 'blockenberg'),
390 checked: !!a.sticky,
391 __nextHasNoMarginBottom: true,
392 onChange: function (v) { setAttributes({ sticky: v }); }
393 }),
394 a.sticky && el(SelectControl, {
395 label: __('Position', 'blockenberg'),
396 value: a.stickyPosition,
397 options: [
398 { label: __('Top', 'blockenberg'), value: 'top' },
399 { label: __('Bottom', 'blockenberg'), value: 'bottom' }
400 ],
401 onChange: function (v) { setAttributes({ stickyPosition: v }); }
402 }),
403 a.sticky && el(RangeControl, {
404 label: __('Z-index', 'blockenberg'),
405 value: a.stickyZIndex,
406 onChange: function (v) { setAttributes({ stickyZIndex: v }); },
407 min: 1,
408 max: 9999
409 })
410 ),
411
412 el(PanelBody, { title: __('Accessibility', 'blockenberg'), initialOpen: false },
413 el(TextControl, {
414 label: __('ARIA label', 'blockenberg'),
415 value: a.ariaLabel,
416 onChange: function (v) { setAttributes({ ariaLabel: v }); },
417 help: __('Describes the region for screen readers.', 'blockenberg')
418 })
419 )
420 );
421
422 function renderItems() {
423 return (a.items || []).map(function (item, index) {
424 var content = el('span', { className: 'bkbg-marquee-item', key: index }, item.text || '');
425 if (a.showSeparator && index < a.items.length - 1) {
426 // When separator is "None" we still keep spacing for readability.
427 return el(Fragment, { key: index },
428 content,
429 a.separator
430 ? el('span', { className: 'bkbg-marquee-separator' }, a.separator)
431 : el('span', { className: 'bkbg-marquee-gap', 'aria-hidden': 'true' })
432 );
433 }
434 return content;
435 });
436 }
437
438 var content = el('div', blockProps,
439 el('div', { className: 'bkbg-marquee-inner' },
440 el('div', { className: 'bkbg-marquee-track' },
441 el('div', { className: 'bkbg-marquee-content' }, renderItems()),
442 el('div', { className: 'bkbg-marquee-content', 'aria-hidden': 'true' }, renderItems())
443 ),
444 a.showCloseButton && el('button', { type: 'button', className: 'bkbg-marquee-close', 'aria-label': __('Close announcement', 'blockenberg') },
445 el('svg', { viewBox: '0 0 24 24', xmlns: 'http://www.w3.org/2000/svg' },
446 el('path', { d: 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z' })
447 )
448 )
449 )
450 );
451
452 return el(Fragment, {}, inspector, content);
453 },
454
455 save: function (props) {
456 var a = props.attributes;
457
458 var styleVars = {
459 '--bkbg-mq-bg': a.backgroundColor,
460 '--bkbg-mq-color': a.textColor,
461 '--bkbg-mq-link-hover': a.linkHoverColor,
462 '--bkbg-mq-py': a.paddingY + 'px',
463 '--bkbg-mq-px': a.paddingX + 'px',
464 '--bkbg-mq-font-size': a.fontSize + 'px',
465 '--bkbg-mq-font-weight': a.fontWeight,
466 '--bkbg-mq-letter-spacing': a.letterSpacing + 'px',
467 '--bkbg-mq-speed': (a.durationSeconds && a.durationSeconds > 0
468 ? clampMin(a.durationSeconds, 5)
469 : clampMin((150 - (a.speed || 50)), 5)
470 ) + 's',
471 '--bkbg-mq-sep-spacing': a.separatorSpacing + 'px',
472 '--bkbg-mq-border-color': a.borderColor,
473 '--bkbg-mq-border-width': a.borderWidth + 'px',
474 '--bkbg-mq-close-color': a.closeButtonColor,
475 '--bkbg-mq-z-index': a.stickyZIndex
476 };
477
478 var wrapperClass = 'bkbg-marquee-wrap';
479 if (a.shadow) wrapperClass += ' has-shadow';
480 if (a.borderTop) wrapperClass += ' has-border-top';
481 if (a.borderBottom) wrapperClass += ' has-border-bottom';
482 if (a.sticky) wrapperClass += ' is-sticky is-sticky-' + a.stickyPosition;
483 if (a.textTransform !== 'none') wrapperClass += ' text-' + a.textTransform;
484 if (a.reduceMotion) wrapperClass += ' respects-motion';
485
486 var blockProps = useBlockProps.save({
487 className: wrapperClass,
488 style: styleVars,
489 'data-instance-id': a.instanceId || undefined,
490 'data-direction': a.direction,
491 'data-pause-hover': a.pauseOnHover ? '1' : '0',
492 'data-pause-click': a.pauseOnClick ? '1' : '0',
493 'data-close-persist': a.closePersist ? '1' : '0',
494 'data-close-persist-days': String(a.closePersistDays),
495 'role': 'region',
496 'aria-label': a.ariaLabel || 'Announcements'
497 });
498
499 function renderItems() {
500 return (a.items || []).map(function (item, index) {
501 var Tag = item.link ? 'a' : 'span';
502 var linkProps = item.link ? {
503 href: item.link,
504 target: item.newTab ? '_blank' : undefined,
505 rel: item.newTab ? 'noopener noreferrer' : undefined
506 } : {};
507
508 var content = el(Tag, Object.assign({ className: 'bkbg-marquee-item', key: index }, linkProps), item.text || '');
509
510 if (a.showSeparator && index < a.items.length - 1) {
511 return el(Fragment, { key: index },
512 content,
513 a.separator
514 ? el('span', { className: 'bkbg-marquee-separator', 'aria-hidden': 'true' }, a.separator)
515 : el('span', { className: 'bkbg-marquee-gap', 'aria-hidden': 'true' })
516 );
517 }
518 return content;
519 });
520 }
521
522 return el('div', blockProps,
523 el('div', { className: 'bkbg-marquee-inner' },
524 el('div', { className: 'bkbg-marquee-track' },
525 el('div', { className: 'bkbg-marquee-content' }, renderItems())
526 ),
527 a.showCloseButton && el('button', { type: 'button', className: 'bkbg-marquee-close', 'aria-label': __('Close announcement', 'blockenberg') },
528 el('svg', { viewBox: '0 0 24 24', xmlns: 'http://www.w3.org/2000/svg' },
529 el('path', { d: 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z' })
530 )
531 )
532 )
533 );
534 }
535 });
536 });
537