Bug 1239437 - Stir in React and Redux. r=pbrosset,jlongster draft
authorJ. Ryan Stinnett <jryans@gmail.com>
Fri, 22 Jan 2016 20:36:59 -0600
changeset 324478 24f1704511b9b753347dd0736f79ce8c8f9af7a6
parent 324477 28217f2b31b827699cd2da17a06f526aa33f7cea
child 324479 71b1feac3bd564e69990d56b0cb3032b3d47d71d
push id9925
push userjryans@gmail.com
push dateSat, 23 Jan 2016 02:38:47 +0000
reviewerspbrosset, jlongster
bugs1239437
milestone46.0a1
Bug 1239437 - Stir in React and Redux. r=pbrosset,jlongster
devtools/client/responsive.html/actions/index.js
devtools/client/responsive.html/actions/location.js
devtools/client/responsive.html/actions/moz.build
devtools/client/responsive.html/actions/viewports.js
devtools/client/responsive.html/app.js
devtools/client/responsive.html/components/browser.js
devtools/client/responsive.html/components/moz.build
devtools/client/responsive.html/components/viewport.js
devtools/client/responsive.html/components/viewports.js
devtools/client/responsive.html/index.js
devtools/client/responsive.html/index.xhtml
devtools/client/responsive.html/manager.js
devtools/client/responsive.html/models.js
devtools/client/responsive.html/moz.build
devtools/client/responsive.html/reducers.js
devtools/client/responsive.html/reducers/location.js
devtools/client/responsive.html/reducers/moz.build
devtools/client/responsive.html/reducers/viewports.js
devtools/client/responsive.html/store.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/actions/index.js
@@ -0,0 +1,32 @@
+/* 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";
+
+// This file lists all of the actions available in responsive design.  This
+// central list of constants makes it easy to see all possible action names at
+// a glance.  Please add a comment with each new action type.
+
+createEnum([
+
+  // The location of the page has changed.  This may be triggered by the user
+  // directly entering a new URL, navigating with links, etc.
+  "CHANGE_LOCATION",
+
+  // Add an additional viewport to display the document.
+  "ADD_VIEWPORT",
+
+], module.exports);
+
+/**
+ * Create a simple enum-like object with keys mirrored to values from an array.
+ * This makes comparison to a specfic value simpler without having to repeat and
+ * mis-type the value.
+ */
+function createEnum(array, target) {
+  for (let key of array) {
+    target[key] = key;
+  }
+  return target;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/actions/location.js
@@ -0,0 +1,22 @@
+/* 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 { CHANGE_LOCATION } = require("./index");
+
+module.exports = {
+
+  /**
+   * The location of the page has changed.  This may be triggered by the user
+   * directly entering a new URL, navigating with links, etc.
+   */
+  changeLocation(location) {
+    return {
+      type: CHANGE_LOCATION,
+      location,
+    };
+  },
+
+};
copy from devtools/client/responsive.html/moz.build
copy to devtools/client/responsive.html/actions/moz.build
--- a/devtools/client/responsive.html/moz.build
+++ b/devtools/client/responsive.html/actions/moz.build
@@ -1,10 +1,11 @@
 # -*- Mode: python; c-basic-offset: 4; 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(
