Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
567 lines
16 KiB
JavaScript
Executable file
567 lines
16 KiB
JavaScript
Executable file
/* global MozActivity, IconsHelper, LazyLoader, applications */
|
|
/* global BookmarksDatabase, focusManager, SmartModalDialog */
|
|
/* global FTEWizard, Template, AppInstallManager, AppInstallDialogs */
|
|
/* global PreviewWindow, SystemBanner, ManifestHelper, BookmarkManager */
|
|
|
|
(function(window) {
|
|
'use strict';
|
|
|
|
var BUTTON_TYPE = 'contextmenu';
|
|
|
|
// XXX: We use icon filename to determine if we have pin-to-card option
|
|
// because no additional informations can be sent by mozbrowsercontextmenu for
|
|
// now.
|
|
// specifically for popping up FTE.
|
|
var PIN_TO_CARD_ICON_NAME = 'ic_pin.png';
|
|
var ADD_TO_APPS_ICON_PATH = '/style/icons/add_to_apps.png';
|
|
var DELETE_FROM_APPS_ICON_PATH = '/style/icons/delete_from_apps.png';
|
|
|
|
var _id = 0;
|
|
/**
|
|
* The ContextMenu of the AppWindow.
|
|
*
|
|
* @class BrowserContextMenu
|
|
* @param {AppWindow} app The app window instance
|
|
* where this dialog should popup.
|
|
* @extends BaseUI
|
|
*/
|
|
var BrowserContextMenu = window.BrowserContextMenu = function(app) {
|
|
this.app = app;
|
|
this.containerElement = app.element;
|
|
// One to one mapping.
|
|
this.instanceID = _id++;
|
|
this._injected = false;
|
|
this.app.element.addEventListener('mozbrowsercontextmenu', this);
|
|
this.systemBanner = new SystemBanner();
|
|
return this;
|
|
};
|
|
|
|
BrowserContextMenu.prototype = Object.create(window.BaseUI.prototype);
|
|
|
|
BrowserContextMenu.prototype.handleFocus = function(scrollable, elem) {
|
|
if (elem.nodeName) {
|
|
elem.focus();
|
|
}
|
|
};
|
|
|
|
BrowserContextMenu.prototype.CLASS_NAME = 'BrowserContextMenu';
|
|
BrowserContextMenu.prototype.ELEMENT_PREFIX = 'contextmenu-';
|
|
|
|
BrowserContextMenu.prototype.customID = function am_customID() {
|
|
if (this.app) {
|
|
return '[' + this.app.origin + ']';
|
|
} else {
|
|
return '';
|
|
}
|
|
};
|
|
|
|
BrowserContextMenu.prototype.handleEvent = function bcm_handleEvent(evt) {
|
|
switch (evt.type) {
|
|
case 'mozbrowsercontextmenu':
|
|
this.show(evt);
|
|
break;
|
|
case 'modal-dialog-will-open':
|
|
focusManager.focus();
|
|
break;
|
|
case 'modal-dialog-opened':
|
|
this.app.publish('contextmenu-shown');
|
|
// Check focus again in case we have FTE on top of modal dialog.
|
|
focusManager.focus();
|
|
break;
|
|
case 'modal-dialog-closed':
|
|
this.element.classList.remove('visible');
|
|
focusManager.focus();
|
|
this.app.publish('contextmenu-hidden');
|
|
break;
|
|
}
|
|
};
|
|
|
|
BrowserContextMenu.prototype._fetchElements = function bcm__fetchElements() {
|
|
var id = this.CLASS_NAME + this.instanceID;
|
|
this.element = document.getElementById(id);
|
|
this.modalDialog = new SmartModalDialog(this.element);
|
|
this.fteWizard = new FTEWizard('systemContextMenuFTE');
|
|
};
|
|
|
|
BrowserContextMenu.prototype._registerEvents = function bcm__regEvents() {
|
|
// listen the event bubbled from modal dialog to determine the state
|
|
this.element.addEventListener('modal-dialog-will-open', this);
|
|
this.element.addEventListener('modal-dialog-opened', this);
|
|
this.element.addEventListener('modal-dialog-closed', this);
|
|
};
|
|
|
|
BrowserContextMenu.prototype.view = function() {
|
|
return '<div id="' + this.CLASS_NAME + this.instanceID + '" ' +
|
|
'class="contextmenu"></div>';
|
|
};
|
|
|
|
BrowserContextMenu.prototype._destroy = function() {
|
|
focusManager.removeUI(this);
|
|
};
|
|
|
|
BrowserContextMenu.prototype.show = function(evt) {
|
|
var detail = evt.detail;
|
|
|
|
var hasContextMenu = detail.contextmenu &&
|
|
detail.contextmenu.items.length > 0;
|
|
|
|
if (this.app instanceof PreviewWindow) {
|
|
evt.preventDefault();
|
|
evt.stopPropagation();
|
|
|
|
var listItemTask;
|
|
if (this.app.isAppLike) {
|
|
listItemTask = this._listAddWebsiteToAppsItem(
|
|
this.app.identity, this.app.features.name, this.app.features.iconUrl);
|
|
} else {
|
|
listItemTask = this._listAddAppToAppsItem(this.app.manifestURL);
|
|
}
|
|
|
|
listItemTask.then((item) => {
|
|
this.showMenu([item]);
|
|
});
|
|
|
|
return;
|
|
} else if (hasContextMenu && AppInstallManager.isMarketplaceAppActive()) {
|
|
// XXX: Since there's no proper API to handle "Add to Apps" and "Delete
|
|
// from Apps" within the Marketplace app, we ask it to set the label
|
|
// of the menuitem in a predefined format so the system app can
|
|
// override it and do the corresponding action.
|
|
//
|
|
// Format: Base on different content types:
|
|
// - App: '#app:<manifestURL>'
|
|
// - Website: '#website:<url>,<name>,<iconUrl>'
|
|
var addToAppsChoice = detail.contextmenu.items.find((choice) => {
|
|
return choice.label && choice.label.charAt(0) == '#';
|
|
});
|
|
|
|
if (addToAppsChoice) {
|
|
evt.preventDefault();
|
|
evt.stopPropagation();
|
|
|
|
this._listAddToAppsItem(addToAppsChoice).then((item) => {
|
|
this.showMenu([item]);
|
|
});
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
var hasSystemTargets = detail.systemTargets &&
|
|
detail.systemTargets.length > 0;
|
|
|
|
// Nothing to show
|
|
if (!hasSystemTargets && !hasContextMenu) {
|
|
return;
|
|
}
|
|
|
|
// context menus in certified apps that only have system targets are
|
|
// currently disabled. https://bugzilla.mozilla.org/show_bug.cgi?id=1010160
|
|
// is tracking reenabling
|
|
if (!hasContextMenu && hasSystemTargets && this.app.isCertified()) {
|
|
return;
|
|
}
|
|
|
|
var items = this._listItems(detail);
|
|
|
|
if (!items.length) {
|
|
return;
|
|
}
|
|
|
|
// Notify the embedder we are handling the context menu
|
|
evt.preventDefault();
|
|
evt.stopPropagation();
|
|
this.showMenu(items);
|
|
};
|
|
|
|
BrowserContextMenu.prototype.isFocusable = function() {
|
|
return this.modalDialog &&
|
|
this.modalDialog.element.classList.contains('visible');
|
|
};
|
|
|
|
BrowserContextMenu.prototype.focus = function() {
|
|
document.activeElement.blur();
|
|
if (this.fteWizard.running) {
|
|
this.fteWizard.focus();
|
|
} else {
|
|
this.modalDialog.focus();
|
|
}
|
|
|
|
};
|
|
|
|
BrowserContextMenu.prototype.getElement = function() {
|
|
return this.element;
|
|
};
|
|
|
|
BrowserContextMenu.prototype.showMenu = function(menus) {
|
|
if (!this._injected) {
|
|
focusManager.addUI(this);
|
|
this.render();
|
|
this._injected = true;
|
|
}
|
|
|
|
// Initialize FTE if necessary.
|
|
if (!this.fteWizard.launched) {
|
|
var hasPinIcon = menus.some((item) => {
|
|
return item.menuIcon &&
|
|
item.menuIcon.search(PIN_TO_CARD_ICON_NAME) !== -1;
|
|
});
|
|
|
|
if(hasPinIcon) {
|
|
this.initFTE(this.modalDialog.element);
|
|
}
|
|
}
|
|
|
|
this.element.classList.add('visible');
|
|
this.modalDialog.open({ 'buttonSettings': menus,
|
|
'onButtonRendered': function buttonRendered(button, item) {
|
|
if (item.menuIcon) {
|
|
var icon = document.createElement('div');
|
|
icon.classList.add('icon');
|
|
icon.style.backgroundImage = 'url(' + item.menuIcon + ')';
|
|
button.appendChild(icon);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
BrowserContextMenu.prototype.initFTE = function(parent) {
|
|
if (!this.fteWizard) {
|
|
return;
|
|
}
|
|
|
|
if (!this.fteViewElem) {
|
|
var template = new Template('fte_template');
|
|
var fteViewElem = document.createElement('div');
|
|
fteViewElem.className = 'ctxmenu-fte';
|
|
fteViewElem.insertAdjacentHTML('beforeend', template.interpolate());
|
|
parent.appendChild(fteViewElem);
|
|
this.fteViewElem = fteViewElem;
|
|
}
|
|
|
|
this.fteWizard.init({
|
|
container: this.fteViewElem,
|
|
onfinish: () => {
|
|
this.focus();
|
|
}
|
|
});
|
|
};
|
|
|
|
BrowserContextMenu.prototype._listItems = function(detail) {
|
|
var items = [];
|
|
|
|
// contextmenu.items are specified by the web content via html5
|
|
// context menu api
|
|
if (detail.contextmenu && detail.contextmenu.items.length) {
|
|
var that = this;
|
|
detail.contextmenu.items.forEach(function(choice) {
|
|
items.push({
|
|
type: BUTTON_TYPE,
|
|
textRaw: choice.label,
|
|
menuIcon: that.app.origin + '/' + choice.icon,
|
|
onClick: function() {
|
|
detail.contextMenuItemSelected(choice.id);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
if (detail.systemTargets) {
|
|
detail.systemTargets.forEach(function(item) {
|
|
this.generateSystemMenuItem(item).forEach(function(menuItem) {
|
|
items.push(menuItem);
|
|
});
|
|
}, this);
|
|
}
|
|
|
|
if (items.length > 0) {
|
|
items[0].defaultFocus = true;
|
|
}
|
|
|
|
return items;
|
|
};
|
|
|
|
BrowserContextMenu.prototype._listAddToAppsItem = function(choice) {
|
|
//XXX: Please refer to the definition of the label's format mentioned above.
|
|
if (choice.label.startsWith('#app:')) {
|
|
var manifestURL = choice.label.substr('#app:'.length);
|
|
return this._listAddAppToAppsItem(manifestURL);
|
|
} else if (choice.label.startsWith('#website:')) {
|
|
var data = choice.label.substr('#website:'.length).split(',')
|
|
.map((value) => {
|
|
return decodeURIComponent(value);
|
|
});
|
|
return this._listAddWebsiteToAppsItem(data[0], data[1], data[2]);
|
|
}
|
|
|
|
return Promise.reject();
|
|
};
|
|
|
|
BrowserContextMenu.prototype._listAddAppToAppsItem = function(manifestURL) {
|
|
return new Promise((resolve, reject) => {
|
|
var _ = navigator.mozL10n.get;
|
|
if (!AppInstallManager.getAppAddedState(manifestURL)) {
|
|
resolve({
|
|
type: BUTTON_TYPE,
|
|
textRaw: _('add-to-apps'),
|
|
menuIcon: ADD_TO_APPS_ICON_PATH,
|
|
onClick: () => {
|
|
if (this.app instanceof PreviewWindow) {
|
|
var app = applications.getByManifestURL(manifestURL);
|
|
AppInstallManager.handleAddAppToApps(app);
|
|
} else {
|
|
// Since AppInstallManager treats all install requests coming from
|
|
// Marketplace as preview requests, we have to pause this
|
|
// mechanism first when we want to "real" install a app without
|
|
// the preview.
|
|
AppInstallManager.pausePreview = true;
|
|
navigator.mozApps.install(manifestURL);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
resolve({
|
|
type: BUTTON_TYPE,
|
|
textRaw: _('delete-from-apps'),
|
|
menuIcon: DELETE_FROM_APPS_ICON_PATH,
|
|
onClick: () => {
|
|
var app = applications.getByManifestURL(manifestURL);
|
|
var name =
|
|
new ManifestHelper(app.manifest || app.updateManifest).name;
|
|
navigator.mozApps.mgmt.uninstall(app).onsuccess = () => {
|
|
if (this.app instanceof PreviewWindow) {
|
|
this.app.close();
|
|
}
|
|
this.systemBanner.show({
|
|
id: 'deleted-from-apps',
|
|
args: {
|
|
appName: name
|
|
}
|
|
});
|
|
};
|
|
}
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
BrowserContextMenu.prototype._listAddWebsiteToAppsItem =
|
|
function(url, name, iconUrl) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
BookmarkManager.get(url).then((bookmark) => {
|
|
var _ = navigator.mozL10n.get;
|
|
if (!bookmark) {
|
|
resolve({
|
|
type: BUTTON_TYPE,
|
|
textRaw: _('add-to-apps'),
|
|
menuIcon: ADD_TO_APPS_ICON_PATH,
|
|
onClick: () => {
|
|
BookmarkManager.add({
|
|
name: name,
|
|
url: url,
|
|
iconUrl: iconUrl
|
|
}).then(() => {
|
|
this.systemBanner.show({
|
|
id: 'added-to-apps',
|
|
args: {
|
|
appName: name
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|
|
} else {
|
|
// We have 3 places that use the same type of dialog.
|
|
// 1. removeing card in smart-home.
|
|
// 2. uninstalling bookmark in app-deck.
|
|
// 3. here.
|
|
// If we need further changes on the behavior of confirmation, be sure
|
|
// to check these 3 places.
|
|
resolve({
|
|
type: BUTTON_TYPE,
|
|
textRaw: _('delete-from-apps'),
|
|
menuIcon: DELETE_FROM_APPS_ICON_PATH,
|
|
onClick: () => {
|
|
AppInstallManager.appInstallDialogs.show(
|
|
AppInstallDialogs.TYPES.UninstallDialog,
|
|
{
|
|
manifest: {
|
|
name: bookmark.name
|
|
}
|
|
}
|
|
).then(() => {
|
|
BookmarkManager.remove(url).then(() => {
|
|
if (this.app instanceof PreviewWindow) {
|
|
this.app.close();
|
|
}
|
|
this.systemBanner.show({
|
|
id: 'deleted-from-apps',
|
|
args: {
|
|
appName: bookmark.name
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}).catch(reject);
|
|
});
|
|
};
|
|
|
|
BrowserContextMenu.prototype.hide = function() {
|
|
if (!this.element) {
|
|
return false;
|
|
}
|
|
|
|
this.modalDialog.element.close();
|
|
return true;
|
|
};
|
|
|
|
BrowserContextMenu.prototype.openUrl = function(url) {
|
|
/* jshint nonew: false */
|
|
new MozActivity({
|
|
name: 'view',
|
|
data: {
|
|
type: 'url',
|
|
url: url
|
|
}
|
|
});
|
|
};
|
|
|
|
BrowserContextMenu.prototype.shareUrl = function(url) {
|
|
/* jshint nonew: false */
|
|
new MozActivity({
|
|
name: 'share',
|
|
data: {
|
|
type: 'url',
|
|
url: url
|
|
}
|
|
});
|
|
};
|
|
|
|
BrowserContextMenu.prototype.bookmarkUrl = function(url, name) {
|
|
var favicons = this.app.favicons;
|
|
|
|
/* jshint nonew: false */
|
|
var data = {
|
|
type: 'url',
|
|
url: url,
|
|
name: name,
|
|
iconable: false
|
|
};
|
|
|
|
LazyLoader.load('shared/js/icons_helper.js', (() => {
|
|
IconsHelper.getIcon(url, null, {icons: favicons}).then(icon => {
|
|
if (icon) {
|
|
data.icon = icon;
|
|
}
|
|
new MozActivity({
|
|
name: 'save-bookmark',
|
|
data: data
|
|
});
|
|
});
|
|
}));
|
|
};
|
|
|
|
BrowserContextMenu.prototype.newWindow = function(manifest) {
|
|
var newTabApp = applications.getByManifestURL(manifest);
|
|
newTabApp.launch();
|
|
};
|
|
|
|
BrowserContextMenu.prototype.generateSystemMenuItem = function(item) {
|
|
|
|
var nodeName = item.nodeName.toUpperCase();
|
|
var documentURI = item.data.documentURI;
|
|
var uri = item.data.uri;
|
|
var text = item.data.text;
|
|
|
|
switch (nodeName) {
|
|
case 'A':
|
|
return [{
|
|
id: 'open-in-new-window',
|
|
type: BUTTON_TYPE,
|
|
textL10nId: 'open-in-new-window',
|
|
onClick: this.openUrl.bind(this, uri)
|
|
}, {
|
|
id: 'bookmark-link',
|
|
type: BUTTON_TYPE,
|
|
textL10nId: 'add-link-to-home-screen',
|
|
onClick: this.bookmarkUrl.bind(this, uri, text)
|
|
}, {
|
|
id: 'save-link',
|
|
type: BUTTON_TYPE,
|
|
textL10nId: 'save-link',
|
|
onClick: this.app.browser.element.download.bind(
|
|
this.app.browser.element, uri, { referrer: documentURI })
|
|
}, {
|
|
id: 'share-link',
|
|
type: BUTTON_TYPE,
|
|
textL10nId: 'share-link',
|
|
onClick: this.shareUrl.bind(this, uri)
|
|
}];
|
|
|
|
case 'IMG':
|
|
case 'VIDEO':
|
|
case 'AUDIO':
|
|
var typeMap = {
|
|
'IMG': 'image',
|
|
'VIDEO': 'video',
|
|
'AUDIO': 'audio'
|
|
};
|
|
var type = typeMap[nodeName];
|
|
if (nodeName === 'VIDEO' && !item.data.hasVideo) {
|
|
type = 'audio';
|
|
}
|
|
|
|
return [{
|
|
id: 'save-' + type,
|
|
type: BUTTON_TYPE,
|
|
textL10nId: 'save-' + type,
|
|
onClick: this.app.browser.element.download.bind(
|
|
this.app.browser.element, uri, { referrer: documentURI })
|
|
}, {
|
|
id: 'share-' + type,
|
|
type: BUTTON_TYPE,
|
|
textL10nId: 'share-' + type,
|
|
onClick: this.shareUrl.bind(this, uri)
|
|
}];
|
|
|
|
default:
|
|
return [];
|
|
}
|
|
};
|
|
|
|
BrowserContextMenu.prototype.showDefaultMenu = function(manifest, name) {
|
|
return new Promise((resolve) => {
|
|
var config = this.app.config;
|
|
var menuData = [];
|
|
|
|
menuData.push({
|
|
});
|
|
|
|
BookmarksDatabase.get(config.url).then((result) => {
|
|
if (!result) {
|
|
menuData.push({
|
|
id: 'add-to-homescreen',
|
|
type: BUTTON_TYPE,
|
|
textL10nId: 'add-to-home-screen',
|
|
onClick: this.bookmarkUrl.bind(this, config.url, name)
|
|
});
|
|
}
|
|
|
|
menuData.push({
|
|
id: 'share',
|
|
type: BUTTON_TYPE,
|
|
textL10nId: 'share',
|
|
onClick: this.shareUrl.bind(this, config.url)
|
|
});
|
|
|
|
this.showMenu(menuData);
|
|
resolve();
|
|
});
|
|
});
|
|
};
|
|
|
|
}(window));
|