diff options
author | Matt A. Tobin <email@mattatobin.com> | 2018-02-10 02:51:36 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2018-02-10 02:51:36 -0500 |
commit | 37d5300335d81cecbecc99812747a657588c63eb (patch) | |
tree | 765efa3b6a56bb715d9813a8697473e120436278 /toolkit/jetpack/sdk/tabs | |
parent | b2bdac20c02b12f2057b9ef70b0a946113a00e00 (diff) | |
parent | 4fb11cd5966461bccc3ed1599b808237be6b0de9 (diff) | |
download | UXP-37d5300335d81cecbecc99812747a657588c63eb.tar UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.gz UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.lz UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.xz UXP-37d5300335d81cecbecc99812747a657588c63eb.zip |
Merge branch 'ext-work'
Diffstat (limited to 'toolkit/jetpack/sdk/tabs')
-rw-r--r-- | toolkit/jetpack/sdk/tabs/common.js | 34 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/tabs/events.js | 39 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/tabs/helpers.js | 22 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/tabs/namespace.js | 10 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/tabs/observer.js | 113 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/tabs/tab-fennec.js | 249 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/tabs/tab-firefox.js | 353 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/tabs/tab.js | 24 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/tabs/tabs-firefox.js | 135 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/tabs/utils.js | 370 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/tabs/worker.js | 17 |
11 files changed, 1366 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/tabs/common.js b/toolkit/jetpack/sdk/tabs/common.js new file mode 100644 index 000000000..9ee512a7b --- /dev/null +++ b/toolkit/jetpack/sdk/tabs/common.js @@ -0,0 +1,34 @@ +/* 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 { validateOptions } = require("../deprecated/api-utils"); +const { data } = require("../self"); + +function Options(options) { + if ('string' === typeof options) + options = { url: options }; + + return validateOptions(options, { + url: { + is: ["string"], + map: (v) => v ? data.url(v) : v + }, + inBackground: { + map: Boolean, + is: ["undefined", "boolean"] + }, + isPinned: { is: ["undefined", "boolean"] }, + isPrivate: { is: ["undefined", "boolean"] }, + inNewWindow: { is: ["undefined", "boolean"] }, + onOpen: { is: ["undefined", "function"] }, + onClose: { is: ["undefined", "function"] }, + onReady: { is: ["undefined", "function"] }, + onLoad: { is: ["undefined", "function"] }, + onPageShow: { is: ["undefined", "function"] }, + onActivate: { is: ["undefined", "function"] }, + onDeactivate: { is: ["undefined", "function"] } + }); +} +exports.Options = Options; diff --git a/toolkit/jetpack/sdk/tabs/events.js b/toolkit/jetpack/sdk/tabs/events.js new file mode 100644 index 000000000..65650f9dc --- /dev/null +++ b/toolkit/jetpack/sdk/tabs/events.js @@ -0,0 +1,39 @@ +/* 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 ON_PREFIX = "on"; +const TAB_PREFIX = "Tab"; + +const EVENTS = { + ready: "DOMContentLoaded", + load: "load", // Used for non-HTML content + pageshow: "pageshow", // Used for cached content + open: "TabOpen", + close: "TabClose", + activate: "TabSelect", + deactivate: null, + pinned: "TabPinned", + unpinned: "TabUnpinned" +} +exports.EVENTS = EVENTS; + +Object.keys(EVENTS).forEach(function(name) { + EVENTS[name] = { + name: name, + listener: createListenerName(name), + dom: EVENTS[name] + } +}); + +function createListenerName (name) { + if (name === 'pageshow') + return 'onPageShow'; + else + return ON_PREFIX + name.charAt(0).toUpperCase() + name.substr(1); +} diff --git a/toolkit/jetpack/sdk/tabs/helpers.js b/toolkit/jetpack/sdk/tabs/helpers.js new file mode 100644 index 000000000..b2c8aa013 --- /dev/null +++ b/toolkit/jetpack/sdk/tabs/helpers.js @@ -0,0 +1,22 @@ +/* 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' +}; + + +// NOTE: This file should only export Tab instances + + +const { getTabForBrowser: getRawTabForBrowser } = require('./utils'); +const { modelFor } = require('../model/core'); + +exports.getTabForRawTab = modelFor; + +function getTabForBrowser(browser) { + return modelFor(getRawTabForBrowser(browser)) || null; +} +exports.getTabForBrowser = getTabForBrowser; diff --git a/toolkit/jetpack/sdk/tabs/namespace.js b/toolkit/jetpack/sdk/tabs/namespace.js new file mode 100644 index 000000000..3553b1a99 --- /dev/null +++ b/toolkit/jetpack/sdk/tabs/namespace.js @@ -0,0 +1,10 @@ +/* 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'; + +var { ns } = require('../core/namespace'); + +exports.tabsNS = ns(); +exports.tabNS = ns(); +exports.rawTabNS = ns(); diff --git a/toolkit/jetpack/sdk/tabs/observer.js b/toolkit/jetpack/sdk/tabs/observer.js new file mode 100644 index 000000000..4e935cd62 --- /dev/null +++ b/toolkit/jetpack/sdk/tabs/observer.js @@ -0,0 +1,113 @@ +/* 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 { EventTarget } = require("../event/target"); +const { emit } = require("../event/core"); +const { DOMEventAssembler } = require("../deprecated/events/assembler"); +const { Class } = require("../core/heritage"); +const { getActiveTab, getTabs } = require("./utils"); +const { browserWindowIterator } = require("../deprecated/window-utils"); +const { isBrowser, windows, getMostRecentBrowserWindow } = require("../window/utils"); +const { observer: windowObserver } = require("../windows/observer"); +const { when } = require("../system/unload"); + +const EVENTS = { + "TabOpen": "open", + "TabClose": "close", + "TabSelect": "select", + "TabMove": "move", + "TabPinned": "pinned", + "TabUnpinned": "unpinned" +}; + +const selectedTab = Symbol("observer/state/selectedTab"); + +// Event emitter objects used to register listeners and emit events on them +// when they occur. +const Observer = Class({ + implements: [EventTarget, DOMEventAssembler], + initialize() { + this[selectedTab] = null; + // Currently Gecko does not dispatch any event on the previously selected + // tab before / after "TabSelect" is dispatched. In order to work around this + // limitation we keep track of selected tab and emit "deactivate" event with + // that before emitting "activate" on selected tab. + this.on("select", tab => { + const selected = this[selectedTab]; + if (selected !== tab) { + if (selected) { + emit(this, 'deactivate', selected); + } + + if (tab) { + this[selectedTab] = tab; + emit(this, 'activate', this[selectedTab]); + } + } + }); + + + // We also observe opening / closing windows in order to add / remove it's + // containers to the observed list. + windowObserver.on("open", chromeWindow => { + if (isBrowser(chromeWindow)) { + this.observe(chromeWindow); + } + }); + + windowObserver.on("close", chromeWindow => { + if (isBrowser(chromeWindow)) { + // Bug 751546: Emit `deactivate` event on window close immediatly + // Otherwise we are going to face "dead object" exception on `select` event + if (getActiveTab(chromeWindow) === this[selectedTab]) { + emit(this, "deactivate", this[selectedTab]); + this[selectedTab] = null; + } + this.ignore(chromeWindow); + } + }); + + + // Currently gecko does not dispatches "TabSelect" events when different + // window gets activated. To work around this limitation we emulate "select" + // event for this case. + windowObserver.on("activate", chromeWindow => { + if (isBrowser(chromeWindow)) { + emit(this, "select", getActiveTab(chromeWindow)); + } + }); + + // We should synchronize state, since probably we already have at least one + // window open. + for (let chromeWindow of browserWindowIterator()) { + this.observe(chromeWindow); + } + + when(_ => { + // Don't dispatch a deactivate event during unload. + this[selectedTab] = null; + }); + }, + /** + * Events that are supported and emitted by the module. + */ + supportedEventsTypes: Object.keys(EVENTS), + /** + * Function handles all the supported events on all the windows that are + * observed. Method is used to proxy events to the listeners registered on + * this event emitter. + * @param {Event} event + * Keyboard event being emitted. + */ + handleEvent: function handleEvent(event) { + emit(this, EVENTS[event.type], event.target, event); + } +}); + +exports.observer = new Observer(); diff --git a/toolkit/jetpack/sdk/tabs/tab-fennec.js b/toolkit/jetpack/sdk/tabs/tab-fennec.js new file mode 100644 index 000000000..3927337f6 --- /dev/null +++ b/toolkit/jetpack/sdk/tabs/tab-fennec.js @@ -0,0 +1,249 @@ +/* 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 } = require('chrome'); +const { Class } = require('../core/heritage'); +const { tabNS, rawTabNS } = require('./namespace'); +const { EventTarget } = require('../event/target'); +const { activateTab, getTabTitle, setTabTitle, closeTab, getTabURL, + getTabContentWindow, getTabForBrowser, setTabURL, getOwnerWindow, + getTabContentDocument, getTabContentType, getTabId, isTab } = require('./utils'); +const { emit } = require('../event/core'); +const { isPrivate } = require('../private-browsing/utils'); +const { isWindowPrivate } = require('../window/utils'); +const { when: unload } = require('../system/unload'); +const { BLANK } = require('../content/thumbnail'); +const { viewFor } = require('../view/core'); +const { EVENTS } = require('./events'); +const { modelFor } = require('../model/core'); + +const ERR_FENNEC_MSG = 'This method is not yet supported by Fennec'; + +const Tab = Class({ + extends: EventTarget, + initialize: function initialize(options) { + options = options.tab ? options : { tab: options }; + let tab = options.tab; + + EventTarget.prototype.initialize.call(this, options); + let tabInternals = tabNS(this); + rawTabNS(tab).tab = this; + + let window = tabInternals.window = options.window || getOwnerWindow(tab); + tabInternals.tab = tab; + + // TabReady + let onReady = tabInternals.onReady = onTabReady.bind(this); + tab.browser.addEventListener(EVENTS.ready.dom, onReady, false); + + // TabPageShow + let onPageShow = tabInternals.onPageShow = onTabPageShow.bind(this); + tab.browser.addEventListener(EVENTS.pageshow.dom, onPageShow, false); + + // TabLoad + let onLoad = tabInternals.onLoad = onTabLoad.bind(this); + tab.browser.addEventListener(EVENTS.load.dom, onLoad, true); + + // TabClose + let onClose = tabInternals.onClose = onTabClose.bind(this); + window.BrowserApp.deck.addEventListener(EVENTS.close.dom, onClose, false); + + unload(cleanupTab.bind(null, this)); + }, + + /** + * The title of the page currently loaded in the tab. + * Changing this property changes an actual title. + * @type {String} + */ + get title() { + return getTabTitle(tabNS(this).tab); + }, + set title(title) { + setTabTitle(tabNS(this).tab, title); + }, + + /** + * Location of the page currently loaded in this tab. + * Changing this property will loads page under under the specified location. + * @type {String} + */ + get url() { + return tabNS(this).closed ? undefined : getTabURL(tabNS(this).tab); + }, + set url(url) { + setTabURL(tabNS(this).tab, url); + }, + + getThumbnail: function() { + // TODO: implement! + console.error(ERR_FENNEC_MSG); + + // return 80x45 blank default + return BLANK; + }, + + /** + * tab's document readyState, or 'uninitialized' if it doesn't even exist yet. + */ + get readyState() { + let doc = getTabContentDocument(tabNS(this).tab); + return doc && doc.readyState || 'uninitialized'; + }, + + get id() { + return getTabId(tabNS(this).tab); + }, + + /** + * The index of the tab relative to other tabs in the application window. + * Changing this property will change order of the actual position of the tab. + * @type {Number} + */ + get index() { + if (tabNS(this).closed) return undefined; + + let tabs = tabNS(this).window.BrowserApp.tabs; + let tab = tabNS(this).tab; + for (var i = tabs.length; i >= 0; i--) { + if (tabs[i] === tab) + return i; + } + return null; + }, + set index(value) { + console.error(ERR_FENNEC_MSG); // TODO + }, + + /** + * Whether or not tab is pinned (Is an app-tab). + * @type {Boolean} + */ + get isPinned() { + console.error(ERR_FENNEC_MSG); // TODO + return false; // TODO + }, + pin: function pin() { + console.error(ERR_FENNEC_MSG); // TODO + }, + unpin: function unpin() { + console.error(ERR_FENNEC_MSG); // TODO + }, + + /** + * Returns the MIME type that the document loaded in the tab is being + * rendered as. + * @type {String} + */ + get contentType() { + return getTabContentType(tabNS(this).tab); + }, + + /** + * Create a worker for this tab, first argument is options given to Worker. + * @type {Worker} + */ + attach: function attach(options) { + // BUG 792946 https://bugzilla.mozilla.org/show_bug.cgi?id=792946 + // TODO: fix this circular dependency + let { Worker } = require('./worker'); + return Worker(options, getTabContentWindow(tabNS(this).tab)); + }, + + /** + * Make this tab active. + */ + activate: function activate() { + activateTab(tabNS(this).tab, tabNS(this).window); + }, + + /** + * Close the tab + */ + close: function close(callback) { + let tab = this; + this.once(EVENTS.close.name, function () { + tabNS(tab).closed = true; + if (callback) callback(); + }); + + closeTab(tabNS(this).tab); + }, + + /** + * Reload the tab + */ + reload: function reload() { + tabNS(this).tab.browser.reload(); + } +}); +exports.Tab = Tab; + +// Implement `viewFor` polymorphic function for the Tab +// instances. +viewFor.define(Tab, x => tabNS(x).tab); + +function cleanupTab(tab) { + let tabInternals = tabNS(tab); + if (!tabInternals.tab) + return; + + if (tabInternals.tab.browser) { + tabInternals.tab.browser.removeEventListener(EVENTS.ready.dom, tabInternals.onReady, false); + tabInternals.tab.browser.removeEventListener(EVENTS.pageshow.dom, tabInternals.onPageShow, false); + tabInternals.tab.browser.removeEventListener(EVENTS.load.dom, tabInternals.onLoad, true); + } + tabInternals.onReady = null; + tabInternals.onPageShow = null; + tabInternals.onLoad = null; + tabInternals.window.BrowserApp.deck.removeEventListener(EVENTS.close.dom, tabInternals.onClose, false); + tabInternals.onClose = null; + rawTabNS(tabInternals.tab).tab = null; + tabInternals.tab = null; + tabInternals.window = null; +} + +function onTabReady(event) { + let win = event.target.defaultView; + + // ignore frames + if (win === win.top) { + emit(this, 'ready', this); + } +} + +function onTabLoad (event) { + let win = event.target.defaultView; + + // ignore frames + if (win === win.top) { + emit(this, 'load', this); + } +} + +function onTabPageShow(event) { + let win = event.target.defaultView; + if (win === win.top) + emit(this, 'pageshow', this, event.persisted); +} + +// TabClose +function onTabClose(event) { + let rawTab = getTabForBrowser(event.target); + if (tabNS(this).tab !== rawTab) + return; + + emit(this, EVENTS.close.name, this); + cleanupTab(this); +}; + +isPrivate.implement(Tab, tab => { + return isWindowPrivate(getTabContentWindow(tabNS(tab).tab)); +}); + +// Implement `modelFor` function for the Tab instances. +modelFor.when(isTab, rawTab => { + return rawTabNS(rawTab).tab; +}); diff --git a/toolkit/jetpack/sdk/tabs/tab-firefox.js b/toolkit/jetpack/sdk/tabs/tab-firefox.js new file mode 100644 index 000000000..f1da92379 --- /dev/null +++ b/toolkit/jetpack/sdk/tabs/tab-firefox.js @@ -0,0 +1,353 @@ +/* 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 { observer } = require('./observer'); +const { observer: windowObserver } = require('../windows/observer'); +const { addListItem, removeListItem } = require('../util/list'); +const { viewFor } = require('../view/core'); +const { modelFor } = require('../model/core'); +const { emit, setListeners } = require('../event/core'); +const { EventTarget } = require('../event/target'); +const { getBrowserForTab, setTabURL, getTabId, getTabURL, getTabForBrowser, + getTabs, getTabTitle, setTabTitle, getIndex, closeTab, reload, move, + activateTab, pin, unpin, isTab } = require('./utils'); +const { isBrowser, getInnerId, isWindowPrivate } = require('../window/utils'); +const { getThumbnailURIForWindow, BLANK } = require("../content/thumbnail"); +const { when } = require('../system/unload'); +const { ignoreWindow, isPrivate } = require('../private-browsing/utils') +const { defer } = require('../lang/functional'); +const { getURL } = require('../url/utils'); +const { frames, remoteRequire } = require('../remote/parent'); +remoteRequire('sdk/content/tab-events'); + +const modelsFor = new WeakMap(); +const viewsFor = new WeakMap(); +const destroyed = new WeakMap(); + +const tabEvents = {}; +exports.tabEvents = tabEvents; + +function browser(tab) { + return getBrowserForTab(viewsFor.get(tab)); +} + +function isDestroyed(tab) { + return destroyed.has(tab); +} + +function isClosed(tab) { + if (!viewsFor.has(tab)) + return true; + return viewsFor.get(tab).closing; +} + +// private tab attribute where the remote cached value is stored +const remoteReadyStateCached = Symbol("remoteReadyStateCached"); + +const Tab = Class({ + implements: [EventTarget], + initialize: function(tabElement, options = null) { + modelsFor.set(tabElement, this); + viewsFor.set(this, tabElement); + + if (options) { + EventTarget.prototype.initialize.call(this, options); + + if (options.isPinned) + this.pin(); + + // Note that activate is defered and so will run after any open event + // is sent out + if (!options.inBackground) + this.activate(); + } + + getURL.implement(this, tab => tab.url); + isPrivate.implement(this, tab => { + return isWindowPrivate(viewsFor.get(tab).ownerDocument.defaultView); + }); + }, + + get id() { + return isDestroyed(this) ? undefined : getTabId(viewsFor.get(this)); + }, + + get title() { + return isDestroyed(this) ? undefined : getTabTitle(viewsFor.get(this)); + }, + + set title(val) { + if (isDestroyed(this)) + return; + + setTabTitle(viewsFor.get(this), val); + }, + + get url() { + return isDestroyed(this) ? undefined : getTabURL(viewsFor.get(this)); + }, + + set url(val) { + if (isDestroyed(this)) + return; + + setTabURL(viewsFor.get(this), val); + }, + + get contentType() { + return isDestroyed(this) ? undefined : browser(this).documentContentType; + }, + + get index() { + return isDestroyed(this) ? undefined : getIndex(viewsFor.get(this)); + }, + + set index(val) { + if (isDestroyed(this)) + return; + + move(viewsFor.get(this), val); + }, + + get isPinned() { + return isDestroyed(this) ? undefined : viewsFor.get(this).pinned; + }, + + get window() { + if (isClosed(this)) + return undefined; + + // TODO: Remove the dependency on the windows module, see bug 792670 + require('../windows'); + let tabElement = viewsFor.get(this); + let domWindow = tabElement.ownerDocument.defaultView; + return modelFor(domWindow); + }, + + get readyState() { + return isDestroyed(this) ? undefined : this[remoteReadyStateCached] || "uninitialized"; + }, + + pin: function() { + if (isDestroyed(this)) + return; + + pin(viewsFor.get(this)); + }, + + unpin: function() { + if (isDestroyed(this)) + return; + + unpin(viewsFor.get(this)); + }, + + close: function(callback) { + let tabElement = viewsFor.get(this); + + if (isDestroyed(this) || !tabElement || !tabElement.parentNode) { + if (callback) + callback(); + return; + } + + this.once('close', () => { + this.destroy(); + if (callback) + callback(); + }); + + closeTab(tabElement); + }, + + reload: function() { + if (isDestroyed(this)) + return; + + reload(viewsFor.get(this)); + }, + + activate: defer(function() { + if (isDestroyed(this)) + return; + + activateTab(viewsFor.get(this)); + }), + + getThumbnail: function() { + if (isDestroyed(this)) + return BLANK; + + // TODO: This is unimplemented in e10s: bug 1148601 + if (browser(this).isRemoteBrowser) { + console.error('This method is not supported with E10S'); + return BLANK; + } + return getThumbnailURIForWindow(browser(this).contentWindow); + }, + + attach: function(options) { + if (isDestroyed(this)) + return; + + let { Worker } = require('../content/worker'); + let { connect, makeChildOptions } = require('../content/utils'); + + let worker = Worker(options); + worker.once("detach", () => { + worker.destroy(); + }); + + let attach = frame => { + let childOptions = makeChildOptions(options); + frame.port.emit("sdk/tab/attach", childOptions); + connect(worker, frame, { id: childOptions.id, url: this.url }); + }; + + // Do this synchronously if possible + let frame = frames.getFrameForBrowser(browser(this)); + if (frame) { + attach(frame); + } + else { + let listener = (frame) => { + if (frame.frameElement != browser(this)) + return; + + frames.off("attach", listener); + attach(frame); + }; + frames.on("attach", listener); + } + + return worker; + }, + + destroy: function() { + if (isDestroyed(this)) + return; + + destroyed.set(this, true); + } +}); +exports.Tab = Tab; + +viewFor.define(Tab, tab => viewsFor.get(tab)); + +// Returns the high-level window for this DOM window if the windows module has +// ever been loaded otherwise returns null +function maybeWindowFor(domWindow) { + try { + return modelFor(domWindow); + } + catch (e) { + return null; + } +} + +function tabEmit(tab, event, ...args) { + // Don't emit events for destroyed tabs + if (isDestroyed(tab)) + return; + + // If the windows module was never loaded this will return null. We don't need + // to emit to the window.tabs object in this case as nothing can be listening. + let tabElement = viewsFor.get(tab); + let window = maybeWindowFor(tabElement.ownerDocument.defaultView); + if (window) + emit(window.tabs, event, tab, ...args); + + emit(tabEvents, event, tab, ...args); + emit(tab, event, tab, ...args); +} + +function windowClosed(domWindow) { + if (!isBrowser(domWindow)) + return; + + for (let tabElement of getTabs(domWindow)) { + tabEventListener("close", tabElement); + } +} +windowObserver.on('close', windowClosed); + +// Don't want to send close events after unloaded +when(_ => { + windowObserver.off('close', windowClosed); +}); + +// Listen for tabbrowser events +function tabEventListener(event, tabElement, ...args) { + let domWindow = tabElement.ownerDocument.defaultView; + + if (ignoreWindow(domWindow)) + return; + + // Don't send events for tabs that are already closing + if (event != "close" && (tabElement.closing || !tabElement.parentNode)) + return; + + let tab = modelsFor.get(tabElement); + if (!tab) + tab = new Tab(tabElement); + + let window = maybeWindowFor(domWindow); + + if (event == "open") { + // Note, add to the window tabs first because if this is the first access to + // window.tabs it will be prefilling itself with everything from tabs + if (window) + addListItem(window.tabs, tab); + // The tabs module will take care of adding to its internal list + } + else if (event == "close") { + if (window) + removeListItem(window.tabs, tab); + // The tabs module will take care of removing from its internal list + } + else if (event == "init" || event == "create" || event == "ready" || event == "load") { + // Ignore load events from before browser windows have fully loaded, these + // are for about:blank in the initial tab + if (isBrowser(domWindow) && !domWindow.gBrowserInit.delayedStartupFinished) + return; + + // update the cached remote readyState value + let { readyState } = args[0] || {}; + tab[remoteReadyStateCached] = readyState; + } + + if (event == "init") { + // Do not emit events for the detected existent tabs, we only need to cache + // their current document.readyState value. + return; + } + + tabEmit(tab, event, ...args); + + // The tab object shouldn't be reachable after closed + if (event == "close") { + viewsFor.delete(tab); + modelsFor.delete(tabElement); + } +} +observer.on('*', tabEventListener); + +// Listen for tab events from content +frames.port.on('sdk/tab/event', (frame, event, ...args) => { + if (!frame.isTab) + return; + + let tabElement = getTabForBrowser(frame.frameElement); + if (!tabElement) + return; + + tabEventListener(event, tabElement, ...args); +}); + +// Implement `modelFor` function for the Tab instances.. +modelFor.when(isTab, view => { + return modelsFor.get(view); +}); diff --git a/toolkit/jetpack/sdk/tabs/tab.js b/toolkit/jetpack/sdk/tabs/tab.js new file mode 100644 index 000000000..fa2272494 --- /dev/null +++ b/toolkit/jetpack/sdk/tabs/tab.js @@ -0,0 +1,24 @@ +/* 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 { getTargetWindow } = require("../content/mod"); +const { getTabContentWindow, isTab } = require("./utils"); +const { viewFor } = require("../view/core"); + +if (require('../system/xul-app').name == 'Fennec') { + module.exports = require('./tab-fennec'); +} +else { + module.exports = require('./tab-firefox'); +} + +getTargetWindow.when(isTab, tab => getTabContentWindow(tab)); + +getTargetWindow.when(x => x instanceof module.exports.Tab, + tab => getTabContentWindow(viewFor(tab))); diff --git a/toolkit/jetpack/sdk/tabs/tabs-firefox.js b/toolkit/jetpack/sdk/tabs/tabs-firefox.js new file mode 100644 index 000000000..1eefecb4c --- /dev/null +++ b/toolkit/jetpack/sdk/tabs/tabs-firefox.js @@ -0,0 +1,135 @@ +/* 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 { Tab, tabEvents } = require('./tab'); +const { EventTarget } = require('../event/target'); +const { emit, setListeners } = require('../event/core'); +const { pipe } = require('../event/utils'); +const { observer: windowObserver } = require('../windows/observer'); +const { List, addListItem, removeListItem } = require('../util/list'); +const { modelFor } = require('../model/core'); +const { viewFor } = require('../view/core'); +const { getTabs, getSelectedTab } = require('./utils'); +const { getMostRecentBrowserWindow, isBrowser } = require('../window/utils'); +const { Options } = require('./common'); +const { isPrivate } = require('../private-browsing'); +const { ignoreWindow, isWindowPBSupported } = require('../private-browsing/utils') +const { isPrivateBrowsingSupported } = require('sdk/self'); + +const supportPrivateTabs = isPrivateBrowsingSupported && isWindowPBSupported; + +const Tabs = Class({ + implements: [EventTarget], + extends: List, + initialize: function() { + List.prototype.initialize.call(this); + + // We must do the list manipulation here where the object is extensible + this.on("open", tab => { + addListItem(this, tab); + }); + + this.on("close", tab => { + removeListItem(this, tab); + }); + }, + + get activeTab() { + let activeDomWin = getMostRecentBrowserWindow(); + if (!activeDomWin) + return null; + return modelFor(getSelectedTab(activeDomWin)); + }, + + open: function(options) { + options = Options(options); + + // TODO: Remove the dependency on the windows module: bug 792670 + let windows = require('../windows').browserWindows; + let activeWindow = windows.activeWindow; + + let privateState = supportPrivateTabs && options.isPrivate; + // When no isPrivate option was passed use the private state of the active + // window + if (activeWindow && privateState === undefined) + privateState = isPrivate(activeWindow); + + function getWindow(privateState) { + for (let window of windows) { + if (privateState === isPrivate(window)) { + return window; + } + } + return null; + } + + function openNewWindowWithTab() { + windows.open({ + url: options.url, + isPrivate: privateState, + onOpen: function(newWindow) { + let tab = newWindow.tabs[0]; + setListeners(tab, options); + + if (options.isPinned) + tab.pin(); + + // We don't emit the open event for the first tab in a new window so + // do it now the listeners are attached + emit(tab, "open", tab); + } + }); + } + + if (options.inNewWindow) + return openNewWindowWithTab(); + + // if the active window is in the state that we need then use it + if (activeWindow && (privateState === isPrivate(activeWindow))) + return activeWindow.tabs.open(options); + + // find a window in the state that we need + let window = getWindow(privateState); + if (window) + return window.tabs.open(options); + + return openNewWindowWithTab(); + } +}); + +const allTabs = new Tabs(); +// Export a new object with allTabs as the prototype, otherwise allTabs becomes +// frozen and addListItem and removeListItem don't work correctly. +module.exports = Object.create(allTabs); +pipe(tabEvents, module.exports); + +function addWindowTab(window, tabElement) { + let tab = new Tab(tabElement); + if (window) + addListItem(window.tabs, tab); + addListItem(allTabs, tab); + emit(allTabs, "open", tab); +} + +// Find tabs in already open windows +for (let tabElement of getTabs()) + addWindowTab(null, tabElement); + +// Detect tabs in new windows +windowObserver.on('open', domWindow => { + if (!isBrowser(domWindow) || ignoreWindow(domWindow)) + return; + + let window = null; + try { + modelFor(domWindow); + } + catch (e) { } + + for (let tabElement of getTabs(domWindow)) { + addWindowTab(window, tabElement); + } +}); diff --git a/toolkit/jetpack/sdk/tabs/utils.js b/toolkit/jetpack/sdk/tabs/utils.js new file mode 100644 index 000000000..eae3d41fe --- /dev/null +++ b/toolkit/jetpack/sdk/tabs/utils.js @@ -0,0 +1,370 @@ +/* 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' +}; + + +// NOTE: This file should only deal with xul/native tabs + + +const { Ci, Cu } = require('chrome'); +const { defer } = require("../lang/functional"); +const { windows, isBrowser } = require('../window/utils'); +const { isPrivateBrowsingSupported } = require('../self'); +const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm"); + +// Bug 834961: ignore private windows when they are not supported +function getWindows() { + return windows(null, { includePrivate: isPrivateBrowsingSupported }); +} + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +// Define predicate functions that can be used to detech weather +// we deal with fennec tabs or firefox tabs. + +// Predicate to detect whether tab is XUL "Tab" node. +const isXULTab = tab => + tab instanceof Ci.nsIDOMNode && + tab.nodeName === "tab" && + tab.namespaceURI === XUL_NS; +exports.isXULTab = isXULTab; + +// Predicate to detecet whether given tab is a fettec tab. +// Unfortunately we have to guess via duck typinng of: +// http://mxr.mozilla.org/mozilla-central/source/mobile/android/chrome/content/browser.js#2583 +const isFennecTab = tab => + tab && + tab.QueryInterface && + Ci.nsIBrowserTab && + tab.QueryInterface(Ci.nsIBrowserTab) === tab; +exports.isFennecTab = isFennecTab; + +const isTab = x => isXULTab(x) || isFennecTab(x); +exports.isTab = isTab; + +function activateTab(tab, window) { + let gBrowser = getTabBrowserForTab(tab); + + // normal case + if (gBrowser) { + gBrowser.selectedTab = tab; + } + // fennec ? + else if (window && window.BrowserApp) { + window.BrowserApp.selectTab(tab); + } + return null; +} +exports.activateTab = activateTab; + +function getTabBrowser(window) { + // bug 1009938 - may be null in SeaMonkey + return window.gBrowser || window.getBrowser(); +} +exports.getTabBrowser = getTabBrowser; + +function getTabContainer(window) { + return getTabBrowser(window).tabContainer; +} +exports.getTabContainer = getTabContainer; + +/** + * Returns the tabs for the `window` if given, or the tabs + * across all the browser's windows otherwise. + * + * @param {nsIWindow} [window] + * A reference to a window + * + * @returns {Array} an array of Tab objects + */ +function getTabs(window) { + if (arguments.length === 0) { + return getWindows(). + filter(isBrowser). + reduce((tabs, window) => tabs.concat(getTabs(window)), []); + } + + // fennec + if (window.BrowserApp) + return window.BrowserApp.tabs; + + // firefox - default + return Array.filter(getTabContainer(window).children, t => !t.closing); +} +exports.getTabs = getTabs; + +function getActiveTab(window) { + return getSelectedTab(window); +} +exports.getActiveTab = getActiveTab; + +function getOwnerWindow(tab) { + // normal case + if (tab.ownerDocument) + return tab.ownerDocument.defaultView; + + // try fennec case + return getWindowHoldingTab(tab); +} +exports.getOwnerWindow = getOwnerWindow; + +// fennec +function getWindowHoldingTab(rawTab) { + for (let window of getWindows()) { + // this function may be called when not using fennec, + // but BrowserApp is only defined on Fennec + if (!window.BrowserApp) + continue; + + for (let tab of window.BrowserApp.tabs) { + if (tab === rawTab) + return window; + } + } + + return null; +} + +function openTab(window, url, options) { + options = options || {}; + + // fennec? + if (window.BrowserApp) { + return window.BrowserApp.addTab(url, { + selected: options.inBackground ? false : true, + pinned: options.isPinned || false, + isPrivate: options.isPrivate || false, + parentId: window.BrowserApp.selectedTab.id + }); + } + + // firefox + let newTab = window.gBrowser.addTab(url); + if (!options.inBackground) { + activateTab(newTab); + } + return newTab; +}; +exports.openTab = openTab; + +function isTabOpen(tab) { + // try normal case then fennec case + return !!((tab.linkedBrowser) || getWindowHoldingTab(tab)); +} +exports.isTabOpen = isTabOpen; + +function closeTab(tab) { + let gBrowser = getTabBrowserForTab(tab); + // normal case? + if (gBrowser) { + // Bug 699450: the tab may already have been detached + if (!tab.parentNode) + return; + return gBrowser.removeTab(tab); + } + + let window = getWindowHoldingTab(tab); + // fennec? + if (window && window.BrowserApp) { + // Bug 699450: the tab may already have been detached + if (!tab.browser) + return; + return window.BrowserApp.closeTab(tab); + } + return null; +} +exports.closeTab = closeTab; + +function getURI(tab) { + if (tab.browser) // fennec + return tab.browser.currentURI.spec; + return tab.linkedBrowser.currentURI.spec; +} +exports.getURI = getURI; + +function getTabBrowserForTab(tab) { + let outerWin = getOwnerWindow(tab); + if (outerWin) + return getOwnerWindow(tab).gBrowser; + return null; +} +exports.getTabBrowserForTab = getTabBrowserForTab; + +function getBrowserForTab(tab) { + if (tab.browser) // fennec + return tab.browser; + + return tab.linkedBrowser; +} +exports.getBrowserForTab = getBrowserForTab; + +function getTabId(tab) { + if (tab.browser) // fennec + return tab.id + + return String.split(tab.linkedPanel, 'panel').pop(); +} +exports.getTabId = getTabId; + +function getTabForId(id) { + return getTabs().find(tab => getTabId(tab) === id) || null; +} +exports.getTabForId = getTabForId; + +function getTabTitle(tab) { + return getBrowserForTab(tab).contentTitle || tab.label || ""; +} +exports.getTabTitle = getTabTitle; + +function setTabTitle(tab, title) { + title = String(title); + if (tab.browser) { + // Fennec + tab.browser.contentDocument.title = title; + } + else { + let browser = getBrowserForTab(tab); + // Note that we aren't actually setting the document title in e10s, just + // the title the browser thinks the content has + if (browser.isRemoteBrowser) + browser._contentTitle = title; + else + browser.contentDocument.title = title; + } + tab.label = String(title); +} +exports.setTabTitle = setTabTitle; + +function getTabContentDocument(tab) { + return getBrowserForTab(tab).contentDocument; +} +exports.getTabContentDocument = getTabContentDocument; + +function getTabContentWindow(tab) { + return getBrowserForTab(tab).contentWindow; +} +exports.getTabContentWindow = getTabContentWindow; + +/** + * Returns all tabs' content windows across all the browsers' windows + */ +function getAllTabContentWindows() { + return getTabs().map(getTabContentWindow); +} +exports.getAllTabContentWindows = getAllTabContentWindows; + +// gets the tab containing the provided window +function getTabForContentWindow(window) { + return getTabs().find(tab => getTabContentWindow(tab) === window.top) || null; +} +exports.getTabForContentWindow = getTabForContentWindow; + +// only sdk/selection.js is relying on shims +function getTabForContentWindowNoShim(window) { + function getTabContentWindowNoShim(tab) { + let browser = getBrowserForTab(tab); + return ShimWaiver.getProperty(browser, "contentWindow"); + } + return getTabs().find(tab => getTabContentWindowNoShim(tab) === window.top) || null; +} +exports.getTabForContentWindowNoShim = getTabForContentWindowNoShim; + +function getTabURL(tab) { + return String(getBrowserForTab(tab).currentURI.spec); +} +exports.getTabURL = getTabURL; + +function setTabURL(tab, url) { + let browser = getBrowserForTab(tab); + browser.loadURI(String(url)); +} +// "TabOpen" event is fired when it's still "about:blank" is loaded in the +// changing `location` property of the `contentDocument` has no effect since +// seems to be either ignored or overridden by internal listener, there for +// location change is enqueued for the next turn of event loop. +exports.setTabURL = defer(setTabURL); + +function getTabContentType(tab) { + return getBrowserForTab(tab).contentDocument.contentType; +} +exports.getTabContentType = getTabContentType; + +function getSelectedTab(window) { + if (window.BrowserApp) // fennec? + return window.BrowserApp.selectedTab; + if (window.gBrowser) + return window.gBrowser.selectedTab; + return null; +} +exports.getSelectedTab = getSelectedTab; + + +function getTabForBrowser(browser) { + for (let window of getWindows()) { + // this function may be called when not using fennec + if (!window.BrowserApp) + continue; + + for (let tab of window.BrowserApp.tabs) { + if (tab.browser === browser) + return tab; + } + } + + let tabbrowser = browser.getTabBrowser && browser.getTabBrowser() + return !!tabbrowser && tabbrowser.getTabForBrowser(browser); +} +exports.getTabForBrowser = getTabForBrowser; + +function pin(tab) { + let gBrowser = getTabBrowserForTab(tab); + // TODO: Implement Fennec support + if (gBrowser) gBrowser.pinTab(tab); +} +exports.pin = pin; + +function unpin(tab) { + let gBrowser = getTabBrowserForTab(tab); + // TODO: Implement Fennec support + if (gBrowser) gBrowser.unpinTab(tab); +} +exports.unpin = unpin; + +function isPinned(tab) { + return !!tab.pinned; +} +exports.isPinned = isPinned; + +function reload(tab) { + getBrowserForTab(tab).reload(); +} +exports.reload = reload + +function getIndex(tab) { + let gBrowser = getTabBrowserForTab(tab); + // Firefox + if (gBrowser) { + return tab._tPos; + } + // Fennec + else { + let window = getWindowHoldingTab(tab) + let tabs = window.BrowserApp.tabs; + for (let i = tabs.length; i >= 0; i--) + if (tabs[i] === tab) return i; + } +} +exports.getIndex = getIndex; + +function move(tab, index) { + let gBrowser = getTabBrowserForTab(tab); + // Firefox + if (gBrowser) gBrowser.moveTabTo(tab, index); + // TODO: Implement fennec support +} +exports.move = move; diff --git a/toolkit/jetpack/sdk/tabs/worker.js b/toolkit/jetpack/sdk/tabs/worker.js new file mode 100644 index 000000000..d2ba33696 --- /dev/null +++ b/toolkit/jetpack/sdk/tabs/worker.js @@ -0,0 +1,17 @@ +/* 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 ContentWorker = require('../content/worker').Worker; + +function Worker(options, window) { + options.window = window; + + let worker = ContentWorker(options); + worker.once("detach", function detach() { + worker.destroy(); + }); + return worker; +} +exports.Worker = Worker;
\ No newline at end of file |