mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
3558ffd353
CLOSED TREE
589 lines
19 KiB
JavaScript
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();
|