gecko/mobile/chrome/content/content.js

677 lines
25 KiB
JavaScript
Raw Normal View History

// This stays here because otherwise it's hard to tell if there's a parsing error
dump("###################################### content loaded\n");
// how many milliseconds before the mousedown and the overlay of an element
const kTapOverlayTimeout = 200;
let Cc = Components.classes;
let Ci = Components.interfaces;
let gFocusManager = Cc["@mozilla.org/focus-manager;1"]
.getService(Ci.nsIFocusManager);
let gPrefService = Cc["@mozilla.org/preferences-service;1"]
.getService(Ci.nsIPrefBranch2);
let XULDocument = Ci.nsIDOMXULDocument;
let HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement;
let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement;
let HTMLFrameElement = Ci.nsIDOMHTMLFrameElement;
/** Send message to UI thread with browser guid as the first parameter. */
function sendMessage(name) {
sendAsyncMessage(name, Array.prototype.slice.call(arguments, 1));
}
/** Watches for mouse click in content and redirect them to the best found target **/
const ElementTouchHelper = {
get radius() {
delete this.radius;
return this.radius = { "top": gPrefService.getIntPref("browser.ui.touch.top"),
"right": gPrefService.getIntPref("browser.ui.touch.right"),
"bottom": gPrefService.getIntPref("browser.ui.touch.bottom"),
"left": gPrefService.getIntPref("browser.ui.touch.left")
};
},
get weight() {
delete this.weight;
return this.weight = { "visited": gPrefService.getIntPref("browser.ui.touch.weight.visited")
};
},
/* Retrieve the closest element to a point by looking at borders position */
getClosest: function getClosest(aWindowUtils, aX, aY) {
let target = aWindowUtils.elementFromPoint(aX, aY,
true, /* ignore root scroll frame*/
false); /* don't flush layout */
let nodes = aWindowUtils.nodesFromRect(aX, aY, this.radius.top,
this.radius.right,
this.radius.bottom,
this.radius.left, true, false);
// return early if the click is just over a clickable element
if (this._isElementClickable(target, nodes))
return target;
let threshold = Number.POSITIVE_INFINITY;
for (let i = 0; i < nodes.length; i++) {
let current = nodes[i];
if (!current.mozMatchesSelector || !this._isElementClickable(current))
continue;
let rect = current.getBoundingClientRect();
let distance = this._computeDistanceFromRect(aX, aY, rect);
// increase a little bit the weight for already visited items
if (current && current.mozMatchesSelector("*:visited"))
distance *= (this.weight.visited / 100);
if (distance < threshold) {
target = current;
threshold = distance;
}
}
return target;
},
_isElementClickable: function _isElementClickable(aElement, aElementsInRect) {
let isClickable = this._hasMouseListener(aElement);
// If possible looks in the parents node to find a target
if (aElement && !isClickable && aElementsInRect) {
let parentNode = aElement.parentNode;
let count = aElementsInRect.length;
for (let i = 0; i < count && parentNode; i++) {
if (aElementsInRect[i] != parentNode)
continue;
isClickable = this._hasMouseListener(parentNode);
if (isClickable)
break;
parentNode = parentNode.parentNode;
}
}
return aElement && (isClickable || aElement.mozMatchesSelector("a,*:link,*:visited,*[role=button],button,input,select,label"));
},
_computeDistanceFromRect: function _computeDistanceFromRect(aX, aY, aRect) {
let x = 0, y = 0;
let xmost = aRect.left + aRect.width;
let ymost = aRect.top + aRect.height;
// compute horizontal distance from left/right border depending if X is
// before/inside/after the element's rectangle
if (aRect.left < aX && aX < xmost)
x = Math.min(xmost - aX, aX - aRect.left);
else if (aX < aRect.left)
x = aRect.left - aX;
else if (aX > xmost)
x = aX - xmost;
// compute vertical distance from top/bottom border depending if Y is
// above/inside/below the element's rectangle
if (aRect.top < aY && aY < ymost)
y = Math.min(ymost - aY, aY - aRect.top);
else if (aY < aRect.top)
y = aRect.top - aY;
if (aY > ymost)
y = aY - ymost;
return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
},
_els: Cc["@mozilla.org/eventlistenerservice;1"].getService(Ci.nsIEventListenerService),
_clickableEvents: ["mousedown", "mouseup", "click"],
_hasMouseListener: function _hasMouseListener(aElement) {
let els = this._els;
let listeners = els.getListenerInfoFor(aElement, {});
for (let i = 0; i < listeners.length; i++) {
if (this._clickableEvents.indexOf(listeners[i].type) != -1)
return true;
}
return false;
}
};
/**
* @param x,y Browser coordinates
* @return Element at position, null if no active browser or no element found
*/
function elementFromPoint(x, y) {
// browser's elementFromPoint expect browser-relative client coordinates.
// subtract browser's scroll values to adjust
let cwu = Util.getWindowUtils(content);
let scroll = Util.getScrollOffset(content);
x = x - scroll.x
y = y - scroll.y;
let elem = ElementTouchHelper.getClosest(cwu, x, y);
// step through layers of IFRAMEs and FRAMES to find innermost element
while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
// adjust client coordinates' origin to be top left of iframe viewport
let rect = elem.getBoundingClientRect();
x -= rect.left;
y -= rect.top;
let windowUtils = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
elem = ElementTouchHelper.getClosest(windowUtils, x, y);
}
return elem;
}
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 = Util.getScrollOffset(content);
let r = aElement.getBoundingClientRect();
// step out of iframes and frames, offsetting scroll values
for (let frame = aElement.ownerDocument.defaultView; 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);
}
function getContentClientRects(aElement) {
let offset = Util.getScrollOffset(content);
let nativeRects = aElement.getClientRects();
// step out of iframes and frames, offsetting scroll values
for (let frame = aElement.ownerDocument.defaultView; 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));
}
let result = [];
for (let i = nativeRects.length - 1; i >= 0; i--) {
let r = nativeRects[i];
result.push({ left: r.left + offset.x,
top: r.top + offset.y,
width: r.width,
height: r.height
});
}
return result;
};
/** Reponsible for sending messages about viewport size changes and painting. */
function Coalescer() {
this._pendingDirtyRect = new Rect(0, 0, 0, 0);
this._pendingSizeChange = null;
this._timer = null;
// XXX When moving back and forward in docShell history, MozAfterPaint does not get called properly and
// some dirty rectangles are never flagged properly. To fix this, coalescer will fake a paint event that
// dirties the entire viewport.
this._incremental = false;
}
Coalescer.prototype = {
start: function startCoalescing() {
this._emptyPage();
this._timer = content.document.defaultView.setInterval(this, 1000);
},
stop: function stopCoalescing() {
content.document.defaultView.clearInterval(this._timer);
this._timer = null;
this.flush()
},
handleEvent: function handleEvent(aEvent) {
switch (aEvent.type) {
case "MozAfterPaint": {
let win = aEvent.originalTarget;
let scrollOffset = Util.getScrollOffset(win);
this.dirty(scrollOffset, aEvent.clientRects);
break;
}
case "MozScrolledAreaChanged": {
// XXX if it's possible to get a scroll area change with the same values,
// it would be optimal if this didn't send the same message twice.
let doc = aEvent.originalTarget;
let win = doc.defaultView;
let scrollOffset = Util.getScrollOffset(win);
if (win.parent != win) // We are only interested in root scroll pane changes
return;
this.sizeChange(scrollOffset, aEvent.x, aEvent.y, aEvent.width, aEvent.height);
break;
}
case "MozApplicationManifest": {
let doc = aEvent.originalTarget;
sendAsyncMessage("Browser:MozApplicationManifest", {
location: doc.documentURIObject.spec,
manifest: doc.documentElement.getAttribute("manifest"),
charset: doc.characterSet
});
break;
}
case "scroll":
sendSyncMessage("Browser:PageScroll", {});
break;
}
},
/** Next scroll size change event will invalidate all previous content. See constructor. */
_emptyPage: function _emptyPage() {
this._incremental = false;
},
sizeChange: function sizeChange(scrollOffset, x, y, width, height) {
// Adjust width and height from the incoming event properties so that we
// ignore changes to width and height contributed by growth in page
// quadrants other than x > 0 && y > 0.
x = x + scrollOffset.x;
y = y + scrollOffset.y;
this._pendingSizeChange = {
width: width + (x < 0 ? x : 0),
height: height + (y < 0 ? y : 0)
};
// Clear any pending dirty rectangles since entire viewport will be invalidated
// anyways.
let rect = this._pendingDirtyRect;
rect.top = rect.bottom;
rect.left = rect.right;
if (this._timer == null)
this.flush()
},
dirty: function dirty(scrollOffset, clientRects) {
if (!this._pendingSizeChange) {
let unionRect = this._pendingDirtyRect;
for (let i = clientRects.length - 1; i >= 0; i--) {
let e = clientRects.item(i);
unionRect.expandToContain(new Rect(
e.left + scrollOffset.x, e.top + scrollOffset.y, e.width, e.height));
}
if (this._timer == null)
this.flush()
}
},
flush: function flush() {
let dirtyRect = this._pendingDirtyRect;
let sizeChange = this._pendingSizeChange;
if (sizeChange) {
sendSyncMessage("Browser:MozScrolledAreaChanged", { width: sizeChange.width, height: sizeChange.height });
if (!this._incremental)
sendSyncMessage("Browser:MozAfterPaint", { rects: [ { left: 0, top: 0, right: sizeChange.width, bottom: sizeChange.height } ] });
this._pendingSizeChange = null;
// After first size change has been issued, assume subsequent size changes are only incremental
// changes to the current page.
this._incremental = true;
}
else if (!dirtyRect.isEmpty()) {
// No size change has occurred, but areas have been dirtied.
sendSyncMessage("Browser:MozAfterPaint", { rects: [dirtyRect] });
// Reset the rect to empty
dirtyRect.top = dirtyRect.bottom;
dirtyRect.left = dirtyRect.right;
}
}
};
/**
* Responsible for sending messages about security, location, and page load state.
* @param loadingController Object with methods startLoading and stopLoading
*/
function ProgressController(loadingController) {
this._webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
this._overrideService = null;
this._hostChanged = false;
this._state = null;
this._loadingController = loadingController || this._defaultLoadingController;
}
ProgressController.prototype = {
// Default loading callbacks do nothing
_defaultLoadingController: {
startLoading: function() {},
stopLoading: function() {}
},
onStateChange: function onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
// ignore notification that aren't about the main document (iframes, etc)
let win = aWebProgress.DOMWindow;
if (win != win.parent)
return;
// If you want to observe other state flags, be sure they're listed in the
// Tab._createBrowser's call to addProgressListener
if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
this._loadingController.startLoading();
}
else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
this._loadingController.stopLoading();
}
}
},
/** This method is called to indicate progress changes for the currently loading page. */
onProgressChange: function onProgressChange(aWebProgress, aRequest, aCurSelf, aMaxSelf, aCurTotal, aMaxTotal) {
},
/** This method is called to indicate a change to the current location. */
onLocationChange: function onLocationChange(aWebProgress, aRequest, aLocationURI) {
},
/**
* This method is called to indicate a status changes for the currently
* loading page. The message is already formatted for display.
*/
onStatusChange: function onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
},
/** This method is called when the security state of the browser changes. */
onSecurityChange: function onSecurityChange(aWebProgress, aRequest, aState) {
},
QueryInterface: function QueryInterface(aIID) {
if (aIID.equals(Ci.nsIWebProgressListener) ||
aIID.equals(Ci.nsISupportsWeakReference) ||
aIID.equals(Ci.nsISupports)) {
return this;
}
throw Components.results.NS_ERROR_NO_INTERFACE;
},
start: function start() {
let flags = Ci.nsIWebProgress.NOTIFY_STATE_NETWORK;
let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this, flags);
},
stop: function stop() {
let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
webProgress.removeProgressListener(this);
}
};
/** Can't think of a good description of this class. It probably does too much? */
function Content() {
addMessageListener("Browser:Blur", this);
addMessageListener("Browser:Focus", this);
addMessageListener("Browser:MouseDown", this);
addMessageListener("Browser:MouseUp", this);
addMessageListener("Browser:MouseCancel", this);
addMessageListener("Browser:SaveAs", this);
this._coalescer = new Coalescer();
addEventListener("MozAfterPaint", this._coalescer, false);
addEventListener("MozScrolledAreaChanged", this._coalescer, false);
addEventListener("MozApplicationManifest", this._coalescer, false);
addEventListener("scroll", this._coalescer, false);
this._progressController = new ProgressController(this);
this._progressController.start();
this._formAssistant = new FormAssistant();
}
Content.prototype = {
receiveMessage: function receiveMessage(aMessage) {
let json = aMessage.json;
let x = json.x;
let y = json.y;
switch (aMessage.name) {
case "Browser:Blur":
docShell.isOffScreenBrowser = false;
this._selected = false;
break;
case "Browser:Focus":
docShell.isOffScreenBrowser = true;
this._selected = true;
break;
case "Browser:MouseDown":
if (this._overlayTimeout)
return;
this._overlayTimeout = content.document.defaultView.setTimeout(function() {
let element = elementFromPoint(x, y);
if (!element || !element.mozMatchesSelector("*:link,*:visited,*:link *,*:visited *,*[role=button],button,input,option,select,textarea,label"))
return;
let rects = getContentClientRects(element);
sendSyncMessage("Browser:Highlight", { rects: rects });
}, kTapOverlayTimeout);
break;
case "Browser:MouseUp":
let element = elementFromPoint(x, y);
// the element can be out of the cX/cY point because of the touch radius
// ignore the redirection if the element is a HTMLHtmlElement (bug 562981)
let rect = getBoundingContentRect(element);
if (!rect.isEmpty() && !(element instanceof HTMLHtmlElement) &&
((x < rect.left || (x > rect.left + rect.width)) || (y < rect.top || (y > rect.top + rect.height)))) {
let point = rect.center();
x = point.x;
y = point.y;
}
if (!this._formAssistant.open(element)) {
this._sendMouseEvent("mousedown", element, x, y);
this._sendMouseEvent("mouseup", element, x, y);
}
case "Browser:MouseCancel":
if (this._overlayTimeout) {
content.document.defaultView.clearTimeout(this._overlayTimeout);
this._overlayTimeout = 0;
}
break;
case "Browser:SaveAs":
if (json.type != Ci.nsIPrintSettings.kOutputFormatPDF)
return;
let printSettings = Cc["@mozilla.org/gfx/printsettings-service;1"]
.getService(Ci.nsIPrintSettingsService)
.newPrintSettings;
printSettings.printSilent = true;
printSettings.showPrintProgress = false;
printSettings.printBGImages = true;
printSettings.printBGColors = true;
printSettings.printToFile = true;
printSettings.toFileName = json.filePath;
printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs;
printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
//XXX we probably need a preference here, the header can be useful
printSettings.footerStrCenter = "";
printSettings.footerStrLeft = "";
printSettings.footerStrRight = "";
printSettings.headerStrCenter = "";
printSettings.headerStrLeft = "";
printSettings.headerStrRight = "";
let listener = {
onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
sendAsyncMessage("Browser:SaveAs:Return", { type: json.type, id: json.id, referrer: json.referrer });
}
},
onProgressChange : function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) {},
// stubs for the nsIWebProgressListener interfaces which nsIWebBrowserPrint doesn't use.
onLocationChange : function() { throw "Unexpected onLocationChange"; },
onStatusChange : function() { throw "Unexpected onStatusChange"; },
onSecurityChange : function() { throw "Unexpected onSecurityChange"; }
};
let webBrowserPrint = content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebBrowserPrint);
webBrowserPrint.print(printSettings, listener);
break;
}
},
_sendMouseEvent: function _sendMouseEvent(name, element, x, y) {
let windowUtils = Util.getWindowUtils(content);
let scrollOffset = Util.getScrollOffset(content);
// the element can be out of the cX/cY point because of the touch radius
let rect = getBoundingContentRect(element);
if (!rect.isEmpty() && !(element instanceof HTMLHtmlElement) ||
x < rect.left || x > rect.right ||
y < rect.top || y > rect.bottom) {
let point = rect.center();
x = point.x;
y = point.y;
}
windowUtils.sendMouseEvent(name, x - scrollOffset.x, y - scrollOffset.y, 0, 1, 0, true);
},
startLoading: function startLoading() {
this._loading = true;
this._coalescer.start();
},
stopLoading: function stopLoading() {
this._loading = false;
this._coalescer.stop();
},
isSelected: function isSelected() {
return this._selected;
}
};
let contentObject = new Content();
let ViewportHandler = {
metadata: null,
init: function init() {
addEventListener("DOMWindowCreated", this, false);
addEventListener("DOMMetaAdded", this, false);
addEventListener("DOMContentLoaded", this, false);
addEventListener("pageshow", this, false);
},
handleEvent: function handleEvent(aEvent) {
switch (aEvent.type) {
case "DOMWindowCreated":
this.resetMetadata();
break;
case "DOMMetaAdded":
let target = aEvent.originalTarget;
let isRootDocument = (target.ownerDocument == content.document);
if (isRootDocument && target.name == "viewport")
this.updateMetadata();
break;
case "DOMContentLoaded":
case "pageshow":
if (!this.metadata)
this.updateMetadata();
break;
}
},
resetMetadata: function resetMetadata() {
this.metadata = null;
sendAsyncMessage("Browser:ViewportMetadata", {});
},
updateMetadata: function updateMetadata() {
this.metadata = this.getViewportMetadata();
sendAsyncMessage("Browser:ViewportMetadata", this.metadata);
},
getViewportMetadata: function getViewportMetadata() {
let dpiScale = gPrefService.getIntPref("zoom.dpiScale") / 100;
let doctype = content.document.doctype;
if (doctype && /(WAP|WML|Mobile)/.test(doctype.publicId))
return { defaultZoom: dpiScale, autoSize: true };
let windowUtils = Util.getWindowUtils(content);
let handheldFriendly = windowUtils.getDocumentMetadata("HandheldFriendly");
if (handheldFriendly == "true")
return { defaultZoom: dpiScale, autoSize: true };
if (content.document instanceof XULDocument)
return { defaultZoom: 1.0, autoSize: true, allowZoom: false };
// viewport details found here
// http://developer.apple.com/safari/library/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html
// http://developer.apple.com/safari/library/documentation/AppleApplications/Reference/SafariWebContent/UsingtheViewport/UsingtheViewport.html
// Note: These values will be NaN if parseFloat or parseInt doesn't find a number.
// Remember that NaN is contagious: Math.max(1, NaN) == Math.min(1, NaN) == NaN.
let viewportScale = parseFloat(windowUtils.getDocumentMetadata("viewport-initial-scale"));
let viewportMinScale = parseFloat(windowUtils.getDocumentMetadata("viewport-minimum-scale"));
let viewportMaxScale = parseFloat(windowUtils.getDocumentMetadata("viewport-maximum-scale"));
let viewportWidthStr = windowUtils.getDocumentMetadata("viewport-width");
let viewportHeightStr = windowUtils.getDocumentMetadata("viewport-height");
viewportScale = Util.clamp(viewportScale, kViewportMinScale, kViewportMaxScale);
viewportMinScale = Util.clamp(viewportMinScale, kViewportMinScale, kViewportMaxScale);
viewportMaxScale = Util.clamp(viewportMaxScale, kViewportMinScale, kViewportMaxScale);
// If initial scale is 1.0 and width is not set, assume width=device-width
let autoSize = (viewportWidthStr == "device-width" ||
viewportHeightStr == "device-height" ||
(viewportScale == 1.0 && !viewportWidthStr));
let viewportWidth = Util.clamp(parseInt(viewportWidthStr), kViewportMinWidth, kViewportMaxWidth);
let viewportHeight = Util.clamp(parseInt(viewportHeightStr), kViewportMinHeight, kViewportMaxHeight);
// Zoom level is the final (device pixel : CSS pixel) ratio for content.
// Since web content specifies scale as (reference pixel : CSS pixel) ratio,
// multiply the requested scale by a constant (device pixel : reference pixel)
// factor to account for high DPI devices.
//
// See bug 561445 or any of the examples of chrome/tests/browser_viewport_XX.html
// for more information and examples.
let defaultZoom = viewportScale * dpiScale;
let minZoom = viewportMinScale * dpiScale;
let maxZoom = viewportMaxScale * dpiScale;
return {
defaultZoom: defaultZoom,
minZoom: minZoom,
maxZoom: maxZoom,
width: viewportWidth,
height: viewportHeight,
autoSize: autoSize,
allowZoom: windowUtils.getDocumentMetadata("viewport-user-scalable") != "no"
};
}
};
ViewportHandler.init();