Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 112 additions & 38 deletions jsscripts/ContextMenuHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
* 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/. */

const kXLinkNamespace = "http://www.w3.org/1999/xlink";
let Ci = Components.interfaces;
let Cc = Components.classes;

this.kXLinkNamespace = "http://www.w3.org/1999/xlink";

dump("### ContextMenuHandler.js loaded\n");

Expand Down Expand Up @@ -102,6 +105,8 @@ var ContextMenuHandler = {
if (Util.isTextInput(this._target)) {
// select all text in the input control
this._target.select();
} else if (Util.isEditableContent(this._target)) {
this._target.ownerDocument.execCommand("selectAll", false);
} else {
// select the entire document
content.getSelection().selectAllChildren(content.document);
Expand All @@ -118,6 +123,14 @@ var ContextMenuHandler = {
} else {
Util.dumpLn("error: target element does not support nsIDOMNSEditableElement");
}
} else if (Util.isEditableContent(this._target)) {
try {
this._target.ownerDocument.execCommand("paste",
false,
Ci.nsIClipboard.kGlobalClipboard);
} catch (ex) {
dump("ContextMenuHandler: exception pasting into contentEditable: " + ex.message + "\n");
}
}
this.reset();
},
Expand All @@ -134,6 +147,12 @@ var ContextMenuHandler = {
} else {
Util.dumpLn("error: target element does not support nsIDOMNSEditableElement");
}
} else if (Util.isEditableContent(this._target)) {
try {
this._target.ownerDocument.execCommand("cut", false);
} catch (ex) {
dump("ContextMenuHandler: exception cutting from contentEditable: " + ex.message + "\n");
}
}
this.reset();
},
Expand All @@ -146,6 +165,13 @@ var ContextMenuHandler = {
} else {
Util.dumpLn("error: target element does not support nsIDOMNSEditableElement");
}
} else if (Util.isEditableContent(this._target)) {
try {
this._target.ownerDocument.execCommand("copy", false);
} catch (ex) {
dump("ContextMenuHandler: exception copying from contentEditable: " +
ex.message + "\n");
}
} else {
let selectionText = this._previousState.string;

Expand Down Expand Up @@ -188,23 +214,23 @@ var ContextMenuHandler = {
contentDisposition: "",
string: "",
};
let uniqueStateTypes = new Set();

// Do checks for nodes that never have children.
if (popupNode.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
// See if the user clicked on an image.
if (popupNode instanceof Ci.nsIImageLoadingContent && popupNode.currentURI) {
state.types.push("image");
uniqueStateTypes.add("image");
state.label = state.mediaURL = popupNode.currentURI.spec;
imageUrl = state.mediaURL;
this._target = popupNode;

// Retrieve the type of image from the cache since the url can fail to
// provide valuable informations
try {
let tools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools);
let imageCache = tools.getImgCacheForDocument(content.document);
let props = imageCache.findEntryProperties(popupNode.currentURI);

let imageCache = Cc["@mozilla.org/image/cache;1"].getService(Ci.imgICache);
let props = imageCache.findEntryProperties(popupNode.currentURI,
content.document.characterSet);
if (props) {
state.contentType = String(props.get("type", Ci.nsISupportsCString));
state.contentDisposition = String(props.get("content-disposition",
Expand All @@ -219,6 +245,7 @@ var ContextMenuHandler = {

let elem = popupNode;
let isText = false;
let isEditableText = false;

while (elem) {
if (elem.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
Expand All @@ -231,56 +258,70 @@ var ContextMenuHandler = {
continue;
}

state.types.push("link");
uniqueStateTypes.add("link");
state.label = state.linkURL = this._getLinkURL(elem);
state.linkTitle = popupNode.textContent || popupNode.title;
state.linkProtocol = this._getProtocol(this._getURI(state.linkURL));
// mark as text so we can pickup on selection below
isText = true;
break;
} else if (Util.isTextInput(elem)) {
let selectionStart = elem.selectionStart;
let selectionEnd = elem.selectionEnd;
}
// is the target contentEditable (not just inheriting contentEditable)
// or the entire document in designer mode.
else if (elem.contentEditable == "true" ||
Util.isOwnerDocumentInDesignMode(elem)) {
this._target = elem;
isEditableText = true;
isText = true;
uniqueStateTypes.add("input-text");

state.types.push("input-text");
if (elem.textContent.length) {
uniqueStateTypes.add("selectable");
} else {
uniqueStateTypes.add("input-empty");
}
break;
}
// is the target a text input
else if (Util.isTextInput(elem)) {
this._target = elem;
isEditableText = true;
uniqueStateTypes.add("input-text");

let selectionStart = elem.selectionStart;
let selectionEnd = elem.selectionEnd;

// Don't include "copy" for password fields.
if (!(elem instanceof Ci.nsIDOMHTMLInputElement) || elem.mozIsTextField(true)) {
// If there is a selection add cut and copy
if (selectionStart != selectionEnd) {
state.types.push("cut");
state.types.push("copy");
uniqueStateTypes.add("cut");
uniqueStateTypes.add("copy");
state.string = elem.value.slice(selectionStart, selectionEnd);
} else if (elem.value && elem.textLength) {
// There is text and it is not selected so add selectable items
state.types.push("selectable");
uniqueStateTypes.add("selectable");
state.string = elem.value;
}
}

if (!elem.textLength) {
state.types.push("input-empty");
}

let flavors = ["text/unicode"];
let cb = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
let hasData = cb.hasDataMatchingFlavors(flavors,
flavors.length,
Ci.nsIClipboard.kGlobalClipboard);
if (hasData && !elem.readOnly) {
state.types.push("paste");
uniqueStateTypes.add("input-empty");
}
break;
} else if (Util.isText(elem)) {
}
// is the target an element containing text content
else if (Util.isText(elem)) {
isText = true;
} else if (elem instanceof Ci.nsIDOMHTMLMediaElement ||
}
// is the target a media element
else if (elem instanceof Ci.nsIDOMHTMLMediaElement ||
elem instanceof targetWindow.HTMLVideoElement) {
state.label = state.mediaURL = (elem.currentSrc || elem.src);
state.types.push((elem.paused || elem.ended) ?
uniqueStateTypes.add((elem.paused || elem.ended) ?
"media-paused" : "media-playing");
if (elem instanceof targetWindow.HTMLVideoElement) {
state.types.push("video");
uniqueStateTypes.add("video");
}
}
}
Expand All @@ -293,36 +334,68 @@ var ContextMenuHandler = {
// If this is text and has a selection, we want to bring
// up the copy option on the context menu.
let selection = targetWindow.getSelection();
if (selection && selection.toString().length > 0) {
if (selection && this._tapInSelection(selection, aX, aY)) {
state.string = targetWindow.getSelection().toString();
state.types.push("copy");
state.types.push("selected-text");
uniqueStateTypes.add("copy");
uniqueStateTypes.add("selected-text");
if (isEditableText) {
uniqueStateTypes.add("cut");
}
} else {
// Add general content text if this isn't anything specific
if (state.types.indexOf("image") == -1 &&
state.types.indexOf("media") == -1 &&
state.types.indexOf("video") == -1 &&
state.types.indexOf("link") == -1 &&
state.types.indexOf("input-text") == -1) {
state.types.push("content-text");
if (!(
uniqueStateTypes.has("image") ||
uniqueStateTypes.has("media") ||
uniqueStateTypes.has("video") ||
uniqueStateTypes.has("link") ||
uniqueStateTypes.has("input-text")
)) {
uniqueStateTypes.add("content-text");
}
}
}

// Is paste applicable here?
if (isEditableText) {
let flavors = ["text/unicode"];
let cb = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
let hasData = cb.hasDataMatchingFlavors(flavors,
flavors.length,
Ci.nsIClipboard.kGlobalClipboard);
// add paste if there's data
if (hasData && !elem.readOnly) {
uniqueStateTypes.add("paste");
}
}
// populate position and event source
state.xPos = offsetX + aX;
state.yPos = offsetY + aY;
state.source = aInputSrc;

for (let i = 0; i < this._types.length; i++)
if (this._types[i].handler(state, popupNode))
state.types.push(this._types[i].name);
uniqueStateTypes.add(this._types[i].name);

state.types = [type for (type of uniqueStateTypes)];
this._previousState = state;

sendAsyncMessage("Content:ContextMenu", state);
},

_tapInSelection: function (aSelection, aX, aY) {
if (!aSelection || !aSelection.rangeCount) {
return false;
}
for (let idx = 0; idx < aSelection.rangeCount; idx++) {
let range = aSelection.getRangeAt(idx);
let rect = range.getBoundingClientRect();
if (Util.pointWithinDOMRect(aX, aY, rect)) {
return true;
}
}
return false;
},

_getLinkURL: function ch_getLinkURL(aLink) {
let href = aLink.href;
if (href)
Expand Down Expand Up @@ -369,5 +442,6 @@ var ContextMenuHandler = {
this._types = this._types.filter(function(type) type.name != aName);
}
};
this.ContextMenuHandler = ContextMenuHandler;

ContextMenuHandler.init();
Loading