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

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

648 lines 36.3 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 useState = wp.element.useState;
5 var useEffect = wp.element.useEffect;
6 var __ = wp.i18n.__;
7 var registerBlockType = wp.blocks.registerBlockType;
8 var InspectorControls = wp.blockEditor.InspectorControls;
9 var useBlockProps = wp.blockEditor.useBlockProps;
10 var RichText = wp.blockEditor.RichText;
11 var PanelBody = wp.components.PanelBody;
12 var ColorPicker = wp.components.ColorPicker;
13 var Popover = wp.components.Popover;
14 var Button = wp.components.Button;
15 var ToggleControl = wp.components.ToggleControl;
16 var RangeControl = wp.components.RangeControl;
17 var SelectControl = wp.components.SelectControl;
18
19 // ── Generate unique block ID ─────────────────────────────────────────────────
20 function generateId() {
21 return 'bq' + Math.random().toString(36).substr(2, 8);
22 }
23
24 // ── Decorative quote mark SVG ────────────────────────────────────────────────
25 // Uses SVG <text> elements for authentic typographic marks.
26 function buildMarkSVG(markStyle, markSize, markColor) {
27 var base = {
28 xmlns: 'http://www.w3.org/2000/svg',
29 'aria-hidden': 'true',
30 focusable: 'false',
31 style: { display: 'block' }
32 };
33
34 if (markStyle === 'fancy') {
35 // Typographic open-double-quote " (U+201C) — curly serif
36 return el('svg', Object.assign({}, base, { viewBox: '0 0 120 100', width: markSize, height: Math.round(markSize * 0.84) }),
37 el('text', { x: '4', y: '96', fontSize: '140', fontFamily: 'Georgia,"Times New Roman",serif', fill: markColor }, '\u201C')
38 );
39 }
40 if (markStyle === 'simple') {
41 // Straight double quote "
42 return el('svg', Object.assign({}, base, { viewBox: '0 0 80 70', width: markSize, height: Math.round(markSize * 0.875) }),
43 el('text', { x: '2', y: '66', fontSize: '100', fontFamily: 'Arial,Helvetica,sans-serif', fill: markColor }, '"')
44 );
45 }
46 if (markStyle === 'chevron') {
47 // Double angle quotation mark »
48 return el('svg', Object.assign({}, base, { viewBox: '0 0 80 64', width: markSize, height: Math.round(markSize * 0.8) }),
49 el('text', { x: '0', y: '60', fontSize: '88', fontFamily: 'Georgia,serif', fill: markColor }, '\u00BB')
50 );
51 }
52 if (markStyle === 'ornate') {
53 // Heavy double turned comma quotation mark ❝ (U+275D)
54 return el('svg', Object.assign({}, base, { viewBox: '0 0 100 88', width: markSize, height: Math.round(markSize * 0.88) }),
55 el('text', { x: '0', y: '84', fontSize: '120', fontFamily: 'Georgia,serif', fill: markColor }, '\u275D')
56 );
57 }
58 // lines: two horizontal rectangular strokes (typographic em-dash pair)
59 var lh = Math.max(3, Math.round(markSize * 0.07));
60 var lw = Math.round(markSize * 0.8);
61 var gap = lh * 2.5;
62 return el('svg', Object.assign({}, base, { viewBox: '0 0 ' + (lw + 2) + ' ' + (lh * 2 + gap), width: lw + 2, height: lh * 2 + gap }),
63 el('rect', { x: 0, y: 0, width: lw, height: lh, rx: lh / 2, fill: markColor }),
64 el('rect', { x: 0, y: lh + gap, width: Math.round(lw * 0.6), height: lh, rx: lh / 2, fill: markColor })
65 );
66 }
67
68 // ── CSS custom properties on wrapper ────────────────────────────────────────
69 function buildWrapStyle(a) {
70 return {
71 '--bkbg-bq-accent-color': a.accentColor,
72 '--bkbg-bq-accent-w': a.accentLineWidth + 'px',
73 '--bkbg-bq-accent-r': a.accentLineRadius + 'px',
74 '--bkbg-bq-bg': a.bgColor,
75 '--bkbg-bq-radius': a.borderRadius + 'px',
76 '--bkbg-bq-pad-t': a.paddingTop + 'px',
77 '--bkbg-bq-pad-r': a.paddingRight + 'px',
78 '--bkbg-bq-pad-b': a.paddingBottom + 'px',
79 '--bkbg-bq-pad-l': a.paddingLeft + 'px',
80 '--bkbg-bq-mark-opacity': (a.markOpacity / 100),
81 '--bkbg-bq-mark-side-gap': a.markOpacity + 'px',
82 '--bkbg-bq-shadow': a.enableShadow ? '0 4px 28px rgba(0,0,0,0.09)' : 'none',
83 '--bkbg-bq-quote-size': a.quoteFontSize + 'px',
84 '--bkbg-bq-quote-weight': a.quoteFontWeight,
85 '--bkbg-bq-quote-fstyle': a.quoteFontStyle,
86 '--bkbg-bq-quote-lh': a.quoteLH,
87 '--bkbg-bq-quote-color': a.quoteColor,
88 '--bkbg-bq-quote-spacing': a.quoteSpacing + 'px',
89 '--bkbg-bq-name-size': a.nameSize + 'px',
90 '--bkbg-bq-name-weight': a.nameWeight,
91 '--bkbg-bq-name-color': a.nameColor,
92 '--bkbg-bq-role-size': a.roleSize + 'px',
93 '--bkbg-bq-role-weight': a.roleWeight,
94 '--bkbg-bq-role-color': a.roleColor,
95 '--bkbg-bq-photo-size': a.photoSize + 'px',
96 '--bkbg-bq-photo-border': a.photoBorder ? ('2px solid ' + a.photoBorderColor) : 'none',
97 '--bkbg-bq-gap-photo': a.gapPhotoText + 'px'
98 };
99 }
100
101 registerBlockType('blockenberg/blockquote', {
102 title: __('Blockquote / Pull Quote', 'blockenberg'),
103 icon: 'format-quote',
104 category: 'blockenberg',
105 description: __('Stylish blockquote with accent line, decorative marks, author photo and role.', 'blockenberg'),
106
107 edit: function (props) {
108 var a = props.attributes;
109 var setAttributes = props.setAttributes;
110
111 // ── Generate blockId on first mount ──────────────────────────────
112 useEffect(function () {
113 if (!a.blockId) { setAttributes({ blockId: generateId() }); }
114 }, []);
115
116 // ── Color picker state (one open at a time) ────────────────────────
117 var openColorKeyState = useState(null);
118 var openColorKey = openColorKeyState[0];
119 var setOpenColorKey = openColorKeyState[1];
120
121 function renderColorControl(key, label, value, onChange) {
122 var isOpen = openColorKey === key;
123 return el('div', { key: key, style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0', gap: '8px' } },
124 el('span', { style: { fontSize: '12px', color: '#1e1e1e', flex: 1, lineHeight: 1.4 } }, label),
125 el('div', { style: { position: 'relative', flexShrink: 0 } },
126 el('button', {
127 type: 'button',
128 title: value || 'none',
129 onClick: function () { setOpenColorKey(isOpen ? null : key); },
130 style: {
131 width: '28px', height: '28px', borderRadius: '4px',
132 border: isOpen ? '2px solid #007cba' : '2px solid #ddd',
133 cursor: 'pointer', padding: 0, display: 'block',
134 background: value || '#ffffff', flexShrink: 0
135 }
136 }),
137 isOpen && el(Popover, {
138 position: 'bottom left',
139 onClose: function () { setOpenColorKey(null); }
140 },
141 el('div', {
142 style: { padding: '8px' },
143 onMouseDown: function (e) { e.stopPropagation(); }
144 },
145 el('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '6px' } },
146 el('strong', { style: { fontSize: '12px' } }, label),
147 el(Button, { icon: 'no-alt', isSmall: true, onClick: function () { setOpenColorKey(null); } })
148 ),
149 el(ColorPicker, {
150 color: value,
151 enableAlpha: true,
152 onChange: function (c) { onChange(c); }
153 })
154 )
155 )
156 )
157 );
158 }
159
160 // ── Open wp.media for author photo ───────────────────────────────
161 function openPhotoLibrary() {
162 var frame = wp.media({
163 title: __('Select Author Photo', 'blockenberg'),
164 button: { text: __('Use as Author Photo', 'blockenberg') },
165 multiple: false,
166 library: { type: 'image' }
167 });
168 frame.on('select', function () {
169 var att = frame.state().get('selection').first().toJSON();
170 setAttributes({ photoUrl: att.url, photoAlt: att.alt || att.title || '' });
171 });
172 frame.open();
173 }
174
175 // ── Option lists ──────────────────────────────────────────────────
176 var quoteStyleOptions = [
177 { label: __('Line — left accent bar', 'blockenberg'), value: 'line' },
178 { label: __('Quotes — mark centred', 'blockenberg'), value: 'quotes' },
179 { label: __('Card — boxed background', 'blockenberg'), value: 'card' },
180 { label: __('Minimal — clean type', 'blockenberg'), value: 'minimal' },
181 { label: __('Bordered — full border', 'blockenberg'), value: 'bordered' },
182 { label: __('Top Bar — header accent', 'blockenberg'), value: 'top-bar' }
183 ];
184
185 var quoteAlignOptions = [
186 { label: __('Left', 'blockenberg'), value: 'left' },
187 { label: __('Center', 'blockenberg'), value: 'center' },
188 { label: __('Right', 'blockenberg'), value: 'right' }
189 ];
190
191 var markStyleOptions = [
192 { label: __('Fancy — curly serif "', 'blockenberg'), value: 'fancy' },
193 { label: __('Simple — straight "', 'blockenberg'), value: 'simple' },
194 { label: __('Chevron — »', 'blockenberg'), value: 'chevron' },
195 { label: __('Ornate — ❝ heavy', 'blockenberg'), value: 'ornate' },
196 { label: __('Lines — two strokes', 'blockenberg'), value: 'lines' }
197 ];
198
199 var markPositionOptions = [
200 { label: __('Side (left of text)', 'blockenberg'), value: 'float' },
201 { label: __('Side (right of text)', 'blockenberg'), value: 'right' },
202 { label: __('Above quote text', 'blockenberg'), value: 'above' }
203 ];
204
205 var authorLayoutOptions = [
206 { label: __('Horizontal — photo left', 'blockenberg'), value: 'horizontal' },
207 { label: __('Horizontal — photo right', 'blockenberg'), value: 'horizontal-right' },
208 { label: __('Vertical — photo above', 'blockenberg'), value: 'vertical' }
209 ];
210
211 var photoShapeOptions = [
212 { label: __('Circle', 'blockenberg'), value: 'circle' },
213 { label: __('Rounded', 'blockenberg'), value: 'rounded' },
214 { label: __('Square', 'blockenberg'), value: 'square' }
215 ];
216
217 var fontWeightOptions = [
218 { label: '300 — Light', value: 300 },
219 { label: '400 — Regular', value: 400 },
220 { label: '500 — Medium', value: 500 },
221 { label: '600 — Semi Bold', value: 600 },
222 { label: '700 — Bold', value: 700 },
223 { label: '800 — Extra Bold', value: 800 }
224 ];
225
226 var quoteFontStyleOptions = [
227 { label: __('Italic', 'blockenberg'), value: 'italic' },
228 { label: __('Normal', 'blockenberg'), value: 'normal' }
229 ];
230
231 var hasAccentLineControl = (
232 a.quoteStyle === 'line' ||
233 a.quoteStyle === 'bordered'||
234 a.quoteStyle === 'top-bar'
235 );
236
237 // ── Inspector panels ──────────────────────────────────────────────
238 var inspector = el(InspectorControls, {},
239
240 // ── Style ─────────────────────────────────────────────────────
241 el(PanelBody, { title: __('Style', 'blockenberg'), initialOpen: true },
242 el(SelectControl, {
243 label: __('Quote Style', 'blockenberg'),
244 value: a.quoteStyle,
245 options: quoteStyleOptions,
246 onChange: function (v) { setAttributes({ quoteStyle: v }); }
247 }),
248 el(SelectControl, {
249 label: __('Alignment', 'blockenberg'),
250 value: a.quoteAlign,
251 options: quoteAlignOptions,
252 onChange: function (v) { setAttributes({ quoteAlign: v }); }
253 }),
254 el('hr', {}),
255 el(ToggleControl, {
256 label: __('Show Decorative Mark', 'blockenberg'),
257 checked: a.showMark,
258 __nextHasNoMarginBottom: true,
259 onChange: function (v) { setAttributes({ showMark: v }); }
260 }),
261 a.showMark && el(Fragment, {},
262 el(SelectControl, {
263 label: __('Mark Style', 'blockenberg'),
264 value: a.markStyle,
265 options: markStyleOptions,
266 onChange: function (v) { setAttributes({ markStyle: v }); }
267 }),
268 el(SelectControl, {
269 label: __('Mark Position', 'blockenberg'),
270 value: a.markPosition,
271 options: markPositionOptions,
272 onChange: function (v) { setAttributes({ markPosition: v }); }
273 }),
274 el(RangeControl, {
275 label: __('Mark Size (px)', 'blockenberg'),
276 value: a.markSize,
277 min: 32,
278 max: 200,
279 onChange: function (v) { setAttributes({ markSize: v }); }
280 }),
281 a.markPosition === 'float' && el(RangeControl, {
282 label: __('Gap between mark and text (px)', 'blockenberg'),
283 value: a.markOpacity,
284 min: 0,
285 max: 60,
286 onChange: function (v) { setAttributes({ markOpacity: v }); }
287 }),
288 a.markPosition === 'right' && el(RangeControl, {
289 label: __('Gap between text and mark (px)', 'blockenberg'),
290 value: a.markOpacity,
291 min: 0,
292 max: 60,
293 onChange: function (v) { setAttributes({ markOpacity: v }); }
294 })
295 )
296 ),
297
298 // ── Typography ────────────────────────────────────────────────
299 el(PanelBody, { title: __('Typography', 'blockenberg'), initialOpen: false },
300 el('p', { style: { margin: '4px 0 6px', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase', color: '#888' } },
301 __('Quote Text', 'blockenberg')
302 ),
303 el(RangeControl, {
304 label: __('Font Size (px)', 'blockenberg'),
305 value: a.quoteFontSize, min: 12, max: 52,
306 onChange: function (v) { setAttributes({ quoteFontSize: v }); }
307 }),
308 el(SelectControl, {
309 label: __('Font Weight', 'blockenberg'),
310 value: a.quoteFontWeight,
311 options: fontWeightOptions,
312 onChange: function (v) { setAttributes({ quoteFontWeight: parseInt(v, 10) }); }
313 }),
314 el(SelectControl, {
315 label: __('Font Style', 'blockenberg'),
316 value: a.quoteFontStyle,
317 options: quoteFontStyleOptions,
318 onChange: function (v) { setAttributes({ quoteFontStyle: v }); }
319 }),
320 el(RangeControl, {
321 label: __('Line Height', 'blockenberg'),
322 value: a.quoteLH, min: 1, max: 2.5, step: 0.05,
323 onChange: function (v) { setAttributes({ quoteLH: v }); }
324 }),
325 el('hr', {}),
326 el('p', { style: { margin: '4px 0 6px', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase', color: '#888' } },
327 __('Author Name', 'blockenberg')
328 ),
329 el(RangeControl, {
330 label: __('Font Size (px)', 'blockenberg'),
331 value: a.nameSize, min: 10, max: 32,
332 onChange: function (v) { setAttributes({ nameSize: v }); }
333 }),
334 el(SelectControl, {
335 label: __('Font Weight', 'blockenberg'),
336 value: a.nameWeight,
337 options: fontWeightOptions,
338 onChange: function (v) { setAttributes({ nameWeight: parseInt(v, 10) }); }
339 }),
340 el('hr', {}),
341 el('p', { style: { margin: '4px 0 6px', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase', color: '#888' } },
342 __('Role / Title', 'blockenberg')
343 ),
344 el(RangeControl, {
345 label: __('Font Size (px)', 'blockenberg'),
346 value: a.roleSize, min: 10, max: 28,
347 onChange: function (v) { setAttributes({ roleSize: v }); }
348 }),
349 el(SelectControl, {
350 label: __('Font Weight', 'blockenberg'),
351 value: a.roleWeight,
352 options: fontWeightOptions,
353 onChange: function (v) { setAttributes({ roleWeight: parseInt(v, 10) }); }
354 })
355 ),
356
357 // ── Author ────────────────────────────────────────────────────
358 el(PanelBody, { title: __('Author', 'blockenberg'), initialOpen: false },
359 el(ToggleControl, {
360 label: __('Show Author', 'blockenberg'),
361 checked: a.showAuthor,
362 __nextHasNoMarginBottom: true,
363 onChange: function (v) { setAttributes({ showAuthor: v }); }
364 }),
365 a.showAuthor && el(Fragment, {},
366 el(SelectControl, {
367 label: __('Author Layout', 'blockenberg'),
368 value: a.authorLayout,
369 options: authorLayoutOptions,
370 onChange: function (v) { setAttributes({ authorLayout: v }); }
371 }),
372 el(ToggleControl, {
373 label: __('Show Role / Title', 'blockenberg'),
374 checked: a.showRole,
375 __nextHasNoMarginBottom: true,
376 onChange: function (v) { setAttributes({ showRole: v }); }
377 }),
378 el('hr', {}),
379 el(ToggleControl, {
380 label: __('Show Author Photo', 'blockenberg'),
381 checked: a.showPhoto,
382 __nextHasNoMarginBottom: true,
383 onChange: function (v) { setAttributes({ showPhoto: v }); }
384 }),
385 a.showPhoto && el(Fragment, {},
386 el(SelectControl, {
387 label: __('Photo Shape', 'blockenberg'),
388 value: a.photoShape,
389 options: photoShapeOptions,
390 onChange: function (v) { setAttributes({ photoShape: v }); }
391 }),
392 el(RangeControl, {
393 label: __('Photo Size (px)', 'blockenberg'),
394 value: a.photoSize, min: 24, max: 120,
395 onChange: function (v) { setAttributes({ photoSize: v }); }
396 }),
397 el(RangeControl, {
398 label: __('Gap photo → text (px)', 'blockenberg'),
399 value: a.gapPhotoText, min: 0, max: 48,
400 onChange: function (v) { setAttributes({ gapPhotoText: v }); }
401 }),
402 el(ToggleControl, {
403 label: __('Photo Border', 'blockenberg'),
404 checked: a.photoBorder,
405 __nextHasNoMarginBottom: true,
406 onChange: function (v) { setAttributes({ photoBorder: v }); }
407 }),
408 el('div', { style: { marginTop: '8px' } },
409 a.photoUrl
410 ? el('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' } },
411 el('img', {
412 src: a.photoUrl, alt: '',
413 style: { width: '48px', height: '48px', objectFit: 'cover', borderRadius: '50%', border: '2px solid #ddd', flexShrink: 0 }
414 }),
415 el(Button, {
416 isSecondary: true, isSmall: true,
417 onClick: openPhotoLibrary
418 }, __('Change', 'blockenberg')),
419 el(Button, {
420 isDestructive: true, isSmall: true,
421 onClick: function () { setAttributes({ photoUrl: '', photoAlt: '' }); }
422 }, __('Remove', 'blockenberg'))
423 )
424 : el(Button, {
425 isPrimary: true, isSmall: true,
426 onClick: openPhotoLibrary
427 }, __('Choose Author Photo', 'blockenberg'))
428 )
429 )
430 )
431 ),
432
433 // ── Colors ────────────────────────────────────────────────────
434 el(PanelBody, { title: __('Colors', 'blockenberg'), initialOpen: false },
435 el('p', { style: { margin: '4px 0 6px', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase', color: '#888' } },
436 __('Quote', 'blockenberg')
437 ),
438 renderColorControl('quoteColor', __('Quote text', 'blockenberg'), a.quoteColor,
439 function (c) { setAttributes({ quoteColor: c }); }),
440
441 el('hr', {}),
442 el('p', { style: { margin: '4px 0 6px', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase', color: '#888' } },
443 __('Accent', 'blockenberg')
444 ),
445 renderColorControl('accentColor', __('Accent / border color', 'blockenberg'), a.accentColor,
446 function (c) { setAttributes({ accentColor: c }); }),
447
448 a.showMark && el(Fragment, {},
449 el('hr', {}),
450 el('p', { style: { margin: '4px 0 6px', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase', color: '#888' } },
451 __('Quote Mark', 'blockenberg')
452 ),
453 renderColorControl('markColor', __('Mark color', 'blockenberg'), a.markColor,
454 function (c) { setAttributes({ markColor: c }); })
455 ),
456
457 el('hr', {}),
458 el('p', { style: { margin: '4px 0 6px', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase', color: '#888' } },
459 __('Author', 'blockenberg')
460 ),
461 renderColorControl('nameColor', __('Author name', 'blockenberg'), a.nameColor,
462 function (c) { setAttributes({ nameColor: c }); }),
463 a.showRole && renderColorControl('roleColor', __('Role / title', 'blockenberg'), a.roleColor,
464 function (c) { setAttributes({ roleColor: c }); }),
465
466 el('hr', {}),
467 el('p', { style: { margin: '4px 0 6px', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase', color: '#888' } },
468 __('Block', 'blockenberg')
469 ),
470 renderColorControl('bgColor', __('Background', 'blockenberg'), a.bgColor,
471 function (c) { setAttributes({ bgColor: c }); }),
472 a.showPhoto && a.photoBorder && renderColorControl('photoBorderColor', __('Photo border', 'blockenberg'), a.photoBorderColor,
473 function (c) { setAttributes({ photoBorderColor: c }); })
474 ),
475
476 // ── Layout & Spacing ──────────────────────────────────────────
477 el(PanelBody, { title: __('Layout & Spacing', 'blockenberg'), initialOpen: false },
478 el(RangeControl, { label: __('Padding Top (px)', 'blockenberg'), value: a.paddingTop, min: 0, max: 120, onChange: function (v) { setAttributes({ paddingTop: v }); } }),
479 el(RangeControl, { label: __('Padding Right (px)', 'blockenberg'), value: a.paddingRight, min: 0, max: 120, onChange: function (v) { setAttributes({ paddingRight: v }); } }),
480 el(RangeControl, { label: __('Padding Bottom (px)', 'blockenberg'), value: a.paddingBottom, min: 0, max: 120, onChange: function (v) { setAttributes({ paddingBottom: v }); } }),
481 el(RangeControl, { label: __('Padding Left (px)', 'blockenberg'), value: a.paddingLeft, min: 0, max: 120, onChange: function (v) { setAttributes({ paddingLeft: v }); } }),
482 el('hr', {}),
483 el(RangeControl, {
484 label: __('Border Radius (px)', 'blockenberg'),
485 value: a.borderRadius, min: 0, max: 48,
486 onChange: function (v) { setAttributes({ borderRadius: v }); }
487 }),
488 el(ToggleControl, {
489 label: __('Drop Shadow', 'blockenberg'),
490 checked: a.enableShadow,
491 __nextHasNoMarginBottom: true,
492 onChange: function (v) { setAttributes({ enableShadow: v }); }
493 }),
494 el('hr', {}),
495 el(RangeControl, {
496 label: __('Gap after quote text (px)', 'blockenberg'),
497 value: a.quoteSpacing, min: 0, max: 80,
498 onChange: function (v) { setAttributes({ quoteSpacing: v }); }
499 }),
500 hasAccentLineControl && el(Fragment, {},
501 el(RangeControl, {
502 label: __('Accent Line Width (px)', 'blockenberg'),
503 value: a.accentLineWidth, min: 1, max: 16,
504 onChange: function (v) { setAttributes({ accentLineWidth: v }); }
505 }),
506 el(RangeControl, {
507 label: __('Accent Line Radius (px)', 'blockenberg'),
508 value: a.accentLineRadius, min: 0, max: 8,
509 onChange: function (v) { setAttributes({ accentLineRadius: v }); }
510 })
511 )
512 )
513 );
514
515 // ── Build sub-elements ────────────────────────────────────────────
516 var markEl = a.showMark
517 ? el('span', { className: 'bkbg-bq-mark', 'aria-hidden': 'true' },
518 buildMarkSVG(a.markStyle, a.markSize, a.markColor))
519 : null;
520
521 // Author photo: real img or click-to-upload placeholder in editor
522 var photoEl = a.showPhoto
523 ? el('div', { className: 'bkbg-bq-photo-wrap', 'data-shape': a.photoShape },
524 a.photoUrl
525 ? el('img', { className: 'bkbg-bq-photo', src: a.photoUrl, alt: a.photoAlt })
526 : el('div', {
527 className: 'bkbg-bq-photo-placeholder',
528 onClick: openPhotoLibrary,
529 title: __('Click to add author photo', 'blockenberg'),
530 style: { cursor: 'pointer' }
531 }, '+')
532 )
533 : null;
534
535 var authorEl = a.showAuthor
536 ? el('div', { className: 'bkbg-bq-author', 'data-layout': a.authorLayout },
537 a.showPhoto && photoEl,
538 el('div', { className: 'bkbg-bq-info' },
539 el(RichText, {
540 tagName: 'p',
541 className: 'bkbg-bq-name',
542 value: a.authorName,
543 onChange: function (v) { setAttributes({ authorName: v }); },
544 placeholder: __('Author name…', 'blockenberg'),
545 allowedFormats: ['core/bold', 'core/italic', 'core/text-color']
546 }),
547 a.showRole && el(RichText, {
548 tagName: 'p',
549 className: 'bkbg-bq-role',
550 value: a.authorRole,
551 onChange: function (v) { setAttributes({ authorRole: v }); },
552 placeholder: __('Role or title…', 'blockenberg'),
553 allowedFormats: ['core/bold', 'core/italic', 'core/text-color']
554 })
555 )
556 )
557 : null;
558
559 // ── Edit render ───────────────────────────────────────────────────
560 var blockProps = useBlockProps({
561 className: 'bkbg-editor-wrap',
562 'data-block-label': 'Blockquote'
563 });
564
565 return el('div', blockProps,
566 inspector,
567 el('figure', Object.assign(
568 { className: 'bkbg-bq-wrap', style: buildWrapStyle(a) },
569 {
570 'data-style': a.quoteStyle,
571 'data-quote-align': a.quoteAlign,
572 'data-mark-pos': a.showMark ? a.markPosition : 'none',
573 'data-photo-shape': a.photoShape,
574 'data-author-layout': a.authorLayout
575 }
576 ),
577 // Float mark = flex row, mark on left
578 a.showMark && a.markPosition === 'float' && markEl,
579 // Right mark = flex row, mark on right (inserted after body via CSS order)
580 a.showMark && a.markPosition === 'right' && markEl,
581 el('div', { className: 'bkbg-bq-body' },
582 // Above mark = in-flow before quote text
583 a.showMark && a.markPosition === 'above' && markEl,
584 el(RichText, {
585 tagName: 'blockquote',
586 className: 'bkbg-bq-text',
587 value: a.quoteText,
588 onChange: function (v) { setAttributes({ quoteText: v }); },
589 placeholder: __('Type your quote here…', 'blockenberg'),
590 allowedFormats: ['core/bold', 'core/italic', 'core/text-color', 'core/link']
591 }),
592 authorEl
593 )
594 )
595 );
596 },
597
598 // ── Save ─────────────────────────────────────────────────────────────────
599 save: function (props) {
600 var a = props.attributes;
601 var RichTextContent = wp.blockEditor.RichText.Content;
602
603 var markEl = a.showMark
604 ? el('span', { className: 'bkbg-bq-mark', 'aria-hidden': 'true' },
605 buildMarkSVG(a.markStyle, a.markSize, a.markColor))
606 : null;
607
608 var photoEl = a.showPhoto && a.photoUrl
609 ? el('div', { className: 'bkbg-bq-photo-wrap', 'data-shape': a.photoShape },
610 el('img', { className: 'bkbg-bq-photo', src: a.photoUrl, alt: a.photoAlt }))
611 : null;
612
613 var authorEl = a.showAuthor
614 ? el('div', { className: 'bkbg-bq-author', 'data-layout': a.authorLayout },
615 a.showPhoto && photoEl,
616 el('div', { className: 'bkbg-bq-info' },
617 el(RichTextContent, { tagName: 'p', className: 'bkbg-bq-name', value: a.authorName }),
618 a.showRole && el(RichTextContent, { tagName: 'p', className: 'bkbg-bq-role', value: a.authorRole })
619 )
620 )
621 : null;
622
623 return el('figure', Object.assign(
624 { className: 'bkbg-bq-wrap', style: buildWrapStyle(a) },
625 {
626 'data-style': a.quoteStyle,
627 'data-quote-align': a.quoteAlign,
628 'data-mark-pos': a.showMark ? a.markPosition : 'none',
629 'data-photo-shape': a.photoShape,
630 'data-author-layout': a.authorLayout
631 }
632 ),
633 a.showMark && a.markPosition === 'float' && markEl,
634 a.showMark && a.markPosition === 'right' && markEl,
635 el('div', { className: 'bkbg-bq-body' },
636 a.showMark && a.markPosition === 'above' && markEl,
637 el(RichTextContent, {
638 tagName: 'blockquote',
639 className: 'bkbg-bq-text',
640 value: a.quoteText
641 }),
642 authorEl
643 )
644 );
645 }
646 });
647 });
648