mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
45fe6d3ae2
This patch does the following: * It adds nsITransferable::Init(nsILoadContext*). The load context might be null, which means that the transferable is non-private, but if it's non-null, we extract the boolean value for the privacy mode and store it in the transferable. * It adds checks in debug builds to make sure that Init is always called, in form of fatal assertions. * It adds nsIDOMDocument* agruments to nsIClipboardHelper methods which represent the document that the string is coming from. nsIClipboardHelper implementation internally gets the nsILoadContext from that and passes it on to the transferable upon creation. The reason that I did this was that nsIClipboardHelper is supposed to be a high-level helper, and in most of its call sites, we have easy access to a document object. * It modifies all of the call sites of the above interfaces according to this change. * It adds a GetLoadContext helper to nsIDocument to help with changing the call sites.
1465 lines
55 KiB
JavaScript
1465 lines
55 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/.
|
|
|
|
function nsContextMenu(aXulMenu, aBrowser, aIsShift) {
|
|
this.shouldDisplay = true;
|
|
this.initMenu(aBrowser, aXulMenu, aIsShift);
|
|
}
|
|
|
|
// Prototype for nsContextMenu "class."
|
|
nsContextMenu.prototype = {
|
|
initMenu: function CM_initMenu(aBrowser, aXulMenu, aIsShift) {
|
|
// Get contextual info.
|
|
this.setTarget(document.popupNode, document.popupRangeParent,
|
|
document.popupRangeOffset);
|
|
if (!this.shouldDisplay)
|
|
return;
|
|
|
|
this.browser = aBrowser;
|
|
|
|
this.hasPageMenu = false;
|
|
if (!aIsShift) {
|
|
this.hasPageMenu = PageMenu.maybeBuildAndAttachMenu(this.target,
|
|
aXulMenu);
|
|
}
|
|
|
|
this.isFrameImage = document.getElementById("isFrameImage");
|
|
this.ellipsis = "\u2026";
|
|
try {
|
|
this.ellipsis = gPrefService.getComplexValue("intl.ellipsis",
|
|
Ci.nsIPrefLocalizedString).data;
|
|
} catch (e) { }
|
|
this.isTextSelected = this.isTextSelection();
|
|
this.isContentSelected = this.isContentSelection();
|
|
this.onPlainTextLink = false;
|
|
|
|
// Initialize (disable/remove) menu items.
|
|
this.initItems();
|
|
},
|
|
|
|
hiding: function CM_hiding() {
|
|
InlineSpellCheckerUI.clearSuggestionsFromMenu();
|
|
InlineSpellCheckerUI.clearDictionaryListFromMenu();
|
|
InlineSpellCheckerUI.uninit();
|
|
},
|
|
|
|
initItems: function CM_initItems() {
|
|
this.initPageMenuSeparator();
|
|
this.initOpenItems();
|
|
this.initNavigationItems();
|
|
this.initViewItems();
|
|
this.initMiscItems();
|
|
this.initSpellingItems();
|
|
this.initSaveItems();
|
|
this.initClipboardItems();
|
|
this.initMediaPlayerItems();
|
|
this.initLeaveDOMFullScreenItems();
|
|
},
|
|
|
|
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));
|
|
}
|
|
|
|
// Time to do some bad things and see if we've highlighted a URL that
|
|
// isn't actually linked.
|
|
if (this.isTextSelected && !this.onLink) {
|
|
// Ok, we have some text, let's figure out if it looks like a URL.
|
|
let selection = document.commandDispatcher.focusedWindow
|
|
.getSelection();
|
|
let linkText = selection.toString().trim();
|
|
let uri;
|
|
if (/^(?:https?|ftp):/i.test(linkText)) {
|
|
try {
|
|
uri = makeURI(linkText);
|
|
} catch (ex) {}
|
|
}
|
|
// Check if this could be a valid url, just missing the protocol.
|
|
else if (/^(?:[a-z\d-]+\.)+[a-z]+$/i.test(linkText)) {
|
|
// Now let's see if this is an intentional link selection. Our guess is
|
|
// based on whether the selection begins/ends with whitespace or is
|
|
// preceded/followed by a non-word character.
|
|
|
|
// selection.toString() trims trailing whitespace, so we look for
|
|
// that explicitly in the first and last ranges.
|
|
let beginRange = selection.getRangeAt(0);
|
|
let delimitedAtStart = /^\s/.test(beginRange);
|
|
if (!delimitedAtStart) {
|
|
let container = beginRange.startContainer;
|
|
let offset = beginRange.startOffset;
|
|
if (container.nodeType == Node.TEXT_NODE && offset > 0)
|
|
delimitedAtStart = /\W/.test(container.textContent[offset - 1]);
|
|
else
|
|
delimitedAtStart = true;
|
|
}
|
|
|
|
let delimitedAtEnd = false;
|
|
if (delimitedAtStart) {
|
|
let endRange = selection.getRangeAt(selection.rangeCount - 1);
|
|
delimitedAtEnd = /\s$/.test(endRange);
|
|
if (!delimitedAtEnd) {
|
|
let container = endRange.endContainer;
|
|
let offset = endRange.endOffset;
|
|
if (container.nodeType == Node.TEXT_NODE &&
|
|
offset < container.textContent.length)
|
|
delimitedAtEnd = /\W/.test(container.textContent[offset]);
|
|
else
|
|
delimitedAtEnd = true;
|
|
}
|
|
}
|
|
|
|
if (delimitedAtStart && delimitedAtEnd) {
|
|
let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"]
|
|
.getService(Ci.nsIURIFixup);
|
|
try {
|
|
uri = uriFixup.createFixupURI(linkText, uriFixup.FIXUP_FLAG_NONE);
|
|
} catch (ex) {}
|
|
}
|
|
}
|
|
|
|
if (uri && uri.host) {
|
|
this.linkURI = uri;
|
|
this.linkURL = this.linkURI.spec;
|
|
this.onPlainTextLink = true;
|
|
}
|
|
}
|
|
|
|
var shouldShow = this.onSaveableLink || isMailtoInternal || this.onPlainTextLink;
|
|
this.showItem("context-openlink", shouldShow);
|
|
this.showItem("context-openlinkintab", shouldShow);
|
|
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.showItem("context-back", shouldShow);
|
|
this.showItem("context-forward", shouldShow);
|
|
this.showItem("context-reload", shouldShow);
|
|
this.showItem("context-stop", shouldShow);
|
|
this.showItem("context-sep-stop", shouldShow);
|
|
|
|
// 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.mozFullScreenElement != 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);
|
|
this.showItem("context-sendpage", shouldShow);
|
|
|
|
// Save+Send link depends on whether we're in a link, or selected text matches valid URL pattern.
|
|
this.showItem("context-savelink", this.onSaveableLink || this.onPlainTextLink);
|
|
this.showItem("context-sendlink", this.onSaveableLink || this.onPlainTextLink);
|
|
|
|
// Save image depends on having loaded its content, video and audio don't.
|
|
this.showItem("context-saveimage", this.onLoadedImage || this.onCanvas);
|
|
this.showItem("context-savevideo", this.onVideo);
|
|
this.showItem("context-saveaudio", this.onAudio);
|
|
this.showItem("context-video-saveimage", this.onVideo);
|
|
this.setItemAttr("context-savevideo", "disabled", !this.mediaURL);
|
|
this.setItemAttr("context-saveaudio", "disabled", !this.mediaURL);
|
|
// Send media URL (but not for canvas, since it's a big data: URL)
|
|
this.showItem("context-sendimage", this.onImage);
|
|
this.showItem("context-sendvideo", this.onVideo);
|
|
this.showItem("context-sendaudio", this.onAudio);
|
|
this.setItemAttr("context-sendvideo", "disabled", !this.mediaURL);
|
|
this.setItemAttr("context-sendaudio", "disabled", !this.mediaURL);
|
|
},
|
|
|
|
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 = 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 = this.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);
|
|
},
|
|
|
|
initMiscItems: function CM_initMiscItems() {
|
|
var isTextSelected = this.isTextSelected;
|
|
|
|
// Use "Bookmark This Link" if on a link.
|
|
this.showItem("context-bookmarkpage",
|
|
!(this.isContentSelected || this.onTextInput || this.onLink ||
|
|
this.onImage || this.onVideo || this.onAudio));
|
|
this.showItem("context-bookmarklink", (this.onLink && !this.onMailtoLink) || this.onPlainTextLink);
|
|
this.showItem("context-searchselect", isTextSelected);
|
|
this.showItem("context-keywordfield",
|
|
this.onTextInput && this.onKeywordField);
|
|
this.showItem("frame", this.inFrame);
|
|
this.showItem("frame-sep", this.inFrame && isTextSelected);
|
|
|
|
// Hide menu entries for images, show otherwise
|
|
if (this.inFrame) {
|
|
if (mimeTypeIsTextBased(this.target.ownerDocument.contentType))
|
|
this.isFrameImage.removeAttribute('hidden');
|
|
else
|
|
this.isFrameImage.setAttribute('hidden', 'true');
|
|
}
|
|
|
|
// BiDi UI
|
|
this.showItem("context-sep-bidi", top.gBidiUI);
|
|
this.showItem("context-bidi-text-direction-toggle",
|
|
this.onTextInput && top.gBidiUI);
|
|
this.showItem("context-bidi-page-direction-toggle",
|
|
!this.onTextInput && top.gBidiUI);
|
|
},
|
|
|
|
initSpellingItems: function() {
|
|
var canSpell = InlineSpellCheckerUI.canSpellCheck;
|
|
var onMisspelling = InlineSpellCheckerUI.overMisspelling;
|
|
var showUndo = canSpell && InlineSpellCheckerUI.canUndo();
|
|
this.showItem("spell-check-enabled", canSpell);
|
|
this.showItem("spell-separator", canSpell || this.onEditableArea);
|
|
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", InlineSpellCheckerUI.enabled);
|
|
if (canSpell) {
|
|
var dictMenu = document.getElementById("spell-dictionaries-menu");
|
|
var dictSep = document.getElementById("spell-language-separator");
|
|
InlineSpellCheckerUI.addDictionaryListToMenu(dictMenu, dictSep);
|
|
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-add-dictionaries-main", true);
|
|
}
|
|
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-showcontrols", onMedia && !this.target.controls);
|
|
this.showItem("context-media-hidecontrols", onMedia && this.target.controls);
|
|
this.showItem("context-video-fullscreen", this.onVideo && this.target.ownerDocument.mozFullScreenElement == null);
|
|
var statsShowing = this.onVideo && this.target.wrappedJSObject.mozMediaStatisticsShowing;
|
|
this.showItem("context-video-showstats", this.onVideo && this.target.controls && !statsShowing);
|
|
this.showItem("context-video-hidestats", this.onVideo && this.target.controls && statsShowing);
|
|
|
|
// Disable them when there isn't a valid media source loaded.
|
|
if (onMedia) {
|
|
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-showcontrols", "disabled", hasError);
|
|
this.setItemAttr("context-media-hidecontrols", "disabled", hasError);
|
|
if (this.onVideo) {
|
|
let canSaveSnapshot = this.target.readyState >= this.target.HAVE_CURRENT_DATA;
|
|
this.setItemAttr("context-video-saveimage", "disabled", !canSaveSnapshot);
|
|
this.setItemAttr("context-video-fullscreen", "disabled", hasError);
|
|
this.setItemAttr("context-video-showstats", "disabled", hasError);
|
|
this.setItemAttr("context-video-hidestats", "disabled", hasError);
|
|
}
|
|
}
|
|
this.showItem("context-media-sep-commands", onMedia);
|
|
},
|
|
|
|
inspectNode: function CM_inspectNode() {
|
|
if (InspectorUI.isTreePanelOpen) {
|
|
InspectorUI.inspectNode(this.target);
|
|
InspectorUI.stopInspecting();
|
|
} else {
|
|
InspectorUI.openInspectorUI(this.target);
|
|
}
|
|
},
|
|
|
|
// Set various context menu attributes based on the state of the world.
|
|
setTarget: function (aNode, aRangeParent, aRangeOffset) {
|
|
const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
if (aNode.namespaceURI == xulNS ||
|
|
aNode.nodeType == Node.DOCUMENT_NODE ||
|
|
this.isTargetAFormControl(aNode)) {
|
|
this.shouldDisplay = false;
|
|
return;
|
|
}
|
|
|
|
// Initialize contextual info.
|
|
this.onImage = false;
|
|
this.onLoadedImage = false;
|
|
this.onCompletedImage = false;
|
|
this.onCanvas = false;
|
|
this.onVideo = false;
|
|
this.onAudio = false;
|
|
this.onTextInput = false;
|
|
this.onKeywordField = false;
|
|
this.mediaURL = "";
|
|
this.onLink = false;
|
|
this.onMailtoLink = false;
|
|
this.onSaveableLink = false;
|
|
this.link = null;
|
|
this.linkURL = "";
|
|
this.linkURI = null;
|
|
this.linkProtocol = "";
|
|
this.onMathML = false;
|
|
this.inFrame = false;
|
|
this.inSyntheticDoc = false;
|
|
this.hasBGImage = false;
|
|
this.bgImageURL = "";
|
|
this.onEditableArea = false;
|
|
this.isDesignMode = false;
|
|
|
|
// Remember the node that was clicked.
|
|
this.target = aNode;
|
|
|
|
// Check if we are in a synthetic document (stand alone image, video, etc.).
|
|
this.inSyntheticDoc = this.target.ownerDocument.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.
|
|
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))
|
|
this.onCompletedImage = true;
|
|
|
|
this.mediaURL = this.target.currentURI.spec;
|
|
}
|
|
else if (this.target instanceof HTMLCanvasElement) {
|
|
this.onCanvas = true;
|
|
}
|
|
else if (this.target instanceof HTMLVideoElement) {
|
|
this.mediaURL = this.target.currentSrc || this.target.src;
|
|
// 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;
|
|
this.mediaURL = this.target.currentSrc || this.target.src;
|
|
}
|
|
else if (this.target instanceof HTMLInputElement ) {
|
|
this.onTextInput = this.isTargetATextBox(this.target);
|
|
// Allow spellchecking UI on all text and search inputs.
|
|
if (this.onTextInput && ! this.target.readOnly &&
|
|
(this.target.type == "text" || this.target.type == "search")) {
|
|
this.onEditableArea = true;
|
|
InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
|
|
InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
|
|
}
|
|
this.onKeywordField = this.isTargetAKeywordField(this.target);
|
|
}
|
|
else if (this.target instanceof HTMLTextAreaElement) {
|
|
this.onTextInput = true;
|
|
if (!this.target.readOnly) {
|
|
this.onEditableArea = true;
|
|
InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
|
|
InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
|
|
}
|
|
}
|
|
else if (this.target instanceof HTMLHtmlElement) {
|
|
var bodyElt = this.target.ownerDocument.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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
((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.linkProtocol = this.getLinkProtocol();
|
|
this.onMailtoLink = (this.linkProtocol == "mailto");
|
|
this.onSaveableLink = this.isLinkSaveable( this.link );
|
|
}
|
|
|
|
// 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 = this.target.ownerDocument.defaultView;
|
|
if (docDefaultView != docDefaultView.top)
|
|
this.inFrame = true;
|
|
|
|
// if the document is editable, show context menu like in text inputs
|
|
if (!this.onEditableArea) {
|
|
var win = this.target.ownerDocument.defaultView;
|
|
if (win) {
|
|
var isEditable = false;
|
|
try {
|
|
var editingSession = win.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIWebNavigation)
|
|
.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIEditingSession);
|
|
if (editingSession.windowIsEditable(win) &&
|
|
this.getComputedStyle(this.target, "-moz-user-modify") == "read-write") {
|
|
isEditable = true;
|
|
}
|
|
}
|
|
catch(ex) {
|
|
// If someone built with composer disabled, we can't get an editing session.
|
|
}
|
|
|
|
if (isEditable) {
|
|
this.onTextInput = true;
|
|
this.onKeywordField = false;
|
|
this.onImage = false;
|
|
this.onLoadedImage = false;
|
|
this.onCompletedImage = false;
|
|
this.onMathML = false;
|
|
this.inFrame = false;
|
|
this.hasBGImage = false;
|
|
this.isDesignMode = true;
|
|
this.onEditableArea = true;
|
|
InlineSpellCheckerUI.init(editingSession.getEditorForWindow(win));
|
|
var canSpell = InlineSpellCheckerUI.canSpellCheck;
|
|
InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
|
|
this.showItem("spell-check-enabled", canSpell);
|
|
this.showItem("spell-separator", canSpell);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// 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" );
|
|
},
|
|
|
|
// Open linked-to URL in a new window.
|
|
openLink : function () {
|
|
var doc = this.target.ownerDocument;
|
|
urlSecurityCheck(this.linkURL, doc.nodePrincipal);
|
|
openLinkIn(this.linkURL, "window",
|
|
{ charset: doc.characterSet,
|
|
referrerURI: doc.documentURIObject });
|
|
},
|
|
|
|
// Open linked-to URL in a new tab.
|
|
openLinkInTab: function() {
|
|
var doc = this.target.ownerDocument;
|
|
urlSecurityCheck(this.linkURL, doc.nodePrincipal);
|
|
openLinkIn(this.linkURL, "tab",
|
|
{ charset: doc.characterSet,
|
|
referrerURI: doc.documentURIObject });
|
|
},
|
|
|
|
// open URL in current tab
|
|
openLinkInCurrent: function() {
|
|
var doc = this.target.ownerDocument;
|
|
urlSecurityCheck(this.linkURL, doc.nodePrincipal);
|
|
openLinkIn(this.linkURL, "current",
|
|
{ charset: doc.characterSet,
|
|
referrerURI: doc.documentURIObject });
|
|
},
|
|
|
|
// Open frame in a new tab.
|
|
openFrameInTab: function() {
|
|
var doc = this.target.ownerDocument;
|
|
var frameURL = doc.location.href;
|
|
var referrer = doc.referrer;
|
|
openLinkIn(frameURL, "tab",
|
|
{ charset: doc.characterSet,
|
|
referrerURI: referrer ? makeURI(referrer) : null });
|
|
},
|
|
|
|
// Reload clicked-in frame.
|
|
reloadFrame: function() {
|
|
this.target.ownerDocument.location.reload();
|
|
},
|
|
|
|
// Open clicked-in frame in its own window.
|
|
openFrame: function() {
|
|
var doc = this.target.ownerDocument;
|
|
var frameURL = doc.location.href;
|
|
var referrer = doc.referrer;
|
|
openLinkIn(frameURL, "window",
|
|
{ charset: doc.characterSet,
|
|
referrerURI: referrer ? makeURI(referrer) : null });
|
|
},
|
|
|
|
// Open clicked-in frame in the same window.
|
|
showOnlyThisFrame: function() {
|
|
var doc = this.target.ownerDocument;
|
|
var frameURL = doc.location.href;
|
|
|
|
urlSecurityCheck(frameURL, this.browser.contentPrincipal,
|
|
Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
|
|
var referrer = doc.referrer;
|
|
openUILinkIn(frameURL, "current", { disallowInheritPrincipal: true,
|
|
referrerURI: referrer ? makeURI(referrer) : null });
|
|
},
|
|
|
|
// View Partial Source
|
|
viewPartialSource: function(aContext) {
|
|
var focusedWindow = document.commandDispatcher.focusedWindow;
|
|
if (focusedWindow == window)
|
|
focusedWindow = content;
|
|
|
|
var docCharset = null;
|
|
if (focusedWindow)
|
|
docCharset = "charset=" + focusedWindow.document.characterSet;
|
|
|
|
// "View Selection Source" and others such as "View MathML Source"
|
|
// are mutually exclusive, with the precedence given to the selection
|
|
// when there is one
|
|
var reference = null;
|
|
if (aContext == "selection")
|
|
reference = focusedWindow.getSelection();
|
|
else if (aContext == "mathml")
|
|
reference = this.target;
|
|
else
|
|
throw "not reached";
|
|
|
|
// unused (and play nice for fragments generated via XSLT too)
|
|
var docUrl = null;
|
|
window.openDialog("chrome://global/content/viewPartialSource.xul",
|
|
"_blank", "scrollbars,resizable,chrome,dialog=no",
|
|
docUrl, docCharset, reference, aContext);
|
|
},
|
|
|
|
// Open new "view source" window with the frame's URL.
|
|
viewFrameSource: function() {
|
|
BrowserViewSourceOfDocument(this.target.ownerDocument);
|
|
},
|
|
|
|
viewInfo: function() {
|
|
BrowserPageInfo(this.target.ownerDocument.defaultView.top.document);
|
|
},
|
|
|
|
viewImageInfo: function() {
|
|
BrowserPageInfo(this.target.ownerDocument.defaultView.top.document,
|
|
"mediaTab", this.target);
|
|
},
|
|
|
|
viewFrameInfo: function() {
|
|
BrowserPageInfo(this.target.ownerDocument);
|
|
},
|
|
|
|
reloadImage: function(e) {
|
|
urlSecurityCheck(this.mediaURL,
|
|
this.browser.contentPrincipal,
|
|
Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
|
|
|
|
if (this.target instanceof Ci.nsIImageLoadingContent)
|
|
this.target.forceReload();
|
|
},
|
|
|
|
// Change current window to the URL of the image, video, or audio.
|
|
viewMedia: function(e) {
|
|
var viewURL;
|
|
|
|
if (this.onCanvas)
|
|
viewURL = this.target.toDataURL();
|
|
else {
|
|
viewURL = this.mediaURL;
|
|
urlSecurityCheck(viewURL,
|
|
this.browser.contentPrincipal,
|
|
Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
|
|
}
|
|
|
|
var doc = this.target.ownerDocument;
|
|
openUILink(viewURL, e, { disallowInheritPrincipal: true,
|
|
referrerURI: doc.documentURIObject });
|
|
},
|
|
|
|
saveVideoFrameAsImage: function () {
|
|
urlSecurityCheck(this.mediaURL, this.browser.contentPrincipal,
|
|
Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
|
|
let name = "";
|
|
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";
|
|
var video = this.target;
|
|
var canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
|
|
canvas.width = video.videoWidth;
|
|
canvas.height = video.videoHeight;
|
|
var ctxDraw = canvas.getContext("2d");
|
|
ctxDraw.drawImage(video, 0, 0);
|
|
saveImageURL(canvas.toDataURL("image/jpeg", ""), name, "SaveImageTitle", true, false, document.documentURIObject);
|
|
},
|
|
|
|
fullScreenVideo: function () {
|
|
let video = this.target;
|
|
if (document.mozFullScreenEnabled)
|
|
video.mozRequestFullScreen();
|
|
},
|
|
|
|
leaveDOMFullScreen: function() {
|
|
document.mozCancelFullScreen();
|
|
},
|
|
|
|
// Change current window to the URL of the background image.
|
|
viewBGImage: function(e) {
|
|
urlSecurityCheck(this.bgImageURL,
|
|
this.browser.contentPrincipal,
|
|
Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
|
|
var doc = this.target.ownerDocument;
|
|
openUILink(this.bgImageURL, e, { disallowInheritPrincipal: true,
|
|
referrerURI: doc.documentURIObject });
|
|
},
|
|
|
|
disableSetDesktopBackground: function() {
|
|
// Disable the Set as Desktop Background menu item if we're still trying
|
|
// to load the image or the load failed.
|
|
if (!(this.target instanceof Ci.nsIImageLoadingContent))
|
|
return true;
|
|
|
|
if (("complete" in this.target) && !this.target.complete)
|
|
return true;
|
|
|
|
if (this.target.currentURI.schemeIs("javascript"))
|
|
return true;
|
|
|
|
var request = this.target
|
|
.QueryInterface(Ci.nsIImageLoadingContent)
|
|
.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
|
|
if (!request)
|
|
return true;
|
|
|
|
return false;
|
|
},
|
|
|
|
setDesktopBackground: function() {
|
|
// Paranoia: check disableSetDesktopBackground again, in case the
|
|
// image changed since the context menu was initiated.
|
|
if (this.disableSetDesktopBackground())
|
|
return;
|
|
|
|
urlSecurityCheck(this.target.currentURI.spec,
|
|
this.target.ownerDocument.nodePrincipal);
|
|
|
|
// 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.
|
|
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
|
.getService(Components.interfaces.nsIWindowMediator);
|
|
var dbWin = wm.getMostRecentWindow("Shell:SetDesktopBackground");
|
|
if (dbWin) {
|
|
dbWin.gSetBackground.init(this.target);
|
|
dbWin.focus();
|
|
}
|
|
else {
|
|
openDialog(kDesktopBackgroundURL, "",
|
|
"centerscreen,chrome,dialog=no,dependent,resizable=no",
|
|
this.target);
|
|
}
|
|
#else
|
|
// On non-Mac platforms, the Set Wallpaper dialog is modal.
|
|
openDialog(kDesktopBackgroundURL, "",
|
|
"centerscreen,chrome,dialog,modal,dependent",
|
|
this.target);
|
|
#endif
|
|
},
|
|
|
|
// Save URL of clicked-on frame.
|
|
saveFrame: function () {
|
|
saveDocument(this.target.ownerDocument);
|
|
},
|
|
|
|
// 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) {
|
|
// 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);
|
|
promptSvc.alert(doc.defaultView, title, msg);
|
|
} catch (ex) {}
|
|
return;
|
|
}
|
|
|
|
var extHelperAppSvc =
|
|
Cc["@mozilla.org/uriloader/external-helper-app-service;1"].
|
|
getService(Ci.nsIExternalHelperAppService);
|
|
var channel = aRequest.QueryInterface(Ci.nsIChannel);
|
|
this.extListener =
|
|
extHelperAppSvc.doContent(channel.contentType, aRequest,
|
|
doc.defaultView, true);
|
|
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, doc.documentURIObject);
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
// set up a channel to do the saving
|
|
var ioService = Cc["@mozilla.org/network/io-service;1"].
|
|
getService(Ci.nsIIOService);
|
|
var channel = ioService.newChannelFromURI(makeURI(linkURL));
|
|
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 = doc.documentURIObject;
|
|
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.asyncOpen(new saveAsListener(), null);
|
|
},
|
|
|
|
// Save URL of clicked-on link.
|
|
saveLink: function() {
|
|
var doc = this.target.ownerDocument;
|
|
var linkText;
|
|
// If selected text is found to match valid URL pattern.
|
|
if (this.onPlainTextLink)
|
|
linkText = document.commandDispatcher.focusedWindow.getSelection().toString().trim();
|
|
else
|
|
linkText = this.linkText();
|
|
urlSecurityCheck(this.linkURL, doc.nodePrincipal);
|
|
|
|
this.saveHelper(this.linkURL, linkText, null, true, doc);
|
|
},
|
|
|
|
sendLink: function() {
|
|
// we don't know the title of the link so pass in an empty string
|
|
MailIntegration.sendMessage( this.linkURL, "" );
|
|
},
|
|
|
|
// Backwards-compatibility wrapper
|
|
saveImage : function() {
|
|
if (this.onCanvas || this.onImage)
|
|
this.saveMedia();
|
|
},
|
|
|
|
// Save URL of the clicked upon image, video, or audio.
|
|
saveMedia: function() {
|
|
var doc = this.target.ownerDocument;
|
|
if (this.onCanvas) {
|
|
// Bypass cache, since it's a data: URL.
|
|
saveImageURL(this.target.toDataURL(), "canvas.png", "SaveImageTitle",
|
|
true, false, doc.documentURIObject);
|
|
}
|
|
else if (this.onImage) {
|
|
urlSecurityCheck(this.mediaURL, doc.nodePrincipal);
|
|
saveImageURL(this.mediaURL, null, "SaveImageTitle", false,
|
|
false, doc.documentURIObject);
|
|
}
|
|
else if (this.onVideo || this.onAudio) {
|
|
urlSecurityCheck(this.mediaURL, doc.nodePrincipal);
|
|
var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
|
|
this.saveHelper(this.mediaURL, null, dialogTitle, false, doc);
|
|
}
|
|
},
|
|
|
|
// Backwards-compatibility wrapper
|
|
sendImage : function() {
|
|
if (this.onCanvas || this.onImage)
|
|
this.sendMedia();
|
|
},
|
|
|
|
sendMedia: function() {
|
|
MailIntegration.sendMessage(this.mediaURL, "");
|
|
},
|
|
|
|
// 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 {
|
|
var characterSet = this.target.ownerDocument.characterSet;
|
|
const textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"].
|
|
getService(Ci.nsITextToSubURI);
|
|
addresses = textToSubURI.unEscapeURIForUI(characterSet, addresses);
|
|
}
|
|
catch(ex) {
|
|
// Do nothing.
|
|
}
|
|
|
|
var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
|
|
getService(Ci.nsIClipboardHelper);
|
|
clipboard.copyString(addresses, document);
|
|
},
|
|
|
|
///////////////
|
|
// 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.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.
|
|
linkText: 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;
|
|
},
|
|
|
|
// Get selected text. Only display the first 15 chars.
|
|
isTextSelection: function() {
|
|
// Get 16 characters, so that we can trim the selection if it's greater
|
|
// than 15 chars
|
|
var selectedText = getBrowserSelection(16);
|
|
|
|
if (!selectedText)
|
|
return false;
|
|
|
|
if (selectedText.length > 15)
|
|
selectedText = selectedText.substr(0,15) + this.ellipsis;
|
|
|
|
// Use the current engine if the search bar is visible, the default
|
|
// engine otherwise.
|
|
var engineName = "";
|
|
var ss = Cc["@mozilla.org/browser/search-service;1"].
|
|
getService(Ci.nsIBrowserSearchService);
|
|
if (isElementVisible(BrowserSearch.searchBar))
|
|
engineName = ss.currentEngine.name;
|
|
else
|
|
engineName = ss.defaultEngine.name;
|
|
|
|
// format "Search <engine> for <selection>" string to show in menu
|
|
var menuLabel = gNavigatorBundle.getFormattedString("contextMenuSearchText",
|
|
[engineName,
|
|
selectedText]);
|
|
document.getElementById("context-searchselect").label = menuLabel;
|
|
document.getElementById("context-searchselect").accessKey =
|
|
gNavigatorBundle.getString("contextMenuSearchText.accesskey");
|
|
|
|
return true;
|
|
},
|
|
|
|
// Returns true if anything is selected.
|
|
isContentSelection: function() {
|
|
return !document.commandDispatcher.focusedWindow.getSelection().isCollapsed;
|
|
},
|
|
|
|
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";
|
|
},
|
|
|
|
// Returns true if aNode is a from control (except text boxes and images).
|
|
// This is used to disable the context menu for form controls.
|
|
isTargetAFormControl: function(aNode) {
|
|
if (aNode instanceof HTMLInputElement)
|
|
return (!aNode.mozIsTextField(false) && aNode.type != "image");
|
|
|
|
return (aNode instanceof HTMLButtonElement) ||
|
|
(aNode instanceof HTMLSelectElement) ||
|
|
(aNode instanceof HTMLOptionElement) ||
|
|
(aNode instanceof HTMLOptGroupElement);
|
|
},
|
|
|
|
isTargetATextBox: function(node) {
|
|
if (node instanceof HTMLInputElement)
|
|
return node.mozIsTextField(false);
|
|
|
|
return (node instanceof HTMLTextAreaElement);
|
|
},
|
|
|
|
isTargetAKeywordField: function(aNode) {
|
|
if (!(aNode instanceof HTMLInputElement))
|
|
return false;
|
|
|
|
var form = aNode.form;
|
|
if (!form || aNode.type == "password")
|
|
return false;
|
|
|
|
var method = form.method.toUpperCase();
|
|
|
|
// These are the following types of forms we can create keywords for:
|
|
//
|
|
// method encoding type can create keyword
|
|
// GET * YES
|
|
// * YES
|
|
// POST YES
|
|
// POST application/x-www-form-urlencoded YES
|
|
// POST text/plain NO (a little tricky to do)
|
|
// POST multipart/form-data NO
|
|
// POST everything else YES
|
|
return (method == "GET" || method == "") ||
|
|
(form.enctype != "text/plain") && (form.enctype != "multipart/form-data");
|
|
},
|
|
|
|
// 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() {
|
|
var linkText;
|
|
// If selected text is found to match valid URL pattern.
|
|
if (this.onPlainTextLink)
|
|
linkText = document.commandDispatcher.focusedWindow.getSelection().toString().trim();
|
|
else
|
|
linkText = this.linkText();
|
|
window.top.PlacesCommandHook.bookmarkLink(PlacesUtils.bookmarksMenuFolderId, this.linkURL,
|
|
linkText);
|
|
},
|
|
|
|
addBookmarkForFrame: function CM_addBookmarkForFrame() {
|
|
var doc = this.target.ownerDocument;
|
|
var uri = doc.documentURIObject;
|
|
|
|
var itemId = PlacesUtils.getMostRecentBookmarkForURI(uri);
|
|
if (itemId == -1) {
|
|
var title = doc.title;
|
|
var description = PlacesUIUtils.getDescriptionFromDocument(doc);
|
|
PlacesUIUtils.showBookmarkDialog({ action: "add"
|
|
, type: "bookmark"
|
|
, uri: uri
|
|
, title: title
|
|
, description: description
|
|
, hiddenRows: [ "description"
|
|
, "location"
|
|
, "loadInSidebar"
|
|
, "keyword" ]
|
|
}, window.top);
|
|
}
|
|
else {
|
|
PlacesUIUtils.showBookmarkDialog({ action: "edit"
|
|
, type: "bookmark"
|
|
, itemId: itemId
|
|
}, window.top);
|
|
}
|
|
},
|
|
|
|
savePageAs: function CM_savePageAs() {
|
|
saveDocument(this.browser.contentDocument);
|
|
},
|
|
|
|
sendPage: function CM_sendPage() {
|
|
MailIntegration.sendLinkForWindow(this.browser.contentWindow);
|
|
},
|
|
|
|
printFrame: function CM_printFrame() {
|
|
PrintUtils.print(this.target.ownerDocument.defaultView);
|
|
},
|
|
|
|
switchPageDirection: function CM_switchPageDirection() {
|
|
SwitchDocumentDirection(this.browser.contentWindow);
|
|
},
|
|
|
|
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;
|
|
case "hidecontrols":
|
|
media.removeAttribute("controls");
|
|
break;
|
|
case "showcontrols":
|
|
media.setAttribute("controls", "true");
|
|
break;
|
|
case "showstats":
|
|
var event = document.createEvent("CustomEvent");
|
|
event.initCustomEvent("media-showStatistics", false, true, true);
|
|
media.dispatchEvent(event);
|
|
break;
|
|
case "hidestats":
|
|
var event = document.createEvent("CustomEvent");
|
|
event.initCustomEvent("media-showStatistics", false, true, false);
|
|
media.dispatchEvent(event);
|
|
break;
|
|
}
|
|
},
|
|
|
|
copyMediaLocation : function () {
|
|
var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
|
|
getService(Ci.nsIClipboardHelper);
|
|
clipboard.copyString(this.mediaURL, document);
|
|
},
|
|
|
|
get imageURL() {
|
|
if (this.onImage)
|
|
return this.mediaURL;
|
|
return "";
|
|
}
|
|
};
|