| 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 |
|