releases-comm-central/mail/base/content/nsContextMenu.js

1182 строки
35 KiB
JavaScript

/**
* 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/. */
/* import-globals-from folderDisplay.js */
/* import-globals-from mailTabs.js */
/* import-globals-from mailWindow.js */
/* import-globals-from messageDisplay.js */
/* import-globals-from utilityOverlay.js */
/* global EnigmailURIs: false, gEncryptedURIService: true */
var { InlineSpellChecker, SpellCheckHelper } = ChromeUtils.importESModule(
"resource://gre/modules/InlineSpellChecker.sys.mjs"
);
var { PlacesUtils } = ChromeUtils.importESModule(
"resource://gre/modules/PlacesUtils.sys.mjs"
);
var { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
var { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
);
ChromeUtils.defineModuleGetter(
this,
"MailUtils",
"resource:///modules/MailUtils.jsm"
);
var { E10SUtils } = ChromeUtils.importESModule(
"resource://gre/modules/E10SUtils.sys.mjs"
);
var gSpellChecker = new InlineSpellChecker();
/** Called by ContextMenuParent.sys.mjs */
function openContextMenu({ data }, browser, actor) {
if (!browser.hasAttribute("context")) {
return;
}
let spellInfo = data.spellInfo;
let frameReferrerInfo = data.frameReferrerInfo;
let linkReferrerInfo = data.linkReferrerInfo;
let principal = data.principal;
let storagePrincipal = data.storagePrincipal;
let documentURIObject = makeURI(
data.docLocation,
data.charSet,
makeURI(data.baseURI)
);
if (frameReferrerInfo) {
frameReferrerInfo = E10SUtils.deserializeReferrerInfo(frameReferrerInfo);
}
if (linkReferrerInfo) {
linkReferrerInfo = E10SUtils.deserializeReferrerInfo(linkReferrerInfo);
}
nsContextMenu.contentData = {
context: data.context,
browser,
actor,
editFlags: data.editFlags,
spellInfo,
principal,
storagePrincipal,
documentURIObject,
docLocation: data.docLocation,
charSet: data.charSet,
referrerInfo: E10SUtils.deserializeReferrerInfo(data.referrerInfo),
frameReferrerInfo,
linkReferrerInfo,
contentType: data.contentType,
contentDisposition: data.contentDisposition,
frameID: data.frameID,
frameOuterWindowID: data.frameID,
frameBrowsingContext: BrowsingContext.get(data.frameBrowsingContextID),
selectionInfo: data.selectionInfo,
disableSetDesktopBackground: data.disableSetDesktopBackground,
loginFillInfo: data.loginFillInfo,
parentAllowsMixedContent: data.parentAllowsMixedContent,
userContextId: data.userContextId,
webExtContextData: data.webExtContextData,
};
let popup = browser.ownerDocument.getElementById(
browser.getAttribute("context")
);
let context = nsContextMenu.contentData.context;
// We don't have access to the original event here, as that happened in
// another process. Therefore we synthesize a new MouseEvent to propagate the
// inputSource to the subsequently triggered popupshowing event.
let newEvent = document.createEvent("MouseEvent");
let screenX = context.screenXDevPx / window.devicePixelRatio;
let screenY = context.screenYDevPx / window.devicePixelRatio;
newEvent.initNSMouseEvent(
"contextmenu",
true,
true,
null,
0,
screenX,
screenY,
0,
0,
false,
false,
false,
false,
0,
null,
0,
context.mozInputSource
);
popup.openPopupAtScreen(newEvent.screenX, newEvent.screenY, true, newEvent);
}
/** Called by a popupshowing event via fillMailContextMenu x3. */
class nsContextMenu {
constructor(aXulMenu, aIsShift) {
this.xulMenu = aXulMenu;
// Get contextual info.
this.setContext();
if (!this.shouldDisplay) {
return;
}
// Message Related Items
this.inAMessage = false;
this.inThreadPane = false;
this.inStandaloneWindow = false;
this.numSelectedMessages = 0;
this.isNewsgroup = false;
this.hideMailItems = false;
this.isContentSelected =
!this.selectionInfo || !this.selectionInfo.docSelectionIsCollapsed;
this.setMessageTargets();
if (!aIsShift) {
// The rest of this block sends menu information to WebExtensions.
let subject = {
menu: aXulMenu,
tab: document.getElementById("tabmail")
? document.getElementById("tabmail").currentTabInfo
: window,
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.frameID,
webExtBrowserType: this.webExtBrowserType,
webExtContextData: this.contentData
? this.contentData.webExtContextData
: undefined,
};
if (this.inThreadPane) {
subject.displayedFolder = gFolderDisplay.view.displayedFolder;
subject.selectedMessages = gFolderDisplay.selectedMessages;
}
subject.wrappedJSObject = subject;
Services.obs.notifyObservers(subject, "on-build-contextmenu");
}
// Reset after "on-build-contextmenu" notification in case selection was
// changed during the notification.
this.isContentSelected =
!this.selectionInfo || !this.selectionInfo.docSelectionIsCollapsed;
this.initItems();
// If all items in the menu are hidden, set this.shouldDisplay to false
// so that the callers know to not even display the empty menu.
let contextPopup = document.getElementById("mailContext");
for (let item of contextPopup.children) {
if (!item.hidden) {
return;
}
}
// All items must have been hidden.
this.shouldDisplay = false;
}
setContext() {
let context = Object.create(null);
if (nsContextMenu.contentData) {
this.contentData = nsContextMenu.contentData;
context = this.contentData.context;
nsContextMenu.contentData = null;
}
this.shouldDisplay = !this.contentData || context.shouldDisplay;
this.timeStamp = context.timeStamp;
// Assign what's _possibly_ needed from `context` sent by ContextMenuChild.sys.mjs
// 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.canSpellCheck = context.canSpellCheck;
this.hasBGImage = context.hasBGImage;
this.hasMultipleBGImages = context.hasMultipleBGImages;
this.isDesignMode = context.isDesignMode;
this.inFrame = context.inFrame;
this.inPDFViewer = context.inPDFViewer;
this.inSrcdocFrame = context.inSrcdocFrame;
this.inSyntheticDoc = context.inSyntheticDoc;
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.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.frameID = context.frameID;
this.frameOuterWindowID = context.frameOuterWindowID;
this.frameBrowsingContext = BrowsingContext.get(
context.frameBrowsingContextID
);
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) {
return;
}
this.browser = this.contentData.browser;
if (this.browser && this.browser.currentURI.spec == "about:blank") {
this.shouldDisplay = false;
return;
}
this.selectionInfo = this.contentData.selectionInfo;
this.actor = this.contentData.actor;
this.textSelected = this.selectionInfo?.text;
this.isTextSelected = !!this.textSelected?.length;
this.webExtBrowserType = this.browser.getAttribute(
"webextension-view-type"
);
if (context.shouldInitInlineSpellCheckerUINoChildren) {
gSpellChecker.initFromRemote(
this.contentData.spellInfo,
this.actor.manager
);
}
if (this.contentData.spellInfo) {
this.spellSuggestions = this.contentData.spellInfo.spellSuggestions;
}
if (context.shouldInitInlineSpellCheckerUIWithChildren) {
gSpellChecker.initFromRemote(
this.contentData.spellInfo,
this.actor.manager
);
let canSpell = gSpellChecker.canSpellCheck && this.canSpellCheck;
this.showItem("mailContext-spell-check-enabled", canSpell);
this.showItem("mailContext-spell-separator", canSpell);
}
}
hiding() {
if (this.actor) {
this.actor.hiding();
}
this.contentData = null;
gSpellChecker.clearSuggestionsFromMenu();
gSpellChecker.clearDictionaryListFromMenu();
gSpellChecker.uninit();
}
initItems() {
this.initSaveItems();
this.initClipboardItems();
this.initMediaPlayerItems();
this.initBrowserItems();
this.initMessageItems();
this.initSpellingItems();
this.initSeparators();
}
addDictionaries() {
openDictionaryList();
}
initSpellingItems() {
let canSpell =
gSpellChecker.canSpellCheck &&
!gSpellChecker.initialSpellCheckPending &&
this.canSpellCheck;
let showDictionaries = canSpell && gSpellChecker.enabled;
let onMisspelling = gSpellChecker.overMisspelling;
let showUndo = canSpell && gSpellChecker.canUndo();
this.showItem("mailContext-spell-check-enabled", canSpell);
this.showItem("mailContext-spell-separator", canSpell);
document
.getElementById("mailContext-spell-check-enabled")
.setAttribute("checked", canSpell && gSpellChecker.enabled);
this.showItem("mailContext-spell-add-to-dictionary", onMisspelling);
this.showItem("mailContext-spell-undo-add-to-dictionary", showUndo);
// suggestion list
this.showItem(
"mailContext-spell-suggestions-separator",
onMisspelling || showUndo
);
if (onMisspelling) {
let addMenuItem = document.getElementById(
"mailContext-spell-add-to-dictionary"
);
let suggestionCount = gSpellChecker.addSuggestionsToMenu(
addMenuItem.parentNode,
addMenuItem,
this.spellSuggestions
);
this.showItem("mailContext-spell-no-suggestions", suggestionCount == 0);
} else {
this.showItem("mailContext-spell-no-suggestions", false);
}
// dictionary list
this.showItem("mailContext-spell-dictionaries", showDictionaries);
if (canSpell) {
let dictMenu = document.getElementById(
"mailContext-spell-dictionaries-menu"
);
let dictSep = document.getElementById(
"mailContext-spell-language-separator"
);
let count = gSpellChecker.addDictionaryListToMenu(dictMenu, dictSep);
this.showItem(dictSep, count > 0);
this.showItem("mailContext-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("mailContext-spell-language-separator", showDictionaries);
this.showItem(
"mailContext-spell-add-dictionaries-main",
showDictionaries
);
} else {
this.showItem("mailContext-spell-add-dictionaries-main", false);
}
}
initSaveItems() {
this.showItem("mailContext-savelink", this.onSaveableLink);
this.showItem("mailContext-saveimage", this.onLoadedImage);
}
initClipboardItems() {
// Copy depends on whether there is selected text.
// Enabling this context menu item is now done through the global
// command updating system.
goUpdateGlobalEditMenuItems();
this.showItem("mailContext-cut", !this.inAMessage && this.onTextInput);
this.showItem(
"mailContext-copy",
!this.inThreadPane &&
!this.onPlayableMedia &&
(this.isContentSelected || this.onTextInput)
);
this.showItem("mailContext-paste", !this.inAMessage && this.onTextInput);
this.showItem("mailContext-undo", !this.inAMessage && this.onTextInput);
// Select all not available in the thread pane or on playable media.
this.showItem(
"mailContext-selectall",
!this.inThreadPane && !this.onPlayableMedia
);
this.showItem("mailContext-copyemail", this.onMailtoLink);
this.showItem("mailContext-copylink", this.onLink && !this.onMailtoLink);
this.showItem("mailContext-copyimage", this.onImage);
this.showItem(
"mailContext-composeemailto",
this.onMailtoLink && !this.inThreadPane
);
this.showItem(
"mailContext-addemail",
this.onMailtoLink && !this.inThreadPane
);
let searchTheWeb = document.getElementById("mailContext-searchTheWeb");
this.showItem(
searchTheWeb,
!this.inThreadPane && !this.onPlayableMedia && this.isContentSelected
);
if (!searchTheWeb.hidden) {
let selection = document.commandDispatcher.focusedWindow
.getSelection()
.toString();
let bundle = document.getElementById("bundle_messenger");
let key = "openSearch.label";
let abbrSelection;
if (selection.length > 15) {
key += ".truncated";
abbrSelection = selection.slice(0, 15);
} else {
abbrSelection = selection;
}
searchTheWeb.label = bundle.getFormattedString(key, [
Services.search.defaultEngine.name,
abbrSelection,
]);
searchTheWeb.value = selection;
}
}
initMediaPlayerItems() {
let onMedia = this.onVideo || this.onAudio;
// Several mutually exclusive items.... play/pause, mute/unmute, show/hide
this.showItem("mailContext-media-play", onMedia && this.target.paused);
this.showItem("mailContext-media-pause", onMedia && !this.target.paused);
this.showItem("mailContext-media-mute", onMedia && !this.target.muted);
this.showItem("mailContext-media-unmute", onMedia && this.target.muted);
if (onMedia) {
let hasError =
this.target.error != null ||
this.target.networkState == this.target.NETWORK_NO_SOURCE;
this.setItemAttr("mailContext-media-play", "disabled", hasError);
this.setItemAttr("mailContext-media-pause", "disabled", hasError);
this.setItemAttr("mailContext-media-mute", "disabled", hasError);
this.setItemAttr("mailContext-media-unmute", "disabled", hasError);
}
}
initBrowserItems() {
// Work out if we are a context menu on a special item e.g. an image, link
// etc.
let notOnSpecialItem = !(
this.inAMessage ||
this.isContentSelected ||
this.onCanvas ||
this.onLink ||
this.onImage ||
this.onAudio ||
this.onVideo ||
this.onTextInput
);
// Ensure these commands are updated with their current status.
if (notOnSpecialItem) {
goUpdateCommand("cmd_stop");
goUpdateCommand("cmd_reload");
}
// These only needs showing if we're not on something special.
this.showItem("mailContext-stop", notOnSpecialItem);
this.showItem("mailContext-reload", notOnSpecialItem);
let loadedProtocol = "";
if (this.target && this.target.ownerGlobal?.top.location) {
loadedProtocol = this.target.ownerGlobal?.top.location.protocol;
}
// Only show open in browser if we're not on a special item and we're not
// on an about: or chrome: protocol - for these protocols the browser is
// unlikely to show the same thing as we do (if at all), so therefore don't
// offer the option.
this.showItem(
"mailContext-openInBrowser",
notOnSpecialItem && ["http:", "https:"].includes(loadedProtocol)
);
// Only show mailContext-openLinkInBrowser if we're on a link and it isn't
// a mailto link.
this.showItem(
"mailContext-openLinkInBrowser",
this.onLink && ["http", "https"].includes(this.linkProtocol)
);
}
/* eslint-disable complexity */
initMessageItems() {
// If we're not in a message related tab, we're just going to bulk hide most
// items as this simplifies the logic below.
if (!this.inAMessage) {
const messageTabSpecificItems = [
"mailContext-openNewWindow",
"threadPaneContext-openNewTab",
"mailContext-openConversation",
"mailContext-openContainingFolder",
"mailContext-archive",
"mailContext-replySender",
"mailContext-replyNewsgroup",
"mailContext-replyAll",
"mailContext-replyList",
"mailContext-forward",
"mailContext-forwardAsMenu",
"mailContext-multiForwardAsAttachment",
"mailContext-redirect",
"mailContext-editAsNew",
"mailContext-editDraftMsg",
"mailContext-newMsgFromTemplate",
"mailContext-editTemplateMsg",
"mailContext-copyMessageUrl",
"mailContext-moveMenu",
"mailContext-copyMenu",
"mailContext-moveToFolderAgain",
"mailContext-decryptToFolder",
"mailContext-ignoreThread",
"mailContext-ignoreSubthread",
"mailContext-watchThread",
"mailContext-tags",
"mailContext-mark",
"mailContext-saveAs",
"mailContext-print",
"mailContext-delete",
"downloadSelected",
"mailContext-reportPhishingURL",
"mailContext-calendar-convert-menu",
];
for (let i = 0; i < messageTabSpecificItems.length; ++i) {
this.showItem(messageTabSpecificItems[i], false);
}
return;
}
let canMove = gFolderDisplay.canDeleteSelectedMessages;
// Show the Open in New Window and New Tab options if there is exactly one
// message selected.
this.showItem(
"mailContext-openNewWindow",
this.numSelectedMessages == 1 && this.inThreadPane
);
this.showItem(
"threadPaneContext-openNewTab",
this.numSelectedMessages == 1 && this.inThreadPane
);
this.showItem(
"mailContext-openConversation",
this.numSelectedMessages == 1 &&
this.inThreadPane &&
ConversationOpener.isMessageIndexed(gFolderDisplay.selectedMessage)
);
this.showItem(
"mailContext-openContainingFolder",
!gFolderDisplay.folderPaneVisible &&
this.numSelectedMessages == 1 &&
!gMessageDisplay.isDummy
);
this.setSingleSelection("mailContext-replySender");
this.setSingleSelection("mailContext-replyNewsgroup", this.isNewsgroup);
this.setSingleSelection("mailContext-replyAll");
this.setSingleSelection("mailContext-replyList");
this.setSingleSelection("mailContext-forward");
this.setSingleSelection("mailContext-forwardAsMenu");
this.setSingleSelection("mailContext-redirect");
this.setSingleSelection("mailContext-editAsNew");
this.setSingleSelection(
"mailContext-editDraftMsg",
!document.getElementById("cmd_editDraftMsg").hidden
);
this.setSingleSelection(
"mailContext-newMsgFromTemplate",
!document.getElementById("cmd_newMsgFromTemplate").hidden
);
this.setSingleSelection(
"mailContext-editTemplateMsg",
!document.getElementById("cmd_editTemplateMsg").hidden
);
this.showItem(
"mailContext-multiForwardAsAttachment",
this.numSelectedMessages > 1 && this.inThreadPane && !this.hideMailItems
);
this.setSingleSelection("mailContext-copyMessageUrl", this.isNewsgroup);
let msgModifyItems =
this.numSelectedMessages > 0 &&
!this.hideMailItems &&
!this.onPlayableMedia &&
!(this.numSelectedMessages == 1 && gMessageDisplay.isDummy);
let canArchive = gFolderDisplay.canArchiveSelectedMessages;
this.showItem(
"mailContext-archive",
canMove && msgModifyItems && canArchive
);
// Set up the move menu. We can't move from newsgroups.
this.showItem("mailContext-moveMenu", msgModifyItems && !this.isNewsgroup);
// disable move if we can't delete message(s) from this folder
this.enableItem("mailContext-moveMenu", canMove && !this.onPlayableMedia);
// Copy is available as long as something is selected.
let canCopy =
msgModifyItems ||
(gMessageDisplay.isDummy && window.arguments[0].scheme == "file");
this.showItem("mailContext-copyMenu", canCopy);
this.showItem("mailContext-moveToFolderAgain", msgModifyItems);
if (msgModifyItems) {
initMoveToFolderAgainMenu(
document.getElementById("mailContext-moveToFolderAgain")
);
goUpdateCommand("cmd_moveToFolderAgain");
}
let showDecrypt = this.numSelectedMessages > 1;
if (this.numSelectedMessages == 1) {
let msgURI = gFolderDisplay.selectedMessageUris[0];
showDecrypt =
EnigmailURIs.isEncryptedUri(msgURI) ||
gEncryptedURIService.isEncrypted(msgURI);
}
this.showItem("mailContext-decryptToFolder", showDecrypt);
this.showItem("mailContext-tags", msgModifyItems);
this.showItem("mailContext-mark", msgModifyItems);
this.showItem(
"mailContext-ignoreThread",
!this.inStandaloneWindow &&
this.numSelectedMessages >= 1 &&
!this.hideMailItems &&
!this.onPlayableMedia
);
this.showItem(
"mailContext-ignoreSubthread",
!this.inStandaloneWindow &&
this.numSelectedMessages >= 1 &&
!this.hideMailItems &&
!this.onPlayableMedia
);
this.showItem(
"mailContext-watchThread",
!this.inStandaloneWindow &&
this.numSelectedMessages > 0 &&
!this.hideMailItems &&
!this.onPlayableMedia
);
this.showItem("mailContext-afterWatchThread", !this.inStandaloneWindow);
this.showItem(
"mailContext-saveAs",
this.numSelectedMessages > 0 &&
!this.hideMailItems &&
!gMessageDisplay.isDummy &&
!this.onPlayableMedia
);
// XXX Not quite modifying the message, but the same rules apply at the
// moment as we can't print non-message content from the message pane yet.
this.showItem("mailContext-print", msgModifyItems);
this.showItem(
"mailContext-delete",
msgModifyItems && (this.isNewsgroup || canMove)
);
// This function is needed for the case where a folder is just loaded (while
// there isn't a message loaded in the message pane), a right-click is done
// in the thread pane. This function will disable enable the 'Delete
// Message' menu item.
goUpdateCommand("cmd_delete");
this.showItem(
"downloadSelected",
this.numSelectedMessages > 1 && !this.hideMailItems
);
this.showItem(
"mailContext-reportPhishingURL",
!this.inThreadPane && this.onLink && !this.onMailtoLink
);
this.setSingleSelection("mailContext-calendar-convert-menu");
}
initSeparators() {
let separators = Array.from(
this.xulMenu.querySelectorAll(":scope > menuseparator")
);
let lastShownSeparator = null;
for (let separator of separators) {
let shouldShow = this.shouldShowSeparator(separator);
if (
!shouldShow &&
lastShownSeparator &&
separator.classList.contains("webextension-group-separator")
) {
// The separator for the WebExtension elements group must be shown, hide
// the last shown menu separator instead.
lastShownSeparator.hidden = true;
shouldShow = true;
}
if (shouldShow) {
lastShownSeparator = separator;
}
separator.hidden = !shouldShow;
}
this.checkLastSeparator(this.xulMenu);
}
setMessageTargets() {
if (this.browser) {
this.inAMessage = ["imap", "mailbox", "news", "snews"].includes(
this.browser.currentURI.scheme
);
this.inThreadPane = false;
if (!this.inAMessage) {
this.inStandaloneWindow = true;
this.numSelectedMessages = 0;
this.isNewsgroup = false;
this.hideMailItems = true;
return;
}
} else {
this.inThreadPane = true;
}
this.inAMessage = true;
this.inStandaloneWindow = false;
this.numSelectedMessages = gFolderDisplay.selectedCount;
this.isNewsgroup = gFolderDisplay.selectedMessageIsNews;
// Don't show mail items for links/images, just show related items.
this.hideMailItems = !this.inThreadPane && (this.onImage || this.onLink);
}
/**
* Get a computed style property for an element.
*
* @param aElem
* A DOM node
* @param aProp
* The desired CSS property
* @returns the value of the property
*/
getComputedStyle(aElem, aProp) {
return aElem.ownerGlobal.getComputedStyle(aElem).getPropertyValue(aProp);
}
/**
* Determine whether the clicked-on link can be saved, and whether it
* may be saved according to the ScriptSecurityManager.
*
* @returns true if the protocol can be persisted and if the target has
* permission to link to the URL, false if not
*/
isLinkSaveable() {
try {
Services.scriptSecurityManager.checkLoadURIWithPrincipal(
this.target.nodePrincipal,
this.linkURI,
Ci.nsIScriptSecurityManager.STANDARD
);
} catch (e) {
// Don't save things we can't link to.
return false;
}
// 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"
)
);
}
/**
* Save URL of clicked-on link.
*/
saveLink() {
saveURL(
this.linkURL,
null,
this.linkTextStr,
null,
true,
null,
null,
null,
document
);
}
/**
* Save a clicked-on image.
*/
saveImage() {
saveURL(
this.imageInfo.currentSrc,
null,
null,
"SaveImageTitle",
false,
null,
null,
null,
document
);
}
/**
* Extract email addresses from a mailto: link and put them on the
* 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.
const kMailToLength = 7; // length of "mailto:"
var url = this.linkURL;
var qmark = url.indexOf("?");
var addresses;
if (qmark > kMailToLength) {
addresses = url.substring(kMailToLength, qmark);
} else {
addresses = url.substr(kMailToLength);
}
// Let's try to unescape it using a character set.
try {
addresses = Services.textToSubURI.unEscapeURIForUI(addresses);
} catch (ex) {
// Do nothing.
}
var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
Ci.nsIClipboardHelper
);
clipboard.copyString(addresses);
}
// ---------
// Utilities
/**
* Set a DOM node's hidden property by passing in the node's id or the
* element itself.
*
* @param aItemOrId
* a DOM node or the id of a DOM node
* @param aShow
* true to show, false to hide
*/
showItem(aItemOrId, aShow) {
var item =
aItemOrId.constructor == String
? document.getElementById(aItemOrId)
: aItemOrId;
if (item) {
item.hidden = !aShow;
}
}
/**
* Set a DOM node's disabled property by passing in the node's id or the
* element itself.
*
* @param aItemOrId A DOM node or the id of a DOM node
* @param aEnabled True to enable the element, false to disable.
*/
enableItem(aItemOrId, aEnabled) {
var item =
aItemOrId.constructor == String
? document.getElementById(aItemOrId)
: aItemOrId;
item.disabled = !aEnabled;
}
/**
* Most menu items are visible if there's 1 or 0 messages selected, and
* enabled if there's exactly one selected. Handle those here.
* Exception: playable media is selected, in which case, don't show them.
*
* @param aID the id of the element to display/enable
* @param aShow (optional) - an additional criteria to evaluate when we
* decide whether to display the element. If false, we'll hide
* the item no matter what messages are selected.
*/
setSingleSelection(aID, aShow) {
let show = aShow != undefined ? aShow : true;
this.showItem(
aID,
this.numSelectedMessages == 1 &&
!this.hideMailItems &&
show &&
!this.onPlayableMedia
);
this.enableItem(aID, this.numSelectedMessages == 1);
}
/**
* 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).
*
* @param aId
* The id of an element
* @param aAttr
* The attribute name
* @param aVal
* The value to set the attribute to, or null to remove the 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);
}
}
}
/**
* Get an absolute URL for clicked-on link, from the href property or by
* resolving an XLink URL by hand.
*
* @returns the string absolute URL for the clicked-on link
*/
getLinkURL() {
if (this.link.href) {
return this.link.href;
}
var href = this.link.getAttributeNS("http://www.w3.org/1999/xlink", "href");
if (!href || href.trim() == "") {
// Without this we try to save as the current doc,
// for example, HTML case also throws if empty.
throw new Error("Empty href");
}
href = this.makeURLAbsolute(this.link.baseURI, href);
return href;
}
/**
* Generate a URI object from the linkURL spec
*
* @returns an nsIURI if possible, or null if not
*/
getLinkURI() {
try {
return Services.io.newURI(this.linkURL);
} catch (ex) {
// e.g. empty URL string
}
return null;
}
/**
* Get the scheme for the clicked-on linkURI, if present.
*
* @returns a scheme, possibly undefined, or null if there's no linkURI
*/
getLinkProtocol() {
if (this.linkURI) {
return this.linkURI.scheme; // Can be |undefined|.
}
return null;
}
/**
* Get the text of the clicked-on link.
*
* @returns {string}
*/
linkText() {
return this.linkTextStr;
}
/**
* Determines whether the focused window has something selected.
*
* @returns true if there is a selection, false if not
*/
isContentSelection() {
return !document.commandDispatcher.focusedWindow.getSelection().isCollapsed;
}
/**
* Convert relative URL to absolute, using a provided <base>.
*
* @param aBase
* The URL string to use as the base
* @param aUrl
* The possibly-relative URL string
* @returns The string absolute URL
*/
makeURLAbsolute(aBase, aUrl) {
// Construct nsIURL.
var baseURI = Services.io.newURI(aBase);
return Services.io.newURI(baseURI.resolve(aUrl)).spec;
}
/**
* Determine whether a DOM node is a text or password input, or a textarea.
*
* @param aNode
* The DOM node to check
* @returns true for textboxes, false for other elements
*/
isTargetATextBox(aNode) {
if (HTMLInputElement.isInstance(aNode)) {
return aNode.type == "text" || aNode.type == "password";
}
return HTMLTextAreaElement.isInstance(aNode);
}
/**
* Determine whether a separator should be shown based on whether
* there are any non-hidden items between it and the previous separator.
*
* @param {DomElement} element - The separator element.
* @returns {boolean} True if the separator should be shown, false if not.
*/
shouldShowSeparator(element) {
if (element) {
let sibling = element.previousElementSibling;
while (sibling && sibling.localName != "menuseparator") {
if (!sibling.hidden) {
return true;
}
sibling = sibling.previousElementSibling;
}
}
return false;
}
/**
* Ensures that there isn't a separator shown at the bottom of the menu.
*
* @param aPopup The menu to check.
*/
checkLastSeparator(aPopup) {
let sibling = aPopup.lastElementChild;
while (sibling) {
if (!sibling.hidden) {
if (sibling.localName == "menuseparator") {
// If we got here then the item is a menuseparator and everything
// below it hidden.
sibling.setAttribute("hidden", true);
return;
}
return;
}
sibling = sibling.previousElementSibling;
}
}
openInBrowser() {
let url = this.target.ownerGlobal?.top.location.href;
PlacesUtils.history
.insert({
url,
visits: [
{
date: new Date(),
},
],
})
.catch(console.error);
Cc["@mozilla.org/uriloader/external-protocol-service;1"]
.getService(Ci.nsIExternalProtocolService)
.loadURI(Services.io.newURI(url));
}
openLinkInBrowser() {
PlacesUtils.history
.insert({
url: this.linkURL,
visits: [
{
date: new Date(),
},
],
})
.catch(console.error);
Cc["@mozilla.org/uriloader/external-protocol-service;1"]
.getService(Ci.nsIExternalProtocolService)
.loadURI(this.linkURI);
}
mediaCommand(command) {
var media = this.target;
switch (command) {
case "play":
media.play();
break;
case "pause":
media.pause();
break;
case "mute":
media.muted = true;
break;
case "unmute":
media.muted = false;
break;
// XXX hide controls & show controls don't work in emails as Javascript is
// disabled. May want to consider later for RSS feeds.
}
}
}