summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/fonts/components
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/inspector/fonts/components/Font.js139
-rw-r--r--devtools/client/inspector/fonts/components/FontAxis.js75
-rw-r--r--devtools/client/inspector/fonts/components/FontEditor.js357
-rw-r--r--devtools/client/inspector/fonts/components/FontList.js82
-rw-r--r--devtools/client/inspector/fonts/components/FontName.js53
-rw-r--r--devtools/client/inspector/fonts/components/FontOrigin.js79
-rw-r--r--devtools/client/inspector/fonts/components/FontOverview.js80
-rw-r--r--devtools/client/inspector/fonts/components/FontPreview.js40
-rw-r--r--devtools/client/inspector/fonts/components/FontPreviewInput.js77
-rw-r--r--devtools/client/inspector/fonts/components/FontPropertyValue.js434
-rw-r--r--devtools/client/inspector/fonts/components/FontSize.js87
-rw-r--r--devtools/client/inspector/fonts/components/FontStyle.js69
-rw-r--r--devtools/client/inspector/fonts/components/FontWeight.js45
-rw-r--r--devtools/client/inspector/fonts/components/FontsApp.js71
-rw-r--r--devtools/client/inspector/fonts/components/LetterSpacing.js105
-rw-r--r--devtools/client/inspector/fonts/components/LineHeight.js101
-rw-r--r--devtools/client/inspector/fonts/components/moz.build24
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",
+)