Bug 803719: Add a toDOMRange API to CaretPosition and use it to maintain position for reflow-on-zoom feature. [r=kats,tn]

This commit is contained in:
Scott Johnson 2013-04-16 16:08:38 -05:00
parent 7c06a2ec68
commit c84bf8d036
7 changed files with 135 additions and 29 deletions

View File

@ -7,7 +7,7 @@
#include "nsContentUtils.h"
nsDOMCaretPosition::nsDOMCaretPosition(nsINode* aNode, uint32_t aOffset)
: mOffset(aOffset), mOffsetNode(aNode)
: mOffset(aOffset), mOffsetNode(aNode), mAnonymousContentNode(nullptr)
{
SetIsDOMBinding();
}
@ -21,6 +21,37 @@ nsINode* nsDOMCaretPosition::GetOffsetNode() const
return mOffsetNode;
}
already_AddRefed<nsClientRect>
nsDOMCaretPosition::GetClientRect() const
{
if (!mOffsetNode) {
return nullptr;
}
nsRefPtr<nsClientRect> rect;
nsRefPtr<nsRange> domRange;
nsCOMPtr<nsINode> node;
if (mAnonymousContentNode) {
node = mAnonymousContentNode;
} else {
node = mOffsetNode;
}
nsresult creationRv = nsRange::CreateRange(node, mOffset, node,
mOffset,
getter_AddRefs<nsRange>(domRange));
if (!NS_SUCCEEDED(creationRv)) {
return nullptr;
}
NS_ASSERTION(domRange, "unable to retrieve valid dom range from CaretPosition");
rect = domRange->GetBoundingClientRect();
return rect.forget();
}
JSObject*
nsDOMCaretPosition::WrapObject(JSContext *aCx, JSObject *aScope)
{

View File

@ -8,7 +8,10 @@
#include "nsCycleCollectionParticipant.h"
#include "nsCOMPtr.h"
#include "nsINode.h"
#include "nsRange.h"
#include "nsWrapperCache.h"
#include "nsRect.h"
#include "nsClientRect.h"
/**
* Implementation of a DOM Caret Position, which is a node and offset within
@ -46,6 +49,32 @@ public:
*/
nsINode* GetOffsetNode() const;
/**
* Retrieve the bounding rectangle of this CaretPosition object.
*
* @returns An nsClientRect representing the bounding rectangle of this
* CaretPosition, if one can be successfully determined, otherwise
* nullptr.
*/
already_AddRefed<nsClientRect> GetClientRect() const;
/**
* Set the anonymous content node that is the actual parent of this
* CaretPosition object. In situations where the DOM node for a CaretPosition
* actually lies within an anonymous content node (e.g. a textarea), the
* actual parent is not set as the offset node. This is used to get the
* correct bounding box of a CaretPosition object that lies within a textarea
* or input element.
*
* @param aNode A pointer to an nsINode object that is the actual element
* within which this CaretPosition lies, but is an anonymous content
* node.
*/
void SetAnonymousContentNode(nsINode* aNode)
{
mAnonymousContentNode = aNode;
}
nsISupports* GetParentObject() const
{
return GetOffsetNode();
@ -56,8 +85,10 @@ public:
protected:
virtual ~nsDOMCaretPosition();
uint32_t mOffset;
nsCOMPtr<nsINode> mOffsetNode;
nsCOMPtr<nsINode> mAnonymousContentNode;
};
#endif

View File

@ -9323,6 +9323,13 @@ nsIDocument::CaretPositionFromPoint(float aX, float aY)
if (textArea || (input &&
NS_SUCCEEDED(input->MozIsTextField(false, &isText)) &&
isText)) {
// If the anonymous content node has a child, then we need to make sure
// that we get the appropriate child, as otherwise the offset may not be
// correct when we construct a range for it.
nsCOMPtr<nsIContent> firstChild = anonNode->GetFirstChild();
if (firstChild) {
anonNode = firstChild;
}
offset = nsContentUtils::GetAdjustedOffsetInTextControl(ptFrame, offset);
node = nonanon;
} else {
@ -9332,6 +9339,9 @@ nsIDocument::CaretPositionFromPoint(float aX, float aY)
}
nsRefPtr<nsDOMCaretPosition> aCaretPos = new nsDOMCaretPosition(node, offset);
if (nodeIsAnonymous) {
aCaretPos->SetAnonymousContentNode(anonNode);
}
return aCaretPos.forget();
}

View File

