forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			226 lines
		
	
	
	
		
			7.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			226 lines
		
	
	
	
		
			7.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */
 | |
| /* vim: set ts=2 sw=2 sts=2 et: */
 | |
| 
 | |
| /* 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/. */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| function debug(msg) {
 | |
|   // dump("BrowserElementPanning - " + msg + "\n");
 | |
| }
 | |
| 
 | |
| debug("loaded");
 | |
| 
 | |
| var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu }  = Components;
 | |
| Cu.import("resource://gre/modules/Services.jsm");
 | |
| Cu.import("resource://gre/modules/Geometry.jsm");
 | |
| 
 | |
| const kObservedEvents = [
 | |
|   "BEC:ShownModalPrompt",
 | |
|   "Activity:Success",
 | |
|   "Activity:Error"
 | |
| ];
 | |
| 
 | |
| const ContentPanning = {
 | |
|   init: function cp_init() {
 | |
|     addEventListener("unload",
 | |
| 		     this._unloadHandler.bind(this),
 | |
| 		     /* useCapture = */ false,
 | |
| 		     /* wantsUntrusted = */ false);
 | |
| 
 | |
|     addMessageListener("Viewport:Change", this._recvViewportChange.bind(this));
 | |
|     addMessageListener("Gesture:DoubleTap", this._recvDoubleTap.bind(this));
 | |
|     addEventListener("visibilitychange", this._handleVisibilityChange.bind(this));
 | |
|     kObservedEvents.forEach((topic) => {
 | |
|       Services.obs.addObserver(this, topic, false);
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   observe: function cp_observe(subject, topic, data) {
 | |
|     this._resetHover();
 | |
|   },
 | |
| 
 | |
|   get _domUtils() {
 | |
|     delete this._domUtils;
 | |
|     return this._domUtils = Cc['@mozilla.org/inspector/dom-utils;1']
 | |
|                               .getService(Ci.inIDOMUtils);
 | |
|   },
 | |
| 
 | |
|   _resetHover: function cp_resetHover() {
 | |
|     const kStateHover = 0x00000004;
 | |
|     try {
 | |
|       let element = content.document.createElement('foo');
 | |
|       this._domUtils.setContentState(element, kStateHover);
 | |
|     } catch(e) {}
 | |
|   },
 | |
| 
 | |
|   _recvViewportChange: function(data) {
 | |
|     let metrics = data.json;
 | |
|     this._viewport = new Rect(metrics.x, metrics.y,
 | |
|                               metrics.viewport.width,
 | |
|                               metrics.viewport.height);
 | |
|     this._cssCompositedRect = new Rect(metrics.x, metrics.y,
 | |
|                                        metrics.cssCompositedRect.width,
 | |
|                                        metrics.cssCompositedRect.height);
 | |
|     this._cssPageRect = new Rect(metrics.cssPageRect.x,
 | |
|                                  metrics.cssPageRect.y,
 | |
|                                  metrics.cssPageRect.width,
 | |
|                                  metrics.cssPageRect.height);
 | |
|   },
 | |
| 
 | |
|   _recvDoubleTap: function(data) {
 | |
|     data = data.json;
 | |
| 
 | |
|     // We haven't received a metrics update yet; don't do anything.
 | |
|     if (this._viewport == null) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let win = content;
 | |
| 
 | |
|     let element = ElementTouchHelper.anyElementFromPoint(win, data.x, data.y);
 | |
|     if (!element) {
 | |
|       this._zoomOut();
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     while (element && !this._shouldZoomToElement(element))
 | |
|       element = element.parentNode;
 | |
| 
 | |
|     if (!element) {
 | |
|       this._zoomOut();
 | |
|     } else {
 | |
|       const margin = 15;
 | |
|       let rect = ElementTouchHelper.getBoundingContentRect(element);
 | |
| 
 | |
|       let cssPageRect = this._cssPageRect;
 | |
|       let viewport = this._viewport;
 | |
|       let bRect = new Rect(Math.max(cssPageRect.x, rect.x - margin),
 | |
|                            rect.y,
 | |
|                            rect.w + 2 * margin,
 | |
|                            rect.h);
 | |
|       // constrict the rect to the screen's right edge
 | |
|       bRect.width = Math.min(bRect.width, cssPageRect.right - bRect.x);
 | |
| 
 | |
|       // if the rect is already taking up most of the visible area and is stretching the
 | |
|       // width of the page, then we want to zoom out instead.
 | |
|       if (this._isRectZoomedIn(bRect, this._cssCompositedRect)) {
 | |
|         this._zoomOut();
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       rect.x = Math.round(bRect.x);
 | |
|       rect.y = Math.round(bRect.y);
 | |
|       rect.w = Math.round(bRect.width);
 | |
|       rect.h = Math.round(bRect.height);
 | |
| 
 | |
|       // if the block we're zooming to is really tall, and the user double-tapped
 | |
|       // more than a screenful of height from the top of it, then adjust the y-coordinate
 | |
|       // so that we center the actual point the user double-tapped upon. this prevents
 | |
|       // flying to the top of a page when double-tapping to zoom in (bug 761721).
 | |
|       // the 1.2 multiplier is just a little fuzz to compensate for bRect including horizontal
 | |
|       // margins but not vertical ones.
 | |
|       let cssTapY = viewport.y + data.y;
 | |
|       if ((bRect.height > rect.h) && (cssTapY > rect.y + (rect.h * 1.2))) {
 | |
|         rect.y = cssTapY - (rect.h / 2);
 | |
|       }
 | |
| 
 | |
|       Services.obs.notifyObservers(docShell, 'browser-zoom-to-rect', JSON.stringify(rect));
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _handleVisibilityChange: function(evt) {
 | |
|     if (!evt.target.hidden)
 | |
|       return;
 | |
| 
 | |
|     this._resetHover();
 | |
|   },
 | |
| 
 | |
|   _shouldZoomToElement: function(aElement) {
 | |
|     let win = aElement.ownerDocument.defaultView;
 | |
|     if (win.getComputedStyle(aElement, null).display == "inline")
 | |
|       return false;
 | |
|     if (aElement instanceof Ci.nsIDOMHTMLLIElement)
 | |
|       return false;
 | |
|     if (aElement instanceof Ci.nsIDOMHTMLQuoteElement)
 | |
|       return false;
 | |
|     return true;
 | |
|   },
 | |
| 
 | |
|   _zoomOut: function() {
 | |
|     let rect = new Rect(0, 0, 0, 0);
 | |
|     Services.obs.notifyObservers(docShell, 'browser-zoom-to-rect', JSON.stringify(rect));
 | |
|   },
 | |
| 
 | |
|   _isRectZoomedIn: function(aRect, aViewport) {
 | |
|     // This function checks to see if the area of the rect visible in the
 | |
|     // viewport (i.e. the "overlapArea" variable below) is approximately
 | |
|     // the max area of the rect we can show.
 | |
|     let vRect = new Rect(aViewport.x, aViewport.y, aViewport.width, aViewport.height);
 | |
|     let overlap = vRect.intersect(aRect);
 | |
|     let overlapArea = overlap.width * overlap.height;
 | |
|     let availHeight = Math.min(aRect.width * vRect.height / vRect.width, aRect.height);
 | |
|     let showing = overlapArea / (aRect.width * availHeight);
 | |
|     let ratioW = (aRect.width / vRect.width);
 | |
|     let ratioH = (aRect.height / vRect.height);
 | |
| 
 | |
|     return (showing > 0.9 && (ratioW > 0.9 || ratioH > 0.9));
 | |
|   },
 | |
| 
 | |
|   _unloadHandler: function() {
 | |
|     kObservedEvents.forEach((topic) => {
 | |
|       Services.obs.removeObserver(this, topic);
 | |
|     });
 | |
|   }
 | |
| };
 | |
| 
 | |
| const ElementTouchHelper = {
 | |
|   anyElementFromPoint: function(aWindow, aX, aY) {
 | |
|     let cwu = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
 | |
|     let elem = cwu.elementFromPoint(aX, aY, true, true);
 | |
| 
 | |
|     let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement;
 | |
|     let HTMLFrameElement = Ci.nsIDOMHTMLFrameElement;
 | |
|     while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
 | |
|       let rect = elem.getBoundingClientRect();
 | |
|       aX -= rect.left;
 | |
|       aY -= rect.top;
 | |
|       cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
 | |
|       elem = cwu.elementFromPoint(aX, aY, true, true);
 | |
|     }
 | |
| 
 | |
|     return elem;
 | |
|   },
 | |
| 
 | |
|   getBoundingContentRect: function(aElement) {
 | |
|     if (!aElement)
 | |
|       return {x: 0, y: 0, w: 0, h: 0};
 | |
| 
 | |
|     let document = aElement.ownerDocument;
 | |
|     while (document.defaultView.frameElement)
 | |
|       document = document.defaultView.frameElement.ownerDocument;
 | |
| 
 | |
|     let cwu = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
 | |
|     let scrollX = {}, scrollY = {};
 | |
|     cwu.getScrollXY(false, scrollX, scrollY);
 | |
| 
 | |
|     let r = aElement.getBoundingClientRect();
 | |
| 
 | |
|     // step out of iframes and frames, offsetting scroll values
 | |
|     for (let frame = aElement.ownerDocument.defaultView; frame.frameElement && 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;
 | |
|       scrollX.value += rect.left + parseInt(left);
 | |
|       scrollY.value += rect.top + parseInt(top);
 | |
|     }
 | |
| 
 | |
|     return {x: r.left + scrollX.value,
 | |
|             y: r.top + scrollY.value,
 | |
|             w: r.width,
 | |
|             h: r.height };
 | |
|   }
 | |
| };
 | 
