summaryrefslogtreecommitdiffstats
path: root/browser/base/content/nsContextMenu.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/nsContextMenu.js')
-rw-r--r--browser/base/content/nsContextMenu.js1878
1 files changed, 1878 insertions, 0 deletions
diff --git a/browser/base/content/nsContextMenu.js b/browser/base/content/nsContextMenu.js
new file mode 100644
index 000000000..8eb9b034f
--- /dev/null
+++ b/browser/base/content/nsContextMenu.js
@@ -0,0 +1,1878 @@
+/* -*- 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/.
+
+Components.utils.import("resource://gre/modules/ContextualIdentityService.jsm");
+Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm");
+Components.utils.import("resource://gre/modules/LoginManagerContextMenu.jsm");
+Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
+var gContextMenuContentData = null;
+
+function nsContextMenu(aXulMenu, aIsShift) {
+ this.shouldDisplay = true;
+ this.initMenu(aXulMenu, aIsShift);
+}
+
+// Prototype for nsContextMenu "class."
+nsContextMenu.prototype = {
+ initMenu: function CM_initMenu(aXulMenu, aIsShift) {
+ // Get contextual info.
+ this.setTarget(document.popupNode, document.popupRangeParent,
+ document.popupRangeOffset);
+ if (!this.shouldDisplay)
+ return;
+
+ this.hasPageMenu = false;
+ this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
+ if (!aIsShift) {
+ if (this.isRemote) {
+ this.hasPageMenu =
+ PageMenuParent.addToPopup(gContextMenuContentData.customMenuItems,
+ this.browser, aXulMenu);
+ }
+ else {
+ this.hasPageMenu = PageMenuParent.buildAndAddToPopup(this.target, aXulMenu);
+ }
+
+ let subject = {
+ menu: aXulMenu,
+ tab: gBrowser ? gBrowser.getTabForBrowser(this.browser) : undefined,
+ 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,
+ onEditableArea: this.onEditableArea,
+ srcUrl: this.mediaURL,
+ frameUrl: gContextMenuContentData ? gContextMenuContentData.docLocation : undefined,
+ pageUrl: this.browser ? this.browser.currentURI.spec : undefined,
+ linkUrl: this.linkURL,
+ selectionText: this.isTextSelected ? this.selectionInfo.text : undefined,
+ };
+ subject.wrappedJSObject = subject;
+ Services.obs.notifyObservers(subject, "on-build-contextmenu", null);
+ }
+
+ this.isFrameImage = document.getElementById("isFrameImage");
+ this.ellipsis = "\u2026";
+ try {
+ this.ellipsis = gPrefService.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();
+
+ // Register this opening of the menu with telemetry:
+ this._checkTelemetryForMenu(aXulMenu);
+ },
+
+ hiding: function CM_hiding() {
+ gContextMenuContentData = null;
+ InlineSpellCheckerUI.clearSuggestionsFromMenu();
+ InlineSpellCheckerUI.clearDictionaryListFromMenu();
+ InlineSpellCheckerUI.uninit();
+ LoginManagerContextMenu.clearLoginsFromMenu(document);
+
+ // This handler self-deletes, only run it if it is still there:
+ if (this._onPopupHiding) {
+ this._onPopupHiding();
+ }
+ },
+
+ initItems: function CM_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: function CM_initPageMenuSeparator() {
+ this.showItem("page-menu-separator", this.hasPageMenu);
+ },
+
+ initOpenItems: function CM_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 (gContextMenuContentData.userContextId) {
+ inContainer = true;
+ var item = document.getElementById("context-openlinkincontainertab");
+
+ item.setAttribute("data-usercontextid", gContextMenuContentData.userContextId);
+
+ var label =
+ ContextualIdentityService.getUserContextLabel(gContextMenuContentData.userContextId);
+ item.setAttribute("label",
+ gBrowserBundle.formatStringFromName("userContextOpenLink.label",
+ [label], 1));
+ }
+
+ var shouldShow = this.onSaveableLink || isMailtoInternal || this.onPlainTextLink;
+ var isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
+ var showContainers = Services.prefs.getBoolPref("privacy.userContext.enabled");
+ this.showItem("context-openlink", shouldShow && !isWindowPrivate);
+ this.showItem("context-openlinkprivate", shouldShow);
+ 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: function CM_initNavigationItems() {
+ var shouldShow = !(this.isContentSelected || this.onLink || this.onImage ||
+ this.onCanvas || this.onVideo || this.onAudio ||
+ this.onTextInput || this.onSocial);
+ this.showItem("context-navigation", shouldShow);
+ this.showItem("context-sep-navigation", shouldShow);
+
+ let stopped = XULBrowserWindow.stopCommand.getAttribute("disabled") == "true";
+
+ let stopReloadItem = "";
+ if (shouldShow || this.onSocial) {
+ stopReloadItem = (stopped || this.onSocial) ? "reload" : "stop";
+ }
+
+ this.showItem("context-reload", stopReloadItem == "reload");
+ this.showItem("context-stop", stopReloadItem == "stop");
+
+ // XXX: Stop is determined in browser.js; the canStop broadcaster is broken
+ //this.setItemAttrFromNode( "context-stop", "disabled", "canStop" );
+ },
+
+ initLeaveDOMFullScreenItems: function CM_initLeaveFullScreenItem() {
+ // only show the option if the user is in DOM fullscreen
+ var shouldShow = (this.target.ownerDocument.fullscreenElement != null);
+ 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: function CM_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-castvideo", 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);
+ let shouldShowCast = Services.prefs.getBoolPref("browser.casting.enabled");
+ // getServicesForVideo alone would be sufficient here (it depends on
+ // SimpleServiceDiscovery.services), but SimpleServiceDiscovery is guaranteed
+ // to be already loaded, since we load it on startup in nsBrowserGlue,
+ // and CastingApps isn't, so check SimpleServiceDiscovery.services first
+ // to avoid needing to load CastingApps.jsm if we don't need to.
+ shouldShowCast = shouldShowCast && this.mediaURL &&
+ SimpleServiceDiscovery.services.length > 0 &&
+ CastingApps.getServicesForVideo(this.target).length > 0;
+ this.setItemAttr("context-castvideo", "disabled", !shouldShowCast);
+ },
+
+ initViewItems: function CM_initViewItems() {
+ // View source is always OK, unless in directory listing.
+ this.showItem("context-viewpartialsource-selection",
+ this.isContentSelected);
+ this.showItem("context-viewpartialsource-mathml",
+ this.onMathML && !this.isContentSelected);
+
+ var shouldShow = !(this.isContentSelected ||
+ this.onImage || this.onCanvas ||
+ this.onVideo || this.onAudio ||
+ this.onLink || this.onTextInput);
+ var showInspect = !this.onSocial && gPrefService.getBoolPref("devtools.inspector.enabled");
+ this.showItem("context-viewsource", shouldShow);
+ this.showItem("context-viewinfo", shouldShow);
+ this.showItem("inspect-separator", showInspect);
+ this.showItem("context-inspect", showInspect);
+
+ 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;
+#ifdef HAVE_SHELL_SERVICE
+ // Only enable Set as Desktop Background if we can get the shell service.
+ var shell = getShellService();
+ if (shell)
+ haveSetDesktopBackground = shell.canSetDesktopBackground;
+#endif
+ this.showItem("context-setDesktopBackground",
+ haveSetDesktopBackground && this.onLoadedImage);
+
+ if (haveSetDesktopBackground && this.onLoadedImage) {
+ document.getElementById("context-setDesktopBackground")
+ .disabled = gContextMenuContentData.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);
+ this.showItem("context-viewimagedesc", this.onImage && this.imageDescURL !== "");
+ },
+
+ initMiscItems: function CM_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.onSocial ||
+ this.onCanvas));
+ bookmarkPage.setAttribute("tooltiptext", bookmarkPage.getAttribute("buttontooltiptext"));
+
+ this.showItem("context-bookmarklink", (this.onLink && !this.onMailtoLink &&
+ !this.onSocial) || this.onPlainTextLink);
+ this.showItem("context-keywordfield",
+ this.onTextInput && this.onKeywordField);
+ this.showItem("frame", this.inFrame);
+
+ let showSearchSelect = (this.isTextSelected || this.onLink) && !this.onImage;
+ this.showItem("context-searchselect", showSearchSelect);
+ if (showSearchSelect) {
+ this.formatSearchContextItem();
+ }
+
+ // 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.isFrameImage.removeAttribute('hidden');
+ else
+ this.isFrameImage.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);
+
+ // SocialShare
+ let shareButton = SocialShare.shareButton;
+ let shareEnabled = shareButton && !shareButton.disabled && !this.onSocial;
+ let pageShare = shareEnabled && !(this.isContentSelected ||
+ this.onTextInput || this.onLink || this.onImage ||
+ this.onVideo || this.onAudio || this.onCanvas);
+ this.showItem("context-sharepage", pageShare);
+ this.showItem("context-shareselect", shareEnabled && this.isContentSelected);
+ this.showItem("context-sharelink", shareEnabled && (this.onLink || this.onPlainTextLink) && !this.onMailtoLink);
+ this.showItem("context-shareimage", shareEnabled && this.onImage);
+ this.showItem("context-sharevideo", shareEnabled && this.onVideo);
+ this.setItemAttr("context-sharevideo", "disabled", !this.mediaURL || this.mediaURL.startsWith("blob:"));
+ },
+
+ initSpellingItems: function() {
+ 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.onEditableArea) {
+ // 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: function() {
+ // 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.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));
+
+#ifdef CONTEXT_COPY_IMAGE_CONTENTS
+ // Copy image contents depends on whether we're on an image.
+ this.showItem("context-copyimage-contents", this.onImage);
+#endif
+ // 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: function() {
+ 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.fullscreenElement == null);
+ 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.showItem("context-media-sep-commands", onMedia);
+ },
+
+ initClickToPlayItems: function() {
+ this.showItem("context-ctp-play", this.onCTPPlugin);
+ this.showItem("context-ctp-hide", this.onCTPPlugin);
+ this.showItem("context-sep-ctp", this.onCTPPlugin);
+ },
+
+ initPasswordManagerItems: function() {
+ let loginFillInfo = gContextMenuContentData && gContextMenuContentData.loginFillInfo;
+
+ // If we could not find a password field we
+ // don't want to show the form fill option.
+ let showFill = loginFillInfo && loginFillInfo.passwordField.found;
+
+ // Disable the fill option if the user has set a master password
+ // or if the password field or target field are disabled.
+ 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 documentURI = gContextMenuContentData.documentURIObject;
+ let fragment = LoginManagerContextMenu.addLoginsToMenu(this.target, this.browser, documentURI);
+
+ this.showItem("fill-login-no-logins", !fragment);
+
+ if (!fragment) {
+ return;
+ }
+ let popup = document.getElementById("fill-login-popup");
+ let insertBeforeElement = document.getElementById("fill-login-no-logins");
+ popup.insertBefore(fragment, insertBeforeElement);
+ },
+
+ initSyncItems: function() {
+ gFxAccounts.initPageContextMenu(this);
+ },
+
+ openPasswordManager: function() {
+ LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
+ },
+
+ inspectNode: function() {
+ let {devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ let gBrowser = this.browser.ownerGlobal.gBrowser;
+ let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
+
+ return gDevTools.showToolbox(target, "inspector").then(toolbox => {
+ let inspector = toolbox.getCurrentPanel();
+
+ // new-node-front tells us when the node has been selected, whether the
+ // browser is remote or not.
+ let onNewNode = inspector.selection.once("new-node-front");
+
+ this.browser.messageManager.sendAsyncMessage("debug:inspect", {}, {node: this.target});
+ inspector.walker.findInspectingNode().then(nodeFront => {
+ inspector.selection.setNodeFront(nodeFront, "browser-context-menu");
+ });
+
+ return onNewNode.then(() => {
+ // Now that the node has been selected, wait until the inspector is
+ // fully updated.
+ return inspector.once("inspector-updated");
+ });
+ });
+ },
+
+ // Set various context menu attributes based on the state of the world.
+ setTarget: function (aNode, aRangeParent, aRangeOffset) {
+ // gContextMenuContentData.isRemote tells us if the event came from a remote
+ // process. gContextMenuContentData can be null if something (like tests)
+ // opens the context menu directly.
+ let editFlags;
+ this.isRemote = gContextMenuContentData && gContextMenuContentData.isRemote;
+ if (this.isRemote) {
+ aNode = gContextMenuContentData.event.target;
+ aRangeParent = gContextMenuContentData.event.rangeParent;
+ aRangeOffset = gContextMenuContentData.event.rangeOffset;
+ editFlags = gContextMenuContentData.editFlags;
+ }
+
+ const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ if (aNode.nodeType == Node.DOCUMENT_NODE ||
+ // Not display on XUL element but relax for <label class="text-link">
+ (aNode.namespaceURI == xulNS && !isXULTextLinkLabel(aNode))) {
+ this.shouldDisplay = false;
+ return;
+ }
+
+ // Initialize contextual info.
+ this.onImage = false;
+ this.onLoadedImage = false;
+ this.onCompletedImage = false;
+ this.imageDescURL = "";
+ this.onCanvas = false;
+ this.onVideo = false;
+ this.onAudio = false;
+ this.onDRMMedia = false;
+ this.onTextInput = false;
+ this.onNumeric = false;
+ this.onKeywordField = false;
+ this.mediaURL = "";
+ this.onLink = false;
+ this.onMailtoLink = false;
+ this.onSaveableLink = false;
+ this.link = null;
+ this.linkURL = "";
+ this.linkURI = null;
+ this.linkTextStr = "";
+ this.linkProtocol = "";
+ this.linkDownload = "";
+ this.linkHasNoReferrer = false;
+ this.onMathML = false;
+ this.inFrame = false;
+ this.inSrcdocFrame = false;
+ this.inSyntheticDoc = false;
+ this.hasBGImage = false;
+ this.bgImageURL = "";
+ this.onEditableArea = false;
+ this.isDesignMode = false;
+ this.onCTPPlugin = false;
+ this.canSpellCheck = false;
+ this.onPassword = false;
+
+ if (this.isRemote) {
+ this.selectionInfo = gContextMenuContentData.selectionInfo;
+ } else {
+ this.selectionInfo = BrowserUtils.getSelectionDetails(window);
+ }
+
+ this.textSelected = this.selectionInfo.text;
+ this.isTextSelected = this.textSelected.length != 0;
+
+ // Remember the node that was clicked.
+ this.target = aNode;
+
+ let ownerDoc = this.target.ownerDocument;
+ this.ownerDoc = ownerDoc;
+
+ // If this is a remote context menu event, use the information from
+ // gContextMenuContentData instead.
+ if (this.isRemote) {
+ this.browser = gContextMenuContentData.browser;
+ this.principal = gContextMenuContentData.principal;
+ this.frameOuterWindowID = gContextMenuContentData.frameOuterWindowID;
+ } else {
+ editFlags = SpellCheckHelper.isEditable(this.target, window);
+ this.browser = ownerDoc.defaultView
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler;
+ this.principal = ownerDoc.nodePrincipal;
+ this.frameOuterWindowID = ownerDoc.defaultView
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .outerWindowID;
+ }
+ this.onSocial = !!this.browser.getAttribute("origin");
+
+ // Check if we are in a synthetic document (stand alone image, video, etc.).
+ this.inSyntheticDoc = ownerDoc.mozSyntheticDocument;
+ // First, do checks for nodes that never have children.
+ if (this.target.nodeType == Node.ELEMENT_NODE) {
+ // See if the user clicked on an image. This check mirrors
+ // nsDocumentViewer::GetInImage. Make sure to update both if this is
+ // changed.
+ if (this.target instanceof Ci.nsIImageLoadingContent &&
+ this.target.currentURI) {
+ this.onImage = true;
+
+ var request =
+ this.target.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
+ if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE))
+ this.onLoadedImage = true;
+ if (request &&
+ (request.imageStatus & request.STATUS_LOAD_COMPLETE) &&
+ !(request.imageStatus & request.STATUS_ERROR)) {
+ this.onCompletedImage = true;
+ }
+
+ this.mediaURL = this.target.currentURI.spec;
+
+ var descURL = this.target.getAttribute("longdesc");
+ if (descURL) {
+ this.imageDescURL = makeURLAbsolute(ownerDoc.body.baseURI, descURL);
+ }
+ }
+ else if (this.target instanceof HTMLCanvasElement) {
+ this.onCanvas = true;
+ }
+ else if (this.target instanceof HTMLVideoElement) {
+ let mediaURL = this.target.currentSrc || this.target.src;
+ if (this.isMediaURLReusable(mediaURL)) {
+ this.mediaURL = mediaURL;
+ }
+ if (this._isProprietaryDRM()) {
+ this.onDRMMedia = true;
+ }
+ // Firefox always creates a HTMLVideoElement when loading an ogg file
+ // directly. If the media is actually audio, be smarter and provide a
+ // context menu with audio operations.
+ if (this.target.readyState >= this.target.HAVE_METADATA &&
+ (this.target.videoWidth == 0 || this.target.videoHeight == 0)) {
+ this.onAudio = true;
+ } else {
+ this.onVideo = true;
+ }
+ }
+ else if (this.target instanceof HTMLAudioElement) {
+ this.onAudio = true;
+ let mediaURL = this.target.currentSrc || this.target.src;
+ if (this.isMediaURLReusable(mediaURL)) {
+ this.mediaURL = mediaURL;
+ }
+ if (this._isProprietaryDRM()) {
+ this.onDRMMedia = true;
+ }
+ }
+ else if (editFlags & (SpellCheckHelper.INPUT | SpellCheckHelper.TEXTAREA)) {
+ this.onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0;
+ this.onNumeric = (editFlags & SpellCheckHelper.NUMERIC) !== 0;
+ this.onEditableArea = (editFlags & SpellCheckHelper.EDITABLE) !== 0;
+ this.onPassword = (editFlags & SpellCheckHelper.PASSWORD) !== 0;
+ if (this.onEditableArea) {
+ if (this.isRemote) {
+ InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
+ }
+ else {
+ InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
+ InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
+ }
+ }
+ this.onKeywordField = (editFlags & SpellCheckHelper.KEYWORD);
+ }
+ else if (this.target instanceof HTMLHtmlElement) {
+ var bodyElt = ownerDoc.body;
+ if (bodyElt) {
+ let computedURL;
+ try {
+ computedURL = this.getComputedURL(bodyElt, "background-image");
+ this._hasMultipleBGImages = false;
+ } catch (e) {
+ this._hasMultipleBGImages = true;
+ }
+ if (computedURL) {
+ this.hasBGImage = true;
+ this.bgImageURL = makeURLAbsolute(bodyElt.baseURI,
+ computedURL);
+ }
+ }
+ }
+ else if ((this.target instanceof HTMLEmbedElement ||
+ this.target instanceof HTMLObjectElement ||
+ this.target instanceof HTMLAppletElement) &&
+ this.target.displayedType == HTMLObjectElement.TYPE_NULL &&
+ this.target.pluginFallbackType == HTMLObjectElement.PLUGIN_CLICK_TO_PLAY) {
+ this.onCTPPlugin = true;
+ }
+
+ this.canSpellCheck = this._isSpellCheckEnabled(this.target);
+ }
+ else if (this.target.nodeType == Node.TEXT_NODE) {
+ // For text nodes, look at the parent node to determine the spellcheck attribute.
+ this.canSpellCheck = this.target.parentNode &&
+ this._isSpellCheckEnabled(this.target);
+ }
+
+ // Second, bubble out, looking for items of interest that can have childen.
+ // Always pick the innermost link, background image, etc.
+ const XMLNS = "http://www.w3.org/XML/1998/namespace";
+ var elem = this.target;
+ while (elem) {
+ if (elem.nodeType == Node.ELEMENT_NODE) {
+ // Link?
+ if (!this.onLink &&
+ // Be consistent with what hrefAndLinkNodeForClickEvent
+ // does in browser.js
+ (isXULTextLinkLabel(elem) ||
+ (elem instanceof HTMLAnchorElement && elem.href) ||
+ (elem instanceof HTMLAreaElement && elem.href) ||
+ elem instanceof HTMLLinkElement ||
+ elem.getAttributeNS("http://www.w3.org/1999/xlink", "type") == "simple")) {
+
+ // Target is a link or a descendant of a link.
+ this.onLink = true;
+
+ // Remember corresponding element.
+ this.link = elem;
+ this.linkURL = this.getLinkURL();
+ this.linkURI = this.getLinkURI();
+ this.linkTextStr = this.getLinkText();
+ this.linkProtocol = this.getLinkProtocol();
+ this.onMailtoLink = (this.linkProtocol == "mailto");
+ this.onSaveableLink = this.isLinkSaveable( this.link );
+ this.linkHasNoReferrer = BrowserUtils.linkHasNoReferrer(elem);
+ try {
+ if (elem.download) {
+ // Ignore download attribute on cross-origin links
+ this.principal.checkMayLoad(this.linkURI, false, true);
+ this.linkDownload = elem.download;
+ }
+ }
+ catch (ex) {}
+ }
+
+ // Background image? Don't bother if we've already found a
+ // background image further down the hierarchy. Otherwise,
+ // we look for the computed background-image style.
+ if (!this.hasBGImage &&
+ !this._hasMultipleBGImages) {
+ let bgImgUrl;
+ try {
+ bgImgUrl = this.getComputedURL(elem, "background-image");
+ this._hasMultipleBGImages = false;
+ } catch (e) {
+ this._hasMultipleBGImages = true;
+ }
+ if (bgImgUrl) {
+ this.hasBGImage = true;
+ this.bgImageURL = makeURLAbsolute(elem.baseURI,
+ bgImgUrl);
+ }
+ }
+ }
+
+ elem = elem.parentNode;
+ }
+
+ // See if the user clicked on MathML
+ const NS_MathML = "http://www.w3.org/1998/Math/MathML";
+ if ((this.target.nodeType == Node.TEXT_NODE &&
+ this.target.parentNode.namespaceURI == NS_MathML)
+ || (this.target.namespaceURI == NS_MathML))
+ this.onMathML = true;
+
+ // See if the user clicked in a frame.
+ var docDefaultView = ownerDoc.defaultView;
+ if (docDefaultView != docDefaultView.top) {
+ this.inFrame = true;
+
+ if (ownerDoc.isSrcdocDocument) {
+ this.inSrcdocFrame = true;
+ }
+ }
+
+ // if the document is editable, show context menu like in text inputs
+ if (!this.onEditableArea) {
+ if (editFlags & SpellCheckHelper.CONTENTEDITABLE) {
+ // If this.onEditableArea is false but editFlags is CONTENTEDITABLE, then
+ // the document itself must be editable.
+ this.onTextInput = true;
+ this.onKeywordField = false;
+ this.onImage = false;
+ this.onLoadedImage = false;
+ this.onCompletedImage = false;
+ this.onMathML = false;
+ this.inFrame = false;
+ this.inSrcdocFrame = false;
+ this.hasBGImage = false;
+ this.isDesignMode = true;
+ this.onEditableArea = true;
+ if (this.isRemote) {
+ InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
+ }
+ else {
+ var targetWin = ownerDoc.defaultView;
+ var editingSession = targetWin.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession);
+ InlineSpellCheckerUI.init(editingSession.getEditorForWindow(targetWin));
+ InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
+ }
+ var canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck;
+ this.showItem("spell-check-enabled", canSpell);
+ this.showItem("spell-separator", canSpell);
+ }
+ }
+
+ function isXULTextLinkLabel(node) {
+ return node.namespaceURI == xulNS &&
+ node.tagName == "label" &&
+ node.classList.contains('text-link') &&
+ node.href;
+ }
+ },
+
+ // Returns the computed style attribute for the given element.
+ getComputedStyle: function(aElem, aProp) {
+ return aElem.ownerDocument
+ .defaultView
+ .getComputedStyle(aElem, "").getPropertyValue(aProp);
+ },
+
+ // Returns a "url"-type computed style attribute value, with the url() stripped.
+ getComputedURL: function(aElem, aProp) {
+ var url = aElem.ownerDocument
+ .defaultView.getComputedStyle(aElem, "")
+ .getPropertyCSSValue(aProp);
+ if (url instanceof CSSValueList) {
+ if (url.length != 1)
+ throw "found multiple URLs";
+ url = url[0];
+ }
+ return url.primitiveType == CSSPrimitiveValue.CSS_URI ?
+ url.getStringValue() : null;
+ },
+
+ // Returns true if clicked-on link targets a resource that can be saved.
+ isLinkSaveable: function(aLink) {
+ // We don't do the Right Thing for news/snews yet, so turn them off
+ // until we do.
+ return this.linkProtocol && !(
+ this.linkProtocol == "mailto" ||
+ this.linkProtocol == "javascript" ||
+ this.linkProtocol == "news" ||
+ this.linkProtocol == "snews" );
+ },
+
+ _isSpellCheckEnabled: function(aNode) {
+ // We can always force-enable spellchecking on textboxes
+ if (this.isTargetATextBox(aNode)) {
+ return true;
+ }
+ // We can never spell check something which is not content editable
+ var editable = aNode.isContentEditable;
+ if (!editable && aNode.ownerDocument) {
+ editable = aNode.ownerDocument.designMode == "on";
+ }
+ if (!editable) {
+ return false;
+ }
+ // Otherwise make sure that nothing in the parent chain disables spellchecking
+ return aNode.spellcheck;
+ },
+
+ _isProprietaryDRM: function() {
+ return this.target.isEncrypted && this.target.mediaKeys &&
+ this.target.mediaKeys.keySystem != "org.w3.clearkey";
+ },
+
+ _openLinkInParameters : function (extra) {
+ let params = { charset: gContextMenuContentData.charSet,
+ originPrincipal: this.principal,
+ referrerURI: gContextMenuContentData.documentURIObject,
+ referrerPolicy: gContextMenuContentData.referrerPolicy,
+ noReferrer: this.linkHasNoReferrer };
+ for (let p in extra) {
+ params[p] = extra[p];
+ }
+
+ // If we want to change userContextId, we must be sure that we don't
+ // propagate the referrer.
+ if ("userContextId" in params &&
+ params.userContextId != gContextMenuContentData.userContextId) {
+ params.noReferrer = true;
+ }
+
+ return params;
+ },
+
+ // Open linked-to URL in a new window.
+ openLink : function () {
+ urlSecurityCheck(this.linkURL, this.principal);
+ openLinkIn(this.linkURL, "window", this._openLinkInParameters());
+ },
+
+ // Open linked-to URL in a new private window.
+ openLinkInPrivateWindow : function () {
+ urlSecurityCheck(this.linkURL, this.principal);
+ openLinkIn(this.linkURL, "window",
+ this._openLinkInParameters({ private: true }));
+ },
+
+ // Open linked-to URL in a new tab.
+ openLinkInTab: function(event) {
+ urlSecurityCheck(this.linkURL, this.principal);
+ let referrerURI = gContextMenuContentData.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 (gContextMenuContentData.parentAllowsMixedContent) {
+ const sm = Services.scriptSecurityManager;
+ try {
+ let targetURI = this.linkURI;
+ sm.checkSameOriginURI(referrerURI, targetURI, false);
+ 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: function() {
+ urlSecurityCheck(this.linkURL, this.principal);
+ openLinkIn(this.linkURL, "current", this._openLinkInParameters());
+ },
+
+ // Open frame in a new tab.
+ openFrameInTab: function() {
+ let referrer = gContextMenuContentData.referrer;
+ openLinkIn(gContextMenuContentData.docLocation, "tab",
+ { charset: gContextMenuContentData.charSet,
+ referrerURI: referrer ? makeURI(referrer) : null });
+ },
+
+ // Reload clicked-in frame.
+ reloadFrame: function() {
+ this.browser.messageManager.sendAsyncMessage("ContextMenu:ReloadFrame",
+ null, { target: this.target });
+ },
+
+ // Open clicked-in frame in its own window.
+ openFrame: function() {
+ let referrer = gContextMenuContentData.referrer;
+ openLinkIn(gContextMenuContentData.docLocation, "window",
+ { charset: gContextMenuContentData.charSet,
+ referrerURI: referrer ? makeURI(referrer) : null });
+ },
+
+ // Open clicked-in frame in the same window.
+ showOnlyThisFrame: function() {
+ urlSecurityCheck(gContextMenuContentData.docLocation,
+ this.browser.contentPrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+ let referrer = gContextMenuContentData.referrer;
+ openUILinkIn(gContextMenuContentData.docLocation, "current",
+ { disallowInheritPrincipal: true,
+ referrerURI: referrer ? makeURI(referrer) : null });
+ },
+
+ reload: function(event) {
+ BrowserReloadOrDuplicate(event);
+ },
+
+ // View Partial Source
+ viewPartialSource: function(aContext) {
+ let inWindow = !Services.prefs.getBoolPref("view_source.tab");
+ let openSelectionFn = inWindow ? null : function() {
+ let tabBrowser = gBrowser;
+ // In the case of popups, we need to find a non-popup browser window.
+ if (!tabBrowser || !window.toolbar.visible) {
+ // This returns only non-popup browser windows by default.
+ let browserWindow = RecentWindow.getMostRecentBrowserWindow();
+ tabBrowser = browserWindow.gBrowser;
+ }
+ let tab = tabBrowser.loadOneTab("about:blank", {
+ relatedToCurrent: true,
+ inBackground: false
+ });
+ return tabBrowser.getBrowserForTab(tab);
+ }
+
+ let target = aContext == "mathml" ? this.target : null;
+ top.gViewSourceUtils.viewPartialSourceInBrowser(gBrowser.selectedBrowser, target, openSelectionFn);
+ },
+
+ // Open new "view source" window with the frame's URL.
+ viewFrameSource: function() {
+ BrowserViewSourceOfDocument({
+ browser: this.browser,
+ URL: gContextMenuContentData.docLocation,
+ outerWindowID: this.frameOuterWindowID,
+ });
+ },
+
+ viewInfo: function() {
+ BrowserPageInfo(gContextMenuContentData.docLocation, null, null, null, this.browser);
+ },
+
+ viewImageInfo: function() {
+ BrowserPageInfo(gContextMenuContentData.docLocation, "mediaTab",
+ this.target, null, this.browser);
+ },
+
+ viewImageDesc: function(e) {
+ urlSecurityCheck(this.imageDescURL,
+ this.browser.contentPrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+ openUILink(this.imageDescURL, e, { disallowInheritPrincipal: true,
+ referrerURI: gContextMenuContentData.documentURIObject });
+ },
+
+ viewFrameInfo: function() {
+ BrowserPageInfo(gContextMenuContentData.docLocation, null, null,
+ this.frameOuterWindowID, this.browser);
+ },
+
+ reloadImage: function() {
+ urlSecurityCheck(this.mediaURL,
+ this.browser.contentPrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+
+ this.browser.messageManager.sendAsyncMessage("ContextMenu:ReloadImage",
+ null, { target: this.target });
+ },
+
+ _canvasToBlobURL: function(target) {
+ let mm = this.browser.messageManager;
+ return new Promise(function(resolve) {
+ mm.sendAsyncMessage("ContextMenu:Canvas:ToBlobURL", {}, { target });
+
+ let onMessage = (message) => {
+ mm.removeMessageListener("ContextMenu:Canvas:ToBlobURL:Result", onMessage);
+ resolve(message.data.blobURL);
+ };
+ mm.addMessageListener("ContextMenu:Canvas:ToBlobURL:Result", onMessage);
+ });
+ },
+
+ // Change current window to the URL of the image, video, or audio.
+ viewMedia: function(e) {
+ let referrerURI = gContextMenuContentData.documentURIObject;
+ if (this.onCanvas) {
+ this._canvasToBlobURL(this.target).then(function(blobURL) {
+ openUILink(blobURL, e, { disallowInheritPrincipal: true,
+ referrerURI: referrerURI });
+ }, Cu.reportError);
+ }
+ else {
+ urlSecurityCheck(this.mediaURL,
+ this.browser.contentPrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+ openUILink(this.mediaURL, e, { disallowInheritPrincipal: true,
+ referrerURI: referrerURI });
+ }
+ },
+
+ saveVideoFrameAsImage: function () {
+ let mm = this.browser.messageManager;
+ 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";
+
+ mm.sendAsyncMessage("ContextMenu:SaveVideoFrameAsImage", {}, {
+ target: this.target,
+ });
+
+ let onMessage = (message) => {
+ mm.removeMessageListener("ContextMenu:SaveVideoFrameAsImage:Result", onMessage);
+ let dataURL = message.data.dataURL;
+ saveImageURL(dataURL, name, "SaveImageTitle", true, false,
+ document.documentURIObject, null, null, null,
+ isPrivate);
+ };
+ mm.addMessageListener("ContextMenu:SaveVideoFrameAsImage:Result", onMessage);
+ },
+
+ leaveDOMFullScreen: function() {
+ document.exitFullscreen();
+ },
+
+ // Change current window to the URL of the background image.
+ viewBGImage: function(e) {
+ urlSecurityCheck(this.bgImageURL,
+ this.browser.contentPrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+ openUILink(this.bgImageURL, e, { disallowInheritPrincipal: true,
+ referrerURI: gContextMenuContentData.documentURIObject });
+ },
+
+ setDesktopBackground: function() {
+ let mm = this.browser.messageManager;
+
+ mm.sendAsyncMessage("ContextMenu:SetAsDesktopBackground", null,
+ { target: this.target });
+
+ let onMessage = (message) => {
+ mm.removeMessageListener("ContextMenu:SetAsDesktopBackground:Result",
+ onMessage);
+
+ if (message.data.disable)
+ return;
+
+ let image = document.createElementNS('http://www.w3.org/1999/xhtml', 'img');
+ image.src = message.data.dataUrl;
+
+ // Confirm since it's annoying if you hit this accidentally.
+ const kDesktopBackgroundURL =
+ "chrome://browser/content/setDesktopBackground.xul";
+#ifdef XP_MACOSX
+ // On Mac, the Set Desktop Background window is not modal.
+ // Don't open more than one Set Desktop Background window.
+ const wm = Cc["@mozilla.org/appshell/window-mediator;1"].
+ getService(Ci.nsIWindowMediator);
+ let dbWin = wm.getMostRecentWindow("Shell:SetDesktopBackground");
+ if (dbWin) {
+ dbWin.gSetBackground.init(image);
+ dbWin.focus();
+ }
+ else {
+ openDialog(kDesktopBackgroundURL, "",
+ "centerscreen,chrome,dialog=no,dependent,resizable=no",
+ image);
+ }
+#else
+ // On non-Mac platforms, the Set Wallpaper dialog is modal.
+ openDialog(kDesktopBackgroundURL, "",
+ "centerscreen,chrome,dialog,modal,dependent",
+ image);
+#endif
+ };
+
+ mm.addMessageListener("ContextMenu:SetAsDesktopBackground:Result", onMessage);
+ },
+
+ // Save URL of clicked-on frame.
+ saveFrame: function () {
+ 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: function(linkURL, linkText, dialogTitle, bypassCache, doc, docURI,
+ windowID, linkDownload) {
+ // 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() {}
+ saveAsListener.prototype = {
+ extListener: null,
+
+ onStartRequest: function saveLinkAs_onStartRequest(aRequest, aContext) {
+
+ // 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 sbs = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService);
+ const bundle = sbs.createBundle(
+ "chrome://mozapps/locale/downloads/downloads.properties");
+
+ const title = bundle.GetStringFromName("downloadErrorAlertTitle");
+ const msg = bundle.GetStringFromName("downloadErrorGeneric");
+
+ const promptSvc = Cc["@mozilla.org/embedcomp/prompt-service;1"].
+ getService(Ci.nsIPromptService);
+ const wm = Cc["@mozilla.org/appshell/window-mediator;1"].
+ getService(Ci.nsIWindowMediator);
+ let window = wm.getOuterWindowWithId(windowID);
+ promptSvc.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, aContext);
+ },
+
+ onStopRequest: function saveLinkAs_onStopRequest(aRequest, aContext,
+ 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, docURI,
+ doc);
+ }
+ if (this.extListener)
+ this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
+ },
+
+ onDataAvailable: function saveLinkAs_onDataAvailable(aRequest, aContext,
+ aInputStream,
+ aOffset, aCount) {
+ this.extListener.onDataAvailable(aRequest, aContext, 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);
+ return;
+ }
+ }
+
+ // setting up a new channel for 'right click - save link as ...'
+ // ideally we should use:
+ // * doc - as the loadingNode, and/or
+ // * this.principal - as the loadingPrincipal
+ // for now lets use systemPrincipal to bypass mixedContentBlocker
+ // checks after redirects, see bug: 1136055
+ var channel = NetUtil.newChannel({
+ uri: makeURI(linkURL),
+ loadUsingSystemPrincipal: true
+ });
+
+ if (linkDownload)
+ channel.contentDispositionFilename = linkDownload;
+ if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
+ let docIsPrivate = PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser);
+ 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.referrer = docURI;
+ if (channel instanceof Ci.nsIHttpChannelInternal)
+ channel.forceAllowThirdPartyCookie = true;
+ }
+
+ // fallback to the old way if we don't see the headers quickly
+ var timeToWait =
+ gPrefService.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.asyncOpen2(new saveAsListener());
+ },
+
+ // Save URL of clicked-on link.
+ saveLink: function() {
+ urlSecurityCheck(this.linkURL, this.principal);
+ this.saveHelper(this.linkURL, this.linkTextStr, null, true, this.ownerDoc,
+ gContextMenuContentData.documentURIObject,
+ this.frameOuterWindowID,
+ this.linkDownload);
+ },
+
+ // Backwards-compatibility wrapper
+ saveImage : function() {
+ if (this.onCanvas || this.onImage)
+ this.saveMedia();
+ },
+
+ // Save URL of the clicked upon image, video, or audio.
+ saveMedia: function() {
+ let doc = this.ownerDoc;
+ let referrerURI = gContextMenuContentData.documentURIObject;
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
+ if (this.onCanvas) {
+ // Bypass cache, since it's a data: URL.
+ this._canvasToBlobURL(this.target).then(function(blobURL) {
+ saveImageURL(blobURL, "canvas.png", "SaveImageTitle",
+ true, false, referrerURI, null, null, null,
+ isPrivate);
+ }, Cu.reportError);
+ }
+ else if (this.onImage) {
+ urlSecurityCheck(this.mediaURL, this.principal);
+ saveImageURL(this.mediaURL, null, "SaveImageTitle", false,
+ false, referrerURI, null, gContextMenuContentData.contentType,
+ gContextMenuContentData.contentDisposition, isPrivate);
+ }
+ else if (this.onVideo || this.onAudio) {
+ urlSecurityCheck(this.mediaURL, this.principal);
+ var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
+ this.saveHelper(this.mediaURL, null, dialogTitle, false, doc, referrerURI,
+ this.frameOuterWindowID, "");
+ }
+ },
+
+ // Backwards-compatibility wrapper
+ sendImage : function() {
+ if (this.onCanvas || this.onImage)
+ this.sendMedia();
+ },
+
+ sendMedia: function() {
+ MailIntegration.sendMessage(this.mediaURL, "");
+ },
+
+ castVideo: function() {
+ CastingApps.openExternal(this.target, window);
+ },
+
+ populateCastVideoMenu: function(popup) {
+ let videoEl = this.target;
+ popup.innerHTML = null;
+ let doc = popup.ownerDocument;
+ let services = CastingApps.getServicesForVideo(videoEl);
+ services.forEach(service => {
+ let item = doc.createElement("menuitem");
+ item.setAttribute("label", service.friendlyName);
+ item.addEventListener("command", event => {
+ CastingApps.sendVideoToService(videoEl, service);
+ });
+ popup.appendChild(item);
+ });
+ },
+
+ playPlugin: function() {
+ gPluginHandler.contextMenuCommand(this.browser, this.target, "play");
+ },
+
+ hidePlugin: function() {
+ gPluginHandler.contextMenuCommand(this.browser, this.target, "hide");
+ },
+
+ // Generate email address and put it on clipboard.
+ copyEmail: function() {
+ // 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 {
+ const textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"].
+ getService(Ci.nsITextToSubURI);
+ addresses = textToSubURI.unEscapeURIForUI(gContextMenuContentData.charSet,
+ addresses);
+ }
+ catch(ex) {
+ // Do nothing.
+ }
+
+ var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(addresses);
+ },
+
+ copyLink: function() {
+ // 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);
+ },
+
+ ///////////////
+ // Utilities //
+ ///////////////
+
+ // Show/hide one item (specified via name or the item element itself).
+ showItem: function(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: function(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);
+ }
+ }
+ },
+
+ // Set context menu attribute according to like attribute of another node
+ // (such as a broadcaster).
+ setItemAttrFromNode: function(aItem_id, aAttr, aOther_id) {
+ var elem = document.getElementById(aOther_id);
+ if (elem && elem.getAttribute(aAttr) == "true")
+ this.setItemAttr(aItem_id, aAttr, "true");
+ else
+ this.setItemAttr(aItem_id, aAttr, null);
+ },
+
+ // Temporary workaround for DOM api not yet implemented by XUL nodes.
+ cloneNode: function(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;
+ },
+
+ // Generate fully qualified URL for clicked-on link.
+ getLinkURL: function() {
+ var href = this.link.href;
+ if (href)
+ return href;
+
+ href = this.link.getAttribute("href") ||
+ this.link.getAttributeNS("http://www.w3.org/1999/xlink", "href");
+
+ if (!href || !href.match(/\S/)) {
+ // Without this we try to save as the current doc,
+ // for example, HTML case also throws if empty
+ throw "Empty href";
+ }
+
+ return makeURLAbsolute(this.link.baseURI, href);
+ },
+
+ getLinkURI: function() {
+ try {
+ return makeURI(this.linkURL);
+ }
+ catch (ex) {
+ // e.g. empty URL string
+ }
+
+ return null;
+ },
+
+ getLinkProtocol: function() {
+ if (this.linkURI)
+ return this.linkURI.scheme; // can be |undefined|
+
+ return null;
+ },
+
+ // Get text of link.
+ getLinkText: function() {
+ var text = gatherTextUnder(this.link);
+ if (!text || !text.match(/\S/)) {
+ text = this.link.getAttribute("title");
+ if (!text || !text.match(/\S/)) {
+ text = this.link.getAttribute("alt");
+ if (!text || !text.match(/\S/))
+ text = this.linkURL;
+ }
+ }
+
+ return text;
+ },
+
+ // Kept for addon compat
+ linkText: function() {
+ return this.linkTextStr;
+ },
+
+ isMediaURLReusable: function(aURL) {
+ if (aURL.startsWith("blob:")) {
+ return URL.isValidURL(aURL);
+ }
+ return true;
+ },
+
+ toString: function () {
+ return "contextMenu.target = " + this.target + "\n" +
+ "contextMenu.onImage = " + this.onImage + "\n" +
+ "contextMenu.onLink = " + this.onLink + "\n" +
+ "contextMenu.link = " + this.link + "\n" +
+ "contextMenu.inFrame = " + this.inFrame + "\n" +
+ "contextMenu.hasBGImage = " + this.hasBGImage + "\n";
+ },
+
+ isTargetATextBox: function(node) {
+ if (node instanceof HTMLInputElement)
+ return node.mozIsTextField(false);
+
+ return (node instanceof HTMLTextAreaElement);
+ },
+
+ // 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: function (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: function() {
+ var uri = formatURL("browser.dictionaries.download.url", true);
+
+ var locale = "-";
+ try {
+ locale = gPrefService.getComplexValue("intl.accept_languages",
+ Ci.nsIPrefLocalizedString).data;
+ }
+ catch (e) { }
+
+ var version = "-";
+ try {
+ version = Cc["@mozilla.org/xre/app-info;1"].
+ getService(Ci.nsIXULAppInfo).version;
+ }
+ catch (e) { }
+
+ uri = uri.replace(/%LOCALE%/, escape(locale)).replace(/%VERSION%/, version);
+
+ var newWindowPref = gPrefService.getIntPref("browser.link.open_newwindow");
+ var where = newWindowPref == 3 ? "tab" : "window";
+
+ openUILinkIn(uri, where);
+ },
+
+ bookmarkThisPage: function CM_bookmarkThisPage() {
+ window.top.PlacesCommandHook.bookmarkPage(this.browser, PlacesUtils.bookmarksMenuFolderId, true);
+ },
+
+ bookmarkLink: function CM_bookmarkLink() {
+ window.top.PlacesCommandHook.bookmarkLink(PlacesUtils.bookmarksMenuFolderId,
+ this.linkURL, this.linkTextStr);
+ },
+
+ addBookmarkForFrame: function CM_addBookmarkForFrame() {
+ let uri = gContextMenuContentData.documentURIObject;
+ let mm = this.browser.messageManager;
+
+ let onMessage = (message) => {
+ mm.removeMessageListener("ContextMenu:BookmarkFrame:Result", onMessage);
+
+ window.top.PlacesCommandHook.bookmarkLink(PlacesUtils.bookmarksMenuFolderId,
+ uri.spec,
+ message.data.title,
+ message.data.description)
+ .catch(Components.utils.reportError);
+ };
+ mm.addMessageListener("ContextMenu:BookmarkFrame:Result", onMessage);
+
+ mm.sendAsyncMessage("ContextMenu:BookmarkFrame", null, { target: this.target });
+ },
+
+ shareLink: function CM_shareLink() {
+ SocialShare.sharePage(null, { url: this.linkURI.spec }, this.target);
+ },
+
+ shareImage: function CM_shareImage() {
+ SocialShare.sharePage(null, { url: this.imageURL, previews: [ this.mediaURL ] }, this.target);
+ },
+
+ shareVideo: function CM_shareVideo() {
+ SocialShare.sharePage(null, { url: this.mediaURL, source: this.mediaURL }, this.target);
+ },
+
+ shareSelect: function CM_shareSelect() {
+ SocialShare.sharePage(null, { url: this.browser.currentURI.spec, text: this.textSelected }, this.target);
+ },
+
+ savePageAs: function CM_savePageAs() {
+ saveBrowser(this.browser);
+ },
+
+ printFrame: function CM_printFrame() {
+ PrintUtils.printWindow(this.frameOuterWindowID, this.browser);
+ },
+
+ switchPageDirection: function CM_switchPageDirection() {
+ this.browser.messageManager.sendAsyncMessage("SwitchDocumentDirection");
+ },
+
+ mediaCommand : function CM_mediaCommand(command, data) {
+ let mm = this.browser.messageManager;
+ let win = this.browser.ownerGlobal;
+ let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ mm.sendAsyncMessage("ContextMenu:MediaCommand",
+ {command: command,
+ data: data,
+ handlingUserInput: windowUtils.isHandlingUserInput},
+ {element: this.target});
+ },
+
+ copyMediaLocation : function () {
+ var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(this.mediaURL);
+ },
+
+ drmLearnMore: function(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";
+ }
+ openUILinkIn(drmInfoURL, dest);
+ },
+
+ get imageURL() {
+ if (this.onImage)
+ return this.mediaURL;
+ return "";
+ },
+
+ // Formats the 'Search <engine> for "<selection or link text>"' context menu.
+ formatSearchContextItem: function() {
+ var menuItem = document.getElementById("context-searchselect");
+ let selectedText = this.isTextSelected ? this.textSelected : this.linkTextStr;
+
+ // Store searchTerms in context menu item so we know what to search onclick
+ menuItem.searchTerms = selectedText;
+
+ // 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.currentEngine.name;
+ var menuLabel = gNavigatorBundle.getFormattedString("contextMenuSearch",
+ [engineName,
+ selectedText]);
+ menuItem.label = menuLabel;
+ menuItem.accessKey = gNavigatorBundle.getString("contextMenuSearch.accesskey");
+ },
+
+ _getTelemetryClickInfo: function(aXulMenu) {
+ this._onPopupHiding = () => {
+ aXulMenu.ownerDocument.removeEventListener("command", activationHandler, true);
+ aXulMenu.removeEventListener("popuphiding", this._onPopupHiding, true);
+ delete this._onPopupHiding;
+
+ let eventKey = [
+ this._telemetryPageContext,
+ this._telemetryHadCustomItems ? "withcustom" : "withoutcustom"
+ ];
+ let target = this._telemetryClickID || "close-without-interaction";
+ BrowserUITelemetry.registerContextMenuInteraction(eventKey, target);
+ };
+ let activationHandler = (e) => {
+ // Deal with command events being routed to command elements; figure out
+ // what triggered the event (which will have the right e.target)
+ if (e.sourceEvent) {
+ e = e.sourceEvent;
+ }
+ // Target should be in the menu (this catches using shortcuts for items
+ // not in the menu while the menu is up)
+ if (!aXulMenu.contains(e.target)) {
+ return;
+ }
+
+ // Check if this is a page menu item:
+ if (e.target.hasAttribute(PageMenuParent.GENERATEDITEMID_ATTR)) {
+ this._telemetryClickID = "custom-page-item";
+ } else {
+ this._telemetryClickID = (e.target.id || "unknown").replace(/^context-/i, "");
+ }
+ };
+ aXulMenu.ownerDocument.addEventListener("command", activationHandler, true);
+ aXulMenu.addEventListener("popuphiding", this._onPopupHiding, true);
+ },
+
+ _getTelemetryPageContextInfo: function() {
+ let rv = [];
+ for (let k of ["isContentSelected", "onLink", "onImage", "onCanvas", "onVideo", "onAudio",
+ "onTextInput", "onSocial"]) {
+ if (this[k]) {
+ rv.push(k.replace(/^(?:is|on)(.)/, (match, firstLetter) => firstLetter.toLowerCase()));
+ }
+ }
+ if (!rv.length) {
+ rv.push('other');
+ }
+
+ return JSON.stringify(rv);
+ },
+
+ _checkTelemetryForMenu: function(aXulMenu) {
+ this._telemetryClickID = null;
+ this._telemetryPageContext = this._getTelemetryPageContextInfo();
+ this._telemetryHadCustomItems = this.hasPageMenu;
+ this._getTelemetryClickInfo(aXulMenu);
+ },
+
+ createContainerMenu: function(aEvent) {
+ return createUserContextMenu(aEvent, true,
+ gContextMenuContentData.userContextId);
+ },
+};