@ -211,6 +211,22 @@ nsRange::~nsRange()
DoSetRange(nullptr, 0, nullptr, 0, nullptr);
}
/* static */
nsresult
nsRange::CreateRange(nsINode* aStartParent, int32_t aStartOffset,
nsINode* aEndParent, int32_t aEndOffset,
nsRange** aRange)
{
nsCOMPtr<nsIDOMNode> startDomNode = do_QueryInterface(aStartParent);
nsCOMPtr<nsIDOMNode> endDomNode = do_QueryInterface(aEndParent);
nsresult rv = CreateRange(startDomNode, aStartOffset, endDomNode, aEndOffset,
aRange);
return rv;
}
/* static */
nsresult
nsRange::CreateRange(nsIDOMNode* aStartParent, int32_t aStartOffset,

View File

@ -65,6 +65,9 @@ public:
static nsresult CreateRange(nsIDOMNode* aStartParent, int32_t aStartOffset,
nsIDOMNode* aEndParent, int32_t aEndOffset,
nsIDOMRange** aRange);
static nsresult CreateRange(nsINode* aStartParent, int32_t aStartOffset,
nsINode* aEndParent, int32_t aEndOffset,
nsRange** aRange);
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_AMBIGUOUS(nsRange, nsIDOMRange)

View File

@ -9,4 +9,6 @@ interface CaretPosition {
*/
readonly attribute Node? offsetNode;
readonly attribute unsigned long offset;
ClientRect getClientRect();
};

View File

@ -2836,15 +2836,11 @@ Tab.prototype = {
// Adjust the max line box width to be no more than the viewport width, but
// only if the reflow-on-zoom preference is enabled.
if (BrowserEventHandler.mReflozPref && BrowserEventHandler._mLastPinchData) {
let webNav = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
let docShell = webNav.QueryInterface(Ci.nsIDocShell);
let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
let viewportWidth = gScreenWidth / aViewport.zoom;
// We add in a bit of fudge just so that the end characters don't accidentally
// get clipped. 15px is an arbitrary choice.
docViewer.changeMaxLineBoxWidth(viewportWidth - 15);
BrowserEventHandler._mLastPinchData = null;
let isZooming = aViewport.zoom != this._zoom;
if (isZooming &&
BrowserEventHandler.mReflozPref &&
BrowserApp.selectedTab._mReflozPoint) {
BrowserApp.selectedTab.performReflowOnZoom(aViewport);
}
let win = this.browser.contentWindow;
@ -3890,7 +3886,9 @@ var BrowserEventHandler = {
break;
case "MozMagnifyGesture":
this.onPinchFinish(aData, this._mLastPinchPoint.x, this._mLastPinchPoint.y);
if (BrowserEventHandler.mReflozPref) {
this.onPinchFinish(aData, BrowserApp.selectedTab._mReflozPoint.x, BrowserApp.selectedTab._mReflozPoint.y);
}
break;
default:
@ -4001,21 +3999,32 @@ var BrowserEventHandler = {
sendMessageToJava(rect);
},
_zoomInAndSnapToElement: function(aX, aY, aElement) {
let viewport = BrowserApp.selectedTab.getViewport();
if (viewport.zoom < 1.0) {
// We don't want to do this on zoom out.
_zoomInAndSnapToRange: function(aRange) {
if (!aRange) {
Cu.reportError("aRange is null in zoomInAndSnapToRange. Unable to maintain position.");
return;
}
let viewport = BrowserApp.selectedTab.getViewport();
let fudge = 15; // Add a bit of fudge.
let win = BrowserApp.selectedBrowser.contentWindow;
let boundingElement = aRange.offsetNode;
while (!boundingElement.getBoundingClientRect && boundingElement.parentNode) {
boundingElement = boundingElement.parentNode;
}
let rect = ElementTouchHelper.getBoundingContentRect(aElement);
let rect = ElementTouchHelper.getBoundingContentRect(boundingElement);
let drRect = aRange.getClientRect();
let scrollTop =
BrowserApp.selectedBrowser.contentDocument.documentElement.scrollTop ||
BrowserApp.selectedBrowser.contentDocument.body.scrollTop;
// We subtract half the height of the viewport so that we can (ideally)
// center the area of interest on the screen.
let topPos = scrollTop + drRect.top - (viewport.cssHeight / 2.0);
rect.type = "Browser:ZoomToRect";
rect.x = Math.max(viewport.cssPageLeft, rect.x - fudge);
rect.y = viewport.cssY;
rect.y = Math.max(topPos, viewport.cssPageTop);
rect.w = viewport.cssWidth;
rect.h = viewport.cssHeight;
@ -4023,18 +4032,24 @@ var BrowserEventHandler = {
},
onPinch: function(aData) {
let data = JSON.parse(aData);
this._mLastPinchPoint = {x: data.x, y: data.y};
// We only want to do this if reflow-on-zoom is enabled.
if (BrowserEventHandler.mReflozPref &&
!BrowserApp.selectedTab._mReflozPoint) {
let data = JSON.parse(aData);
let zoomPointX = data.x;
let zoomPointY = data.y;
BrowserApp.selectedTab._mReflozPoint = { x: zoomPointX, y: zoomPointY,
range: BrowserApp.selectedBrowser.contentDocument.caretPositionFromPoint(zoomPointX, zoomPointY) };
}
},
onPinchFinish: function(aData, aX, aY) {
if (this.mReflozPref) {
let data = JSON.parse(aData);
let pinchElement = ElementTouchHelper.anyElementFromPoint(aX, aY);
data.element = pinchElement;
BrowserApp.selectedTab._mLastPinchElement = pinchElement;
this._mLastPinchData = data;
this._zoomInAndSnapToElement(data.x, data.y, data.element);
// We only want to do this if reflow-on-zoom is enabled.
if (BrowserEventHandler.mReflozPref) {
let range = BrowserApp.selectedTab._mReflozPoint.range;
this._zoomInAndSnapToRange(range);
BrowserApp.selectedTab._mReflozPoint = null;
}
},
@ -4055,8 +4070,6 @@ var BrowserEventHandler = {
_highlightElement: null,
_mLastPinchData: null,
_doTapHighlight: function _doTapHighlight(aElement) {
DOMUtils.setContentState(aElement, kStateActive);
this._highlightElement = aElement;