diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /devtools/client/responsivedesign | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/responsivedesign')
17 files changed, 2886 insertions, 0 deletions
diff --git a/devtools/client/responsivedesign/moz.build b/devtools/client/responsivedesign/moz.build new file mode 100644 index 000000000..215b4e8fa --- /dev/null +++ b/devtools/client/responsivedesign/moz.build @@ -0,0 +1,11 @@ +# 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/. + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] + +DevToolsModules( + 'resize-commands.js', + 'responsivedesign-child.js', + 'responsivedesign.jsm', +) diff --git a/devtools/client/responsivedesign/resize-commands.js b/devtools/client/responsivedesign/resize-commands.js new file mode 100644 index 000000000..b2f884df8 --- /dev/null +++ b/devtools/client/responsivedesign/resize-commands.js @@ -0,0 +1,96 @@ +/* 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 { Cc, Ci, Cu } = require("chrome"); + +loader.lazyImporter(this, "ResponsiveUIManager", "resource://devtools/client/responsivedesign/responsivedesign.jsm"); + +const BRAND_SHORT_NAME = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle("chrome://branding/locale/brand.properties"). + GetStringFromName("brandShortName"); + +const l10n = require("gcli/l10n"); + +exports.items = [ + { + name: "resize", + description: l10n.lookup("resizeModeDesc") + }, + { + item: "command", + runAt: "client", + name: "resize on", + description: l10n.lookup("resizeModeOnDesc"), + manual: l10n.lookupFormat("resizeModeManual2", [BRAND_SHORT_NAME]), + exec: gcli_cmd_resize + }, + { + item: "command", + runAt: "client", + name: "resize off", + description: l10n.lookup("resizeModeOffDesc"), + manual: l10n.lookupFormat("resizeModeManual2", [BRAND_SHORT_NAME]), + exec: gcli_cmd_resize + }, + { + item: "command", + runAt: "client", + name: "resize toggle", + buttonId: "command-button-responsive", + buttonClass: "command-button command-button-invertable", + tooltipText: l10n.lookup("resizeModeToggleTooltip"), + description: l10n.lookup("resizeModeToggleDesc"), + manual: l10n.lookupFormat("resizeModeManual2", [BRAND_SHORT_NAME]), + state: { + isChecked: function (aTarget) { + if (!aTarget.tab) { + return false; + } + return ResponsiveUIManager.isActiveForTab(aTarget.tab); + }, + onChange: function (aTarget, aChangeHandler) { + if (aTarget.tab) { + ResponsiveUIManager.on("on", aChangeHandler); + ResponsiveUIManager.on("off", aChangeHandler); + } + }, + offChange: function (aTarget, aChangeHandler) { + // Do not check for target.tab as it may already be null during destroy + ResponsiveUIManager.off("on", aChangeHandler); + ResponsiveUIManager.off("off", aChangeHandler); + }, + }, + exec: gcli_cmd_resize + }, + { + item: "command", + runAt: "client", + name: "resize to", + description: l10n.lookup("resizeModeToDesc"), + params: [ + { + name: "width", + type: "number", + description: l10n.lookup("resizePageArgWidthDesc"), + }, + { + name: "height", + type: "number", + description: l10n.lookup("resizePageArgHeightDesc"), + }, + ], + exec: gcli_cmd_resize + } +]; + +function* gcli_cmd_resize(args, context) { + let browserWindow = context.environment.chromeWindow; + yield ResponsiveUIManager.handleGcliCommand(browserWindow, + browserWindow.gBrowser.selectedTab, + this.name, + args); +} diff --git a/devtools/client/responsivedesign/responsivedesign-child.js b/devtools/client/responsivedesign/responsivedesign-child.js new file mode 100644 index 000000000..a6ce091e2 --- /dev/null +++ b/devtools/client/responsivedesign/responsivedesign-child.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/. */ + +"use strict"; + +/* global content, docShell, addEventListener, addMessageListener, + removeEventListener, removeMessageListener, sendAsyncMessage, Services */ + +var global = this; + +// Guard against loading this frame script mutiple times +(function () { + if (global.responsiveFrameScriptLoaded) { + return; + } + + var Ci = Components.interfaces; + const gDeviceSizeWasPageSize = docShell.deviceSizeIsPageSize; + const gFloatingScrollbarsStylesheet = Services.io.newURI("chrome://devtools/skin/floating-scrollbars-responsive-design.css", null, null); + var gRequiresFloatingScrollbars; + + var active = false; + var resizeNotifications = false; + + addMessageListener("ResponsiveMode:Start", startResponsiveMode); + addMessageListener("ResponsiveMode:Stop", stopResponsiveMode); + addMessageListener("ResponsiveMode:IsActive", isActive); + + function debug(msg) { + // dump(`RDM CHILD: ${msg}\n`); + } + + /** + * Used by tests to verify the state of responsive mode. + */ + function isActive() { + sendAsyncMessage("ResponsiveMode:IsActive:Done", { active }); + } + + function startResponsiveMode({data:data}) { + debug("START"); + if (active) { + debug("ALREADY STARTED"); + sendAsyncMessage("ResponsiveMode:Start:Done"); + return; + } + addMessageListener("ResponsiveMode:RequestScreenshot", screenshot); + let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress); + webProgress.addProgressListener(WebProgressListener, Ci.nsIWebProgress.NOTIFY_ALL); + docShell.deviceSizeIsPageSize = true; + gRequiresFloatingScrollbars = data.requiresFloatingScrollbars; + if (data.notifyOnResize) { + startOnResize(); + } + + // At this point, a content viewer might not be loaded for this + // docshell. makeScrollbarsFloating will be triggered by onLocationChange. + if (docShell.contentViewer) { + makeScrollbarsFloating(); + } + active = true; + sendAsyncMessage("ResponsiveMode:Start:Done"); + } + + function onResize() { + let { width, height } = content.screen; + debug(`EMIT RESIZE: ${width} x ${height}`); + sendAsyncMessage("ResponsiveMode:OnContentResize", { + width, + height, + }); + } + + function bindOnResize() { + content.addEventListener("resize", onResize, false); + } + + function startOnResize() { + debug("START ON RESIZE"); + if (resizeNotifications) { + return; + } + resizeNotifications = true; + bindOnResize(); + addEventListener("DOMWindowCreated", bindOnResize, false); + } + + function stopOnResize() { + debug("STOP ON RESIZE"); + if (!resizeNotifications) { + return; + } + resizeNotifications = false; + content.removeEventListener("resize", onResize, false); + removeEventListener("DOMWindowCreated", bindOnResize, false); + } + + function stopResponsiveMode() { + debug("STOP"); + if (!active) { + debug("ALREADY STOPPED, ABORT"); + return; + } + active = false; + removeMessageListener("ResponsiveMode:RequestScreenshot", screenshot); + let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress); + webProgress.removeProgressListener(WebProgressListener); + docShell.deviceSizeIsPageSize = gDeviceSizeWasPageSize; + restoreScrollbars(); + stopOnResize(); + sendAsyncMessage("ResponsiveMode:Stop:Done"); + } + + function makeScrollbarsFloating() { + if (!gRequiresFloatingScrollbars) { + return; + } + + let allDocShells = [docShell]; + + for (let i = 0; i < docShell.childCount; i++) { + let child = docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell); + allDocShells.push(child); + } + + for (let d of allDocShells) { + let win = d.contentViewer.DOMDocument.defaultView; + let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); + try { + winUtils.loadSheet(gFloatingScrollbarsStylesheet, win.AGENT_SHEET); + } catch (e) { } + } + + flushStyle(); + } + + function restoreScrollbars() { + let allDocShells = [docShell]; + for (let i = 0; i < docShell.childCount; i++) { + allDocShells.push(docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell)); + } + for (let d of allDocShells) { + let win = d.contentViewer.DOMDocument.defaultView; + let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); + try { + winUtils.removeSheet(gFloatingScrollbarsStylesheet, win.AGENT_SHEET); + } catch (e) { } + } + flushStyle(); + } + + function flushStyle() { + // Force presContext destruction + let isSticky = docShell.contentViewer.sticky; + docShell.contentViewer.sticky = false; + docShell.contentViewer.hide(); + docShell.contentViewer.show(); + docShell.contentViewer.sticky = isSticky; + } + + function screenshot() { + let canvas = content.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + let ratio = content.devicePixelRatio; + let width = content.innerWidth * ratio; + let height = content.innerHeight * ratio; + canvas.mozOpaque = true; + canvas.width = width; + canvas.height = height; + let ctx = canvas.getContext("2d"); + ctx.scale(ratio, ratio); + ctx.drawWindow(content, content.scrollX, content.scrollY, width, height, "#fff"); + sendAsyncMessage("ResponsiveMode:RequestScreenshot:Done", canvas.toDataURL()); + } + + var WebProgressListener = { + onLocationChange(webProgress, request, URI, flags) { + if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) { + return; + } + makeScrollbarsFloating(); + }, + QueryInterface: function QueryInterface(aIID) { + if (aIID.equals(Ci.nsIWebProgressListener) || + aIID.equals(Ci.nsISupportsWeakReference) || + aIID.equals(Ci.nsISupports)) { + return this; + } + throw Components.results.NS_ERROR_NO_INTERFACE; + } + }; +})(); + +global.responsiveFrameScriptLoaded = true; +sendAsyncMessage("ResponsiveMode:ChildScriptReady"); diff --git a/devtools/client/responsivedesign/responsivedesign.jsm b/devtools/client/responsivedesign/responsivedesign.jsm new file mode 100644 index 000000000..f67d1912c --- /dev/null +++ b/devtools/client/responsivedesign/responsivedesign.jsm @@ -0,0 +1,1193 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const Ci = Components.interfaces; +const Cu = Components.utils; + +var {loader, require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +var Telemetry = require("devtools/client/shared/telemetry"); +var {showDoorhanger} = require("devtools/client/shared/doorhanger"); +var {TouchEventSimulator} = require("devtools/shared/touch/simulator"); +var {Task} = require("devtools/shared/task"); +var promise = require("promise"); +var DevToolsUtils = require("devtools/shared/DevToolsUtils"); +var flags = require("devtools/shared/flags"); +var Services = require("Services"); +var EventEmitter = require("devtools/shared/event-emitter"); +var {ViewHelpers} = require("devtools/client/shared/widgets/view-helpers"); +var { LocalizationHelper } = require("devtools/shared/l10n"); +var { EmulationFront } = require("devtools/shared/fronts/emulation"); + +loader.lazyImporter(this, "SystemAppProxy", + "resource://gre/modules/SystemAppProxy.jsm"); +loader.lazyRequireGetter(this, "DebuggerClient", + "devtools/shared/client/main", true); +loader.lazyRequireGetter(this, "DebuggerServer", + "devtools/server/main", true); + +this.EXPORTED_SYMBOLS = ["ResponsiveUIManager"]; + +const MIN_WIDTH = 50; +const MIN_HEIGHT = 50; + +const MAX_WIDTH = 10000; +const MAX_HEIGHT = 10000; + +const SLOW_RATIO = 6; +const ROUND_RATIO = 10; + +const INPUT_PARSER = /(\d+)[^\d]+(\d+)/; + +const SHARED_L10N = new LocalizationHelper("devtools/client/locales/shared.properties"); + +function debug(msg) { + // dump(`RDM UI: ${msg}\n`); +} + +var ActiveTabs = new Map(); + +var Manager = { + /** + * Check if the a tab is in a responsive mode. + * Leave the responsive mode if active, + * active the responsive mode if not active. + * + * @param aWindow the main window. + * @param aTab the tab targeted. + */ + toggle: function (aWindow, aTab) { + if (this.isActiveForTab(aTab)) { + ActiveTabs.get(aTab).close(); + } else { + this.openIfNeeded(aWindow, aTab); + } + }, + + /** + * Launches the responsive mode. + * + * @param aWindow the main window. + * @param aTab the tab targeted. + * @returns {ResponsiveUI} the instance of ResponsiveUI for the current tab. + */ + openIfNeeded: Task.async(function* (aWindow, aTab) { + let ui; + if (!this.isActiveForTab(aTab)) { + ui = new ResponsiveUI(aWindow, aTab); + yield ui.inited; + } else { + ui = this.getResponsiveUIForTab(aTab); + } + return ui; + }), + + /** + * Returns true if responsive view is active for the provided tab. + * + * @param aTab the tab targeted. + */ + isActiveForTab: function (aTab) { + return ActiveTabs.has(aTab); + }, + + /** + * Return the responsive UI controller for a tab. + */ + getResponsiveUIForTab: function (aTab) { + return ActiveTabs.get(aTab); + }, + + /** + * Handle gcli commands. + * + * @param aWindow the browser window. + * @param aTab the tab targeted. + * @param aCommand the command name. + * @param aArgs command arguments. + */ + handleGcliCommand: Task.async(function* (aWindow, aTab, aCommand, aArgs) { + switch (aCommand) { + case "resize to": + let ui = yield this.openIfNeeded(aWindow, aTab); + ui.setViewportSize(aArgs); + break; + case "resize on": + this.openIfNeeded(aWindow, aTab); + break; + case "resize off": + if (this.isActiveForTab(aTab)) { + yield ActiveTabs.get(aTab).close(); + } + break; + case "resize toggle": + this.toggle(aWindow, aTab); + default: + } + }) +}; + +EventEmitter.decorate(Manager); + +// If the new HTML RDM UI is enabled and e10s is enabled by default (e10s is required for +// the new HTML RDM UI to function), delegate the ResponsiveUIManager API over to that +// tool instead. Performing this delegation here allows us to contain the pref check to a +// single place. +if (Services.prefs.getBoolPref("devtools.responsive.html.enabled") && + Services.appinfo.browserTabsRemoteAutostart) { + let { ResponsiveUIManager } = + require("devtools/client/responsive.html/manager"); + this.ResponsiveUIManager = ResponsiveUIManager; +} else { + this.ResponsiveUIManager = Manager; +} + +var defaultPresets = [ + // Phones + {key: "320x480", width: 320, height: 480}, // iPhone, B2G, with <meta viewport> + {key: "360x640", width: 360, height: 640}, // Android 4, phones, with <meta viewport> + + // Tablets + {key: "768x1024", width: 768, height: 1024}, // iPad, with <meta viewport> + {key: "800x1280", width: 800, height: 1280}, // Android 4, Tablet, with <meta viewport> + + // Default width for mobile browsers, no <meta viewport> + {key: "980x1280", width: 980, height: 1280}, + + // Computer + {key: "1280x600", width: 1280, height: 600}, + {key: "1920x900", width: 1920, height: 900}, +]; + +function ResponsiveUI(aWindow, aTab) +{ + this.mainWindow = aWindow; + this.tab = aTab; + this.mm = this.tab.linkedBrowser.messageManager; + this.tabContainer = aWindow.gBrowser.tabContainer; + this.browser = aTab.linkedBrowser; + this.chromeDoc = aWindow.document; + this.container = aWindow.gBrowser.getBrowserContainer(this.browser); + this.stack = this.container.querySelector(".browserStack"); + this._telemetry = new Telemetry(); + + // Let's bind some callbacks. + this.bound_presetSelected = this.presetSelected.bind(this); + this.bound_handleManualInput = this.handleManualInput.bind(this); + this.bound_addPreset = this.addPreset.bind(this); + this.bound_removePreset = this.removePreset.bind(this); + this.bound_rotate = this.rotate.bind(this); + this.bound_screenshot = () => this.screenshot(); + this.bound_touch = this.toggleTouch.bind(this); + this.bound_close = this.close.bind(this); + this.bound_startResizing = this.startResizing.bind(this); + this.bound_stopResizing = this.stopResizing.bind(this); + this.bound_onDrag = this.onDrag.bind(this); + this.bound_changeUA = this.changeUA.bind(this); + this.bound_onContentResize = this.onContentResize.bind(this); + + this.mm.addMessageListener("ResponsiveMode:OnContentResize", + this.bound_onContentResize); + + // We must be ready to handle window or tab close now that we have saved + // ourselves in ActiveTabs. Otherwise we risk leaking the window. + this.mainWindow.addEventListener("unload", this); + this.tab.addEventListener("TabClose", this); + this.tabContainer.addEventListener("TabSelect", this); + + ActiveTabs.set(this.tab, this); + + this.inited = this.init(); +} + +ResponsiveUI.prototype = { + _transitionsEnabled: true, + get transitionsEnabled() { + return this._transitionsEnabled; + }, + set transitionsEnabled(aValue) { + this._transitionsEnabled = aValue; + if (aValue && !this._resizing && this.stack.hasAttribute("responsivemode")) { + this.stack.removeAttribute("notransition"); + } else if (!aValue) { + this.stack.setAttribute("notransition", "true"); + } + }, + + init: Task.async(function* () { + debug("INIT BEGINS"); + let ready = this.waitForMessage("ResponsiveMode:ChildScriptReady"); + this.mm.loadFrameScript("resource://devtools/client/responsivedesign/responsivedesign-child.js", true); + yield ready; + + let requiresFloatingScrollbars = + !this.mainWindow.matchMedia("(-moz-overlay-scrollbars)").matches; + let started = this.waitForMessage("ResponsiveMode:Start:Done"); + debug("SEND START"); + this.mm.sendAsyncMessage("ResponsiveMode:Start", { + requiresFloatingScrollbars, + // Tests expect events on resize to yield on various size changes + notifyOnResize: flags.testing, + }); + yield started; + + // Load Presets + this.loadPresets(); + + // Setup the UI + this.container.setAttribute("responsivemode", "true"); + this.stack.setAttribute("responsivemode", "true"); + this.buildUI(); + this.checkMenus(); + + // Rotate the responsive mode if needed + try { + if (Services.prefs.getBoolPref("devtools.responsiveUI.rotate")) { + this.rotate(); + } + } catch (e) {} + + // Touch events support + this.touchEnableBefore = false; + this.touchEventSimulator = new TouchEventSimulator(this.browser); + + yield this.connectToServer(); + this.userAgentInput.hidden = false; + + // Hook to display promotional Developer Edition doorhanger. + // Only displayed once. + showDoorhanger({ + window: this.mainWindow, + type: "deveditionpromo", + anchor: this.chromeDoc.querySelector("#content") + }); + + // Notify that responsive mode is on. + this._telemetry.toolOpened("responsive"); + ResponsiveUIManager.emit("on", { tab: this.tab }); + }), + + connectToServer: Task.async(function* () { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + this.client = new DebuggerClient(DebuggerServer.connectPipe()); + yield this.client.connect(); + let {tab} = yield this.client.getTab(); + yield this.client.attachTab(tab.actor); + this.emulationFront = EmulationFront(this.client, tab); + }), + + loadPresets: function () { + // Try to load presets from prefs + let presets = defaultPresets; + if (Services.prefs.prefHasUserValue("devtools.responsiveUI.presets")) { + try { + presets = JSON.parse(Services.prefs.getCharPref("devtools.responsiveUI.presets")); + } catch (e) { + // User pref is malformated. + console.error("Could not parse pref `devtools.responsiveUI.presets`: " + e); + } + } + + this.customPreset = {key: "custom", custom: true}; + + if (Array.isArray(presets)) { + this.presets = [this.customPreset].concat(presets); + } else { + console.error("Presets value (devtools.responsiveUI.presets) is malformated."); + this.presets = [this.customPreset]; + } + + try { + let width = Services.prefs.getIntPref("devtools.responsiveUI.customWidth"); + let height = Services.prefs.getIntPref("devtools.responsiveUI.customHeight"); + this.customPreset.width = Math.min(MAX_WIDTH, width); + this.customPreset.height = Math.min(MAX_HEIGHT, height); + + this.currentPresetKey = Services.prefs.getCharPref("devtools.responsiveUI.currentPreset"); + } catch (e) { + // Default size. The first preset (custom) is the one that will be used. + let bbox = this.stack.getBoundingClientRect(); + + this.customPreset.width = bbox.width - 40; // horizontal padding of the container + this.customPreset.height = bbox.height - 80; // vertical padding + toolbar height + + this.currentPresetKey = this.presets[1].key; // most common preset + } + }, + + /** + * Destroy the nodes. Remove listeners. Reset the style. + */ + close: Task.async(function* () { + debug("CLOSE BEGINS"); + if (this.closing) { + debug("ALREADY CLOSING, ABORT"); + return; + } + this.closing = true; + + // If we're closing very fast (in tests), ensure init has finished. + debug("CLOSE: WAIT ON INITED"); + yield this.inited; + debug("CLOSE: INITED DONE"); + + this.unCheckMenus(); + // Reset style of the stack. + debug(`CURRENT SIZE: ${this.stack.getAttribute("style")}`); + let style = "max-width: none;" + + "min-width: 0;" + + "max-height: none;" + + "min-height: 0;"; + debug("RESET STACK SIZE"); + this.stack.setAttribute("style", style); + + // Wait for resize message before stopping in the child when testing, + // but only if we should expect to still get a message. + if (flags.testing && this.tab.linkedBrowser.messageManager) { + yield this.waitForMessage("ResponsiveMode:OnContentResize"); + } + + if (this.isResizing) + this.stopResizing(); + + // Remove listeners. + this.menulist.removeEventListener("select", this.bound_presetSelected, true); + this.menulist.removeEventListener("change", this.bound_handleManualInput, true); + this.mainWindow.removeEventListener("unload", this); + this.tab.removeEventListener("TabClose", this); + this.tabContainer.removeEventListener("TabSelect", this); + this.rotatebutton.removeEventListener("command", this.bound_rotate, true); + this.screenshotbutton.removeEventListener("command", this.bound_screenshot, true); + this.closebutton.removeEventListener("command", this.bound_close, true); + this.addbutton.removeEventListener("command", this.bound_addPreset, true); + this.removebutton.removeEventListener("command", this.bound_removePreset, true); + this.touchbutton.removeEventListener("command", this.bound_touch, true); + this.userAgentInput.removeEventListener("blur", this.bound_changeUA, true); + + // Removed elements. + this.container.removeChild(this.toolbar); + if (this.bottomToolbar) { + this.bottomToolbar.remove(); + delete this.bottomToolbar; + } + this.stack.removeChild(this.resizer); + this.stack.removeChild(this.resizeBarV); + this.stack.removeChild(this.resizeBarH); + + this.stack.classList.remove("fxos-mode"); + + // Unset the responsive mode. + this.container.removeAttribute("responsivemode"); + this.stack.removeAttribute("responsivemode"); + + ActiveTabs.delete(this.tab); + if (this.touchEventSimulator) { + this.touchEventSimulator.stop(); + } + + yield this.client.close(); + this.client = this.emulationFront = null; + + this._telemetry.toolClosed("responsive"); + + if (this.tab.linkedBrowser.messageManager) { + let stopped = this.waitForMessage("ResponsiveMode:Stop:Done"); + this.tab.linkedBrowser.messageManager.sendAsyncMessage("ResponsiveMode:Stop"); + yield stopped; + } + + this.inited = null; + ResponsiveUIManager.emit("off", { tab: this.tab }); + }), + + waitForMessage(message) { + return new Promise(resolve => { + let listener = () => { + this.mm.removeMessageListener(message, listener); + resolve(); + }; + this.mm.addMessageListener(message, listener); + }); + }, + + /** + * Emit an event when the content has been resized. Only used in tests. + */ + onContentResize: function (msg) { + ResponsiveUIManager.emit("content-resize", { + tab: this.tab, + width: msg.data.width, + height: msg.data.height, + }); + }, + + /** + * Handle events + */ + handleEvent: function (aEvent) { + switch (aEvent.type) { + case "TabClose": + case "unload": + this.close(); + break; + case "TabSelect": + if (this.tab.selected) { + this.checkMenus(); + } else if (!this.mainWindow.gBrowser.selectedTab.responsiveUI) { + this.unCheckMenus(); + } + break; + } + }, + + getViewportBrowser() { + return this.browser; + }, + + /** + * Check the menu items. + */ + checkMenus: function RUI_checkMenus() { + this.chromeDoc.getElementById("menu_responsiveUI").setAttribute("checked", "true"); + }, + + /** + * Uncheck the menu items. + */ + unCheckMenus: function RUI_unCheckMenus() { + let el = this.chromeDoc.getElementById("menu_responsiveUI"); + if (el) { + el.setAttribute("checked", "false"); + } + }, + + /** + * Build the toolbar and the resizers. + * + * <vbox class="browserContainer"> From tabbrowser.xml + * <toolbar class="devtools-responsiveui-toolbar"> + * <menulist class="devtools-responsiveui-menulist"/> // presets + * <toolbarbutton tabindex="0" class="devtools-responsiveui-toolbarbutton" tooltiptext="rotate"/> // rotate + * <toolbarbutton tabindex="0" class="devtools-responsiveui-toolbarbutton" tooltiptext="screenshot"/> // screenshot + * <toolbarbutton tabindex="0" class="devtools-responsiveui-toolbarbutton" tooltiptext="Leave Responsive Design Mode"/> // close + * </toolbar> + * <stack class="browserStack"> From tabbrowser.xml + * <browser/> + * <box class="devtools-responsiveui-resizehandle" bottom="0" right="0"/> + * <box class="devtools-responsiveui-resizebarV" top="0" right="0"/> + * <box class="devtools-responsiveui-resizebarH" bottom="0" left="0"/> + * // Additional button in FxOS mode: + * <button class="devtools-responsiveui-sleep-button" /> + * <vbox class="devtools-responsiveui-volume-buttons"> + * <button class="devtools-responsiveui-volume-up-button" /> + * <button class="devtools-responsiveui-volume-down-button" /> + * </vbox> + * </stack> + * <toolbar class="devtools-responsiveui-hardware-button"> + * <toolbarbutton class="devtools-responsiveui-home-button" /> + * </toolbar> + * </vbox> + */ + buildUI: function RUI_buildUI() { + // Toolbar + this.toolbar = this.chromeDoc.createElement("toolbar"); + this.toolbar.className = "devtools-responsiveui-toolbar"; + this.toolbar.setAttribute("fullscreentoolbar", "true"); + + this.menulist = this.chromeDoc.createElement("menulist"); + this.menulist.className = "devtools-responsiveui-menulist"; + this.menulist.setAttribute("editable", "true"); + + this.menulist.addEventListener("select", this.bound_presetSelected, true); + this.menulist.addEventListener("change", this.bound_handleManualInput, true); + + this.menuitems = new Map(); + + let menupopup = this.chromeDoc.createElement("menupopup"); + this.registerPresets(menupopup); + this.menulist.appendChild(menupopup); + + this.addbutton = this.chromeDoc.createElement("menuitem"); + this.addbutton.setAttribute("label", this.strings.GetStringFromName("responsiveUI.addPreset")); + this.addbutton.addEventListener("command", this.bound_addPreset, true); + + this.removebutton = this.chromeDoc.createElement("menuitem"); + this.removebutton.setAttribute("label", this.strings.GetStringFromName("responsiveUI.removePreset")); + this.removebutton.addEventListener("command", this.bound_removePreset, true); + + menupopup.appendChild(this.chromeDoc.createElement("menuseparator")); + menupopup.appendChild(this.addbutton); + menupopup.appendChild(this.removebutton); + + this.rotatebutton = this.chromeDoc.createElement("toolbarbutton"); + this.rotatebutton.setAttribute("tabindex", "0"); + this.rotatebutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.rotate2")); + this.rotatebutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-rotate"; + this.rotatebutton.addEventListener("command", this.bound_rotate, true); + + this.screenshotbutton = this.chromeDoc.createElement("toolbarbutton"); + this.screenshotbutton.setAttribute("tabindex", "0"); + this.screenshotbutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.screenshot")); + this.screenshotbutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-screenshot"; + this.screenshotbutton.addEventListener("command", this.bound_screenshot, true); + + this.closebutton = this.chromeDoc.createElement("toolbarbutton"); + this.closebutton.setAttribute("tabindex", "0"); + this.closebutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-close"; + this.closebutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.close1")); + this.closebutton.addEventListener("command", this.bound_close, true); + + this.toolbar.appendChild(this.closebutton); + this.toolbar.appendChild(this.menulist); + this.toolbar.appendChild(this.rotatebutton); + + this.touchbutton = this.chromeDoc.createElement("toolbarbutton"); + this.touchbutton.setAttribute("tabindex", "0"); + this.touchbutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.touch")); + this.touchbutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-touch"; + this.touchbutton.addEventListener("command", this.bound_touch, true); + this.toolbar.appendChild(this.touchbutton); + + this.toolbar.appendChild(this.screenshotbutton); + + this.userAgentInput = this.chromeDoc.createElement("textbox"); + this.userAgentInput.className = "devtools-responsiveui-textinput"; + this.userAgentInput.setAttribute("placeholder", + this.strings.GetStringFromName("responsiveUI.userAgentPlaceholder")); + this.userAgentInput.addEventListener("blur", this.bound_changeUA, true); + this.userAgentInput.hidden = true; + this.toolbar.appendChild(this.userAgentInput); + + // Resizers + let resizerTooltip = this.strings.GetStringFromName("responsiveUI.resizerTooltip"); + this.resizer = this.chromeDoc.createElement("box"); + this.resizer.className = "devtools-responsiveui-resizehandle"; + this.resizer.setAttribute("right", "0"); + this.resizer.setAttribute("bottom", "0"); + this.resizer.setAttribute("tooltiptext", resizerTooltip); + this.resizer.onmousedown = this.bound_startResizing; + + this.resizeBarV = this.chromeDoc.createElement("box"); + this.resizeBarV.className = "devtools-responsiveui-resizebarV"; + this.resizeBarV.setAttribute("top", "0"); + this.resizeBarV.setAttribute("right", "0"); + this.resizeBarV.setAttribute("tooltiptext", resizerTooltip); + this.resizeBarV.onmousedown = this.bound_startResizing; + + this.resizeBarH = this.chromeDoc.createElement("box"); + this.resizeBarH.className = "devtools-responsiveui-resizebarH"; + this.resizeBarH.setAttribute("bottom", "0"); + this.resizeBarH.setAttribute("left", "0"); + this.resizeBarH.setAttribute("tooltiptext", resizerTooltip); + this.resizeBarH.onmousedown = this.bound_startResizing; + + this.container.insertBefore(this.toolbar, this.stack); + this.stack.appendChild(this.resizer); + this.stack.appendChild(this.resizeBarV); + this.stack.appendChild(this.resizeBarH); + }, + + // FxOS custom controls + buildPhoneUI: function () { + this.stack.classList.add("fxos-mode"); + + let sleepButton = this.chromeDoc.createElement("button"); + sleepButton.className = "devtools-responsiveui-sleep-button"; + sleepButton.setAttribute("top", 0); + sleepButton.setAttribute("right", 0); + sleepButton.addEventListener("mousedown", () => { + SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "Power"}); + }); + sleepButton.addEventListener("mouseup", () => { + SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "Power"}); + }); + this.stack.appendChild(sleepButton); + + let volumeButtons = this.chromeDoc.createElement("vbox"); + volumeButtons.className = "devtools-responsiveui-volume-buttons"; + volumeButtons.setAttribute("top", 0); + volumeButtons.setAttribute("left", 0); + + let volumeUp = this.chromeDoc.createElement("button"); + volumeUp.className = "devtools-responsiveui-volume-up-button"; + volumeUp.addEventListener("mousedown", () => { + SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "AudioVolumeUp"}); + }); + volumeUp.addEventListener("mouseup", () => { + SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "AudioVolumeUp"}); + }); + + let volumeDown = this.chromeDoc.createElement("button"); + volumeDown.className = "devtools-responsiveui-volume-down-button"; + volumeDown.addEventListener("mousedown", () => { + SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "AudioVolumeDown"}); + }); + volumeDown.addEventListener("mouseup", () => { + SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "AudioVolumeDown"}); + }); + + volumeButtons.appendChild(volumeUp); + volumeButtons.appendChild(volumeDown); + this.stack.appendChild(volumeButtons); + + let bottomToolbar = this.chromeDoc.createElement("toolbar"); + bottomToolbar.className = "devtools-responsiveui-hardware-buttons"; + bottomToolbar.setAttribute("align", "center"); + bottomToolbar.setAttribute("pack", "center"); + + let homeButton = this.chromeDoc.createElement("toolbarbutton"); + homeButton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-home-button"; + homeButton.addEventListener("mousedown", () => { + SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "Home"}); + }); + homeButton.addEventListener("mouseup", () => { + SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "Home"}); + }); + bottomToolbar.appendChild(homeButton); + this.bottomToolbar = bottomToolbar; + this.container.appendChild(bottomToolbar); + }, + + /** + * Validate and apply any user input on the editable menulist + */ + handleManualInput: function RUI_handleManualInput() { + let userInput = this.menulist.inputField.value; + let value = INPUT_PARSER.exec(userInput); + let selectedPreset = this.menuitems.get(this.selectedItem); + + // In case of an invalide value, we show back the last preset + if (!value || value.length < 3) { + this.setMenuLabel(this.selectedItem, selectedPreset); + return; + } + + this.rotateValue = false; + + if (!selectedPreset.custom) { + let menuitem = this.customMenuitem; + this.currentPresetKey = this.customPreset.key; + this.menulist.selectedItem = menuitem; + } + + let w = this.customPreset.width = parseInt(value[1], 10); + let h = this.customPreset.height = parseInt(value[2], 10); + + this.saveCustomSize(); + this.setViewportSize({ + width: w, + height: h, + }); + }, + + /** + * Build the presets list and append it to the menupopup. + * + * @param aParent menupopup. + */ + registerPresets: function RUI_registerPresets(aParent) { + let fragment = this.chromeDoc.createDocumentFragment(); + let doc = this.chromeDoc; + + for (let preset of this.presets) { + let menuitem = doc.createElement("menuitem"); + menuitem.setAttribute("ispreset", true); + this.menuitems.set(menuitem, preset); + + if (preset.key === this.currentPresetKey) { + menuitem.setAttribute("selected", "true"); + this.selectedItem = menuitem; + } + + if (preset.custom) { + this.customMenuitem = menuitem; + } + + this.setMenuLabel(menuitem, preset); + fragment.appendChild(menuitem); + } + aParent.appendChild(fragment); + }, + + /** + * Set the menuitem label of a preset. + * + * @param aMenuitem menuitem to edit. + * @param aPreset associated preset. + */ + setMenuLabel: function RUI_setMenuLabel(aMenuitem, aPreset) { + let size = SHARED_L10N.getFormatStr("dimensions", + Math.round(aPreset.width), Math.round(aPreset.height)); + + // .inputField might be not reachable yet (async XBL loading) + if (this.menulist.inputField) { + this.menulist.inputField.value = size; + } + + if (aPreset.custom) { + size = this.strings.formatStringFromName("responsiveUI.customResolution", [size], 1); + } else if (aPreset.name != null && aPreset.name !== "") { + size = this.strings.formatStringFromName("responsiveUI.namedResolution", [size, aPreset.name], 2); + } + + aMenuitem.setAttribute("label", size); + }, + + /** + * When a preset is selected, apply it. + */ + presetSelected: function RUI_presetSelected() { + if (this.menulist.selectedItem.getAttribute("ispreset") === "true") { + this.selectedItem = this.menulist.selectedItem; + + this.rotateValue = false; + let selectedPreset = this.menuitems.get(this.selectedItem); + this.loadPreset(selectedPreset); + this.currentPresetKey = selectedPreset.key; + this.saveCurrentPreset(); + + // Update the buttons hidden status according to the new selected preset + if (selectedPreset == this.customPreset) { + this.addbutton.hidden = false; + this.removebutton.hidden = true; + } else { + this.addbutton.hidden = true; + this.removebutton.hidden = false; + } + } + }, + + /** + * Apply a preset. + */ + loadPreset(preset) { + this.setViewportSize(preset); + }, + + /** + * Add a preset to the list and the memory + */ + addPreset: function RUI_addPreset() { + let w = this.customPreset.width; + let h = this.customPreset.height; + let newName = {}; + + let title = this.strings.GetStringFromName("responsiveUI.customNamePromptTitle1"); + let message = this.strings.formatStringFromName("responsiveUI.customNamePromptMsg", [w, h], 2); + let promptOk = Services.prompt.prompt(null, title, message, newName, null, {}); + + if (!promptOk) { + // Prompt has been cancelled + this.menulist.selectedItem = this.selectedItem; + return; + } + + let newPreset = { + key: w + "x" + h, + name: newName.value, + width: w, + height: h + }; + + this.presets.push(newPreset); + + // Sort the presets according to width/height ascending order + this.presets.sort(function RUI_sortPresets(aPresetA, aPresetB) { + // We keep custom preset at first + if (aPresetA.custom && !aPresetB.custom) { + return 1; + } + if (!aPresetA.custom && aPresetB.custom) { + return -1; + } + + if (aPresetA.width === aPresetB.width) { + if (aPresetA.height === aPresetB.height) { + return 0; + } else { + return aPresetA.height > aPresetB.height; + } + } else { + return aPresetA.width > aPresetB.width; + } + }); + + this.savePresets(); + + let newMenuitem = this.chromeDoc.createElement("menuitem"); + newMenuitem.setAttribute("ispreset", true); + this.setMenuLabel(newMenuitem, newPreset); + + this.menuitems.set(newMenuitem, newPreset); + let idx = this.presets.indexOf(newPreset); + let beforeMenuitem = this.menulist.firstChild.childNodes[idx + 1]; + this.menulist.firstChild.insertBefore(newMenuitem, beforeMenuitem); + + this.menulist.selectedItem = newMenuitem; + this.currentPresetKey = newPreset.key; + this.saveCurrentPreset(); + }, + + /** + * remove a preset from the list and the memory + */ + removePreset: function RUI_removePreset() { + let selectedPreset = this.menuitems.get(this.selectedItem); + let w = selectedPreset.width; + let h = selectedPreset.height; + + this.presets.splice(this.presets.indexOf(selectedPreset), 1); + this.menulist.firstChild.removeChild(this.selectedItem); + this.menuitems.delete(this.selectedItem); + + this.customPreset.width = w; + this.customPreset.height = h; + let menuitem = this.customMenuitem; + this.setMenuLabel(menuitem, this.customPreset); + this.menulist.selectedItem = menuitem; + this.currentPresetKey = this.customPreset.key; + + this.setViewportSize({ + width: w, + height: h, + }); + + this.savePresets(); + }, + + /** + * Swap width and height. + */ + rotate: function RUI_rotate() { + let selectedPreset = this.menuitems.get(this.selectedItem); + let width = this.rotateValue ? selectedPreset.height : selectedPreset.width; + let height = this.rotateValue ? selectedPreset.width : selectedPreset.height; + + this.setViewportSize({ + width: height, + height: width, + }); + + if (selectedPreset.custom) { + this.saveCustomSize(); + } else { + this.rotateValue = !this.rotateValue; + this.saveCurrentPreset(); + } + }, + + /** + * Take a screenshot of the page. + * + * @param aFileName name of the screenshot file (used for tests). + */ + screenshot: function RUI_screenshot(aFileName) { + let filename = aFileName; + if (!filename) { + let date = new Date(); + let month = ("0" + (date.getMonth() + 1)).substr(-2, 2); + let day = ("0" + date.getDate()).substr(-2, 2); + let dateString = [date.getFullYear(), month, day].join("-"); + let timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0]; + filename = this.strings.formatStringFromName("responsiveUI.screenshotGeneratedFilename", [dateString, timeString], 2); + } + let mm = this.tab.linkedBrowser.messageManager; + let chromeWindow = this.chromeDoc.defaultView; + let doc = chromeWindow.document; + function onScreenshot(aMessage) { + mm.removeMessageListener("ResponsiveMode:RequestScreenshot:Done", onScreenshot); + chromeWindow.saveURL(aMessage.data, filename + ".png", null, true, true, doc.documentURIObject, doc); + } + mm.addMessageListener("ResponsiveMode:RequestScreenshot:Done", onScreenshot); + mm.sendAsyncMessage("ResponsiveMode:RequestScreenshot"); + }, + + /** + * Enable/Disable mouse -> touch events translation. + */ + enableTouch: function RUI_enableTouch() { + this.touchbutton.setAttribute("checked", "true"); + return this.touchEventSimulator.start(); + }, + + disableTouch: function RUI_disableTouch() { + this.touchbutton.removeAttribute("checked"); + return this.touchEventSimulator.stop(); + }, + + hideTouchNotification: function RUI_hideTouchNotification() { + let nbox = this.mainWindow.gBrowser.getNotificationBox(this.browser); + let n = nbox.getNotificationWithValue("responsive-ui-need-reload"); + if (n) { + n.close(); + } + }, + + toggleTouch: Task.async(function* () { + this.hideTouchNotification(); + if (this.touchEventSimulator.enabled) { + this.disableTouch(); + } else { + let isReloadNeeded = yield this.enableTouch(); + if (isReloadNeeded) { + if (Services.prefs.getBoolPref("devtools.responsiveUI.no-reload-notification")) { + return; + } + + let nbox = this.mainWindow.gBrowser.getNotificationBox(this.browser); + + var buttons = [{ + label: this.strings.GetStringFromName("responsiveUI.notificationReload"), + callback: () => { + this.browser.reload(); + }, + accessKey: this.strings.GetStringFromName("responsiveUI.notificationReload_accesskey"), + }, { + label: this.strings.GetStringFromName("responsiveUI.dontShowReloadNotification"), + callback: function () { + Services.prefs.setBoolPref("devtools.responsiveUI.no-reload-notification", true); + }, + accessKey: this.strings.GetStringFromName("responsiveUI.dontShowReloadNotification_accesskey"), + }]; + + nbox.appendNotification( + this.strings.GetStringFromName("responsiveUI.needReload"), + "responsive-ui-need-reload", + null, + nbox.PRIORITY_INFO_LOW, + buttons); + } + } + }), + + waitForReload() { + let navigatedDeferred = promise.defer(); + let onNavigated = (_, { state }) => { + if (state != "stop") { + return; + } + this.client.removeListener("tabNavigated", onNavigated); + navigatedDeferred.resolve(); + }; + this.client.addListener("tabNavigated", onNavigated); + return navigatedDeferred.promise; + }, + + /** + * Change the user agent string + */ + changeUA: Task.async(function* () { + let value = this.userAgentInput.value; + let changed; + if (value) { + changed = yield this.emulationFront.setUserAgentOverride(value); + this.userAgentInput.setAttribute("attention", "true"); + } else { + changed = yield this.emulationFront.clearUserAgentOverride(); + this.userAgentInput.removeAttribute("attention"); + } + if (changed) { + let reloaded = this.waitForReload(); + this.tab.linkedBrowser.reload(); + yield reloaded; + } + ResponsiveUIManager.emit("userAgentChanged", { tab: this.tab }); + }), + + /** + * Get the current width and height. + */ + getSize() { + let width = Number(this.stack.style.minWidth.replace("px", "")); + let height = Number(this.stack.style.minHeight.replace("px", "")); + return { + width, + height, + }; + }, + + /** + * Change the size of the viewport. + */ + setViewportSize({ width, height }) { + debug(`SET SIZE TO ${width} x ${height}`); + if (width) { + this.setWidth(width); + } + if (height) { + this.setHeight(height); + } + }, + + setWidth: function RUI_setWidth(aWidth) { + aWidth = Math.min(Math.max(aWidth, MIN_WIDTH), MAX_WIDTH); + this.stack.style.maxWidth = this.stack.style.minWidth = aWidth + "px"; + + if (!this.ignoreX) + this.resizeBarH.setAttribute("left", Math.round(aWidth / 2)); + + let selectedPreset = this.menuitems.get(this.selectedItem); + + if (selectedPreset.custom) { + selectedPreset.width = aWidth; + this.setMenuLabel(this.selectedItem, selectedPreset); + } + }, + + setHeight: function RUI_setHeight(aHeight) { + aHeight = Math.min(Math.max(aHeight, MIN_HEIGHT), MAX_HEIGHT); + this.stack.style.maxHeight = this.stack.style.minHeight = aHeight + "px"; + + if (!this.ignoreY) + this.resizeBarV.setAttribute("top", Math.round(aHeight / 2)); + + let selectedPreset = this.menuitems.get(this.selectedItem); + if (selectedPreset.custom) { + selectedPreset.height = aHeight; + this.setMenuLabel(this.selectedItem, selectedPreset); + } + }, + /** + * Start the process of resizing the browser. + * + * @param aEvent + */ + startResizing: function RUI_startResizing(aEvent) { + let selectedPreset = this.menuitems.get(this.selectedItem); + + if (!selectedPreset.custom) { + this.customPreset.width = this.rotateValue ? selectedPreset.height : selectedPreset.width; + this.customPreset.height = this.rotateValue ? selectedPreset.width : selectedPreset.height; + + let menuitem = this.customMenuitem; + this.setMenuLabel(menuitem, this.customPreset); + + this.currentPresetKey = this.customPreset.key; + this.menulist.selectedItem = menuitem; + } + this.mainWindow.addEventListener("mouseup", this.bound_stopResizing, true); + this.mainWindow.addEventListener("mousemove", this.bound_onDrag, true); + this.container.style.pointerEvents = "none"; + + this._resizing = true; + this.stack.setAttribute("notransition", "true"); + + this.lastScreenX = aEvent.screenX; + this.lastScreenY = aEvent.screenY; + + this.ignoreY = (aEvent.target === this.resizeBarV); + this.ignoreX = (aEvent.target === this.resizeBarH); + + this.isResizing = true; + }, + + /** + * Resizing on mouse move. + * + * @param aEvent + */ + onDrag: function RUI_onDrag(aEvent) { + let shift = aEvent.shiftKey; + let ctrl = !aEvent.shiftKey && aEvent.ctrlKey; + + let screenX = aEvent.screenX; + let screenY = aEvent.screenY; + + let deltaX = screenX - this.lastScreenX; + let deltaY = screenY - this.lastScreenY; + + if (this.ignoreY) + deltaY = 0; + if (this.ignoreX) + deltaX = 0; + + if (ctrl) { + deltaX /= SLOW_RATIO; + deltaY /= SLOW_RATIO; + } + + let width = this.customPreset.width + deltaX; + let height = this.customPreset.height + deltaY; + + if (shift) { + let roundedWidth, roundedHeight; + roundedWidth = 10 * Math.floor(width / ROUND_RATIO); + roundedHeight = 10 * Math.floor(height / ROUND_RATIO); + screenX += roundedWidth - width; + screenY += roundedHeight - height; + width = roundedWidth; + height = roundedHeight; + } + + if (width < MIN_WIDTH) { + width = MIN_WIDTH; + } else { + this.lastScreenX = screenX; + } + + if (height < MIN_HEIGHT) { + height = MIN_HEIGHT; + } else { + this.lastScreenY = screenY; + } + + this.setViewportSize({ width, height }); + }, + + /** + * Stop End resizing + */ + stopResizing: function RUI_stopResizing() { + this.container.style.pointerEvents = "auto"; + + this.mainWindow.removeEventListener("mouseup", this.bound_stopResizing, true); + this.mainWindow.removeEventListener("mousemove", this.bound_onDrag, true); + + this.saveCustomSize(); + + delete this._resizing; + if (this.transitionsEnabled) { + this.stack.removeAttribute("notransition"); + } + this.ignoreY = false; + this.ignoreX = false; + this.isResizing = false; + }, + + /** + * Store the custom size as a pref. + */ + saveCustomSize: function RUI_saveCustomSize() { + Services.prefs.setIntPref("devtools.responsiveUI.customWidth", this.customPreset.width); + Services.prefs.setIntPref("devtools.responsiveUI.customHeight", this.customPreset.height); + }, + + /** + * Store the current preset as a pref. + */ + saveCurrentPreset: function RUI_saveCurrentPreset() { + Services.prefs.setCharPref("devtools.responsiveUI.currentPreset", this.currentPresetKey); + Services.prefs.setBoolPref("devtools.responsiveUI.rotate", this.rotateValue); + }, + + /** + * Store the list of all registered presets as a pref. + */ + savePresets: function RUI_savePresets() { + // We exclude the custom one + let registeredPresets = this.presets.filter(function (aPreset) { + return !aPreset.custom; + }); + + Services.prefs.setCharPref("devtools.responsiveUI.presets", JSON.stringify(registeredPresets)); + }, +}; + +loader.lazyGetter(ResponsiveUI.prototype, "strings", function () { + return Services.strings.createBundle("chrome://devtools/locale/responsiveUI.properties"); +}); diff --git a/devtools/client/responsivedesign/test/.eslintrc.js b/devtools/client/responsivedesign/test/.eslintrc.js new file mode 100644 index 000000000..ba1263286 --- /dev/null +++ b/devtools/client/responsivedesign/test/.eslintrc.js @@ -0,0 +1,10 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../.eslintrc.mochitests.js", + "globals": { + "ResponsiveUI": true, + "helpers": true + } +}; diff --git a/devtools/client/responsivedesign/test/browser.ini b/devtools/client/responsivedesign/test/browser.ini new file mode 100644 index 000000000..6a8f5a8d9 --- /dev/null +++ b/devtools/client/responsivedesign/test/browser.ini @@ -0,0 +1,21 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + head.js + touch.html + !/devtools/client/commandline/test/helpers.js + !/devtools/client/framework/test/shared-head.js + +[browser_responsive_cmd.js] +[browser_responsivecomputedview.js] +skip-if = e10s && debug # Bug 1252201 - Docshell leak on debug e10s +[browser_responsiveruleview.js] +skip-if = e10s && debug # Bug 1252201 - Docshell leak on debug e10s +[browser_responsiveui.js] +[browser_responsiveui_touch.js] +[browser_responsiveuiaddcustompreset.js] +[browser_responsive_devicewidth.js] +[browser_responsiveui_customuseragent.js] +[browser_responsiveui_window_close.js] +skip-if = (os == 'linux') && e10s && debug # Bug 1277274 diff --git a/devtools/client/responsivedesign/test/browser_responsive_cmd.js b/devtools/client/responsivedesign/test/browser_responsive_cmd.js new file mode 100644 index 000000000..8c8e798d0 --- /dev/null +++ b/devtools/client/responsivedesign/test/browser_responsive_cmd.js @@ -0,0 +1,143 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// +// Whitelisting this test. +// As part of bug 1077403, the leaking uncaught rejection should be fixed. +// +thisTestLeaksUncaughtRejectionsAndShouldBeFixed("destroy"); + +function test() { + let manager = ResponsiveUIManager; + let done; + + function isOpen() { + return gBrowser.getBrowserContainer(gBrowser.selectedBrowser) + .hasAttribute("responsivemode"); + } + + helpers.addTabWithToolbar("data:text/html;charset=utf-8,hi", (options) => { + return helpers.audit(options, [ + { + setup() { + done = once(manager, "on"); + return helpers.setInput(options, "resize toggle"); + }, + check: { + input: "resize toggle", + hints: "", + markup: "VVVVVVVVVVVVV", + status: "VALID" + }, + exec: { + output: "" + }, + post: Task.async(function* () { + yield done; + ok(isOpen(), "responsive mode is open"); + }), + }, + { + setup() { + done = once(manager, "off"); + return helpers.setInput(options, "resize toggle"); + }, + check: { + input: "resize toggle", + hints: "", + markup: "VVVVVVVVVVVVV", + status: "VALID" + }, + exec: { + output: "" + }, + post: Task.async(function* () { + yield done; + ok(!isOpen(), "responsive mode is closed"); + }), + }, + { + setup() { + done = once(manager, "on"); + return helpers.setInput(options, "resize on"); + }, + check: { + input: "resize on", + hints: "", + markup: "VVVVVVVVV", + status: "VALID" + }, + exec: { + output: "" + }, + post: Task.async(function* () { + yield done; + ok(isOpen(), "responsive mode is open"); + }), + }, + { + setup() { + done = once(manager, "off"); + return helpers.setInput(options, "resize off"); + }, + check: { + input: "resize off", + hints: "", + markup: "VVVVVVVVVV", + status: "VALID" + }, + exec: { + output: "" + }, + post: Task.async(function* () { + yield done; + ok(!isOpen(), "responsive mode is closed"); + }), + }, + { + setup() { + done = once(manager, "on"); + return helpers.setInput(options, "resize to 400 400"); + }, + check: { + input: "resize to 400 400", + hints: "", + markup: "VVVVVVVVVVVVVVVVV", + status: "VALID", + args: { + width: { value: 400 }, + height: { value: 400 }, + } + }, + exec: { + output: "" + }, + post: Task.async(function* () { + yield done; + ok(isOpen(), "responsive mode is open"); + }), + }, + { + setup() { + done = once(manager, "off"); + return helpers.setInput(options, "resize off"); + }, + check: { + input: "resize off", + hints: "", + markup: "VVVVVVVVVV", + status: "VALID" + }, + exec: { + output: "" + }, + post: Task.async(function* () { + yield done; + ok(!isOpen(), "responsive mode is closed"); + }), + }, + ]); + }).then(finish); +} diff --git a/devtools/client/responsivedesign/test/browser_responsive_devicewidth.js b/devtools/client/responsivedesign/test/browser_responsive_devicewidth.js new file mode 100644 index 000000000..604a20783 --- /dev/null +++ b/devtools/client/responsivedesign/test/browser_responsive_devicewidth.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(function* () { + let tab = yield addTab("about:logo"); + let { rdm, manager } = yield openRDM(tab); + ok(rdm, "An instance of the RDM should be attached to the tab."); + yield setSize(rdm, manager, 110, 500); + + info("Checking initial width/height properties."); + yield doInitialChecks(); + + info("Changing the RDM size"); + yield setSize(rdm, manager, 90, 500); + + info("Checking for screen props"); + yield checkScreenProps(); + + info("Setting docShell.deviceSizeIsPageSize to false"); + yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + let docShell = content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + docShell.deviceSizeIsPageSize = false; + }); + + info("Checking for screen props once again."); + yield checkScreenProps2(); + + yield closeRDM(rdm); +}); + +function* doInitialChecks() { + let {innerWidth, matchesMedia} = yield grabContentInfo(); + is(innerWidth, 110, "initial width should be 110px"); + ok(!matchesMedia, "media query shouldn't match."); +} + +function* checkScreenProps() { + let {matchesMedia, screen} = yield grabContentInfo(); + ok(matchesMedia, "media query should match"); + isnot(window.screen.width, screen.width, + "screen.width should not be the size of the screen."); + is(screen.width, 90, "screen.width should be the page width"); + is(screen.height, 500, "screen.height should be the page height"); +} + +function* checkScreenProps2() { + let {matchesMedia, screen} = yield grabContentInfo(); + ok(!matchesMedia, "media query should be re-evaluated."); + is(window.screen.width, screen.width, + "screen.width should be the size of the screen."); +} + +function grabContentInfo() { + return ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + return { + screen: { + width: content.screen.width, + height: content.screen.height + }, + innerWidth: content.innerWidth, + matchesMedia: content.matchMedia("(max-device-width:100px)").matches + }; + }); +} diff --git a/devtools/client/responsivedesign/test/browser_responsivecomputedview.js b/devtools/client/responsivedesign/test/browser_responsivecomputedview.js new file mode 100644 index 000000000..eee2dbc03 --- /dev/null +++ b/devtools/client/responsivedesign/test/browser_responsivecomputedview.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that when the viewport is resized, the computed-view refreshes. + +const TEST_URI = "data:text/html;charset=utf-8,<html><style>" + + "div {" + + " width: 500px;" + + " height: 10px;" + + " background: purple;" + + "} " + + "@media screen and (max-width: 200px) {" + + " div { " + + " width: 100px;" + + " }" + + "};" + + "</style><div></div></html>"; + +add_task(function* () { + yield addTab(TEST_URI); + + info("Open the responsive design mode and set its size to 500x500 to start"); + let { rdm, manager } = yield openRDM(); + yield setSize(rdm, manager, 500, 500); + + info("Open the inspector, computed-view and select the test node"); + let {inspector, view} = yield openComputedView(); + yield selectNode("div", inspector); + + info("Try shrinking the viewport and checking the applied styles"); + yield testShrink(view, inspector, rdm, manager); + + info("Try growing the viewport and checking the applied styles"); + yield testGrow(view, inspector, rdm, manager); + + yield closeRDM(rdm); + yield closeToolbox(); +}); + +function* testShrink(computedView, inspector, rdm, manager) { + is(computedWidth(computedView), "500px", "Should show 500px initially."); + + let onRefresh = inspector.once("computed-view-refreshed"); + yield setSize(rdm, manager, 100, 100); + yield onRefresh; + + is(computedWidth(computedView), "100px", "Should be 100px after shrinking."); +} + +function* testGrow(computedView, inspector, rdm, manager) { + let onRefresh = inspector.once("computed-view-refreshed"); + yield setSize(rdm, manager, 500, 500); + yield onRefresh; + + is(computedWidth(computedView), "500px", "Should be 500px after growing."); +} + +function computedWidth(computedView) { + for (let prop of computedView.propertyViews) { + if (prop.name === "width") { + return prop.valueNode.textContent; + } + } + return null; +} diff --git a/devtools/client/responsivedesign/test/browser_responsiveruleview.js b/devtools/client/responsivedesign/test/browser_responsiveruleview.js new file mode 100644 index 000000000..5c3698e78 --- /dev/null +++ b/devtools/client/responsivedesign/test/browser_responsiveruleview.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that when the viewport is resized, the rule-view refreshes. +// Also test that ESC does open the split-console, and that the RDM menu item +// gets updated correctly when needed. +// TODO: split this test. + +const TEST_URI = "data:text/html;charset=utf-8,<html><style>" + + "div {" + + " width: 500px;" + + " height: 10px;" + + " background: purple;" + + "} " + + "@media screen and (max-width: 200px) {" + + " div { " + + " width: 100px;" + + " }" + + "};" + + "</style><div></div></html>"; + +add_task(function* () { + yield addTab(TEST_URI); + + info("Open the responsive design mode and set its size to 500x500 to start"); + let { rdm, manager } = yield openRDM(); + yield setSize(rdm, manager, 500, 500); + + info("Open the inspector, rule-view and select the test node"); + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + + info("Try shrinking the viewport and checking the applied styles"); + yield testShrink(view, rdm, manager); + + info("Try growing the viewport and checking the applied styles"); + yield testGrow(view, rdm, manager); + + info("Check that ESC still opens the split console"); + yield testEscapeOpensSplitConsole(inspector); + + yield closeToolbox(); + + info("Test the state of the RDM menu item"); + yield testMenuItem(rdm); + + Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled"); +}); + +function* testShrink(ruleView, rdm, manager) { + is(numberOfRules(ruleView), 2, "Should have two rules initially."); + + info("Resize to 100x100 and wait for the rule-view to update"); + let onRefresh = ruleView.once("ruleview-refreshed"); + yield setSize(rdm, manager, 100, 100); + yield onRefresh; + + is(numberOfRules(ruleView), 3, "Should have three rules after shrinking."); +} + +function* testGrow(ruleView, rdm, manager) { + info("Resize to 500x500 and wait for the rule-view to update"); + let onRefresh = ruleView.once("ruleview-refreshed"); + yield setSize(rdm, manager, 500, 500); + yield onRefresh; + + is(numberOfRules(ruleView), 2, "Should have two rules after growing."); +} + +function* testEscapeOpensSplitConsole(inspector) { + ok(!inspector._toolbox._splitConsole, "Console is not split."); + + info("Press escape"); + let onSplit = inspector._toolbox.once("split-console"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield onSplit; + + ok(inspector._toolbox._splitConsole, "Console is split after pressing ESC."); +} + +function* testMenuItem(rdm) { + is(document.getElementById("menu_responsiveUI").getAttribute("checked"), + "true", "The menu item is checked"); + + yield closeRDM(rdm); + + is(document.getElementById("menu_responsiveUI").getAttribute("checked"), + "false", "The menu item is unchecked"); +} + +function numberOfRules(ruleView) { + return ruleView.element.querySelectorAll(".ruleview-code").length; +} diff --git a/devtools/client/responsivedesign/test/browser_responsiveui.js b/devtools/client/responsivedesign/test/browser_responsiveui.js new file mode 100644 index 000000000..283974d0f --- /dev/null +++ b/devtools/client/responsivedesign/test/browser_responsiveui.js @@ -0,0 +1,250 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(function* () { + let tab = yield addTab("data:text/html,mop"); + + let {rdm, manager} = yield openRDM(tab, "menu"); + let container = gBrowser.getBrowserContainer(); + is(container.getAttribute("responsivemode"), "true", + "Should be in responsive mode."); + is(document.getElementById("menu_responsiveUI").getAttribute("checked"), + "true", "Menu item should be checked"); + + ok(rdm, "An instance of the RDM should be attached to the tab."); + + let originalWidth = (yield getSizing()).width; + + let documentLoaded = waitForDocLoadComplete(); + gBrowser.loadURI("data:text/html;charset=utf-8,mop" + + "<div style%3D'height%3A5000px'><%2Fdiv>"); + yield documentLoaded; + + let newWidth = (yield getSizing()).width; + is(originalWidth, newWidth, "Floating scrollbars shouldn't change the width"); + + yield testPresets(rdm, manager); + + info("Testing mouse resizing"); + yield testManualMouseResize(rdm, manager); + + info("Testing mouse resizing with shift key"); + yield testManualMouseResize(rdm, manager, "shift"); + + info("Testing mouse resizing with ctrl key"); + yield testManualMouseResize(rdm, manager, "ctrl"); + + info("Testing resizing with user custom keyboard input"); + yield testResizeUsingCustomInput(rdm, manager); + + info("Testing invalid keyboard input"); + yield testInvalidUserInput(rdm); + + info("Testing rotation"); + yield testRotate(rdm, manager); + + let {width: widthBeforeClose, height: heightBeforeClose} = yield getSizing(); + + info("Restarting responsive mode"); + yield closeRDM(rdm); + + let resized = waitForResizeTo(manager, widthBeforeClose, heightBeforeClose); + ({rdm} = yield openRDM(tab, "keyboard")); + yield resized; + + let currentSize = yield getSizing(); + is(currentSize.width, widthBeforeClose, "width should be restored"); + is(currentSize.height, heightBeforeClose, "height should be restored"); + + container = gBrowser.getBrowserContainer(); + is(container.getAttribute("responsivemode"), "true", "In responsive mode."); + is(document.getElementById("menu_responsiveUI").getAttribute("checked"), + "true", "menu item should be checked"); + + let isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1; + if (!isWinXP) { + yield testScreenshot(rdm); + } + + yield closeRDM(rdm); + is(document.getElementById("menu_responsiveUI").getAttribute("checked"), + "false", "menu item should be unchecked"); +}); + +function* testPresets(rdm, manager) { + // Starting from length - 4 because last 3 items are not presets : + // the separator, the add button and the remove button + for (let c = rdm.menulist.firstChild.childNodes.length - 4; c >= 0; c--) { + let item = rdm.menulist.firstChild.childNodes[c]; + let [width, height] = extractSizeFromString(item.getAttribute("label")); + yield setPresetIndex(rdm, manager, c); + + let {width: contentWidth, height: contentHeight} = yield getSizing(); + is(contentWidth, width, "preset" + c + ": the width should be changed"); + is(contentHeight, height, "preset" + c + ": the height should be changed"); + } +} + +function* testManualMouseResize(rdm, manager, pressedKey) { + yield setSize(rdm, manager, 100, 100); + + let {width: initialWidth, height: initialHeight} = yield getSizing(); + is(initialWidth, 100, "Width should be reset to 100"); + is(initialHeight, 100, "Height should be reset to 100"); + + let x = 2, y = 2; + EventUtils.synthesizeMouse(rdm.resizer, x, y, {type: "mousedown"}, window); + + let mouseMoveParams = {type: "mousemove"}; + if (pressedKey == "shift") { + x += 23; y += 10; + mouseMoveParams.shiftKey = true; + } else if (pressedKey == "ctrl") { + x += 120; y += 60; + mouseMoveParams.ctrlKey = true; + } else { + x += 20; y += 10; + } + + EventUtils.synthesizeMouse(rdm.resizer, x, y, mouseMoveParams, window); + EventUtils.synthesizeMouse(rdm.resizer, x, y, {type: "mouseup"}, window); + + yield once(manager, "content-resize"); + + let expectedWidth = initialWidth + 20; + let expectedHeight = initialHeight + 10; + info("initial width: " + initialWidth); + info("initial height: " + initialHeight); + + yield verifyResize(rdm, expectedWidth, expectedHeight); +} + +function* testResizeUsingCustomInput(rdm, manager) { + let {width: initialWidth, height: initialHeight} = yield getSizing(); + let expectedWidth = initialWidth - 20, expectedHeight = initialHeight - 10; + + let userInput = expectedWidth + " x " + expectedHeight; + rdm.menulist.inputField.value = ""; + rdm.menulist.focus(); + processStringAsKey(userInput); + + // While typing, the size should not change + let currentSize = yield getSizing(); + is(currentSize.width, initialWidth, "Typing shouldn't change the width"); + is(currentSize.height, initialHeight, "Typing shouldn't change the height"); + + // Only the `change` event must change the size + EventUtils.synthesizeKey("VK_RETURN", {}); + + yield once(manager, "content-resize"); + + yield verifyResize(rdm, expectedWidth, expectedHeight); +} + +function* testInvalidUserInput(rdm) { + let {width: initialWidth, height: initialHeight} = yield getSizing(); + let index = rdm.menulist.selectedIndex; + let expectedValue = initialWidth + "\u00D7" + initialHeight; + let expectedLabel = rdm.menulist.firstChild.firstChild.getAttribute("label"); + + let userInput = "I'm wrong"; + + rdm.menulist.inputField.value = ""; + rdm.menulist.focus(); + processStringAsKey(userInput); + EventUtils.synthesizeKey("VK_RETURN", {}); + + let currentSize = yield getSizing(); + is(currentSize.width, initialWidth, "Width should not change"); + is(currentSize.height, initialHeight, "Height should not change"); + is(rdm.menulist.selectedIndex, index, "Selected item should not change."); + is(rdm.menulist.value, expectedValue, "Value should be reset"); + + let label = rdm.menulist.firstChild.firstChild.getAttribute("label"); + is(label, expectedLabel, "Custom menuitem's label should not change"); +} + +function* testRotate(rdm, manager) { + yield setSize(rdm, manager, 100, 200); + + let {width: initialWidth, height: initialHeight} = yield getSizing(); + rdm.rotate(); + + yield once(manager, "content-resize"); + + let newSize = yield getSizing(); + is(newSize.width, initialHeight, "The width should now be the height."); + is(newSize.height, initialWidth, "The height should now be the width."); + + let label = rdm.menulist.firstChild.firstChild.getAttribute("label"); + let [width, height] = extractSizeFromString(label); + is(width, initialHeight, "Width in label should be updated"); + is(height, initialWidth, "Height in label should be updated"); +} + +function* verifyResize(rdm, expectedWidth, expectedHeight) { + let currentSize = yield getSizing(); + is(currentSize.width, expectedWidth, "Width should now change"); + is(currentSize.height, expectedHeight, "Height should now change"); + + is(rdm.menulist.selectedIndex, -1, "Custom menuitem cannot be selected"); + + let label = rdm.menulist.firstChild.firstChild.getAttribute("label"); + let value = rdm.menulist.value; + isnot(label, value, + "The menulist item label should be different than the menulist value"); + + let [width, height] = extractSizeFromString(label); + is(width, expectedWidth, "Width in label should be updated"); + is(height, expectedHeight, "Height in label should be updated"); + + [width, height] = extractSizeFromString(value); + is(width, expectedWidth, "Value should be updated with new width"); + is(height, expectedHeight, "Value should be updated with new height"); +} + +function* testScreenshot(rdm) { + info("Testing screenshot"); + rdm.screenshot("responsiveui"); + let {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {}); + + while (true) { + // while(true) until we find the file. + // no need for a timeout, the test will get killed anyway. + let file = FileUtils.getFile("DfltDwnld", [ "responsiveui.png" ]); + if (file.exists()) { + ok(true, "Screenshot file exists"); + file.remove(false); + break; + } + info("checking if file exists in 200ms"); + yield wait(200); + } +} + +function* getSizing() { + let browser = gBrowser.selectedBrowser; + let sizing = yield ContentTask.spawn(browser, {}, function* () { + return { + width: content.innerWidth, + height: content.innerHeight + }; + }); + return sizing; +} + +function extractSizeFromString(str) { + let numbers = str.match(/(\d+)[^\d]*(\d+)/); + if (numbers) { + return [numbers[1], numbers[2]]; + } + return [null, null]; +} + +function processStringAsKey(str) { + for (let i = 0, l = str.length; i < l; i++) { + EventUtils.synthesizeKey(str.charAt(i), {}); + } +} diff --git a/devtools/client/responsivedesign/test/browser_responsiveui_customuseragent.js b/devtools/client/responsivedesign/test/browser_responsiveui_customuseragent.js new file mode 100644 index 000000000..35efc4c14 --- /dev/null +++ b/devtools/client/responsivedesign/test/browser_responsiveui_customuseragent.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = "data:text/html, Custom User Agent test"; +const DEFAULT_UA = Cc["@mozilla.org/network/protocol;1?name=http"] + .getService(Ci.nsIHttpProtocolHandler) + .userAgent; +const CHROME_UA = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36" + + " (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"; +add_task(function* () { + yield addTab(TEST_URI); + + let {rdm, manager} = yield openRDM(); + yield testUserAgent(DEFAULT_UA); + + info("Setting UA to " + CHROME_UA); + yield setUserAgent(CHROME_UA, rdm, manager); + yield testUserAgent(CHROME_UA); + + info("Resetting UA"); + yield setUserAgent("", rdm, manager); + yield testUserAgent(DEFAULT_UA); + + info("Setting UA to " + CHROME_UA); + yield setUserAgent(CHROME_UA, rdm, manager); + yield testUserAgent(CHROME_UA); + + info("Closing responsive mode"); + + yield closeRDM(rdm); + yield testUserAgent(DEFAULT_UA); +}); + +function* setUserAgent(ua, rdm, manager) { + let input = rdm.userAgentInput; + input.focus(); + input.value = ua; + let onUAChanged = once(manager, "userAgentChanged"); + input.blur(); + yield onUAChanged; + + if (ua !== "") { + ok(input.hasAttribute("attention"), "UA input should be highlighted"); + } else { + ok(!input.hasAttribute("attention"), "UA input shouldn't be highlighted"); + } +} + +function* testUserAgent(value) { + let ua = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () { + return content.navigator.userAgent; + }); + is(ua, value, `UA should be set to ${value}`); +} diff --git a/devtools/client/responsivedesign/test/browser_responsiveui_touch.js b/devtools/client/responsivedesign/test/browser_responsiveui_touch.js new file mode 100644 index 000000000..c23d2dd12 --- /dev/null +++ b/devtools/client/responsivedesign/test/browser_responsiveui_touch.js @@ -0,0 +1,148 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = "http://mochi.test:8888/browser/devtools/client/" + + "responsivedesign/test/touch.html"; +const layoutReflowSynthMouseMove = "layout.reflow.synthMouseMove"; +const domViewportEnabled = "dom.meta-viewport.enabled"; + +add_task(function* () { + let tab = yield addTab(TEST_URI); + let {rdm} = yield openRDM(tab); + yield pushPrefs([layoutReflowSynthMouseMove, false]); + yield testWithNoTouch(); + yield rdm.enableTouch(); + yield testWithTouch(); + yield rdm.disableTouch(); + yield testWithNoTouch(); + yield closeRDM(rdm); +}); + +function* testWithNoTouch() { + let div = content.document.querySelector("div"); + let x = 0, y = 0; + + info("testWithNoTouch: Initial test parameter and mouse mouse outside div element"); + x = -1, y = -1; + yield BrowserTestUtils.synthesizeMouse("div", x, y, + { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser); + div.style.transform = "none"; + div.style.backgroundColor = ""; + + info("testWithNoTouch: Move mouse into the div element"); + yield BrowserTestUtils.synthesizeMouseAtCenter("div", { type: "mousemove", isSynthesized: false }, + gBrowser.selectedBrowser); + is(div.style.backgroundColor, "red", "mouseenter or mouseover should work"); + + info("testWithNoTouch: Drag the div element"); + yield BrowserTestUtils.synthesizeMouseAtCenter("div", { type: "mousedown", isSynthesized: false }, + gBrowser.selectedBrowser); + x = 100; y = 100; + yield BrowserTestUtils.synthesizeMouse("div", x, y, + { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser); + is(div.style.transform, "none", "touchmove shouldn't work"); + yield BrowserTestUtils.synthesizeMouse("div", x, y, + { type: "mouseup", isSynthesized: false }, gBrowser.selectedBrowser); + + info("testWithNoTouch: Move mouse out of the div element"); + x = -1; y = -1; + yield BrowserTestUtils.synthesizeMouse("div", x, y, + { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser); + is(div.style.backgroundColor, "blue", "mouseout or mouseleave should work"); + + info("testWithNoTouch: Click the div element"); + yield synthesizeClick(div); + is(div.dataset.isDelay, "false", "300ms delay between touch events and mouse events should not work"); +} + +function* testWithTouch() { + let div = content.document.querySelector("div"); + let x = 0, y = 0; + + info("testWithTouch: Initial test parameter and mouse mouse outside div element"); + x = -1, y = -1; + yield BrowserTestUtils.synthesizeMouse("div", x, y, + { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser); + div.style.transform = "none"; + div.style.backgroundColor = ""; + + info("testWithTouch: Move mouse into the div element"); + yield BrowserTestUtils.synthesizeMouseAtCenter("div", { type: "mousemove", isSynthesized: false }, + gBrowser.selectedBrowser); + isnot(div.style.backgroundColor, "red", "mouseenter or mouseover should not work"); + + info("testWithTouch: Drag the div element"); + yield BrowserTestUtils.synthesizeMouseAtCenter("div", { type: "mousedown", isSynthesized: false }, + gBrowser.selectedBrowser); + x = 100; y = 100; + yield BrowserTestUtils.synthesizeMouse("div", x, y, + { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser); + isnot(div.style.transform, "none", "touchmove should work"); + yield BrowserTestUtils.synthesizeMouse("div", x, y, + { type: "mouseup", isSynthesized: false }, gBrowser.selectedBrowser); + + info("testWithTouch: Move mouse out of the div element"); + x = -1; y = -1; + yield BrowserTestUtils.synthesizeMouse("div", x, y, + { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser); + isnot(div.style.backgroundColor, "blue", "mouseout or mouseleave should not work"); + + yield testWithMetaViewportEnabled(); + yield testWithMetaViewportDisabled(); +} + +function* testWithMetaViewportEnabled() { + yield pushPrefs([domViewportEnabled, true]); + let meta = content.document.querySelector("meta[name=viewport]"); + let div = content.document.querySelector("div"); + div.dataset.isDelay = "false"; + + info("testWithMetaViewportEnabled: click the div element with <meta name='viewport'>"); + meta.content = ""; + yield synthesizeClick(div); + is(div.dataset.isDelay, "true", "300ms delay between touch events and mouse events should work"); + + info("testWithMetaViewportEnabled: click the div element with <meta name='viewport' content='user-scalable=no'>"); + meta.content = "user-scalable=no"; + yield synthesizeClick(div); + is(div.dataset.isDelay, "false", "300ms delay between touch events and mouse events should not work"); + + info("testWithMetaViewportEnabled: click the div element with <meta name='viewport' content='minimum-scale=maximum-scale'>"); + meta.content = "minimum-scale=maximum-scale"; + yield synthesizeClick(div); + is(div.dataset.isDelay, "false", "300ms delay between touch events and mouse events should not work"); + + info("testWithMetaViewportEnabled: click the div element with <meta name='viewport' content='width=device-width'>"); + meta.content = "width=device-width"; + yield synthesizeClick(div); + is(div.dataset.isDelay, "false", "300ms delay between touch events and mouse events should not work"); +} + +function* testWithMetaViewportDisabled() { + yield pushPrefs([domViewportEnabled, false]); + let meta = content.document.querySelector("meta[name=viewport]"); + let div = content.document.querySelector("div"); + div.dataset.isDelay = "false"; + + info("testWithMetaViewportDisabled: click the div element with <meta name='viewport'>"); + meta.content = ""; + yield synthesizeClick(div); + is(div.dataset.isDelay, "true", "300ms delay between touch events and mouse events should work"); +} + +function synthesizeClick(element) { + let waitForClickEvent = BrowserTestUtils.waitForEvent(element, "click"); + BrowserTestUtils.synthesizeMouseAtCenter(element, { type: "mousedown", isSynthesized: false }, + gBrowser.selectedBrowser); + BrowserTestUtils.synthesizeMouseAtCenter(element, { type: "mouseup", isSynthesized: false }, + gBrowser.selectedBrowser); + return waitForClickEvent; +} + +function pushPrefs(...aPrefs) { + let deferred = promise.defer(); + SpecialPowers.pushPrefEnv({"set": aPrefs}, deferred.resolve); + return deferred.promise; +} diff --git a/devtools/client/responsivedesign/test/browser_responsiveui_window_close.js b/devtools/client/responsivedesign/test/browser_responsiveui_window_close.js new file mode 100644 index 000000000..a5f890a86 --- /dev/null +++ b/devtools/client/responsivedesign/test/browser_responsiveui_window_close.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(function* () { + let newWindowPromise = BrowserTestUtils.waitForNewWindow(); + window.open("about:blank", "_blank"); + let newWindow = yield newWindowPromise; + + newWindow.focus(); + yield once(newWindow.gBrowser, "load", true); + + let tab = newWindow.gBrowser.selectedTab; + yield ResponsiveUIManager.openIfNeeded(newWindow, tab); + + // Close the window on a tab with an active responsive design UI and + // wait for the UI to gracefully shutdown. This has leaked the window + // in the past. + ok(ResponsiveUIManager.isActiveForTab(tab), + "ResponsiveUI should be active for tab when the window is closed"); + let offPromise = once(ResponsiveUIManager, "off"); + yield BrowserTestUtils.closeWindow(newWindow); + yield offPromise; +}); diff --git a/devtools/client/responsivedesign/test/browser_responsiveuiaddcustompreset.js b/devtools/client/responsivedesign/test/browser_responsiveuiaddcustompreset.js new file mode 100644 index 000000000..3ab54b601 --- /dev/null +++ b/devtools/client/responsivedesign/test/browser_responsiveuiaddcustompreset.js @@ -0,0 +1,121 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(function* () { + let tab = yield addTab("data:text/html;charset=utf8,Test RDM custom presets"); + + let { rdm, manager } = yield openRDM(tab); + + let oldPrompt = Services.prompt; + Services.prompt = { + value: "", + returnBool: true, + prompt: function (parent, dialogTitle, text, value, checkMsg, checkState) { + value.value = this.value; + return this.returnBool; + } + }; + + registerCleanupFunction(() => { + Services.prompt = oldPrompt; + }); + + // Is it open? + let container = gBrowser.getBrowserContainer(); + is(container.getAttribute("responsivemode"), "true", + "Should be in responsive mode."); + + ok(rdm, "RDM instance should be attached to the tab."); + + // Tries to add a custom preset and cancel the prompt + let idx = rdm.menulist.selectedIndex; + let presetCount = rdm.presets.length; + + Services.prompt.value = ""; + Services.prompt.returnBool = false; + rdm.addbutton.doCommand(); + + is(idx, rdm.menulist.selectedIndex, + "selected item shouldn't change after add preset and cancel"); + is(presetCount, rdm.presets.length, + "number of presets shouldn't change after add preset and cancel"); + + // Adds the custom preset with "Testing preset" + Services.prompt.value = "Testing preset"; + Services.prompt.returnBool = true; + + let resized = once(manager, "content-resize"); + let customHeight = 123, customWidth = 456; + rdm.startResizing({}); + rdm.setViewportSize({ + width: customWidth, + height: customHeight, + }); + rdm.stopResizing({}); + + rdm.addbutton.doCommand(); + yield resized; + + yield closeRDM(rdm); + + ({rdm} = yield openRDM(tab)); + is(container.getAttribute("responsivemode"), "true", + "Should be in responsive mode."); + + let presetLabel = "456" + "\u00D7" + "123 (Testing preset)"; + let customPresetIndex = yield getPresetIndex(rdm, manager, presetLabel); + ok(customPresetIndex >= 0, "(idx = " + customPresetIndex + ") should be the" + + " previously added preset in the list of items"); + + yield setPresetIndex(rdm, manager, customPresetIndex); + + let browser = gBrowser.selectedBrowser; + yield ContentTask.spawn(browser, null, function* () { + let {innerWidth, innerHeight} = content; + Assert.equal(innerWidth, 456, "Selecting preset should change the width"); + Assert.equal(innerHeight, 123, "Selecting preset should change the height"); + }); + + info(`menulist count: ${rdm.menulist.itemCount}`); + + rdm.removebutton.doCommand(); + + yield setPresetIndex(rdm, manager, 2); + let deletedPresetA = rdm.menulist.selectedItem.getAttribute("label"); + rdm.removebutton.doCommand(); + + yield setPresetIndex(rdm, manager, 2); + let deletedPresetB = rdm.menulist.selectedItem.getAttribute("label"); + rdm.removebutton.doCommand(); + + yield closeRDM(rdm); + ({rdm} = yield openRDM(tab)); + + customPresetIndex = yield getPresetIndex(rdm, manager, deletedPresetA); + is(customPresetIndex, -1, + "Deleted preset " + deletedPresetA + " should not be in the list anymore"); + + customPresetIndex = yield getPresetIndex(rdm, manager, deletedPresetB); + is(customPresetIndex, -1, + "Deleted preset " + deletedPresetB + " should not be in the list anymore"); + + yield closeRDM(rdm); +}); + +var getPresetIndex = Task.async(function* (rdm, manager, presetLabel) { + var testOnePreset = Task.async(function* (c) { + if (c == 0) { + return -1; + } + yield setPresetIndex(rdm, manager, c); + + let item = rdm.menulist.firstChild.childNodes[c]; + if (item.getAttribute("label") === presetLabel) { + return c; + } + return testOnePreset(c - 1); + }); + return testOnePreset(rdm.menulist.firstChild.childNodes.length - 4); +}); diff --git a/devtools/client/responsivedesign/test/head.js b/devtools/client/responsivedesign/test/head.js new file mode 100644 index 000000000..3228021f6 --- /dev/null +++ b/devtools/client/responsivedesign/test/head.js @@ -0,0 +1,302 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); +// shared-head.js handles imports, constants, and utility functions +let sharedHeadURI = testDir + "../../../framework/test/shared-head.js"; +Services.scriptloader.loadSubScript(sharedHeadURI, this); + +// Import the GCLI test helper +let gcliHelpersURI = testDir + "../../../commandline/test/helpers.js"; +Services.scriptloader.loadSubScript(gcliHelpersURI, this); + +flags.testing = true; +Services.prefs.setBoolPref("devtools.responsive.html.enabled", false); + +registerCleanupFunction(() => { + flags.testing = false; + Services.prefs.clearUserPref("devtools.responsive.html.enabled"); + Services.prefs.clearUserPref("devtools.responsiveUI.currentPreset"); + Services.prefs.clearUserPref("devtools.responsiveUI.customHeight"); + Services.prefs.clearUserPref("devtools.responsiveUI.customWidth"); + Services.prefs.clearUserPref("devtools.responsiveUI.presets"); + Services.prefs.clearUserPref("devtools.responsiveUI.rotate"); +}); + +SimpleTest.requestCompleteLog(); + +const { ResponsiveUIManager } = Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm", {}); + +/** + * Open the Responsive Design Mode + * @param {Tab} The browser tab to open it into (defaults to the selected tab). + * @param {method} The method to use to open the RDM (values: menu, keyboard) + * @return {rdm, manager} Returns the RUI instance and the manager + */ +var openRDM = Task.async(function* (tab = gBrowser.selectedTab, + method = "menu") { + let manager = ResponsiveUIManager; + + let opened = once(manager, "on"); + let resized = once(manager, "content-resize"); + if (method == "menu") { + document.getElementById("menu_responsiveUI").doCommand(); + } else { + synthesizeKeyFromKeyTag(document.getElementById("key_responsiveUI")); + } + yield opened; + + let rdm = manager.getResponsiveUIForTab(tab); + rdm.transitionsEnabled = false; + registerCleanupFunction(() => { + rdm.transitionsEnabled = true; + }); + + // Wait for content to resize. This is triggered async by the preset menu + // auto-selecting its default entry once it's in the document. + yield resized; + + return {rdm, manager}; +}); + +/** + * Close a responsive mode instance + * @param {rdm} ResponsiveUI instance for the tab + */ +var closeRDM = Task.async(function* (rdm) { + let manager = ResponsiveUIManager; + if (!rdm) { + rdm = manager.getResponsiveUIForTab(gBrowser.selectedTab); + } + let closed = once(manager, "off"); + let resized = once(manager, "content-resize"); + rdm.close(); + yield resized; + yield closed; +}); + +/** + * Open the toolbox, with the inspector tool visible. + * @return a promise that resolves when the inspector is ready + */ +var openInspector = Task.async(function* () { + info("Opening the inspector"); + let target = TargetFactory.forTab(gBrowser.selectedTab); + + let inspector, toolbox; + + // Checking if the toolbox and the inspector are already loaded + // The inspector-updated event should only be waited for if the inspector + // isn't loaded yet + toolbox = gDevTools.getToolbox(target); + if (toolbox) { + inspector = toolbox.getPanel("inspector"); + if (inspector) { + info("Toolbox and inspector already open"); + return { + toolbox: toolbox, + inspector: inspector + }; + } + } + + info("Opening the toolbox"); + toolbox = yield gDevTools.showToolbox(target, "inspector"); + yield waitForToolboxFrameFocus(toolbox); + inspector = toolbox.getPanel("inspector"); + + info("Waiting for the inspector to update"); + if (inspector._updateProgress) { + yield inspector.once("inspector-updated"); + } + + return { + toolbox: toolbox, + inspector: inspector + }; +}); + +var closeToolbox = Task.async(function* () { + let target = TargetFactory.forTab(gBrowser.selectedTab); + yield gDevTools.closeToolbox(target); +}); + +/** + * Wait for the toolbox frame to receive focus after it loads + * @param {Toolbox} toolbox + * @return a promise that resolves when focus has been received + */ +function waitForToolboxFrameFocus(toolbox) { + info("Making sure that the toolbox's frame is focused"); + let def = promise.defer(); + waitForFocus(def.resolve, toolbox.win); + return def.promise; +} + +/** + * Open the toolbox, with the inspector tool visible, and the sidebar that + * corresponds to the given id selected + * @return a promise that resolves when the inspector is ready and the sidebar + * view is visible and ready + */ +var openInspectorSideBar = Task.async(function* (id) { + let {toolbox, inspector} = yield openInspector(); + + info("Selecting the " + id + " sidebar"); + inspector.sidebar.select(id); + + return { + toolbox: toolbox, + inspector: inspector, + view: inspector[id].view || inspector[id].computedView + }; +}); + +/** + * Checks whether the inspector's sidebar corresponding to the given id already + * exists + * @param {InspectorPanel} + * @param {String} + * @return {Boolean} + */ +function hasSideBarTab(inspector, id) { + return !!inspector.sidebar.getWindowForTab(id); +} + +/** + * Open the toolbox, with the inspector tool visible, and the computed-view + * sidebar tab selected. + * @return a promise that resolves when the inspector is ready and the computed + * view is visible and ready + */ +function openComputedView() { + return openInspectorSideBar("computedview"); +} + +/** + * Open the toolbox, with the inspector tool visible, and the rule-view + * sidebar tab selected. + * @return a promise that resolves when the inspector is ready and the rule + * view is visible and ready + */ +function openRuleView() { + return openInspectorSideBar("ruleview"); +} + +/** + * Add a new test tab in the browser and load the given url. + * @param {String} url The url to be loaded in the new tab + * @return a promise that resolves to the tab object when the url is loaded + */ +var addTab = Task.async(function* (url) { + info("Adding a new tab with URL: '" + url + "'"); + + window.focus(); + + let tab = gBrowser.selectedTab = gBrowser.addTab(url); + let browser = tab.linkedBrowser; + + yield BrowserTestUtils.browserLoaded(browser); + info("URL '" + url + "' loading complete"); + + return tab; +}); + +/** + * Waits for the next load to complete in the current browser. + * + * @return promise + */ +function waitForDocLoadComplete(aBrowser = gBrowser) { + let deferred = promise.defer(); + let progressListener = { + onStateChange: function (webProgress, req, flags, status) { + let docStop = Ci.nsIWebProgressListener.STATE_IS_NETWORK | + Ci.nsIWebProgressListener.STATE_STOP; + info(`Saw state ${flags.toString(16)} and status ${status.toString(16)}`); + + // When a load needs to be retargetted to a new process it is cancelled + // with NS_BINDING_ABORTED so ignore that case + if ((flags & docStop) == docStop && status != Cr.NS_BINDING_ABORTED) { + aBrowser.removeProgressListener(progressListener); + info("Browser loaded"); + deferred.resolve(); + } + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]) + }; + aBrowser.addProgressListener(progressListener); + info("Waiting for browser load"); + return deferred.promise; +} + +/** + * Get the NodeFront for a node that matches a given css selector, via the + * protocol. + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return {Promise} Resolves to the NodeFront instance + */ +function getNodeFront(selector, {walker}) { + if (selector._form) { + return selector; + } + return walker.querySelector(walker.rootNode, selector); +} + +/** + * Set the inspector's current selection to the first match of the given css + * selector + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @param {String} reason Defaults to "test" which instructs the inspector not + * to highlight the node upon selection + * @return {Promise} Resolves when the inspector is updated with the new node + */ +var selectNode = Task.async(function* (selector, inspector, reason = "test") { + info("Selecting the node for '" + selector + "'"); + let nodeFront = yield getNodeFront(selector, inspector); + let updated = inspector.once("inspector-updated"); + inspector.selection.setNodeFront(nodeFront, reason); + yield updated; +}); + +function waitForResizeTo(manager, width, height) { + return new Promise(resolve => { + let onResize = (_, data) => { + if (data.width != width || data.height != height) { + return; + } + manager.off("content-resize", onResize); + info(`Got content-resize to ${width} x ${height}`); + resolve(); + }; + info(`Waiting for content-resize to ${width} x ${height}`); + manager.on("content-resize", onResize); + }); +} + +var setPresetIndex = Task.async(function* (rdm, manager, index) { + info(`Current preset: ${rdm.menulist.selectedIndex}, change to: ${index}`); + if (rdm.menulist.selectedIndex != index) { + let resized = once(manager, "content-resize"); + rdm.menulist.selectedIndex = index; + yield resized; + } +}); + +var setSize = Task.async(function* (rdm, manager, width, height) { + let size = rdm.getSize(); + info(`Current size: ${size.width} x ${size.height}, ` + + `set to: ${width} x ${height}`); + if (size.width != width || size.height != height) { + let resized = waitForResizeTo(manager, width, height); + rdm.setViewportSize({ width, height }); + yield resized; + } +}); diff --git a/devtools/client/responsivedesign/test/touch.html b/devtools/client/responsivedesign/test/touch.html new file mode 100644 index 000000000..f93d36f6c --- /dev/null +++ b/devtools/client/responsivedesign/test/touch.html @@ -0,0 +1,85 @@ +<!DOCTYPE html> + +<meta charset="utf-8" /> +<meta name="viewport" /> +<title>test</title> + + +<style> + div { + border:1px solid red; + width: 100px; height: 100px; + } +</style> + +<div data-is-delay="false"></div> + +<script> + var div = document.querySelector("div"); + var initX, initY; + var previousEvent = "", touchendTime = 0; + var updatePreviousEvent = function(e){ + previousEvent = e.type; + }; + + div.style.transform = "none"; + div.style.backgroundColor = ""; + + div.addEventListener("touchstart", function(evt) { + var touch = evt.changedTouches[0]; + initX = touch.pageX; + initY = touch.pageY; + updatePreviousEvent(evt); + }, true); + + div.addEventListener("touchmove", function(evt) { + var touch = evt.changedTouches[0]; + var deltaX = touch.pageX - initX; + var deltaY = touch.pageY - initY; + div.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)"; + updatePreviousEvent(evt); + }, true); + + div.addEventListener("touchend", function(evt) { + if (!evt.touches.length) { + div.style.transform = "none"; + } + touchendTime = performance.now(); + updatePreviousEvent(evt); + }, true); + + div.addEventListener("mouseenter", function(evt) { + div.style.backgroundColor = "red"; + updatePreviousEvent(evt); + }, true); + div.addEventListener("mouseover", function(evt) { + div.style.backgroundColor = "red"; + updatePreviousEvent(evt); + }, true); + + div.addEventListener("mouseout", function(evt) { + div.style.backgroundColor = "blue"; + updatePreviousEvent(evt); + }, true); + + div.addEventListener("mouseleave", function(evt) { + div.style.backgroundColor = "blue"; + updatePreviousEvent(evt); + }, true); + + div.addEventListener("mousedown", function(evt){ + if (previousEvent === "touchend" && touchendTime !== 0) { + let now = performance.now(); + div.dataset.isDelay = ((now - touchendTime) >= 300) ? true : false; + } else { + div.dataset.isDelay = false; + } + updatePreviousEvent(evt); + }, true); + + div.addEventListener("mousemove", updatePreviousEvent, true); + + div.addEventListener("mouseup", updatePreviousEvent, true); + + div.addEventListener("click", updatePreviousEvent, true); +</script> |