Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
1300 lines
41 KiB
JavaScript
1300 lines
41 KiB
JavaScript
/* global BookmarksDatabase */
|
|
/* global eventSafety */
|
|
/* global IconsHelper */
|
|
/* global LazyLoader */
|
|
/* global ModalDialog */
|
|
/* global MozActivity */
|
|
/* global SettingsListener */
|
|
/* global Service */
|
|
/* global SystemBanner */
|
|
|
|
'use strict';
|
|
|
|
(function(exports) {
|
|
const PINNING_PREF = 'dev.gaia.pinning_the_web';
|
|
// 32px + 4px padding added by the Icon renderer
|
|
const ICON_SIZE = 32 + 4;
|
|
const PREVIEW_ICON_SIZE = 64;
|
|
|
|
var _id = 0;
|
|
|
|
var newTabManifestURL = null;
|
|
SettingsListener.observe('rocketbar.newTabAppURL', '',
|
|
function(url) {
|
|
// The application list in applications.js is not yet ready, so we store
|
|
// only the manifestURL for now and we look up the application whenever
|
|
// we trigger a new window.
|
|
newTabManifestURL = url ? url.match(/(^.*?:\/\/.*?\/)/)[1] +
|
|
'manifest.webapp' : '';
|
|
});
|
|
|
|
/**
|
|
* The chrome UI of the AppWindow.
|
|
*
|
|
* @class AppChrome
|
|
* @param {AppWindow} app The app window instance this chrome belongs to.
|
|
* @extends BaseUI
|
|
*/
|
|
var AppChrome = function AppChrome(app) {
|
|
this.app = app;
|
|
this.instanceID = _id++;
|
|
this.containerElement = app.element;
|
|
this._themeChanged = false;
|
|
this.scrollable = app.browserContainer;
|
|
this._currentOrigin = app.origin || '';
|
|
this._currentIconUrl = '';
|
|
this.render();
|
|
|
|
if (this.app.themeColor && this.app.themeColor !== '') {
|
|
// If titlestatechanged is fired during creation, appChrome won't have
|
|
// been set yet.
|
|
app.appChrome = this;
|
|
|
|
this._themeChanged = true;
|
|
this.setThemeColor(this.app.themeColor);
|
|
}
|
|
|
|
this.reConfig();
|
|
};
|
|
|
|
AppChrome.prototype = Object.create(window.BaseUI.prototype);
|
|
|
|
AppChrome.prototype.CLASS_NAME = 'AppChrome';
|
|
|
|
AppChrome.prototype.EVENT_PREFIX = 'chrome';
|
|
|
|
AppChrome.prototype._DEBUG = false;
|
|
|
|
AppChrome.prototype.reConfig = function() {
|
|
var chrome = this.app.config.chrome;
|
|
if (!chrome) {
|
|
return;
|
|
}
|
|
|
|
this.pinned = chrome.pinned || false;
|
|
this.debug('AppChrome#' + this.app.instanceID + ', pinned ' + this.pinned);
|
|
|
|
if (this.isSearchApp()) {
|
|
this._fixedTitle = true;
|
|
this.app.element.classList.add('search-app');
|
|
} else {
|
|
this._fixedTitle = false;
|
|
this.app.element.classList.remove('search-app');
|
|
|
|
if (!this.app.isHomescreen) {
|
|
this.title.textContent = this.app.name;
|
|
}
|
|
}
|
|
|
|
if (this.app.isHomescreen || this.isSearchApp()) {
|
|
this.title.setAttribute('data-l10n-id', 'search-or-enter-address');
|
|
}
|
|
|
|
if (chrome.bar) {
|
|
this.app.element.classList.add('bar');
|
|
this.bar.classList.add('visible');
|
|
}
|
|
|
|
if (chrome.scrollable) {
|
|
this.app.element.classList.add('collapsible');
|
|
|
|
if (this.app.isPrivateBrowser()) {
|
|
this.element.classList.add('private');
|
|
} else if (!this.app.themeColor) {
|
|
this.app.element.classList.add('light');
|
|
}
|
|
|
|
this.scrollable.scrollgrab = true;
|
|
}
|
|
|
|
if (chrome.maximized) {
|
|
this.expand();
|
|
|
|
if (!this.app.isBrowser() && !this.app.isHomescreen) {
|
|
this.app.element.classList.add('scrollable');
|
|
}
|
|
}
|
|
|
|
this.setSiteIcon();
|
|
};
|
|
|
|
AppChrome.prototype.combinedView = function an_combinedView() {
|
|
var className = this.CLASS_NAME + this.instanceID;
|
|
|
|
return `<div class="chrome chrome-combined" id="${className}">
|
|
<gaia-progress></gaia-progress>
|
|
<section role="dialog" class="pin-dialog hidden">
|
|
<header>
|
|
<h2 class="pin-type" data-l10n-id="pinning-pin-type"></h2>
|
|
</header>
|
|
<div class="card-container">
|
|
<div class="pin-site-icon"></div>
|
|
<h2 class="pin-site-name"></h2>
|
|
</div>
|
|
<div class="footer-container">
|
|
<button data-l10n-id="pinning-pin" data-action="pin"
|
|
class="pin-button">
|
|
Pin
|
|
</button>
|
|
<footer>
|
|
<span data-l10n-id="from">from</span>
|
|
<span class="origin"></span>
|
|
</footer>
|
|
</div>
|
|
</section>
|
|
<div class="pin-scrim"></div>
|
|
<div class="controls">
|
|
<button type="button" class="back-button"
|
|
data-l10n-id="back-button" disabled></button>
|
|
<button type="button" class="forward-button"
|
|
data-l10n-id="forward-button" disabled></button>
|
|
<div class="urlbar js-chrome-ssl-information">
|
|
<div class="urlbar-hit-area"></div>
|
|
<span class="pb-icon"></span>
|
|
<div class="site-icon"></div>
|
|
<div class="chrome-ssl-indicator chrome-title-container">
|
|
<span class="title" dir="auto"></span>
|
|
</div>
|
|
<button type="button" class="reload-button"
|
|
data-l10n-id="reload-button" disabled></button>
|
|
<button type="button" class="stop-button"
|
|
data-l10n-id="stop-button"></button>
|
|
</div>
|
|
<button type="button" class="menu-button" alt="Menu"></button>
|
|
<button type="button" class="windows-button"
|
|
data-l10n-id="windows-button"></button>
|
|
</div>
|
|
</div>`;
|
|
};
|
|
|
|
AppChrome.prototype.view = function an_view() {
|
|
var className = this.CLASS_NAME + this.instanceID;
|
|
|
|
return `<div class="chrome chrome-plain" id="${className}">
|
|
<gaia-progress></gaia-progress>
|
|
<section role="region" class="bar">
|
|
<gaia-header action="close" class='js-chrome-ssl-information'>
|
|
<div class="chrome-ssl-indicator chrome-ssl-indicator-ltr">
|
|
</div>
|
|
<h1 class="chrome-title-container">
|
|
<bdi dir="auto" class="title"></bdi>
|
|
</h1>
|
|
<div class="chrome-ssl-indicator chrome-ssl-indicator-rtl">
|
|
</div>
|
|
</gaia-header>
|
|
</section>
|
|
</div>`;
|
|
};
|
|
|
|
AppChrome.prototype.overflowMenuView = function an_overflowMenuView() {
|
|
var template = `<gaia-overflow-menu>
|
|
|
|
<button id="new-window" data-l10n-id="new-window">
|
|
</button>
|
|
|
|
<button id="new-private-window" data-l10n-id="new-private-window">
|
|
</button>
|
|
|
|
<button id="add-to-home" data-l10n-id="add-to-home-screen" hidden>
|
|
</button>
|
|
|
|
<button id="share" data-l10n-id="share">
|
|
</button>
|
|
|
|
</gaia-overflow-menu>`;
|
|
return template;
|
|
};
|
|
|
|
AppChrome.prototype.__defineGetter__('height', function ac_getHeight() {
|
|
if (this._height) {
|
|
return this._height;
|
|
}
|
|
|
|
this._height = this.element.getBoundingClientRect().height;
|
|
return this._height;
|
|
});
|
|
|
|
AppChrome.prototype._fetchElements = function ac__fetchElements() {
|
|
this.element = this.containerElement.querySelector('.chrome');
|
|
|
|
this.progress = this.element.querySelector('gaia-progress');
|
|
this.reloadButton = this.element.querySelector('.reload-button');
|
|
this.forwardButton = this.element.querySelector('.forward-button');
|
|
this.stopButton = this.element.querySelector('.stop-button');
|
|
this.backButton = this.element.querySelector('.back-button');
|
|
this.menuButton = this.element.querySelector('.menu-button');
|
|
this.windowsButton = this.element.querySelector('.windows-button');
|
|
this.title = this.element.querySelector('.chrome-title-container > .title');
|
|
this.urlbar = this.element.querySelector('.urlbar');
|
|
this.siteIcon = this.element.querySelector('.site-icon');
|
|
LazyLoader.load(['js/system_banner.js']).then(function() {
|
|
this.systemBanner = new SystemBanner();
|
|
}.bind(this));
|
|
|
|
if (this.useCombinedChrome()) {
|
|
this.pinDialog = this.element.querySelector('.pin-dialog');
|
|
this.pinButton = this.element.querySelector('.pin-button');
|
|
this.originElement = this.pinDialog.querySelector('.origin');
|
|
this.pinCardContainer = this.pinDialog.querySelector('.card-container');
|
|
this.pinSiteIcon = this.pinCardContainer.querySelector('.pin-site-icon');
|
|
this.pinSiteName = this.pinCardContainer.querySelector('.pin-site-name');
|
|
this.pinType = this.pinDialog.querySelector('.pin-type');
|
|
this.pinScrim = this.element.querySelector('.pin-scrim');
|
|
}
|
|
|
|
this.sslIndicator =
|
|
this.element.querySelector('.js-chrome-ssl-information');
|
|
|
|
this.bar = this.element.querySelector('.bar');
|
|
if (this.bar) {
|
|
this.header = this.element.querySelector('gaia-header');
|
|
}
|
|
|
|
if (this.reloadButton) {
|
|
this.reloadButton.disabled = !this.hasNavigation();
|
|
}
|
|
};
|
|
|
|
AppChrome.prototype.handleEvent = function ac_handleEvent(evt) {
|
|
switch (evt.type) {
|
|
case 'rocketbar-overlayclosed':
|
|
window.removeEventListener('rocketbar-overlayclosed', this);
|
|
this.collapse();
|
|
break;
|
|
|
|
case 'rocketbar-activating':
|
|
this.hidePinDialogCard();
|
|
break;
|
|
|
|
case 'click':
|
|
this.handleClickEvent(evt);
|
|
break;
|
|
|
|
case 'action':
|
|
this.handleActionEvent(evt);
|
|
break;
|
|
|
|
case 'scroll':
|
|
this.handleScrollEvent(evt);
|
|
break;
|
|
|
|
case '_loading':
|
|
this.show(this.progress);
|
|
this.progress.start();
|
|
break;
|
|
|
|
case '_loaded':
|
|
this.hide(this.progress);
|
|
this.progress.stop();
|
|
break;
|
|
|
|
case 'mozbrowserloadstart':
|
|
this.handleLoadStart(evt);
|
|
break;
|
|
|
|
case 'mozbrowserloadend':
|
|
this.handleLoadEnd(evt);
|
|
break;
|
|
|
|
case 'mozbrowsererror':
|
|
this.handleError(evt);
|
|
break;
|
|
|
|
case '_locationchange':
|
|
this.hidePinDialogCard();
|
|
this.handleLocationChange();
|
|
break;
|
|
|
|
case 'mozbrowserscrollareachanged':
|
|
this.handleScrollAreaChanged(evt);
|
|
break;
|
|
|
|
case '_securitychange':
|
|
this.handleSecurityChanged(evt);
|
|
break;
|
|
|
|
case '_namechanged':
|
|
this.handleNameChanged();
|
|
break;
|
|
|
|
case 'mozbrowsermetachange':
|
|
this.handleMetaChange(evt);
|
|
break;
|
|
case 'pins-scopechange':
|
|
this.handleBookmarksScopeChange(evt);
|
|
break;
|
|
}
|
|
};
|
|
|
|
AppChrome.prototype.handleClickEvent = function ac_handleClickEvent(evt) {
|
|
evt.stopPropagation(); // We'll handle all clicks here, thanks.
|
|
|
|
switch (evt.currentTarget) {
|
|
case this.reloadButton:
|
|
this.app.reload();
|
|
break;
|
|
|
|
case this.stopButton:
|
|
this.app.stop();
|
|
break;
|
|
|
|
case this.backButton:
|
|
this.app.back();
|
|
break;
|
|
|
|
case this.forwardButton:
|
|
this.app.forward();
|
|
break;
|
|
|
|
case this.siteIcon:
|
|
evt.stopImmediatePropagation();
|
|
this.onClickSiteIcon();
|
|
break;
|
|
|
|
case this.urlbar:
|
|
case this.title:
|
|
this.titleClicked();
|
|
break;
|
|
|
|
case this.menuButton:
|
|
this.showOverflowMenu();
|
|
break;
|
|
|
|
case this.windowsButton:
|
|
this.showWindows();
|
|
break;
|
|
|
|
case this.newWindowButton:
|
|
evt.stopImmediatePropagation();
|
|
// XXX: this isn't a function!
|
|
// this.onNewWindow();
|
|
break;
|
|
|
|
case this.newPrivateWinButton:
|
|
// Currently not in use, awaiting shared menu web components work.
|
|
evt.stopImmediatePropagation();
|
|
this.onNewPrivateWindow();
|
|
break;
|
|
|
|
case this.addToHomeButton:
|
|
evt.stopImmediatePropagation();
|
|
this.onAddToHome();
|
|
break;
|
|
|
|
case this.shareButton:
|
|
evt.stopImmediatePropagation();
|
|
this.onShare();
|
|
break;
|
|
|
|
case this.pinButton:
|
|
if (this.pinButton.dataset.action == 'unpin') {
|
|
this.unpinSite();
|
|
} else {
|
|
this.pinSite();
|
|
}
|
|
break;
|
|
|
|
case this.pinScrim:
|
|
this.hidePinDialogCard();
|
|
break;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Pin current page in places database.
|
|
*/
|
|
AppChrome.prototype.pinPage = function ac_pinPage() {
|
|
Service && Service.request('Places:setPinned', this.app.config.url, true)
|
|
.then(() => {
|
|
this.app.debug('Succeeding in pinning ' + this.app.config.url);
|
|
this.systemBanner.show('page-pinned-to-home-screen');
|
|
var screenshotBlob = this.app.getCachedScreenshotBlob();
|
|
if (screenshotBlob) {
|
|
Service.request('Places:saveScreenshot', this.app.config.url,
|
|
screenshotBlob);
|
|
}
|
|
if (this._themeChanged) {
|
|
Service.request('Places:saveThemeColor', this.app.config.url,
|
|
this.app.themeColor, true);
|
|
}
|
|
}, () => {
|
|
console.error('Failed to pin ' + this.app.config.url);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Unpin current page in places database.
|
|
*/
|
|
AppChrome.prototype.unpinPage = function ac_unpinPage() {
|
|
Service && Service.request('Places:setPinned', this.app.config.url, false)
|
|
.then(() => {
|
|
this.app.debug('Succeeding in unpinning ' + this.app.config.url);
|
|
this.systemBanner.show('page-unpinned-from-home-screen');
|
|
}, () => {
|
|
console.error('Failed to unpin ' + this.app.config.url);
|
|
});
|
|
};
|
|
|
|
AppChrome.prototype.getSiteUrl = function() {
|
|
var pageUrl = this.app.config.url;
|
|
var webManifest = this.app.webManifest;
|
|
var startURL = (webManifest && this.app.webManifest.start_url) ?
|
|
new URL(this.app.webManifest.start_url, pageUrl) : null;
|
|
return startURL ? startURL.href : pageUrl;
|
|
};
|
|
|
|
/**
|
|
* Pin current site in bookmarks database.
|
|
*/
|
|
AppChrome.prototype.pinSite = function ac_pinSite() {
|
|
var siteObject = {};
|
|
var manifestURL = this.app.webManifestURL;
|
|
var manifestObject = this.app.webManifest;
|
|
var pageURL = new URL(this.app.config.url);
|
|
var hostname = pageURL.hostname;
|
|
|
|
siteObject.type = 'url';
|
|
siteObject.iconable = false;
|
|
siteObject.icons = this.app.favicons;
|
|
siteObject.frecency = 1;
|
|
siteObject.pinned = true;
|
|
siteObject.pinnedFrom = pageURL.href;
|
|
siteObject.name = this.app.name || hostname;
|
|
|
|
var siteUrl = this.getSiteUrl();
|
|
siteObject.id = siteUrl;
|
|
siteObject.url = siteUrl;
|
|
|
|
if (manifestURL && manifestObject) {
|
|
siteObject.webManifestUrl = manifestURL;
|
|
siteObject.webManifest = manifestObject;
|
|
siteObject.name = manifestObject.short_name || manifestObject.name ||
|
|
hostname;
|
|
if (manifestObject.scope) {
|
|
siteObject.scope = manifestObject.scope;
|
|
}
|
|
}
|
|
|
|
// Set the .icon property before saving for
|
|
// backwards compatibility with verticalhome
|
|
IconsHelper.getIcon(siteObject.url, null,
|
|
{icons: this.app.favicons}, siteObject).then(icon => {
|
|
siteObject.icon = icon;
|
|
BookmarksDatabase.put(siteObject, siteObject.id)
|
|
.then(() => {
|
|
this.app.debug('pinSite: ' + siteObject.id);
|
|
this.systemBanner.show('site-pinned-to-home-screen');
|
|
})
|
|
.catch((error) => {
|
|
console.error('pinSite, Failed to pin site: ' + error);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Unpin current site from bookmarks database.
|
|
*/
|
|
AppChrome.prototype.unpinSite = function ac_pinSite() {
|
|
if (!this.pinned) {
|
|
return;
|
|
}
|
|
var siteId = this.getSiteUrl();
|
|
BookmarksDatabase.remove(siteId)
|
|
.then(() => {
|
|
this.app.debug('unpinSite: ' + siteId);
|
|
this.systemBanner.show('site-unpinned-from-home-screen');
|
|
// 'removed' listener will call unpin()
|
|
}, (evt) => {
|
|
this.app.debug('unpinSite, unpinning cancelled for: ' + siteId);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Put browser chrome in pinned state.
|
|
*/
|
|
AppChrome.prototype.pin = function ac_pin() {
|
|
this.hidePinDialogCard();
|
|
this.collapse();
|
|
this.pinned = true;
|
|
this.app.element.classList.remove('collapsible');
|
|
};
|
|
|
|
/**
|
|
* Remove pinned state from the browser.
|
|
*/
|
|
AppChrome.prototype.unpin = function ac_unpin() {
|
|
this.hidePinDialogCard();
|
|
this.pinned = false;
|
|
if (this.app.config && this.app.config.chrome.scrollable) {
|
|
this.app.element.classList.add('collapsible');
|
|
}
|
|
this.expand();
|
|
};
|
|
|
|
AppChrome.prototype.titleClicked = function ac_titleClicked() {
|
|
var contextMenu = this.app.contextmenu && this.app.contextmenu.isShown();
|
|
var locked = Service && Service.query('locked');
|
|
|
|
if (locked || contextMenu) {
|
|
return;
|
|
}
|
|
|
|
if (!this.isMaximized() && this.app.isBrowser()) {
|
|
if (this.pinned) {
|
|
this.app.element.classList.add('collapsible');
|
|
this.scrollable.scrollTop = 0;
|
|
}
|
|
this.maximize();
|
|
} else {
|
|
window.dispatchEvent(new CustomEvent('global-search-request'));
|
|
}
|
|
};
|
|
|
|
AppChrome.prototype.onClickSiteIcon = function onClickSiteIcon() {
|
|
if (!this.app.isBrowser() || this.app.loading || !this.isMaximized()) {
|
|
this.app.debug('Pinning is only enabled in the browser');
|
|
return;
|
|
}
|
|
|
|
if (this.pinDialog.classList.contains('hidden')) {
|
|
this.setPinDialogCard();
|
|
} else {
|
|
this.hidePinDialogCard();
|
|
}
|
|
};
|
|
|
|
AppChrome.prototype.setPinDialogCard = function ac_setPinDialogCard() {
|
|
if (this.pinned) {
|
|
navigator.mozL10n.setAttributes(this.pinType, 'pinning-unpin-type', {
|
|
'type': this.app.name
|
|
});
|
|
this.pinButton.setAttribute('data-l10n-id', 'pinning-unpin-site');
|
|
this.pinButton.dataset.action = 'unpin';
|
|
} else {
|
|
navigator.mozL10n.setAttributes(this.pinType, 'pinning-pin-type', {
|
|
'type': this.app.name
|
|
});
|
|
this.pinButton.setAttribute('data-l10n-id', 'pinning-pin');
|
|
this.pinButton.dataset.action = 'pin';
|
|
}
|
|
this.pinSiteName.textContent = this.app.name;
|
|
this.setOrigin();
|
|
this.pinDialog.classList.remove('hidden');
|
|
};
|
|
|
|
AppChrome.prototype.setOrigin = function ac_setOrigin() {
|
|
var origin = this._currentOrigin.split('://')[1].split('.');
|
|
var tld = this.originElement.querySelector('.tld');
|
|
|
|
if (!tld) {
|
|
tld = document.createElement('span');
|
|
tld.className = 'tld';
|
|
} else {
|
|
tld.remove();
|
|
}
|
|
|
|
tld.textContent = origin.slice(origin.length - 2, origin.length).join('.');
|
|
|
|
if (origin.length > 2) {
|
|
var subdomains = origin.slice(0, origin.length - 2).join('.');
|
|
this.originElement.textContent = subdomains + '.';
|
|
}
|
|
|
|
this.originElement.appendChild(tld);
|
|
};
|
|
|
|
AppChrome.prototype.hidePinDialogCard = function ac_hidePinDialogCard(url) {
|
|
this.pinDialog && this.pinDialog.classList.add('hidden');
|
|
};
|
|
|
|
AppChrome.prototype.expand = function ac_expand() {
|
|
this.element.classList.add('maximized');
|
|
};
|
|
|
|
AppChrome.prototype.collapse = function ac_collapse() {
|
|
this.hidePinDialogCard();
|
|
this.element.classList.remove('maximized');
|
|
};
|
|
|
|
AppChrome.prototype.handleActionEvent = function ac_handleActionEvent(evt) {
|
|
if (evt.detail.type === 'close') {
|
|
this.app.kill();
|
|
}
|
|
};
|
|
|
|
AppChrome.prototype.handleScrollEvent = function ac_handleScrollEvent(evt) {
|
|
if (!this.containerElement.classList.contains('scrollable')) {
|
|
return;
|
|
}
|
|
|
|
// Ideally we'd animate based on scroll position, but until we have
|
|
// the necessary spec and implementation, we'll animate completely to
|
|
// the expanded or collapsed state depending on whether it's at the
|
|
// top or not.
|
|
// XXX Open a bug since I wonder if there is scrollgrab rounding issue
|
|
// somewhere. While panning from the bottom to the top, there is often
|
|
// a scrollTop position of scrollTopMax - 1, which triggers the transition!
|
|
if (this.scrollable.scrollTop >= this.scrollable.scrollTopMax - 1) {
|
|
this.collapse();
|
|
if (this.pinned) {
|
|
this.app.element.classList.remove('collapsible');
|
|
}
|
|
} else if (!this.pinned) {
|
|
this.expand();
|
|
}
|
|
|
|
if (this.app.isActive()) {
|
|
this.app.publish('titlestatechanged');
|
|
}
|
|
};
|
|
|
|
AppChrome.prototype._pinningObserver = function ac__pinningObserver(enabled) {
|
|
// Disable the pinning doorhanger in 2.5 since we have removed all
|
|
// functionality from it. Beyond 2.5 we will use the doorhanger for
|
|
// Tracking protection and privacy configuration.
|
|
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1207710
|
|
var targets = [
|
|
// this.siteIcon, * See comment above. *
|
|
this.pinScrim
|
|
];
|
|
var method = enabled ? 'addEventListener' : 'removeEventListener';
|
|
targets.forEach(element => {
|
|
element[method]('click', this);
|
|
});
|
|
};
|
|
|
|
AppChrome.prototype.handleBookmarksScopeChange = function (evt) {
|
|
var scope = evt.detail.scope;
|
|
var inScope = !!(scope && this.app.inScope(scope));
|
|
|
|
if (scope && !inScope) {
|
|
return;
|
|
}
|
|
|
|
var origin = new URL(evt.detail.url).origin;
|
|
var name = evt.detail.name;
|
|
|
|
if (inScope || this.app.matchesOriginAndName(origin, name)) {
|
|
switch (evt.detail.action) {
|
|
case 'add':
|
|
case 'update':
|
|
this.pin();
|
|
break;
|
|
case 'remove':
|
|
this.unpin();
|
|
break;
|
|
default:
|
|
this.app.debug('Unknown pins-scopechange action: ' +
|
|
evt.detail.action);
|
|
}
|
|
}
|
|
};
|
|
|
|
AppChrome.prototype._registerEvents = function ac__registerEvents() {
|
|
if (this.useCombinedChrome()) {
|
|
LazyLoader.load('shared/js/bookmarks_database.js').then(() => {
|
|
this.updateAddToHomeButton();
|
|
}).catch((err) => {
|
|
console.error(err);
|
|
});
|
|
LazyLoader.load('shared/elements/gaia_overflow_menu/script.js');
|
|
|
|
window.addEventListener('pins-scopechange', this);
|
|
window.addEventListener('rocketbar-activating', this);
|
|
this.stopButton.addEventListener('click', this);
|
|
this.reloadButton.addEventListener('click', this);
|
|
this.backButton.addEventListener('click', this);
|
|
this.forwardButton.addEventListener('click', this);
|
|
this.urlbar.addEventListener('click', this);
|
|
this.title.addEventListener('click', this);
|
|
this.scrollable.addEventListener('scroll', this);
|
|
this.menuButton.addEventListener('click', this);
|
|
this.windowsButton.addEventListener('click', this);
|
|
this.pinButton.addEventListener('click', this);
|
|
|
|
// Adding or removing the click listener, depending on
|
|
// the 'Pinning the Web' setting enabled or disabled
|
|
this._boundPinningObserver =
|
|
this._boundPinningObserver || this._pinningObserver.bind(this);
|
|
SettingsListener.observe(PINNING_PREF, '', this._boundPinningObserver);
|
|
|
|
} else {
|
|
this.header.addEventListener('action', this);
|
|
}
|
|
|
|
this.app.element.addEventListener('mozbrowserloadstart', this);
|
|
this.app.element.addEventListener('mozbrowserloadend', this);
|
|
this.app.element.addEventListener('mozbrowsererror', this);
|
|
this.app.element.addEventListener('mozbrowsermetachange', this);
|
|
this.app.element.addEventListener('mozbrowserscrollareachanged', this);
|
|
this.app.element.addEventListener('_locationchange', this);
|
|
this.app.element.addEventListener('_namechanged', this);
|
|
this.app.element.addEventListener('_securitychange', this);
|
|
this.app.element.addEventListener('_loading', this);
|
|
this.app.element.addEventListener('_loaded', this);
|
|
|
|
var element = this.element;
|
|
|
|
var animEnd = function(evt) {
|
|
if (evt && evt.target !== element) {
|
|
return;
|
|
}
|
|
var publishEvent = this.isMaximized() ? 'expanded' : 'collapsed';
|
|
this.app.publish('chrome' + publishEvent);
|
|
}.bind(this);
|
|
|
|
element.addEventListener('transitionend', animEnd);
|
|
};
|
|
|
|
AppChrome.prototype._unregisterEvents = function ac__unregisterEvents() {
|
|
|
|
if (this.useCombinedChrome()) {
|
|
window.removeEventListener('pins-scopechange', this);
|
|
window.removeEventListener('rocketbar-activating', this);
|
|
this.stopButton.removeEventListener('click', this);
|
|
this.menuButton.removeEventListener('click', this);
|
|
this.windowsButton.removeEventListener('click', this);
|
|
this.pinButton.removeEventListener('click', this);
|
|
this.reloadButton.removeEventListener('click', this);
|
|
this.backButton.removeEventListener('click', this);
|
|
this.forwardButton.removeEventListener('click', this);
|
|
this.title.removeEventListener('click', this);
|
|
this.scrollable.removeEventListener('scroll', this);
|
|
|
|
SettingsListener.unobserve(PINNING_PREF, this._boundPinningObserver);
|
|
|
|
if (this.newWindowButton) {
|
|
this.newWindowButton.removeEventListener('click', this);
|
|
}
|
|
|
|
if (this.addToHomeButton) {
|
|
this.addToHomeButton.removeEventListener('click', this);
|
|
}
|
|
|
|
if (this.shareButton) {
|
|
this.shareButton.removeEventListener('click', this);
|
|
}
|
|
} else {
|
|
this.header.removeEventListener('action', this);
|
|
}
|
|
|
|
if (!this.app) {
|
|
return;
|
|
}
|
|
|
|
this.app.element.removeEventListener('mozbrowserloadstart', this);
|
|
this.app.element.removeEventListener('mozbrowserloadend', this);
|
|
this.app.element.removeEventListener('mozbrowsererror', this);
|
|
this.app.element.removeEventListener('mozbrowsermetachange', this);
|
|
this.app.element.removeEventListener('_locationchange', this);
|
|
this.app.element.removeEventListener('_namechanged', this);
|
|
this.app.element.removeEventListener('_loading', this);
|
|
this.app.element.removeEventListener('_loaded', this);
|
|
this.app = null;
|
|
};
|
|
|
|
// Name has priority over the rest
|
|
AppChrome.prototype.handleNameChanged =
|
|
function ac_handleNameChanged(evt) {
|
|
if (this._fixedTitle) {
|
|
return;
|
|
}
|
|
if (!this.app.isHomescreen && !this.isSearchApp()) {
|
|
this.title.textContent = this.app.name;
|
|
} else {
|
|
this.title.setAttribute('data-l10n-id', 'search-or-enter-address');
|
|
}
|
|
};
|
|
|
|
AppChrome.prototype.handleScrollAreaChanged = function(evt) {
|
|
// Make sure the scroll-area-changed is coming from the right element.
|
|
if (evt && (!this.app || !this.app.browser ||
|
|
evt.target !== this.app.browser.element)) {
|
|
return;
|
|
}
|
|
|
|
// Check if the page has become scrollable and add the scrollable class.
|
|
// We don't check if a page has stopped being scrollable to avoid oddness
|
|
// with a page oscillating between scrollable/non-scrollable states, and
|
|
// other similar issues that Firefox for Android is still dealing with
|
|
// today.
|
|
if (this.containerElement.classList.contains('scrollable')) {
|
|
return;
|
|
}
|
|
|
|
// Cache the last given scroll area height so this function can be called
|
|
// without an event object.
|
|
if (evt) {
|
|
this.browserScrollHeight = evt.detail.height;
|
|
}
|
|
|
|
// Don't respond to this event when we aren't visible. The container size
|
|
// isn't updated in this case, which can ending up incorrectly causing it
|
|
// to be labelled as scrollable.
|
|
if (!this.app.isVisible()) {
|
|
return;
|
|
}
|
|
|
|
// We allow the bar to collapse if the page is greater than the area of the
|
|
// window with a collapsed bar. Strictly speaking, we'd allow it to
|
|
// collapse if it was greater than the area of the window with the expanded
|
|
// bar, but due to prevalent use of -webkit-box-sizing and plain mistakes,
|
|
// this causes too many false-positives.
|
|
if (this.browserScrollHeight > this.containerElement.clientHeight) {
|
|
this.containerElement.classList.add('scrollable');
|
|
}
|
|
};
|
|
|
|
AppChrome.prototype.handleSecurityChanged = function(evt) {
|
|
var sslState = this.app.getSSLState();
|
|
this.sslIndicator.dataset.ssl = sslState;
|
|
this.sslIndicator.classList.toggle(
|
|
'chrome-has-ssl-indicator', sslState === 'broken' || sslState === 'secure'
|
|
);
|
|
if (this.pinDialog) {
|
|
this.pinDialog.classList.toggle('secure', sslState === 'secure');
|
|
}
|
|
};
|
|
|
|
AppChrome.prototype.handleMetaChange =
|
|
function ac__handleMetaChange(evt) {
|
|
var detail = evt.detail;
|
|
if (detail.name !== 'theme-color' || !detail.type) {
|
|
return;
|
|
}
|
|
|
|
// If the theme-color meta is removed, let's reset the color.
|
|
var color = '';
|
|
|
|
// Otherwise, set it to the color that has been asked.
|
|
if (detail.type !== 'removed') {
|
|
color = detail.content;
|
|
}
|
|
|
|
this._themeChanged = true;
|
|
this.setThemeColor(color);
|
|
if (!this.app.isHomescreen && this.app.config.url) {
|
|
Service && Service.request('Places:saveThemeColor',
|
|
this.app.config.url, color, true);
|
|
}
|
|
};
|
|
|
|
AppChrome.prototype.setThemeColor = function ac_setThemColor(color) {
|
|
// Overwrite theme color for private windows and add private class
|
|
if (this.app.isPrivateBrowser()) {
|
|
color = '#392E54';
|
|
this.containerElement.classList.add('private');
|
|
}
|
|
|
|
var bottomApp = this.app.getBottomMostWindow();
|
|
|
|
if (this.app.CLASS_NAME === 'PopupWindow' &&
|
|
bottomApp &&
|
|
bottomApp.themeColor) {
|
|
color = bottomApp.themeColor;
|
|
}
|
|
|
|
this.app.themeColor = color;
|
|
this.element.style.backgroundColor = color;
|
|
|
|
if (!this.app.isHomescreen) {
|
|
this.scrollable.style.backgroundColor = color;
|
|
}
|
|
|
|
if (color === '') {
|
|
this.app.element.classList.add('light');
|
|
this.app.publish('titlestatechanged');
|
|
return;
|
|
}
|
|
|
|
if (color === 'transparent') {
|
|
this.app.element.classList.remove('light');
|
|
this.app.publish('titlestatechanged');
|
|
return;
|
|
}
|
|
|
|
var self = this;
|
|
var finishedFade = false;
|
|
var endBackgroundFade = function(evt) {
|
|
if (evt && evt.propertyName != 'background-color') {
|
|
return;
|
|
}
|
|
finishedFade = true;
|
|
if (self.element) {
|
|
self.element.removeEventListener('transitionend', endBackgroundFade);
|
|
}
|
|
};
|
|
eventSafety(this.element, 'transitionend', endBackgroundFade, 1000);
|
|
|
|
window.requestAnimationFrame(function updateAppColor() {
|
|
if (finishedFade || !self.element) {
|
|
return;
|
|
}
|
|
|
|
var computedColor = window.getComputedStyle(self.element).backgroundColor;
|
|
var colorCodes = /rgb\((\d+), (\d+), (\d+)\)/.exec(computedColor);
|
|
if (!colorCodes || colorCodes.length === 0) {
|
|
return;
|
|
}
|
|
|
|
var r = parseInt(colorCodes[1]);
|
|
var g = parseInt(colorCodes[2]);
|
|
var b = parseInt(colorCodes[3]);
|
|
var brightness =
|
|
Math.sqrt((r*r) * 0.241 + (g*g) * 0.691 + (b*b) * 0.068);
|
|
|
|
var wasLight = self.app.element.classList.contains('light');
|
|
var isLight = brightness > 200;
|
|
if (wasLight != isLight) {
|
|
self.app.element.classList.toggle('light', isLight);
|
|
self.app.publish('titlestatechanged');
|
|
}
|
|
window.requestAnimationFrame(updateAppColor);
|
|
});
|
|
};
|
|
|
|
AppChrome.prototype.render = function() {
|
|
this.publish('willrender');
|
|
|
|
var view = this.useCombinedChrome() ? this.combinedView() : this.view();
|
|
this.app.element.insertAdjacentHTML('afterbegin', view);
|
|
|
|
this._fetchElements();
|
|
this._registerEvents();
|
|
this.publish('rendered');
|
|
};
|
|
|
|
AppChrome.prototype.useLightTheming = function ac_useLightTheming() {
|
|
// The rear window should dictate the status bar color when the front
|
|
// window is a popup.
|
|
if (this.app.CLASS_NAME == 'PopupWindow' &&
|
|
this.app.rearWindow &&
|
|
this.app.rearWindow.appChrome) {
|
|
return this.app.rearWindow.appChrome.useLightTheming();
|
|
}
|
|
// All other cases can use the front window.
|
|
return this.app.element.classList.contains('light');
|
|
};
|
|
|
|
AppChrome.prototype.useCombinedChrome = function ac_useCombinedChrome(evt) {
|
|
return this.app.config.chrome && !this.app.config.chrome.bar;
|
|
};
|
|
|
|
AppChrome.prototype.updateAddToHomeButton =
|
|
function ac_updateAddToHomeButton() {
|
|
if (!this.addToHomeButton || !BookmarksDatabase) {
|
|
return;
|
|
}
|
|
|
|
// Enable/disable the bookmark option
|
|
BookmarksDatabase.get(this._currentURL).then(function resolve(result) {
|
|
this.addToHomeButton.hidden = !!result;
|
|
}.bind(this),
|
|
function reject() {
|
|
this.addToHomeButton.hidden = true;
|
|
}.bind(this));
|
|
};
|
|
|
|
AppChrome.prototype.handleLocationChange =
|
|
function ac_handleLocationChange() {
|
|
if (!this.app) {
|
|
return;
|
|
}
|
|
|
|
// Check if this is just a location-change to an anchor tag.
|
|
var anchorChange = false;
|
|
|
|
if (this._currentURL && this.app.config.url) {
|
|
anchorChange =
|
|
this._currentURL.replace(/#.*/g, '') ===
|
|
this.app.config.url.replace(/#.*/g, '');
|
|
}
|
|
|
|
this._currentURL = this.app.config.url;
|
|
if (!this._fixedTitle && !this.app.isHomescreen) {
|
|
this.title.textContent = this.app.name;
|
|
}
|
|
|
|
if (this.backButton && this.forwardButton) {
|
|
this.app.canGoForward(function forwardSuccess(result) {
|
|
if (!this.hasNavigation()) {
|
|
return;
|
|
}
|
|
this.forwardButton.disabled = !result;
|
|
}.bind(this));
|
|
|
|
this.app.canGoBack(function backSuccess(result) {
|
|
if (!this.hasNavigation()) {
|
|
return;
|
|
}
|
|
this.backButton.disabled = !result;
|
|
}.bind(this));
|
|
}
|
|
|
|
this.updateAddToHomeButton();
|
|
|
|
// We only change the icon back to the default one if the new page is not
|
|
// on the same domain than the previous one.
|
|
// In both cases, we look for the best icon after `mozbrowserloadend`.
|
|
var origin = new URL(this._currentURL).origin;
|
|
if (this._currentOrigin !== origin) {
|
|
this.setSiteIcon();
|
|
this._currentOrigin = origin;
|
|
}
|
|
|
|
if (!this.app.isBrowser()) {
|
|
return;
|
|
}
|
|
|
|
if (!anchorChange) {
|
|
// Make the rocketbar unscrollable until the page resizes to the
|
|
// appropriate height.
|
|
this.containerElement.classList.remove('scrollable');
|
|
this.scrollable.scrollTop = 0;
|
|
|
|
Service.request('PinsManager:isPinned', this._currentURL)
|
|
.then((isPinned) => {
|
|
isPinned ? this.pin() : this.unpin();
|
|
});
|
|
}
|
|
|
|
// Set the title for the private browser landing page.
|
|
// This is explicitly placed in the locationchange handler as it's
|
|
// currently possibly to navigate back to the landing page with the
|
|
// back button. Otherwise it could be in the constructor.
|
|
if (this.app.isPrivateBrowser() &&
|
|
this.app.config.url.startsWith('app:')) {
|
|
this.title.dataset.l10nId = 'search-or-enter-address';
|
|
}
|
|
};
|
|
|
|
AppChrome.prototype.handleLoadStart = function ac_handleLoadStart(evt) {
|
|
this.containerElement.classList.add('loading');
|
|
this._themeChanged = false;
|
|
};
|
|
|
|
AppChrome.prototype.handleLoadEnd = function ac_handleLoadEnd(evt) {
|
|
this.containerElement.classList.remove('loading');
|
|
if (!this._themeChanged) {
|
|
this.setThemeColor('');
|
|
}
|
|
this.setSiteIcon();
|
|
this.setPinPreviewIcon();
|
|
};
|
|
|
|
AppChrome.prototype.handleError = function ac_handleError(evt) {
|
|
if (evt.detail && evt.detail.type === 'fatal') {
|
|
return;
|
|
}
|
|
if (this.useCombinedChrome() && this.app.config.chrome.scrollable &&
|
|
!this.pinned) {
|
|
// When we get an error, keep the rocketbar maximized.
|
|
this.expand();
|
|
this.containerElement.classList.remove('scrollable');
|
|
}
|
|
};
|
|
|
|
AppChrome.prototype.maximize = function ac_maximize(callback) {
|
|
var element = this.element;
|
|
this.expand();
|
|
window.addEventListener('rocketbar-overlayclosed', this);
|
|
|
|
if (!callback) {
|
|
return;
|
|
}
|
|
eventSafety(element, 'transitionend', callback, 250);
|
|
};
|
|
|
|
AppChrome.prototype.isMaximized = function ac_isMaximized() {
|
|
return this.element.classList.contains('maximized');
|
|
};
|
|
|
|
AppChrome.prototype.isSearch = function ac_isSearch() {
|
|
var dataset = this.app.config;
|
|
return dataset.searchURL && this._currentURL === dataset.searchURL;
|
|
};
|
|
|
|
AppChrome.prototype.isSearchApp = function() {
|
|
return this.app.config.manifest &&
|
|
this.app.config.manifest.role === 'search';
|
|
};
|
|
|
|
AppChrome.prototype.hasNavigation = function ac_hasNavigation(evt) {
|
|
return this.app.isBrowser() ||
|
|
(this.app.config.chrome && this.app.config.chrome.navigation);
|
|
};
|
|
|
|
AppChrome.prototype.addBookmark = function ac_addBookmark() {
|
|
var dataset = this.app.config;
|
|
var favicons = this.app.favicons;
|
|
|
|
var name;
|
|
if (this.isSearch()) {
|
|
name = dataset.searchName;
|
|
} else {
|
|
name = this.title.textContent;
|
|
}
|
|
var url = this._currentURL;
|
|
|
|
LazyLoader.load('shared/js/icons_helper.js').then(() => {
|
|
IconsHelper.getIcon(url, null, {icons: favicons}).then(icon => {
|
|
var activity = new MozActivity({
|
|
name: 'save-bookmark',
|
|
data: {
|
|
type: 'url',
|
|
url: url,
|
|
name: name,
|
|
icon: icon,
|
|
iconable: false
|
|
}
|
|
});
|
|
|
|
if (this.addToHomeButton) {
|
|
activity.onsuccess = function onsuccess() {
|
|
this.addToHomeButton.hidden = true;
|
|
}.bind(this);
|
|
}
|
|
});
|
|
}).catch((err) => {
|
|
console.error(err);
|
|
});
|
|
};
|
|
|
|
AppChrome.prototype.onAddBookmark = function ac_onAddBookmark() {
|
|
var self = this;
|
|
function selected(value) {
|
|
if (value) {
|
|
self.addBookmark();
|
|
}
|
|
}
|
|
|
|
var title = 'add-to-home-screen';
|
|
var options = [];
|
|
|
|
if (this.isSearch()) {
|
|
var dataset = this.app.config;
|
|
options.push({
|
|
id: 'search',
|
|
text: {
|
|
raw: dataset.searchName
|
|
}
|
|
});
|
|
} else {
|
|
options.push({
|
|
id: 'origin',
|
|
text: {
|
|
raw: this.title.textContent
|
|
}
|
|
});
|
|
}
|
|
|
|
ModalDialog.selectOne(title, options, selected);
|
|
};
|
|
|
|
AppChrome.prototype.showWindows = function ac_showWindows() {
|
|
window.dispatchEvent(
|
|
new CustomEvent('taskmanagershow',
|
|
{ detail: { filter: 'browser-only' }})
|
|
);
|
|
};
|
|
|
|
AppChrome.prototype.__defineGetter__('overflowMenu',
|
|
// Instantiate the overflow menu when it's needed
|
|
function ac_getOverflowMenu() {
|
|
if (!this._overflowMenu && this.useCombinedChrome() &&
|
|
window.GaiaOverflowMenu) {
|
|
this.app.element.insertAdjacentHTML('afterbegin',
|
|
this.overflowMenuView());
|
|
this._overflowMenu = this.containerElement.
|
|
querySelector('gaia-overflow-menu');
|
|
this.newWindowButton = this._overflowMenu.
|
|
querySelector('#new-window');
|
|
this.newPrivateWinButton = this._overflowMenu.
|
|
querySelector('#new-private-window');
|
|
this.addToHomeButton = this._overflowMenu.
|
|
querySelector('#add-to-home');
|
|
this.shareButton = this._overflowMenu.
|
|
querySelector('#share');
|
|
|
|
this.newWindowButton.addEventListener('click', this);
|
|
this.newPrivateWinButton.addEventListener('click', this);
|
|
this.addToHomeButton.addEventListener('click', this);
|
|
this.shareButton.addEventListener('click', this);
|
|
|
|
this.updateAddToHomeButton();
|
|
}
|
|
|
|
return this._overflowMenu;
|
|
});
|
|
|
|
AppChrome.prototype.showOverflowMenu = function ac_showOverflowMenu() {
|
|
this.overflowMenu.show();
|
|
};
|
|
|
|
AppChrome.prototype.hideOverflowMenu = function ac_hideOverflowMenu() {
|
|
this.overflowMenu.hide();
|
|
};
|
|
|
|
/* Bug 1054466 switched the browser overflow menu to use the system style,
|
|
* but we eventually want to switch back to the new style. We can do that
|
|
* by removing this function.
|
|
*/
|
|
AppChrome.prototype.showOverflowMenu = function ac_showOverflowMenu() {
|
|
if (this.app.contextmenu) {
|
|
var name = this.isSearch() ?
|
|
this.app.config.searchName : this.title.textContent;
|
|
this.app.contextmenu.showDefaultMenu(newTabManifestURL, name);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Display the website icon in the URL bar, if any.
|
|
* If the url parameter is specified, it is loaded immediately. Otherwise,
|
|
* we look for the best possible icon for this website.
|
|
*
|
|
* @param {string?} url
|
|
*/
|
|
AppChrome.prototype.setSiteIcon = function ac_setSiteIcon() {
|
|
if (!this.siteIcon || this.app.isPrivateBrowser()) {
|
|
return;
|
|
}
|
|
|
|
this.app.getSiteIconUrl(ICON_SIZE)
|
|
.then(iconObject => {
|
|
// We compare the original icon URL, otherwise there is a flickering
|
|
// effect because a different object url is created each time.
|
|
if (this._currentIconUrl !== iconObject.originalUrl) {
|
|
this.siteIcon.style.backgroundImage = iconObject.url;
|
|
this._currentIconUrl = iconObject.originalUrl;
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
// Installed apps have a manifest icon even when the Places cache has
|
|
// no favicon. Keep the original chrome slot and select a crisp source.
|
|
const icons = this.app.manifest && this.app.manifest.icons;
|
|
const sizes = Object.keys(icons || {}).map(Number).filter(Number.isFinite).sort((a,b)=>a-b);
|
|
const size = sizes.find(n => n >= ICON_SIZE * devicePixelRatio) || sizes[sizes.length-1];
|
|
const icon = size && icons[size];
|
|
this.siteIcon.style.backgroundImage = icon ? 'url(' + JSON.stringify(new URL(icon, this.app.manifestURL).href) + ')' : '';
|
|
this.app.debug('setSiteIcon, error from getSiteIcon: %s', err);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Populate the pin preview doorhanger with current site/app icon.
|
|
*/
|
|
AppChrome.prototype.setPinPreviewIcon = function ac_setPinPreviewIcon() {
|
|
if (!this.pinSiteIcon) {
|
|
return;
|
|
}
|
|
this.app.getSiteIconUrl(PREVIEW_ICON_SIZE).then(iconObject => {
|
|
this.pinSiteIcon.style.backgroundImage = iconObject.url;
|
|
}).catch((err) => {
|
|
this.app.debug('setPinPreviewIcon, error from getSiteIcon: %s', err);
|
|
this.pinSiteIcon.style.backgroundImage = '';
|
|
});
|
|
};
|
|
|
|
exports.AppChrome = AppChrome;
|
|
}(window));
|