-    'index.css',
-    'manager.js',
+    'index.js',
+    'location.js',
+    'viewports.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/actions/viewports.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 { ADD_VIEWPORT } = require("./index");
+
+module.exports = {
+
+  /**
+   * Add an additional viewport to display the document.
+   */
+  addViewport() {
+    return {
+      type: ADD_VIEWPORT,
+    };
+  },
+
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/app.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 { createClass, createFactory } =
+  require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+
+const Models = require("./models");
+const Viewports = createFactory(require("./components/viewports"));
+
+let App = createClass({
+
+  displayName: "App",
+
+  propTypes: Models.app,
+
+  render() {
+    let {
+      location,
+      viewports,
+    } = this.props;
+
+    // For the moment, the app is just the viewports.  This seems likely to
+    // change assuming we add a global toolbar or something similar.
+    return Viewports({
+      location,
+      viewports,
+    });
+  },
+
+});
+
+module.exports = connect(state => state)(App);
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/components/browser.js
@@ -0,0 +1,37 @@
+/* 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 { DOM: dom, createClass } =
+  require("devtools/client/shared/vendor/react");
+
+const Models = require("../models");
+
+module.exports = createClass({
+
+  displayName: "Browser",
+
+  propTypes: {
+    location: Models.app.location,
+    width: Models.viewport.width,
+    height: Models.viewport.height,
+  },
+
+  render() {
+    let {
+      location,
+      width,
+      height,
+    } = this.props;
+
+    return dom.iframe({
+      className: "browser",
+      src: location,
+      width,
+      height,
+    });
+  },
+
+});
copy from devtools/client/responsive.html/moz.build
copy to devtools/client/responsive.html/components/moz.build
--- a/devtools/client/responsive.html/moz.build
+++ b/devtools/client/responsive.html/components/moz.build
@@ -1,10 +1,11 @@
 # -*- Mode: python; c-basic-offset: 4; 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(
-    'index.css',
-    'manager.js',
+    'browser.js',
+    'viewport.js',
+    'viewports.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/components/viewport.js
@@ -0,0 +1,39 @@
+/* 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 { DOM: dom, createClass, createFactory, PropTypes } =
+  require("devtools/client/shared/vendor/react");
+
+const Models = require("../models");
+const Browser = createFactory(require("./browser"));
+
+module.exports = createClass({
+
+  displayName: "Viewport",
+
+  propTypes: {
+    location: Models.app.location,
+    viewport: PropTypes.shape(Models.viewport).isRequired,
+  },
+
+  render() {
+    let {
+      location,
+      viewport,
+    } = this.props;
+
+    // Additional elements will soon appear here around the Browser, like drag
+    // handles, etc.
+    return dom.div({
+      className: "viewport",
+    }, Browser({
+      location,
+      width: viewport.width,
+      height: viewport.height,
+    }));
+  },
+
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/components/viewports.js
@@ -0,0 +1,38 @@
+/* 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 { DOM: dom, createClass, createFactory } =
+  require("devtools/client/shared/vendor/react");
+
+const Models = require("../models");
+const Viewport = createFactory(require("./viewport"));
+
+module.exports = createClass({
+
+  displayName: "Viewports",
+
+  propTypes: Models.app,
+
+  render() {
+    let {
+      location,
+      viewports,
+    } = this.props;
+
+    let children = viewports.map((viewport, index) => {
+      return Viewport({
+        key: index,
+        location,
+        viewport,
+      });
+    });
+
+    return dom.div({
+      id: "viewports",
+    }, children);
+  },
+
+});
--- a/devtools/client/responsive.html/index.js
+++ b/devtools/client/responsive.html/index.js
@@ -1,17 +1,83 @@
 /* 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/. */
 
 /* eslint-env browser */
 
 "use strict";
 
-window.addViewport = contentURI => {
+const { utils: Cu } = Components;
+const { BrowserLoader } =
+  Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+const { require } =
+  BrowserLoader("resource://devtools/client/responsive.html/", this);
+const Telemetry = require("devtools/client/shared/telemetry");
+
+const { createFactory, createElement } =
+  require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { Provider } = require("devtools/client/shared/vendor/react-redux");
+
+const App = createFactory(require("./app"));
+const Store = require("./store");
+const { changeLocation } = require("./actions/location");
+const { addViewport } = require("./actions/viewports");
+
+let bootstrap = {
+
+  telemetry: new Telemetry(),
+
+  store: null,
+
+  init() {
+    // TODO: Should we track this as a separate tool from the old version?
+    // See bug 1242057.
+    this.telemetry.toolOpened("responsive");
+    let store = this.store = Store();
+    let app = createElement(App);
+    let provider = createElement(Provider, { store }, app);
+    ReactDOM.render(provider, document.querySelector("#app"));
+  },
+
+  destroy() {
+    ReactDOM.unmountComponentAtNode(document.querySelector("#app"));
+    this.store = null;
+    this.telemetry.toolClosed("responsive");
+    this.telemetry = null;
+  },
+
+  /**
+   * While most actions will be dispatched by React components, some external
+   * APIs that coordinate with the larger browser UI may also have actions to
+   * to dispatch.  They can do so here.
+   */
+  dispatch(action) {
+    this.store.dispatch(action);
+  },
+
+};
+
+window.addEventListener("load", function onLoad() {
+  window.removeEventListener("load", onLoad);
+  bootstrap.init();
+});
+
+window.addEventListener("unload", function onUnload() {
+  window.removeEventListener("unload", onUnload);
+  bootstrap.destroy();
+});
+
+// Allows quick testing of actions from the console
+window.dispatch = action => bootstrap.dispatch(action);
+
+/**
+ * Called by manager.js to add the initial viewport based on the original page.
+ */
+window.addInitialViewport = contentURI => {
   try {
-    let frame = document.createElement("iframe");
-    frame.setAttribute("src", contentURI);
-    document.body.appendChild(frame);
+    bootstrap.dispatch(changeLocation(contentURI));
+    bootstrap.dispatch(addViewport());
   } catch (e) {
     console.error(e);
   }
 };
--- a/devtools/client/responsive.html/index.xhtml
+++ b/devtools/client/responsive.html/index.xhtml
@@ -11,10 +11,11 @@
     <link rel="stylesheet" type="text/css"
           href="resource://devtools/client/responsive.html/index.css"/>
     <script type="application/javascript;version=1.8"
             src="chrome://devtools/content/shared/theme-switching.js"></script>
     <script type="application/javascript;version=1.8"
             src="./index.js"></script>
   </head>
   <body class="theme-body" role="application">
+    <div id="app"/>
   </body>
 </html>
--- a/devtools/client/responsive.html/manager.js
+++ b/devtools/client/responsive.html/manager.js
@@ -157,17 +157,17 @@ ResponsiveUI.prototype = {
    * bug 1238160 about <iframe mozbrowser> for more details.
    */
   init: Task.async(function*() {
     let tabBrowser = this.tab.linkedBrowser;
     let contentURI = tabBrowser.documentURI.spec;
     tabBrowser.loadURI(TOOL_URL);
     yield tabLoaded(this.tab);
     let toolWindow = tabBrowser.contentWindow;
-    toolWindow.addViewport(contentURI);
+    toolWindow.addInitialViewport(contentURI);
   }),
 
   destroy() {
     let tabBrowser = this.tab.linkedBrowser;
     tabBrowser.goBack();
     this.window = null;
     this.tab = null;
   },
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/models.js
@@ -0,0 +1,30 @@
+/* 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");
+
+// These models use React PropTypes to describe the expected "shape" of various
+// objects that get passed down as props to components.
+
+let viewport = exports.viewport = {
+
+  // The width of the viewport
+  width: PropTypes.number.isRequired,
+
+  // The height of the viewport
+  height: PropTypes.number.isRequired,
+
+};
+
+exports.app = {
+
+  // The location of the document displayed in the viewport(s)
+  location: PropTypes.string.isRequired,
+
+  // Array of one or more viewports to display the document
+  viewports: PropTypes.arrayOf(PropTypes.shape(viewport)).isRequired,
+
+};
--- a/devtools/client/responsive.html/moz.build
+++ b/devtools/client/responsive.html/moz.build
@@ -1,10 +1,20 @@
 # -*- Mode: python; c-basic-offset: 4; 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',
+]
+
 DevToolsModules(
+    'app.js',
     'index.css',
     'manager.js',
+    'models.js',
+    'reducers.js',
+    'store.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/reducers.js
@@ -0,0 +1,8 @@
+/* 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";
+
+exports.location = require("./reducers/location");
+exports.viewports = require("./reducers/viewports");
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/reducers/location.js
@@ -0,0 +1,25 @@
+/* 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 { CHANGE_LOCATION } = require("../actions/index");
+
+const INITIAL_LOCATION = "about:blank";
+
+let reducers = {
+
+  [CHANGE_LOCATION](_, action) {
+    return action.location;
+  },
+
+};
+
+module.exports = function(location = INITIAL_LOCATION, action) {
+  let reducer = reducers[action.type];
+  if (!reducer) {
+    return location;
+  }
+  return reducer(location, action);
+};
copy from devtools/client/responsive.html/moz.build
copy to devtools/client/responsive.html/reducers/moz.build
--- a/devtools/client/responsive.html/moz.build
+++ b/devtools/client/responsive.html/reducers/moz.build
@@ -1,10 +1,10 @@
 # -*- Mode: python; c-basic-offset: 4; 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(
-    'index.css',
-    'manager.js',
+    'location.js',
+    'viewports.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/reducers/viewports.js
@@ -0,0 +1,33 @@
+/* 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 { ADD_VIEWPORT } = require("../actions/index");
+
+const INITIAL_VIEWPORTS = [];
+const INITIAL_VIEWPORT = {
+  width: 320,
+  height: 480,
+};
+
+let reducers = {
+
+  [ADD_VIEWPORT](viewports) {
+    // For the moment, there can be at most one viewport.
+    if (viewports.length === 1) {
+      return viewports;
+    }
+    return [...viewports, INITIAL_VIEWPORT];
+  },
+
+};
+
+module.exports = function(viewports = INITIAL_VIEWPORTS, action) {
+  let reducer = reducers[action.type];
+  if (!reducer) {
+    return viewports;
+  }
+  return reducer(viewports, action);
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/store.js
@@ -0,0 +1,33 @@
+/* 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 { combineReducers } = require("devtools/client/shared/vendor/redux");
+const createStore = require("devtools/client/shared/redux/create-store");
+const reducers = require("./reducers");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+
+module.exports = function() {
+  let shouldLog = false;
+  let history;
+
+  // If testing, store the action history in an array
+  // we'll later attach to the store
+  if (DevToolsUtils.testing) {
+    history = [];
+    shouldLog = true;
+  }
+
+  let store = createStore({
+    log: shouldLog,
+    history
+  })(combineReducers(reducers), {});
+
+  if (history) {
+    store.history = history;
+  }
+
+  return store;
+};