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 / assets / js / typography-control.js

typography-control.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.7, at assets/js/typography-control.js

747 lines 33.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Blockenberg Typography Control
3 *
4 * Reusable Elementor-like typography popover control.
5 * Exposed as: window.bkbgTypographyControl
6 *
7 * Usage in a block's index.js:
8 * var TypographyControl = window.bkbgTypographyControl;
9 * el(TypographyControl, {
10 * label: __('Title Typography'),
11 * value: attributes.titleTypo || {},
12 * onChange: function(v) { setAttributes({ titleTypo: v }); }
13 * })
14 */
15 ( function () {
16 'use strict';
17
18 var el = wp.element.createElement;
19 var useState = wp.element.useState;
20 var useEffect = wp.element.useEffect;
21 var useRef = wp.element.useRef;
22 var Button = wp.components.Button;
23 var Popover = wp.components.Popover;
24 var SelectControl = wp.components.SelectControl;
25 var __ = wp.i18n.__;
26
27 /* -------------------------------------------------------
28 Default typography object shape
29 ------------------------------------------------------- */
30 var DEFAULT_TYPO = {
31 family : '',
32 sizeDesktop : '', sizeTablet : '', sizeMobile : '', sizeUnit : 'px',
33 weight : '',
34 transform : '',
35 style : '',
36 decoration : '',
37 lineHeightDesktop : '', lineHeightTablet : '', lineHeightMobile : '', lineHeightUnit : 'px',
38 letterSpacingDesktop: '', letterSpacingTablet: '', letterSpacingMobile: '', letterSpacingUnit: 'px',
39 wordSpacingDesktop : '', wordSpacingTablet : '', wordSpacingMobile : '', wordSpacingUnit : 'px'
40 };
41
42 /* -------------------------------------------------------
43 System fonts (never loaded from Google)
44 ------------------------------------------------------- */
45 var SYSTEM_FONTS = [
46 'Arial', 'Georgia', 'Helvetica', 'Tahoma',
47 'Times New Roman', 'Trebuchet MS', 'Verdana'
48 ];
49
50 /* -------------------------------------------------------
51 Load a Google Font into a document's <head>
52 ------------------------------------------------------- */
53 function injectFontLink( targetDoc, id, href ) {
54 if ( ! targetDoc || ! targetDoc.head ) return;
55 if ( targetDoc.getElementById( id ) ) return;
56 var link = targetDoc.createElement( 'link' );
57 link.id = id;
58 link.rel = 'stylesheet';
59 link.href = href;
60 targetDoc.head.appendChild( link );
61 }
62
63 function loadGoogleFont( family ) {
64 if ( ! family ) return;
65 if ( SYSTEM_FONTS.indexOf( family ) !== -1 ) return;
66
67 var id = 'bkbg-gf-' + family.replace( /\s+/g, '-' ).toLowerCase();
68 var href = 'https://fonts.googleapis.com/css2?family=' +
69 encodeURIComponent( family ) +
70 ':wght@300;400;500;600;700;800;900&display=swap';
71
72 // 1. Outer document (admin page shell)
73 injectFontLink( document, id, href );
74
75 // 2. Editor iframe — WordPress 5.9+ uses an iframe canvas.
76 // The iframe may not exist yet on first call; we also retry once
77 // after a short delay to handle late-mounting iframes.
78 function injectIntoEditorIframe() {
79 // Selectors used by different WP versions
80 var selectors = [
81 'iframe[name="editor-canvas"]',
82 '.editor-canvas__iframe',
83 'iframe.editor-canvas__iframe',
84 '.block-editor-iframe__container iframe'
85 ];
86 var i, iframe;
87 for ( i = 0; i < selectors.length; i++ ) {
88 iframe = document.querySelector( selectors[ i ] );
89 if ( iframe ) break;
90 }
91 if ( iframe && iframe.contentDocument ) {
92 injectFontLink( iframe.contentDocument, id, href );
93 }
94 }
95
96 injectIntoEditorIframe();
97 // Retry after 500 ms in case the iframe wasn't mounted yet
98 setTimeout( injectIntoEditorIframe, 500 );
99 }
100
101 /* -------------------------------------------------------
102 SVG Icons
103 ------------------------------------------------------- */
104 function iconDevice( type, isActive ) {
105 var c = isActive ? '#e50087' : 'currentColor';
106 if ( type === 'desktop' ) {
107 return el( 'svg', { width: 13, height: 13, viewBox: '0 0 24 24', fill: 'none' },
108 el( 'path', { d: 'M20 3H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h6l-1 2H8v2h8v-2h-1l-1-2h6c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 12H4V5h16v10z', fill: c } )
109 );
110 }
111 if ( type === 'tablet' ) {
112 return el( 'svg', { width: 13, height: 13, viewBox: '0 0 24 24', fill: 'none' },
113 el( 'path', { d: 'M18.5 0h-14A2.5 2.5 0 0 0 2 2.5v19A2.5 2.5 0 0 0 4.5 24h14a2.5 2.5 0 0 0 2.5-2.5v-19A2.5 2.5 0 0 0 18.5 0zm-7 23c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm7.5-4H4V3h15v16z', fill: c } )
114 );
115 }
116 // mobile
117 return el( 'svg', { width: 13, height: 13, viewBox: '0 0 24 24', fill: 'none' },
118 el( 'path', { d: 'M15.5 1h-8A2.5 2.5 0 0 0 5 3.5v17A2.5 2.5 0 0 0 7.5 23h8a2.5 2.5 0 0 0 2.5-2.5v-17A2.5 2.5 0 0 0 15.5 1zm-4 21c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4.5-4H7V4h9v14z', fill: c } )
119 );
120 }
121
122 function iconPencil() {
123 return el( 'svg', { width: 13, height: 13, viewBox: '0 0 24 24', fill: 'none' },
124 el( 'path', { d: 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z', fill: 'currentColor' } )
125 );
126 }
127
128 function iconReset() {
129 return el( 'svg', { width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none' },
130 el( 'path', { d: 'M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z', fill: 'currentColor' } )
131 );
132 }
133
134 function iconClose() {
135 return el( 'svg', { width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none' },
136 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', fill: 'currentColor' } )
137 );
138 }
139
140 /* -------------------------------------------------------
141 Unit Selector (reused from inspector-tabs pattern)
142 ------------------------------------------------------- */
143 function UnitSelector( props ) {
144 var unit = props.unit || 'px';
145 var units = props.units || [ 'px', 'em', 'rem' ];
146 var onChange = props.onChange;
147
148 var _pop = useState( false );
149 var isOpen = _pop[ 0 ];
150 var setOpen = _pop[ 1 ];
151
152 return el( 'div', { className: 'bkbg-unit-selector' },
153 el( Button, {
154 className: 'bkbg-unit-btn',
155 onClick: function () { setOpen( ! isOpen ); }
156 },
157 el( 'span', { className: 'bkbg-unit-value' }, unit ),
158 el( 'span', { className: 'dashicons dashicons-arrow-down-alt2 bkbg-unit-caret', 'aria-hidden': true } )
159 ),
160 isOpen && el( Popover, {
161 position: 'bottom right',
162 onClose: function () { setOpen( false ); },
163 noArrow: true,
164 focusOnMount: 'container',
165 className: 'bkbg-unit-popover'
166 },
167 el( 'div', { className: 'bkbg-unit-list' },
168 units.map( function ( u ) {
169 return el( Button, {
170 key: u,
171 className: 'bkbg-unit-option' + ( u === unit ? ' is-active' : '' ),
172 onClick: function () { onChange( u ); setOpen( false ); }
173 }, u );
174 } )
175 )
176 )
177 );
178 }
179
180 /* -------------------------------------------------------
181 Editor preview device sync helpers
182 (mirrors the same helpers in inspector-tabs.js)
183 ------------------------------------------------------- */
184 function normalizePreviewDeviceName( raw ) {
185 if ( ! raw ) return 'desktop';
186 var v = String( raw ).toLowerCase();
187 if ( v === 'tablet' ) return 'tablet';
188 if ( v === 'mobile' ) return 'mobile';
189 return 'desktop';
190 }
191
192 function getEditorPreviewDevice() {
193 var stores = [ 'core/edit-post', 'core/edit-site' ];
194 for ( var i = 0; i < stores.length; i++ ) {
195 var sel = wp.data && wp.data.select ? wp.data.select( stores[ i ] ) : null;
196 if ( ! sel ) continue;
197 if ( typeof sel.__experimentalGetPreviewDeviceType === 'function' ) {
198 return normalizePreviewDeviceName( sel.__experimentalGetPreviewDeviceType() );
199 }
200 if ( typeof sel.getDeviceType === 'function' ) {
201 return normalizePreviewDeviceName( sel.getDeviceType() );
202 }
203 }
204 return 'desktop';
205 }
206
207 function setEditorPreviewDevice( deviceName ) {
208 var mapped = deviceName === 'desktop' ? 'Desktop' : ( deviceName === 'tablet' ? 'Tablet' : 'Mobile' );
209 var stores = [ 'core/edit-post', 'core/edit-site' ];
210 stores.forEach( function ( storeName ) {
211 var dispatcher = wp.data && wp.data.dispatch ? wp.data.dispatch( storeName ) : null;
212 if ( ! dispatcher ) return;
213 if ( typeof dispatcher.__experimentalSetPreviewDeviceType === 'function' ) {
214 dispatcher.__experimentalSetPreviewDeviceType( mapped );
215 } else if ( typeof dispatcher.setDeviceType === 'function' ) {
216 dispatcher.setDeviceType( mapped );
217 }
218 } );
219 }
220
221 /* -------------------------------------------------------
222 Device Switcher (compact, inline for typo panel)
223 ------------------------------------------------------- */
224 function TypoDeviceSwitcher( props ) {
225 var device = props.device;
226 var onChange = props.onChange;
227
228 var _pop = useState( false );
229 var isOpen = _pop[ 0 ];
230 var setOpen = _pop[ 1 ];
231
232 return el( 'div', { className: 'bkbg-device-switcher' },
233 el( Button, {
234 className: 'bkbg-device-btn',
235 onClick: function () { setOpen( ! isOpen ); },
236 'aria-label': __( 'Switch device', 'blockenberg' )
237 }, iconDevice( device, false ) ),
238 isOpen && el( Popover, {
239 position: 'bottom center',
240 onClose: function () { setOpen( false ); },
241 noArrow: true,
242 focusOnMount: 'container',
243 className: 'bkbg-device-popover'
244 },
245 el( 'div', { className: 'bkbg-device-list' },
246 [ 'desktop', 'tablet', 'mobile' ].map( function ( d ) {
247 return el( Button, {
248 key: d,
249 className: 'bkbg-device-option' + ( d === device ? ' is-active' : '' ),
250 onClick: function () {
251 onChange( d );
252 setEditorPreviewDevice( d );
253 setOpen( false );
254 }
255 }, iconDevice( d, d === device ) );
256 } )
257 )
258 )
259 );
260 }
261
262 /* -------------------------------------------------------
263 Responsive Slider+Input control
264 prefix : 'size' | 'lineHeight' | 'letterSpacing' | 'wordSpacing'
265 unitKey : 'sizeUnit' | 'lineHeightUnit' | etc.
266 ------------------------------------------------------- */
267 function ResponsiveSliderControl( props ) {
268 var label = props.label;
269 var prefix = props.prefix; // e.g. 'size'
270 var unitKey = props.unitKey; // e.g. 'sizeUnit'
271 var units = props.units || [ 'px', 'em', 'rem' ];
272 var minVal = props.min !== undefined ? props.min : 0;
273 var maxVal = props.max !== undefined ? props.max : 200;
274 var stepVal = props.step || 1;
275 var typo = props.typo;
276 var onChange = props.onChange;
277 var device = props.device;
278 var onDeviceChange = props.onDeviceChange;
279
280 // Build the responsive attribute key, e.g. 'sizeDesktop'
281 var suffix = device === 'desktop' ? 'Desktop' : ( device === 'tablet' ? 'Tablet' : 'Mobile' );
282 var actualKey = prefix + suffix; // e.g. 'sizeDesktop'
283 var value = typo[ actualKey ] !== undefined ? typo[ actualKey ] : '';
284 var unit = typo[ unitKey ] || units[ 0 ];
285
286 // Adjust max for relative units
287 var effectiveMax = maxVal;
288 if ( unit === 'em' || unit === 'rem' ) { effectiveMax = 20; }
289 if ( unit === 'vw' ) { effectiveMax = 100; }
290 if ( unit === 'lh' || unit === 'rlh' ) { effectiveMax = 10; }
291
292 var effectiveStep = ( unit === 'em' || unit === 'rem' || unit === 'lh' || unit === 'rlh' ) ? 0.1 : stepVal;
293
294 function updateValue( v ) {
295 var upd = {};
296 upd[ actualKey ] = v;
297 onChange( upd );
298 }
299
300 function updateUnit( u ) {
301 var upd = {};
302 upd[ unitKey ] = u;
303 onChange( upd );
304 }
305
306 return el( 'div', { className: 'bkbg-typo-field bkbg-typo-responsive-field' },
307 // Row: label + device switcher + unit selector
308 el( 'div', { className: 'bkbg-typo-field-header' },
309 el( 'span', { className: 'bkbg-typo-field-label' }, label ),
310 el( 'div', { className: 'bkbg-typo-field-header-right' },
311 el( TypoDeviceSwitcher, { device: device, onChange: onDeviceChange } ),
312 el( UnitSelector, { unit: unit, units: units, onChange: updateUnit } )
313 )
314 ),
315 // Row: slider + number input
316 el( 'div', { className: 'bkbg-typo-slider-row' },
317 el( 'input', {
318 type : 'range',
319 className : 'bkbg-typo-range',
320 min : minVal,
321 max : effectiveMax,
322 step : effectiveStep,
323 value : value !== '' ? parseFloat( value ) : minVal,
324 onChange : function ( e ) { updateValue( e.target.value ); }
325 } ),
326 el( 'input', {
327 type : 'number',
328 className : 'bkbg-typo-number',
329 value : value,
330 min : minVal,
331 max : effectiveMax,
332 step : effectiveStep,
333 placeholder: '',
334 onChange : function ( e ) { updateValue( e.target.value ); }
335 } )
336 )
337 );
338 }
339
340 /* -------------------------------------------------------
341 Main TypographyControl Component
342 ------------------------------------------------------- */
343 function TypographyControl( props ) {
344 var label = props.label;
345 var value = props.value || {};
346 var onChange = props.onChange;
347
348 // Merge stored value with defaults
349 var typo = {};
350 var k;
351 for ( k in DEFAULT_TYPO ) { typo[ k ] = DEFAULT_TYPO[ k ]; }
352 for ( k in value ) { if ( value[ k ] !== undefined && value[ k ] !== null ) typo[ k ] = value[ k ]; }
353
354 var _open = useState( false );
355 var isOpen = _open[ 0 ];
356 var setOpen = _open[ 1 ];
357
358 var _device = useState( getEditorPreviewDevice() );
359 var device = _device[ 0 ];
360 var setDevice = _device[ 1 ];
361
362 var anchorRef = useRef( null );
363
364 // Sync device with the editor's responsive preview toolbar (two-way)
365 useEffect( function () {
366 if ( ! wp.data || typeof wp.data.subscribe !== 'function' ) return;
367 var unsubscribe = wp.data.subscribe( function () {
368 var editorDevice = getEditorPreviewDevice();
369 setDevice( function ( current ) {
370 return current === editorDevice ? current : editorDevice;
371 } );
372 } );
373 return function () {
374 if ( typeof unsubscribe === 'function' ) unsubscribe();
375 };
376 }, [] );
377
378 // Load font in editor whenever family changes
379 useEffect( function () {
380 if ( typo.family ) { loadGoogleFont( typo.family ); }
381 }, [ typo.family ] );
382
383 function update( updates ) {
384 var next = {};
385 for ( k in typo ) { next[ k ] = typo[ k ]; }
386 for ( k in updates ) { next[ k ] = updates[ k ]; }
387 onChange( next );
388 }
389
390 function reset() { onChange( {} ); }
391
392 // Detect if any non-default value is set
393 function hasValue() {
394 for ( k in DEFAULT_TYPO ) {
395 var v = typo[ k ];
396 if ( v !== null && v !== undefined && v !== '' && v !== DEFAULT_TYPO[ k ] ) { return true; }
397 }
398 return false;
399 }
400
401 /* ----- Font family options ----- */
402 var fontOptions = [ { label: __( 'Default', 'blockenberg' ), value: '' } ];
403
404 // System fonts group
405 SYSTEM_FONTS.forEach( function ( f ) {
406 fontOptions.push( { label: f, value: f } );
407 } );
408
409 // Google Fonts (injected by PHP via wp_localize_script → window.bkbgGoogleFonts)
410 var gFonts = ( typeof window.bkbgGoogleFonts !== 'undefined' ) ? window.bkbgGoogleFonts : [];
411 gFonts.forEach( function ( f ) {
412 fontOptions.push( { label: f, value: f } );
413 } );
414
415 /* ----- Option lists ----- */
416 var weightOptions = [
417 { label: __( 'Default', 'blockenberg' ), value: '' },
418 { label: __( '100 (Thin)', 'blockenberg' ), value: '100' },
419 { label: __( '200 (Extra Light)', 'blockenberg' ), value: '200' },
420 { label: __( '300 (Light)', 'blockenberg' ), value: '300' },
421 { label: __( '400 (Normal)', 'blockenberg' ), value: '400' },
422 { label: __( '500 (Medium)', 'blockenberg' ), value: '500' },
423 { label: __( '600 (Semi Bold)', 'blockenberg' ), value: '600' },
424 { label: __( '700 (Bold)', 'blockenberg' ), value: '700' },
425 { label: __( '800 (Extra Bold)', 'blockenberg' ), value: '800' },
426 { label: __( '900 (Black)', 'blockenberg' ), value: '900' },
427 { label: __( 'Normal', 'blockenberg' ), value: 'normal' },
428 { label: __( 'Bold', 'blockenberg' ), value: 'bold' }
429 ];
430
431 var transformOptions = [
432 { label: __( 'Default', 'blockenberg' ), value: '' },
433 { label: __( 'Uppercase', 'blockenberg' ), value: 'uppercase' },
434 { label: __( 'Lowercase', 'blockenberg' ), value: 'lowercase' },
435 { label: __( 'Capitalize', 'blockenberg' ), value: 'capitalize' },
436 { label: __( 'Normal', 'blockenberg' ), value: 'none' }
437 ];
438
439 var styleOptions = [
440 { label: __( 'Default', 'blockenberg' ), value: '' },
441 { label: __( 'Normal', 'blockenberg' ), value: 'normal' },
442 { label: __( 'Italic', 'blockenberg' ), value: 'italic' },
443 { label: __( 'Oblique', 'blockenberg' ), value: 'oblique' }
444 ];
445
446 var decorationOptions = [
447 { label: __( 'Default', 'blockenberg' ), value: '' },
448 { label: __( 'Underline', 'blockenberg' ), value: 'underline' },
449 { label: __( 'Overline', 'blockenberg' ), value: 'overline' },
450 { label: __( 'Line Through', 'blockenberg' ), value: 'line-through' },
451 { label: __( 'None', 'blockenberg' ), value: 'none' }
452 ];
453
454 /* ----- Render ----- */
455 return el( 'div', { className: 'bkbg-typo-control' },
456
457 // Label row with pencil button
458 el( 'div', {
459 ref : anchorRef,
460 className : 'bkbg-typo-label-row'
461 },
462 el( 'span', { className: 'bkbg-typo-label' + ( hasValue() ? ' has-value' : '' ) }, label ),
463 el( Button, {
464 className : 'bkbg-typo-edit-btn' + ( hasValue() ? ' has-value' : '' ),
465 onClick : function () { setOpen( ! isOpen ); },
466 'aria-label': __( 'Edit Typography', 'blockenberg' )
467 }, iconPencil() )
468 ),
469
470 // Typography popover panel
471 isOpen && el( Popover, {
472 anchor : anchorRef.current,
473 placement : 'left-start',
474 offset : 12,
475 onClose : function () { setOpen( false ); },
476 noArrow : true,
477 focusOnMount : 'container',
478 className : 'bkbg-typo-popover'
479 },
480 el( 'div', { className: 'bkbg-typo-panel' },
481
482 // Panel header
483 el( 'div', { className: 'bkbg-typo-panel-header' },
484 el( 'span', { className: 'bkbg-typo-panel-title' }, __( 'Typography', 'blockenberg' ) ),
485 el( 'div', { className: 'bkbg-typo-panel-actions' },
486 el( Button, {
487 className : 'bkbg-typo-header-btn',
488 onClick : reset,
489 'aria-label': __( 'Reset typography', 'blockenberg' ),
490 isSmall : true
491 }, iconReset() ),
492 el( Button, {
493 className : 'bkbg-typo-header-btn',
494 onClick : function () { setOpen( false ); },
495 'aria-label': __( 'Close', 'blockenberg' ),
496 isSmall : true
497 }, iconClose() )
498 )
499 ),
500
501 // Family
502 el( 'div', { className: 'bkbg-typo-field' },
503 el( 'label', { className: 'bkbg-typo-field-label' }, __( 'Family', 'blockenberg' ) ),
504 el( SelectControl, {
505 value : typo.family,
506 options : fontOptions,
507 onChange : function ( v ) { update( { family: v } ); },
508 __nextHasNoMarginBottom: true
509 } )
510 ),
511
512 // Size (responsive)
513 el( ResponsiveSliderControl, {
514 label : __( 'Size', 'blockenberg' ),
515 prefix : 'size',
516 unitKey : 'sizeUnit',
517 units : [ 'px', 'em', 'rem', 'vw' ],
518 min : 0,
519 max : 200,
520 typo : typo,
521 onChange : update,
522 device : device,
523 onDeviceChange: setDevice
524 } ),
525
526 // Weight
527 el( 'div', { className: 'bkbg-typo-field' },
528 el( 'label', { className: 'bkbg-typo-field-label' }, __( 'Weight', 'blockenberg' ) ),
529 el( SelectControl, {
530 value : typo.weight,
531 options : weightOptions,
532 onChange : function ( v ) { update( { weight: v } ); },
533 __nextHasNoMarginBottom: true
534 } )
535 ),
536
537 // Transform
538 el( 'div', { className: 'bkbg-typo-field' },
539 el( 'label', { className: 'bkbg-typo-field-label' }, __( 'Transform', 'blockenberg' ) ),
540 el( SelectControl, {
541 value : typo.transform,
542 options : transformOptions,
543 onChange : function ( v ) { update( { transform: v } ); },
544 __nextHasNoMarginBottom: true
545 } )
546 ),
547
548 // Style
549 el( 'div', { className: 'bkbg-typo-field' },
550 el( 'label', { className: 'bkbg-typo-field-label' }, __( 'Style', 'blockenberg' ) ),
551 el( SelectControl, {
552 value : typo.style,
553 options : styleOptions,
554 onChange : function ( v ) { update( { style: v } ); },
555 __nextHasNoMarginBottom: true
556 } )
557 ),
558
559 // Decoration
560 el( 'div', { className: 'bkbg-typo-field' },
561 el( 'label', { className: 'bkbg-typo-field-label' }, __( 'Decoration', 'blockenberg' ) ),
562 el( SelectControl, {
563 value : typo.decoration,
564 options : decorationOptions,
565 onChange : function ( v ) { update( { decoration: v } ); },
566 __nextHasNoMarginBottom: true
567 } )
568 ),
569
570 // Line Height (responsive)
571 el( ResponsiveSliderControl, {
572 label : __( 'Line Height', 'blockenberg' ),
573 prefix : 'lineHeight',
574 unitKey : 'lineHeightUnit',
575 units : [ 'px', 'em', 'rem', 'lh', 'rlh' ],
576 min : 0,
577 max : 200,
578 typo : typo,
579 onChange : update,
580 device : device,
581 onDeviceChange: setDevice
582 } ),
583
584 // Letter Spacing (responsive)
585 el( ResponsiveSliderControl, {
586 label : __( 'Letter Spacing', 'blockenberg' ),
587 prefix : 'letterSpacing',
588 unitKey : 'letterSpacingUnit',
589 units : [ 'px', 'em', 'rem' ],
590 min : -10,
591 max : 50,
592 typo : typo,
593 onChange : update,
594 device : device,
595 onDeviceChange: setDevice
596 } ),
597
598 // Word Spacing (responsive)
599 el( ResponsiveSliderControl, {
600 label : __( 'Word Spacing', 'blockenberg' ),
601 prefix : 'wordSpacing',
602 unitKey : 'wordSpacingUnit',
603 units : [ 'px', 'em', 'rem' ],
604 min : -10,
605 max : 50,
606 typo : typo,
607 onChange : update,
608 device : device,
609 onDeviceChange: setDevice
610 } )
611
612 ) // end bkbg-typo-panel
613 ) // end Popover
614 ); // end bkbg-typo-control
615 }
616
617 /* -------------------------------------------------------
618 Utility: convert a typography object to CSS vars
619 prefix : CSS variable prefix, e.g. '--bkbg-ac-header-'
620
621 Responsive properties (size, line-height, letter-spacing,
622 word-spacing) are output as three device-specific vars:
623 prefix + 'font-size-d' (desktop)
624 prefix + 'font-size-t' (tablet)
625 prefix + 'font-size-m' (mobile)
626 CSS then picks the right one via @media + var() fallback.
627
628 Non-responsive properties (family, weight, transform,
629 style, decoration) are output as a single var.
630 ------------------------------------------------------- */
631 function typoCssVars( typo, prefix ) {
632 if ( ! typo ) return {};
633 var vars = {};
634
635 // --- Non-responsive ---
636 if ( typo.family ) {
637 vars[ prefix + 'font-family' ] = '\'' + typo.family + '\', sans-serif';
638 }
639 if ( typo.weight ) { vars[ prefix + 'font-weight' ] = typo.weight; }
640 if ( typo.transform ) { vars[ prefix + 'text-transform' ] = typo.transform; }
641 if ( typo.style ) { vars[ prefix + 'font-style' ] = typo.style; }
642 if ( typo.decoration ) { vars[ prefix + 'text-decoration'] = typo.decoration; }
643
644 // --- Responsive: size ---
645 var sizeUnit = typo.sizeUnit || 'px';
646 if ( typo.sizeDesktop !== '' && typo.sizeDesktop !== undefined && typo.sizeDesktop !== null ) {
647 vars[ prefix + 'font-size-d' ] = typo.sizeDesktop + sizeUnit;
648 }
649 if ( typo.sizeTablet !== '' && typo.sizeTablet !== undefined && typo.sizeTablet !== null ) {
650 vars[ prefix + 'font-size-t' ] = typo.sizeTablet + sizeUnit;
651 }
652 if ( typo.sizeMobile !== '' && typo.sizeMobile !== undefined && typo.sizeMobile !== null ) {
653 vars[ prefix + 'font-size-m' ] = typo.sizeMobile + sizeUnit;
654 }
655
656 // --- Responsive: line-height ---
657 var lhUnit = typo.lineHeightUnit || 'px';
658 if ( typo.lineHeightDesktop !== '' && typo.lineHeightDesktop !== undefined && typo.lineHeightDesktop !== null ) {
659 vars[ prefix + 'line-height-d' ] = typo.lineHeightDesktop + lhUnit;
660 }
661 if ( typo.lineHeightTablet !== '' && typo.lineHeightTablet !== undefined && typo.lineHeightTablet !== null ) {
662 vars[ prefix + 'line-height-t' ] = typo.lineHeightTablet + lhUnit;
663 }
664 if ( typo.lineHeightMobile !== '' && typo.lineHeightMobile !== undefined && typo.lineHeightMobile !== null ) {
665 vars[ prefix + 'line-height-m' ] = typo.lineHeightMobile + lhUnit;
666 }
667
668 // --- Responsive: letter-spacing ---
669 var lsUnit = typo.letterSpacingUnit || 'px';
670 if ( typo.letterSpacingDesktop !== '' && typo.letterSpacingDesktop !== undefined && typo.letterSpacingDesktop !== null ) {
671 vars[ prefix + 'letter-spacing-d' ] = typo.letterSpacingDesktop + lsUnit;
672 vars[ prefix + 'letter-spacing' ] = typo.letterSpacingDesktop + lsUnit; // base alias so CSS always picks it up
673 }
674 if ( typo.letterSpacingTablet !== '' && typo.letterSpacingTablet !== undefined && typo.letterSpacingTablet !== null ) {
675 vars[ prefix + 'letter-spacing-t' ] = typo.letterSpacingTablet + lsUnit;
676 }
677 if ( typo.letterSpacingMobile !== '' && typo.letterSpacingMobile !== undefined && typo.letterSpacingMobile !== null ) {
678 vars[ prefix + 'letter-spacing-m' ] = typo.letterSpacingMobile + lsUnit;
679 }
680
681 // --- Responsive: word-spacing ---
682 var wsUnit = typo.wordSpacingUnit || 'px';
683 if ( typo.wordSpacingDesktop !== '' && typo.wordSpacingDesktop !== undefined && typo.wordSpacingDesktop !== null ) {
684 vars[ prefix + 'word-spacing-d' ] = typo.wordSpacingDesktop + wsUnit;
685 vars[ prefix + 'word-spacing' ] = typo.wordSpacingDesktop + wsUnit; // base alias
686 }
687 if ( typo.wordSpacingTablet !== '' && typo.wordSpacingTablet !== undefined && typo.wordSpacingTablet !== null ) {
688 vars[ prefix + 'word-spacing-t' ] = typo.wordSpacingTablet + wsUnit;
689 }
690 if ( typo.wordSpacingMobile !== '' && typo.wordSpacingMobile !== undefined && typo.wordSpacingMobile !== null ) {
691 vars[ prefix + 'word-spacing-m' ] = typo.wordSpacingMobile + wsUnit;
692 }
693
694 return vars;
695 }
696
697 /* -------------------------------------------------------
698 Expose globally
699 ------------------------------------------------------- */
700 window.bkbgTypographyControl = TypographyControl;
701 window.bkbgTypoCssVars = typoCssVars;
702 window.bkbgLoadGoogleFont = loadGoogleFont;
703
704 /* -------------------------------------------------------
705 Auto-load fonts on editor init
706 Scans all blocks for *Typo attributes and loads their
707 Google Fonts so they appear in the canvas immediately,
708 not only after the inspector panel is opened.
709 ------------------------------------------------------- */
710 wp.domReady( function () {
711 if ( ! wp.data || ! wp.data.select ) { return; }
712
713 function loadFontsForBlocks( blockList ) {
714 if ( ! blockList || ! blockList.length ) { return; }
715 blockList.forEach( function ( block ) {
716 if ( block.attributes ) {
717 Object.keys( block.attributes ).forEach( function ( key ) {
718 var isTypoKey = ( key.length > 4 && key.slice( -4 ) === 'Typo' ) || ( key.indexOf( 'typo' ) === 0 );
719 if ( isTypoKey ) {
720 var typo = block.attributes[ key ];
721 if ( typo && typo.family ) {
722 loadGoogleFont( typo.family );
723 }
724 }
725 } );
726 }
727 // Recurse into inner blocks
728 if ( block.innerBlocks && block.innerBlocks.length ) {
729 loadFontsForBlocks( block.innerBlocks );
730 }
731 } );
732 }
733
734 function scanAllBlocks() {
735 var blocks = wp.data.select( 'core/block-editor' ).getBlocks();
736 loadFontsForBlocks( blocks );
737 }
738
739 // Run immediately (outer document fonts), then after delays
740 // to catch the editor iframe at different stages of readiness.
741 scanAllBlocks();
742 setTimeout( scanAllBlocks, 800 );
743 setTimeout( scanAllBlocks, 2500 );
744 } );
745
746 } )();
747