342 lines
8.6 KiB
JavaScript
342 lines
8.6 KiB
JavaScript
/* 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;
|
|
}
|
|
|
|
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() {
|
|
// TODO: This will use CPOWs in e10s: bug 1146606
|
|
return isDestroyed(this) ? undefined : browser(this).contentDocument.readyState;
|
|
},
|
|
|
|
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;
|
|
|
|
listener.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 == "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;
|
|
}
|
|
|
|
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);
|
|
});
|