diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /devtools/client/inspector/fonts/components | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/inspector/fonts/components')
17 files changed, 1918 insertions, 0 deletions
diff --git a/devtools/client/inspector/fonts/components/Font.js b/devtools/client/inspector/fonts/components/Font.js new file mode 100644 index 0000000000..c761a3d2f1 --- /dev/null +++ b/devtools/client/inspector/fonts/components/Font.js @@ -0,0 +1,139 @@ +/* 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 FontName = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontName.js") +); +const FontOrigin = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontOrigin.js") +); +const FontPreview = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontPreview.js") +); + +const Types = require("resource://devtools/client/inspector/fonts/types.js"); + +class Font extends PureComponent { + static get propTypes() { + return { + font: PropTypes.shape(Types.font).isRequired, + onPreviewClick: PropTypes.func, + onToggleFontHighlight: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + + this.state = { + isFontFaceRuleExpanded: false, + }; + + this.onFontFaceRuleToggle = this.onFontFaceRuleToggle.bind(this); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(newProps) { + if (this.props.font.name === newProps.font.name) { + return; + } + + this.setState({ + isFontFaceRuleExpanded: false, + }); + } + + onFontFaceRuleToggle(event) { + this.setState({ + isFontFaceRuleExpanded: !this.state.isFontFaceRuleExpanded, + }); + event.stopPropagation(); + } + + renderFontCSSCode(rule, ruleText) { + if (!rule) { + return null; + } + + // Cut the rule text in 3 parts: the selector, the declarations, the closing brace. + // This way we can collapse the declarations by default and display an expander icon + // to expand them again. + const leading = ruleText.substring(0, ruleText.indexOf("{") + 1); + const body = ruleText.substring( + ruleText.indexOf("{") + 1, + ruleText.lastIndexOf("}") + ); + const trailing = ruleText.substring(ruleText.lastIndexOf("}")); + + const { isFontFaceRuleExpanded } = this.state; + + return dom.pre( + { + className: "font-css-code", + }, + this.renderFontCSSCodeTwisty(), + leading, + isFontFaceRuleExpanded + ? body + : dom.span({ + className: "font-css-code-expander", + onClick: this.onFontFaceRuleToggle, + }), + trailing + ); + } + + renderFontCSSCodeTwisty() { + const { isFontFaceRuleExpanded } = this.state; + + const attributes = { + className: "theme-twisty", + onClick: this.onFontFaceRuleToggle, + }; + if (isFontFaceRuleExpanded) { + attributes.open = "true"; + } + + return dom.span(attributes); + } + + renderFontFamilyName(family) { + if (!family) { + return null; + } + + return dom.div({ className: "font-family-name" }, family); + } + + render() { + const { font, onPreviewClick, onToggleFontHighlight } = this.props; + + const { CSSFamilyName, previewUrl, rule, ruleText } = font; + + return dom.li( + { + className: "font", + }, + dom.div( + {}, + this.renderFontFamilyName(CSSFamilyName), + FontName({ font, onToggleFontHighlight }) + ), + FontOrigin({ font }), + FontPreview({ onPreviewClick, previewUrl }), + this.renderFontCSSCode(rule, ruleText) + ); + } +} + +module.exports = Font; diff --git a/devtools/client/inspector/fonts/components/FontAxis.js b/devtools/client/inspector/fonts/components/FontAxis.js new file mode 100644 index 0000000000..f695c06b29 --- /dev/null +++ b/devtools/client/inspector/fonts/components/FontAxis.js @@ -0,0 +1,75 @@ +/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const FontPropertyValue = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontPropertyValue.js") +); + +const Types = require("resource://devtools/client/inspector/fonts/types.js"); + +class FontAxis extends PureComponent { + static get propTypes() { + return { + axis: PropTypes.shape(Types.fontVariationAxis), + disabled: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired, + value: PropTypes.number.isRequired, + }; + } + + /** + * Naive implementation to get increment step for variable font axis that ensures + * fine grained control based on range of values between min and max. + * + * @param {Number} min + * Minumum value for range. + * @param {Number} max + * Maximum value for range. + * @return {Number} + * Step value used in range input for font axis. + */ + getAxisStep(min, max) { + let step = 1; + const delta = parseInt(max, 10) - parseInt(min, 10); + + if (delta <= 1) { + step = 0.001; + } else if (delta <= 10) { + step = 0.01; + } else if (delta <= 100) { + step = 0.1; + } + + return step; + } + + render() { + const { axis, value, onChange } = this.props; + + return FontPropertyValue({ + className: "font-control-axis", + disabled: this.props.disabled, + label: axis.name, + min: axis.minValue, + minLabel: true, + max: axis.maxValue, + maxLabel: true, + name: axis.tag, + nameLabel: true, + onChange, + step: this.getAxisStep(axis.minValue, axis.maxValue), + value, + }); + } +} + +module.exports = FontAxis; diff --git a/devtools/client/inspector/fonts/components/FontEditor.js b/devtools/client/inspector/fonts/components/FontEditor.js new file mode 100644 index 0000000000..b98118c9fc --- /dev/null +++ b/devtools/client/inspector/fonts/components/FontEditor.js @@ -0,0 +1,357 @@ +/* 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; diff --git a/devtools/client/inspector/fonts/components/FontList.js b/devtools/client/inspector/fonts/components/FontList.js new file mode 100644 index 0000000000..3199ff583e --- /dev/null +++ b/devtools/client/inspector/fonts/components/FontList.js @@ -0,0 +1,82 @@ +/* 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 { + createElement, + createFactory, + createRef, + Fragment, + 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 Font = createFactory( + require("resource://devtools/client/inspector/fonts/components/Font.js") +); +const FontPreviewInput = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontPreviewInput.js") +); + +const Types = require("resource://devtools/client/inspector/fonts/types.js"); + +class FontList extends PureComponent { + static get propTypes() { + return { + fontOptions: PropTypes.shape(Types.fontOptions).isRequired, + fonts: PropTypes.arrayOf(PropTypes.shape(Types.font)).isRequired, + onPreviewTextChange: PropTypes.func.isRequired, + onToggleFontHighlight: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + + this.onPreviewClick = this.onPreviewClick.bind(this); + this.previewInputRef = createRef(); + } + + /** + * Handler for clicks on the font preview image. + * Requests the FontPreviewInput component, if one exists, to focus its input field. + */ + onPreviewClick() { + this.previewInputRef.current && this.previewInputRef.current.focus(); + } + + render() { + const { fonts, fontOptions, onPreviewTextChange, onToggleFontHighlight } = + this.props; + + const { previewText } = fontOptions; + const { onPreviewClick } = this; + + const list = dom.ul( + { + className: "fonts-list", + }, + fonts.map((font, i) => + Font({ + key: i, + font, + onPreviewClick, + onToggleFontHighlight, + }) + ) + ); + + const previewInput = FontPreviewInput({ + ref: this.previewInputRef, + onPreviewTextChange, + previewText, + }); + + return createElement(Fragment, null, previewInput, list); + } +} + +module.exports = FontList; diff --git a/devtools/client/inspector/fonts/components/FontName.js b/devtools/client/inspector/fonts/components/FontName.js new file mode 100644 index 0000000000..d68f91293c --- /dev/null +++ b/devtools/client/inspector/fonts/components/FontName.js @@ -0,0 +1,53 @@ +/* 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 { + 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 Types = require("resource://devtools/client/inspector/fonts/types.js"); + +class FontName extends PureComponent { + static get propTypes() { + return { + font: PropTypes.shape(Types.font).isRequired, + onToggleFontHighlight: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + this.onNameMouseOver = this.onNameMouseOver.bind(this); + this.onNameMouseOut = this.onNameMouseOut.bind(this); + } + + onNameMouseOver() { + const { font, onToggleFontHighlight } = this.props; + + onToggleFontHighlight(font, true); + } + + onNameMouseOut() { + const { font, onToggleFontHighlight } = this.props; + + onToggleFontHighlight(font, false); + } + + render() { + return dom.span( + { + className: "font-name", + onMouseOver: this.onNameMouseOver, + onMouseOut: this.onNameMouseOut, + }, + this.props.font.name + ); + } +} + +module.exports = FontName; diff --git a/devtools/client/inspector/fonts/components/FontOrigin.js b/devtools/client/inspector/fonts/components/FontOrigin.js new file mode 100644 index 0000000000..aa9a6cdebc --- /dev/null +++ b/devtools/client/inspector/fonts/components/FontOrigin.js @@ -0,0 +1,79 @@ +/* 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 { + 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"); + +loader.lazyRequireGetter( + this, + "clipboardHelper", + "resource://devtools/shared/platform/clipboard.js" +); + +const { + getStr, +} = require("resource://devtools/client/inspector/fonts/utils/l10n.js"); +const Types = require("resource://devtools/client/inspector/fonts/types.js"); + +class FontOrigin extends PureComponent { + static get propTypes() { + return { + font: PropTypes.shape(Types.font).isRequired, + }; + } + + constructor(props) { + super(props); + this.onCopyURL = this.onCopyURL.bind(this); + } + + clipTitle(title, maxLength = 512) { + if (title.length > maxLength) { + return title.substring(0, maxLength - 2) + "…"; + } + return title; + } + + onCopyURL() { + clipboardHelper.copyString(this.props.font.URI); + } + + render() { + const url = this.props.font.URI; + + if (!url) { + return dom.p( + { + className: "font-origin system", + }, + getStr("fontinspector.system") + ); + } + + return dom.p( + { + className: "font-origin remote", + }, + dom.span( + { + className: "url", + title: this.clipTitle(url), + }, + url + ), + dom.button({ + className: "copy-icon", + onClick: this.onCopyURL, + title: getStr("fontinspector.copyURL"), + }) + ); + } +} + +module.exports = FontOrigin; diff --git a/devtools/client/inspector/fonts/components/FontOverview.js b/devtools/client/inspector/fonts/components/FontOverview.js new file mode 100644 index 0000000000..82da06aaac --- /dev/null +++ b/devtools/client/inspector/fonts/components/FontOverview.js @@ -0,0 +1,80 @@ +/* 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 Accordion = createFactory( + require("resource://devtools/client/shared/components/Accordion.js") +); +const FontList = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontList.js") +); + +const { + getStr, +} = require("resource://devtools/client/inspector/fonts/utils/l10n.js"); +const Types = require("resource://devtools/client/inspector/fonts/types.js"); + +class FontOverview extends PureComponent { + static get propTypes() { + return { + fontData: PropTypes.shape(Types.fontData).isRequired, + fontOptions: PropTypes.shape(Types.fontOptions).isRequired, + onPreviewTextChange: PropTypes.func.isRequired, + onToggleFontHighlight: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + this.onToggleFontHighlightGlobal = (font, show) => { + this.props.onToggleFontHighlight(font, show, false); + }; + } + + renderFonts() { + const { fontData, fontOptions, onPreviewTextChange } = this.props; + + const fonts = fontData.allFonts; + + if (!fonts.length) { + return null; + } + + return Accordion({ + items: [ + { + header: getStr("fontinspector.allFontsOnPageHeader"), + id: "font-list-details", + component: FontList, + componentProps: { + fontOptions, + fonts, + onPreviewTextChange, + onToggleFontHighlight: this.onToggleFontHighlightGlobal, + }, + opened: false, + }, + ], + }); + } + + render() { + return dom.div( + { + id: "font-container", + }, + this.renderFonts() + ); + } +} + +module.exports = FontOverview; diff --git a/devtools/client/inspector/fonts/components/FontPreview.js b/devtools/client/inspector/fonts/components/FontPreview.js new file mode 100644 index 0000000000..dda18845f3 --- /dev/null +++ b/devtools/client/inspector/fonts/components/FontPreview.js @@ -0,0 +1,40 @@ +/* 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 { + 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 Types = require("resource://devtools/client/inspector/fonts/types.js"); + +class FontPreview extends PureComponent { + static get propTypes() { + return { + onPreviewClick: PropTypes.func, + previewUrl: Types.font.previewUrl.isRequired, + }; + } + + static get defaultProps() { + return { + onPreviewClick: () => {}, + }; + } + + render() { + const { onPreviewClick, previewUrl } = this.props; + + return dom.img({ + className: "font-preview", + onClick: onPreviewClick, + src: previewUrl, + }); + } +} + +module.exports = FontPreview; diff --git a/devtools/client/inspector/fonts/components/FontPreviewInput.js b/devtools/client/inspector/fonts/components/FontPreviewInput.js new file mode 100644 index 0000000000..80ae15f778 --- /dev/null +++ b/devtools/client/inspector/fonts/components/FontPreviewInput.js @@ -0,0 +1,77 @@ +/* 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 { + createRef, + 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 Types = require("resource://devtools/client/inspector/fonts/types.js"); +const { + getStr, +} = require("resource://devtools/client/inspector/fonts/utils/l10n.js"); + +const PREVIEW_TEXT_MAX_LENGTH = 30; + +class FontPreviewInput extends PureComponent { + static get propTypes() { + return { + onPreviewTextChange: PropTypes.func.isRequired, + previewText: Types.fontOptions.previewText.isRequired, + }; + } + + constructor(props) { + super(props); + + this.onChange = this.onChange.bind(this); + this.onFocus = this.onFocus.bind(this); + this.inputRef = createRef(); + + this.state = { + value: this.props.previewText, + }; + } + + onChange(e) { + const value = e.target.value; + this.props.onPreviewTextChange(value); + + this.setState(prevState => { + return { ...prevState, value }; + }); + } + + onFocus(e) { + e.target.select(); + } + + focus() { + this.inputRef.current.focus(); + } + + render() { + return dom.div( + { + id: "font-preview-input-container", + }, + dom.input({ + className: "devtools-searchinput", + onChange: this.onChange, + onFocus: this.onFocus, + maxLength: PREVIEW_TEXT_MAX_LENGTH, + placeholder: getStr("fontinspector.previewTextPlaceholder"), + ref: this.inputRef, + type: "text", + value: this.state.value, + }) + ); + } +} + +module.exports = FontPreviewInput; diff --git a/devtools/client/inspector/fonts/components/FontPropertyValue.js b/devtools/client/inspector/fonts/components/FontPropertyValue.js new file mode 100644 index 0000000000..59920818c4 --- /dev/null +++ b/devtools/client/inspector/fonts/components/FontPropertyValue.js @@ -0,0 +1,434 @@ +/* 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 { + createElement, + Fragment, + 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 { + toFixed, +} = require("resource://devtools/client/inspector/fonts/utils/font-utils.js"); + +class FontPropertyValue extends PureComponent { + static get propTypes() { + return { + // Whether to allow input values above the value defined by the `max` prop. + allowOverflow: PropTypes.bool, + // Whether to allow input values below the value defined by the `min` prop. + allowUnderflow: PropTypes.bool, + className: PropTypes.string, + defaultValue: PropTypes.number, + disabled: PropTypes.bool.isRequired, + label: PropTypes.string.isRequired, + min: PropTypes.number.isRequired, + // Whether to show the `min` prop value as a label. + minLabel: PropTypes.bool, + max: PropTypes.number.isRequired, + // Whether to show the `max` prop value as a label. + maxLabel: PropTypes.bool, + name: PropTypes.string.isRequired, + // Whether to show the `name` prop value as an extra label (used to show axis tags). + nameLabel: PropTypes.bool, + onChange: PropTypes.func.isRequired, + step: PropTypes.number, + // Whether to show the value input field. + showInput: PropTypes.bool, + // Whether to show the unit select dropdown. + showUnit: PropTypes.bool, + unit: PropTypes.string, + unitOptions: PropTypes.array, + value: PropTypes.number, + valueLabel: PropTypes.string, + }; + } + + static get defaultProps() { + return { + allowOverflow: false, + allowUnderflow: false, + className: "", + minLabel: false, + maxLabel: false, + nameLabel: false, + step: 1, + showInput: true, + showUnit: true, + unit: null, + unitOptions: [], + }; + } + + constructor(props) { + super(props); + this.state = { + // Whether the user is dragging the slider thumb or pressing on the numeric stepper. + interactive: false, + // Snapshot of the value from props before the user starts editing the number input. + // Used to restore the value when the input is left invalid. + initialValue: this.props.value, + // Snapshot of the value from props. Reconciled with props on blur. + // Used while the user is interacting with the inputs. + value: this.props.value, + }; + + this.onBlur = this.onBlur.bind(this); + this.onChange = this.onChange.bind(this); + this.onFocus = this.onFocus.bind(this); + this.onMouseDown = this.onMouseDown.bind(this); + this.onMouseUp = this.onMouseUp.bind(this); + this.onUnitChange = this.onUnitChange.bind(this); + } + + /** + * Given a `prop` key found on the component's props, check the matching `propLabel`. + * If `propLabel` is true, return the `prop` value; Otherwise, return null. + * + * @param {String} prop + * Key found on the component's props. + * @return {Number|null} + */ + getPropLabel(prop) { + const label = this.props[`${prop}Label`]; + // Decimal count used to limit numbers in labels. + const decimals = Math.abs(Math.log10(this.props.step)); + + return label ? toFixed(this.props[prop], decimals) : null; + } + + /** + * Check if the given value is valid according to the constraints of this component. + * Ensure it is a number and that it does not go outside the min/max limits, unless + * allowed by the `allowOverflow` and `allowUnderflow` props. + * + * @param {Number} value + * Numeric value + * @return {Boolean} + * Whether the value conforms to the components contraints. + */ + isValueValid(value) { + const { allowOverflow, allowUnderflow, min, max } = this.props; + + if (typeof value !== "number" || isNaN(value)) { + return false; + } + + // Ensure it does not go below minimum value, unless underflow is allowed. + if (min !== undefined && value < min && !allowUnderflow) { + return false; + } + + // Ensure it does not go over maximum value, unless overflow is allowed. + if (max !== undefined && value > max && !allowOverflow) { + return false; + } + + return true; + } + + /** + * Handler for "blur" events from the range and number input fields. + * Reconciles the value between internal state and props. + * Marks the input as non-interactive so it may update in response to changes in props. + */ + onBlur() { + const isValid = this.isValueValid(this.state.value); + let value; + + if (isValid) { + value = this.state.value; + } else if (this.state.value !== null) { + value = Math.max( + this.props.min, + Math.min(this.state.value, this.props.max) + ); + } else { + value = this.state.initialValue; + } + + // Avoid updating the value if a keyword value like "normal" is present + if (!this.props.valueLabel) { + this.updateValue(value); + } + + this.toggleInteractiveState(false); + } + + /** + * Handler for "change" events from the range and number input fields. Calls the change + * handler provided with props and updates internal state with the current value. + * + * Number inputs in Firefox can't be trusted to filter out non-digit characters, + * therefore we must implement our own validation. + * @see https://bugzilla.mozilla.org/show_bug.cgi?id=1398528 + * + * @param {Event} e + * Change event. + */ + onChange(e) { + // Regular expresion to check for floating point or integer numbers. Accept negative + // numbers only if the min value is negative. Otherwise, expect positive numbers. + // Whitespace and non-digit characters are invalid (aside from a single dot). + const regex = + this.props.min && this.props.min < 0 + ? /^-?[0-9]+(.[0-9]+)?$/ + : /^[0-9]+(.[0-9]+)?$/; + let string = e.target.value.trim(); + + if (e.target.validity.badInput) { + return; + } + + // Prefix with zero if the string starts with a dot: .5 => 0.5 + if (string.charAt(0) === "." && string.length > 1) { + string = "0" + string; + e.target.value = string; + } + + // Accept empty strings to allow the input value to be completely erased while typing. + // A null value will be handled on blur. @see this.onBlur() + if (string === "") { + this.setState(prevState => { + return { + ...prevState, + value: null, + }; + }); + + return; + } + + if (!regex.test(string)) { + return; + } + + const value = parseFloat(string); + this.updateValue(value); + } + + onFocus(e) { + if (e.target.type === "number") { + e.target.select(); + } + + this.setState(prevState => { + return { + ...prevState, + interactive: true, + initialValue: this.props.value, + }; + }); + } + + onUnitChange(e) { + this.props.onChange( + this.props.name, + this.props.value, + this.props.unit, + e.target.value + ); + // Reset internal state value and wait for converted value from props. + this.setState(prevState => { + return { + ...prevState, + value: null, + }; + }); + } + + onMouseDown() { + this.toggleInteractiveState(true); + } + + onMouseUp() { + this.toggleInteractiveState(false); + } + + /** + * Toggle the "interactive" state which causes render() to use `value` fom internal + * state instead of from props to prevent jittering during continous dragging of the + * range input thumb or incrementing from the number input. + * + * @param {Boolean} isInteractive + * Whether to mark the interactive state on or off. + */ + toggleInteractiveState(isInteractive) { + this.setState(prevState => { + return { + ...prevState, + interactive: isInteractive, + }; + }); + } + + /** + * Calls the given `onChange` callback with the current property name, value and unit + * if the value is valid according to the constraints of this component (min, max). + * Updates the internal state with the current value. This will be used to render the + * UI while the input is interactive and the user may be typing a value that's not yet + * valid. + * + * @see this.onBlur() for logic reconciling the internal state with props. + * + * @param {Number} value + * Numeric property value. + */ + updateValue(value) { + if (this.isValueValid(value)) { + this.props.onChange(this.props.name, value, this.props.unit); + } + + this.setState(prevState => { + return { + ...prevState, + value, + }; + }); + } + + renderUnitSelect() { + if (!this.props.unitOptions.length) { + return null; + } + + // Ensure the select element has the current unit type even if we don't recognize it. + // The unit conversion function will use a 1-to-1 scale for unrecognized units. + const options = this.props.unitOptions.includes(this.props.unit) + ? this.props.unitOptions + : this.props.unitOptions.concat([this.props.unit]); + + return dom.select( + { + className: "font-value-select", + disabled: this.props.disabled, + onChange: this.onUnitChange, + value: this.props.unit, + }, + options.map(unit => { + return dom.option( + { + key: unit, + value: unit, + }, + unit + ); + }) + ); + } + + renderLabelContent() { + const { label, name, nameLabel } = this.props; + + const labelEl = dom.span( + { + className: "font-control-label-text", + "aria-describedby": nameLabel ? `detail-${name}` : null, + }, + label + ); + + // Show the `name` prop value as an additional label if the `nameLabel` prop is true. + const detailEl = nameLabel + ? dom.span( + { + className: "font-control-label-detail", + id: `detail-${name}`, + }, + this.getPropLabel("name") + ) + : null; + + return createElement(Fragment, null, labelEl, detailEl); + } + + renderValueLabel() { + if (!this.props.valueLabel) { + return null; + } + + return dom.div({ className: "font-value-label" }, this.props.valueLabel); + } + + render() { + // Guard against bad axis data. + if (this.props.min === this.props.max) { + return null; + } + + const propsValue = + this.props.value !== null ? this.props.value : this.props.defaultValue; + + const defaults = { + min: this.props.min, + max: this.props.max, + onBlur: this.onBlur, + onChange: this.onChange, + onFocus: this.onFocus, + step: this.props.step, + // While interacting with the range and number inputs, prevent updating value from + // outside props which is debounced and causes jitter on successive renders. + value: this.state.interactive ? this.state.value : propsValue, + }; + + const range = dom.input({ + ...defaults, + onMouseDown: this.onMouseDown, + onMouseUp: this.onMouseUp, + className: "font-value-slider", + disabled: this.props.disabled, + name: this.props.name, + title: this.props.label, + type: "range", + }); + + const input = dom.input({ + ...defaults, + // Remove lower limit from number input if it is allowed to underflow. + min: this.props.allowUnderflow ? null : this.props.min, + // Remove upper limit from number input if it is allowed to overflow. + max: this.props.allowOverflow ? null : this.props.max, + name: this.props.name, + className: "font-value-input", + disabled: this.props.disabled, + type: "number", + }); + + return dom.label( + { + className: `font-control ${this.props.className}`, + disabled: this.props.disabled, + }, + dom.div( + { + className: "font-control-label", + title: this.props.label, + }, + this.renderLabelContent() + ), + dom.div( + { + className: "font-control-input", + }, + dom.div( + { + className: "font-value-slider-container", + "data-min": this.getPropLabel("min"), + "data-max": this.getPropLabel("max"), + }, + range + ), + this.renderValueLabel(), + this.props.showInput && input, + this.props.showUnit && this.renderUnitSelect() + ) + ); + } +} + +module.exports = FontPropertyValue; diff --git a/devtools/client/inspector/fonts/components/FontSize.js b/devtools/client/inspector/fonts/components/FontSize.js new file mode 100644 index 0000000000..dbb67a66d9 --- /dev/null +++ b/devtools/client/inspector/fonts/components/FontSize.js @@ -0,0 +1,87 @@ +/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const FontPropertyValue = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontPropertyValue.js") +); + +const { + getStr, +} = require("resource://devtools/client/inspector/fonts/utils/l10n.js"); +const { + getUnitFromValue, + getStepForUnit, +} = require("resource://devtools/client/inspector/fonts/utils/font-utils.js"); + +class FontSize extends PureComponent { + static get propTypes() { + return { + disabled: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired, + value: PropTypes.string.isRequired, + }; + } + + constructor(props) { + super(props); + this.historicMax = {}; + } + + render() { + const value = parseFloat(this.props.value); + const unit = getUnitFromValue(this.props.value); + let max; + switch (unit) { + case "em": + case "rem": + max = 4; + break; + case "vh": + case "vw": + case "vmin": + case "vmax": + max = 10; + break; + case "%": + max = 200; + break; + default: + max = 72; + break; + } + + // Allow the upper bound to increase so it accomodates the out-of-bounds value. + max = Math.max(max, value); + // Ensure we store the max value ever reached for this unit type. This will be the + // max value of the input and slider. Without this memoization, the value and slider + // thumb get clamped at the upper bound while decrementing an out-of-bounds value. + this.historicMax[unit] = this.historicMax[unit] + ? Math.max(this.historicMax[unit], max) + : max; + + return FontPropertyValue({ + allowOverflow: true, + disabled: this.props.disabled, + label: getStr("fontinspector.fontSizeLabel"), + min: 0, + max: this.historicMax[unit], + name: "font-size", + onChange: this.props.onChange, + step: getStepForUnit(unit), + unit, + unitOptions: ["em", "rem", "%", "px", "vh", "vw"], + value, + }); + } +} + +module.exports = FontSize; diff --git a/devtools/client/inspector/fonts/components/FontStyle.js b/devtools/client/inspector/fonts/components/FontStyle.js new file mode 100644 index 0000000000..ccb411958a --- /dev/null +++ b/devtools/client/inspector/fonts/components/FontStyle.js @@ -0,0 +1,69 @@ +/* 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 { + 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 { + getStr, +} = require("resource://devtools/client/inspector/fonts/utils/l10n.js"); + +class FontStyle extends PureComponent { + static get propTypes() { + return { + disabled: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired, + value: PropTypes.string.isRequired, + }; + } + + constructor(props) { + super(props); + this.name = "font-style"; + this.onToggle = this.onToggle.bind(this); + } + + onToggle(e) { + this.props.onChange( + this.name, + e.target.checked ? "italic" : "normal", + null + ); + } + + render() { + return dom.label( + { + className: "font-control", + }, + dom.span( + { + className: "font-control-label", + }, + getStr("fontinspector.fontItalicLabel") + ), + dom.div( + { + className: "font-control-input", + }, + dom.input({ + checked: + this.props.value === "italic" || this.props.value === "oblique", + className: "devtools-checkbox-toggle", + disabled: this.props.disabled, + name: this.name, + onChange: this.onToggle, + type: "checkbox", + }) + ) + ); + } +} + +module.exports = FontStyle; diff --git a/devtools/client/inspector/fonts/components/FontWeight.js b/devtools/client/inspector/fonts/components/FontWeight.js new file mode 100644 index 0000000000..2c86f2f318 --- /dev/null +++ b/devtools/client/inspector/fonts/components/FontWeight.js @@ -0,0 +1,45 @@ +/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const FontPropertyValue = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontPropertyValue.js") +); + +const { + getStr, +} = require("resource://devtools/client/inspector/fonts/utils/l10n.js"); + +class FontWeight extends PureComponent { + static get propTypes() { + return { + disabled: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired, + value: PropTypes.string.isRequired, + }; + } + + render() { + return FontPropertyValue({ + disabled: this.props.disabled, + label: getStr("fontinspector.fontWeightLabel"), + min: 100, + max: 900, + name: "font-weight", + onChange: this.props.onChange, + step: 100, + unit: null, + value: parseFloat(this.props.value), + }); + } +} + +module.exports = FontWeight; diff --git a/devtools/client/inspector/fonts/components/FontsApp.js b/devtools/client/inspector/fonts/components/FontsApp.js new file mode 100644 index 0000000000..66e820314a --- /dev/null +++ b/devtools/client/inspector/fonts/components/FontsApp.js @@ -0,0 +1,71 @@ +/* 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 { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); + +const FontEditor = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontEditor.js") +); +const FontOverview = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontOverview.js") +); + +const Types = require("resource://devtools/client/inspector/fonts/types.js"); + +class FontsApp extends PureComponent { + static get propTypes() { + return { + fontData: PropTypes.shape(Types.fontData).isRequired, + fontEditor: PropTypes.shape(Types.fontEditor).isRequired, + fontOptions: PropTypes.shape(Types.fontOptions).isRequired, + onInstanceChange: PropTypes.func.isRequired, + onPreviewTextChange: PropTypes.func.isRequired, + onPropertyChange: PropTypes.func.isRequired, + onToggleFontHighlight: PropTypes.func.isRequired, + }; + } + + render() { + const { + fontData, + fontEditor, + fontOptions, + onInstanceChange, + onPreviewTextChange, + onPropertyChange, + onToggleFontHighlight, + } = this.props; + + return dom.div( + { + className: "theme-sidebar inspector-tabpanel", + id: "sidebar-panel-fontinspector", + }, + FontEditor({ + fontEditor, + onInstanceChange, + onPropertyChange, + onToggleFontHighlight, + }), + FontOverview({ + fontData, + fontOptions, + onPreviewTextChange, + onToggleFontHighlight, + }) + ); + } +} + +module.exports = connect(state => state)(FontsApp); diff --git a/devtools/client/inspector/fonts/components/LetterSpacing.js b/devtools/client/inspector/fonts/components/LetterSpacing.js new file mode 100644 index 0000000000..42d6ddfa61 --- /dev/null +++ b/devtools/client/inspector/fonts/components/LetterSpacing.js @@ -0,0 +1,105 @@ +/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const FontPropertyValue = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontPropertyValue.js") +); + +const { + getStr, +} = require("resource://devtools/client/inspector/fonts/utils/l10n.js"); +const { + getUnitFromValue, + getStepForUnit, +} = require("resource://devtools/client/inspector/fonts/utils/font-utils.js"); + +class LetterSpacing extends PureComponent { + static get propTypes() { + return { + disabled: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired, + value: PropTypes.string.isRequired, + }; + } + + constructor(props) { + super(props); + // Local state for min/max bounds indexed by unit to allow user input that + // goes out-of-bounds while still providing a meaningful default range. The indexing + // by unit is needed to account for unit conversion (ex: em to px) where the operation + // may result in out-of-bounds values. Avoiding React's state and setState() because + // `value` is a prop coming from the Redux store while min/max are local. Reconciling + // value/unit changes is needlessly complicated and adds unnecessary re-renders. + this.historicMin = {}; + this.historicMax = {}; + } + + getDefaultMinMax(unit) { + let min; + let max; + switch (unit) { + case "px": + min = -10; + max = 10; + break; + default: + min = -0.2; + max = 0.6; + break; + } + + return { min, max }; + } + + render() { + // For a unitless or a NaN value, default unit to "em". + const unit = getUnitFromValue(this.props.value) || "em"; + // When the initial value of "letter-spacing" is "normal", the parsed value + // is not a number (NaN). Guard by setting the default value to 0. + const isKeywordValue = this.props.value === "normal"; + const value = isKeywordValue ? 0 : parseFloat(this.props.value); + + let { min, max } = this.getDefaultMinMax(unit); + min = Math.min(min, value); + max = Math.max(max, value); + // Allow lower and upper bounds to move to accomodate the incoming value. + this.historicMin[unit] = this.historicMin[unit] + ? Math.min(this.historicMin[unit], min) + : min; + this.historicMax[unit] = this.historicMax[unit] + ? Math.max(this.historicMax[unit], max) + : max; + + return FontPropertyValue({ + allowOverflow: true, + allowUnderflow: true, + disabled: this.props.disabled, + label: getStr("fontinspector.letterSpacingLabel"), + min: this.historicMin[unit], + max: this.historicMax[unit], + name: "letter-spacing", + onChange: this.props.onChange, + // Increase the increment granularity because letter spacing is very sensitive. + step: getStepForUnit(unit) / 100, + // Show the value input and unit only when the value is not a keyword. + showInput: !isKeywordValue, + showUnit: !isKeywordValue, + unit, + unitOptions: ["em", "rem", "px"], + value, + // Show the value as a read-only label if it's a keyword. + valueLabel: isKeywordValue ? this.props.value : null, + }); + } +} + +module.exports = LetterSpacing; diff --git a/devtools/client/inspector/fonts/components/LineHeight.js b/devtools/client/inspector/fonts/components/LineHeight.js new file mode 100644 index 0000000000..34949dc7e8 --- /dev/null +++ b/devtools/client/inspector/fonts/components/LineHeight.js @@ -0,0 +1,101 @@ +/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const FontPropertyValue = createFactory( + require("resource://devtools/client/inspector/fonts/components/FontPropertyValue.js") +); + +const { + getStr, +} = require("resource://devtools/client/inspector/fonts/utils/l10n.js"); +const { + getUnitFromValue, + getStepForUnit, +} = require("resource://devtools/client/inspector/fonts/utils/font-utils.js"); + +class LineHeight extends PureComponent { + static get propTypes() { + return { + disabled: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired, + value: PropTypes.string.isRequired, + }; + } + + constructor(props) { + super(props); + this.historicMax = {}; + } + + render() { + // When the initial value of "line-height" is "normal", the parsed value + // is not a number (NaN). Guard by setting the default value to 1.2. + // This will be the starting point for changing the value by dragging the slider. + // @see https://searchfox.org/mozilla-central/rev/1133b6716d9a8131c09754f3f29288484896b8b6/layout/generic/ReflowInput.cpp#2786 + const isKeywordValue = this.props.value === "normal"; + const value = isKeywordValue ? 1.2 : parseFloat(this.props.value); + + // When values for line-height are be unitless, getUnitFromValue() returns null. + // In that case, set the unit to an empty string for special treatment in conversion. + const unit = getUnitFromValue(this.props.value) || ""; + let max; + switch (unit) { + case "": + case "em": + case "rem": + max = 2; + break; + case "vh": + case "vw": + case "vmin": + case "vmax": + max = 15; + break; + case "%": + max = 200; + break; + default: + max = 108; + break; + } + + // Allow the upper bound to increase so it accomodates the out-of-bounds value. + max = Math.max(max, value); + // Ensure we store the max value ever reached for this unit type. This will be the + // max value of the input and slider. Without this memoization, the value and slider + // thumb get clamped at the upper bound while decrementing an out-of-bounds value. + this.historicMax[unit] = this.historicMax[unit] + ? Math.max(this.historicMax[unit], max) + : max; + + return FontPropertyValue({ + allowOverflow: true, + disabled: this.props.disabled, + label: getStr("fontinspector.lineHeightLabelCapitalized"), + min: 0, + max: this.historicMax[unit], + name: "line-height", + onChange: this.props.onChange, + step: getStepForUnit(unit), + // Show the value input and unit only when the value is not a keyword. + showInput: !isKeywordValue, + showUnit: !isKeywordValue, + unit, + unitOptions: ["", "em", "%", "px"], + value, + // Show the value as a read-only label if it's a keyword. + valueLabel: isKeywordValue ? this.props.value : null, + }); + } +} + +module.exports = LineHeight; diff --git a/devtools/client/inspector/fonts/components/moz.build b/devtools/client/inspector/fonts/components/moz.build new file mode 100644 index 0000000000..8838777f65 --- /dev/null +++ b/devtools/client/inspector/fonts/components/moz.build @@ -0,0 +1,24 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DevToolsModules( + "Font.js", + "FontAxis.js", + "FontEditor.js", + "FontList.js", + "FontName.js", + "FontOrigin.js", + "FontOverview.js", + "FontPreview.js", + "FontPreviewInput.js", + "FontPropertyValue.js", + "FontsApp.js", + "FontSize.js", + "FontStyle.js", + "FontWeight.js", + "LetterSpacing.js", + "LineHeight.js", +) |