/* 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); });