Bug 1363097 - Convert font inspector to React/Redux. r=gl
authorMike Park <mikeparkms@gmail.com>
Wed, 10 May 2017 14:57:05 -0400
changeset 368810 010ba1c14577521798596bcdfca470f0a7d894f6
parent 368809 129793760f2d718ed61ba17d9aacd57c7ec6785b
child 368811 18a31ab2a1f000eb566db44a03eb40da71bcf004
push id92549
push usergabriel.luong@gmail.com
push dateFri, 14 Jul 2017 03:07:51 +0000
treeherdermozilla-inbound@010ba1c14577 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgl
bugs1363097
milestone56.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1363097 - Convert font inspector to React/Redux. r=gl MozReview-Commit-ID: Du5ZqnnLaE1
devtools/client/inspector/fonts/actions/font-options.js
devtools/client/inspector/fonts/actions/fonts.js
devtools/client/inspector/fonts/actions/index.js
devtools/client/inspector/fonts/actions/moz.build
devtools/client/inspector/fonts/components/App.js
devtools/client/inspector/fonts/components/Font.js
devtools/client/inspector/fonts/components/FontList.js
devtools/client/inspector/fonts/components/moz.build
devtools/client/inspector/fonts/fonts.js
devtools/client/inspector/fonts/moz.build
devtools/client/inspector/fonts/reducers/font-options.js
devtools/client/inspector/fonts/reducers/fonts.js
devtools/client/inspector/fonts/reducers/moz.build
devtools/client/inspector/fonts/test/browser_fontinspector.js
devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews-show-all.js
devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews.js
devtools/client/inspector/fonts/test/browser_fontinspector_theme-change.js
devtools/client/inspector/fonts/test/head.js
devtools/client/inspector/fonts/types.js
devtools/client/inspector/fonts/utils/l10n.js
devtools/client/inspector/fonts/utils/moz.build
devtools/client/inspector/inspector.js
devtools/client/inspector/inspector.xhtml
devtools/client/inspector/reducers.js
devtools/client/shared/browser-loader.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/actions/font-options.js
@@ -0,0 +1,34 @@
+/* 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 {
+  UPDATE_PREVIEW_TEXT,
+  UPDATE_SHOW_ALL_FONTS,
+} = require("./index");
+
+module.exports = {
+
+  /**
+   * Update the preview text in the font inspector
+   */
+  updatePreviewText(previewText) {
+    return {
+      type: UPDATE_PREVIEW_TEXT,
+      previewText,
+    };
+  },
+
+  /**
+   * Update whether to show all fonts in the font inspector
+   */
+  updateShowAllFonts(showAllFonts) {
+    return {
+      type: UPDATE_SHOW_ALL_FONTS,
+      showAllFonts,
+    };
+  },
+
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/actions/fonts.js
@@ -0,0 +1,23 @@
+/* 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 {
+  UPDATE_FONTS,
+} = require("./index");
+
+module.exports = {
+
+  /**
+   * Update the list of fonts in the font inspector
+   */
+  updateFonts(fonts) {
+    return {
+      type: UPDATE_FONTS,
+      fonts,
+    };
+  },
+
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/actions/index.js
@@ -0,0 +1,20 @@
+/* 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 { createEnum } = require("devtools/client/shared/enum");
+
+createEnum([
+
+  // Update the list of fonts.
+  "UPDATE_FONTS",
+
+  // Update the preview text.
+  "UPDATE_PREVIEW_TEXT",
+
+  // Update whether to show all fonts.
+  "UPDATE_SHOW_ALL_FONTS",
+
+], module.exports);
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/actions/moz.build
@@ -0,0 +1,11 @@
+# -*- 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-options.js',
+    'fonts.js',
+    'index.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/components/App.js
@@ -0,0 +1,84 @@
+/* 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 { addons, createClass, createFactory, DOM: dom, PropTypes } =
+  require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
+
+const SearchBox = createFactory(require("devtools/client/shared/components/search-box"));
+const FontList = createFactory(require("./FontList"));
+
+const { getStr } = require("../utils/l10n");
+const Types = require("../types");
+
+const PREVIEW_UPDATE_DELAY = 150;
+
+const App = createClass({
+
+  displayName: "App",
+
+  propTypes: {
+    fonts: PropTypes.arrayOf(PropTypes.shape(Types.font)).isRequired,
+    onPreviewFonts: PropTypes.func.isRequired,
+    onShowAllFont: PropTypes.func.isRequired,
+    onTextBoxContextMenu: PropTypes.func.isRequired,
+  },
+
+  mixins: [ addons.PureRenderMixin ],
+
+  componentDidMount() {
+    let { onTextBoxContextMenu } = this.props;
+
+    let searchInput = findDOMNode(this).querySelector(".devtools-textinput");
+    searchInput.addEventListener("contextmenu", onTextBoxContextMenu);
+  },
+
+  componentWillUnmount() {
+    let { onTextBoxContextMenu } = this.props;
+
+    let searchInput = findDOMNode(this).querySelector(".devtools-textinput");
+    searchInput.removeEventListener("contextmenu", onTextBoxContextMenu);
+  },
+
+  render() {
+    let {
+      fonts,
+      onPreviewFonts,
+      onShowAllFont,
+    } = this.props;
+
+    return dom.div(
+      {
+        className: "devtools-monospace theme-sidebar inspector-tabpanel",
+        id: "sidebar-panel-fontinspector"
+      },
+      dom.div(
+        {
+          className: "devtools-toolbar"
+        },
+        SearchBox({
+          delay: PREVIEW_UPDATE_DELAY,
+          placeholder: getStr("fontinspector.previewText"),
+          type: "text",
+          onChange: onPreviewFonts,
+        }),
+        dom.label(
+          {
+            id: "font-showall",
+            className: "theme-link",
+            title: getStr("fontinspector.seeAll.tooltip"),
+            onClick: onShowAllFont,
+          },
+          getStr("fontinspector.seeAll")
+        )
+      ),
+      FontList({ fonts })
+    );
+  }
+});
+
+module.exports = connect(state => state)(App);
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/components/Font.js
@@ -0,0 +1,156 @@
+/* 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 { addons, createClass, DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react");
+
+const { getStr } = require("../utils/l10n");
+const Types = require("../types");
+
+module.exports = createClass({
+
+  displayName: "Font",
+
+  propTypes: PropTypes.shape(Types.font).isRequired,
+
+  mixins: [ addons.PureRenderMixin ],
+
+  getSectionClasses() {
+    let { font } = this.props;
+
+    let classes = ["font"];
+    classes.push((font.URI) ? "is-remote" : "is-local");
+
+    if (font.rule) {
+      classes.push("has-code");
+    }
+
+    return classes.join(" ");
+  },
+
+  renderFontCSS(cssFamilyName) {
+    return dom.p(
+      {
+        className: "font-css"
+      },
+      dom.span(
+        {},
+        getStr("fontinspector.usedAs")
+      ),
+      " \"",
+      dom.span(
+        {
+          className: "font-css-name"
+        },
+        cssFamilyName
+      ),
+      "\""
+    );
+  },
+
+  renderFontCSSCode(rule, ruleText) {
+    return dom.pre(
+      {
+        className: "font-css-code"
+      },
+      rule ? ruleText : null
+    );
+  },
+
+  renderFontFormatURL(url, format) {
+    return dom.p(
+      {
+        className: "font-format-url"
+      },
+      dom.input(
+        {
+          className: "font-url",
+          readOnly: "readonly",
+          value: url
+        }
+      ),
+      " ",
+      format ?
+        dom.span(
+          {
+            className: "font-format"
+          },
+          format
+        )
+        :
+        dom.span(
+          {
+            className: "font-format",
+            hidden: "true"
+          },
+          format
+        )
+    );
+  },
+
+  renderFontName(name) {
+    return dom.h1(
+      {
+        className: "font-name",
+      },
+      name
+    );
+  },
+
+  renderFontPreview(previewUrl) {
+    return dom.div(
+      {
+        className: "font-preview-container",
+      },
+      dom.img(
+        {
+          className: "font-preview",
+          src: previewUrl
+        }
+      )
+    );
+  },
+
+  render() {
+    let { font } = this.props;
+    let {
+      CSSFamilyName,
+      format,
+      name,
+      previewUrl,
+      rule,
+      ruleText,
+      URI,
+    } = font;
+
+    return dom.section(
+      {
+        className: this.getSectionClasses(),
+      },
+      this.renderFontPreview(previewUrl),
+      dom.div(
+        {
+          className: "font-info",
+        },
+        this.renderFontName(name),
+        dom.span(
+          {
+            className: "font-is-local",
+          },
+          " " + getStr("fontinspector.system")
+        ),
+        dom.span(
+          {
+            className: "font-is-remote",
+          },
+          " " + getStr("fontinspector.remote")
+        ),
+        this.renderFontFormatURL(URI, format),
+        this.renderFontCSS(CSSFamilyName),
+        this.renderFontCSSCode(rule, ruleText)
+      )
+    );
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/components/FontList.js
@@ -0,0 +1,43 @@
+/* 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 { addons, createClass, createFactory, DOM: dom, PropTypes } =
+  require("devtools/client/shared/vendor/react");
+
+const Font = createFactory(require("./Font"));
+
+const Types = require("../types");
+
+module.exports = createClass({
+
+  displayName: "FontList",
+
+  propTypes: {
+    fonts: PropTypes.arrayOf(PropTypes.shape(Types.font)).isRequired
+  },
+
+  mixins: [ addons.PureRenderMixin ],
+
+  render() {
+    let { fonts } = this.props;
+
+    return dom.div(
+      {
+        id: "font-container"
+      },
+      dom.ul(
+        {
+          id: "all-fonts"
+        },
+        fonts.map((font, i) => Font({
+          key: i,
+          font
+        }))
+      )
+    );
+  },
+
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/components/moz.build
@@ -0,0 +1,11 @@
+# -*- 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(
+    'App.js',
+    'Font.js',
+    'FontList.js',
+)
--- a/devtools/client/inspector/fonts/fonts.js
+++ b/devtools/client/inspector/fonts/fonts.js
@@ -1,250 +1,187 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=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 {gDevTools} = require("devtools/client/framework/devtools");
+const Services = require("Services");
+const { Task } = require("devtools/shared/task");
+const { getColor } = require("devtools/client/shared/theme");
+
+const { createFactory, createElement } = require("devtools/client/shared/vendor/react");
+const { Provider } = require("devtools/client/shared/vendor/react-redux");
+
+const { gDevTools } = require("devtools/client/framework/devtools");
 
-const DEFAULT_PREVIEW_TEXT = "Abc";
-const PREVIEW_UPDATE_DELAY = 150;
+const App = createFactory(require("./components/App"));
 
-const {Task} = require("devtools/shared/task");
-const {getColor} = require("devtools/client/shared/theme");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const INSPECTOR_L10N =
+  new LocalizationHelper("devtools/client/locales/inspector.properties");
+
+const { updateFonts } = require("./actions/fonts");
+const { updatePreviewText, updateShowAllFonts } = require("./actions/font-options");
 
 function FontInspector(inspector, window) {
+  this.document = window.document;
   this.inspector = inspector;
   this.pageStyle = this.inspector.pageStyle;
-  this.chromeDoc = window.document;
-  this.init();
+  this.store = inspector.store;
+
+  this.update = this.update.bind(this);
+
+  this.onNewNode = this.onNewNode.bind(this);
+  this.onPreviewFonts = this.onPreviewFonts.bind(this);
+  this.onShowAllFont = this.onShowAllFont.bind(this);
+  this.onThemeChanged = this.onThemeChanged.bind(this);
 }
 
 FontInspector.prototype = {
-  init: function () {
-    this.update = this.update.bind(this);
-    this.onNewNode = this.onNewNode.bind(this);
-    this.onThemeChanged = this.onThemeChanged.bind(this);
+  init() {
+    if (!this.inspector) {
+      return;
+    }
+
+    let app = App({
+      onPreviewFonts: this.onPreviewFonts,
+      onShowAllFont: this.onShowAllFont,
+      onTextBoxContextMenu: this.inspector.onTextBoxContextMenu
+    });
+
+    let provider = createElement(Provider, {
+      store: this.store,
+      id: "fontinspector",
+      title: INSPECTOR_L10N.getStr("inspector.sidebar.fontInspectorTitle"),
+      key: "fontinspector",
+    }, app);
+
+    let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar");
+
+    this.inspector.addSidebarTab(
+      "fontinspector",
+      INSPECTOR_L10N.getStr("inspector.sidebar.fontInspectorTitle"),
+      provider,
+      defaultTab == "fontinspector"
+    );
+
     this.inspector.selection.on("new-node-front", this.onNewNode);
     this.inspector.sidebar.on("fontinspector-selected", this.onNewNode);
-    this.showAll = this.showAll.bind(this);
-    this.showAllLink = this.chromeDoc.getElementById("font-showall");
-    this.showAllLink.addEventListener("click", this.showAll);
-    this.previewTextChanged = this.previewTextChanged.bind(this);
-    this.previewInput = this.chromeDoc.getElementById("font-preview-text-input");
-    this.previewInput.addEventListener("input", this.previewTextChanged);
-    this.previewInput.addEventListener("contextmenu",
-      this.inspector.onTextBoxContextMenu);
 
     // Listen for theme changes as the color of the previews depend on the theme
     gDevTools.on("theme-switched", this.onThemeChanged);
 
-    this.update();
-  },
-
-  /**
-   * Is the fontinspector visible in the sidebar?
-   */
-  isActive: function () {
-    return this.inspector.sidebar &&
-           this.inspector.sidebar.getCurrentTabID() == "fontinspector";
+    this.store.dispatch(updatePreviewText(""));
+    this.store.dispatch(updateShowAllFonts(false));
+    this.update(false, "");
   },
 
   /**
-   * Remove listeners.
+   * Destruction function called when the inspector is destroyed. Removes event listeners
+   * and cleans up references.
    */
   destroy: function () {
-    this.chromeDoc = null;
+    this.inspector.selection.off("new-node-front", this.onNewNode);
     this.inspector.sidebar.off("fontinspector-selected", this.onNewNode);
-    this.inspector.selection.off("new-node-front", this.onNewNode);
-    this.showAllLink.removeEventListener("click", this.showAll);
-    this.previewInput.removeEventListener("input", this.previewTextChanged);
-    this.previewInput.removeEventListener("contextmenu",
-      this.inspector.onTextBoxContextMenu);
-
     gDevTools.off("theme-switched", this.onThemeChanged);
 
-    if (this._previewUpdateTimeout) {
-      clearTimeout(this._previewUpdateTimeout);
-    }
+    this.document = null;
+    this.inspector = null;
+    this.pageStyle = null;
+    this.store = null;
+  },
+
+  /**
+   * Returns true if the font inspector panel is visible, and false otherwise.
+   */
+  isPanelVisible() {
+    return this.inspector.sidebar &&
+           this.inspector.sidebar.getCurrentTabID() === "fontinspector";
   },
 
   /**
    * Selection 'new-node' event handler.
    */
