Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- |
michael@0 | 2 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 5 | |
michael@0 | 6 | // This stays here because otherwise it's hard to tell if there's a parsing error |
michael@0 | 7 | dump("### Content.js loaded\n"); |
michael@0 | 8 | |
michael@0 | 9 | let Cc = Components.classes; |
michael@0 | 10 | let Ci = Components.interfaces; |
michael@0 | 11 | let Cu = Components.utils; |
michael@0 | 12 | let Cr = Components.results; |
michael@0 | 13 | |
michael@0 | 14 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 15 | |
michael@0 | 16 | XPCOMUtils.defineLazyGetter(this, "Services", function() { |
michael@0 | 17 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 18 | return Services; |
michael@0 | 19 | }); |
michael@0 | 20 | |
michael@0 | 21 | XPCOMUtils.defineLazyGetter(this, "Rect", function() { |
michael@0 | 22 | Cu.import("resource://gre/modules/Geometry.jsm"); |
michael@0 | 23 | return Rect; |
michael@0 | 24 | }); |
michael@0 | 25 | |
michael@0 | 26 | XPCOMUtils.defineLazyGetter(this, "Point", function() { |
michael@0 | 27 | Cu.import("resource://gre/modules/Geometry.jsm"); |
michael@0 | 28 | return Point; |
michael@0 | 29 | }); |
michael@0 | 30 | |
michael@0 | 31 | XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent", |
michael@0 | 32 | "resource://gre/modules/LoginManagerContent.jsm"); |
michael@0 | 33 | |
michael@0 | 34 | XPCOMUtils.defineLazyServiceGetter(this, "gFocusManager", |
michael@0 | 35 | "@mozilla.org/focus-manager;1", "nsIFocusManager"); |
michael@0 | 36 | |
michael@0 | 37 | XPCOMUtils.defineLazyServiceGetter(this, "gDOMUtils", |
michael@0 | 38 | "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils"); |
michael@0 | 39 | |
michael@0 | 40 | this.XULDocument = Ci.nsIDOMXULDocument; |
michael@0 | 41 | this.HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement; |
michael@0 | 42 | this.HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement; |
michael@0 | 43 | this.HTMLFrameElement = Ci.nsIDOMHTMLFrameElement; |
michael@0 | 44 | this.HTMLFrameSetElement = Ci.nsIDOMHTMLFrameSetElement; |
michael@0 | 45 | this.HTMLSelectElement = Ci.nsIDOMHTMLSelectElement; |
michael@0 | 46 | this.HTMLOptionElement = Ci.nsIDOMHTMLOptionElement; |
michael@0 | 47 | |
michael@0 | 48 | const kReferenceDpi = 240; // standard "pixel" size used in some preferences |
michael@0 | 49 | |
michael@0 | 50 | const kStateActive = 0x00000001; // :active pseudoclass for elements |
michael@0 | 51 | |
michael@0 | 52 | const kZoomToElementMargin = 16; // in px |
michael@0 | 53 | |
michael@0 | 54 | /* |
michael@0 | 55 | * getBoundingContentRect |
michael@0 | 56 | * |
michael@0 | 57 | * @param aElement |
michael@0 | 58 | * @return Bounding content rect adjusted for scroll and frame offsets. |
michael@0 | 59 | */ |
michael@0 | 60 | function getBoundingContentRect(aElement) { |
michael@0 | 61 | if (!aElement) |
michael@0 | 62 | return new Rect(0, 0, 0, 0); |
michael@0 | 63 | |
michael@0 | 64 | let document = aElement.ownerDocument; |
michael@0 | 65 | while(document.defaultView.frameElement) |
michael@0 | 66 | document = document.defaultView.frameElement.ownerDocument; |
michael@0 | 67 | |
michael@0 | 68 | let offset = ContentScroll.getScrollOffset(content); |
michael@0 | 69 | offset = new Point(offset.x, offset.y); |
michael@0 | 70 | |
michael@0 | 71 | let r = aElement.getBoundingClientRect(); |
michael@0 | 72 | |
michael@0 | 73 | // step out of iframes and frames, offsetting scroll values |
michael@0 | 74 | let view = aElement.ownerDocument.defaultView; |
michael@0 | 75 | for (let frame = view; frame != content; frame = frame.parent) { |
michael@0 | 76 | // adjust client coordinates' origin to be top left of iframe viewport |
michael@0 | 77 | let rect = frame.frameElement.getBoundingClientRect(); |
michael@0 | 78 | let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth; |
michael@0 | 79 | let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth; |
michael@0 | 80 | offset.add(rect.left + parseInt(left), rect.top + parseInt(top)); |
michael@0 | 81 | } |
michael@0 | 82 | |
michael@0 | 83 | return new Rect(r.left + offset.x, r.top + offset.y, r.width, r.height); |
michael@0 | 84 | } |
michael@0 | 85 | this.getBoundingContentRect = getBoundingContentRect; |
michael@0 | 86 | |
michael@0 | 87 | /* |
michael@0 | 88 | * getOverflowContentBoundingRect |
michael@0 | 89 | * |
michael@0 | 90 | * @param aElement |
michael@0 | 91 | * @return Bounding content rect adjusted for scroll and frame offsets. |
michael@0 | 92 | */ |
michael@0 | 93 | |
michael@0 | 94 | function getOverflowContentBoundingRect(aElement) { |
michael@0 | 95 | let r = getBoundingContentRect(aElement); |
michael@0 | 96 | |
michael@0 | 97 | // If the overflow is hidden don't bother calculating it |
michael@0 | 98 | let computedStyle = aElement.ownerDocument.defaultView.getComputedStyle(aElement); |
michael@0 | 99 | let blockDisplays = ["block", "inline-block", "list-item"]; |
michael@0 | 100 | if ((blockDisplays.indexOf(computedStyle.getPropertyValue("display")) != -1 && |
michael@0 | 101 | computedStyle.getPropertyValue("overflow") == "hidden") || |
michael@0 | 102 | aElement instanceof HTMLSelectElement) { |
michael@0 | 103 | return r; |
michael@0 | 104 | } |
michael@0 | 105 | |
michael@0 | 106 | for (let i = 0; i < aElement.childElementCount; i++) { |
michael@0 | 107 | r = r.union(getBoundingContentRect(aElement.children[i])); |
michael@0 | 108 | } |
michael@0 | 109 | |
michael@0 | 110 | return r; |
michael@0 | 111 | } |
michael@0 | 112 | this.getOverflowContentBoundingRect = getOverflowContentBoundingRect; |
michael@0 | 113 | |
michael@0 | 114 | /* |
michael@0 | 115 | * Content |
michael@0 | 116 | * |
michael@0 | 117 | * Browser event receiver for content. |
michael@0 | 118 | */ |
michael@0 | 119 | let Content = { |
michael@0 | 120 | _debugEvents: false, |
michael@0 | 121 | |
michael@0 | 122 | get formAssistant() { |
michael@0 | 123 | delete this.formAssistant; |
michael@0 | 124 | return this.formAssistant = new FormAssistant(); |
michael@0 | 125 | }, |
michael@0 | 126 | |
michael@0 | 127 | init: function init() { |
michael@0 | 128 | // Asyncronous messages sent from the browser |
michael@0 | 129 | addMessageListener("Browser:Blur", this); |
michael@0 | 130 | addMessageListener("Browser:SaveAs", this); |
michael@0 | 131 | addMessageListener("Browser:MozApplicationCache:Fetch", this); |
michael@0 | 132 | addMessageListener("Browser:SetCharset", this); |
michael@0 | 133 | addMessageListener("Browser:CanUnload", this); |
michael@0 | 134 | addMessageListener("Browser:PanBegin", this); |
michael@0 | 135 | addMessageListener("Gesture:SingleTap", this); |
michael@0 | 136 | addMessageListener("Gesture:DoubleTap", this); |
michael@0 | 137 | |
michael@0 | 138 | addEventListener("touchstart", this, false); |
michael@0 | 139 | addEventListener("click", this, true); |
michael@0 | 140 | addEventListener("keydown", this); |
michael@0 | 141 | addEventListener("keyup", this); |
michael@0 | 142 | |
michael@0 | 143 | // Synchronous events caught during the bubbling phase |
michael@0 | 144 | addEventListener("MozApplicationManifest", this, false); |
michael@0 | 145 | addEventListener("DOMContentLoaded", this, false); |
michael@0 | 146 | addEventListener("DOMAutoComplete", this, false); |
michael@0 | 147 | addEventListener("DOMFormHasPassword", this, false); |
michael@0 | 148 | addEventListener("blur", this, false); |
michael@0 | 149 | // Attach a listener to watch for "click" events bubbling up from error |
michael@0 | 150 | // pages and other similar page. This lets us fix bugs like 401575 which |
michael@0 | 151 | // require error page UI to do privileged things, without letting error |
michael@0 | 152 | // pages have any privilege themselves. |
michael@0 | 153 | addEventListener("click", this, false); |
michael@0 | 154 | |
michael@0 | 155 | docShell.useGlobalHistory = true; |
michael@0 | 156 | }, |
michael@0 | 157 | |
michael@0 | 158 | /******************************************* |
michael@0 | 159 | * Events |
michael@0 | 160 | */ |
michael@0 | 161 | |
michael@0 | 162 | handleEvent: function handleEvent(aEvent) { |
michael@0 | 163 | if (this._debugEvents) Util.dumpLn("Content:", aEvent.type); |
michael@0 | 164 | switch (aEvent.type) { |
michael@0 | 165 | case "MozApplicationManifest": { |
michael@0 | 166 | let doc = aEvent.originalTarget; |
michael@0 | 167 | sendAsyncMessage("Browser:MozApplicationManifest", { |
michael@0 | 168 | location: doc.documentURIObject.spec, |
michael@0 | 169 | manifest: doc.documentElement.getAttribute("manifest"), |
michael@0 | 170 | charset: doc.characterSet |
michael@0 | 171 | }); |
michael@0 | 172 | break; |
michael@0 | 173 | } |
michael@0 | 174 | |
michael@0 | 175 | case "keyup": |
michael@0 | 176 | // If after a key is pressed we still have no input, then close |
michael@0 | 177 | // the autocomplete. Perhaps the user used backspace or delete. |
michael@0 | 178 | // Allow down arrow to trigger autofill popup on empty input. |
michael@0 | 179 | if ((!aEvent.target.value && aEvent.keyCode != aEvent.DOM_VK_DOWN) |
michael@0 | 180 | || aEvent.keyCode == aEvent.DOM_VK_ESCAPE) |
michael@0 | 181 | this.formAssistant.close(); |
michael@0 | 182 | else |
michael@0 | 183 | this.formAssistant.open(aEvent.target, aEvent); |
michael@0 | 184 | break; |
michael@0 | 185 | |
michael@0 | 186 | case "click": |
michael@0 | 187 | // Workaround for bug 925457: we sometimes don't recognize the |
michael@0 | 188 | // correct tap target or are unable to identify if it's editable. |
michael@0 | 189 | // Instead always save tap co-ordinates for the keyboard to look for |
michael@0 | 190 | // when it is up. |
michael@0 | 191 | SelectionHandler.onClickCoords(aEvent.clientX, aEvent.clientY); |
michael@0 | 192 | |
michael@0 | 193 | if (aEvent.eventPhase == aEvent.BUBBLING_PHASE) |
michael@0 | 194 | this._onClickBubble(aEvent); |
michael@0 | 195 | else |
michael@0 | 196 | this._onClickCapture(aEvent); |
michael@0 | 197 | break; |
michael@0 | 198 | |
michael@0 | 199 | case "DOMFormHasPassword": |
michael@0 | 200 | LoginManagerContent.onFormPassword(aEvent); |
michael@0 | 201 | break; |
michael@0 | 202 | |
michael@0 | 203 | case "DOMContentLoaded": |
michael@0 | 204 | this._maybeNotifyErrorPage(); |
michael@0 | 205 | break; |
michael@0 | 206 | |
michael@0 | 207 | case "DOMAutoComplete": |
michael@0 | 208 | case "blur": |
michael@0 | 209 | LoginManagerContent.onUsernameInput(aEvent); |
michael@0 | 210 | break; |
michael@0 | 211 | |
michael@0 | 212 | case "touchstart": |
michael@0 | 213 | this._onTouchStart(aEvent); |
michael@0 | 214 | break; |
michael@0 | 215 | } |
michael@0 | 216 | }, |
michael@0 | 217 | |
michael@0 | 218 | receiveMessage: function receiveMessage(aMessage) { |
michael@0 | 219 | if (this._debugEvents) Util.dumpLn("Content:", aMessage.name); |
michael@0 | 220 | let json = aMessage.json; |
michael@0 | 221 | let x = json.x; |
michael@0 | 222 | let y = json.y; |
michael@0 | 223 | |
michael@0 | 224 | switch (aMessage.name) { |
michael@0 | 225 | case "Browser:Blur": |
michael@0 | 226 | gFocusManager.clearFocus(content); |
michael@0 | 227 | break; |
michael@0 | 228 | |
michael@0 | 229 | case "Browser:CanUnload": |
michael@0 | 230 | let canUnload = docShell.contentViewer.permitUnload(); |
michael@0 | 231 | sendSyncMessage("Browser:CanUnload:Return", { permit: canUnload }); |
michael@0 | 232 | break; |
michael@0 | 233 | |
michael@0 | 234 | case "Browser:SaveAs": |
michael@0 | 235 | break; |
michael@0 | 236 | |
michael@0 | 237 | case "Browser:MozApplicationCache:Fetch": { |
michael@0 | 238 | let currentURI = Services.io.newURI(json.location, json.charset, null); |
michael@0 | 239 | let manifestURI = Services.io.newURI(json.manifest, json.charset, currentURI); |
michael@0 | 240 | let updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"] |
michael@0 | 241 | .getService(Ci.nsIOfflineCacheUpdateService); |
michael@0 | 242 | updateService.scheduleUpdate(manifestURI, currentURI, content); |
michael@0 | 243 | break; |
michael@0 | 244 | } |
michael@0 | 245 | |
michael@0 | 246 | case "Browser:SetCharset": { |
michael@0 | 247 | docShell.gatherCharsetMenuTelemetry(); |
michael@0 | 248 | docShell.charset = json.charset; |
michael@0 | 249 | |
michael@0 | 250 | let webNav = docShell.QueryInterface(Ci.nsIWebNavigation); |
michael@0 | 251 | webNav.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE); |
michael@0 | 252 | break; |
michael@0 | 253 | } |
michael@0 | 254 | |
michael@0 | 255 | case "Browser:PanBegin": |
michael@0 | 256 | this._cancelTapHighlight(); |
michael@0 | 257 | break; |
michael@0 | 258 | |
michael@0 | 259 | case "Gesture:SingleTap": |
michael@0 | 260 | this._onSingleTap(json.x, json.y, json.modifiers); |
michael@0 | 261 | break; |
michael@0 | 262 | |
michael@0 | 263 | case "Gesture:DoubleTap": |
michael@0 | 264 | this._onDoubleTap(json.x, json.y); |
michael@0 | 265 | break; |
michael@0 | 266 | } |
michael@0 | 267 | }, |
michael@0 | 268 | |
michael@0 | 269 | /****************************************************** |
michael@0 | 270 | * Event handlers |
michael@0 | 271 | */ |
michael@0 | 272 | |
michael@0 | 273 | _onTouchStart: function _onTouchStart(aEvent) { |
michael@0 | 274 | let element = aEvent.target; |
michael@0 | 275 | |
michael@0 | 276 | // There is no need to have a feedback for disabled element |
michael@0 | 277 | let isDisabled = element instanceof HTMLOptionElement ? |
michael@0 | 278 | (element.disabled || element.parentNode.disabled) : element.disabled; |
michael@0 | 279 | if (isDisabled) |
michael@0 | 280 | return; |
michael@0 | 281 | |
michael@0 | 282 | // Set the target element to active |
michael@0 | 283 | this._doTapHighlight(element); |
michael@0 | 284 | }, |
michael@0 | 285 | |
michael@0 | 286 | _onClickCapture: function _onClickCapture(aEvent) { |
michael@0 | 287 | let element = aEvent.target; |
michael@0 | 288 | |
michael@0 | 289 | ContextMenuHandler.reset(); |
michael@0 | 290 | |
michael@0 | 291 | // Only show autocomplete after the item is clicked |
michael@0 | 292 | if (!this.lastClickElement || this.lastClickElement != element) { |
michael@0 | 293 | this.lastClickElement = element; |
michael@0 | 294 | if (aEvent.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_MOUSE && |
michael@0 | 295 | !(element instanceof HTMLSelectElement)) { |
michael@0 | 296 | return; |
michael@0 | 297 | } |
michael@0 | 298 | } |
michael@0 | 299 | |
michael@0 | 300 | this.formAssistant.focusSync = true; |
michael@0 | 301 | this.formAssistant.open(element, aEvent); |
michael@0 | 302 | this._cancelTapHighlight(); |
michael@0 | 303 | this.formAssistant.focusSync = false; |
michael@0 | 304 | |
michael@0 | 305 | // A tap on a form input triggers touch input caret selection |
michael@0 | 306 | if (Util.isEditable(element) && |
michael@0 | 307 | aEvent.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH) { |
michael@0 | 308 | let { offsetX, offsetY } = Util.translateToTopLevelWindow(element); |
michael@0 | 309 | sendAsyncMessage("Content:SelectionCaret", { |
michael@0 | 310 | xPos: aEvent.clientX + offsetX, |
michael@0 | 311 | yPos: aEvent.clientY + offsetY |
michael@0 | 312 | }); |
michael@0 | 313 | } else { |
michael@0 | 314 | SelectionHandler.closeSelection(); |
michael@0 | 315 | } |
michael@0 | 316 | }, |
michael@0 | 317 | |
michael@0 | 318 | // Checks clicks we care about - events bubbling up from about pages. |
michael@0 | 319 | _onClickBubble: function _onClickBubble(aEvent) { |
michael@0 | 320 | // Don't trust synthetic events |
michael@0 | 321 | if (!aEvent.isTrusted) |
michael@0 | 322 | return; |
michael@0 | 323 | |
michael@0 | 324 | let ot = aEvent.originalTarget; |
michael@0 | 325 | let errorDoc = ot.ownerDocument; |
michael@0 | 326 | if (!errorDoc) |
michael@0 | 327 | return; |
michael@0 | 328 | |
michael@0 | 329 | // If the event came from an ssl error page, it is probably either |
michael@0 | 330 | // "Add Exception…" or "Get me out of here!" button. |
michael@0 | 331 | if (/^about:certerror\?e=nssBadCert/.test(errorDoc.documentURI)) { |
michael@0 | 332 | let perm = errorDoc.getElementById("permanentExceptionButton"); |
michael@0 | 333 | let temp = errorDoc.getElementById("temporaryExceptionButton"); |
michael@0 | 334 | if (ot == temp || ot == perm) { |
michael@0 | 335 | let action = (ot == perm ? "permanent" : "temporary"); |
michael@0 | 336 | sendAsyncMessage("Browser:CertException", |
michael@0 | 337 | { url: errorDoc.location.href, action: action }); |
michael@0 | 338 | } else if (ot == errorDoc.getElementById("getMeOutOfHereButton")) { |
michael@0 | 339 | sendAsyncMessage("Browser:CertException", |
michael@0 | 340 | { url: errorDoc.location.href, action: "leave" }); |
michael@0 | 341 | } |
michael@0 | 342 | } else if (/^about:blocked/.test(errorDoc.documentURI)) { |
michael@0 | 343 | // The event came from a button on a malware/phishing block page |
michael@0 | 344 | // First check whether it's malware or phishing, so that we can |
michael@0 | 345 | // use the right strings/links. |
michael@0 | 346 | let isMalware = /e=malwareBlocked/.test(errorDoc.documentURI); |
michael@0 | 347 | |
michael@0 | 348 | if (ot == errorDoc.getElementById("getMeOutButton")) { |
michael@0 | 349 | sendAsyncMessage("Browser:BlockedSite", |
michael@0 | 350 | { url: errorDoc.location.href, action: "leave" }); |
michael@0 | 351 | } else if (ot == errorDoc.getElementById("reportButton")) { |
michael@0 | 352 | // This is the "Why is this site blocked" button. For malware, |
michael@0 | 353 | // we can fetch a site-specific report, for phishing, we redirect |
michael@0 | 354 | // to the generic page describing phishing protection. |
michael@0 | 355 | let action = isMalware ? "report-malware" : "report-phishing"; |
michael@0 | 356 | sendAsyncMessage("Browser:BlockedSite", |
michael@0 | 357 | { url: errorDoc.location.href, action: action }); |
michael@0 | 358 | } else if (ot == errorDoc.getElementById("ignoreWarningButton")) { |
michael@0 | 359 | // Allow users to override and continue through to the site, |
michael@0 | 360 | // but add a notify bar as a reminder, so that they don't lose |
michael@0 | 361 | // track after, e.g., tab switching. |
michael@0 | 362 | let webNav = docShell.QueryInterface(Ci.nsIWebNavigation); |
michael@0 | 363 | webNav.loadURI(content.location, |
michael@0 | 364 | Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER, |
michael@0 | 365 | null, null, null); |
michael@0 | 366 | } |
michael@0 | 367 | } |
michael@0 | 368 | }, |
michael@0 | 369 | |
michael@0 | 370 | _onSingleTap: function (aX, aY, aModifiers) { |
michael@0 | 371 | let utils = Util.getWindowUtils(content); |
michael@0 | 372 | for (let type of ["mousemove", "mousedown", "mouseup"]) { |
michael@0 | 373 | utils.sendMouseEventToWindow(type, aX, aY, 0, 1, aModifiers, true, 1.0, |
michael@0 | 374 | Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH); |
michael@0 | 375 | } |
michael@0 | 376 | }, |
michael@0 | 377 | |
michael@0 | 378 | _onDoubleTap: function (aX, aY) { |
michael@0 | 379 | let { element } = Content.getCurrentWindowAndOffset(aX, aY); |
michael@0 | 380 | while (element && !this._shouldZoomToElement(element)) { |
michael@0 | 381 | element = element.parentNode; |
michael@0 | 382 | } |
michael@0 | 383 | |
michael@0 | 384 | if (!element) { |
michael@0 | 385 | this._zoomOut(); |
michael@0 | 386 | } else { |
michael@0 | 387 | this._zoomToElement(element); |
michael@0 | 388 | } |
michael@0 | 389 | }, |
michael@0 | 390 | |
michael@0 | 391 | /****************************************************** |
michael@0 | 392 | * Zoom utilities |
michael@0 | 393 | */ |
michael@0 | 394 | _zoomOut: function() { |
michael@0 | 395 | let rect = new Rect(0,0,0,0); |
michael@0 | 396 | this._zoomToRect(rect); |
michael@0 | 397 | }, |
michael@0 | 398 | |
michael@0 | 399 | _zoomToElement: function(aElement) { |
michael@0 | 400 | let rect = getBoundingContentRect(aElement); |
michael@0 | 401 | this._inflateRect(rect, kZoomToElementMargin); |
michael@0 | 402 | this._zoomToRect(rect); |
michael@0 | 403 | }, |
michael@0 | 404 | |
michael@0 | 405 | _inflateRect: function(aRect, aMargin) { |
michael@0 | 406 | aRect.left -= aMargin; |
michael@0 | 407 | aRect.top -= aMargin; |
michael@0 | 408 | aRect.bottom += aMargin; |
michael@0 | 409 | aRect.right += aMargin; |
michael@0 | 410 | }, |
michael@0 | 411 | |
michael@0 | 412 | _zoomToRect: function (aRect) { |
michael@0 | 413 | let utils = Util.getWindowUtils(content); |
michael@0 | 414 | let viewId = utils.getViewId(content.document.documentElement); |
michael@0 | 415 | let presShellId = {}; |
michael@0 | 416 | utils.getPresShellId(presShellId); |
michael@0 | 417 | sendAsyncMessage("Content:ZoomToRect", { |
michael@0 | 418 | rect: aRect, |
michael@0 | 419 | presShellId: presShellId.value, |
michael@0 | 420 | viewId: viewId, |
michael@0 | 421 | }); |
michael@0 | 422 | }, |
michael@0 | 423 | |
michael@0 | 424 | _shouldZoomToElement: function(aElement) { |
michael@0 | 425 | let win = aElement.ownerDocument.defaultView; |
michael@0 | 426 | if (win.getComputedStyle(aElement, null).display == "inline") { |
michael@0 | 427 | return false; |
michael@0 | 428 | } |
michael@0 | 429 | else if (aElement instanceof Ci.nsIDOMHTMLLIElement) { |
michael@0 | 430 | return false; |
michael@0 | 431 | } |
michael@0 | 432 | else if (aElement instanceof Ci.nsIDOMHTMLQuoteElement) { |
michael@0 | 433 | return false; |
michael@0 | 434 | } |
michael@0 | 435 | else { |
michael@0 | 436 | return true; |
michael@0 | 437 | } |
michael@0 | 438 | }, |
michael@0 | 439 | |
michael@0 | 440 | |
michael@0 | 441 | /****************************************************** |
michael@0 | 442 | * General utilities |
michael@0 | 443 | */ |
michael@0 | 444 | |
michael@0 | 445 | /* |
michael@0 | 446 | * Retrieve the total offset from the window's origin to the sub frame |
michael@0 | 447 | * element including frame and scroll offsets. The resulting offset is |
michael@0 | 448 | * such that: |
michael@0 | 449 | * sub frame coords + offset = root frame position |
michael@0 | 450 | */ |
michael@0 | 451 | getCurrentWindowAndOffset: function(x, y) { |
michael@0 | 452 | // If the element at the given point belongs to another document (such |
michael@0 | 453 | // as an iframe's subdocument), the element in the calling document's |
michael@0 | 454 | // DOM (e.g. the iframe) is returned. |
michael@0 | 455 | let utils = Util.getWindowUtils(content); |
michael@0 | 456 | let element = utils.elementFromPoint(x, y, true, false); |
michael@0 | 457 | let offset = { x:0, y:0 }; |
michael@0 | 458 | |
michael@0 | 459 | while (element && (element instanceof HTMLIFrameElement || |
michael@0 | 460 | element instanceof HTMLFrameElement)) { |
michael@0 | 461 | // get the child frame position in client coordinates |
michael@0 | 462 | let rect = element.getBoundingClientRect(); |
michael@0 | 463 | |
michael@0 | 464 | // calculate offsets for digging down into sub frames |
michael@0 | 465 | // using elementFromPoint: |
michael@0 | 466 | |
michael@0 | 467 | // Get the content scroll offset in the child frame |
michael@0 | 468 | scrollOffset = ContentScroll.getScrollOffset(element.contentDocument.defaultView); |
michael@0 | 469 | // subtract frame and scroll offset from our elementFromPoint coordinates |
michael@0 | 470 | x -= rect.left + scrollOffset.x; |
michael@0 | 471 | y -= rect.top + scrollOffset.y; |
michael@0 | 472 | |
michael@0 | 473 | // calculate offsets we'll use to translate to client coords: |
michael@0 | 474 | |
michael@0 | 475 | // add frame client offset to our total offset result |
michael@0 | 476 | offset.x += rect.left; |
michael@0 | 477 | offset.y += rect.top; |
michael@0 | 478 | |
michael@0 | 479 | // get the frame's nsIDOMWindowUtils |
michael@0 | 480 | utils = element.contentDocument |
michael@0 | 481 | .defaultView |
michael@0 | 482 | .QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 483 | .getInterface(Ci.nsIDOMWindowUtils); |
michael@0 | 484 | |
michael@0 | 485 | // retrieve the target element in the sub frame at x, y |
michael@0 | 486 | element = utils.elementFromPoint(x, y, true, false); |
michael@0 | 487 | } |
michael@0 | 488 | |
michael@0 | 489 | if (!element) |
michael@0 | 490 | return {}; |
michael@0 | 491 | |
michael@0 | 492 | return { |
michael@0 | 493 | element: element, |
michael@0 | 494 | contentWindow: element.ownerDocument.defaultView, |
michael@0 | 495 | offset: offset, |
michael@0 | 496 | utils: utils |
michael@0 | 497 | }; |
michael@0 | 498 | }, |
michael@0 | 499 | |
michael@0 | 500 | |
michael@0 | 501 | _maybeNotifyErrorPage: function _maybeNotifyErrorPage() { |
michael@0 | 502 | // Notify browser that an error page is being shown instead |
michael@0 | 503 | // of the target location. Necessary to get proper thumbnail |
michael@0 | 504 | // updates on chrome for error pages. |
michael@0 | 505 | if (content.location.href !== content.document.documentURI) |
michael@0 | 506 | sendAsyncMessage("Browser:ErrorPage", null); |
michael@0 | 507 | }, |
michael@0 | 508 | |
michael@0 | 509 | _highlightElement: null, |
michael@0 | 510 | |
michael@0 | 511 | _doTapHighlight: function _doTapHighlight(aElement) { |
michael@0 | 512 | gDOMUtils.setContentState(aElement, kStateActive); |
michael@0 | 513 | this._highlightElement = aElement; |
michael@0 | 514 | }, |
michael@0 | 515 | |
michael@0 | 516 | _cancelTapHighlight: function _cancelTapHighlight(aElement) { |
michael@0 | 517 | gDOMUtils.setContentState(content.document.documentElement, kStateActive); |
michael@0 | 518 | this._highlightElement = null; |
michael@0 | 519 | }, |
michael@0 | 520 | }; |
michael@0 | 521 | |
michael@0 | 522 | Content.init(); |
michael@0 | 523 | |
michael@0 | 524 | var FormSubmitObserver = { |
michael@0 | 525 | init: function init(){ |
michael@0 | 526 | addMessageListener("Browser:TabOpen", this); |
michael@0 | 527 | addMessageListener("Browser:TabClose", this); |
michael@0 | 528 | |
michael@0 | 529 | addEventListener("pageshow", this, false); |
michael@0 | 530 | |
michael@0 | 531 | Services.obs.addObserver(this, "invalidformsubmit", false); |
michael@0 | 532 | }, |
michael@0 | 533 | |
michael@0 | 534 | handleEvent: function handleEvent(aEvent) { |
michael@0 | 535 | let target = aEvent.originalTarget; |
michael@0 | 536 | let isRootDocument = (target == content.document || target.ownerDocument == content.document); |
michael@0 | 537 | if (!isRootDocument) |
michael@0 | 538 | return; |
michael@0 | 539 | |
michael@0 | 540 | // Reset invalid submit state on each pageshow |
michael@0 | 541 | if (aEvent.type == "pageshow") |
michael@0 | 542 | Content.formAssistant.invalidSubmit = false; |
michael@0 | 543 | }, |
michael@0 | 544 | |
michael@0 | 545 | receiveMessage: function receiveMessage(aMessage) { |
michael@0 | 546 | let json = aMessage.json; |
michael@0 | 547 | switch (aMessage.name) { |
michael@0 | 548 | case "Browser:TabOpen": |
michael@0 | 549 | Services.obs.addObserver(this, "formsubmit", false); |
michael@0 | 550 | break; |
michael@0 | 551 | case "Browser:TabClose": |
michael@0 | 552 | Services.obs.removeObserver(this, "formsubmit"); |
michael@0 | 553 | break; |
michael@0 | 554 | } |
michael@0 | 555 | }, |
michael@0 | 556 | |
michael@0 | 557 | notify: function notify(aFormElement, aWindow, aActionURI, aCancelSubmit) { |
michael@0 | 558 | // Do not notify unless this is the window where the submit occurred |
michael@0 | 559 | if (aWindow == content) |
michael@0 | 560 | // We don't need to send any data along |
michael@0 | 561 | sendAsyncMessage("Browser:FormSubmit", {}); |
michael@0 | 562 | }, |
michael@0 | 563 | |
michael@0 | 564 | notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) { |
michael@0 | 565 | if (!aInvalidElements.length) |
michael@0 | 566 | return; |
michael@0 | 567 | |
michael@0 | 568 | let element = aInvalidElements.queryElementAt(0, Ci.nsISupports); |
michael@0 | 569 | if (!(element instanceof HTMLInputElement || |
michael@0 | 570 | element instanceof HTMLTextAreaElement || |
michael@0 | 571 | element instanceof HTMLSelectElement || |
michael@0 | 572 | element instanceof HTMLButtonElement)) { |
michael@0 | 573 | return; |
michael@0 | 574 | } |
michael@0 | 575 | |
michael@0 | 576 | Content.formAssistant.invalidSubmit = true; |
michael@0 | 577 | Content.formAssistant.open(element); |
michael@0 | 578 | }, |
michael@0 | 579 | |
michael@0 | 580 | QueryInterface : function(aIID) { |
michael@0 | 581 | if (!aIID.equals(Ci.nsIFormSubmitObserver) && |
michael@0 | 582 | !aIID.equals(Ci.nsISupportsWeakReference) && |
michael@0 | 583 | !aIID.equals(Ci.nsISupports)) |
michael@0 | 584 | throw Cr.NS_ERROR_NO_INTERFACE; |
michael@0 | 585 | return this; |
michael@0 | 586 | } |
michael@0 | 587 | }; |
michael@0 | 588 | this.Content = Content; |
michael@0 | 589 | |
michael@0 | 590 | FormSubmitObserver.init(); |