Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
1294 lines
40 KiB
JavaScript
Executable file
1294 lines
40 KiB
JavaScript
Executable file
/* 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) => {
|
|
this.siteIcon.style.backgroundImage = '';
|
|
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));
|