summaryrefslogtreecommitdiffstats
path: root/toolkit/jetpack/sdk/tabs
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2018-02-10 02:51:36 -0500
committerMatt A. Tobin <email@mattatobin.com>2018-02-10 02:51:36 -0500
commit37d5300335d81cecbecc99812747a657588c63eb (patch)
tree765efa3b6a56bb715d9813a8697473e120436278 /toolkit/jetpack/sdk/tabs
parentb2bdac20c02b12f2057b9ef70b0a946113a00e00 (diff)
parent4fb11cd5966461bccc3ed1599b808237be6b0de9 (diff)
downloadUXP-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.js34
-rw-r--r--toolkit/jetpack/sdk/tabs/events.js39
-rw-r--r--toolkit/jetpack/sdk/tabs/helpers.js22
-rw-r--r--toolkit/jetpack/sdk/tabs/namespace.js10
-rw-r--r--toolkit/jetpack/sdk/tabs/observer.js113
-rw-r--r--toolkit/jetpack/sdk/tabs/tab-fennec.js249
-rw-r--r--toolkit/jetpack/sdk/tabs/tab-firefox.js353
-rw-r--r--toolkit/jetpack/sdk/tabs/tab.js24
-rw-r--r--toolkit/jetpack/sdk/tabs/tabs-firefox.js135
-rw-r--r--toolkit/jetpack/sdk/tabs/utils.js370
-rw-r--r--toolkit/jetpack/sdk/tabs/worker.js17
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