/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { createFactory, PureComponent, } = require("resource://devtools/client/shared/vendor/react.js"); const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); const FontAxis = createFactory( require("resource://devtools/client/inspector/fonts/components/FontAxis.js") ); const FontName = createFactory( require("resource://devtools/client/inspector/fonts/components/FontName.js") ); const FontSize = createFactory( require("resource://devtools/client/inspector/fonts/components/FontSize.js") ); const FontStyle = createFactory( require("resource://devtools/client/inspector/fonts/components/FontStyle.js") ); const FontWeight = createFactory( require("resource://devtools/client/inspector/fonts/components/FontWeight.js") ); const LetterSpacing = createFactory( require("resource://devtools/client/inspector/fonts/components/LetterSpacing.js") ); const LineHeight = createFactory( require("resource://devtools/client/inspector/fonts/components/LineHeight.js") ); const { getStr, } = require("resource://devtools/client/inspector/fonts/utils/l10n.js"); const Types = require("resource://devtools/client/inspector/fonts/types.js"); // Maximum number of font families to be shown by default. Any others will be hidden // under a collapsed <details> element with a toggle to reveal them. const MAX_FONTS = 3; class FontEditor extends PureComponent { static get propTypes() { return { fontEditor: PropTypes.shape(Types.fontEditor).isRequired, onInstanceChange: PropTypes.func.isRequired, onPropertyChange: PropTypes.func.isRequired, onToggleFontHighlight: PropTypes.func.isRequired, }; } /** * Get an array of FontAxis components with editing controls for of the given variable * font axes. If no axes were given, return null. * If an axis' value was declared on the font-variation-settings CSS property or was * changed using the font editor, use that value, otherwise use the axis default. * * @param {Array} fontAxes * Array of font axis instances * @param {Object} editedAxes * Object with axes and values edited by the user or defined in the CSS * declaration for font-variation-settings. * @return {Array|null} */ renderAxes(fontAxes = [], editedAxes) { if (!fontAxes.length) { return null; } return fontAxes.map(axis => { return FontAxis({ key: axis.tag, axis, disabled: this.props.fontEditor.disabled, onChange: this.props.onPropertyChange, minLabel: true, maxLabel: true, value: editedAxes[axis.tag] || axis.defaultValue, }); }); } /** * Render fonts used on the selected node grouped by font-family. * * @param {Array} fonts * Fonts used on selected node. * @return {DOMNode} */ renderUsedFonts(fonts) { if (!fonts.length) { return null; } // Group fonts by family name. const fontGroups = fonts.reduce((acc, font) => { const family = font.CSSFamilyName.toString(); acc[family] = acc[family] || []; acc[family].push(font); return acc; }, {}); const renderedFontGroups = Object.keys(fontGroups).map(family => { return this.renderFontGroup(family, fontGroups[family]); }); const topFontsList = renderedFontGroups.slice(0, MAX_FONTS); const moreFontsList = renderedFontGroups.slice( MAX_FONTS, renderedFontGroups.length ); const moreFonts = !moreFontsList.length ? null : dom.details( {}, dom.summary( {}, dom.span( { className: "label-open" }, getStr("fontinspector.showMore") ), dom.span( { className: "label-close" }, getStr("fontinspector.showLess") ) ), moreFontsList ); return dom.label( { className: "font-control font-control-used-fonts", }, dom.span( { className: "font-control-label", }, getStr("fontinspector.fontsUsedLabel") ), dom.div( { className: "font-control-box", }, topFontsList, moreFonts ) ); } renderFontGroup(family, fonts = []) { const group = fonts.map(font => { return FontName({ key: font.name, font, onToggleFontHighlight: this.props.onToggleFontHighlight, }); }); return dom.div( { key: family, className: "font-group", }, dom.div( { className: "font-family-name", }, family ), group ); } renderFontSize(value) { return ( value !== null && FontSize({ key: `${this.props.fontEditor.id}:font-size`, disabled: this.props.fontEditor.disabled, onChange: this.props.onPropertyChange, value, }) ); } renderLineHeight(value) { return ( value !== null && LineHeight({ key: `${this.props.fontEditor.id}:line-height`, disabled: this.props.fontEditor.disabled, onChange: this.props.onPropertyChange, value, }) ); } renderLetterSpacing(value) { return ( value !== null && LetterSpacing({ key: `${this.props.fontEditor.id}:letter-spacing`, disabled: this.props.fontEditor.disabled, onChange: this.props.onPropertyChange, value, }) ); } renderFontStyle(value) { return ( value && FontStyle({ onChange: this.props.onPropertyChange, disabled: this.props.fontEditor.disabled, value, }) ); } renderFontWeight(value) { return ( value !== null && FontWeight({ onChange: this.props.onPropertyChange, disabled: this.props.fontEditor.disabled, value, }) ); } /** * Get a dropdown which allows selecting between variation instances defined by a font. * * @param {Array} fontInstances * Named variation instances as provided with the font file. * @param {Object} selectedInstance * Object with information about the currently selected variation instance. * Example: * { * name: "Custom", * values: [] * } * @return {DOMNode} */ renderInstances(fontInstances = [], selectedInstance = {}) { // Append a "Custom" instance entry which represents the latest manual axes changes. const customInstance = { name: getStr("fontinspector.customInstanceName"), values: this.props.fontEditor.customInstanceValues, }; fontInstances = [...fontInstances, customInstance]; // Generate the <option> elements for the dropdown. const instanceOptions = fontInstances.map(instance => dom.option( { key: instance.name, value: instance.name, }, instance.name ) ); // Generate the dropdown. const instanceSelect = dom.select( { className: "font-control-input font-value-select", value: selectedInstance.name || customInstance.name, onChange: e => { const instance = fontInstances.find( inst => e.target.value === inst.name ); instance && this.props.onInstanceChange(instance.name, instance.values); }, }, instanceOptions ); return dom.label( { className: "font-control", }, dom.span( { className: "font-control-label", }, getStr("fontinspector.fontInstanceLabel") ), instanceSelect ); } renderWarning(warning) { return dom.div( { id: "font-editor", }, dom.div( { className: "devtools-sidepanel-no-result", }, warning ) ); } render() { const { fontEditor } = this.props; const { fonts, axes, instance, properties, warning } = fontEditor; // Pick the first font to show editor controls regardless of how many fonts are used. const font = fonts[0]; const hasFontAxes = font?.variationAxes; const hasFontInstances = font?.variationInstances?.length > 0; const hasSlantOrItalicAxis = font?.variationAxes?.find(axis => { return axis.tag === "slnt" || axis.tag === "ital"; }); const hasWeightAxis = font?.variationAxes?.find(axis => { return axis.tag === "wght"; }); // Show the empty state with a warning message when a used font was not found. if (!font) { return this.renderWarning(warning); } return dom.div( { id: "font-editor", }, // Always render UI for used fonts. this.renderUsedFonts(fonts), // Render UI for font variation instances if they are defined. hasFontInstances && this.renderInstances(font.variationInstances, instance), // Always render UI for font size. this.renderFontSize(properties["font-size"]), // Always render UI for line height. this.renderLineHeight(properties["line-height"]), // Always render UI for letter spacing. this.renderLetterSpacing(properties["letter-spacing"]), // Render UI for font weight if no "wght" registered axis is defined. !hasWeightAxis && this.renderFontWeight(properties["font-weight"]), // Render UI for font style if no "slnt" or "ital" registered axis is defined. !hasSlantOrItalicAxis && this.renderFontStyle(properties["font-style"]), // Render UI for each variable font axis if any are defined. hasFontAxes && this.renderAxes(font.variationAxes, axes) ); } } module.exports = FontEditor;