diff options
Diffstat (limited to 'devtools/client/responsive.html/components')
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, "&"); + + 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, + }); + }) + ); + }, + +}); |