FontEditor.js (10158B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 createFactory, 9 PureComponent, 10 } = require("resource://devtools/client/shared/vendor/react.mjs"); 11 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 12 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 13 14 const FontAxis = createFactory( 15 require("resource://devtools/client/inspector/fonts/components/FontAxis.js") 16 ); 17 const FontName = createFactory( 18 require("resource://devtools/client/inspector/fonts/components/FontName.js") 19 ); 20 const FontSize = createFactory( 21 require("resource://devtools/client/inspector/fonts/components/FontSize.js") 22 ); 23 const FontStyle = createFactory( 24 require("resource://devtools/client/inspector/fonts/components/FontStyle.js") 25 ); 26 const FontWeight = createFactory( 27 require("resource://devtools/client/inspector/fonts/components/FontWeight.js") 28 ); 29 const LetterSpacing = createFactory( 30 require("resource://devtools/client/inspector/fonts/components/LetterSpacing.js") 31 ); 32 const LineHeight = createFactory( 33 require("resource://devtools/client/inspector/fonts/components/LineHeight.js") 34 ); 35 36 const { 37 getStr, 38 } = require("resource://devtools/client/inspector/fonts/utils/l10n.js"); 39 const Types = require("resource://devtools/client/inspector/fonts/types.js"); 40 41 // Maximum number of font families to be shown by default. Any others will be hidden 42 // under a collapsed <details> element with a toggle to reveal them. 43 const MAX_FONTS = 3; 44 45 class FontEditor extends PureComponent { 46 static get propTypes() { 47 return { 48 fontEditor: PropTypes.shape(Types.fontEditor).isRequired, 49 onInstanceChange: PropTypes.func.isRequired, 50 onPropertyChange: PropTypes.func.isRequired, 51 onToggleFontHighlight: PropTypes.func.isRequired, 52 }; 53 } 54 55 /** 56 * Get an array of FontAxis components with editing controls for of the given variable 57 * font axes. If no axes were given, return null. 58 * If an axis' value was declared on the font-variation-settings CSS property or was 59 * changed using the font editor, use that value, otherwise use the axis default. 60 * 61 * @param {Array} fontAxes 62 * Array of font axis instances 63 * @param {object} editedAxes 64 * Object with axes and values edited by the user or defined in the CSS 65 * declaration for font-variation-settings. 66 * @return {Array|null} 67 */ 68 renderAxes(fontAxes = [], editedAxes) { 69 if (!fontAxes.length) { 70 return null; 71 } 72 73 return fontAxes.map(axis => { 74 return FontAxis({ 75 key: axis.tag, 76 axis, 77 disabled: this.props.fontEditor.disabled, 78 onChange: this.props.onPropertyChange, 79 minLabel: true, 80 maxLabel: true, 81 value: editedAxes[axis.tag] || axis.defaultValue, 82 }); 83 }); 84 } 85 86 /** 87 * Render fonts used on the selected node grouped by font-family. 88 * 89 * @param {Array} fonts 90 * Fonts used on selected node. 91 * @return {DOMNode} 92 */ 93 renderUsedFonts(fonts) { 94 if (!fonts.length) { 95 return null; 96 } 97 98 // Group fonts by family name. 99 const fontGroups = fonts.reduce((acc, font) => { 100 const family = font.CSSFamilyName.toString(); 101 acc[family] = acc[family] || []; 102 acc[family].push(font); 103 return acc; 104 }, {}); 105 106 const renderedFontGroups = Object.keys(fontGroups).map(family => { 107 return this.renderFontGroup(family, fontGroups[family]); 108 }); 109 110 const topFontsList = renderedFontGroups.slice(0, MAX_FONTS); 111 const moreFontsList = renderedFontGroups.slice( 112 MAX_FONTS, 113 renderedFontGroups.length 114 ); 115 116 const moreFonts = !moreFontsList.length 117 ? null 118 : dom.details( 119 {}, 120 dom.summary( 121 {}, 122 dom.span( 123 { className: "label-open" }, 124 getStr("fontinspector.showMore") 125 ), 126 dom.span( 127 { className: "label-close" }, 128 getStr("fontinspector.showLess") 129 ) 130 ), 131 moreFontsList 132 ); 133 134 return dom.label( 135 { 136 className: "font-control font-control-used-fonts", 137 }, 138 dom.span( 139 { 140 className: "font-control-label", 141 }, 142 getStr("fontinspector.fontsUsedLabel") 143 ), 144 dom.div( 145 { 146 className: "font-control-box", 147 }, 148 topFontsList, 149 moreFonts 150 ) 151 ); 152 } 153 154 renderFontGroup(family, fonts = []) { 155 const group = fonts.map((font, i) => { 156 return FontName({ 157 key: font.name + ":" + i, 158 font, 159 onToggleFontHighlight: this.props.onToggleFontHighlight, 160 }); 161 }); 162 163 return dom.div( 164 { 165 key: family, 166 className: "font-group", 167 }, 168 dom.div( 169 { 170 className: "font-family-name", 171 }, 172 family 173 ), 174 group 175 ); 176 } 177 178 renderFontSize(value) { 179 return ( 180 value !== null && 181 FontSize({ 182 key: `${this.props.fontEditor.id}:font-size`, 183 disabled: this.props.fontEditor.disabled, 184 onChange: this.props.onPropertyChange, 185 value, 186 }) 187 ); 188 } 189 190 renderLineHeight(value) { 191 return ( 192 value !== null && 193 LineHeight({ 194 key: `${this.props.fontEditor.id}:line-height`, 195 disabled: this.props.fontEditor.disabled, 196 onChange: this.props.onPropertyChange, 197 value, 198 }) 199 ); 200 } 201 202 renderLetterSpacing(value) { 203 return ( 204 value !== null && 205 LetterSpacing({ 206 key: `${this.props.fontEditor.id}:letter-spacing`, 207 disabled: this.props.fontEditor.disabled, 208 onChange: this.props.onPropertyChange, 209 value, 210 }) 211 ); 212 } 213 214 renderFontStyle(value) { 215 return ( 216 value && 217 FontStyle({ 218 onChange: this.props.onPropertyChange, 219 disabled: this.props.fontEditor.disabled, 220 value, 221 }) 222 ); 223 } 224 225 renderFontWeight(value) { 226 return ( 227 value !== null && 228 FontWeight({ 229 onChange: this.props.onPropertyChange, 230 disabled: this.props.fontEditor.disabled, 231 value, 232 }) 233 ); 234 } 235 236 /** 237 * Get a dropdown which allows selecting between variation instances defined by a font. 238 * 239 * @param {Array} fontInstances 240 * Named variation instances as provided with the font file. 241 * @param {object} selectedInstance 242 * Object with information about the currently selected variation instance. 243 * Example: 244 * { 245 * name: "Custom", 246 * values: [] 247 * } 248 * @return {DOMNode} 249 */ 250 renderInstances(fontInstances = [], selectedInstance = {}) { 251 // Append a "Custom" instance entry which represents the latest manual axes changes. 252 const customInstance = { 253 name: getStr("fontinspector.customInstanceName"), 254 values: this.props.fontEditor.customInstanceValues, 255 }; 256 fontInstances = [...fontInstances, customInstance]; 257 258 // Generate the <option> elements for the dropdown. 259 const instanceOptions = fontInstances.map(instance => 260 dom.option( 261 { 262 key: instance.name, 263 value: instance.name, 264 }, 265 instance.name 266 ) 267 ); 268 269 // Generate the dropdown. 270 const instanceSelect = dom.select( 271 { 272 className: "font-control-input font-value-select", 273 value: selectedInstance.name || customInstance.name, 274 onChange: e => { 275 const instance = fontInstances.find( 276 inst => e.target.value === inst.name 277 ); 278 instance && 279 this.props.onInstanceChange(instance.name, instance.values); 280 }, 281 }, 282 instanceOptions 283 ); 284 285 return dom.label( 286 { 287 className: "font-control", 288 }, 289 dom.span( 290 { 291 className: "font-control-label", 292 }, 293 getStr("fontinspector.fontInstanceLabel") 294 ), 295 instanceSelect 296 ); 297 } 298 299 renderWarning(warning) { 300 return dom.div( 301 { 302 id: "font-editor", 303 }, 304 dom.div( 305 { 306 className: "devtools-sidepanel-no-result", 307 }, 308 warning 309 ) 310 ); 311 } 312 313 render() { 314 const { fontEditor } = this.props; 315 const { fonts, axes, instance, properties, warning } = fontEditor; 316 // Pick the first font to show editor controls regardless of how many fonts are used. 317 const font = fonts[0]; 318 const hasFontAxes = font?.variationAxes; 319 const hasFontInstances = font?.variationInstances?.length > 0; 320 const hasSlantOrItalicAxis = font?.variationAxes?.find(axis => { 321 return axis.tag === "slnt" || axis.tag === "ital"; 322 }); 323 const hasWeightAxis = font?.variationAxes?.find(axis => { 324 return axis.tag === "wght"; 325 }); 326 327 // Show the empty state with a warning message when a used font was not found. 328 if (!font) { 329 return this.renderWarning(warning); 330 } 331 332 return dom.div( 333 { 334 id: "font-editor", 335 }, 336 // Always render UI for used fonts. 337 this.renderUsedFonts(fonts), 338 // Render UI for font variation instances if they are defined. 339 hasFontInstances && 340 this.renderInstances(font.variationInstances, instance), 341 // Always render UI for font size. 342 this.renderFontSize(properties["font-size"]), 343 // Always render UI for line height. 344 this.renderLineHeight(properties["line-height"]), 345 // Always render UI for letter spacing. 346 this.renderLetterSpacing(properties["letter-spacing"]), 347 // Render UI for font weight if no "wght" registered axis is defined. 348 !hasWeightAxis && this.renderFontWeight(properties["font-weight"]), 349 // Render UI for font style if no "slnt" or "ital" registered axis is defined. 350 !hasSlantOrItalicAxis && this.renderFontStyle(properties["font-style"]), 351 // Render UI for each variable font axis if any are defined. 352 hasFontAxes && this.renderAxes(font.variationAxes, axes) 353 ); 354 } 355 } 356 357 module.exports = FontEditor;