diff options
Diffstat (limited to 'devtools/client/responsive/utils')
-rw-r--r-- | devtools/client/responsive/utils/e10s.js | 99 | ||||
-rw-r--r-- | devtools/client/responsive/utils/key.js | 25 | ||||
-rw-r--r-- | devtools/client/responsive/utils/l10n.js | 16 | ||||
-rw-r--r-- | devtools/client/responsive/utils/message.js | 55 | ||||
-rw-r--r-- | devtools/client/responsive/utils/moz.build | 16 | ||||
-rw-r--r-- | devtools/client/responsive/utils/notification.js | 60 | ||||
-rw-r--r-- | devtools/client/responsive/utils/orientation.js | 76 | ||||
-rw-r--r-- | devtools/client/responsive/utils/ua.js | 129 | ||||
-rw-r--r-- | devtools/client/responsive/utils/window.js | 43 |
9 files changed, 519 insertions, 0 deletions
diff --git a/devtools/client/responsive/utils/e10s.js b/devtools/client/responsive/utils/e10s.js new file mode 100644 index 0000000000..62ec6924a6 --- /dev/null +++ b/devtools/client/responsive/utils/e10s.js @@ -0,0 +1,99 @@ +/* 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"; + +// The prefix used for RDM messages in content. +// see: devtools/client/responsive/browser/content.js +const MESSAGE_PREFIX = "ResponsiveMode:"; +const REQUEST_DONE_SUFFIX = ":Done"; + +/** + * Registers a message `listener` that is called every time messages of + * specified `message` is emitted on the given message manager. + * @param {nsIMessageListenerManager} mm + * The Message Manager + * @param {String} message + * The message. It will be prefixed with the constant `MESSAGE_PREFIX` + * @param {Function} listener + * The listener function that processes the message. + */ +function on(mm, message, listener) { + mm.addMessageListener(MESSAGE_PREFIX + message, listener); +} +exports.on = on; + +/** + * Removes a message `listener` for the specified `message` on the given + * message manager. + * @param {nsIMessageListenerManager} mm + * The Message Manager + * @param {String} message + * The message. It will be prefixed with the constant `MESSAGE_PREFIX` + * @param {Function} listener + * The listener function that processes the message. + */ +function off(mm, message, listener) { + mm.removeMessageListener(MESSAGE_PREFIX + message, listener); +} +exports.off = off; + +/** + * Resolves a promise the next time the specified `message` is sent over the + * given message manager. + * @param {nsIMessageListenerManager} mm + * The Message Manager + * @param {String} message + * The message. It will be prefixed with the constant `MESSAGE_PREFIX` + * @returns {Promise} + * A promise that is resolved when the given message is emitted. + */ +function once(mm, message) { + return new Promise(resolve => { + on(mm, message, function onMessage({ data }) { + off(mm, message, onMessage); + resolve(data); + }); + }); +} +exports.once = once; + +/** + * Asynchronously emit a `message` to the listeners of the given message + * manager. + * + * @param {nsIMessageListenerManager} mm + * The Message Manager + * @param {String} message + * The message. It will be prefixed with the constant `MESSAGE_PREFIX`. + * @param {Object} data + * A JSON object containing data to be delivered to the listeners. + */ +function emit(mm, message, data) { + mm.sendAsyncMessage(MESSAGE_PREFIX + message, data); +} +exports.emit = emit; + +/** + * Asynchronously send a "request" over the given message manager, and returns + * a promise that is resolved when the request is complete. + * + * @param {nsIMessageListenerManager} mm + * The Message Manager + * @param {String} message + * The message. It will be prefixed with the constant `MESSAGE_PREFIX`, and + * also suffixed with `REQUEST_DONE_SUFFIX` for the reply. + * @param {Object} data + * A JSON object containing data to be delivered to the listeners. + * @returns {Promise} + * A promise that is resolved when the request is done. + */ +function request(mm, message, data) { + const done = once(mm, message + REQUEST_DONE_SUFFIX); + + emit(mm, message, data); + + return done; +} +exports.request = request; diff --git a/devtools/client/responsive/utils/key.js b/devtools/client/responsive/utils/key.js new file mode 100644 index 0000000000..22c7278b2f --- /dev/null +++ b/devtools/client/responsive/utils/key.js @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js"); + +/** + * Helper to check if the provided key matches one of the expected keys. + * Keys will be prefixed with DOM_VK_ and should match a key in KeyCodes. + * + * @param {String} key + * the key to check (can be a keyCode). + * @param {...String} keys + * list of possible keys allowed. + * @return {Boolean} true if the key matches one of the keys. + */ +function isKeyIn(key, ...keys) { + return keys.some(expectedKey => { + return key === KeyCodes["DOM_VK_" + expectedKey]; + }); +} + +exports.isKeyIn = isKeyIn; diff --git a/devtools/client/responsive/utils/l10n.js b/devtools/client/responsive/utils/l10n.js new file mode 100644 index 0000000000..bccea57040 --- /dev/null +++ b/devtools/client/responsive/utils/l10n.js @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); +const STRINGS_URI = "devtools/client/locales/responsive.properties"; +const L10N = new LocalizationHelper(STRINGS_URI); + +module.exports = { + getStr: (...args) => L10N.getStr(...args), + getFormatStr: (...args) => L10N.getFormatStr(...args), + getFormatStrWithNumbers: (...args) => L10N.getFormatStrWithNumbers(...args), + numberWithDecimals: (...args) => L10N.numberWithDecimals(...args), +}; diff --git a/devtools/client/responsive/utils/message.js b/devtools/client/responsive/utils/message.js new file mode 100644 index 0000000000..d06f95ab27 --- /dev/null +++ b/devtools/client/responsive/utils/message.js @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const REQUEST_DONE_SUFFIX = ":done"; + +function wait(win, type) { + return new Promise(resolve => { + const onMessage = event => { + if (event.data.type !== type) { + return; + } + win.removeEventListener("message", onMessage); + resolve(); + }; + win.addEventListener("message", onMessage); + }); +} + +/** + * Post a message to some window. + * + * @param win + * The window to post to. + * @param typeOrMessage + * Either a string or and an object representing the message to send. + * If this is a string, it will be expanded into an object with the string as the + * `type` field. If this is an object, it will be sent as is. + */ +function post(win, typeOrMessage) { + // When running unit tests on XPCShell, there is no window to send messages to. + if (!win) { + return; + } + + let message = typeOrMessage; + if (typeof typeOrMessage == "string") { + message = { + type: typeOrMessage, + }; + } + win.postMessage(message, "*"); +} + +function request(win, type) { + const done = wait(win, type + REQUEST_DONE_SUFFIX); + post(win, type); + return done; +} + +exports.wait = wait; +exports.post = post; +exports.request = request; diff --git a/devtools/client/responsive/utils/moz.build b/devtools/client/responsive/utils/moz.build new file mode 100644 index 0000000000..503da8932a --- /dev/null +++ b/devtools/client/responsive/utils/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "e10s.js", + "key.js", + "l10n.js", + "message.js", + "notification.js", + "orientation.js", + "ua.js", + "window.js", +) diff --git a/devtools/client/responsive/utils/notification.js b/devtools/client/responsive/utils/notification.js new file mode 100644 index 0000000000..a261fe4252 --- /dev/null +++ b/devtools/client/responsive/utils/notification.js @@ -0,0 +1,60 @@ +/* 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"; + +loader.lazyRequireGetter( + this, + "gDevTools", + "resource://devtools/client/framework/devtools.js", + true +); + +/** + * Displays a notification either at the browser or toolbox level, depending on whether + * a toolbox is currently open for this tab. + * + * @param window + * The main browser chrome window. + * @param tab + * The browser tab. + * @param options + * Other options associated with opening. Currently includes: + * - `toolbox`: Whether initiated via toolbox button + * - `msg`: String to show in the notification + * - `priority`: Priority level for the notification, which affects the icon and + * overall appearance. + */ +async function showNotification( + window, + tab, + { toolboxButton, msg, priority } = {} +) { + // Default to using the browser's per-tab notification box + let nbox = window.gBrowser.getNotificationBox(tab.linkedBrowser); + + // If opening was initiated by a toolbox button, check for an open + // toolbox for the tab. If one exists, use the toolbox's notification box so that the + // message is placed closer to the action taken by the user. + if (toolboxButton) { + const toolbox = gDevTools.getToolboxForTab(tab); + if (toolbox) { + nbox = toolbox.notificationBox; + } + } + + const value = "devtools-responsive"; + if (nbox.getNotificationWithValue(value)) { + // Notification already displayed + return; + } + + if (!priority) { + priority = nbox.PRIORITY_INFO_MEDIUM; + } + + nbox.appendNotification(value, { label: msg, priority }); +} + +exports.showNotification = showNotification; diff --git a/devtools/client/responsive/utils/orientation.js b/devtools/client/responsive/utils/orientation.js new file mode 100644 index 0000000000..500eab8faa --- /dev/null +++ b/devtools/client/responsive/utils/orientation.js @@ -0,0 +1,76 @@ +/* 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 { + PORTRAIT_PRIMARY, + LANDSCAPE_PRIMARY, +} = require("resource://devtools/client/responsive/constants.js"); + +/** + * Helper that gets the screen orientation of the device displayed in the RDM viewport. + * This function take in both a device and viewport object and an optional rotated angle. + * If a rotated angle is passed, then we calculate what the orientation type of the device + * would be in relation to its current orientation. Otherwise, return the current + * orientation and angle. + * + * @param {Object} device + * The device whose content is displayed in the viewport. Used to determine the + * primary orientation. + * @param {Object} viewport + * The viewport displaying device content. Used to determine the current + * orientation type of the device while in RDM. + * @param {Number|null} angleToRotateTo + * Optional. The rotated angle specifies the degree to which the device WILL be + * turned to. If undefined, then only return the current orientation and angle + * of the device. + * @return {Object} the orientation of the device. + */ +function getOrientation(device, viewport, angleToRotateTo = null) { + const { width: deviceWidth, height: deviceHeight } = device; + const { width: viewportWidth, height: viewportHeight } = viewport; + + // Determine the primary orientation of the device screen. + const primaryOrientation = + deviceHeight >= deviceWidth ? PORTRAIT_PRIMARY : LANDSCAPE_PRIMARY; + + // Determine the current orientation of the device screen. + const currentOrientation = + viewportHeight >= viewportWidth ? PORTRAIT_PRIMARY : LANDSCAPE_PRIMARY; + + // Calculate the orientation angle of the device. + let angle; + + if (typeof angleToRotateTo === "number") { + angle = angleToRotateTo; + } else if (currentOrientation !== primaryOrientation) { + angle = 90; + } else { + angle = 0; + } + + // Calculate the orientation type of the device. + let orientationType = currentOrientation; + + // If the viewport orientation is different from the primary orientation and the angle + // to rotate to is 0, then we are moving the device orientation back to its primary + // orientation. + if (currentOrientation !== primaryOrientation && angleToRotateTo === 0) { + orientationType = primaryOrientation; + } else if (angleToRotateTo === 90 || angleToRotateTo === 270) { + if (currentOrientation.includes("portrait")) { + orientationType = LANDSCAPE_PRIMARY; + } else if (currentOrientation.includes("landscape")) { + orientationType = PORTRAIT_PRIMARY; + } + } + + return { + type: orientationType, + angle, + }; +} + +exports.getOrientation = getOrientation; diff --git a/devtools/client/responsive/utils/ua.js b/devtools/client/responsive/utils/ua.js new file mode 100644 index 0000000000..70c99c552d --- /dev/null +++ b/devtools/client/responsive/utils/ua.js @@ -0,0 +1,129 @@ +/* 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 BROWSERS = [ + { + name: "Firefox", + mustContain: new RegExp(`(?:Firefox|FxiOS)\/(${getVersionRegex(1, 1)})`), + }, + { + name: "Opera", + mustContain: new RegExp(`(?:OPR|Opera)\/(${getVersionRegex(1, 1)})`), + }, + { + name: "Safari", + mustContain: new RegExp(`Version\/(${getVersionRegex(1, 1)}).+Safari`), + mustNotContain: new RegExp("Chrome|Chromium"), + }, + { + name: "Edge", + mustContain: new RegExp(`Edge\/(${getVersionRegex(0, 1)})`), + }, + { + name: "Chrome", + mustContain: new RegExp(`(?:Chrome|CriOS)\/(${getVersionRegex(1, 1)})`), + }, + { + name: "IE", + mustContain: new RegExp(`MSIE (${getVersionRegex(1, 1)})`), + }, +]; + +const OSES = [ + { + name: "iOS", + minMinorVersionCount: 0, + mustContain: new RegExp(`CPU iPhone OS (${getVersionRegex(0, 2)})`), + }, + { + name: "iPadOS", + minMinorVersionCount: 0, + mustContain: new RegExp(`CPU OS (${getVersionRegex(0, 2)})`), + }, + { + name: "Windows Phone", + minMinorVersionCount: 1, + mustContain: new RegExp(`Windows Phone (${getVersionRegex(1, 2)})`), + }, + { + name: "Chrome OS", + minMinorVersionCount: 1, + mustContain: new RegExp(`CrOS .+ (${getVersionRegex(1, 2)})`), + }, + { + name: "Android", + minMinorVersionCount: 0, + mustContain: new RegExp(`Android (${getVersionRegex(0, 2)})`), + }, + { + name: "Windows NT", + minMinorVersionCount: 1, + mustContain: new RegExp(`Windows NT (${getVersionRegex(1, 2)})`), + }, + { + name: "Mac OSX", + minMinorVersionCount: 1, + mustContain: new RegExp(`Intel Mac OS X (${getVersionRegex(1, 2)})`), + }, + { + name: "Linux", + mustContain: new RegExp("Linux"), + }, +]; + +function getVersionRegex(minMinorVersionCount, maxMinorVersionCount) { + return `\\d+(?:[._][0-9a-z]+){${minMinorVersionCount},${maxMinorVersionCount}}`; +} + +function detect(ua, dataset) { + for (const { + name, + mustContain, + mustNotContain, + minMinorVersionCount, + } of dataset) { + const result = mustContain.exec(ua); + + if (!result) { + continue; + } + + if (mustNotContain && mustNotContain.test(ua)) { + continue; + } + + let version = null; + + if (result && result.length === 2) { + // Remove most minor version if that expresses 0. + let parts = result[1].match(/([0-9a-z]+)/g); + parts = parts.reverse(); + const validVersionIndex = parts.findIndex( + part => parseInt(part, 10) !== 0 + ); + if (validVersionIndex !== -1) { + parts = parts.splice(validVersionIndex); + for (let i = 0; i < minMinorVersionCount + 1 - parts.length; i++) { + parts.unshift(0); + } + } + version = parts.reverse().join("."); + } + + return { name, version }; + } + + return null; +} + +function parseUserAgent(ua) { + return { + browser: detect(ua, BROWSERS), + os: detect(ua, OSES), + }; +} + +module.exports = { parseUserAgent }; diff --git a/devtools/client/responsive/utils/window.js b/devtools/client/responsive/utils/window.js new file mode 100644 index 0000000000..f9bdaf0879 --- /dev/null +++ b/devtools/client/responsive/utils/window.js @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Returns the `nsIDOMWindow` toplevel window for any child/inner window + */ +function getTopLevelWindow(window) { + return window.browsingContext.topChromeWindow; +} +exports.getTopLevelWindow = getTopLevelWindow; + +function getDOMWindowUtils(window) { + return window.windowUtils; +} +exports.getDOMWindowUtils = getDOMWindowUtils; + +/** + * Check if the given browser window has finished the startup. + * @params {nsIDOMWindow} window + */ +const isStartupFinished = window => window.gBrowserInit?.delayedStartupFinished; + +function startup(window) { + return new Promise(resolve => { + if (isStartupFinished(window)) { + resolve(window); + return; + } + Services.obs.addObserver(function listener({ subject }) { + if (subject === window) { + Services.obs.removeObserver( + listener, + "browser-delayed-startup-finished" + ); + resolve(window); + } + }, "browser-delayed-startup-finished"); + }); +} +exports.startup = startup; |