diff options
Diffstat (limited to 'toolkit/jetpack/sdk/window')
-rw-r--r-- | toolkit/jetpack/sdk/window/browser.js | 54 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/window/events.js | 68 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/window/helpers.js | 81 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/window/namespace.js | 6 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/window/utils.js | 460 |
5 files changed, 669 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/window/browser.js b/toolkit/jetpack/sdk/window/browser.js new file mode 100644 index 000000000..380b5a486 --- /dev/null +++ b/toolkit/jetpack/sdk/window/browser.js @@ -0,0 +1,54 @@ +/* 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 { Class } = require('../core/heritage'); +const { windowNS } = require('./namespace'); +const { on, off, once } = require('../event/core'); +const { method } = require('../lang/functional'); +const { getWindowTitle } = require('./utils'); +const unload = require('../system/unload'); +const { EventTarget } = require('../event/target'); +const { isPrivate } = require('../private-browsing/utils'); +const { isWindowPrivate, isFocused } = require('../window/utils'); +const { viewFor } = require('../view/core'); + +const ERR_FENNEC_MSG = 'This method is not yet supported by Fennec, consider using require("sdk/tabs") instead'; + +const BrowserWindow = Class({ + initialize: function initialize(options) { + EventTarget.prototype.initialize.call(this, options); + windowNS(this).window = options.window; + }, + activate: function activate() { + // TODO + return null; + }, + close: function() { + throw new Error(ERR_FENNEC_MSG); + return null; + }, + get title() { + return getWindowTitle(windowNS(this).window); + }, + // NOTE: Fennec only has one window, which is assumed below + // TODO: remove assumption below + // NOTE: tabs requires windows + get tabs() { + return require('../tabs'); + }, + get activeTab() { + return require('../tabs').activeTab; + }, + on: method(on), + removeListener: method(off), + once: method(once) +}); +exports.BrowserWindow = BrowserWindow; + +const getWindowView = window => windowNS(window).window; + +viewFor.define(BrowserWindow, getWindowView); +isPrivate.define(BrowserWindow, (window) => isWindowPrivate(viewFor(window).window)); +isFocused.define(BrowserWindow, (window) => isFocused(viewFor(window).window)); diff --git a/toolkit/jetpack/sdk/window/events.js b/toolkit/jetpack/sdk/window/events.js new file mode 100644 index 000000000..b1d3a1f3e --- /dev/null +++ b/toolkit/jetpack/sdk/window/events.js @@ -0,0 +1,68 @@ +/* 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"; + +module.metadata = { + "stability": "unstable" +}; + +const { Ci, Cu } = require("chrome"); +const { observe } = require("../event/chrome"); +const { open } = require("../event/dom"); +const { windows } = require("../window/utils"); +const { filter, merge, map, expand } = require("../event/utils"); + +function documentMatches(weakWindow, event) { + let window = weakWindow.get(); + return window && event.target === window.document; +} + +function makeStrictDocumentFilter(window) { + // Note: Do not define a closure within this function. Otherwise + // you may leak the window argument. + let weak = Cu.getWeakReference(window); + return documentMatches.bind(null, weak); +} + +function toEventWithDefaultViewTarget({type, target}) { + return { type: type, target: target.defaultView } +} + +// Function registers single shot event listeners for relevant window events +// that forward events to exported event stream. +function eventsFor(window) { + // NOTE: Do no use pass a closure from this function into a stream + // transform function. You will capture the window in the + // closure and leak the window until the event stream is + // completely closed. + let interactive = open(window, "DOMContentLoaded", { capture: true }); + let complete = open(window, "load", { capture: true }); + let states = merge([interactive, complete]); + let changes = filter(states, makeStrictDocumentFilter(window)); + return map(changes, toEventWithDefaultViewTarget); +} + +// Create our event channels. We do this in a separate function to +// minimize the chance of leaking intermediate objects on the global. +function makeEvents() { + // In addition to observing windows that are open we also observe windows + // that are already already opened in case they're in process of loading. + var opened = windows(null, { includePrivate: true }); + var currentEvents = merge(opened.map(eventsFor)); + + // Register system event listeners for top level window open / close. + function rename({type, target, data}) { + return { type: rename[type], target: target, data: data } + } + rename.domwindowopened = "open"; + rename.domwindowclosed = "close"; + + var openEvents = map(observe("domwindowopened"), rename); + var closeEvents = map(observe("domwindowclosed"), rename); + var futureEvents = expand(openEvents, ({target}) => eventsFor(target)); + + return merge([currentEvents, futureEvents, openEvents, closeEvents]); +} + +exports.events = makeEvents(); diff --git a/toolkit/jetpack/sdk/window/helpers.js b/toolkit/jetpack/sdk/window/helpers.js new file mode 100644 index 000000000..56cfcaba7 --- /dev/null +++ b/toolkit/jetpack/sdk/window/helpers.js @@ -0,0 +1,81 @@ +/* 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 { defer, all } = require('../core/promise'); +const events = require('../system/events'); +const { open: openWindow, onFocus, getToplevelWindow, + isInteractive, isStartupFinished, getOuterId } = require('./utils'); +const { Ci } = require("chrome"); + +function open(uri, options) { + return promise(openWindow.apply(null, arguments), 'load').then(focus); +} +exports.open = open; + +function close(window) { + let deferred = defer(); + let toplevelWindow = getToplevelWindow(window); + let outerId = getOuterId(toplevelWindow); + events.on("outer-window-destroyed", function onclose({subject}) { + let id = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + if (id == outerId) { + events.off("outer-window-destroyed", onclose); + deferred.resolve(); + } + }, true); + window.close(); + return deferred.promise; +} +exports.close = close; + +function focus(window) { + let p = onFocus(window); + window.focus(); + return p; +} +exports.focus = focus; + +function ready(window) { + let { promise: result, resolve } = defer(); + + if (isInteractive(window)) + resolve(window); + else + resolve(promise(window, 'DOMContentLoaded')); + + return result; +} +exports.ready = ready; + +function startup(window) { + let { promise: result, resolve } = defer(); + + if (isStartupFinished(window)) { + resolve(window); + } else { + events.on("browser-delayed-startup-finished", function listener({subject}) { + if (subject === window) { + events.off("browser-delayed-startup-finished", listener); + resolve(window); + } + }); + } + + return result; +} +exports.startup = startup; + +function promise(target, evt, capture) { + let deferred = defer(); + capture = !!capture; + + target.addEventListener(evt, function eventHandler() { + target.removeEventListener(evt, eventHandler, capture); + deferred.resolve(target); + }, capture); + + return deferred.promise; +} +exports.promise = promise; diff --git a/toolkit/jetpack/sdk/window/namespace.js b/toolkit/jetpack/sdk/window/namespace.js new file mode 100644 index 000000000..b486f888d --- /dev/null +++ b/toolkit/jetpack/sdk/window/namespace.js @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +exports.windowNS = require('../core/namespace').ns(); diff --git a/toolkit/jetpack/sdk/window/utils.js b/toolkit/jetpack/sdk/window/utils.js new file mode 100644 index 000000000..db91a0fed --- /dev/null +++ b/toolkit/jetpack/sdk/window/utils.js @@ -0,0 +1,460 @@ +/* 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'; + +module.metadata = { + 'stability': 'unstable' +}; + +const { Cc, Ci } = require('chrome'); +const array = require('../util/array'); +const { defer } = require('sdk/core/promise'); +const { dispatcher } = require("../util/dispatcher"); + +const windowWatcher = Cc['@mozilla.org/embedcomp/window-watcher;1']. + getService(Ci.nsIWindowWatcher); +const appShellService = Cc['@mozilla.org/appshell/appShellService;1']. + getService(Ci.nsIAppShellService); +const WM = Cc['@mozilla.org/appshell/window-mediator;1']. + getService(Ci.nsIWindowMediator); +const io = Cc['@mozilla.org/network/io-service;1']. + getService(Ci.nsIIOService); +const FM = Cc["@mozilla.org/focus-manager;1"]. + getService(Ci.nsIFocusManager); + +const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; + +const prefs = require("../preferences/service"); +const BROWSER = 'navigator:browser', + URI_BROWSER = prefs.get('browser.chromeURL', null), + NAME = '_blank', + FEATURES = 'chrome,all,dialog=no,non-private'; + +function isWindowPrivate(win) { + if (!win) + return false; + + // if the pbService is undefined, the PrivateBrowsingUtils.jsm is available, + // and the app is Firefox, then assume per-window private browsing is + // enabled. + try { + return win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext) + .usePrivateBrowsing; + } + catch(e) {} + + // Sometimes the input is not a nsIDOMWindow.. but it is still a winodw. + try { + return !!win.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing; + } + catch (e) {} + + return false; +} +exports.isWindowPrivate = isWindowPrivate; + +function getMostRecentBrowserWindow() { + return getMostRecentWindow(BROWSER); +} +exports.getMostRecentBrowserWindow = getMostRecentBrowserWindow; + +function getHiddenWindow() { + return appShellService.hiddenDOMWindow; +} +exports.getHiddenWindow = getHiddenWindow; + +function getMostRecentWindow(type) { + return WM.getMostRecentWindow(type); +} +exports.getMostRecentWindow = getMostRecentWindow; + +/** + * Returns the ID of the window's current inner window. + */ +function getInnerId(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; +}; +exports.getInnerId = getInnerId; + +/** + * Returns the ID of the window's outer window. + */ +function getOuterId(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils).outerWindowID; +}; +exports.getOuterId = getOuterId; + +/** + * Returns window by the outer window id. + */ +const getByOuterId = WM.getOuterWindowWithId; +exports.getByOuterId = getByOuterId; + +const getByInnerId = WM.getCurrentInnerWindowWithId; +exports.getByInnerId = getByInnerId; + +/** + * Returns `nsIXULWindow` for the given `nsIDOMWindow`. + */ +function getXULWindow(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebNavigation). + QueryInterface(Ci.nsIDocShellTreeItem). + treeOwner.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIXULWindow); +}; +exports.getXULWindow = getXULWindow; + +function getDOMWindow(xulWindow) { + return xulWindow.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindow); +} +exports.getDOMWindow = getDOMWindow; + +/** + * Returns `nsIBaseWindow` for the given `nsIDOMWindow`. + */ +function getBaseWindow(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebNavigation). + QueryInterface(Ci.nsIDocShell). + QueryInterface(Ci.nsIDocShellTreeItem). + treeOwner. + QueryInterface(Ci.nsIBaseWindow); +} +exports.getBaseWindow = getBaseWindow; + +/** + * Returns the `nsIDOMWindow` toplevel window for any child/inner window + */ +function getToplevelWindow(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); +} +exports.getToplevelWindow = getToplevelWindow; + +function getWindowDocShell(window) { + return window.gBrowser.docShell; +} +exports.getWindowDocShell = getWindowDocShell; + +function getWindowLoadingContext(window) { + return getWindowDocShell(window). + QueryInterface(Ci.nsILoadContext); +} +exports.getWindowLoadingContext = getWindowLoadingContext; + +const isTopLevel = window => window && getToplevelWindow(window) === window; +exports.isTopLevel = isTopLevel; + +/** + * Takes hash of options and serializes it to a features string that + * can be used passed to `window.open`. For more details on features string see: + * https://developer.mozilla.org/en/DOM/window.open#Position_and_size_features + */ +function serializeFeatures(options) { + return Object.keys(options).reduce(function(result, name) { + let value = options[name]; + + // the chrome and private features are special + if ((name == 'private' || name == 'chrome' || name == 'all')) + return result + ((value === true) ? ',' + name : ''); + + return result + ',' + name + '=' + + (value === true ? 'yes' : value === false ? 'no' : value); + }, '').substr(1); +} + +/** + * Opens a top level window and returns it's `nsIDOMWindow` representation. + * @params {String} uri + * URI of the document to be loaded into window. + * @params {nsIDOMWindow} options.parent + * Used as parent for the created window. + * @params {String} options.name + * Optional name that is assigned to the window. + * @params {Object} options.features + * Map of key, values like: `{ width: 10, height: 15, chrome: true, private: true }`. + */ +function open(uri, options) { + uri = uri || URI_BROWSER; + options = options || {}; + + if (!uri) + throw new Error('browser.chromeURL is undefined, please provide an explicit uri'); + + if (['chrome', 'resource', 'data'].indexOf(io.newURI(uri, null, null).scheme) < 0) + throw new Error('only chrome, resource and data uris are allowed'); + + let newWindow = windowWatcher. + openWindow(options.parent || null, + uri, + options.name || null, + options.features ? serializeFeatures(options.features) : null, + options.args || null); + + return newWindow; +} +exports.open = open; + +function onFocus(window) { + let { resolve, promise } = defer(); + + if (isFocused(window)) { + resolve(window); + } + else { + window.addEventListener("focus", function focusListener() { + window.removeEventListener("focus", focusListener, true); + resolve(window); + }, true); + } + + return promise; +} +exports.onFocus = onFocus; + +var isFocused = dispatcher("window-isFocused"); +isFocused.when(x => x instanceof Ci.nsIDOMWindow, (window) => { + const FM = Cc["@mozilla.org/focus-manager;1"]. + getService(Ci.nsIFocusManager); + + let childTargetWindow = {}; + FM.getFocusedElementForWindow(window, true, childTargetWindow); + childTargetWindow = childTargetWindow.value; + + let focusedChildWindow = {}; + if (FM.activeWindow) { + FM.getFocusedElementForWindow(FM.activeWindow, true, focusedChildWindow); + focusedChildWindow = focusedChildWindow.value; + } + + return (focusedChildWindow === childTargetWindow); +}); +exports.isFocused = isFocused; + +/** + * Opens a top level window and returns it's `nsIDOMWindow` representation. + * Same as `open` but with more features + * @param {Object} options + * + */ +function openDialog(options) { + options = options || {}; + + let features = options.features || FEATURES; + let featureAry = features.toLowerCase().split(','); + + if (!!options.private) { + // add private flag if private window is desired + if (!array.has(featureAry, 'private')) { + featureAry.push('private'); + } + + // remove the non-private flag ig a private window is desired + let nonPrivateIndex = featureAry.indexOf('non-private'); + if (nonPrivateIndex >= 0) { + featureAry.splice(nonPrivateIndex, 1); + } + + features = featureAry.join(','); + } + + let browser = getMostRecentBrowserWindow(); + + // if there is no browser then do nothing + if (!browser) + return undefined; + + let newWindow = browser.openDialog.apply( + browser, + array.flatten([ + options.url || URI_BROWSER, + options.name || NAME, + features, + options.args || null + ]) + ); + + return newWindow; +} +exports.openDialog = openDialog; + +/** + * Returns an array of all currently opened windows. + * Note that these windows may still be loading. + */ +function windows(type, options) { + options = options || {}; + let list = []; + let winEnum = WM.getEnumerator(type); + while (winEnum.hasMoreElements()) { + let window = winEnum.getNext().QueryInterface(Ci.nsIDOMWindow); + // Only add non-private windows when pb permission isn't set, + // unless an option forces the addition of them. + if (!window.closed && (options.includePrivate || !isWindowPrivate(window))) { + list.push(window); + } + } + return list; +} +exports.windows = windows; + +/** + * Check if the given window is interactive. + * i.e. if its "DOMContentLoaded" event has already been fired. + * @params {nsIDOMWindow} window + */ +const isInteractive = window => + window.document.readyState === "interactive" || + isDocumentLoaded(window) || + // XUL documents stays '"uninitialized"' until it's `readyState` becomes + // `"complete"`. + isXULDocumentWindow(window) && window.document.readyState === "interactive"; +exports.isInteractive = isInteractive; + +/** + * Check if the given browser window has finished the startup. + * @params {nsIDOMWindow} window + */ +const isStartupFinished = (window) => + isBrowser(window) && + window.gBrowserInit && + window.gBrowserInit.delayedStartupFinished; + +exports.isStartupFinished = isStartupFinished; + +const isXULDocumentWindow = ({document}) => + document.documentElement && + document.documentElement.namespaceURI === XUL_NS; + +/** + * Check if the given window is completely loaded. + * i.e. if its "load" event has already been fired and all possible DOM content + * is done loading (the whole DOM document, images content, ...) + * @params {nsIDOMWindow} window + */ +function isDocumentLoaded(window) { + return window.document.readyState == "complete"; +} +exports.isDocumentLoaded = isDocumentLoaded; + +function isBrowser(window) { + try { + return window.document.documentElement.getAttribute("windowtype") === BROWSER; + } + catch (e) {} + return false; +}; +exports.isBrowser = isBrowser; + +function getWindowTitle(window) { + return window && window.document ? window.document.title : null; +} +exports.getWindowTitle = getWindowTitle; + +function isXULBrowser(window) { + return !!(isBrowser(window) && window.XULBrowserWindow); +} +exports.isXULBrowser = isXULBrowser; + +/** + * Returns the most recent focused window + */ +function getFocusedWindow() { + let window = WM.getMostRecentWindow(BROWSER); + + return window ? window.document.commandDispatcher.focusedWindow : null; +} +exports.getFocusedWindow = getFocusedWindow; + +/** + * Returns the focused browser window if any, or the most recent one. + * Opening new window, updates most recent window, but focus window + * changes later; so most recent window and focused window are not always + * the same. + */ +function getFocusedBrowser() { + let window = FM.activeWindow; + return isBrowser(window) ? window : getMostRecentBrowserWindow() +} +exports.getFocusedBrowser = getFocusedBrowser; + +/** + * Returns the focused element in the most recent focused window + */ +function getFocusedElement() { + let window = WM.getMostRecentWindow(BROWSER); + + return window ? window.document.commandDispatcher.focusedElement : null; +} +exports.getFocusedElement = getFocusedElement; + +function getFrames(window) { + return Array.slice(window.frames).reduce(function(frames, frame) { + return frames.concat(frame, getFrames(frame)); + }, []); +} +exports.getFrames = getFrames; + +function getScreenPixelsPerCSSPixel(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils).screenPixelsPerCSSPixel; +} +exports.getScreenPixelsPerCSSPixel = getScreenPixelsPerCSSPixel; + +function getOwnerBrowserWindow(node) { + /** + Takes DOM node and returns browser window that contains it. + **/ + let window = getToplevelWindow(node.ownerDocument.defaultView); + // If anchored window is browser then it's target browser window. + return isBrowser(window) ? window : null; +} +exports.getOwnerBrowserWindow = getOwnerBrowserWindow; + +function getParentWindow(window) { + try { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem).parent + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + } + catch (e) {} + return null; +} +exports.getParentWindow = getParentWindow; + + +function getParentFrame(window) { + try { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem).parent + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + } + catch (e) {} + return null; +} +exports.getParentWindow = getParentWindow; + +// The element in which the window is embedded, or `null` +// if the window is top-level. Similar to `window.frameElement` +// but can cross chrome-content boundries. +const getFrameElement = target => + (target instanceof Ci.nsIDOMDocument ? target.defaultView : target). + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils). + containerElement; +exports.getFrameElement = getFrameElement; |