PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.14
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.14
2.0.14 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.14, at assets/js/typography-control.js

759 lines 34.3 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 editor = wp.data && wp.data.select ? wp.data.select( 'core/editor' ) : null;
194 if ( editor && typeof editor.getDeviceType === 'function' ) {
195 return normalizePreviewDeviceName( editor.getDeviceType() );
196 }
197 var stores = [ 'core/edit-post', 'core/edit-site' ];
198 for ( var i = 0; i < stores.length; i++ ) {
199 var sel = wp.data && wp.data.select ? wp.data.select( stores[ i ] ) : null;
200 if ( ! sel ) continue;
201 if ( typeof sel.__experimentalGetPreviewDeviceType === 'function' ) {
202 return normalizePreviewDeviceName( sel.__experimentalGetPreviewDeviceType() );
203 }
204 if ( typeof sel.getDeviceType === 'function' ) {
205 return normalizePreviewDeviceName( sel.getDeviceType() );
206 }
207 }
208 return 'desktop';
209 }
210
211 function setEditorPreviewDevice( deviceName ) {
212 var mapped = deviceName === 'desktop' ? 'Desktop' : ( deviceName === 'tablet' ? 'Tablet' : 'Mobile' );
213 var editor = wp.data && wp.data.dispatch ? wp.data.dispatch( 'core/editor' ) : null;
214 if ( editor && typeof editor.setDeviceType === 'function' ) {
215 editor.setDeviceType( mapped );
216 return;
217 }
218 var stores = [ 'core/edit-post', 'core/edit-site' ];
219 stores.forEach( function ( storeName ) {
220 var dispatcher = wp.data && wp.data.dispatch ? wp.data.dispatch( storeName ) : null;
221 if ( ! dispatcher ) return;
222 if ( typeof dispatcher.__experimentalSetPreviewDeviceType === 'function' ) {
223 dispatcher.__experimentalSetPreviewDeviceType( mapped );
224 } else if ( typeof dispatcher.setDeviceType === 'function' ) {
225 dispatcher.setDeviceType( mapped );
226 }
227 } );
228 }
229
230 /* -------------------------------------------------------
231 Device Switcher (compact, inline for typo panel)
232 ------------------------------------------------------- */
233 function TypoDeviceSwitcher( props ) {
234 var device = props.device;
235 var onChange = props.onChange;
236
237 var _pop = useState( false );
238 var isOpen = _pop[ 0 ];
239 var setOpen = _pop[ 1 ];
240
241 return el( 'div', { className: 'bkbg-device-switcher' },
242 el( Button, {
243 className: 'bkbg-device-btn',
244 onClick: function () { setOpen( ! isOpen ); },
245 'aria-label': __( 'Switch device', 'blockenberg' ),
246 'aria-expanded': isOpen
247 }, iconDevice( device, false ) ),
248 isOpen && el( Popover, {
249 position: 'bottom center',
250 onClose: function () { setOpen( false ); },
251 noArrow: true,
252 focusOnMount: 'container',
253 className: 'bkbg-device-popover'
254 },
255 el( 'div', { className: 'bkbg-device-list' },
256 [ 'desktop', 'tablet', 'mobile' ].map( function ( d ) {
257 return el( Button, {
258 key: d,
259 className: 'bkbg-device-option' + ( d === device ? ' is-active' : '' ),
260 'aria-label': d === 'desktop' ? __( 'Desktop', 'blockenberg' ) : ( d === 'tablet' ? __( 'Tablet', 'blockenberg' ) : __( 'Mobile', 'blockenberg' ) ),
261 'aria-pressed': d === device,
262 onClick: function () {
263 onChange( d );
264 setEditorPreviewDevice( d );
265 setOpen( false );
266 }
267 }, iconDevice( d, d === device ) );
268 } )
269 )
270 )
271 );
272 }
273
274 /* -------------------------------------------------------
275 Responsive Slider+Input control
276 prefix : 'size' | 'lineHeight' | 'letterSpacing' | 'wordSpacing'
277 unitKey : 'sizeUnit' | 'lineHeightUnit' | etc.
278 ------------------------------------------------------- */
279 function ResponsiveSliderControl( props ) {
280 var label = props.label;
281 var prefix = props.prefix; // e.g. 'size'
282 var unitKey = props.unitKey; // e.g. 'sizeUnit'
283 var units = props.units || [ 'px', 'em', 'rem' ];
284 var minVal = props.min !== undefined ? props.min : 0;
285 var maxVal = props.max !== undefined ? props.max : 200;
286 var stepVal = props.step || 1;
287 var typo = props.typo;
288 var onChange = props.onChange;
289 var device = props.device;
290 var onDeviceChange = props.onDeviceChange;
291
292 // Build the responsive attribute key, e.g. 'sizeDesktop'
293 var suffix = device === 'desktop' ? 'Desktop' : ( device === 'tablet' ? 'Tablet' : 'Mobile' );
294 var actualKey = prefix + suffix; // e.g. 'sizeDesktop'
295 var value = typo[ actualKey ] !== undefined ? typo[ actualKey ] : '';
296 var unit = typo[ unitKey ] || units[ 0 ];
297
298 // Adjust max for relative units
299 var effectiveMax = maxVal;
300 if ( unit === 'em' || unit === 'rem' ) { effectiveMax = 20; }
301 if ( unit === 'vw' ) { effectiveMax = 100; }
302 if ( unit === 'lh' || unit === 'rlh' ) { effectiveMax = 10; }
303
304 var effectiveStep = ( unit === 'em' || unit === 'rem' || unit === 'lh' || unit === 'rlh' ) ? 0.1 : stepVal;
305
306 function updateValue( v ) {
307 var upd = {};
308 upd[ actualKey ] = v;
309 onChange( upd );
310 }
311
312 function updateUnit( u ) {
313 var upd = {};
314 upd[ unitKey ] = u;
315 onChange( upd );
316 }
317
318 return el( 'div', { className: 'bkbg-typo-field bkbg-typo-responsive-field' },
319 // Row: label + device switcher + unit selector
320 el( 'div', { className: 'bkbg-typo-field-header' },
321 el( 'span', { className: 'bkbg-typo-field-label' }, label ),
322 el( 'div', { className: 'bkbg-typo-field-header-right' },
323 el( TypoDeviceSwitcher, { device: device, onChange: onDeviceChange } ),
324 el( UnitSelector, { unit: unit, units: units, onChange: updateUnit } )
325 )
326 ),
327 // Row: slider + number input
328 el( 'div', { className: 'bkbg-typo-slider-row' },
329 el( 'input', {
330 type : 'range',
331 className : 'bkbg-typo-range',
332 min : minVal,
333 max : effectiveMax,
334 step : effectiveStep,
335 value : value !== '' ? parseFloat( value ) : minVal,
336 onChange : function ( e ) { updateValue( e.target.value ); }
337 } ),
338 el( 'input', {
339 type : 'number',
340 className : 'bkbg-typo-number',
341 value : value,
342 min : minVal,
343 max : effectiveMax,
344 step : effectiveStep,
345 placeholder: '',
346 onChange : function ( e ) { updateValue( e.target.value ); }
347 } )
348 )
349 );
350 }
351
352 /* -------------------------------------------------------
353 Main TypographyControl Component
354 ------------------------------------------------------- */
355 function TypographyControl( props ) {
356 var label = props.label;
357 var value = props.value || {};
358 var onChange = props.onChange;
359
360 // Merge stored value with defaults
361 var typo = {};
362 var k;
363 for ( k in DEFAULT_TYPO ) { typo[ k ] = DEFAULT_TYPO[ k ]; }
364 for ( k in value ) { if ( value[ k ] !== undefined && value[ k ] !== null ) typo[ k ] = value[ k ]; }
365
366 var _open = useState( false );
367 var isOpen = _open[ 0 ];
368 var setOpen = _open[ 1 ];
369
370 var _device = useState( getEditorPreviewDevice() );
371 var device = _device[ 0 ];
372 var setDevice = _device[ 1 ];
373
374 var anchorRef = useRef( null );
375
376 // Sync device with the editor's responsive preview toolbar (two-way)
377 useEffect( function () {
378 if ( ! wp.data || typeof wp.data.subscribe !== 'function' ) return;
379 var unsubscribe = wp.data.subscribe( function () {
380 var editorDevice = getEditorPreviewDevice();
381 setDevice( function ( current ) {
382 return current === editorDevice ? current : editorDevice;
383 } );
384 } );
385 return function () {
386 if ( typeof unsubscribe === 'function' ) unsubscribe();
387 };
388 }, [] );
389
390 // Load font in editor whenever family changes
391 useEffect( function () {
392 if ( typo.family ) { loadGoogleFont( typo.family ); }
393 }, [ typo.family ] );
394
395 function update( updates ) {
396 var next = {};
397 for ( k in typo ) { next[ k ] = typo[ k ]; }
398 for ( k in updates ) { next[ k ] = updates[ k ]; }
399 onChange( next );
400 }
401
402 function reset() { onChange( {} ); }
403
404 // Detect if any non-default value is set
405 function hasValue() {
406 for ( k in DEFAULT_TYPO ) {
407 var v = typo[ k ];
408 if ( v !== null && v !== undefined && v !== '' && v !== DEFAULT_TYPO[ k ] ) { return true; }
409 }
410 return false;
411 }
412
413 /* ----- Font family options ----- */
414 var fontOptions = [ { label: __( 'Default', 'blockenberg' ), value: '' } ];
415
416 // System fonts group
417 SYSTEM_FONTS.forEach( function ( f ) {
418 fontOptions.push( { label: f, value: f } );
419 } );
420
421 // Google Fonts (injected by PHP via wp_localize_script → window.bkbgGoogleFonts)
422 var gFonts = ( typeof window.bkbgGoogleFonts !== 'undefined' ) ? window.bkbgGoogleFonts : [];
423 gFonts.forEach( function ( f ) {
424 fontOptions.push( { label: f, value: f } );
425 } );
426
427 /* ----- Option lists ----- */
428 var weightOptions = [
429 { label: __( 'Default', 'blockenberg' ), value: '' },
430 { label: __( '100 (Thin)', 'blockenberg' ), value: '100' },
431 { label: __( '200 (Extra Light)', 'blockenberg' ), value: '200' },
432 { label: __( '300 (Light)', 'blockenberg' ), value: '300' },
433 { label: __( '400 (Normal)', 'blockenberg' ), value: '400' },
434 { label: __( '500 (Medium)', 'blockenberg' ), value: '500' },
435 { label: __( '600 (Semi Bold)', 'blockenberg' ), value: '600' },
436 { label: __( '700 (Bold)', 'blockenberg' ), value: '700' },
437 { label: __( '800 (Extra Bold)', 'blockenberg' ), value: '800' },
438 { label: __( '900 (Black)', 'blockenberg' ), value: '900' },
439 { label: __( 'Normal', 'blockenberg' ), value: 'normal' },
440 { label: __( 'Bold', 'blockenberg' ), value: 'bold' }
441 ];
442
443 var transformOptions = [
444 { label: __( 'Default', 'blockenberg' ), value: '' },
445 { label: __( 'Uppercase', 'blockenberg' ), value: 'uppercase' },
446 { label: __( 'Lowercase', 'blockenberg' ), value: 'lowercase' },
447 { label: __( 'Capitalize', 'blockenberg' ), value: 'capitalize' },
448 { label: __( 'Normal', 'blockenberg' ), value: 'none' }
449 ];
450
451 var styleOptions = [
452 { label: __( 'Default', 'blockenberg' ), value: '' },
453 { label: __( 'Normal', 'blockenberg' ), value: 'normal' },
454 { label: __( 'Italic', 'blockenberg' ), value: 'italic' },
455 { label: __( 'Oblique', 'blockenberg' ), value: 'oblique' }
456 ];
457
458 var decorationOptions = [
459 { label: __( 'Default', 'blockenberg' ), value: '' },
460 { label: __( 'Underline', 'blockenberg' ), value: 'underline' },
461 { label: __( 'Overline', 'blockenberg' ), value: 'overline' },
462 { label: __( 'Line Through', 'blockenberg' ), value: 'line-through' },
463 { label: __( 'None', 'blockenberg' ), value: 'none' }
464 ];
465
466 /* ----- Render ----- */
467 return el( 'div', { className: 'bkbg-typo-control' },
468
469 // Label row with pencil button
470 el( 'div', {
471 ref : anchorRef,
472 className : 'bkbg-typo-label-row'
473 },
474 el( 'span', { className: 'bkbg-typo-label' + ( hasValue() ? ' has-value' : '' ) }, label ),
475 el( Button, {
476 className : 'bkbg-typo-edit-btn' + ( hasValue() ? ' has-value' : '' ),
477 onClick : function () { setOpen( ! isOpen ); },
478 'aria-label': __( 'Edit Typography', 'blockenberg' )
479 }, iconPencil() )
480 ),
481
482 // Typography popover panel
483 isOpen && el( Popover, {
484 anchor : anchorRef.current,
485 placement : 'left-start',
486 offset : 12,
487 onClose : function () { setOpen( false ); },
488 noArrow : true,
489 focusOnMount : 'container',
490 className : 'bkbg-typo-popover'
491 },
492 el( 'div', { className: 'bkbg-typo-panel' },
493
494 // Panel header
495 el( 'div', { className: 'bkbg-typo-panel-header' },
496 el( 'span', { className: 'bkbg-typo-panel-title' }, __( 'Typography', 'blockenberg' ) ),
497 el( 'div', { className: 'bkbg-typo-panel-actions' },
498 el( Button, {
499 className : 'bkbg-typo-header-btn',
500 onClick : reset,
501 'aria-label': __( 'Reset typography', 'blockenberg' ),
502 isSmall : true
503 }, iconReset() ),
504 el( Button, {
505 className : 'bkbg-typo-header-btn',
506 onClick : function () { setOpen( false ); },
507 'aria-label': __( 'Close', 'blockenberg' ),
508 isSmall : true
509 }, iconClose() )
510 )
511 ),
512
513 // Family
514 el( 'div', { className: 'bkbg-typo-field' },
515 el( 'label', { className: 'bkbg-typo-field-label' }, __( 'Family', 'blockenberg' ) ),
516 el( SelectControl, {
517 value : typo.family,
518 options : fontOptions,
519 onChange : function ( v ) { update( { family: v } ); },
520 __nextHasNoMarginBottom: true
521 } )
522 ),
523
524 // Size (responsive)
525 el( ResponsiveSliderControl, {
526 label : __( 'Size', 'blockenberg' ),
527 prefix : 'size',
528 unitKey : 'sizeUnit',
529 units : [ 'px', 'em', 'rem', 'vw' ],
530 min : 0,
531 max : 200,
532 typo : typo,
533 onChange : update,
534 device : device,
535 onDeviceChange: setDevice
536 } ),
537
538 // Weight
539 el( 'div', { className: 'bkbg-typo-field' },
540 el( 'label', { className: 'bkbg-typo-field-label' }, __( 'Weight', 'blockenberg' ) ),
541 el( SelectControl, {
542 value : typo.weight,
543 options : weightOptions,
544 onChange : function ( v ) { update( { weight: v } ); },
545 __nextHasNoMarginBottom: true
546 } )
547 ),
548
549 // Transform
550 el( 'div', { className: 'bkbg-typo-field' },
551 el( 'label', { className: 'bkbg-typo-field-label' }, __( 'Transform', 'blockenberg' ) ),
552 el( SelectControl, {
553 value : typo.transform,
554 options : transformOptions,
555 onChange : function ( v ) { update( { transform: v } ); },
556 __nextHasNoMarginBottom: true
557 } )
558 ),
559
560 // Style
561 el( 'div', { className: 'bkbg-typo-field' },
562 el( 'label', { className: 'bkbg-typo-field-label' }, __( 'Style', 'blockenberg' ) ),
563 el( SelectControl, {
564 value : typo.style,
565 options : styleOptions,
566 onChange : function ( v ) { update( { style: v } ); },
567 __nextHasNoMarginBottom: true
568 } )
569 ),
570
571 // Decoration
572 el( 'div', { className: 'bkbg-typo-field' },
573 el( 'label', { className: 'bkbg-typo-field-label' }, __( 'Decoration', 'blockenberg' ) ),
574 el( SelectControl, {
575 value : typo.decoration,
576 options : decorationOptions,
577 onChange : function ( v ) { update( { decoration: v } ); },
578 __nextHasNoMarginBottom: true
579 } )
580 ),
581
582 // Line Height (responsive)
583 el( ResponsiveSliderControl, {
584 label : __( 'Line Height', 'blockenberg' ),
585 prefix : 'lineHeight',
586 unitKey : 'lineHeightUnit',
587 units : [ 'px', 'em', 'rem', 'lh', 'rlh' ],
588 min : 0,
589 max : 200,
590 typo : typo,
591 onChange : update,
592 device : device,
593 onDeviceChange: setDevice
594 } ),
595
596 // Letter Spacing (responsive)
597 el( ResponsiveSliderControl, {
598 label : __( 'Letter Spacing', 'blockenberg' ),
599 prefix : 'letterSpacing',
600 unitKey : 'letterSpacingUnit',
601 units : [ 'px', 'em', 'rem' ],
602 min : -10,
603 max : 50,
604 typo : typo,
605 onChange : update,
606 device : device,
607 onDeviceChange: setDevice
608 } ),
609
610 // Word Spacing (responsive)
611 el( ResponsiveSliderControl, {
612 label : __( 'Word Spacing', 'blockenberg' ),
613 prefix : 'wordSpacing',
614 unitKey : 'wordSpacingUnit',
615 units : [ 'px', 'em', 'rem' ],
616 min : -10,
617 max : 50,
618 typo : typo,
619 onChange : update,
620 device : device,
621 onDeviceChange: setDevice
622 } )
623
624 ) // end bkbg-typo-panel
625 ) // end Popover
626 ); // end bkbg-typo-control
627 }
628
629 /* -------------------------------------------------------
630 Utility: convert a typography object to CSS vars
631 prefix : CSS variable prefix, e.g. '--bkbg-ac-header-'
632
633 Responsive properties (size, line-height, letter-spacing,
634 word-spacing) are output as three device-specific vars:
635 prefix + 'font-size-d' (desktop)
636 prefix + 'font-size-t' (tablet)
637 prefix + 'font-size-m' (mobile)
638 CSS then picks the right one via @media + var() fallback.
639
640 Non-responsive properties (family, weight, transform,
641 style, decoration) are output as a single var.
642 ------------------------------------------------------- */
643 function typoCssVars( typo, prefix ) {
644 if ( ! typo ) return {};
645 var vars = {};
646
647 // --- Non-responsive ---
648 if ( typo.family ) {
649 vars[ prefix + 'font-family' ] = '\'' + typo.family + '\', sans-serif';
650 }
651 if ( typo.weight ) { vars[ prefix + 'font-weight' ] = typo.weight; }
652 if ( typo.transform ) { vars[ prefix + 'text-transform' ] = typo.transform; }
653 if ( typo.style ) { vars[ prefix + 'font-style' ] = typo.style; }
654 if ( typo.decoration ) { vars[ prefix + 'text-decoration'] = typo.decoration; }
655
656 // --- Responsive: size ---
657 var sizeUnit = typo.sizeUnit || 'px';
658 if ( typo.sizeDesktop !== '' && typo.sizeDesktop !== undefined && typo.sizeDesktop !== null ) {
659 vars[ prefix + 'font-size-d' ] = typo.sizeDesktop + sizeUnit;
660 }
661 if ( typo.sizeTablet !== '' && typo.sizeTablet !== undefined && typo.sizeTablet !== null ) {
662 vars[ prefix + 'font-size-t' ] = typo.sizeTablet + sizeUnit;
663 }
664 if ( typo.sizeMobile !== '' && typo.sizeMobile !== undefined && typo.sizeMobile !== null ) {
665 vars[ prefix + 'font-size-m' ] = typo.sizeMobile + sizeUnit;
666 }
667
668 // --- Responsive: line-height ---
669 var lhUnit = typo.lineHeightUnit || 'px';
670 if ( typo.lineHeightDesktop !== '' && typo.lineHeightDesktop !== undefined && typo.lineHeightDesktop !== null ) {
671 vars[ prefix + 'line-height-d' ] = typo.lineHeightDesktop + lhUnit;
672 }
673 if ( typo.lineHeightTablet !== '' && typo.lineHeightTablet !== undefined && typo.lineHeightTablet !== null ) {
674 vars[ prefix + 'line-height-t' ] = typo.lineHeightTablet + lhUnit;
675 }
676 if ( typo.lineHeightMobile !== '' && typo.lineHeightMobile !== undefined && typo.lineHeightMobile !== null ) {
677 vars[ prefix + 'line-height-m' ] = typo.lineHeightMobile + lhUnit;
678 }
679
680 // --- Responsive: letter-spacing ---
681 var lsUnit = typo.letterSpacingUnit || 'px';
682 if ( typo.letterSpacingDesktop !== '' && typo.letterSpacingDesktop !== undefined && typo.letterSpacingDesktop !== null ) {
683 vars[ prefix + 'letter-spacing-d' ] = typo.letterSpacingDesktop + lsUnit;
684 vars[ prefix + 'letter-spacing' ] = typo.letterSpacingDesktop + lsUnit; // base alias so CSS always picks it up
685 }
686 if ( typo.letterSpacingTablet !== '' && typo.letterSpacingTablet !== undefined && typo.letterSpacingTablet !== null ) {
687 vars[ prefix + 'letter-spacing-t' ] = typo.letterSpacingTablet + lsUnit;
688 }
689 if ( typo.letterSpacingMobile !== '' && typo.letterSpacingMobile !== undefined && typo.letterSpacingMobile !== null ) {
690 vars[ prefix + 'letter-spacing-m' ] = typo.letterSpacingMobile + lsUnit;
691 }
692
693 // --- Responsive: word-spacing ---
694 var wsUnit = typo.wordSpacingUnit || 'px';
695 if ( typo.wordSpacingDesktop !== '' && typo.wordSpacingDesktop !== undefined && typo.wordSpacingDesktop !== null ) {
696 vars[ prefix + 'word-spacing-d' ] = typo.wordSpacingDesktop + wsUnit;
697 vars[ prefix + 'word-spacing' ] = typo.wordSpacingDesktop + wsUnit; // base alias
698 }
699 if ( typo.wordSpacingTablet !== '' && typo.wordSpacingTablet !== undefined && typo.wordSpacingTablet !== null ) {
700 vars[ prefix + 'word-spacing-t' ] = typo.wordSpacingTablet + wsUnit;
701 }
702 if ( typo.wordSpacingMobile !== '' && typo.wordSpacingMobile !== undefined && typo.wordSpacingMobile !== null ) {
703 vars[ prefix + 'word-spacing-m' ] = typo.wordSpacingMobile + wsUnit;
704 }
705
706 return vars;
707 }
708
709 /* -------------------------------------------------------
710 Expose globally
711 ------------------------------------------------------- */
712 window.bkbgTypographyControl = TypographyControl;
713 window.bkbgTypoCssVars = typoCssVars;
714 window.bkbgLoadGoogleFont = loadGoogleFont;
715
716 /* -------------------------------------------------------
717 Auto-load fonts on editor init
718 Scans all blocks for *Typo attributes and loads their
719 Google Fonts so they appear in the canvas immediately,
720 not only after the inspector panel is opened.
721 ------------------------------------------------------- */
722 wp.domReady( function () {
723 if ( ! wp.data || ! wp.data.select ) { return; }
724
725 function loadFontsForBlocks( blockList ) {
726 if ( ! blockList || ! blockList.length ) { return; }
727 blockList.forEach( function ( block ) {
728 if ( block.attributes ) {
729 Object.keys( block.attributes ).forEach( function ( key ) {
730 var isTypoKey = ( key.length > 4 && key.slice( -4 ) === 'Typo' ) || ( key.indexOf( 'typo' ) === 0 );
731 if ( isTypoKey ) {
732 var typo = block.attributes[ key ];
733 if ( typo && typo.family ) {
734 loadGoogleFont( typo.family );
735 }
736 }
737 } );
738 }
739 // Recurse into inner blocks
740 if ( block.innerBlocks && block.innerBlocks.length ) {
741 loadFontsForBlocks( block.innerBlocks );
742 }
743 } );
744 }
745
746 function scanAllBlocks() {
747 var blocks = wp.data.select( 'core/block-editor' ).getBlocks();
748 loadFontsForBlocks( blocks );
749 }
750
751 // Run immediately (outer document fonts), then after delays
752 // to catch the editor iframe at different stages of readiness.
753 scanAllBlocks();
754 setTimeout( scanAllBlocks, 800 );
755 setTimeout( scanAllBlocks, 2500 );
756 } );
757
758 } )();
759