forked from mirrors/gecko-dev
Depends on D49941 Using ContentDOMReference instead of creating an array of selectors makes inspect element more stable in case the page is modified between after the contextmenu opens. It will also make the feature easier to make fission compatible Differential Revision: https://phabricator.services.mozilla.com/D49303 --HG-- extra : moz-landing-system : lando
2011 lines
61 KiB
JavaScript
2011 lines
61 KiB
JavaScript
/* -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set ts=2 sw=2 sts=2 et tw=80: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
function openContextMenu(aMessage, aBrowser, aActor) {
|
|
if (BrowserHandler.kiosk) {
|
|
// Don't display context menus in kiosk mode
|
|
return;
|
|
}
|
|
let data = aMessage.data;
|
|
let browser = aBrowser;
|
|
let actor = aActor;
|
|
let spellInfo = data.spellInfo;
|
|
let frameReferrerInfo = data.frameReferrerInfo;
|
|
let linkReferrerInfo = data.linkReferrerInfo;
|
|
let principal = data.principal;
|
|
let storagePrincipal = data.storagePrincipal;
|
|
|
|
if (spellInfo) {
|
|
spellInfo.target = browser.messageManager;
|
|
}
|
|
|
|
let documentURIObject = makeURI(
|
|
data.docLocation,
|
|
data.charSet,
|
|
makeURI(data.baseURI)
|
|
);
|
|
|
|
if (frameReferrerInfo) {
|
|
frameReferrerInfo = E10SUtils.deserializeReferrerInfo(frameReferrerInfo);
|
|
}
|
|
|
|
if (linkReferrerInfo) {
|
|
linkReferrerInfo = E10SUtils.deserializeReferrerInfo(linkReferrerInfo);
|
|
}
|
|
|
|
// For now, JS Window Actors don't deserialize Principals automatically, so we
|
|
// have to do it ourselves. See bug 1557852.
|
|
if (principal) {
|
|
principal = E10SUtils.deserializePrincipal(principal);
|
|
}
|
|
if (storagePrincipal) {
|
|
storagePrincipal = E10SUtils.deserializePrincipal(storagePrincipal);
|
|
}
|
|
|
|
if (data.context.principal) {
|
|
data.context.principal = E10SUtils.deserializePrincipal(
|
|
data.context.principal
|
|
);
|
|
}
|
|
if (data.context.storagePrincipal) {
|
|
data.context.storagePrincipal = E10SUtils.deserializePrincipal(
|
|
data.context.storagePrincipal
|
|
);
|
|
}
|
|
|
|
nsContextMenu.contentData = {
|
|
context: data.context,
|
|
browser,
|
|
actor,
|
|
editFlags: data.editFlags,
|
|
spellInfo,
|
|
principal,
|
|
storagePrincipal,
|
|
customMenuItems: data.customMenuItems,
|
|
documentURIObject,
|
|
docLocation: data.docLocation,
|
|
charSet: data.charSet,
|
|
referrerInfo: E10SUtils.deserializeReferrerInfo(data.referrerInfo),
|
|
frameReferrerInfo,
|
|
linkReferrerInfo,
|
|
contentType: data.contentType,
|
|
contentDisposition: data.contentDisposition,
|
|
frameOuterWindowID: data.frameOuterWindowID,
|
|
selectionInfo: data.selectionInfo,
|
|
disableSetDesktopBackground: data.disableSetDesktopBackground,
|
|
loginFillInfo: data.loginFillInfo,
|
|
parentAllowsMixedContent: data.parentAllowsMixedContent,
|
|
userContextId: data.userContextId,
|
|
webExtContextData: data.webExtContextData,
|
|
};
|
|
|
|
let popup = browser.ownerDocument.getElementById("contentAreaContextMenu");
|
|
let context = nsContextMenu.contentData.context;
|
|
|
|
// The event is a CPOW that can't be passed into the native openPopupAtScreen
|
|
// function. Therefore we synthesize a new MouseEvent to propagate the
|
|
// inputSource to the subsequently triggered popupshowing event.
|
|
var newEvent = document.createEvent("MouseEvent");
|
|
newEvent.initNSMouseEvent(
|
|
"contextmenu",
|
|
true,
|
|
true,
|
|
null,
|
|
0,
|
|
context.screenX,
|
|
context.screenY,
|
|
0,
|
|
0,
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
0,
|
|
null,
|
|
0,
|
|
context.mozInputSource
|
|
);
|
|
popup.openPopupAtScreen(newEvent.screenX, newEvent.screenY, true, newEvent);
|
|
}
|
|
|
|
class nsContextMenu {
|
|
constructor(aXulMenu, aIsShift) {
|
|
// Get contextual info.
|
|
this.setContext();
|
|
|
|
if (!this.shouldDisplay) {
|
|
return;
|
|
}
|
|
|
|
this.hasPageMenu = false;
|
|
this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
|
|
if (!aIsShift) {
|
|
this.hasPageMenu = PageMenuParent.addToPopup(
|
|
this.contentData.customMenuItems,
|
|
this.browser,
|
|
aXulMenu
|
|
);
|
|
|
|
let tab =
|
|
gBrowser && gBrowser.getTabForBrowser
|
|
? gBrowser.getTabForBrowser(this.browser)
|
|
: undefined;
|
|
|
|
let subject = {
|
|
menu: aXulMenu,
|
|
tab,
|
|
timeStamp: this.timeStamp,
|
|
isContentSelected: this.isContentSelected,
|
|
inFrame: this.inFrame,
|
|
isTextSelected: this.isTextSelected,
|
|
onTextInput: this.onTextInput,
|
|
onLink: this.onLink,
|
|
onImage: this.onImage,
|
|
onVideo: this.onVideo,
|
|
onAudio: this.onAudio,
|
|
onCanvas: this.onCanvas,
|
|
onEditable: this.onEditable,
|
|
onSpellcheckable: this.onSpellcheckable,
|
|
onPassword: this.onPassword,
|
|
srcUrl: this.mediaURL,
|
|
frameUrl: this.contentData ? this.contentData.docLocation : undefined,
|
|
pageUrl: this.browser ? this.browser.currentURI.spec : undefined,
|
|
linkText: this.linkTextStr,
|
|
linkUrl: this.linkURL,
|
|
selectionText: this.isTextSelected
|
|
? this.selectionInfo.fullText
|
|
: undefined,
|
|
frameId: this.frameOuterWindowID,
|
|
webExtBrowserType: this.webExtBrowserType,
|
|
webExtContextData: this.contentData
|
|
? this.contentData.webExtContextData
|
|
: undefined,
|
|
};
|
|
subject.wrappedJSObject = subject;
|
|
Services.obs.notifyObservers(subject, "on-build-contextmenu");
|
|
}
|
|
|
|
this.viewFrameSourceElement = document.getElementById(
|
|
"context-viewframesource"
|
|
);
|
|
this.ellipsis = "\u2026";
|
|
try {
|
|
this.ellipsis = Services.prefs.getComplexValue(
|
|
"intl.ellipsis",
|
|
Ci.nsIPrefLocalizedString
|
|
).data;
|
|
} catch (e) {}
|
|
|
|
// Reset after "on-build-contextmenu" notification in case selection was
|
|
// changed during the notification.
|
|
this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
|
|
this.onPlainTextLink = false;
|
|
|
|
let bookmarkPage = document.getElementById("context-bookmarkpage");
|
|
if (bookmarkPage) {
|
|
BookmarkingUI.onCurrentPageContextPopupShowing();
|
|
}
|
|
|
|
// Initialize (disable/remove) menu items.
|
|
this.initItems();
|
|
}
|
|
|
|
setContext() {
|
|
let context = Object.create(null);
|
|
|
|
if (nsContextMenu.contentData) {
|
|
this.contentData = nsContextMenu.contentData;
|
|
context = this.contentData.context;
|
|
nsContextMenu.contentData = null;
|
|
}
|
|
|
|
this.shouldDisplay = context.shouldDisplay;
|
|
this.timeStamp = context.timeStamp;
|
|
|
|
// Assign what's _possibly_ needed from `context` sent by ContextMenuChild.jsm
|
|
// Keep this consistent with the similar code in ContextMenu's _setContext
|
|
this.bgImageURL = context.bgImageURL;
|
|
this.imageDescURL = context.imageDescURL;
|
|
this.imageInfo = context.imageInfo;
|
|
this.mediaURL = context.mediaURL;
|
|
this.webExtBrowserType = context.webExtBrowserType;
|
|
|
|
this.canSpellCheck = context.canSpellCheck;
|
|
this.hasBGImage = context.hasBGImage;
|
|
this.hasMultipleBGImages = context.hasMultipleBGImages;
|
|
this.isDesignMode = context.isDesignMode;
|
|
this.inFrame = context.inFrame;
|
|
this.inSrcdocFrame = context.inSrcdocFrame;
|
|
this.inSyntheticDoc = context.inSyntheticDoc;
|
|
this.inTabBrowser = context.inTabBrowser;
|
|
this.inWebExtBrowser = context.inWebExtBrowser;
|
|
|
|
this.link = context.link;
|
|
this.linkDownload = context.linkDownload;
|
|
this.linkProtocol = context.linkProtocol;
|
|
this.linkTextStr = context.linkTextStr;
|
|
this.linkURL = context.linkURL;
|
|
this.linkURI = this.getLinkURI(); // can't send; regenerate
|
|
|
|
this.onAudio = context.onAudio;
|
|
this.onCanvas = context.onCanvas;
|
|
this.onCompletedImage = context.onCompletedImage;
|
|
this.onCTPPlugin = context.onCTPPlugin;
|
|
this.onDRMMedia = context.onDRMMedia;
|
|
this.onPiPVideo = context.onPiPVideo;
|
|
this.onEditable = context.onEditable;
|
|
this.onImage = context.onImage;
|
|
this.onKeywordField = context.onKeywordField;
|
|
this.onLink = context.onLink;
|
|
this.onLoadedImage = context.onLoadedImage;
|
|
this.onMailtoLink = context.onMailtoLink;
|
|
this.onMozExtLink = context.onMozExtLink;
|
|
this.onNumeric = context.onNumeric;
|
|
this.onPassword = context.onPassword;
|
|
this.onSaveableLink = context.onSaveableLink;
|
|
this.onSpellcheckable = context.onSpellcheckable;
|
|
this.onTextInput = context.onTextInput;
|
|
this.onVideo = context.onVideo;
|
|
|
|
this.target = context.target;
|
|
this.targetIdentifier = context.targetIdentifier;
|
|
|
|
this.principal = context.principal;
|
|
this.storagePrincipal = context.storagePrincipal;
|
|
this.frameOuterWindowID = context.frameOuterWindowID;
|
|
|
|
this.inSyntheticDoc = context.inSyntheticDoc;
|
|
this.inAboutDevtoolsToolbox = context.inAboutDevtoolsToolbox;
|
|
|
|
// Everything after this isn't sent directly from ContextMenu
|
|
if (this.target) {
|
|
this.ownerDoc = this.target.ownerDocument;
|
|
}
|
|
|
|
this.csp = E10SUtils.deserializeCSP(context.csp);
|
|
|
|
if (this.contentData) {
|
|
this.browser = this.contentData.browser;
|
|
this.selectionInfo = this.contentData.selectionInfo;
|
|
this.actor = this.contentData.actor;
|
|
} else {
|
|
this.browser = this.ownerDoc.defaultView.docShell.chromeEventHandler;
|
|
this.selectionInfo = BrowserUtils.getSelectionDetails(window);
|
|
this.actor = this.browser.browsingContext.currentWindowGlobal.getActor(
|
|
"ContextMenu"
|
|
);
|
|
}
|
|
|
|
const { gBrowser } = this.browser.ownerGlobal;
|
|
|
|
this.textSelected = this.selectionInfo.text;
|
|
this.isTextSelected = !!this.textSelected.length;
|
|
this.webExtBrowserType = this.browser.getAttribute(
|
|
"webextension-view-type"
|
|
);
|
|
this.inWebExtBrowser = !!this.webExtBrowserType;
|
|
this.inTabBrowser =
|
|
gBrowser && gBrowser.getTabForBrowser
|
|
? !!gBrowser.getTabForBrowser(this.browser)
|
|
: false;
|
|
|
|
if (context.shouldInitInlineSpellCheckerUINoChildren) {
|
|
InlineSpellCheckerUI.initFromRemote(
|
|
this.contentData.spellInfo,
|
|
this.actor.manager
|
|
);
|
|
}
|
|
|
|
if (context.shouldInitInlineSpellCheckerUIWithChildren) {
|
|
InlineSpellCheckerUI.initFromRemote(
|
|
this.contentData.spellInfo,
|
|
this.actor.manager
|
|
);
|
|
let canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck;
|
|
this.showItem("spell-check-enabled", canSpell);
|
|
this.showItem("spell-separator", canSpell);
|
|
}
|
|
} // setContext
|
|
|
|
hiding() {
|
|
if (this.actor) {
|
|
this.actor.hiding();
|
|
}
|
|
|
|
this.contentData = null;
|
|
InlineSpellCheckerUI.clearSuggestionsFromMenu();
|
|
InlineSpellCheckerUI.clearDictionaryListFromMenu();
|
|
InlineSpellCheckerUI.uninit();
|
|
if (
|
|
Cu.isModuleLoaded("resource://gre/modules/LoginManagerContextMenu.jsm")
|
|
) {
|
|
nsContextMenu.LoginManagerContextMenu.clearLoginsFromMenu(document);
|
|
}
|
|
|
|
// This handler self-deletes, only run it if it is still there:
|
|
if (this._onPopupHiding) {
|
|
this._onPopupHiding();
|
|
}
|
|
}
|
|
|
|
initItems() {
|
|
this.initPageMenuSeparator();
|
|
this.initOpenItems();
|
|
this.initNavigationItems();
|
|
this.initViewItems();
|
|
this.initMiscItems();
|
|
this.initSpellingItems();
|
|
this.initSaveItems();
|
|
this.initClipboardItems();
|
|
this.initMediaPlayerItems();
|
|
this.initLeaveDOMFullScreenItems();
|
|
this.initClickToPlayItems();
|
|
this.initPasswordManagerItems();
|
|
this.initSyncItems();
|
|
}
|
|
|
|
initPageMenuSeparator() {
|
|
this.showItem("page-menu-separator", this.hasPageMenu);
|
|
}
|
|
|
|
initOpenItems() {
|
|
var isMailtoInternal = false;
|
|
if (this.onMailtoLink) {
|
|
var mailtoHandler = Cc[
|
|
"@mozilla.org/uriloader/external-protocol-service;1"
|
|
]
|
|
.getService(Ci.nsIExternalProtocolService)
|
|
.getProtocolHandlerInfo("mailto");
|
|
isMailtoInternal =
|
|
!mailtoHandler.alwaysAskBeforeHandling &&
|
|
mailtoHandler.preferredAction == Ci.nsIHandlerInfo.useHelperApp &&
|
|
mailtoHandler.preferredApplicationHandler instanceof
|
|
Ci.nsIWebHandlerApp;
|
|
}
|
|
|
|
if (
|
|
this.isTextSelected &&
|
|
!this.onLink &&
|
|
this.selectionInfo &&
|
|
this.selectionInfo.linkURL
|
|
) {
|
|
this.linkURL = this.selectionInfo.linkURL;
|
|
try {
|
|
this.linkURI = makeURI(this.linkURL);
|
|
} catch (ex) {}
|
|
|
|
this.linkTextStr = this.selectionInfo.linkText;
|
|
this.onPlainTextLink = true;
|
|
}
|
|
|
|
var inContainer = false;
|
|
if (this.contentData.userContextId) {
|
|
inContainer = true;
|
|
var item = document.getElementById("context-openlinkincontainertab");
|
|
|
|
item.setAttribute("data-usercontextid", this.contentData.userContextId);
|
|
|
|
var label = ContextualIdentityService.getUserContextLabel(
|
|
this.contentData.userContextId
|
|
);
|
|
item.setAttribute(
|
|
"label",
|
|
gBrowserBundle.formatStringFromName("userContextOpenLink.label", [
|
|
label,
|
|
])
|
|
);
|
|
}
|
|
|
|
var shouldShow =
|
|
this.onSaveableLink || isMailtoInternal || this.onPlainTextLink;
|
|
var isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
|
|
let showContainers =
|
|
Services.prefs.getBoolPref("privacy.userContext.enabled") &&
|
|
ContextualIdentityService.getPublicIdentities().length;
|
|
this.showItem("context-openlink", shouldShow && !isWindowPrivate);
|
|
this.showItem(
|
|
"context-openlinkprivate",
|
|
shouldShow && PrivateBrowsingUtils.enabled
|
|
);
|
|
this.showItem("context-openlinkintab", shouldShow && !inContainer);
|
|
this.showItem("context-openlinkincontainertab", shouldShow && inContainer);
|
|
this.showItem(
|
|
"context-openlinkinusercontext-menu",
|
|
shouldShow && !isWindowPrivate && showContainers
|
|
);
|
|
this.showItem("context-openlinkincurrent", this.onPlainTextLink);
|
|
this.showItem("context-sep-open", shouldShow);
|
|
}
|
|
|
|
initNavigationItems() {
|
|
var shouldShow =
|
|
!(
|
|
this.isContentSelected ||
|
|
this.onLink ||
|
|
this.onImage ||
|
|
this.onCanvas ||
|
|
this.onVideo ||
|
|
this.onAudio ||
|
|
this.onTextInput
|
|
) && this.inTabBrowser;
|
|
this.showItem("context-navigation", shouldShow);
|
|
this.showItem("context-sep-navigation", shouldShow);
|
|
|
|
let stopped =
|
|
XULBrowserWindow.stopCommand.getAttribute("disabled") == "true";
|
|
|
|
let stopReloadItem = "";
|
|
if (shouldShow || !this.inTabBrowser) {
|
|
stopReloadItem = stopped || !this.inTabBrowser ? "reload" : "stop";
|
|
}
|
|
|
|
this.showItem("context-reload", stopReloadItem == "reload");
|
|
this.showItem("context-stop", stopReloadItem == "stop");
|
|
}
|
|
|
|
initLeaveDOMFullScreenItems() {
|
|
// only show the option if the user is in DOM fullscreen
|
|
var shouldShow = this.target.ownerDocument.fullscreen;
|
|
this.showItem("context-leave-dom-fullscreen", shouldShow);
|
|
|
|
// Explicitly show if in DOM fullscreen, but do not hide it has already been shown
|
|
if (shouldShow) {
|
|
this.showItem("context-media-sep-commands", true);
|
|
}
|
|
}
|
|
|
|
initSaveItems() {
|
|
var shouldShow = !(
|
|
this.onTextInput ||
|
|
this.onLink ||
|
|
this.isContentSelected ||
|
|
this.onImage ||
|
|
this.onCanvas ||
|
|
this.onVideo ||
|
|
this.onAudio
|
|
);
|
|
this.showItem("context-savepage", shouldShow);
|
|
|
|
// Save link depends on whether we're in a link, or selected text matches valid URL pattern.
|
|
this.showItem(
|
|
"context-savelink",
|
|
this.onSaveableLink || this.onPlainTextLink
|
|
);
|
|
|
|
// Save image depends on having loaded its content, video and audio don't.
|
|
this.showItem("context-saveimage", this.onLoadedImage || this.onCanvas);
|
|
this.showItem("context-savevideo", this.onVideo);
|
|
this.showItem("context-saveaudio", this.onAudio);
|
|
this.showItem("context-video-saveimage", this.onVideo);
|
|
this.setItemAttr("context-savevideo", "disabled", !this.mediaURL);
|
|
this.setItemAttr("context-saveaudio", "disabled", !this.mediaURL);
|
|
// Send media URL (but not for canvas, since it's a big data: URL)
|
|
this.showItem("context-sendimage", this.onImage);
|
|
this.showItem("context-sendvideo", this.onVideo);
|
|
this.showItem("context-sendaudio", this.onAudio);
|
|
let mediaIsBlob = this.mediaURL.startsWith("blob:");
|
|
this.setItemAttr(
|
|
"context-sendvideo",
|
|
"disabled",
|
|
!this.mediaURL || mediaIsBlob
|
|
);
|
|
this.setItemAttr(
|
|
"context-sendaudio",
|
|
"disabled",
|
|
!this.mediaURL || mediaIsBlob
|
|
);
|
|
}
|
|
|
|
initViewItems() {
|
|
// View source is always OK, unless in directory listing.
|
|
this.showItem(
|
|
"context-viewpartialsource-selection",
|
|
!this.inAboutDevtoolsToolbox && this.isContentSelected
|
|
);
|
|
|
|
var shouldShow = !(
|
|
this.isContentSelected ||
|
|
this.onImage ||
|
|
this.onCanvas ||
|
|
this.onVideo ||
|
|
this.onAudio ||
|
|
this.onLink ||
|
|
this.onTextInput
|
|
);
|
|
|
|
var showInspect =
|
|
this.inTabBrowser &&
|
|
!this.inAboutDevtoolsToolbox &&
|
|
Services.prefs.getBoolPref("devtools.inspector.enabled", true) &&
|
|
!Services.prefs.getBoolPref("devtools.policy.disabled", false);
|
|
|
|
var showInspectA11Y =
|
|
showInspect &&
|
|
// Only when accessibility service started.
|
|
Services.appinfo.accessibilityEnabled &&
|
|
this.inTabBrowser &&
|
|
Services.prefs.getBoolPref("devtools.enabled", true) &&
|
|
Services.prefs.getBoolPref("devtools.accessibility.enabled", true) &&
|
|
!Services.prefs.getBoolPref("devtools.policy.disabled", false);
|
|
|
|
this.showItem("context-viewsource", shouldShow);
|
|
this.showItem("context-viewinfo", shouldShow);
|
|
// The page info is broken for WebExtension popups, as the browser is
|
|
// destroyed when the popup is closed.
|
|
this.setItemAttr(
|
|
"context-viewinfo",
|
|
"disabled",
|
|
this.webExtBrowserType === "popup"
|
|
);
|
|
this.showItem("inspect-separator", showInspect);
|
|
this.showItem("context-inspect", showInspect);
|
|
|
|
this.showItem("context-inspect-a11y", showInspectA11Y);
|
|
|
|
this.showItem("context-sep-viewsource", shouldShow);
|
|
|
|
// Set as Desktop background depends on whether an image was clicked on,
|
|
// and only works if we have a shell service.
|
|
var haveSetDesktopBackground = false;
|
|
|
|
if (
|
|
AppConstants.HAVE_SHELL_SERVICE &&
|
|
Services.policies.isAllowed("setDesktopBackground")
|
|
) {
|
|
// Only enable Set as Desktop Background if we can get the shell service.
|
|
var shell = getShellService();
|
|
if (shell) {
|
|
haveSetDesktopBackground = shell.canSetDesktopBackground;
|
|
}
|
|
}
|
|
|
|
this.showItem(
|
|
"context-setDesktopBackground",
|
|
haveSetDesktopBackground && this.onLoadedImage
|
|
);
|
|
|
|
if (haveSetDesktopBackground && this.onLoadedImage) {
|
|
document.getElementById(
|
|
"context-setDesktopBackground"
|
|
).disabled = this.contentData.disableSetDesktopBackground;
|
|
}
|
|
|
|
// Reload image depends on an image that's not fully loaded
|
|
this.showItem(
|
|
"context-reloadimage",
|
|
this.onImage && !this.onCompletedImage
|
|
);
|
|
|
|
// View image depends on having an image that's not standalone
|
|
// (or is in a frame), or a canvas.
|
|
this.showItem(
|
|
"context-viewimage",
|
|
(this.onImage && (!this.inSyntheticDoc || this.inFrame)) || this.onCanvas
|
|
);
|
|
|
|
// View video depends on not having a standalone video.
|
|
this.showItem(
|
|
"context-viewvideo",
|
|
this.onVideo && (!this.inSyntheticDoc || this.inFrame)
|
|
);
|
|
this.setItemAttr("context-viewvideo", "disabled", !this.mediaURL);
|
|
|
|
// View background image depends on whether there is one, but don't make
|
|
// background images of a stand-alone media document available.
|
|
this.showItem(
|
|
"context-viewbgimage",
|
|
shouldShow && !this.hasMultipleBGImages && !this.inSyntheticDoc
|
|
);
|
|
this.showItem(
|
|
"context-sep-viewbgimage",
|
|
shouldShow && !this.hasMultipleBGImages && !this.inSyntheticDoc
|
|
);
|
|
document.getElementById("context-viewbgimage").disabled = !this.hasBGImage;
|
|
|
|
this.showItem("context-viewimageinfo", this.onImage);
|
|
// The image info popup is broken for WebExtension popups, since the browser
|
|
// is destroyed when the popup is closed.
|
|
this.setItemAttr(
|
|
"context-viewimageinfo",
|
|
"disabled",
|
|
this.webExtBrowserType === "popup"
|
|
);
|
|
this.showItem(
|
|
"context-viewimagedesc",
|
|
this.onImage && this.imageDescURL !== ""
|
|
);
|
|
}
|
|
|
|
initMiscItems() {
|
|
// Use "Bookmark This Link" if on a link.
|
|
let bookmarkPage = document.getElementById("context-bookmarkpage");
|
|
this.showItem(
|
|
bookmarkPage,
|
|
!(
|
|
this.isContentSelected ||
|
|
this.onTextInput ||
|
|
this.onLink ||
|
|
this.onImage ||
|
|
this.onVideo ||
|
|
this.onAudio ||
|
|
this.onCanvas ||
|
|
this.inWebExtBrowser
|
|
)
|
|
);
|
|
|
|
this.showItem(
|
|
"context-bookmarklink",
|
|
(this.onLink && !this.onMailtoLink && !this.onMozExtLink) ||
|
|
this.onPlainTextLink
|
|
);
|
|
this.showItem(
|
|
"context-keywordfield",
|
|
this.onTextInput && this.onKeywordField
|
|
);
|
|
this.showItem("frame", this.inFrame);
|
|
|
|
if (this.inFrame) {
|
|
// To make it easier to debug the browser running with out-of-process iframes, we
|
|
// display the process PID of the iframe in the context menu for the subframe.
|
|
let frameOsPid = this.actor.manager.browsingContext.currentWindowGlobal
|
|
.osPid;
|
|
this.setItemAttr("context-frameOsPid", "label", "PID: " + frameOsPid);
|
|
}
|
|
|
|
this.showAndFormatSearchContextItem();
|
|
|
|
// srcdoc cannot be opened separately due to concerns about web
|
|
// content with about:srcdoc in location bar masquerading as trusted
|
|
// chrome/addon content.
|
|
// No need to also test for this.inFrame as this is checked in the parent
|
|
// submenu.
|
|
this.showItem("context-showonlythisframe", !this.inSrcdocFrame);
|
|
this.showItem("context-openframeintab", !this.inSrcdocFrame);
|
|
this.showItem("context-openframe", !this.inSrcdocFrame);
|
|
this.showItem("context-bookmarkframe", !this.inSrcdocFrame);
|
|
this.showItem("open-frame-sep", !this.inSrcdocFrame);
|
|
|
|
this.showItem("frame-sep", this.inFrame && this.isTextSelected);
|
|
|
|
// Hide menu entries for images, show otherwise
|
|
if (this.inFrame) {
|
|
if (
|
|
BrowserUtils.mimeTypeIsTextBased(this.target.ownerDocument.contentType)
|
|
) {
|
|
this.viewFrameSourceElement.removeAttribute("hidden");
|
|
} else {
|
|
this.viewFrameSourceElement.setAttribute("hidden", "true");
|
|
}
|
|
}
|
|
|
|
// BiDi UI
|
|
this.showItem("context-sep-bidi", !this.onNumeric && top.gBidiUI);
|
|
this.showItem(
|
|
"context-bidi-text-direction-toggle",
|
|
this.onTextInput && !this.onNumeric && top.gBidiUI
|
|
);
|
|
this.showItem(
|
|
"context-bidi-page-direction-toggle",
|
|
!this.onTextInput && top.gBidiUI
|
|
);
|
|
}
|
|
|
|
initSpellingItems() {
|
|
var canSpell =
|
|
InlineSpellCheckerUI.canSpellCheck &&
|
|
!InlineSpellCheckerUI.initialSpellCheckPending &&
|
|
this.canSpellCheck;
|
|
let showDictionaries = canSpell && InlineSpellCheckerUI.enabled;
|
|
var onMisspelling = InlineSpellCheckerUI.overMisspelling;
|
|
var showUndo = canSpell && InlineSpellCheckerUI.canUndo();
|
|
this.showItem("spell-check-enabled", canSpell);
|
|
this.showItem("spell-separator", canSpell);
|
|
document
|
|
.getElementById("spell-check-enabled")
|
|
.setAttribute("checked", canSpell && InlineSpellCheckerUI.enabled);
|
|
|
|
this.showItem("spell-add-to-dictionary", onMisspelling);
|
|
this.showItem("spell-undo-add-to-dictionary", showUndo);
|
|
|
|
// suggestion list
|
|
this.showItem("spell-suggestions-separator", onMisspelling || showUndo);
|
|
if (onMisspelling) {
|
|
var suggestionsSeparator = document.getElementById(
|
|
"spell-add-to-dictionary"
|
|
);
|
|
var numsug = InlineSpellCheckerUI.addSuggestionsToMenu(
|
|
suggestionsSeparator.parentNode,
|
|
suggestionsSeparator,
|
|
5
|
|
);
|
|
this.showItem("spell-no-suggestions", numsug == 0);
|
|
} else {
|
|
this.showItem("spell-no-suggestions", false);
|
|
}
|
|
|
|
// dictionary list
|
|
this.showItem("spell-dictionaries", showDictionaries);
|
|
if (canSpell) {
|
|
var dictMenu = document.getElementById("spell-dictionaries-menu");
|
|
var dictSep = document.getElementById("spell-language-separator");
|
|
let count = InlineSpellCheckerUI.addDictionaryListToMenu(
|
|
dictMenu,
|
|
dictSep
|
|
);
|
|
this.showItem(dictSep, count > 0);
|
|
this.showItem("spell-add-dictionaries-main", false);
|
|
} else if (this.onSpellcheckable) {
|
|
// when there is no spellchecker but we might be able to spellcheck
|
|
// add the add to dictionaries item. This will ensure that people
|
|
// with no dictionaries will be able to download them
|
|
this.showItem("spell-language-separator", showDictionaries);
|
|
this.showItem("spell-add-dictionaries-main", showDictionaries);
|
|
} else {
|
|
this.showItem("spell-add-dictionaries-main", false);
|
|
}
|
|
}
|
|
|
|
initClipboardItems() {
|
|
// Copy depends on whether there is selected text.
|
|
// Enabling this context menu item is now done through the global
|
|
// command updating system
|
|
// this.setItemAttr( "context-copy", "disabled", !this.isTextSelected() );
|
|
goUpdateGlobalEditMenuItems();
|
|
|
|
this.showItem("context-undo", this.onTextInput);
|
|
this.showItem("context-sep-undo", this.onTextInput);
|
|
this.showItem("context-cut", this.onTextInput);
|
|
this.showItem("context-copy", this.isContentSelected || this.onTextInput);
|
|
this.showItem("context-paste", this.onTextInput);
|
|
this.showItem("context-delete", this.onTextInput);
|
|
this.showItem("context-sep-paste", this.onTextInput);
|
|
this.showItem(
|
|
"context-selectall",
|
|
!(
|
|
this.onLink ||
|
|
this.onImage ||
|
|
this.onVideo ||
|
|
this.onAudio ||
|
|
this.inSyntheticDoc
|
|
) || this.isDesignMode
|
|
);
|
|
this.showItem(
|
|
"context-sep-selectall",
|
|
!this.inAboutDevtoolsToolbox && this.isContentSelected
|
|
);
|
|
|
|
// XXX dr
|
|
// ------
|
|
// nsDocumentViewer.cpp has code to determine whether we're
|
|
// on a link or an image. we really ought to be using that...
|
|
|
|
// Copy email link depends on whether we're on an email link.
|
|
this.showItem("context-copyemail", this.onMailtoLink);
|
|
|
|
// Copy link location depends on whether we're on a non-mailto link.
|
|
this.showItem("context-copylink", this.onLink && !this.onMailtoLink);
|
|
this.showItem(
|
|
"context-sep-copylink",
|
|
this.onLink && (this.onImage || this.onVideo || this.onAudio)
|
|
);
|
|
|
|
// Copy image contents depends on whether we're on an image.
|
|
// Note: the element doesn't exist on all platforms, but showItem() takes
|
|
// care of that by itself.
|
|
this.showItem("context-copyimage-contents", this.onImage);
|
|
|
|
// Copy image location depends on whether we're on an image.
|
|
this.showItem("context-copyimage", this.onImage);
|
|
this.showItem("context-copyvideourl", this.onVideo);
|
|
this.showItem("context-copyaudiourl", this.onAudio);
|
|
this.setItemAttr("context-copyvideourl", "disabled", !this.mediaURL);
|
|
this.setItemAttr("context-copyaudiourl", "disabled", !this.mediaURL);
|
|
this.showItem(
|
|
"context-sep-copyimage",
|
|
this.onImage || this.onVideo || this.onAudio
|
|
);
|
|
}
|
|
|
|
initMediaPlayerItems() {
|
|
var onMedia = this.onVideo || this.onAudio;
|
|
// Several mutually exclusive items... play/pause, mute/unmute, show/hide
|
|
this.showItem(
|
|
"context-media-play",
|
|
onMedia && (this.target.paused || this.target.ended)
|
|
);
|
|
this.showItem(
|
|
"context-media-pause",
|
|
onMedia && !this.target.paused && !this.target.ended
|
|
);
|
|
this.showItem("context-media-mute", onMedia && !this.target.muted);
|
|
this.showItem("context-media-unmute", onMedia && this.target.muted);
|
|
this.showItem(
|
|
"context-media-playbackrate",
|
|
onMedia && this.target.duration != Number.POSITIVE_INFINITY
|
|
);
|
|
this.showItem("context-media-loop", onMedia);
|
|
this.showItem(
|
|
"context-media-showcontrols",
|
|
onMedia && !this.target.controls
|
|
);
|
|
this.showItem(
|
|
"context-media-hidecontrols",
|
|
this.target.controls &&
|
|
(this.onVideo || (this.onAudio && !this.inSyntheticDoc))
|
|
);
|
|
this.showItem(
|
|
"context-video-fullscreen",
|
|
this.onVideo && !this.target.ownerDocument.fullscreen
|
|
);
|
|
{
|
|
let shouldDisplay =
|
|
Services.prefs.getBoolPref(
|
|
"media.videocontrols.picture-in-picture.enabled"
|
|
) &&
|
|
this.onVideo &&
|
|
!this.target.ownerDocument.fullscreen;
|
|
this.showItem("context-video-pictureinpicture", shouldDisplay);
|
|
}
|
|
this.showItem("context-media-eme-learnmore", this.onDRMMedia);
|
|
this.showItem("context-media-eme-separator", this.onDRMMedia);
|
|
|
|
// Disable them when there isn't a valid media source loaded.
|
|
if (onMedia) {
|
|
this.setItemAttr(
|
|
"context-media-playbackrate-050x",
|
|
"checked",
|
|
this.target.playbackRate == 0.5
|
|
);
|
|
this.setItemAttr(
|
|
"context-media-playbackrate-100x",
|
|
"checked",
|
|
this.target.playbackRate == 1.0
|
|
);
|
|
this.setItemAttr(
|
|
"context-media-playbackrate-125x",
|
|
"checked",
|
|
this.target.playbackRate == 1.25
|
|
);
|
|
this.setItemAttr(
|
|
"context-media-playbackrate-150x",
|
|
"checked",
|
|
this.target.playbackRate == 1.5
|
|
);
|
|
this.setItemAttr(
|
|
"context-media-playbackrate-200x",
|
|
"checked",
|
|
this.target.playbackRate == 2.0
|
|
);
|
|
this.setItemAttr("context-media-loop", "checked", this.target.loop);
|
|
var hasError =
|
|
this.target.error != null ||
|
|
this.target.networkState == this.target.NETWORK_NO_SOURCE;
|
|
this.setItemAttr("context-media-play", "disabled", hasError);
|
|
this.setItemAttr("context-media-pause", "disabled", hasError);
|
|
this.setItemAttr("context-media-mute", "disabled", hasError);
|
|
this.setItemAttr("context-media-unmute", "disabled", hasError);
|
|
this.setItemAttr("context-media-playbackrate", "disabled", hasError);
|
|
this.setItemAttr("context-media-playbackrate-050x", "disabled", hasError);
|
|
this.setItemAttr("context-media-playbackrate-100x", "disabled", hasError);
|
|
this.setItemAttr("context-media-playbackrate-125x", "disabled", hasError);
|
|
this.setItemAttr("context-media-playbackrate-150x", "disabled", hasError);
|
|
this.setItemAttr("context-media-playbackrate-200x", "disabled", hasError);
|
|
this.setItemAttr("context-media-showcontrols", "disabled", hasError);
|
|
this.setItemAttr("context-media-hidecontrols", "disabled", hasError);
|
|
if (this.onVideo) {
|
|
let canSaveSnapshot =
|
|
!this.onDRMMedia &&
|
|
this.target.readyState >= this.target.HAVE_CURRENT_DATA;
|
|
this.setItemAttr(
|
|
"context-video-saveimage",
|
|
"disabled",
|
|
!canSaveSnapshot
|
|
);
|
|
this.setItemAttr("context-video-fullscreen", "disabled", hasError);
|
|
this.setItemAttr(
|
|
"context-video-pictureinpicture",
|
|
"checked",
|
|
this.onPiPVideo
|
|
);
|
|
this.setItemAttr(
|
|
"context-video-pictureinpicture",
|
|
"disabled",
|
|
!this.onPiPVideo && hasError
|
|
);
|
|
}
|
|
}
|
|
this.showItem("context-media-sep-commands", onMedia);
|
|
}
|
|
|
|
initClickToPlayItems() {
|
|
this.showItem("context-ctp-play", this.onCTPPlugin);
|
|
this.showItem("context-ctp-hide", this.onCTPPlugin);
|
|
this.showItem("context-sep-ctp", this.onCTPPlugin);
|
|
}
|
|
|
|
initPasswordManagerItems() {
|
|
let loginFillInfo = this.contentData && this.contentData.loginFillInfo;
|
|
let documentURI = this.contentData.documentURIObject;
|
|
|
|
// If we could not find a password field we
|
|
// don't want to show the form fill option.
|
|
let showFill =
|
|
loginFillInfo &&
|
|
loginFillInfo.passwordField.found &&
|
|
!documentURI.schemeIs("about");
|
|
|
|
// Disable the fill option if the user hasn't unlocked with their master password
|
|
// or if the password field or target field are disabled.
|
|
// XXX: Bug 1529025 to respect signon.rememberSignons
|
|
let disableFill =
|
|
!loginFillInfo ||
|
|
!Services.logins ||
|
|
!Services.logins.isLoggedIn ||
|
|
loginFillInfo.passwordField.disabled ||
|
|
(!this.onPassword && loginFillInfo.usernameField.disabled);
|
|
|
|
this.showItem("fill-login-separator", showFill);
|
|
this.showItem("fill-login", showFill);
|
|
this.setItemAttr("fill-login", "disabled", disableFill);
|
|
|
|
// Set the correct label for the fill menu
|
|
let fillMenu = document.getElementById("fill-login");
|
|
if (this.onPassword) {
|
|
fillMenu.setAttribute("label", fillMenu.getAttribute("label-password"));
|
|
fillMenu.setAttribute(
|
|
"accesskey",
|
|
fillMenu.getAttribute("accesskey-password")
|
|
);
|
|
} else {
|
|
fillMenu.setAttribute("label", fillMenu.getAttribute("label-login"));
|
|
fillMenu.setAttribute(
|
|
"accesskey",
|
|
fillMenu.getAttribute("accesskey-login")
|
|
);
|
|
}
|
|
|
|
if (!showFill || disableFill) {
|
|
return;
|
|
}
|
|
|
|
let formOrigin = LoginHelper.getLoginOrigin(documentURI.spec);
|
|
let fragment = nsContextMenu.LoginManagerContextMenu.addLoginsToMenu(
|
|
this.targetIdentifier,
|
|
this.browser,
|
|
formOrigin
|
|
);
|
|
let isGeneratedPasswordEnabled =
|
|
LoginHelper.generationAvailable && LoginHelper.generationEnabled;
|
|
let canFillGeneratedPassword =
|
|
this.onPassword &&
|
|
isGeneratedPasswordEnabled &&
|
|
Services.logins.getLoginSavingEnabled(formOrigin);
|
|
|
|
this.showItem("fill-login-no-logins", !fragment);
|
|
this.showItem("fill-login-generated-password", canFillGeneratedPassword);
|
|
this.showItem("generated-password-separator", canFillGeneratedPassword);
|
|
|
|
this.setItemAttr(
|
|
"fill-login-generated-password",
|
|
"disabled",
|
|
PrivateBrowsingUtils.isWindowPrivate(window)
|
|
);
|
|
|
|
if (!fragment) {
|
|
return;
|
|
}
|
|
let popup = document.getElementById("fill-login-popup");
|
|
let insertBeforeElement = document.getElementById("fill-login-no-logins");
|
|
popup.insertBefore(fragment, insertBeforeElement);
|
|
}
|
|
|
|
initSyncItems() {
|
|
gSync.updateContentContextMenu(this);
|
|
}
|
|
|
|
openPasswordManager() {
|
|
LoginHelper.openPasswordManager(window, {
|
|
filterString: this.contentData.documentURIObject.host,
|
|
entryPoint: "contextmenu",
|
|
});
|
|
}
|
|
|
|
fillGeneratedPassword() {
|
|
nsContextMenu.LoginManagerContextMenu.fillGeneratedPassword(
|
|
this.targetIdentifier,
|
|
this.contentData.documentURIObject,
|
|
this.browser
|
|
);
|
|
}
|
|
|
|
inspectNode() {
|
|
return nsContextMenu.DevToolsShim.inspectNode(
|
|
gBrowser.selectedTab,
|
|
this.targetIdentifier
|
|
);
|
|
}
|
|
|
|
inspectA11Y() {
|
|
return nsContextMenu.DevToolsShim.inspectA11Y(
|
|
gBrowser.selectedTab,
|
|
this.targetIdentifier
|
|
);
|
|
}
|
|
|
|
_openLinkInParameters(extra) {
|
|
let params = {
|
|
charset: this.contentData.charSet,
|
|
originPrincipal: this.principal,
|
|
originStoragePrincipal: this.storagePrincipal,
|
|
triggeringPrincipal: this.principal,
|
|
csp: this.csp,
|
|
frameOuterWindowID: this.contentData.frameOuterWindowID,
|
|
};
|
|
for (let p in extra) {
|
|
params[p] = extra[p];
|
|
}
|
|
|
|
let referrerInfo = this.onLink
|
|
? this.contentData.linkReferrerInfo
|
|
: this.contentData.referrerInfo;
|
|
// If we want to change userContextId, we must be sure that we don't
|
|
// propagate the referrer.
|
|
if (
|
|
("userContextId" in params &&
|
|
params.userContextId != this.contentData.userContextId) ||
|
|
this.onPlainTextLink
|
|
) {
|
|
referrerInfo = new ReferrerInfo(
|
|
referrerInfo.referrerPolicy,
|
|
false,
|
|
referrerInfo.originalReferrer
|
|
);
|
|
}
|
|
|
|
params.referrerInfo = referrerInfo;
|
|
return params;
|
|
}
|
|
|
|
// Open linked-to URL in a new window.
|
|
openLink() {
|
|
openLinkIn(this.linkURL, "window", this._openLinkInParameters());
|
|
}
|
|
|
|
// Open linked-to URL in a new private window.
|
|
openLinkInPrivateWindow() {
|
|
openLinkIn(
|
|
this.linkURL,
|
|
"window",
|
|
this._openLinkInParameters({ private: true })
|
|
);
|
|
}
|
|
|
|
// Open linked-to URL in a new tab.
|
|
openLinkInTab(event) {
|
|
let referrerURI = this.contentData.documentURIObject;
|
|
|
|
// if its parent allows mixed content and the referring URI passes
|
|
// a same origin check with the target URI, we can preserve the users
|
|
// decision of disabling MCB on a page for it's child tabs.
|
|
let persistAllowMixedContentInChildTab = false;
|
|
|
|
if (this.contentData.parentAllowsMixedContent) {
|
|
const sm = Services.scriptSecurityManager;
|
|
try {
|
|
let targetURI = this.linkURI;
|
|
let isPrivateWin =
|
|
this.browser.contentPrincipal.originAttributes.privateBrowsingId > 0;
|
|
sm.checkSameOriginURI(referrerURI, targetURI, false, isPrivateWin);
|
|
persistAllowMixedContentInChildTab = true;
|
|
} catch (e) {}
|
|
}
|
|
|
|
let params = {
|
|
allowMixedContent: persistAllowMixedContentInChildTab,
|
|
userContextId: parseInt(event.target.getAttribute("data-usercontextid")),
|
|
};
|
|
|
|
openLinkIn(this.linkURL, "tab", this._openLinkInParameters(params));
|
|
}
|
|
|
|
// open URL in current tab
|
|
openLinkInCurrent() {
|
|
openLinkIn(this.linkURL, "current", this._openLinkInParameters());
|
|
}
|
|
|
|
// Open frame in a new tab.
|
|
openFrameInTab() {
|
|
openLinkIn(this.contentData.docLocation, "tab", {
|
|
charset: this.contentData.charSet,
|
|
triggeringPrincipal: this.browser.contentPrincipal,
|
|
csp: this.browser.csp,
|
|
referrerInfo: this.contentData.frameReferrerInfo,
|
|
});
|
|
}
|
|
|
|
// Reload clicked-in frame.
|
|
reloadFrame(aEvent) {
|
|
let forceReload = aEvent.shiftKey;
|
|
this.actor.reloadFrame(this.targetIdentifier, forceReload);
|
|
}
|
|
|
|
// Open clicked-in frame in its own window.
|
|
openFrame() {
|
|
openLinkIn(this.contentData.docLocation, "window", {
|
|
charset: this.contentData.charSet,
|
|
triggeringPrincipal: this.browser.contentPrincipal,
|
|
csp: this.browser.csp,
|
|
referrerInfo: this.contentData.frameReferrerInfo,
|
|
});
|
|
}
|
|
|
|
// Open clicked-in frame in the same window.
|
|
showOnlyThisFrame() {
|
|
urlSecurityCheck(
|
|
this.contentData.docLocation,
|
|
this.browser.contentPrincipal,
|
|
Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
|
|
);
|
|
openWebLinkIn(this.contentData.docLocation, "current", {
|
|
referrerInfo: this.contentData.frameReferrerInfo,
|
|
triggeringPrincipal: this.browser.contentPrincipal,
|
|
});
|
|
}
|
|
|
|
// View Partial Source
|
|
viewPartialSource() {
|
|
let { browser } = this;
|
|
let openSelectionFn = function() {
|
|
let tabBrowser = gBrowser;
|
|
const inNewWindow = !Services.prefs.getBoolPref("view_source.tab");
|
|
// In the case of popups, we need to find a non-popup browser window.
|
|
// We might also not have a tabBrowser reference (if this isn't in a
|
|
// a tabbrowser scope) or might have a fake/stub tabbrowser reference
|
|
// (in the sidebar). Deal with those cases:
|
|
if (!tabBrowser || !tabBrowser.loadOneTab || !window.toolbar.visible) {
|
|
// This returns only non-popup browser windows by default.
|
|
let browserWindow = BrowserWindowTracker.getTopWindow();
|
|
tabBrowser = browserWindow.gBrowser;
|
|
}
|
|
let relatedToCurrent = gBrowser && gBrowser.selectedBrowser == browser;
|
|
let tab = tabBrowser.loadOneTab("about:blank", {
|
|
relatedToCurrent,
|
|
inBackground: inNewWindow,
|
|
skipAnimation: inNewWindow,
|
|
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
|
});
|
|
const viewSourceBrowser = tabBrowser.getBrowserForTab(tab);
|
|
if (inNewWindow) {
|
|
tabBrowser.hideTab(tab);
|
|
tabBrowser.replaceTabsWithWindow(tab);
|
|
}
|
|
return viewSourceBrowser;
|
|
};
|
|
|
|
top.gViewSourceUtils.viewPartialSourceInBrowser(browser, openSelectionFn);
|
|
}
|
|
|
|
// Open new "view source" window with the frame's URL.
|
|
viewFrameSource() {
|
|
BrowserViewSourceOfDocument({
|
|
browser: this.browser,
|
|
URL: this.contentData.docLocation,
|
|
outerWindowID: this.frameOuterWindowID,
|
|
});
|
|
}
|
|
|
|
viewInfo() {
|
|
BrowserPageInfo(
|
|
this.contentData.docLocation,
|
|
null,
|
|
null,
|
|
null,
|
|
this.browser
|
|
);
|
|
}
|
|
|
|
viewImageInfo() {
|
|
BrowserPageInfo(
|
|
this.contentData.docLocation,
|
|
"mediaTab",
|
|
this.imageInfo,
|
|
null,
|
|
this.browser
|
|
);
|
|
}
|
|
|
|
viewImageDesc(e) {
|
|
urlSecurityCheck(
|
|
this.imageDescURL,
|
|
this.principal,
|
|
Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
|
|
);
|
|
openUILink(this.imageDescURL, e, {
|
|
referrerInfo: this.contentData.referrerInfo,
|
|
triggeringPrincipal: this.principal,
|
|
csp: this.csp,
|
|
});
|
|
}
|
|
|
|
viewFrameInfo() {
|
|
BrowserPageInfo(
|
|
this.contentData.docLocation,
|
|
null,
|
|
null,
|
|
this.actor.browsingContext,
|
|
this.browser
|
|
);
|
|
}
|
|
|
|
reloadImage() {
|
|
urlSecurityCheck(
|
|
this.mediaURL,
|
|
this.principal,
|
|
Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
|
|
);
|
|
this.actor.reloadImage(this.targetIdentifier);
|
|
}
|
|
|
|
_canvasToBlobURL(targetIdentifier) {
|
|
return this.actor.canvasToBlobURL(targetIdentifier);
|
|
}
|
|
|
|
// Change current window to the URL of the image, video, or audio.
|
|
viewMedia(e) {
|
|
let referrerInfo = this.contentData.referrerInfo;
|
|
let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
|
|
if (this.onCanvas) {
|
|
this._canvasToBlobURL(this.targetIdentifier).then(function(blobURL) {
|
|
openUILink(blobURL, e, {
|
|
referrerInfo,
|
|
triggeringPrincipal: systemPrincipal,
|
|
});
|
|
}, Cu.reportError);
|
|
} else {
|
|
urlSecurityCheck(
|
|
this.mediaURL,
|
|
this.principal,
|
|
Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
|
|
);
|
|
openUILink(this.mediaURL, e, {
|
|
referrerInfo,
|
|
forceAllowDataURI: true,
|
|
triggeringPrincipal: this.principal,
|
|
csp: this.csp,
|
|
});
|
|
}
|
|
}
|
|
|
|
saveVideoFrameAsImage() {
|
|
let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
|
|
|
|
let name = "";
|
|
if (this.mediaURL) {
|
|
try {
|
|
let uri = makeURI(this.mediaURL);
|
|
let url = uri.QueryInterface(Ci.nsIURL);
|
|
if (url.fileBaseName) {
|
|
name = decodeURI(url.fileBaseName) + ".jpg";
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
if (!name) {
|
|
name = "snapshot.jpg";
|
|
}
|
|
|
|
// Cache this because we fetch the data async
|
|
let referrerInfo = this.contentData.referrerInfo;
|
|
|
|
this.actor.saveVideoFrameAsImage(this.targetIdentifier).then(dataURL => {
|
|
// FIXME can we switch this to a blob URL?
|
|
saveImageURL(
|
|
dataURL,
|
|
name,
|
|
"SaveImageTitle",
|
|
true, // bypass cache
|
|
false, // don't skip prompt for where to save
|
|
referrerInfo, // referrer info
|
|
null, // document
|
|
null, // content type
|
|
null, // content disposition
|
|
isPrivate,
|
|
this.principal
|
|
);
|
|
});
|
|
}
|
|
|
|
leaveDOMFullScreen() {
|
|
document.exitFullscreen();
|
|
}
|
|
|
|
// Change current window to the URL of the background image.
|
|
viewBGImage(e) {
|
|
urlSecurityCheck(
|
|
this.bgImageURL,
|
|
this.principal,
|
|
Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
|
|
);
|
|
|
|
openUILink(this.bgImageURL, e, {
|
|
referrerInfo: this.contentData.referrerInfo,
|
|
triggeringPrincipal: this.principal,
|
|
csp: this.csp,
|
|
});
|
|
}
|
|
|
|
setDesktopBackground() {
|
|
if (!Services.policies.isAllowed("setDesktopBackground")) {
|
|
return;
|
|
}
|
|
|
|
this.actor
|
|
.setAsDesktopBackground(this.targetIdentifier)
|
|
.then(({ failed, dataURL, imageName }) => {
|
|
if (failed) {
|
|
return;
|
|
}
|
|
|
|
let image = document.createElementNS(
|
|
"http://www.w3.org/1999/xhtml",
|
|
"img"
|
|
);
|
|
image.src = dataURL;
|
|
|
|
// Confirm since it's annoying if you hit this accidentally.
|
|
const kDesktopBackgroundURL =
|
|
"chrome://browser/content/setDesktopBackground.xul";
|
|
|
|
if (AppConstants.platform == "macosx") {
|
|
// On Mac, the Set Desktop Background window is not modal.
|
|
// Don't open more than one Set Desktop Background window.
|
|
let dbWin = Services.wm.getMostRecentWindow(
|
|
"Shell:SetDesktopBackground"
|
|
);
|
|
if (dbWin) {
|
|
dbWin.gSetBackground.init(image, imageName);
|
|
dbWin.focus();
|
|
} else {
|
|
openDialog(
|
|
kDesktopBackgroundURL,
|
|
"",
|
|
"centerscreen,chrome,dialog=no,dependent,resizable=no",
|
|
image,
|
|
imageName
|
|
);
|
|
}
|
|
} else {
|
|
// On non-Mac platforms, the Set Wallpaper dialog is modal.
|
|
openDialog(
|
|
kDesktopBackgroundURL,
|
|
"",
|
|
"centerscreen,chrome,dialog,modal,dependent",
|
|
image,
|
|
imageName
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Save URL of clicked-on frame.
|
|
saveFrame() {
|
|
saveBrowser(this.browser, false, this.frameOuterWindowID);
|
|
}
|
|
|
|
// Helper function to wait for appropriate MIME-type headers and
|
|
// then prompt the user with a file picker
|
|
saveHelper(
|
|
linkURL,
|
|
linkText,
|
|
dialogTitle,
|
|
bypassCache,
|
|
doc,
|
|
referrerInfo,
|
|
windowID,
|
|
linkDownload,
|
|
isContentWindowPrivate
|
|
) {
|
|
// canonical def in nsURILoader.h
|
|
const NS_ERROR_SAVE_LINK_AS_TIMEOUT = 0x805d0020;
|
|
|
|
// an object to proxy the data through to
|
|
// nsIExternalHelperAppService.doContent, which will wait for the
|
|
// appropriate MIME-type headers and then prompt the user with a
|
|
// file picker
|
|
function saveAsListener(principal) {
|
|
this._triggeringPrincipal = principal;
|
|
}
|
|
saveAsListener.prototype = {
|
|
extListener: null,
|
|
|
|
onStartRequest: function saveLinkAs_onStartRequest(aRequest) {
|
|
// if the timer fired, the error status will have been caused by that,
|
|
// and we'll be restarting in onStopRequest, so no reason to notify
|
|
// the user
|
|
if (aRequest.status == NS_ERROR_SAVE_LINK_AS_TIMEOUT) {
|
|
return;
|
|
}
|
|
|
|
timer.cancel();
|
|
|
|
// some other error occured; notify the user...
|
|
if (!Components.isSuccessCode(aRequest.status)) {
|
|
try {
|
|
const bundle = Services.strings.createBundle(
|
|
"chrome://mozapps/locale/downloads/downloads.properties"
|
|
);
|
|
|
|
const title = bundle.GetStringFromName("downloadErrorAlertTitle");
|
|
const msg = bundle.GetStringFromName("downloadErrorGeneric");
|
|
|
|
let window = Services.wm.getOuterWindowWithId(windowID);
|
|
Services.prompt.alert(window, title, msg);
|
|
} catch (ex) {}
|
|
return;
|
|
}
|
|
|
|
let extHelperAppSvc = Cc[
|
|
"@mozilla.org/uriloader/external-helper-app-service;1"
|
|
].getService(Ci.nsIExternalHelperAppService);
|
|
let channel = aRequest.QueryInterface(Ci.nsIChannel);
|
|
this.extListener = extHelperAppSvc.doContent(
|
|
channel.contentType,
|
|
aRequest,
|
|
null,
|
|
true,
|
|
window
|
|
);
|
|
this.extListener.onStartRequest(aRequest);
|
|
},
|
|
|
|
onStopRequest: function saveLinkAs_onStopRequest(aRequest, aStatusCode) {
|
|
if (aStatusCode == NS_ERROR_SAVE_LINK_AS_TIMEOUT) {
|
|
// do it the old fashioned way, which will pick the best filename
|
|
// it can without waiting.
|
|
saveURL(
|
|
linkURL,
|
|
linkText,
|
|
dialogTitle,
|
|
bypassCache,
|
|
false,
|
|
referrerInfo,
|
|
doc,
|
|
isContentWindowPrivate,
|
|
this._triggeringPrincipal
|
|
);
|
|
}
|
|
if (this.extListener) {
|
|
this.extListener.onStopRequest(aRequest, aStatusCode);
|
|
}
|
|
},
|
|
|
|
onDataAvailable: function saveLinkAs_onDataAvailable(
|
|
aRequest,
|
|
aInputStream,
|
|
aOffset,
|
|
aCount
|
|
) {
|
|
this.extListener.onDataAvailable(
|
|
aRequest,
|
|
aInputStream,
|
|
aOffset,
|
|
aCount
|
|
);
|
|
},
|
|
};
|
|
|
|
function callbacks() {}
|
|
callbacks.prototype = {
|
|
getInterface: function sLA_callbacks_getInterface(aIID) {
|
|
if (aIID.equals(Ci.nsIAuthPrompt) || aIID.equals(Ci.nsIAuthPrompt2)) {
|
|
// If the channel demands authentication prompt, we must cancel it
|
|
// because the save-as-timer would expire and cancel the channel
|
|
// before we get credentials from user. Both authentication dialog
|
|
// and save as dialog would appear on the screen as we fall back to
|
|
// the old fashioned way after the timeout.
|
|
timer.cancel();
|
|
channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
|
|
}
|
|
throw Cr.NS_ERROR_NO_INTERFACE;
|
|
},
|
|
};
|
|
|
|
// if it we don't have the headers after a short time, the user
|
|
// won't have received any feedback from their click. that's bad. so
|
|
// we give up waiting for the filename.
|
|
function timerCallback() {}
|
|
timerCallback.prototype = {
|
|
notify: function sLA_timer_notify(aTimer) {
|
|
channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
|
|
},
|
|
};
|
|
|
|
// setting up a new channel for 'right click - save link as ...'
|
|
var channel = NetUtil.newChannel({
|
|
uri: makeURI(linkURL),
|
|
loadingPrincipal: this.principal,
|
|
contentPolicyType: Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD,
|
|
securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS,
|
|
});
|
|
|
|
if (linkDownload) {
|
|
channel.contentDispositionFilename = linkDownload;
|
|
}
|
|
if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
|
|
let docIsPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
|
|
channel.setPrivate(docIsPrivate);
|
|
}
|
|
channel.notificationCallbacks = new callbacks();
|
|
|
|
let flags = Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS;
|
|
|
|
if (bypassCache) {
|
|
flags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
|
|
}
|
|
|
|
if (channel instanceof Ci.nsICachingChannel) {
|
|
flags |= Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY;
|
|
}
|
|
|
|
channel.loadFlags |= flags;
|
|
|
|
if (channel instanceof Ci.nsIHttpChannel) {
|
|
channel.referrerInfo = referrerInfo;
|
|
if (channel instanceof Ci.nsIHttpChannelInternal) {
|
|
channel.forceAllowThirdPartyCookie = true;
|
|
}
|
|
}
|
|
|
|
// fallback to the old way if we don't see the headers quickly
|
|
var timeToWait = Services.prefs.getIntPref(
|
|
"browser.download.saveLinkAsFilenameTimeout"
|
|
);
|
|
var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
timer.initWithCallback(
|
|
new timerCallback(),
|
|
timeToWait,
|
|
timer.TYPE_ONE_SHOT
|
|
);
|
|
|
|
// kick off the channel with our proxy object as the listener
|
|
channel.asyncOpen(new saveAsListener(this.principal));
|
|
}
|
|
|
|
// Save URL of clicked-on link.
|
|
saveLink() {
|
|
let referrerInfo = this.onLink
|
|
? this.contentData.linkReferrerInfo
|
|
: this.contentData.referrerInfo;
|
|
|
|
let isContentWindowPrivate = this.ownerDoc.isPrivate;
|
|
this.saveHelper(
|
|
this.linkURL,
|
|
this.linkTextStr,
|
|
null,
|
|
true,
|
|
this.ownerDoc,
|
|
referrerInfo,
|
|
this.frameOuterWindowID,
|
|
this.linkDownload,
|
|
isContentWindowPrivate
|
|
);
|
|
}
|
|
|
|
// Backwards-compatibility wrapper
|
|
saveImage() {
|
|
if (this.onCanvas || this.onImage) {
|
|
this.saveMedia();
|
|
}
|
|
}
|
|
|
|
// Save URL of the clicked upon image, video, or audio.
|
|
saveMedia() {
|
|
let doc = this.ownerDoc;
|
|
let isContentWindowPrivate = this.ownerDoc.isPrivate;
|
|
let referrerInfo = this.contentData.referrerInfo;
|
|
let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
|
|
if (this.onCanvas) {
|
|
// Bypass cache, since it's a data: URL.
|
|
this._canvasToBlobURL(this.targetIdentifier).then(function(blobURL) {
|
|
saveImageURL(
|
|
blobURL,
|
|
"canvas.png",
|
|
"SaveImageTitle",
|
|
true,
|
|
false,
|
|
referrerInfo,
|
|
null,
|
|
null,
|
|
null,
|
|
isPrivate,
|
|
document.nodePrincipal /* system, because blob: */
|
|
);
|
|
}, Cu.reportError);
|
|
} else if (this.onImage) {
|
|
urlSecurityCheck(this.mediaURL, this.principal);
|
|
saveImageURL(
|
|
this.mediaURL,
|
|
null,
|
|
"SaveImageTitle",
|
|
false,
|
|
false,
|
|
referrerInfo,
|
|
null,
|
|
this.contentData.contentType,
|
|
this.contentData.contentDisposition,
|
|
isPrivate,
|
|
this.principal
|
|
);
|
|
} else if (this.onVideo || this.onAudio) {
|
|
var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
|
|
this.saveHelper(
|
|
this.mediaURL,
|
|
null,
|
|
dialogTitle,
|
|
false,
|
|
doc,
|
|
referrerInfo,
|
|
this.frameOuterWindowID,
|
|
"",
|
|
isContentWindowPrivate
|
|
);
|
|
}
|
|
}
|
|
|
|
// Backwards-compatibility wrapper
|
|
sendImage() {
|
|
if (this.onCanvas || this.onImage) {
|
|
this.sendMedia();
|
|
}
|
|
}
|
|
|
|
sendMedia() {
|
|
MailIntegration.sendMessage(this.mediaURL, "");
|
|
}
|
|
|
|
playPlugin() {
|
|
this.actor.pluginCommand("play", this.targetIdentifier);
|
|
}
|
|
|
|
hidePlugin() {
|
|
this.actor.pluginCommand("hide", this.targetIdentifier);
|
|
}
|
|
|
|
// Generate email address and put it on clipboard.
|
|
copyEmail() {
|
|
// Copy the comma-separated list of email addresses only.
|
|
// There are other ways of embedding email addresses in a mailto:
|
|
// link, but such complex parsing is beyond us.
|
|
var url = this.linkURL;
|
|
var qmark = url.indexOf("?");
|
|
var addresses;
|
|
|
|
// 7 == length of "mailto:"
|
|
addresses = qmark > 7 ? url.substring(7, qmark) : url.substr(7);
|
|
|
|
// Let's try to unescape it using a character set
|
|
// in case the address is not ASCII.
|
|
try {
|
|
addresses = Services.textToSubURI.unEscapeURIForUI(
|
|
this.contentData.charSet,
|
|
addresses
|
|
);
|
|
} catch (ex) {
|
|
// Do nothing.
|
|
}
|
|
|
|
var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
|
|
Ci.nsIClipboardHelper
|
|
);
|
|
clipboard.copyString(addresses);
|
|
}
|
|
|
|
copyLink() {
|
|
// If we're in a view source tab, remove the view-source: prefix
|
|
let linkURL = this.linkURL.replace(/^view-source:/, "");
|
|
var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
|
|
Ci.nsIClipboardHelper
|
|
);
|
|
clipboard.copyString(linkURL);
|
|
}
|
|
|
|
addKeywordForSearchField() {
|
|
this.actor.getSearchFieldBookmarkData(this.targetIdentifier).then(data => {
|
|
let title = gNavigatorBundle.getFormattedString(
|
|
"addKeywordTitleAutoFill",
|
|
[data.title]
|
|
);
|
|
PlacesUIUtils.showBookmarkDialog(
|
|
{
|
|
action: "add",
|
|
type: "bookmark",
|
|
uri: makeURI(data.spec),
|
|
title,
|
|
keyword: "",
|
|
postData: data.postData,
|
|
charSet: data.charset,
|
|
hiddenRows: ["location", "tags"],
|
|
},
|
|
window
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Utilities
|
|
*/
|
|
|
|
/**
|
|
* Show/hide one item (specified via name or the item element itself).
|
|
* If the element is not found, then this function finishes silently.
|
|
*
|
|
* @param {Element|String} aItemOrId The item element or the name of the element
|
|
* to show.
|
|
* @param {Boolean} aShow Set to true to show the item, false to hide it.
|
|
*/
|
|
showItem(aItemOrId, aShow) {
|
|
var item =
|
|
aItemOrId.constructor == String
|
|
? document.getElementById(aItemOrId)
|
|
: aItemOrId;
|
|
if (item) {
|
|
item.hidden = !aShow;
|
|
}
|
|
}
|
|
|
|
// Set given attribute of specified context-menu item. If the
|
|
// value is null, then it removes the attribute (which works
|
|
// nicely for the disabled attribute).
|
|
setItemAttr(aID, aAttr, aVal) {
|
|
var elem = document.getElementById(aID);
|
|
if (elem) {
|
|
if (aVal == null) {
|
|
// null indicates attr should be removed.
|
|
elem.removeAttribute(aAttr);
|
|
} else {
|
|
// Set attr=val.
|
|
elem.setAttribute(aAttr, aVal);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Temporary workaround for DOM api not yet implemented by XUL nodes.
|
|
cloneNode(aItem) {
|
|
// Create another element like the one we're cloning.
|
|
var node = document.createElement(aItem.tagName);
|
|
|
|
// Copy attributes from argument item to the new one.
|
|
var attrs = aItem.attributes;
|
|
for (var i = 0; i < attrs.length; i++) {
|
|
var attr = attrs.item(i);
|
|
node.setAttribute(attr.nodeName, attr.nodeValue);
|
|
}
|
|
|
|
// Voila!
|
|
return node;
|
|
}
|
|
|
|
getLinkURI() {
|
|
try {
|
|
return makeURI(this.linkURL);
|
|
} catch (ex) {
|
|
// e.g. empty URL string
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Kept for addon compat
|
|
linkText() {
|
|
return this.linkTextStr;
|
|
}
|
|
|
|
// Determines whether or not the separator with the specified ID should be
|
|
// shown or not by determining if there are any non-hidden items between it
|
|
// and the previous separator.
|
|
shouldShowSeparator(aSeparatorID) {
|
|
var separator = document.getElementById(aSeparatorID);
|
|
if (separator) {
|
|
var sibling = separator.previousSibling;
|
|
while (sibling && sibling.localName != "menuseparator") {
|
|
if (!sibling.hidden) {
|
|
return true;
|
|
}
|
|
sibling = sibling.previousSibling;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
addDictionaries() {
|
|
var uri = formatURL("browser.dictionaries.download.url", true);
|
|
|
|
var locale = "-";
|
|
try {
|
|
locale = Services.prefs.getComplexValue(
|
|
"intl.accept_languages",
|
|
Ci.nsIPrefLocalizedString
|
|
).data;
|
|
} catch (e) {}
|
|
|
|
var version = "-";
|
|
try {
|
|
version = Services.appinfo.version;
|
|
} catch (e) {}
|
|
|
|
uri = uri.replace(/%LOCALE%/, escape(locale)).replace(/%VERSION%/, version);
|
|
|
|
var newWindowPref = Services.prefs.getIntPref(
|
|
"browser.link.open_newwindow"
|
|
);
|
|
var where = newWindowPref == 3 ? "tab" : "window";
|
|
|
|
openTrustedLinkIn(uri, where);
|
|
}
|
|
|
|
bookmarkThisPage() {
|
|
window.top.PlacesCommandHook.bookmarkPage().catch(Cu.reportError);
|
|
}
|
|
|
|
bookmarkLink() {
|
|
window.top.PlacesCommandHook.bookmarkLink(
|
|
this.linkURL,
|
|
this.linkTextStr
|
|
).catch(Cu.reportError);
|
|
}
|
|
|
|
addBookmarkForFrame() {
|
|
let uri = this.contentData.documentURIObject;
|
|
|
|
this.actor.getFrameTitle(this.targetIdentifier).then(title => {
|
|
window.top.PlacesCommandHook.bookmarkLink(uri.spec, title).catch(
|
|
Cu.reportError
|
|
);
|
|
});
|
|
}
|
|
|
|
savePageAs() {
|
|
saveBrowser(this.browser);
|
|
}
|
|
|
|
printFrame() {
|
|
PrintUtils.printWindow(this.frameOuterWindowID, this.browser);
|
|
}
|
|
|
|
switchPageDirection() {
|
|
gBrowser.selectedBrowser.sendMessageToActor(
|
|
"SwitchDocumentDirection",
|
|
{},
|
|
"SwitchDocumentDirection",
|
|
true
|
|
);
|
|
}
|
|
|
|
mediaCommand(command, data) {
|
|
this.actor.mediaCommand(this.targetIdentifier, command, data);
|
|
}
|
|
|
|
copyMediaLocation() {
|
|
var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
|
|
Ci.nsIClipboardHelper
|
|
);
|
|
clipboard.copyString(this.mediaURL);
|
|
}
|
|
|
|
drmLearnMore(aEvent) {
|
|
let drmInfoURL =
|
|
Services.urlFormatter.formatURLPref("app.support.baseURL") +
|
|
"drm-content";
|
|
let dest = whereToOpenLink(aEvent);
|
|
// Don't ever want this to open in the same tab as it'll unload the
|
|
// DRM'd video, which is going to be a bad idea in most cases.
|
|
if (dest == "current") {
|
|
dest = "tab";
|
|
}
|
|
openTrustedLinkIn(drmInfoURL, dest);
|
|
}
|
|
|
|
get imageURL() {
|
|
if (this.onImage) {
|
|
return this.mediaURL;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
// Formats the 'Search <engine> for "<selection or link text>"' context menu.
|
|
showAndFormatSearchContextItem() {
|
|
const docIsPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
|
|
const privatePref = "browser.search.separatePrivateDefault.ui.enabled";
|
|
let showSearchSelect =
|
|
!this.inAboutDevtoolsToolbox &&
|
|
(this.isTextSelected || this.onLink) &&
|
|
!this.onImage;
|
|
// Don't show the private search item when we're already in a private
|
|
// browsing window.
|
|
let showPrivateSearchSelect =
|
|
showSearchSelect &&
|
|
!docIsPrivate &&
|
|
Services.prefs.getBoolPref(privatePref);
|
|
|
|
let menuItem = document.getElementById("context-searchselect");
|
|
let menuItemPrivate = document.getElementById(
|
|
"context-searchselect-private"
|
|
);
|
|
menuItem.hidden = !showSearchSelect;
|
|
menuItemPrivate.hidden = !showPrivateSearchSelect;
|
|
// If we're not showing the menu items, we can skip formatting the labels.
|
|
if (!showSearchSelect) {
|
|
return;
|
|
}
|
|
|
|
let selectedText = this.isTextSelected
|
|
? this.textSelected
|
|
: this.linkTextStr;
|
|
|
|
// Store searchTerms in context menu item so we know what to search onclick
|
|
menuItem.searchTerms = menuItemPrivate.searchTerms = selectedText;
|
|
menuItem.principal = menuItemPrivate.principal = this.principal;
|
|
menuItem.csp = menuItemPrivate.csp = this.csp;
|
|
|
|
// Copied to alert.js' prefillAlertInfo().
|
|
// If the JS character after our truncation point is a trail surrogate,
|
|
// include it in the truncated string to avoid splitting a surrogate pair.
|
|
if (selectedText.length > 15) {
|
|
let truncLength = 15;
|
|
let truncChar = selectedText[15].charCodeAt(0);
|
|
if (truncChar >= 0xdc00 && truncChar <= 0xdfff) {
|
|
truncLength++;
|
|
}
|
|
selectedText = selectedText.substr(0, truncLength) + this.ellipsis;
|
|
}
|
|
|
|
// format "Search <engine> for <selection>" string to show in menu
|
|
let engineName = Services.search.defaultEngine.name;
|
|
let privateEngineName = Services.search.defaultPrivateEngine.name;
|
|
menuItem.usePrivate = docIsPrivate;
|
|
let menuLabel = gNavigatorBundle.getFormattedString("contextMenuSearch", [
|
|
docIsPrivate ? privateEngineName : engineName,
|
|
selectedText,
|
|
]);
|
|
menuItem.label = menuLabel;
|
|
menuItem.accessKey = gNavigatorBundle.getString(
|
|
"contextMenuSearch.accesskey"
|
|
);
|
|
|
|
if (showPrivateSearchSelect) {
|
|
let otherEngine = engineName != privateEngineName;
|
|
let accessKey = "contextMenuPrivateSearch.accesskey";
|
|
if (otherEngine) {
|
|
menuItemPrivate.label = gNavigatorBundle.getFormattedString(
|
|
"contextMenuPrivateSearchOtherEngine",
|
|
[privateEngineName]
|
|
);
|
|
accessKey = "contextMenuPrivateSearchOtherEngine.accesskey";
|
|
} else {
|
|
menuItemPrivate.label = gNavigatorBundle.getString(
|
|
"contextMenuPrivateSearch"
|
|
);
|
|
}
|
|
menuItemPrivate.accessKey = gNavigatorBundle.getString(accessKey);
|
|
}
|
|
}
|
|
|
|
createContainerMenu(aEvent) {
|
|
let createMenuOptions = {
|
|
isContextMenu: true,
|
|
excludeUserContextId: this.contentData.userContextId,
|
|
};
|
|
return createUserContextMenu(aEvent, createMenuOptions);
|
|
}
|
|
|
|
doCustomCommand(generatedItemId, handlingUserInput) {
|
|
this.actor.doCustomCommand(generatedItemId, handlingUserInput);
|
|
}
|
|
}
|
|
|
|
XPCOMUtils.defineLazyModuleGetters(nsContextMenu, {
|
|
LoginManagerContextMenu: "resource://gre/modules/LoginManagerContextMenu.jsm",
|
|
DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.jsm",
|
|
});
|