gecko/browser/metro/base/content/contenthandlers/Content.js

589 lines
19 KiB
JavaScript

// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/* 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/. */
// This stays here because otherwise it's hard to tell if there's a parsing error
dump("### Content.js loaded\n");
let Cc = Components.classes;
let Ci = Components.interfaces;
let Cu = Components.utils;
let Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "Services", function() {
Cu.import("resource://gre/modules/Services.jsm");
return Services;
});
XPCOMUtils.defineLazyGetter(this, "Rect", function() {
Cu.import("resource://gre/modules/Geometry.jsm");
return Rect;
});
XPCOMUtils.defineLazyGetter(this, "Point", function() {
Cu.import("resource://gre/modules/Geometry.jsm");
return Point;
});
XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
"resource://gre/modules/LoginManagerContent.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "gFocusManager",
"@mozilla.org/focus-manager;1", "nsIFocusManager");
XPCOMUtils.defineLazyServiceGetter(this, "gDOMUtils",
"@mozilla.org/inspector/dom-utils;1", "inIDOMUtils");
let XULDocument = Ci.nsIDOMXULDocument;
let HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement;
let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement;
let HTMLFrameElement = Ci.nsIDOMHTMLFrameElement;
let HTMLFrameSetElement = Ci.nsIDOMHTMLFrameSetElement;
let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement;
let HTMLOptionElement = Ci.nsIDOMHTMLOptionElement;
const kReferenceDpi = 240; // standard "pixel" size used in some preferences
const kStateActive = 0x00000001; // :active pseudoclass for elements
const kZoomToElementMargin = 16; // in px
/*
* getBoundingContentRect
*
* @param aElement
* @return Bounding content rect adjusted for scroll and frame offsets.
*/
function getBoundingContentRect(aElement) {
if (!aElement)
return new Rect(0, 0, 0, 0);
let document = aElement.ownerDocument;
while(document.defaultView.frameElement)
document = document.defaultView.frameElement.ownerDocument;
let offset = ContentScroll.getScrollOffset(content);
offset = new Point(offset.x, offset.y);
let r = aElement.getBoundingClientRect();
// step out of iframes and frames, offsetting scroll values
let view = aElement.ownerDocument.defaultView;
for (let frame = view; frame != content; frame = frame.parent) {
// adjust client coordinates' origin to be top left of iframe viewport
let rect = frame.frameElement.getBoundingClientRect();
let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
offset.add(rect.left + parseInt(left), rect.top + parseInt(top));
}
return new Rect(r.left + offset.x, r.top + offset.y, r.width, r.height);
}
/*
* getOverflowContentBoundingRect
*
* @param aElement
* @return Bounding content rect adjusted for scroll and frame offsets.
*/
function getOverflowContentBoundingRect(aElement) {
let r = getBoundingContentRect(aElement);
// If the overflow is hidden don't bother calculating it
let computedStyle = aElement.ownerDocument.defaultView.getComputedStyle(aElement);
let blockDisplays = ["block", "inline-block", "list-item"];
if ((blockDisplays.indexOf(computedStyle.getPropertyValue("display")) != -1 &&
computedStyle.getPropertyValue("overflow") == "hidden") ||
aElement instanceof HTMLSelectElement) {
return r;
}
for (let i = 0; i < aElement.childElementCount; i++) {
r = r.union(getBoundingContentRect(aElement.children[i]));
}
return r;
}
/*
* Content
*
* Browser event receiver for content.
*/
let Content = {
_debugEvents: false,
get formAssistant() {
delete this.formAssistant;
return this.formAssistant = new FormAssistant();
},
init: function init() {
// Asyncronous messages sent from the browser
addMessageListener("Browser:Blur", this);
addMessageListener("Browser:SaveAs", this);
addMessageListener("Browser:MozApplicationCache:Fetch", this);
addMessageListener("Browser:SetCharset", this);
addMessageListener("Browser:CanUnload", this);
addMessageListener("Browser:PanBegin", this);
addMessageListener("Gesture:SingleTap", this);
addMessageListener("Gesture:DoubleTap", this);
addEventListener("touchstart", this, false);
addEventListener("click", this, true);
addEventListener("keydown", this);
addEventListener("keyup", this);
// Synchronous events caught during the bubbling phase
addEventListener("MozApplicationManifest", this, false);
addEventListener("DOMContentLoaded", this, false);
addEventListener("DOMAutoComplete", this, false);
addEventListener("DOMFormHasPassword", this, false);
addEventListener("blur", this, false);
// Attach a listener to watch for "click" events bubbling up from error
// pages and other similar page. This lets us fix bugs like 401575 which
// require error page UI to do privileged things, without letting error
// pages have any privilege themselves.
addEventListener("click", this, false);
docShell.useGlobalHistory = true;
},
/*******************************************
* Events
*/
handleEvent: function handleEvent(aEvent) {
if (this._debugEvents) Util.dumpLn("Content:", aEvent.type);
switch (aEvent.type) {
case "MozApplicationManifest": {
let doc = aEvent.originalTarget;
sendAsyncMessage("Browser:MozApplicationManifest", {
location: doc.documentURIObject.spec,
manifest: doc.documentElement.getAttribute("manifest"),
charset: doc.characterSet
});
break;
}
case "keyup":
// If after a key is pressed we still have no input, then close
// the autocomplete. Perhaps the user used backspace or delete.
// Allow down arrow to trigger autofill popup on empty input.
if ((!aEvent.target.value && aEvent.keyCode != aEvent.DOM_VK_DOWN)
|| aEvent.keyCode == aEvent.DOM_VK_ESCAPE)
this.formAssistant.close();
else
this.formAssistant.open(aEvent.target, aEvent);
break;
case "click":
// Workaround for bug 925457: we sometimes don't recognize the
// correct tap target or are unable to identify if it's editable.
// Instead always save tap co-ordinates for the keyboard to look for
// when it is up.
SelectionHandler.onClickCoords(aEvent.clientX, aEvent.clientY);
if (aEvent.eventPhase == aEvent.BUBBLING_PHASE)
this._onClickBubble(aEvent);
else
this._onClickCapture(aEvent);
break;
case "DOMFormHasPassword":
LoginManagerContent.onFormPassword(aEvent);
break;
case "DOMContentLoaded":
LoginManagerContent.onContentLoaded(aEvent);
this._maybeNotifyErrorPage();
break;
case "DOMAutoComplete":
case "blur":
LoginManagerContent.onUsernameInput(aEvent);
break;
case "touchstart":
this._onTouchStart(aEvent);
break;
}
},
receiveMessage: function receiveMessage(aMessage) {
if (this._debugEvents) Util.dumpLn("Content:", aMessage.name);
let json = aMessage.json;
let x = json.x;
let y = json.y;
switch (aMessage.name) {
case "Browser:Blur":
gFocusManager.clearFocus(content);
break;
case "Browser:CanUnload":
let canUnload = docShell.contentViewer.permitUnload();
sendSyncMessage("Browser:CanUnload:Return", { permit: canUnload });
break;
case "Browser:SaveAs":
break;
case "Browser:MozApplicationCache:Fetch": {
let currentURI = Services.io.newURI(json.location, json.charset, null);
let manifestURI = Services.io.newURI(json.manifest, json.charset, currentURI);
let updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"]
.getService(Ci.nsIOfflineCacheUpdateService);
updateService.scheduleUpdate(manifestURI, currentURI, content);
break;
}
case "Browser:SetCharset": {
docShell.gatherCharsetMenuTelemetry();
docShell.charset = json.charset;
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
webNav.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
break;
}
case "Browser:PanBegin":
this._cancelTapHighlight();
break;
case "Gesture:SingleTap":
this._onSingleTap(json.x, json.y, json.modifiers);
break;
case "Gesture:DoubleTap":
this._onDoubleTap(json.x, json.y);
break;
}
},
/******************************************************
* Event handlers
*/
_onTouchStart: function _onTouchStart(aEvent) {
let element = aEvent.target;
// There is no need to have a feedback for disabled element
let isDisabled = element instanceof HTMLOptionElement ?
(element.disabled || element.parentNode.disabled) : element.disabled;
if (isDisabled)
return;
// Set the target element to active
this._doTapHighlight(element);
},
_onClickCapture: function _onClickCapture(aEvent) {
let element = aEvent.target;
ContextMenuHandler.reset();
// Only show autocomplete after the item is clicked
if (!this.lastClickElement || this.lastClickElement != element) {
this.lastClickElement = element;
if (aEvent.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_MOUSE &&
!(element instanceof HTMLSelectElement)) {
return;
}
}
this.formAssistant.focusSync = true;
this.formAssistant.open(element, aEvent);
this._cancelTapHighlight();
this.formAssistant.focusSync = false;
// A tap on a form input triggers touch input caret selection
if (Util.isEditable(element) &&
aEvent.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH) {
let { offsetX, offsetY } = Util.translateToTopLevelWindow(element);
sendAsyncMessage("Content:SelectionCaret", {
xPos: aEvent.clientX + offsetX,
yPos: aEvent.clientY + offsetY
});
} else {
SelectionHandler.closeSelection();
}
},
// Checks clicks we care about - events bubbling up from about pages.
_onClickBubble: function _onClickBubble(aEvent) {
// Don't trust synthetic events
if (!aEvent.isTrusted)
return;
let ot = aEvent.originalTarget;
let errorDoc = ot.ownerDocument;
if (!errorDoc)
return;
// If the event came from an ssl error page, it is probably either
// "Add Exception…" or "Get me out of here!" button.
if (/^about:certerror\?e=nssBadCert/.test(errorDoc.documentURI)) {
let perm = errorDoc.getElementById("permanentExceptionButton");
let temp = errorDoc.getElementById("temporaryExceptionButton");
if (ot == temp || ot == perm) {
let action = (ot == perm ? "permanent" : "temporary");
sendAsyncMessage("Browser:CertException",
{ url: errorDoc.location.href, action: action });
} else if (ot == errorDoc.getElementById("getMeOutOfHereButton")) {
sendAsyncMessage("Browser:CertException",
{ url: errorDoc.location.href, action: "leave" });
}
} else if (/^about:blocked/.test(errorDoc.documentURI)) {
// The event came from a button on a malware/phishing block page
// First check whether it's malware or phishing, so that we can
// use the right strings/links.
let isMalware = /e=malwareBlocked/.test(errorDoc.documentURI);
if (ot == errorDoc.getElementById("getMeOutButton")) {
sendAsyncMessage("Browser:BlockedSite",
{ url: errorDoc.location.href, action: "leave" });
} else if (ot == errorDoc.getElementById("reportButton")) {
// This is the "Why is this site blocked" button. For malware,
// we can fetch a site-specific report, for phishing, we redirect
// to the generic page describing phishing protection.
let action = isMalware ? "report-malware" : "report-phishing";
sendAsyncMessage("Browser:BlockedSite",
{ url: errorDoc.location.href, action: action });
} else if (ot == errorDoc.getElementById("ignoreWarningButton")) {
// Allow users to override and continue through to the site,
// but add a notify bar as a reminder, so that they don't lose
// track after, e.g., tab switching.
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
webNav.loadURI(content.location,
Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER,
null, null, null);
}
}
},
_onSingleTap: function (aX, aY, aModifiers) {
let utils = Util.getWindowUtils(content);
for (let type of ["mousemove", "mousedown", "mouseup"]) {
utils.sendMouseEventToWindow(type, aX, aY, 0, 1, aModifiers, true, 1.0,
Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH);
}
},
_onDoubleTap: function (aX, aY) {
let { element } = Content.getCurrentWindowAndOffset(aX, aY);
while (element && !this._shouldZoomToElement(element)) {
element = element.parentNode;
}
if (!element) {
this._zoomOut();
} else {
this._zoomToElement(element);
}
},
/******************************************************
* Zoom utilities
*/
_zoomOut: function() {
let rect = new Rect(0,0,0,0);
this._zoomToRect(rect);
},
_zoomToElement: function(aElement) {
let rect = getBoundingContentRect(aElement);
this._inflateRect(rect, kZoomToElementMargin);
this._zoomToRect(rect);
},
_inflateRect: function(aRect, aMargin) {
aRect.left -= aMargin;
aRect.top -= aMargin;
aRect.bottom += aMargin;
aRect.right += aMargin;
},
_zoomToRect: function (aRect) {
let utils = Util.getWindowUtils(content);
let viewId = utils.getViewId(content.document.documentElement);
let presShellId = {};
utils.getPresShellId(presShellId);
sendAsyncMessage("Content:ZoomToRect", {
rect: aRect,
presShellId: presShellId.value,
viewId: viewId,
});
},
_shouldZoomToElement: function(aElement) {
let win = aElement.ownerDocument.defaultView;
if (win.getComputedStyle(aElement, null).display == "inline") {
return false;
}
else if (aElement instanceof Ci.nsIDOMHTMLLIElement) {
return false;
}
else if (aElement instanceof Ci.nsIDOMHTMLQuoteElement) {
return false;
}
else {
return true;
}
},
/******************************************************
* General utilities
*/
/*
* Retrieve the total offset from the window's origin to the sub frame
* element including frame and scroll offsets. The resulting offset is
* such that:
* sub frame coords + offset = root frame position
*/
getCurrentWindowAndOffset: function(x, y) {
// If the element at the given point belongs to another document (such
// as an iframe's subdocument), the element in the calling document's
// DOM (e.g. the iframe) is returned.
let utils = Util.getWindowUtils(content);
let element = utils.elementFromPoint(x, y, true, false);
let offset = { x:0, y:0 };
while (element && (element instanceof HTMLIFrameElement ||
element instanceof HTMLFrameElement)) {
// get the child frame position in client coordinates
let rect = element.getBoundingClientRect();
// calculate offsets for digging down into sub frames
// using elementFromPoint:
// Get the content scroll offset in the child frame
scrollOffset = ContentScroll.getScrollOffset(element.contentDocument.defaultView);
// subtract frame and scroll offset from our elementFromPoint coordinates
x -= rect.left + scrollOffset.x;
y -= rect.top + scrollOffset.y;
// calculate offsets we'll use to translate to client coords:
// add frame client offset to our total offset result
offset.x += rect.left;
offset.y += rect.top;
// get the frame's nsIDOMWindowUtils
utils = element.contentDocument
.defaultView
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
// retrieve the target element in the sub frame at x, y
element = utils.elementFromPoint(x, y, true, false);
}
if (!element)
return {};
return {
element: element,
contentWindow: element.ownerDocument.defaultView,
offset: offset,
utils: utils
};
},
_maybeNotifyErrorPage: function _maybeNotifyErrorPage() {
// Notify browser that an error page is being shown instead
// of the target location. Necessary to get proper thumbnail
// updates on chrome for error pages.
if (content.location.href !== content.document.documentURI)
sendAsyncMessage("Browser:ErrorPage", null);
},
_highlightElement: null,
_doTapHighlight: function _doTapHighlight(aElement) {
gDOMUtils.setContentState(aElement, kStateActive);
this._highlightElement = aElement;
},
_cancelTapHighlight: function _cancelTapHighlight(aElement) {
gDOMUtils.setContentState(content.document.documentElement, kStateActive);
this._highlightElement = null;
},
};
Content.init();
var FormSubmitObserver = {
init: function init(){
addMessageListener("Browser:TabOpen", this);
addMessageListener("Browser:TabClose", this);
addEventListener("pageshow", this, false);
Services.obs.addObserver(this, "invalidformsubmit", false);
},
handleEvent: function handleEvent(aEvent) {
let target = aEvent.originalTarget;
let isRootDocument = (target == content.document || target.ownerDocument == content.document);
if (!isRootDocument)
return;
// Reset invalid submit state on each pageshow
if (aEvent.type == "pageshow")
Content.formAssistant.invalidSubmit = false;
},
receiveMessage: function receiveMessage(aMessage) {
let json = aMessage.json;
switch (aMessage.name) {
case "Browser:TabOpen":
Services.obs.addObserver(this, "formsubmit", false);
break;
case "Browser:TabClose":
Services.obs.removeObserver(this, "formsubmit");
break;
}
},
notify: function notify(aFormElement, aWindow, aActionURI, aCancelSubmit) {
// Do not notify unless this is the window where the submit occurred
if (aWindow == content)
// We don't need to send any data along
sendAsyncMessage("Browser:FormSubmit", {});
},
notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) {
if (!aInvalidElements.length)
return;
let element = aInvalidElements.queryElementAt(0, Ci.nsISupports);
if (!(element instanceof HTMLInputElement ||
element instanceof HTMLTextAreaElement ||
element instanceof HTMLSelectElement ||
element instanceof HTMLButtonElement)) {
return;
}
Content.formAssistant.invalidSubmit = true;
Content.formAssistant.open(element);
},
QueryInterface : function(aIID) {
if (!aIID.equals(Ci.nsIFormSubmitObserver) &&
!aIID.equals(Ci.nsISupportsWeakReference) &&
!aIID.equals(Ci.nsISupports))
throw Cr.NS_ERROR_NO_INTERFACE;
return this;
}
};
FormSubmitObserver.init();