PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.7
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.7
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 / cta-pulsing-button / index.js

index.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.7, at blocks/cta-pulsing-button/index.js

682 lines 40.1 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 Fragment = wp.element.Fragment;
4 var useState = wp.element.useState;
5 var __ = wp.i18n.__;
6 var registerBlockType = wp.blocks.registerBlockType;
7 var InspectorControls = wp.blockEditor.InspectorControls;
8 var useBlockProps = wp.blockEditor.useBlockProps;
9 var RichText = wp.blockEditor.RichText;
10 var PanelBody = wp.components.PanelBody;
11 var ColorPicker = wp.components.ColorPicker;
12 var Popover = wp.components.Popover;
13 var Button = wp.components.Button;
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 Notice = wp.components.Notice;
19
20 var _pbTC, _pbTV;
21 function _tc() { return _pbTC || (_pbTC = window.bkbgTypographyControl); }
22 function _tv(obj, prefix) { var fn = _pbTV || (_pbTV = window.bkbgTypoCssVars); return fn ? fn(obj, prefix) : {}; }
23
24 // ── Color swatch control ────────────────────────────────────────────────────
25 function makeColorControl(openKey, setOpenKey) {
26 return function renderColorControl(key, label, value, onChange) {
27 var isOpen = openKey === key;
28 return el('div', {
29 key: key,
30 style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0', gap: '8px' }
31 },
32 el('span', { style: { fontSize: '12px', color: '#1e1e1e', flex: 1, lineHeight: 1.4 } }, label),
33 el('div', { style: { position: 'relative', flexShrink: 0 } },
34 el('button', {
35 type: 'button',
36 title: value || 'none',
37 onClick: function () { setOpenKey(isOpen ? null : key); },
38 style: {
39 width: '28px', height: '28px', borderRadius: '4px',
40 border: isOpen ? '2px solid #007cba' : '2px solid #ddd',
41 cursor: 'pointer', padding: 0, display: 'block',
42 background: value || '#ffffff', flexShrink: 0
43 }
44 }),
45 isOpen && el(Popover, { position: 'bottom left', onClose: function () { setOpenKey(null); } },
46 el('div', { style: { padding: '8px' }, onMouseDown: function (e) { e.stopPropagation(); } },
47 el('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '6px' } },
48 el('strong', { style: { fontSize: '12px' } }, label),
49 el(Button, { icon: 'no-alt', isSmall: true, onClick: function () { setOpenKey(null); } })
50 ),
51 el(ColorPicker, { color: value, enableAlpha: true, onChange: onChange })
52 )
53 )
54 )
55 );
56 };
57 }
58
59 // ── Section sub-label ───────────────────────────────────────────────────────
60 function sectionLabel(text) {
61 return el('p', {
62 style: { margin: '8px 0 4px', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase', color: '#888', letterSpacing: '.5px' }
63 }, text);
64 }
65
66 // ── Icon SVG paths ──────────────────────────────────────────────────────────
67 var ICON_PATHS = {
68 'arrow-right': 'M5 12h14M12 5l7 7-7 7',
69 'chevron': 'M9 18l6-6-6-6',
70 'play': 'M5 3l14 9-14 9V3z',
71 'star': 'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z',
72 'fire': 'M8.5 14.5A2.5 2.5 0 0011 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 11-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 002.5 2.5z',
73 'external': 'M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3'
74 };
75
76 function buildIconSvg(a, size) {
77 var s = (size || a.iconSize) + 'px';
78 var path = ICON_PATHS[a.iconType] || ICON_PATHS['arrow-right'];
79 var isPlay = a.iconType === 'play';
80 return el('svg', {
81 viewBox: '0 0 24 24',
82 width: s,
83 height: s,
84 fill: isPlay ? a.iconColor : 'none',
85 stroke: isPlay ? 'none' : a.iconColor,
86 strokeWidth: '2',
87 strokeLinecap: 'round',
88 strokeLinejoin: 'round',
89 style: { display: 'block', flexShrink: 0, transition: 'transform 0.22s' },
90 xmlns: 'http://www.w3.org/2000/svg',
91 'aria-hidden': 'true'
92 },
93 el('path', { d: path })
94 );
95 }
96
97 // ── Button inline style ─────────────────────────────────────────────────────
98 function buildButtonStyle(a) {
99 var isOutline = a.buttonStyle === 'outline' || a.buttonStyle === 'outline-pill';
100 var isPill = a.buttonStyle === 'pill' || a.buttonStyle === 'outline-pill';
101 var isGradient = a.buttonStyle === 'gradient';
102 var isGhost = a.buttonStyle === 'ghost';
103 var radius = isPill ? '9999px' : a.buttonBorderRadius + 'px';
104
105 var bg, color, border;
106 if (isGhost) {
107 bg = 'rgba(255,255,255,0.15)';
108 color = a.buttonTextColor;
109 border = '2px solid rgba(255,255,255,0.35)';
110 } else if (isGradient) {
111 bg = 'linear-gradient(' + a.gradientAngle + 'deg, ' + a.gradientFrom + ', ' + a.gradientTo + ')';
112 color = a.buttonTextColor;
113 border = 'none';
114 } else if (isOutline) {
115 bg = 'transparent';
116 color = a.buttonBorderColor;
117 border = a.buttonBorderWidth + 'px solid ' + a.buttonBorderColor;
118 } else {
119 bg = a.buttonBg;
120 color = a.buttonTextColor;
121 border = a.buttonBorderWidth + 'px solid ' + a.buttonBg;
122 }
123
124 var shadow = a.showButtonShadow
125 ? a.buttonShadowH + 'px ' + a.buttonShadowV + 'px ' + a.buttonShadowBlur + 'px ' + a.buttonShadowSpread + 'px ' + a.buttonShadowColor
126 : 'none';
127
128 return {
129 display: 'inline-flex',
130 alignItems: 'center',
131 justifyContent: 'center',
132 gap: Math.round(a.buttonFontSize * 0.45) + 'px',
133 padding: a.buttonPaddingV + 'px ' + a.buttonPaddingH + 'px',
134 background: bg,
135 color: color,
136 border: border,
137 borderRadius: radius,
138 lineHeight: '1.2',
139 cursor: 'pointer',
140 boxShadow: shadow,
141 textDecoration: 'none',
142 fontFamily: 'inherit',
143 whiteSpace: 'nowrap',
144 position: 'relative',
145 userSelect: 'none'
146 };
147 }
148
149 // ── Pulse wrapper style (CSS vars for animation) ────────────────────────────
150 function buildPulseWrapStyle(a) {
151 return Object.assign({
152 display: 'inline-block',
153 position: 'relative',
154 verticalAlign: 'top',
155 '--bkbg-pb-pulse-color': a.pulseColor,
156 '--bkbg-pb-pulse-speed': a.pulseSpeed + 'ms',
157 '--bkbg-pb-pulse-scale': a.pulseScale,
158 '--bkbg-pb-border-radius': a.buttonBorderRadius + 'px',
159 '--bkbg-pb-btn-fs': a.buttonFontSize + 'px',
160 '--bkbg-pb-btn-fw': a.buttonFontWeight,
161 '--bkbg-pb-btn-ls': a.buttonLetterSpacing + 'px',
162 '--bkbg-pb-btn-tt': a.buttonTextTransform,
163 '--bkbg-pb-sub-fs': a.subtextSize + 'px',
164 }, _tv(a.typoButton, '--bkbg-pb-btn-'), _tv(a.typoSubtext, '--bkbg-pb-sub-'));
165 }
166
167 // ────────────────────────────────────────────────────────────────────────────
168 registerBlockType('blockenberg/cta-pulsing-button', {
169 title: __('CTA Pulsing Button', 'blockenberg'),
170 icon: 'button',
171 category: 'bkbg-marketing',
172
173 edit: function (props) {
174 var a = props.attributes;
175 var setAttributes = props.setAttributes;
176
177 var openColorKeyState = useState(null);
178 var openColorKey = openColorKeyState[0];
179 var setOpenColorKey = openColorKeyState[1];
180 var renderColorControl = makeColorControl(openColorKey, setOpenColorKey);
181
182 // ── Select options ─────────────────────────────────────────────────
183 var buttonStyleOptions = [
184 { label: __('Solid', 'blockenberg'), value: 'solid' },
185 { label: __('Outline', 'blockenberg'), value: 'outline' },
186 { label: __('Pill (solid)', 'blockenberg'), value: 'pill' },
187 { label: __('Pill (outline)', 'blockenberg'), value: 'outline-pill'},
188 { label: __('Gradient', 'blockenberg'), value: 'gradient' },
189 { label: __('Ghost (glass)', 'blockenberg'), value: 'ghost' }
190 ];
191
192 var buttonSizeOptions = [
193 { label: __('Small', 'blockenberg'), value: 'sm' },
194 { label: __('Medium', 'blockenberg'), value: 'md' },
195 { label: __('Large', 'blockenberg'), value: 'lg' },
196 { label: __('Extra Large', 'blockenberg'), value: 'xl' }
197 ];
198
199 var alignOptions = [
200 { label: __('Left', 'blockenberg'), value: 'left' },
201 { label: __('Center', 'blockenberg'), value: 'center' },
202 { label: __('Right', 'blockenberg'), value: 'right' }
203 ];
204
205 var fontWeightOptions = [
206 { label: '300 — Light', value: 300 },
207 { label: '400 — Regular', value: 400 },
208 { label: '500 — Medium', value: 500 },
209 { label: '600 — Semi Bold', value: 600 },
210 { label: '700 — Bold', value: 700 },
211 { label: '800 — Extra Bold',value: 800 },
212 { label: '900 — Black', value: 900 }
213 ];
214
215 var textTransformOptions = [
216 { label: __('None', 'blockenberg'), value: 'none' },
217 { label: __('Uppercase', 'blockenberg'), value: 'uppercase' },
218 { label: __('Capitalize', 'blockenberg'), value: 'capitalize'},
219 { label: __('Lowercase', 'blockenberg'), value: 'lowercase' }
220 ];
221
222 var iconTypeOptions = [
223 { label: __('Arrow Right', 'blockenberg'), value: 'arrow-right' },
224 { label: __('Chevron Right', 'blockenberg'), value: 'chevron' },
225 { label: __('Play triangle', 'blockenberg'), value: 'play' },
226 { label: __('Star', 'blockenberg'), value: 'star' },
227 { label: __('Fire', 'blockenberg'), value: 'fire' },
228 { label: __('External link', 'blockenberg'), value: 'external' }
229 ];
230
231 var iconPositionOptions = [
232 { label: __('Left of text', 'blockenberg'), value: 'left' },
233 { label: __('Right of text', 'blockenberg'), value: 'right' }
234 ];
235
236 var pulseEffectOptions = [
237 { label: __('None', 'blockenberg'), value: 'none' },
238 { label: __('Ring', 'blockenberg'), value: 'ring' },
239 { label: __('Double Ring', 'blockenberg'), value: 'double-ring' },
240 { label: __('Ripple', 'blockenberg'), value: 'ripple' },
241 { label: __('Glow', 'blockenberg'), value: 'glow' },
242 { label: __('Wave', 'blockenberg'), value: 'wave' },
243 { label: __('Breath', 'blockenberg'), value: 'breath' },
244 { label: __('Neon', 'blockenberg'), value: 'neon' },
245 { label: __('Heartbeat', 'blockenberg'), value: 'heartbeat' },
246 { label: __('Float', 'blockenberg'), value: 'float' }
247 ];
248
249 var pulseRingsOptions = [
250 { label: __('Single ring', 'blockenberg'), value: 'single' },
251 { label: __('Double rings', 'blockenberg'), value: 'double' }
252 ];
253
254 // Size preset
255 function applySize(v) {
256 var presets = {
257 sm: { buttonSize: 'sm', buttonFontSize: 13, buttonPaddingV: 10, buttonPaddingH: 22 },
258 md: { buttonSize: 'md', buttonFontSize: 17, buttonPaddingV: 16, buttonPaddingH: 36 },
259 lg: { buttonSize: 'lg', buttonFontSize: 20, buttonPaddingV: 20, buttonPaddingH: 48 },
260 xl: { buttonSize: 'xl', buttonFontSize: 24, buttonPaddingV: 24, buttonPaddingH: 60 }
261 };
262 setAttributes(presets[v] || { buttonSize: v });
263 }
264
265 var isOutline = a.buttonStyle === 'outline' || a.buttonStyle === 'outline-pill';
266 var isGradient = a.buttonStyle === 'gradient';
267 var isGhost = a.buttonStyle === 'ghost';
268 var hasPulse = a.pulseEffect !== 'none';
269
270 var btnStyle = buildButtonStyle(a);
271 var pulseWrapStyle = buildPulseWrapStyle(a);
272 var iconEl = a.showIcon ? buildIconSvg(a) : null;
273
274 // ── Pulse class for editor preview ─────────────────────────────────
275 var pulseClass = 'bkbg-pb-pulse-wrap';
276 if (hasPulse) {
277 pulseClass += ' bkbg-pb-pulse--' + a.pulseEffect;
278 if (a.pulseEffect === 'ring' && a.pulseRings === 'double') {
279 pulseClass += ' bkbg-pb-pulse--double-rings';
280 }
281 }
282
283 // ── Inspector ─────────────────────────────────────────────────────
284 var inspector = el(InspectorControls, {},
285
286 // ── Button & Link ─────────────────────────────────────────────
287 el(PanelBody, { title: __('Button & Link', 'blockenberg'), initialOpen: true },
288 el(TextControl, {
289 label: __('URL', 'blockenberg'),
290 value: a.url,
291 type: 'url',
292 placeholder: 'https://',
293 onChange: function (v) { setAttributes({ url: v }); }
294 }),
295 el(ToggleControl, {
296 label: __('Open in new tab', 'blockenberg'),
297 checked: a.urlNewTab,
298 __nextHasNoMarginBottom: true,
299 onChange: function (v) { setAttributes({ urlNewTab: v }); }
300 }),
301 el(ToggleControl, {
302 label: __('Add rel="nofollow"', 'blockenberg'),
303 checked: a.urlNoFollow,
304 __nextHasNoMarginBottom: true,
305 onChange: function (v) { setAttributes({ urlNoFollow: v }); }
306 }),
307 el('hr', {}),
308 el(SelectControl, {
309 label: __('Alignment', 'blockenberg'),
310 value: a.alignment,
311 options: alignOptions,
312 onChange: function (v) { setAttributes({ alignment: v }); }
313 })
314 ),
315
316 // ── Button Style ──────────────────────────────────────────────
317 el(PanelBody, { title: __('Button Style', 'blockenberg'), initialOpen: true },
318 el(SelectControl, {
319 label: __('Style', 'blockenberg'),
320 value: a.buttonStyle,
321 options: buttonStyleOptions,
322 onChange: function (v) { setAttributes({ buttonStyle: v }); }
323 }),
324 el(SelectControl, {
325 label: __('Size Preset', 'blockenberg'),
326 value: a.buttonSize,
327 options: buttonSizeOptions,
328 onChange: applySize
329 }),
330 el('hr', {}),
331 sectionLabel(__('Typography', 'blockenberg')),
332 el(SelectControl, {
333 label: __('Text Transform', 'blockenberg'),
334 value: a.buttonTextTransform,
335 options: textTransformOptions,
336 onChange: function (v) { setAttributes({ buttonTextTransform: v }); }
337 }),
338 el('hr', {}),
339 sectionLabel(__('Spacing & Shape', 'blockenberg')),
340 el(RangeControl, {
341 label: __('Padding Vertical (px)', 'blockenberg'),
342 value: a.buttonPaddingV, min: 4, max: 48,
343 onChange: function (v) { setAttributes({ buttonPaddingV: v }); }
344 }),
345 el(RangeControl, {
346 label: __('Padding Horizontal (px)', 'blockenberg'),
347 value: a.buttonPaddingH, min: 8, max: 100,
348 onChange: function (v) { setAttributes({ buttonPaddingH: v }); }
349 }),
350 el(RangeControl, {
351 label: __('Border Radius (px)', 'blockenberg'),
352 help: __('Pill styles always use full rounding.', 'blockenberg'),
353 value: a.buttonBorderRadius, min: 0, max: 60,
354 onChange: function (v) { setAttributes({ buttonBorderRadius: v }); }
355 })
356 ),
357
358 // ── Button Colors ─────────────────────────────────────────────
359
360 el( PanelBody, { title: __( 'Typography', 'blockenberg' ), initialOpen: false },
361 _tc() && el(_tc(), { label: __('Button Text', 'blockenberg'), value: a.typoButton, onChange: function (v) { setAttributes({ typoButton: v }); } }),
362 _tc() && el(_tc(), { label: __('Subtext', 'blockenberg'), value: a.typoSubtext, onChange: function (v) { setAttributes({ typoSubtext: v }); } }),
363 el(RangeControl, {
364 label: __('Badge Font Size (px)', 'blockenberg'),
365 value: a.badgeFontSize, min: 8, max: 18,
366 onChange: function (v) { setAttributes({ badgeFontSize: v }); }
367 }),
368 a.showSubtext && el(Fragment, {},
369 renderColorControl('subtextColor', __('Subtext Color', 'blockenberg'), a.subtextColor, function (c) { setAttributes({ subtextColor: c }); }),
370 el(RangeControl, {
371 label: __('Subtext Spacing Above (px)', 'blockenberg'),
372 value: a.subtextSpacing, min: 4, max: 40,
373 onChange: function (v) { setAttributes({ subtextSpacing: v }); }
374 })
375 )
376 ),
377 el(PanelBody, { title: __('Button Colors', 'blockenberg'), initialOpen: false },
378 isGradient
379 ? el(Fragment, {},
380 sectionLabel(__('Gradient', 'blockenberg')),
381 renderColorControl('gradientFrom', __('Gradient From', 'blockenberg'), a.gradientFrom, function (c) { setAttributes({ gradientFrom: c }); }),
382 renderColorControl('gradientTo', __('Gradient To', 'blockenberg'), a.gradientTo, function (c) { setAttributes({ gradientTo: c }); }),
383 el(RangeControl, {
384 label: __('Gradient Angle (deg)', 'blockenberg'),
385 value: a.gradientAngle, min: 0, max: 360,
386 onChange: function (v) { setAttributes({ gradientAngle: v }); }
387 }),
388 el('hr', {}),
389 renderColorControl('buttonTextColor', __('Text Color', 'blockenberg'), a.buttonTextColor, function (c) { setAttributes({ buttonTextColor: c }); }),
390 renderColorControl('buttonTextColorHover', __('Text Color (hover)', 'blockenberg'), a.buttonTextColorHover, function (c) { setAttributes({ buttonTextColorHover: c }); })
391 )
392 : isOutline
393 ? el(Fragment, {},
394 renderColorControl('buttonBorderColor', __('Border & Text Color', 'blockenberg'), a.buttonBorderColor, function (c) { setAttributes({ buttonBorderColor: c }); }),
395 renderColorControl('buttonBgHover', __('Background (hover)', 'blockenberg'), a.buttonBgHover, function (c) { setAttributes({ buttonBgHover: c }); }),
396 renderColorControl('buttonTextColorHover', __('Text Color (hover)', 'blockenberg'), a.buttonTextColorHover, function (c) { setAttributes({ buttonTextColorHover: c }); }),
397 el(RangeControl, {
398 label: __('Border Width (px)', 'blockenberg'),
399 value: a.buttonBorderWidth, min: 1, max: 8,
400 onChange: function (v) { setAttributes({ buttonBorderWidth: v }); }
401 })
402 )
403 : isGhost
404 ? el(Fragment, {},
405 renderColorControl('buttonTextColor', __('Text Color', 'blockenberg'), a.buttonTextColor, function (c) { setAttributes({ buttonTextColor: c }); }),
406 renderColorControl('buttonTextColorHover', __('Text Color (hover)', 'blockenberg'), a.buttonTextColorHover, function (c) { setAttributes({ buttonTextColorHover: c }); })
407 )
408 : el(Fragment, {},
409 renderColorControl('buttonBg', __('Background', 'blockenberg'), a.buttonBg, function (c) { setAttributes({ buttonBg: c }); }),
410 renderColorControl('buttonBgHover', __('Background (hover)', 'blockenberg'), a.buttonBgHover, function (c) { setAttributes({ buttonBgHover: c }); }),
411 renderColorControl('buttonTextColor', __('Text Color', 'blockenberg'), a.buttonTextColor, function (c) { setAttributes({ buttonTextColor: c }); }),
412 renderColorControl('buttonTextColorHover', __('Text Color (hover)', 'blockenberg'), a.buttonTextColorHover, function (c) { setAttributes({ buttonTextColorHover: c }); })
413 )
414 ),
415
416 // ── Button Shadow ─────────────────────────────────────────────
417 el(PanelBody, { title: __('Button Shadow', 'blockenberg'), initialOpen: false },
418 el(ToggleControl, {
419 label: __('Enable Shadow', 'blockenberg'),
420 checked: a.showButtonShadow,
421 __nextHasNoMarginBottom: true,
422 onChange: function (v) { setAttributes({ showButtonShadow: v }); }
423 }),
424 a.showButtonShadow && el(Fragment, {},
425 renderColorControl('buttonShadowColor', __('Shadow Color', 'blockenberg'), a.buttonShadowColor, function (c) { setAttributes({ buttonShadowColor: c }); }),
426 el(RangeControl, { label: __('Horizontal (px)', 'blockenberg'), value: a.buttonShadowH, min: -30, max: 30, onChange: function (v) { setAttributes({ buttonShadowH: v }); } }),
427 el(RangeControl, { label: __('Vertical (px)', 'blockenberg'), value: a.buttonShadowV, min: -20, max: 50, onChange: function (v) { setAttributes({ buttonShadowV: v }); } }),
428 el(RangeControl, { label: __('Blur (px)', 'blockenberg'), value: a.buttonShadowBlur, min: 0, max: 80, onChange: function (v) { setAttributes({ buttonShadowBlur: v }); } }),
429 el(RangeControl, { label: __('Spread (px)', 'blockenberg'), value: a.buttonShadowSpread, min: -10, max: 30, onChange: function (v) { setAttributes({ buttonShadowSpread: v }); } })
430 )
431 ),
432
433 // ── Pulse Animation ───────────────────────────────────────────
434 el(PanelBody, { title: __('Pulse Animation', 'blockenberg'), initialOpen: true },
435 el(SelectControl, {
436 label: __('Effect', 'blockenberg'),
437 value: a.pulseEffect,
438 options: pulseEffectOptions,
439 onChange: function (v) { setAttributes({ pulseEffect: v }); }
440 }),
441 a.pulseEffect !== 'none' && el(Fragment, {},
442 renderColorControl('pulseColor', __('Pulse Color', 'blockenberg'), a.pulseColor, function (c) { setAttributes({ pulseColor: c }); }),
443 el(RangeControl, {
444 label: __('Speed (ms)', 'blockenberg'),
445 help: __('Lower = faster pulse.', 'blockenberg'),
446 value: a.pulseSpeed, min: 600, max: 4000, step: 100,
447 onChange: function (v) { setAttributes({ pulseSpeed: v }); }
448 }),
449 (a.pulseEffect === 'ring' || a.pulseEffect === 'ripple') && el(RangeControl, {
450 label: __('Ring Scale', 'blockenberg'),
451 help: __('How far the rings expand (1.0 = no expansion).', 'blockenberg'),
452 value: a.pulseScale, min: 1.0, max: 3.0, step: 0.05,
453 onChange: function (v) { setAttributes({ pulseScale: v }); }
454 }),
455 a.pulseEffect === 'ring' && el(SelectControl, {
456 label: __('Ring Count', 'blockenberg'),
457 value: a.pulseRings,
458 options: pulseRingsOptions,
459 onChange: function (v) { setAttributes({ pulseRings: v }); }
460 })
461 )
462 ),
463
464 // ── Icon ──────────────────────────────────────────────────────
465 el(PanelBody, { title: __('Icon', 'blockenberg'), initialOpen: false },
466 el(ToggleControl, {
467 label: __('Show Icon', 'blockenberg'),
468 checked: a.showIcon,
469 __nextHasNoMarginBottom: true,
470 onChange: function (v) { setAttributes({ showIcon: v }); }
471 }),
472 a.showIcon && el(Fragment, {},
473 el(SelectControl, {
474 label: __('Icon Style', 'blockenberg'),
475 value: a.iconType,
476 options: iconTypeOptions,
477 onChange: function (v) { setAttributes({ iconType: v }); }
478 }),
479 el(SelectControl, {
480 label: __('Position', 'blockenberg'),
481 value: a.iconPosition,
482 options: iconPositionOptions,
483 onChange: function (v) { setAttributes({ iconPosition: v }); }
484 }),
485 el(RangeControl, {
486 label: __('Size (px)', 'blockenberg'),
487 value: a.iconSize, min: 10, max: 40,
488 onChange: function (v) { setAttributes({ iconSize: v }); }
489 }),
490 renderColorControl('iconColor', __('Color', 'blockenberg'), a.iconColor, function (c) { setAttributes({ iconColor: c }); }),
491 el(ToggleControl, {
492 label: __('Nudge icon on hover', 'blockenberg'),
493 checked: a.iconMoveOnHover,
494 __nextHasNoMarginBottom: true,
495 onChange: function (v) { setAttributes({ iconMoveOnHover: v }); }
496 })
497 )
498 ),
499
500 // ── Badge ─────────────────────────────────────────────────────
501 el(PanelBody, { title: __('Badge', 'blockenberg'), initialOpen: false },
502 el(ToggleControl, {
503 label: __('Show Badge', 'blockenberg'),
504 help: __('Small tag attached to the top-right corner of the button.', 'blockenberg'),
505 checked: a.showBadge,
506 __nextHasNoMarginBottom: true,
507 onChange: function (v) { setAttributes({ showBadge: v }); }
508 }),
509 a.showBadge && el(Fragment, {},
510 el(TextControl, {
511 label: __('Badge Text', 'blockenberg'),
512 value: a.badgeText,
513 onChange: function (v) { setAttributes({ badgeText: v }); }
514 }),
515 renderColorControl('badgeBg', __('Badge Background', 'blockenberg'), a.badgeBg, function (c) { setAttributes({ badgeBg: c }); }),
516 renderColorControl('badgeColor', __('Badge Text Color', 'blockenberg'), a.badgeColor, function (c) { setAttributes({ badgeColor: c }); })
517 )
518 ),
519
520 // ── Subtext ───────────────────────────────────────────────────
521 el(PanelBody, { title: __('Subtext Below Button', 'blockenberg'), initialOpen: false },
522 el(ToggleControl, {
523 label: __('Show Subtext', 'blockenberg'),
524 checked: a.showSubtext,
525 __nextHasNoMarginBottom: true,
526 onChange: function (v) { setAttributes({ showSubtext: v }); }
527 }),
528 )
529 );
530
531 // ── Build badge element ────────────────────────────────────────────
532 var badgeEl = a.showBadge && a.badgeText
533 ? el('span', { className: 'bkbg-pb-badge', style: {
534 position: 'absolute',
535 top: '-10px',
536 right: '-12px',
537 background: a.badgeBg,
538 color: a.badgeColor,
539 fontSize: a.badgeFontSize + 'px',
540 fontWeight: 700,
541 lineHeight: 1,
542 padding: '3px 7px',
543 borderRadius:'10px',
544 whiteSpace: 'nowrap',
545 zIndex: 2
546 } }, a.badgeText)
547 : null;
548
549 // ── Icon element with proper flex order ───────────────────────────
550 var iconLeft = a.showIcon && a.iconPosition === 'left' ? iconEl : null;
551 var iconRight = a.showIcon && a.iconPosition === 'right' ? iconEl : null;
552
553 // ── Editor output ─────────────────────────────────────────────────
554 return el(Fragment, {},
555 inspector,
556 el('div', useBlockProps({ className: 'bkbg-pb-outer' }),
557 el('div', { className: 'bkbg-pb-align-wrap', style: { textAlign: a.alignment } },
558 el('div', { className: pulseClass, style: pulseWrapStyle },
559 el('button', { className: 'bkbg-pb-btn' + (a.iconMoveOnHover ? ' bkbg-pb-icon-hover' : ''), type: 'button', style: btnStyle },
560 iconLeft,
561 el(RichText, {
562 tagName: 'span',
563 className: 'bkbg-pb-label',
564 value: a.buttonText,
565 onChange: function (v) { setAttributes({ buttonText: v }); },
566 allowedFormats: [],
567 placeholder: __('Button text…', 'blockenberg')
568 }),
569 iconRight,
570 badgeEl
571 )
572 ),
573 a.showSubtext && el(RichText, {
574 tagName: 'p',
575 className: 'bkbg-pb-subtext',
576 value: a.subtext,
577 onChange: function (v) { setAttributes({ subtext: v }); },
578 allowedFormats: ['core/bold', 'core/italic', 'core/link'],
579 placeholder: __('e.g. No credit card required', 'blockenberg'),
580 style: {
581 color: a.subtextColor,
582 marginTop: a.subtextSpacing + 'px',
583 marginBottom: 0,
584 padding: 0,
585 textAlign: a.alignment
586 }
587 })
588 )
589 )
590 );
591 },
592
593 // ── Save ────────────────────────────────────────────────────────────────
594 save: function (props) {
595 var a = props.attributes;
596 var RichTextContent = wp.blockEditor.RichText.Content;
597 var btnStyle = buildButtonStyle(a);
598
599 var pulseWrapStyle = buildPulseWrapStyle(a);
600 var pulseClass = 'bkbg-pb-pulse-wrap';
601 if (a.pulseEffect !== 'none') {
602 pulseClass += ' bkbg-pb-pulse--' + a.pulseEffect;
603 if (a.pulseEffect === 'ring' && a.pulseRings === 'double') {
604 pulseClass += ' bkbg-pb-pulse--double-rings';
605 }
606 }
607
608 // Icon
609 var iconEl = null;
610 if (a.showIcon) {
611 iconEl = el('svg', {
612 viewBox: '0 0 24 24',
613 width: a.iconSize + 'px',
614 height: a.iconSize + 'px',
615 fill: a.iconType === 'play' ? a.iconColor : 'none',
616 stroke: a.iconType === 'play' ? 'none' : a.iconColor,
617 strokeWidth: '2',
618 strokeLinecap: 'round',
619 strokeLinejoin: 'round',
620 style: { display: 'block', flexShrink: 0 },
621 xmlns: 'http://www.w3.org/2000/svg',
622 'aria-hidden': 'true',
623 className: 'bkbg-pb-icon'
624 },
625 el('path', { d: ({ 'arrow-right': 'M5 12h14M12 5l7 7-7 7', 'chevron': 'M9 18l6-6-6-6', 'play': 'M5 3l14 9-14 9V3z', 'star': 'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z', 'fire': 'M8.5 14.5A2.5 2.5 0 0011 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 11-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 002.5 2.5z', 'external': 'M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3' })[a.iconType] || 'M5 12h14M12 5l7 7-7 7' })
626 );
627 }
628
629 var iconLeft = a.showIcon && a.iconPosition === 'left' ? iconEl : null;
630 var iconRight = a.showIcon && a.iconPosition === 'right' ? iconEl : null;
631
632 // Badge
633 var badgeEl = a.showBadge && a.badgeText
634 ? el('span', { className: 'bkbg-pb-badge', style: {
635 position: 'absolute', top: '-10px', right: '-12px',
636 background: a.badgeBg, color: a.badgeColor,
637 fontSize: a.badgeFontSize + 'px', fontWeight: 700,
638 lineHeight: 1, padding: '3px 7px', borderRadius: '10px',
639 whiteSpace: 'nowrap', zIndex: 2
640 } }, a.badgeText)
641 : null;
642
643 // Link attrs
644 var linkAttrs = {
645 className: 'bkbg-pb-btn' + (a.iconMoveOnHover ? ' bkbg-pb-icon-hover' : ''),
646 href: a.url || '#',
647 style: btnStyle,
648 'data-pb-bg-hover': a.buttonBgHover,
649 'data-pb-text-hover': a.buttonTextColorHover
650 };
651 if (a.urlNewTab) { linkAttrs.target = '_blank'; }
652 if (a.urlNoFollow){ linkAttrs.rel = 'nofollow' + (a.urlNewTab ? ' noopener noreferrer' : ''); }
653 else if (a.urlNewTab) { linkAttrs.rel = 'noopener noreferrer'; }
654
655 return el('div', wp.blockEditor.useBlockProps.save({ className: 'bkbg-pb-outer' }),
656 el('div', { className: 'bkbg-pb-align-wrap', style: { textAlign: a.alignment } },
657 el('div', { className: pulseClass, style: pulseWrapStyle },
658 el('a', linkAttrs,
659 iconLeft,
660 el(RichTextContent, { tagName: 'span', className: 'bkbg-pb-label', value: a.buttonText }),
661 iconRight,
662 badgeEl
663 )
664 ),
665 a.showSubtext && el(RichTextContent, {
666 tagName: 'p',
667 className: 'bkbg-pb-subtext',
668 value: a.subtext,
669 style: {
670 color: a.subtextColor,
671 marginTop: a.subtextSpacing + 'px',
672 marginBottom: 0,
673 padding: 0,
674 textAlign: a.alignment
675 }
676 })
677 )
678 );
679 }
680 });
681 }() );
682