summaryrefslogtreecommitdiffstats
path: root/devtools/client/responsive.html/components
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/responsive.html/components')
-rw-r--r--devtools/client/responsive.html/components/browser.js149
-rw-r--r--devtools/client/responsive.html/components/device-modal.js181
-rw-r--r--devtools/client/responsive.html/components/device-selector.js122
-rw-r--r--devtools/client/responsive.html/components/dpr-selector.js131
-rw-r--r--devtools/client/responsive.html/components/global-toolbar.js101
-rw-r--r--devtools/client/responsive.html/components/moz.build19
-rw-r--r--devtools/client/responsive.html/components/network-throttling-selector.js92
-rw-r--r--devtools/client/responsive.html/components/resizable-viewport.js195
-rw-r--r--devtools/client/responsive.html/components/viewport-dimension.js173
-rw-r--r--devtools/client/responsive.html/components/viewport-toolbar.js55
-rw-r--r--devtools/client/responsive.html/components/viewport.js114
-rw-r--r--devtools/client/responsive.html/components/viewports.js70
12 files changed, 1402 insertions, 0 deletions
diff --git a/devtools/client/responsive.html/components/browser.js b/devtools/client/responsive.html/components/browser.js
new file mode 100644
index 000000000..f2902905b
--- /dev/null
+++ b/devtools/client/responsive.html/components/browser.js
@@ -0,0 +1,149 @@
+/* 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";
+
+const { Task } = require("devtools/shared/task");
+const flags = require("devtools/shared/flags");
+const { getToplevelWindow } = require("sdk/window/utils");
+const { DOM: dom, createClass, addons, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+
+const Types = require("../types");
+const e10s = require("../utils/e10s");
+const message = require("../utils/message");
+
+module.exports = createClass({
+
+ /**
+ * This component is not allowed to depend directly on frequently changing
+ * data (width, height) due to the use of `dangerouslySetInnerHTML` below.
+ * Any changes in props will cause the <iframe> to be removed and added again,
+ * throwing away the current state of the page.
+ */
+ displayName: "Browser",
+
+ propTypes: {
+ location: Types.location.isRequired,
+ swapAfterMount: PropTypes.bool.isRequired,
+ onBrowserMounted: PropTypes.func.isRequired,
+ onContentResize: PropTypes.func.isRequired,
+ },
+
+ mixins: [ addons.PureRenderMixin ],
+
+ /**
+ * Once the browser element has mounted, load the frame script and enable
+ * various features, like floating scrollbars.
+ */
+ componentDidMount: Task.async(function* () {
+ // If we are not swapping browsers after mount, it's safe to start the frame
+ // script now.
+ if (!this.props.swapAfterMount) {
+ yield this.startFrameScript();
+ }
+
+ // Notify manager.js that this browser has mounted, so that it can trigger
+ // a swap if needed and continue with the rest of its startup.
+ this.props.onBrowserMounted();
+
+ // If we are swapping browsers after mount, wait for the swap to complete
+ // and start the frame script after that.
+ if (this.props.swapAfterMount) {
+ yield message.wait(window, "start-frame-script");
+ yield this.startFrameScript();
+ message.post(window, "start-frame-script:done");
+ }
+
+ // Stop the frame script when requested in the future.
+ message.wait(window, "stop-frame-script").then(() => {
+ this.stopFrameScript();
+ });
+ }),
+
+ onContentResize(msg) {
+ let { onContentResize } = this.props;
+ let { width, height } = msg.data;
+ onContentResize({
+ width,
+ height,
+ });
+ },
+
+ startFrameScript: Task.async(function* () {
+ let { onContentResize } = this;
+ let browser = this.refs.browserContainer.querySelector("iframe.browser");
+ let mm = browser.frameLoader.messageManager;
+
+ // Notify tests when the content has received a resize event. This is not
+ // quite the same timing as when we _set_ a new size around the browser,
+ // since it still needs to do async work before the content is actually
+ // resized to match.
+ e10s.on(mm, "OnContentResize", onContentResize);
+
+ let ready = e10s.once(mm, "ChildScriptReady");
+ mm.loadFrameScript("resource://devtools/client/responsivedesign/" +
+ "responsivedesign-child.js", true);
+ yield ready;
+
+ let browserWindow = getToplevelWindow(window);
+ let requiresFloatingScrollbars =
+ !browserWindow.matchMedia("(-moz-overlay-scrollbars)").matches;
+
+ yield e10s.request(mm, "Start", {
+ requiresFloatingScrollbars,
+ // Tests expect events on resize to yield on various size changes
+ notifyOnResize: flags.testing,
+ });
+ }),
+
+ stopFrameScript: Task.async(function* () {
+ let { onContentResize } = this;
+
+ let browser = this.refs.browserContainer.querySelector("iframe.browser");
+ let mm = browser.frameLoader.messageManager;
+ e10s.off(mm, "OnContentResize", onContentResize);
+ yield e10s.request(mm, "Stop");
+ message.post(window, "stop-frame-script:done");
+ }),
+
+ render() {
+ let {
+ location,
+ } = this.props;
+
+ // innerHTML expects & to be an HTML entity
+ location = location.replace(/&/g, "&amp;");
+
+ return dom.div(
+ {
+ ref: "browserContainer",
+ className: "browser-container",
+
+ /**
+ * React uses a whitelist for attributes, so we need some way to set
+ * attributes it does not know about, such as @mozbrowser. If this were
+ * the only issue, we could use componentDidMount or ref: node => {} to
+ * set the atttibutes. In the case of @remote, the attribute must be set
+ * before the element is added to the DOM to have any effect, which we
+ * are able to do with this approach.
+ *
+ * @noisolation and @allowfullscreen are needed so that these frames
+ * have the same access to browser features as regular browser tabs.
+ * The `swapFrameLoaders` platform API we use compares such features
+ * before allowing the swap to proceed.
+ */
+ dangerouslySetInnerHTML: {
+ __html: `<iframe class="browser" mozbrowser="true" remote="true"
+ noisolation="true" allowfullscreen="true"
+ src="${location}" width="100%" height="100%">
+ </iframe>`
+ }
+ }
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/components/device-modal.js b/devtools/client/responsive.html/components/device-modal.js
new file mode 100644
index 000000000..d28b97472
--- /dev/null
+++ b/devtools/client/responsive.html/components/device-modal.js
@@ -0,0 +1,181 @@
+/* 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";
+
+const { DOM: dom, createClass, PropTypes, addons } =
+ require("devtools/client/shared/vendor/react");
+const { getStr } = require("../utils/l10n");
+const Types = require("../types");
+
+module.exports = createClass({
+ displayName: "DeviceModal",
+
+ propTypes: {
+ devices: PropTypes.shape(Types.devices).isRequired,
+ onDeviceListUpdate: PropTypes.func.isRequired,
+ onUpdateDeviceDisplayed: PropTypes.func.isRequired,
+ onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+ },
+
+ mixins: [ addons.PureRenderMixin ],
+
+ getInitialState() {
+ return {};
+ },
+
+ componentDidMount() {
+ window.addEventListener("keydown", this.onKeyDown, true);
+ },
+
+ componentWillReceiveProps(nextProps) {
+ let {
+ devices,
+ } = nextProps;
+
+ for (let type of devices.types) {
+ for (let device of devices[type]) {
+ this.setState({
+ [device.name]: device.displayed,
+ });
+ }
+ }
+ },
+
+ componentWillUnmount() {
+ window.removeEventListener("keydown", this.onKeyDown, true);
+ },
+
+ onDeviceCheckboxClick({ target }) {
+ this.setState({
+ [target.value]: !this.state[target.value]
+ });
+ },
+
+ onDeviceModalSubmit() {
+ let {
+ devices,
+ onDeviceListUpdate,
+ onUpdateDeviceDisplayed,
+ onUpdateDeviceModalOpen,
+ } = this.props;
+
+ let preferredDevices = {
+ "added": new Set(),
+ "removed": new Set(),
+ };
+
+ for (let type of devices.types) {
+ for (let device of devices[type]) {
+ let newState = this.state[device.name];
+
+ if (device.featured && !newState) {
+ preferredDevices.removed.add(device.name);
+ } else if (!device.featured && newState) {
+ preferredDevices.added.add(device.name);
+ }
+
+ if (this.state[device.name] != device.displayed) {
+ onUpdateDeviceDisplayed(device, type, this.state[device.name]);
+ }
+ }
+ }
+
+ onDeviceListUpdate(preferredDevices);
+ onUpdateDeviceModalOpen(false);
+ },
+
+ onKeyDown(event) {
+ if (!this.props.devices.isModalOpen) {
+ return;
+ }
+ // Escape keycode
+ if (event.keyCode === 27) {
+ let {
+ onUpdateDeviceModalOpen
+ } = this.props;
+ onUpdateDeviceModalOpen(false);
+ }
+ },
+
+ render() {
+ let {
+ devices,
+ onUpdateDeviceModalOpen,
+ } = this.props;
+
+ const sortedDevices = {};
+ for (let type of devices.types) {
+ sortedDevices[type] = Object.assign([], devices[type])
+ .sort((a, b) => a.name.localeCompare(b.name));
+ }
+
+ return dom.div(
+ {
+ id: "device-modal-wrapper",
+ className: this.props.devices.isModalOpen ? "opened" : "closed",
+ },
+ dom.div(
+ {
+ className: "device-modal container",
+ },
+ dom.button({
+ id: "device-close-button",
+ className: "toolbar-button devtools-button",
+ onClick: () => onUpdateDeviceModalOpen(false),
+ }),
+ dom.div(
+ {
+ className: "device-modal-content",
+ },
+ devices.types.map(type => {
+ return dom.div(
+ {
+ className: "device-type",
+ key: type,
+ },
+ dom.header(
+ {
+ className: "device-header",
+ },
+ type
+ ),
+ sortedDevices[type].map(device => {
+ return dom.label(
+ {
+ className: "device-label",
+ key: device.name,
+ },
+ dom.input({
+ className: "device-input-checkbox",
+ type: "checkbox",
+ value: device.name,
+ checked: this.state[device.name],
+ onChange: this.onDeviceCheckboxClick,
+ }),
+ device.name
+ );
+ })
+ );
+ })
+ ),
+ dom.button(
+ {
+ id: "device-submit-button",
+ onClick: this.onDeviceModalSubmit,
+ },
+ getStr("responsive.done")
+ )
+ ),
+ dom.div(
+ {
+ className: "modal-overlay",
+ onClick: () => onUpdateDeviceModalOpen(false),
+ }
+ )
+ );
+ },
+});
diff --git a/devtools/client/responsive.html/components/device-selector.js b/devtools/client/responsive.html/components/device-selector.js
new file mode 100644
index 000000000..3215ce5fb
--- /dev/null
+++ b/devtools/client/responsive.html/components/device-selector.js
@@ -0,0 +1,122 @@
+/* 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 { getStr } = require("../utils/l10n");
+const { DOM: dom, createClass, PropTypes, addons } =
+ require("devtools/client/shared/vendor/react");
+
+const Types = require("../types");
+const OPEN_DEVICE_MODAL_VALUE = "OPEN_DEVICE_MODAL";
+
+module.exports = createClass({
+ displayName: "DeviceSelector",
+
+ propTypes: {
+ devices: PropTypes.shape(Types.devices).isRequired,
+ selectedDevice: PropTypes.string.isRequired,
+ onChangeDevice: PropTypes.func.isRequired,
+ onResizeViewport: PropTypes.func.isRequired,
+ onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+ },
+
+ mixins: [ addons.PureRenderMixin ],
+
+ onSelectChange({ target }) {
+ let {
+ devices,
+ onChangeDevice,
+ onResizeViewport,
+ onUpdateDeviceModalOpen,
+ } = this.props;
+
+ if (target.value === OPEN_DEVICE_MODAL_VALUE) {
+ onUpdateDeviceModalOpen(true);
+ return;
+ }
+ for (let type of devices.types) {
+ for (let device of devices[type]) {
+ if (device.name === target.value) {
+ onResizeViewport(device.width, device.height);
+ onChangeDevice(device);
+ return;
+ }
+ }
+ }
+ },
+
+ render() {
+ let {
+ devices,
+ selectedDevice,
+ } = this.props;
+
+ let options = [];
+ for (let type of devices.types) {
+ for (let device of devices[type]) {
+ if (device.displayed) {
+ options.push(device);
+ }
+ }
+ }
+
+ options.sort(function (a, b) {
+ return a.name.localeCompare(b.name);
+ });
+
+ let selectClass = "viewport-device-selector";
+ if (selectedDevice) {
+ selectClass += " selected";
+ }
+
+ let state = devices.listState;
+ let listContent;
+
+ if (state == Types.deviceListState.LOADED) {
+ listContent = [dom.option({
+ value: "",
+ title: "",
+ disabled: true,
+ hidden: true,
+ }, getStr("responsive.noDeviceSelected")),
+ options.map(device => {
+ return dom.option({
+ key: device.name,
+ value: device.name,
+ title: "",
+ }, device.name);
+ }),
+ dom.option({
+ value: OPEN_DEVICE_MODAL_VALUE,
+ title: "",
+ }, getStr("responsive.editDeviceList"))];
+ } else if (state == Types.deviceListState.LOADING
+ || state == Types.deviceListState.INITIALIZED) {
+ listContent = [dom.option({
+ value: "",
+ title: "",
+ disabled: true,
+ }, getStr("responsive.deviceListLoading"))];
+ } else if (state == Types.deviceListState.ERROR) {
+ listContent = [dom.option({
+ value: "",
+ title: "",
+ disabled: true,
+ }, getStr("responsive.deviceListError"))];
+ }
+
+ return dom.select(
+ {
+ className: selectClass,
+ value: selectedDevice,
+ title: selectedDevice,
+ onChange: this.onSelectChange,
+ disabled: (state !== Types.deviceListState.LOADED),
+ },
+ ...listContent
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/components/dpr-selector.js b/devtools/client/responsive.html/components/dpr-selector.js
new file mode 100644
index 000000000..31b8db1c2
--- /dev/null
+++ b/devtools/client/responsive.html/components/dpr-selector.js
@@ -0,0 +1,131 @@
+/* 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";
+
+const { DOM: dom, createClass, PropTypes, addons } =
+ require("devtools/client/shared/vendor/react");
+
+const Types = require("../types");
+const { getStr, getFormatStr } = require("../utils/l10n");
+
+const PIXEL_RATIO_PRESET = [1, 2, 3];
+
+const createVisibleOption = value =>
+ dom.option({
+ value,
+ title: value,
+ key: value,
+ }, value);
+
+const createHiddenOption = value =>
+ dom.option({
+ value,
+ title: value,
+ hidden: true,
+ disabled: true,
+ }, value);
+
+module.exports = createClass({
+ displayName: "DPRSelector",
+
+ propTypes: {
+ devices: PropTypes.shape(Types.devices).isRequired,
+ displayPixelRatio: Types.pixelRatio.value.isRequired,
+ selectedDevice: PropTypes.string.isRequired,
+ selectedPixelRatio: PropTypes.shape(Types.pixelRatio).isRequired,
+ onChangePixelRatio: PropTypes.func.isRequired,
+ },
+
+ mixins: [ addons.PureRenderMixin ],
+
+ getInitialState() {
+ return {
+ isFocused: false
+ };
+ },
+
+ onFocusChange({type}) {
+ this.setState({
+ isFocused: type === "focus"
+ });
+ },
+
+ onSelectChange({ target }) {
+ this.props.onChangePixelRatio(+target.value);
+ },
+
+ render() {
+ let {
+ devices,
+ displayPixelRatio,
+ selectedDevice,
+ selectedPixelRatio,
+ } = this.props;
+
+ let hiddenOptions = [];
+
+ for (let type of devices.types) {
+ for (let device of devices[type]) {
+ if (device.displayed &&
+ !hiddenOptions.includes(device.pixelRatio) &&
+ !PIXEL_RATIO_PRESET.includes(device.pixelRatio)) {
+ hiddenOptions.push(device.pixelRatio);
+ }
+ }
+ }
+
+ if (!PIXEL_RATIO_PRESET.includes(displayPixelRatio)) {
+ hiddenOptions.push(displayPixelRatio);
+ }
+
+ let state = devices.listState;
+ let isDisabled = (state !== Types.deviceListState.LOADED) || (selectedDevice !== "");
+ let selectorClass = "";
+ let title;
+
+ if (isDisabled) {
+ selectorClass += " disabled";
+ title = getFormatStr("responsive.autoDPR", selectedDevice);
+ } else {
+ title = getStr("responsive.devicePixelRatio");
+
+ if (selectedPixelRatio.value) {
+ selectorClass += " selected";
+ }
+ }
+
+ if (this.state.isFocused) {
+ selectorClass += " focused";
+ }
+
+ let listContent = PIXEL_RATIO_PRESET.map(createVisibleOption);
+
+ if (state == Types.deviceListState.LOADED) {
+ listContent = listContent.concat(hiddenOptions.map(createHiddenOption));
+ }
+
+ return dom.label(
+ {
+ id: "global-dpr-selector",
+ className: selectorClass,
+ title,
+ },
+ "DPR",
+ dom.select(
+ {
+ value: selectedPixelRatio.value || displayPixelRatio,
+ disabled: isDisabled,
+ onChange: this.onSelectChange,
+ onFocus: this.onFocusChange,
+ onBlur: this.onFocusChange,
+ },
+ ...listContent
+ )
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/components/global-toolbar.js b/devtools/client/responsive.html/components/global-toolbar.js
new file mode 100644
index 000000000..6c31fa338
--- /dev/null
+++ b/devtools/client/responsive.html/components/global-toolbar.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 { DOM: dom, createClass, createFactory, PropTypes, addons } =
+ require("devtools/client/shared/vendor/react");
+
+const { getStr } = require("../utils/l10n");
+const Types = require("../types");
+const DPRSelector = createFactory(require("./dpr-selector"));
+const NetworkThrottlingSelector = createFactory(require("./network-throttling-selector"));
+
+module.exports = createClass({
+ displayName: "GlobalToolbar",
+
+ propTypes: {
+ devices: PropTypes.shape(Types.devices).isRequired,
+ displayPixelRatio: Types.pixelRatio.value.isRequired,
+ networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
+ screenshot: PropTypes.shape(Types.screenshot).isRequired,
+ selectedDevice: PropTypes.string.isRequired,
+ selectedPixelRatio: PropTypes.shape(Types.pixelRatio).isRequired,
+ touchSimulation: PropTypes.shape(Types.touchSimulation).isRequired,
+ onChangeNetworkThrottling: PropTypes.func.isRequired,
+ onChangePixelRatio: PropTypes.func.isRequired,
+ onChangeTouchSimulation: PropTypes.func.isRequired,
+ onExit: PropTypes.func.isRequired,
+ onScreenshot: PropTypes.func.isRequired,
+ },
+
+ mixins: [ addons.PureRenderMixin ],
+
+ render() {
+ let {
+ devices,
+ displayPixelRatio,
+ networkThrottling,
+ screenshot,
+ selectedDevice,
+ selectedPixelRatio,
+ touchSimulation,
+ onChangeNetworkThrottling,
+ onChangePixelRatio,
+ onChangeTouchSimulation,
+ onExit,
+ onScreenshot,
+ } = this.props;
+
+ let touchButtonClass = "toolbar-button devtools-button";
+ if (touchSimulation.enabled) {
+ touchButtonClass += " active";
+ }
+
+ return dom.header(
+ {
+ id: "global-toolbar",
+ className: "container",
+ },
+ dom.span(
+ {
+ className: "title",
+ },
+ getStr("responsive.title")
+ ),
+ NetworkThrottlingSelector({
+ networkThrottling,
+ onChangeNetworkThrottling,
+ }),
+ DPRSelector({
+ devices,
+ displayPixelRatio,
+ selectedDevice,
+ selectedPixelRatio,
+ onChangePixelRatio,
+ }),
+ dom.button({
+ id: "global-touch-simulation-button",
+ className: touchButtonClass,
+ title: (touchSimulation.enabled ?
+ getStr("responsive.disableTouch") : getStr("responsive.enableTouch")),
+ onClick: () => onChangeTouchSimulation(!touchSimulation.enabled),
+ }),
+ dom.button({
+ id: "global-screenshot-button",
+ className: "toolbar-button devtools-button",
+ title: getStr("responsive.screenshot"),
+ onClick: onScreenshot,
+ disabled: screenshot.isCapturing,
+ }),
+ dom.button({
+ id: "global-exit-button",
+ className: "toolbar-button devtools-button",
+ title: getStr("responsive.exit"),
+ onClick: onExit,
+ })
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/components/moz.build b/devtools/client/responsive.html/components/moz.build
new file mode 100644
index 000000000..4ad36f992
--- /dev/null
+++ b/devtools/client/responsive.html/components/moz.build
@@ -0,0 +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/.
+
+DevToolsModules(
+ 'browser.js',
+ 'device-modal.js',
+ 'device-selector.js',
+ 'dpr-selector.js',
+ 'global-toolbar.js',
+ 'network-throttling-selector.js',
+ 'resizable-viewport.js',
+ 'viewport-dimension.js',
+ 'viewport-toolbar.js',
+ 'viewport.js',
+ 'viewports.js',
+)
diff --git a/devtools/client/responsive.html/components/network-throttling-selector.js b/devtools/client/responsive.html/components/network-throttling-selector.js
new file mode 100644
index 000000000..fa9f5c6a0
--- /dev/null
+++ b/devtools/client/responsive.html/components/network-throttling-selector.js
@@ -0,0 +1,92 @@
+/* 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, PropTypes, addons } =
+ require("devtools/client/shared/vendor/react");
+
+const Types = require("../types");
+const { getStr } = require("../utils/l10n");
+const throttlingProfiles = require("devtools/client/shared/network-throttling-profiles");
+
+module.exports = createClass({
+
+ displayName: "NetworkThrottlingSelector",
+
+ propTypes: {
+ networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
+ onChangeNetworkThrottling: PropTypes.func.isRequired,
+ },
+
+ mixins: [ addons.PureRenderMixin ],
+
+ onSelectChange({ target }) {
+ let {
+ onChangeNetworkThrottling,
+ } = this.props;
+
+ if (target.value == getStr("responsive.noThrottling")) {
+ onChangeNetworkThrottling(false, "");
+ return;
+ }
+
+ for (let profile of throttlingProfiles) {
+ if (profile.id === target.value) {
+ onChangeNetworkThrottling(true, profile.id);
+ return;
+ }
+ }
+ },
+
+ render() {
+ let {
+ networkThrottling,
+ } = this.props;
+
+ let selectClass = "";
+ let selectedProfile;
+ if (networkThrottling.enabled) {
+ selectClass += " selected";
+ selectedProfile = networkThrottling.profile;
+ } else {
+ selectedProfile = getStr("responsive.noThrottling");
+ }
+
+ let listContent = [
+ dom.option(
+ {
+ key: "disabled",
+ },
+ getStr("responsive.noThrottling")
+ ),
+ dom.option(
+ {
+ key: "divider",
+ className: "divider",
+ disabled: true,
+ }
+ ),
+ throttlingProfiles.map(profile => {
+ return dom.option(
+ {
+ key: profile.id,
+ },
+ profile.id
+ );
+ }),
+ ];
+
+ return dom.select(
+ {
+ id: "global-network-throttling-selector",
+ className: selectClass,
+ value: selectedProfile,
+ onChange: this.onSelectChange,
+ },
+ ...listContent
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/components/resizable-viewport.js b/devtools/client/responsive.html/components/resizable-viewport.js
new file mode 100644
index 000000000..1d94cd052
--- /dev/null
+++ b/devtools/client/responsive.html/components/resizable-viewport.js
@@ -0,0 +1,195 @@
+/* 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/. */
+
+/* global window */
+
+"use strict";
+
+const { DOM: dom, createClass, createFactory, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+
+const Constants = require("../constants");
+const Types = require("../types");
+const Browser = createFactory(require("./browser"));
+const ViewportToolbar = createFactory(require("./viewport-toolbar"));
+
+const VIEWPORT_MIN_WIDTH = Constants.MIN_VIEWPORT_DIMENSION;
+const VIEWPORT_MIN_HEIGHT = Constants.MIN_VIEWPORT_DIMENSION;
+
+module.exports = createClass({
+
+ displayName: "ResizableViewport",
+
+ propTypes: {
+ devices: PropTypes.shape(Types.devices).isRequired,
+ location: Types.location.isRequired,
+ screenshot: PropTypes.shape(Types.screenshot).isRequired,
+ swapAfterMount: PropTypes.bool.isRequired,
+ viewport: PropTypes.shape(Types.viewport).isRequired,
+ onBrowserMounted: PropTypes.func.isRequired,
+ onChangeDevice: PropTypes.func.isRequired,
+ onContentResize: PropTypes.func.isRequired,
+ onRemoveDevice: PropTypes.func.isRequired,
+ onResizeViewport: PropTypes.func.isRequired,
+ onRotateViewport: PropTypes.func.isRequired,
+ onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+ },
+
+ getInitialState() {
+ return {
+ isResizing: false,
+ lastClientX: 0,
+ lastClientY: 0,
+ ignoreX: false,
+ ignoreY: false,
+ };
+ },
+
+ onResizeStart({ target, clientX, clientY }) {
+ window.addEventListener("mousemove", this.onResizeDrag, true);
+ window.addEventListener("mouseup", this.onResizeStop, true);
+
+ this.setState({
+ isResizing: true,
+ lastClientX: clientX,
+ lastClientY: clientY,
+ ignoreX: target === this.refs.resizeBarY,
+ ignoreY: target === this.refs.resizeBarX,
+ });
+ },
+
+ onResizeStop() {
+ window.removeEventListener("mousemove", this.onResizeDrag, true);
+ window.removeEventListener("mouseup", this.onResizeStop, true);
+
+ this.setState({
+ isResizing: false,
+ lastClientX: 0,
+ lastClientY: 0,
+ ignoreX: false,
+ ignoreY: false,
+ });
+ },
+
+ onResizeDrag({ clientX, clientY }) {
+ if (!this.state.isResizing) {
+ return;
+ }
+
+ let { lastClientX, lastClientY, ignoreX, ignoreY } = this.state;
+ // the viewport is centered horizontally, so horizontal resize resizes
+ // by twice the distance the mouse was dragged - on left and right side.
+ let deltaX = 2 * (clientX - lastClientX);
+ let deltaY = (clientY - lastClientY);
+
+ if (ignoreX) {
+ deltaX = 0;
+ }
+ if (ignoreY) {
+ deltaY = 0;
+ }
+
+ let width = this.props.viewport.width + deltaX;
+ let height = this.props.viewport.height + deltaY;
+
+ if (width < VIEWPORT_MIN_WIDTH) {
+ width = VIEWPORT_MIN_WIDTH;
+ } else {
+ lastClientX = clientX;
+ }
+
+ if (height < VIEWPORT_MIN_HEIGHT) {
+ height = VIEWPORT_MIN_HEIGHT;
+ } else {
+ lastClientY = clientY;
+ }
+
+ // Update the viewport store with the new width and height.
+ this.props.onResizeViewport(width, height);
+ // Change the device selector back to an unselected device
+ // TODO: Bug 1332754: Logic like this probably belongs in the action creator.
+ if (this.props.viewport.device) {
+ // In bug 1329843 and others, we may eventually stop this approach of removing the
+ // the properties of the device on resize. However, at the moment, there is no
+ // way to edit dPR when a device is selected, and there is no UI at all for editing
+ // UA, so it's important to keep doing this for now.
+ this.props.onRemoveDevice();
+ }
+
+ this.setState({
+ lastClientX,
+ lastClientY
+ });
+ },
+
+ render() {
+ let {
+ devices,
+ location,
+ screenshot,
+ swapAfterMount,
+ viewport,
+ onBrowserMounted,
+ onChangeDevice,
+ onContentResize,
+ onResizeViewport,
+ onRotateViewport,
+ onUpdateDeviceModalOpen,
+ } = this.props;
+
+ let resizeHandleClass = "viewport-resize-handle";
+ if (screenshot.isCapturing) {
+ resizeHandleClass += " hidden";
+ }
+
+ let contentClass = "viewport-content";
+ if (this.state.isResizing) {
+ contentClass += " resizing";
+ }
+
+ return dom.div(
+ {
+ className: "resizable-viewport",
+ },
+ ViewportToolbar({
+ devices,
+ selectedDevice: viewport.device,
+ onChangeDevice,
+ onResizeViewport,
+ onRotateViewport,
+ onUpdateDeviceModalOpen,
+ }),
+ dom.div(
+ {
+ className: contentClass,
+ style: {
+ width: viewport.width + "px",
+ height: viewport.height + "px",
+ },
+ },
+ Browser({
+ location,
+ swapAfterMount,
+ onBrowserMounted,
+ onContentResize,
+ })
+ ),
+ dom.div({
+ className: resizeHandleClass,
+ onMouseDown: this.onResizeStart,
+ }),
+ dom.div({
+ ref: "resizeBarX",
+ className: "viewport-horizontal-resize-handle",
+ onMouseDown: this.onResizeStart,
+ }),
+ dom.div({
+ ref: "resizeBarY",
+ className: "viewport-vertical-resize-handle",
+ onMouseDown: this.onResizeStart,
+ })
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/components/viewport-dimension.js b/devtools/client/responsive.html/components/viewport-dimension.js
new file mode 100644
index 000000000..a359cecf7
--- /dev/null
+++ b/devtools/client/responsive.html/components/viewport-dimension.js
@@ -0,0 +1,173 @@
+/* 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, PropTypes } =
+ require("devtools/client/shared/vendor/react");
+
+const Constants = require("../constants");
+const Types = require("../types");
+
+module.exports = createClass({
+ displayName: "ViewportDimension",
+
+ propTypes: {
+ viewport: PropTypes.shape(Types.viewport).isRequired,
+ onRemoveDevice: PropTypes.func.isRequired,
+ onResizeViewport: PropTypes.func.isRequired,
+ },
+
+ getInitialState() {
+ let { width, height } = this.props.viewport;
+
+ return {
+ width,
+ height,
+ isEditing: false,
+ isInvalid: false,
+ };
+ },
+
+ componentWillReceiveProps(nextProps) {
+ let { width, height } = nextProps.viewport;
+
+ this.setState({
+ width,
+ height,
+ });
+ },
+
+ validateInput(value) {
+ let isInvalid = true;
+
+ // Check the value is a number and greater than MIN_VIEWPORT_DIMENSION
+ if (/^\d{3,4}$/.test(value) &&
+ parseInt(value, 10) >= Constants.MIN_VIEWPORT_DIMENSION) {
+ isInvalid = false;
+ }
+
+ this.setState({
+ isInvalid,
+ });
+ },
+
+ onInputBlur() {
+ let { width, height } = this.props.viewport;
+
+ if (this.state.width != width || this.state.height != height) {
+ this.onInputSubmit();
+ }
+
+ this.setState({
+ isEditing: false,
+ inInvalid: false,
+ });
+ },
+
+ onInputChange({ target }) {
+ if (target.value.length > 4) {
+ return;
+ }
+
+ if (this.refs.widthInput == target) {
+ this.setState({ width: target.value });
+ this.validateInput(target.value);
+ }
+
+ if (this.refs.heightInput == target) {
+ this.setState({ height: target.value });
+ this.validateInput(target.value);
+ }
+ },
+
+ onInputFocus() {
+ this.setState({
+ isEditing: true,
+ });
+ },
+
+ onInputKeyUp({ target, keyCode }) {
+ // On Enter, submit the input
+ if (keyCode == 13) {
+ this.onInputSubmit();
+ }
+
+ // On Esc, blur the target
+ if (keyCode == 27) {
+ target.blur();
+ }
+ },
+
+ onInputSubmit() {
+ if (this.state.isInvalid) {
+ let { width, height } = this.props.viewport;
+
+ this.setState({
+ width,
+ height,
+ isInvalid: false,
+ });
+
+ return;
+ }
+
+ // Change the device selector back to an unselected device
+ // TODO: Bug 1332754: Logic like this probably belongs in the action creator.
+ if (this.props.viewport.device) {
+ this.props.onRemoveDevice();
+ }
+ this.props.onResizeViewport(parseInt(this.state.width, 10),
+ parseInt(this.state.height, 10));
+ },
+
+ render() {
+ let editableClass = "viewport-dimension-editable";
+ let inputClass = "viewport-dimension-input";
+
+ if (this.state.isEditing) {
+ editableClass += " editing";
+ inputClass += " editing";
+ }
+
+ if (this.state.isInvalid) {
+ editableClass += " invalid";
+ }
+
+ return dom.div(
+ {
+ className: "viewport-dimension",
+ },
+ dom.div(
+ {
+ className: editableClass,
+ },
+ dom.input({
+ ref: "widthInput",
+ className: inputClass,
+ size: 4,
+ value: this.state.width,
+ onBlur: this.onInputBlur,
+ onChange: this.onInputChange,
+ onFocus: this.onInputFocus,
+ onKeyUp: this.onInputKeyUp,
+ }),
+ dom.span({
+ className: "viewport-dimension-separator",
+ }, "×"),
+ dom.input({
+ ref: "heightInput",
+ className: inputClass,
+ size: 4,
+ value: this.state.height,
+ onBlur: this.onInputBlur,
+ onChange: this.onInputChange,
+ onFocus: this.onInputFocus,
+ onKeyUp: this.onInputKeyUp,
+ })
+ )
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/components/viewport-toolbar.js b/devtools/client/responsive.html/components/viewport-toolbar.js
new file mode 100644
index 000000000..7cbc73f67
--- /dev/null
+++ b/devtools/client/responsive.html/components/viewport-toolbar.js
@@ -0,0 +1,55 @@
+/* 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, addons } =
+ require("devtools/client/shared/vendor/react");
+
+const Types = require("../types");
+const DeviceSelector = createFactory(require("./device-selector"));
+
+module.exports = createClass({
+ displayName: "ViewportToolbar",
+
+ propTypes: {
+ devices: PropTypes.shape(Types.devices).isRequired,
+ selectedDevice: PropTypes.string.isRequired,
+ onChangeDevice: PropTypes.func.isRequired,
+ onResizeViewport: PropTypes.func.isRequired,
+ onRotateViewport: PropTypes.func.isRequired,
+ onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+ },
+
+ mixins: [ addons.PureRenderMixin ],
+
+ render() {
+ let {
+ devices,
+ selectedDevice,
+ onChangeDevice,
+ onResizeViewport,
+ onRotateViewport,
+ onUpdateDeviceModalOpen,
+ } = this.props;
+
+ return dom.div(
+ {
+ className: "viewport-toolbar container",
+ },
+ DeviceSelector({
+ devices,
+ selectedDevice,
+ onChangeDevice,
+ onResizeViewport,
+ onUpdateDeviceModalOpen,
+ }),
+ dom.button({
+ className: "viewport-rotate-button toolbar-button devtools-button",
+ onClick: onRotateViewport,
+ })
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/components/viewport.js b/devtools/client/responsive.html/components/viewport.js
new file mode 100644
index 000000000..fe41b41ee
--- /dev/null
+++ b/devtools/client/responsive.html/components/viewport.js
@@ -0,0 +1,114 @@
+/* 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 Types = require("../types");
+const ResizableViewport = createFactory(require("./resizable-viewport"));
+const ViewportDimension = createFactory(require("./viewport-dimension"));
+
+module.exports = createClass({
+
+ displayName: "Viewport",
+
+ propTypes: {
+ devices: PropTypes.shape(Types.devices).isRequired,
+ location: Types.location.isRequired,
+ screenshot: PropTypes.shape(Types.screenshot).isRequired,
+ swapAfterMount: PropTypes.bool.isRequired,
+ viewport: PropTypes.shape(Types.viewport).isRequired,
+ onBrowserMounted: PropTypes.func.isRequired,
+ onChangeDevice: PropTypes.func.isRequired,
+ onContentResize: PropTypes.func.isRequired,
+ onRemoveDevice: PropTypes.func.isRequired,
+ onResizeViewport: PropTypes.func.isRequired,
+ onRotateViewport: PropTypes.func.isRequired,
+ onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+ },
+
+ onChangeDevice(device) {
+ let {
+ viewport,
+ onChangeDevice,
+ } = this.props;
+
+ onChangeDevice(viewport.id, device);
+ },
+
+ onRemoveDevice() {
+ let {
+ viewport,
+ onRemoveDevice,
+ } = this.props;
+
+ onRemoveDevice(viewport.id);
+ },
+
+ onResizeViewport(width, height) {
+ let {
+ viewport,
+ onResizeViewport,
+ } = this.props;
+
+ onResizeViewport(viewport.id, width, height);
+ },
+
+ onRotateViewport() {
+ let {
+ viewport,
+ onRotateViewport,
+ } = this.props;
+
+ onRotateViewport(viewport.id);
+ },
+
+ render() {
+ let {
+ devices,
+ location,
+ screenshot,
+ swapAfterMount,
+ viewport,
+ onBrowserMounted,
+ onContentResize,
+ onUpdateDeviceModalOpen,
+ } = this.props;
+
+ let {
+ onChangeDevice,
+ onRemoveDevice,
+ onRotateViewport,
+ onResizeViewport,
+ } = this;
+
+ return dom.div(
+ {
+ className: "viewport",
+ },
+ ViewportDimension({
+ viewport,
+ onRemoveDevice,
+ onResizeViewport,
+ }),
+ ResizableViewport({
+ devices,
+ location,
+ screenshot,
+ swapAfterMount,
+ viewport,
+ onBrowserMounted,
+ onChangeDevice,
+ onContentResize,
+ onRemoveDevice,
+ onResizeViewport,
+ onRotateViewport,
+ onUpdateDeviceModalOpen,
+ })
+ );
+ },
+
+});
diff --git a/devtools/client/responsive.html/components/viewports.js b/devtools/client/responsive.html/components/viewports.js
new file mode 100644
index 000000000..b305d1e07
--- /dev/null
+++ b/devtools/client/responsive.html/components/viewports.js
@@ -0,0 +1,70 @@
+/* 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 Types = require("../types");
+const Viewport = createFactory(require("./viewport"));
+
+module.exports = createClass({
+
+ displayName: "Viewports",
+
+ propTypes: {
+ devices: PropTypes.shape(Types.devices).isRequired,
+ location: Types.location.isRequired,
+ screenshot: PropTypes.shape(Types.screenshot).isRequired,
+ viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
+ onBrowserMounted: PropTypes.func.isRequired,
+ onChangeDevice: PropTypes.func.isRequired,
+ onContentResize: PropTypes.func.isRequired,
+ onRemoveDevice: PropTypes.func.isRequired,
+ onResizeViewport: PropTypes.func.isRequired,
+ onRotateViewport: PropTypes.func.isRequired,
+ onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+ },
+
+ render() {
+ let {
+ devices,
+ location,
+ screenshot,
+ viewports,
+ onBrowserMounted,
+ onChangeDevice,
+ onContentResize,
+ onRemoveDevice,
+ onResizeViewport,
+ onRotateViewport,
+ onUpdateDeviceModalOpen,
+ } = this.props;
+
+ return dom.div(
+ {
+ id: "viewports",
+ },
+ viewports.map((viewport, i) => {
+ return Viewport({
+ key: viewport.id,
+ devices,
+ location,
+ screenshot,
+ swapAfterMount: i == 0,
+ viewport,
+ onBrowserMounted,
+ onChangeDevice,
+ onContentResize,
+ onRemoveDevice,
+ onResizeViewport,
+ onRotateViewport,
+ onUpdateDeviceModalOpen,
+ });
+ })
+ );
+ },
+
+});