michael@0: /*This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: 'use strict'; michael@0: michael@0: const { Trait } = require("../deprecated/traits"); michael@0: const { EventEmitter } = require("../deprecated/events"); michael@0: const { defer } = require("../lang/functional"); michael@0: const { has } = require("../util/array"); michael@0: const { EVENTS } = require("./events"); michael@0: const { getThumbnailURIForWindow } = require("../content/thumbnail"); michael@0: const { getFaviconURIForLocation } = require("../io/data"); michael@0: const { activateTab, getOwnerWindow, getBrowserForTab, getTabTitle, setTabTitle, michael@0: getTabURL, setTabURL, getTabContentType, getTabId } = require('./utils'); michael@0: const { getOwnerWindow: getPBOwnerWindow } = require('../private-browsing/window/utils'); michael@0: const viewNS = require('../core/namespace').ns(); michael@0: const { deprecateUsage } = require('../util/deprecate'); michael@0: const { getURL } = require('../url/utils'); michael@0: const { viewFor } = require('../view/core'); michael@0: michael@0: // Array of the inner instances of all the wrapped tabs. michael@0: const TABS = []; michael@0: michael@0: /** michael@0: * Trait used to create tab wrappers. michael@0: */ michael@0: const TabTrait = Trait.compose(EventEmitter, { michael@0: on: Trait.required, michael@0: _emit: Trait.required, michael@0: /** michael@0: * Tab DOM element that is being wrapped. michael@0: */ michael@0: _tab: null, michael@0: /** michael@0: * Window wrapper whose tab this object represents. michael@0: */ michael@0: window: null, michael@0: constructor: function Tab(options) { michael@0: this._onReady = this._onReady.bind(this); michael@0: this._onLoad = this._onLoad.bind(this); michael@0: this._onPageShow = this._onPageShow.bind(this); michael@0: this._tab = options.tab; michael@0: // TODO: Remove this dependency michael@0: let window = this.window = options.window || require('../windows').BrowserWindow({ window: getOwnerWindow(this._tab) }); michael@0: michael@0: // Setting event listener if was passed. michael@0: for each (let type in EVENTS) { michael@0: let listener = options[type.listener]; michael@0: if (listener) { michael@0: this.on(type.name, options[type.listener]); michael@0: } michael@0: // window spreads this event. michael@0: if (!has(['ready', 'load', 'pageshow'], (type.name))) michael@0: window.tabs.on(type.name, this._onEvent.bind(this, type.name)); michael@0: } michael@0: michael@0: this.on(EVENTS.close.name, this.destroy.bind(this)); michael@0: michael@0: this._browser.addEventListener(EVENTS.ready.dom, this._onReady, true); michael@0: this._browser.addEventListener(EVENTS.load.dom, this._onLoad, true); michael@0: this._browser.addEventListener(EVENTS.pageshow.dom, this._onPageShow, true); michael@0: michael@0: if (options.isPinned) michael@0: this.pin(); michael@0: michael@0: viewNS(this._public).tab = this._tab; michael@0: getPBOwnerWindow.implement(this._public, getChromeTab); michael@0: viewFor.implement(this._public, getTabView); michael@0: michael@0: // Add tabs to getURL method michael@0: getURL.implement(this._public, (function (obj) this._public.url).bind(this)); michael@0: michael@0: // Since we will have to identify tabs by a DOM elements facade function michael@0: // is used as constructor that collects all the instances and makes sure michael@0: // that they more then one wrapper is not created per tab. michael@0: return this; michael@0: }, michael@0: destroy: function destroy() { michael@0: this._removeAllListeners(); michael@0: if (this._tab) { michael@0: let browser = this._browser; michael@0: // The tab may already be removed from DOM -or- not yet added michael@0: if (browser) { michael@0: browser.removeEventListener(EVENTS.ready.dom, this._onReady, true); michael@0: browser.removeEventListener(EVENTS.load.dom, this._onLoad, true); michael@0: browser.removeEventListener(EVENTS.pageshow.dom, this._onPageShow, true); michael@0: } michael@0: this._tab = null; michael@0: TABS.splice(TABS.indexOf(this), 1); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Internal listener that emits public event 'ready' when the page of this michael@0: * tab is loaded, from DOMContentLoaded michael@0: */ michael@0: _onReady: function _onReady(event) { michael@0: // IFrames events will bubble so we need to ignore those. michael@0: if (event.target == this._contentDocument) michael@0: this._emit(EVENTS.ready.name, this._public); michael@0: }, michael@0: michael@0: /** michael@0: * Internal listener that emits public event 'load' when the page of this michael@0: * tab is loaded, for triggering on non-HTML content, bug #671305 michael@0: */ michael@0: _onLoad: function _onLoad(event) { michael@0: // IFrames events will bubble so we need to ignore those. michael@0: if (event.target == this._contentDocument) { michael@0: this._emit(EVENTS.load.name, this._public); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Internal listener that emits public event 'pageshow' when the page of this michael@0: * tab is loaded from cache, bug #671305 michael@0: */ michael@0: _onPageShow: function _onPageShow(event) { michael@0: // IFrames events will bubble so we need to ignore those. michael@0: if (event.target == this._contentDocument) { michael@0: this._emit(EVENTS.pageshow.name, this._public, event.persisted); michael@0: } michael@0: }, michael@0: /** michael@0: * Internal tab event router. Window will emit tab related events for all it's michael@0: * tabs, this listener will propagate all the events for this tab to it's michael@0: * listeners. michael@0: */ michael@0: _onEvent: function _onEvent(type, tab) { michael@0: if (viewNS(tab).tab == this._tab) michael@0: this._emit(type, tab); michael@0: }, michael@0: /** michael@0: * Browser DOM element where page of this tab is currently loaded. michael@0: */ michael@0: get _browser() getBrowserForTab(this._tab), michael@0: /** michael@0: * Window DOM element containing this tab. michael@0: */ michael@0: get _window() getOwnerWindow(this._tab), michael@0: /** michael@0: * Document object of the page that is currently loaded in this tab. michael@0: */ michael@0: get _contentDocument() this._browser.contentDocument, michael@0: /** michael@0: * Window object of the page that is currently loaded in this tab. michael@0: */ michael@0: get _contentWindow() this._browser.contentWindow, michael@0: michael@0: /** michael@0: * Unique id for the tab, actually maps to tab.linkedPanel but with some munging. michael@0: */ michael@0: get id() this._tab ? getTabId(this._tab) : undefined, michael@0: michael@0: /** michael@0: * The title of the page currently loaded in the tab. michael@0: * Changing this property changes an actual title. michael@0: * @type {String} michael@0: */ michael@0: get title() this._tab ? getTabTitle(this._tab) : undefined, michael@0: set title(title) this._tab && setTabTitle(this._tab, title), michael@0: michael@0: /** michael@0: * Returns the MIME type that the document loaded in the tab is being michael@0: * rendered as. michael@0: * @type {String} michael@0: */ michael@0: get contentType() this._tab ? getTabContentType(this._tab) : undefined, michael@0: michael@0: /** michael@0: * Location of the page currently loaded in this tab. michael@0: * Changing this property will loads page under under the specified location. michael@0: * @type {String} michael@0: */ michael@0: get url() this._tab ? getTabURL(this._tab) : undefined, michael@0: set url(url) this._tab && setTabURL(this._tab, url), michael@0: /** michael@0: * URI of the favicon for the page currently loaded in this tab. michael@0: * @type {String} michael@0: */ michael@0: get favicon() { michael@0: deprecateUsage( michael@0: 'tab.favicon is deprecated, ' + michael@0: 'please use require("sdk/places/favicon").getFavicon instead.' michael@0: ); michael@0: return this._tab ? getFaviconURIForLocation(this.url) : undefined michael@0: }, michael@0: /** michael@0: * The CSS style for the tab michael@0: */ michael@0: get style() null, // TODO michael@0: /** michael@0: * The index of the tab relative to other tabs in the application window. michael@0: * Changing this property will change order of the actual position of the tab. michael@0: * @type {Number} michael@0: */ michael@0: get index() michael@0: this._tab ? michael@0: this._window.gBrowser.getBrowserIndexForDocument(this._contentDocument) : michael@0: undefined, michael@0: set index(value) michael@0: this._tab && this._window.gBrowser.moveTabTo(this._tab, value), michael@0: /** michael@0: * Thumbnail data URI of the page currently loaded in this tab. michael@0: * @type {String} michael@0: */ michael@0: getThumbnail: function getThumbnail() michael@0: this._tab ? getThumbnailURIForWindow(this._contentWindow) : undefined, michael@0: /** michael@0: * Whether or not tab is pinned (Is an app-tab). michael@0: * @type {Boolean} michael@0: */ michael@0: get isPinned() this._tab ? this._tab.pinned : undefined, michael@0: pin: function pin() { michael@0: if (!this._tab) michael@0: return; michael@0: this._window.gBrowser.pinTab(this._tab); michael@0: }, michael@0: unpin: function unpin() { michael@0: if (!this._tab) michael@0: return; michael@0: this._window.gBrowser.unpinTab(this._tab); michael@0: }, michael@0: michael@0: /** michael@0: * Create a worker for this tab, first argument is options given to Worker. michael@0: * @type {Worker} michael@0: */ michael@0: attach: function attach(options) { michael@0: if (!this._tab) michael@0: return; michael@0: // BUG 792946 https://bugzilla.mozilla.org/show_bug.cgi?id=792946 michael@0: // TODO: fix this circular dependency michael@0: let { Worker } = require('./worker'); michael@0: return Worker(options, this._contentWindow); michael@0: }, michael@0: michael@0: /** michael@0: * Make this tab active. michael@0: * Please note: That this function is called asynchronous since in E10S that michael@0: * will be the case. Besides this function is called from a constructor where michael@0: * we would like to return instance before firing a 'TabActivated' event. michael@0: */ michael@0: activate: defer(function activate() { michael@0: if (!this._tab) michael@0: return; michael@0: activateTab(this._tab); michael@0: }), michael@0: /** michael@0: * Close the tab michael@0: */ michael@0: close: function close(callback) { michael@0: // Bug 699450: the tab may already have been detached michael@0: if (!this._tab || !this._tab.parentNode) { michael@0: if (callback) michael@0: callback(); michael@0: return; michael@0: } michael@0: if (callback) michael@0: this.once(EVENTS.close.name, callback); michael@0: this._window.gBrowser.removeTab(this._tab); michael@0: }, michael@0: /** michael@0: * Reload the tab michael@0: */ michael@0: reload: function reload() { michael@0: if (!this._tab) michael@0: return; michael@0: this._window.gBrowser.reloadTab(this._tab); michael@0: } michael@0: }); michael@0: michael@0: function getChromeTab(tab) { michael@0: return getOwnerWindow(viewNS(tab).tab); michael@0: } michael@0: michael@0: // Implement `viewFor` polymorphic function for the Tab michael@0: // instances. michael@0: const getTabView = tab => viewNS(tab).tab; michael@0: michael@0: function Tab(options, existingOnly) { michael@0: let chromeTab = options.tab; michael@0: for each (let tab in TABS) { michael@0: if (chromeTab == tab._tab) michael@0: return tab._public; michael@0: } michael@0: // If called asked to return only existing wrapper, michael@0: // we should return null here as no matching Tab object has been found michael@0: if (existingOnly) michael@0: return null; michael@0: michael@0: let tab = TabTrait(options); michael@0: TABS.push(tab); michael@0: return tab._public; michael@0: } michael@0: Tab.prototype = TabTrait.prototype; michael@0: exports.Tab = Tab;