mirror of
https://github.com/ManchildProductions/binoc-central-mirror.git
synced 2026-06-16 08:29:26 +00:00
997 lines
38 KiB
JavaScript
997 lines
38 KiB
JavaScript
/** ***** BEGIN LICENSE BLOCK *****
|
|
* 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/InlineSpellChecker.jsm");
|
|
Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
|
|
Components.utils.import("resource://gre/modules/Services.jsm");
|
|
Components.utils.import("resource://gre/modules/AppConstants.jsm");
|
|
Components.utils.import("resource:///modules/MailUtils.js");
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "PageMenuParent", function() {
|
|
let tmp = {};
|
|
Cu.import("resource://gre/modules/PageMenu.jsm", tmp);
|
|
return new tmp.PageMenuParent();
|
|
});
|
|
|
|
var gSpellChecker = new InlineSpellChecker();
|
|
|
|
function nsContextMenu(aXulMenu, aIsShift) {
|
|
this.target = null;
|
|
this.menu = null;
|
|
this.onTextInput = false;
|
|
this.onEditableArea = false;
|
|
this.onImage = false;
|
|
this.onLoadedImage = false;
|
|
this.onCanvas = false;
|
|
this.onVideo = false;
|
|
this.onAudio = false;
|
|
this.onPlayableMedia = false;
|
|
this.onLink = false;
|
|
this.onMailtoLink = false;
|
|
this.onSaveableLink = false;
|
|
this.onMetaDataItem = false;
|
|
this.onMathML = false;
|
|
this.link = false;
|
|
this.linkURL = "";
|
|
this.linkURI = null;
|
|
this.linkProtocol = null;
|
|
this.mediaURL = "";
|
|
this.isContentSelected = false;
|
|
this.shouldDisplay = true;
|
|
|
|
// Message Related Items
|
|
this.inMessageArea = false;
|
|
this.inThreadPane = false;
|
|
this.inStandaloneWindow = false;
|
|
this.messagepaneIsBlank = false;
|
|
this.numSelectedMessages = 0;
|
|
this.isNewsgroup = false;
|
|
this.hideMailItems = false;
|
|
|
|
this.initMenu(aXulMenu, aIsShift);
|
|
}
|
|
|
|
nsContextMenu.prototype = {
|
|
/**
|
|
* Init: set properties based on the clicked-on element and the state of
|
|
* the world, then determine which context menu items to show based on
|
|
* those properties.
|
|
*/
|
|
initMenu : function CM_initMenu(aPopup, aIsShift) {
|
|
this.menu = aPopup;
|
|
|
|
// Get contextual info.
|
|
this.setTarget(document.popupNode);
|
|
this.setMessageTargets(document.popupNode);
|
|
|
|
if (!this.inThreadPane && this.messagepaneIsBlank) {
|
|
this.shouldDisplay = false;
|
|
return;
|
|
}
|
|
|
|
this.isContentSelected = this.isContentSelection();
|
|
|
|
this.hasPageMenu = false;
|
|
if (!aIsShift) {
|
|
this.hasPageMenu = PageMenuParent.buildAndAddToPopup(this.target,
|
|
aPopup);
|
|
}
|
|
|
|
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;
|
|
},
|
|
initItems : function CM_initItems() {
|
|
this.initPageMenuSeparator();
|
|
this.initSaveItems();
|
|
this.initClipboardItems();
|
|
this.initMediaPlayerItems();
|
|
this.initBrowserItems();
|
|
this.initMessageItems();
|
|
this.initSpellingItems();
|
|
this.initSeparators();
|
|
},
|
|
addDictionaries: function CM_addDictionaries() {
|
|
openDictionaryList();
|
|
},
|
|
initPageMenuSeparator: function CM_initPageMenuSeparator() {
|
|
this.showItem("page-menu-separator", this.hasPageMenu);
|
|
},
|
|
initSpellingItems: function CM_initSpellingItems() {
|
|
let canSpell = gSpellChecker.canSpellCheck;
|
|
let onMisspelling = gSpellChecker.overMisspelling;
|
|
this.showItem("mailContext-spell-check-enabled", canSpell);
|
|
this.showItem("mailContext-spell-separator", canSpell || this.onEditableArea);
|
|
if (canSpell) {
|
|
document.getElementById("mailContext-spell-check-enabled")
|
|
.setAttribute("checked", gSpellChecker.enabled);
|
|
}
|
|
|
|
this.showItem("mailContext-spell-add-to-dictionary", onMisspelling);
|
|
|
|
// suggestion list
|
|
this.showItem("mailContext-spell-suggestions-separator", onMisspelling);
|
|
if (onMisspelling) {
|
|
let addMenuItem =
|
|
document.getElementById("mailContext-spell-add-to-dictionary");
|
|
let suggestionCount =
|
|
gSpellChecker.addSuggestionsToMenu(addMenuItem.parentNode,
|
|
addMenuItem, 5);
|
|
this.showItem("mailContext-spell-no-suggestions", suggestionCount == 0);
|
|
} else {
|
|
this.showItem("mailContext-spell-no-suggestions", false);
|
|
}
|
|
|
|
// dictionary list
|
|
this.showItem("mailContext-spell-dictionaries", gSpellChecker.enabled);
|
|
if (canSpell) {
|
|
let dictMenu = document.getElementById("mailContext-spell-dictionaries-menu");
|
|
let dictSep = document.getElementById("mailContext-spell-language-separator");
|
|
gSpellChecker.addDictionaryListToMenu(dictMenu, dictSep);
|
|
this.showItem("mailContext-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("mailContext-spell-add-dictionaries-main", true);
|
|
} else {
|
|
this.showItem("mailContext-spell-add-dictionaries-main", false);
|
|
}
|
|
},
|
|
initSaveItems : function CM_initSaveItems() {
|
|
this.showItem("mailContext-savelink", this.onSaveableLink);
|
|
this.showItem("mailContext-saveimage", this.onLoadedImage);
|
|
},
|
|
initClipboardItems : function CM_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.inMessageArea && this.onTextInput);
|
|
this.showItem("mailContext-copy",
|
|
!this.inThreadPane && !this.onPlayableMedia &&
|
|
(this.isContentSelected || this.onTextInput));
|
|
this.showItem("mailContext-paste", !this.inMessageArea && this.onTextInput);
|
|
|
|
this.showItem("mailContext-undo", !this.inMessageArea && 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.currentEngine.name, abbrSelection
|
|
]);
|
|
searchTheWeb.value = selection;
|
|
}
|
|
},
|
|
initMediaPlayerItems: function CM_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: function CM_initBrowserItems() {
|
|
// Work out if we are a context menu on a special item e.g. an image, link
|
|
// etc.
|
|
let notOnSpecialItem = !(this.inMessageArea || this.isContentSelected ||
|
|
this.onCanvas || this.onLink || this.onImage ||
|
|
this.onPlayableMedia || 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.ownerDocument.defaultView.top.location)
|
|
loadedProtocol = this.target.ownerDocument.defaultView.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 &&
|
|
loadedProtocol &&
|
|
loadedProtocol != "about:" && loadedProtocol != "chrome:");
|
|
|
|
// Only show mailContext-openLinkInBrowser if we're on a link and it isn't
|
|
// a mailto link.
|
|
this.showItem("mailContext-openLinkInBrowser",
|
|
this.onLink && !this.onMailtoLink &&
|
|
this.linkProtocol != "about" && this.linkProtocol != "chrome");
|
|
},
|
|
initMessageItems: function CM_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.inMessageArea) {
|
|
const messageTabSpecificItems = [
|
|
"mailContext-openNewWindow", "threadPaneContext-openNewTab",
|
|
"mailContext-openConversation", "mailContext-openContainingFolder",
|
|
"mailContext-archive", "mailContext-replySender",
|
|
"mailContext-editAsNew", "mailContext-editDraftMsg",
|
|
"mailContext-replyNewsgroup", "mailContext-replyAll",
|
|
"mailContext-replyList", "mailContext-forward",
|
|
"mailContext-forwardAsMenu", "mailContext-multiForwardAsAttachment",
|
|
"mailContext-copyMessageUrl", "mailContext-moveMenu",
|
|
"mailContext-copyMenu", "mailContext-moveToFolderAgain",
|
|
"mailContext-ignoreThread", "mailContext-ignoreSubthread",
|
|
"mailContext-watchThread",
|
|
"mailContext-tags", "mailContext-mark", "mailContext-saveAs",
|
|
"mailContext-printpreview", "mailContext-print", "mailContext-delete",
|
|
"downloadSelected", "mailContext-reportPhishingURL"
|
|
];
|
|
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 &&
|
|
gConversationOpener.isSelectedMessageIndexed());
|
|
this.showItem("mailContext-openContainingFolder",
|
|
!gFolderDisplay.folderPaneVisible &&
|
|
this.numSelectedMessages == 1);
|
|
|
|
this.setSingleSelection("mailContext-replySender");
|
|
this.setSingleSelection("mailContext-editAsNew");
|
|
this.setSingleSelection("mailContext-editDraftMsg",
|
|
document.getElementById("cmd_editDraftMsg")
|
|
.getAttribute("hidden") != "true");
|
|
this.setSingleSelection("mailContext-replyNewsgroup", this.isNewsgroup);
|
|
this.setSingleSelection("mailContext-replyAll");
|
|
this.setSingleSelection("mailContext-replyList");
|
|
this.setSingleSelection("mailContext-forward");
|
|
this.setSingleSelection("mailContext-forwardAsMenu");
|
|
|
|
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");
|
|
}
|
|
|
|
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);
|
|
|
|
|
|
if (AppConstants.platform == "macosx")
|
|
this.showItem("mailContext-printpreview", false);
|
|
else
|
|
this.setSingleSelection("mailContext-printpreview");
|
|
|
|
// 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);
|
|
},
|
|
initSeparators: function CM_initSeparators() {
|
|
const mailContextSeparators = [
|
|
"mailContext-sep-open-browser", "mailContext-sep-link",
|
|
"mailContext-sep-open", "mailContext-sep-open2",
|
|
"mailContext-sep-reply", "paneContext-afterMove",
|
|
"mailContext-sep-afterTagAddNew", "mailContext-sep-afterTagRemoveAll",
|
|
"mailContext-sep-afterMarkAllRead", "mailContext-sep-afterMarkFlagged",
|
|
"mailContext-sep-afterMarkMenu", "mailContext-afterWatchThread",
|
|
"mailContext-sep-edit",
|
|
"mailContext-sep-copy", "mailContext-sep-reportPhishing",
|
|
"mailContext-sep-undo", "mailContext-sep-clipboard",
|
|
"mailContext-spell-suggestions-separator", "mailContext-spell-separator",
|
|
];
|
|
mailContextSeparators.forEach(this.hideIfAppropriate, this);
|
|
|
|
this.checkLastSeparator(this.menu);
|
|
},
|
|
|
|
/**
|
|
* Set the nsContextMenu properties based on the selected node and
|
|
* its ancestors.
|
|
*/
|
|
setTarget : function CM_setTarget(aNode) {
|
|
// Clear any old spellchecking items from the menu, this used to
|
|
// be in the menu hiding code but wasn't getting called in all
|
|
// situations. Here, we can ensure it gets cleaned up any time the
|
|
// menu is shown. Note: must be before uninit because that clears the
|
|
// internal vars
|
|
// We also need to do that before we possibly bail because we just clicked
|
|
// on some XUL node. Otherwise, dictionary choices just accumulate until we
|
|
// right-click on some HTML element again.
|
|
gSpellChecker.clearSuggestionsFromMenu();
|
|
gSpellChecker.clearDictionaryListFromMenu();
|
|
gSpellChecker.uninit();
|
|
|
|
const xulNS =
|
|
"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
if (aNode.namespaceURI == xulNS) {
|
|
if (aNode.localName == "treecol") {
|
|
// The column header was clicked, show the column picker.
|
|
let treecols = aNode.parentNode;
|
|
let nodeList = document.getAnonymousNodes(treecols);
|
|
let treeColPicker;
|
|
for (let i = 0; i < nodeList.length; i++) {
|
|
if (nodeList.item(i).localName == "treecolpicker") {
|
|
treeColPicker = nodeList.item(i);
|
|
break;
|
|
}
|
|
}
|
|
let popup = document.getAnonymousElementByAttribute(treeColPicker, "anonid", "popup");
|
|
treeColPicker.buildPopup(popup);
|
|
popup.openPopup(aNode, "before_start", 0, 0, true);
|
|
this.shouldDisplay = false;
|
|
}
|
|
return;
|
|
}
|
|
this.onImage = false;
|
|
this.onLoadedImage = false;
|
|
this.onMetaDataItem = false;
|
|
this.onTextInput = false;
|
|
this.onEditableArea = false;
|
|
this.imageURL = "";
|
|
this.onLink = false;
|
|
this.onVideo = false;
|
|
this.onAudio = false;
|
|
this.mediaURL = "";
|
|
this.linkURL = "";
|
|
this.linkURI = null;
|
|
this.linkProtocol = null;
|
|
this.onMathML = false;
|
|
|
|
this.target = aNode;
|
|
|
|
// Set up early the right flags for editable / not editable.
|
|
let editFlags = SpellCheckHelper.isEditable(this.target, window);
|
|
this.onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0;
|
|
this.onEditableArea = (editFlags & SpellCheckHelper.EDITABLE) !== 0;
|
|
|
|
// First, do checks for nodes that never have children.
|
|
if (this.target.nodeType == Node.ELEMENT_NODE) {
|
|
if (this.target instanceof Components.interfaces.nsIImageLoadingContent &&
|
|
this.target.currentURI) {
|
|
this.onImage = true;
|
|
this.onMetaDataItem = true;
|
|
|
|
var request = this.target.getRequest(Components.interfaces.nsIImageLoadingContent.CURRENT_REQUEST);
|
|
if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE))
|
|
this.onLoadedImage = true;
|
|
|
|
this.imageURL = this.target.currentURI.spec;
|
|
} else if (this.target instanceof HTMLInputElement) {
|
|
this.onTextInput = this.isTargetATextBox(this.target);
|
|
} else if (editFlags & (SpellCheckHelper.INPUT | SpellCheckHelper.TEXTAREA)) {
|
|
if (!this.target.readOnly) {
|
|
this.onEditableArea = true;
|
|
gSpellChecker.init(this.target.QueryInterface(Components.interfaces.nsIDOMNSEditableElement).editor);
|
|
gSpellChecker.initFromEvent(document.popupRangeParent, document.popupRangeOffset);
|
|
}
|
|
} else if (editFlags & (SpellCheckHelper.CONTENTEDITABLE)) {
|
|
let targetWin = this.target.ownerDocument.defaultView;
|
|
let editingSession = targetWin.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
|
.getInterface(Components.interfaces.nsIWebNavigation)
|
|
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
|
.getInterface(Components.interfaces.nsIEditingSession);
|
|
gSpellChecker.init(editingSession.getEditorForWindow(targetWin));
|
|
gSpellChecker.initFromEvent(document.popupRangeParent, document.popupRangeOffset);
|
|
} else if (this.target instanceof HTMLCanvasElement) {
|
|
this.onCanvas = true;
|
|
} else if (this.target instanceof HTMLVideoElement) {
|
|
this.onVideo = true;
|
|
this.onPlayableMedia = true;
|
|
this.mediaURL = this.target.currentSrc || this.target.src;
|
|
} else if (this.target instanceof HTMLAudioElement) {
|
|
this.onAudio = true;
|
|
this.onPlayableMedia = true;
|
|
this.mediaURL = this.target.currentSrc || this.target.src;
|
|
// Browser supports background images here but we don't need to.
|
|
}
|
|
}
|
|
|
|
// Second, bubble out, looking for items of interest that might be
|
|
// parents of the click target, picking the innermost of each.
|
|
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 &&
|
|
((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;
|
|
this.onMetaDataItem = true;
|
|
// Remember corresponding element.
|
|
this.link = elem;
|
|
this.linkURL = this.getLinkURL();
|
|
this.linkURI = this.getLinkURI();
|
|
this.linkProtocol = this.getLinkProtocol();
|
|
this.onMailtoLink = (this.linkProtocol == "mailto");
|
|
this.onSaveableLink = this.isLinkSaveable();
|
|
}
|
|
|
|
// Text input?
|
|
if (!this.onTextInput) {
|
|
this.onTextInput = this.isTargetATextBox(elem);
|
|
}
|
|
|
|
// Metadata item?
|
|
if (!this.onMetaDataItem) {
|
|
if ((elem instanceof HTMLQuoteElement && elem.cite) ||
|
|
(elem instanceof HTMLTableElement && elem.summary) ||
|
|
(elem instanceof HTMLModElement &&
|
|
(elem.cite || elem.dateTime)) ||
|
|
(elem instanceof HTMLElement &&
|
|
(elem.title || elem.lang)) ||
|
|
(elem.getAttributeNS(XMLNS, "lang"))) {
|
|
this.onMetaDataItem = true;
|
|
}
|
|
}
|
|
|
|
// Browser supports background images here but we don't need to.
|
|
}
|
|
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;
|
|
},
|
|
|
|
setMessageTargets: function CM_setMessageTargets(aNode) {
|
|
let tabmail = document.getElementById("tabmail");
|
|
if (tabmail) {
|
|
// Not all tabs are message tabs - if we're in a tab mode that is in
|
|
// mailTabType's list of modes, then we'll assume it is a message related
|
|
// tab.
|
|
this.inMessageArea = tabmail.selectedTab.mode.name in mailTabType.modes;
|
|
this.inStandaloneWindow = false;
|
|
}
|
|
else {
|
|
// Assume that if we haven't got a tabmail item, then we're in standalone
|
|
// window
|
|
this.inMessageArea = true;
|
|
this.inStandaloneWindow = true;
|
|
}
|
|
|
|
if (!this.inMessageArea) {
|
|
this.inThreadPane = false;
|
|
this.numSelectedMessages = 1;
|
|
this.isNewsgroup = false;
|
|
this.hideMailItems = true;
|
|
return;
|
|
}
|
|
|
|
this.inThreadPane = this.popupNodeIsInThreadPane(aNode);
|
|
this.messagepaneIsBlank = (document.getElementById("messagepane")
|
|
.contentWindow.location.href == "about:blank");
|
|
|
|
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
|
|
* @return the value of the property
|
|
*/
|
|
getComputedStyle: function CM_getComputedStyle(aElem, aProp) {
|
|
return aElem.ownerDocument.defaultView.getComputedStyle(aElem, "")
|
|
.getPropertyValue(aProp);
|
|
},
|
|
|
|
/**
|
|
* Generate a URL string from a computed style property, for things like
|
|
* |style="background-image:url(...)"|
|
|
* @return a "url"-type computed style attribute value, with the "url(" and
|
|
* ")" stripped.
|
|
*/
|
|
getComputedURL: function CM_getComputedURL(aElem, aProp) {
|
|
var url = aElem.ownerDocument.defaultView.getComputedStyle(aElem, "")
|
|
.getPropertyCSSValue(aProp);
|
|
return (url.primitiveType == CSSPrimitiveValue.CSS_URI) ? url.getStringValue() : null;
|
|
},
|
|
|
|
/**
|
|
* Determine whether the clicked-on link can be saved, and whether it
|
|
* may be saved according to the ScriptSecurityManager.
|
|
* @return true if the protocol can be persisted and if the target has
|
|
* permission to link to the URL, false if not
|
|
*/
|
|
isLinkSaveable : function CM_isLinkSaveable() {
|
|
try {
|
|
const nsIScriptSecurityManager =
|
|
Components.interfaces.nsIScriptSecurityManager;
|
|
Services.scriptSecurityManager.checkLoadURIWithPrincipal(this.target.nodePrincipal,
|
|
this.linkURI, 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 : function CM_saveLink() {
|
|
saveURL(this.linkURL, this.linkText(), null, true, null, null, document);
|
|
},
|
|
|
|
/**
|
|
* Save a clicked-on image.
|
|
*/
|
|
saveImage : function CM_saveImage() {
|
|
saveURL(this.imageURL, null, "SaveImageTitle", false, null, null, document);
|
|
},
|
|
|
|
/**
|
|
* Extract email addresses from a mailto: link and put them on the
|
|
* clipboard.
|
|
*/
|
|
copyEmail : function CM_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 {
|
|
var characterSet = this.target.ownerDocument.characterSet;
|
|
const textToSubURI = Components.classes["@mozilla.org/intl/texttosuburi;1"]
|
|
.getService(Components.interfaces.nsITextToSubURI);
|
|
addresses = textToSubURI.unEscapeURIForUI(characterSet, addresses);
|
|
}
|
|
catch(ex) {
|
|
// Do nothing.
|
|
}
|
|
|
|
var clipboard = Components.classes["@mozilla.org/widget/clipboardhelper;1"]
|
|
.getService(Components.interfaces.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 : function CM_showItem(aItemOrId, aShow) {
|
|
var item = aItemOrId.constructor == String ? document.getElementById(aItemOrId) : aItemOrId;
|
|
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: function CM_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: function CM_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 : function CM_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.
|
|
* @return the string absolute URL for the clicked-on link
|
|
*/
|
|
getLinkURL : function CM_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 "Empty href";
|
|
}
|
|
href = this.makeURLAbsolute(this.link.baseURI,href);
|
|
return href;
|
|
},
|
|
|
|
/**
|
|
* Generate a URI object from the linkURL spec
|
|
* @return an nsIURI if possible, or null if not
|
|
*/
|
|
getLinkURI: function CM_getLinkURI() {
|
|
try {
|
|
return Services.io.newURI(this.linkURL, null, null);
|
|
} catch (ex) {
|
|
// e.g. empty URL string
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Get the scheme for the clicked-on linkURI, if present.
|
|
* @return a scheme, possibly undefined, or null if there's no linkURI
|
|
*/
|
|
getLinkProtocol: function CM_getLinkProtocol() {
|
|
if (this.linkURI)
|
|
return this.linkURI.scheme; // can be |undefined|
|
|
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Get some text, any text, for the clicked-on link.
|
|
* @return the link text, title, alt, href, or "" if everything fails
|
|
*/
|
|
linkText : function CM_linkText() {
|
|
var text = gatherTextUnder(this.link);
|
|
if (!text || (text.trim() == "")) {
|
|
text = this.link.getAttribute("title");
|
|
if (!text || (text.trim() == "")) {
|
|
text = this.link.getAttribute("alt");
|
|
if (!text || (text.trim() == "")) {
|
|
if (this.link.href) {
|
|
text = this.link.href;
|
|
} else {
|
|
text = getAttributeNS("http://www.w3.org/1999/xlink", "href");
|
|
if (text && !(text.trim() == "")) {
|
|
text = this.makeURLAbsolute(this.link.baseURI, text);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return text;
|
|
},
|
|
|
|
/**
|
|
* Determines whether the focused window has something selected.
|
|
* @return true if there is a selection, false if not
|
|
*/
|
|
isContentSelection : function CM_isContentSelection() {
|
|
return !document.commandDispatcher.focusedWindow.getSelection().isCollapsed;
|
|
},
|
|
|
|
/**
|
|
* Determines whether the context menu was triggered by a node that's a child
|
|
* of the threadpane by looking for a parent node with id="threadTree".
|
|
* @return true if the popupNode is a child of the threadpane, otherwise false
|
|
*/
|
|
popupNodeIsInThreadPane: function CM_popupNodeIsInThreadPane(aNode) {
|
|
var node = aNode;
|
|
while (node)
|
|
{
|
|
if (node.id == "threadTree")
|
|
return true;
|
|
|
|
node = node.parentNode;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* 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
|
|
* @return The string absolute URL
|
|
*/
|
|
makeURLAbsolute : function CM_makeURLAbsolute(aBase, aUrl) {
|
|
// Construct nsIURL.
|
|
var baseURI = Services.io.newURI(aBase, null, null);
|
|
|
|
return Services.io.newURI(baseURI.resolve(aUrl), null, null).spec;
|
|
},
|
|
|
|
/**
|
|
* Determine whether a DOM node is a text or password input, or a textarea.
|
|
* @param aNode
|
|
* The DOM node to check
|
|
* @return true for textboxes, false for other elements
|
|
*/
|
|
isTargetATextBox : function CM_isTargetATextBox(aNode) {
|
|
if (aNode instanceof HTMLInputElement)
|
|
return (aNode.type == "text" || aNode.type == "password");
|
|
|
|
return (aNode instanceof HTMLTextAreaElement);
|
|
},
|
|
|
|
/**
|
|
* Hide a separator based on whether there are any non-hidden items between
|
|
* it and the previous separator.
|
|
*
|
|
* @param aSeparatorID The id of the separator element.
|
|
*/
|
|
hideIfAppropriate: function CM_hideIfAppropriate(aSeparatorID) {
|
|
this.showItem(aSeparatorID, this.shouldShowSeparator(aSeparatorID));
|
|
},
|
|
|
|
/**
|
|
* Determine whether a separator should be shown based on whether
|
|
* there are any non-hidden items between it and the previous separator.
|
|
* @param aSeparatorID
|
|
* The id of the separator element
|
|
* @return true if the separator should be shown, false if not
|
|
*/
|
|
shouldShowSeparator : function CM_shouldShowSeparator(aSeparatorID) {
|
|
var separator = document.getElementById(aSeparatorID);
|
|
if (separator) {
|
|
var sibling = separator.previousSibling;
|
|
while (sibling && sibling.localName != "menuseparator") {
|
|
if (sibling.getAttribute("hidden") != "true")
|
|
return true;
|
|
sibling = sibling.previousSibling;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Ensures that there isn't a separator shown at the bottom of the menu.
|
|
*
|
|
* @param aPopup The menu to check.
|
|
*/
|
|
checkLastSeparator: function CM_checkLastSeparator(aPopup) {
|
|
let sibling = aPopup.lastChild;
|
|
while (sibling) {
|
|
if (sibling.getAttribute("hidden") != "true") {
|
|
if (sibling.localName == "menuseparator") {
|
|
// If we got here then the item is a menuseparator and everything
|
|
// below it hidden.
|
|
sibling.setAttribute("hidden", true);
|
|
return;
|
|
}
|
|
else
|
|
return;
|
|
}
|
|
sibling = sibling.previousSibling;
|
|
}
|
|
},
|
|
|
|
openInBrowser: function CM_openInBrowser() {
|
|
let uri = Services.io.newURI(this.target.ownerDocument.defaultView.
|
|
top.location.href, null, null);
|
|
PlacesUtils.asyncHistory.updatePlaces({
|
|
uri: uri,
|
|
visits: [{
|
|
visitDate: Date.now() * 1000,
|
|
transitionType: Components.interfaces.nsINavHistoryService.TRANSITION_LINK
|
|
}]
|
|
});
|
|
Components.classes["@mozilla.org/uriloader/external-protocol-service;1"]
|
|
.getService(Components.interfaces.nsIExternalProtocolService)
|
|
.loadURI(uri);
|
|
},
|
|
|
|
openLinkInBrowser: function CM_openLinkInBrowser() {
|
|
PlacesUtils.asyncHistory.updatePlaces({
|
|
uri: this.linkURI,
|
|
visits: [{
|
|
visitDate: Date.now() * 1000,
|
|
transitionType: Components.interfaces.nsINavHistoryService.TRANSITION_LINK
|
|
}]
|
|
});
|
|
Components.classes["@mozilla.org/uriloader/external-protocol-service;1"]
|
|
.getService(Components.interfaces.nsIExternalProtocolService)
|
|
.loadURI(this.linkURI);
|
|
},
|
|
|
|
mediaCommand : function CM_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.
|
|
}
|
|
}
|
|
};
|