-  onNewNode: function () {
-    if (this.isActive() &&
-        this.inspector.selection.isConnected() &&
-        this.inspector.selection.isElementNode()) {
-      this.undim();
+  onNewNode() {
+    if (this.isPanelVisible()) {
+      this.store.dispatch(updateShowAllFonts(false));
       this.update();
-    } else {
-      this.dim();
+    }
+  },
+
+  /**
+   * Handler for the "theme-switched" event.
+   */
+  onThemeChanged(event, frame) {
+    if (frame === this.document.defaultView) {
+      this.update();
     }
   },
 
   /**
-   * The text to use for previews. Returns either the value user has typed to
-   * the preview input or DEFAULT_PREVIEW_TEXT if the input is empty or contains
-   * only whitespace.
-   */
-  getPreviewText: function () {
-    let inputText = this.previewInput.value.trim();
-    if (inputText === "") {
-      return DEFAULT_PREVIEW_TEXT;
-    }
-
-    return inputText;
-  },
-
-  /**
-   * Preview input 'input' event handler.
+   * Handler for change in preview input.
    */
-  previewTextChanged: function () {
-    if (this._previewUpdateTimeout) {
-      clearTimeout(this._previewUpdateTimeout);
-    }
-
-    this._previewUpdateTimeout = setTimeout(() => {
-      this.update(this._lastUpdateShowedAllFonts);
-    }, PREVIEW_UPDATE_DELAY);
-  },
-
-  /**
-   * Callback for the theme-switched event.
-   */
-  onThemeChanged: function (event, frame) {
-    if (frame === this.chromeDoc.defaultView) {
-      this.update(this._lastUpdateShowedAllFonts);
-    }
+  onPreviewFonts(value) {
+    this.store.dispatch(updatePreviewText(value));
+    this.update();
   },
 
   /**
-   * Hide the font list. No node are selected.
+   * Handler for click on show all fonts button.
    */
-  dim: function () {
-    let panel = this.chromeDoc.getElementById("sidebar-panel-fontinspector");
-    panel.classList.add("dim");
-    this.clear();
-  },
-
-  /**
-   * Show the font list. A node is selected.
-   */
-  undim: function () {
-    let panel = this.chromeDoc.getElementById("sidebar-panel-fontinspector");
-    panel.classList.remove("dim");
+  onShowAllFont() {
+    this.store.dispatch(updateShowAllFonts(true));
+    this.update();
   },
 
-  /**
-   * Clears the font list.
-   */
-  clear: function () {
-    this.chromeDoc.querySelector("#all-fonts").innerHTML = "";
-  },
-
- /**
-  * Retrieve all the font info for the selected node and display it.
-  */
-  update: Task.async(function* (showAllFonts) {
+  update: Task.async(function* () {
     let node = this.inspector.selection.nodeFront;
-    let panel = this.chromeDoc.getElementById("sidebar-panel-fontinspector");
 
     if (!node ||
-        !this.isActive() ||
+        !this.isPanelVisible() ||
         !this.inspector.selection.isConnected() ||
-        !this.inspector.selection.isElementNode() ||
-        panel.classList.contains("dim")) {
+        !this.inspector.selection.isElementNode()) {
       return;
     }
 
-    this._lastUpdateShowedAllFonts = showAllFonts;
+    let { fontOptions } = this.store.getState();
+    let { showAllFonts, previewText } = fontOptions;
 
     let options = {
       includePreviews: true,
-      previewText: this.getPreviewText(),
+      previewText,
       previewFillStyle: getColor("body-color")
     };
 
     let fonts = [];
     if (showAllFonts) {
       fonts = yield this.pageStyle.getAllUsedFontFaces(options)
                       .catch(console.error);
     } else {
       fonts = yield this.pageStyle.getUsedFontFaces(node, options)
                       .catch(console.error);
     }
 
     if (!fonts || !fonts.length) {
       // No fonts to display. Clear the previously shown fonts.
-      this.clear();
+      this.store.dispatch(updateFonts(fonts));
       return;
     }
 
     for (let font of fonts) {
       font.previewUrl = yield font.preview.data.string();
     }
 
     // in case we've been destroyed in the meantime
-    if (!this.chromeDoc) {
+    if (!this.document) {
       return;
     }
 
-    // Make room for the new fonts.
-    this.clear();
-
-    for (let font of fonts) {
-      this.render(font);
-    }
+    this.store.dispatch(updateFonts(fonts));
 
     this.inspector.emit("fontinspector-updated");
-  }),
-
-  /**
-   * Display the information of one font.
-   */
-  render: function (font) {
-    let s = this.chromeDoc.querySelector("#font-template > section");
-    s = s.cloneNode(true);
-
-    s.querySelector(".font-name").textContent = font.name;
-    s.querySelector(".font-css-name").textContent = font.CSSFamilyName;
-
-    if (font.URI) {
-      s.classList.add("is-remote");
-    } else {
-      s.classList.add("is-local");
-    }
-
-    let formatElem = s.querySelector(".font-format");
-    if (font.format) {
-      formatElem.textContent = font.format;
-    } else {
-      formatElem.hidden = true;
-    }
-
-    s.querySelector(".font-url").value = font.URI;
-
-    if (font.rule) {
-      // This is the @font-face{…} code.
-      let cssText = font.ruleText;
-
-      s.classList.add("has-code");
-      s.querySelector(".font-css-code").textContent = cssText;
-    }
-    let preview = s.querySelector(".font-preview");
-    preview.src = font.previewUrl;
-
-    this.chromeDoc.querySelector("#all-fonts").appendChild(s);
-  },
-
-  /**
-   * Show all fonts for the document (including iframes)
-   */
-  showAll: function () {
-    this.update(true);
-  },
+  })
 };
 
-exports.FontInspector = FontInspector;
+module.exports = FontInspector;
--- a/devtools/client/inspector/fonts/moz.build
+++ b/devtools/client/inspector/fonts/moz.build
@@ -1,11 +1,19 @@
 # -*- 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/.
 
+DIRS += [
+    'actions',
+    'components',
+    'reducers',
+    'utils',
+]
+
 DevToolsModules(
     'fonts.js',
+    'types.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/reducers/font-options.js
@@ -0,0 +1,35 @@
+/* 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 {
+  UPDATE_PREVIEW_TEXT,
+  UPDATE_SHOW_ALL_FONTS
+} = require("../actions/index");
+
+const INITIAL_FONT_OPTIONS = {
+  previewText: "Abc",
+  showAllFonts: false,
+};
+
+let reducers = {
+
+  [UPDATE_PREVIEW_TEXT](fontOptions, { previewText }) {
+    return Object.assign({}, fontOptions, { previewText });
+  },
+
+  [UPDATE_SHOW_ALL_FONTS](fontOptions, { showAllFonts }) {
+    return Object.assign({}, fontOptions, { showAllFonts });
+  },
+
+};
+
+module.exports = function (fontOptions = INITIAL_FONT_OPTIONS, action) {
+  let reducer = reducers[action.type];
+  if (!reducer) {
+    return fontOptions;
+  }
+  return reducer(fontOptions, action);
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/reducers/fonts.js
@@ -0,0 +1,27 @@
+/* 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 {
+  UPDATE_FONTS,
+} = require("../actions/index");
+
+const INITIAL_FONTS = [];
+
+let reducers = {
+
+  [UPDATE_FONTS](_, { fonts }) {
+    return fonts;
+  },
+
+};
+
+module.exports = function (fonts = INITIAL_FONTS, action) {
+  let reducer = reducers[action.type];
+  if (!reducer) {
+    return fonts;
+  }
+  return reducer(fonts, action);
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/reducers/moz.build
@@ -0,0 +1,10 @@
+# -*- 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-options.js',
+    'fonts.js',
+)
--- a/devtools/client/inspector/fonts/test/browser_fontinspector.js
+++ b/devtools/client/inspector/fonts/test/browser_fontinspector.js
@@ -31,17 +31,17 @@ const FONTS = [{
   format: "",
   cssName: "barnormal"
 }];
 
 add_task(function* () {
   let { inspector, view } = yield openFontInspectorForURL(TEST_URI);
   ok(!!view, "Font inspector document is alive.");
 
-  let viewDoc = view.chromeDoc;
+  let viewDoc = view.document;
 
   yield testBodyFonts(inspector, viewDoc);
   yield testDivFonts(inspector, viewDoc);
   yield testShowAllFonts(inspector, viewDoc);
 });
 
 function* testBodyFonts(inspector, viewDoc) {
   let s = viewDoc.querySelectorAll("#all-fonts > section");
--- a/devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews-show-all.js
+++ b/devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews-show-all.js
@@ -5,17 +5,17 @@
 
 // Test that correct previews are shown if the text is edited after 'Show all'
 // button is pressed.
 
 const TEST_URI = URL_ROOT + "browser_fontinspector.html";
 
 add_task(function* () {
   let { inspector, view } = yield openFontInspectorForURL(TEST_URI);
-  let viewDoc = view.chromeDoc;
+  let viewDoc = view.document;
 
   info("Selecting a node that doesn't contain all document fonts.");
   yield selectNode(".normal-text", inspector);
 
   let normalTextNumPreviews =
     viewDoc.querySelectorAll("#all-fonts .font-preview").length;
 
   let onUpdated = inspector.once("fontinspector-updated");
--- a/devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews.js
+++ b/devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews.js
@@ -6,17 +6,17 @@
 // Test that previews change when the preview text changes. It doesn't check the
 // exact preview images because they are drawn on a canvas causing them to vary
 // between systems, platforms and software versions.
 
 const TEST_URI = URL_ROOT + "browser_fontinspector.html";
 
 add_task(function* () {
   let {view} = yield openFontInspectorForURL(TEST_URI);
-  let viewDoc = view.chromeDoc;
+  let viewDoc = view.document;
 
   let previews = viewDoc.querySelectorAll("#all-fonts .font-preview");
   let initialPreviews = [...previews].map(p => p.src);
 
   info("Typing 'Abc' to check that the reference previews are correct.");
   yield updatePreviewText(view, "Abc");
   checkPreviewImages(viewDoc, initialPreviews, true);
 
--- a/devtools/client/inspector/fonts/test/browser_fontinspector_theme-change.js
+++ b/devtools/client/inspector/fonts/test/browser_fontinspector_theme-change.js
@@ -14,17 +14,17 @@ const originalTheme = getTheme();
 
 registerCleanupFunction(() => {
   info(`Restoring theme to '${originalTheme}.`);
   setTheme(originalTheme);
 });
 
 add_task(function* () {
   let { inspector, view } = yield openFontInspectorForURL(TEST_URI);
-  let { chromeDoc: doc } = view;
+  let { document: doc } = view;
 
   yield selectNode(".normal-text", inspector);
 
   // Store the original preview URI for later comparison.
   let originalURI = doc.querySelector("#all-fonts .font-preview").src;
   let newTheme = originalTheme === "light" ? "dark" : "light";
 
   info(`Original theme was '${originalTheme}'.`);
--- a/devtools/client/inspector/fonts/test/head.js
+++ b/devtools/client/inspector/fonts/test/head.js
@@ -56,18 +56,18 @@ var openFontInspectorForURL = Task.async
  * preview images to be updated.
  *
  * @param {FontInspector} view - The FontInspector instance.
  * @param {String} text - The text to preview.
  */
 function* updatePreviewText(view, text) {
   info(`Changing the preview text to '${text}'`);
 
-  let doc = view.chromeDoc;
-  let input = doc.getElementById("font-preview-text-input");
+  let doc = view.document;
+  let input = doc.querySelector("#sidebar-panel-fontinspector .devtools-textinput");
   let update = view.inspector.once("fontinspector-updated");
 
   info("Focusing the input field.");
   input.focus();
 
   is(doc.activeElement, input, "The input was focused.");
 
   info("Blanking the input field.");
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/types.js
@@ -0,0 +1,36 @@
+/* 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 { PropTypes } = require("devtools/client/shared/vendor/react");
+
+/**
+ * A single font.
+ */
+exports.font = {
+
+  // The name of the font family
+  CSSFamilyName: PropTypes.string,
+
+  // The format of the font
+  format: PropTypes.string,
+
+  // The name of the font
+  name: PropTypes.string,
+
+  // URL for the font preview
+  previewUrl: PropTypes.string,
+
+  // Object containing the CSS rule for the font
+  rule: PropTypes.object,
+
+  // The text of the CSS rule
+  ruleText: PropTypes.string,
+
+  // The URI of the font file
+  URI: PropTypes.string,
+
+};
+
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/utils/l10n.js
@@ -0,0 +1,12 @@
+/* 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 { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/font-inspector.properties");
+
+module.exports = {
+  getStr: (...args) => L10N.getStr(...args),
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/utils/moz.build
@@ -0,0 +1,9 @@
+# -*- 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(
+    'l10n.js',
+)
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -580,17 +580,17 @@ Inspector.prototype = {
         break;
       case "boxmodel":
         // box-model isn't a panel on its own, it used to, now it is being used by
         // computed view and layout which retrieves an instance via getPanel.
         const BoxModel = require("devtools/client/inspector/boxmodel/box-model");
         panel = new BoxModel(this, this.panelWin);
         break;
       case "fontinspector":
-        const {FontInspector} = require("devtools/client/inspector/fonts/fonts");
+        const FontInspector = require("devtools/client/inspector/fonts/fonts");
         panel = new FontInspector(this, this.panelWin);
         break;
       default:
         // This is a custom panel or a non lazy-loaded one.
         return null;
     }
     this._panels.set(id, panel);
     return panel;
@@ -636,20 +636,19 @@ Inspector.prototype = {
         "animationinspector",
         INSPECTOR_L10N.getStr("inspector.sidebar.animationInspectorTitle"),
         "chrome://devtools/content/animationinspector/animation-inspector.xhtml",
         defaultTab == "animationinspector");
     }
 
     if (Services.prefs.getBoolPref("devtools.fontinspector.enabled") &&
         this.canGetUsedFontFaces) {
-      this.sidebar.addExistingTab(
-        "fontinspector",
-        INSPECTOR_L10N.getStr("inspector.sidebar.fontInspectorTitle"),
-        defaultTab == "fontinspector");
+      const FontInspector = this.browserRequire("devtools/client/inspector/fonts/fonts");
+      this.fontinspector = new FontInspector(this, this.panelWin);
+      this.fontinspector.init();
 
       this.sidebar.toggleTab(true, "fontinspector");
     }
 
     // Setup the splitter before the sidebar is displayed so,
     // we don't miss any events.
     this.setupSplitter();
 
@@ -976,16 +975,20 @@ Inspector.prototype = {
     if (this.gridInspector) {
       this.gridInspector.destroy();
     }
 
     if (this.layoutview) {
       this.layoutview.destroy();
     }
 
+    if (this.fontinspector) {
+      this.fontinspector.destroy();
+    }
+
     let cssPropertiesDestroyer = this._cssPropertiesLoaded.then(({front}) => {
       if (front) {
         front.destroy();
       }
     });
 
     this.sidebar.off("select", this.onSidebarSelect);
     let sidebarDestroyer = this.sidebar.destroy();
--- a/devtools/client/inspector/inspector.xhtml
+++ b/devtools/client/inspector/inspector.xhtml
@@ -140,54 +140,16 @@
             <div id="propertyContainer" class="theme-separator" tabindex="0" dir="ltr">
             </div>
 
             <div id="computedview-no-results" hidden="" data-localization="content=inspector.noProperties"></div>
           </div>
         </div>
       </div>
 
-      <div id="sidebar-panel-fontinspector" class="devtools-monospace theme-sidebar inspector-tabpanel"
-                data-localization-bundle="devtools/client/locales/font-inspector.properties">
-        <div class="devtools-toolbar">
-          <div class="devtools-searchbox">
-            <input id="font-preview-text-input" class="devtools-textinput" type="search"
-                        data-localization="placeholder=fontinspector.previewText"/>
-          </div>
-          <label id="font-showall" class="theme-link"
-                      data-localization="content=fontinspector.seeAll;
-                                         title=fontinspector.seeAll.tooltip"></label>
-        </div>
-
-        <div id="font-container">
-          <ul id="all-fonts"></ul>
-        </div>
-
-        <div id="font-template">
-          <section class="font">
-            <div class="font-preview-container">
-              <img class="font-preview"></img>
-            </div>
-            <div class="font-info">
-              <h1 class="font-name"></h1>
-              <span class="font-is-local" data-localization="content=fontinspector.system"></span>
-              <span class="font-is-remote" data-localization="content=fontinspector.remote"></span>
-              <p class="font-format-url">
-                <input readonly="readonly" class="font-url"></input>
-                <span class="font-format"></span>
-              </p>
-              <p class="font-css">
-                <span data-localization="content=fontinspector.usedAs"></span> "<span class="font-css-name"></span>"
-              </p>
-              <pre class="font-css-code"></pre>
-            </div>
-          </section>
-        </div>
-      </div>
-
       <div id="sidebar-panel-animationinspector" class="devtools-monospace theme-sidebar inspector-tabpanel">
         <iframe class="devtools-inspector-tab-frame"></iframe>
       </div>
     </div>
 
   </div>
 </body>
 </html>
--- a/devtools/client/inspector/reducers.js
+++ b/devtools/client/inspector/reducers.js
@@ -3,10 +3,12 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 // This file exposes the Redux reducers of the box model, grid and grid highlighter
 // settings.
 
 exports.boxModel = require("devtools/client/inspector/boxmodel/reducers/box-model");
+exports.fontOptions = require("devtools/client/inspector/fonts/reducers/font-options");
+exports.fonts = require("devtools/client/inspector/fonts/reducers/fonts");
 exports.grids = require("devtools/client/inspector/grids/reducers/grids");
 exports.highlighterSettings = require("devtools/client/inspector/grids/reducers/highlighter-settings");
--- a/devtools/client/shared/browser-loader.js
+++ b/devtools/client/shared/browser-loader.js
@@ -9,16 +9,17 @@ const { devtools } = Cu.import("resource
 const { joinURI } = devtools.require("devtools/shared/path");
 const { assert } = devtools.require("devtools/shared/DevToolsUtils");
 const Services = devtools.require("Services");
 const { AppConstants } = devtools.require("resource://gre/modules/AppConstants.jsm");
 
 const BROWSER_BASED_DIRS = [
   "resource://devtools/client/inspector/boxmodel",
   "resource://devtools/client/inspector/computed",
+  "resource://devtools/client/inspector/fonts",
   "resource://devtools/client/inspector/grids",
   "resource://devtools/client/inspector/layout",
   "resource://devtools/client/jsonview",
   "resource://devtools/client/shared/source-map",
   "resource://devtools/client/shared/redux",
   "resource://devtools/client/shared/vendor",
 ];